v1.108.187 - both fusion exits record the features they measured
The first of the two adjacent findings 1.108.186 reported instead of folding in.
A default is not a measurement
get_ranked_context's fusion exit passed no extract_ledger_features payload at all, so top1_score, top2_score and identity_hit were recorded as their defaults on every ranking-ledger row it ever wrote. Three of regret's six signals read those columns, and once a default is in the ledger nothing downstream can tell it from a measurement.
search_symbols' fusion exit 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 of what the identity channel found. Both are fixed together — shipping one honest producer and one false one would have guaranteed a third release on the same column.
Identity comes from the identity channel's own raw_scores. Both exits build that channel, so they know per symbol whether identity matched, and build_identity_channel admits only symbols scoring above zero.
The part a failing test found, not a reading
⚠ compute_confidence sniffs the same identity key. When no has_identity_match is passed it scores identity 1.0 known-true / 0.7 unknown / 0.6 known-false — so feeding identity through the input attach_confidence already receives would have moved the confidence of every fusion search. Measured at 0.742 → 0.783 on one fixture.
That is a ranking-adjacent behaviour change with its own justification, not a side effect of recording a column, so the ledger input is now separate and the confidence call keeps the input it has always had. The separation is pinned behaviourally rather than by reading the source: attach_confidence must receive rows without an identity key on the same call where the ledger features receive rows with one.
The history, and why this discriminator is weaker on purpose
New ledger_trust.identity_label_is_trustworthy. Unlike the semantic rule shipped in 1.108.186, identity_hit has no value a pre-fix row alone can carry — pre-fix is always 0, and 0 is also a perfectly honest post-fix answer, so matching on the column would discard good evidence forever. What is exact: a row that returned symbols while recording no top1_score can only have come from the exit that passed no features. Post-fix a non-empty result always carries a fused score, and an empty result carries no returned_ids to match on.
⚠ search_symbols_fusion's history is not separable, and no heuristic is used. It always recorded top scores and only omitted the identity key, so its pre-fix rows are indistinguishable from honest post-fix rows that had no identity match. There is no version or timestamp column to bound it by. The recency window is the only remedy, and that is stated rather than papered over.
Two more things the tests caught
- ⚠ An identity guard on regret's
vocabulary_gapwas unreachable and was removed. That signal requiressemantic_used, and aget_ranked_context_fusionrow withsemantic_used=1is already refused by the 1.108.186 rule — so the rows are suppressed either way. An unreachable guard reads like protection and is not. Found by a non-vacuity test failing, not by inspection. - A pre-fix single-result row was clustered as
thin_resulton the strength of a default. That detector treatstop1_score is Noneas thin, which reads a missing measurement as a weak one. Only the known-featureless rows are skipped; the empty-result, weak-score and other-producer cases are unchanged and pinned.
Disclosure follows the 1.108.186 pattern: events_without_ledger_features on analyze_regret, no_ledger_features per repo in analyze_perf --ledger, and identity_hits no longer counts a default as a miss. Both keys are absent — not zero — for a caller whose ledger holds no such row.
New tests/test_v1_108_187.py (24). Non-vacuity shown on both exits: reverting either producer fails four tests. A pre-fix ledger stays readable and unrewritten. Suite 6139 passed plus the known 12 local-ONNX environment failures that are green in CI; Tests 9/9 and Replay green before publishing — the replay gate matters here because the confidence separation is what kept ranking output still. No tool-count change, no INDEX_VERSION change, no schema change.
Still open from the 1.108.186 findings: token_tracker._ensure_loaded(None) on the first savings write repins the perf db to ~/.code-index even when a tool was handed a storage_path.
Design context: #377, proposed by @mightydanp.