|
2 | 2 |
|
3 | 3 | All notable changes to jcodemunch-mcp are documented here. |
4 | 4 |
|
| 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 | + |
5 | 178 | ## [1.108.182] - 2026-07-26 - a stall has a name and a ceiling |
6 | 179 |
|
7 | 180 | ### Fixed |
|
0 commit comments