@@ -16,6 +16,13 @@ export type ResolvedImportEdge = ImportEdge & {
1616
1717export type BackEdgeMap = Record < string , string [ ] > ;
1818
19+ // The ranked target spine. Back-edge detection is defined ONLY between two ranked
20+ // zones: an edge whose source outranks its target (lower number imports higher) is a
21+ // spine back-edge. Zones NOT in this map are intentionally unranked (see
22+ // `UNRANKED_ZONES`); the gate does not rank them, so ranking their edges would claim a
23+ // back-edge guarantee the code does not make. Every production zone must be either
24+ // ranked here or listed as unranked — `unclassifiedZones` and `model.test.ts` guard
25+ // that no zone is silently unclassified.
1926const TARGET_DAG_RANK = new Map ( [
2027 [ 'kernel' , 0 ] ,
2128 [ 'contracts' , 1 ] ,
@@ -31,6 +38,40 @@ const TARGET_DAG_RANK = new Map([
3138 [ 'cli' , 6 ] ,
3239] ) ;
3340
41+ export const RANKED_ZONES : ReadonlySet < string > = new Set ( TARGET_DAG_RANK . keys ( ) ) ;
42+
43+ // Zones deliberately left OUT of the ranked spine. They are NOT unenforced: every file
44+ // in them is still subject to the global production value-import cycle rejection (R4)
45+ // and the R1-R3 move rules. They only opt out of spine back-edge ranking, for one of
46+ // two deliberate reasons:
47+ // - root: `(root)` entrypoints (src/cli.ts, src/backend.ts, …) compose the spine from
48+ // above rather than sitting inside it.
49+ // - peripheral: satellite feature/adapter zones the spine does not depend on in a
50+ // fixed rank order. Assigning them a rank would invent a back-edge direction the
51+ // architecture does not commit to.
52+ export const UNRANKED_ZONES : ReadonlySet < string > = new Set ( [
53+ '(root)' ,
54+ 'cloud-webdriver' ,
55+ 'compat' ,
56+ 'mcp' ,
57+ 'metro' ,
58+ 'recording' ,
59+ 'remote' ,
60+ 'replay' ,
61+ 'screenshot-diff' ,
62+ 'sdk' ,
63+ 'snapshot' ,
64+ 'utils' ,
65+ ] ) ;
66+
67+ export type ZoneClassification = 'ranked' | 'unranked' | 'unclassified' ;
68+
69+ export function classifyZone ( zone : string ) : ZoneClassification {
70+ if ( RANKED_ZONES . has ( zone ) ) return 'ranked' ;
71+ if ( UNRANKED_ZONES . has ( zone ) ) return 'unranked' ;
72+ return 'unclassified' ;
73+ }
74+
3475function scanDynamicImports ( line : string , lineNo : number ) : ImportEdge [ ] {
3576 const edges : ImportEdge [ ] = [ ] ;
3677 const re = / i m p o r t \s * \( \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g;
@@ -102,12 +143,25 @@ export function topFolder(file: string): string {
102143 return match ? match [ 1 ] ! : '(root)' ;
103144}
104145
105- function targetDagZone ( file : string ) : string {
146+ export function targetDagZone ( file : string ) : string {
106147 if ( file . startsWith ( 'src/daemon/client/' ) ) return 'daemon-client' ;
107148 if ( file . startsWith ( 'src/daemon/' ) ) return 'daemon-server' ;
108149 return topFolder ( file ) ;
109150}
110151
152+ // The set of zones every production file resolves into. A zone that is neither ranked
153+ // nor listed as intentionally unranked is an unclassified drift signal.
154+ export function collectZones ( files : readonly string [ ] ) : Set < string > {
155+ return new Set ( files . map ( targetDagZone ) ) ;
156+ }
157+
158+ // Zones present in `files` that are neither ranked nor intentionally unranked. A new
159+ // `src/<folder>/` must be classified deliberately; leaving it unclassified would let
160+ // its back-edges silently escape the ranked spine. Empty means the partition holds.
161+ export function unclassifiedZones ( files : readonly string [ ] ) : string [ ] {
162+ return [ ...collectZones ( files ) ] . filter ( ( zone ) => classifyZone ( zone ) === 'unclassified' ) . sort ( ) ;
163+ }
164+
111165function resolveTargetFile (
112166 fromFile : string ,
113167 spec : string ,
0 commit comments