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>
209 lines
8.3 KiB
TypeScript
209 lines
8.3 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Emit Matrixify-format CSVs for the 4 new metaobject definition groups
|
|
// produced by generate-pilot-content.ts:
|
|
// - scientific_study
|
|
// - timeline_phase + timeline_block (with phases as list.metaobject_reference)
|
|
// - comparison_column + comparison_row + comparison_table
|
|
// - faq_pair + faq_block
|
|
//
|
|
// Output: content_population_exports/pilot-extended-<definition>.csv
|
|
// One CSV per definition, long format (one row per Handle x Field x Value).
|
|
//
|
|
// Usage: bun scripts/export-extended-metaobjects.ts
|
|
|
|
import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
const ROOT = join(import.meta.dir, '..')
|
|
const IN_DIR = join(ROOT, 'data', 'content', 'pilot-extended')
|
|
const OUT_DIR = join(ROOT, 'content_population_exports')
|
|
|
|
const META_COLS = ['Handle', 'Display Name', 'Status', 'Definition: Handle', 'Definition: Name', 'Field', 'Value'] as const
|
|
|
|
const DEFINITIONS = {
|
|
scientific_study: 'Scientific Study',
|
|
timeline_phase: 'Timeline Phase',
|
|
timeline_block: 'Timeline Block (Results)',
|
|
comparison_column: 'Comparison Column',
|
|
comparison_row: 'Comparison Row',
|
|
comparison_table: 'Comparison Table',
|
|
faq_pair: 'FAQ Pair',
|
|
faq_block: 'FAQ Block'
|
|
} as const
|
|
|
|
type DefKey = keyof typeof DEFINITIONS
|
|
|
|
function csvEscape(v: unknown): string {
|
|
if (Array.isArray(v)) v = v.join(', ')
|
|
if (v === undefined || v === null) v = ''
|
|
const s = String(v)
|
|
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
}
|
|
|
|
function toCsv(rows: (string | number | boolean | null | undefined)[][]): string {
|
|
return rows.map(r => r.map(csvEscape).join(',')).join('\n') + '\n'
|
|
}
|
|
|
|
type RowBuckets = Record<DefKey, (string | number | boolean | null | undefined)[][]>
|
|
|
|
function newBuckets(): RowBuckets {
|
|
return Object.fromEntries(
|
|
(Object.keys(DEFINITIONS) as DefKey[]).map(k => [k, [META_COLS as unknown as string[]]])
|
|
) as RowBuckets
|
|
}
|
|
|
|
function addField(
|
|
buckets: RowBuckets,
|
|
def: DefKey,
|
|
handle: string,
|
|
displayName: string,
|
|
status: string,
|
|
field: string,
|
|
value: unknown
|
|
) {
|
|
buckets[def].push([handle, displayName, status, def, DEFINITIONS[def], field, value as any])
|
|
}
|
|
|
|
function emitScientificStudy(buckets: RowBuckets, ss: any) {
|
|
if (!ss?.handle) return
|
|
const def: DefKey = 'scientific_study'
|
|
const h = ss.handle, n = ss.name_internal || ss.headline || h, st = ss.status || 'draft'
|
|
addField(buckets, def, h, n, st, 'name_internal', ss.name_internal || '')
|
|
addField(buckets, def, h, n, st, 'headline', ss.headline || '')
|
|
addField(buckets, def, h, n, st, 'body_copy', ss.body_copy || '')
|
|
addField(buckets, def, h, n, st, 'link_url', ss.link_url || '')
|
|
addField(buckets, def, h, n, st, 'link_label', ss.link_label || '')
|
|
const stats = (ss.stats as any[]) || []
|
|
for (let i = 0; i < 3; i++) {
|
|
const s = stats[i] || {}
|
|
addField(buckets, def, h, n, st, `stat_${i + 1}_value`, s.value ?? '')
|
|
addField(buckets, def, h, n, st, `stat_${i + 1}_label`, s.label ?? '')
|
|
}
|
|
addField(buckets, def, h, n, st, 'image', ss.image || '')
|
|
}
|
|
|
|
function emitTimeline(buckets: RowBuckets, tb: any) {
|
|
if (!tb?.handle) return
|
|
const def: DefKey = 'timeline_block'
|
|
const h = tb.handle, n = tb.name_internal || h, st = tb.status || 'draft'
|
|
addField(buckets, def, h, n, st, 'headline', tb.headline || '')
|
|
addField(buckets, def, h, n, st, 'subtitle', tb.subtitle || '')
|
|
addField(buckets, def, h, n, st, 'intro_copy', tb.intro_copy || '')
|
|
addField(buckets, def, h, n, st, 'study_link_label', tb.study_link_label || '')
|
|
addField(buckets, def, h, n, st, 'study_link_url', tb.study_link_url || '')
|
|
|
|
const phases = (tb.phases as any[]) || []
|
|
const phaseRefs = phases.map(p => `timeline_phase.${p.handle}`).join(', ')
|
|
addField(buckets, def, h, n, st, 'phases', phaseRefs)
|
|
|
|
// phase entries on their own sheet
|
|
const pdef: DefKey = 'timeline_phase'
|
|
for (const p of phases) {
|
|
const ph = p.handle, pn = p.phase_label || ph, pst = p.status || 'draft'
|
|
addField(buckets, pdef, ph, pn, pst, 'phase_label', p.phase_label || '')
|
|
addField(buckets, pdef, ph, pn, pst, 'body_copy', p.body_copy || '')
|
|
}
|
|
}
|
|
|
|
function emitComparison(buckets: RowBuckets, ct: any) {
|
|
if (!ct?.handle) return
|
|
const tdef: DefKey = 'comparison_table'
|
|
const h = ct.handle, n = ct.name_internal || h, st = ct.status || 'draft'
|
|
addField(buckets, tdef, h, n, st, 'section_title', ct.section_title || '')
|
|
addField(buckets, tdef, h, n, st, 'subtitle', ct.subtitle || '')
|
|
const cols = (ct.columns as any[]) || []
|
|
const rows = (ct.rows as any[]) || []
|
|
addField(buckets, tdef, h, n, st, 'columns', cols.map(c => `comparison_column.${c.handle}`).join(', '))
|
|
addField(buckets, tdef, h, n, st, 'rows', rows.map(r => `comparison_row.${r.handle}`).join(', '))
|
|
|
|
const cdef: DefKey = 'comparison_column'
|
|
for (const c of cols) {
|
|
const ch = c.handle, cn = c.name || ch
|
|
addField(buckets, cdef, ch, cn, 'draft', 'name', c.name || '')
|
|
addField(buckets, cdef, ch, cn, 'draft', 'is_us', c.is_us ? 'true' : 'false')
|
|
addField(buckets, cdef, ch, cn, 'draft', 'accent_colour', c.accent_colour || '')
|
|
addField(buckets, cdef, ch, cn, 'draft', 'image', c.image || '')
|
|
}
|
|
|
|
const rdef: DefKey = 'comparison_row'
|
|
for (const r of rows) {
|
|
const rh = r.handle, rn = r.feature_label || rh, rst = r.status || 'draft'
|
|
addField(buckets, rdef, rh, rn, rst, 'feature_label', r.feature_label || '')
|
|
addField(buckets, rdef, rh, rn, rst, 'values_json', JSON.stringify(r.values || {}))
|
|
}
|
|
}
|
|
|
|
function emitFaq(buckets: RowBuckets, fb: any) {
|
|
if (!fb?.handle) return
|
|
const bdef: DefKey = 'faq_block'
|
|
const h = fb.handle, n = fb.name_internal || h, st = fb.status || 'draft'
|
|
addField(buckets, bdef, h, n, st, 'section_title', fb.section_title || '')
|
|
addField(buckets, bdef, h, n, st, 'cta_label', fb.cta_label || '')
|
|
addField(buckets, bdef, h, n, st, 'cta_url', fb.cta_url || '')
|
|
|
|
const pairs = (fb.pairs as any[]) || []
|
|
addField(buckets, bdef, h, n, st, 'pairs', pairs.map(p => `faq_pair.${p.handle}`).join(', '))
|
|
|
|
const pdef: DefKey = 'faq_pair'
|
|
for (const p of pairs) {
|
|
const ph = p.handle, pn = p.question || ph, pst = p.status || 'draft'
|
|
addField(buckets, pdef, ph, pn, pst, 'scope', p.scope || 'product')
|
|
addField(buckets, pdef, ph, pn, pst, 'question', p.question || '')
|
|
addField(buckets, pdef, ph, pn, pst, 'answer', p.answer || '')
|
|
addField(buckets, pdef, ph, pn, pst, 'source_citation_id', p.source_citation_id || '')
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
if (!existsSync(IN_DIR)) {
|
|
console.error(`Input dir missing: ${IN_DIR}. Run generate-pilot-content.ts first.`)
|
|
process.exit(1)
|
|
}
|
|
mkdirSync(OUT_DIR, { recursive: true })
|
|
const buckets = newBuckets()
|
|
const files = readdirSync(IN_DIR).filter(f => f.endsWith('.json'))
|
|
if (files.length === 0) {
|
|
console.error(`No pilot-extended JSONs in ${IN_DIR}`); process.exit(1)
|
|
}
|
|
for (const f of files) {
|
|
const doc = JSON.parse(readFileSync(join(IN_DIR, f), 'utf8'))
|
|
emitScientificStudy(buckets, doc.scientificStudy)
|
|
emitTimeline(buckets, doc.timelineBlock)
|
|
emitComparison(buckets, doc.comparisonTable)
|
|
emitFaq(buckets, doc.faqBlock)
|
|
}
|
|
|
|
const summary: Record<string, number> = {}
|
|
for (const def of Object.keys(DEFINITIONS) as DefKey[]) {
|
|
const out = toCsv(buckets[def])
|
|
const rowCount = buckets[def].length - 1
|
|
const fileName = `pilot-extended-${def}.csv`
|
|
writeFileSync(join(OUT_DIR, fileName), out, 'utf8')
|
|
summary[def] = rowCount
|
|
console.log(` ${fileName.padEnd(45)} ${rowCount} rows`)
|
|
}
|
|
|
|
const manifestPath = join(OUT_DIR, 'pilot-extended-manifest.json')
|
|
writeFileSync(
|
|
manifestPath,
|
|
JSON.stringify(
|
|
{
|
|
generatedAt: new Date().toISOString(),
|
|
source: 'data/content/pilot-extended/',
|
|
skus: files.map(f => f.replace(/\.json$/, '')),
|
|
definitions: summary,
|
|
notes: [
|
|
'Long-format Matrixify CSVs per metaobject definition.',
|
|
'Import order must respect references: comparison_column + comparison_row + timeline_phase + faq_pair FIRST, then comparison_table + timeline_block + faq_block + scientific_study.',
|
|
'Confirm with Lewis that the Shopify template supports list.metaobject_reference for timeline_block.phases, comparison_table.columns/rows, faq_block.pairs.'
|
|
]
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
console.log(`\nManifest: ${manifestPath}`)
|
|
}
|
|
|
|
await main()
|