# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Versioning note: the repository tag versions **the contract** (file set, key names, case ids, disposition semantics). Each JSON file additionally carries its own `"version"` field, bumped when that file changes. ## [0.9.0] — 2026-08-13 **A normative rule stated its own premise and then applied itself beyond it.** `spec/conformance-corpus.md` §7 justified the fixture-is-ground-truth ordering with *"**Two implementations** that return different verdicts…"* and then stated the rule with no scope at all. For `signatures/active-content.json` there is no second implementation — the seed runtime authored both the payloads and the table — and that runtime has stated that the classification behind it is calibration it does not freeze. §7 as written made a reserved change on their side into a bug on their side. **Breaking in category, minor in number.** This changes disposition semantics, which the versioning note at the top of this file counts as contract. The repository is in 0.x, where a breaking change is a minor bump by the rules — the same reading `[0.3.0]` recorded: *read the entry, not the version number*. ### Changed - **`spec/conformance-corpus.md` — new §7.1, *Where the second paragraph does not hold*.** The scope is keyed on a **structural property**, never on a table name: a case whose scope is a table only one runtime implements, whose payload that runtime authored. A rule naming `active-content` would rot the day a second runtime implements it. §7's own second paragraph already carried the premise; §7.1 makes it explicit and states the disposition for the case the premise excludes — the fixture is not rewritten on the divergence alone, the divergence is recorded against the version pinned, and re-pinning is a separate release. That is the disposition §5 already applies to a stale `observed_out_of_scope` entry, extended to the one place where it can reach a verdict. **It creates no fourth verdict, and that constraint shaped the wording.** `schema/conformance-declaration.schema.json` closes `result` with `additionalProperties: false` over four counts plus two arithmetic invariants; a fifth verdict would have broken every consumer's parser, which is a worse break than the one intended. A case whose expected findings are not produced still **fails** and is still named in `failed_cases`. What §7.1 changes is what the failure licenses concluding, not what is reported. Two limits stated in the section rather than left to be inferred: it does **not** reach a third-party implementer of the same table — against them the fixture is the contract, exactly as §7 says, and that is the only thing these cases can prove while one runtime is all there is — and it is **not** a licence for a runtime to self-declare, since the exemption is carried by the corpus's provenance record for the scope and not asserted per case by whoever failed. Superseded text is named rather than edited away, following §6's own pattern: *"Through corpus version 0.8.1 this section stated the rule above with no scope at all."* **The competing reading was tested and disposed of**, because it is the one that would have avoided this release: that §7's existing hatch (*"unless the fixture itself is proven wrong"*) already covered it. It does not. The hatch's consequence is that **the fixture changes**, and the manifest field asserts the opposite — pinned, not rewritten, re-pinning a separate decision. And a runtime recalibrating does not prove the earlier classification wrong: the fixture measured `de09711` / `0.4.0` correctly, and a later release does not reach back and falsify an earlier measurement. The case fits neither of §7's two dispositions, which is the defect. - **`conformance/manifest.json` `0.6.1` → `0.6.2` — `active_content_provenance.pins_a_version_not_a_frozen_classification` no longer records an open question.** The retirement is **partial and it is quoted, not dropped**, per the house style this field established one release ago (*"a correction that does not say what it corrects cannot be audited"*). What falls is only the open-question status; the clause *"section 7 … is NOT amended by this block"* **stays true and is kept**, because §7 was amended by its own release and not by a data file. Value change only — read back from disk against `HEAD` with a flattened key diff: `added: 0, removed: 0, changed: 2` (the field and `version`), and the new string printed and read rather than inferred from the count, since a value edit reports `changed: 1` whatever it wrote. Six prose dashes in the new text were written `--` and promoted to `—` before commit: `--` is the variant-suffix separator token of §6's case-id grammar, and every other occurrence of it in this file is that token, a real case id, or a CLI flag. ### Neighbours — measured, and the ones left alone are named A sweep for the retired premise was run over the whole repository, widened past *"ground truth"* to the second paragraph's own wording (*"one of them has a bug"*, *"two implementations"*), since a restatement in that phrasing would have survived the first search. - **`CONVENTIONS.md` — changed.** Carried the rule unscoped and called the proven-wrong hatch *"the one way that reverses"*. There are now two, and both are listed. - **`CLAUDE.md` — changed.** The Norwegian restatement that governs sessions in this repository carried the same unscoped rule; left alone, the next session here would have acted on it. - **`SECURITY.md` §2 — minimal cross-reference only.** Its claim is about a fixture that expects **too little**, and §7.1 narrows *who the rule reaches*, not that direction. The conclusion survives intact, so it was not rewritten. - **`SECURITY.md` "Why a confirmed defect is usually not fixed here first" — untouched.** Its *"two implementations answering differently"* is about extracted **data** diverging from its source, not about fixtures. - **`README.md` — untouched.** Its conformance row says *"Ground truth"* as a descriptor and does not restate the disagreement rule, and it already names the asymmetry it would otherwise hide: the seven active-content cases are *"measured against the one runtime that implements that table"*. Nothing there became false. - **`docs/extraction-plan.md` — untouched, and it is supporting evidence rather than a stale neighbour.** It already records that the calibration file *"inverts this repository's central rule"* — so this is the second place the unscoped rule was known not to hold, and the first was documented before this release. ### Not in this release Whether the seven active-content cases still pass at the seed runtime's `v1.1.0` is **unmeasured**, and §7.1 is silent on it. No case was minted, no data file touched, no id string proposed. ## [0.8.1] — 2026-08-13 **The field 0.8.0 added to make the exposure precise stated it with a hand-derived count, and the count was wrong.** Caught in the same session, before any consumer read it, and corrected inside the field rather than by rewriting it. No measurement changed and no verdict moved. ### Fixed - **`conformance/manifest.json` 0.6.0 → 0.6.1 — `active_content_provenance.pins_a_version_not_a_frozen_classification` now enumerates instead of totalling.** As published it read *"three of the four dimensions they name as calibration cannot move one of these cases at all. The fourth can: which `active:` ids a payload yields IS the classification"*. Two defects in one sentence. First, the total was derived by hand over a taxonomy the field had itself recategorized: the seed runtime's four calibration dimensions are severities, thresholds, **lexicon entries** and dispositions, and lexicon entries are *not* absent from these fixtures — `active__data-uri` carries `data-uri:executable` and `active__raw-html` carries `hybrid-xss:script-tag` in `observed_out_of_scope`, both verified as members of `lexicon/injection-lexicon.json` and non-members of `signatures/active-content.json`. Second, *"the fourth"* silently substituted the classification for lexicon entries as the fourth item of their sentence, which it is not — the classification is what they addressed separately. - The replacement names three things and totals none of them: severities/thresholds/dispositions are absent and move no verdict; lexicon entries move no verdict either — spec section 5 forbids failing a runtime over `observed_out_of_scope` — but a lexicon calibration change **ages** those two entries as evidence, which is the exposure `active_content_measurement_0_7_0.movement_sweep.residue_is_the_field_no_test_protects` already names as a class, and this corpus pins a stale residue entry rather than rewriting it; and the active-content classification is the one thing that can move a verdict. The retired sentence is **quoted** in the field's `AMENDED IN 0.6.1` clause, not merely dropped, for the same reason `scope_planned.$comment` quotes what it retired: a correction that does not say what it corrects cannot be audited. ## [0.8.0] — 2026-08-13 **The seven active-content fixtures pin a VERSION of the seed runtime, and nothing said so.** That runtime tagged `v1.0.0` on 2026-08-13 and stated that the freeze covers its exported Python surface only, excluding detection behaviour: severities, thresholds, lexicon entries and dispositions are calibration there and move in minor and patch releases. The manifest already pinned commit and version per measurement block, but nowhere recorded that the thing pinned is a version rather than a frozen classification. No case is minted, no data file is touched, no id is proposed. ### Added - **`conformance/manifest.json` 0.5.2 → 0.6.0 — `active_content_provenance.pins_a_version_not_a_frozen_classification`.** One field, scoping the neighbouring `asymmetry` rather than replacing it, and deliberately narrower than the runtime's own statement. The exposure is bounded by what the fixtures assert, which was read from all seven rather than assumed: every finding carries `pattern_id` and nothing else — no severity, no threshold, no disposition — so three of the four dimensions that runtime names as calibration cannot move one of these cases at all. The fourth can, because which `active:` ids a payload yields *is* the classification. The field names both pins (six at 0.4.0 / `de09711`, the seventh at 0.7.0 / `be9759b`) rather than one, since a single version would flatten two measurements into one header — the defect `superseded_for_one_case` exists to prevent. Their v1.0.0 statement is **attributed** to their coord message of 2026-08-13T20:40:31Z, not restated as a fact measured from this side. - The field also names the disposition of a future divergence, so it is not left to be inferred: a later 1.x that classifies one of these payloads differently is not a breach by them and does not make the fixture wrong. The fixture stays ground truth at its pinned version, the divergence is measured and recorded, and re-pinning is a separate decision — the same disposition this corpus already applies to a stale `observed_out_of_scope` entry. ### Fixed - **`docs/secret-egress-divergence.md:75-76` carried the same misquote `conformance/manifest.json` had corrected in 0.7.3**, named there as a deliberate omission and closed here. The field's value ends ``ascending `order` `` — the backticks are the field's own. The fix is *not* the one the omission note implied: those two lines are a single code span delimited by **single** backticks across a line break, so inserting the field's backticks inside it would have terminated the span at the first one and rendered the quote broken. The outer delimiter is promoted to double backticks instead, which is what lets the inner singles survive. The manifest's correction ported as a literal string because JSON has no backtick semantics; markdown does. Verified by extracting the span from the file on disk, unfolding the line break, and comparing to the decoded value in `signatures/secret-egress.json` — equal — and by confirming no backtick run of length ≥ 2 sits inside the span. ### Not done, and named rather than left silent - **`spec/conformance-corpus.md` section 7 is untouched.** It states the disagreement rule without scope: *"The fixture is ground truth. A runtime that disagrees is wrong."* Read against the active-content scope, whose only implementing runtime has now said in writing that its classification may legitimately move, that rule would call a calibration change there a bug. The manifest field records the interaction and explicitly does not amend the spec. Whether the normative rule needs a scope is a decision for its own release. ## [0.7.3] — 2026-08-13 **The README still argued the premise 0.7.2 retired, and the two files sat on a public remote disagreeing.** `README.md` opened the egress gap with "It is not an id question at all"; the blocker it sends the reader to for authority now opens reason (1) with "NO ID SPACE ON THE COMMONS SIDE. This is the hard blocker." Before 0.7.2 the README was merely out of date. After it, our own commit had made it contradictory — the same defect class 0.7.1 existed to close. No data moves, no case is minted, no id is proposed. ### Fixed - **The README now carries the three measured reasons instead of the retired one.** (1) No id space on the commons side — the hard blocker, and the only one an answer can resolve; the answer belongs to the runtimes that own the seeds. (2) Match semantics disagree, and an id space would not close it. (3) Membership diverges in both directions and the divergence is inherited: the two sides hold 19 entries and 25, and they are ports of two *different* source tables in one source repository. The counts survived the falsification; only the causal claim fell, so `different tables` is kept and "cut at different granularities" is gone. The paragraph deliberately does **not** restate the outgoing question's status: that is true on the day it is written, nothing tests README prose, and `conformance/manifest.json` already carries the date. The standing `entry_points_by_scope` requirement is likewise left out rather than printed as a fourth reason. - **`conformance/manifest.json` 0.5.1 → 0.5.2: the blocker misquoted the contract it cites.** It rendered the field as `match_semantics: "… evaluated in ascending order"`; the value in `signatures/secret-egress.json` ends ``ascending `order` `` — the backticks are the field's own. A blocker that misquotes the semantics it is blocking on invites a consumer to implement the wrong one. The data file is unchanged and was never wrong — only the quotation of it was, which `scope_planned.$comment` now records. Verified by reading the edited file back from disk and matching the decoded string against the data file that owns it; `json.tool` passes on wrong escaping. Known and deliberately left: `docs/secret-egress-divergence.md` renders the same value without its backticks. That document is `Status: informative` and was outside this release's scope. ## [0.7.2] — 2026-08-13 **`scope_planned.blockers` named the premise that `docs/secret-egress-divergence.md` falsified.** The blocker read "19 entries … 25 at different cut points" — one table cut at two granularities, waiting on a reconciliation of two ports. Measured 2026-08-13: they are ports of **two different source tables** in the same source repository, so no reconciliation of the ports was ever going to close it. No data moves in this release, and no case is minted — only the recorded reason a case cannot be. ### Fixed - **The egress blocker now carries the three measured reasons, kept independent.** (1) Commons has no id space for this table: seed A (`hooks/scripts/pre-edit-secrets.mjs`) carries a name and a pattern per entry and nothing else, so entries are keyed by human-readable `name` while the guard emits `egress:`, and a fixture names labels. This is the only one of the three an answer can resolve, and it is the outgoing question. (2) Match semantics disagree: `first match wins` with `ordering.normative: true` here, against `finditer` over all 25 patterns there — one witness, an `Authorization` header holding a three-part JWT, produces **one** label under commons' declared contract and **two** from the guard. (3) Membership diverges both ways and is inherited from two different seeds (seed A 19 entries, seed B 33, the guard ported 25, 8 unported), so re-measuring either port cannot close it. The blocker points to `docs/secret-egress-divergence.md` for the method behind every number. - **Two hand-carried numbers in the retired text are corrected in the same string.** `aws-access-key-id` was called "the one clean one-to-one": measured, only **2 of 19** commons patterns are byte-identical to a guard pattern after unescaping, and AWS is not among them — the guard anchors the same run as `\bAKIA[0-9A-Z]{16}\b`. `GitHub Token` was called "four ids there": measured on witnesses it maps to **three**, and leaves `ghu_` and `ghr_` covered by no guard id. Both were transcription, not measurement. What is retracted is quoted in place; the full retired text stands in git at `conformance/manifest.json` 0.5.0. - **`scope_planned.$comment` said "a distinct unresolved question" — singular.** Left alone it would tell a reader the case becomes mintable when an answer arrives, which is true of one reason in three. Amended alongside the blocker rather than after it, since the two are read together. ### Measured - **The standing requirement was measured here, not transcribed from the document.** `entry_points_by_scope.scopes` carries **no entry at all** for `signatures/secret-egress.json` — the three declared scopes are the lexicon, active-content and carriers. Entry point, findings accessor and fixture presentation must be filled for both runtimes before a first egress case, independently of the three reasons. It is recorded as a requirement, not as a fourth reason: it would stand even if all three were resolved tomorrow. - **No id string is proposed, in this file or anywhere else.** Checked against the two outgoing coord messages of 2026-08-13 rather than assumed: both state in as many words that no id is being proposed. Naming an id in a shared space is the exception `carrier:*` established, it requires both runtimes asked first, and both are unanswered. `conformance/manifest.json` 0.5.0 → 0.5.1. No case directory, no `expected.json` and no signature table changed; `git status` shows one file besides this changelog. ## [0.7.1] — 2026-08-13 Two loose ends from `0.7.0`, neither of which changes a contract. ### Fixed - **The README still named the manifest as the authority for the variant rule.** It read "see `case_id_derivation.variant_suffix` in the manifest" — true until `0.7.0`, when the rule became normative in `spec/conformance-corpus.md` §6 and the manifest's block became the *measurement* behind it rather than the contract. Left alone it would have reproduced in one line the same defect `0.7.0` closed: a reader sent to the wrong authority. ### Measured - **The `__` half of the derivation was re-measured too, not just the `--` half.** `0.7.0` made a point of re-measuring `--` rather than copying the manifest's `0.3.0` numbers forward, while the adjacent sentence asserting that `__` "does not occur anywhere in the ratified id space" was inherited untested — a claim about this release's own soundness that the release did not check. Measured now across all five published id spaces: the 83 lexicon ids, the 7 `active:` ids, the 3 `carrier:` ids, the 7 malware rule ids and the 19 secret-egress entry names carry **neither** `__` nor `--`. The one-to-one transform holds. No text changed; the sentence was true. It is now true *and* measured. ## [0.7.0] — 2026-08-13 **The normative spec forbade, in as many words, a case the corpus has shipped since `0.3.0`.** `spec/conformance-corpus.md` §6 read *"Such a payload MUST NOT be given a discriminated case id; the derivation rule is the contract, and a suffix would break the reverse transform"* while `conformance/manifest.json` defined `case_id_derivation. variant_suffix` and `conformance/hybrid-xss__script-tag--src-no-close/` sat on disk under it. The manifest was the correct party; the spec was simply never updated when the derivation was extended. **No data moves in this release — only the normative text that describes it.** ### Fixed - **§6's derivation block now states the rule the corpus actually uses.** ``` before case_id = pattern_id with ":" replaced by "__" pattern_id = case_id with "__" replaced by ":" after case_id = pattern_id with ":" replaced by "__", optionally followed by "--" and a variant slug of [a-z0-9-] pattern_id = case_id truncated at the first "--" if present, then "__" replaced by ":" ``` The `--`-absence measurement moves into the spec as the reason the reverse transform stays **lexical**: a runtime MUST be able to recover a `pattern_id` by splitting the string, and MUST NOT need a lookup against the published id list to find where the id ends and the variant begins. Re-measured at this commit rather than copied from the manifest's 0.3.0 numbers: `--` occurs in none of the 83 lexicon ids, none of the three `carrier:` ids, none of the `active:` construct ids, none of the seven malware rule ids and none of the 19 secret-egress entry names — and in exactly one of the 94 case ids, the variant itself. It does occur inside *pattern* values (`