Files
justvitamin/scripts/generate-source-acquisition-next-actions.ts
T
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
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>
2026-06-02 18:50:09 +08:00

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}`)