#!/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 intelRoot = join(root, 'data', 'intelligence') 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 compact(value: any) { if (!value) return '' if (Array.isArray(value)) return value .filter(Boolean) .slice(0, 6) .map(item => { if (typeof item !== 'object') return String(item) return item.aspect || item.theme || item.motivation || item.profile || item.summary || item.extractionTask || JSON.stringify(item) }) .join(' | ') if (typeof value === 'object') { return Object.values(value).flat().filter(Boolean).slice(0, 6).map(item => { if (typeof item !== 'object') return String(item) return item.aspect || item.theme || item.motivation || item.profile || item.summary || item.extractionTask || JSON.stringify(item) }).join(' | ') } return String(value) } const rows: any[] = [] for (const file of readdirSync(productsRoot).filter(file => file.endsWith('.json')).sort()) { const product = readJson(join(productsRoot, file), {}) const sku = product.sku || file.replace(/\.json$/, '') const mappedRef = product.metafields?.pdpClinicallyShownTo || '' const claimsEvidence = readJson(join(intelRoot, sku, 'claims-evidence.json'), {}) const audience = readJson(join(intelRoot, sku, 'audience-profile.json'), {}) const aspects = readJson(join(intelRoot, sku, 'review-aspects.json'), {}) const workflow = readJson(join(workflowRoot, `${sku}.json`), {}) const clinicalField = workflow.fields?.pdpClinicallyShownTo || {} if (mappedRef) continue rows.push({ SKU: sku, ProductName: product.name || '', Handle: product.handle || '', CurrentClinicalRef: mappedRef, ComplianceStatus: product.approval?.compliance || workflow.approval?.compliance || '', FieldStatus: clinicalField.status || '', EvidenceGate: claimsEvidence.complianceGate || 'missing_source_backed_claim', CandidateThemes: compact(claimsEvidence.candidateNonClinicalThemes || claimsEvidence.themes || claimsEvidence.claimThemes || claimsEvidence.claimsNeeded), AudienceSignals: compact(audience.profiles || audience.motivations || audience.audiences || audience.summary), ReviewSignals: compact(aspects.aspects || aspects.topAspects || aspects.summary), RequiredDecision: 'Compliance owner must either map a source-backed authorised claim from data/sources/claims/claims-library.json or explicitly approve leaving clinically_shown_to blank.', SafeDefault: 'blank_no_fake_claim', SuggestedNextAction: 'Review product label/PDP, identify authorised nutrient/function claim if applicable, add cited claim to claims library, then rerun bun run claims:sync-content.', SourceFiles: [`data/intelligence/${sku}/claims-evidence.json`, `data/content/products/${sku}.json`] }) } const manifest = { generatedAt, ready: rows.length === 0, purpose: 'Actionable compliance review packet for SKUs intentionally left blank because no source-backed clinically_shown_to claim exists.', rule: 'This packet must not be used to auto-generate clinical claims. It exists to route the remaining compliance decisions to a human/regulatory owner.', counts: { unmatchedProducts: rows.length, totalProducts: readdirSync(productsRoot).filter(file => file.endsWith('.json')).length }, rows } const headers = ['SKU', 'ProductName', 'Handle', 'CurrentClinicalRef', 'ComplianceStatus', 'FieldStatus', 'EvidenceGate', 'CandidateThemes', 'AudienceSignals', 'ReviewSignals', 'RequiredDecision', 'SafeDefault', 'SuggestedNextAction', 'SourceFiles'] writeFileSync(join(outRoot, 'unmatched_claims_review_packet.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') writeFileSync(join(outRoot, 'unmatched_claims_review_packet.csv'), csv(rows, headers), 'utf8') const md = [ '# Unmatched clinically_shown_to review packet', '', `Generated: ${generatedAt}`, '', 'This packet lists every SKU where `pdp.clinically_shown_to` is deliberately blank because the dashboard does not have source-backed claim evidence. Do not fill these with generic/fake claims.', '', `Unmatched products: ${rows.length}`, '', '## Required decision', 'For each SKU, the compliance owner must either map a cited authorised claim into `data/sources/claims/claims-library.json` or approve leaving `clinically_shown_to` blank.', '', '## Rows', ...rows.map(row => [ `### ${row.SKU} - ${row.ProductName}`, `- Evidence gate: ${row.EvidenceGate}`, `- Current field status: ${row.FieldStatus || 'unknown'}`, `- Candidate themes/signals: ${row.CandidateThemes || row.AudienceSignals || row.ReviewSignals || 'none captured'}`, `- Required decision: ${row.RequiredDecision}`, `- Safe default: ${row.SafeDefault}`, '' ].join('\n')), '## Regenerate after review', 'After claims are added or blank decisions are approved, run `bun run claims:sync-content && bun run claims:unmatched-review && bun run export:content && bun run audit:content`.', '' ].join('\n') writeFileSync(join(outRoot, 'unmatched_claims_review_packet.md'), md, 'utf8') console.log(`Unmatched clinically_shown_to products: ${rows.length}`) if (!manifest.ready) process.exitCode = 1