Skip to content

Commit 2fd7db5

Browse files
jgravelleclaude
andcommitted
release: v1.108.186 — the ledger stops claiming a channel that never ran
get_ranked_context's fusion exit recorded semantic_used=True on every ranking-ledger row while building lexical, identity and structural channels and never a similarity one. Its own _meta.channels listed the three and its verdict said channels.semantic: "off", so the response and the ledger disagreed about the same call. Deferred in 1.108.185 because the column feeds a learned parameter; this is the follow-through. The flag is derived from the channels actually built rather than hardcoded False, so a future similarity channel is picked up with no second edit, and the verdict's semantic_channel now comes off the same derivation. Flipping it was not sufficient: the ledger is append-only and WeightTuner learns semantic_weight by splitting rows on that column. Reproduced first — 30 honest lexical rows at 0.80 plus 30 mislabelled fusion rows at 0.40 walked semantic_weight 0.5 to 0.45 on a ledger whose honest labels decline to move it. New retrieval/ledger_trust.py holds the one rule, shared by all three consumers. The rows are exactly identifiable: tool="get_ranked_context_fusion" is written by no other producer. Historical rows are excluded and disclosed, neither rewritten nor counted as semantic-off. A corrective UPDATE was rejected on charter grounds, and folding them into the off group would move the weight just as far. Also fixed: regret's vocabulary_gap signal reported "the agent's term doesn't match a symbol name" on every confident fusion call, because that exit passed no ledger features either, so identity_hit defaulted to 0 while the identity channel ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4e49d46 commit 2fd7db5

9 files changed

Lines changed: 655 additions & 14 deletions

File tree

CHANGELOG.md

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

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

5+
## [1.108.186] - 2026-07-26 - the ledger stops claiming a channel that never ran
6+
7+
### Fixed
8+
9+
- ⚠ **`get_ranked_context`'s fusion exit recorded `semantic_used=True` on every
10+
ranking-ledger row while building no similarity channel.** That exit builds
11+
lexical, identity and structural — its own `_meta.channels` lists the three, and
12+
since v1.108.185 its verdict reports `channels.semantic: "off"` — so the response
13+
and the ledger disagreed about the same call, and the ledger was the one that was
14+
wrong. Disclosed as deliberately deferred in v1.108.185 because the column feeds a
15+
learned parameter; this is the follow-through.
16+
17+
The flag is now derived from the channels actually built
18+
(`any(ch.name == "similarity" for ch in channels)`) rather than hardcoded to
19+
`False`, so adding a similarity channel there picks it up with no second edit —
20+
the same shape `search_symbols_fusion` already had. The verdict's
21+
`semantic_channel` now comes off the same derivation, so the two cannot diverge
22+
again.
23+
24+
- ⚠ **Flipping the flag was necessary and not sufficient: the ledger is
25+
append-only.** `WeightTuner` learns a per-repo `semantic_weight` by splitting rows
26+
on that column and comparing mean confidence between the groups, so pre-fix rows
27+
keep steering it for the whole 90-day recency window. **Reproduced before fixing:
28+
30 honest lexical rows at 0.80 confidence plus 30 mislabelled fusion rows at 0.40
29+
walked `semantic_weight` from 0.5 to 0.45, on a ledger whose honest labels decline
30+
to move it at all.**
31+
32+
New `retrieval/ledger_trust.py` holds the one rule that says which labels are not
33+
evidence, shared by all three consumers rather than copied into each. **The rows
34+
are exactly identifiable with no heuristic:** they carry
35+
`tool = "get_ranked_context_fusion"`, a value no other producer writes, so a
36+
pre-fix row is precisely that tool with `semantic_used = 1` and a post-fix row is
37+
the same tool with `0`. A tool-only rule would have discarded the fix's own honest
38+
rows.
39+
40+
- ⚠ **`suggest_corrections` was reporting a vocabulary gap on calls where identity
41+
matching ran.** Regret's `vocabulary_gap` signal is the conjunction
42+
`not identity_hit and semantic_used`, and that exit hardcoded the semantic flag
43+
AND passed no ledger features at all — so `identity_hit` defaulted to 0 while the
44+
identity channel was one of the three it built. Every confident fusion call was a
45+
textbook match for "the agent's term doesn't match a symbol name", and those
46+
clusters become config patches shown to the user. Untrusted rows are dropped from
47+
that signal.
48+
49+
### Changed
50+
51+
- **The historical rows are excluded, disclosed, and neither rewritten nor counted
52+
as semantic-off.** Three options were weighed. A one-time corrective `UPDATE` was
53+
rejected on charter grounds, not convenience: the tuner has only ever written the
54+
`tuning.jsonc` sidecar and `suggest_corrections` is read-only by charter, so
55+
rewriting a user's telemetry rows is a charter change. Letting the 90-day window
56+
age them out was also rejected — that is learning from a corpus we know is
57+
mislabelled. **Unknown is a third answer, not a vote for `semantic_used=0`:**
58+
folding those rows into the OFF group would be the same error mirrored, and it
59+
would move the weight just as far. Every consumer that reads the column now
60+
discloses the count — `events_semantic_label_unknown` on the tuner's signals and
61+
on `analyze_regret`, `semantic_label_unknown` per repo in `analyze_perf --ledger`.
62+
63+
- ⚠ **The exclusion expires, and a drift guard says when.** It is only honest while
64+
that exit builds no similarity channel; if one is added, its `semantic_used=1`
65+
rows become real evidence and the rule would start discarding good data. A test
66+
fails if the exit grows a similarity channel while the rule is still
67+
unconditional.
68+
69+
### Notes
70+
71+
- **Real blast radius is small and worth stating plainly.** The ledger is opt-in
72+
(`perf_telemetry_enabled`, default `False`), so most installs hold no rows at all
73+
and were never affected. Installs that enabled telemetry and used
74+
`get_ranked_context(fusion=True)` have mislabelled rows, and for them the learned
75+
weight and the correction suggestions were both reading a lie.
76+
- Compatibility is pinned rather than asserted: a pre-fix ledger stays readable and
77+
unrewritten, and the new disclosure keys are absent — not zero — for a caller
78+
whose ledger holds no mislabelled row.
79+
- New `tests/test_v1_108_186.py` (22), including the real-route ledger check, a
80+
non-vacuity test for the derivation (patch a channel to be named `similarity` and
81+
both the ledger and the verdict must follow), non-vacuity tests that show the same
82+
ledger moving the weight and raising the cluster with the guard disabled, and the
83+
drift guard. No tool-count change, no `INDEX_VERSION` change, no schema change.
84+
- Found on the way and deliberately NOT folded in: `get_ranked_context`'s fusion
85+
exit passes no `extract_ledger_features` payload, so `top1_score`, `top2_score`
86+
and `identity_hit` are defaults rather than measurements on every row it writes;
87+
and `token_tracker._ensure_loaded(None)` on the first savings write repins the
88+
perf db to `~/.code-index` even when a tool was given a `storage_path`.
89+
590
## [1.108.185] - 2026-07-26 - fusion gets a verdict, and fusion can prove absence
691

