Skip to content

Commit 32ede68

Browse files
jgravelleclaude
andcommitted
release: v1.108.188 — telemetry rows land in the store the caller named
Every reader of the perf database took a base path — ranking_db_query, WeightTuner, analyze_perf — but the writers passed none. Writes resolved through _State._base_path, which is whatever the first caller of _ensure_loaded happened to pass, and most savings writes pass nothing. So it was usually None and every row landed in ~/.code-index no matter which storage_path the tool was handed. Measured with a fresh tracker state and a boxed home: five producers wrote five rows to the default store while ranking_db_query against the named store returned zero. WeightTuner on a non-default store was therefore learning from a ledger the searches had never written to. On a default install writer and reader coincide, which is why this stayed invisible. Both tables in that file had it. ranking_events gets base_path at all six producer call sites via the new tools._utils.ledger_base_path, and tool_calls gets it from the dispatcher, which already extracts repo from the same arguments. analyze_perf reads both through one base path. _perf_db_failed now covers the default database only: one unwritable caller-supplied store must not disable telemetry for every other store. An explicit base path is not cached, because it belongs to one call. Deliberately unchanged: where the savings total lives. That counter is a process-global lifetime total loaded once, and re-anchoring it mid-process would move a user's cumulative savings between files. Pinned by a test. Nine tests from 1.108.186 and 1.108.187 were corrected: their fixtures put the ledger in a different directory from the storage path and captured rows only because writes ignored storage_path. Both releases' pins were re-verified as non-vacuous afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 38b28f9 commit 32ede68

12 files changed

Lines changed: 428 additions & 21 deletions

CHANGELOG.md

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

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

5+
## [1.108.188] - 2026-07-26 - telemetry rows land in the store the caller named
6+
7+
### Fixed
8+
9+
- ⚠ **Every reader of the perf database took a base path; the writers passed none.**
10+
`ranking_db_query`, `WeightTuner` and `analyze_perf` all resolve through an explicit
11+
base, but writes resolved through `_State._base_path` — whatever the FIRST caller of
12+
`_ensure_loaded` happened to pass. Most savings writes pass nothing, so it was
13+
usually `None` and every row landed in `~/.code-index` no matter which
14+
`storage_path` the tool was handed.
15+
16+
**A search against a named store therefore wrote to one database and read from
17+
another.** Measured, not argued: with a fresh tracker state and a boxed home, five
18+
producers wrote five rows to `~/.code-index` while
19+
`ranking_db_query(base_path=<named store>)` returned zero. Reported as an adjacent
20+
finding in 1.108.186, left open in 1.108.187, fixed here.
21+
22+
- **`WeightTuner` on a non-default store was learning from a ledger the searches had
23+
never written to** — the same base path it reads through was the one nothing wrote
24+
to. On a default install writer and reader coincided, which is why this stayed
25+
invisible.
26+
27+
- **Both tables in that file had it.** `ranking_events` gets `base_path` at all six
28+
producer call sites (`search_symbols` lexical/semantic/fusion, `get_ranked_context`
29+
main/fusion, `plan_turn`), and `tool_calls` gets it from the dispatcher, which
30+
already extracts `repo` from the same arguments. `analyze_perf` reads both through
31+
one base path, so fixing one and not the other would have left the tool half blind.
32+
33+
- ⚠ **`_perf_db_failed` is a process-wide kill switch, and it now covers the DEFAULT
34+
database only.** It was written when there was one path it could possibly mean; one
35+
unwritable caller-supplied store must not take telemetry down for every other store
36+
in the process.
37+
38+
### Changed
39+
40+
- New `tools._utils.ledger_base_path(store)`, returning `None` when a store cannot
41+
name itself — which restores the previous default rather than dropping the row.
42+
- An explicit base path is **not cached**. It belongs to one call, not to the process:
43+
caching it would make the second store in a session silently write to the first
44+
one's database. The default path keeps its cache.
45+
- ⚠ **Deliberately NOT changed: where the savings TOTAL lives.** `_ensure_loaded`
46+
still pins `_base_path` on first call. That counter is a process-global lifetime
47+
total loaded once from disk, and re-anchoring it mid-process would move a user's
48+
cumulative savings between files. Telemetry rows are per-call facts about a named
49+
store; the savings total is not. Pinned by a test so the asymmetry is a decision
50+
rather than an oversight.
51+
52+
### Notes
53+
54+
- ⚠ **Nine tests from 1.108.186 and 1.108.187 had to be corrected, and that is part of
55+
the finding:** their fixtures put the ledger in a different directory from the
56+
storage path and captured rows only BECAUSE writes ignored `storage_path`. They now
57+
point at the store, which is where rows actually go. The 1.108.186 and 1.108.187
58+
pins were re-verified as non-vacuous under the corrected fixtures.
59+
- The practical effect for tests: a test that writes ranking events no longer needs
60+
`_state._loaded = True` to stay off the developer's real `telemetry.db`, because it
61+
no longer routes through `_base_path` at all.
62+
- New `tests/test_v1_108_188.py` (14). Reverting the routing fails six of them.
63+
Compatibility pinned: a call with no `base_path` still uses the default store.
64+
- No tool-count change, no `INDEX_VERSION` change, no schema change. Replay gate green
65+
(mrr/ndcg/recall 1.0).
66+
567
## [1.108.187] - 2026-07-26 - both fusion exits record the features they measured
668

