From 61bc5e25968581fabc7c77434168980a956281d8 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:39:07 +0100 Subject: [PATCH 1/3] fix(k9): retain subprocess failures and collect structured conformance evidence --- .githooks/validate-k9.sh | 31 ++- 1-formats/k9/tools/conformance-report.mjs | 193 ++++++++++++++++++ .../k9/tools/conformance-report.test.mjs | 83 ++++++++ 3 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 1-formats/k9/tools/conformance-report.mjs create mode 100644 1-formats/k9/tools/conformance-report.test.mjs diff --git a/.githooks/validate-k9.sh b/.githooks/validate-k9.sh index a9f8ea6a6..773c2556b 100755 --- a/.githooks/validate-k9.sh +++ b/.githooks/validate-k9.sh @@ -120,15 +120,30 @@ fi # per-file verdicts coming from a single process rather than N slightly # different environments. REPORT="$(mktemp)" +trap 'rm -f "$REPORT"' EXIT +STRICT_ARGS=() +case "$LAYER" in L2|L3|all) STRICT_ARGS=(--strict) ;; esac set +e -bash "$VALIDATOR" --layer "$LAYER" --json --quiet "${FILES[@]}" > "$REPORT" 2>/dev/null +bash "$VALIDATOR" "${STRICT_ARGS[@]}" --layer "$LAYER" --json --quiet "${FILES[@]}" > "$REPORT" +VALIDATOR_RC=$? set -e +if [ -n "${K9_JSON_REPORT:-}" ]; then cp "$REPORT" "$K9_JSON_REPORT"; fi +RESULTS=$(grep -cE '"verdict":"(pass|fail|error|routed)"' "$REPORT" || true) +if [ "$RESULTS" -ne "${#FILES[@]}" ]; then + echo "[validate-k9] ERROR: expected ${#FILES[@]} file results, received $RESULTS (validator exit $VALIDATOR_RC)" >&2 + exit 2 +fi +case "$VALIDATOR_RC" in 0|1) ;; *) + echo "[validate-k9] ERROR: validator incomplete or unavailable (exit $VALIDATOR_RC)" >&2 + exit "$VALIDATOR_RC" ;; +esac -CHANGED="$STAGED_FILES" +CHANGED="${K9_CHANGED_FILES:-$STAGED_FILES}" HARD=0 ADVISORY=0 CONFORMING=0 STALE=0 +ROUTED=0 while IFS= read -r line; do [ -z "$line" ] && continue @@ -140,7 +155,10 @@ while IFS= read -r line; do rules="$(printf '%s' "$line" | grep -oE '"rule":"K9-[ESNC][0-9]+"' | sed 's/.*:"//; s/"//' | sort -u | tr '\n' ' ' | sed 's/ $//' || true)" case "$verdict" in - pass|routed) + routed) + ROUTED=$((ROUTED + 1)) + continue ;; + pass) CONFORMING=$((CONFORMING + 1)) if in_ledger "$file"; then # A conforming file must not keep its exemption: that is how a ledger @@ -166,13 +184,18 @@ while IFS= read -r line; do done < "$REPORT" rm -f "$REPORT" +if [ "$VALIDATOR_RC" -ne 0 ] && [ "$HARD" -eq 0 ] && [ "$ADVISORY" -eq 0 ]; then + echo "[validate-k9] ERROR: validator failed without attributed violations" >&2 + exit 2 +fi + if [ "$STALE" -gt 0 ] || [ "$HARD" -gt 0 ]; then echo -e "${RED}[validate-k9] $HARD violation(s), $STALE stale ledger entr(ies)${NC}" >&2 echo -e "${RED}[validate-k9] spec: 1-formats/k9/spec/K9-CONTRACT-SPEC.adoc · plan: 1-formats/k9/spec/MIGRATION-1058.adoc${NC}" >&2 exit 1 fi -echo -e "${GRN}[validate-k9]${NC} $CONFORMING conforming, $ADVISORY grandfathered (layer $LAYER, contract v1.0.0)" +echo -e "${GRN}[validate-k9]${NC} $CONFORMING conforming, $ADVISORY grandfathered, $ROUTED routed out of scope (layer $LAYER, contract v1.0.0)" if [ "$LAYER" = "L0" ] || [ "$LAYER" = "L1" ]; then # Say out loud what was NOT checked. A lexical pass is not a conformance # result, and the difference is the whole point of §12. diff --git a/1-formats/k9/tools/conformance-report.mjs b/1-formats/k9/tools/conformance-report.mjs new file mode 100644 index 000000000..0d8c6611b --- /dev/null +++ b/1-formats/k9/tools/conformance-report.mjs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +// hypatia: allow cicd_rules/javascript_detected -- Bun/GitHub JSON reporting boundary; zero dependencies. +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +export const VERSION = 1; +const titles = { deed: 'DEED syntax', typecheck: 'K9 contract typecheck', selftest: 'K9 validator self-test', fixtures: 'K9 positive/negative controls', corpus: 'K9 live corpus (L0–L2)' }; +const marker = ''; +const clean = s => String(s).replace(/\x1b\[[0-9;]*m/g, ''); +const item = (id, status, reason = '', advisory = false) => ({ id, status, reason, advisory }); +const read = p => fs.readFileSync(p, 'utf8'); +export function counts(items) { + const c = { passed: 0, failed: 0, indeterminate: 0, not_applicable: 0, advisory: 0 }; + for (const i of items) { + if (i.status === 'pass') c.passed++; + else if (i.status === 'fail') { c.failed++; if (i.advisory) c.advisory++; } + else if (i.status === 'not_applicable') c.not_applicable++; + else c.indeterminate++; + } + return c; +} +export function state(items) { + if (!items.length) return 'orange'; + if (items.some(i => i.status === 'fail' && !i.advisory)) return 'red'; + if (items.some(i => i.status === 'indeterminate')) return 'orange'; + if (items.some(i => i.advisory)) return 'yellow'; + return items.every(i => i.status === 'not_applicable') ? 'grey' : 'green'; +} +export function assertions(output) { + return clean(output).split('\n').flatMap(line => { + const m = /^(ok|FAIL|SKIP)\s+(.+)$/.exec(line); + return m ? [item(m[2], m[1] === 'ok' ? 'pass' : m[1] === 'FAIL' ? 'fail' : 'indeterminate', m[1] === 'ok' ? '' : m[2])] : []; + }); +} +export function checkedAssertions(output, code, expected) { + const rows = assertions(output); + if (!rows.length) rows.push(item('check execution', 'indeterminate', 'No assertion evidence was produced.')); + if (expected !== undefined && rows.length !== expected) rows.push(item('coverage', 'indeterminate', `Expected ${expected} controls, received ${rows.length} results.`)); + if (code !== 0 && !rows.some(i => i.status !== 'pass')) rows.push(item('check execution', 'indeterminate', `Process exited ${code} despite passing assertion lines.`)); + return rows; +} +export function corpusItems(text, expected, ledger, changed) { + const rows = [], seen = new Set(); + for (const line of text.trim().split('\n').filter(Boolean)) { + let r; + try { r = JSON.parse(line); } catch { rows.push(item('validator output', 'indeterminate', 'Malformed JSON result.')); continue; } + if (!expected.includes(r.file) || seen.has(r.file) || !['pass', 'fail', 'error', 'routed'].includes(r.verdict) || !Array.isArray(r.findings)) { + rows.push(item(String(r.file ?? 'validator output'), 'indeterminate', 'Unexpected, duplicate or invalid result.')); continue; + } + seen.add(r.file); + const unavailable = r.findings.filter(f => f.severity === 'skipped'); + const errors = r.findings.filter(f => f.severity === 'error'); + const warnings = r.findings.filter(f => f.severity === 'warning'); + const reason = r.findings.map(f => `${f.rule}: ${f.message}`).join('; '); + if (r.verdict === 'routed') rows.push(item(r.file, 'not_applicable', 'Routed to another dialect; not validated as a K9 contract.')); + else if (errors.length || r.verdict === 'fail' || r.verdict === 'error') rows.push(item(r.file, 'fail', reason, ledger.has(r.file) && !changed.has(r.file))); + else if (unavailable.length) rows.push(item(r.file, 'indeterminate', reason)); + else if (ledger.has(r.file)) rows.push(item(r.file, 'fail', 'Stale debt exemption: this file now conforms.')); + else rows.push(item(r.file, 'pass', warnings.length ? reason : '', warnings.length > 0)); + } + for (const f of expected) if (!seen.has(f)) rows.push(item(f, 'indeterminate', 'Validator did not return a result for this file.')); + if (!expected.length) rows.push(item('live corpus', 'not_applicable', 'No tracked live K9 contracts or routing files.')); + return rows; +} +function walk(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]); +} +function execute(root, command, args, env = {}) { + const r = spawnSync(command, args, { cwd: root, env: { ...process.env, ...env }, encoding: 'utf8', timeout: 180000, maxBuffer: 32 * 1024 * 1024 }); + return { code: r.error || r.signal ? null : r.status, output: clean((r.stdout ?? '') + (r.stderr ?? '') + (r.error ? `\n${r.error.message}` : '')) }; +} +export function collect(root, directory, env = process.env) { + fs.mkdirSync(directory, { recursive: true }); + const run = (name, cmd, args, extra = {}) => { + const r = execute(root, cmd, args, extra); + fs.writeFileSync(path.join(directory, `${name}.log`), r.output); + return r; + }; + const categories = []; + const add = (id, items) => categories.push({ id, title: titles[id], items, counts: counts(items), state: state(items) }); + const tracked = execute(root, 'git', ['ls-files', '-z']); + if (tracked.code !== 0) throw Error('Cannot enumerate the tracked corpus.'); + const files = tracked.output.split('\0').filter(Boolean); + const deeds = files.filter(f => !f.startsWith('9-archive/') && !f.includes('/fixtures/') && f.endsWith('.deed')); + if (!deeds.some(f => /^[^/]+_chora\.deed$/.test(f))) deeds.push('__missing_repo_deed__'); + const runtime = env.BUN_BIN || 'bun'; + const deedRows = []; + for (const f of deeds) { + if (f === '__missing_repo_deed__') { deedRows.push(item('repo deed', 'indeterminate', 'No tracked root _chora.deed found.')); continue; } + const r = run(`deed-${deedRows.length}`, runtime, ['1-formats/deed/tools/deed_lint.js', f]); + const ok = r.code === 0 && r.output.includes(`OK ${f}`); + deedRows.push(item(f, ok ? 'pass' : r.code === 1 && r.output.includes(`FAIL ${f}`) ? 'fail' : 'indeterminate', ok ? '' : r.output.trim() || 'No DEED validator evidence.')); + } + add('deed', deedRows); + const contract = '1-formats/k9/spec/contract/k9_contract.ncl'; + const nr = run('typecheck', 'nickel', ['typecheck', contract]); + add('typecheck', [item(contract, nr.code === 0 ? 'pass' : nr.code === null ? 'indeterminate' : 'fail', nr.code === 0 ? '' : nr.output.trim())]); + const validator = '1-formats/k9/tools/k9-validate.sh'; + const sr = run('selftest', 'bash', [validator, '--self-test']); + let self = checkedAssertions(sr.output, sr.code); + if (sr.code === 0 && !sr.output.includes('self-test: all assertions passed')) self.push(item('completion', 'indeterminate', 'Missing self-test completion evidence.')); + add('selftest', self); + const fixtures = walk(path.join(root, '1-formats/k9/tools/fixtures')).filter(f => /\.(k9|ncl)$/.test(f)); + const fr = run('fixtures', 'bash', [validator, '--strict', '--fixtures', '1-formats/k9/tools/fixtures']); + let controls = checkedAssertions(fr.output, fr.code, fixtures.length); + const names = fixtures.map(f => path.basename(f)); + for (const name of names) if (!controls.some(r => r.id === name || r.id.startsWith(name + ' '))) controls.push(item(name, 'indeterminate', 'Expected control has no result.')); + if (new Set(names).size !== names.length) controls.push(item('fixture identity', 'indeterminate', 'Duplicate fixture basenames cannot be attributed uniquely.')); + if (state(categories.find(c => c.id === 'typecheck').items) !== 'green') { + controls = controls.map(i => i.status === 'pass' ? item(i.id, 'indeterminate', 'Normative contract did not typecheck; fixture verdict is not established.') : i); + } + add('fixtures', controls); + const expected = files.filter(f => /\.(k9|k9\.ncl)$/.test(f) && !f.startsWith('9-archive/') && !f.startsWith('1-formats/k9/tools/fixtures/')); + const ledgerFile = path.join(root, '.machine_readable/k9-contract-debt.txt'); + const ledger = new Set(fs.existsSync(ledgerFile) ? read(ledgerFile).split('\n').filter(l => l && !/^\s*#/.test(l)) : []); + let changed = new Set(), baseResolved = true; + if (env.K9_BASE_SHA) { + const diff = execute(root, 'git', ['diff', '--name-only', '-z', `${env.K9_BASE_SHA}...${env.K9_HEAD_SHA || 'HEAD'}`]); + baseResolved = diff.code === 0; + changed = new Set(diff.output.split('\0').filter(Boolean)); + } + const jsonFile = path.join(directory, 'corpus.jsonl'); + const cr = run('corpus', 'bash', ['.githooks/validate-k9.sh'], { K9_VALIDATE_LAYER: 'L2', K9_JSON_REPORT: jsonFile, K9_CHANGED_FILES: [...changed].join('\n') }); + const corpus = corpusItems(fs.existsSync(jsonFile) ? read(jsonFile) : '', expected, ledger, changed); + if (!baseResolved) corpus.push(item('change scope', 'indeterminate', 'Cannot resolve the PR base/head range; debt exemptions cannot be determined.')); + if (cr.code !== 0 && !corpus.some(i => i.status === 'indeterminate' || i.status === 'fail' && !i.advisory)) corpus.push(item('corpus gate', 'indeterminate', `Gate exited ${cr.code}; inspect corpus.log.`)); + add('corpus', corpus); + const profileFiles = [contract, validator, '.githooks/validate-k9.sh', '1-formats/deed/tools/deed_lint.js', '1-formats/k9/tools/conformance-report.mjs']; + const profile = crypto.createHash('sha256').update(profileFiles.map(f => f + '\0' + read(path.join(root, f))).join('\0') + JSON.stringify({ fixtures: names.sort(), expected: expected.sort(), deeds: deeds.sort() })).digest('hex'); + const all = categories.flatMap(c => c.items); + return { schema: VERSION, profile, generated: new Date().toISOString(), sha: env.K9_HEAD_SHA || execute(root, 'git', ['rev-parse', 'HEAD']).output.trim(), run: env.GITHUB_RUN_ID || 'local', pr: env.PR_NUMBER || 'local', url: env.GITHUB_RUN_ID ? `https://github.com/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : '', categories, state: state(all), counts: counts(all) }; +} +const escape = s => String(s).replace(/[&<>|`]/g, c => ({ '&': '&', '<': '<', '>': '>', '|': '|', '`': '`' }[c])).replace(/\r?\n/g, ' '); +export function delta(current, previous, category) { + if (!previous || previous.schema !== VERSION || previous.profile !== current.profile || previous.sha === current.sha || previous.pr !== current.pr) return '— no comparable baseline'; + const before = previous.categories.find(c => c.id === category.id); + if (!before || before.counts.indeterminate || category.counts.indeterminate) return '— indeterminate'; + const a = before.counts.failed, b = category.counts.failed; + return b < a ? `↑ improved (${a - b} fewer failures)` : b > a ? `↓ regressed (${b - a} more failures)` : '↔ unchanged'; +} +export function render(report, previous = null) { + const labels = { red: '🔴 failing', orange: '🟠 indeterminate', yellow: '🟡 advisory', green: '🟢 conformant', grey: '⚪ not applicable' }; + const colour = { red: 'red', orange: 'orange', yellow: 'yellow', green: 'brightgreen', grey: 'lightgrey' }; + const badge = `https://img.shields.io/badge/DEED%E2%80%93K9-${encodeURIComponent(report.state)}-${colour[report.state]}`; + let out = `${marker}\n![DEED–K9 ${report.state}](${badge})\n\n${labels[report.state]} · commit \`${escape(report.sha.slice(0, 12))}\` · ${report.url ? `[run ${escape(report.run)}](${report.url})` : 'local run'}\n\n`; + out += '| Category | Status | Pass | Fail | Indeterminate | N/A | Since previous revision |\n|---|---|---:|---:|---:|---:|---|\n'; + for (const c of report.categories) out += `| ${c.title} | ${labels[c.state]} | ${c.counts.passed} | ${c.counts.failed}${c.counts.advisory ? ` (${c.counts.advisory} advisory)` : ''} | ${c.counts.indeterminate} | ${c.counts.not_applicable} | ${delta(report, previous, c)} |\n`; + for (const c of report.categories) { + const findings = c.items.filter(i => i.status !== 'pass' || i.advisory); + if (!findings.length) continue; + out += `\n
\n${c.title}: ${findings.length} finding(s) / scope notes\n\n`; + for (const i of findings.slice(0, 20)) out += `- **${escape(i.id)}** — ${escape(i.status)}${i.advisory ? ' (advisory)' : ''}: ${escape(i.reason).slice(0, 1200)}\n`; + if (findings.length > 20) out += `- ${findings.length - 20} more; see the structured report and logs.\n`; + out += '\n
\n'; + } + out += '\nScope: DEED syntax and K9 contracts L0–L2. L3 signature verification and routed coordination dialects are not certified. Green is limited to this scope. Orange is missing evidence; grey is an explicit scope exclusion. Logs and report.json are in the dated conformance-evidence artifact on the linked run.\n'; + const baseline = { schema: report.schema, profile: report.profile, pr: report.pr, sha: report.sha, categories: report.categories.map(c => ({ id: c.id, counts: c.counts })) }; + out += `\n\n`; + return out; +} +export function baseline(body) { + try { + const m = //.exec(body); + if (!m || m[1].length > 32000) return null; + const p = JSON.parse(Buffer.from(m[1], 'base64').toString()); + if (!Array.isArray(p.categories) || !p.categories.every(c => c.counts && ['passed','failed','indeterminate','not_applicable','advisory'].every(k => Number.isSafeInteger(c.counts[k]) && c.counts[k] >= 0))) return null; + return p; + } catch { return null; } +} +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const root = process.cwd(); + const safe = x => String(x || 'local').replace(/[^a-zA-Z0-9_-]/g, '_'); + const date = new Date().toISOString().slice(0, 10); + const relative = `tests/conformance/${date}/pr-${safe(process.env.PR_NUMBER)}/run-${safe(process.env.GITHUB_RUN_ID)}-${safe(process.env.GITHUB_RUN_ATTEMPT || '1')}`; + const directory = path.resolve(root, relative); + let report; + try { report = collect(root, directory); } + catch (e) { const items = [item('report collection', 'indeterminate', e.message)]; report = { schema: VERSION, profile: '', sha: process.env.K9_HEAD_SHA || '', pr: process.env.PR_NUMBER || 'local', run: process.env.GITHUB_RUN_ID || 'local', url: '', categories: [{ id: 'corpus', title: 'Collection', items, counts: counts(items), state: 'orange' }], state: 'orange', counts: counts(items) }; } + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(path.join(directory, 'report.json'), JSON.stringify(report, null, 2) + '\n'); + const previous = process.env.K9_PREVIOUS_REPORT && fs.existsSync(process.env.K9_PREVIOUS_REPORT) ? baseline(read(process.env.K9_PREVIOUS_REPORT)) : null; + const markdown = render(report, previous); + fs.writeFileSync(path.join(directory, 'report.md'), markdown); + if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown); + if (process.env.GITHUB_ENV) fs.appendFileSync(process.env.GITHUB_ENV, `K9_REPORT_DIR=${relative}\n`); + console.log(markdown); + process.exitCode = report.state === 'red' ? 1 : report.state === 'orange' ? 2 : 0; +} diff --git a/1-formats/k9/tools/conformance-report.test.mjs b/1-formats/k9/tools/conformance-report.test.mjs new file mode 100644 index 000000000..79d6034ee --- /dev/null +++ b/1-formats/k9/tools/conformance-report.test.mjs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +// hypatia: allow cicd_rules/javascript_detected -- Bun reporting boundary regression tests. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { counts, state, checkedAssertions, corpusItems, render, delta, baseline } from './conformance-report.mjs'; +const row = (status, advisory = false) => ({ id: 'x', status, advisory, reason: '' }); +const json = (file, verdict = 'pass', findings = []) => JSON.stringify({ file, verdict, findings }); +test('known blocking failure remains red when other evidence is missing', () => { + assert.equal(state([row('fail'), row('indeterminate')]), 'red'); + assert.equal(state([row('fail', true)]), 'yellow'); + assert.equal(state([row('fail', true), row('indeterminate')]), 'orange'); + assert.equal(state([row('not_applicable')]), 'grey'); + assert.equal(state([row('pass'), row('not_applicable')]), 'green'); + assert.equal(state([]), 'orange'); +}); +test('empty, partial and interrupted assertion streams never become a pass', () => { + assert.equal(state(checkedAssertions('', 0)), 'orange'); + assert.equal(state(checkedAssertions('ok one', 0, 2)), 'orange'); + assert.equal(state(checkedAssertions('ok one', 1)), 'orange'); + assert.equal(state(checkedAssertions('FAIL one', 0)), 'red'); + assert.equal(state(checkedAssertions('ok one\nSKIP two', 0)), 'orange'); +}); +test('missing or malformed per-file evidence is indeterminate', () => { + assert.equal(state(corpusItems('', ['a'], new Set(), new Set())), 'orange'); + assert.equal(state(corpusItems('not json', ['a'], new Set(), new Set())), 'orange'); + assert.equal(state(corpusItems(json('a') + '\n' + json('a'), ['a'], new Set(), new Set())), 'orange'); + assert.equal(state(corpusItems(json('b'), ['a'], new Set(), new Set())), 'orange'); +}); +test('missing semantic verifier is not a conformance pass', () => { + const result = corpusItems(json('a', 'pass', [{ severity: 'skipped', rule: 'K9-N000', message: 'no nickel' }]), ['a'], new Set(), new Set()); + assert.equal(state(result), 'orange'); + assert.equal(counts(result).indeterminate, 1); +}); +test('debt is advisory only while untouched, and conforming exemptions fail', () => { + const data = json('a', 'fail', [{ severity: 'error', rule: 'K9-E001', message: 'invalid' }]); + assert.equal(state(corpusItems(data, ['a'], new Set(['a']), new Set())), 'yellow'); + assert.equal(state(corpusItems(data, ['a'], new Set(['a']), new Set(['a']))), 'red'); + assert.equal(state(corpusItems(json('a'), ['a'], new Set(['a']), new Set())), 'red'); +}); +test('routed dialects are excluded rather than counted as validated contracts', () => { + const items = corpusItems(json('coordination.k9', 'routed'), ['coordination.k9'], new Set(), new Set()); + assert.equal(counts(items).passed, 0); + assert.equal(counts(items).not_applicable, 1); +}); +test('comparisons require the same scope/profile and a different revision', () => { + const c = { id: 'corpus', counts: counts([row('pass')]) }; + const current = { schema: 1, profile: 'p', pr: '12', sha: 'new', categories: [c] }; + const previous = { ...current, sha: 'old', categories: [{ ...c, counts: counts([row('fail')]) }] }; + assert.match(delta(current, previous, c), /improved/); + assert.match(delta(current, { ...previous, profile: 'other' }, c), /no comparable/); + assert.match(delta(current, current, c), /no comparable/); + assert.match(delta(current, { ...previous, categories: [{ ...c, counts: counts([row('indeterminate')]) }] }, c), /indeterminate/); +}); +test('report escapes untrusted evidence and preserves a small valid baseline', () => { + const items = [{ id: '', status: 'indeterminate', reason: 'bad | value\nnext' }]; + const r = { schema: 1, state: 'orange', sha: 'abc', profile: 'p', pr: '1', run: 'local', url: '', categories: [{ id: 'corpus', title: 'Corpus', items, counts: counts(items), state: 'orange' }] }; + const body = render(r); + assert.ok(!body.includes('