Skip to content

fix(node): accept a line-numbered file reference in file-view (#1831) - #1836

Open
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/1831-node-line-range
Open

fix(node): accept a line-numbered file reference in file-view (#1831)#1836
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/1831-node-line-range

Conversation

@maxmilian

Copy link
Copy Markdown
Contributor

Fixes #1831.

The problem

codegraph node "src/app.ts:42-80" — the way an agent or a human pastes a file reference — answers:

No indexed file matches "src/app.ts:42-80". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.

for a file that is indexed. The reply is byte-for-byte identical to the one a genuinely missing path gets (codegraph node "scripts/nope.ts"), so the caller cannot tell "wrong path" from "right path, wrong spelling" and has no way to discover that dropping the :42-80 would have worked.

The cause is in handleFileView: its normalizer only touches separators and leading/trailing slashes, so the :42-80 stays part of the filename and never matches an indexed path.

Why this shape belongs here

Codegraph already handles it elsewhere. explore strips exactly these suffixes from agent-written paths — src/search/query-paths.ts:135:

// Line references ride along in agent-written paths: `foo.ts:123`,
// `foo.ts:12-40`, `foo.ts#L88`. The file is what gets pinned.
s = s.replace(/(?::\d+(?:-\d+)?|#L\d+(?:-L?\d+)?)$/, '');

So this is not a new convention — it is file-view catching up with the one the codebase already committed to. This PR reuses that exact pattern (with capture groups added, so the range is kept rather than discarded).

#L<n> is fixed too. The issue only reports :<a>-<b>, but src/app.ts#L42 and src/app.ts#L42-L80 — the GitHub-permalink spelling, and the other half of the precedent regex — failed identically. Fixing one and not the other would have left the same confusing message behind.

The fix

About eight lines in handleFileView, plus lifting the existing resolution cascade into a resolveOne() helper so it can be called twice:

  1. Literal first. The path is resolved exactly as written before anything is stripped, so a file genuinely named foo:12 still resolves to itself. The suffix path is reached only when the literal spelling matches nothing — zero regression by construction, not by assumption.
  2. Then strip, and keep the range. :<a>-<b> maps to offset=a, limit=b-a+1. A bare :<a> / #L<a> is a start-here pointer — offset=a with the default limit, exactly what Read(file, offset=N) does.
  3. The caller still wins. An explicit offset/limit (or --offset / --limit) overrides the suffix, so nothing is silently rewritten under a caller that asked for a specific window.
  4. A genuine miss still reports a miss, suffix or not.

No new gate counters, so docs/design/telemetry.md is untouched.

Verification

8 new cases in __tests__/node-file-view.test.ts and __tests__/cli-node-command.test.ts covering :a, :a-b, #Ln, #La-Lb, #La-b, a full repo-relative path with a suffix, caller-offset precedence, and a genuine miss.

Two of them are guards rather than assertions of the fix:

  • Vacuity guard — the same file without the suffix resolves, so a suffixed failure can only come from the suffix.
  • Genuine missdoes-not-exist.ts:10-20 must still report a miss.

Red-armed. With the fix reverted and the tests kept, 6 fail with the exact No indexed file matches "…" string; the vacuity guard and the genuine-miss case pass in that state, which is what makes the other six meaningful. With the fix applied: 23 passed (23) across both files.

Full suite: 4428 passed, with 14 pre-existing failures across 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) — identical count and identical files on the unmodified base commit, verified by stashing the change and re-running those files. npm run build is clean.

End-to-end on a real indexed project:

$ codegraph node "src/tiers.ts:1-3"
**src/tiers.ts** — 3 lines, 1 symbol · no other indexed file depends on it
1	export function tierExecution() { return deployRunner(); }
2	import { deployRunner } from "./deploy-runner";

$ codegraph node "scripts/nope.ts:1-3"
No indexed file matches "scripts/nope.ts:1-3". …

One thing I could not cover with a fixture

I wanted a regression fixture for a file whose name literally ends in :N, to pin the literal-first ordering. It turns out such a file can never reach this code path: the indexer resolves language by extension, and .ts:12 matches none, so getFiles() never contains such a path. The ordering is still guaranteed structurally — resolveOne(fileArg) runs before any stripping — just not by a fixture. Flagging it rather than leaving it look like an oversight.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XDR1wm73oH8J8cRWnKyv9m

…chenry#1831)

`codegraph node "src/app.ts:42-80"` answered `No indexed file matches`
for a file that IS indexed, worded byte-for-byte like a genuine miss, so
there was no way to tell "wrong path" from "right path, wrong spelling".
The file-view normalizer treated the `:42-80` as part of the filename.

explore has stripped exactly these shapes all along
(src/search/query-paths.ts) — file-view now does too, and the range it
strips becomes the read window.

- The literal spelling is resolved FIRST; only when that finds nothing is
  the suffix stripped, so a file genuinely named `foo:12` still wins.
- Supports `:<a>`, `:<a>-<b>`, `#L<n>`, `#L<a>-L<b>` (and `#L<a>-<b>`).
- A stripped range maps to `offset=a, limit=b-a+1`; a bare `:<a>` is a
  start-here pointer (Read given only an offset). An explicit
  offset/limit from the caller always wins over the suffix.
- `#L<n>` was not mentioned in the issue but was broken the same way.

Tests: 8 new cases across __tests__/node-file-view.test.ts and
__tests__/cli-node-command.test.ts, including a vacuity guard (the same
path without the suffix resolves) and a genuine-miss case. Red-armed:
with the fix reverted, 6 of them fail with the exact
`No indexed file matches` string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDR1wm73oH8J8cRWnKyv9m
@maxmilian
maxmilian marked this pull request as ready for review September 11, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node "<file>:<a>-<b>" answers "No indexed file matches" for an indexed file — indistinguishable from a real miss

1 participant