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
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#202
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by a lane cross-reading a file it was already editing. It would not have caught it otherwise, which is the point.
Measured
returned a hit with:
line: 7689— which isstruct NavigationConfidence {snippet: "u64,\n) -> NavigationConfidence {"— cut from ~8394, a function return typeTwo different sites, rendered adjacently, in one hit.
Why the existing disclosure does not cover it
snippet_linewas 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
lineandsnippetside by side with nothing saying they may describe different places. A reader who does not already know thatsnippet_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_textis the same hazard one tool over — and worse in one respect, becauselineis the field callers feed straight intoread_code.What must NOT be done
snippet_linemandatory 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.snippetwhen its origin is unknown. The excerpt is useful even unlocated, and removing it would cost callers the thing they came for.line.lineis 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_linecould 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 wheresnippet_lineis present and the two agree.Worth checking while in there whether
lineandsnippetcan diverge even whensnippet_lineIS 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_textline 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).Fixed and released in v0.27.0 via
730bf15— this issue was simply never closed.Each
search_texthit now carriessnippet_originwith a genuine three-state reading:other_line— MEASURED to differ, andsnippet_linesays where the snippet actually came fromunlocated— the excerpt's origin could not be located, so agreement is UNKNOWN, never "the same"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 bysnippet_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 assertssnippet_origin_semanticsis 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=truethe census is recounted against word boundaries butlinewas left on the substring population, solinenamed a site the caller's own filter had just REJECTED.finish_census_pairnow finishes that pair too.Mutations run to RED: M1 (
snippet_origin'sNonearm returnsNone) → RED, 2 tests; M2 (dropsnippet_originfromConciseTextHit) → RED; M7 (drop thelinere-point infinish_census_pair) → RED.I confirmed the behaviour live in this session rather than reading the diff — a
search_textcall in the running server returnedsnippet_origin: "other_line"withsnippet_line: 728besideline: 94, and the hoisted semantics string.answer_provenancewas byte-identical: nothing says which generation answered #245