@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.22

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 (249) hide show
  1. package/CHANGELOG.md +508 -6
  2. package/README.md +77 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +27 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +28 -0
  7. package/src/cli.ts +1374 -103
  8. package/src/command.ts +953 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +106 -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 +206 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/resolve.ts +530 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/ast.ts +3176 -85
  20. package/src/rules/circular-refs.ts +154 -0
  21. package/src/rules/composes-declarations.ts +704 -0
  22. package/src/rules/context-no-surface-types.ts +68 -8
  23. package/src/rules/contour-exists.ts +251 -0
  24. package/src/rules/contour-ids.ts +15 -0
  25. package/src/rules/dead-internal-trail.ts +154 -0
  26. package/src/rules/draft-file-marking.ts +160 -0
  27. package/src/rules/draft-visible-debt.ts +87 -0
  28. package/src/rules/error-mapping-completeness.ts +288 -0
  29. package/src/rules/example-valid.ts +401 -0
  30. package/src/rules/fires-declarations.ts +758 -0
  31. package/src/rules/implementation-returns-result.ts +1265 -95
  32. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  33. package/src/rules/incomplete-crud.ts +580 -0
  34. package/src/rules/index.ts +219 -18
  35. package/src/rules/intent-propagation.ts +127 -0
  36. package/src/rules/layer-field-name-drift.ts +96 -0
  37. package/src/rules/metadata.ts +654 -0
  38. package/src/rules/missing-reconcile.ts +98 -0
  39. package/src/rules/missing-visibility.ts +110 -0
  40. package/src/rules/no-destructured-compose.ts +192 -0
  41. package/src/rules/no-dev-permit-in-source.ts +99 -0
  42. package/src/rules/no-direct-implementation-call.ts +7 -7
  43. package/src/rules/no-legacy-layer-imports.ts +211 -0
  44. package/src/rules/no-native-error-result.ts +111 -0
  45. package/src/rules/no-redundant-result-error-wrap.ts +331 -0
  46. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  47. package/src/rules/no-sync-result-assumption.ts +1134 -99
  48. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  49. package/src/rules/no-throw-in-implementation.ts +10 -9
  50. package/src/rules/no-top-level-surface.ts +389 -0
  51. package/src/rules/on-references-exist.ts +194 -0
  52. package/src/rules/orphaned-signal.ts +150 -0
  53. package/src/rules/owner-projection-parity.ts +146 -0
  54. package/src/rules/permit-governance.ts +25 -0
  55. package/src/rules/public-export-example-coverage.ts +553 -0
  56. package/src/rules/public-internal-deep-imports.ts +517 -0
  57. package/src/rules/public-output-schema.ts +29 -0
  58. package/src/rules/public-union-output-discriminants.ts +150 -0
  59. package/src/rules/read-intent-fires.ts +187 -0
  60. package/src/rules/reference-exists.ts +98 -0
  61. package/src/rules/registry-names.ts +145 -0
  62. package/src/rules/resolved-import-boundary.ts +146 -0
  63. package/src/rules/resource-declarations.ts +704 -0
  64. package/src/rules/resource-exists.ts +179 -0
  65. package/src/rules/resource-id-grammar.ts +65 -0
  66. package/src/rules/resource-mock-coverage.ts +115 -0
  67. package/src/rules/scan.ts +38 -25
  68. package/src/rules/scheduled-destroy-intent.ts +44 -0
  69. package/src/rules/signal-graph-coaching.ts +191 -0
  70. package/src/rules/specs.ts +9 -5
  71. package/src/rules/static-resource-accessor-preference.ts +657 -0
  72. package/src/rules/surface-facet-coherence.ts +370 -0
  73. package/src/rules/trail-versioning-source.ts +1094 -0
  74. package/src/rules/trail-versioning-topo.ts +172 -0
  75. package/src/rules/types.ts +270 -6
  76. package/src/rules/unmaterialized-activation-source.ts +84 -0
  77. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  78. package/src/rules/valid-describe-refs.ts +160 -32
  79. package/src/rules/valid-detour-contract.ts +78 -0
  80. package/src/rules/warden-export-symmetry.ts +533 -0
  81. package/src/rules/warden-rules-use-ast.ts +996 -0
  82. package/src/rules/webhook-route-collision.ts +243 -0
  83. package/src/trails/activation-orphan.trail.ts +84 -0
  84. package/src/trails/circular-refs.trail.ts +29 -0
  85. package/src/trails/composes-declarations.trail.ts +22 -0
  86. package/src/trails/context-no-surface-types.trail.ts +21 -0
  87. package/src/trails/contour-exists.trail.ts +21 -0
  88. package/src/trails/dead-internal-trail.trail.ts +26 -0
  89. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  90. package/src/trails/draft-file-marking.trail.ts +16 -0
  91. package/src/trails/draft-visible-debt.trail.ts +16 -0
  92. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  93. package/src/trails/example-valid.trail.ts +25 -0
  94. package/src/trails/fires-declarations.trail.ts +23 -0
  95. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  96. package/src/trails/implementation-returns-result.trail.ts +20 -0
  97. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  98. package/src/trails/incomplete-crud.trail.ts +39 -0
  99. package/src/trails/index.ts +78 -0
  100. package/src/trails/intent-propagation.trail.ts +30 -0
  101. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  102. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  103. package/src/trails/missing-reconcile.trail.ts +33 -0
  104. package/src/trails/missing-visibility.trail.ts +22 -0
  105. package/src/trails/no-destructured-compose.trail.ts +44 -0
  106. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  107. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  108. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  109. package/src/trails/no-native-error-result.trail.ts +18 -0
  110. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  111. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  112. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  113. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  114. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  115. package/src/trails/no-top-level-surface.trail.ts +43 -0
  116. package/src/trails/on-references-exist.trail.ts +21 -0
  117. package/src/trails/orphaned-signal.trail.ts +36 -0
  118. package/src/trails/owner-projection-parity.trail.ts +26 -0
  119. package/src/trails/pending-force.trail.ts +21 -0
  120. package/src/trails/permit-governance.trail.ts +51 -0
  121. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  122. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  123. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  124. package/src/trails/public-output-schema.trail.ts +55 -0
  125. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  126. package/src/trails/read-intent-fires.trail.ts +20 -0
  127. package/src/trails/reference-exists.trail.ts +25 -0
  128. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  129. package/src/trails/resource-declarations.trail.ts +25 -0
  130. package/src/trails/resource-exists.trail.ts +27 -0
  131. package/src/trails/resource-id-grammar.trail.ts +39 -0
  132. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  133. package/src/trails/run.ts +162 -0
  134. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  135. package/src/trails/schema.ts +194 -0
  136. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  137. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  138. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  139. package/src/trails/topo.ts +6 -0
  140. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  141. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  142. package/src/trails/valid-describe-refs.trail.ts +18 -0
  143. package/src/trails/valid-detour-contract.trail.ts +71 -0
  144. package/src/trails/version-gap.trail.ts +35 -0
  145. package/src/trails/version-pinned-compose.trail.ts +23 -0
  146. package/src/trails/version-without-examples.trail.ts +38 -0
  147. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  148. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  149. package/src/trails/webhook-route-collision.trail.ts +50 -0
  150. package/src/trails/wrap-rule.ts +213 -0
  151. package/src/workspaces.ts +238 -0
  152. package/.turbo/turbo-build.log +0 -1
  153. package/.turbo/turbo-lint.log +0 -3
  154. package/.turbo/turbo-typecheck.log +0 -1
  155. package/dist/cli.d.ts +0 -46
  156. package/dist/cli.d.ts.map +0 -1
  157. package/dist/cli.js +0 -221
  158. package/dist/cli.js.map +0 -1
  159. package/dist/drift.d.ts +0 -26
  160. package/dist/drift.d.ts.map +0 -1
  161. package/dist/drift.js +0 -27
  162. package/dist/drift.js.map +0 -1
  163. package/dist/formatters.d.ts +0 -29
  164. package/dist/formatters.d.ts.map +0 -1
  165. package/dist/formatters.js +0 -87
  166. package/dist/formatters.js.map +0 -1
  167. package/dist/index.d.ts +0 -26
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -26
  170. package/dist/index.js.map +0 -1
  171. package/dist/rules/ast.d.ts +0 -41
  172. package/dist/rules/ast.d.ts.map +0 -1
  173. package/dist/rules/ast.js +0 -163
  174. package/dist/rules/ast.js.map +0 -1
  175. package/dist/rules/context-no-surface-types.d.ts +0 -12
  176. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  177. package/dist/rules/context-no-surface-types.js +0 -96
  178. package/dist/rules/context-no-surface-types.js.map +0 -1
  179. package/dist/rules/implementation-returns-result.d.ts +0 -13
  180. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  181. package/dist/rules/implementation-returns-result.js +0 -231
  182. package/dist/rules/implementation-returns-result.js.map +0 -1
  183. package/dist/rules/index.d.ts +0 -22
  184. package/dist/rules/index.d.ts.map +0 -1
  185. package/dist/rules/index.js +0 -41
  186. package/dist/rules/index.js.map +0 -1
  187. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  188. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  189. package/dist/rules/no-direct-impl-in-route.js +0 -46
  190. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  191. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  192. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  193. package/dist/rules/no-direct-implementation-call.js +0 -39
  194. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  195. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  196. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  197. package/dist/rules/no-sync-result-assumption.js +0 -98
  198. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  199. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  200. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  201. package/dist/rules/no-throw-in-detour-target.js +0 -87
  202. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  203. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  204. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  205. package/dist/rules/no-throw-in-implementation.js +0 -34
  206. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  207. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  208. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  209. package/dist/rules/prefer-schema-inference.js +0 -86
  210. package/dist/rules/prefer-schema-inference.js.map +0 -1
  211. package/dist/rules/scan.d.ts +0 -8
  212. package/dist/rules/scan.d.ts.map +0 -1
  213. package/dist/rules/scan.js +0 -32
  214. package/dist/rules/scan.js.map +0 -1
  215. package/dist/rules/specs.d.ts +0 -29
  216. package/dist/rules/specs.d.ts.map +0 -1
  217. package/dist/rules/specs.js +0 -192
  218. package/dist/rules/specs.js.map +0 -1
  219. package/dist/rules/structure.d.ts +0 -13
  220. package/dist/rules/structure.d.ts.map +0 -1
  221. package/dist/rules/structure.js +0 -142
  222. package/dist/rules/structure.js.map +0 -1
  223. package/dist/rules/types.d.ts +0 -52
  224. package/dist/rules/types.d.ts.map +0 -1
  225. package/dist/rules/types.js +0 -2
  226. package/dist/rules/types.js.map +0 -1
  227. package/dist/rules/valid-describe-refs.d.ts +0 -7
  228. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  229. package/dist/rules/valid-describe-refs.js +0 -51
  230. package/dist/rules/valid-describe-refs.js.map +0 -1
  231. package/dist/rules/valid-detour-refs.d.ts +0 -6
  232. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  233. package/dist/rules/valid-detour-refs.js +0 -116
  234. package/dist/rules/valid-detour-refs.js.map +0 -1
  235. package/src/__tests__/cli.test.ts +0 -198
  236. package/src/__tests__/drift.test.ts +0 -74
  237. package/src/__tests__/formatters.test.ts +0 -157
  238. package/src/__tests__/implementation-returns-result.test.ts +0 -75
  239. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  240. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  241. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  242. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  243. package/src/__tests__/rules.test.ts +0 -188
  244. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  245. package/src/rules/no-direct-impl-in-route.ts +0 -77
  246. package/src/rules/no-throw-in-detour-target.ts +0 -150
  247. package/src/rules/valid-detour-refs.ts +0 -187
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -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,111 @@
1
+ import { identifierName, offsetToLine, parse, walk } from './ast.js';
2
+ import { isFrameworkInternalFile } from './scan.js';
3
+ import type { AstNode } from './ast.js';
4
+ import type { WardenDiagnostic, WardenRule } from './types.js';
5
+
6
+ const RULE_NAME = 'no-native-error-result';
7
+
8
+ const NATIVE_ERROR_CONSTRUCTORS = new Set([
9
+ 'AggregateError',
10
+ 'Error',
11
+ 'EvalError',
12
+ 'RangeError',
13
+ 'ReferenceError',
14
+ 'SyntaxError',
15
+ 'TypeError',
16
+ 'URIError',
17
+ ]);
18
+
19
+ const getMemberPropertyName = (node: AstNode): string | null => {
20
+ if (
21
+ node.type !== 'MemberExpression' &&
22
+ node.type !== 'StaticMemberExpression'
23
+ ) {
24
+ return null;
25
+ }
26
+
27
+ return identifierName((node as unknown as { property?: AstNode }).property);
28
+ };
29
+
30
+ const isResultObject = (node: AstNode | undefined): boolean => {
31
+ if (!node) {
32
+ return false;
33
+ }
34
+
35
+ if (identifierName(node) === 'Result') {
36
+ return true;
37
+ }
38
+
39
+ return getMemberPropertyName(node) === 'Result';
40
+ };
41
+
42
+ const isResultErrCall = (node: AstNode): boolean => {
43
+ if (node.type !== 'CallExpression') {
44
+ return false;
45
+ }
46
+
47
+ const { callee } = node as unknown as { callee?: AstNode };
48
+ if (!callee || getMemberPropertyName(callee) !== 'err') {
49
+ return false;
50
+ }
51
+
52
+ return isResultObject((callee as unknown as { object?: AstNode }).object);
53
+ };
54
+
55
+ const isNativeErrorConstruction = (node: AstNode | undefined): boolean => {
56
+ if (!node || node.type !== 'NewExpression') {
57
+ return false;
58
+ }
59
+
60
+ const constructorName = identifierName(
61
+ (node as unknown as { callee?: AstNode }).callee
62
+ );
63
+ return constructorName
64
+ ? NATIVE_ERROR_CONSTRUCTORS.has(constructorName)
65
+ : false;
66
+ };
67
+
68
+ const getFirstArgument = (node: AstNode): AstNode | undefined =>
69
+ (node as unknown as { arguments?: readonly AstNode[] }).arguments?.[0];
70
+
71
+ const createDiagnostic = (
72
+ filePath: string,
73
+ sourceCode: string,
74
+ node: AstNode
75
+ ): WardenDiagnostic => ({
76
+ filePath,
77
+ line: offsetToLine(sourceCode, node.start),
78
+ message:
79
+ 'Use a specific TrailsError subclass with Result.err(...) instead of native Error.',
80
+ rule: RULE_NAME,
81
+ severity: 'error',
82
+ });
83
+
84
+ export const noNativeErrorResult: WardenRule = {
85
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
86
+ if (isFrameworkInternalFile(filePath)) {
87
+ return [];
88
+ }
89
+
90
+ const ast = parse(filePath, sourceCode);
91
+ if (!ast) {
92
+ return [];
93
+ }
94
+
95
+ const diagnostics: WardenDiagnostic[] = [];
96
+ walk(ast, (node) => {
97
+ if (
98
+ isResultErrCall(node) &&
99
+ isNativeErrorConstruction(getFirstArgument(node))
100
+ ) {
101
+ diagnostics.push(createDiagnostic(filePath, sourceCode, node));
102
+ }
103
+ });
104
+
105
+ return diagnostics;
106
+ },
107
+ description:
108
+ 'Require Result.err(...) calls to carry specific TrailsError subclasses instead of native Error.',
109
+ name: RULE_NAME,
110
+ severity: 'error',
111
+ };
@@ -0,0 +1,331 @@
1
+ import {
2
+ collectScopeFrameBindings,
3
+ findBlazeBodies,
4
+ findTrailDefinitions,
5
+ getMemberExpression,
6
+ identifierName,
7
+ isMemberAccessNonComputed,
8
+ offsetToLine,
9
+ parse,
10
+ walkWithScopes,
11
+ } from './ast.js';
12
+ import {
13
+ collectAllResultHelperNames,
14
+ collectNamespaceHelperImports,
15
+ collectResultTypeNames,
16
+ findNearestBindingScope,
17
+ isHelperCall,
18
+ isResultExpression,
19
+ trackScopedResultHelperDeclaration,
20
+ } from './implementation-returns-result.js';
21
+ import { isTestFile } from './scan.js';
22
+ import type { AstNode } from './ast.js';
23
+ import type {
24
+ MutableScopedHelperMap,
25
+ NamespaceHelperMap,
26
+ ScopedHelperMap,
27
+ } from './implementation-returns-result.js';
28
+ import type { WardenDiagnostic, WardenRule } from './types.js';
29
+
30
+ const RULE_NAME = 'no-redundant-result-error-wrap';
31
+
32
+ const getStaticMemberName = (node: AstNode | undefined): string | null => {
33
+ if (!node || !isMemberAccessNonComputed(node)) {
34
+ return null;
35
+ }
36
+ return identifierName((node as unknown as { property?: AstNode }).property);
37
+ };
38
+
39
+ const isResultObject = (node: AstNode | undefined): boolean => {
40
+ if (!node) {
41
+ return false;
42
+ }
43
+ if (identifierName(node) === 'Result') {
44
+ return true;
45
+ }
46
+ return getStaticMemberName(node) === 'Result';
47
+ };
48
+
49
+ const isResultErrCall = (node: AstNode): boolean => {
50
+ if (node.type !== 'CallExpression') {
51
+ return false;
52
+ }
53
+ const { callee } = node as unknown as { callee?: AstNode };
54
+ const member = getMemberExpression(callee);
55
+ if (!member || getStaticMemberName(callee) !== 'err') {
56
+ return false;
57
+ }
58
+ return isResultObject(member.object);
59
+ };
60
+
61
+ const getSingleArgument = (node: AstNode): AstNode | null => {
62
+ const args = (node as unknown as { arguments?: readonly AstNode[] })
63
+ .arguments;
64
+ return args?.length === 1 ? (args[0] ?? null) : null;
65
+ };
66
+
67
+ const getErrorSourceVariable = (node: AstNode | null): string | null => {
68
+ if (!node || getStaticMemberName(node) !== 'error') {
69
+ return null;
70
+ }
71
+ const member = getMemberExpression(node);
72
+ return identifierName(member?.object);
73
+ };
74
+
75
+ const isResultProducingExpression = (
76
+ node: AstNode,
77
+ helperNames: ReadonlySet<string>,
78
+ namespaceHelpers: NamespaceHelperMap,
79
+ scopes: readonly ReadonlySet<string>[],
80
+ scopedHelpers: ScopedHelperMap
81
+ ): boolean =>
82
+ isResultExpression(node) ||
83
+ isHelperCall(node, helperNames, namespaceHelpers, scopes, scopedHelpers);
84
+
85
+ type ResultProvenance = Map<ReadonlySet<string>, Set<string>>;
86
+
87
+ const markResultVariable = (
88
+ provenance: ResultProvenance,
89
+ scope: ReadonlySet<string>,
90
+ name: string
91
+ ): void => {
92
+ const names = provenance.get(scope);
93
+ if (names) {
94
+ names.add(name);
95
+ return;
96
+ }
97
+ provenance.set(scope, new Set([name]));
98
+ };
99
+
100
+ const clearResultVariable = (
101
+ provenance: ResultProvenance,
102
+ scope: ReadonlySet<string>,
103
+ name: string
104
+ ): void => {
105
+ provenance.get(scope)?.delete(name);
106
+ };
107
+
108
+ const hasResultProvenance = (
109
+ provenance: ResultProvenance,
110
+ scope: ReadonlySet<string>,
111
+ name: string
112
+ ): boolean => provenance.get(scope)?.has(name) ?? false;
113
+
114
+ const createDiagnostic = (
115
+ filePath: string,
116
+ sourceCode: string,
117
+ node: AstNode,
118
+ trailId: string,
119
+ variableName: string
120
+ ): WardenDiagnostic => ({
121
+ filePath,
122
+ line: offsetToLine(sourceCode, node.start),
123
+ message: `Trail "${trailId}": Result.err(${variableName}.error) re-wraps a Result that already carries that error. Return ${variableName} directly to preserve the original Result boundary.`,
124
+ rule: RULE_NAME,
125
+ severity: 'warn',
126
+ });
127
+
128
+ const trackVariableDeclarator = (
129
+ node: AstNode,
130
+ provenance: ResultProvenance,
131
+ helperNames: ReadonlySet<string>,
132
+ namespaceHelpers: NamespaceHelperMap,
133
+ scopedHelpers: ScopedHelperMap,
134
+ scopes: readonly ReadonlySet<string>[]
135
+ ): void => {
136
+ const { id, init } = node as unknown as { id?: AstNode; init?: AstNode };
137
+ const name = identifierName(id);
138
+ if (!name) {
139
+ return;
140
+ }
141
+ const scope = findNearestBindingScope(name, scopes);
142
+ if (!scope) {
143
+ return;
144
+ }
145
+ if (
146
+ init &&
147
+ isResultProducingExpression(
148
+ init,
149
+ helperNames,
150
+ namespaceHelpers,
151
+ scopes,
152
+ scopedHelpers
153
+ )
154
+ ) {
155
+ markResultVariable(provenance, scope, name);
156
+ return;
157
+ }
158
+ clearResultVariable(provenance, scope, name);
159
+ };
160
+
161
+ const trackAssignmentExpression = (
162
+ node: AstNode,
163
+ provenance: ResultProvenance,
164
+ helperNames: ReadonlySet<string>,
165
+ namespaceHelpers: NamespaceHelperMap,
166
+ scopedHelpers: ScopedHelperMap,
167
+ scopes: readonly ReadonlySet<string>[]
168
+ ): void => {
169
+ const { left, operator, right } = node as unknown as {
170
+ left?: AstNode;
171
+ operator?: string;
172
+ right?: AstNode;
173
+ };
174
+ const name = identifierName(left);
175
+ if (!name) {
176
+ return;
177
+ }
178
+ const scope = findNearestBindingScope(name, scopes);
179
+ if (!scope) {
180
+ return;
181
+ }
182
+ if (
183
+ operator === '=' &&
184
+ right &&
185
+ isResultProducingExpression(
186
+ right,
187
+ helperNames,
188
+ namespaceHelpers,
189
+ scopes,
190
+ scopedHelpers
191
+ )
192
+ ) {
193
+ markResultVariable(provenance, scope, name);
194
+ return;
195
+ }
196
+ clearResultVariable(provenance, scope, name);
197
+ };
198
+
199
+ const checkReturnStatement = (
200
+ node: AstNode,
201
+ provenance: ResultProvenance,
202
+ scopes: readonly ReadonlySet<string>[],
203
+ filePath: string,
204
+ sourceCode: string,
205
+ trailId: string,
206
+ diagnostics: WardenDiagnostic[]
207
+ ): void => {
208
+ const { argument } = node as unknown as { argument?: AstNode };
209
+ if (!argument || !isResultErrCall(argument)) {
210
+ return;
211
+ }
212
+ const variableName = getErrorSourceVariable(getSingleArgument(argument));
213
+ if (!variableName) {
214
+ return;
215
+ }
216
+ const scope = findNearestBindingScope(variableName, scopes);
217
+ if (!scope || !hasResultProvenance(provenance, scope, variableName)) {
218
+ return;
219
+ }
220
+ diagnostics.push(
221
+ createDiagnostic(filePath, sourceCode, argument, trailId, variableName)
222
+ );
223
+ };
224
+
225
+ const checkBlazeBody = (
226
+ blaze: AstNode,
227
+ trailId: string,
228
+ filePath: string,
229
+ sourceCode: string,
230
+ helperNames: ReadonlySet<string>,
231
+ namespaceHelpers: NamespaceHelperMap,
232
+ resultTypeNames: ReadonlySet<string>,
233
+ diagnostics: WardenDiagnostic[]
234
+ ): void => {
235
+ const { body } = blaze as unknown as { body?: AstNode };
236
+ if (
237
+ !body ||
238
+ (body.type !== 'BlockStatement' && body.type !== 'FunctionBody')
239
+ ) {
240
+ return;
241
+ }
242
+
243
+ const provenance: ResultProvenance = new Map();
244
+ const scopedHelpers: MutableScopedHelperMap = new Map();
245
+ const implScope = collectScopeFrameBindings(blaze);
246
+ const initialScopes = implScope.size > 0 ? [implScope] : [];
247
+
248
+ walkWithScopes(
249
+ body,
250
+ (node, scopes) => {
251
+ if (node.type === 'VariableDeclarator') {
252
+ trackScopedResultHelperDeclaration(
253
+ node,
254
+ scopes,
255
+ sourceCode,
256
+ resultTypeNames,
257
+ scopedHelpers
258
+ );
259
+ trackVariableDeclarator(
260
+ node,
261
+ provenance,
262
+ helperNames,
263
+ namespaceHelpers,
264
+ scopedHelpers,
265
+ scopes
266
+ );
267
+ return;
268
+ }
269
+ if (node.type === 'AssignmentExpression') {
270
+ trackAssignmentExpression(
271
+ node,
272
+ provenance,
273
+ helperNames,
274
+ namespaceHelpers,
275
+ scopedHelpers,
276
+ scopes
277
+ );
278
+ return;
279
+ }
280
+ if (node.type === 'ReturnStatement') {
281
+ checkReturnStatement(
282
+ node,
283
+ provenance,
284
+ scopes,
285
+ filePath,
286
+ sourceCode,
287
+ trailId,
288
+ diagnostics
289
+ );
290
+ }
291
+ },
292
+ { initialScopes, stopAtNestedFunctions: true }
293
+ );
294
+ };
295
+
296
+ export const noRedundantResultErrorWrap: WardenRule = {
297
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
298
+ if (isTestFile(filePath)) {
299
+ return [];
300
+ }
301
+
302
+ const ast = parse(filePath, sourceCode);
303
+ if (!ast) {
304
+ return [];
305
+ }
306
+
307
+ const diagnostics: WardenDiagnostic[] = [];
308
+ const helperNames = collectAllResultHelperNames(ast, sourceCode, filePath);
309
+ const namespaceHelpers = collectNamespaceHelperImports(ast, filePath);
310
+ const resultTypeNames = collectResultTypeNames(ast);
311
+ for (const def of findTrailDefinitions(ast)) {
312
+ for (const blaze of findBlazeBodies(def.config)) {
313
+ checkBlazeBody(
314
+ blaze,
315
+ def.id,
316
+ filePath,
317
+ sourceCode,
318
+ helperNames,
319
+ namespaceHelpers,
320
+ resultTypeNames,
321
+ diagnostics
322
+ );
323
+ }
324
+ }
325
+ return diagnostics;
326
+ },
327
+ description:
328
+ 'Warn when blazes re-wrap an existing Result error with Result.err(x.error) instead of returning the Result directly.',
329
+ name: RULE_NAME,
330
+ severity: 'warn',
331
+ };