#!/usr/bin/env bun import { existsSync, readFileSync, writeFileSync } from 'fs' import { join } from 'path' const root = process.cwd() const outRoot = join(root, 'content_population_exports') const contentRoot = join(root, 'data', 'content') const intelRoot = join(root, 'data', 'intelligence') const catalogPath = join(root, 'data', 'pipeline', 'catalog.json') const requiredViews = [ { view: 'review-aspects', source: 'Feefo raw reviews', gate: 'first_party' }, { view: 'strengths-weaknesses', source: 'Feefo raw reviews', gate: 'first_party' }, { view: 'conversion-blockers', source: 'Feefo raw reviews', gate: 'first_party' }, { view: 'improvements', source: 'Feefo raw reviews', gate: 'first_party' }, { view: 'image-audit', source: 'PDP/image crawl + Feefo blockers', gate: 'image' }, { view: 'photo-brief', source: 'Image audit + product content', gate: 'image' }, { view: 'competitor-comparison', source: 'Competitor PDP + Amazon/Trustpilot', gate: 'external_sources' }, { view: 'amazon-reviews', source: 'Amazon source drops', gate: 'amazon' }, { view: 'reddit-pulse', source: 'Reddit/social source drops', gate: 'reddit' }, { view: 'flavor-intelligence', source: 'Competitor PDP + Amazon/Reddit', gate: 'npd' }, { view: 'audience-profile', source: 'Feefo + Amazon + Reddit', gate: 'audience' }, { view: 'claims-evidence', source: 'Clinical claims library', gate: 'claims' } ] 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 priority(totalReviews: number) { if (totalReviews >= 500) return 'P0' if (totalReviews >= 250) return 'P1' if (totalReviews >= 100) return 'P2' return 'P3' } const catalogBySku = Object.fromEntries(readJson(catalogPath, []).map((product: any) => [product.sku, product])) const productFiles = (await Array.fromAsync(new Bun.Glob('*.json').scan({ cwd: join(contentRoot, 'products') }))) .sort() .map(file => readJson(join(contentRoot, 'products', file))) const rows: any[] = [] for (const product of productFiles) { const catalog = catalogBySku[product.sku] || {} const missingViews = [] for (const spec of requiredViews) { const exists = existsSync(join(intelRoot, product.sku, `${spec.view}.json`)) if (!exists) { rows.push({ Priority: priority(Number(catalog.total_reviews || 0)), SKU: product.sku, Product: product.name || catalog.name || '', TotalReviews: catalog.total_reviews || 0, View: spec.view, RequiredSource: spec.source, Gate: spec.gate, TargetPath: `data/intelligence/${product.sku}/${spec.view}.json`, Status: 'missing' }) missingViews.push(spec.view) } } } rows.sort((a, b) => a.Priority.localeCompare(b.Priority) || Number(b.TotalReviews) - Number(a.TotalReviews) || a.SKU.localeCompare(b.SKU) || a.View.localeCompare(b.View)) const bySku = new Map() for (const row of rows) { const entry = bySku.get(row.SKU) || { SKU: row.SKU, Product: row.Product, TotalReviews: row.TotalReviews, Priority: row.Priority, MissingViews: [], MissingCount: 0 } entry.MissingViews.push(row.View) entry.MissingCount += 1 bySku.set(row.SKU, entry) } const skuRows = [...bySku.values()].map(row => ({ ...row, MissingViews: row.MissingViews.join(' | ') })).sort((a, b) => a.Priority.localeCompare(b.Priority) || Number(b.TotalReviews) - Number(a.TotalReviews) || a.SKU.localeCompare(b.SKU)) const gateSummary = requiredViews.map(spec => ({ View: spec.view, Gate: spec.gate, RequiredSource: spec.source, Missing: rows.filter(row => row.View === spec.view).length, Present: productFiles.length - rows.filter(row => row.View === spec.view).length, Total: productFiles.length })) writeFileSync(join(outRoot, 'intelligence_generation_workqueue.csv'), csv(rows, ['Priority', 'SKU', 'Product', 'TotalReviews', 'View', 'RequiredSource', 'Gate', 'TargetPath', 'Status']), 'utf8') writeFileSync(join(outRoot, 'intelligence_generation_by_sku.csv'), csv(skuRows, ['Priority', 'SKU', 'Product', 'TotalReviews', 'MissingCount', 'MissingViews']), 'utf8') writeFileSync(join(outRoot, 'intelligence_generation_summary.csv'), csv(gateSummary, ['View', 'Gate', 'RequiredSource', 'Present', 'Missing', 'Total']), 'utf8') const manifest = { generatedAt: new Date().toISOString(), ready: rows.length === 0, totalProducts: productFiles.length, missingTasks: rows.length, affectedSkus: skuRows.length, gateSummary, topSkus: skuRows.slice(0, 25) } writeFileSync(join(outRoot, 'intelligence_generation_workqueue.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') console.log(`Intelligence workqueue ready: ${manifest.ready}`) console.log(`Missing tasks: ${manifest.missingTasks}`) console.log(`Affected SKUs: ${manifest.affectedSkus}`) if (!manifest.ready) process.exitCode = 1