feat(guard): bump the pin to >=0.3,<0.4 and pin Door C's allow_reserved=False

Measure first, widen after. The 19-fixture guard-surface suite was re-run
against v0.3.4 in a scratch venv before the range moved, and reproduced the
three deltas measured against v0.3.3 exactly, with none added. v0.3.4 is the
tag pinned rather than v0.3.3 because it shipped first and repairs a quadratic
regex (okf._MD_LINK_RE) that sits on Door C's own call path.

Door C now passes allow_reserved=False explicitly. The guard added the keyword
in the 0.3 line and defaults it True for received bundles, which would merge a
sender's index.md / log.md instead of rejecting them. The override keeps the
unconditional reserved-name refusal committed to before the keyword existed,
and the reason is structural rather than a second opinion on the guard's scan:
Door C generates the merged bundle's index.md from what it merged and writes
every merged concept verbatim, so a sender's index.md would be a second and
irreconcilable claim on one path.

This is not a behaviour change for anyone on the previous pin: under v0.2.0
the keyword did not exist and reserved names were refused by construction.

The floor is >=0.3 and not >=0.2 for a measured reason. allow_reserved is
absent in v0.2.0 and present from v0.3.0 onward, checked across all five tags:
a >=0.2 floor would admit a version that raises TypeError on every Door C
import. That measurement also corrects a recorded premise -- the plan said the
keyword "shipped in v0.3.3", which read the first version we ran the suite
against as the version it was introduced in. The conclusion held; the reason
did not, and the reason is what a future bump would have relied on.

test_door_c_pins_allow_reserved_false_against_the_guards_default locks both
halves: that the guard still defaults True, without which the override is a
no-op that would pass forever over nothing, and that Door C overrides it.

586 tests, mypy --strict clean, goldens byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V2v1hrDhrff2H3y2TNJHkF
This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 21:08:53 +02:00
commit f536e1384d
12 changed files with 156 additions and 34 deletions

View file

@ -37,7 +37,8 @@ Outcome recorded in STATE before Phase 2 code starts.
### Stage 0 revisited — measuring guard 0.3.1 before widening the pin
The pin held is `>=0.2,<0.3`, which can only ever resolve `v0.2.0`. The guard repo
The pin held at the time was `>=0.2,<0.3`, which could only ever resolve
`v0.2.0` (it has since moved to `>=0.3,<0.4`, resolving `v0.3.4`). The guard repo
gates its own 1.0 on our fixture suite being measured against `v0.3.1`, so this
measurement is owed outward and is that repo's only named blocker. Order is
*measure first, widen after*: widening the range before measuring would ship a
@ -61,9 +62,24 @@ Procedure — do not vary it, each step exists because of a specific failure mod
5. Green → one release: `>=0.2,<0.4`, uv tag `v0.3.1`, CLAUDE.md and README
updated together. Red → no bump, and the finding goes back to the guard.
Until this lands, consumers of v0.4.0 resolve guard 0.2.0 (`pyproject.toml:25`).
That is a silent under-defend: no build fails, so nothing surfaces it. The
release in step 5 is what closes it.
**Done, and step 5's predicted range was wrong — deliberately superseded.** The
bump landed as `>=0.3,<0.4` against tag `v0.3.4`, not `>=0.2,<0.4` against
`v0.3.1`. Two corrections, both forced rather than chosen:
- **The floor had to rise to `0.3`.** Door C now passes `allow_reserved=False`,
a keyword measured absent in `0.2.0` and present from `v0.3.0` onward. A
`>=0.2` floor would let a consumer resolve `0.2.0` and take a `TypeError` on
every Door C import — the range would have admitted a version the code cannot
call. This is the one case where widening past the measured version is not
conservative but broken. The signature was measured across all five tags
rather than inferred from the one the fixtures happened to run against.
- **The tag is the newest measured one, not the first one asked for.** `v0.3.2`,
`v0.3.3` and `v0.3.4` all shipped while this was open. The suite was re-run
against `v0.3.4` before the bump — pinning the tag that was measured is the
whole point of the order, and an older tag would have been measured against a
guard nobody would install.
That also closes the under-defend below: consumers no longer resolve `0.2.0`.
## Phase 2 — Doors B/C (split: guard-independent first)
@ -172,7 +188,7 @@ These are the objective checks that a stage is done and the next may begin:
matches + installable in CI" or an explicit "front-load guard-independent
half; guard integration blocked on <named item>".
2. **Phase 2 → Phase 3:** Phase 1 golden suite still byte-for-byte; `pyproject`
runtime deps == exactly `llm-ingestion-guard>=0.2,<0.3`; persist-gate proof
runtime deps == exactly one range on `llm-ingestion-guard`; persist-gate proof
test green (a fail-secure fixture yields zero new files).
3. **Phase 3 → Phase 4:** golden suite byte-identical under `DEFAULT`
(`git diff --stat examples/` empty for the phase); `STRICT_V1` cross-profile

