import assert from 'node:assert/strict'; import path from 'node:path'; import { digest, object, text } from './workflow-case-lib.mjs'; export const canonicalPath = (root, file) => path.relative(root, path.resolve(root, text(file, 'input path'))); export function caseEvidence(receipt, shaFile) { object(receipt, 'case manifest'); assert.ok(Array.isArray(receipt.cases)); const cases = receipt.cases.map(item => { object(item, 'case'); text(item.kind, 'case.kind'); assert.ok(item.inputs === undefined || Array.isArray(item.inputs)); assert.ok(item.artifacts === undefined || Array.isArray(item.artifacts)); const bindings = [...(item.inputs ?? []), ...(item.artifacts ?? [])]; const tests = item.result?.tests; const observations = bindings.map(ref => { object(ref, 'case binding'); text(ref.path, 'case binding.path'); assert.ok(hash(ref.sha256)); const actualSha256 = shaFile(ref.path); assert.ok(actualSha256 === null || hash(actualSha256)); return { path: ref.path, expectedSha256: ref.sha256, actualSha256 }; }); const bound = Boolean(item.inputs?.length && item.artifacts?.length) && observations.every(ref => ref.actualSha256 === ref.expectedSha256); const eligible = item.status === 'PASS' && item.process?.code === 0 && item.process.timedOut === false && item.process.cancelled === false && item.result?.passed === true && Number.isSafeInteger(tests) && tests > 0 && bound; return { kind: item.kind, reportedTests: tests ?? null, bound, eligible, currentSelectedTests: eligible ? tests : 0, observations }; }); return { cases, acceptance: 'NOT_CLAIMED', remainingGates: receipt.remainingGates ?? [] }; } const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); function byId(values, label) { assert.ok(Array.isArray(values), `${label}: expected array`); const result = new Map(); for (const item of values) { object(item, label); const id = text(item.id, `${label}.id`); assert.ok(!result.has(id), `${label}: duplicate ${id}`); result.set(id, item); } return result; } export function coverageBaseline(catalog, inventory, historical, shaFile) { object(catalog, 'catalog'); object(inventory, 'inventory'); object(historical, 'integrity'); const routes = byId(catalog.entries, 'routes'), units = byId(inventory.units, 'units'); const ids = [...new Set([...routes.keys(), ...units.keys()])].sort(); assert.ok(hash(inventory.authority?.sourceTreeSha256), 'inventory source hash required'); assert.ok(Array.isArray(inventory.sourceFiles), 'sourceFiles must be an array'); const sources = inventory.sourceFiles.map(file => { object(file, 'source'); text(file.path, 'source.path'); text(file.sourcePath, 'source.sourcePath'); assert.ok(hash(file.sha256)); const actualSha256 = shaFile(file.path); assert.ok(actualSha256 === null || hash(actualSha256)); return { ...file, actualSha256, status: actualSha256 === null ? 'MISSING' : actualSha256 === file.sha256 ? 'MATCH' : 'DRIFT' }; }); const sourceTreeSha256 = digest(sources.map(file => `${file.sourcePath}\0${file.actualSha256 ?? 'MISSING'}\n`).join('')); const inventorySnapshotMatches = digest(sources.map(file => `${file.sourcePath}\0${file.sha256}\n`).join('')) === inventory.authority.sourceTreeSha256; const references = new Map(); const inspect = reference => { if (reference === undefined || reference === null) return { status: 'UNBOUND' }; object(reference, 'reference'); text(reference.path, 'reference.path'); assert.ok(hash(reference.sha256), 'reference SHA-256 required'); const key = `${reference.path}\0${reference.sha256}`; if (!references.has(key)) { const actualSha256 = shaFile(reference.path); assert.ok(actualSha256 === null || hash(actualSha256)); references.set(key, { path: reference.path, expectedSha256: reference.sha256, actualSha256, status: actualSha256 === null ? 'MISSING' : actualSha256 === reference.sha256 ? 'MATCH' : 'DRIFT' }); } return references.get(key); }; assert.ok(Array.isArray(historical.references), 'historical references must be an array'); for (const entry of historical.references) inspect({ path: entry.path, sha256: entry.expectedSha256 }); const evidence = ids.map(id => { const unit = units.get(id), binding = unit?.coverage, recorded = unit?.coverageStatus === 'VERIFIED'; const manifest = inspect(binding?.evidenceManifest), production = inspect(binding?.productionEvidence); const referenceMatches = manifest.status === 'MATCH' && production.status === 'MATCH'; const sourceMatches = hash(binding?.sourceTreeSha256) && binding.sourceTreeSha256 === sourceTreeSha256; const reasons = [!unit && 'NOT_IN_INVENTORY', !routes.has(id) && 'NOT_IN_CATALOG', !recorded && 'NO_RECORDED_PASS', !referenceMatches && 'EVIDENCE_REFERENCE_INVALID', !sourceMatches && 'SOURCE_VERSION_UNBOUND', !inventorySnapshotMatches && 'INVENTORY_SNAPSHOT_INVALID'].filter(Boolean); return { id, recorded, referenceMatches, sourceMatches, eligible: reasons.length === 0, reasons }; }); const refs = [...references.values()].sort((a, b) => a.path.localeCompare(b.path) || a.expectedSha256.localeCompare(b.expectedSha256)); const count = (array, predicate) => array.filter(predicate).length; const verifiedUnits = count(evidence, unit => unit.eligible), unitCount = ids.length; const reconciliation = { catalogUnits: routes.size, inventoryUnits: units.size, denominator: unitCount, catalogOnly: ids.filter(id => !units.has(id)), inventoryOnly: ids.filter(id => !routes.has(id)) }; const integrity = { referencedArtifacts: refs.length, matchingArtifacts: count(refs, ref => ref.status === 'MATCH'), driftedArtifacts: count(refs, ref => ref.status === 'DRIFT'), missingArtifacts: count(refs, ref => ref.status === 'MISSING') }; const history = { recordedVerifiedUnits: count(evidence, unit => unit.recorded), referenceValidUnits: count(evidence, unit => unit.recorded && unit.referenceMatches), oldIntegrityObservedAt: historical.observedAt ?? null, oldDriftedArtifacts: historical.driftedArtifacts }; const source = { scope: 'declared inventory source files', inventoryTreeSha256: inventory.authority.sourceTreeSha256, currentScopeSha256: sourceTreeSha256, changedFiles: count(sources, file => file.status !== 'MATCH'), inventorySnapshotMatches: digest(sources.map(file => `${file.sourcePath}\0${file.sha256}\n`).join('')) === inventory.authority.sourceTreeSha256 }; return { schemaVersion: 1, unitCount, verifiedUnits, unverifiedUnits: unitCount - verifiedUnits, sourceTreeSha256, reconciliation, integrity, history, source, evidence, references: refs, sources }; }