@ontrails/warden 1.0.0-beta.3 → 1.0.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +674 -9
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +28 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +137 -0
  7. package/src/cli.ts +1451 -104
  8. package/src/command.ts +966 -0
  9. package/src/config.ts +193 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +122 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +217 -14
  16. package/src/project-context.ts +192 -0
  17. package/src/project-rules.ts +290 -0
  18. package/src/resolve.ts +531 -0
  19. package/src/rules/activation-orphan.ts +97 -0
  20. package/src/rules/ast.ts +4008 -86
  21. package/src/rules/circular-refs.ts +154 -0
  22. package/src/rules/cli-command-route-coherence.ts +177 -0
  23. package/src/rules/composes-declarations.ts +837 -0
  24. package/src/rules/context-no-surface-types.ts +78 -15
  25. package/src/rules/contour-exists.ts +251 -0
  26. package/src/rules/contour-ids.ts +15 -0
  27. package/src/rules/dead-internal-trail.ts +161 -0
  28. package/src/rules/dead-public-trail.ts +258 -0
  29. package/src/rules/draft-file-marking.ts +160 -0
  30. package/src/rules/draft-visible-debt.ts +87 -0
  31. package/src/rules/duplicate-public-contract.ts +91 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +735 -0
  35. package/src/rules/implementation-returns-result.ts +1409 -166
  36. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  37. package/src/rules/incomplete-crud.ts +581 -0
  38. package/src/rules/index.ts +235 -18
  39. package/src/rules/intent-propagation.ts +127 -0
  40. package/src/rules/layer-field-name-drift.ts +102 -0
  41. package/src/rules/library-projection-coherence.ts +100 -0
  42. package/src/rules/metadata.ts +755 -0
  43. package/src/rules/missing-reconcile.ts +98 -0
  44. package/src/rules/missing-visibility.ts +110 -0
  45. package/src/rules/no-destructured-compose.ts +194 -0
  46. package/src/rules/no-dev-permit-in-source.ts +99 -0
  47. package/src/rules/no-direct-implementation-call.ts +7 -7
  48. package/src/rules/no-legacy-layer-imports.ts +211 -0
  49. package/src/rules/no-native-error-result.ts +118 -0
  50. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  51. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  52. package/src/rules/no-sync-result-assumption.ts +1135 -98
  53. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  54. package/src/rules/no-throw-in-implementation.ts +10 -9
  55. package/src/rules/no-top-level-surface.ts +371 -0
  56. package/src/rules/on-references-exist.ts +194 -0
  57. package/src/rules/orphaned-signal.ts +150 -0
  58. package/src/rules/owner-projection-parity.ts +143 -0
  59. package/src/rules/permit-governance.ts +25 -0
  60. package/src/rules/public-export-example-coverage.ts +562 -0
  61. package/src/rules/public-internal-deep-imports.ts +457 -0
  62. package/src/rules/public-output-schema.ts +29 -0
  63. package/src/rules/public-union-output-discriminants.ts +150 -0
  64. package/src/rules/read-intent-fires.ts +187 -0
  65. package/src/rules/reference-exists.ts +98 -0
  66. package/src/rules/registry-names.ts +155 -0
  67. package/src/rules/resolved-import-boundary.ts +146 -0
  68. package/src/rules/resource-declarations.ts +693 -0
  69. package/src/rules/resource-exists.ts +179 -0
  70. package/src/rules/resource-id-grammar.ts +65 -0
  71. package/src/rules/resource-mock-coverage.ts +115 -0
  72. package/src/rules/scan.ts +38 -25
  73. package/src/rules/scheduled-destroy-intent.ts +44 -0
  74. package/src/rules/signal-graph-coaching.ts +191 -0
  75. package/src/rules/specs.ts +9 -5
  76. package/src/rules/static-resource-accessor-preference.ts +649 -0
  77. package/src/rules/surface-facet-coherence.ts +362 -0
  78. package/src/rules/trail-fork-coaching.ts +616 -0
  79. package/src/rules/trail-versioning-source.ts +1072 -0
  80. package/src/rules/trail-versioning-topo.ts +172 -0
  81. package/src/rules/types.ts +297 -8
  82. package/src/rules/unmaterialized-activation-source.ts +84 -0
  83. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  84. package/src/rules/valid-describe-refs.ts +162 -32
  85. package/src/rules/valid-detour-contract.ts +78 -0
  86. package/src/rules/warden-export-symmetry.ts +540 -0
  87. package/src/rules/warden-rules-use-ast.ts +1114 -0
  88. package/src/rules/webhook-route-collision.ts +243 -0
  89. package/src/trails/activation-orphan.trail.ts +84 -0
  90. package/src/trails/circular-refs.trail.ts +29 -0
  91. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  92. package/src/trails/composes-declarations.trail.ts +22 -0
  93. package/src/trails/context-no-surface-types.trail.ts +21 -0
  94. package/src/trails/contour-exists.trail.ts +21 -0
  95. package/src/trails/dead-internal-trail.trail.ts +26 -0
  96. package/src/trails/dead-public-trail.trail.ts +31 -0
  97. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  98. package/src/trails/draft-file-marking.trail.ts +16 -0
  99. package/src/trails/draft-visible-debt.trail.ts +16 -0
  100. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  101. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  102. package/src/trails/example-valid.trail.ts +25 -0
  103. package/src/trails/fires-declarations.trail.ts +23 -0
  104. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  105. package/src/trails/implementation-returns-result.trail.ts +20 -0
  106. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  107. package/src/trails/incomplete-crud.trail.ts +39 -0
  108. package/src/trails/index.ts +83 -0
  109. package/src/trails/intent-propagation.trail.ts +30 -0
  110. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  111. package/src/trails/library-projection-coherence.trail.ts +43 -0
  112. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  113. package/src/trails/missing-reconcile.trail.ts +33 -0
  114. package/src/trails/missing-visibility.trail.ts +22 -0
  115. package/src/trails/no-destructured-compose.trail.ts +44 -0
  116. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  117. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  118. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  119. package/src/trails/no-native-error-result.trail.ts +18 -0
  120. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  121. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  122. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  123. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  124. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  125. package/src/trails/no-top-level-surface.trail.ts +43 -0
  126. package/src/trails/on-references-exist.trail.ts +21 -0
  127. package/src/trails/orphaned-signal.trail.ts +36 -0
  128. package/src/trails/owner-projection-parity.trail.ts +26 -0
  129. package/src/trails/pending-force.trail.ts +21 -0
  130. package/src/trails/permit-governance.trail.ts +51 -0
  131. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  132. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  133. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  134. package/src/trails/public-output-schema.trail.ts +55 -0
  135. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  136. package/src/trails/read-intent-fires.trail.ts +20 -0
  137. package/src/trails/reference-exists.trail.ts +25 -0
  138. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  139. package/src/trails/resource-declarations.trail.ts +25 -0
  140. package/src/trails/resource-exists.trail.ts +27 -0
  141. package/src/trails/resource-id-grammar.trail.ts +39 -0
  142. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  143. package/src/trails/run.ts +162 -0
  144. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  145. package/src/trails/schema.ts +198 -0
  146. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  147. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  148. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  149. package/src/trails/topo.ts +6 -0
  150. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  151. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  152. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  153. package/src/trails/valid-describe-refs.trail.ts +18 -0
  154. package/src/trails/valid-detour-contract.trail.ts +71 -0
  155. package/src/trails/version-gap.trail.ts +35 -0
  156. package/src/trails/version-pinned-compose.trail.ts +23 -0
  157. package/src/trails/version-without-examples.trail.ts +38 -0
  158. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  159. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  160. package/src/trails/webhook-route-collision.trail.ts +50 -0
  161. package/src/trails/wrap-rule.ts +214 -0
  162. package/src/workspaces.ts +199 -0
  163. package/.turbo/turbo-build.log +0 -1
  164. package/.turbo/turbo-lint.log +0 -3
  165. package/.turbo/turbo-typecheck.log +0 -1
  166. package/dist/cli.d.ts +0 -46
  167. package/dist/cli.d.ts.map +0 -1
  168. package/dist/cli.js +0 -221
  169. package/dist/cli.js.map +0 -1
  170. package/dist/drift.d.ts +0 -26
  171. package/dist/drift.d.ts.map +0 -1
  172. package/dist/drift.js +0 -27
  173. package/dist/drift.js.map +0 -1
  174. package/dist/formatters.d.ts +0 -29
  175. package/dist/formatters.d.ts.map +0 -1
  176. package/dist/formatters.js +0 -87
  177. package/dist/formatters.js.map +0 -1
  178. package/dist/index.d.ts +0 -26
  179. package/dist/index.d.ts.map +0 -1
  180. package/dist/index.js +0 -26
  181. package/dist/index.js.map +0 -1
  182. package/dist/rules/ast.d.ts +0 -41
  183. package/dist/rules/ast.d.ts.map +0 -1
  184. package/dist/rules/ast.js +0 -163
  185. package/dist/rules/ast.js.map +0 -1
  186. package/dist/rules/context-no-surface-types.d.ts +0 -12
  187. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  188. package/dist/rules/context-no-surface-types.js +0 -96
  189. package/dist/rules/context-no-surface-types.js.map +0 -1
  190. package/dist/rules/implementation-returns-result.d.ts +0 -13
  191. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  192. package/dist/rules/implementation-returns-result.js +0 -277
  193. package/dist/rules/implementation-returns-result.js.map +0 -1
  194. package/dist/rules/index.d.ts +0 -22
  195. package/dist/rules/index.d.ts.map +0 -1
  196. package/dist/rules/index.js +0 -41
  197. package/dist/rules/index.js.map +0 -1
  198. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  199. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  200. package/dist/rules/no-direct-impl-in-route.js +0 -46
  201. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  202. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  203. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  204. package/dist/rules/no-direct-implementation-call.js +0 -39
  205. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  206. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  207. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  208. package/dist/rules/no-sync-result-assumption.js +0 -98
  209. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  210. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  211. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  212. package/dist/rules/no-throw-in-detour-target.js +0 -87
  213. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  214. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  215. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  216. package/dist/rules/no-throw-in-implementation.js +0 -34
  217. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  218. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  219. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  220. package/dist/rules/prefer-schema-inference.js +0 -86
  221. package/dist/rules/prefer-schema-inference.js.map +0 -1
  222. package/dist/rules/scan.d.ts +0 -8
  223. package/dist/rules/scan.d.ts.map +0 -1
  224. package/dist/rules/scan.js +0 -32
  225. package/dist/rules/scan.js.map +0 -1
  226. package/dist/rules/specs.d.ts +0 -29
  227. package/dist/rules/specs.d.ts.map +0 -1
  228. package/dist/rules/specs.js +0 -192
  229. package/dist/rules/specs.js.map +0 -1
  230. package/dist/rules/structure.d.ts +0 -13
  231. package/dist/rules/structure.d.ts.map +0 -1
  232. package/dist/rules/structure.js +0 -142
  233. package/dist/rules/structure.js.map +0 -1
  234. package/dist/rules/types.d.ts +0 -52
  235. package/dist/rules/types.d.ts.map +0 -1
  236. package/dist/rules/types.js +0 -2
  237. package/dist/rules/types.js.map +0 -1
  238. package/dist/rules/valid-describe-refs.d.ts +0 -7
  239. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  240. package/dist/rules/valid-describe-refs.js +0 -51
  241. package/dist/rules/valid-describe-refs.js.map +0 -1
  242. package/dist/rules/valid-detour-refs.d.ts +0 -6
  243. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  244. package/dist/rules/valid-detour-refs.js +0 -116
  245. package/dist/rules/valid-detour-refs.js.map +0 -1
  246. package/src/__tests__/cli.test.ts +0 -198
  247. package/src/__tests__/drift.test.ts +0 -74
  248. package/src/__tests__/formatters.test.ts +0 -157
  249. package/src/__tests__/implementation-returns-result.test.ts +0 -128
  250. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  251. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  252. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  253. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  254. package/src/__tests__/rules.test.ts +0 -215
  255. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  256. package/src/rules/no-direct-impl-in-route.ts +0 -77
  257. package/src/rules/no-throw-in-detour-target.ts +0 -150
  258. package/src/rules/valid-detour-refs.ts +0 -187
  259. package/tsconfig.json +0 -9
  260. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,154 @@
