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

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 +835 -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,384 @@
1
+ import { getMemberExpression } from './source/stores.js';
2
+ import {
3
+ collectScopeFrameBindings,
4
+ findImplementationBodies,
5
+ findTrailDefinitions,
6
+ getNodeArgument,
7
+ getNodeArguments,
8
+ getNodeBodyNode,
9
+ getNodeCallee,
10
+ getNodeId,
11
+ getNodeInit,
12
+ getNodeLeft,
13
+ getNodeOperator,
14
+ getNodeProperty,
15
+ getNodeRight,
16
+ identifierName,
17
+ isMemberAccessNonComputed,
18
+ offsetToLine,
19
+ parse,
20
+ walkWithParents,
21
+ walkWithScopes,
22
+ } from '@ontrails/source';
23
+ import type { AstNode, AstParentContext } from '@ontrails/source';
24
+ import {
25
+ collectAllResultHelperNames,
26
+ collectNamespaceHelperImports,
27
+ collectResultTypeNames,
28
+ findNearestBindingScope,
29
+ isHelperCall,
30
+ isResultExpression,
31
+ trackScopedResultHelperDeclaration,
32
+ } from './implementation-returns-result.js';
33
+ import { isTestFile } from './scan.js';
34
+ import type {
35
+ MutableScopedHelperMap,
36
+ NamespaceHelperMap,
37
+ ScopedHelperMap,
38
+ } from './implementation-returns-result.js';
39
+ import type { WardenDiagnostic, WardenRule } from './types.js';
40
+
41
+ const RULE_NAME = 'no-redundant-result-error-wrap';
42
+
43
+ const getStaticMemberName = (node: AstNode | undefined): string | null => {
44
+ if (!node || !isMemberAccessNonComputed(node)) {
45
+ return null;
46
+ }
47
+ return identifierName(getNodeProperty(node));
48
+ };
49
+
50
+ const isResultObject = (node: AstNode | undefined): boolean => {
51
+ if (!node) {
52
+ return false;
53
+ }
54
+ if (identifierName(node) === 'Result') {
55
+ return true;
56
+ }
57
+ return getStaticMemberName(node) === 'Result';
58
+ };
59
+
60
+ const isResultErrCall = (node: AstNode): boolean => {
61
+ if (node.type !== 'CallExpression') {
62
+ return false;
63
+ }
64
+ const callee = getNodeCallee(node);
65
+ const member = getMemberExpression(callee);
66
+ if (!member || getStaticMemberName(callee) !== 'err') {
67
+ return false;
68
+ }
69
+ return isResultObject(member.object);
70
+ };
71
+
72
+ const getSingleArgument = (node: AstNode): AstNode | null => {
73
+ const args = getNodeArguments(node);
74
+ return args?.length === 1 ? (args[0] ?? null) : null;
75
+ };
76
+
77
+ const getErrorSourceVariable = (node: AstNode | null): string | null => {
78
+ if (!node || getStaticMemberName(node) !== 'error') {
79
+ return null;
80
+ }
81
+ const member = getMemberExpression(node);
82
+ return identifierName(member?.object);
83
+ };
84
+
85
+ const isResultProducingExpression = (
86
+ node: AstNode,
87
+ helperNames: ReadonlySet<string>,
88
+ namespaceHelpers: NamespaceHelperMap,
89
+ scopes: readonly ReadonlySet<string>[],
90
+ scopedHelpers: ScopedHelperMap
91
+ ): boolean =>
92
+ isResultExpression(node) ||
93
+ isHelperCall(node, helperNames, namespaceHelpers, scopes, scopedHelpers);
94
+
95
+ type ResultProvenance = Map<ReadonlySet<string>, Set<string>>;
96
+
97
+ const markResultVariable = (
98
+ provenance: ResultProvenance,
99
+ scope: ReadonlySet<string>,
100
+ name: string
101
+ ): void => {
102
+ const names = provenance.get(scope);
103
+ if (names) {
104
+ names.add(name);
105
+ return;
106
+ }
107
+ provenance.set(scope, new Set([name]));
108
+ };
109
+
110
+ const clearResultVariable = (
111
+ provenance: ResultProvenance,
112
+ scope: ReadonlySet<string>,
113
+ name: string
114
+ ): void => {
115
+ provenance.get(scope)?.delete(name);
116
+ };
117
+
118
+ const hasResultProvenance = (
119
+ provenance: ResultProvenance,
120
+ scope: ReadonlySet<string>,
121
+ name: string
122
+ ): boolean => provenance.get(scope)?.has(name) ?? false;
123
+
124
+ const createDiagnostic = (
125
+ filePath: string,
126
+ sourceCode: string,
127
+ node: AstNode,
128
+ ownerLabel: string,
129
+ variableName: string
130
+ ): WardenDiagnostic => ({
131
+ filePath,
132
+ line: offsetToLine(sourceCode, node.start),
133
+ message: `${ownerLabel}: Result.err(${variableName}.error) re-wraps a Result that already carries that error. Return ${variableName} directly to preserve the original Result boundary.`,
134
+ rule: RULE_NAME,
135
+ severity: 'warn',
136
+ });
137
+
138
+ const trackVariableDeclarator = (
139
+ node: AstNode,
140
+ provenance: ResultProvenance,
141
+ helperNames: ReadonlySet<string>,
142
+ namespaceHelpers: NamespaceHelperMap,
143
+ scopedHelpers: ScopedHelperMap,
144
+ scopes: readonly ReadonlySet<string>[]
145
+ ): void => {
146
+ const id = getNodeId(node);
147
+ const init = getNodeInit(node);
148
+ const name = identifierName(id);
149
+ if (!name) {
150
+ return;
151
+ }
152
+ const scope = findNearestBindingScope(name, scopes);
153
+ if (!scope) {
154
+ return;
155
+ }
156
+ if (
157
+ init &&
158
+ isResultProducingExpression(
159
+ init,
160
+ helperNames,
161
+ namespaceHelpers,
162
+ scopes,
163
+ scopedHelpers
164
+ )
165
+ ) {
166
+ markResultVariable(provenance, scope, name);
167
+ return;
168
+ }
169
+ clearResultVariable(provenance, scope, name);
170
+ };
171
+
172
+ const trackAssignmentExpression = (
173
+ node: AstNode,
174
+ provenance: ResultProvenance,
175
+ helperNames: ReadonlySet<string>,
176
+ namespaceHelpers: NamespaceHelperMap,
177
+ scopedHelpers: ScopedHelperMap,
178
+ scopes: readonly ReadonlySet<string>[]
179
+ ): void => {
180
+ const left = getNodeLeft(node);
181
+ const operator = getNodeOperator(node);
182
+ const right = getNodeRight(node);
183
+ const name = identifierName(left);
184
+ if (!name) {
185
+ return;
186
+ }
187
+ const scope = findNearestBindingScope(name, scopes);
188
+ if (!scope) {
189
+ return;
190
+ }
191
+ if (
192
+ operator === '=' &&
193
+ right &&
194
+ isResultProducingExpression(
195
+ right,
196
+ helperNames,
197
+ namespaceHelpers,
198
+ scopes,
199
+ scopedHelpers
200
+ )
201
+ ) {
202
+ markResultVariable(provenance, scope, name);
203
+ return;
204
+ }
205
+ clearResultVariable(provenance, scope, name);
206
+ };
207
+
208
+ const checkReturnStatement = (
209
+ node: AstNode,
210
+ provenance: ResultProvenance,
211
+ scopes: readonly ReadonlySet<string>[],
212
+ filePath: string,
213
+ sourceCode: string,
214
+ ownerLabel: string,
215
+ diagnostics: WardenDiagnostic[]
216
+ ): void => {
217
+ const argument = getNodeArgument(node);
218
+ if (!argument || !isResultErrCall(argument)) {
219
+ return;
220
+ }
221
+ const variableName = getErrorSourceVariable(getSingleArgument(argument));
222
+ if (!variableName) {
223
+ return;
224
+ }
225
+ const scope = findNearestBindingScope(variableName, scopes);
226
+ if (!scope || !hasResultProvenance(provenance, scope, variableName)) {
227
+ return;
228
+ }
229
+ diagnostics.push(
230
+ createDiagnostic(filePath, sourceCode, argument, ownerLabel, variableName)
231
+ );
232
+ };
233
+
234
+ const functionBody = (node: AstNode): AstNode | null => {
235
+ const body = getNodeBodyNode(node);
236
+ return body &&
237
+ (body.type === 'BlockStatement' || body.type === 'FunctionBody')
238
+ ? body
239
+ : null;
240
+ };
241
+
242
+ const checkFunctionBody = (
243
+ owner: AstNode,
244
+ ownerLabel: string,
245
+ filePath: string,
246
+ sourceCode: string,
247
+ helperNames: ReadonlySet<string>,
248
+ namespaceHelpers: NamespaceHelperMap,
249
+ resultTypeNames: ReadonlySet<string>,
250
+ diagnostics: WardenDiagnostic[]
251
+ ): void => {
252
+ const body = functionBody(owner);
253
+ if (!body) {
254
+ return;
255
+ }
256
+
257
+ const provenance: ResultProvenance = new Map();
258
+ const scopedHelpers: MutableScopedHelperMap = new Map();
259
+ const implScope = collectScopeFrameBindings(owner);
260
+ const initialScopes = implScope.size > 0 ? [implScope] : [];
261
+
262
+ walkWithScopes(
263
+ body,
264
+ (node, scopes) => {
265
+ if (node.type === 'VariableDeclarator') {
266
+ trackScopedResultHelperDeclaration(
267
+ node,
268
+ scopes,
269
+ sourceCode,
270
+ resultTypeNames,
271
+ scopedHelpers
272
+ );
273
+ trackVariableDeclarator(
274
+ node,
275
+ provenance,
276
+ helperNames,
277
+ namespaceHelpers,
278
+ scopedHelpers,
279
+ scopes
280
+ );
281
+ return;
282
+ }
283
+ if (node.type === 'AssignmentExpression') {
284
+ trackAssignmentExpression(
285
+ node,
286
+ provenance,
287
+ helperNames,
288
+ namespaceHelpers,
289
+ scopedHelpers,
290
+ scopes
291
+ );
292
+ return;
293
+ }
294
+ if (node.type === 'ReturnStatement') {
295
+ checkReturnStatement(
296
+ node,
297
+ provenance,
298
+ scopes,
299
+ filePath,
300
+ sourceCode,
301
+ ownerLabel,
302
+ diagnostics
303
+ );
304
+ }
305
+ },
306
+ { initialScopes, stopAtNestedFunctions: true }
307
+ );
308
+ };
309
+
310
+ const isFunctionLike = (node: AstNode): boolean =>
311
+ node.type === 'FunctionDeclaration' ||
312
+ node.type === 'FunctionExpression' ||
313
+ node.type === 'ArrowFunctionExpression';
314
+
315
+ const functionName = (node: AstNode, context: AstParentContext): string => {
316
+ const declaredName = identifierName(getNodeId(node));
317
+ if (declaredName) {
318
+ return declaredName;
319
+ }
320
+ if (context.parent?.type === 'VariableDeclarator') {
321
+ const assignedName = identifierName(getNodeId(context.parent));
322
+ if (assignedName) {
323
+ return assignedName;
324
+ }
325
+ }
326
+ return 'anonymous function';
327
+ };
328
+
329
+ const functionOwnerLabel = (node: AstNode, context: AstParentContext): string =>
330
+ `Function "${functionName(node, context)}"`;
331
+
332
+ export const noRedundantResultErrorWrap: WardenRule = {
333
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
334
+ if (isTestFile(filePath)) {
335
+ return [];
336
+ }
337
+
338
+ const ast = parse(filePath, sourceCode);
339
+ if (!ast) {
340
+ return [];
341
+ }
342
+
343
+ const diagnostics: WardenDiagnostic[] = [];
344
+ const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
345
+ const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
346
+ const resultTypeNames = collectResultTypeNames(ast);
347
+ const implementationStarts = new Set<number>();
348
+ for (const def of findTrailDefinitions(ast)) {
349
+ for (const implementation of findImplementationBodies(def.config)) {
350
+ implementationStarts.add(implementation.start);
351
+ checkFunctionBody(
352
+ implementation,
353
+ `Trail "${def.id}"`,
354
+ filePath,
355
+ sourceCode,
356
+ helperNames,
357
+ namespaceHelpers,
358
+ resultTypeNames,
359
+ diagnostics
360
+ );
361
+ }
362
+ }
363
+ walkWithParents(ast, (node, context) => {
364
+ if (!isFunctionLike(node) || implementationStarts.has(node.start)) {
365
+ return;
366
+ }
367
+ checkFunctionBody(
368
+ node,
369
+ functionOwnerLabel(node, context),
370
+ filePath,
371
+ sourceCode,
372
+ helperNames,
373
+ namespaceHelpers,
374
+ resultTypeNames,
375
+ diagnostics
376
+ );
377
+ });
378
+ return diagnostics;
379
+ },
380
+ description:
381
+ 'Warn when code re-wraps an existing Result error with Result.err(x.error) instead of returning the Result directly.',
382
+ name: RULE_NAME,
383
+ severity: 'warn',
384
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Flags retired `cross` composition vocabulary after the beta.19 compose cutover.
3
+ *
4
+ * Exact authored terms that have a mechanical successor carry safe
5
+ * `term-rewrite` edits. Larger identifiers and type prefixes remain
6
+ * review-required because the text-only rule cannot prove the intended symbol
7
+ * boundary.
8
+ */
9
+ import { resolve, sep } from 'node:path';
10
+
11
+ import { requireGovernedVocabularyTransition } from './retired-vocabulary.js';
12
+ import type { WardenDiagnostic, WardenFix, WardenRule } from './types.js';
13
+
14
+ const RULE_NAME = 'no-retired-cross-vocabulary';
15
+
16
+ const CROSS_COMPOSE_TRANSITION =
17
+ requireGovernedVocabularyTransition('cross-compose');
18
+
19
+ if (CROSS_COMPOSE_TRANSITION.target.kind !== 'single') {
20
+ throw new Error('cross-compose transition must have a single target.');
21
+ }
22
+
23
+ const COMPOSE_TARGET = CROSS_COMPOSE_TRANSITION.target.to;
24
+
25
+ const SAFE_REWRITES = Object.entries(
26
+ CROSS_COMPOSE_TRANSITION.safeRewriteForms
27
+ ).map(([from, to]) => ({ from, to }));
28
+
29
+ const REVIEW_TERMS = CROSS_COMPOSE_TRANSITION.reviewForms;
30
+
31
+ const IDENTIFIER_CHAR = /[$0-9A-Z_a-z]/u;
32
+
33
+ const ALLOWED_PATH_SUFFIXES: readonly string[] = [
34
+ '/docs/migration/cross-to-compose.md',
35
+ '/docs/releases/beta15-to-beta19.md',
36
+ '/docs/adr/0049-composition-is-compose-not-cross.md',
37
+ '/packages/warden/src/rules/no-retired-cross-vocabulary.ts',
38
+ '/packages/warden/src/rules/metadata.ts',
39
+ '/packages/warden/src/rules/retired-vocabulary.ts',
40
+ '/packages/warden/src/trails/governed-symbol-residue.trail.ts',
41
+ '/packages/warden/src/trails/no-retired-cross-vocabulary.trail.ts',
42
+ '/scripts/vocab-cutover-rewrite.ts',
43
+ ];
44
+
45
+ interface SafeRewriteMatch {
46
+ readonly from: string;
47
+ readonly index: number;
48
+ readonly to: string;
49
+ }
50
+
51
+ interface ReviewMatch {
52
+ readonly index: number;
53
+ readonly term: string;
54
+ }
55
+
56
+ const normalizePath = (filePath: string): string =>
57
+ resolve(filePath).split(sep).join('/');
58
+
59
+ const isAllowedFile = (filePath: string): boolean => {
60
+ const normalized = normalizePath(filePath);
61
+ return ALLOWED_PATH_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
62
+ };
63
+
64
+ const isIdentifierChar = (value: string): boolean =>
65
+ value !== '' && IDENTIFIER_CHAR.test(value);
66
+
67
+ const isReviewPrefix = (
68
+ sourceCode: string,
69
+ index: number,
70
+ term: string
71
+ ): boolean => !(term === 'cross' && sourceCode.startsWith('crosses', index));
72
+
73
+ const isStandaloneSpan = (
74
+ sourceCode: string,
75
+ start: number,
76
+ end: number
77
+ ): boolean => {
78
+ const before = start === 0 ? '' : (sourceCode[start - 1] ?? '');
79
+ const after = sourceCode[end] ?? '';
80
+ return !(isIdentifierChar(before) || isIdentifierChar(after));
81
+ };
82
+
83
+ const findSafeRewriteMatches = (
84
+ sourceCode: string
85
+ ): readonly SafeRewriteMatch[] => {
86
+ const matches: SafeRewriteMatch[] = [];
87
+ for (const rewrite of SAFE_REWRITES) {
88
+ let fromIndex = 0;
89
+ while (fromIndex < sourceCode.length) {
90
+ const index = sourceCode.indexOf(rewrite.from, fromIndex);
91
+ if (index === -1) {
92
+ break;
93
+ }
94
+ const end = index + rewrite.from.length;
95
+ if (isStandaloneSpan(sourceCode, index, end)) {
96
+ matches.push({ ...rewrite, index });
97
+ }
98
+ fromIndex = end;
99
+ }
100
+ }
101
+ return matches.toSorted((a, b) => a.index - b.index);
102
+ };
103
+
104
+ const findReviewMatches = (sourceCode: string): readonly ReviewMatch[] => {
105
+ const safeSpans = findSafeRewriteMatches(sourceCode).map((match) => ({
106
+ end: match.index + match.from.length,
107
+ start: match.index,
108
+ }));
109
+ const isInsideSafeSpan = (index: number): boolean =>
110
+ safeSpans.some((span) => index >= span.start && index < span.end);
111
+
112
+ const matches: ReviewMatch[] = [];
113
+ for (const term of REVIEW_TERMS) {
114
+ let fromIndex = 0;
115
+ while (fromIndex < sourceCode.length) {
116
+ const index = sourceCode.indexOf(term, fromIndex);
117
+ if (index === -1) {
118
+ break;
119
+ }
120
+ const end = index + term.length;
121
+ if (!isInsideSafeSpan(index)) {
122
+ const before = index === 0 ? '' : (sourceCode[index - 1] ?? '');
123
+ const after = sourceCode[end] ?? '';
124
+ if (
125
+ !isIdentifierChar(before) &&
126
+ isIdentifierChar(after) &&
127
+ isReviewPrefix(sourceCode, index, term)
128
+ ) {
129
+ matches.push({ index, term });
130
+ }
131
+ }
132
+ fromIndex = end;
133
+ }
134
+ }
135
+ return matches.toSorted((a, b) => a.index - b.index);
136
+ };
137
+
138
+ const lineForOffset = (sourceCode: string, offset: number): number => {
139
+ let line = 1;
140
+ for (let i = 0; i < offset; i += 1) {
141
+ if (sourceCode.codePointAt(i) === 10) {
142
+ line += 1;
143
+ }
144
+ }
145
+ return line;
146
+ };
147
+
148
+ const safeFix = (match: SafeRewriteMatch): WardenFix => ({
149
+ class: 'term-rewrite',
150
+ edits: [
151
+ {
152
+ end: match.index + match.from.length,
153
+ replacement: match.to,
154
+ start: match.index,
155
+ },
156
+ ],
157
+ reason: `Retired composition vocabulary '${match.from}' has a mechanical beta.19 replacement '${match.to}'.`,
158
+ safety: 'safe',
159
+ });
160
+
161
+ const reviewFix = (term: string): WardenFix => ({
162
+ class: 'term-rewrite',
163
+ reason: `Retired composition vocabulary '${term}' appears in a larger or ambiguous form. Review before migrating to ${COMPOSE_TARGET} vocabulary.`,
164
+ safety: 'review',
165
+ });
166
+
167
+ const safeMessage = (match: SafeRewriteMatch): string =>
168
+ `Retired composition vocabulary '${match.from}' should be '${match.to}' after the beta.19 compose cutover.`;
169
+
170
+ const reviewMessage = (term: string): string =>
171
+ `Retired composition vocabulary '${term}' needs review before migrating to ${COMPOSE_TARGET} vocabulary.`;
172
+
173
+ export const noRetiredCrossVocabulary: WardenRule = {
174
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
175
+ if (isAllowedFile(filePath)) {
176
+ return [];
177
+ }
178
+
179
+ const reviewMatches = findReviewMatches(sourceCode);
180
+ if (reviewMatches.length > 0) {
181
+ return reviewMatches.map((match) => ({
182
+ filePath,
183
+ fix: reviewFix(match.term),
184
+ line: lineForOffset(sourceCode, match.index),
185
+ message: reviewMessage(match.term),
186
+ rule: RULE_NAME,
187
+ severity: 'error',
188
+ }));
189
+ }
190
+
191
+ return findSafeRewriteMatches(sourceCode).map((match) => ({
192
+ filePath,
193
+ fix: safeFix(match),
194
+ line: lineForOffset(sourceCode, match.index),
195
+ message: safeMessage(match),
196
+ rule: RULE_NAME,
197
+ severity: 'error',
198
+ }));
199
+ },
200
+ description:
201
+ 'Disallow retired cross composition vocabulary after the beta.19 compose cutover.',
202
+ name: RULE_NAME,
203
+ severity: 'error',
204
+ };