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>
146 lines
6.0 KiB
TypeScript
146 lines
6.0 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 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}`)
|