Files
justvitamin/scripts/generate-handoff-gate-dependency-map.ts
T
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

145 lines
12 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 verification = readJson(join(outRoot, 'handoff_verification_run.json'), { checks: [] })
const preflight = readJson(join(outRoot, 'reviewer_input_preflight.json'), { blockingRows: [] })
const ownerResponses = readJson(join(outRoot, 'owner_response_intake.json'), { rows: [] })
const evidenceDossier = readJson(join(outRoot, 'handoff_evidence_dossier.json'), { requirements: [] })
const gateMeta: Record<string, any> = {
export_content: { owner: 'Dashboard', lane: 'Export generation', inputs: ['data/content/products/', 'data/content/metaobjects.json', 'data/content/smart-collections.json'], unblock: 'Already passing; rerun after approvals or content changes.', upstream: [] },
validate_assets: { owner: 'Dashboard / design', lane: 'Asset file presence', inputs: ['C:/Users/uldvs/OneDrive/Desktop/jv-assets', 'content_population_exports/asset_manifest_validation.*'], unblock: 'Already passing for file presence; human image approval is separate.', upstream: [] },
validate_template: { owner: 'Dashboard', lane: 'Matrixify template schema', inputs: ['Just Vitamins - Content Population Templates workbook', 'content_population_exports/*matrixify.csv'], unblock: 'Already passing; rerun after emitter changes.', upstream: [] },
source_request_pack: { owner: 'Research / compliance / image', lane: 'Source request pack', inputs: ['content_population_exports/source_request_*'], unblock: 'Already passing; use as owner request evidence.', upstream: [] },
validate_sources: { owner: 'Research', lane: 'Source drop validation', inputs: ['data/sources/amazon/', 'data/sources/trustpilot/', 'data/sources/reddit/'], unblock: 'Already passing for current source drops; more source evidence may still be needed by owners.', upstream: [] },
validate_claims: { owner: 'Compliance', lane: 'Claims library validation', inputs: ['data/content/claims-library.json'], unblock: 'Already passing structurally; remaining blank clinical claims still need decisions or source-backed refs.', upstream: [] },
clinical_blank_decisions: { owner: 'Compliance owner / Umar', lane: 'Compliance blank/source-backed claim decisions', inputs: ['data/content/compliance/clinical-blank-decisions.csv'], unblock: 'Fill approved_blank rows with approvedBy, approvedAt, rationale, constraintsAcknowledged=yes or provide source-backed claim refs and rerun claims sync.', upstream: ['Compliance clinical blanks'] },
intelligence_queue: { owner: 'Dashboard intelligence lane', lane: 'Full catalogue intelligence coverage', inputs: ['data/intelligence/**'], unblock: 'Already passing; rerun if product scope changes.', upstream: [] },
product_approval_decisions: { owner: 'Umar / JV reviewer', lane: 'Product approval decisions', inputs: ['data/content/approvals/product-approval-decisions.csv'], unblock: 'Fill approve_all decisions with approver, timestamp, review evidence, and all acknowledgements.', upstream: ['Product approval decisions'] },
product_approval_review_packet: { owner: 'Umar / JV reviewer', lane: 'Product approval review packet', inputs: ['data/content/approvals/product-approval-decisions.csv', 'content_population_exports/product_approval_review_packet.*'], unblock: 'Clears only after product approvals validate and no product remains blocked.', upstream: ['Product approval decisions'] },
handoff_actions: { owner: 'Project owner', lane: 'Handoff action closure', inputs: ['content_population_exports/handoff_action_plan.*'], unblock: 'Closes after compliance, approval, staging, and provider owner lanes are resolved.', upstream: ['Compliance clinical blanks', 'Product approval decisions', 'Staging import evidence', 'DataForSEO provider response', 'DataForSEO spend approval'] },
executive_decision_brief: { owner: 'Project owner / Umar / Lewis / Euan', lane: 'Executive decision closure', inputs: ['content_population_exports/executive_decision_brief.*'], unblock: 'Closes after named decisions have recorded evidence and owners.', upstream: ['Compliance clinical blanks', 'Product approval decisions', 'Staging import evidence', 'DataForSEO provider response'] },
staging_handoff_packet: { owner: 'Lewis/Euan + dashboard owner', lane: 'Staging handoff readiness', inputs: ['content_population_exports/staging_handoff_packet.*', 'data/staging/**'], unblock: 'Import files are present; real import/PDP/cutover evidence must be recorded.', upstream: ['Staging import evidence', 'Staging PDP spot checks', 'Staging cutover approval'] },
staging_evidence: { owner: 'Lewis/Euan + dashboard owner', lane: 'Staging evidence', inputs: ['data/staging/import-logs/', 'data/staging/pdp-checks/', 'data/staging/cutover-approval.json'], unblock: 'Save clean import logs, at least 5 real PDP spot checks, and signed cutover approval.', upstream: ['Staging import evidence', 'Staging PDP spot checks', 'Staging cutover approval'] },
staging_checklist: { owner: 'Lewis/Euan + dashboard owner', lane: 'Staging checklist', inputs: ['content_population_exports/staging_import_checklist.*', 'data/staging/**'], unblock: 'Clears after staging evidence validator passes and preflight/import/PDP/cutover checks are recorded.', upstream: ['Staging import evidence', 'Staging PDP spot checks', 'Staging cutover approval'] },
delivery_index: { owner: 'Project owner', lane: 'Delivery packet completeness', inputs: ['content_population_exports/delivery_index.*'], unblock: 'Already passing; only proves files exist, not final readiness.', upstream: [] },
content_audit: { owner: 'Dashboard / project owner', lane: 'Final content audit', inputs: ['content_population_exports/content_population_readiness.*'], unblock: 'Final aggregate gate; clears after compliance and approvals resolve remaining export blockers.', upstream: ['Compliance clinical blanks', 'Product approval decisions'] }
}
function preflightMatches(meta: any, row: any) {
const areas = meta.upstream || []
return areas.some((area: string) => row.Area === area)
}
function responseMatches(meta: any, row: any) {
return row.Lane === meta.lane || row.Owner === meta.owner || String(meta.lane || '').includes(row.Lane) || String(row.Lane || '').includes(meta.lane)
}
const rows = (verification.checks || []).map((check: any, index: number) => {
const meta = gateMeta[check.Name] || { owner: 'Project owner', lane: check.Name, inputs: [], unblock: 'Inspect command output and rerun the named command.', upstream: [] }
const preflightRows = (preflight.blockingRows || []).filter((row: any) => preflightMatches(meta, row))
const responseRows = (ownerResponses.rows || []).filter((row: any) => responseMatches(meta, row))
const requirementRows = (evidenceDossier.requirements || []).filter((req: any) => String(req.RemainingWork || '').includes(meta.owner) || String(req.Requirement || '').toLowerCase().includes(String(meta.lane || '').split(' ')[0]?.toLowerCase()))
const blockerEvidence = preflightRows.length
? preflightRows.map((row: any) => `${row.Area}: ${row.Evidence}`).join(' | ')
: check.Status === 'pass'
? String(check.Stdout || '').split('\n').slice(0, 2).join(' | ')
: String(check.Stdout || check.Stderr || '').split('\n').slice(0, 2).join(' | ')
return {
Sequence: index + 1,
Gate: check.Name,
Status: check.Status,
ExitCode: check.ExitCode,
Command: check.Command,
Owner: meta.owner,
Lane: meta.lane,
Inputs: meta.inputs,
UpstreamBlockers: meta.upstream,
BlockerEvidence: blockerEvidence,
OwnerResponseStatus: responseRows.map((row: any) => `${row.Lane}: ${row.Status}`).join(' | ') || (check.Status === 'pass' ? 'not needed while passing' : 'no owner response recorded'),
EvidenceDossierLinks: requirementRows.map((row: any) => row.Requirement),
UnblockInstruction: meta.unblock,
NextCommand: check.Command
}
})
const blockedRows = rows.filter(row => row.Status !== 'pass')
const ownerGroups = [...new Set(blockedRows.map(row => row.Owner))].map(owner => ({
Owner: owner,
BlockedGates: blockedRows.filter(row => row.Owner === owner).length,
Gates: blockedRows.filter(row => row.Owner === owner).map(row => row.Gate).join(' | '),
FirstAction: blockedRows.find(row => row.Owner === owner)?.UnblockInstruction || ''
}))
const manifest = {
generatedAt,
ready: blockedRows.length === 0,
purpose: 'Dependency map from master verifier gates to the owner inputs and evidence that unblock them. It prevents rerunning gates blindly and keeps owner/external blockers explicit.',
counts: {
gates: rows.length,
passing: rows.filter(row => row.Status === 'pass').length,
blocked: blockedRows.length,
ownersWithBlockedGates: ownerGroups.length
},
rows,
blockedRows,
ownerGroups,
files: {
markdown: 'content_population_exports/handoff_gate_dependency_map.md',
json: 'content_population_exports/handoff_gate_dependency_map.json',
csv: 'content_population_exports/handoff_gate_dependency_map.csv',
ownerCsv: 'content_population_exports/handoff_gate_dependency_by_owner.csv'
}
}
writeFileSync(join(outRoot, 'handoff_gate_dependency_map.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
writeFileSync(join(outRoot, 'handoff_gate_dependency_map.csv'), csv(rows, ['Sequence', 'Gate', 'Status', 'ExitCode', 'Command', 'Owner', 'Lane', 'Inputs', 'UpstreamBlockers', 'BlockerEvidence', 'OwnerResponseStatus', 'EvidenceDossierLinks', 'UnblockInstruction', 'NextCommand']), 'utf8')
writeFileSync(join(outRoot, 'handoff_gate_dependency_by_owner.csv'), csv(ownerGroups, ['Owner', 'BlockedGates', 'Gates', 'FirstAction']), 'utf8')
writeFileSync(join(outRoot, 'handoff_gate_dependency_map.md'), [
'# Handoff gate dependency map',
'',
`Generated: ${generatedAt}`,
'',
`Ready: **${manifest.ready ? 'YES' : 'NO'}**`,
`Gates passing: ${manifest.counts.passing}/${manifest.counts.gates}`,
`Blocked gates: ${manifest.counts.blocked}`,
`Owners with blocked gates: ${manifest.counts.ownersWithBlockedGates}`,
'',
'This map ties each master verifier gate to the actual owner input needed to unblock it. It does not approve content, create staging proof, or authorize provider spend.',
'',
'## Blocked gates by owner',
'| Owner | Blocked gates | Gates | First action |',
'|---|---:|---|---|',
...ownerGroups.map(row => `| ${row.Owner} | ${row.BlockedGates} | ${row.Gates.replace(/\|/g, '/')} | ${row.FirstAction.replace(/\|/g, '/')} |`),
'',
'## Blocked gates',
...blockedRows.map(row => `- **${row.Gate}** (${row.Owner}): ${row.BlockerEvidence || row.UnblockInstruction}. Next: ${row.UnblockInstruction}`),
'',
'## Final rule',
'Use this map to route work, then rerun the named gate commands. Final readiness still requires `bun run verify:handoff` to pass.',
''
].join('\n'), 'utf8')
console.log(`Handoff gate dependency map ready: ${manifest.ready}`)
console.log(`Gates passing: ${manifest.counts.passing}/${manifest.counts.gates}`)
console.log(`Blocked gates: ${manifest.counts.blocked}`)
if (!manifest.ready) process.exitCode = 1