Files
justvitamin/scripts/generate-intelligence-workqueue.ts
T
Omair Saleh 056c47581f 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>
2026-06-02 18:50:09 +08:00

96 lines
5.2 KiB
TypeScript

#!/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<string, any>()
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