Skip to content

Commit 54db050

Browse files
jgravelleclaude
andcommitted
test: a receipt on a real checkout, because fresh is what a fixture cannot fake
Every plain-folder fixture reports freshness `not_tracked`, so a suite built only from them never exercises the path where both revisions are read and compared. That path is the whole point of the snapshot binding. The v1.108.180 lesson, applied to Phase 2. Also covers the absence half on a real checkout: a zero-result scan measures the working tree, so its receipt binds `clean` rather than `unknown`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent aff932c commit 54db050

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ All notable changes to jcodemunch-mcp are documented here.
171171
change response bytes on calls that never asked for a receipt, so it is its own
172172
change, not a rider on this one.
173173

174-
- New `tests/test_v1_108_183.py` (78). No tool-count change, no `INDEX_VERSION`
174+
- New `tests/test_v1_108_183.py` (79). No tool-count change, no `INDEX_VERSION`
175175
change, no new background process, no write to the user's repository or index
176176
store.
177177

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# jcodemunch-mcp — Project Brief
22

33
## Current State
4-
- **Version:** 1.108.183 — **A receipt says what it proves, and only that (#377 Phase 2, P1+P2).** ⚠ **Phase 1 attested a ref matching a served symbol id OR THE FILE COMPONENT OF ONE, so citing `src/auth/session.py` was attested when `session.py::refresh_token#function` was the only thing retrieved — and the finalized handoff gave a reader no way to tell those two citations apart.** New `evidence/receipts.py` binds one canonical subject (symbol id + file + line range + `content_sha256`) to one snapshot and to the operation that actually ran, and derives the id from exactly those three, so **different snapshots cannot share an evidence id**. Read at `munch://evidence/<id>`; **NO new tool** (tool-schema budget); the response carries an **id**, not the receipt. Opt-in `receipt` arg on the four registered producers only. ⚠ **P2 is the load-bearing half and shipped together with P1 on purpose — an envelope without registration is a contract anything can mint into.** `evidence/producers.py` declares per producer: verdict shape, proof kinds, canonical projector, completeness/freshness/coverage/integrity semantics. **The early-return lesson made STRUCTURAL: minting requires a verdict of the shape the producer registered**, so an exit that asserts an answer without calling `build_verdict` cannot mint — not as a listed exception, but because it never produced the thing a receipt derives from. ⚠ **finalize_handoff: THE INPUT PICKS THE CONTRACT.** No receipt cited ⇒ historical broadening stands, body byte-identical, but the receipt now NAMES it (`evidence_precision`, `broadened_refs`). A receipt cited ⇒ a file-level ref backed only by a served symbol is REFUSED as over-broad. ⚠ **Absence receipts LINK to the scan `note_absence` recorded (`handoff.absence_ref_for`) so `absence_refusal` stays the ONLY implementation of the refusal rules; `capabilities.proves_absence` is advisory and finalization re-derives it.** ⚠ **Divergence found while wiring: `build_symbol_verdict` never got the item-4 four-state treatment, so ITS `channels.index` says `fresh` for a plain folder with no revision — a `get_symbol_source` receipt therefore probes `repo_freshness` DIRECTLY (trusting the channel would be the 1.108.176 mistake again). Visible inside one envelope and pinned.** ⚠ **Subject read from the SERVED row only, never re-read from the index** — `search_symbols` at standard detail serves no `end_line`/hash, so the receipt omits both and says so in `limitations`. Full digests + fail-closed on collision (an id that ever named two receipts names neither). `_meta.receipts` in `UNIVERSAL_META_JSON`. `schemas/evidence-receipt.schema.json` published in the same commit. ⚠ **NOT done: session keying / full expiry taxonomy / deep-freeze = P3; redacted rendering + `absence_scans_attested` rename = P4; suite parity = P5.** ⚠ **Two ungated absence assertions FOUND, not fixed, said out loud: `search_symbols`'s SEMANTIC exit emits "Do not claim this feature exists" with NO `_meta.verdict` at all (so no absence gate since 1.108.166 applies to it), and both FUSION exits emit neither. Neither can mint — the gate working — and tests pin it against the source. Fixing the prose would change bytes on calls that never asked for a receipt, so it is its own change.** New `tests/test_v1_108_183.py` (78). No tool-count/INDEX_VERSION change.
4+
- **Version:** 1.108.183 — **A receipt says what it proves, and only that (#377 Phase 2, P1+P2).** ⚠ **Phase 1 attested a ref matching a served symbol id OR THE FILE COMPONENT OF ONE, so citing `src/auth/session.py` was attested when `session.py::refresh_token#function` was the only thing retrieved — and the finalized handoff gave a reader no way to tell those two citations apart.** New `evidence/receipts.py` binds one canonical subject (symbol id + file + line range + `content_sha256`) to one snapshot and to the operation that actually ran, and derives the id from exactly those three, so **different snapshots cannot share an evidence id**. Read at `munch://evidence/<id>`; **NO new tool** (tool-schema budget); the response carries an **id**, not the receipt. Opt-in `receipt` arg on the four registered producers only. ⚠ **P2 is the load-bearing half and shipped together with P1 on purpose — an envelope without registration is a contract anything can mint into.** `evidence/producers.py` declares per producer: verdict shape, proof kinds, canonical projector, completeness/freshness/coverage/integrity semantics. **The early-return lesson made STRUCTURAL: minting requires a verdict of the shape the producer registered**, so an exit that asserts an answer without calling `build_verdict` cannot mint — not as a listed exception, but because it never produced the thing a receipt derives from. ⚠ **finalize_handoff: THE INPUT PICKS THE CONTRACT.** No receipt cited ⇒ historical broadening stands, body byte-identical, but the receipt now NAMES it (`evidence_precision`, `broadened_refs`). A receipt cited ⇒ a file-level ref backed only by a served symbol is REFUSED as over-broad. ⚠ **Absence receipts LINK to the scan `note_absence` recorded (`handoff.absence_ref_for`) so `absence_refusal` stays the ONLY implementation of the refusal rules; `capabilities.proves_absence` is advisory and finalization re-derives it.** ⚠ **Divergence found while wiring: `build_symbol_verdict` never got the item-4 four-state treatment, so ITS `channels.index` says `fresh` for a plain folder with no revision — a `get_symbol_source` receipt therefore probes `repo_freshness` DIRECTLY (trusting the channel would be the 1.108.176 mistake again). Visible inside one envelope and pinned.** ⚠ **Subject read from the SERVED row only, never re-read from the index** — `search_symbols` at standard detail serves no `end_line`/hash, so the receipt omits both and says so in `limitations`. Full digests + fail-closed on collision (an id that ever named two receipts names neither). `_meta.receipts` in `UNIVERSAL_META_JSON`. `schemas/evidence-receipt.schema.json` published in the same commit. ⚠ **NOT done: session keying / full expiry taxonomy / deep-freeze = P3; redacted rendering + `absence_scans_attested` rename = P4; suite parity = P5.** ⚠ **Two ungated absence assertions FOUND, not fixed, said out loud: `search_symbols`'s SEMANTIC exit emits "Do not claim this feature exists" with NO `_meta.verdict` at all (so no absence gate since 1.108.166 applies to it), and both FUSION exits emit neither. Neither can mint — the gate working — and tests pin it against the source. Fixing the prose would change bytes on calls that never asked for a receipt, so it is its own change.** New `tests/test_v1_108_183.py` (79). No tool-count/INDEX_VERSION change.
55
- **Prior (1.108.182):** — **A stall has a name and a ceiling (#375 REOPENED).** ⚠ **@dkiaulakis' re-run at 1.108.176 measured NO improvement on the subtree that stalled at 1.108.169 — 268s vs 240s, both unfinished. The 1.108.171 regex fix removed one known cost centre; it could not remove the SHAPE of the failure, which is that discovery and parsing were both unbounded and both reported nothing.** Three bounds: (1) `discover_providers(folder, budget_seconds=, skipped=)` runs each provider's detect+load under a wall-clock ceiling (`JCODEMUNCH_PROVIDER_BUDGET_SECONDS`, 30s, 0 disables); an overrun is skipped and NAMED via `providers_skipped` + a warning that states the CONSEQUENCE (symbols indexed, but no context from that provider and its import edges — route mounts, template renders — absent from the graph). (2) New `_route_utils.iter_source_files` prunes dependency dirs AT THE WALK: `ExpressProvider.load` ran FIVE recursive globs and dropped `node_modules` by substring AFTER enumerating it, same defect in `_scan_package_json_forced_paths`'s `rglob` — **measured 0.639s → 0.001s for the identical 41 files over a 9,600-file node_modules, warm and alternating.** (3) `parse_file_budgeted` bounds one file's parse (`JCODEMUNCH_PARSE_BUDGET_SECONDS`, 20s); raises `ParseBudgetExceeded`, which the three existing call sites' `except Exception` already turns into a by-name warning. ⚠ **Armed only at/above 128 KiB — the common path parses inline, unchanged.** ⚠ **THE LIMIT, STATED: a watchdog stops the CALLER waiting; it cannot stop the work.** Python cannot preempt a thread and tree-sitter is C, so an abandoned parse/provider keeps burning CPU. `ContextProvider.budget_expired()` is the cooperative half and only helps where a provider POLLS it — wired into the Express walk, not the other thirteen. ⚠ **Not yet reproduced on Darius' box; the walk + regex evidence says his 268s should be gone, and the strongest untested hypothesis is a stale resolved install (25+ leaked stdio instances, a :3049 server on the same db, hand-patched files an upgrade overwrote).** New `tests/test_v1_108_182.py` (23). No tool-count/INDEX_VERSION change.
66
- **Prior (1.108.181):** — **Uncommitted work is represented, PER SCOPE (#377 hardening item 5 — closes the last open 1.x item).** ⚠ **Git HEAD sits still while the tree holds a modified file, a brand-new UNTRACKED implementation, a deletion or a rename into/out of scope — so every freshness gate we had could report `fresh` over a corpus that never read the file the target lives in. A scan that RETURNS rows carries per-file freshness ON them; a scan that returns NOTHING has nowhere to put it.** New `subject_state.working_tree_state` gives the SCOPE its own state (`clean`/`dirty_in_scope`/`dirty_outside_scope`/`unknown`/`not_applicable`) as `verdict.working_tree`, wired into `search_symbols` / `search_text` / BOTH `get_ranked_context` exits. ⚠ **TWO restrictions are what keep it from becoming noise, and both are load-bearing: (1) work OUTSIDE the scanned scope NEVER blocks (his own distinction — a dirty file elsewhere cannot invalidate a narrow proof); (2) an edit the index has ALREADY re-read is NOT a gap — `files_not_in_index` compares each dirty in-scope path against `index.file_mtimes`, so a watcher-fresh corpus keeps proving absence. Without (2) the gate fires on EVERY developer with unsaved work in an up-to-date repo, and a signal that fires constantly is one people learn to ignore.** ⚠ **Measured for a ZERO-RESULT scan only** (a `git status` per search would tax the common path); reading is TTL-cached and SHARED with the 1.108.178 cached-negative revalidation. ⚠ **Rename handling: `R old -> new` records the NEW path — the one that exists in the tree and the one a search would have had to see;** quoted/backslashed paths normalised to compare against index paths. `working_tree` published in the verdict schema. New `tests/test_v1_108_181.py` (27). No tool-count/INDEX_VERSION change.
77
- **Older releases (1.108.180 and earlier):** see `CHANGELOG.md`. The 1.108.180 entry ("unknown freshness stops collapsing into fresh", #377 item 4) and 1.108.179 ("the subject has to hold still for the scan", item 6) are there in full.

tests/test_v1_108_183.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,71 @@ def test_the_scorer_is_pinned(self, _r183_env):
504504
eid, _ = _r183_serve_and_receipt(server, repo)
505505
assert _r183_receipts.lookup(eid)[0]["snapshot"]["scorer_version"] == SCORER_VERSION
506506

507+
def test_a_real_checkout_binds_fresh_with_both_revisions(self, tmp_path, monkeypatch):
508+
"""`fresh` is the one state a fixture cannot fake (the v1.108.180 lesson).
509+
510+
On a plain folder every receipt reports `not_tracked`, so a suite made only
511+
of folder fixtures would never exercise the path where both revisions are
512+
read and compared, which is the whole point of the snapshot binding.
513+
"""
514+
import subprocess
515+
516+
monkeypatch.setenv("CODE_INDEX_PATH", str(tmp_path / "idx"))
517+
proj = tmp_path / "gitproj"
518+
(proj / "src").mkdir(parents=True)
519+
(proj / "src" / "a.py").write_text(
520+
"def refresh_token(user):\n return user\n", encoding="utf-8"
521+
)
522+
for cmd in (
523+
["git", "init", "-q"],
524+
["git", "config", "user.email", "t@example.invalid"],
525+
["git", "config", "user.name", "t"],
526+
["git", "add", "-A"],
527+
["git", "commit", "-qm", "init"],
528+
):
529+
if subprocess.run(cmd, cwd=proj, capture_output=True).returncode != 0:
530+
pytest.skip("git is unavailable in this environment")
531+
from jcodemunch_mcp import server
532+
533+
_r183_receipts.clear_receipts()
534+
try:
535+
indexed, _ = _r183_call(server, "index_folder", {"path": str(proj)})
536+
repo = indexed["repo"]
537+
body, _ = _r183_call(
538+
server,
539+
"get_symbol_source",
540+
{
541+
"repo": repo,
542+
"symbol_id": "src/a.py::refresh_token#function",
543+
"receipt": True,
544+
"format": "json",
545+
},
546+
)
547+
snapshot = _r183_receipts.lookup(_r183_receipt_ids(body)[0])[0]["snapshot"]
548+
assert snapshot["freshness"] == "fresh"
549+
assert snapshot["indexed_revision"]
550+
assert snapshot["indexed_revision"] == snapshot["live_revision"]
551+
assert snapshot["conditions"] == []
552+
553+
# A zero-result scan measures the tree, so this one is not `unknown`.
554+
absent, _ = _r183_call(
555+
server,
556+
"search_symbols",
557+
{
558+
"repo": repo,
559+
"query": "zzz_absent_thing",
560+
"receipt": True,
561+
"format": "json",
562+
},
563+
)
564+
envelope = _r183_receipts.lookup(_r183_receipt_ids(absent)[0])[0]
565+
assert envelope["snapshot"]["working_tree"] == "clean"
566+
assert envelope["capabilities"]["proves_absence"] is True
567+
assert envelope["absence_ref"].startswith("absent:")
568+
finally:
569+
_r183_receipts.clear_receipts()
570+
_handoff_mod.clear_absences()
571+
507572
def test_coverage_is_fingerprinted_so_two_corpora_cannot_share_an_id(self):
508573
a = _r183_receipts.coverage_fingerprint({"files_indexed": 10, "complete": True})
509574
b = _r183_receipts.coverage_fingerprint({"files_indexed": 11, "complete": True})

0 commit comments

Comments
 (0)