@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
@@ -1,227 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { contextNoSurfaceTypes } from '../rules/context-no-surface-types.js';
4
- import { noDirectImplInRoute } from '../rules/no-direct-impl-in-route.js';
5
- import { noThrowInImplementation } from '../rules/no-throw-in-implementation.js';
6
- import { validDetourRefs } from '../rules/valid-detour-refs.js';
7
-
8
- const TEST_FILE = 'test.ts';
9
-
10
- // ---------------------------------------------------------------------------
11
- // no-throw-in-implementation
12
- // ---------------------------------------------------------------------------
13
- describe('no-throw-in-implementation', () => {
14
- test('flags throw inside implementation body', () => {
15
- const code = `
16
- trail("entity.show", {
17
- run: async (input, ctx) => {
18
- throw new Error("boom");
19
- }
20
- })`;
21
- const diagnostics = noThrowInImplementation.check(code, TEST_FILE);
22
- expect(diagnostics.length).toBe(1);
23
- expect(diagnostics[0]?.rule).toBe('no-throw-in-implementation');
24
- expect(diagnostics[0]?.severity).toBe('error');
25
- });
26
-
27
- test('allows Result.err() in implementation', () => {
28
- const code = `
29
- trail("entity.show", {
30
- run: async (input, ctx) => {
31
- return Result.err(new NotFoundError("not found"));
32
- }
33
- })`;
34
- const diagnostics = noThrowInImplementation.check(code, TEST_FILE);
35
- expect(diagnostics.length).toBe(0);
36
- });
37
-
38
- test('does not flag throw outside implementation', () => {
39
- const code = `
40
- function helper() {
41
- throw new Error("boom");
42
- }
43
-
44
- trail("entity.show", {
45
- run: async (input, ctx) => {
46
- return Result.ok(data);
47
- }
48
- })`;
49
- const diagnostics = noThrowInImplementation.check(code, TEST_FILE);
50
- expect(diagnostics.length).toBe(0);
51
- });
52
- });
53
-
54
- // ---------------------------------------------------------------------------
55
- // context-no-surface-types
56
- // ---------------------------------------------------------------------------
57
- describe('context-no-surface-types', () => {
58
- test('flags express import in trail file', () => {
59
- const code = `
60
- import { Request, Response } from "express";
61
- trail("entity.show", {
62
- run: async (input, ctx) => {
63
- return Result.ok(data);
64
- }
65
- })`;
66
- const diagnostics = contextNoSurfaceTypes.check(code, TEST_FILE);
67
- expect(diagnostics.length).toBe(1);
68
- expect(diagnostics[0]?.rule).toBe('context-no-surface-types');
69
- expect(diagnostics[0]?.message).toContain('express');
70
- });
71
-
72
- test('flags McpSession import in trail file', () => {
73
- const code = `
74
- import type { McpSession } from "@modelcontextprotocol/sdk";
75
- trail("entity.show", {
76
- run: async (input, ctx) => {
77
- return Result.ok(data);
78
- }
79
- })`;
80
- const diagnostics = contextNoSurfaceTypes.check(code, TEST_FILE);
81
- expect(diagnostics.length).toBe(1);
82
- });
83
-
84
- test('allows @ontrails/core imports in trail file', () => {
85
- const code = `
86
- import { trail, Result } from "@ontrails/core";
87
- trail("entity.show", {
88
- run: async (input, ctx) => {
89
- return Result.ok(data);
90
- }
91
- })`;
92
- const diagnostics = contextNoSurfaceTypes.check(code, TEST_FILE);
93
- expect(diagnostics.length).toBe(0);
94
- });
95
-
96
- test('ignores files without trail() calls', () => {
97
- const code = `
98
- import { Request, Response } from "express";
99
- export function handleRequest(req: Request, res: Response) {}`;
100
- const diagnostics = contextNoSurfaceTypes.check(code, TEST_FILE);
101
- expect(diagnostics.length).toBe(0);
102
- });
103
- });
104
-
105
- // ---------------------------------------------------------------------------
106
- // valid-detour-refs
107
- // ---------------------------------------------------------------------------
108
- describe('valid-detour-refs', () => {
109
- test('flags detour target that does not exist', () => {
110
- const code = `
111
- trail("entity.show", {
112
- detours: [{ target: "entity.edit" }],
113
- run: async (input, ctx) => Result.ok(data)
114
- })`;
115
- const diagnostics = validDetourRefs.check(code, TEST_FILE);
116
- expect(diagnostics.length).toBe(1);
117
- expect(diagnostics[0]?.message).toContain('entity.edit');
118
- });
119
-
120
- test('passes when detour target exists', () => {
121
- const code = `
122
- trail("entity.edit", {
123
- run: async (input, ctx) => Result.ok(data)
124
- })
125
-
126
- trail("entity.show", {
127
- detours: [{ target: "entity.edit" }],
128
- run: async (input, ctx) => Result.ok(data)
129
- })`;
130
- const diagnostics = validDetourRefs.check(code, TEST_FILE);
131
- expect(diagnostics.length).toBe(0);
132
- });
133
-
134
- test('uses project context when available', () => {
135
- const code = `
136
- trail("entity.show", {
137
- detours: [{ target: "entity.edit" }],
138
- run: async (input, ctx) => Result.ok(data)
139
- })`;
140
- const context = { knownTrailIds: new Set(['entity.show', 'entity.edit']) };
141
- const diagnostics = validDetourRefs.checkWithContext(
142
- code,
143
- TEST_FILE,
144
- context
145
- );
146
- expect(diagnostics.length).toBe(0);
147
- });
148
-
149
- test('flags detour target in trail with follow that does not exist', () => {
150
- const code = `
151
- trail("entity.onboard", {
152
- detours: [{ target: "entity.missing" }],
153
- follow: ["entity.create"],
154
- run: async (input, ctx) => Result.ok(data)
155
- })`;
156
- const diagnostics = validDetourRefs.check(code, TEST_FILE);
157
- expect(diagnostics.length).toBe(1);
158
- expect(diagnostics[0]?.message).toContain('entity.missing');
159
- });
160
-
161
- test('passes when trail with follow detour target exists', () => {
162
- const code = `
163
- trail("entity.fallback", {
164
- run: async (input, ctx) => Result.ok(data)
165
- })
166
-
167
- trail("entity.onboard", {
168
- detours: [{ target: "entity.fallback" }],
169
- follow: ["entity.create"],
170
- run: async (input, ctx) => Result.ok(data)
171
- })`;
172
- const diagnostics = validDetourRefs.check(code, TEST_FILE);
173
- expect(diagnostics.length).toBe(0);
174
- });
175
- });
176
-
177
- // ---------------------------------------------------------------------------
178
- // no-direct-impl-in-route
179
- // ---------------------------------------------------------------------------
180
- describe('no-direct-impl-in-route', () => {
181
- test('warns on direct .run() call in trail with follow', () => {
182
- const code = `
183
- trail("entity.onboard", {
184
- follow: ["entity.create"],
185
- run: async (input, ctx) => {
186
- const result = await entityCreate.run(data);
187
- return Result.ok(result);
188
- }
189
- })`;
190
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
191
- expect(diagnostics.length).toBe(1);
192
- expect(diagnostics[0]?.severity).toBe('warn');
193
- expect(diagnostics[0]?.message).toContain('ctx.follow');
194
- });
195
-
196
- test('allows ctx.follow() calls', () => {
197
- const code = `
198
- trail("entity.onboard", {
199
- follow: ["entity.create"],
200
- run: async (input, ctx) => {
201
- const result = await ctx.follow("entity.create", data);
202
- return Result.ok(result);
203
- }
204
- })`;
205
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
206
- expect(diagnostics.length).toBe(0);
207
- });
208
-
209
- test('ignores trails without follow', () => {
210
- const code = `
211
- trail("entity.show", {
212
- run: async (input, ctx) => {
213
- const result = await someTrail.run(data);
214
- return Result.ok(result);
215
- }
216
- })`;
217
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
218
- expect(diagnostics.length).toBe(0);
219
- });
220
-
221
- test('ignores files without trail() calls', () => {
222
- const code = `
223
- const result = await someTrail.run(data);`;
224
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
225
- expect(diagnostics.length).toBe(0);
226
- });
227
- });
@@ -1,60 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { validDescribeRefs } from '../rules/valid-describe-refs.js';
4
-
5
- describe('valid-describe-refs', () => {
6
- test('warns when a describe @see tag points to a missing trail', () => {
7
- const code = `
8
- trail("entity.show", {
9
- input: z.object({
10
- query: z.string().describe("Search query. @see entity.search"),
11
- }),
12
- run: (input) => Result.ok(input),
13
- })`;
14
-
15
- const diagnostics = validDescribeRefs.check(code, 'src/entity.ts');
16
-
17
- expect(diagnostics).toHaveLength(1);
18
- expect(diagnostics[0]?.rule).toBe('valid-describe-refs');
19
- expect(diagnostics[0]?.message).toContain('entity.search');
20
- });
21
-
22
- test('allows local @see references that resolve in the same file', () => {
23
- const code = `
24
- trail("entity.search", {
25
- input: z.object({ query: z.string() }),
26
- run: (input) => Result.ok(input),
27
- })
28
-
29
- trail("entity.show", {
30
- input: z.object({
31
- query: z.string().describe("Search query. @see entity.search"),
32
- }),
33
- run: (input) => Result.ok(input),
34
- })`;
35
-
36
- const diagnostics = validDescribeRefs.check(code, 'src/entity.ts');
37
-
38
- expect(diagnostics).toHaveLength(0);
39
- });
40
-
41
- test('uses project context for cross-file @see references', () => {
42
- const code = `
43
- trail("entity.show", {
44
- input: z.object({
45
- query: z.string().describe("Search query. @see entity.search"),
46
- }),
47
- run: (input) => Result.ok(input),
48
- })`;
49
-
50
- const diagnostics = validDescribeRefs.checkWithContext(
51
- code,
52
- 'src/entity.ts',
53
- {
54
- knownTrailIds: new Set(['entity.search', 'entity.show']),
55
- }
56
- );
57
-
58
- expect(diagnostics).toHaveLength(0);
59
- });
60
- });
package/src/rules/ast.ts DELETED
@@ -1,213 +0,0 @@
1
- /**
2
- * Shared AST utilities for warden rules.
3
- *
4
- * Uses oxc-parser for native-speed TypeScript parsing. Provides a lightweight
5
- * walker and helpers for finding trail implementation bodies.
6
- */
7
-
8
- import { parseSync } from 'oxc-parser';
9
-
10
- // ---------------------------------------------------------------------------
11
- // Types (minimal, avoiding full @oxc-project/types dep)
12
- // ---------------------------------------------------------------------------
13
-
14
- export interface AstNode {
15
- readonly type: string;
16
- readonly start: number;
17
- readonly end: number;
18
- readonly key?: { readonly name?: string };
19
- readonly value?: AstNode;
20
- readonly body?: AstNode | readonly AstNode[];
21
- readonly [key: string]: unknown;
22
- }
23
-
24
- // ---------------------------------------------------------------------------
25
- // Parser
26
- // ---------------------------------------------------------------------------
27
-
28
- /** Parse TypeScript source into an AST. Returns null on parse failure. */
29
- export const parse = (filePath: string, sourceCode: string): AstNode | null => {
30
- try {
31
- const result = parseSync(filePath, sourceCode, { sourceType: 'module' });
32
- return result.program as unknown as AstNode;
33
- } catch {
34
- return null;
35
- }
36
- };
37
-
38
- // ---------------------------------------------------------------------------
39
- // Walker
40
- // ---------------------------------------------------------------------------
41
-
42
- /** Walk an AST node tree, calling `visit` on every node. */
43
- export const walk = (node: unknown, visit: (node: AstNode) => void): void => {
44
- if (!node || typeof node !== 'object') {
45
- return;
46
- }
47
- const n = node as AstNode;
48
- if (n.type) {
49
- visit(n);
50
- }
51
- for (const val of Object.values(n)) {
52
- if (Array.isArray(val)) {
53
- for (const item of val) {
54
- walk(item, visit);
55
- }
56
- } else if (val && typeof val === 'object' && (val as AstNode).type) {
57
- walk(val, visit);
58
- }
59
- }
60
- };
61
-
62
- // ---------------------------------------------------------------------------
63
- // Helpers
64
- // ---------------------------------------------------------------------------
65
-
66
- /** Find the byte offset's line number (1-based) in source code. */
67
- export const offsetToLine = (sourceCode: string, offset: number): number => {
68
- let line = 1;
69
- for (let i = 0; i < offset && i < sourceCode.length; i += 1) {
70
- if (sourceCode[i] === '\n') {
71
- line += 1;
72
- }
73
- }
74
- return line;
75
- };
76
-
77
- /** Find all `run:` property values in an AST. */
78
- export const findRunBodies = (ast: AstNode): AstNode[] => {
79
- const bodies: AstNode[] = [];
80
- walk(ast, (node) => {
81
- if (node.type === 'Property' && node.key?.name === 'run' && node.value) {
82
- bodies.push(node.value);
83
- }
84
- });
85
- return bodies;
86
- };
87
-
88
- export interface TrailDefinition {
89
- /** Trail ID string, e.g. "entity.show" */
90
- readonly id: string;
91
- /** "trail" or "event" */
92
- readonly kind: string;
93
- /** The config object argument (second arg to trail() call) */
94
- readonly config: AstNode;
95
- /** Start offset of the call expression */
96
- readonly start: number;
97
- }
98
-
99
- /**
100
- * Find all `trail("id", { ... })` and `event("id", { ... })` call sites.
101
- *
102
- * Returns the trail ID, kind, and config object node for each definition.
103
- */
104
- const TRAIL_CALLEE_NAMES = new Set(['trail', 'event']);
105
-
106
- const getTrailCalleeName = (node: AstNode): string | null => {
107
- if (node.type !== 'CallExpression') {
108
- return null;
109
- }
110
- const callee = node['callee'] as AstNode | undefined;
111
- if (!callee || callee.type !== 'Identifier') {
112
- return null;
113
- }
114
- const { name } = callee as unknown as { name?: string };
115
- return name && TRAIL_CALLEE_NAMES.has(name) ? name : null;
116
- };
117
-
118
- const extractTrailArgs = (
119
- node: AstNode
120
- ): { idArg: AstNode; configArg: AstNode } | null => {
121
- const args = node['arguments'] as readonly AstNode[] | undefined;
122
- if (!args || args.length < 2) {
123
- return null;
124
- }
125
- const [idArg, configArg] = args;
126
- if (!idArg || !configArg) {
127
- return null;
128
- }
129
- return { configArg, idArg };
130
- };
131
-
132
- const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
133
- const calleeName = getTrailCalleeName(node);
134
- if (!calleeName) {
135
- return null;
136
- }
137
-
138
- const trailArgs = extractTrailArgs(node);
139
- if (!trailArgs) {
140
- return null;
141
- }
142
-
143
- const trailId = (trailArgs.idArg as unknown as { value?: string }).value;
144
- if (!trailId) {
145
- return null;
146
- }
147
-
148
- return {
149
- config: trailArgs.configArg,
150
- id: trailId,
151
- kind: calleeName,
152
- start: node.start,
153
- };
154
- };
155
-
156
- /** Check if a node is a call to `.run()` on some object. */
157
- export const isRunCall = (node: AstNode): boolean => {
158
- if (node.type !== 'CallExpression') {
159
- return false;
160
- }
161
- const callee = node['callee'] as AstNode | undefined;
162
- if (!callee) {
163
- return false;
164
- }
165
- if (
166
- callee.type !== 'StaticMemberExpression' &&
167
- callee.type !== 'MemberExpression'
168
- ) {
169
- return false;
170
- }
171
- const prop = (callee as unknown as { property?: AstNode }).property;
172
- return (
173
- prop?.type === 'Identifier' &&
174
- (prop as unknown as { name: string }).name === 'run'
175
- );
176
- };
177
-
178
- export const findTrailDefinitions = (ast: AstNode): TrailDefinition[] => {
179
- const definitions: TrailDefinition[] = [];
180
-
181
- walk(ast, (node) => {
182
- const def = extractTrailDefinition(node);
183
- if (def) {
184
- definitions.push(def);
185
- }
186
- });
187
-
188
- return definitions;
189
- };
190
-
191
- // ---------------------------------------------------------------------------
192
- // Config property extraction helpers
193
- // ---------------------------------------------------------------------------
194
-
195
- /** Find a Property node by key name inside an ObjectExpression config. */
196
- export const findConfigProperty = (
197
- config: AstNode,
198
- propertyName: string
199
- ): AstNode | null => {
200
- if (config.type !== 'ObjectExpression') {
201
- return null;
202
- }
203
- const properties = config['properties'] as readonly AstNode[] | undefined;
204
- if (!properties) {
205
- return null;
206
- }
207
- for (const prop of properties) {
208
- if (prop.type === 'Property' && prop.key?.name === propertyName) {
209
- return prop;
210
- }
211
- }
212
- return null;
213
- };
@@ -1,81 +0,0 @@
1
- /**
2
- * Detects trail implementations with `follow` that call `.run()` directly.
3
- *
4
- * Uses AST parsing to find trail definitions that declare `follow` and check for
5
- * `.run()` call expressions in their bodies.
6
- */
7
-
8
- import {
9
- findConfigProperty,
10
- findRunBodies,
11
- findTrailDefinitions,
12
- isRunCall,
13
- offsetToLine,
14
- parse,
15
- walk,
16
- } from './ast.js';
17
- import type { WardenDiagnostic, WardenRule } from './types.js';
18
-
19
- interface AstNode {
20
- readonly type: string;
21
- readonly start: number;
22
- readonly end: number;
23
- readonly [key: string]: unknown;
24
- }
25
-
26
- const findImplCallsInTrailWithFollow = (
27
- def: { readonly config: AstNode },
28
- filePath: string,
29
- sourceCode: string,
30
- diagnostics: WardenDiagnostic[]
31
- ): void => {
32
- for (const body of findRunBodies(def.config as AstNode)) {
33
- walk(body, (node) => {
34
- if (isRunCall(node as AstNode)) {
35
- diagnostics.push({
36
- filePath,
37
- line: offsetToLine(sourceCode, node.start),
38
- message:
39
- 'Use ctx.follow("trailId", input) instead of direct .run() calls. ctx.follow() validates input and propagates tracing.',
40
- rule: 'no-direct-impl-in-route',
41
- severity: 'warn',
42
- });
43
- }
44
- });
45
- }
46
- };
47
-
48
- const hasFollowProperty = (config: AstNode): boolean =>
49
- findConfigProperty(config as AstNode, 'follow') !== null;
50
-
51
- /**
52
- * Detects trails with `follow` that call another trail's `.run()` directly.
53
- */
54
- export const noDirectImplInRoute: WardenRule = {
55
- check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
56
- if (!/\btrail\s*\(/.test(sourceCode)) {
57
- return [];
58
- }
59
-
60
- const ast = parse(filePath, sourceCode);
61
- if (!ast) {
62
- return [];
63
- }
64
-
65
- const diagnostics: WardenDiagnostic[] = [];
66
- const followDefs = findTrailDefinitions(ast as AstNode).filter((d) =>
67
- hasFollowProperty(d.config as AstNode)
68
- );
69
-
70
- for (const def of followDefs) {
71
- findImplCallsInTrailWithFollow(def, filePath, sourceCode, diagnostics);
72
- }
73
-
74
- return diagnostics;
75
- },
76
- description:
77
- 'Prefer ctx.follow() over direct .run() calls in trail bodies with follow.',
78
- name: 'no-direct-impl-in-route',
79
-
80
- severity: 'warn',
81
- };