You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(context): adaptive surfacing dropped the memory most worth recalling (ADR-0029)
Found while writing an MCP test. A saved hard constraint ("must not add a
Redis dependency") was not surfaced for the task "rate limiting", and the
report said it had been "ranked below the budget cut". The budget was 19,200
tokens and the record was 12. It had not lost a budget contest -- it never
entered one.
`ContextRanker.rank` returns only what it could score: a candidate sharing no
term with the task is absent from the result, not present with score zero.
`select()` iterated that list and applied the budget to it, so any record with
no lexical overlap was dropped before the budget was consulted. Measured on a
real .verity/: task "speed up the cache" surfaced 0 of 4 records, all silently
discarded.
This inverts the mechanism's purpose. Persisted memory is most valuable when
the decisive fact is NOT inferable from the task's wording -- that is exactly
ADR-0020's finding, the first pilot in eight to produce a success-rate split.
Selecting memory by lexical overlap optimizes against the one case it exists
for. And `classify.py` protects MEMORY items as CRITICAL unconditionally, but
nothing that never reaches the pipeline can be protected by it.
Unscored candidates now rank last instead of vanishing, so every candidate is
either selected or genuinely did not fit (invariant 6's principle applied to
selection). `degraded_reason` says when this happened and what was done.
Keyed on ContextItem.id, not content_hash -- the hash is empty on a plain
item, which made every unscored candidate look like a duplicate of every
other, caught by a test failing for exactly that reason.
Same pass also wires the last two engines to MCP, the CLI having been
exercised first as rule 5 requires:
- `risk_of_changing` -- per-file tiers with reasons; refuses an empty graph
- `should_recall_memory` -- the trigger, budget and records, and it
distinguishes "nothing crossed a threshold" from "nothing is saved"
This is the third finding in one verification pass with the same shape
(0027, 0028, 0029): a collaborator returned less than the caller assumed, the
caller could not tell "nothing found" from "nothing looked at", and the
resulting message was confidently wrong rather than absent. ADR-0029 names it
as a class and the check that catches it.
644 tests pass, lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/adr/README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -46,6 +46,7 @@ verdict was "this pilot could not have detected an effect."
46
46
|[0026](0026-risk-adaptive-verification.md)| Can verification depth scale with file risk, using only signals already in the graph? | Yes — `classify_file_risk` tiers by path convention / blast radius / fan-in / untested symbols; `rules_for_tier` gates rule depth; both builtin rules tagged with risk tiers and sql-injection caveat backfilled |
47
47
|[0027](0027-retained-trial-evidence.md)| Did ADR-0022 actually close invariant 7? |**No** — six checkable failures, including a default output path inside `.gitignore` and a CLI that could not express the metric ADR-0022 claimed to reproduce. Evidence is now a diff against a hash-pinned fixture; unretained ⇒ unpublishable, mechanically |
48
48
|[0028](0028-the-mocked-test-that-could-not-fail.md)| Did ADR-0026's risk tiering work outside its own tests? |**No** — any path form but the ingester's silently yielded zero signals and a clean-looking `low` for every file. The suite's only `MagicMock` could not express the difference; tests rewritten against a real graph, and `unittest.mock` is gone |
49
+
|[0029](0029-unrankable-memory-is-not-irrelevant-memory.md)| Does adaptive surfacing recall the records that matter most? |**It did the opposite** — BM25 omits zero-overlap candidates entirely, so a constraint sharing no word with the task was dropped before the budget applied and reported as a budget outcome. 0 of 4 records surfaced on a real store. Unscored candidates now rank last instead of vanishing |
0 commit comments