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>
194 lines
8.9 KiB
TypeScript
194 lines
8.9 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
const root = process.cwd()
|
|
const outRoot = join(root, 'content_population_exports')
|
|
const stagingRoot = join(root, 'data', 'staging')
|
|
const pdpQueueRoot = join(stagingRoot, 'pdp-check-queue')
|
|
const migrationOutRoot = join(root, '..', 'JV Migration to shopify', 'content_population_exports')
|
|
const productsRoot = join(root, 'data', 'content', 'products')
|
|
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'
|
|
}
|
|
function fileMeta(label: string, path: string, importOrder: number | '') {
|
|
const present = existsSync(path)
|
|
const stat = present ? statSync(path) : null
|
|
return {
|
|
Label: label,
|
|
ImportOrder: importOrder,
|
|
Path: path,
|
|
Present: present ? 'yes' : 'no',
|
|
SizeBytes: stat?.size || 0,
|
|
LastModified: stat?.mtime?.toISOString() || '',
|
|
Use: importOrder === '' ? 'Evidence/reference only' : 'Matrixify staging import candidate'
|
|
}
|
|
}
|
|
|
|
mkdirSync(outRoot, { recursive: true })
|
|
mkdirSync(pdpQueueRoot, { recursive: true })
|
|
|
|
const handoff = readJson(join(outRoot, 'handoff_manifest.json'), {})
|
|
const approvalGate = readJson(join(outRoot, 'product_approval_decision_gate.json'), {})
|
|
const blankGate = readJson(join(outRoot, 'clinical_blank_decision_template.json'), {})
|
|
const products = readdirSync(productsRoot)
|
|
.filter(file => file.endsWith('.json'))
|
|
.sort()
|
|
.map(file => readJson(join(productsRoot, file), {}))
|
|
|
|
const importFiles = [
|
|
fileMeta('Metaobjects', join(migrationOutRoot, 'metaobjects_all.csv'), 1),
|
|
fileMeta('Smart collection promo cards', join(migrationOutRoot, 'smart_collections_promo_cards.csv'), 2),
|
|
fileMeta('Product/variant metafields', join(migrationOutRoot, 'products_content_v5.csv'), 3),
|
|
fileMeta('Product blockers', join(migrationOutRoot, 'products_content_v5_blockers.csv'), ''),
|
|
fileMeta('Asset validation', join(migrationOutRoot, 'asset_manifest_validation.csv'), ''),
|
|
fileMeta('Dashboard handoff manifest', join(outRoot, 'handoff_manifest.json'), ''),
|
|
fileMeta('Dashboard export blockers', join(outRoot, 'export_blockers.csv'), '')
|
|
]
|
|
|
|
const prioritySkus = ['JV-D1000', 'JV-D4000', 'JV-TURMERIC500', 'JV-D400', 'JV-5HTP100', 'JV-B12', 'JV-COLLAGEN400', 'JV-OMEGA31000', 'JV-VISISOFT', 'JV-ZINC10']
|
|
const productBySku = new Map(products.map(product => [product.sku, product]))
|
|
const sampleRows = prioritySkus
|
|
.filter(sku => productBySku.has(sku))
|
|
.map((sku, index) => {
|
|
const product: any = productBySku.get(sku)
|
|
const row = {
|
|
Sequence: index + 1,
|
|
SKU: sku,
|
|
Handle: product.handle || '',
|
|
ProductName: product.name || '',
|
|
Reason: index < 3 ? 'pilot/high-touch content SKU' : product.metafields?.pdpClinicallyShownTo ? 'source-backed clinical claim mapped' : 'representative blank-claim/approval blocker SKU',
|
|
ClinicalRef: product.metafields?.pdpClinicallyShownTo || '',
|
|
RequiredChecks: 'productMetafields | variantMetafields | metaobjectReferences | fileReferences | faqFields | promoCard | collectionMapping | mobileLayout',
|
|
TemplatePath: `data/staging/pdp-check-queue/${sku}.json`
|
|
}
|
|
const template = {
|
|
sku,
|
|
handle: row.Handle,
|
|
productName: row.ProductName,
|
|
reason: row.Reason,
|
|
stagingUrl: '',
|
|
checkedAt: '',
|
|
checkedBy: '',
|
|
checks: {
|
|
productMetafields: 'pending',
|
|
variantMetafields: 'pending',
|
|
metaobjectReferences: 'pending',
|
|
fileReferences: 'pending',
|
|
faqFields: 'pending',
|
|
promoCard: 'pending',
|
|
collectionMapping: 'pending',
|
|
mobileLayout: 'pending'
|
|
},
|
|
issues: [],
|
|
approvedForProduction: false,
|
|
note: 'Copy completed passing checks to data/staging/pdp-checks/ after real staging URL review. Do not mark pass without inspecting staging.'
|
|
}
|
|
writeFileSync(join(pdpQueueRoot, `${sku}.json`), JSON.stringify(template, null, 2) + '\n', 'utf8')
|
|
return row
|
|
})
|
|
|
|
const actionRows = [
|
|
{
|
|
Sequence: 1,
|
|
Owner: 'Dashboard owner',
|
|
Action: 'Resolve current dashboard blockers before sending import files as ready.',
|
|
RequiredEvidence: 'handoff_manifest.readyForLewis=true and export_blockers.csv empty.',
|
|
CurrentEvidence: `readyForLewis=${Boolean(handoff.readyForLewis)}; blockers=${handoff.counts?.blockers ?? 'unknown'}`
|
|
},
|
|
{
|
|
Sequence: 2,
|
|
Owner: 'Compliance owner',
|
|
Action: 'Complete source-backed claims or approved blank decisions.',
|
|
RequiredEvidence: 'clinical_blank_decision_template ready OR all needed clinical refs mapped and approved.',
|
|
CurrentEvidence: `validBlankApprovals=${blankGate.counts?.validBlankApprovals ?? 0}; pendingBlankDecisions=${blankGate.counts?.pending ?? 'unknown'}`
|
|
},
|
|
{
|
|
Sequence: 3,
|
|
Owner: 'Umar / JV reviewer',
|
|
Action: 'Complete product approval decision file.',
|
|
RequiredEvidence: 'product_approval_decision_gate ready; 190 valid approvals or explicitly rejected rows resolved.',
|
|
CurrentEvidence: `validApprovals=${approvalGate.counts?.validApprovals ?? 0}; pending=${approvalGate.counts?.pending ?? 'unknown'}`
|
|
},
|
|
{
|
|
Sequence: 4,
|
|
Owner: 'Lewis/Euan',
|
|
Action: 'Import files in order on staging and capture Matrixify logs.',
|
|
RequiredEvidence: 'Clean import logs saved under data/staging/import-logs/.',
|
|
CurrentEvidence: 'No import logs captured yet.'
|
|
},
|
|
{
|
|
Sequence: 5,
|
|
Owner: 'Dashboard owner + Lewis/Euan',
|
|
Action: 'Complete PDP spot-check queue against real staging URLs.',
|
|
RequiredEvidence: 'At least 5 passing JSON checks copied to data/staging/pdp-checks/.',
|
|
CurrentEvidence: `${sampleRows.length} PDP check queue templates generated; no completed checks claimed.`
|
|
}
|
|
]
|
|
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: Boolean(handoff.readyForLewis),
|
|
purpose: 'Lewis/Euan staging handoff packet with exact import files, sequence, blockers, and PDP spot-check queue. It does not claim staging evidence is complete.',
|
|
counts: {
|
|
importFiles: importFiles.length,
|
|
presentImportFiles: importFiles.filter(row => row.Present === 'yes').length,
|
|
pdpSpotCheckQueue: sampleRows.length,
|
|
currentExportBlockers: handoff.counts?.blockers ?? null,
|
|
approvalPending: approvalGate.counts?.pending ?? null,
|
|
blankDecisionPending: blankGate.counts?.pending ?? null
|
|
},
|
|
importFiles,
|
|
sampleRows,
|
|
actionRows
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'staging_handoff_packet.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'staging_handoff_import_files.csv'), csv(importFiles, ['Label', 'ImportOrder', 'Path', 'Present', 'SizeBytes', 'LastModified', 'Use']), 'utf8')
|
|
writeFileSync(join(outRoot, 'staging_pdp_spot_check_queue.csv'), csv(sampleRows, ['Sequence', 'SKU', 'Handle', 'ProductName', 'Reason', 'ClinicalRef', 'RequiredChecks', 'TemplatePath']), 'utf8')
|
|
writeFileSync(join(outRoot, 'staging_handoff_actions.csv'), csv(actionRows, ['Sequence', 'Owner', 'Action', 'RequiredEvidence', 'CurrentEvidence']), 'utf8')
|
|
writeFileSync(join(outRoot, 'staging_handoff_packet.md'), [
|
|
'# JV staging handoff packet',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
'This is the Lewis/Euan staging handoff packet. It gives the import sequence and check queue, but it does not mark staging complete.',
|
|
'',
|
|
`Ready to hand off: **${manifest.ready ? 'YES' : 'NO'}**`,
|
|
'',
|
|
'## Current blockers',
|
|
`- Export blockers: ${manifest.counts.currentExportBlockers}`,
|
|
`- Product approval pending: ${manifest.counts.approvalPending}`,
|
|
`- Blank clinical decision pending: ${manifest.counts.blankDecisionPending}`,
|
|
'',
|
|
'## Import files',
|
|
...importFiles.map(row => `- ${row.ImportOrder || 'ref'}: ${row.Label} — ${row.Present} — \`${row.Path}\``),
|
|
'',
|
|
'## PDP spot-check queue',
|
|
...sampleRows.map(row => `- ${row.Sequence}. ${row.SKU} — ${row.ProductName} (${row.Reason})`),
|
|
'',
|
|
'## Actions',
|
|
...actionRows.map(row => `- ${row.Sequence}. ${row.Owner}: ${row.Action} Current: ${row.CurrentEvidence}`),
|
|
'',
|
|
'## Rule',
|
|
'Do not copy queued PDP templates into `data/staging/pdp-checks/` until the real staging URL has been inspected and every check is actually pass.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Staging handoff packet ready: ${manifest.ready}`)
|
|
console.log(`Import files present: ${manifest.counts.presentImportFiles}/${manifest.counts.importFiles}`)
|
|
console.log(`PDP spot-check queue: ${manifest.counts.pdpSpotCheckQueue}`)
|
|
if (!manifest.ready) process.exitCode = 1
|