792
### Fixed

CLAUDE.md

Lines changed: 4 additions & 3 deletions
Large diffs are not rendered by default.

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.185"
3+
version = "1.108.186"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Which ``ranking_events`` labels can be believed (v1.108.186).
2+
3+
The ledger is append-only history. When a producer records a column wrongly,
4+
the fix stops NEW rows from lying but cannot make the OLD rows honest, and
5+
every consumer that reads the column keeps reading the lie until the recency
6+
window ages it out. This module is the one place that says which labels are
7+
untrustworthy, so the three consumers (``retrieval.tuning``,
8+
``retrieval.regret``, ``tools.analyze_perf``) share a single rule rather than
9+
three copies drifting apart.
10+
11+
**The mislabel this exists for.** Until v1.108.186,
12+
``_get_ranked_context_fusion`` recorded ``semantic_used=1`` on every row while
13+
building only the lexical, identity and structural channels — never a
14+
similarity channel. Its own ``_meta.channels`` listed the three, and from
15+
v1.108.185 its verdict reported ``channels.semantic: "off"``, so the response
16+
and the ledger disagreed about the same call.
17+
18+
**Why the rows are exactly identifiable and no heuristic is needed.** Those
19+
rows carry ``tool = "get_ranked_context_fusion"``, a value no other producer
20+
writes. A pre-fix row is therefore precisely
21+
``(tool == "get_ranked_context_fusion", semantic_used == 1)``, and a post-fix
22+
row from the same exit is the same tool with ``semantic_used == 0``.
23+
24+
**The label is UNKNOWN, not False.** An untrusted row is not evidence that
25+
semantic was off; it is evidence that this row cannot say. Consumers put it in
26+
a third bucket and disclose the count rather than folding it into either side
27+
— treating unknown as False would be the same class of error in the opposite
28+
direction.
29+
30+
⚠ **This rule expires when the exit gains a similarity channel.** If
31+
``_get_ranked_context_fusion`` ever builds one, its ``semantic_used=1`` rows
32+
become honest and this predicate would start discarding good evidence.
33+
``tests/test_v1_108_186.py`` carries a drift guard that fails if that exit
34+
grows a similarity channel while this rule is still unconditional.
35+
"""
36+
37+
from __future__ import annotations
38+
39+
from typing import Sequence
40+
41+
# Column indices into a ranking_events row tuple, as SELECTed by
42+
# ``token_tracker.ranking_db_query``:
43+
# 0 ts 1 repo 2 tool 3 query_hash 4 query 5 returned_ids
44+
# 6 top1_score 7 top2_score 8 confidence 9 semantic_used
45+
# 10 identity_hit 11 repo_is_stale
46+
_TOOL = 2
47+
_SEMANTIC_USED = 9
48+
49+
# (tool, semantic_used-as-bool) pairs whose semantic label is not evidence.
50+
# Keep this a frozenset of exact pairs: a tool-only rule would also discard the
51+
# honest post-fix rows, which are the ones a consumer most wants to read.
52+
_UNTRUSTED_SEMANTIC_LABELS = frozenset({
53+
# v1.108.186. Pre-fix rows from get_ranked_context's fusion exit. The exit
54+
# built no similarity channel, so a `1` here reflects a hardcoded argument
55+
# rather than anything the search did.
56+
("get_ranked_context_fusion", True),
57+
})
58+
59+
# The tool whose pre-fix rows the rule above covers. Exported so the drift
60+
# guard names it once rather than restating the string.
61+
MISLABELLED_FUSION_TOOL = "get_ranked_context_fusion"
62+
63+
64+
def semantic_label_is_trustworthy(row: Sequence) -> bool:
65+
"""True when ``row``'s ``semantic_used`` column is evidence of anything.
66+
67+
A short row (fewer columns than the ledger SELECT returns) is treated as
68+
trustworthy: this predicate exists to refuse a KNOWN lie, and refusing rows
69+
it cannot classify would turn a shape surprise into silent data loss.
70+
"""
71+
try:
72+
tool = row[_TOOL]
73+
semantic_used = bool(row[_SEMANTIC_USED])
74+
except (IndexError, TypeError, KeyError):
75+
return True
76+
return (tool, semantic_used) not in _UNTRUSTED_SEMANTIC_LABELS

src/jcodemunch_mcp/retrieval/regret.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from ..storage import token_tracker as _tt
2828
from .. import config as _config
29+
from .ledger_trust import semantic_label_is_trustworthy as _semantic_label_is_trustworthy
2930

3031
# Column indices into a ranking_events row tuple.
3132
_TS, _REPO, _TOOL, _QH, _QUERY, _RETURNED = 0, 1, 2, 3, 4, 5
@@ -177,11 +178,23 @@ def _detect_stale_at_query(events: list[tuple]) -> list[dict]:
177178

178179
def _detect_vocabulary_gap(by_qh: "dict[str, list[tuple]]") -> list[dict]:
179180
"""Identity miss rescued by semantic search => the agent's term doesn't
180-
match a symbol name but means one. The strongest novelty signal."""
181+
match a symbol name but means one. The strongest novelty signal.
182+
183+
⚠ v1.108.186. This signal is the conjunction ``not identity_hit and
184+
semantic_used``, which made every pre-fix ``get_ranked_context_fusion`` row a
185+
textbook match: that exit hardcoded ``semantic_used=1`` and passed no ledger
186+
features at all, so ``identity_hit`` defaulted to 0 while the identity channel
187+
was in fact one of the three it built. A confident fusion call therefore
188+
reported "the agent's term doesn't match a symbol name" on a call where
189+
identity matching ran, and ``suggest_corrections`` turns these clusters into
190+
config patches shown to the user. Rows whose semantic label is not evidence
191+
cannot support this signal, so they are dropped from it.
192+
"""
181193
out = []
182194
for qh, rows in by_qh.items():
183195
hits = [r for r in rows
184-
if not r[_IDHIT] and r[_SEM]
196+
if _semantic_label_is_trustworthy(r)
197+
and not r[_IDHIT] and r[_SEM]
185198
and r[_CONF] is not None and r[_CONF] >= VOCAB_CONF_FLOOR]
186199
if len(hits) >= VOCAB_RECUR:
187200
out.append(_cluster(
@@ -230,6 +243,13 @@ def analyze_regret(
230243
)
231244
return base
232245

246+
# v1.108.186. Disclosed rather than silently dropped: the vocabulary_gap signal
247+
# ignores these rows, and a reader comparing `events_analyzed` against the
248+
# clusters found deserves to know some rows could not support one of the six.
249+
_untrusted = sum(1 for r in events if not _semantic_label_is_trustworthy(r))
250+
if _untrusted:
251+
base["events_semantic_label_unknown"] = _untrusted
252+
233253
by_qh = _by_query_hash(events)
234254
clusters: list[dict] = []
235255
clusters += _detect_requery_churn(by_qh)

src/jcodemunch_mcp/retrieval/tuning.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
the ledger (default 90 days) rather than the lifetime history, so old
1717
events can't anchor weights to a query distribution that no longer
1818
exists (``max_age_days=0`` restores the lifetime read).
19+
20+
v1.108.186: rows whose ``semantic_used`` label is not evidence are excluded
21+
from both groups and disclosed as ``events_semantic_label_unknown`` — see
22+
``retrieval.ledger_trust``. Unknown is a third answer here, not a vote for
23+
``semantic_used=0``.
1924
"""
2025

2126
from __future__ import annotations
@@ -29,6 +34,7 @@
2934
from typing import Optional
3035

3136
from ..storage import token_tracker as _tt
37+
from .ledger_trust import semantic_label_is_trustworthy as _semantic_label_is_trustworthy
3238

3339
logger = logging.getLogger(__name__)
3440

@@ -177,6 +183,14 @@ def _propose(
177183

178184
confidences_with_sem: list[float] = []
179185
confidences_without_sem: list[float] = []
186+
# v1.108.186. A THIRD bucket, because the answer to "was semantic used on
187+
# this row" can be "this row cannot say". Pre-fix `get_ranked_context_fusion`
188+
# rows recorded `semantic_used=1` from a hardcoded argument while building no
189+
# similarity channel, and the ledger keeps them for the whole recency window.
190+
# They are not evidence that semantic was ON, and folding them into the OFF
191+
# group would be the same error mirrored — so they are counted, disclosed,
192+
# and excluded from both means.
193+
unknown_semantic_label = 0
180194

181195
# Schema (column index): 0 ts, 1 repo, 2 tool, 3 query_hash,
182196
# 4 query, 5 returned_ids, 6 top1, 7 top2, 8 confidence,
@@ -185,6 +199,9 @@ def _propose(
185199
conf = row[8]
186200
if conf is None:
187201
continue
202+
if not _semantic_label_is_trustworthy(row):
203+
unknown_semantic_label += 1
204+
continue
188205
sem_used = bool(row[9])
189206
(confidences_with_sem if sem_used else confidences_without_sem).append(float(conf))
190207

@@ -196,6 +213,17 @@ def _propose(
196213
"mean_confidence_semantic_on": _round(mean_sem_on),
197214
"mean_confidence_semantic_off": _round(mean_sem_off),
198215
}
216+
if unknown_semantic_label:
217+
# Disclosed on every proposal that had to drop rows, including the ones
218+
# that then decline to move a weight — a silent exclusion reads as "the
219+
# ledger simply had nothing to say".
220+
signals["events_semantic_label_unknown"] = unknown_semantic_label
221+
signals["semantic_label_unknown_note"] = (
222+
"Rows recorded before v1.108.186 by get_ranked_context's fusion exit "
223+
"carry semantic_used=1 from a hardcoded argument, not from a "
224+
"similarity channel. They are excluded from both groups rather than "
225+
"counted as semantic-off, and age out with the recency window."
226+
)
199227

200228
new_sem = None
201229
if mean_sem_on is not None and mean_sem_off is not None:

src/jcodemunch_mcp/tools/analyze_perf.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
from typing import Optional
1919

2020
from ..storage import token_tracker as _tt
21+
from ..retrieval.ledger_trust import (
22+
semantic_label_is_trustworthy as _semantic_label_is_trustworthy,
23+
)
2124

2225

2326
_DEFAULT_TOP = 20
@@ -78,6 +81,11 @@ def _ledger_summary(rows: list[tuple], top: int) -> dict:
7881
"stale_events": 0,
7982
"identity_hits": 0,
8083
"semantic_used": 0,
84+
# v1.108.186. Rows whose semantic_used column is not evidence — see
85+
# retrieval.ledger_trust. Counted separately so `semantic_used` stays a
86+
# count of rows that mean it, and reported (rather than dropped) because
87+
# a shrunken count with no explanation reads as a usage change.
88+
"semantic_label_unknown": 0,
8189
})
8290
rb["events"] += 1
8391
if conf is not None:
@@ -87,14 +95,23 @@ def _ledger_summary(rows: list[tuple], top: int) -> dict:
8795
rb["stale_events"] += 1
8896
if ident:
8997
rb["identity_hits"] += 1
90-
if sem:
98+
if not _semantic_label_is_trustworthy(
99+
(ts, repo, tool, qh, query, returned_ids, top1, top2, conf, sem, ident, stale)
100+
):
101+
rb["semantic_label_unknown"] += 1
102+
elif sem:
91103
rb["semantic_used"] += 1
92104
tb = by_tool.setdefault(tool, {"events": 0})
93105
tb["events"] += 1
94106
for repo_name, rb in by_repo.items():
95107
ct = rb.pop("_conf_count", 0)
96108
total = rb.pop("_conf_total", 0.0)
97109
rb["avg_confidence"] = round(total / ct, 3) if ct else 0.0
110+
# v1.108.186. Present only when there is something to disclose, so a caller
111+
# whose ledger holds no mislabelled row sees the byte-identical shape it saw
112+
# before. Compatibility here is pinned by a test, not asserted.
113+
if not rb.get("semantic_label_unknown"):
114+
rb.pop("semantic_label_unknown", None)
98115
repo_ranked = sorted(by_repo.items(), key=lambda kv: kv[1]["events"], reverse=True)[:top]
99116
tool_ranked = sorted(by_tool.items(), key=lambda kv: kv[1]["events"], reverse=True)
100117
return {

src/jcodemunch_mcp/tools/get_ranked_context.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -834,25 +834,26 @@ def _get_tokens_fusion(sym):
834834
[{"score": item.get("fusion_score")} for item in context_items],
835835
is_stale=_probe.repo_is_stale,
836836
)
837+
# v1.108.186. This was a hardcoded `semantic_used=True` while the channel list
838+
# above builds lexical, identity and structural — never similarity — so every
839+
# row this exit has ever written claimed a channel that did not run. Derived
840+
# from the channels actually built rather than hardcoded False: if anyone adds a
841+
# similarity channel here, the ledger follows without a second edit. Same shape
842+
# as `search_symbols_fusion`, which records its real `similarity_used` flag.
843+
_semantic_used = any(ch.name == "similarity" for ch in channels)
837844
_record_ranking_event(
838845
tool="get_ranked_context_fusion",
839846
repo=f"{owner}/{name}",
840847
query=query,
841848
returned_ids=[c.get("symbol_id", "") for c in context_items],
842849
confidence=fusion_result["_meta"].get("confidence"),
843-
semantic_used=True,
850+
semantic_used=_semantic_used,
844851
repo_is_stale=_probe.repo_is_stale,
845852
)
846853

847854
# v1.108.185. The last exit in this tool without a verdict. The non-fusion
848855
# no-candidate return got one in v1.108.179 and the main path has had one since
849856
# v1.108.166; this branch was simply never revisited.
850-
#
851-
# ⚠ This exit builds NO similarity channel at all — lexical, identity and
852-
# structural only — so `channels.semantic` stays `off`, which is the truth
853-
# rather than the `semantic_used=True` the ranking ledger above records for it.
854-
# Left alone here: that argument feeds weight tuning and changing it would move
855-
# a learned parameter, which is not this change.
856857
from ..retrieval.verdict import retrieval_verdict_for_index as _rv
857858
_vres = _rv(
858859
index,
@@ -866,6 +867,10 @@ def _get_tokens_fusion(sym):
866867
query_terms=query_terms,
867868
scope=scope,
868869
state_before=state_before,
870+
# v1.108.186. Same derivation as the ledger row above, so the response and
871+
# the ledger cannot disagree about the same call again — which is exactly
872+
# what they did while this defaulted to `off` and the ledger said True.
873+
semantic_channel="ok" if _semantic_used else "off",
869874
)
870875
fusion_result["_meta"]["verdict"] = _vres["verdict"]
871876
if _vres["negative_evidence"] is not None:

0 commit comments

Comments
 (0)