no_outline and path_not_indexed frame a PERMANENT exclusion as transient, and send the caller to a tool that cannot answer either #243

Closed
opened 2026-09-09 17:19:06 +02:00 by buildagent · 1 comment
Member

Flagged by the #238 lane as a follow-up candidate; verified on master 1d3228e before filing. Small, but it is the one shape this tracker keeps closing, in the two errors an agent meets first.

The fact

Two hints, one per tool, both worded the same way:

crates/mcp-server/src/server.rs:15120   (file_outline, `no_outline`)
crates/mcp-server/src/server.rs:15666   (get_dependencies, `path_not_indexed`)

  "No such indexed file. Verify the path is project-relative with forward
   slashes (or absolute under a known root); if the file was just created
   the index may not have caught up — search_text on its name confirms
   whether it's indexed."

For a path the walker permanently refuses — a dot-directory, a gitignored file, a nested checkout, a file over the size cap — every clause of that is wrong in the same direction:

  1. "if the file was just created / may not have caught up" frames a permanent exclusion as a race. The correct reading is "this path will never be indexed, on this repository, under this configuration". An agent told it is a timing problem retries, or waits.
  2. "search_text on its name confirms whether it's indexed" — it does not. search_text returns total: 0 for an excluded path exactly as it does for a not-yet-indexed one. The remedy hands the caller the same ambiguity they came with, one tool over.

So the error is corrective in shape and misleading in content: it produces a next step that cannot terminate.

The tool that does answer it

index_coverage(path) exists, is cheap, and returns a strict three-state verdict with the rule that fired:

verdict: "never" | "pending" | "indexed"
reason:  "hidden" | "ignore_file" | "too_large" | "nested_checkout" | …
proof:   { at: ".claude", detail: "hidden directory; only .github, .gitlab
                                   and .forgejo are allowlisted" }
hint:    "Permanent. Use shell for this path, or change the rule that
          excludes it."

never vs pending is precisely the distinction these two hints collapse — and index_coverage is already the thing the evidence_gaps semantics string points readers at. The vocabulary exists; these two errors predate it and were never rejoined to it.

Why it is worth a number

This is where an agent lands first. file_outline on a path the index cannot see is the opening move of half the sessions in this repo, and #201 is on record for what happens next: a lane read internal_error, concluded the tool was broken, and fell back to grep for the rest of the session — in a repo whose CLAUDE.md makes preferring the index a hard rule. A hint that sends the reader to a tool that cannot answer is the same dead end with better manners.

What must NOT be done

  • Do not always call index_coverage before erroring. That pays a probe on every miss, including the common typo. Name it in the hint; let the caller spend the round trip only when they need the answer.
  • Do not drop the transient clause. A file genuinely can be new and not yet walked — that is pending, and it is a real state. The defect is that transient is offered as the explanation rather than one of two.
  • Do not replace it with prose in the tool description. Description text is unread and expensive here, and measured as such.
  • Do not fix one and leave the other. Two call sites, one wording, one clause — this project's rule is one mechanism, not a fix per site.

Shape of a fix

Both hints name index_coverage(path) as the way to tell a permanent exclusion from a lag, and stop asserting the transient reading as the likely one. One shared string, since the two sites already say the same thing.

What a fix must prove

  • The hint from a path excluded by a walker rule names index_coverage, and following it returns verdict: "never" with the rule — end to end, not just the string.
  • Anti-vacuity, both arms: a plain typo still gets the path-shape advice it gets today (the common case must not regress into a diagnostic lecture), and a genuinely not-yet-walked file still reads as possibly-transient.
  • Both call sites are covered by one clause; a mutation of that clause reddens both, or they are not shared.
  • No probe is added to the error path — measured, not asserted.

#238 (where this was spotted; its part 1 fixed the same class for empty_population), #201 (the corrective-error work these two hints predate), #220 (a blind spot that reports as a measurement is worse than one that reports as a limit).

Filed 2026-09-09 against master 1d3228e.

Flagged by the #238 lane as a follow-up candidate; verified on `master` `1d3228e` before filing. Small, but it is the one shape this tracker keeps closing, in the two errors an agent meets first. ## The fact Two hints, one per tool, both worded the same way: ``` crates/mcp-server/src/server.rs:15120 (file_outline, `no_outline`) crates/mcp-server/src/server.rs:15666 (get_dependencies, `path_not_indexed`) "No such indexed file. Verify the path is project-relative with forward slashes (or absolute under a known root); if the file was just created the index may not have caught up — search_text on its name confirms whether it's indexed." ``` For a path the walker **permanently** refuses — a dot-directory, a gitignored file, a nested checkout, a file over the size cap — every clause of that is wrong in the same direction: 1. **"if the file was just created / may not have caught up"** frames a permanent exclusion as a *race*. The correct reading is "this path will never be indexed, on this repository, under this configuration". An agent told it is a timing problem retries, or waits. 2. **"`search_text` on its name confirms whether it's indexed"** — it does not. `search_text` returns `total: 0` for an excluded path exactly as it does for a not-yet-indexed one. The remedy hands the caller the *same ambiguity* they came with, one tool over. So the error is corrective in shape and misleading in content: it produces a next step that cannot terminate. ## The tool that does answer it `index_coverage(path)` exists, is cheap, and returns a strict three-state verdict with the rule that fired: ``` verdict: "never" | "pending" | "indexed" reason: "hidden" | "ignore_file" | "too_large" | "nested_checkout" | … proof: { at: ".claude", detail: "hidden directory; only .github, .gitlab and .forgejo are allowlisted" } hint: "Permanent. Use shell for this path, or change the rule that excludes it." ``` `never` vs `pending` is precisely the distinction these two hints collapse — and `index_coverage` is *already* the thing the `evidence_gaps` semantics string points readers at. The vocabulary exists; these two errors predate it and were never rejoined to it. ## Why it is worth a number This is where an agent lands **first**. `file_outline` on a path the index cannot see is the opening move of half the sessions in this repo, and #201 is on record for what happens next: a lane read `internal_error`, concluded the tool was broken, and fell back to `grep` for the rest of the session — in a repo whose CLAUDE.md makes preferring the index a hard rule. A hint that sends the reader to a tool that cannot answer is the same dead end with better manners. ## What must NOT be done - **Do not always call `index_coverage` before erroring.** That pays a probe on every miss, including the common typo. Name it in the hint; let the caller spend the round trip only when they need the answer. - **Do not drop the transient clause.** A file genuinely can be new and not yet walked — that is `pending`, and it is a real state. The defect is that transient is offered as *the* explanation rather than one of two. - **Do not replace it with prose in the tool description.** Description text is unread and expensive here, and measured as such. - **Do not fix one and leave the other.** Two call sites, one wording, one clause — this project's rule is one mechanism, not a fix per site. ## Shape of a fix Both hints name `index_coverage(path)` as the way to tell a permanent exclusion from a lag, and stop asserting the transient reading as the likely one. One shared string, since the two sites already say the same thing. ## What a fix must prove - The hint from a path excluded by a walker rule names `index_coverage`, and following it returns `verdict: "never"` with the rule — end to end, not just the string. - **Anti-vacuity, both arms:** a plain typo still gets the path-shape advice it gets today (the common case must not regress into a diagnostic lecture), and a genuinely not-yet-walked file still reads as possibly-transient. - Both call sites are covered by one clause; a mutation of that clause reddens **both**, or they are not shared. - No probe is added to the error path — measured, not asserted. ## Related #238 (where this was spotted; its part 1 fixed the same class for `empty_population`), #201 (the corrective-error work these two hints predate), #220 (a blind spot that reports as a measurement is worse than one that reports as a limit). Filed 2026-09-09 against `master` `1d3228e`.
Author
Member

Fixed on master at e97972c (merged 98d3e3f, in origin/master). 15 mutations, 15 RED, 0 survivors.

Both hints now derive from one shared clause, PATH_NOT_INDEXED_NEXT_STEP (crates/mcp-server/src/server.rs:5360), which names index_coverage(path) and keeps the transient reading as one of two rather than the only story.

I re-ran the deciding mutation myself — putting the search_text regression back inside the shared const:

an_excluded_path_is_sent_to_a_call_that_returns_never_with_the_rule ... FAILED
  `file_outline`'s hint must name the call that terminates, not one that
  answers `total: 0` for both states.

The test walks both call sites in one body, and a separate test (the_two_hints_are_one_clause) asserts they carry the same string — which correctly stays GREEN under that mutation, because both moved together. Content graded by one test, sharing by another. Mutating one site's wording alone (M3) reds the second: "one clause, two call sites — a change to how this project explains an unindexed path must be a change to ONE string."

One correction to this issue. "Two hints, same wording" is not quite true on master — they were two different strings saying the same wrong thing (no_outline also carried "…is indexed at all", and path_not_indexed has a directory sibling). The fix still lands as one clause; worth noting because "one wording" would have let a reader assume a shared const already existed.

Cost, measured: the no_outline reply grows 770 → 1,036 B and path_not_indexed 299 → 572 B — error paths only. A successful call pays nothing, and the startup payload is byte-for-byte identical (66,048 chars before and after).

Anti-vacuity arms both run: a_plain_typo_still_gets_the_path_shape_advice_first (M4 reds if the diagnosis is put before the correction — "paying for the diagnosis before the correction is how a corrective error becomes a lecture"), and a_new_unwalked_file_still_reads_as_transient_and_resolves_to_pending (M5 reds if the transient sentence is deleted).

Verification: fmt 0 · clippy -D warnings 0 · rustdoc -D warnings 0 · cargo test --workspace 331 result lines · both the snapshot and COSI_E2E_LEG=daemon legs 0 · corpus_ratchet with baseline.json unmoved · precision_gate 7/7 phantoms=0.

Closing — and noting it sat fixed-and-open for a couple of hours, which is the defect I closed seven other issues for today.

Fixed on `master` at `e97972c` (merged `98d3e3f`, in `origin/master`). **15 mutations, 15 RED, 0 survivors.** Both hints now derive from one shared clause, `PATH_NOT_INDEXED_NEXT_STEP` (`crates/mcp-server/src/server.rs:5360`), which names `index_coverage(path)` and keeps the transient reading as *one of two* rather than the only story. I re-ran the deciding mutation myself — putting the `search_text` regression back **inside the shared const**: ``` an_excluded_path_is_sent_to_a_call_that_returns_never_with_the_rule ... FAILED `file_outline`'s hint must name the call that terminates, not one that answers `total: 0` for both states. ``` The test walks **both** call sites in one body, and a separate test (`the_two_hints_are_one_clause`) asserts they carry the same string — which correctly stays GREEN under that mutation, because both moved together. Content graded by one test, sharing by another. Mutating one site's wording alone (M3) reds the second: *"one clause, two call sites — a change to how this project explains an unindexed path must be a change to ONE string."* **One correction to this issue.** "Two hints, same wording" is not quite true on `master` — they were two *different* strings saying the same wrong thing (`no_outline` also carried "…is indexed **at all**", and `path_not_indexed` has a directory sibling). The fix still lands as one clause; worth noting because "one wording" would have let a reader assume a shared `const` already existed. Cost, measured: the `no_outline` reply grows 770 → 1,036 B and `path_not_indexed` 299 → 572 B — **error paths only**. A successful call pays nothing, and the startup payload is byte-for-byte identical (66,048 chars before and after). Anti-vacuity arms both run: `a_plain_typo_still_gets_the_path_shape_advice_first` (M4 reds if the diagnosis is put before the correction — *"paying for the diagnosis before the correction is how a corrective error becomes a lecture"*), and `a_new_unwalked_file_still_reads_as_transient_and_resolves_to_pending` (M5 reds if the transient sentence is deleted). Verification: `fmt` 0 · `clippy -D warnings` 0 · rustdoc `-D warnings` 0 · `cargo test --workspace` 331 result lines · both the snapshot and `COSI_E2E_LEG=daemon` legs 0 · `corpus_ratchet` with `baseline.json` unmoved · `precision_gate` 7/7 phantoms=0. Closing — and noting it sat fixed-and-open for a couple of hours, which is the defect I closed seven other issues for today.
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#243
No description provided.