Files
justvitamin/scripts/generate-clinical-blank-decision-template.ts
T
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

198 lines
7.9 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const root = process.cwd()
const contentRoot = join(root, 'data', 'content')
const productsRoot = join(contentRoot, 'products')
const decisionRoot = join(contentRoot, 'compliance')
const decisionPath = join(decisionRoot, 'clinical-blank-decisions.json')
const decisionCsvPath = join(decisionRoot, 'clinical-blank-decisions.csv')
const outRoot = join(root, 'content_population_exports')
const generatedAt = new Date().toISOString()
function readJson(path: string, fallback: any = null) {
if (!existsSync(path)) return fallback
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
}
function csv(rows: any[], headers: string[]) {
const esc = (value: any) => {
if (Array.isArray(value)) value = value.join(' | ')
if (value === undefined || value === null) value = ''
value = String(value)
return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value
}
return [headers.join(','), ...rows.map(row => headers.map(header => esc(row[header])).join(','))].join('\n') + '\n'
}
function parseCsv(text: string) {
const rows: string[][] = []
let row: string[] = []
let cell = ''
let quoted = false
for (let i = 0; i < text.length; i++) {
const ch = text[i]
const next = text[i + 1]
if (quoted && ch === '"' && next === '"') {
cell += '"'
i += 1
} else if (ch === '"') {
quoted = !quoted
} else if (!quoted && ch === ',') {
row.push(cell)
cell = ''
} else if (!quoted && (ch === '\n' || ch === '\r')) {
if (ch === '\r' && next === '\n') i += 1
row.push(cell)
if (row.some(value => value !== '')) rows.push(row)
row = []
cell = ''
} else {
cell += ch
}
}
if (cell || row.length) {
row.push(cell)
if (row.some(value => value !== '')) rows.push(row)
}
const headers = rows.shift() || []
return rows.map(values => Object.fromEntries(headers.map((header, index) => [header.replace(/^\uFEFF/, ''), values[index] ?? ''])))
}
function bool(value: any) {
return value === true || String(value || '').toLowerCase() === 'true' || String(value || '').toLowerCase() === 'yes'
}
mkdirSync(decisionRoot, { recursive: true })
mkdirSync(outRoot, { recursive: true })
const existing = readJson(decisionPath, { decisions: [] })
const csvExisting = existsSync(decisionCsvPath)
? parseCsv(readFileSync(decisionCsvPath, 'utf8')).map((row: any) => ({
sku: row.sku || row.SKU,
decision: row.decision || row.Decision,
approvedBy: row.approvedBy || row.ApprovedBy,
approvedAt: row.approvedAt || row.ApprovedAt,
rationale: row.rationale || row.Rationale,
evidencePath: row.evidencePath || row.EvidencePath,
constraintsAcknowledged: bool(row.constraintsAcknowledged || row.ConstraintsAcknowledged),
note: row.note || row.Note
}))
: []
const existingBySku = new Map([...(existing.decisions || []), ...csvExisting].map((row: any) => [row.sku, row]))
const products = readdirSync(productsRoot)
.filter(file => file.endsWith('.json'))
.sort()
.map(file => readJson(join(productsRoot, file), {}))
const decisions = products
.filter(product => !product.metafields?.pdpClinicallyShownTo)
.map(product => {
const previous: any = existingBySku.get(product.sku) || {}
return {
sku: product.sku,
productName: product.name || '',
handle: product.handle || '',
decision: previous.decision || 'pending',
approvedBy: previous.approvedBy || '',
approvedAt: previous.approvedAt || '',
rationale: previous.rationale || '',
evidencePath: previous.evidencePath || `data/intelligence/${product.sku}/claims-evidence.json`,
constraintsAcknowledged: Boolean(previous.constraintsAcknowledged),
note: previous.note || 'Use decision=approved_blank only when compliance explicitly approves that this SKU should ship without pdp.clinically_shown_to. Do not use this to bypass missing evidence.'
}
})
const validBlankApprovals = decisions.filter(row =>
row.decision === 'approved_blank' &&
row.approvedBy &&
row.approvedAt &&
row.rationale &&
row.constraintsAcknowledged === true
)
const invalidApprovedBlank = decisions.filter(row =>
row.decision === 'approved_blank' &&
!validBlankApprovals.some(valid => valid.sku === row.sku)
)
const decisionFile = {
generatedAt,
purpose: 'Compliance-owner decisions for SKUs where clinically_shown_to is intentionally blank.',
allowedDecisions: ['pending', 'approved_blank', 'requires_source_backed_claim'],
guardrail: 'A blank clinical-claim field is export-safe only when decision=approved_blank, approvedBy/approvedAt/rationale are filled, and constraintsAcknowledged=true.',
decisions
}
writeFileSync(decisionPath, JSON.stringify(decisionFile, null, 2) + '\n', 'utf8')
const rows = decisions.map(row => ({
SKU: row.sku,
ProductName: row.productName,
Decision: row.decision,
ApprovedBy: row.approvedBy,
ApprovedAt: row.approvedAt,
ConstraintsAcknowledged: row.constraintsAcknowledged ? 'yes' : 'no',
ValidBlankApproval: validBlankApprovals.some(valid => valid.sku === row.sku) ? 'yes' : 'no',
Rationale: row.rationale,
EvidencePath: row.evidencePath,
Note: row.note
}))
const manifest = {
generatedAt,
ready: decisions.length > 0 && validBlankApprovals.length === decisions.length,
decisionFile: 'data/content/compliance/clinical-blank-decisions.json',
counts: {
decisions: decisions.length,
pending: decisions.filter(row => row.decision === 'pending').length,
requiresSourceBackedClaim: decisions.filter(row => row.decision === 'requires_source_backed_claim').length,
validBlankApprovals: validBlankApprovals.length,
invalidApprovedBlank: invalidApprovedBlank.length
},
guardrail: decisionFile.guardrail,
rows
}
writeFileSync(join(outRoot, 'clinical_blank_decision_template.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
writeFileSync(join(outRoot, 'clinical_blank_decision_template.csv'), csv(rows, ['SKU', 'ProductName', 'Decision', 'ApprovedBy', 'ApprovedAt', 'ConstraintsAcknowledged', 'ValidBlankApproval', 'Rationale', 'EvidencePath', 'Note']), 'utf8')
writeFileSync(decisionCsvPath, csv(rows.map(row => ({
sku: row.SKU,
productName: row.ProductName,
decision: row.Decision,
approvedBy: row.ApprovedBy,
approvedAt: row.ApprovedAt,
rationale: row.Rationale,
evidencePath: row.EvidencePath,
constraintsAcknowledged: row.ConstraintsAcknowledged,
note: row.Note
})), ['sku', 'productName', 'decision', 'approvedBy', 'approvedAt', 'rationale', 'evidencePath', 'constraintsAcknowledged', 'note']), 'utf8')
writeFileSync(join(outRoot, 'clinical_blank_decision_template.md'), [
'# Clinical blank-decision template',
'',
`Generated: ${generatedAt}`,
'',
'This is the approval template for SKUs that should intentionally ship with a blank `pdp.clinically_shown_to` field. It does not approve anything by itself.',
'',
`Decision file: \`${manifest.decisionFile}\``,
`Editable CSV: \`data/content/compliance/clinical-blank-decisions.csv\``,
'',
'## Guardrail',
manifest.guardrail,
'',
'## Counts',
`- Decisions: ${manifest.counts.decisions}`,
`- Pending: ${manifest.counts.pending}`,
`- Requires source-backed claim: ${manifest.counts.requiresSourceBackedClaim}`,
`- Valid blank approvals: ${manifest.counts.validBlankApprovals}`,
`- Invalid approved_blank rows: ${manifest.counts.invalidApprovedBlank}`,
'',
'## How to use',
'1. Compliance owner reviews each SKU.',
'2. Set `decision` to `approved_blank` only if the clinical-claim module should be omitted for that SKU.',
'3. Fill `approvedBy`, `approvedAt`, `rationale`, and set `constraintsAcknowledged` to `true`.',
'4. Re-run `bun run compliance:blank-decisions && bun run export:content`.',
''
].join('\n'), 'utf8')
console.log(`Clinical blank decisions: ${decisions.length}`)
console.log(`Valid blank approvals: ${validBlankApprovals.length}`)
if (!manifest.ready) process.exitCode = 1