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>
182 lines
6.8 KiB
TypeScript
182 lines
6.8 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, mkdirSync, readdirSync, readFileSync, 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 templateRoot = join(stagingRoot, '_templates')
|
|
const importLogRoot = join(stagingRoot, 'import-logs')
|
|
const pdpCheckRoot = join(stagingRoot, 'pdp-checks')
|
|
|
|
function readJson(path: string, fallback: any = null) {
|
|
if (!existsSync(path)) return fallback
|
|
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
|
|
}
|
|
|
|
function writeJson(path: string, data: any) {
|
|
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
}
|
|
|
|
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 listJson(dir: string) {
|
|
if (!existsSync(dir)) return []
|
|
return readdirSync(dir)
|
|
.filter(file => file.endsWith('.json'))
|
|
.map(file => ({ file, path: join(dir, file), data: readJson(join(dir, file), {}) }))
|
|
}
|
|
|
|
mkdirSync(outRoot, { recursive: true })
|
|
mkdirSync(templateRoot, { recursive: true })
|
|
mkdirSync(importLogRoot, { recursive: true })
|
|
mkdirSync(pdpCheckRoot, { recursive: true })
|
|
|
|
const generatedAt = new Date().toISOString()
|
|
const handoff = readJson(join(outRoot, 'handoff_manifest.json'), {})
|
|
|
|
const importLogTemplate = {
|
|
importType: 'products|metaobjects|smart_collections',
|
|
sourceFile: 'products_content_v5.csv',
|
|
environment: 'staging',
|
|
importedAt: '2026-05-19T00:00:00.000Z',
|
|
importedBy: 'Lewis/Euan',
|
|
totalRows: 350,
|
|
successfulRows: 350,
|
|
failedRows: 0,
|
|
warningRows: 0,
|
|
matrixifyJobUrl: '',
|
|
notes: ''
|
|
}
|
|
const pdpTemplate = {
|
|
sku: 'JV-D1000',
|
|
handle: 'vitamin-d3-1000iu',
|
|
stagingUrl: 'https://staging-store.myshopify.com/products/example',
|
|
checkedAt: '2026-05-19T00:00:00.000Z',
|
|
checkedBy: 'Dashboard owner / Lewis / Euan',
|
|
checks: {
|
|
productMetafields: 'pass',
|
|
variantMetafields: 'pass',
|
|
metaobjectReferences: 'pass',
|
|
fileReferences: 'pass',
|
|
faqFields: 'pass',
|
|
promoCard: 'pass',
|
|
collectionMapping: 'pass',
|
|
mobileLayout: 'pass'
|
|
},
|
|
issues: [],
|
|
approvedForProduction: false
|
|
}
|
|
const cutoverTemplate = {
|
|
approved: false,
|
|
approvedBy: '',
|
|
approvedAt: '',
|
|
stagingImportEvidence: 'data/staging/import-logs/',
|
|
pdpSpotCheckEvidence: 'data/staging/pdp-checks/',
|
|
knownRisksAccepted: [],
|
|
notes: ''
|
|
}
|
|
writeJson(join(templateRoot, 'import-log-template.json'), importLogTemplate)
|
|
writeJson(join(templateRoot, 'pdp-spot-check-template.json'), pdpTemplate)
|
|
writeJson(join(templateRoot, 'cutover-approval-template.json'), cutoverTemplate)
|
|
|
|
const importLogs = listJson(importLogRoot)
|
|
const pdpChecks = listJson(pdpCheckRoot)
|
|
const cutover = readJson(join(stagingRoot, 'cutover-approval.json'), null)
|
|
const expectedImports = [
|
|
{ type: 'metaobjects', expectedRows: handoff.counts?.metaobjectRows ?? 0 },
|
|
{ type: 'smart_collections', expectedRows: handoff.counts?.smartCollectionRows ?? 0 },
|
|
{ type: 'products', expectedRows: handoff.counts?.variantRows ?? 0 }
|
|
]
|
|
|
|
const rows: any[] = []
|
|
for (const expected of expectedImports) {
|
|
const matches = importLogs.filter(log => log.data.importType === expected.type)
|
|
const clean = matches.some(log => Number(log.data.failedRows || 0) === 0 && Number(log.data.successfulRows || 0) >= expected.expectedRows)
|
|
rows.push({
|
|
Gate: `import_log_${expected.type}`,
|
|
RequiredEvidence: `Matrixify ${expected.type} staging import log with failedRows=0 and successfulRows>=${expected.expectedRows}.`,
|
|
CurrentStatus: clean ? 'pass' : matches.length ? 'blocked' : 'missing',
|
|
EvidencePath: matches.map(log => `data/staging/import-logs/${log.file}`).join(' | '),
|
|
Blocking: clean ? 'no' : 'yes'
|
|
})
|
|
}
|
|
|
|
const passingPdpChecks = pdpChecks.filter(check => {
|
|
const values = Object.values(check.data.checks || {})
|
|
return values.length > 0 && values.every(value => value === 'pass') && (check.data.issues || []).length === 0
|
|
})
|
|
rows.push({
|
|
Gate: 'pdp_spot_checks',
|
|
RequiredEvidence: 'At least 5 PDP spot-check JSON files with all checks=pass and no issues.',
|
|
CurrentStatus: passingPdpChecks.length >= 5 ? 'pass' : pdpChecks.length ? 'blocked' : 'missing',
|
|
EvidencePath: pdpChecks.map(check => `data/staging/pdp-checks/${check.file}`).join(' | '),
|
|
Blocking: passingPdpChecks.length >= 5 ? 'no' : 'yes'
|
|
})
|
|
rows.push({
|
|
Gate: 'cutover_approval',
|
|
RequiredEvidence: 'data/staging/cutover-approval.json with approved=true, approver, date, and accepted known risks.',
|
|
CurrentStatus: cutover?.approved && cutover?.approvedBy && cutover?.approvedAt ? 'pass' : cutover ? 'blocked' : 'missing',
|
|
EvidencePath: cutover ? 'data/staging/cutover-approval.json' : '',
|
|
Blocking: cutover?.approved && cutover?.approvedBy && cutover?.approvedAt ? 'no' : 'yes'
|
|
})
|
|
|
|
const ready = rows.every(row => row.Blocking === 'no')
|
|
const report = {
|
|
generatedAt,
|
|
ready,
|
|
evidenceRoot: 'data/staging',
|
|
templates: [
|
|
'data/staging/_templates/import-log-template.json',
|
|
'data/staging/_templates/pdp-spot-check-template.json',
|
|
'data/staging/_templates/cutover-approval-template.json'
|
|
],
|
|
counts: {
|
|
gates: rows.length,
|
|
blocking: rows.filter(row => row.Blocking === 'yes').length,
|
|
importLogs: importLogs.length,
|
|
pdpChecks: pdpChecks.length,
|
|
passingPdpChecks: passingPdpChecks.length,
|
|
cutoverApprovalPresent: Boolean(cutover)
|
|
},
|
|
gates: rows
|
|
}
|
|
|
|
writeJson(join(outRoot, 'staging_evidence_validation.json'), report)
|
|
writeFileSync(join(outRoot, 'staging_evidence_validation.csv'), csv(rows, ['Gate', 'RequiredEvidence', 'CurrentStatus', 'EvidencePath', 'Blocking']), 'utf8')
|
|
const md = [
|
|
'# JV staging evidence validation',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`Ready: **${ready ? 'YES' : 'NO'}**`,
|
|
'',
|
|
'## Counts',
|
|
`- Import logs: ${report.counts.importLogs}`,
|
|
`- PDP checks: ${report.counts.pdpChecks}`,
|
|
`- Passing PDP checks: ${report.counts.passingPdpChecks}`,
|
|
`- Blocking gates: ${report.counts.blocking}/${report.counts.gates}`,
|
|
'',
|
|
'## Templates written',
|
|
...report.templates.map(path => `- \`${path}\``),
|
|
'',
|
|
'## Gates',
|
|
...rows.map(row => `- ${row.Blocking === 'no' ? '[PASS]' : '[BLOCKED]'} ${row.Gate}: ${row.RequiredEvidence} Current: ${row.CurrentStatus}`),
|
|
''
|
|
].join('\n')
|
|
writeFileSync(join(outRoot, 'staging_evidence_validation.md'), md, 'utf8')
|
|
|
|
console.log(`Staging evidence ready: ${ready}`)
|
|
console.log(`Import logs: ${report.counts.importLogs}`)
|
|
console.log(`PDP checks: ${report.counts.pdpChecks}`)
|
|
console.log(`Blocking gates: ${report.counts.blocking}`)
|
|
if (!ready) process.exitCode = 1
|