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>
145 lines
12 KiB
TypeScript
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
|