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>
152 lines
7.8 KiB
TypeScript
152 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 = ''
|
|
value = Array.isArray(value) ? value.join(' | ') : 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 dateKey = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: 'Asia/Kuala_Lumpur',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit'
|
|
}).format(new Date())
|
|
|
|
const handoff = readJson(join(outRoot, 'handoff_manifest.json'), {})
|
|
const exportReadiness = readJson(join(outRoot, 'content_population_readiness.json'), {})
|
|
const verification = readJson(join(outRoot, 'handoff_verification_run.json'), {})
|
|
const delivery = readJson(join(outRoot, 'delivery_index.json'), {})
|
|
const burndown = readJson(join(outRoot, 'blocker_burndown.json'), {})
|
|
const dataforseo = readJson(join(outRoot, 'dataforseo_live_connection.json'), {})
|
|
const reviewsDocs = readJson(join(outRoot, 'dataforseo_reviews_docs_status.json'), {})
|
|
const supportGate = readJson(join(outRoot, 'dataforseo_support_response_gate.json'), {})
|
|
const productApprovals = readJson(join(outRoot, 'product_approval_decision_gate.json'), {})
|
|
const clinicalBlanks = readJson(join(outRoot, 'clinical_blank_decision_template.json'), {})
|
|
const stagingEvidence = readJson(join(outRoot, 'staging_evidence_validation.json'), {})
|
|
|
|
const verificationCounts = verification.counts || {}
|
|
const deliveryCounts = delivery.counts || {}
|
|
const burndownCounts = burndown.counts || {}
|
|
const dataforseoCounts = dataforseo.counts || {}
|
|
const stagingCounts = stagingEvidence.counts || {}
|
|
|
|
const statusRows = [
|
|
{
|
|
Area: 'Export/handoff',
|
|
Status: handoff.readyForLewis === true ? 'ready' : 'blocked',
|
|
Evidence: `dashboard blockers ${handoff.counts?.blockers ?? 'n/a'}; readiness ready=${exportReadiness.ready === true}`,
|
|
NextAction: 'Clear compliance and approval blockers before Lewis/Euan import handoff.'
|
|
},
|
|
{
|
|
Area: 'Master verification',
|
|
Status: verification.ready === true ? 'ready' : 'blocked',
|
|
Evidence: `${verificationCounts.passing ?? 0}/${verificationCounts.checks ?? 0} checks passing; ${verificationCounts.blocked ?? 0} blocked`,
|
|
NextAction: 'Keep failed-by-design gates visible until human decisions and staging evidence exist.'
|
|
},
|
|
{
|
|
Area: 'Delivery packet index',
|
|
Status: delivery.ready === true ? 'ready' : 'blocked',
|
|
Evidence: `${deliveryCounts.present ?? 0}/${deliveryCounts.files ?? 0} packet files present`,
|
|
NextAction: 'Packet completeness is not final readiness; rerun after new artifacts are added.'
|
|
},
|
|
{
|
|
Area: 'Compliance blank decisions',
|
|
Status: clinicalBlanks.ready === true ? 'ready' : 'blocked',
|
|
Evidence: `${clinicalBlanks.counts?.validBlankApprovals ?? 0}/${clinicalBlanks.counts?.decisions ?? 0} approved blank decisions`,
|
|
NextAction: 'Compliance must fill approved_blank decisions or provide source-backed claim mappings.'
|
|
},
|
|
{
|
|
Area: 'Product approvals',
|
|
Status: productApprovals.ready === true ? 'ready' : 'blocked',
|
|
Evidence: `${productApprovals.counts?.validApprovals ?? 0}/${productApprovals.counts?.products ?? 0} valid approvals`,
|
|
NextAction: 'Umar/JV reviewer must fill decision=approve_all with evidence and lane acknowledgements.'
|
|
},
|
|
{
|
|
Area: 'Staging evidence',
|
|
Status: stagingEvidence.ready === true ? 'ready' : 'blocked',
|
|
Evidence: `${stagingCounts.passing ?? 0}/${stagingCounts.gates ?? 0} gates passing; import logs ${stagingCounts.importLogs ?? 0}; PDP checks ${stagingCounts.pdpSpotChecks ?? 0}`,
|
|
NextAction: 'Record Matrixify import logs, staging PDP spot checks, and cutover approval.'
|
|
},
|
|
{
|
|
Area: 'DataForSEO account connection',
|
|
Status: dataforseo.accountConnection?.status_code === 20000 ? 'connected' : 'blocked',
|
|
Evidence: `account ${dataforseo.accountConnection?.status_code ?? 'n/a'} ${dataforseo.accountConnection?.status ?? dataforseo.accountConnection?.status_message ?? ''}; credentials printed=${dataforseo.credentialPrinted === true}`,
|
|
NextAction: 'Keep using the pi-fresh-foundation .env credential source; do not guess credentials or print secrets.'
|
|
},
|
|
{
|
|
Area: 'DataForSEO Reviews endpoint',
|
|
Status: reviewsDocs.endpointTemporarilyUnavailable === true || supportGate.ready !== true ? 'blocked' : 'ready',
|
|
Evidence: `docs unavailable=${reviewsDocs.endpointTemporarilyUnavailable === true}; support gate ready=${supportGate.ready === true}; payload rows ${dataforseoCounts.taskPayloadRows ?? 0}`,
|
|
NextAction: 'Do not run Reviews paid pilot until provider endpoint/support response and spend approval gates pass.'
|
|
},
|
|
{
|
|
Area: 'Blocker burn-down',
|
|
Status: burndown.ready === true ? 'ready' : 'blocked',
|
|
Evidence: `${burndownCounts.totalRemaining ?? 0} remaining evidence items`,
|
|
NextAction: 'Use blocker_burndown_next_actions.csv as the live owner checklist.'
|
|
}
|
|
]
|
|
|
|
const manifest = {
|
|
generatedAt,
|
|
date: dateKey,
|
|
ready: statusRows.every(row => row.Status === 'ready' || row.Status === 'connected'),
|
|
headline: `Handoff remains blocked: ${burndownCounts.totalRemaining ?? 0} evidence items remaining; verification ${verificationCounts.passing ?? 0}/${verificationCounts.checks ?? 0} passing; delivery packet ${deliveryCounts.present ?? 0}/${deliveryCounts.files ?? 0} present.`,
|
|
guardrails: [
|
|
'No fake approvals: approval/compliance CSVs must be filled by human reviewers.',
|
|
'No fake clinical claims: source-backed claim refs only, otherwise approved blank decision required.',
|
|
'No guessed DataForSEO credentials: scripts load DATAFORSEO_* from the pi-fresh-foundation .env path and record credentialPrinted=false.',
|
|
'No DataForSEO Reviews paid retry while the provider docs/support/spend gates remain blocked.'
|
|
],
|
|
rows: statusRows,
|
|
sourceFiles: {
|
|
handoffManifest: 'content_population_exports/handoff_manifest.json',
|
|
readinessAudit: 'content_population_exports/content_population_readiness.json',
|
|
verification: 'content_population_exports/handoff_verification_run.json',
|
|
deliveryIndex: 'content_population_exports/delivery_index.json',
|
|
blockerBurndown: 'content_population_exports/blocker_burndown.json',
|
|
dataforseoLiveConnection: 'content_population_exports/dataforseo_live_connection.json'
|
|
}
|
|
}
|
|
|
|
writeFileSync(join(outRoot, `daily_status_${dateKey}.json`), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, `daily_status_${dateKey}.csv`), csv(statusRows, ['Area', 'Status', 'Evidence', 'NextAction']), 'utf8')
|
|
writeFileSync(join(outRoot, `daily_status_${dateKey}.md`), [
|
|
`# JV daily handoff status - ${dateKey}`,
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
manifest.headline,
|
|
'',
|
|
'## Guardrails',
|
|
...manifest.guardrails.map(item => `- ${item}`),
|
|
'',
|
|
'## Status rows',
|
|
'| Area | Status | Evidence | Next action |',
|
|
'|---|---|---|---|',
|
|
...statusRows.map(row => `| ${row.Area} | ${row.Status} | ${String(row.Evidence).replace(/\|/g, '/')} | ${String(row.NextAction).replace(/\|/g, '/')} |`),
|
|
'',
|
|
'## Completion rule',
|
|
'This daily status is a progress snapshot. The goal is not complete until `bun run verify:handoff` passes and the export blockers, product approvals, compliance decisions, staging import logs, PDP spot checks, and cutover approval are all resolved with evidence.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Daily status ready: ${manifest.ready}`)
|
|
console.log(manifest.headline)
|