Files
justvitamin/scripts/export-extended-metaobjects.ts
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

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()