Files
justvitamin/scripts/validate-assets.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

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}`)