@@ -157,6 +157,18 @@ function checkLayeringRules(edges: readonly ResolvedImportEdge[]): LayeringViola
157157 return violations ;
158158}
159159
160+ /**
161+ * Catches: a production import cycle — A imports B imports A at the value level — that a
162+ * file-by-file review cannot see because each edge looks locally fine; only walking the
163+ * whole graph exposes the loop. No other gate looks at cycles at all.
164+ * Evidence: 3d70943550 (#984) introduced the import-direction DAG gate this cycle check
165+ * anchors; f19864e486 (#1410) added the dependency-graph report built on the same model.
166+ * Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or
167+ * test file to size separately).
168+ * Kill criterion: delete when the build graph itself enforces acyclicity (e.g. project
169+ * references per zone reject a cyclic reference edge at tsc time) so a static walk here
170+ * is redundant with a compiler error.
171+ */
160172function checkCycles ( edges : readonly ResolvedImportEdge [ ] ) : LayeringViolation [ ] {
161173 return findValueImportCycles ( edges ) . map ( ( cycle ) => ( {
162174 rule : 'R4 value-import-cycle' ,
@@ -191,6 +203,18 @@ function checkRecordRuntimeOwnership(sources: ReadonlyMap<string, string>): Laye
191203 } ) ;
192204}
193205
206+ /**
207+ * Catches: a value import that runs against the ranked target spine's declared order (a lower
208+ * zone importing a higher one) — the runtime-consequential half of what R6 also checks for
209+ * type-only edges; neither zone-policy.ts's table nor the cycle check names direction.
210+ * Evidence: 3d70943550 (#984) introduced the ranked spine and its back-edge check; docs/
211+ * dependency-graph-findings.md tracks the count this rule ratchets.
212+ * Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or
213+ * test file to size separately).
214+ * Kill criterion: delete when the spine's zones become physically separate packages (R11's
215+ * package-boundaries model) so an inverted import fails to resolve rather than needing a
216+ * graph walk to catch.
217+ */
194218function checkBackEdges ( edges : readonly ResolvedImportEdge [ ] ) : LayeringViolation [ ] {
195219 const seen = new Set < string > ( ) ;
196220 return edges . flatMap ( ( edge ) => {
@@ -209,6 +233,18 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation
209233 } ) ;
210234}
211235
236+ // Catches: a type-only import against the ranked spine's declared order — a design-level
237+ // dependency (zone A is stated in terms of zone B) that R5 is blind to because it costs
238+ // nothing at runtime, so nothing else flags "the type shape leaks the wrong direction."
239+ // Evidence: the R5-adjacent commits in check.ts's history introduced this ratchet; the 61-to-5
240+ // reduction and the two remaining deliberate inversions are recorded below and in
241+ // docs/dependency-graph-findings.md.
242+ // Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or test
243+ // file to size separately).
244+ // Kill criterion: delete when the two remaining inversions (commands/mcp -> client,
245+ // commands -> daemon-server) are resolved by moving the projection registry or the route
246+ // union to a lower zone, leaving the ratchet at zero with nothing left to hold down.
247+ //
212248// R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a type-only import
213249// is free at runtime), but "zone A is declared in terms of zone B" is still a boundary claim, and
214250// ranking type edges surfaced 61 of them. Down to 5, and every one of the 5 is now a deliberate
0 commit comments