#!/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 reviewRoot = join(root, 'data', 'sources', 'reviews') mkdirSync(outRoot, { recursive: true }) mkdirSync(reviewRoot, { 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 ingest = readJson(join(outRoot, 'dataforseo_ingest_validation.json'), {}) const spendGate = readJson(join(outRoot, 'source_acquisition_spend_gate.json'), {}) const apifyGapfill = readJson(join(outRoot, 'apify_gapfill_manifest.json'), {}) const reviewPath = join(reviewRoot, 'dataforseo-stage1-review.json') const reviewTemplate = { reviewed: false, reviewedBy: '', reviewedAt: '', provider: 'DataForSEO', stage: 1, rawOutputInspected: false, costReviewed: false, outputHasReviewText: false, outputHasRating: false, outputHasDate: false, outputHasSourceUrl: false, costAcceptable: false, decision: 'pending', allowedDecisions: ['scale_dataforseo', 'run_apify_gapfill', 'stop_adjust_source_map'], namedGapForApify: '', notes: '' } if (!existsSync(reviewPath)) writeFileSync(reviewPath, JSON.stringify(reviewTemplate, null, 2) + '\n', 'utf8') const review = readJson(reviewPath, reviewTemplate) const convertedReviews = Number(ingest.counts?.convertedReviews || 0) const convertedFiles = Number(ingest.counts?.convertedFiles || 0) const issues = Number(ingest.counts?.issues || 0) const apifyNamedGap = String(review.namedGapForApify || '').trim() const checks = [ { Check: 'Raw DataForSEO result ingested', Status: convertedFiles > 0 && convertedReviews > 0 ? 'pass' : 'blocked', Evidence: `${convertedFiles} converted files / ${convertedReviews} converted reviews` }, { Check: 'Ingest has no structural issues', Status: convertedFiles > 0 && issues === 0 ? 'pass' : 'blocked', Evidence: `${issues} ingest issues` }, { Check: 'Reviewer inspected raw output', Status: review.reviewed === true && review.rawOutputInspected === true ? 'pass' : 'blocked', Evidence: review.reviewed ? `${review.reviewedBy || 'unknown'} at ${review.reviewedAt || 'unknown'}` : 'review not recorded' }, { Check: 'Required evidence fields present', Status: review.outputHasReviewText && review.outputHasRating && review.outputHasDate && review.outputHasSourceUrl ? 'pass' : 'blocked', Evidence: `text=${!!review.outputHasReviewText}, rating=${!!review.outputHasRating}, date=${!!review.outputHasDate}, url=${!!review.outputHasSourceUrl}` }, { Check: 'Cost reviewed and acceptable', Status: review.costReviewed === true && review.costAcceptable === true ? 'pass' : 'blocked', Evidence: `costReviewed=${!!review.costReviewed}, costAcceptable=${!!review.costAcceptable}` }, { Check: 'Decision recorded', Status: review.reviewed === true && reviewTemplate.allowedDecisions.includes(review.decision) ? 'pass' : 'blocked', Evidence: `decision=${review.decision || 'pending'}` }, { Check: 'Apify gap named if selected', Status: review.decision === 'run_apify_gapfill' ? (apifyNamedGap ? 'pass' : 'blocked') : 'pass', Evidence: review.decision === 'run_apify_gapfill' ? (apifyNamedGap || 'missing named gap') : 'not selecting Apify gap-fill' } ].map(row => ({ ...row, Blocking: row.Status === 'pass' ? 'no' : 'yes' })) const ready = checks.every(row => row.Status === 'pass') const decision = ready ? review.decision : 'pending' const recommendations = ready ? decision === 'scale_dataforseo' ? ['Scale DataForSEO only to the next controlled priority batch; keep Apify off.'] : decision === 'run_apify_gapfill' ? [`Run only the Apify first batch (${apifyGapfill.counts?.firstBatchTargets || 0} targets) for named gap: ${apifyNamedGap}.`] : ['Stop and adjust the source map before any further provider spend.'] : ['Do not scale DataForSEO or run Apify yet.', 'Complete raw ingest, evidence-field review, cost review, and explicit decision first.'] const manifest = { generatedAt, ready, decision, reviewFile: 'data/sources/reviews/dataforseo-stage1-review.json', counts: { checks: checks.length, passing: checks.filter(row => row.Status === 'pass').length, blocked: checks.filter(row => row.Status !== 'pass').length, convertedFiles, convertedReviews, ingestIssues: issues }, checks, recommendations, review } writeFileSync(join(outRoot, 'dataforseo_pilot_review.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') writeFileSync(join(outRoot, 'dataforseo_pilot_review.csv'), csv(checks, ['Check', 'Status', 'Blocking', 'Evidence']), 'utf8') writeFileSync(join(outRoot, 'dataforseo_pilot_review.md'), [ '# DataForSEO Stage 1 pilot review gate', '', `Generated: ${generatedAt}`, '', `Ready: ${ready ? 'YES' : 'NO'}`, `Decision: ${decision}`, '', 'This gate is the stop/review step between paid Stage 1 output and any scale-up or Apify gap-fill spend.', '', '## Checks', ...checks.map(row => `- ${row.Status === 'pass' ? '[pass]' : '[blocked]'} ${row.Check} — ${row.Evidence}`), '', '## Recommendations', ...recommendations.map(item => `- ${item}`), '', '## Review file', '- `data/sources/reviews/dataforseo-stage1-review.json`', '' ].join('\n'), 'utf8') console.log(`DataForSEO pilot review ready: ${ready}`) console.log(`Decision: ${decision}`) console.log(`Checks passing: ${manifest.counts.passing}/${manifest.counts.checks}`)