Skip to content

Build manifest: one route per fact - #490

Merged
Firehed merged 19 commits into
mainfrom
manifest/one-route-per-fact
Sep 4, 2026
Merged

Build manifest: one route per fact#490
Firehed merged 19 commits into
mainfrom
manifest/one-route-per-fact

Conversation

@Firehed

@Firehed Firehed commented Sep 4, 2026

Copy link
Copy Markdown
Owner

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

  • Manifest rules. The freeze becomes a closed set: a Tighten or Lateral edit is allowed only when the row names it, which is what steps 26 to 31 already did in practice. The TextFallbackHelper rule 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 named Composite<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.
  • Steps 32 to 52 (retire is now step-53). 32 and 33 are the two ledgers: a shape test that derives every implementation of a watched interface and checks the composite's name, the family namespace, and that only the root names an implementation, plus transitional rows for routes with no interface yet; and a grid that runs every position-taking CodeResolver method across the three parse states. Today's violations are pending entries that run before they skip. 34 to 42 collapse parse health behind one SyntaxSource interface: the interface first, then CompositeSyntaxSource over PhpParserSyntaxSource with a memoizing decorator, the skeleton source carrying exactly what the text producer carries today, one write path, nodeAt on 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 folds SymbolBackend into SymbolSource under CompositeSymbolSource. 52 moves every family into its own namespace.
  • Skills. do-next gets a route check, stops on a false fact in the row, and may make the edits its row names. review-slice gets one-route, skip, and fixture questions.
  • RFC 1 §4.11 and Appendix A describe the collapse below the positional layer.
  • CLAUDE.md gains the one-route invariant and drops three names of classes that no longer exist.
  • bin/check-baseline-shrink comment 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 the bin/ 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

  • php-parser 5 records startLine, endLine, startFilePos, and endFilePos on every node; the walkers read exactly those plus the namespace kind, parent, and resolvedName attributes.
  • The member-access AST path already accepts php-parser's Expr\Error as a missing member name, so a synthesized $this-> node has a known shape.
  • A structure-only skeleton would change the broken-file goldens, because the write path would switch tiers with fewer members, so step-36 carries the members the text producer carries today, in one step.

Design assumptions to confirm before step-35 starts

  • The tree shape stays php-parser's node model. A future producer for another parser converts into it. Nothing above the composite learns which producer answered.
  • Composition stays in Server::forProject and KnowledgeStack::forProject, the hand-written container a real one replaces line for line; the 38 test sites that build a parser today get a factory under tests/, and no factory lives in src/. Composites take an iterable so a tagged collection plugs in, and the memo is cleared through a one-method interface.
  • The node at the cursor is a method on SyntaxSource, so a detector holds one dependency; the cursor-text source is one more implementation in the composite, not a second seam.

Notes

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.41%. Comparing base (a4b48c8) to head (8509fec).
✅ All tests successful. No failed tests found.

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.
📢 Have feedback on the report? Share it here.

@Firehed Firehed mentioned this pull request Sep 4, 2026
4 tasks
@Firehed
Firehed marked this pull request as ready for review September 4, 2026 17:25
@Firehed
Firehed merged commit 6bdefcb into main Sep 4, 2026
9 checks passed
@Firehed
Firehed deleted the manifest/one-route-per-fact branch September 4, 2026 21:37
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.
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