Files
justvitamin/scripts/generate-blocker-burndown.ts
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
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>
2026-06-02 18:50:09 +08:00

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