769
### Fixed

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# jcodemunch-mcp — Project Brief
22

33
## Current State
4-
- **Version:** 1.108.187 — **Both fusion exits record the ledger features they MEASURED.** First of the two adjacent findings .186 reported instead of folding in. ⚠ **`_get_ranked_context_fusion` passed NO `extract_ledger_features` payload at all, so `top1_score`/`top2_score`/`identity_hit` were DEFAULTS on every row it ever wrote — and three of regret's six signals read those columns. A default is indistinguishable from a measurement once it is in the ledger.** Identity now comes from the identity channel's own `raw_scores` (that exit BUILDS the channel, so unlike its sibling it knows per symbol; `build_identity_channel` admits only symbols scoring >0). ⚠ **`search_symbols_fusion` had the NARROWER half of the same defect — it passed the features but built their input as `{"score": s}` with no `identity` key, so `identity_hit` was False on every fusion row regardless. Fixed TOGETHER: shipping one honest and one false guarantees a third release on the same column.** ⚠⚠ **THE LESSON, and a FAILING TEST found it rather than reading: `compute_confidence` SNIFFS the same `identity` key when no `has_identity_match` is passed (1.0 known-true / 0.7 unknown / 0.6 known-false), so sharing one input would have MOVED the confidence of every fusion search (measured 0.742 → 0.783). The ledger input is therefore SEPARATE from the confidence input — recording a column must not become a ranking change. Pinned BEHAVIOURALLY: `attach_confidence` must receive rows WITHOUT the key on the same call where the ledger features receive rows WITH it.** ⚠ **`ledger_trust.identity_label_is_trustworthy` has a WEAKER discriminator than the semantic one ON PURPOSE: `identity_hit` has no value a pre-fix row alone can carry (pre-fix is always 0, and 0 is an honest post-fix answer), so matching the column would discard good evidence forever. What IS exact: a row that RETURNED symbols while recording NO `top1_score` can only come from the featureless exit.** ⚠ **`search_symbols_fusion`'s history is NOT separable and NO heuristic is used — it always recorded top scores, so pre-fix rows are indistinguishable from honest post-fix ones; the recency window is the only remedy, stated rather than papered over.** ⚠ **An identity guard on regret's `vocabulary_gap` was UNREACHABLE and was REMOVED — found by a failing non-vacuity test: the signal requires `semantic_used`, and such a row is already refused by the .186 rule. An unreachable guard reads like protection and is not.** Also: a pre-fix single-result row was clustered `thin_result` on a default (`top1 is None` counts as thin — a MISSING measurement read as a weak one); only those rows are skipped, every other producer unchanged. `events_without_ledger_features` / `no_ledger_features` disclosed, absent-not-zero on a clean ledger. New `tests/test_v1_108_187.py` (24). No tool-count/INDEX_VERSION/schema change.
4+
- **Version:** 1.108.188 — **Telemetry rows land in the STORE THE CALLER NAMED.** Second of .186's two findings, left open by .187. ⚠⚠ **EVERY READER of the perf db took a base path (`ranking_db_query`, `WeightTuner`, `analyze_perf`); the WRITERS passed NONE — they resolved through `_State._base_path`, which is whatever the FIRST caller of `_ensure_loaded` happened to pass, and most savings writes pass nothing. So it was usually `None` and every row landed in `~/.code-index` NO MATTER what `storage_path` the tool was handed. A search against a named store WROTE TO ONE DATABASE AND READ FROM ANOTHER.** MEASURED with a fresh state + boxed home: 5 producers wrote 5 rows to the default while `ranking_db_query(base_path=named)` returned ZERO. **`WeightTuner` on a non-default store was learning from a ledger the searches had never written to; on a DEFAULT install writer and reader coincide, which is why this stayed invisible.** **BOTH tables had it:** `ranking_events` via `base_path=` at all SIX producer call sites (new `tools._utils.ledger_base_path(store)`), and `tool_calls` from the dispatcher (which already extracted `repo` from the same arguments). `analyze_perf` reads both through ONE base path, so fixing one would leave it half blind. ⚠ **`_perf_db_failed` is a process-wide kill switch and now covers the DEFAULT db ONLY — one unwritable caller-supplied store must not take telemetry down for every other store.** ⚠ **An explicit base is NOT cached (it belongs to one CALL, not the process; caching it makes the SECOND store in a session silently write to the FIRST one's db); the default keeps its cache.** ⚠ **DELIBERATELY NOT CHANGED: where the savings TOTAL lives. `_ensure_loaded` still pins `_base_path` on first call — that counter is a process-global LIFETIME total loaded once, and re-anchoring it mid-process would move a user's cumulative savings between files. Telemetry rows are per-call facts about a named store; the savings total is not. PINNED BY A TEST so the asymmetry is a decision, not an oversight.** ⚠ **NINE tests from .186/.187 had to be CORRECTED and that is part of the finding: their fixtures put the ledger in a DIFFERENT directory from the storage path and captured rows only BECAUSE writes ignored `storage_path`. Both releases' pins re-verified non-vacuous under the corrected fixtures.** Practical effect: a test writing ranking events no longer needs `_state._loaded = True` to stay off the real telemetry.db. New `tests/test_v1_108_188.py` (14). No tool-count/INDEX_VERSION/schema change.
5+
- **Prior (1.108.187):** — **Both fusion exits record the ledger features they MEASURED.** First of the two adjacent findings .186 reported instead of folding in. ⚠ **`_get_ranked_context_fusion` passed NO `extract_ledger_features` payload at all, so `top1_score`/`top2_score`/`identity_hit` were DEFAULTS on every row it ever wrote — and three of regret's six signals read those columns. A default is indistinguishable from a measurement once it is in the ledger.** Identity now comes from the identity channel's own `raw_scores` (that exit BUILDS the channel, so unlike its sibling it knows per symbol; `build_identity_channel` admits only symbols scoring >0). ⚠ **`search_symbols_fusion` had the NARROWER half of the same defect — it passed the features but built their input as `{"score": s}` with no `identity` key, so `identity_hit` was False on every fusion row regardless. Fixed TOGETHER: shipping one honest and one false guarantees a third release on the same column.** ⚠⚠ **THE LESSON, and a FAILING TEST found it rather than reading: `compute_confidence` SNIFFS the same `identity` key when no `has_identity_match` is passed (1.0 known-true / 0.7 unknown / 0.6 known-false), so sharing one input would have MOVED the confidence of every fusion search (measured 0.742 → 0.783). The ledger input is therefore SEPARATE from the confidence input — recording a column must not become a ranking change. Pinned BEHAVIOURALLY: `attach_confidence` must receive rows WITHOUT the key on the same call where the ledger features receive rows WITH it.** ⚠ **`ledger_trust.identity_label_is_trustworthy` has a WEAKER discriminator than the semantic one ON PURPOSE: `identity_hit` has no value a pre-fix row alone can carry (pre-fix is always 0, and 0 is an honest post-fix answer), so matching the column would discard good evidence forever. What IS exact: a row that RETURNED symbols while recording NO `top1_score` can only come from the featureless exit.** ⚠ **`search_symbols_fusion`'s history is NOT separable and NO heuristic is used — it always recorded top scores, so pre-fix rows are indistinguishable from honest post-fix ones; the recency window is the only remedy, stated rather than papered over.** ⚠ **An identity guard on regret's `vocabulary_gap` was UNREACHABLE and was REMOVED — found by a failing non-vacuity test: the signal requires `semantic_used`, and such a row is already refused by the .186 rule. An unreachable guard reads like protection and is not.** Also: a pre-fix single-result row was clustered `thin_result` on a default (`top1 is None` counts as thin — a MISSING measurement read as a weak one); only those rows are skipped, every other producer unchanged. `events_without_ledger_features` / `no_ledger_features` disclosed, absent-not-zero on a clean ledger. New `tests/test_v1_108_187.py` (24). No tool-count/INDEX_VERSION/schema change.
56
- **Prior (1.108.186):** — **The ranking ledger stops claiming a channel that never ran.** ⚠ **`_get_ranked_context_fusion` recorded `semantic_used=True` on EVERY row while building lexical/identity/structural and NEVER a similarity channel — its own `_meta.channels` listed the three and (since .185) its verdict said `channels.semantic: "off"`, so the RESPONSE and the LEDGER disagreed about the same call.** Disclosed as deferred in .185 because the column feeds a LEARNED parameter; this is the follow-through. Flag now DERIVED (`any(ch.name == "similarity" for ch in channels)`) not hardcoded `False`, so a future similarity channel is picked up with no second edit (the shape `search_symbols_fusion` already had); the verdict's `semantic_channel` comes off the SAME derivation so the two cannot diverge again. ⚠⚠ **FLIPPING THE FLAG IS NECESSARY AND NOT SUFFICIENT — the ledger is APPEND-ONLY. REPRODUCED FIRST: 30 honest lexical rows @0.80 + 30 mislabelled fusion rows @0.40 walked `semantic_weight` 0.5 → 0.45, on a ledger whose honest labels DECLINE to move it.** New `retrieval/ledger_trust.py` = the ONE rule, shared by all three consumers instead of copied. **The rows are EXACTLY identifiable, no heuristic: `tool="get_ranked_context_fusion"` is written by no other producer, so pre-fix = that tool with `semantic_used=1` and post-fix = the same tool with `0`. A tool-ONLY rule would discard the fix's own honest rows.** ⚠ **THIRD CONSUMER FOUND: `suggest_corrections` was reporting a vocabulary gap on calls where identity matching RAN.** Regret's `vocabulary_gap` is the conjunction `not identity_hit and semantic_used`, and that exit hardcoded the semantic flag AND passed NO ledger features — so `identity_hit` defaulted to 0 while the identity channel was one of the three it built. Every confident fusion call was a textbook match, and those clusters become config patches shown to the user. ⚠ **HISTORICAL ROWS: EXCLUDED + DISCLOSED, neither rewritten nor counted as OFF. A one-time corrective `UPDATE` was rejected on CHARTER grounds (the tuner has only ever written the `tuning.jsonc` sidecar; `suggest_corrections` is read-only) — not convenience. Aging them out over 90 days was rejected too: that is learning from a corpus we KNOW is mislabelled. UNKNOWN IS A THIRD ANSWER, not a vote for `semantic_used=0` — folding them into the OFF group is the same error mirrored and moves the weight just as far.** `events_semantic_label_unknown` (tuner signals + `analyze_regret`), `semantic_label_unknown` (`analyze_perf --ledger`). ⚠ **The exclusion EXPIRES and a drift guard says when** — it is only honest while that exit builds no similarity channel. **Blast radius stated plainly: the ledger is OPT-IN (`perf_telemetry_enabled`, default False), so most installs hold no rows and were never affected.** New `tests/test_v1_108_186.py` (22). No tool-count/INDEX_VERSION/schema change.
6-
- **Prior (1.108.185):** — **Fusion gets a verdict, and fusion CAN prove absence. EVERY retrieval exit in the suite now carries a verdict.** The last two silent exits (`_search_symbols_fusion`, `_get_ranked_context_fusion`) emitted no verdict AND no negative evidence — nothing false, nothing honest either. Both now build the same verdict as their tools' other exits and finally receive `_state_before` (#377 item 6). ⚠ **THE REVIEW REACHED THE OPPOSITE CONCLUSION TO .184's, which is why modes are REVIEWED rather than having a rule ported onto them: `build_lexical_channel` and `build_identity_channel` each score EVERY eligible candidate with no cap, and `fuse` keeps every symbol appearing in ANY channel — so an empty fused set means every candidate scored zero on both BM25 and identity, the same CORPUS FACT the lexical path's absence rests on. The similarity channel can only ADD symbols, never remove one, so its absence weakens RANKING and cannot manufacture a false absence. That asymmetry is what the semantic exit lacked ⇒ semantic carries `absence_unprovable`, fusion does not.** ⚠ **DEFECT 1, and it made `absent` unreachable anyway: `build_structural_channel` treated "restrict to NOTHING" as "no restriction" (`if candidate_ids and ...` — a FALSY check), while both callers pass `lexical ∪ identity`. A query matching nothing produced an EMPTY set, skipped the filter, and let every symbol with nonzero PageRank in — so a no-match fusion query returned THE WHOLE REPOSITORY ranked by centrality, and with no verdict nobody could see it was reporting centrality as relevance.** `None` and `set()` are now different. ⚠ **DEFECT 2, a read that WROTE: `EmbeddingStore._connect` runs a WAL pragma + CREATE-TABLE on EVERY connection, so the fusion "does this repo have embeddings" probe wrote to the db it was reading, bumped `_db_mtime_ns`, and made the FIRST fusion search of any process report `rebuilding` + `moved_during_scan` ⇒ `degraded` ⇒ `absent` unreachable. Same defect `runtime/confidence.py` was fixed for.** New `get_all_readonly`. ⚠⚠ **`mode=ro` ALONE DOES NOT FIX IT and the first attempt shipped nothing: read-only in SQLite means "cannot modify the DATABASE", not "cannot touch the filesystem" — a plain ro connection to a WAL db still CREATES the `-wal`/`-shm` sidecars (measured None → present), and `_db_mtime_ns` maxes over `.db` AND `-wal`. `immutable=1` is the flag that guarantees no sidecar; the trade-off (vectors in an un-checkpointed WAL go unread ⇒ similarity channel skipped) can only weaken RANKING, never fabricate absence.** Both no-candidate early returns now carry `incomplete: empty_scope` (#377 item 9). **NEW `verdict.retrieval_verdict_for_index`** — sibling of `symbol_verdict_for_index`/`file_verdict_for_index`; three exits shipped verdict-less because adding one meant reproducing FIVE call patterns by hand, so the wrapper is the structural answer. ⚠ **The receipt gate fired on its own author a SECOND time** (both .183 fusion pins failed) → `completeness_by_mode["fusion"]` on both producers + `fusion` in both `mode_args`. New `tests/test_v1_108_185.py` (39) incl. a drift guard that every emitted `search_mode` is declared. No tool-count/INDEX_VERSION/schema change.
77
- **Older releases (1.108.182 and earlier):** see `CHANGELOG.md`. The 1.108.182 entry ("a stall has a name and a ceiling", #375) and the 1.108.177-.181 #377 hardening arc are there in full.
88
- **INDEX_VERSION:** 17
9-
- **Tests:** 6139 passed, 7 skipped (1.108.187) + the KNOWN 12 local-ONNX `test_semantic_search` env failures (green in CI on all 4 ubuntu jobs — never read them as a regression)
9+
- **Tests:** 6153 passed, 7 skipped (1.108.188) + the KNOWN 12 local-ONNX `test_semantic_search` env failures (green in CI on all 4 ubuntu jobs — never read them as a regression)
1010
- **Python:** >=3.10
1111
- **Tool count:** 90 in `full` (front door hidden; +1 v1.108.111 `get_parity_map`, +1 v1.108.112 `get_decorator_census`, +1 v1.108.113 `get_architecture_metrics`); `tool_surface=counter` exposes a 3-tool front door (`order`/`menu`/`route`) instead
1212

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.187"
3+
version = "1.108.188"
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)