@@ -156,6 +156,20 @@ function checkLayeringRules(edges: readonly ResolvedImportEdge[]): LayeringViola
156156 return violations ;
157157}
158158
159+ /**
160+ * Catches: a production import cycle — A imports B imports A at the value level — that a
161+ * file-by-file review cannot see because each edge looks locally fine; only walking the
162+ * whole graph exposes the loop. No other gate looks at cycles at all.
163+ * Evidence: 3d70943550 (#984) introduced the import-direction DAG gate this cycle check
164+ * anchors; f19864e486 (#1410) added the dependency-graph report built on the same model.
165+ * Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or
166+ * test file to size separately).
167+ * Kill criterion: none enforced today; retire only by maintainer decision that an acyclic
168+ * value-import graph no longer matters. tsc rejects a cyclic `references` edge between
169+ * projects, but no tsconfig declares references today and the A4 spike found they are a
170+ * build-cache mechanism, not a boundary: value imports inside one project are never
171+ * cycle-checked, and a cross-package edge resolves through root node_modules with no error.
172+ */
159173function checkCycles ( edges : readonly ResolvedImportEdge [ ] ) : LayeringViolation [ ] {
160174 return findValueImportCycles ( edges ) . map ( ( cycle ) => ( {
161175 rule : 'R4 value-import-cycle' ,
@@ -190,6 +204,19 @@ function checkRecordRuntimeOwnership(sources: ReadonlyMap<string, string>): Laye
190204 } ) ;
191205}
192206
207+ /**
208+ * Catches: a value import that runs against the ranked target spine's declared order (a lower
209+ * zone importing a higher one) — the runtime-consequential half of what R6 also checks for
210+ * type-only edges; neither zone-policy.ts's table nor the cycle check names direction.
211+ * Evidence: 3d70943550 (#984) introduced the ranked spine and its back-edge check; docs/
212+ * dependency-graph-findings.md tracks the count this rule ratchets.
213+ * Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or
214+ * test file to size separately).
215+ * Kill criterion: none enforced today; retire only by maintainer decision that the ranked spine's
216+ * import direction no longer matters. Splitting zones into packages would not replace it: the
217+ * A4 spike found an undeclared workspace package still resolves through root node_modules and
218+ * a relative tunnel into another package's src still compiles.
219+ */
193220function checkBackEdges ( edges : readonly ResolvedImportEdge [ ] ) : LayeringViolation [ ] {
194221 const seen = new Set < string > ( ) ;
195222 return edges . flatMap ( ( edge ) => {
@@ -208,6 +235,18 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation
208235 } ) ;
209236}
210237
238+ // Catches: a type-only import against the ranked spine's declared order — a design-level
239+ // dependency (zone A is stated in terms of zone B) that R5 is blind to because it costs
240+ // nothing at runtime, so nothing else flags "the type shape leaks the wrong direction."
241+ // Evidence: the R5-adjacent commits in check.ts's history introduced this ratchet; the 61-to-5
242+ // reduction and the two remaining deliberate inversions are recorded below and in
243+ // docs/dependency-graph-findings.md.
244+ // Cost: not attributed (folded into check.ts's whole-graph pass; no standalone module or test
245+ // file to size separately).
246+ // Kill criterion: none enforced today; retire only by maintainer decision that type-only spine
247+ // inversions no longer matter. Reaching zero remaining inversions does not retire it: at zero
248+ // the ratchet is what keeps the count from regrowing, and tsc never rejects a type-only edge.
249+ //
211250// R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a type-only import
212251// is free at runtime), but "zone A is declared in terms of zone B" is still a boundary claim, and
213252// ranking type edges surfaced 61 of them. Down to 5, and every one of the 5 is now a deliberate
0 commit comments