#!/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, 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