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>
73 lines
3.1 KiB
TypeScript
73 lines
3.1 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, readdirSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { intelPath, readJson, readPilotDoc, sourcePath, writeJson, now } from './lib/intel-utils'
|
|
|
|
const root = process.cwd()
|
|
const productsRoot = join(root, 'data', 'content', 'products')
|
|
const library = readJson(sourcePath('claims', 'claims-library.json'), { claims: [] })
|
|
const productFiles = existsSync(productsRoot)
|
|
? readdirSync(productsRoot).filter(file => file.endsWith('.json')).sort()
|
|
: []
|
|
|
|
function claimMatchesProduct(claim: any, product: any) {
|
|
const sku = product.sku
|
|
const name = String(product.name || '').toLowerCase()
|
|
const ingredients = String(product.metafields?.pdpKeyIngredients || '').toLowerCase()
|
|
const handle = String(claim.handle || '').toLowerCase()
|
|
if ((claim.skus || []).includes(sku)) return true
|
|
if (handle.startsWith('vitamin-d') && (name.includes('vitamin d') || ingredients.includes('vitamin-d'))) return true
|
|
if (handle.startsWith('vitamin-k') && (name.includes(' k2') || ingredients.includes('vitamin-k'))) return true
|
|
if (handle.startsWith('vitamin-c') && (name.includes('vitamin c') || ingredients.includes('vitamin-c'))) return true
|
|
return false
|
|
}
|
|
|
|
function candidateThemes(product: any, sw: any) {
|
|
const fromSw = (sw.items || []).slice(0, 7).map((item: any) => ({
|
|
rank: item.rank,
|
|
theme: item.insight,
|
|
source: `strengths-weaknesses rank ${item.rank}`,
|
|
allowedUse: 'VoC/benefit copy only; do not use as clinically_shown_to without source-backed evidence.',
|
|
status: 'not_a_clinical_claim'
|
|
}))
|
|
if (fromSw.length) return fromSw
|
|
return (product.metafields?.pdpProductBenefits || []).slice(0, 7).map((theme: string, index: number) => ({
|
|
rank: index + 1,
|
|
theme,
|
|
source: 'product content benefits seed',
|
|
allowedUse: 'Product/content seed only; do not use as clinically_shown_to without source-backed evidence and final approval.',
|
|
status: 'not_a_clinical_claim'
|
|
}))
|
|
}
|
|
|
|
let withClaims = 0
|
|
for (const file of productFiles) {
|
|
const product = readJson(join(productsRoot, file), {})
|
|
const sku = product.sku || file.replace(/\.json$/, '')
|
|
let aspects: any = { name: product.name || sku }
|
|
let sw: any = { items: [] }
|
|
try {
|
|
const pilot = readPilotDoc(sku)
|
|
aspects = pilot.aspects || aspects
|
|
sw = pilot.sw || sw
|
|
} catch (_) {}
|
|
const matched = (library.claims || []).filter((claim: any) => claimMatchesProduct(claim, product))
|
|
if (matched.length) withClaims += 1
|
|
writeJson(intelPath(sku, 'claims-evidence.json'), {
|
|
sku,
|
|
name: aspects.name || product.name || sku,
|
|
generatedAt: now(),
|
|
generatedBy: 'extract-claims.ts',
|
|
n: matched.length,
|
|
complianceGate: matched.length ? 'source_backed_claims_available_for_review' : 'no_source_backed_claim_matched_for_product',
|
|
sourceLibrary: 'data/sources/claims/claims-library.json',
|
|
reviewRequired: true,
|
|
claims: matched,
|
|
candidateNonClinicalThemes: candidateThemes(product, sw)
|
|
})
|
|
console.log(`${sku}: ${matched.length} source-backed clinical claims`)
|
|
}
|
|
|
|
console.log(`Claims evidence files: ${productFiles.length}`)
|
|
console.log(`Products with matched claims: ${withClaims}`)
|