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

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 +497 -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
@@ -1,188 +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
- implementation: 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
- implementation: 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
- implementation: 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
- implementation: 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
- implementation: 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
- implementation: 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
- implementation: 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
- implementation: async (input, ctx) => Result.ok(data)
124
- })
125
-
126
- trail("entity.show", {
127
- detours: [{ target: "entity.edit" }],
128
- implementation: 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
- implementation: 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
-
150
- // ---------------------------------------------------------------------------
151
- // no-direct-impl-in-route
152
- // ---------------------------------------------------------------------------
153
- describe('no-direct-impl-in-route', () => {
154
- test('warns on direct .implementation() call in route', () => {
155
- const code = `
156
- hike("entity.onboard", {
157
- follows: ["entity.create"],
158
- implementation: async (input, ctx) => {
159
- const result = await entityCreate.implementation(data);
160
- return Result.ok(result);
161
- }
162
- })`;
163
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
164
- expect(diagnostics.length).toBe(1);
165
- expect(diagnostics[0]?.severity).toBe('warn');
166
- expect(diagnostics[0]?.message).toContain('ctx.follow');
167
- });
168
-
169
- test('allows ctx.follow() calls', () => {
170
- const code = `
171
- hike("entity.onboard", {
172
- follows: ["entity.create"],
173
- implementation: async (input, ctx) => {
174
- const result = await ctx.follow("entity.create", data);
175
- return Result.ok(result);
176
- }
177
- })`;
178
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
179
- expect(diagnostics.length).toBe(0);
180
- });
181
-
182
- test('ignores files without hike() calls', () => {
183
- const code = `
184
- const result = await someTrail.implementation(data);`;
185
- const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
186
- expect(diagnostics.length).toBe(0);
187
- });
188
- });
@@ -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
- implementation: (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
- implementation: (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
- implementation: (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
- implementation: (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
- });
@@ -1,77 +0,0 @@
1
- /**
2
- * Detects hike implementations that call `.implementation()` directly.
3
- *
4
- * Uses AST parsing to find hike definition bodies and check for
5
- * `.implementation()` call expressions.
6
- */
7
-
8
- import {
9
- findImplementationBodies,
10
- findTrailDefinitions,
11
- isImplementationCall,
12
- offsetToLine,
13
- parse,
14
- walk,
15
- } from './ast.js';
16
- import type { WardenDiagnostic, WardenRule } from './types.js';
17
-
18
- interface AstNode {
19
- readonly type: string;
20
- readonly start: number;
21
- readonly end: number;
22
- readonly [key: string]: unknown;
23
- }
24
-
25
- const findImplCallsInHike = (
26
- def: { readonly config: AstNode },
27
- filePath: string,
28
- sourceCode: string,
29
- diagnostics: WardenDiagnostic[]
30
- ): void => {
31
- for (const body of findImplementationBodies(def.config as AstNode)) {
32
- walk(body, (node) => {
33
- if (isImplementationCall(node as AstNode)) {
34
- diagnostics.push({
35
- filePath,
36
- line: offsetToLine(sourceCode, node.start),
37
- message:
38
- 'Use ctx.follow("trailId", input) instead of direct .implementation() calls. ctx.follow() validates input and propagates tracing.',
39
- rule: 'no-direct-impl-in-route',
40
- severity: 'warn',
41
- });
42
- }
43
- });
44
- }
45
- };
46
-
47
- /**
48
- * Detects routes that call another trail's `.implementation()` directly.
49
- */
50
- export const noDirectImplInRoute: WardenRule = {
51
- check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
52
- if (!/\bhike\s*\(/.test(sourceCode)) {
53
- return [];
54
- }
55
-
56
- const ast = parse(filePath, sourceCode);
57
- if (!ast) {
58
- return [];
59
- }
60
-
61
- const diagnostics: WardenDiagnostic[] = [];
62
- const hikeDefs = findTrailDefinitions(ast as AstNode).filter(
63
- (d) => d.kind === 'hike'
64
- );
65
-
66
- for (const def of hikeDefs) {
67
- findImplCallsInHike(def, filePath, sourceCode, diagnostics);
68
- }
69
-
70
- return diagnostics;
71
- },
72
- description:
73
- 'Prefer ctx.follow() over direct .implementation() calls in route bodies.',
74
- name: 'no-direct-impl-in-route',
75
-
76
- severity: 'warn',
77
- };
@@ -1,150 +0,0 @@
1
- /**
2
- * Flags throws in implementations that are used as detour targets.
3
- *
4
- * Uses AST parsing for accurate detection of detour target IDs and
5
- * throw statements within those trail implementations.
6
- */
7
-
8
- import {
9
- findImplementationBodies,
10
- findTrailDefinitions,
11
- offsetToLine,
12
- parse,
13
- walk,
14
- } from './ast.js';
15
- import { isTestFile } from './scan.js';
16
- import type {
17
- ProjectAwareWardenRule,
18
- ProjectContext,
19
- WardenDiagnostic,
20
- } from './types.js';
21
-
22
- interface AstNode {
23
- readonly type: string;
24
- readonly start: number;
25
- readonly end: number;
26
- readonly [key: string]: unknown;
27
- }
28
-
29
- /** Collect all trail IDs referenced as detour targets in the AST. */
30
- const collectDetourTargets = (ast: AstNode): ReadonlySet<string> => {
31
- const targets = new Set<string>();
32
-
33
- walk(ast, (node) => {
34
- if (
35
- node.type !== 'Property' ||
36
- node.key?.name !== 'detours' ||
37
- !node.value
38
- ) {
39
- return;
40
- }
41
-
42
- walk(node.value as AstNode, (inner) => {
43
- if (inner.type === 'Literal' || inner.type === 'StringLiteral') {
44
- const { value } = inner as unknown as { value?: unknown };
45
- if (typeof value === 'string' && value.includes('.')) {
46
- targets.add(value);
47
- }
48
- }
49
- });
50
- });
51
-
52
- return targets;
53
- };
54
-
55
- /** Find throws in implementation bodies of targeted trails. */
56
- const findThrowsInTargetedTrails = (
57
- ast: AstNode,
58
- sourceCode: string,
59
- filePath: string,
60
- detourTargets: ReadonlySet<string>
61
- ): WardenDiagnostic[] => {
62
- const diagnostics: WardenDiagnostic[] = [];
63
-
64
- for (const def of findTrailDefinitions(ast)) {
65
- if (!detourTargets.has(def.id)) {
66
- continue;
67
- }
68
-
69
- for (const body of findImplementationBodies(def.config as AstNode)) {
70
- walk(body, (node) => {
71
- if (node.type === 'ThrowStatement') {
72
- diagnostics.push({
73
- filePath,
74
- line: offsetToLine(sourceCode, node.start),
75
- message: `Trail "${def.id}" is a detour target and must not throw. Use Result.err() instead.`,
76
- rule: 'no-throw-in-detour-target',
77
- severity: 'error',
78
- });
79
- }
80
- });
81
- }
82
- }
83
-
84
- return diagnostics;
85
- };
86
-
87
- const checkThrowInDetourTargets = (
88
- sourceCode: string,
89
- filePath: string,
90
- detourTargets: ReadonlySet<string>
91
- ): readonly WardenDiagnostic[] => {
92
- if (isTestFile(filePath)) {
93
- return [];
94
- }
95
-
96
- const ast = parse(filePath, sourceCode);
97
- if (!ast) {
98
- return [];
99
- }
100
-
101
- return findThrowsInTargetedTrails(
102
- ast as AstNode,
103
- sourceCode,
104
- filePath,
105
- detourTargets
106
- );
107
- };
108
-
109
- /**
110
- * Flags throws in implementations that are used as detour targets.
111
- */
112
- export const noThrowInDetourTarget: ProjectAwareWardenRule = {
113
- check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
114
- const ast = parse(filePath, sourceCode);
115
- if (!ast) {
116
- return [];
117
- }
118
- return checkThrowInDetourTargets(
119
- sourceCode,
120
- filePath,
121
- collectDetourTargets(ast as AstNode)
122
- );
123
- },
124
- checkWithContext(
125
- sourceCode: string,
126
- filePath: string,
127
- context: ProjectContext
128
- ): readonly WardenDiagnostic[] {
129
- if (context.detourTargetTrailIds) {
130
- return checkThrowInDetourTargets(
131
- sourceCode,
132
- filePath,
133
- context.detourTargetTrailIds
134
- );
135
- }
136
- const ast = parse(filePath, sourceCode);
137
- if (!ast) {
138
- return [];
139
- }
140
- return checkThrowInDetourTargets(
141
- sourceCode,
142
- filePath,
143
- collectDetourTargets(ast as AstNode)
144
- );
145
- },
146
- description:
147
- 'Disallow throw statements inside implementations that are referenced as detour targets.',
148
- name: 'no-throw-in-detour-target',
149
- severity: 'error',
150
- };
@@ -1,187 +0,0 @@
1
- import { collectTrailIds } from './specs.js';
2
- import type {
3
- ProjectAwareWardenRule,
4
- ProjectContext,
5
- WardenDiagnostic,
6
- } from './types.js';
7
-
8
- interface BraceState {
9
- depth: number;
10
- found: boolean;
11
- }
12
-
13
- const trackBraces = (line: string, state: BraceState): void => {
14
- for (const ch of line) {
15
- if (ch === '{') {
16
- state.depth += 1;
17
- state.found = true;
18
- }
19
- if (ch === '}') {
20
- state.depth -= 1;
21
- }
22
- }
23
- };
24
-
25
- const collectArrayText = (lines: readonly string[], start: number): string => {
26
- let text = '';
27
- for (let k = start; k < lines.length && k < start + 20; k += 1) {
28
- const line = lines[k];
29
- if (!line) {
30
- continue;
31
- }
32
- text += `${line}\n`;
33
- if (text.includes(']')) {
34
- break;
35
- }
36
- }
37
- return text;
38
- };
39
-
40
- const findMissingDetourTargets = (
41
- text: string,
42
- knownIds: ReadonlySet<string>
43
- ): string[] => {
44
- const missing: string[] = [];
45
- for (const m of text.matchAll(/target\s*:\s*["'`]([^"'`]+)["'`]/g)) {
46
- const [, id] = m;
47
- if (id && !knownIds.has(id)) {
48
- missing.push(id);
49
- }
50
- }
51
- return missing;
52
- };
53
-
54
- const findMissingPlainDetours = (
55
- text: string,
56
- knownIds: ReadonlySet<string>
57
- ): string[] => {
58
- const missing: string[] = [];
59
- const cleaned = text.replaceAll(/target\s*:\s*["'`][^"'`]+["'`]/g, '');
60
- for (const m of cleaned.matchAll(/["'`]([^"'`]+)["'`]/g)) {
61
- const [, id] = m;
62
- if (id && id.includes('.') && !knownIds.has(id)) {
63
- missing.push(id);
64
- }
65
- }
66
- return missing;
67
- };
68
-
69
- const findAllMissingDetours = (
70
- text: string,
71
- knownIds: ReadonlySet<string>
72
- ): string[] => [
73
- ...findMissingDetourTargets(text, knownIds),
74
- ...findMissingPlainDetours(text, knownIds),
75
- ];
76
-
77
- const addMissingDetourDiagnostics = (
78
- specLine: string,
79
- j: number,
80
- lines: readonly string[],
81
- trailId: string,
82
- lineNum: number,
83
- filePath: string,
84
- knownIds: ReadonlySet<string>,
85
- diagnostics: WardenDiagnostic[]
86
- ): void => {
87
- if (!/\bdetours\s*:/.test(specLine)) {
88
- return;
89
- }
90
- for (const targetId of findAllMissingDetours(
91
- collectArrayText(lines, j),
92
- knownIds
93
- )) {
94
- diagnostics.push({
95
- filePath,
96
- line: lineNum,
97
- message: `Trail "${trailId}" has detour targeting "${targetId}" which is not defined.`,
98
- rule: 'valid-detour-refs',
99
- severity: 'error',
100
- });
101
- }
102
- };
103
-
104
- const scanTrailDetours = (
105
- lines: readonly string[],
106
- startIndex: number,
107
- trailId: string,
108
- filePath: string,
109
- knownIds: ReadonlySet<string>,
110
- diagnostics: WardenDiagnostic[]
111
- ): void => {
112
- const braceState: BraceState = { depth: 0, found: false };
113
- for (let j = startIndex; j < lines.length && j < startIndex + 200; j += 1) {
114
- const specLine = lines[j];
115
- if (!specLine) {
116
- continue;
117
- }
118
- trackBraces(specLine, braceState);
119
- addMissingDetourDiagnostics(
120
- specLine,
121
- j,
122
- lines,
123
- trailId,
124
- startIndex + 1,
125
- filePath,
126
- knownIds,
127
- diagnostics
128
- );
129
- if (braceState.found && braceState.depth <= 0) {
130
- break;
131
- }
132
- }
133
- };
134
-
135
- const processLine = (
136
- line: string,
137
- i: number,
138
- lines: readonly string[],
139
- filePath: string,
140
- knownIds: ReadonlySet<string>,
141
- diagnostics: WardenDiagnostic[]
142
- ): void => {
143
- const trailMatch = line.match(/\btrail\s*\(\s*["'`]([^"'`]+)["'`]/);
144
- if (!trailMatch) {
145
- return;
146
- }
147
- const [, trailId] = trailMatch;
148
- if (!trailId) {
149
- return;
150
- }
151
- scanTrailDetours(lines, i, trailId, filePath, knownIds, diagnostics);
152
- };
153
-
154
- const checkDetourRefs = (
155
- sourceCode: string,
156
- filePath: string,
157
- knownIds: ReadonlySet<string>
158
- ): readonly WardenDiagnostic[] => {
159
- const diagnostics: WardenDiagnostic[] = [];
160
- const lines = sourceCode.split('\n');
161
- for (let i = 0; i < lines.length; i += 1) {
162
- const line = lines[i];
163
- if (line) {
164
- processLine(line, i, lines, filePath, knownIds, diagnostics);
165
- }
166
- }
167
- return diagnostics;
168
- };
169
-
170
- /**
171
- * Checks that all trail IDs referenced in `detours` declarations exist.
172
- */
173
- export const validDetourRefs: ProjectAwareWardenRule = {
174
- check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
175
- return checkDetourRefs(sourceCode, filePath, collectTrailIds(sourceCode));
176
- },
177
- checkWithContext(
178
- sourceCode: string,
179
- filePath: string,
180
- context: ProjectContext
181
- ): readonly WardenDiagnostic[] {
182
- return checkDetourRefs(sourceCode, filePath, context.knownTrailIds);
183
- },
184
- description: 'Ensure all detour target trail IDs reference defined trails.',
185
- name: 'valid-detour-refs',
186
- severity: 'error',
187
- };
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"],
8
- "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
- }
@@ -1 +0,0 @@
1
- {"root":["./src/cli.ts","./src/drift.ts","./src/formatters.ts","./src/index.ts","./src/rules/ast.ts","./src/rules/context-no-surface-types.ts","./src/rules/implementation-returns-result.ts","./src/rules/index.ts","./src/rules/no-direct-impl-in-route.ts","./src/rules/no-direct-implementation-call.ts","./src/rules/no-sync-result-assumption.ts","./src/rules/no-throw-in-detour-target.ts","./src/rules/no-throw-in-implementation.ts","./src/rules/prefer-schema-inference.ts","./src/rules/scan.ts","./src/rules/specs.ts","./src/rules/structure.ts","./src/rules/types.ts","./src/rules/valid-describe-refs.ts","./src/rules/valid-detour-refs.ts"],"version":"5.9.3"}