Skip to content

Commit 4e49d46

Browse files
jgravelleclaude
andcommitted
release: v1.108.185 — fusion gets a verdict, and fusion can prove absence
The last two exits without one. Unlike the semantic exit, neither asserted anything false — no verdict and no absence prose either — so a zero-result fusion search came back as a bare empty response: nothing wrong, and nothing honest. Every retrieval exit in the suite now carries a verdict, pinned structurally rather than by inspection. The review reached the OPPOSITE conclusion to 1.108.184's, which is why modes get 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, so its absence weakens ranking and cannot manufacture a false absence. That asymmetry is what the semantic exit lacked. Two defects found on the way, either of which made `absent` unreachable: - build_structural_channel treated "restrict to nothing" as "no restriction". The guard was a falsy check, and both callers pass lexical | identity — so a query matching nothing produced an EMPTY set, skipped the filter, and let every symbol with nonzero PageRank into the channel. 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. - EmbeddingStore._connect runs a WAL pragma and a CREATE-TABLE script on every connection, so the fusion embeddings probe wrote to the database it was reading, bumped _db_mtime_ns, and made the first fusion search of any process report rebuilding + moved_during_scan. mode=ro alone does NOT fix it: a read-only connection to a WAL database still creates the -wal and -shm sidecars, and _db_mtime_ns maxes over the .db AND the -wal. immutable=1 is the flag that guarantees no sidecar. Also: both no-candidate early returns now carry incomplete: empty_scope; new verdict.retrieval_verdict_for_index binds the five index signals every exit was assembling by hand; the receipt gate fired on its own author a second time, so completeness_by_mode["fusion"] is declared for both producers. Suite 6093 passed + the known 12 local-ONNX env failures. Replay gate green. No tool-count change, no INDEX_VERSION change, no schema change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a11cad3 commit 4e49d46

12 files changed

Lines changed: 1055 additions & 83 deletions

File tree

CHANGELOG.md

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

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

