Files
justvitamin/scripts/validate-reviewer-input-preflight.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

156 lines
12 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const root = process.cwd()
const outRoot = join(root, 'content_population_exports')
mkdirSync(outRoot, { recursive: true })
function readText(path: string) {
return existsSync(path) ? readFileSync(path, 'utf8').replace(/^\uFEFF/, '') : ''
}
function readJson(path: string, fallback: any = null) {
if (!existsSync(path)) return fallback
return JSON.parse(readText(path))
}
function parseCsv(text: string) {
const rows: string[][] = []
let row: string[] = []
let cell = ''
let quoted = false
for (let i = 0; i < text.length; i++) {
const char = text[i]
const next = text[i + 1]
if (quoted) {
if (char === '"' && next === '"') { cell += '"'; i++ }
else if (char === '"') quoted = false
else cell += char
} else {
if (char === '"') quoted = true
else if (char === ',') { row.push(cell); cell = '' }
else if (char === '\n') { row.push(cell); rows.push(row); row = []; cell = '' }
else if (char !== '\r') cell += char
}
}
if (cell.length || row.length) { row.push(cell); rows.push(row) }
const [headers = [], ...body] = rows.filter(r => r.some(c => c.trim() !== ''))
return body.map(values => Object.fromEntries(headers.map((h, i) => [h, values[i] ?? ''])))
}
function csv(rows: any[], headers: string[]) {
const esc = (value: any) => {
if (value === undefined || value === null) value = ''
if (Array.isArray(value)) value = value.join(' | ')
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 yes(value: any) {
return String(value || '').trim().toLowerCase() === 'yes' || value === true
}
function nonempty(value: any) {
return String(value || '').trim().length > 0
}
function isoish(value: any) {
const text = String(value || '').trim()
if (!text) return false
const time = Date.parse(text)
return Number.isFinite(time)
}
function pushRow(rows: any[], area: string, file: string, check: string, status: 'pass' | 'blocked' | 'missing', evidence: string, nextAction: string, guardrail: string) {
rows.push({ Area: area, File: file, Check: check, Status: status, Evidence: evidence, NextAction: nextAction, Guardrail: guardrail })
}
const generatedAt = new Date().toISOString()
const rows: any[] = []
const clinicalPath = 'data/content/compliance/clinical-blank-decisions.csv'
const clinicalRows = parseCsv(readText(join(root, clinicalPath)))
const clinicalApproved = clinicalRows.filter(row => row.decision === 'approved_blank' && nonempty(row.approvedBy) && isoish(row.approvedAt) && nonempty(row.rationale) && yes(row.constraintsAcknowledged)).length
const clinicalPending = clinicalRows.filter(row => row.decision !== 'approved_blank').length
pushRow(rows, 'Compliance clinical blanks', clinicalPath, 'All blank clinical claim rows have explicit approved_blank decisions with approver, date, rationale, and constraints acknowledgement.', clinicalRows.length && clinicalPending === 0 ? 'pass' : clinicalRows.length ? 'blocked' : 'missing', `${clinicalApproved}/${clinicalRows.length} valid approved_blank rows; ${clinicalPending || clinicalRows.length} still pending/invalid.`, 'Compliance owner must fill the CSV or provide source-backed claims, then run bun run compliance:blank-decisions && bun run claims:sync-content.', 'Do not invent clinical claims or approve blanks without compliance input.')
const approvalPath = 'data/content/approvals/product-approval-decisions.csv'
const approvalRows = parseCsv(readText(join(root, approvalPath)))
const validApprovalRows = approvalRows.filter(row => row.decision === 'approve_all' && nonempty(row.approvedBy) && isoish(row.approvedAt) && nonempty(row.reviewEvidence) && yes(row.contentReviewed) && yes(row.complianceReviewed) && yes(row.imagesReviewed) && yes(row.exportReviewed) && yes(row.noFakeEvidenceAcknowledged)).length
const approvalPending = approvalRows.length - validApprovalRows
pushRow(rows, 'Product approval decisions', approvalPath, 'All products have human approve_all rows with all evidence/acknowledgement fields complete.', approvalRows.length && approvalPending === 0 ? 'pass' : approvalRows.length ? 'blocked' : 'missing', `${validApprovalRows}/${approvalRows.length} valid approve_all rows; ${approvalPending || approvalRows.length} still pending/invalid.`, 'Umar/JV reviewer must complete the CSV, then run bun run approval:decisions && bun run approval:review-packet.', 'Do not self-approve products or treat generated drafts as human approval.')
const providerPath = 'data/sources/provider-responses/dataforseo-reviews-endpoint-response.json'
const provider = readJson(join(root, providerPath), null)
const providerReady = provider && provider.responseReceived === true && (provider.endpointAvailable === true || nonempty(provider.replacementEndpoint)) && provider.providerSaysRetrySafe === true && provider.credentialIncludedInSupportThread === false
pushRow(rows, 'DataForSEO provider response', providerPath, 'Support response proves the Reviews endpoint or replacement endpoint is available and retry-safe without leaking credentials.', providerReady ? 'pass' : provider ? 'blocked' : 'missing', provider ? `responseReceived=${provider.responseReceived}; endpointAvailable=${provider.endpointAvailable}; replacementEndpoint=${provider.replacementEndpoint || 'blank'}; retrySafe=${provider.providerSaysRetrySafe}; credentialIncluded=${provider.credentialIncludedInSupportThread}` : 'Provider response file missing.', 'Wait for DataForSEO support/docs clearance, fill the provider response tracker, then run bun run dataforseo:support-response.', 'Use the real pi-fresh-foundation credentials only in shell env; never paste or store secrets in the response tracker.')
const spendPath = 'data/sources/approvals/dataforseo-stage1-approval.json'
const spend = readJson(join(root, spendPath), null)
const spendReady = spend && spend.approved === true && nonempty(spend.approvedBy) && isoish(spend.approvedAt) && Number(spend.maxSpendApproved) > 0 && Number(spend.maxSpendApproved) <= Number(spend.expectedMaxSpend || spend.maxSpendApproved) && Number(spend.targetCapApproved) === Number(spend.expectedTargetCap) && Number(spend.runLimitApproved) === Number(spend.expectedRunLimit) && Number(spend.depthOrRequestCapApproved) === Number(spend.expectedDepthOrRequestCap) && spend.hardStopAcknowledged === true && spend.noCredentialStorageAcknowledged === true && spend.postRunReviewRequired === true
pushRow(rows, 'DataForSEO spend approval', spendPath, 'Stage 1 approval file has approver, date, capped spend, exact target/run/depth caps, and hard-stop/no-secret/post-review acknowledgements.', spendReady ? 'pass' : spend ? 'blocked' : 'missing', spend ? `approved=${spend.approved}; maxSpendApproved=${spend.maxSpendApproved}; targetCap=${spend.targetCapApproved}/${spend.expectedTargetCap}; runLimit=${spend.runLimitApproved}/${spend.expectedRunLimit}; depth=${spend.depthOrRequestCapApproved}/${spend.expectedDepthOrRequestCap}; hardStop=${spend.hardStopAcknowledged}; noCredentialStorage=${spend.noCredentialStorageAcknowledged}` : 'Approval file missing.', 'Project owner must explicitly fill the approval file after endpoint clearance, then run bun run source:spend-gate.', 'Approval alone must not override provider endpoint/support/docs blockers.')
const importLogDir = 'data/staging/import-logs'
const importLogFull = join(root, importLogDir)
const importLogs = existsSync(importLogFull) ? readdirSync(importLogFull).filter(file => file.endsWith('.json')) : []
pushRow(rows, 'Staging import evidence', importLogDir, 'At least one real Matrixify/staging import log JSON exists outside templates.', importLogs.length > 0 ? 'pass' : existsSync(importLogFull) ? 'blocked' : 'missing', `${importLogs.length} import log file(s) found.`, 'Lewis/Euan must save real import logs under data/staging/import-logs/ and rerun bun run staging:evidence.', 'Queued templates are not import evidence.')
const pdpDir = 'data/staging/pdp-checks'
const pdpFull = join(root, pdpDir)
const pdpChecks = existsSync(pdpFull) ? readdirSync(pdpFull).filter(file => file.endsWith('.json')) : []
pushRow(rows, 'Staging PDP spot checks', pdpDir, 'At least 5 real staging PDP spot-check JSON files exist outside the queue/templates.', pdpChecks.length >= 5 ? 'pass' : existsSync(pdpFull) ? 'blocked' : 'missing', `${pdpChecks.length}/5 PDP check file(s) found.`, 'Save real staging URL spot checks under data/staging/pdp-checks/ and rerun bun run staging:evidence.', 'Queue files under pdp-check-queue are assignments only, not completed checks.')
const cutoverPath = 'data/staging/cutover-approval.json'
const cutover = readJson(join(root, cutoverPath), null)
const cutoverReady = cutover && (cutover.approved === true || cutover.cutoverApproved === true) && nonempty(cutover.approvedBy || cutover.signoffBy) && isoish(cutover.approvedAt || cutover.signoffAt)
pushRow(rows, 'Staging cutover approval', cutoverPath, 'Cutover approval exists with explicit approval/signoff owner and timestamp.', cutoverReady ? 'pass' : cutover ? 'blocked' : 'missing', cutover ? `approved=${cutover.approved ?? cutover.cutoverApproved}; approver=${cutover.approvedBy || cutover.signoffBy || 'blank'}; approvedAt=${cutover.approvedAt || cutover.signoffAt || 'blank'}` : 'Cutover approval file missing.', 'Project owner/Lewis/Euan must sign the cutover approval after successful staging checks.', 'Do not treat handoff packet generation as cutover approval.')
const blockingRows = rows.filter(row => row.Status !== 'pass')
const manifest = {
generatedAt,
ready: blockingRows.length === 0,
purpose: 'Preflight validation for the human-editable reviewer/provider/staging input files before rerunning the heavier handoff gates.',
counts: {
checks: rows.length,
passing: rows.filter(row => row.Status === 'pass').length,
blocked: rows.filter(row => row.Status === 'blocked').length,
missing: rows.filter(row => row.Status === 'missing').length
},
rows,
blockingRows,
files: {
markdown: 'content_population_exports/reviewer_input_preflight.md',
json: 'content_population_exports/reviewer_input_preflight.json',
csv: 'content_population_exports/reviewer_input_preflight.csv'
}
}
writeFileSync(join(outRoot, 'reviewer_input_preflight.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
writeFileSync(join(outRoot, 'reviewer_input_preflight.csv'), csv(rows, ['Area', 'File', 'Check', 'Status', 'Evidence', 'NextAction', 'Guardrail']), 'utf8')
writeFileSync(join(outRoot, 'reviewer_input_preflight.md'), [
'# Reviewer input preflight',
'',
`Generated: ${generatedAt}`,
'',
`Ready: **${manifest.ready ? 'YES' : 'NO'}**`,
`Checks passing: ${manifest.counts.passing}/${manifest.counts.checks}`,
`Blocked: ${manifest.counts.blocked}`,
`Missing: ${manifest.counts.missing}`,
'',
'This preflight validates only reviewer-owned input files. It does not approve content, claim provider availability, spend credits, or replace `bun run verify:handoff`.',
'',
'## Blocking rows',
...(blockingRows.length ? blockingRows.map(row => `- [${String(row.Status).toUpperCase()}] ${row.Area}: ${row.Evidence} Next: ${row.NextAction}`) : ['- none']),
'',
'## All checks',
'| Area | Status | Evidence | File |',
'|---|---|---|---|',
...rows.map(row => `| ${row.Area} | ${row.Status} | ${String(row.Evidence).replace(/\|/g, '/')} | ${row.File} |`),
'',
'## Guardrail',
'Use this before asking reviewers to rerun expensive/heavy gates. If it is blocked, fix the named editable input file first; do not infer decisions from generated packets.',
''
].join('\n'), 'utf8')
console.log(`Reviewer input preflight ready: ${manifest.ready}`)
console.log(`Checks passing: ${manifest.counts.passing}/${manifest.counts.checks}`)
console.log(`Blocked: ${manifest.counts.blocked}`)
console.log(`Missing: ${manifest.counts.missing}`)
if (!manifest.ready) process.exitCode = 1