View file

@ -3,7 +3,8 @@
Status: approved roadmap phase (see `CLAUDE.md`); details settled here before code.
Depends on: Phase 1 (materialization + index primitives are reused, never duplicated).
This phase adds the library's first — and only permitted — runtime dependency:
`llm-ingestion-guard>=0.2,<0.3`.
`llm-ingestion-guard` (pinned `>=0.2,<0.3` when this plan was written; the
window moved to `>=0.3,<0.4` after measurement — see the settled note below).
## Goal
@ -93,7 +94,7 @@ No scanning, sanitizing, or quarantine logic is implemented here.
`Origin`/`Channel` vocabularies Door C validates, the result fields the
adapters read, and the upload preset's shape.
- **The pin stays a range; the git URL is an install channel.** A PEP 508
direct reference pins one tag and cannot express `>=0.2,<0.3`, but it is an
direct reference pins one tag and cannot express a range, but it is an
install-time channel rather than a dependency declaration: the range is
what `pyproject.toml` carries, it is satisfied by the tag install today,
and it resolves normally once the package index exists (confirmed by the
@ -125,17 +126,38 @@ No scanning, sanitizing, or quarantine logic is implemented here.
the same floor Door B applies, with `quarantine_review` reported as its own
bucket rather than folded into rejection.
- **Upstream has released past the pin, and the decision is now taken.** `main`'s
`allow_reserved=True` kwarg shipped in guard `v0.3.3` (confirmed by a
`allow_reserved=True` kwarg was first *observed* by us in guard `v0.3.3` (a
19-fixture measurement against a scratch venv, unrelated to the pinned
install). The kwarg defaults `True`, so an unqualified call now *merges*
install). **It did not ship there** — the signature was measured across every
0.3 tag at bump time and the kwarg is present from `v0.3.0` onward, absent in
`v0.2.0`. The original wording read "first version we ran the suite against"
as "version it was introduced in"; the two coincided only because `v0.3.3`
was the first 0.3 we measured at all. The conclusion it supported was right
and the reason was wrong, so the reason is corrected rather than the outcome
quietly kept. It also decides the pin's floor: `>=0.3` is exactly right, and
would have been wrong either way if the kwarg had really arrived in `0.3.3`.
The kwarg defaults `True`, so an unqualified call now *merges*
`index.md`/`log.md` in a mode-b import instead of path-rejecting them —
reversing this plan's original "no allow_reserved toggle, rejection is
unconditional" reading. Decided: when the pin bumps into the `0.3.x` line,
`guard_adapter.import_gate` passes `allow_reserved=False` explicitly,
keeping the reserved-name refusal this plan committed to. Today the pin is
still `v0.2.0`, where the kwarg does not exist and rejection is
unconditional by construction — no code changes yet; whoever bumps the pin
adds the kwarg and a test pinning it, per assumption B1.
keeping the reserved-name refusal this plan committed to.
**Done.** The pin moved to `>=0.3,<0.4` (resolved `v0.3.4`, not `v0.3.3`
`v0.3.4` shipped first and repairs a quadratic regex on Door C's own call
path). The 19-fixture suite was re-run against `v0.3.4` before the bump and
reproduced the `v0.3.3` deltas exactly, with none added.
`guard_adapter.import_gate` now passes `allow_reserved=False`, and
`test_door_c_pins_allow_reserved_false_against_the_guards_default` pins both
halves: that the guard still defaults `True` (without which the override is
a no-op that would pass forever over nothing) and that Door C overrides it.
The recorded justification is worth sharpening now that it is code: the
guard's `True` default is right *for the guard*, and this library does not
dispute the safety reasoning behind it. Door C's refusal is structural — it
generates the merged bundle's `index.md` from what it merged and writes every
merged concept verbatim, so a sender's `index.md` is a second and
irreconcilable claim on one path, not merely a risk to be scanned.
- **`origin`/`channel` are validated against the guard's pinned vocabulary.**
The guard derives trust from `origin` by enum *identity*, so an unrecognised
string would be silently downgraded to untrusted. The library refuses to
@ -199,5 +221,7 @@ No scanning, sanitizing, or quarantine logic is implemented here.
5. Phase 1 golden suite still passes byte-for-byte (no regression from reuse).
6. Grep-gate: `grep -rn "sanitize\|quarantine\|lexicon" src/` shows no local
security reimplementation (guard imports only).
7. `pyproject.toml` runtime dependencies == exactly `llm-ingestion-guard>=0.2,<0.3`
(automated: `test_the_only_runtime_dependency_is_the_security_boundary`).
7. `pyproject.toml` runtime dependencies == exactly one range on
`llm-ingestion-guard` (automated:
`test_the_only_runtime_dependency_is_the_security_boundary`; the range
itself is `>=0.3,<0.4` since the bump).