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>
169 lines
7.6 KiB
TypeScript
169 lines
7.6 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 })
|
|
const generatedAt = new Date().toISOString()
|
|
|
|
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 (Array.isArray(value)) value = value.join(' | ')
|
|
if (value === undefined || value === null) value = ''
|
|
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 handoff = readJson(join(outRoot, 'handoff_manifest.json'), {})
|
|
const readiness = readJson(join(outRoot, 'content_population_readiness.json'), {})
|
|
const blank = readJson(join(outRoot, 'clinical_blank_decision_template.json'), {})
|
|
const approvals = readJson(join(outRoot, 'product_approval_decision_gate.json'), {})
|
|
const staging = readJson(join(outRoot, 'staging_evidence_validation.json'), {})
|
|
const verifier = readJson(join(outRoot, 'handoff_verification_run.json'), {})
|
|
|
|
const lanes = [
|
|
{
|
|
Lane: 'Compliance clinical claim decisions',
|
|
Owner: 'Compliance owner / Umar',
|
|
StartCount: blank.counts?.decisions ?? 185,
|
|
DoneCount: blank.counts?.validBlankApprovals ?? 0,
|
|
RemainingCount: (blank.counts?.decisions ?? 185) - (blank.counts?.validBlankApprovals ?? 0),
|
|
EvidenceFile: 'data/content/compliance/clinical-blank-decisions.json',
|
|
Command: 'bun run compliance:blank-decisions && bun run claims:sync-content && bun run export:content',
|
|
ClearsBlockerType: 'compliance',
|
|
CurrentBlockers: handoff.blockerSummary?.compliance ?? 0,
|
|
StopRule: 'Use source-backed claim refs or explicit approved_blank rows only; no generated clinical claims.'
|
|
},
|
|
{
|
|
Lane: 'Product approval decisions',
|
|
Owner: 'Umar / JV reviewer',
|
|
StartCount: approvals.counts?.products ?? 190,
|
|
DoneCount: approvals.counts?.validApprovals ?? 0,
|
|
RemainingCount: (approvals.counts?.products ?? 190) - (approvals.counts?.validApprovals ?? 0),
|
|
EvidenceFile: 'data/content/approvals/product-approval-decisions.json',
|
|
Command: 'bun run approval:decisions && bun run approval:review-packet && bun run export:content',
|
|
ClearsBlockerType: 'approval',
|
|
CurrentBlockers: handoff.blockerSummary?.approval ?? 0,
|
|
StopRule: 'All review acknowledgements and no-fake-evidence acknowledgement must be true.'
|
|
},
|
|
{
|
|
Lane: 'Staging import logs',
|
|
Owner: 'Lewis/Euan',
|
|
StartCount: 3,
|
|
DoneCount: Math.max(0, (staging.counts?.importLogs ?? 0)),
|
|
RemainingCount: Math.max(0, 3 - (staging.counts?.importLogs ?? 0)),
|
|
EvidenceFile: 'data/staging/import-logs/',
|
|
Command: 'bun run staging:evidence',
|
|
ClearsBlockerType: 'staging_evidence',
|
|
CurrentBlockers: (staging.gates || []).filter((row: any) => String(row.Gate || '').startsWith('import_log') && row.Blocking === 'yes').length,
|
|
StopRule: 'Logs must show failedRows=0 and successfulRows meeting expected row count.'
|
|
},
|
|
{
|
|
Lane: 'PDP spot checks',
|
|
Owner: 'Dashboard owner + Lewis/Euan',
|
|
StartCount: 5,
|
|
DoneCount: staging.counts?.passingPdpChecks ?? 0,
|
|
RemainingCount: Math.max(0, 5 - (staging.counts?.passingPdpChecks ?? 0)),
|
|
EvidenceFile: 'data/staging/pdp-checks/',
|
|
Command: 'bun run staging:evidence',
|
|
ClearsBlockerType: 'staging_evidence',
|
|
CurrentBlockers: (staging.gates || []).some((row: any) => row.Gate === 'pdp_spot_checks' && row.Blocking === 'yes') ? 1 : 0,
|
|
StopRule: 'At least 5 real staging PDP JSON checks, all checks=pass and no issues.'
|
|
},
|
|
{
|
|
Lane: 'Cutover approval',
|
|
Owner: 'Umar / JV',
|
|
StartCount: 1,
|
|
DoneCount: staging.counts?.cutoverApprovalPresent && !(staging.gates || []).some((row: any) => row.Gate === 'cutover_approval' && row.Blocking === 'yes') ? 1 : 0,
|
|
RemainingCount: staging.counts?.cutoverApprovalPresent && !(staging.gates || []).some((row: any) => row.Gate === 'cutover_approval' && row.Blocking === 'yes') ? 0 : 1,
|
|
EvidenceFile: 'data/staging/cutover-approval.json',
|
|
Command: 'bun run staging:evidence && bun run verify:handoff',
|
|
ClearsBlockerType: 'cutover',
|
|
CurrentBlockers: (staging.gates || []).some((row: any) => row.Gate === 'cutover_approval' && row.Blocking === 'yes') ? 1 : 0,
|
|
StopRule: 'Must be signed after import logs and PDP checks pass.'
|
|
}
|
|
]
|
|
|
|
const totalRemaining = lanes.reduce((sum, row) => sum + Number(row.RemainingCount || 0), 0)
|
|
const totalStart = lanes.reduce((sum, row) => sum + Number(row.StartCount || 0), 0)
|
|
const totalDone = lanes.reduce((sum, row) => sum + Number(row.DoneCount || 0), 0)
|
|
const percentDone = totalStart ? Math.round((totalDone / totalStart) * 1000) / 10 : 0
|
|
|
|
const nextActions = lanes
|
|
.filter(row => row.RemainingCount > 0)
|
|
.map((row, index) => ({
|
|
Sequence: index + 1,
|
|
Lane: row.Lane,
|
|
Owner: row.Owner,
|
|
Remaining: row.RemainingCount,
|
|
EvidenceFile: row.EvidenceFile,
|
|
Command: row.Command,
|
|
StopRule: row.StopRule
|
|
}))
|
|
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: totalRemaining === 0 && Boolean(handoff.readyForLewis) && Boolean(readiness.ready) && Boolean(staging.ready) && Boolean(verifier.ready),
|
|
headline: totalRemaining === 0
|
|
? 'All burn-down lanes are complete; run final verification.'
|
|
: `${totalRemaining} review/evidence items remain across ${lanes.filter(row => row.RemainingCount > 0).length} lanes before handoff can be ready.`,
|
|
counts: {
|
|
totalStart,
|
|
totalDone,
|
|
totalRemaining,
|
|
percentDone,
|
|
exportBlockers: handoff.counts?.blockers ?? null,
|
|
verifierPassing: verifier.counts?.passing ?? null,
|
|
verifierChecks: verifier.counts?.checks ?? null
|
|
},
|
|
lanes,
|
|
nextActions
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'blocker_burndown.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'blocker_burndown.csv'), csv(lanes, ['Lane', 'Owner', 'StartCount', 'DoneCount', 'RemainingCount', 'EvidenceFile', 'Command', 'ClearsBlockerType', 'CurrentBlockers', 'StopRule']), 'utf8')
|
|
writeFileSync(join(outRoot, 'blocker_burndown_next_actions.csv'), csv(nextActions, ['Sequence', 'Lane', 'Owner', 'Remaining', 'EvidenceFile', 'Command', 'StopRule']), 'utf8')
|
|
writeFileSync(join(outRoot, 'blocker_burndown.md'), [
|
|
'# JV blocker burn-down',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`Headline: **${manifest.headline}**`,
|
|
'',
|
|
'## Progress',
|
|
`- Total tracked items: ${totalStart}`,
|
|
`- Done: ${totalDone}`,
|
|
`- Remaining: ${totalRemaining}`,
|
|
`- Percent done: ${percentDone}%`,
|
|
`- Export blockers: ${manifest.counts.exportBlockers}`,
|
|
`- Master verifier: ${manifest.counts.verifierPassing}/${manifest.counts.verifierChecks}`,
|
|
'',
|
|
'## Lanes',
|
|
...lanes.map(row => [
|
|
`### ${row.Lane}`,
|
|
`- Owner: ${row.Owner}`,
|
|
`- Done / remaining: ${row.DoneCount} / ${row.RemainingCount}`,
|
|
`- Evidence file: \`${row.EvidenceFile}\``,
|
|
`- Command: \`${row.Command}\``,
|
|
`- Stop rule: ${row.StopRule}`,
|
|
''
|
|
].join('\n')),
|
|
'## Next actions',
|
|
...nextActions.map(row => `- ${row.Sequence}. ${row.Owner}: finish ${row.Remaining} item(s) in ${row.Lane}. Evidence: \`${row.EvidenceFile}\`.`),
|
|
'',
|
|
'## Guardrail',
|
|
'This burn-down tracks explicit evidence, not intent. Counts only move when evidence files validate.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Blocker burn-down ready: ${manifest.ready}`)
|
|
console.log(`Remaining tracked items: ${totalRemaining}`)
|
|
if (!manifest.ready) process.exitCode = 1
|