# 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.4.3] — 2026-08-11 **No fixture, id or `expected.json` moved — a runtime that passes `0.4.2` passes `0.4.3` unchanged. What changed is a claim this repository made about a runtime it does not own, and the claim was wrong on the day it was written.** ### Fixed - `conformance/manifest.json` `0.3.2` → `0.3.4` — the `scope_planned.blockers` entry for `codepoints/carriers.json` described the guard as emitting two stage-coupled labels per carrier, "the same split for bidi and unicode-tag". The artifact-side label for tags is `lexicon:unicode-tags-present`, emitted from `lexicon.py`, and `output.py` carries a comment saying it deliberately does not repeat it there. **Wrong when written, not stale:** checked at `e671edb` — the commit the sibling secret-egress blocker was measured against — where `coverage.py` already asserted that label, and re-measured at `a59184b`. The correction moves the blocker rather than removing it. The guard's `Finding` carries a `detector` field beside `label`, and the prefix is that field's value: `detector="lexicon"` on `lexicon:unicode-tags-present`, `"output"` on `output:zero-width-present`. **The prefix names the detector, not the pipeline stage** — and for tags a single detector serves both entry points, which is why there is no sixth `output:` label to find. "A commons id would have to be invented stage-neutral" was never the problem. Six labels exist to adopt verbatim, the way the 83 lexicon ids were adopted from this same runtime's port. What blocks adoption is measured and named instead, at llm-security `47905da`: `sanitize:` asserts a strip that runtime does not perform (`scanners/unicode-scanner.mjs` exports one entry point, `scan(targetPath, discovery)`, reporting presence with `scanner: 'UNI'`, a prose title and no id); three of the six name a persist gate it does not have, which the corpus already has a verdict for — §1.1 `not-applicable`, attaching to a declared **table** — but which **publishing the alias is what takes away**: that runtime's suite walks each vendored file for any node carrying `aliases.llm_security` and asserts every registered table is declared, so one aliased carrier id forces `codepoints/carriers.json` into a declared set of what is today the lexicon alone, obliging it to run all six cases and converting the three artifact-side ones into failures; and the entry point pinned for it in `measurement.runtimes` (`scanForInjection`) does not reach carriers at all, so carrier cases need a per-scope entry point this manifest expresses nowhere. Both runtimes already build their carrier sets from `codepoints/carriers.json`, so the divergence is in what a finding is *called* and where it can be *observed*, never in which code points are carriers. ### Asked, not decided - The three objections went to both runtimes over coord on 2026-08-11 as a decision request, each asked the question only it can answer. **Nothing was minted.** A case id is contract surface consumers pin against, and publishing a single carrier alias is itself irreversible — it widens another runtime's declared table set by force of that runtime's own test suite. Minting first would have made a proposal into a fait accompli. The manifest records the request, so a later reader can tell "asked, unanswered" from "nobody asked". - **A correction followed the request the same day, on our own error.** The request asserted that the corpus had no third verdict for a case a runtime cannot reach. It has one — §1.1 `not-applicable` — and this repository wrote that section. The question was put before its own normative spec was re-read; the follow-up says so to both runtimes and restates the choice as mint-input-side-only, accept three standing failures, or publish a guard-only id space with no `llm_security` alias at all. ## [0.4.2] — 2026-08-11 **No data file changed and no pattern moved.** A runtime that passes `0.4.1` passes `0.4.2` unchanged; there is nothing here to re-measure. What the release adds is the rule set an outside contributor could not previously read — including the reason the forge surface is shaped the way it is. ### Added - `CONVENTIONS.md` — the whole rule set a change here is held to, consolidated. **Not new policy:** the charter lives in `CLAUDE.md`, the versioning and vendoring rules in `README.md`, the reporting route in `SECURITY.md`, and the file conventions were visible only in the shape of the files. Collected because a convention that exists only in the maintainer's head is not one an outside reader can meet. Two things in it were previously inferable at best. **Why pull requests are off:** this repository is vendored into independent runtimes that pin a tag, so a change to detection data changes what they *find*, and that has to be coordinated with each consumer **before it exists** — which a merge button cannot do. `org-ops` reached that conclusion on 2026-08-11 from a README line, and the conclusion was right; this file is the ground it was missing. **When a value may change:** the three mechanisms that have moved one so far — re-extraction, retraction, and owner-directed authoring — each named with the `source_fidelity` key that records it, and merit named explicitly as *not* on that list. It also carries the four offline checks that stand in for the CI this organisation does not have. Each was confirmed to go **red** on a violation, not merely green on a clean tree: a JSON file with no `version`, a `spec/` file with no normative marker, and a planted `.sh` were each detected. A check that cannot fail proves nothing. The checks are shell one-liners rather than a script because a script would be `.sh`, and check 4 would fail on the tooling meant to enforce it. - `README.md` — a short **Contributing** section pointing at it, carrying the pull-request answer inline so a reader who never opens the file still gets it. Same pattern the **Reporting a wrong entry** section followed for `SECURITY.md` in `0.3.1`. The four `v0.4.1` references in the install block and the layout table move to `v0.4.2`. This closes the second half of what `org-ops` recorded as missing against the org standard on 2026-08-11. `SECURITY.md` was the first half, in `0.3.1`. ## [0.4.1] — 2026-08-11 **No data file changed and no pattern moved. A number this repository published was wrong, and it was wrong in our favour's opposite direction — the corrected figures are larger.** A runtime that passes `0.4.0` passes `0.4.1` unchanged. ### Fixed - `docs/lexicon-port-divergence.md` (informative) — the ReDoS figures for `hybrid-xss:iframe-src` read **~3× low**, and the Python `script-tag` figure at 32 000 chars read ~4× low. Flagged by `llm-ingestion-pipeline-security` (coord, 2026-08-11T19:51:55Z), who measured the row themselves rather than citing ours. Their diagnosis was measurement surface — their composed `scan_lexicon()` against our standalone regex. **Checked, and that is not the cause:** our standalone 100 000-char figure (7.86 s) sits close to their composed 8.95 s, so the two surfaces differ by far less than the error. Re-measured standalone, Python 3.14.0: `iframe-src` `[^>]*` is 822.7 ms at 32 000 chars and 51 477.4 ms at 256 000, against the published 119.6 ms and 16 857 ms. The Python `script-tag` figure at 256 000 chars *does* reproduce (5.44 s published, 5.22 s measured); the one at 32 000 chars does not (0.021 s against 0.087 s). The error ratios are not constant, so a single mis-sized input does not explain it, and the original harness lived in a previous session's scratchpad and no longer exists. **The cause is recorded as not diagnosable rather than guessed at.** The correction is a box in the document carrying the re-measured table, and the superseded figures are struck in place rather than quietly overwritten — a consumer who cited the old number needs to find out that they did. Nothing about the `0.4.0` decision depends on this. Every corrected figure is larger, the shape is unchanged (quadratic, ×4 per doubling), and both `[^><]*` forms remain flat under both engines. The `0.4.0` entry below still quotes the old `iframe-src` figure; it is left as published, because that section is the record of what was released. ## [0.4.0] — 2026-08-11 **Two detection values changed, by two different mechanisms, and the difference between those mechanisms is the point of the release.** One pattern table was **re-extracted** from a pinned upstream commit, the way every value in this repository has moved until now. Two lexicon rows were **authored here at the source owner's direction**, which has never happened before and required a reason that is not "we measured it and we were right." A runtime that vendors this repository will see findings change. Any consumer asserting byte-identity against `v0.3.0` goes red by construction — `lexicon/injection-lexicon.json` changed pattern text. Ids, labels, aliases, family membership, case ids and every count are unchanged. ### Changed - `lexicon/injection-lexicon.json` `0.7.0` → `0.8.0` — **`hybrid-xss:script-tag` and `hybrid-xss:iframe-src` narrow their unbounded negated class from `[^>]*` to `[^><]*`.** Both forms are quadratic in scan length on input that repeats the tag prefix and never supplies a `>`: each occurrence is a match start and `[^>]*` scans to end of input from each one. Measured in Node v25.8.2 at 16k / 32k / 64k / 128k / 256k chars — script-tag 32.65 / 113.36 / 479.02 / 1988.83 / **7772.25** ms, iframe-src 39.23 / 131.76 / 574.94 / 2469.55 / **9449.94** ms, ×4 per doubling for both. Under `[^><]*` the same inputs cost 0.08–0.66 ms and 0.10–1.00 ms: flat, not merely faster. **Why this is not commons correcting seed data.** The dependency direction inverted. As of `llm-security` `be14867` their four injection tables are built from this file and hold zero local regex literals — measured on their published surface at `47905da`, with their vendored copy of the lexicon confirmed byte-identical to `0.7.0`. So re-extraction was not available as a mechanism: there is no upstream literal left to re-read. They re-measured the finding rather than accepting it, rejected `[^>]{0,256}` because a bound is paddable and `[^>]{1,256}` because it drops bare `` matches the pattern under *both* forms, so it passes either way. Reverting the pattern to its `0.6.0` form fails this case and only this case — mutation-verified in both directions across all 90. **The first case input authored in this repository** rather than reproduced verbatim from a runtime's payload set, recorded in the new `authored_payloads` block rather than folded into `payload_provenance`, whose value is precisely the claim that its inputs are verbatim upstream. That claim stays exactly as strong as it was: 83 of 83. Both witnesses for this axis were named by `llm-security` on 2026-08-10; this is the first of the two. Findings measured through the guard's public API at `0dce50f` / `0.5.0`, with the existing case's committed bytes and digest reproduced by the same harness in the same run as a control. - **`schema/conformance-declaration.schema.json` (`0.1.0`)** — the shape a runtime publishes alongside a conformance result, satisfying the §1.1 MUST that `v0.2.0` created and left without a form. Requested by `llm-security` in those terms (runtime, commit measured, implemented file paths) with the stated reason that two runtimes publishing free-form declarations makes `83/83 + 6 not-applicable` unparseable by anyone but its author. Carries the two arithmetic invariants §1.1 implies but cannot state unambiguously in prose: the four verdict counts MUST sum to the total, and the total MUST equal the corpus case count at the commit measured. Requires the enumeration arrays whenever their counts are non-zero, which turns §1.1's "MUST still be enumerated" from prose into a schema failure. Keeps `error` and `not_applicable` structurally distinct, per §1.1. Records `declaration_source` — whether the declared set is derived from the runner's own constant or hand-maintained beside it — because only the derived form makes the anti-narrowing fence structural. **Deliberately not a gate:** nothing in this repository runs, and no validation was asked for. Mutation-tested: the example validates, and five distinct defect classes are rejected. - **`spec/conformance-corpus.md` §1.1** — normative pointer to that schema, plus a SHOULD that a runtime derive its declared set from the constant its runner uses to accept or reject a `scope`, and record which it did. ### Fixed - `docs/lexicon-port-divergence.md` — the `hybrid-xss:script-tag` row is closed on the closing-tag axis, having reversed twice in three days (guard-diverges → commons-diverges → converged). What remains open is the one-byte span difference: the guard excludes `<` from its negated class and the other two do not, so `