import fs from 'node:fs'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { styleVerification } from './guard-style-evidence.mjs'; export function sourceFingerprint(target, files) { const hash = createHash('sha256'); for (const file of [...new Set(files)].sort()) { hash.update(file.replaceAll('\\', '/')); hash.update('\0'); hash.update(fs.readFileSync(path.resolve(target, file))); hash.update('\0'); } return hash.digest('hex'); } // Optional measurements describe styles, never approval of a consumer override. export function linkCssEvidence(guard, runtime, linked) { const supplied = runtime.cssEvidence; if (supplied === undefined) return { status: 'NOT VERIFIED', checks: [], intentReviews: [], reasons: ['CSS measurements were not supplied'] }; if (!linked) return { status: 'NOT VERIFIED', checks: [], intentReviews: [], reasons: ['CSS evidence is stale or unlinked'] }; if (!supplied || typeof supplied !== 'object') return { status: 'NOT VERIFIED', checks: [], intentReviews: [], reasons: ['Invalid CSS evidence object'] }; const reasons = []; const checks = []; const intentReviews = []; if (!Array.isArray(supplied.checks) || !supplied.checks.length) reasons.push('Named CSS measurements are missing'); for (const check of Array.isArray(supplied.checks) ? supplied.checks : []) { const valid = check && typeof check.name === 'string' && check.name.trim() && ['COMPUTED_STYLE', 'CSS_VARIABLE'].includes(check.kind) && [390, 768, 1280, 1440].includes(check.width) && runtime.viewports?.some(v => v.width === check.width && v.status === 'PASS') && typeof check.selector === 'string' && check.selector.trim() && typeof check.property === 'string' && check.property.trim() && (check.kind !== 'CSS_VARIABLE' || check.property.startsWith('--')) && typeof check.actual === 'string' && check.actual.trim() && typeof check.expected === 'string' && check.expected.trim(); if (!valid) { reasons.push('Incomplete or unsupported CSS measurement'); continue; } checks.push({ ...check, status: check.actual === check.expected ? 'PASS' : 'FAIL' }); } for (const review of Array.isArray(supplied.intentReviews) ? supplied.intentReviews : []) { const issue = guard.issues.find(i => i.code === 'REUSE-003' && i.code === review?.issue?.code && i.file === review.issue.file && i.line === review.issue.line); const reference = review?.reference; let matches = false; // References stay inside the explicit contract source, including real paths. try { const root = fs.realpathSync(guard.contractSource); const file = fs.realpathSync(path.resolve(root, reference.file)); const relative = path.relative(root, file); matches = relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative) && createHash('sha256').update(fs.readFileSync(file)).digest('hex') === reference.sha256; } catch { /* Missing / stale references remain unverified. */ } let lineCount = 0; try { lineCount = fs.readFileSync(path.resolve(guard.contractSource, reference.file), 'utf8').split('\n').length; } catch { /* Invalid reference. */ } if (!issue || !matches || typeof review.rationale !== 'string' || !review.rationale.trim() || !Number.isInteger(reference?.line) || reference.line < 1 || reference.line > lineCount) { reasons.push('CSS intent review has an unmatched issue or unavailable / stale reference'); continue; } intentReviews.push({ issue: review.issue, rationale: review.rationale, reference, status: 'NOT VERIFIED', basis: 'Documented usage review; design intent is not automatically approved' }); } return { status: checks.some(c => c.status === 'FAIL') ? 'FAIL' : checks.length && !reasons.length ? 'PASS' : 'NOT VERIFIED', checks, intentReviews, reasons }; } // Combines independent results without altering the original Guard issues. export function combineEvidence(guard, runtime) { const reasons = []; let fresh = false; try { fresh = guard.sourceFingerprint === sourceFingerprint(guard.target, guard.fingerprintFiles) && guard.contractFingerprint === sourceFingerprint(guard.contractSource, guard.contractFiles); } catch { reasons.push('Guard source or contract files are unavailable'); } const linked = fresh && runtime.sourceFingerprint === guard.sourceFingerprint && runtime.contractFingerprint === guard.contractFingerprint; if (!linked) reasons.push('Runtime evidence is stale or does not match the Guard source / contract'); const expected = [390, 768, 1280, 1440]; const viewportComplete = expected.every(width => runtime.viewports?.some(v => v.width === width && v.status === 'PASS')); const interactionComplete = Array.isArray(runtime.interactions) && runtime.interactions.length > 0 && runtime.interactions.every(i => i.status === 'PASS' && typeof i.name === 'string' && i.name.trim()); const cssEvidence = linkCssEvidence(guard, runtime, linked); const styles = styleVerification(guard, runtime, cssEvidence, linked); const failed = runtime.viewports?.some(v => v.status === 'FAIL') || runtime.interactions?.some(i => i.status === 'FAIL') || cssEvidence.status === 'FAIL' || styles.runtimeStatus === 'FAIL'; const provenBootstrap = guard.runtimeRequired === true && !guard.issues.some(i => i.code === 'RENDER-001'); if (!provenBootstrap) reasons.push('Final React bootstrap was not proven with --result'); const runtimeStatus = !linked || !provenBootstrap ? 'NOT VERIFIED' : failed ? 'FAIL' : viewportComplete && interactionComplete ? 'PASS' : 'NOT VERIFIED'; if (!viewportComplete) reasons.push('Four required viewport results are missing or incomplete'); if (!interactionComplete) reasons.push('Named core interaction checks are missing or incomplete'); const status = guard.issues.some(i => i.severity === 'FAIL') || guard.staticStatus === 'FAIL' || runtimeStatus === 'FAIL' || styles.finalStatus === 'FAIL' ? 'FAIL' : guard.staticStatus === 'PASS' && styles.staticStatus === 'PASS' && runtimeStatus === 'PASS' && (!guard.styleEvidence || styles.designIntentStatus === 'PASS') ? 'PASS' : 'NOT VERIFIED'; return { schemaVersion: 1, scope: guard.scope, status, staticStatus: guard.staticStatus, rawGuardStatus: guard.status, runtimeStatus, linked, reasons, cssEvidence, styleVerification: styles, unresolvedStaticIssues: guard.issues.filter(i => !i.code.startsWith('RENDER-')), limitations: ['Scope is inherited from Guard; PARTIAL never covers the entire screen', 'Native/CSS intent findings are not cleared by browser evidence', 'Runtime assertions are supplied evidence, not independent attestation', 'CSS measurements cover only the named selector / property / viewport; they do not approve design intent or dynamic token states', 'Screen reader speech, physical touch, backend and all states are NOT VERIFIED'] }; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { const [guardFile, runtimeFile] = process.argv.slice(2); const result = combineEvidence(JSON.parse(fs.readFileSync(guardFile)), JSON.parse(fs.readFileSync(runtimeFile))); console.log(JSON.stringify(result, null, 2)); if (result.status === 'FAIL') process.exitCode = 1; } catch (error) { console.error(error.message); process.exitCode = 2; } }