# 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](ADR-0098-whether-the-gate-says-how-old-a-committed-junit-report-is-and-what-it-may-read-to-say-it.md) (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 ``/`` into the report, and orders `` 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. `--freeze` refuses a cited JUnit report that is not green at its root** (added 2026-10-23). A report cited by a `test:` 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 `.xml` files.** 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 --freeze` sealed a report holding a `` 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 in `src/lib/evidence.ts` (`junitTestResult`, per cited case). The adopter's guard refuses to seal a report containing `