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>
156 lines
12 KiB
TypeScript
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
|