NamespaceCycleDetector: two namespaces that reference each other. Neither can be read,
tested or moved without the other, so the pair is one unit wearing two names. Unlike a
declared layer breach it needs no config to be wrong — whatever stack a project meant,
nothing in a cycle can be at the bottom of it — so it fires out of the box.
Calibration reshaped it twice, and both changes are the detector:
- A namespace NESTED in another is part of it, not a peer. `App\Caching` leaning on its
own `App\Caching\Concerns` was flagged — a false positive, and the same freedom two
classes side by side in one namespace already have. Those edges also welded whole
trees into single components (653 findings here, 803 in laravel-data).
- Report the MUTUAL PAIR, not the strongly-connected component. A 130-namespace tangle
is a cycle too, but no single arrow in it is the mistake, so naming all 1483 of them
named nothing. A pair is where a person can decide. Findings dropped to 15/20/78 on
three real trees, each one arrow someone can cut.
Of the two directions it reports the THINNER one: fewest places to change, and after
the cut the pair points one way again.
New shared analysis: Ast\Support\NamespaceGraph — the namespace dependency graph, built
once per codebase, with the mutual pairs and a dependency order. The `layers` command
reads the same graph.
Verified by eye on real hits: App\Models implement App\Contracts while
BackupServiceInterface returns a Model back — the flagged arrow is the accidental one.
The Shop fixture grew a Tokens layer so the layer demo no longer forms a pair of its
own, and the Enums <-> Shipping cycle already sitting in it is now marked.