# Budgeted evidence selection For demarkus maintainers: the design for the roadmap slice that makes one budgeted lookup answer a multi-document question. Status: active (2026-10-10; branch `feat/graph-context-selection` from `main` at `a8d037c0`). Roadmap entry: [budgeted evidence selection](/roadmap/graph-focus.md#budgeted-evidence-selection). Design rationale: [graph focus](/plans/graph-focus.md#make-the-existing-context-budget-useful). ## What the before arms say The OpenCode 2 before arms (r2, 2026-10-10) spend about 11% of model tokens on tool results and about 85% on the prompt replayed per turn. Cost tracks model turns, not evidence size: a three-turn attempt costs about 5,800 tokens, a seven-turn one about 17,000. Every attempt is a lookup followed by one to four fetches, with mark_explore in between for lineage, supersession and dependency questions. Budgeted expansion fired on 7 of 43 lookups because the reader policy says to omit it by default, and even then the reader fetched afterwards. So the lever is turns. A budgeted lookup that already holds the direct passage and the one hop the question needs removes the explore call and one or two fetches. ## Decision Keep `mark_lookup` and its `budget` argument. Change what the shared expansion (`client/lookupexpand`, used by the local MCP and the gateway) puts inside the budget, in four parts. 1. The budget covers the whole result. The table and envelope already printed count against it; expansion gets the remainder. When the table alone exceeds the budget the result says so and names the expansion path (raise the budget or fetch a row), instead of silently adding nothing. 2. Overlapping and duplicate evidence coalesces. Rows on one document expand once per byte range: a later row whose section lies inside, or encloses, an emitted section is reported as covered by that block rather than printed again. A section whose text is byte-identical to one already printed, from another document or anchor, prints as a one-line block naming the first location, so distinct provenance and its version pin survive without a second copy. Rank order is kept; the strongest direct match always prints first. 3. One-hop additions come from the strongest match's own `rel-*` metadata. Every expanded document prints one grouped line of its declared relations under its block (`>>> related: rel-by /a.md, /b.md; rel-genre /g.md`; the subject is the block above). After every direct row has had its turn, the relations of the first document printed, and only that one, expand under the remaining budget, up to three targets, evidence predicates first (`supersedes`, `superseded-by`, `depends-on`, `implements`, `derived-from`) and then the rest in declaration order, each whole when small, else the sections holding the query terms, else the outline, headed with the relation that brought it in. A related document prints its own relation line but is not followed further. Targets already in the table are skipped; targets on another authority or outside the caller's read scope are never fetched, so scope and token never widen. No graph store is consulted, so the hop is fresh by construction and identical on both surfaces. Incoming relations known only to the store are a measured follow-on. 4. Omissions are visible. Rows that did not fit are listed with their size, capped, so the reader can fetch the one it wants. The closing note counts rows and related documents expanded. Fetch count stays bounded by `MaxFetches` across direct and related documents. Body links are navigation, not evidence, and stay out of expansion. Why the strongest match only, and why every predicate: the first replay expanded typed relations of every printed row and added rock, hardcore punk and thrash metal to a question about doom metal, 30% more result tokens for no new evidence. Restricting the hop to the top document kept the one hop that mattered (Nightwish to Helloween through `rel-influenced-by`, which the roadmap's four families would have excluded) and dropped the rest. Which predicates carry evidence is a property of the world's vocabulary (ADR 0004 leaves curation to authors), so the slice prefers the roadmap's families when it must choose and otherwise trusts the author's declaration, inside the budget. ## Not in this slice - Any new tool or default behavior. A lookup without `budget` is byte-identical to today. - Relation edges from the graph store (incoming relations, seeded rows). Added only if measured misses justify a store dependency inside lookup. - Semantic deduplication. Only byte-identical text coalesces. - Shortest paths, centrality and clustering remain evidence-gated follow-ons. ## Evaluation The benchmarks branch was rebuilt the same day as `main` at `a8d037c0` plus the bench overlay (bench-only directories, the read-fence patch to `client/fetch` and `demarkus-mcp`, the `answer-bench` Make target and the artifact ignores), because its client had been 85 commits behind `main` and the r2 before arms had measured a `demarkus-mcp` without PR 561. The GCS corpus pack, the catalog ranking probe and the fan-out bench were dropped in the refresh; everything else was ported to the current client and store APIs. Offline replay first (`TestContextSelectionReplay`, env-gated): the r2 readers' recorded first lookups with `budget` 1500 through both binaries against the frozen corpora. Result tokens 14,254 to 13,900 on latebit (no typed hop fires: the supersession pair is already in the table) and 11,328 to 13,828 on music, where six hops add the Helloween and lineage passages two required fields needed. Mechanics only, not a model score. Paid arms, 16 attempts each, OpenCode 2.0.23 private-server reader, prepared cache `scope-crawl-v1`: - B: `main` client plus read fence, policy `graph-budget-first-v1`, v7 datasets. - C: this slice's client plus read fence, same policy and datasets. `compare` B against C is the software result; server, proxy, runner and OpenCode hashes are equal, only the MCP hash differs. - A′: `main` client plus read fence, policy `graph-section-first-v1`, v6 datasets: the current-client baseline the r2 arms were meant to be. `ComparePolicies` A′ against B is the policy ablation. Acceptance follows the roadmap: no direct-answer regression, multi-document categories at equal or better correctness with fewer tokens per correct answer. Each arm is a paid run approved on 2026-10-10.