Build manifest: one route per fact - #490
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #490 +/- ##
=========================================
Coverage 99.41% 99.41%
Complexity 1865 1865
=========================================
Files 136 136
Lines 4784 4784
=========================================
Hits 4756 4756
Misses 28 28 ☔ View full report in Codecov by Harness. |
Firehed
marked this pull request as ready for review
September 4, 2026 17:25
Firehed
added a commit
that referenced
this pull request
Sep 4, 2026
Stacked on #490. Implements build-manifest step-32. ## Done - [ ] The test runs in `composer test` — `OneRoutePerFactTest` is a plain PHPUnit test under `tests/Architecture/`, picked up by the unit suite. - [ ] The canary test proves every reference form is caught — `testScannerCatchesEveryReferenceForm` reads `tests/Architecture/data/names-an-ingredient.php` and asserts the line of each form: import, extends, property type, parameter type, return type, `new`, static call, class constant, `instanceof`, `::class`, catch. - [ ] Every skip names a step in the manifest — each `pending` entry is `file => step-nn`; the skip message lists them. Three rows skip today (cursor syntax, open-document declarations, docblock types), three pass (syntax tree production, symbol lookup, name resolution rules). - [ ] `CLAUDE.md` Architecture Invariants names the test as the one-route mechanism, and the Guardrails self-check list names it. ## How a row behaves A row passes when only its holders and roots name a route class. A row with violations recorded in `pending` first asserts the violators are exactly that set, then skips naming the steps. So a new violator fails, a cleared violator fails until its entry is removed, and a route or holder class that no longer exists fails. Removing a `pending` entry is the step's work; adding one is a Loosen edit. ## Two readings of the row that I settled - The manifest says the syntax-tree row's ingredients are "the producers step-35 introduces". Those classes do not exist yet, and a row naming a class that does not exist must fail, so today the row's ingredients are php-parser's `Parser` and `ParserFactory`, held by `ParserService`. Step-35 swaps them for the producer classes. - The manifest says the cursor-syntax row's holder is "the cursor reader step-38 introduces". Same problem, and the end state is that `TextFallbackHelper` is deleted (step-39), so the row has no holder: nobody may name it, and every namer today is a pending entry. ## Also in the diff `LateBindingKeywordConfinementTest` and the new test share the file walk, so it moved into a `ScansSourceFiles` trait.
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.
Why
The rebuild's audits and where-rules compressed the M×N into the allowed layer instead of removing it.
Steps 10, 11, 13 and 31 each claimed to make one fact single-route, and each fact has more than one route again today.
The cause is the same every time: two routes to one fact produce different shapes, so a consumer has to hold both and pick, and a class that holds both will chain them wherever the need shows up.
This PR adds the steps that close every fact that has more than one route today, and the mechanism that keeps a fact from reopening.
It writes no fix.
What changes
TextFallbackHelperrule is replaced by the one-route rule in its full shape: one interface per fact; every implementation implements it, the chain included, and the chain is always namedComposite<Interface>; a consumer is typed on the interface and never names an implementation; only the composition root does; the interface, its composite, and every implementation share one namespace named for the interface. Three rules come from reviewing the first two step PRs: a skip runs its assertion first and fails when it passes; a test that tells states apart by fixture pins each state's property and its fixtures differ only in that property; a row states facts, and a false fact stops the implementer rather than inviting an interpretation.CodeResolvermethod across the three parse states. Today's violations are pending entries that run before they skip. 34 to 42 collapse parse health behind oneSyntaxSourceinterface: the interface first, thenCompositeSyntaxSourceoverPhpParserSyntaxSourcewith a memoizing decorator, the skeleton source carrying exactly what the text producer carries today, one write path,nodeAton the interface, a cursor-text source in two steps (member access, calls), then the text helper is deleted. 43 to 46 collapse name resolution and the two open-document stores. 47 to 49 collapse docblock types in three steps (accessor, merge, consumer). 50 makes the built-in origin guard an interface with one implementation. 51 foldsSymbolBackendintoSymbolSourceunderCompositeSymbolSource. 52 moves every family into its own namespace.do-nextgets a route check, stops on a false fact in the row, and may make the edits its row names.review-slicegets one-route, skip, and fixture questions.bin/check-baseline-shrinkcomment now points at step-53.Edits that the enforcement policy reserves for the human
By
enforcement-edits.md, the manifest prose, both skills, the RFC paragraph and thebin/comment are Loosen rows.Merging this PR is the authorisation for them and for the Tighten and Lateral edits each row names.
Facts the rows now state, checked against the vendored php-parser
startLine,endLine,startFilePos, andendFilePoson every node; the walkers read exactly those plus the namespacekind,parent, andresolvedNameattributes.Expr\Erroras a missing member name, so a synthesized$this->node has a known shape.Design assumptions to confirm before step-35 starts
Server::forProjectandKnowledgeStack::forProject, the hand-written container a real one replaces line for line; the 38 test sites that build a parser today get a factory undertests/, and no factory lives insrc/. Composites take aniterableso a tagged collection plugs in, and the memo is cleared through a one-method interface.SyntaxSource, so a detector holds one dependency; the cursor-text source is one more implementation in the composite, not a second seam.Notes