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>
171 lines
7.8 KiB
TypeScript
171 lines
7.8 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')
|
|
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: '<project owner name>',
|
|
approvedAt: '<ISO timestamp>',
|
|
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}`)
|