search_text renders line and snippet from DIFFERENT sites with nothing flagging it — the lying-span family I052 closed for read_code, still open one tool over #202

Closed
opened 2026-09-06 21:31:22 +02:00 by buildagent · 1 comment
Member

Found by a lane cross-reading a file it was already editing. It would not have caught it otherwise, which is the point.

Measured

search_text("NavigationConfidence {")

returned a hit with:

  • line: 7689 — which is struct NavigationConfidence {
  • snippet: "u64,\n) -> NavigationConfidence {" — cut from ~8394, a function return type

Two different sites, rendered adjacently, in one hit.

Why the existing disclosure does not cover it

snippet_line was absent, and that is correct by its own contract: the probe line repeats in the file, so the snippet's origin could not be located unambiguously, and the field is honestly withheld rather than guessed. The three-state discipline is intact — absent means "could not measure", not zero.

The defect is one layer up. The payload still renders line and snippet side by side with nothing saying they may describe different places. A reader who does not already know that snippet_line's absence implies a possible divergence will read the pair as one location. The absent field is a signal only to someone who knows to look for its absence, which is exactly the shape this project keeps closing: a fact the system holds, rendered so the reader cannot recover it.

Why this is a known family, not a new one

I052 closed precisely this for read_code: a span that described its own content, so a caller could never be shown bytes from one place under a header from another. search_text is the same hazard one tool over — and worse in one respect, because line is the field callers feed straight into read_code.

What must NOT be done

  • Do not make snippet_line mandatory by guessing. Guessing an origin for a repeating probe is how a lying span gets created; the current withholding is the correct behaviour and must survive any fix.
  • Do not drop snippet when its origin is unknown. The excerpt is useful even unlocated, and removing it would cost callers the thing they came for.
  • Do not solve it in the tool description. Description prose is unread and expensive — this repo measured a competing lane's descriptive prose at +288 tokens on every session start. The disclosure belongs in the payload, paid for only when it fires.
  • Do not fix it by suppressing line. line is the file's first match and is well-defined; it is the pairing that misleads.

Shape of a fix

The honest minimum is a payload field that fires exactly when snippet_line could not be determined and says so in the reply — so the divergence is stated where the two values are rendered, rather than inferable from a missing key. Bounded and conditional: it costs nothing on the common case where snippet_line is present and the two agree.

Worth checking while in there whether line and snippet can diverge even when snippet_line IS present, and whether any other tool pairs a located field with an unlocated excerpt the same way.

I052 (the same defect closed for read_code), #183 (search_text line numbers without their text — the neighbouring gap in the same census), #197 (payload budget any fix must fit).

Reported 2026-09-06 by the #173/#174 lane, against code-index-mcp 0.26.1 (8d90075).

Found by a lane cross-reading a file it was already editing. It would not have caught it otherwise, which is the point. ## Measured ``` search_text("NavigationConfidence {") ``` returned a hit with: - `line: 7689` — which is `struct NavigationConfidence {` - `snippet: "u64,\n) -> NavigationConfidence {"` — cut from **~8394**, a function return type Two different sites, rendered adjacently, in one hit. ## Why the existing disclosure does not cover it `snippet_line` was **absent**, and that is correct by its own contract: the probe line repeats in the file, so the snippet's origin could not be located unambiguously, and the field is honestly withheld rather than guessed. The three-state discipline is intact — absent means "could not measure", not zero. The defect is one layer up. **The payload still renders `line` and `snippet` side by side with nothing saying they may describe different places.** A reader who does not already know that `snippet_line`'s absence *implies* a possible divergence will read the pair as one location. The absent field is a signal only to someone who knows to look for its absence, which is exactly the shape this project keeps closing: a fact the system holds, rendered so the reader cannot recover it. ## Why this is a known family, not a new one I052 closed precisely this for `read_code`: a span that described its own content, so a caller could never be shown bytes from one place under a header from another. `search_text` is the same hazard one tool over — and worse in one respect, because `line` is the field callers feed straight into `read_code`. ## What must NOT be done - **Do not make `snippet_line` mandatory by guessing.** Guessing an origin for a repeating probe is how a lying span gets *created*; the current withholding is the correct behaviour and must survive any fix. - **Do not drop `snippet` when its origin is unknown.** The excerpt is useful even unlocated, and removing it would cost callers the thing they came for. - **Do not solve it in the tool description.** Description prose is unread and expensive — this repo measured a competing lane's descriptive prose at +288 tokens on *every session start*. The disclosure belongs in the payload, paid for only when it fires. - **Do not fix it by suppressing `line`.** `line` is the file's first match and is well-defined; it is the *pairing* that misleads. ## Shape of a fix The honest minimum is a payload field that fires exactly when `snippet_line` could not be determined and says so in the reply — so the divergence is stated where the two values are rendered, rather than inferable from a missing key. Bounded and conditional: it costs nothing on the common case where `snippet_line` is present and the two agree. Worth checking while in there whether `line` and `snippet` can diverge even when `snippet_line` IS present, and whether any other tool pairs a located field with an unlocated excerpt the same way. ## Related I052 (the same defect closed for `read_code`), #183 (`search_text` line numbers without their text — the neighbouring gap in the same census), #197 (payload budget any fix must fit). Reported 2026-09-06 by the #173/#174 lane, against `code-index-mcp 0.26.1 (8d90075)`.
Author
Member

Fixed and released in v0.27.0 via 730bf15 — this issue was simply never closed.

Each search_text hit now carries snippet_origin with a genuine three-state reading:

  • other_line — MEASURED to differ, and snippet_line says where the snippet actually came from
  • unlocated — the excerpt's origin could not be located, so agreement is UNKNOWN, never "the same"
  • absent — they agree

The sentence that decodes those codes (SNIPPET_ORIGIN_SEMANTICS, crates/mcp-server/src/server.rs:7829) is hoisted once per page rather than repeated per row, emitted by snippet_origin_semantics_for() (:7877) only when some hit on the page needs it.

The commit records the point that made the old design inadequate, and it is the right one: snippet_line's own doc claimed it made the divergence "visible instead of invisible", but absent it is a signal only to a reader who already knows to look for a missing key, and present the divergence is merely derivable by comparing two numbers. A reader should not have to subtract to learn they were lied to.

Measured cost: 406 bytes on a page with two marked hits, 0 on a page where every hit agrees, and 0 tokens on the startup payload — no tool-description prose was added.

Anti-vacuity arm is present and is the one that matters (tool_honesty_b_group_e2e.rs:1370): a page whose every hit agrees asserts snippet_origin_semantics is null, with a precondition assert that only one file matches the probe term. Without it, "always disclose" would pass.

Also fixed under the same clause, found while fixing this: under whole_word=true the census is recounted against word boundaries but line was left on the substring population, so line named a site the caller's own filter had just REJECTED. finish_census_pair now finishes that pair too.

Mutations run to RED: M1 (snippet_origin's None arm returns None) → RED, 2 tests; M2 (drop snippet_origin from ConciseTextHit) → RED; M7 (drop the line re-point in finish_census_pair) → RED.

I confirmed the behaviour live in this session rather than reading the diff — a search_text call in the running server returned snippet_origin: "other_line" with snippet_line: 728 beside line: 94, and the hoisted semantics string.

Fixed and released in **v0.27.0** via `730bf15` — this issue was simply never closed. Each `search_text` hit now carries `snippet_origin` with a genuine three-state reading: - `other_line` — MEASURED to differ, and `snippet_line` says where the snippet actually came from - `unlocated` — the excerpt's origin could not be located, so agreement is **UNKNOWN**, never "the same" - absent — they agree The sentence that decodes those codes (`SNIPPET_ORIGIN_SEMANTICS`, `crates/mcp-server/src/server.rs:7829`) is **hoisted once per page** rather than repeated per row, emitted by `snippet_origin_semantics_for()` (`:7877`) only when some hit on the page needs it. The commit records the point that made the old design inadequate, and it is the right one: `snippet_line`'s own doc claimed it made the divergence "visible instead of invisible", but absent it is a signal only to a reader who already knows to look for a missing key, and present the divergence is merely *derivable* by comparing two numbers. A reader should not have to subtract to learn they were lied to. Measured cost: 406 bytes on a page with two marked hits, **0** on a page where every hit agrees, and 0 tokens on the startup payload — no tool-description prose was added. Anti-vacuity arm is present and is the one that matters (`tool_honesty_b_group_e2e.rs:1370`): a page whose every hit agrees asserts `snippet_origin_semantics` is **null**, with a precondition assert that only one file matches the probe term. Without it, "always disclose" would pass. Also fixed under the same clause, found while fixing this: under `whole_word=true` the census is recounted against word boundaries but `line` was left on the substring population, so `line` named a site the caller's own filter had just REJECTED. `finish_census_pair` now finishes that pair too. Mutations run to RED: M1 (`snippet_origin`'s `None` arm returns `None`) → RED, 2 tests; M2 (drop `snippet_origin` from `ConciseTextHit`) → RED; M7 (drop the `line` re-point in `finish_census_pair`) → RED. I confirmed the behaviour live in this session rather than reading the diff — a `search_text` call in the running server returned `snippet_origin: "other_line"` with `snippet_line: 728` beside `line: 94`, and the hoisted semantics string.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#202
No description provided.