Skip to content

Say where every declaration is - #30

Merged
peopleworks merged 1 commit into
mainfrom
feat/declaration-locations
Aug 22, 2026
Merged

Say where every declaration is#30
peopleworks merged 1 commit into
mainfrom
feat/declaration-locations

Conversation

@peopleworks

Copy link
Copy Markdown
Owner

First step of #23. Nothing in the model knew a line number, and the walkthrough cannot exist without one.

Why this is the first thing

The design in #23 rests on a single promise: every claim in a walkthrough carries a file:line a reader can open and check. That is what separates a traced account from a plausible one, and it is why the graph is extracted rather than generated.

The extraction knew which file a class was in and nothing narrower. So there was nothing to cite with.

It is also worth having on its own. xaf_entity and xaf_controller handed an agent a name, a base class and a body of code, and never said where any of it was — the agent's next move was to search the project for the thing it had just been given.

The one decision that matters

The line comes from the identifier token, not from the declaration's span.

A span begins at the first attribute. Customer in the XPO fixture sits behind a doc comment and four attributes:

[DefaultClassOptions]        // line 11  ← where the span starts
[NavigationItem("Sales")]
[XafDefaultProperty(nameof(Name))]
[Description("A company that places orders.")]
public class Customer : XPCustomObject   // line 15  ← where the name is

Both answers are correct about the syntax. Only one is the line anybody means, and they are four apart. SourceLine.Of takes a SyntaxToken for exactly this reason, and returns zero rather than one when there is no source to point at — one is a plausible-looking claim about a file, and zero is not a line, so nothing renders it as though it were.

Actions and methods carry their own file, not just their own line. A partial controller — the shape the XAF designer generates — can declare a method in a different file from the one the class is cited at. Borrowing the controller's path would produce a citation that opens the wrong file.

What an agent now sees

# ApproveOrderController

Base class `ViewController<DetailView>`.
Declared at `…\Controllers\ApproveOrderController.cs:11`.
Applies to: Order, DetailView only.

## Action `ApproveOrder`

- Type: SimpleAction
- Declared at line 13
…
### `void ApproveAction_Execute(object sender, SimpleActionExecuteEventArgs e)`

Declared at line 31.

The file is named once. That came out of reading a rendered response rather than the assertions — the first version repeated a hundred characters of absolute path on every action and every helper method, which on a controller with eight actions spends a paragraph of an agent's context window on one file name. The same reasoning MaxCodeLength already encodes.

A member in a different file is still printed in full, because that is precisely the case where a reader following line 31 would open the wrong one.

The tests

The important one does not assert a number. It reads the fixture back off disk and requires the cited line to contain the declaration — across every entity and every controller in all seven sample projects:

Assert.True(Regex.IsMatch(text, $@"\b(class|record|interface|struct)\s+{name}\b"),)

That is the only assertion that catches both ways this goes wrong silently: an off-by-one from a zero-based line, and a span that started at an attribute. Plus explicit pins on the Customer attribute case, on an action being cited at its own field rather than at its class, and on the rendered MCP response naming the file exactly once.

355 tests, zero warnings.

What is not here

The slice itself — ProcessSlice.From(project, seed, depth), the call and reference graph, unresolved edges. That is the substantial half of phase 1 and it goes in its own pull request, on top of this.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W45tzJFX3NoSrk7svtQeKT

The extraction knew which file a class was in and nothing narrower. An agent
told about a controller still had to search the file for it, and the
walkthrough in #23 cannot exist at all without this: every claim in one is
supposed to carry a file:line a reader can open and check.

Entities, controllers, actions and methods now carry a one-based line, taken
from the identifier token rather than from the declaration's span. A span
begins at the first attribute, and Customer sits behind a doc comment and four
of them, so the two answers are four lines apart -- and only one of them is
the line anybody means.

Actions and methods carry their own file as well as their own line, because a
partial controller's members need not be declared in the file the controller
itself is cited at.

The MCP tools name the file once and then cite members by line alone. That was
found by reading a rendered response rather than the assertions: the first
version repeated a hundred characters of absolute path on every action and
every helper method, which on a controller with eight actions would have spent
a paragraph of an agent's context on one file name. A member in a different
file is still given in full -- exactly the case where a reader would otherwise
open the wrong one.

The tests read the fixture back off disk and require the cited line to contain
the declaration, across every sample project. It is the only assertion that
catches an off-by-one or a span that started at an attribute. 355 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W45tzJFX3NoSrk7svtQeKT
@peopleworks
peopleworks merged commit 2994381 into main Aug 22, 2026
6 checks passed
@peopleworks
peopleworks deleted the feat/declaration-locations branch August 22, 2026 23:11
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.

1 participant