5+
## [1.108.185] - 2026-07-26 - fusion gets a verdict, and fusion can prove absence
6+
7+
### Fixed
8+
9+
- **The last two exits without a verdict.** `_search_symbols_fusion` and
10+
`_get_ranked_context_fusion` emitted no verdict and no negative evidence, so a
11+
zero-result fusion search came back as a bare empty response: nothing false, and
12+
nothing honest either. Both now build the same verdict as their tools' other
13+
exits, so every gate applies — and both finally receive the pre-scan identity
14+
capture (#377 item 6) that was taken on the shared path and never handed to the
15+
branches that diverge.
16+
17+
- ⚠ **`build_structural_channel` treated "restrict to nothing" as "no
18+
restriction", and a no-match fusion query returned the whole repository.** The
19+
guard was a falsy check (`if candidate_ids and ...`), while both callers pass
20+
`set(lexical.ranked_ids) | set(identity.ranked_ids)`. A query matching nothing
21+
produced an EMPTY set, which skipped the filter and let every symbol with
22+
nonzero PageRank into the channel. Measured live: `fusion=True` on a nonsense
23+
query returned ranked rows, and because fusion had no verdict nobody could see
24+
that it was reporting centrality as relevance. It also made `absent` unreachable
25+
on both fusion paths, so the absence contract could not have applied to them
26+
however carefully it was wired. `None` and `set()` are now different things.
27+
28+
- ⚠ **A read that wrote, and `mode=ro` was not enough to stop it.**
29+
`EmbeddingStore._connect` runs `PRAGMA journal_mode = WAL` and a CREATE-TABLE
30+
script on every connection, so the fusion exit's "does this repo have
31+
embeddings" probe wrote to the database it was reading. That bumped
32+
`_db_mtime_ns`, which made `index_changed_since_load` report
33+
`channels.index: "rebuilding"` and `moved_during_scan` fire — so **the first
34+
fusion search in any process downgraded to `degraded` and could not reach
35+
`absent`**, entirely self-inflicted. Same defect `runtime/confidence.py` was
36+
fixed for.
37+
38+
New `EmbeddingStore.get_all_readonly`. ⚠ **`mode=ro` alone does NOT fix this and
39+
the first attempt shipped nothing:** read-only in SQLite means "cannot modify the
40+
DATABASE", not "cannot touch the filesystem", and a plain read-only connection to
41+
a WAL database still creates the `-wal` and `-shm` sidecars — measured going
42+
None -> present across one fusion search — while `_db_mtime_ns` maxes over the
43+
`.db` AND the `-wal`. `immutable=1` is the flag that guarantees no sidecar. The
44+
trade-off it accepts is stated rather than hidden: vectors sitting in an
45+
un-checkpointed WAL are not read, so the similarity channel may be skipped after
46+
a watcher writes embeddings. That can only weaken the RANKING, because the
47+
channel adds candidates and never removes any, which is the opposite of what the
48+
mtime bump did.
49+
50+
- Both fusion exits' no-candidate early returns now carry a verdict with
51+
`incomplete: empty_scope` (#377 item 9). The filters selected nothing, so not a
52+
byte was read, and a bare empty result reads exactly like a completed search that
53+
found nothing.
54+
55+
### Added
56+
57+
- **`verdict.retrieval_verdict_for_index`**, the sibling of the existing
58+
`symbol_verdict_for_index` and `file_verdict_for_index` wrappers. Three exits
59+
shipped with no verdict at all, and the reason is worth naming: adding one meant
60+
reproducing five call patterns by hand (freshness probe, rebuild check, coverage
61+
contract, movement comparison, scope-level working-tree state), so the cheap
62+
thing was to skip it and hand-roll the answer. The wrapper binds all five, and
63+
is what the two fusion exits now call.
64+
65+
- Both fusion exits emit `negative_evidence` and the warning string their tools'
66+
other exits already emit, gated by every gate. Parity is the point: the same
67+
question must not get a differently-honest answer depending on which ranking mode
68+
ran.
69+
70+
### Changed
71+
72+
- **Fusion CAN prove absence, and the review reached the OPPOSITE conclusion to
73+
1.108.184's.** That is why modes are reviewed rather than having a rule ported
74+
onto them. `build_lexical_channel` and `build_identity_channel` each score every
75+
eligible candidate with no cap, and `fuse` keeps every symbol appearing in ANY
76+
channel, so an empty fused set means every candidate scored zero on both BM25 and
77+
identity — the same corpus fact the lexical path's absence rests on. The
78+
similarity channel can only ADD symbols, never remove one, so its absence weakens
79+
the ranking and cannot manufacture a false absence. That asymmetry is exactly
80+
what the semantic exit lacked, which is why it carries `absence_unprovable` and
81+
fusion does not.
82+
83+
- ⚠ **The receipt gate fired on its own author a second time.** Giving both fusion
84+
exits a verdict made them mintable under `completeness` declarations written for
85+
a different operation, and the pinning tests from 1.108.183 failed again. The
86+
modes were reviewed and `completeness_by_mode["fusion"]` now declares what a
87+
fusion receipt actually rests on, for both producers. `fusion` also joins both
88+
producers' `mode_args`: a fused search and a lexical search of one string are
89+
different operations and must not collide on one evidence id.
90+
91+
### Notes
92+
93+
- `get_ranked_context`'s fusion exit builds NO similarity channel at all, so its
94+
`channels.semantic` is `off` — which is the truth, and differs from the
95+
`semantic_used=True` its ranking-ledger call has always recorded. Left alone: that
96+
argument feeds weight tuning, and moving a learned parameter is not this change.
97+
- New `tests/test_v1_108_185.py` (39), including the structural-backfill defect
98+
pinned at unit and exit level, the sidecar regression pinned by filesystem
99+
assertion, and a drift guard asserting every `search_mode` the code emits has a
100+
declared completeness. **Every retrieval exit in the suite now carries a verdict**,
101+
pinned structurally rather than by inspection. No tool-count change, no
102+
`INDEX_VERSION` change, no schema change.
103+
5104
## [1.108.184] - 2026-07-26 - a ranking cannot prove absence
6105

7106
### Fixed

0 commit comments

Comments
 (0)