Walk one process out of a project - #31
Merged
Merged
Conversation
Phase 1 of #23. ProcessSlice.From takes an action, a controller method, a controller or an entity by name and walks outward: an action to the handler it runs, a method to the methods it calls and the entities it names, a controller to the entities it is activated for, an entity to the rules that govern it. Breadth-first, bounded by depth, syntax-only -- a call is matched by name against what the project declares, with no compilation and no symbol table. The scope is computed rather than asked for, which is the governing decision of the whole feature. A model asked what belongs in "the approval process" answers authoritatively, in a form nobody can check, and is wrong in the places that look exactly like the places it is right. A call that syntax cannot follow is printed rather than dropped. A virtual declaration is followed to what is written beside the call, and reported with every override that may replace it. The consequence is stated because it is the point: the bodies never entered mean entities missing from the slice, so a walk that stopped there in silence would read as a complete account of a process it never went into. Methods now record virtual/abstract and override, which is what makes that distinction possible at all. The depth bound is reported for the same reason: a walk that ran out of things to reach is a whole process, a walk that stopped at its limit is a view of one. Four things the probe found that the tests would only have enshrined: - The action-to-controller edge pointed the wrong way, saying an action declares its controller. Phase 2 draws arrows off this set. - An entity was touched because a property called Customer happened to match a class called Customer. It is now grounded in a declared relationship, so it is right for a reason rather than by luck. - An action's extracted body is its handler's body, so walking both gave the action a copy of every edge the handler had. - Preferring the caller's own declaration hid the virtual dispatch it was supposed to expose. Found while building the fixture for it. New fixture WalkthroughSolution, whose proportions are its point: one action, one handler, and a Recalculate that two controllers override. No existing fixture could reach that case. Nothing consumes the slice yet. The Mermaid diagram, the document, the CLI command and the MCP tool are phase 2. 363 tests, zero warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W45tzJFX3NoSrk7svtQeKT
Comment on lines
+68
to
+72
| foreach (var action in controller.Actions) | ||
| { | ||
| if (action.ActionId.Equals(seed, StringComparison.OrdinalIgnoreCase)) | ||
| return Action(controller, action); | ||
| } |
Comment on lines
+79
to
+86
| foreach (var method in controller.Methods) | ||
| { | ||
| if (method.Name.Equals(seed, StringComparison.OrdinalIgnoreCase) | ||
| || $"{controller.ClassName}.{method.Name}".Equals(seed, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return Method(controller, method); | ||
| } | ||
| } |
Comment on lines
+197
to
+205
| foreach (var related in _project.Entities | ||
| .SelectMany(entity => entity.Relationships) | ||
| .Where(relationship => relationship.PropertyName.Equals(propertyName, StringComparison.Ordinal)) | ||
| .Select(relationship => relationship.RelatedEntity) | ||
| .Distinct(StringComparer.Ordinal)) | ||
| { | ||
| if (Entity(related) is { } entity) | ||
| yield return entity; | ||
| } |
Comment on lines
+214
to
+218
| foreach (var name in controller.ReferencedEntities) | ||
| { | ||
| if (Entity(name) is { } referenced) | ||
| yield return referenced; | ||
| } |
Comment on lines
+245
to
+259
| else if (candidates.Count > 1) | ||
| { | ||
| // The shape a virtual call takes in syntax: a base declaration and its overrides all | ||
| // carry one name, and which of them runs is decided at run time. | ||
| if (seenUnresolved.Add($"{from.Node.Id}|{name}")) | ||
| { | ||
| unresolved.Add(new UnresolvedCall | ||
| { | ||
| From = from.Node.Id, | ||
| CallName = name, | ||
| Candidates = [.. candidates.Select(candidate => candidate.Node.Name)], | ||
| Reason = "several declarations carry this name; syntax alone cannot say which one runs", | ||
| }); | ||
| } | ||
| } |
Comment on lines
+268
to
+272
| foreach (var named in typeNames) | ||
| { | ||
| if (index.Entity(named) is { } entity) | ||
| yield return (entity, SliceEdgeKind.Touches, false); | ||
| } |
Comment on lines
+336
to
+343
| foreach (var node in code.DescendantNodes()) | ||
| { | ||
| if (node is not SimpleNameSyntax name || memberNodes.Contains(name)) | ||
| continue; | ||
|
|
||
| if (seen.Add(name.Identifier.Text)) | ||
| types.Add(name.Identifier.Text); | ||
| } |
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.
Phase 1 of #23 — the deterministic half, the one that needs no AI.
What it does
ProcessSlice.From(project, seed, depth)takes an action, a controller method, a controller or an entity by name and walks outward from it. Breadth-first, bounded, syntax-only: a call is matched by name against what the project declares, with no compilation and no symbol table.Seeded on the XPO fixture's
ApproveOrder:That is the thing the tool could not answer before. It knew every declaration and nothing about the path across them.
The decision this rests on
The scope is computed, not asked for. A model asked what belongs in "the approval process" answers authoritatively, in a form nobody can check, and is wrong in the places that look exactly like the places it is right. A walk answers something reviewable, something that diffs between two extractions, and something whose bad sentences can be fixed later without touching its structure.
What it cannot see, and says so
A call syntax cannot follow is printed, not dropped. The new fixture exists for exactly this shape — a handler calling
Recalculate(), one virtual declaration beside the call, two controllers overriding it:And the consequence is asserted, because it is the whole point: that slice contains no entities at all. The bodies doing the arithmetic were never entered, so what they write is not there. A walk that stopped in silence would read as a complete account of a process it never went into.
The depth bound is reported for the same reason. A walk that ran out of things to reach is a whole process; a walk that stopped at its limit is a view of one; rendering them identically is how a document claims completeness it does not have.
Four things the probe found that tests would only have enshrined
I ran it against the fixtures and read the output before writing a single assertion.
ApproveOrder Declares ApproveOrderController, which is false. Phase 2 draws arrows straight off this set, so a backwards edge is an arrow in a diagram the code does not support. Expansions can now declare an incoming edge.Customerwas reached by coincidence. The handler readsorder.Customer, and the identifierCustomermatched the classCustomer. Right here, wrong the first time an entity is calledStatus. A name written after a dot now counts only when the model declares a relationship of that name pointing at that entity — right for a reason instead of by luck.Recalculate()resolved to the local declaration and reported nothing. Methods now recordvirtual/abstractandoverride, which is what makes the distinction possible at all.The fixture
WalkthroughSolution—Billing.Module, two entities, three controllers. Its proportions are its point: one action, one handler, and aRecalculatethat two controllers override. No existing fixture could reach the unresolved case; every one of them resolves cleanly, which is exactly how a blind spot stays invisible.Deliberate limits, written down in the source
EntityGraphalready answers that question.Not in this PR
Nothing consumes the slice yet. The Mermaid diagram, the Markdown document,
xaflogic walkthroughandxaf_walkthroughare phase 2 — where, per the design, the diagram is emitted from this graph and never drawn by a model.363 tests, zero warnings.
🤖 Generated with Claude Code
https://claude.ai/code/session_01W45tzJFX3NoSrk7svtQeKT