# 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. ## [Unreleased] ## [0.2.0] — 2026-08-11 The contract gained a normative MUST, which is why this is a release rather than a metadata commit: **a runtime that conformed to `v0.1.0` does not conform to this one until it declares the set of commons data files it implements.** In 0.x that is a minor bump by the rules, but it is breaking in substance, and a consumer reading only the version number should learn that here rather than from a failing suite. Everything below this heading was previously listed as unreleased. ### Retracted - **The claim that the Python guard's port cites `severity.mjs` for hybrid severity.** It is false. It was carried in three places — `lexicon/injection-lexicon.json` (`families[hybrid].severity_provenance.not_from`), `docs/lexicon-port-divergence.md` (§ *Severity: the 8 hybrid patterns*), and the `[0.1.0]` entry below — and it was never measured here. It restated an assertion received from `llm-security` (coord message `20260809T201048Z`) as a commons finding. Measured against the guard's own tree, which `llm-ingestion-pipeline-security` asked for twice before this was checked: `severity.mjs` has **never** appeared in `src/llm_ingestion_guard/injection_lexicon.json` at any point in that file's history (`git log -S` returns no commits), and at `0bf0729` — the commit `conformance/manifest.json` pins — the only tree-wide occurrence is `docs/PLAN.md:114`, correctly attributing the *report* module to `output.mjs` + `severity.mjs`. The guard's only source statement for the lexicon is the `note` at `injection_lexicon.json:3`, and it names `injection-patterns.mjs`. **No detection data moves.** `families[hybrid].severity` is still `high`, still sourced to `injection-patterns.mjs:274-281`, re-verified at `b0de0ca`; `severity.mjs` still contains zero occurrences of `CRITICAL_PATTERNS`, `HIGH_PATTERNS`, `MEDIUM_PATTERNS` and `HYBRID_PATTERNS`, re-measured the same day. Only the sentence about the *other* repository falls. The retraction is marked in place rather than edited away, and it is worth naming why this one survived review: the claim arrived bundled with a correct measurement of the same question, from a repository that had done its half properly. The correct half carried the incorrect half past the check — which is precisely the defect `severity_provenance.not_from` was written to warn about, one level up. ### Added - **`not-applicable`, a third conformance verdict** (`spec/conformance-corpus.md` §1.1). A runtime now declares the set of commons data files it implements; a case whose `scope` names a file outside that set is `not-applicable` rather than failed. §1 alone would have reported an architectural difference as a defect — one seeding runtime has no active-content table and never will, and 7 permanent failures say nothing a reader can use. The verdict is fenced so it cannot become an exit: it attaches to a **table**, never to a case (per-case opt-out is the silent skip §1 forbids), a declared set MUST NOT be narrowed to convert failures into `not-applicable`, and such cases MUST still be enumerated rather than dropped from the denominator. §8 now states the consequence: a pass count is unreadable without the declared set beside it. - **Six active-content conformance cases** — `active__markdown-image`, `active__markdown-link`, `active__reference-link`, `active__autolink`, `active__raw-html`, `active__data-uri`. The corpus goes 83 → 89, and `scope_covered` gains `signatures/active-content.json`. Generated from measurement, not written: payload strings were extracted from the seed runtime's `coverage.py` by AST — evaluating each `_scan_case` argument in that module's own namespace rather than retyping detection data — then run through its public output gate. The fixtures were then re-read from disk by a separate checker that re-computed every digest, re-scanned the bytes and applied `exact-within-scope` independently of the generator, because a generator agreeing with itself proves nothing: 6 cases, 0 failed checks. **These six prove less than the 83, and the manifest says so.** Their payloads come from the only runtime implementing the table, so no second implementation's agreement could be measured. They pin one runtime's behaviour as a contract a future implementer can be held to — which is less than cross-runtime agreement and more than nothing. - `signatures/active-content.json` **0.1.0 → 0.2.0** — a `pattern_id_space` block. Unlike the lexicon's, nothing was constructed: `label_format` and the `constructs` keys were already extracted verbatim, and their concatenation *is* what the seed runtime emits. The block states an id space the file already had implicitly, and records that it is ratified by **one** runtime rather than two. ### Changed - `lexicon/injection-lexicon.json` **0.5.0 → 0.5.1** — provenance metadata only; no pattern, id, alias, family or severity value changes. - `conformance/manifest.json` **0.1.1 → 0.2.0** — the six cases, `scope_covered`, `count_by_scope`, separate provenance and measurement blocks for the active-content half (a different source structure at a different commit; one pin must not stand for two measurements), and `scope_planned.blockers`. - **`spec/conformance-corpus.md` §4 no longer claims scoping "asks a question both can answer."** That held only while every case was scoped to the one table both runtimes implement, and stopped being true the moment a case was scoped to a single-runtime table. Scope narrows *what* is compared; it does not make every runtime a valid addressee. The superseded sentence is named in place rather than edited away. ### Fixed - **§4 now states that "belongs to a data file" means published there, never "shares its prefix."** The distinction has a live witness: the seed runtime emits `active:oversize-input`, a self-safety flag about its own scan cap, which carries the `active:` prefix but is no construct in `signatures/active-content.json`. A prefix-matching runtime would pull it into the comparison and fail a case over a finding the corpus makes no claim about. Recorded in that file under `pattern_id_space.not_every_active_label` as well. - **§6 now states the derivation's cost.** `case_id` derives from `pattern_id` alone, so a single-finding scope holds at most one case per pattern id — there is nowhere in the name for a second. The seed runtime's matrix drives *two* payloads at `active:markdown-image`; measured, their in-scope finding sets are identical, and the second's only distinguishing signal (`entropy:base64-blob`) falls outside every table this repository publishes. It was dropped rather than given a discriminated id, which would have broken the reverse transform, and it is named in `conformance/manifest.json` under `omitted_payloads` so that 6 built from 7 offered reads as a decision rather than a miscount. ### Measured, not shipped - **The remaining four cases are blocked on two distinct unresolved questions**, now recorded under `scope_planned.blockers` instead of the earlier blanket "no runtime has agreed to an id space". That framing was wrong for both: - **Carriers (3).** No adoptable id space, and a second problem underneath. The guard emits two *stage-coupled* labels for one carrier — `sanitize:zero-width` on input, `output:zero-width-present` on output, same split for bidi and unicode-tag — while llm-security emits prose titles. A commons id must be invented stage-neutral, which no other id space here required. And since `exact-within-scope` compares a finding *set*, an id aliasing both labels makes the verdict depend on which entry point the runtime was measured through — an entry-point dependence the lexicon cases do not have. - **Secret egress (1).** Not an id-naming question at all. The two runtimes carry **different tables**: 18 entries here against the guard's 25, cut at different granularities (this file's single `GitHub Token` is four ids there, `Private Key PEM Block` three, `Database connection string` four), with membership diverging both ways. `aws-access-key-id` is the one clean 1:1 — which is why exactly one egress case was ever offered. That number was a symptom, not modesty. A shared id space presupposes a table reconciliation nobody has done. ## [0.1.0] — 2026-08-10 Initial extraction. Runtime-neutral detection data, the finding contract, and a conformance corpus, extracted from the `llm-security` Node implementation and a Python guard **without behaviour change** — that invariant is the release, not a caveat on it. What the tag is worth resting on: seven of the eight JSON artefacts were rebuilt from the commons file alone and diffed against their source implementation, three of them against the source module at a pinned commit. The eighth says `verified: false` about itself. The corpus holds 83 cases on which both seeding runtimes were measured agreeing exactly. What it is not: `spec/decode-pipeline.md` does not exist, and the corpus constrains one of the seven data files. Both absences are named in *Not included* rather than papered over. ### Added - `conformance/` — **83 cases, one per injection-lexicon pattern**, plus `manifest.json`. Each case is a directory holding `input.txt` (the exact bytes, no trailing newline) and `expected.json` (the findings, named by commons pattern `id`). Both seeding runtimes were measured producing the **same lexicon finding set on all 83**, through their public entry points — `scanForInjection()` at `b0de0ca` and `scan_output(source=OUTPUT)` at `0bf0729` — with labels mapped to commons ids through the lexicon's own `aliases` block. Not through rebuilt regex tables: a table-level comparison yields a number that describes neither runtime, which is the mistake the divergence document had to retract. **The 13 divergent patterns are in, unmarked, and that is the substantive result.** Their divergence was measured on witness inputs — an attribute run padded past 256 characters, an interior `<`, an unclosed `