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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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'), {})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user