056c47581f
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that presents the 4-section PDP pilot batch back to Umar, compliance, and the board in an editorial format. Adds the full data layer that drives it: 5 source-backed per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated product FAQs, and the Matrixify-ready CSV exports for Lewis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
386 lines
22 KiB
TypeScript
386 lines
22 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
const root = process.cwd()
|
|
const outRoot = join(root, 'content_population_exports')
|
|
mkdirSync(outRoot, { recursive: true })
|
|
|
|
function readJson(path: string, fallback: any = null) {
|
|
if (!existsSync(path)) return fallback
|
|
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
|
|
}
|
|
|
|
function csv(rows: any[], headers: string[]) {
|
|
const esc = (value: any) => {
|
|
if (value === undefined || value === null) value = ''
|
|
if (Array.isArray(value)) value = value.join(' | ')
|
|
value = String(value)
|
|
return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value
|
|
}
|
|
return [headers.join(','), ...rows.map(row => headers.map(header => esc(row[header])).join(','))].join('\n') + '\n'
|
|
}
|
|
|
|
const generatedAt = new Date().toISOString()
|
|
const sourcePlan = readJson(join(outRoot, 'source_acquisition_plan.json'), {})
|
|
const pilot = readJson(join(outRoot, 'dataforseo_pilot_manifest.json'), {})
|
|
const dataforseoLive = readJson(join(outRoot, 'dataforseo_live_connection.json'), {})
|
|
const dataforseoProductsLiveRun = readJson(join(outRoot, 'dataforseo_products_live_run.json'), {})
|
|
const asinLookup = readJson(join(outRoot, 'dataforseo_asin_lookup_queue.json'), {})
|
|
const asinWebEvidence = readJson(join(outRoot, 'dataforseo_asin_web_evidence.json'), {})
|
|
const unresolvedAsinEvidence = readJson(join(outRoot, 'dataforseo_unresolved_asin_evidence.json'), {})
|
|
const asinConfirmation = readJson(join(outRoot, 'dataforseo_asin_confirmation_report.json'), {})
|
|
const asinPromotion = readJson(join(outRoot, 'dataforseo_asin_promotion_report.json'), {})
|
|
const reviewsLivePilot = readJson(join(outRoot, 'dataforseo_reviews_live_pilot.json'), {})
|
|
const reviewsDocsStatus = readJson(join(outRoot, 'dataforseo_reviews_docs_status.json'), {})
|
|
const supportEscalation = readJson(join(outRoot, 'dataforseo_support_escalation.json'), {})
|
|
const supportResponseGate = readJson(join(outRoot, 'dataforseo_support_response_gate.json'), {})
|
|
const approvalRequest = readJson(join(outRoot, 'dataforseo_stage1_approval_request.json'), {})
|
|
const manualAmazonCapture = readJson(join(outRoot, 'manual_amazon_review_capture.json'), {})
|
|
const manualAmazonCaptureValidation = readJson(join(outRoot, 'manual_amazon_review_capture_validation.json'), {})
|
|
const ingest = readJson(join(outRoot, 'dataforseo_ingest_validation.json'), {})
|
|
const budgetGate = readJson(join(outRoot, 'source_acquisition_budget_gate.json'), {})
|
|
const costPlan = readJson(join(outRoot, 'source_acquisition_cost_plan.json'), {})
|
|
const approvalGate = readJson(join(outRoot, 'source_acquisition_approval_gate.json'), {})
|
|
const spendGate = readJson(join(outRoot, 'source_acquisition_spend_gate.json'), {})
|
|
const review = readJson(join(outRoot, 'dataforseo_pilot_review.json'), {})
|
|
const apify = readJson(join(outRoot, 'apify_gapfill_manifest.json'), {})
|
|
const apifyPreflight = readJson(join(outRoot, 'apify_task_preflight.json'), {})
|
|
const pricingRefs = readJson(join(outRoot, 'source_acquisition_pricing_references.json'), {})
|
|
const costScenarios = readJson(join(outRoot, 'source_acquisition_cost_scenarios.json'), {})
|
|
const subscriptionDecision = readJson(join(outRoot, 'source_subscription_decision.json'), {})
|
|
const requiredPacketFiles = [
|
|
'source_acquisition_plan.json',
|
|
'dataforseo_pilot_manifest.json',
|
|
'dataforseo_live_connection.json',
|
|
'dataforseo_products_live_run.json',
|
|
'dataforseo_asin_lookup_queue.json',
|
|
'dataforseo_asin_web_evidence.json',
|
|
'dataforseo_unresolved_asin_evidence.json',
|
|
'dataforseo_asin_confirmation_report.json',
|
|
'dataforseo_asin_promotion_report.json',
|
|
'dataforseo_reviews_live_pilot.json',
|
|
'dataforseo_reviews_docs_status.json',
|
|
'dataforseo_support_escalation.json',
|
|
'dataforseo_support_response_gate.json',
|
|
'dataforseo_stage1_approval_request.json',
|
|
'manual_amazon_review_capture.json',
|
|
'manual_amazon_review_capture_validation.json',
|
|
'dataforseo_ingest_validation.json',
|
|
'dataforseo_pilot_review.json',
|
|
'apify_gapfill_manifest.json',
|
|
'apify_task_preflight.json',
|
|
'source_acquisition_pricing_references.json',
|
|
'source_acquisition_cost_scenarios.json',
|
|
'source_acquisition_budget_gate.json',
|
|
'source_acquisition_cost_plan.json',
|
|
'source_acquisition_approval_gate.json',
|
|
'source_acquisition_spend_gate.json',
|
|
'source_subscription_decision.json'
|
|
]
|
|
const requiredPacketFilesPresent = requiredPacketFiles.filter(file => existsSync(join(outRoot, file))).length
|
|
|
|
const missingAsins = Number(dataforseoLive.counts?.missingAsins ?? pilot.counts?.missingAsins ?? asinLookup.counts?.missingAsins ?? 0)
|
|
const asinPresent = Number(dataforseoLive.counts?.exactAsinsConfirmed ?? asinLookup.counts?.asinPresent ?? 0)
|
|
const readyTasks = Number(pilot.counts?.readyTasks ?? 0)
|
|
const productSearchResultItems = Number(dataforseoProductsLiveRun.counts?.resultItems || dataforseoLive.counts?.candidates || 0)
|
|
const exactAsinsConfirmed = Number(dataforseoLive.counts?.exactAsinsConfirmed ?? asinLookup.counts?.asinPresent ?? 0)
|
|
const confirmedPromotionCandidates = Number(asinConfirmation.counts?.readyForPromotion ?? 0)
|
|
const webLookupAids = Number(asinWebEvidence.counts?.thirdPartyLookupAids ?? 0)
|
|
const webRejected = Number(asinWebEvidence.counts?.rejectedNotTarget ?? 0)
|
|
const unresolvedKeepOut = Number(unresolvedAsinEvidence.counts?.keepOutOfPaidPayload ?? 0)
|
|
const promotedAsins = Number(asinPromotion.counts?.promotable ?? 0)
|
|
const convertedReviews = Number(ingest.counts?.convertedReviews ?? 0)
|
|
const reviewPilotTasks = Number(reviewsLivePilot.counts?.tasksPosted ?? 0)
|
|
const reviewPilotReviews = Number(reviewsLivePilot.counts?.reviewsReturned ?? 0)
|
|
const reviewsDocsUnavailable = reviewsDocsStatus.endpointTemporarilyUnavailable === true
|
|
const supportEscalationReady = supportEscalation.ready === true
|
|
const supportResponsePassing = Number(supportResponseGate.counts?.passing ?? 0)
|
|
const supportResponseChecks = Number(supportResponseGate.counts?.checks ?? 0)
|
|
const approvalRequestReadyChecks = Number(approvalRequest.counts?.readyChecks ?? 0)
|
|
const approvalRequestChecks = Number(approvalRequest.counts?.checks ?? 0)
|
|
const manualAmazonCaptureTargets = Number(manualAmazonCapture.counts?.targets ?? 0)
|
|
const manualAmazonCaptureFilesPresent = Number(manualAmazonCaptureValidation.counts?.filesPresent ?? 0)
|
|
const manualAmazonCaptureFilesValid = Number(manualAmazonCaptureValidation.counts?.filesValid ?? 0)
|
|
const manualAmazonCaptureReviews = Number(manualAmazonCaptureValidation.counts?.reviewsCaptured ?? 0)
|
|
const dataforseoBudgetAllowed = budgetGate.budgetAllowed?.dataforseoStage1 === true
|
|
const apifyBudgetAllowed = budgetGate.budgetAllowed?.apifyStage2 === true
|
|
const dataforseoCostReady = costPlan.costReady?.dataforseoStage1 === true
|
|
const apifyCostReady = costPlan.costReady?.apifyStage2 === true
|
|
const dataforseoApprovalValid = (approvalGate.approvals || []).some((row: any) => row.key === 'dataforseoStage1' && row.valid === true)
|
|
const apifyApprovalValid = (approvalGate.approvals || []).some((row: any) => row.key === 'apifyStage2' && row.valid === true)
|
|
const dataforseoSpendAllowed = spendGate.spendAllowed?.dataforseoStage1 === true
|
|
const apifySpendAllowed = spendGate.spendAllowed?.apifyStage2 === true
|
|
const reviewReady = review.ready === true
|
|
const reviewDecision = review.decision || 'pending'
|
|
const sourcePacketFresh = requiredPacketFilesPresent === requiredPacketFiles.length
|
|
const firstBatchApifyTargets = Number(apify.counts?.firstBatchTargets || 0)
|
|
const apifyPreflightPassing = Number(apifyPreflight.counts?.passing || 0)
|
|
const apifyPreflightChecks = Number(apifyPreflight.counts?.checks || 0)
|
|
const pricingReferences = Number(pricingRefs.counts?.references || 0)
|
|
const costScenarioCount = Number(costScenarios.counts?.scenarios || 0)
|
|
const subscriptionHeadline = subscriptionDecision.headline || ''
|
|
|
|
const asinExamples = (asinLookup.queue || []).slice(0, 9).map((row: any) => `${row.SKU} (${row.Product})`)
|
|
|
|
const rows = [
|
|
{
|
|
Sequence: 1,
|
|
Stage: '0',
|
|
Provider: 'No paid provider',
|
|
Action: 'Fill and verify Stage 1 ASIN map',
|
|
Status: missingAsins === 0 && asinPresent > 0 ? 'done' : 'blocked',
|
|
SpendAllowed: 'no spend required',
|
|
Owner: 'Research / project owner',
|
|
Evidence: `${asinPresent} ASINs present / ${missingAsins} missing; ${webLookupAids} web lookup aids; ${webRejected} rejected not-target leads; ${productSearchResultItems} DataForSEO product-result items saved; ${unresolvedKeepOut} unresolved targets have no exact DataForSEO candidate and stay out of the paid payload`,
|
|
CommandOrFile: 'bun run dataforseo:products-live with explicit allow flag, then bun run dataforseo:live-asin-evidence && bun run dataforseo:unresolved-asins; data/sources/dataforseo/asin-map.csv',
|
|
StopRule: 'Only accept exact Just Vitamins Amazon UK product matches; leave blank if uncertain.',
|
|
WhyNext: readyTasks > 0
|
|
? 'This remains useful no-spend research, but the capped pilot payload already uses only the live-confirmed ASINs.'
|
|
: 'This is the cheapest blocker: without ASINs there is no DataForSEO payload and no reason to approve spend.'
|
|
},
|
|
{
|
|
Sequence: 2,
|
|
Stage: '0',
|
|
Provider: 'No paid provider',
|
|
Action: 'Promote only Amazon-confirmed high-confidence ASIN candidates',
|
|
Status: promotedAsins > 0 ? 'done' : 'blocked',
|
|
SpendAllowed: 'no spend required',
|
|
Owner: 'Research',
|
|
Evidence: `${confirmedPromotionCandidates} promotion confirmations ready; ${promotedAsins} candidates promoted / ${asinPromotion.counts?.candidateRows || 0} candidate rows`,
|
|
CommandOrFile: 'bun run dataforseo:asin-confirmations && bun run dataforseo:promote-asins',
|
|
StopRule: 'Only rows marked ready in the confirmation report plus amazon_confirmed_exact_match and Confidence=high can be copied into asin-map.csv; existing ASINs are not overwritten.',
|
|
WhyNext: 'This closes confirmed ASINs safely while keeping uncertain third-party candidate evidence out of paid task payloads.'
|
|
},
|
|
{
|
|
Sequence: 3,
|
|
Stage: '1',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Regenerate capped Amazon Reviews task payload',
|
|
Status: readyTasks > 0 ? 'ready' : 'blocked',
|
|
SpendAllowed: 'no spend required',
|
|
Owner: 'Research',
|
|
Evidence: `${readyTasks} ready task rows / ${pilot.counts?.stageOneTargets || 0} capped targets; ${missingAsins} unresolved ASINs remain outside the payload`,
|
|
CommandOrFile: 'bun run dataforseo:pilot',
|
|
StopRule: 'Payload must stay within Stage 1 cap before any provider call.',
|
|
WhyNext: 'Turns the verified ASINs into a reviewable paid-call payload without calling DataForSEO.'
|
|
},
|
|
{
|
|
Sequence: 4,
|
|
Stage: '1',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Record an explicit Stage 1 budget cap before approval',
|
|
Status: dataforseoBudgetAllowed ? 'ready' : 'blocked',
|
|
SpendAllowed: dataforseoBudgetAllowed ? 'budget cap recorded' : 'blocked',
|
|
Owner: 'Project owner',
|
|
Evidence: `${budgetGate.counts?.passing || 0}/${budgetGate.counts?.checks || 0} budget checks passing`,
|
|
CommandOrFile: budgetGate.budgetFile || 'data/sources/budgets/source-acquisition-budget.json',
|
|
StopRule: 'Budget must include max spend, max tasks, review depth, and hard stop after the first run. Do not put credentials here.',
|
|
WhyNext: 'A subscription is only safe after the hard cap is written down locally and can be checked by the spend gate.'
|
|
},
|
|
{
|
|
Sequence: 5,
|
|
Stage: '1',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Record current pricing and estimated cost for the capped pilot',
|
|
Status: dataforseoCostReady ? 'ready' : 'blocked',
|
|
SpendAllowed: dataforseoCostReady ? 'cost estimate accepted' : 'blocked',
|
|
Owner: 'Project owner / research',
|
|
Evidence: `${costPlan.estimates?.dataforseoStage1?.estimatedTotal || 0} estimated / ${costPlan.estimates?.dataforseoStage1?.maxSpend || 0} max spend; ${pricingReferences} pricing refs and ${costScenarioCount} scenarios available`,
|
|
CommandOrFile: 'content_population_exports/source_acquisition_cost_scenarios.md + data/sources/budgets/source-acquisition-cost-plan.json && bun run source:cost-plan',
|
|
StopRule: 'Do not enable budget or approval from guessed pricing; record current provider pricing source and keep estimate within max spend.',
|
|
WhyNext: 'This is the anti-credit-burn step before subscription: it turns pricing into an explicit capped first-run estimate.'
|
|
},
|
|
{
|
|
Sequence: 6,
|
|
Stage: '1',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Record human spend approval for one capped pilot run',
|
|
Status: dataforseoSpendAllowed ? 'ready' : 'blocked',
|
|
SpendAllowed: dataforseoSpendAllowed ? 'allowed' : 'blocked',
|
|
Owner: 'Project owner',
|
|
Evidence: `${spendGate.counts?.passing || 0}/${spendGate.counts?.checks || 0} spend-gate checks passing; approval valid=${dataforseoApprovalValid}`,
|
|
CommandOrFile: 'bun run dataforseo:approval-request && bun run source:approvals && ' + (spendGate.approvalFiles?.dataforseoStage1 || 'data/sources/approvals/dataforseo-stage1-approval.json'),
|
|
StopRule: 'Approval should name the cap, provider, marketplace, target count, and expected max spend before credentials are used.',
|
|
WhyNext: 'Prevents accidental credit burn; approval is deliberately local and explicit.'
|
|
},
|
|
{
|
|
Sequence: 7,
|
|
Stage: '1',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Run only the approved DataForSEO pilot and save raw task_get JSON',
|
|
Status: convertedReviews > 0 ? 'done' : (dataforseoSpendAllowed ? 'ready' : 'blocked'),
|
|
SpendAllowed: dataforseoSpendAllowed ? 'allowed for capped pilot only' : 'blocked',
|
|
Owner: 'Research',
|
|
Evidence: `${reviewPilotTasks} review tasks posted / ${reviewPilotReviews} reviews returned; ${ingest.counts?.rawFiles || 0} raw files / ${convertedReviews} converted reviews`,
|
|
CommandOrFile: 'bun run dataforseo:reviews-live then data/sources/dataforseo/raw/',
|
|
StopRule: 'Do not run a second batch until raw output, useful fields, and actual credit cost are reviewed.',
|
|
WhyNext: 'This is the first paid step and should be a single controlled proof of data quality and cost.'
|
|
},
|
|
{
|
|
Sequence: 8,
|
|
Stage: '1 support',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Send provider support escalation if Reviews endpoint remains 50304',
|
|
Status: supportEscalationReady ? 'ready' : 'blocked',
|
|
SpendAllowed: 'no spend required',
|
|
Owner: 'Project owner / support',
|
|
Evidence: `${supportEscalation.issue || 'No support packet generated'}; official docs unavailable=${reviewsDocsUnavailable}`,
|
|
CommandOrFile: 'bun run dataforseo:reviews-docs-status && bun run dataforseo:support-escalation then content_population_exports/dataforseo_support_escalation.md; when support replies, fill data/sources/provider-responses/dataforseo-reviews-endpoint-response.json and run bun run dataforseo:support-response',
|
|
StopRule: 'Escalation packet must contain no credentials and should include endpoint, status code, tiny payload shape, and proof that Products endpoint works.',
|
|
WhyNext: 'This turns the provider-side blocker into an actionable support request instead of more retries or guessing.'
|
|
},
|
|
{
|
|
Sequence: 9,
|
|
Stage: '1 fallback',
|
|
Provider: 'Manual capture',
|
|
Action: 'Capture Amazon reviews manually if DataForSEO support remains blocked',
|
|
Status: manualAmazonCaptureValidation.ready === true ? 'done' : (manualAmazonCaptureTargets > 0 ? 'ready' : 'blocked'),
|
|
SpendAllowed: 'no provider spend',
|
|
Owner: 'Research',
|
|
Evidence: `${manualAmazonCaptureTargets} manual capture targets; ${manualAmazonCaptureFilesPresent}/${manualAmazonCaptureTargets} files present; ${manualAmazonCaptureFilesValid}/${manualAmazonCaptureTargets} files valid; ${manualAmazonCaptureReviews} reviews captured`,
|
|
CommandOrFile: 'bun run amazon:manual-capture && bun run amazon:manual-capture-validate then content_population_exports/manual_amazon_review_capture_validation.md',
|
|
StopRule: 'Capture only the listed Stage 1 ASINs; do not broaden to catalogue scraping.',
|
|
WhyNext: 'This gives a no-spend fallback source path while the DataForSEO Reviews endpoint is unavailable.'
|
|
},
|
|
{
|
|
Sequence: 10,
|
|
Stage: '1',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Normalize raw provider output into the standard Amazon source-drop path',
|
|
Status: ingest.ready === true ? 'done' : (Number(ingest.counts?.rawFiles || 0) > 0 ? 'ready' : 'blocked'),
|
|
SpendAllowed: 'no additional spend',
|
|
Owner: 'Research / dashboard',
|
|
Evidence: `${ingest.counts?.convertedFiles || 0} converted files / ${ingest.counts?.issues || 0} ingest issues`,
|
|
CommandOrFile: 'bun run dataforseo:ingest && bun run validate:sources',
|
|
StopRule: 'Converted source files must preserve review text, rating, date, source URL, SKU, and provider provenance.',
|
|
WhyNext: 'Keeps DataForSEO evidence on the same validation path as manual Amazon drops.'
|
|
},
|
|
{
|
|
Sequence: 11,
|
|
Stage: '1 review',
|
|
Provider: 'DataForSEO',
|
|
Action: 'Review quality, fields, and cost; decide scale vs Apify vs stop',
|
|
Status: reviewReady ? 'ready' : 'blocked',
|
|
SpendAllowed: 'blocks all scale-up spend',
|
|
Owner: 'Project owner / research',
|
|
Evidence: `${review.counts?.passing || 0}/${review.counts?.checks || 0} review checks passing; decision=${reviewDecision}`,
|
|
CommandOrFile: review.reviewFile || 'data/sources/reviews/dataforseo-stage1-review.json',
|
|
StopRule: 'Decision must be one of scale_dataforseo, run_apify_gapfill, or stop_adjust_source_map.',
|
|
WhyNext: 'Prevents the pilot from silently turning into full-catalogue scraping.'
|
|
},
|
|
{
|
|
Sequence: 12,
|
|
Stage: '2',
|
|
Provider: 'Apify selective',
|
|
Action: 'Run Apify only for named gaps after DataForSEO review',
|
|
Status: apifySpendAllowed ? 'ready' : 'blocked',
|
|
SpendAllowed: apifySpendAllowed ? 'allowed for named gap only' : 'blocked',
|
|
Owner: 'Research',
|
|
Evidence: `${firstBatchApifyTargets} first-batch targets / ${apify.counts?.holdTargets || 0} held; Apify preflight ${apifyPreflightPassing}/${apifyPreflightChecks}; Apify budget allowed=${apifyBudgetAllowed}; cost ready=${apifyCostReady}; approval valid=${apifyApprovalValid}; Apify spend allowed=${apifySpendAllowed}`,
|
|
CommandOrFile: 'bun run apify:task-preflight then content_population_exports/apify_competitor_pdp_input_template.json',
|
|
StopRule: 'One actor, first batch only, inspect raw dataset quality/cost before any further run.',
|
|
WhyNext: 'Apify is useful for PDP/social/browser gaps, but only after the cheaper structured pilot proves what is missing.'
|
|
},
|
|
{
|
|
Sequence: 13,
|
|
Stage: 'control',
|
|
Provider: 'All source packet lanes',
|
|
Action: 'Refresh source packet and delivery index after every change',
|
|
Status: sourcePacketFresh ? 'ready' : 'blocked',
|
|
SpendAllowed: 'no spend required',
|
|
Owner: 'Dashboard',
|
|
Evidence: `${requiredPacketFilesPresent}/${requiredPacketFiles.length} required source packet files present`,
|
|
CommandOrFile: 'bun run source:refresh-packet && bun run delivery:index',
|
|
StopRule: 'Packet freshness does not mean final handoff readiness; verify:handoff remains the final gate.',
|
|
WhyNext: 'Keeps the Export Console and handoff packet aligned with the latest source-acquisition state.'
|
|
}
|
|
]
|
|
|
|
const openRows = rows.filter(row => row.Status !== 'done')
|
|
const immediateRows = openRows.filter(row => row.Sequence <= 5)
|
|
const blockedPaidRows = rows.filter(row => row.Provider !== 'No paid provider' && String(row.SpendAllowed).includes('blocked'))
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: reviewReady && (reviewDecision === 'scale_dataforseo' || reviewDecision === 'run_apify_gapfill' || reviewDecision === 'stop_adjust_source_map'),
|
|
recommendation: sourcePlan.recommendation || 'Use DataForSEO first for the capped structured pilot; reserve Apify for named gaps after review.',
|
|
currentInstruction: missingAsins > 0 && readyTasks > 0
|
|
? `Do not broaden spend. ${readyTasks} DataForSEO payload rows are ready from live-confirmed ASINs; ${missingAsins} ASINs remain unresolved and must stay out of the payload unless exact evidence is found.`
|
|
: missingAsins > 0
|
|
? `Do not subscribe or spend yet. Fill ${missingAsins} missing ASINs first.`
|
|
: dataforseoSpendAllowed
|
|
? 'DataForSEO Stage 1 spend is allowed for the capped pilot only; do not scale until pilot review is complete.'
|
|
: 'ASINs/payload may be ready, but spend remains blocked until approval and gate checks pass.',
|
|
counts: {
|
|
actions: rows.length,
|
|
open: openRows.length,
|
|
immediate: immediateRows.length,
|
|
blockedPaidActions: blockedPaidRows.length,
|
|
missingAsins,
|
|
confirmedAsins: exactAsinsConfirmed,
|
|
confirmedPromotionCandidates,
|
|
webLookupAids,
|
|
webRejected,
|
|
unresolvedKeepOut,
|
|
promotedAsins,
|
|
readyTasks,
|
|
productSearchResultItems,
|
|
reviewPilotTasks,
|
|
reviewPilotReviews,
|
|
reviewsDocsUnavailable,
|
|
supportEscalationReady,
|
|
supportResponsePassing,
|
|
supportResponseChecks,
|
|
approvalRequestReadyChecks,
|
|
approvalRequestChecks,
|
|
manualAmazonCaptureTargets,
|
|
manualAmazonCaptureFilesPresent,
|
|
manualAmazonCaptureFilesValid,
|
|
manualAmazonCaptureReviews,
|
|
convertedReviews
|
|
},
|
|
asinExamples,
|
|
subscriptionDecision: subscriptionHeadline,
|
|
actions: rows,
|
|
immediateActions: immediateRows,
|
|
paidSpendBlockedActions: blockedPaidRows
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'source_acquisition_next_actions.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'source_acquisition_next_actions.csv'), csv(rows, ['Sequence', 'Stage', 'Provider', 'Action', 'Status', 'SpendAllowed', 'Owner', 'Evidence', 'CommandOrFile', 'StopRule', 'WhyNext']), 'utf8')
|
|
writeFileSync(join(outRoot, 'source_acquisition_next_actions.md'), [
|
|
'# Source acquisition next actions',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`Recommendation: ${manifest.recommendation}`,
|
|
'',
|
|
`Current instruction: ${manifest.currentInstruction}`,
|
|
'',
|
|
'## Immediate no/low-spend actions',
|
|
...immediateRows.map(row => `- [${row.Status}] ${row.Sequence}. ${row.Action} — ${row.Evidence}. Use: \`${row.CommandOrFile}\``),
|
|
'',
|
|
'## Stage sequence',
|
|
...rows.map(row => [
|
|
`### ${row.Sequence}. ${row.Provider} / Stage ${row.Stage}: ${row.Action}`,
|
|
`- Status: ${row.Status}`,
|
|
`- Spend: ${row.SpendAllowed}`,
|
|
`- Owner: ${row.Owner}`,
|
|
`- Evidence: ${row.Evidence}`,
|
|
`- Command/file: \`${row.CommandOrFile}\``,
|
|
`- Stop rule: ${row.StopRule}`,
|
|
`- Why this order: ${row.WhyNext}`,
|
|
''
|
|
].join('\n')),
|
|
'## ASIN lookup targets',
|
|
...asinExamples.map(item => `- ${item}`),
|
|
'',
|
|
'## Guardrail',
|
|
'DataForSEO is the default first paid lane. Apify stays blocked until DataForSEO raw output is inspected and a named gap is recorded.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Source acquisition next actions ready: ${manifest.ready}`)
|
|
console.log(`Immediate actions: ${manifest.counts.immediate}`)
|
|
console.log(`Missing ASINs: ${manifest.counts.missingAsins}`)
|