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>
132 lines
6.8 KiB
TypeScript
132 lines
6.8 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 next = readJson(join(outRoot, 'source_acquisition_next_actions.json'), {})
|
|
const scenarios = readJson(join(outRoot, 'source_acquisition_cost_scenarios.json'), {})
|
|
const spend = readJson(join(outRoot, 'source_acquisition_spend_gate.json'), {})
|
|
const budget = readJson(join(outRoot, 'source_acquisition_budget_gate.json'), {})
|
|
const costPlan = readJson(join(outRoot, 'source_acquisition_cost_plan.json'), {})
|
|
const approval = readJson(join(outRoot, 'source_acquisition_approval_gate.json'), {})
|
|
const pilot = readJson(join(outRoot, 'dataforseo_pilot_manifest.json'), {})
|
|
const asinLookup = readJson(join(outRoot, 'dataforseo_asin_lookup_queue.json'), {})
|
|
|
|
const dfsStandard = (scenarios.scenarios || []).find((row: any) => row.Provider === 'DataForSEO' && row.Scenario === 'standard_queue_exact_9_asin_depth_10') || {}
|
|
const apifyScenario = (scenarios.scenarios || []).find((row: any) => row.Provider === 'Apify' && row.Scenario === 'one_actor_first_3_targets_reference_cap') || {}
|
|
const rows = [
|
|
{
|
|
Decision: 'Subscribe/run DataForSEO now',
|
|
Recommendation: 'NO - not yet',
|
|
Provider: 'DataForSEO',
|
|
Reason: `${asinLookup.counts?.missingAsins ?? pilot.counts?.missingAsins ?? 0} ASINs missing and ${pilot.counts?.readyTasks || 0} paid payload rows ready`,
|
|
SpendRisk: 'Would create an account/funding path before there is a callable payload.',
|
|
RequiredBeforeYes: 'Confirm 9 Amazon UK ASINs; regenerate payload; fill cost plan/max spend; valid approval; task preflight all green.',
|
|
CapIfLater: `Use standard queue; official-reference API estimate $${dfsStandard.EstimatedUsd ?? 'unknown'} for 9 ASINs at depth 10; suggested local cap reference $${dfsStandard.SuggestedCapUsd ?? 1} after account minimums are checked.`
|
|
},
|
|
{
|
|
Decision: 'Subscribe/run Apify now',
|
|
Recommendation: 'NO - hold',
|
|
Provider: 'Apify',
|
|
Reason: 'DataForSEO pilot has not run, no named gap exists, and Apify spend gate is blocked.',
|
|
SpendRisk: 'Browser scraping can burn variable CU/proxy/storage credits without proving the structured cheaper path first.',
|
|
RequiredBeforeYes: 'DataForSEO pilot review must select run_apify_gapfill and name a gap; cost plan and approval must be valid; Apify preflight all green.',
|
|
CapIfLater: `One actor, first 3 targets only, 3 requests/brand, inspect actual usage; suggested local cap reference $${apifyScenario.SuggestedCapUsd ?? 5}.`
|
|
},
|
|
{
|
|
Decision: 'Next action',
|
|
Recommendation: 'DO THIS FIRST',
|
|
Provider: 'No paid provider',
|
|
Reason: next.currentInstruction || 'Fill ASINs before spend.',
|
|
SpendRisk: 'None; this is no-spend prep.',
|
|
RequiredBeforeYes: 'Use dataforseo_asin_lookup_queue and asin-confirmations worksheet; do not promote third-party hints without Amazon-page exact match.',
|
|
CapIfLater: 'No cap needed for no-spend ASIN verification.'
|
|
},
|
|
{
|
|
Decision: 'Budget/cost setup',
|
|
Recommendation: 'PREP ONLY',
|
|
Provider: 'DataForSEO',
|
|
Reason: `${budget.counts?.passing || 0}/${budget.counts?.checks || 0} budget checks, ${costPlan.counts?.passing || 0}/${costPlan.counts?.checks || 0} cost checks, ${approval.counts?.passing || 0}/${approval.counts?.checks || 0} approval checks`,
|
|
SpendRisk: 'Entering caps is safe; enabling paid runs is still blocked by preflight/spend gates.',
|
|
RequiredBeforeYes: 'Copy pricing scenario into cost plan and set max-spend only after ASIN payload exists.',
|
|
CapIfLater: 'Keep Stage 1 cap tiny and hardStopAfterFirstRun=true.'
|
|
}
|
|
]
|
|
|
|
const readyToSpend = spend.spendAllowed?.dataforseoStage1 === true || spend.spendAllowed?.apifyStage2 === true
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: !readyToSpend,
|
|
headline: 'Do not subscribe or spend provider credits yet; DataForSEO is the first paid lane once ASINs and gates are ready, Apify stays held.',
|
|
currentDecision: {
|
|
dataforseo: 'wait_for_asins_and_gates',
|
|
apify: 'hold_until_post_dataforseo_named_gap',
|
|
immediate: next.currentInstruction || 'Fill missing ASINs first.'
|
|
},
|
|
counts: {
|
|
rows: rows.length,
|
|
missingAsins: asinLookup.counts?.missingAsins ?? pilot.counts?.missingAsins ?? 0,
|
|
readyTasks: pilot.counts?.readyTasks || 0,
|
|
spendChecksPassing: spend.counts?.passing || 0,
|
|
spendChecks: spend.counts?.checks || 0,
|
|
dataforseoEstimateUsd: dfsStandard.EstimatedUsd || 0
|
|
},
|
|
files: {
|
|
reportMd: 'content_population_exports/source_subscription_decision.md',
|
|
reportCsv: 'content_population_exports/source_subscription_decision.csv',
|
|
reportJson: 'content_population_exports/source_subscription_decision.json'
|
|
},
|
|
rows
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'source_subscription_decision.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'source_subscription_decision.csv'), csv(rows, ['Decision', 'Recommendation', 'Provider', 'Reason', 'SpendRisk', 'RequiredBeforeYes', 'CapIfLater']), 'utf8')
|
|
writeFileSync(join(outRoot, 'source_subscription_decision.md'), [
|
|
'# Source subscription decision',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`## Current decision: ${manifest.headline}`,
|
|
'',
|
|
`- Missing ASINs: ${manifest.counts.missingAsins}`,
|
|
`- Ready DataForSEO payload rows: ${manifest.counts.readyTasks}`,
|
|
`- Spend gate: ${manifest.counts.spendChecksPassing}/${manifest.counts.spendChecks} checks passing`,
|
|
`- DataForSEO standard pilot official-reference estimate: $${manifest.counts.dataforseoEstimateUsd}`,
|
|
'',
|
|
'## Decision rows',
|
|
...rows.map(row => [
|
|
`### ${row.Decision}: ${row.Recommendation}`,
|
|
`- Provider: ${row.Provider}`,
|
|
`- Reason: ${row.Reason}`,
|
|
`- Spend risk: ${row.SpendRisk}`,
|
|
`- Required before yes: ${row.RequiredBeforeYes}`,
|
|
`- Cap if later: ${row.CapIfLater}`,
|
|
''
|
|
].join('\n')),
|
|
'## Guardrail',
|
|
'This packet is advisory and does not unlock spend. The spend gate and provider preflights remain authoritative.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Source subscription decision ready: ${manifest.ready}`)
|
|
console.log(manifest.headline)
|
|
console.log(`Rows: ${manifest.counts.rows}`)
|