#!/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 approvalPath = join(root, 'data', 'sources', 'approvals', 'dataforseo-stage1-approval.json') const budgetGate = readJson(join(outRoot, 'source_acquisition_budget_gate.json'), {}) const costPlan = readJson(join(outRoot, 'source_acquisition_cost_plan.json'), {}) const spendGate = readJson(join(outRoot, 'source_acquisition_spend_gate.json'), {}) const preflight = readJson(join(outRoot, 'dataforseo_task_preflight.json'), {}) const pilot = readJson(join(outRoot, 'dataforseo_pilot_manifest.json'), {}) const live = readJson(join(outRoot, 'dataforseo_live_connection.json'), {}) const unresolved = readJson(join(outRoot, 'dataforseo_unresolved_asin_evidence.json'), {}) const support = readJson(join(outRoot, 'dataforseo_support_escalation.json'), {}) const approval = readJson(approvalPath, {}) const payloadRows = Number(preflight.counts?.taskRows ?? pilot.counts?.readyTasks ?? 0) const maxTasks = Number(budgetGate.budget?.dataforseoStage1?.maxTasks ?? 0) const maxSpend = Number(budgetGate.budget?.dataforseoStage1?.maxSpend ?? costPlan.estimates?.dataforseoStage1?.maxSpend ?? 0) const estimatedTotal = Number(costPlan.estimates?.dataforseoStage1?.estimatedTotal ?? 0) const depth = Number(budgetGate.budget?.dataforseoStage1?.maxReviewsPerAsin ?? pilot.guardrails?.recommendedDepth ?? 10) const confirmedAsins = Number(live.counts?.exactAsinsConfirmed ?? 0) const missingAsins = Number(live.counts?.missingAsins ?? pilot.counts?.missingAsins ?? 0) const keepOut = Number(unresolved.counts?.keepOutOfPaidPayload ?? 0) const providerEndpointBlocked = Boolean(support.issue) const approvalValid = (spendGate.checks || []).some((row: any) => row.Provider === 'DataForSEO' && row.Check === 'Human spend approval recorded' && row.Status === 'pass') const proposedApproval = { provider: 'DataForSEO', stage: 1, approved: false, approvedBy: '', approvedAt: '', approvalScope: 'one capped DataForSEO Amazon Reviews Stage 1 pilot only', budgetRef: 'data/sources/budgets/source-acquisition-budget.json', decisionRef: 'content_population_exports/dataforseo_task_preflight.json', currency: budgetGate.currency || costPlan.currency || 'GBP', maxSpendApproved: maxSpend, expectedMaxSpend: maxSpend, targetCapApproved: maxTasks, expectedTargetCap: maxTasks, runLimitApproved: 1, expectedRunLimit: 1, depthOrRequestCapApproved: depth, expectedDepthOrRequestCap: depth, hardStopAcknowledged: true, noCredentialStorageAcknowledged: true, postRunReviewRequired: true, namedGapForApify: '', notes: `Approve only after DataForSEO confirms Reviews endpoint availability. Payload currently has ${payloadRows} live-confirmed ASIN rows; ${missingAsins} unresolved ASINs remain outside payload.` } const rows = [ { Check: 'Payload scoped', Status: payloadRows > 0 && payloadRows <= maxTasks ? 'ready' : 'blocked', Evidence: `${payloadRows} payload rows / ${maxTasks} max tasks` }, { Check: 'ASIN evidence guarded', Status: confirmedAsins === payloadRows && keepOut === missingAsins ? 'ready' : 'review', Evidence: `${confirmedAsins} confirmed ASINs; ${missingAsins} unresolved; ${keepOut} kept out of paid payload` }, { Check: 'Budget cap recorded', Status: budgetGate.budgetAllowed?.dataforseoStage1 === true ? 'ready' : 'blocked', Evidence: `${budgetGate.currency || 'GBP'} ${maxSpend} max spend` }, { Check: 'Cost estimate recorded', Status: costPlan.costReady?.dataforseoStage1 === true ? 'ready' : 'blocked', Evidence: `${costPlan.currency || 'GBP'} ${estimatedTotal} estimated` }, { Check: 'Endpoint availability', Status: providerEndpointBlocked ? 'blocked' : 'review', Evidence: providerEndpointBlocked ? support.issue : 'No provider blocker recorded' }, { Check: 'Approval currently valid', Status: approvalValid ? 'ready' : 'blocked', Evidence: approvalValid ? `${approval.approvedBy || 'unknown'} at ${approval.approvedAt || 'unknown'}` : 'approval file is not yet approved/valid' } ] const manifest = { generatedAt, ready: false, approvalAlreadyValid: approvalValid, recommendation: providerEndpointBlocked ? 'Do not approve/run yet. Send the DataForSEO support escalation first; use this packet only to prepare the exact capped approval once the Reviews endpoint is available.' : 'Approval can be considered only for the one capped Stage 1 pilot, after the project owner fills the local approval file.', approvalFile: 'data/sources/approvals/dataforseo-stage1-approval.json', proposedApproval, counts: { checks: rows.length, readyChecks: rows.filter(row => row.Status === 'ready').length, blockedChecks: rows.filter(row => row.Status === 'blocked').length, payloadRows, confirmedAsins, missingAsins, keepOutOfPaidPayload: keepOut }, checks: rows, files: { approvalRequestMd: 'content_population_exports/dataforseo_stage1_approval_request.md', approvalRequestJson: 'content_population_exports/dataforseo_stage1_approval_request.json', approvalRequestCsv: 'content_population_exports/dataforseo_stage1_approval_request.csv', approvalFile: 'data/sources/approvals/dataforseo-stage1-approval.json', supportEscalation: 'content_population_exports/dataforseo_support_escalation.md' } } writeFileSync(join(outRoot, 'dataforseo_stage1_approval_request.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') writeFileSync(join(outRoot, 'dataforseo_stage1_approval_request.csv'), csv(rows, ['Check', 'Status', 'Evidence']), 'utf8') writeFileSync(join(outRoot, 'dataforseo_stage1_approval_request.md'), [ '# DataForSEO Stage 1 approval request', '', `Generated: ${generatedAt}`, '', `Ready to approve/run now: NO`, '', manifest.recommendation, '', '## Current capped pilot', `- Provider: DataForSEO`, `- Endpoint: ${preflight.endpoint || 'POST https://api.dataforseo.com/v3/merchant/amazon/reviews/task_post'}`, `- Payload rows: ${payloadRows}`, `- Confirmed ASINs: ${confirmedAsins}`, `- Unresolved ASINs outside payload: ${missingAsins}`, `- Review depth cap: ${depth}`, `- Estimated cost: ${costPlan.currency || 'GBP'} ${estimatedTotal}`, `- Max spend cap: ${budgetGate.currency || 'GBP'} ${maxSpend}`, `- Provider blocker: ${support.issue || 'none recorded'}`, '', '## Checks', ...rows.map(row => `- [${row.Status}] ${row.Check}: ${row.Evidence}`), '', '## Approval file values to use only after provider availability is confirmed', '```json', JSON.stringify(proposedApproval, null, 2), '```', '', '## Guardrail', 'This packet is not approval. The local approval file must still be filled by the project owner, the approval gate must validate it, and the paid-run flag must be set only for the capped run window.', '' ].join('\n'), 'utf8') console.log(`DataForSEO Stage 1 approval request ready: ${manifest.ready}`) console.log(`Approval checks ready: ${manifest.counts.readyChecks}/${manifest.counts.checks}`) console.log(`Payload rows: ${payloadRows}`)