Skip to content

Commit aff932c

Browse files
jgravelleclaude
andcommitted
release: v1.108.183 — a receipt says what it proves, and only that (#377 Phase 2)
P1 + P2 together, because an envelope without registration is a contract anything can mint into. The Phase 1 caveat is closed. `_validate_evidence` attested a ref matching a served symbol id OR the file component of one, so citing a whole file was attested when one unrelated symbol from it was the only thing retrieved, and a reader of the finalized handoff could not tell the two citations apart. - evidence/receipts.py: the `jcodemunch.evidence/v1` envelope. One canonical subject (symbol id, file, line range, content_sha256) bound to one snapshot and to the operation that actually ran, with the id derived from exactly those three. Full sha256, never 12 hex. Fail-closed on id reuse over differing content. Served at munch://evidence/<id>; no new tool. - evidence/producers.py: producer registration + canonical projectors. Minting requires a verdict of the SHAPE the producer registered, so an exit that asserts an answer without calling build_verdict cannot mint. Mint runs at the call_tool chokepoint, so an early return cannot bypass it at all. - handoff.py: the input picks the contract. No receipt cited means today's broadening and a byte-identical body, plus evidence_precision and broadened_refs naming what was broadened. A receipt cited means a file-level ref backed only by a served symbol is refused as over-broad. - snapshot.conditions puts every condition inside the identity payload, so limitations cannot diverge between two receipts sharing an id. - schemas/evidence-receipt.schema.json published in the same commit. - FIX: _DECLARED_ARG_KEYS is snapshotted before the compact-schemas strip. compact_schemas pops params out of the same dicts the argument contract read, so a hidden-but-honored param was reported as a caller mistake, which downgrades the verdict and costs a well-formed call its absence evidence. Suite 6025 passed + the known 12 local-ONNX env failures. Replay gate green. core_compact unchanged at 3996; baseline regenerated for the three non-compact profiles only. No tool-count change, no INDEX_VERSION change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent db63d2c commit aff932c

13 files changed

Lines changed: 2925 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,179 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.183] - 2026-07-26 - a receipt says what it proves, and only that
6+
7+
### Added
8+
9+
- **Exact immutable evidence receipts** (`jcodemunch.evidence/v1`),
10+
[#377](https://github.com/jgravelle/jcodemunch-mcp/issues/377) Phase 2, P1 + P2
11+
of the PRD sequencing. Design by **@mightydanp**, whose post-ship review items
12+
12-22 this implements.
13+
14+
Phase 1 (1.108.165) answered *which reference is attached to which claim*. It
15+
did not narrow **what a reference proves**, and we said so publicly when we
16+
shipped it. `handoff._validate_evidence` attests a ref matching a served symbol
17+
id **or the file component of one**, so:
18+
19+
served this session: src/auth/session.py::refresh_token#function
20+
attests: src/auth/session.py::refresh_token#function correct
21+
attests: src/auth/session.py over-broad
22+
23+
A claim citing a whole file was attested when one unrelated symbol from it was
24+
the only thing retrieved, and the reader of a finalized handoff could not tell
25+
those two citations apart. The server was the only party in the exchange that
26+
knew the difference.
27+
28+
A receipt is the narrow answer. New `evidence/receipts.py` binds one canonical
29+
subject — symbol id, file, line range, `content_sha256` — to one snapshot and
30+
to the operation that actually ran, then derives its identity from exactly
31+
those three, so **different snapshots cannot share an evidence id**. Read at
32+
`munch://evidence/<id>`, beside `munch://runtime/identity` (#371) and
33+
`munch://handoff/<id>` (#374). **No new tool**: the tool-schema budget is a
34+
real constraint (90 tools in `full`, `core_compact` pinned at 3996) and the
35+
reviewer flagged it before we did. The response carries an **id**, not the
36+
receipt; the body is read from the resource on demand.
37+
38+
- **Producer registration with canonical projectors** (`evidence/producers.py`),
39+
the load-bearing half. An envelope without registration is a contract anything
40+
can mint into, which is the defect this phase exists to fix, so P1 and P2
41+
shipped together. A reviewed producer declares its verdict shape, the proof
42+
kinds it may mint, its canonical projector, and its completeness / freshness /
43+
coverage / integrity semantics — once.
44+
45+
⚠ **This is where the hardest lesson of the 1.x hardening work goes.**
46+
`get_ranked_context` asserted "Do not claim this feature exists" from an early
47+
return that never called `build_verdict`, so eight shipped absence gates were
48+
invisible to it until 1.108.179 went looking. An early return is a second
49+
implementation of the answer and inherits nothing. Registration makes that
50+
structural rather than remembered: **minting requires a verdict of the shape
51+
the producer registered**, so an exit that asserts an answer without building
52+
one cannot mint — not as a listed exception, but because it never produced the
53+
thing a receipt is derived from.
54+
55+
- **Four producers registered, and the boundary is stated rather than implied.**
56+
`get_symbol_source` (`symbol_definition`; positive only — its "not in the
57+
index" answer carries no scan counts, no coverage and no query, so it cannot
58+
back an absence claim and is not registered to try), `search_symbols` and
59+
`get_ranked_context` (`symbol_definition` + `symbol_lookup_absence`, both
60+
`get_ranked_context` exits since 1.108.179), `search_text`
61+
(`literal_text_absence` only — a text hit is a `(file, line, matched text)`
62+
tuple, not a symbol definition, and giving it a positive kind would mean
63+
inventing a subject nothing downstream knows how to compare).
64+
65+
- **Opt-in `receipt` argument** on those four, and only those four. Default false
66+
is today's response bytes, **pinned by a test**. Passing it to an unregistered
67+
tool is disclosed as `ignored_arguments` (the 1.108.175 contract) rather than
68+
silently accepted, so "no receipt came back" is never a mystery.
69+
70+
- **`schemas/evidence-receipt.schema.json`**, published in the same commit that
71+
emits it. That trap has now fired three times (1.108.168, .176, .180); the
72+
receipt envelope is closed (`additionalProperties: false`) and the tests
73+
validate live receipts from all three shapes against it.
74+
75+
### Changed
76+
77+
- **`_meta.receipts` joins `UNIVERSAL_META_JSON`.** It is the only thing in the
78+
response that names the receipt, so an encoder that dropped it would convert an
79+
opted-in call back into one that proved nothing. Verified across json, compact
80+
and auto encoding, `meta_fields: []`, a partial `meta_fields` filter and
81+
`suppress_meta` — the same ordering rule as 1.108.177 item 10: the receipt is
82+
minted against the COMPLETE internal result and the carrier is re-attached
83+
after presentation filtering. A display preference must never decide what a
84+
scan proved.
85+
86+
- **`finalize_handoff` distinguishes exact evidence from broadened evidence, and
87+
the input picks the contract.** A handoff citing **no** receipt keeps the
88+
historical file-component broadening exactly and renders byte-identically, so
89+
no shipped caller changes — but its receipt now carries `evidence_precision`
90+
(`exact` / `symbol_exact` / `broadened`) and `broadened_refs`, which name the
91+
file-level citation and what actually backed it. A handoff that **does** cite a
92+
receipt is held to the subject its evidence proves: a file-level ref backed
93+
only by a served symbol from that file is **refused** as over-broad, with its
94+
own error and its own fix. That closes the Phase 1 caveat, and
95+
`tests/test_v1_108_183.py` proves the test is not vacuous by showing the same
96+
finalization attests the file ref with the narrowing disabled.
97+
98+
- `absence_attested` now counts absence **receipts** as well as bare `absent:`
99+
tokens: a receipt points at the same recorded scan, and a reader counting
100+
absence proofs would otherwise undercount every receipt-citing handoff. (The
101+
rename to `absence_scans_attested` is P4 and is not in this release.)
102+
103+
### Notes
104+
105+
- **Phase 3 refs keep working, unchanged.** An absence receipt LINKS to the scan
106+
`note_absence` recorded (new `handoff.absence_ref_for` derives the ref from the
107+
same projection) instead of re-deriving whether that scan proves anything, so
108+
`absence_refusal` stays the single implementation of the refusal rules and a
109+
receipt read hours later cannot disagree with the gate that issued it.
110+
`capabilities.proves_absence` is advisory; finalization re-derives it.
111+
112+
- **The snapshot is BOUND, not built.** Every field already ships:
113+
`subject_state.capture` (1.108.178), `FreshnessProbe.repo_freshness`
114+
(1.108.180), `index_coverage_meta` (1.108.176), `verdict.working_tree`
115+
(1.108.181). ⚠ One divergence found while wiring it and handled rather than
116+
papered over: `build_symbol_verdict` never got the item-4 four-state treatment,
117+
so ITS `channels.index` reports `fresh` for a plain indexed folder that has no
118+
revision at all. A receipt from `get_symbol_source` therefore probes
119+
`repo_freshness` directly instead of trusting the channel — believing that
120+
value would have been the 1.108.176 mistake again, a signal read as answering a
121+
question it does not answer. The divergence is visible inside one envelope
122+
(`channels.index: fresh`, `snapshot.freshness: not_tracked`) and pinned.
123+
124+
- **The subject is read from the SERVED row only, never re-read from the index.**
125+
A receipt describes what the caller was handed: `search_symbols` at standard
126+
detail serves no `end_line` and no content hash, so the receipt omits both and
127+
says so in `limitations` rather than quietly filling them in from a lookup the
128+
caller never saw. `detail_level: "full"` serves the body, so the served bytes
129+
are hashed. `limitations` is what makes a weak receipt usable rather than
130+
discardable.
131+
132+
- **Full digests, and fail-closed on collision.** 48 bits of displayed identity
133+
is a birthday problem waiting for a large session (review item 19), so the id
134+
is the whole sha256; rendered prose shows a short prefix, identity and storage
135+
never do. An id that has ever named two different receipts names neither
136+
afterwards — keeping one would make every later citation of it a coin flip.
137+
138+
- ⚠ **`snapshot.conditions` exists because fail-closed and `limitations` were
139+
about to fight each other.** `limitations` is not part of the evidence id, so a
140+
condition that changed it WITHOUT changing the id would give one id two
141+
different bodies — and the fail-closed check would then refuse both, deleting a
142+
receipt an agent was already holding. A cached `search_symbols` replay whose
143+
verdict gains `revalidated` is exactly that case: the generation and both
144+
revisions sit still while the response's honesty changes. So every condition is
145+
now a token INSIDE the snapshot (`rebuilding`, `partial`, `stale`,
146+
`freshness_unknown`, `not_tracked`, `no_coverage_contract`,
147+
`semantic_unavailable`, `replayed_from_stale_cache`, `moved_during_scan`,
148+
`incomplete_inputs`, `omitted_matches`), every limitation is derived from one,
149+
and a subject measured under different conditions therefore gets a different
150+
id — which is what "different snapshot, different id" has to mean when the
151+
conditions are what changed. Found reviewing this change, not by a test failing.
152+
153+
- **DELIBERATELY NOT DONE, so nobody builds on it:** session keying, the full
154+
expiry taxonomy (`wrong_session` / `wrong_subject` / `expired` /
155+
`snapshot_invalidated`) and finalization deep-freeze are P3; redacted public
156+
rendering and the `absence_scans_attested` rename are P4; **suite parity for
157+
jdocmunch and jdatamunch is P5 and is untouched here.** `lookup` does
158+
distinguish `never_recorded` from `evicted`, because a bounded store makes that
159+
one bit unavoidable and reporting the wrong reason would be worse than
160+
reporting none. Receipts are capped at 25 per call, and the truncation is
161+
DISCLOSED in the carrier rather than silent.
162+
163+
- **Two ungated absence assertions found and NOT fixed here, stated plainly
164+
rather than left to be discovered:** `search_symbols`'s semantic exit emits the
165+
legacy `negative_evidence` block — including "Do not claim this feature
166+
exists" — with no `_meta.verdict` at all, so none of the absence gates shipped
167+
since 1.108.166 apply to it; both fusion exits emit neither a verdict nor
168+
negative evidence. Neither can mint a receipt, which is the registration gate
169+
working, and tests pin that against the source so a future verdict there forces
170+
a review before it starts minting. Repairing the semantic exit's prose would
171+
change response bytes on calls that never asked for a receipt, so it is its own
172+
change, not a rider on this one.
173+
174+
- New `tests/test_v1_108_183.py` (78). No tool-count change, no `INDEX_VERSION`
175+
change, no new background process, no write to the user's repository or index
176+
store.
177+
5178
## [1.108.182] - 2026-07-26 - a stall has a name and a ceiling
6179

7180
### Fixed

0 commit comments

Comments
 (0)