Files
baiyue-firstleg/scripts/acceptance-evidence-audit.mjs
T

91 lines
5.3 KiB
JavaScript

import { readFileSync, writeFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { resolve, dirname } from 'node:path'
import { pathToFileURL } from 'node:url'
const levels = new Set(['unit', 'contract', 'http', 'browser', 'device'])
const digest = bytes => createHash('sha256').update(bytes).digest('hex')
const nonempty = value => typeof value === 'string' && value.trim().length > 0
// The reviewed manifest defines scope. Counts and free-text evidence never prove a requirement.
export function auditEvidence(manifest, load) {
if (!manifest || manifest.version !== 1 || !nonempty(manifest.scope)
|| !nonempty(manifest.revision) || !Array.isArray(manifest.requirements)
|| manifest.requirements.length === 0) throw new Error('Invalid requirement manifest')
const ids = new Set()
const results = manifest.requirements.map(requirement => {
const { id, domain, level, checks, evidence } = requirement
if (!nonempty(id) || ids.has(id) || !nonempty(domain) || !levels.has(level)
|| !Array.isArray(checks) || checks.length === 0 || checks.some(x => !nonempty(x))
|| new Set(checks).size !== checks.length) throw new Error('Invalid or duplicate requirement')
ids.add(id)
const reasons = []
if (!evidence || !nonempty(evidence.path) || !/^[a-f0-9]{64}$/.test(evidence.sha256 || '')) {
return { id, domain, level, passed: false, reasons: ['MISSING_EVIDENCE'] }
}
let report
try {
const bytes = load(evidence.path)
if (digest(bytes) !== evidence.sha256) reasons.push('HASH_MISMATCH')
report = JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/, ''))
} catch {
return { id, domain, level, passed: false, reasons: ['UNREADABLE_EVIDENCE'] }
}
if (report.level !== level) reasons.push('EVIDENCE_LEVEL_MISMATCH')
if (report.domain !== domain) reasons.push('BUSINESS_DOMAIN_MISMATCH')
if (report.revision !== manifest.revision) reasons.push('REVISION_MISMATCH')
if (!nonempty(report.runId)) reasons.push('MISSING_RUN_ID')
if (!checkLegacyConsistency(report).passed) reasons.push('CONTRADICTORY_REPORT')
if (report.success !== true || report.fatal || report.completionClaim === false
|| report.summary?.allConditionsComplete === false
|| Number(report.summary?.failed || 0) > 0
|| Number(report.summary?.pendingConditions || 0) > 0
|| Number(report.summary?.skippedModules || 0) > 0) reasons.push('REPORT_NOT_COMPLETE')
if ((report.coverageGaps || []).some(gap => gap.blocking !== false)
|| Number(report.summary?.coverageGaps?.blocking || 0) > 0
|| (report.deploymentBlockers || []).length > 0) reasons.push('BLOCKING_GAP')
const steps = Array.isArray(report.steps) ? report.steps : []
if (steps.some(step => step.passed !== true || step.skipped)) {
reasons.push('FAILED_OR_SKIPPED_STEP')
}
for (const name of checks) {
const matches = steps.filter(step => step.name === name && step.requirementId === id)
if (matches.length !== 1 || matches[0].passed !== true) reasons.push(`CHECK_NOT_PROVEN:${name}`)
else if (['http', 'browser'].includes(level) && (!nonempty(matches[0].requestId)
|| !nonempty(matches[0].marker) || matches[0].marker !== report.runId)) {
reasons.push(`MISSING_RUNTIME_IDENTITY:${name}`)
}
}
return { id, domain, level, passed: reasons.length === 0, reasons }
})
const passed = results.filter(row => row.passed).length
return { version: 1, scope: manifest.scope, revision: manifest.revision,
generatedAt: new Date().toISOString(), completionClaim: passed === results.length,
summary: { requirements: results.length, passed, pending: results.length - passed }, results }
}
export function checkLegacyConsistency(report) {
const reasons = []
if (report.summary?.allConditionsComplete === true && report.completionClaim !== true) reasons.push('CONTRADICTORY_COMPLETION')
if (report.summary?.allConditionsComplete === true
&& report.summary?.checkedScopeEvidence !== report.summary?.requirements) reasons.push('INCOMPLETE_CHECKED_SCOPE')
if (report.summary?.allConditionsComplete === true && (report.conditions || []).some(row => row.remaining)) reasons.push('REMAINING_REQUIREMENTS')
return { passed: reasons.length === 0, reasons,
scope: 'Consistency only; legacy report is not promoted to verified evidence' }
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
try {
const [mode, input, output] = process.argv.slice(2)
if (!['audit', 'check-legacy'].includes(mode) || !input || !output) throw new Error('Usage: acceptance-evidence-audit.mjs audit|check-legacy <input.json> <new-output.json>')
const source = JSON.parse(readFileSync(input, 'utf8').replace(/^\uFEFF/, ''))
const result = mode === 'audit' ? auditEvidence(source, file => readFileSync(resolve(dirname(input), file))) : checkLegacyConsistency(source)
writeFileSync(output, JSON.stringify(result, null, 2) + '\n', { flag: 'wx' })
console.log(JSON.stringify(result.summary || { passed: result.passed, reasons: result.reasons }))
process.exitCode = (mode === 'audit' ? result.completionClaim : result.passed) ? 0 : 1
} catch (error) {
console.error(error.code === 'EEXIST' ? 'Output already exists; preserve prior evidence' : 'Evidence audit failed: invalid arguments, manifest or file')
process.exitCode = 2
}
}