ADR-0102.
ADR-0102-whether-the-seal-hashes-a-junit-report-by-what-the-gate-reads-of-it-rather-than-its-bytes.md
ADR-0102 — Whether the seal hashes a JUnit report by what the gate reads of it, rather than its bytes
Date: 2026-10-22
Status: accepted (2026-10-23) — the maintainer chose A now with D and E from the adopter's follow-up: a documented normaliser for drift, --freeze refuses to seal a cited JUnit report whose root counts failures or errors, and check warns when a cited report carries failures outside the cases it cites. B waits for a second adopter; C is discarded
Deciders: the maintainer
Method: an adopter's report, measured on a copy of its repository with runward 0.44.1 (64 sealed
files, 4 of them JUnit reports); read against a fresh clone of stranxik/runward at main@97f3f02:
src/lib/evidence.ts (sha256, normalizedFileSha256, collectSealableEvidence), the JUnit detector
and reader the gate uses for test: pointers, ADR-0098. The adopter's measurement is reported, not
re-run here
Relates to: ADR-0098
(the age of a committed JUnit report), RWD-2026-0181
Context
What was reported. An adopter seals its mission with check --freeze in CI. Its test runner (vitest,
like most JUnit writers) puts time, timestamp, hostname and <system-out>/<system-err> into the
report, and orders <testsuite> elements differently from one run to the next. Every CI run rewrites the
report, the seal's hash changes, and the gate reports drift with no code changed. The adopter now runs a
normaliser after each suite (attributes and console output removed, suites sorted by name); two runs then
give the same hash.
What the tree does. sha256() (src/lib/evidence.ts) hashes a sealed file's bytes after one
normalisation: line endings, because a Windows checkout rewrites them. A JUnit report is sealed like any
other file. The gate itself, reading a test: pointer through a report, reads case names, verdicts and
failure messages; it never reads time, timestamp, hostname or console output.
Why it matters. A seal that drifts on every run is re-sealed by habit, which is the failure the seal
exists to prevent (docs/operator-role.md, "treat a lock diff in review as a decision, not as noise").
Decision
Decided (2026-10-23). A, D and E. A: the documentation names the attributes to strip before
sealing (time, timestamp, hostname, console output) and the suite order to fix; runward ships no
normaliser yet. D (added below from the same adopter's follow-up): check --freeze refuses to seal a
JUnit report cited by a test: pointer whose root counts failures or errors, whatever the cited cases
say: a seal certifies a crossing, and a report that says tests failed is not one, which is the reason
the seal already refuses a red gate. E: check adds a line when a cited report carries failures
outside the cases it cites ("the report is not green, even though the cited cases are"), advisory,
outside the verdict. B (hashing the projection the gate reads) waits for a second unrelated adopter
meeting the drift: it changes what "sealed" means for one format. C is discarded: it loses the
guarantee that the report read is the report sealed. RWD-2026-0181 stays not-fixed until D and E ship
in a pull request of their own.
The options as proposed on 2026-10-22:
- A. Bytes, as today, and a documented normaliser. The operator removes the volatile parts before sealing; runward documents the attributes to drop and ships nothing.
- B. Hash the projection the gate reads. For a file the JUnit detector recognises, the seal hashes a canonical projection: suites and cases sorted by name, each case's verdict and failure message; every other attribute and console output left out. Same idea as the line-ending normalisation, applied to the format the gate already parses. The witness and the evidence lock would record which hash kind a file carries.
- D.
--freezerefuses a cited JUnit report that is not green at its root (added 2026-10-23). A report cited by atest:pointer whose root counts failures or errors is refused for sealing, even when every cited case is green. - E. An advisory line when a cited report carries failures outside the cited cases (added 2026-10-23). Outside the verdict, on every run.
- C. Do not seal JUnit reports. Seal the test source the pointer names and let ADR-0098 decide what the gate says about a report's age. Loses the guarantee that the report read is the report sealed.
Alternatives discarded
- Ignoring drift on
.xmlfiles. A seal that stops looking is not a seal.
Consequences
- B changes what "sealed" means for one format: a report whose timings changed but whose cases did not stays sealed. That is the gate's own reading, but it must be said in the lock and the witness specification, and existing locks need a migration or a dual read.
- A costs nothing in the tool and leaves every adopter to rediscover the normaliser.
What would settle it
- For A: adopters who seal JUnit reports already normalise them, and a documentation page is enough.
- For B: a second adopter, unrelated, meeting the same drift; a canonical projection specified once and shared by the seal and the witness.
- For C: ADR-0098 settled in a way that makes the report's content irrelevant to the seal.
Reevaluation trigger (mandatory, dated)
Reopen when ADR-0098 is decided, when a second unrelated adopter reports seal drift on a test report, or on 2027-01-22, whichever comes first.
Trigger set on: 2026-10-22 · Watched via: issues mentioning the seal, --freeze, JUnit or drift.
Notes
- 2026-10-23, the same adopter's follow-up. The gate judges a JUnit report by the cases it cites
(ADR-0056), so
check --strict --freezesealed a report holding a<failure>on a case no pointer cited: a flaky test in a 277-file suite. The next CI run regenerated the report without that failure, the seal drifted, and the gate reddened for a defect unrelated to the change. Re-read insrc/lib/evidence.ts(junitTestResult, per cited case). The adopter's guard refuses to seal a report containing<failureor<error. Options D and E above come from that report.
References
src/lib/evidence.ts(sha256, line-ending normalisation), the JUnit reader oftest:pointers.- RWD-2026-0181 in
docs/compliance/known-defects.md; ADR-0098.