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
9.6 KiB
TypeScript
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
|