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>
199 lines
10 KiB
TypeScript
199 lines
10 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')
|
|
const approvalRoot = join(root, 'data', 'sources', 'approvals')
|
|
mkdirSync(outRoot, { recursive: true })
|
|
mkdirSync(approvalRoot, { 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 dataforseoPilot = readJson(join(outRoot, 'dataforseo_pilot_manifest.json'), {})
|
|
const asinLookup = readJson(join(outRoot, 'dataforseo_asin_lookup_queue.json'), {})
|
|
const dataforseoIngest = readJson(join(outRoot, 'dataforseo_ingest_validation.json'), {})
|
|
const apifyGapfill = readJson(join(outRoot, 'apify_gapfill_manifest.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 supportResponseGate = readJson(join(outRoot, 'dataforseo_support_response_gate.json'), {})
|
|
const reviewsDocsStatus = readJson(join(outRoot, 'dataforseo_reviews_docs_status.json'), {})
|
|
const dataforseoApprovalPath = join(approvalRoot, 'dataforseo-stage1-approval.json')
|
|
const apifyApprovalPath = join(approvalRoot, 'apify-stage2-approval.json')
|
|
|
|
const approvalTemplate = {
|
|
approved: false,
|
|
approvedBy: '',
|
|
approvedAt: '',
|
|
scope: 'fill before spending provider credits',
|
|
notes: ''
|
|
}
|
|
if (!existsSync(dataforseoApprovalPath)) writeFileSync(dataforseoApprovalPath, JSON.stringify({ ...approvalTemplate, provider: 'DataForSEO', stage: 1 }, null, 2) + '\n', 'utf8')
|
|
if (!existsSync(apifyApprovalPath)) writeFileSync(apifyApprovalPath, JSON.stringify({ ...approvalTemplate, provider: 'Apify', stage: 2 }, null, 2) + '\n', 'utf8')
|
|
const dataforseoApproval = readJson(dataforseoApprovalPath, {})
|
|
const apifyApproval = readJson(apifyApprovalPath, {})
|
|
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 dataforseoCostReady = costPlan.costReady?.dataforseoStage1 === true
|
|
const apifyCostReady = costPlan.costReady?.apifyStage2 === true
|
|
const readyTasks = Number(dataforseoPilot.counts?.readyTasks || 0)
|
|
const missingAsins = Number(dataforseoPilot.counts?.missingAsins ?? asinLookup.counts?.missingAsins ?? 0)
|
|
|
|
const dataforseoChecks = [
|
|
{
|
|
Check: 'Stage 1 targets capped',
|
|
Status: (dataforseoPilot.counts?.stageOneTargets || 0) > 0 && (dataforseoPilot.counts?.stageOneTargets || 99) <= (dataforseoPilot.guardrails?.maxStageOneTargets || 12) ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: `${dataforseoPilot.counts?.stageOneTargets || 0}/${dataforseoPilot.guardrails?.maxStageOneTargets || 12} targets`
|
|
},
|
|
{
|
|
Check: 'ASIN payload scoped',
|
|
Status: readyTasks > 0 ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: `${readyTasks} ready payload row(s); ${missingAsins} unresolved ASIN(s) remain outside the paid pilot payload`
|
|
},
|
|
{
|
|
Check: 'Task payload ready',
|
|
Status: readyTasks > 0 ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: `${readyTasks} ready task rows`
|
|
},
|
|
{
|
|
Check: 'Budget cap recorded',
|
|
Status: budgetGate.budgetAllowed?.dataforseoStage1 === true ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: budgetGate.budgetAllowed?.dataforseoStage1 === true ? `${budgetGate.currency || 'GBP'} cap recorded` : 'data/sources/budgets/source-acquisition-budget.json not enabled/capped for DataForSEO'
|
|
},
|
|
{
|
|
Check: 'Cost plan accepted',
|
|
Status: dataforseoCostReady ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: dataforseoCostReady ? `${costPlan.currency || 'GBP'} ${costPlan.estimates?.dataforseoStage1?.estimatedTotal || 0} estimated / ${costPlan.estimates?.dataforseoStage1?.maxSpend || 0} max spend` : 'source_acquisition_cost_plan.json not ready for DataForSEO'
|
|
},
|
|
{
|
|
Check: 'Reviews endpoint cleared by support',
|
|
Status: supportResponseGate.ready === true ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: supportResponseGate.ready === true ? 'support response gate ready for capped retry' : `${supportResponseGate.counts?.passing || 0}/${supportResponseGate.counts?.checks || 0} support-response checks passing; endpoint/retry path not confirmed`
|
|
},
|
|
{
|
|
Check: 'Official Reviews docs status allows retry',
|
|
Status: reviewsDocsStatus.ready === true ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: reviewsDocsStatus.ready === true
|
|
? 'official docs no longer mark Reviews endpoint unavailable'
|
|
: `endpointTemporarilyUnavailable=${reviewsDocsStatus.endpointTemporarilyUnavailable === true}; ${reviewsDocsStatus.counts?.passing || 0}/${reviewsDocsStatus.counts?.checks || 0} docs-status checks passing`
|
|
},
|
|
{
|
|
Check: 'Human spend approval recorded',
|
|
Status: dataforseoApproval.approved === true && dataforseoApprovalValid ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: dataforseoApproval.approved && dataforseoApprovalValid ? `${dataforseoApproval.approvedBy || 'unknown'} at ${dataforseoApproval.approvedAt || 'unknown'}` : 'data/sources/approvals/dataforseo-stage1-approval.json not approved or approval gate invalid'
|
|
}
|
|
]
|
|
|
|
const apifyChecks = [
|
|
{
|
|
Check: 'DataForSEO pilot inspected first',
|
|
Status: (dataforseoIngest.counts?.convertedReviews || 0) > 0 ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: `${dataforseoIngest.counts?.convertedReviews || 0} converted DataForSEO reviews`
|
|
},
|
|
{
|
|
Check: 'Apify first batch capped',
|
|
Status: (apifyGapfill.counts?.firstBatchTargets || 99) <= 18 && (apifyGapfill.targets || []).every((row: any) => Number(row.MaxRequestsPerBrand || 0) >= 100 && Number(row.MaxRequestsPerBrand || 9999) <= 1000 && Number(row.MaxDepth || 99) <= 2) ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: `${apifyGapfill.counts?.firstBatchTargets || 0} first-batch targets, ${apifyGapfill.counts?.holdTargets || 0} held`
|
|
},
|
|
{
|
|
Check: 'Named gap documented',
|
|
Status: dataforseoIngest.ready === true && (sourcePlan.counts?.amazonFiles || 0) > 0 ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: dataforseoIngest.ready === true ? 'DataForSEO ingest ready; inspect source plan for gap' : 'DataForSEO pilot not ready/inspected yet'
|
|
},
|
|
{
|
|
Check: 'Budget cap recorded',
|
|
Status: budgetGate.budgetAllowed?.apifyStage2 === true ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: budgetGate.budgetAllowed?.apifyStage2 === true ? `${budgetGate.currency || 'GBP'} cap recorded` : 'data/sources/budgets/source-acquisition-budget.json not enabled/capped for Apify'
|
|
},
|
|
{
|
|
Check: 'Cost plan accepted',
|
|
Status: apifyCostReady ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: apifyCostReady ? `${costPlan.currency || 'GBP'} ${costPlan.estimates?.apifyStage2?.estimatedTotal || 0} estimated / ${costPlan.estimates?.apifyStage2?.maxSpend || 0} max spend` : 'source_acquisition_cost_plan.json not ready for Apify'
|
|
},
|
|
{
|
|
Check: 'Human spend approval recorded',
|
|
Status: apifyApproval.approved === true && apifyApprovalValid ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: apifyApproval.approved && apifyApprovalValid ? `${apifyApproval.approvedBy || 'unknown'} at ${apifyApproval.approvedAt || 'unknown'}` : 'data/sources/approvals/apify-stage2-approval.json not approved or approval gate invalid'
|
|
}
|
|
]
|
|
|
|
const rows = [
|
|
...dataforseoChecks.map(row => ({ Provider: 'DataForSEO', Stage: '1', ...row })),
|
|
...apifyChecks.map(row => ({ Provider: 'Apify', Stage: '2', ...row }))
|
|
]
|
|
const dataforseoAllowed = dataforseoChecks.every(row => row.Status === 'pass')
|
|
const apifyAllowed = apifyChecks.every(row => row.Status === 'pass')
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: dataforseoAllowed && apifyAllowed,
|
|
spendAllowed: {
|
|
dataforseoStage1: dataforseoAllowed,
|
|
apifyStage2: apifyAllowed
|
|
},
|
|
counts: {
|
|
checks: rows.length,
|
|
passing: rows.filter(row => row.Status === 'pass').length,
|
|
blocked: rows.filter(row => row.Status !== 'pass').length
|
|
},
|
|
approvalFiles: {
|
|
dataforseoStage1: 'data/sources/approvals/dataforseo-stage1-approval.json',
|
|
apifyStage2: 'data/sources/approvals/apify-stage2-approval.json'
|
|
},
|
|
checks: rows,
|
|
nextActions: rows.filter(row => row.Status !== 'pass').map(row => `${row.Provider} Stage ${row.Stage}: ${row.Check} - ${row.Evidence}`)
|
|
}
|
|
writeFileSync(join(outRoot, 'source_acquisition_spend_gate.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'source_acquisition_spend_gate.csv'), csv(rows, ['Provider', 'Stage', 'Check', 'Status', 'Blocking', 'Evidence']), 'utf8')
|
|
writeFileSync(join(outRoot, 'source_acquisition_spend_gate.md'), [
|
|
'# Source acquisition spend gate',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`DataForSEO Stage 1 spend allowed: ${dataforseoAllowed ? 'YES' : 'NO'}`,
|
|
`Apify Stage 2 spend allowed: ${apifyAllowed ? 'YES' : 'NO'}`,
|
|
'',
|
|
'This gate intentionally blocks provider spend until prerequisites and explicit local approval files are satisfied.',
|
|
'',
|
|
'## Checks',
|
|
...rows.map(row => `- ${row.Status === 'pass' ? '[pass]' : '[blocked]'} ${row.Provider} Stage ${row.Stage}: ${row.Check} — ${row.Evidence}`),
|
|
'',
|
|
'## Approval files',
|
|
`- \`${manifest.approvalFiles.dataforseoStage1}\``,
|
|
`- \`${manifest.approvalFiles.apifyStage2}\``,
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Source acquisition spend gate ready: ${manifest.ready}`)
|
|
console.log(`DataForSEO spend allowed: ${dataforseoAllowed}`)
|
|
console.log(`Apify spend allowed: ${apifyAllowed}`)
|
|
console.log(`Checks passing: ${manifest.counts.passing}/${manifest.counts.checks}`)
|