@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42

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 (279) hide show
  1. package/CHANGELOG.md +837 -13
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +27 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1570 -105
  7. package/src/command.ts +986 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +233 -23
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +78 -13
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +247 -15
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/resolve.ts +531 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/captured-kernel.ts +375 -0
  20. package/src/rules/circular-refs.ts +150 -0
  21. package/src/rules/cli-command-route-coherence.ts +177 -0
  22. package/src/rules/composes-declarations.ts +839 -0
  23. package/src/rules/context-no-surface-types.ts +79 -15
  24. package/src/rules/dead-internal-trail.ts +161 -0
  25. package/src/rules/dead-public-trail.ts +258 -0
  26. package/src/rules/draft-file-marking.ts +155 -0
  27. package/src/rules/draft-visible-debt.ts +82 -0
  28. package/src/rules/duplicate-exported-symbol.ts +211 -0
  29. package/src/rules/duplicate-public-contract.ts +137 -0
  30. package/src/rules/entity-exists.ts +254 -0
  31. package/src/rules/entity-ids.ts +15 -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 +740 -0
  35. package/src/rules/governed-symbol-residue.ts +438 -0
  36. package/src/rules/implementation-returns-result.ts +1409 -166
  37. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  38. package/src/rules/incomplete-crud.ts +583 -0
  39. package/src/rules/index.ts +277 -10
  40. package/src/rules/intent-propagation.ts +125 -0
  41. package/src/rules/layer-field-name-drift.ts +102 -0
  42. package/src/rules/library-projection-coherence.ts +100 -0
  43. package/src/rules/metadata.ts +871 -0
  44. package/src/rules/missing-reconcile.ts +97 -0
  45. package/src/rules/missing-visibility.ts +111 -0
  46. package/src/rules/no-destructured-compose.ts +196 -0
  47. package/src/rules/no-dev-permit-in-source.ts +99 -0
  48. package/src/rules/no-direct-implementation-call.ts +12 -7
  49. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  50. package/src/rules/no-legacy-layer-imports.ts +211 -0
  51. package/src/rules/no-native-error-result.ts +118 -0
  52. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  53. package/src/rules/no-retired-cross-vocabulary.ts +204 -0
  54. package/src/rules/no-sync-result-assumption.ts +1141 -98
  55. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  56. package/src/rules/no-throw-in-implementation.ts +15 -8
  57. package/src/rules/no-top-level-surface.ts +371 -0
  58. package/src/rules/on-references-exist.ts +194 -0
  59. package/src/rules/orphaned-signal.ts +149 -0
  60. package/src/rules/owner-projection-parity.ts +143 -0
  61. package/src/rules/permit-governance.ts +25 -0
  62. package/src/rules/public-export-example-coverage.ts +573 -0
  63. package/src/rules/public-internal-deep-imports.ts +456 -0
  64. package/src/rules/public-output-schema.ts +29 -0
  65. package/src/rules/public-union-output-discriminants.ts +150 -0
  66. package/src/rules/read-intent-fires.ts +188 -0
  67. package/src/rules/reference-exists.ts +97 -0
  68. package/src/rules/registry-names.ts +167 -0
  69. package/src/rules/resolved-import-boundary.ts +146 -0
  70. package/src/rules/resource-declarations.ts +697 -0
  71. package/src/rules/resource-exists.ts +181 -0
  72. package/src/rules/resource-id-grammar.ts +65 -0
  73. package/src/rules/resource-mock-coverage.ts +115 -0
  74. package/src/rules/retired-vocabulary.ts +633 -0
  75. package/src/rules/scan.ts +38 -25
  76. package/src/rules/scheduled-destroy-intent.ts +44 -0
  77. package/src/rules/signal-graph-coaching.ts +220 -0
  78. package/src/rules/source/composition.ts +165 -0
  79. package/src/rules/source/drafts.ts +164 -0
  80. package/src/rules/source/entities.ts +618 -0
  81. package/src/rules/source/pragmas.ts +45 -0
  82. package/src/rules/source/resources.ts +64 -0
  83. package/src/rules/source/signals.ts +397 -0
  84. package/src/rules/source/stores.ts +310 -0
  85. package/src/rules/specs.ts +9 -5
  86. package/src/rules/static-resource-accessor-preference.ts +653 -0
  87. package/src/rules/surface-overlay-coherence.ts +262 -0
  88. package/src/rules/surface-trailhead-coherence.ts +366 -0
  89. package/src/rules/trail-fork-coaching.ts +625 -0
  90. package/src/rules/trail-versioning-source.ts +1076 -0
  91. package/src/rules/trail-versioning-topo.ts +172 -0
  92. package/src/rules/trailhead-override-divergence.ts +356 -0
  93. package/src/rules/types.ts +354 -8
  94. package/src/rules/unmaterialized-activation-source.ts +85 -0
  95. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  96. package/src/rules/valid-describe-refs.ts +162 -32
  97. package/src/rules/valid-detour-contract.ts +78 -0
  98. package/src/rules/warden-export-symmetry.ts +540 -0
  99. package/src/rules/warden-rules-use-ast.ts +1109 -0
  100. package/src/rules/webhook-route-collision.ts +306 -0
  101. package/src/trails/activation-orphan.trail.ts +84 -0
  102. package/src/trails/captured-kernel.trail.ts +108 -0
  103. package/src/trails/circular-refs.trail.ts +29 -0
  104. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  105. package/src/trails/composes-declarations.trail.ts +22 -0
  106. package/src/trails/context-no-surface-types.trail.ts +21 -0
  107. package/src/trails/dead-internal-trail.trail.ts +26 -0
  108. package/src/trails/dead-public-trail.trail.ts +31 -0
  109. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  110. package/src/trails/draft-file-marking.trail.ts +16 -0
  111. package/src/trails/draft-visible-debt.trail.ts +16 -0
  112. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  113. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  114. package/src/trails/entity-exists.trail.ts +21 -0
  115. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  116. package/src/trails/example-valid.trail.ts +25 -0
  117. package/src/trails/fires-declarations.trail.ts +23 -0
  118. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  119. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  120. package/src/trails/implementation-returns-result.trail.ts +20 -0
  121. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  122. package/src/trails/incomplete-crud.trail.ts +39 -0
  123. package/src/trails/index.ts +89 -0
  124. package/src/trails/intent-propagation.trail.ts +30 -0
  125. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  126. package/src/trails/library-projection-coherence.trail.ts +43 -0
  127. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  128. package/src/trails/missing-reconcile.trail.ts +33 -0
  129. package/src/trails/missing-visibility.trail.ts +22 -0
  130. package/src/trails/no-destructured-compose.trail.ts +44 -0
  131. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  132. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  133. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  134. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  135. package/src/trails/no-native-error-result.trail.ts +18 -0
  136. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  137. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  138. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  139. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  140. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  141. package/src/trails/no-top-level-surface.trail.ts +43 -0
  142. package/src/trails/on-references-exist.trail.ts +21 -0
  143. package/src/trails/orphaned-signal.trail.ts +36 -0
  144. package/src/trails/owner-projection-parity.trail.ts +26 -0
  145. package/src/trails/pending-force.trail.ts +21 -0
  146. package/src/trails/permit-governance.trail.ts +51 -0
  147. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  148. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  149. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  150. package/src/trails/public-output-schema.trail.ts +55 -0
  151. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  152. package/src/trails/read-intent-fires.trail.ts +20 -0
  153. package/src/trails/reference-exists.trail.ts +25 -0
  154. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  155. package/src/trails/resource-declarations.trail.ts +25 -0
  156. package/src/trails/resource-exists.trail.ts +27 -0
  157. package/src/trails/resource-id-grammar.trail.ts +39 -0
  158. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  159. package/src/trails/run.ts +160 -0
  160. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  161. package/src/trails/schema.ts +237 -0
  162. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  163. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  164. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  165. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  166. package/src/trails/topo.ts +6 -0
  167. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  168. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  169. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  170. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  171. package/src/trails/valid-describe-refs.trail.ts +18 -0
  172. package/src/trails/valid-detour-contract.trail.ts +71 -0
  173. package/src/trails/version-gap.trail.ts +35 -0
  174. package/src/trails/version-pinned-compose.trail.ts +23 -0
  175. package/src/trails/version-without-examples.trail.ts +38 -0
  176. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  177. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  178. package/src/trails/webhook-route-collision.trail.ts +50 -0
  179. package/src/trails/wrap-rule.ts +224 -0
  180. package/src/workspaces.ts +199 -0
  181. package/.turbo/turbo-build.log +0 -1
  182. package/.turbo/turbo-lint.log +0 -3
  183. package/.turbo/turbo-typecheck.log +0 -1
  184. package/dist/cli.d.ts +0 -46
  185. package/dist/cli.d.ts.map +0 -1
  186. package/dist/cli.js +0 -218
  187. package/dist/cli.js.map +0 -1
  188. package/dist/drift.d.ts +0 -26
  189. package/dist/drift.d.ts.map +0 -1
  190. package/dist/drift.js +0 -27
  191. package/dist/drift.js.map +0 -1
  192. package/dist/formatters.d.ts +0 -29
  193. package/dist/formatters.d.ts.map +0 -1
  194. package/dist/formatters.js +0 -87
  195. package/dist/formatters.js.map +0 -1
  196. package/dist/index.d.ts +0 -26
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -26
  199. package/dist/index.js.map +0 -1
  200. package/dist/rules/ast.d.ts +0 -41
  201. package/dist/rules/ast.d.ts.map +0 -1
  202. package/dist/rules/ast.js +0 -161
  203. package/dist/rules/ast.js.map +0 -1
  204. package/dist/rules/context-no-surface-types.d.ts +0 -12
  205. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  206. package/dist/rules/context-no-surface-types.js +0 -96
  207. package/dist/rules/context-no-surface-types.js.map +0 -1
  208. package/dist/rules/implementation-returns-result.d.ts +0 -13
  209. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  210. package/dist/rules/implementation-returns-result.js +0 -277
  211. package/dist/rules/implementation-returns-result.js.map +0 -1
  212. package/dist/rules/index.d.ts +0 -15
  213. package/dist/rules/index.d.ts.map +0 -1
  214. package/dist/rules/index.js +0 -34
  215. package/dist/rules/index.js.map +0 -1
  216. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  217. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  218. package/dist/rules/no-direct-impl-in-route.js +0 -47
  219. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  220. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  221. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  222. package/dist/rules/no-direct-implementation-call.js +0 -39
  223. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  224. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  225. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  226. package/dist/rules/no-sync-result-assumption.js +0 -98
  227. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  228. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  229. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  230. package/dist/rules/no-throw-in-detour-target.js +0 -87
  231. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  232. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  233. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  234. package/dist/rules/no-throw-in-implementation.js +0 -34
  235. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  236. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  237. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  238. package/dist/rules/prefer-schema-inference.js +0 -86
  239. package/dist/rules/prefer-schema-inference.js.map +0 -1
  240. package/dist/rules/scan.d.ts +0 -8
  241. package/dist/rules/scan.d.ts.map +0 -1
  242. package/dist/rules/scan.js +0 -32
  243. package/dist/rules/scan.js.map +0 -1
  244. package/dist/rules/specs.d.ts +0 -29
  245. package/dist/rules/specs.d.ts.map +0 -1
  246. package/dist/rules/specs.js +0 -192
  247. package/dist/rules/specs.js.map +0 -1
  248. package/dist/rules/structure.d.ts +0 -13
  249. package/dist/rules/structure.d.ts.map +0 -1
  250. package/dist/rules/structure.js +0 -142
  251. package/dist/rules/structure.js.map +0 -1
  252. package/dist/rules/types.d.ts +0 -52
  253. package/dist/rules/types.d.ts.map +0 -1
  254. package/dist/rules/types.js +0 -2
  255. package/dist/rules/types.js.map +0 -1
  256. package/dist/rules/valid-describe-refs.d.ts +0 -7
  257. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  258. package/dist/rules/valid-describe-refs.js +0 -51
  259. package/dist/rules/valid-describe-refs.js.map +0 -1
  260. package/dist/rules/valid-detour-refs.d.ts +0 -6
  261. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  262. package/dist/rules/valid-detour-refs.js +0 -116
  263. package/dist/rules/valid-detour-refs.js.map +0 -1
  264. package/src/__tests__/cli.test.ts +0 -198
  265. package/src/__tests__/drift.test.ts +0 -74
  266. package/src/__tests__/formatters.test.ts +0 -157
  267. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  268. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  269. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  270. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  271. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  272. package/src/__tests__/rules.test.ts +0 -227
  273. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  274. package/src/rules/ast.ts +0 -213
  275. package/src/rules/no-direct-impl-in-route.ts +0 -81
  276. package/src/rules/no-throw-in-detour-target.ts +0 -150
  277. package/src/rules/valid-detour-refs.ts +0 -187
  278. package/tsconfig.json +0 -9
  279. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Flags leftover legacy app-module CLI alias exports removed in TRL-1207.
