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>
140 lines
6.6 KiB
TypeScript
140 lines
6.6 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
import { dirname, join } from 'path'
|
|
|
|
const root = process.cwd()
|
|
const outRoot = join(root, 'content_population_exports')
|
|
const intakePath = join(root, 'data', 'handoff', 'owner-response-intake.json')
|
|
mkdirSync(outRoot, { recursive: true })
|
|
mkdirSync(dirname(intakePath), { recursive: true })
|
|
|
|
function readJson(path: string, fallback: any = null) {
|
|
if (!existsSync(path)) return fallback
|
|
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
|
|
}
|
|
function writeJson(path: string, data: any) {
|
|
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
}
|
|
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 nonempty(value: any) { return String(value || '').trim().length > 0 }
|
|
function isoish(value: any) { const parsed = Date.parse(String(value || '').trim()); return Number.isFinite(parsed) }
|
|
|
|
const generatedAt = new Date().toISOString()
|
|
const ownerMessages = readJson(join(outRoot, 'owner_handoff_messages.json'), {})
|
|
const templateRows = (ownerMessages.messages || []).map((message: any) => ({
|
|
lane: message.Lane,
|
|
owner: message.Audience,
|
|
messageSubject: message.Subject,
|
|
messageSent: false,
|
|
sentAt: '',
|
|
sentVia: '',
|
|
responseReceived: false,
|
|
responseReceivedAt: '',
|
|
responderName: '',
|
|
responseSummary: '',
|
|
evidenceFilesUpdated: [],
|
|
commandRunAfterUpdate: '',
|
|
gateResultAfterUpdate: 'not_run',
|
|
notes: ''
|
|
}))
|
|
|
|
if (!existsSync(intakePath)) {
|
|
writeJson(intakePath, {
|
|
purpose: 'Editable intake tracker for owner replies to the JV handoff messages. This records communication/evidence receipt only; it does not approve content or bypass any gate.',
|
|
guardrails: [
|
|
'Only set responseReceived=true after the owner has replied with usable evidence or a concrete decision.',
|
|
'Keep evidenceFilesUpdated limited to actual edited files or saved evidence artifacts.',
|
|
'Run the lane command after updating evidence and record the result; do not mark gates passed manually.',
|
|
'Do not store credentials or secrets in this tracker.'
|
|
],
|
|
responses: templateRows
|
|
})
|
|
}
|
|
|
|
const intake = readJson(intakePath, { responses: [] })
|
|
const rows = (ownerMessages.messages || []).map((message: any) => {
|
|
const response = (intake.responses || []).find((row: any) => row.lane === message.Lane) || {}
|
|
const sentOk = response.messageSent === true && isoish(response.sentAt) && nonempty(response.sentVia)
|
|
const responseOk = response.responseReceived === true && isoish(response.responseReceivedAt) && nonempty(response.responderName) && nonempty(response.responseSummary)
|
|
const evidenceOk = Array.isArray(response.evidenceFilesUpdated) && response.evidenceFilesUpdated.length > 0
|
|
const commandOk = nonempty(response.commandRunAfterUpdate) && ['pass', 'blocked_with_new_evidence'].includes(String(response.gateResultAfterUpdate || ''))
|
|
const status = responseOk && evidenceOk && commandOk ? 'ready_for_gate_rerun' : sentOk ? 'awaiting_response_or_evidence' : 'not_sent'
|
|
const blockers = []
|
|
if (!sentOk) blockers.push('message not recorded as sent')
|
|
if (!responseOk) blockers.push('owner response not recorded')
|
|
if (!evidenceOk) blockers.push('evidence files not updated')
|
|
if (!commandOk) blockers.push('post-update command result not recorded')
|
|
return {
|
|
Lane: message.Lane,
|
|
Owner: message.Audience,
|
|
Status: status,
|
|
MessageSent: sentOk ? 'yes' : 'no',
|
|
ResponseReceived: responseOk ? 'yes' : 'no',
|
|
EvidenceUpdated: evidenceOk ? 'yes' : 'no',
|
|
CommandResultRecorded: commandOk ? 'yes' : 'no',
|
|
EvidenceFilesUpdated: response.evidenceFilesUpdated || [],
|
|
NextAction: blockers.length ? blockers.join('; ') : 'Run the lane regeneration command and master verifier.',
|
|
Guardrail: message.Guardrail
|
|
}
|
|
})
|
|
|
|
const readyRows = rows.filter(row => row.Status === 'ready_for_gate_rerun')
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: rows.length > 0 && readyRows.length === rows.length,
|
|
purpose: 'Validates whether owner handoff messages have been sent and whether usable owner responses/evidence updates have been recorded before rerunning blocker gates.',
|
|
intakeFile: 'data/handoff/owner-response-intake.json',
|
|
counts: {
|
|
lanes: rows.length,
|
|
readyForGateRerun: readyRows.length,
|
|
awaitingResponseOrEvidence: rows.filter(row => row.Status === 'awaiting_response_or_evidence').length,
|
|
notSent: rows.filter(row => row.Status === 'not_sent').length
|
|
},
|
|
rows,
|
|
files: {
|
|
markdown: 'content_population_exports/owner_response_intake.md',
|
|
json: 'content_population_exports/owner_response_intake.json',
|
|
csv: 'content_population_exports/owner_response_intake.csv',
|
|
editableIntake: 'data/handoff/owner-response-intake.json'
|
|
}
|
|
}
|
|
|
|
writeJson(join(outRoot, 'owner_response_intake.json'), manifest)
|
|
writeFileSync(join(outRoot, 'owner_response_intake.csv'), csv(rows, ['Lane', 'Owner', 'Status', 'MessageSent', 'ResponseReceived', 'EvidenceUpdated', 'CommandResultRecorded', 'EvidenceFilesUpdated', 'NextAction', 'Guardrail']), 'utf8')
|
|
writeFileSync(join(outRoot, 'owner_response_intake.md'), [
|
|
'# Owner response intake',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`Ready for gate reruns: **${manifest.ready ? 'YES' : 'NO'}**`,
|
|
`Lanes ready: ${manifest.counts.readyForGateRerun}/${manifest.counts.lanes}`,
|
|
`Awaiting response/evidence: ${manifest.counts.awaitingResponseOrEvidence}`,
|
|
`Not sent: ${manifest.counts.notSent}`,
|
|
'',
|
|
`Editable tracker: \`${manifest.intakeFile}\``,
|
|
'',
|
|
'This tracker records owner communications and received evidence. It does not approve products, approve blank clinical claims, create staging evidence, or unlock paid provider calls by itself.',
|
|
'',
|
|
'## Rows',
|
|
'| Lane | Owner | Status | Next action |',
|
|
'|---|---|---|---|',
|
|
...rows.map(row => `| ${row.Lane} | ${row.Owner} | ${row.Status} | ${row.NextAction.replace(/\|/g, '/')} |`),
|
|
'',
|
|
'## Guardrail',
|
|
'After an owner response is recorded, rerun the lane-specific validation command. The authoritative gate remains the generated validator output, not this communication tracker.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Owner response intake ready: ${manifest.ready}`)
|
|
console.log(`Lanes ready: ${manifest.counts.readyForGateRerun}/${manifest.counts.lanes}`)
|
|
console.log(`Not sent: ${manifest.counts.notSent}`)
|
|
if (!manifest.ready) process.exitCode = 1
|