feat(carriers): three cases minted, and the id is named rather than adopted

Both runtimes answered the 2026-08-11 decision request, and they did not ask
for the same name. The guard ruled `sanitize:` names its `Finding.detector`
and offered all three labels for verbatim adoption. llm-security, asked
directly, answered that the name would make its conformance result read as a
claim about neutralisation it does not perform.

Two things decided it. The guard's own unprompted correction: prefix ==
detector holds for those six labels and is no general law in its runtime
(`egress:*` carries detector="output"; decode-and-rescan yields two-part
`decoded:lexicon:*`). A prefix whose meaning is recoverable only by reading
one implementation cannot carry a shared id space. And a measurement taken
here at be9759b: on the surface the guard's own ruling pinned, `sanitize()`
returns changed text on all three carriers, so the counterargument's decisive
case -- that `scan_output` mutates nothing -- does not reach this surface.

Not a mediation. Neither runtime claimed the shared id must equal its label,
and `override:ignore-previous` already carries two different alias strings.

- carriers.json 0.1.0 -> 0.2.0: carrier:zero-width / :bidi-override /
  :unicode-tag, aliased to the guard's labels. No aliases.llm_security --
  that runtime's carrier findings carry no id yet, and publishing the alias
  is the irreversible act that forces the table into its declared set.
- manifest 0.3.4 -> 0.4.0: entry_points_by_scope, carrying findings accessor
  and fixture presentation per scope per runtime. This was objection (c), and
  it blocked minting harder than the name did.
- Corpus 90 -> 93. Measured through sanitize(text, source=Source.INPUT) at
  guard v0.7.0; verified by a separate checker that re-derives everything from
  disk -- a generator agreeing with itself proves nothing.
- CLAUDE.md gains the two rules that are not derivable from the data: a shared
  id space cannot rest on a one-runtime prefix, and publishing an alias -- not
  minting the case -- is the irreversible act.

Not minted on purpose: no artifact-side id (the other runtime would only fail
them), and no ZWJ-exemption case (U+200D between two emoji is exempt on both
guard surfaces since v0.6.1; the fixture avoids it rather than trips it).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U3o4zSQ2kwLsgJNU7apK3Z
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 21:17:44 +02:00
commit 8f08e9ac73
11 changed files with 350 additions and 30 deletions

View file

@ -9,6 +9,85 @@ Versioning note: the repository tag versions **the contract** (file set, key nam
case ids, disposition semantics). Each JSON file additionally carries its own
`"version"` field, bumped when that file changes.
## [0.5.0] — 2026-08-13
**Three cases minted, and the id they carry is the first in this repository that was *named*
rather than adopted. Both consuming runtimes answered the decision request of 2026-08-11, and
they did not ask for the same name.**
### Added
- `conformance/carrier__zero-width`, `carrier__bidi-override`, `carrier__unicode-tag` — the
first cases scoped to `codepoints/carriers.json`. Corpus goes 90 → **93**. Measured through
the guard's input surface `sanitize(text, source=Source.INPUT)` at `be9759b` (v0.7.0),
findings read at `.report.findings`. Each input produces exactly one in-scope finding and no
residue.
- `codepoints/carriers.json` `0.1.0``0.2.0` — a `pattern_id_space` block carrying
`carrier:zero-width`, `carrier:bidi-override` and `carrier:unicode-tag`, each aliased to the
guard's label. **These ids name what was OBSERVED, not what was done about it**, and that is a
departure from how every other id space here was built: the lexicon's 83 and active content's
6 were adopted verbatim from a runtime's own labels.
The guard ruled — measured, and correctly — that its `sanitize:` prefix names the producing
module in `Finding.detector` and never an action, offering all three for verbatim adoption.
The other runtime, asked the question directly, answered that the name would make its
conformance result read as a claim about neutralisation it does not perform. Two things
decided it against verbatim adoption. First, the guard's own unprompted correction: prefix ==
detector holds for those six labels and is **no general law** in its runtime (`egress:*`
carries `detector="output"`; decode-and-rescan yields two-part `decoded:lexicon:*`), and a
prefix whose meaning is recoverable only by reading one implementation cannot carry a *shared*
id space. Second, a measurement taken here on 2026-08-13: on the very surface the guard's
ruling pinned, `sanitize()` returns changed text on all three carriers — so the counter-
argument's decisive case, that `scan_output` mutates nothing, does not reach this surface.
This is not a mediation between two runtimes. Neither claimed the shared id must equal its own
label, and the shape is the one the lexicon already had: `override:ignore-previous` carries two
*different* alias strings today, and neither runtime's label is the id.
- `conformance/manifest.json` `0.3.4``0.4.0``entry_points_by_scope`, stating entry point,
**findings accessor** and **fixture presentation** per scope per runtime. The corpus previously
pinned one entry point per runtime for all cases, which held for exactly as long as every case
was reachable from one surface. Neither pinned entry point reaches carriers. Both runtimes
said independently that the manifest was the right place for this: the guard rather than grow
an API wrapper for this corpus's benefit, llm-security to record that its carrier entry point
is path-based and **cannot measure a bare `input.txt`** — the fixture must reach it as a
discovered file. Also adds `carrier_measurement`.
### Fixed
- `conformance/manifest.json` — the retired carrier blocker asserted a general rule, "THE PREFIX
NAMES THE DETECTOR", about a runtime this repository does not own. The guard corrected it at
its commit `0903785`: true of those six labels, false as a law. The claim is now **scoped to
the six** wherever it is stated, and the general form appears nowhere — checked across
`CONVENTIONS.md`, `README.md`, `spec/` and `docs/`. The retired text is kept verbatim under
`scope_planned.blockers_resolved`, because a record corrected in place cannot be audited.
### Notes
- **`not-applicable` here records a missing name, not a missing capability.** Both runtimes
implement the carrier tables — llm-security builds its zero-width, tag-range and BIDI sets from
this very file — but its carrier findings carry no id yet (`scanner: 'UNI'`, a severity and a
prose title). So `carriers.json` carries **no `aliases.llm_security`**, the file stays outside
that runtime's declared set, and the three cases are `not-applicable` there today. This is
deliberate and stated in the file: publishing that alias is the irreversible act, since it
forces the table into that runtime's declared set by force of its own test suite. The slot is
filled when the label exists and is named to us.
- **Not minted, on purpose.** No artifact-side carrier id (`output:zero-width-present`,
`output:bidi-present`, `lexicon:unicode-tags-present`) — the other runtime has no artifact
surface for carriers, so those cases could only fail there, and a permanently red gate is one a
reader learns to ignore. And no case for the ZWJ exemption: U+200D between two emoji is exempt
on both guard surfaces since v0.6.1, verified here, and the zero-width fixture carries a bare
U+200B between two letters so as not to be failed by design. Whether that exemption is a shared
rule or one runtime's policy has not been put to the other runtime.
- **The guard's v0.7.0 does not move a published fixture.** Re-measured, not inferred: all six
active-content cases produce their committed verdicts at `be9759b`, and the new
`active:raw-html-link` label is confirmed to reach `<a>` and `<area>` only, with
`<a onclick=…>` still grading `active:raw-html` at HIGH. Adopting that seventh construct into
`signatures/active-content.json` is a separate decision and is **not** taken here.
## [0.4.3] — 2026-08-11
**No fixture, id or `expected.json` moved — a runtime that passes `0.4.2` passes `0.4.3`