Skip to content

Commit 45b7c1a

Browse files
jgravelleclaude
andcommitted
release: v1.108.165 — claim-scoped evidence in the handoff contract (#377 phase 1)
A handoff section may now carry caller-authored `claims`, each with its own `evidence_refs`. v1 proved a ref was retrieved this session but never bound it to a sentence: refs landed in one global block at the end of the body. New `_validate_claims` takes {id, statement, evidence_refs, classification?}. Ids are unique across the whole handoff, not per section, since the id is the citation anchor and two sections owning one id makes a citation ambiguous. Statements and classifications are preserved verbatim. Each claim's refs are attested separately through the unchanged `_validate_evidence`, so an unknown ref returns invalid_claims [{claim_id, unknown_refs}] and names the claim that cited it rather than vanishing into one global failure list. Three decisions the proposal left open: - The input picks the contract. No claims anywhere means the schema stays handoff/v1 and the body is byte-identical to v1; claims_attested is omitted rather than reported as 0. - Claims can satisfy evidence_refs, so a caller who scoped everything to claims need not restate it globally. Strictly more permissive. - Claim refs join the canonical index, caller order first, so a v1 consumer reading a v2 handoff still sees every reference where it expects. Design by @mightydanp in #377, which stays open as the arc tracker. Phase 1 deliberately does not narrow what counts as a match; that is phase 2 and is disclosed on the issue. core_compact unchanged at 3996. tests/test_v1_108_165.py (18). Suite 5595. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 95f32ee commit 45b7c1a

7 files changed

Lines changed: 474 additions & 19 deletions

File tree

CHANGELOG.md

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

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

5+
## [1.108.165] - 2026-07-23 - claim-scoped evidence in the handoff contract (handoff/v2 phase 1, #377)
6+
7+
### Added
8+
9+
- **A handoff section may now carry caller-authored `claims`, each with its own
10+
`evidence_refs`.** v1 proved every cited ref was retrieved this session, but
11+
it could not say WHICH retrieval backs which sentence: refs landed in one
12+
global block at the end of the body. Phase 1 of the `handoff/v2` design
13+
proposed by @mightydanp in [#377](https://github.com/jgravelle/jcodemunch-mcp/issues/377)
14+
closes that gap.
15+
16+
Each claim is `{id, statement, evidence_refs, classification?}`. New
17+
`_validate_claims` requires ids unique across the WHOLE handoff, not per
18+
section, since the id is the machine-readable anchor a caller cites and two
19+
sections owning the same id would make that citation ambiguous. Statements
20+
and classifications are preserved verbatim; the server never rewrites one.
21+
Each claim's refs are attested separately through the existing
22+
`_validate_evidence`, so an unknown ref returns `invalid_claims:
23+
[{claim_id, unknown_refs}]` and names the claim that cited it instead of
24+
vanishing into one global failure list. `render_handoff` prints the claim as
25+
a `###` heading with its evidence indented beneath it.
26+
27+
Three decisions the proposal left open:
28+
29+
- **The input picks the contract.** No claims anywhere means the schema
30+
string stays `jcodemunch.handoff/v1` and the body is byte-identical to
31+
what v1 rendered; `claims_attested` is omitted from the receipt entirely
32+
rather than reported as `0`. Any claim promotes the handoff to
33+
`jcodemunch.handoff/v2`.
34+
- **Claims can satisfy `evidence_refs`.** A caller who scoped everything to
35+
claims should not have to restate it globally, so the top-level list may be
36+
empty when claims carry refs. Strictly more permissive; no existing call
37+
changes.
38+
- **Claim refs join the canonical evidence index**, caller order first, so a
39+
v1 consumer reading a v2 handoff still sees every reference in the place it
40+
expects.
41+
42+
Section `content` becomes optional only for a section that carries claims.
43+
`finalize_handoff` is standard tier, so `core_compact` is unchanged at 3996.
44+
New `tests/test_v1_108_165.py` (18, incl. the byte-identical v1 guard). No
45+
`INDEX_VERSION` or tool-count change.
46+
47+
Known limit, disclosed on #377 before anyone builds against it: phase 1 does
48+
not narrow what counts as a match. `_validate_evidence` still attests a ref
49+
against the file component of a served id, so citing a whole file attests
50+
even when one unrelated symbol from it was served. Narrowing that is phase 2
51+
(evidence receipts), which is deferred.
52+
53+
Suite parity same day: jdocmunch-mcp v1.116.0 + jdatamunch-mcp v1.25.0.
54+
555
## [1.108.164] - 2026-07-23 - A path-shaped `repo` arg returns a routed error, not a raw storage crash (#376)
656

757
### Fixed

CLAUDE.md

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

33
## Current State
4-
- **Version:** 1.108.164 — **A path-shaped `repo` arg returns a routed error instead of a raw storage crash (#376, split out of #375; reported from a daily Ubuntu install).** `tools/_utils.resolve_repo` split any `repo` containing `/` on the FIRST separator and returned the halves UNVALIDATED (`_utils.py:117`), so `repo="src/auth/service.ts"` → `owner="src"`, `name="auth/service.ts"`. Callers DO catch `ValueError` from the resolver (`search_symbols.py:626`), but the bad pair then reached `store.load_index` and raised `ValueError: Path separator in name` from `SQLiteIndexStore._safe_repo_component` (`sqlite_store.py:2881`) — **outside every handler**. Since `_safe_repo_component` rejects a separator in `name` at WRITE time, an `owner/name` id can never legitimately carry a second one, so **every two-slash `repo` argument was a guaranteed crash**, on every tool routing through the shared resolver (not just search_symbols). Reproduced on .163, not only the reporter's .158. Fix: a post-split name still containing `/` or `\` is treated as a path — routed to `_resolve_path_repo` first (which now also picks up bare relative MULTI-SEGMENT paths like `apps/web/src` that `_looks_like_path` is deliberately too conservative to match, v1.108.159's acceptance only covered `.`/`~`/backslash/absolute), else raises new `_path_shaped_repo_error` naming `resolve_repo` + `index_folder` + **`file_pattern=` when the arg has a file suffix** (the reporter's ask — an agent passing `repo=<file>` usually meant to scope the search). `_resolve_path_repo`'s unindexed-path raise routes through the same helper so both give one message. **Single-separator ids byte-identical** (`tools/agents.ts` still takes the id path → clean in-band not-loadable error). New `tests/test_v1_108_164.py` (8, incl. the single-slash no-regression guard). NO schema/tool-count/INDEX_VERSION change. Suite 5577 passed + the known 12 local-ONNX semantic env fails.
4+
- **Version:** 1.108.165 — **Claim-scoped evidence: a handoff section may carry caller-authored `claims`, each with its OWN `evidence_refs` (handoff/v2 phase 1, [#377](https://github.com/jgravelle/jcodemunch-mcp/issues/377), @mightydanp).** v1 proved a ref was retrieved this session but never bound it to a sentence — `_validate_evidence` attested against served ids/file components and `render_handoff` dumped every ref into ONE global block. Now `_validate_claims` (new) takes `{id, statement, evidence_refs, classification?}`; **ids unique across the WHOLE handoff, not per section** (the id is the citation anchor — two sections owning one id makes a citation ambiguous); statements/classifications preserved VERBATIM (server never authors); each claim's refs attested SEPARATELY through the unchanged `_validate_evidence`, so an unknown ref returns `invalid_claims: [{claim_id, unknown_refs}]` naming the claim instead of one global failure list. `render_handoff` prints `### <statement>` + `- Claim id:` + indented evidence, and now takes the schema string as a param. **Three calls the proposal left open:** (1) **the INPUT picks the contract** — no claims anywhere ⇒ schema stays `jcodemunch.handoff/v1`, body BYTE-IDENTICAL to v1, `claims_attested` omitted (not `0`); any claim ⇒ `.../v2`. (2) **claims can satisfy `evidence_refs`** (top-level may be empty when claims carry refs — strictly more permissive, no existing call changes). (3) **claim refs join the canonical index**, caller order first, so a v1 consumer reading a v2 handoff sees every ref where it expects. Section `content` optional ONLY when claims present. `finalize_handoff` is standard tier ⇒ **core_compact unchanged 3996**; standard/full +123 tok (baseline refreshed). New `tests/test_v1_108_165.py` (18, incl. the byte-identical-v1 guard). NO INDEX_VERSION/tool-count change. ⚠ **Known limit, disclosed publicly on #377 first:** phase 1 does NOT narrow what counts as a match — the file-component broadening in `_validate_evidence` means citing a whole file still attests when one unrelated symbol from it was served. That's phase 2 (evidence receipts), DEFERRED. Suite parity same day: jdoc v1.116.0 + jdata v1.25.0. **#377 stays OPEN as the arc tracker; phase 3 (absence evidence) is next and is the one we want most.**
5+
- **Prior (1.108.164):** — **A path-shaped `repo` arg returns a routed error instead of a raw storage crash (#376, split out of #375; reported from a daily Ubuntu install).** `tools/_utils.resolve_repo` split any `repo` containing `/` on the FIRST separator and returned the halves UNVALIDATED (`_utils.py:117`), so `repo="src/auth/service.ts"` → `owner="src"`, `name="auth/service.ts"`. Callers DO catch `ValueError` from the resolver (`search_symbols.py:626`), but the bad pair then reached `store.load_index` and raised `ValueError: Path separator in name` from `SQLiteIndexStore._safe_repo_component` (`sqlite_store.py:2881`) — **outside every handler**. Since `_safe_repo_component` rejects a separator in `name` at WRITE time, an `owner/name` id can never legitimately carry a second one, so **every two-slash `repo` argument was a guaranteed crash**, on every tool routing through the shared resolver (not just search_symbols). Reproduced on .163, not only the reporter's .158. Fix: a post-split name still containing `/` or `\` is treated as a path — routed to `_resolve_path_repo` first (which now also picks up bare relative MULTI-SEGMENT paths like `apps/web/src` that `_looks_like_path` is deliberately too conservative to match, v1.108.159's acceptance only covered `.`/`~`/backslash/absolute), else raises new `_path_shaped_repo_error` naming `resolve_repo` + `index_folder` + **`file_pattern=` when the arg has a file suffix** (the reporter's ask — an agent passing `repo=<file>` usually meant to scope the search). `_resolve_path_repo`'s unindexed-path raise routes through the same helper so both give one message. **Single-separator ids byte-identical** (`tools/agents.ts` still takes the id path → clean in-band not-loadable error). New `tests/test_v1_108_164.py` (8, incl. the single-slash no-regression guard). NO schema/tool-count/INDEX_VERSION change. Suite 5577 passed + the known 12 local-ONNX semantic env fails.
56
- **Prior (1.108.163):** — **Audit P2 pair closed: WS-8 license-key transport (V12) + WS-7 benchmark integrity (V1).** **WS-8:** `org/license._check_server` now POSTs `{product, license}` as a form body (key NEVER in the URL → can't land in server/proxy access logs); `cli/install_pack` carries the key in the `X-JCM-License` header. Each has a ONE-SHOT legacy fallback keyed to the exact pre-deploy backend signature ("Missing license parameter." for validate.php POST; the no-license 403 "requires a jCodeMunch license" for packs) so ship order can't strand a paying customer — a REAL key rejection (not found/revoked/expired/"Invalid or expired") never retries. Backend CONFIRMED POST/header-aware on j.gravelle.us 2026-07-23 (live curl verification, all transports) — the two client fallbacks (`_needs_legacy_get_fallback`, `_looks_like_missing_license_response`) can be removed in a future release. Backend mirror EDITED (jjg deploys via SFTP): `C:\MCPs\web\jCodeMunch.com\validate.php` (POST form/JSON body + X-JCM-License header + legacy GET; Cache-Control: no-store) + `starter-packs-system/api/index.php` (`request_license()` helper on download/validate; internal `validate_license()` now POSTs to validate.php). NOTE: root `C:\MCPs\web\validate.php` copy is STALE (pre-jbuffer, May 16) — canonical mirror is `jCodeMunch.com\`. Bonus fix: install-pack's `raise_for_status()` rendered every 4xx JSON license/pack error as "Could not reach the starter packs server" — removed; JSON error branch now actually runs. **WS-7:** `benchmarks/results.md` REGENERATED at .163 against same-day GitHub re-indexes (express 172 / fastapi 1,000 / gin 109 files): **99.6% aggregate** (5,799,695→25,220 tok, 230x), per-query 99.1–99.9%; hand-written A/B sections preserved verbatim (regen→scratch→merge). README headline RECONCILED — the stale "95% average" table (old 34-file capped express index) replaced; README/results.md/METHODOLOGY.md/provenance measured.json all quote 99.6% now (provenance registry itself unchanged — it pins average_pct 99.6 which held; drift-guard test green). "95%+" floor phrasing elsewhere kept (conservative, true). Harness dead fallback fixed (`get_file_content_text` never existed → `store.get_file_content`). `benchmarks/token_baselines/v1.108.163.json` captured (in-process driver; NOTE `__version__` is "unknown" under bare PYTHONPATH — driver stamps version explicitly) → `analyze_perf(compare_release="1.108.163")` resolves. Replay CI gate was already wired (replay.yml, 2% gate) — WS-7 acceptance met. WS-8 CI siblings (SHA-pinned sign-release, dependabot github-actions) verified already done; `uv sync --locked` still deliberately parked (uv.lock drifted). Tests `tests/test_v1_108_163.py` (28 w/ params); suite 5569 passed + the known 12 local-ONNX semantic env fails. **Audit P2 queue now EMPTY — remaining audit work is P3s only.**
67
- **Prior (1.108.162):** — **Canonical handoff contract: `finalize_handoff` + `munch://handoff/<id>` (#374, @mightydanp, CLOSED).** Server-owned canonical final handoff (`jcodemunch.handoff/v1`, shape 3 as accepted): the assistant AUTHORS the analysis; new `handoff.py` owns deterministic assembly (caller sections + named appendices, each exactly once, dup names rejected), session-scoped in-memory persistence (process == session, `_steer_state` precedent, `clear_handoffs()` test hook), identity (`handoff_id = sha256(body)[:16]` — same inputs ⇒ same body/id/hash), and immutable serving. **Marquee = evidence attestation:** `evidence_refs` validate against the session retrieval record — new `token_tracker.served_symbol_ids()` snapshot of the v1.108.146 yield tracker (populated by search_symbols/get_ranked_context `note_served`); a ref = served symbol id OR its file path; unknown refs → in-band error dict → `CallToolResult(isError=True)` + `unknown_refs` + hint (v1.108.74 contract). Receipt `{schema, handoff_id, resource_uri, sha256, length(bytes utf-8), canonical:true(ADVISORY only), evidence_count, appendices}`. Resource follows the #371 pattern: `list_resources()` appends per-handoff rows, `read_resource()` resolves `munch://handoff/<id>` → text/markdown, unknown → ValueError. Registration: all surfaces (_CANONICAL_TOOL_NAMES / Session-Aware Routing category / _TOOL_TIER_STANDARD / Tool schema / dispatch / config.py all_tools ×2); counter `STATE_CHANGING_ACTIONS` (order needs allow_state_change=true; readOnlyHint:false derives) + `EXAMPLES` entry (live-schema-validated). Standard tier — core_compact unchanged (baseline refreshed: core_compact 3996, still <4000 live-ceiling). Tool count **90 → 91** full (test_server counts + expected-names bumped). No char limit, no repo writes, nothing persisted to disk. README "Canonical handoff" section. Tests `tests/test_v1_108_162.py` (22, modeled on the reporter's own citations: v1_108_74 shapes / v1_108_152 resource / counter example parity); suite 5528. **Suite parity (jdoc/jdata) = eventual, versioned contract, NOT a blocker; memory [[project-handoff-contract]] updated.**
78
- **Prior (1.108.161):** — **BM25 tokenizer: Unicode word splitting + CJK character bigrams (suite parity with jdoc v1.114.1 / jdata v1.23.1; jdoc #91 class, @tetiz123).** `search_symbols._TOKEN_RE` was `[a-zA-Z0-9]{2,}` — every non-ASCII char a separator, so CJK symbol names/summaries/docstrings produced ZERO BM25 tokens (lexical channel dead for those corpora) and accented Latin mangled (`café`→`caf`). Fix: `_TOKEN_RE=[^\W_]+` + new `_CJK_RE` (Hangul Jamo/compat/syllables, Hiragana/Katakana+ext, Han unified+ExtA+compat) space-pads runs pre-split (mixed-script tokens split cleanly), then pure-CJK tokens expand to overlapping character bigrams via `_cjk_bigrams` (lone char passes through; the <2 length filter now applies to non-CJK only); CJK tokens skip stemming/abbreviation (English-only). Same expansion at index + query time (both route through `_tokenize`) ⇒ bigram overlap is the match signal. ASCII tokenization byte-identical (camel split/stem/abbrev intact — replay golden unaffected). NO reindex (BM25 tokenizes stored fields at scoring time), NO schema/tool-count/INDEX_VERSION change. Tests `tests/test_v1_108_161.py` (8); full suite 5506 passed.

benchmarks/schema_baseline.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"core_compact": 3996,
33
"core_full": 5419,
4-
"standard_compact": 18752,
5-
"standard_full": 20412,
6-
"full_compact": 20062,
7-
"full_full": 21722
4+
"standard_compact": 18875,
5+
"standard_full": 20535,
6+
"full_compact": 20185,
7+
"full_full": 21845
88
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.164"
3+
version = "1.108.165"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

0 commit comments

Comments
 (0)