Files
justvitamin/scripts/generate-handoff-action-plan.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

169 lines
9.6 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const root = process.cwd()
const outRoot = join(root, 'content_population_exports')
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 sourcePack = readJson(join(outRoot, 'source_request_manifest.json'), {})
const assets = readJson(join(outRoot, 'asset_manifest_validation.json'), {})
const intelligence = readJson(join(outRoot, 'intelligence_generation_workqueue.json'), {})
const claims = readJson(join(outRoot, 'claims_library_validation.json'), {})
const sourceDrops = readJson(join(outRoot, 'source_drop_validation.json'), {})
const generatedAt = new Date().toISOString()
const claimsReady = Boolean(claims.ready)
const sourceDropsReady = Boolean(sourceDrops.ready)
const intelligenceReady = Boolean(intelligence.ready)
const assetsReady = Number(assets.missingUploadCount ?? 0) === 0 && Number(assets.missingReferenceCount ?? 0) === 0
const approvalsReady = readiness.gates?.find((gate: any) => gate.name === 'human_approval_complete')?.ready === true
const handoffReady = Boolean(handoff.readyForLewis)
const actions = [
{
Priority: claimsReady ? 'Done' : 'P0',
Status: claimsReady ? 'complete' : 'blocked',
Lane: 'Regulatory / compliance',
Owner: 'Umar + regulatory source owner',
Action: claimsReady ? 'Claims library present; continue SKU-level compliance approval' : 'Provide source-backed clinical claims library',
EvidenceNeeded: 'data/sources/claims/claims-library.json populated with approved claims, citations/source URLs, disclaimer, approval owner/date, and SKU mappings.',
CurrentEvidence: `claims ready=${Boolean(claims.ready)}; claims=${claims.claimCount || 0}; issues=${claims.issueCount ?? claims.issues?.length ?? 'unknown'}`,
Blocks: 'clinically_shown_to metaobjects, compliance approval, Matrixify handoff readiness',
OutputToRegenerate: 'bun run validate:claims && bun run audit:content && bun run export:content'
},
{
Priority: sourceDropsReady ? 'Watch' : 'P0',
Status: sourceDropsReady ? 'source_seed_present_limited_coverage' : 'blocked',
Lane: 'Research source drops',
Owner: 'Research / data extraction owner',
Action: sourceDropsReady ? 'Source drops validate, but Amazon remains limited seed coverage until provider/manual review path lands' : 'Drop raw Amazon, Trustpilot, and Reddit/social exports in the expected folders',
EvidenceNeeded: 'JSON source files under data/sources/amazon/, data/sources/trustpilot/, and data/sources/reddit/ matching templates from validate:sources.',
CurrentEvidence: `sourceDrops ready=${Boolean(sourceDrops.ready)}; issues=${sourceDrops.issues?.length ?? 0}; source request rows=${sourcePack.counts?.sourceRequestItems || 0}`,
Blocks: 'full catalogue intelligence, competitor comparison, audience profile, collagen NPD, source readiness',
OutputToRegenerate: 'bun run validate:sources && bun run intel:refresh && bun run intelligence:queue'
},
{
Priority: assetsReady ? 'Done' : 'P0',
Status: assetsReady ? 'complete' : 'blocked',
Lane: 'Image production / Shopify Files',
Owner: 'Design + Shopify file uploader',
Action: 'Produce/upload every referenced PDP and metaobject asset filename',
EvidenceNeeded: 'All filenames in asset_manifest_validation.csv exist in C:/Users/uldvs/OneDrive/Desktop/jv-assets or Shopify Files before Matrixify import.',
CurrentEvidence: `required=${assets.requiredCount ?? 0}; missingUploads=${assets.missingUploadCount ?? 0}; missingReferences=${assets.missingReferenceCount ?? 0}`,
Blocks: 'Matrixify file_reference import, PDP visual modules, final handoff readiness',
OutputToRegenerate: 'bun run validate:assets && bun run export:content'
},
{
Priority: intelligenceReady ? 'Done' : 'P1',
Status: intelligenceReady ? 'complete_as_queue_coverage' : 'blocked',
Lane: 'Content intelligence generation',
Owner: 'JV dashboard generation lane',
Action: intelligenceReady ? 'Catalogue intelligence file coverage is complete; keep queue-source caveats through approval' : 'Generate all required intelligence files for every catalogue SKU after source drops land',
EvidenceNeeded: 'Each SKU has all 12 intelligence JSON files: Feefo-derived views, image/photo briefs, competitor, Amazon, Reddit, flavour, audience, and claims evidence.',
CurrentEvidence: `ready=${Boolean(intelligence.ready)}; missingTasks=${intelligence.missingTasks ?? 0}; affectedSkus=${intelligence.affectedSkus ?? 0}`,
Blocks: 'product content generation, audience/claims fields, approval queue, readiness audit',
OutputToRegenerate: 'bun run intelligence:queue && bun run audit:content'
},
{
Priority: approvalsReady ? 'Done' : 'P1',
Status: approvalsReady ? 'complete' : 'blocked',
Lane: 'Human review / approvals',
Owner: 'Umar / JV reviewer',
Action: 'Approve content, compliance, image, and export lanes in dashboard',
EvidenceNeeded: 'Every product content record has approval.content=approved, approval.compliance=approved, approval.images=ready, approval.export=ready.',
CurrentEvidence: `human approval gate=${readiness.gates?.find((gate: any) => gate.name === 'human_approval_complete')?.status || 'unknown'}`,
Blocks: 'final handoff readiness and Lewis/Euan import confidence',
OutputToRegenerate: 'bun run audit:content && bun run export:content'
},
{
Priority: handoffReady ? 'Done' : 'P2',
Status: handoffReady ? 'complete' : 'blocked',
Lane: 'Lewis/Euan staging',
Owner: 'Lewis/Euan / Kubix',
Action: 'Run Matrixify staging import and spot-check 5-10 PDPs',
EvidenceNeeded: 'Staging import logs plus PDP checks for metafields, metaobject refs, file refs, variants, FAQs, promo cards, and collection mappings.',
CurrentEvidence: `readyForLewis=${Boolean(handoff.readyForLewis)}; blockers=${handoff.counts?.blockers ?? 'unknown'}`,
Blocks: 'production cutover',
OutputToRegenerate: 'py build_metaobjects_csv.py; py build_products_csv_v5.py; py build_smart_collections_csv.py; py validate_assets_manifest.py'
}
]
const manifest = {
generatedAt,
ready: assetsReady && approvalsReady && handoffReady,
objective: 'JV Shopify content population handoff',
counts: {
actions: actions.length,
blockedActions: actions.filter(action => action.Status === 'blocked').length,
completedActions: actions.filter(action => action.Status === 'complete' || action.Status === 'complete_as_queue_coverage').length,
watchActions: actions.filter(action => action.Priority === 'Watch').length,
p0: actions.filter(action => action.Priority === 'P0').length,
p1: actions.filter(action => action.Priority === 'P1').length,
p2: actions.filter(action => action.Priority === 'P2').length,
exportBlockers: handoff.counts?.blockers ?? null,
missingAssetUploads: assets.missingUploadCount ?? null,
missingIntelligenceTasks: intelligence.missingTasks ?? null,
sourceRequestItems: sourcePack.counts?.sourceRequestItems ?? null
},
actions
}
writeFileSync(join(outRoot, 'handoff_action_plan.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
writeFileSync(join(outRoot, 'handoff_action_plan.csv'), csv(actions, ['Priority', 'Status', 'Lane', 'Owner', 'Action', 'EvidenceNeeded', 'CurrentEvidence', 'Blocks', 'OutputToRegenerate']), 'utf8')
const md = [
'# JV handoff action plan',
'',
`Generated: ${generatedAt}`,
'',
'This is the execution-facing blocker plan for moving the dashboard from preview-ready to Lewis/Euan handoff-ready. It is evidence-led and does not mark blocked lanes as complete.',
'',
'## Current blocker counts',
`- Export blockers: ${manifest.counts.exportBlockers}`,
`- Missing asset uploads/files: ${manifest.counts.missingAssetUploads}`,
`- Missing intelligence tasks: ${manifest.counts.missingIntelligenceTasks}`,
`- Source request items: ${manifest.counts.sourceRequestItems}`,
`- Active blocked actions: ${manifest.counts.blockedActions}`,
`- Completed/covered actions: ${manifest.counts.completedActions}`,
`- Watch actions: ${manifest.counts.watchActions}`,
'',
'## Actions',
...actions.map(action => [
`### ${action.Priority} - ${action.Lane}`,
`- Status: ${action.Status}`,
`- Owner: ${action.Owner}`,
`- Action: ${action.Action}`,
`- Evidence needed: ${action.EvidenceNeeded}`,
`- Current evidence: ${action.CurrentEvidence}`,
`- Blocks: ${action.Blocks}`,
`- Regenerate: \`${action.OutputToRegenerate}\``,
''
].join('\n')),
'## Stop condition',
'Only mark this handoff ready when `bun run audit:content` passes, `handoff_manifest.json.readyForLewis` is true, Matrixify staging import is complete, and PDP spot checks have passed.',
''
].join('\n')
writeFileSync(join(outRoot, 'handoff_action_plan.md'), md, 'utf8')
console.log(`Handoff action plan ready: ${manifest.ready}`)
console.log(`Actions: ${actions.length}`)
console.log(`P0 actions: ${manifest.counts.p0}`)
console.log(`Blocked actions: ${manifest.counts.blockedActions}`)
if (!manifest.ready) process.exitCode = 1