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>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bun
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const root = process.cwd()
|
||||
const productsRoot = join(root, 'data', 'content', 'products')
|
||||
const workflowRoot = join(root, 'data', 'content', 'workflow')
|
||||
const outRoot = join(root, 'content_population_exports')
|
||||
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 decideBlocker(product: any) {
|
||||
const approval = product.approval || {}
|
||||
const blockers = []
|
||||
if (approval.content !== 'approved') blockers.push('content_approval')
|
||||
if (approval.compliance !== 'approved') blockers.push('compliance_approval')
|
||||
if (approval.images !== 'ready' && approval.images !== 'approved') blockers.push('image_approval')
|
||||
if (approval.export !== 'ready' && approval.export !== 'approved') blockers.push('export_approval')
|
||||
return blockers
|
||||
}
|
||||
|
||||
const rows = readdirSync(productsRoot).filter(file => file.endsWith('.json')).sort().map(file => {
|
||||
const product = readJson(join(productsRoot, file), {})
|
||||
const sku = product.sku || file.replace(/\.json$/, '')
|
||||
const workflow = readJson(join(workflowRoot, `${sku}.json`), {})
|
||||
const blockers = decideBlocker(product)
|
||||
const imageRefs = [
|
||||
product.metafields?.pdpResults,
|
||||
product.metafields?.pdpResultsMobile,
|
||||
product.metafields?.pdpHowWeCompare
|
||||
].filter(Boolean)
|
||||
return {
|
||||
SKU: sku,
|
||||
ProductName: product.name || '',
|
||||
Handle: product.handle || '',
|
||||
ContentApproval: product.approval?.content || '',
|
||||
ComplianceApproval: product.approval?.compliance || '',
|
||||
ImageApproval: product.approval?.images || '',
|
||||
ExportApproval: product.approval?.export || '',
|
||||
ClinicalRef: product.metafields?.pdpClinicallyShownTo || '',
|
||||
ImageRefs: imageRefs,
|
||||
Blockers: blockers,
|
||||
ReadyForExportApproval: blockers.length === 0 ? 'yes' : 'no',
|
||||
WorkflowStage: workflow.stage || '',
|
||||
LastUpdated: product.updatedAt || workflow.updatedAt || '',
|
||||
ReviewerAction: blockers.length === 0
|
||||
? 'Confirm final spot-check and keep approval evidence.'
|
||||
: `Review and approve/resolve: ${blockers.join(', ')}.`,
|
||||
ProductFile: `data/content/products/${sku}.json`,
|
||||
WorkflowFile: `data/content/workflow/${sku}.json`
|
||||
}
|
||||
})
|
||||
|
||||
const blockerCounts = rows.reduce((acc: Record<string, number>, row: any) => {
|
||||
for (const blocker of row.Blockers) acc[blocker] = (acc[blocker] || 0) + 1
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const manifest = {
|
||||
generatedAt,
|
||||
ready: rows.every((row: any) => row.ReadyForExportApproval === 'yes'),
|
||||
purpose: 'Reviewer-facing approval queue packet for the 190 product records. This does not self-approve content.',
|
||||
counts: {
|
||||
products: rows.length,
|
||||
readyForExportApproval: rows.filter((row: any) => row.ReadyForExportApproval === 'yes').length,
|
||||
blockedProducts: rows.filter((row: any) => row.ReadyForExportApproval !== 'yes').length,
|
||||
blockerCounts
|
||||
},
|
||||
rows
|
||||
}
|
||||
|
||||
const headers = ['SKU', 'ProductName', 'Handle', 'ContentApproval', 'ComplianceApproval', 'ImageApproval', 'ExportApproval', 'ClinicalRef', 'ImageRefs', 'Blockers', 'ReadyForExportApproval', 'WorkflowStage', 'LastUpdated', 'ReviewerAction', 'ProductFile', 'WorkflowFile']
|
||||
writeFileSync(join(outRoot, 'product_approval_review_packet.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
||||
writeFileSync(join(outRoot, 'product_approval_review_packet.csv'), csv(rows, headers), 'utf8')
|
||||
|
||||
const md = [
|
||||
'# Product approval review packet',
|
||||
'',
|
||||
`Generated: ${generatedAt}`,
|
||||
'',
|
||||
'This packet turns the product-level approval blocker into a reviewer queue. It does not mark any product approved.',
|
||||
'',
|
||||
`Products: ${manifest.counts.products}`,
|
||||
`Blocked products: ${manifest.counts.blockedProducts}`,
|
||||
`Ready for export approval: ${manifest.counts.readyForExportApproval}`,
|
||||
'',
|
||||
'## Blocker counts',
|
||||
...Object.entries(blockerCounts).map(([key, count]) => `- ${key}: ${count}`),
|
||||
'',
|
||||
'## First 50 reviewer rows',
|
||||
...rows.slice(0, 50).map((row: any) => `- **${row.SKU}** ${row.ProductName}: ${row.Blockers.join(', ') || 'no blockers'}; clinical ref=${row.ClinicalRef || 'blank'}`),
|
||||
'',
|
||||
rows.length > 50 ? '_Full queue is in `product_approval_review_packet.csv`._' : '',
|
||||
'',
|
||||
'## Regenerate after review',
|
||||
'After human approvals are recorded, run `bun run approval:review-packet && bun run export:content && bun run audit:content`.',
|
||||
''
|
||||
].join('\n')
|
||||
writeFileSync(join(outRoot, 'product_approval_review_packet.md'), md, 'utf8')
|
||||
|
||||
console.log(`Product approval review packet ready: ${manifest.ready}`)
|
||||
console.log(`Blocked products: ${manifest.counts.blockedProducts}`)
|
||||
if (!manifest.ready) process.exitCode = 1
|
||||
Reference in New Issue
Block a user