3
+ *
4
+ * Before the surfaces-overlay cutover, apps published CLI alias maps through
5
+ * a module-level export named `cliAliases` or `trailsCliAliases`. TRL-1207 is
6
+ * a hard cutover: app-owned CLI bindings now live in a
7
+ * `surfaceOverlay({ cli: { ... } })` envelope inside the app module's
8
+ * `trailsOverlays` array export, and the legacy export convention was
9
+ * deleted. This rule makes a leftover legacy export an error so stale app
10
+ * modules get a fix-forward diagnostic instead of silently losing their
11
+ * bindings.
12
+ *
13
+ * Detection is AST-scoped and export-shaped:
14
+ *
15
+ * - `export const cliAliases = { ... }` (also `let`/`var`, destructured
16
+ * bindings, and function/class declarations) is flagged.
17
+ * - `export { cliAliases }` and `export { whatever as trailsCliAliases }`
18
+ * specifiers are flagged, including re-export statements such as
19
+ * `export { trailsCliAliases } from './app.js'`.
20
+ * - A non-exported local `const cliAliases = ...` is legal and never
21
+ * flagged, and the identifiers appearing only in strings or comments do
22
+ * not match because the rule inspects export bindings, not raw text.
23
+ */
24
+
25
+ import {
26
+ getNodeArgument,
27
+ getNodeBodyStatements,
28
+ getNodeDeclaration,
29
+ getNodeDeclarations,
30
+ getNodeElements,
31
+ getNodeExported,
32
+ getNodeExportKind,
33
+ getNodeId,
34
+ getNodeLeft,
35
+ getNodeProperties,
36
+ getNodeSpecifiers,
37
+ getNodeValueNode,
38
+ getStringValue,
39
+ identifierName,
40
+ isStringLiteral,
41
+ offsetToLine,
42
+ parse,
43
+ } from '@ontrails/source';
44
+ import type { AstNode } from '@ontrails/source';
45
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
46
+
47
+ const RULE_NAME = 'no-legacy-cli-alias-export';
48
+
49
+ /** Legacy app-module CLI alias export names removed in TRL-1207. */
50
+ const LEGACY_CLI_ALIAS_EXPORT_NAMES: ReadonlySet<string> = new Set([
51
+ 'cliAliases',
52
+ 'trailsCliAliases',
53
+ ]);
54
+
55
+ /** Declaration node types that only propagate types, never value bindings. */
56
+ const TYPE_ONLY_DECLARATION_TYPES: ReadonlySet<string> = new Set([
57
+ 'TSInterfaceDeclaration',
58
+ 'TSTypeAliasDeclaration',
59
+ ]);
60
+
61
+ interface ExportedBinding {
62
+ readonly name: string;
63
+ readonly start: number;
64
+ }
65
+
66
+ /** Exported name of a specifier: identifier or string-literal alias. */
67
+ const exportedSpecifierName = (node: AstNode | undefined): string | null =>
68
+ identifierName(node) ??
69
+ (node && isStringLiteral(node) ? getStringValue(node) : null);
70
+
71
+ /**
72
+ * Expand one binding-pattern node into legacy-named bindings, pushing child
73
+ * patterns onto the worklist. Destructured exports such as
74
+ * `export const { cliAliases } = mod` still introduce a module-level exported
75
+ * value binding, so patterns cannot be skipped.
76
+ */
77
+ const visitBindingPatternNode = (
78
+ node: AstNode,
79
+ into: ExportedBinding[],
80
+ worklist: AstNode[]
81
+ ): void => {
82
+ const name = identifierName(node);
83
+ if (name !== null) {
84
+ if (LEGACY_CLI_ALIAS_EXPORT_NAMES.has(name)) {
85
+ into.push({ name, start: node.start });
86
+ }
87
+ return;
88
+ }
89
+ if (node.type === 'AssignmentPattern') {
90
+ const left = getNodeLeft(node);
91
+ if (left) {
92
+ worklist.push(left);
93
+ }
94
+ return;
95
+ }
96
+ if (node.type === 'RestElement') {
97
+ const argument = getNodeArgument(node);
98
+ if (argument) {
99
+ worklist.push(argument);
100
+ }
101
+ return;
102
+ }
103
+ if (node.type === 'ObjectPattern') {
104
+ for (const property of getNodeProperties(node)) {
105
+ const value = getNodeValueNode(property) ?? getNodeArgument(property);
106
+ if (value) {
107
+ worklist.push(value);
108
+ }
109
+ }
110
+ return;
111
+ }
112
+ if (node.type === 'ArrayPattern') {
113
+ for (const element of getNodeElements(node)) {
114
+ if (element) {
115
+ worklist.push(element);
116
+ }
117
+ }
118
+ }
119
+ };
120
+
121
+ const collectLegacyPatternBindings = (
122
+ pattern: AstNode | undefined,
123
+ into: ExportedBinding[]
124
+ ): void => {
125
+ if (!pattern) {
126
+ return;
127
+ }
128
+ const worklist: AstNode[] = [pattern];
129
+ while (worklist.length > 0) {
130
+ const node = worklist.pop();
131
+ if (node) {
132
+ visitBindingPatternNode(node, into, worklist);
133
+ }
134
+ }
135
+ };
136
+
137
+ const collectLegacyDeclarationBindings = (
138
+ declaration: AstNode
139
+ ): readonly ExportedBinding[] => {
140
+ if (TYPE_ONLY_DECLARATION_TYPES.has(declaration.type)) {
141
+ return [];
142
+ }
143
+ const bindings: ExportedBinding[] = [];
144
+ if (declaration.type === 'VariableDeclaration') {
145
+ for (const declarator of getNodeDeclarations(declaration)) {
146
+ collectLegacyPatternBindings(getNodeId(declarator), bindings);
147
+ }
148
+ return bindings;
149
+ }
150
+ const name = identifierName(getNodeId(declaration));
151
+ if (name !== null && LEGACY_CLI_ALIAS_EXPORT_NAMES.has(name)) {
152
+ bindings.push({ name, start: declaration.start });
153
+ }
154
+ return bindings;
155
+ };
156
+
157
+ const collectLegacySpecifierBindings = (
158
+ statement: AstNode
159
+ ): readonly ExportedBinding[] => {
160
+ const bindings: ExportedBinding[] = [];
161
+ for (const specifier of getNodeSpecifiers(statement)) {
162
+ if (
163
+ specifier.type !== 'ExportSpecifier' ||
164
+ getNodeExportKind(specifier) === 'type'
165
+ ) {
166
+ continue;
167
+ }
168
+ const name = exportedSpecifierName(getNodeExported(specifier));
169
+ if (name !== null && LEGACY_CLI_ALIAS_EXPORT_NAMES.has(name)) {
170
+ bindings.push({ name, start: specifier.start });
171
+ }
172
+ }
173
+ return bindings;
174
+ };
175
+
176
+ const collectLegacyExportBindings = (
177
+ ast: AstNode
178
+ ): readonly ExportedBinding[] => {
179
+ const bindings: ExportedBinding[] = [];
180
+ for (const statement of getNodeBodyStatements(ast)) {
181
+ if (
182
+ statement.type !== 'ExportNamedDeclaration' ||
183
+ getNodeExportKind(statement) === 'type'
184
+ ) {
185
+ continue;
186
+ }
187
+ const declaration = getNodeDeclaration(statement);
188
+ if (declaration) {
189
+ bindings.push(...collectLegacyDeclarationBindings(declaration));
190
+ continue;
191
+ }
192
+ bindings.push(...collectLegacySpecifierBindings(statement));
193
+ }
194
+ return bindings;
195
+ };
196
+
197
+ const buildMessage = (name: string): string =>
198
+ `Legacy CLI alias export '${name}' was removed in the TRL-1207 surfaces-overlay cutover. Wrap the alias map into surfaceOverlay({ cli: { ... } }) from @ontrails/core inside the module's trailsOverlays array export.`;
199
+
200
+ /**
201
+ * Build the fix metadata for a legacy CLI alias export finding.
202
+ *
203
+ * The rewrite is an export restructure, not a rename, so there is no
204
+ * mechanical single-span replacement: the fix carries no edits and Warden
205
+ * never applies it. Downstream, the `export-restructure` fix class routes the
206
+ * finding to the Regrade `export-restructure:cli-aliases` class (TRL-1210),
207
+ * which inverts the alias map into `surfaceOverlay({ cli: { ... } })`
208
+ * bindings inside the module's `trailsOverlays` array export.
209
+ */
210
+ const buildFix = (name: string): WardenFix => ({
211
+ class: 'export-restructure',
212
+ reason: `Legacy CLI alias export '${name}' must be rewritten into a surfaceOverlay({ cli: { ... } }) entry inside the module's trailsOverlays array export; run the Regrade class export-restructure:cli-aliases (TRL-1210) to automate this restructure.`,
213
+ safety: 'review',
214
+ });
215
+
216
+ /**
217
+ * Flags exported bindings named `cliAliases` or `trailsCliAliases`, the
218
+ * legacy app-module CLI alias export convention deleted in TRL-1207.
219
+ */
220
+ export const noLegacyCliAliasExport: WardenRule = {
221
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
222
+ if (
223
+ !(
224
+ sourceCode.includes('cliAliases') ||
225
+ sourceCode.includes('trailsCliAliases')
226
+ )
227
+ ) {
228
+ return [];
229
+ }
230
+ const ast = parse(filePath, sourceCode);
231
+ if (!ast) {
232
+ return [];
233
+ }
234
+ return collectLegacyExportBindings(ast).map((binding) => ({
235
+ filePath,
236
+ fix: buildFix(binding.name),
237
+ line: offsetToLine(sourceCode, binding.start),
238
+ message: buildMessage(binding.name),
239
+ rule: RULE_NAME,
240
+ severity: 'error',
241
+ }));
242
+ },
243
+ description:
244
+ 'Disallow the legacy app-module CLI alias exports (cliAliases, trailsCliAliases) removed in the TRL-1207 surfaces-overlay cutover.',
245
+ name: RULE_NAME,
246
+ severity: 'error',
247
+ };
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Flags references to legacy layer symbols removed during Layer Evolution.
3
+ *
4
+ * The pre-TRL-469 era of Trails shipped three first-party layers as ergonomics
5
+ * over `executeTrail`:
6
+ *
7
+ * - `authLayer` — re-exposed permit enforcement, even though permits were
8
+ * intrinsic to `executeTrail`.
9
+ * - `autoIterateLayer` — wrapped the CLI iteration ergonomics that the
10
+ * surface now derives directly via `--all` (TRL-469).
11
+ * - `dateShortcutsLayer` — wrapped the CLI date-expansion helpers that the
12
+ * surface now derives directly (TRL-470).
13
+ *
14
+ * TRL-475 removed `authLayer`; TRL-476 removed the CLI layer exports
15
+ * `autoIterateLayer` and `dateShortcutsLayer`. Importing them from
16
+ * `@ontrails/permits` or `@ontrails/cli` produces a TypeScript error today;
17
+ * this rule layers a coaching diagnostic on top so authors that still type
18
+ * the symbol — in a comment, a string, or a stale import — get a redirect to
19
+ * the migration ADR during the deprecation window.
20
+ *
21
+ * The rule is intentionally text-based: legacy names appear in a mix of
22
+ * imports, JSDoc, error messages, and migration notes, all of which should
23
+ * surface a coaching message.
24
+ *
25
+ * Allow-list: a small set of files documenting the removal itself reference
26
+ * the literal symbol names. The rule's own implementation file references
27
+ * each name and is also exempt.
28
+ *
29
+ * Test files (`__tests__/`, `*.test.ts`) are filtered before this rule is
30
+ * invoked by the warden runner, so the regression test referencing
31
+ * `authLayer` in its name (`packages/core/src/__tests__/execute-permit.test.ts`)
32
+ * does not need a separate exemption.
33
+ */
34
+ import { resolve, sep } from 'node:path';
35
+
36
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
37
+
38
+ const RULE_NAME = 'no-legacy-layer-imports';
39
+
40
+ /**
41
+ * Legacy layer symbol names that were removed during Layer Evolution.
42
+ *
43
+ * Listed longest-first is not required for correctness (none of these names
44
+ * are substrings of one another), but kept alphabetized for stable output
45
+ * across rule runs.
46
+ */
47
+ const LEGACY_LAYER_NAMES = [
48
+ 'authLayer',
49
+ 'autoIterateLayer',
50
+ 'dateShortcutsLayer',
51
+ ] as const;
52
+
53
+ type LegacyLayerName = (typeof LEGACY_LAYER_NAMES)[number];
54
+
55
+ interface LegacyLayerMigration {
56
+ readonly guidance: string;
57
+ readonly removedIn: 'TRL-475' | 'TRL-476';
58
+ }
59
+
60
+ const LEGACY_LAYER_MIGRATIONS: Record<LegacyLayerName, LegacyLayerMigration> = {
61
+ authLayer: {
62
+ guidance: 'Permit enforcement is intrinsic to executeTrail',
63
+ removedIn: 'TRL-475',
64
+ },
65
+ autoIterateLayer: {
66
+ guidance: 'Pagination uses CLI surface derivation (--all) per TRL-469',
67
+ removedIn: 'TRL-476',
68
+ },
69
+ dateShortcutsLayer: {
70
+ guidance: 'Date shortcuts use CLI surface derivation per TRL-470',
71
+ removedIn: 'TRL-476',
72
+ },
73
+ };
74
+
75
+ /**
76
+ * Path suffixes (in POSIX form) for source files that legitimately reference
77
+ * one or more removed legacy layer names. Other files are flagged.
78
+ *
79
+ * Each entry is matched against the normalized (forward-slash) absolute path
80
+ * with a trailing-segment match, so the rule stays correct regardless of the
81
+ * consumer's repository root.
82
+ */
83
+ const ALLOWED_PATH_SUFFIXES: readonly string[] = [
84
+ // The CLI pagination module documents the removed `autoIterateLayer` in a
85
+ // migration note for apps moving onto the derived `--all` ergonomics.
86
+ '/packages/cli/src/pagination.ts',
87
+ // The CLI date-shortcuts module documents the removed `dateShortcutsLayer`
88
+ // in a migration note for apps moving onto the derived expansion helpers.
89
+ '/packages/cli/src/date-shortcuts.ts',
90
+ // The rule's own implementation file references the literals it searches for.
91
+ '/packages/warden/src/rules/no-legacy-layer-imports.ts',
92
+ // Warden rule metadata names the legacy symbols in the rule's invariant
93
+ // string for traceability between the rule and the removed exports.
94
+ '/packages/warden/src/rules/metadata.ts',
95
+ // The rule trail includes an executable example that demonstrates the
96
+ // diagnostic emitted for legacy layer imports.
97
+ '/packages/warden/src/trails/no-legacy-layer-imports.trail.ts',
98
+ ];
99
+
100
+ const normalizePath = (filePath: string): string =>
101
+ resolve(filePath).split(sep).join('/');
102
+
103
+ const isAllowedFile = (filePath: string): boolean => {
104
+ const normalized = normalizePath(filePath);
105
+ return ALLOWED_PATH_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
106
+ };
107
+
108
+ interface Match {
109
+ readonly name: LegacyLayerName;
110
+ readonly index: number;
111
+ }
112
+
113
+ const IDENTIFIER_CHAR = /[$0-9A-Z_a-z]/u;
114
+
115
+ const isIdentifierChar = (value: string): boolean =>
116
+ value !== '' && IDENTIFIER_CHAR.test(value);
117
+
118
+ const indexOfStandaloneName = (
119
+ sourceCode: string,
120
+ name: LegacyLayerName
121
+ ): number => {
122
+ let fromIndex = 0;
123
+ while (fromIndex < sourceCode.length) {
124
+ const index = sourceCode.indexOf(name, fromIndex);
125
+ if (index === -1) {
126
+ return -1;
127
+ }
128
+ const before = index === 0 ? '' : (sourceCode[index - 1] ?? '');
129
+ const after = sourceCode[index + name.length] ?? '';
130
+ if (!(isIdentifierChar(before) || isIdentifierChar(after))) {
131
+ return index;
132
+ }
133
+ fromIndex = index + name.length;
134
+ }
135
+ return -1;
136
+ };
137
+
138
+ const findFirstMatch = (sourceCode: string): Match | null => {
139
+ let earliest: Match | null = null;
140
+ for (const name of LEGACY_LAYER_NAMES) {
141
+ const index = indexOfStandaloneName(sourceCode, name);
142
+ if (index === -1) {
143
+ continue;
144
+ }
145
+ if (earliest === null || index < earliest.index) {
146
+ earliest = { index, name };
147
+ }
148
+ }
149
+ return earliest;
150
+ };
151
+
152
+ const lineForOffset = (sourceCode: string, offset: number): number => {
153
+ let line = 1;
154
+ for (let i = 0; i < offset; i += 1) {
155
+ if (sourceCode.codePointAt(i) === 10) {
156
+ line += 1;
157
+ }
158
+ }
159
+ return line;
160
+ };
161
+
162
+ const buildMessage = (name: LegacyLayerName): string => {
163
+ const migration = LEGACY_LAYER_MIGRATIONS[name];
164
+ return `Legacy layer '${name}' was removed in ${migration.removedIn}. ${migration.guidance}. See docs/adr/0043-layer-evolution.md.`;
165
+ };
166
+
167
+ /**
168
+ * Build the term-rewrite fix metadata for a legacy layer finding.
169
+ *
170
+ * These layers were removed, not renamed, so there is no mechanical
171
+ * replacement: the fix is review-required and carries no edits. It still
172
+ * advertises the transform class and migration reason so `warden --fix`
173
+ * reports (but never auto-applies) it and downstream regrades can route it.
174
+ */
175
+ const buildFix = (name: LegacyLayerName): WardenFix => {
176
+ const migration = LEGACY_LAYER_MIGRATIONS[name];
177
+ return {
178
+ class: 'term-rewrite',
179
+ reason: `Legacy layer '${name}' was removed in ${migration.removedIn}; ${migration.guidance}. Removal has no mechanical replacement, so it needs human migration.`,
180
+ safety: 'review',
181
+ };
182
+ };
183
+
184
+ /**
185
+ * Flags references to the removed legacy layer symbols in committed source.
186
+ */
187
+ export const noLegacyLayerImports: WardenRule = {
188
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
189
+ if (isAllowedFile(filePath)) {
190
+ return [];
191
+ }
192
+ const match = findFirstMatch(sourceCode);
193
+ if (!match) {
194
+ return [];
195
+ }
196
+ return [
197
+ {
198
+ filePath,
199
+ fix: buildFix(match.name),
200
+ line: lineForOffset(sourceCode, match.index),
201
+ message: buildMessage(match.name),
202
+ rule: RULE_NAME,
203
+ severity: 'error',
204
+ },
205
+ ];
206
+ },
207
+ description:
208
+ 'Disallow references to legacy layer exports (authLayer, autoIterateLayer, dateShortcutsLayer) removed across TRL-475/TRL-476.',
209
+ name: RULE_NAME,
210
+ severity: 'error',
211
+ };
@@ -0,0 +1,118 @@
1
+ import {
2
+ getNodeArguments,
3
+ getNodeCallee,
4
+ getNodeObject,
5
+ getNodeProperty,
6
+ identifierName,
7
+ offsetToLine,
8
+ parse,
9
+ walk,
10
+ } from '@ontrails/source';
11
+ import type { AstNode } from '@ontrails/source';
12
+ import { isFrameworkInternalFile } from './scan.js';
13
+ import type { WardenDiagnostic, WardenRule } from './types.js';
14
+
15
+ const RULE_NAME = 'no-native-error-result';
16
+
17
+ const NATIVE_ERROR_CONSTRUCTORS = new Set([
18
+ 'AggregateError',
19
+ 'Error',
20
+ 'EvalError',
21
+ 'RangeError',
22
+ 'ReferenceError',
23
+ 'SyntaxError',
24
+ 'TypeError',
25
+ 'URIError',
26
+ ]);
27
+
28
+ const getMemberPropertyName = (node: AstNode): string | null => {
29
+ if (
30
+ node.type !== 'MemberExpression' &&
31
+ node.type !== 'StaticMemberExpression'
32
+ ) {
33
+ return null;
34
+ }
35
+
36
+ return identifierName(getNodeProperty(node));
37
+ };
38
+
39
+ const isResultObject = (node: AstNode | undefined): boolean => {
40
+ if (!node) {
41
+ return false;
42
+ }
43
+
44
+ if (identifierName(node) === 'Result') {
45
+ return true;
46
+ }
47
+
48
+ return getMemberPropertyName(node) === 'Result';
49
+ };
50
+
51
+ const isResultErrCall = (node: AstNode): boolean => {
52
+ if (node.type !== 'CallExpression') {
53
+ return false;
54
+ }
55
+
56
+ const callee = getNodeCallee(node);
57
+ if (!callee || getMemberPropertyName(callee) !== 'err') {
58
+ return false;
59
+ }
60
+
61
+ return isResultObject(getNodeObject(callee));
62
+ };
63
+
64
+ const isNativeErrorConstruction = (node: AstNode | undefined): boolean => {
65
+ if (!node || node.type !== 'NewExpression') {
66
+ return false;
67
+ }
68
+
69
+ const constructorName = identifierName(getNodeCallee(node));
70
+ return constructorName
71
+ ? NATIVE_ERROR_CONSTRUCTORS.has(constructorName)
72
+ : false;
73
+ };
74
+
75
+ const getFirstArgument = (node: AstNode): AstNode | undefined =>
76
+ getNodeArguments(node)?.[0];
77
+
78
+ const createDiagnostic = (
79
+ filePath: string,
80
+ sourceCode: string,
81
+ node: AstNode
82
+ ): WardenDiagnostic => ({
83
+ filePath,
84
+ line: offsetToLine(sourceCode, node.start),
85
+ message:
86
+ 'Use a specific TrailsError subclass with Result.err(...) instead of native Error.',
87
+ rule: RULE_NAME,
88
+ severity: 'error',
89
+ });
90
+
91
+ export const noNativeErrorResult: WardenRule = {
92
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
93
+ if (isFrameworkInternalFile(filePath)) {
94
+ return [];
95
+ }
96
+
97
+ const ast = parse(filePath, sourceCode);
98
+ if (!ast) {
99
+ return [];
100
+ }
101
+
102
+ const diagnostics: WardenDiagnostic[] = [];
103
+ walk(ast, (node) => {
104
+ if (
105
+ isResultErrCall(node) &&
106
+ isNativeErrorConstruction(getFirstArgument(node))
107
+ ) {
108
+ diagnostics.push(createDiagnostic(filePath, sourceCode, node));
109
+ }
110
+ });
111
+
112
+ return diagnostics;
113
+ },
114
+ description:
115
+ 'Require Result.err(...) calls to carry specific TrailsError subclasses instead of native Error.',
116
+ name: RULE_NAME,
117
+ severity: 'error',
118
+ };