#!/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}`)