fix(explore): report extension-less paths the index doesn't hold (#1830) - #1837
Draft
maxmilian wants to merge 1 commit into
Draft
fix(explore): report extension-less paths the index doesn't hold (#1830)#1837maxmilian wants to merge 1 commit into
maxmilian wants to merge 1 commit into
Conversation
…bymchenry#1830) A query span like `scripts/deploy` was neither pinned nor reported. `isClearlyPathShaped` demands a dot-extension on the last segment, so a dotless slashed span that resolved to no indexed file fell through every arm: it was not consumed, never entered `unresolvedPathSpans`, and its fragments (`scripts`, `deploy`) went on to feed FTS. The agent got a pile of unrelated source with no hint that the file it named was never read. Both caveat sinks — the empty-subgraph note and the summary line — were already in place; they were simply never handed the span. Shape alone cannot decide this: `and/or`, `input/output` and `client/server` have exactly the same shape, and loosening the shape test to "has a slash" strips that prose out of every query and mints a false "No indexed file uniquely matches `and/or`" caveat (the existing `leaves slash-bearing non-paths alone` test pins this). The file's existence on disk is the discriminator instead — and since query-paths.ts is deliberately pure (no DB, no fs, safe in the query-pool workers), the check is injected by the caller as an `existsOnDisk` predicate. The call site in tools.ts owns the project root and runs it through `validatePathWithinRoot`, so a `../` span cannot probe outside the project, and only a regular file counts — a directory span keeps flowing to the normal matching pipeline. Retrieval effect (AGENTS.md "do not regress"), measured as a deterministic A/B on the built dist over an indexed repro, baseline = upstream/main: - query that is only the dotless path — before: 998 chars of unrelated source, no caveat; after: `No relevant code found ... (no indexed file uniquely matches `scripts/deploy`)`. - same path inside a real symbol query — output byte-identical except the added caveat sentence (+51 chars); the same 4 symbols across 2 files. - query with no path span — byte-identical between arms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDR1wm73oH8J8cRWnKyv9m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1830.
The bug
codegraph_exploretreats a path-shaped span with no extension on its last segment —scripts/deploy,bin/build,scripts/pre-commitin a repo that doesn't index it — as free text. The agent gets back a pile of unrelated source with no indication at all that the file it named was never consulted.Root cause
extractQueryPaths(src/search/query-paths.ts) sends a span tounresolvedPathSpans— the "say the miss out loud" channel — under:and
isClearlyPathShapedrequires a dot-extension on the last segment:scripts/deployhas a slash, so it becomes a candidate;resolveSpanreturns nothing (the file is not indexed); and then both arms are false — so the token is not consumed, never entersunresolvedPathSpans, and its fragments (scripts,deploy) go on to feed FTS.Both downstream caveat sinks already exist and work — the empty-subgraph note and the summary line ("No indexed file uniquely matches …"). They were simply never handed the span.
Why fs-existence and not a looser shape
The obvious "fix" — treat any slash-bearing span as a path — is wrong, and this repo already has a test that says so.
__tests__/query-paths.test.ts'sleaves slash-bearing non-paths aloneasserts thatcomes back completely untouched. Loosening the shape test turns that red, strips
and/or,input/output,client/serverout of queries repo-wide, and mints a falseNo indexed file uniquely matches `and/or`caveat in the response. Shape genuinely cannot separate the two cases —scripts/deployandand/orare the same shape. The file's existence on disk is the only discriminator available.Why the predicate is injected
query-paths.ts's docstring states the module is "Pure string work — no DB, no fs — so it is trivially testable and safe inside the query-pool workers." That is load-bearing, so nofswas added to the module. InsteadextractQueryPathstakes an optionalexistsOnDisk?: (relPath: string) => boolean, and the single call site insrc/mcp/tools.ts— which ownsprojectRoot— supplies it:validatePathWithinRoot, so a../-bearing span in an agent query cannot probe outside the project.src/search) is not a file reference and keeps flowing to the normal matching pipeline.false; the whole extraction is already wrapped incatch { /* path pinning must never fail an explore call */ }.Net change in
query-paths.tsis two lines plus doc.Retrieval effect — AGENTS.md "do not regress"
This change makes a token get consumed and removed from
matchQuery, so it moves retrieval and falls under AGENTS.md's Retrieval performance (do not regress). I ran a deterministic A/B on the builtdist(scripts/agent-eval/probe-explore.mjs) over an indexed repro wherescripts/deployexists on disk but is not indexed. Baseline arm = this branch's merge-base build; fixed arm = this branch.I deliberately did not use
ab-new-vs-baseline.shhere: it drives a real agent on an implementation task, and no such task reliably emits a query naming a dotless unindexed path — both arms would have missed the changed code entirely and the numbers would have been noise.No relevant code found for "…" (no indexed file uniquely matches `scripts/deploy`)The middle row is the important one: when the query carries real content, nothing about what gets retrieved changes — only the caveat is added. The first row is the change the issue asks for, and worth an explicit maintainer call:
unresolvedPathSpansin this module has always coupled "report the span" with "strip it from the query", and this arm inherits that coupling, so a query that is only the dotless path now answers with the caveat instead of unrelated source. I did not decouple the two.Tests
__tests__/query-paths.test.tsgains 6 tests (27 → 33):and/orprose stays untouched even with a predicate present — and the predicate is asserted to have been asked and refused, not bypassed by shape;input/output(slashed, not a file) stays untouched;scripts/pre-commit) still pins by resolution and is never asked about.Red-arm check — deleting only the new
||line, tests unchanged: 3 failed / 30 passed, with all 27 pre-existing tests (including theand/orguard) staying green. Restored: 33/33.npm run buildclean.npm test: 14 failed / 4425 passed / 192 skipped — the same 14 failures across the same 8 files (installer-targets,mcp-callers-truncation,nextjs,object-literal-methods,react-native-bridge,ui-steps-api,ui-steps-api-servers,ui-steps-cross-tier) that the unpatched merge-base produces; unrelated to this change.CHANGELOG.md[Unreleased] → Fixesupdated.Out of scope
The issue also mentions the
prompt-hookhaving the same blind spot, and asks about gating the "Complete source for N files … do NOT re-read them" footer. Both are separate decisions with their own trade-offs; this PR does only the caveat.🤖 Generated with Claude Code
https://claude.ai/code/session_01XDR1wm73oH8J8cRWnKyv9m