1
+ import {
2
+ collectContourReferenceTargetsByName,
3
+ findContourDefinitions,
4
+ offsetToLine,
5
+ parse,
6
+ } from './ast.js';
7
+ import type { AstNode } from './ast.js';
8
+ import { isTestFile } from './scan.js';
9
+ import type {
10
+ ProjectAwareWardenRule,
11
+ ProjectContext,
12
+ WardenDiagnostic,
13
+ } from './types.js';
14
+
15
+ const mergeReferenceGraphs = (
16
+ localGraph: ReadonlyMap<string, readonly string[]>,
17
+ contextGraph?: ReadonlyMap<string, readonly string[]>
18
+ ): ReadonlyMap<string, readonly string[]> => {
19
+ const merged = new Map<string, Set<string>>();
20
+
21
+ const addTargets = (source: string, targets: readonly string[]): void => {
22
+ const existing = merged.get(source);
23
+ if (existing) {
24
+ for (const target of targets) {
25
+ existing.add(target);
26
+ }
27
+ return;
28
+ }
29
+
30
+ merged.set(source, new Set(targets));
31
+ };
32
+
33
+ for (const [source, targets] of contextGraph ?? []) {
34
+ addTargets(source, targets);
35
+ }
36
+
37
+ for (const [source, targets] of localGraph) {
38
+ addTargets(source, targets);
39
+ }
40
+
41
+ return new Map(
42
+ [...merged.entries()].map(([source, targets]) => [source, [...targets]])
43
+ );
44
+ };
45
+
46
+ const findCyclePath = (
47
+ start: string,
48
+ graph: ReadonlyMap<string, readonly string[]>,
49
+ current = start,
50
+ path: readonly string[] = [start],
51
+ active: ReadonlySet<string> = new Set([start])
52
+ ): readonly string[] | null => {
53
+ for (const target of graph.get(current) ?? []) {
54
+ if (target === start) {
55
+ return [...path, target];
56
+ }
57
+
58
+ if (active.has(target)) {
59
+ continue;
60
+ }
61
+
62
+ const cycle = findCyclePath(
63
+ start,
64
+ graph,
65
+ target,
66
+ [...path, target],
67
+ new Set([...active, target])
68
+ );
69
+ if (cycle) {
70
+ return cycle;
71
+ }
72
+ }
73
+
74
+ return null;
75
+ };
76
+
77
+ const buildCircularReferenceDiagnostic = (
78
+ contourName: string,
79
+ cyclePath: readonly string[],
80
+ filePath: string,
81
+ line: number
82
+ ): WardenDiagnostic => ({
83
+ filePath,
84
+ line,
85
+ message: `Contour "${contourName}" participates in circular contour references: ${cyclePath.join(' -> ')}. Break the cycle by removing one contour reference, or extract the shared shape into a new contour neither side depends on.`,
86
+ rule: 'circular-refs',
87
+ severity: 'warn',
88
+ });
89
+
90
+ const checkCircularReferences = (
91
+ ast: AstNode,
92
+ sourceCode: string,
93
+ filePath: string,
94
+ graph: ReadonlyMap<string, readonly string[]>
95
+ ): readonly WardenDiagnostic[] => {
96
+ if (isTestFile(filePath)) {
97
+ return [];
98
+ }
99
+
100
+ return findContourDefinitions(ast).flatMap((definition) => {
101
+ const cyclePath = findCyclePath(definition.name, graph);
102
+ if (!cyclePath) {
103
+ return [];
104
+ }
105
+
106
+ return [
107
+ buildCircularReferenceDiagnostic(
108
+ definition.name,
109
+ cyclePath,
110
+ filePath,
111
+ offsetToLine(sourceCode, definition.start)
112
+ ),
113
+ ];
114
+ });
115
+ };
116
+
117
+ /**
118
+ * Warns when contour references form a direct or transitive cycle.
119
+ */
120
+ export const circularRefs: ProjectAwareWardenRule = {
121
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
122
+ const ast = parse(filePath, sourceCode);
123
+ if (!ast) {
124
+ return [];
125
+ }
126
+
127
+ const graph = collectContourReferenceTargetsByName(ast);
128
+ return checkCircularReferences(ast, sourceCode, filePath, graph);
129
+ },
130
+ checkWithContext(
131
+ sourceCode: string,
132
+ filePath: string,
133
+ context: ProjectContext
134
+ ): readonly WardenDiagnostic[] {
135
+ const ast = parse(filePath, sourceCode);
136
+ if (!ast) {
137
+ return [];
138
+ }
139
+
140
+ const localGraph = collectContourReferenceTargetsByName(
141
+ ast,
142
+ context.knownContourIds
143
+ );
144
+ return checkCircularReferences(
145
+ ast,
146
+ sourceCode,
147
+ filePath,
148
+ mergeReferenceGraphs(localGraph, context.contourReferencesByName)
149
+ );
150
+ },
151
+ description: 'Warn when contour references form direct or transitive cycles.',
152
+ name: 'circular-refs',
153
+ severity: 'warn',
154
+ };
@@ -0,0 +1,177 @@
1
+ import { deriveTrailCliCommandProjection } from '@ontrails/core';
2
+ import type { CliCommandRoute, Topo } from '@ontrails/core';
3
+ import type { TopoGraph } from '@ontrails/topographer';
4
+
5
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
6
+
7
+ const RULE_NAME = 'cli-command-route-coherence';
8
+ const TOPO_FILE = '<topo>';
9
+
10
+ interface RouteClaim {
11
+ readonly kind: CliCommandRoute['kind'];
12
+ readonly path: readonly string[];
13
+ readonly source: CliCommandRoute['source'];
14
+ readonly trailId: string;
15
+ }
16
+
17
+ const routeKey = (path: readonly string[]): string => path.join('\0');
18
+
19
+ const renderPath = (path: readonly string[]): string => path.join(' ');
20
+
21
+ const claimLabel = (claim: RouteClaim): string =>
22
+ `${claim.kind} route for trail "${claim.trailId}" (${claim.source})`;
23
+
24
+ const buildProjectionDiagnostic = (
25
+ trailId: string,
26
+ error: unknown
27
+ ): WardenDiagnostic => ({
28
+ filePath: TOPO_FILE,
29
+ line: 1,
30
+ message: `CLI command route projection for trail "${trailId}" is invalid: ${
31
+ error instanceof Error ? error.message : String(error)
32
+ }`,
33
+ rule: RULE_NAME,
34
+ severity: 'error',
35
+ });
36
+
37
+ const buildCollisionDiagnostic = (
38
+ path: readonly string[],
39
+ claims: readonly RouteClaim[]
40
+ ): WardenDiagnostic => ({
41
+ filePath: TOPO_FILE,
42
+ line: 1,
43
+ message: `CLI command route collision on "${renderPath(path)}": ${claims
44
+ .toSorted((a, b) => a.trailId.localeCompare(b.trailId))
45
+ .map(claimLabel)
46
+ .join(
47
+ ', '
48
+ )}. Rename or remove one CLI alias so every accepted command path normalizes into exactly one trail contract.`,
49
+ rule: RULE_NAME,
50
+ severity: 'error',
51
+ });
52
+
53
+ const buildGraphTargetDiagnostic = (
54
+ entryId: string,
55
+ route: CliCommandRoute,
56
+ knownTrailIds: ReadonlySet<string>
57
+ ): WardenDiagnostic => ({
58
+ filePath: TOPO_FILE,
59
+ line: 1,
60
+ message: knownTrailIds.has(route.target)
61
+ ? `Serialized CLI command route "${renderPath(route.path)}" is stored under trail "${entryId}" but targets "${route.target}". Route facts must stay attached to their owning trail.`
62
+ : `Serialized CLI command route "${renderPath(route.path)}" targets unknown trail "${route.target}". Surface-owned aliases must target existing trail IDs.`,
63
+ rule: RULE_NAME,
64
+ severity: 'error',
65
+ });
66
+
67
+ const collectTopoClaims = (
68
+ topo: Topo
69
+ ): {
70
+ readonly claims: readonly RouteClaim[];
71
+ readonly diagnostics: readonly WardenDiagnostic[];
72
+ } => {
73
+ const claims: RouteClaim[] = [];
74
+ const diagnostics: WardenDiagnostic[] = [];
75
+
76
+ for (const trail of topo.list()) {
77
+ try {
78
+ const projection = deriveTrailCliCommandProjection(trail);
79
+ for (const route of projection.routes) {
80
+ claims.push({
81
+ kind: route.kind,
82
+ path: route.path,
83
+ source: route.source,
84
+ trailId: trail.id,
85
+ });
86
+ }
87
+ } catch (error: unknown) {
88
+ diagnostics.push(buildProjectionDiagnostic(trail.id, error));
89
+ }
90
+ }
91
+
92
+ return { claims, diagnostics };
93
+ };
94
+
95
+ const collectGraphClaims = (
96
+ graph: TopoGraph | undefined
97
+ ): readonly RouteClaim[] => {
98
+ if (graph === undefined) {
99
+ return [];
100
+ }
101
+
102
+ const claims: RouteClaim[] = [];
103
+ for (const entry of graph.entries) {
104
+ if (entry.kind !== 'trail') {
105
+ continue;
106
+ }
107
+ for (const route of entry.cli?.routes ?? []) {
108
+ claims.push({
109
+ kind: route.kind,
110
+ path: route.path,
111
+ source: route.source,
112
+ trailId: entry.id,
113
+ });
114
+ }
115
+ }
116
+ return claims;
117
+ };
118
+
119
+ const collectCollisionDiagnostics = (
120
+ claims: readonly RouteClaim[]
121
+ ): readonly WardenDiagnostic[] => {
122
+ const claimsByRoute = new Map<string, RouteClaim[]>();
123
+ for (const claim of claims) {
124
+ const key = routeKey(claim.path);
125
+ const current = claimsByRoute.get(key) ?? [];
126
+ current.push(claim);
127
+ claimsByRoute.set(key, current);
128
+ }
129
+
130
+ return [...claimsByRoute.values()]
131
+ .filter((grouped) => grouped.length > 1)
132
+ .map((grouped) =>
133
+ buildCollisionDiagnostic(grouped[0]?.path ?? [], grouped)
134
+ );
135
+ };
136
+
137
+ const collectGraphTargetDiagnostics = (
138
+ graph: TopoGraph | undefined,
139
+ knownTrailIds: ReadonlySet<string>
140
+ ): readonly WardenDiagnostic[] => {
141
+ if (graph === undefined) {
142
+ return [];
143
+ }
144
+
145
+ const diagnostics: WardenDiagnostic[] = [];
146
+ for (const entry of graph.entries) {
147
+ if (entry.kind !== 'trail') {
148
+ continue;
149
+ }
150
+ for (const route of entry.cli?.routes ?? []) {
151
+ if (route.target !== entry.id || !knownTrailIds.has(route.target)) {
152
+ diagnostics.push(
153
+ buildGraphTargetDiagnostic(entry.id, route, knownTrailIds)
154
+ );
155
+ }
156
+ }
157
+ }
158
+ return diagnostics;
159
+ };
160
+
161
+ export const cliCommandRouteCoherence: TopoAwareWardenRule = {
162
+ checkTopo(topo, context) {
163
+ const { claims, diagnostics } = collectTopoClaims(topo);
164
+ const knownTrailIds = new Set(topo.trails.keys());
165
+ const routeClaims =
166
+ context?.graph === undefined ? claims : collectGraphClaims(context.graph);
167
+ return [
168
+ ...diagnostics,
169
+ ...collectCollisionDiagnostics(routeClaims),
170
+ ...collectGraphTargetDiagnostics(context?.graph, knownTrailIds),
171
+ ];
172
+ },
173
+ description:
174
+ 'Ensure CLI command routes and aliases resolve to one coherent trail contract.',
175
+ name: RULE_NAME,
176
+ severity: 'error',
177
+ };