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>
59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
import { join } from 'path'
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
|
|
export const ROOT = join(import.meta.dir, '..', '..')
|
|
export const INTEL_ROOT = join(ROOT, 'data', 'intelligence')
|
|
export const SOURCES_ROOT = join(ROOT, 'data', 'sources')
|
|
export const CONTENT_PRODUCTS_ROOT = join(ROOT, 'data', 'content', 'products')
|
|
export const PILOT_SKUS = ['JV-D1000', 'JV-D4000', 'JV-TURMERIC500']
|
|
|
|
export function readJson(path: string, fallback: any = null) {
|
|
if (!existsSync(path)) return fallback
|
|
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
|
|
}
|
|
export function writeJson(path: string, data: any) {
|
|
mkdirSync(join(path, '..'), { recursive: true })
|
|
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
}
|
|
export function intelPath(sku: string, file: string) { return join(INTEL_ROOT, sku, file) }
|
|
export function sourcePath(...parts: string[]) { return join(SOURCES_ROOT, ...parts) }
|
|
export function now() { return new Date().toISOString() }
|
|
export function slug(value: string) { return String(value || '').toLowerCase().replace(/&/g, 'and').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') }
|
|
export function stripHtml(html: string) {
|
|
return String(html || '')
|
|
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/ /g, ' ')
|
|
.replace(/&/g, '&')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
}
|
|
export function listJson(dir: string) {
|
|
if (!existsSync(dir)) return []
|
|
return readdirSync(dir).filter(f => f.endsWith('.json')).map(f => join(dir, f))
|
|
}
|
|
export function sourceStatus(kind: string) {
|
|
const dir = sourcePath(kind)
|
|
const files = listJson(dir)
|
|
return files.length ? `available:${files.length}` : 'missing'
|
|
}
|
|
export function loadCompetitorConfig() {
|
|
return readJson(sourcePath('competitor-sources.json'), { competitors: [] }).competitors || []
|
|
}
|
|
export function listProducts() {
|
|
if (!existsSync(CONTENT_PRODUCTS_ROOT)) return []
|
|
return readdirSync(CONTENT_PRODUCTS_ROOT)
|
|
.filter(file => file.endsWith('.json'))
|
|
.sort()
|
|
.map(file => readJson(join(CONTENT_PRODUCTS_ROOT, file), {}))
|
|
}
|
|
export function readPilotDoc(sku: string) {
|
|
return {
|
|
aspects: readJson(intelPath(sku, 'review-aspects.json'), {}),
|
|
sw: readJson(intelPath(sku, 'strengths-weaknesses.json'), {}),
|
|
blockers: readJson(intelPath(sku, 'conversion-blockers.json'), {}),
|
|
improvements: readJson(intelPath(sku, 'improvements.json'), {})
|
|
}
|
|
}
|