48 if (receipt.version !== 1 && receipt.version !== 2) add('version', 'Must equal 1 or 2.')
49 if (!statuses.has(receipt.status)) add('status', 'Must be verified, partial, or blocked.')
50 if (receipt.evidenceSource !== undefined && !evidenceSources.has(receipt.evidenceSource)) add('evidenceSource', 'Must be executed-now, supplied, or mixed.')
51 if (receipt.version === 2 && !evidenceSources.has(receipt.evidenceSource)) add('evidenceSource', 'Version 2 requires an evidence source.')
52 if (!nonEmpty(receipt.problem)) add('problem', 'Must be a non-empty string.')
53
54 if (!isObject(receipt.baseline)) {
55 add('baseline', 'Must be an object.')
56 } else {
57 rejectUnknown(receipt.baseline, new Set(['command', 'result', 'evidence']), 'baseline')
58 if (!nonEmpty(receipt.baseline.command)) add('baseline.command', 'Must be a non-empty string.')
59 if (!baselineResults.has(receipt.baseline.result)) add('baseline.result', 'Must be failed, observed, or not-run.')
60 if (!nonEmpty(receipt.baseline.evidence)) add('baseline.evidence', 'Must be a non-empty string.')
61 }
62
63 if (!isObject(receipt.rootCause)) {
64 add('rootCause', 'Must be an object.')
65 } else {
66 rejectUnknown(receipt.rootCause, new Set(['summary', 'evidence']), 'rootCause')
67 if (!nonEmpty(receipt.rootCause.summary)) add('rootCause.summary', 'Must be a non-empty string.')
68 if (!Array.isArray(receipt.rootCause.evidence)) {
108 if (!Array.isArray(receipt.changes) || receipt.changes.length === 0) add('changes', 'Verified requires at least one changed file or artifact.')
109 if (!Array.isArray(receipt.verification) || receipt.verification.length === 0) add('verification', 'Verified requires at least one verification check.')
110 if (Array.isArray(receipt.verification) && receipt.verification.some((entry) => entry?.result !== 'passed')) add('verification', 'Every verification check must pass for verified status.')
111 if (Array.isArray(receipt.gaps) && receipt.gaps.length > 0) add('gaps', 'Verified status cannot contain proof gaps.')
112 }
113
114 if (receipt.status === 'partial' && Array.isArray(receipt.gaps) && receipt.gaps.length === 0) add('gaps', 'Partial status must name at least one missing proof layer.')
115 if (receipt.status === 'blocked' && Array.isArray(receipt.gaps) && receipt.gaps.length === 0) add('gaps', 'Blocked status must name the external blocking condition.')
116
117 return { valid: issues.length === 0, issues }
118}
119
120async function main() {
121 const path = process.argv[2]
122 if (!path) throw new Error('Usage: node scripts/validate-receipt.mjs <receipt.json> [--json]')
123
124 let input = ''
125 if (path === '-') {
126 process.stdin.setEncoding('utf8')
127 for await (const chunk of process.stdin) input += chunk