Files
justvitamin/scripts/validate-source-acquisition-spend.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

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