056c47581f
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>
63 lines
3.1 KiB
TypeScript
63 lines
3.1 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { readdirSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { ROOT, readJson, writeJson } from './lib/intel-utils'
|
|
|
|
const assetRoot = process.env.ASSET_ROOT || join(ROOT, '..', '..', 'jv-assets')
|
|
const contentRoot = join(ROOT, 'data', 'content')
|
|
const productsRoot = join(contentRoot, 'products')
|
|
const outRoot = join(ROOT, 'content_population_exports')
|
|
|
|
function walk(dir: string): { name: string; path: string }[] {
|
|
try {
|
|
return readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
|
const path = join(dir, entry.name)
|
|
if (entry.isDirectory()) return walk(path)
|
|
if (entry.isFile()) return [{ name: entry.name, path }]
|
|
return []
|
|
})
|
|
} catch (_) { return [] }
|
|
}
|
|
function csv(rows: any[], headers: string[]) {
|
|
const esc = (v: any) => {
|
|
v = v ?? ''
|
|
v = String(v)
|
|
return /[",\n\r]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v
|
|
}
|
|
return [headers.join(','), ...rows.map(r => headers.map(h => esc(r[h])).join(','))].join('\n') + '\n'
|
|
}
|
|
const files = walk(assetRoot)
|
|
const available = new Map(files.map(f => [f.name, f.path]))
|
|
const productFiles = readdirSync(productsRoot).filter(f => f.endsWith('.json'))
|
|
const required: any[] = []
|
|
const missingReferences: any[] = []
|
|
for (const file of productFiles) {
|
|
const product = readJson(join(productsRoot, file), null)
|
|
for (const field of ['pdpResults', 'pdpResultsMobile', 'pdpHowWeCompare']) {
|
|
const filename = product?.metafields?.[field]
|
|
if (filename) required.push({ Kind: 'product', Owner: product.sku, Field: field, Filename: filename, Exists: available.has(filename) ? 'yes' : 'no', Path: available.get(filename) || '' })
|
|
else missingReferences.push({ Kind: 'product', Owner: product.sku, Field: field, Filename: '', Exists: 'no', Path: 'No filename assigned' })
|
|
}
|
|
}
|
|
const meta = readJson(join(contentRoot, 'metaobjects.json'), { definitions: {} })
|
|
for (const [definition, rows] of Object.entries(meta.definitions || {}) as any) {
|
|
for (const row of rows) {
|
|
for (const field of ['icon', 'image', 'background_image']) {
|
|
const filename = row[field]
|
|
if (filename) required.push({ Kind: 'metaobject', Owner: `${definition}.${row.handle || row.name || ''}`, Field: field, Filename: filename, Exists: available.has(filename) ? 'yes' : 'no', Path: available.get(filename) || '' })
|
|
}
|
|
}
|
|
}
|
|
const allRows = [...required, ...missingReferences]
|
|
writeFileSync(join(outRoot, 'asset_manifest_validation.csv'), csv(allRows, ['Kind', 'Owner', 'Field', 'Filename', 'Exists', 'Path']), 'utf8')
|
|
writeJson(join(outRoot, 'asset_manifest_validation.json'), {
|
|
generatedAt: new Date().toISOString(), assetRoot, availableCount: files.length,
|
|
requiredCount: required.length, missingUploadCount: required.filter(r => r.Exists === 'no').length,
|
|
missingReferenceCount: missingReferences.length, ready: required.every(r => r.Exists === 'yes') && missingReferences.length === 0,
|
|
required, missingReferences
|
|
})
|
|
console.log(`Available assets: ${files.length}`)
|
|
console.log(`Required filenames: ${required.length}`)
|
|
console.log(`Missing uploads: ${required.filter(r => r.Exists === 'no').length}`)
|
|
console.log(`Missing filename refs: ${missingReferences.length}`)
|