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

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 (60) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +6 -6
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +1 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/rules/ast.d.ts +6 -6
  8. package/dist/rules/ast.d.ts.map +1 -1
  9. package/dist/rules/ast.js +8 -10
  10. package/dist/rules/ast.js.map +1 -1
  11. package/dist/rules/context-no-surface-types.js +1 -1
  12. package/dist/rules/context-no-surface-types.js.map +1 -1
  13. package/dist/rules/implementation-returns-result.d.ts +1 -1
  14. package/dist/rules/implementation-returns-result.d.ts.map +1 -1
  15. package/dist/rules/implementation-returns-result.js +52 -6
  16. package/dist/rules/implementation-returns-result.js.map +1 -1
  17. package/dist/rules/index.d.ts +1 -8
  18. package/dist/rules/index.d.ts.map +1 -1
  19. package/dist/rules/index.js +1 -8
  20. package/dist/rules/index.js.map +1 -1
  21. package/dist/rules/no-direct-impl-in-route.d.ts +4 -4
  22. package/dist/rules/no-direct-impl-in-route.d.ts.map +1 -1
  23. package/dist/rules/no-direct-impl-in-route.js +15 -14
  24. package/dist/rules/no-direct-impl-in-route.js.map +1 -1
  25. package/dist/rules/no-direct-implementation-call.d.ts +3 -3
  26. package/dist/rules/no-direct-implementation-call.js +7 -7
  27. package/dist/rules/no-direct-implementation-call.js.map +1 -1
  28. package/dist/rules/no-sync-result-assumption.d.ts +1 -1
  29. package/dist/rules/no-sync-result-assumption.js +5 -5
  30. package/dist/rules/no-sync-result-assumption.js.map +1 -1
  31. package/dist/rules/no-throw-in-detour-target.js +2 -2
  32. package/dist/rules/no-throw-in-detour-target.js.map +1 -1
  33. package/dist/rules/no-throw-in-implementation.d.ts +1 -1
  34. package/dist/rules/no-throw-in-implementation.js +3 -3
  35. package/dist/rules/no-throw-in-implementation.js.map +1 -1
  36. package/dist/rules/specs.d.ts +1 -1
  37. package/dist/rules/specs.d.ts.map +1 -1
  38. package/dist/rules/specs.js +2 -2
  39. package/dist/rules/specs.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/cli.test.ts +7 -7
  42. package/src/__tests__/drift.test.ts +1 -1
  43. package/src/__tests__/implementation-returns-result.test.ts +60 -6
  44. package/src/__tests__/no-direct-implementation-call.test.ts +8 -8
  45. package/src/__tests__/no-sync-result-assumption.test.ts +6 -6
  46. package/src/__tests__/no-throw-in-detour-target.test.ts +6 -6
  47. package/src/__tests__/prefer-schema-inference.test.ts +4 -4
  48. package/src/__tests__/rules.test.ts +59 -20
  49. package/src/__tests__/valid-describe-refs.test.ts +4 -4
  50. package/src/cli.ts +1 -4
  51. package/src/rules/ast.ts +10 -14
  52. package/src/rules/context-no-surface-types.ts +1 -1
  53. package/src/rules/implementation-returns-result.ts +63 -6
  54. package/src/rules/index.ts +1 -8
  55. package/src/rules/no-direct-impl-in-route.ts +20 -16
  56. package/src/rules/no-direct-implementation-call.ts +7 -7
  57. package/src/rules/no-sync-result-assumption.ts +5 -5
  58. package/src/rules/no-throw-in-detour-target.ts +2 -2
  59. package/src/rules/no-throw-in-implementation.ts +3 -3
  60. package/src/rules/specs.ts +5 -5
@@ -10,7 +10,7 @@ trail("entity.show", {
10
10
  fields: {
11
11
  firstName: { label: "First Name" },
12
12
  },
13
- implementation: (input) => Result.ok(input),
13
+ run: (input) => Result.ok(input),
14
14
  })`;
15
15
 
16
16
  const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
@@ -31,7 +31,7 @@ trail("entity.paint", {
31
31
  options: [{ value: "red" }, { value: "green" }],
32
32
  },
33
33
  },
34
- implementation: (input) => Result.ok(input),
34
+ run: (input) => Result.ok(input),
35
35
  })`;
36
36
 
37
37
  const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
@@ -56,7 +56,7 @@ trail("entity.paint", {
56
56
  },
57
57
  displayName: { label: "Public name" },
58
58
  },
59
- implementation: (input) => Result.ok(input),
59
+ run: (input) => Result.ok(input),
60
60
  })`;
61
61
 
62
62
  const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
@@ -74,7 +74,7 @@ trail("entity.show", {
74
74
  message: "Who should we greet?",
75
75
  },
76
76
  },
77
- implementation: (input) => Result.ok(input),
77
+ run: (input) => Result.ok(input),
78
78
  })`;
79
79
 
80
80
  const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
@@ -14,7 +14,7 @@ describe('no-throw-in-implementation', () => {
14
14
  test('flags throw inside implementation body', () => {
15
15
  const code = `
16
16
  trail("entity.show", {
17
- implementation: async (input, ctx) => {
17
+ run: async (input, ctx) => {
18
18
  throw new Error("boom");
19
19
  }
20
20
  })`;
@@ -27,7 +27,7 @@ trail("entity.show", {
27
27
  test('allows Result.err() in implementation', () => {
28
28
  const code = `
29
29
  trail("entity.show", {
30
- implementation: async (input, ctx) => {
30
+ run: async (input, ctx) => {
31
31
  return Result.err(new NotFoundError("not found"));
32
32
  }
33
33
  })`;
@@ -42,7 +42,7 @@ function helper() {
42
42
  }
43
43
 
44
44
  trail("entity.show", {
45
- implementation: async (input, ctx) => {
45
+ run: async (input, ctx) => {
46
46
  return Result.ok(data);
47
47
  }
48
48
  })`;
@@ -59,7 +59,7 @@ describe('context-no-surface-types', () => {
59
59
  const code = `
60
60
  import { Request, Response } from "express";
61
61
  trail("entity.show", {
62
- implementation: async (input, ctx) => {
62
+ run: async (input, ctx) => {
63
63
  return Result.ok(data);
64
64
  }
65
65
  })`;
@@ -73,7 +73,7 @@ trail("entity.show", {
73
73
  const code = `
74
74
  import type { McpSession } from "@modelcontextprotocol/sdk";
75
75
  trail("entity.show", {
76
- implementation: async (input, ctx) => {
76
+ run: async (input, ctx) => {
77
77
  return Result.ok(data);
78
78
  }
79
79
  })`;
@@ -85,7 +85,7 @@ trail("entity.show", {
85
85
  const code = `
86
86
  import { trail, Result } from "@ontrails/core";
87
87
  trail("entity.show", {
88
- implementation: async (input, ctx) => {
88
+ run: async (input, ctx) => {
89
89
  return Result.ok(data);
90
90
  }
91
91
  })`;
@@ -110,7 +110,7 @@ describe('valid-detour-refs', () => {
110
110
  const code = `
111
111
  trail("entity.show", {
112
112
  detours: [{ target: "entity.edit" }],
113
- implementation: async (input, ctx) => Result.ok(data)
113
+ run: async (input, ctx) => Result.ok(data)
114
114
  })`;
115
115
  const diagnostics = validDetourRefs.check(code, TEST_FILE);
116
116
  expect(diagnostics.length).toBe(1);
@@ -120,12 +120,12 @@ trail("entity.show", {
120
120
  test('passes when detour target exists', () => {
121
121
  const code = `
122
122
  trail("entity.edit", {
123
- implementation: async (input, ctx) => Result.ok(data)
123
+ run: async (input, ctx) => Result.ok(data)
124
124
  })
125
125
 
126
126
  trail("entity.show", {
127
127
  detours: [{ target: "entity.edit" }],
128
- implementation: async (input, ctx) => Result.ok(data)
128
+ run: async (input, ctx) => Result.ok(data)
129
129
  })`;
130
130
  const diagnostics = validDetourRefs.check(code, TEST_FILE);
131
131
  expect(diagnostics.length).toBe(0);
@@ -135,7 +135,7 @@ trail("entity.show", {
135
135
  const code = `
136
136
  trail("entity.show", {
137
137
  detours: [{ target: "entity.edit" }],
138
- implementation: async (input, ctx) => Result.ok(data)
138
+ run: async (input, ctx) => Result.ok(data)
139
139
  })`;
140
140
  const context = { knownTrailIds: new Set(['entity.show', 'entity.edit']) };
141
141
  const diagnostics = validDetourRefs.checkWithContext(
@@ -145,18 +145,45 @@ trail("entity.show", {
145
145
  );
146
146
  expect(diagnostics.length).toBe(0);
147
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
+ });
148
175
  });
149
176
 
150
177
  // ---------------------------------------------------------------------------
151
178
  // no-direct-impl-in-route
152
179
  // ---------------------------------------------------------------------------
153
180
  describe('no-direct-impl-in-route', () => {
154
- test('warns on direct .implementation() call in route', () => {
181
+ test('warns on direct .run() call in trail with follow', () => {
155
182
  const code = `
156
- hike("entity.onboard", {
157
- follows: ["entity.create"],
158
- implementation: async (input, ctx) => {
159
- const result = await entityCreate.implementation(data);
183
+ trail("entity.onboard", {
184
+ follow: ["entity.create"],
185
+ run: async (input, ctx) => {
186
+ const result = await entityCreate.run(data);
160
187
  return Result.ok(result);
161
188
  }
162
189
  })`;
@@ -168,9 +195,9 @@ hike("entity.onboard", {
168
195
 
169
196
  test('allows ctx.follow() calls', () => {
170
197
  const code = `
171
- hike("entity.onboard", {
172
- follows: ["entity.create"],
173
- implementation: async (input, ctx) => {
198
+ trail("entity.onboard", {
199
+ follow: ["entity.create"],
200
+ run: async (input, ctx) => {
174
201
  const result = await ctx.follow("entity.create", data);
175
202
  return Result.ok(result);
176
203
  }
@@ -179,9 +206,21 @@ hike("entity.onboard", {
179
206
  expect(diagnostics.length).toBe(0);
180
207
  });
181
208
 
182
- test('ignores files without hike() calls', () => {
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', () => {
183
222
  const code = `
184
- const result = await someTrail.implementation(data);`;
223
+ const result = await someTrail.run(data);`;
185
224
  const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
186
225
  expect(diagnostics.length).toBe(0);
187
226
  });
@@ -9,7 +9,7 @@ trail("entity.show", {
9
9
  input: z.object({
10
10
  query: z.string().describe("Search query. @see entity.search"),
11
11
  }),
12
- implementation: (input) => Result.ok(input),
12
+ run: (input) => Result.ok(input),
13
13
  })`;
14
14
 
15
15
  const diagnostics = validDescribeRefs.check(code, 'src/entity.ts');
@@ -23,14 +23,14 @@ trail("entity.show", {
23
23
  const code = `
24
24
  trail("entity.search", {
25
25
  input: z.object({ query: z.string() }),
26
- implementation: (input) => Result.ok(input),
26
+ run: (input) => Result.ok(input),
27
27
  })
28
28
 
29
29
  trail("entity.show", {
30
30
  input: z.object({
31
31
  query: z.string().describe("Search query. @see entity.search"),
32
32
  }),
33
- implementation: (input) => Result.ok(input),
33
+ run: (input) => Result.ok(input),
34
34
  })`;
35
35
 
36
36
  const diagnostics = validDescribeRefs.check(code, 'src/entity.ts');
@@ -44,7 +44,7 @@ trail("entity.show", {
44
44
  input: z.object({
45
45
  query: z.string().describe("Search query. @see entity.search"),
46
46
  }),
47
- implementation: (input) => Result.ok(input),
47
+ run: (input) => Result.ok(input),
48
48
  })`;
49
49
 
50
50
  const diagnostics = validDescribeRefs.checkWithContext(
package/src/cli.ts CHANGED
@@ -152,10 +152,7 @@ const loadSourceFiles = async (
152
152
  };
153
153
 
154
154
  const buildProjectContextFromTopo = (appTopo: Topo): ProjectContext => {
155
- const knownTrailIds = new Set<string>([
156
- ...appTopo.trails.keys(),
157
- ...appTopo.hikes.keys(),
158
- ]);
155
+ const knownTrailIds = new Set<string>(appTopo.trails.keys());
159
156
 
160
157
  const detourTargetTrailIds = new Set<string>();
161
158
  for (const t of appTopo.trails.values()) {
package/src/rules/ast.ts CHANGED
@@ -74,15 +74,11 @@ export const offsetToLine = (sourceCode: string, offset: number): number => {
74
74
  return line;
75
75
  };
76
76
 
77
- /** Find all `implementation:` property values in an AST. */
78
- export const findImplementationBodies = (ast: AstNode): AstNode[] => {
77
+ /** Find all `run:` property values in an AST. */
78
+ export const findRunBodies = (ast: AstNode): AstNode[] => {
79
79
  const bodies: AstNode[] = [];
80
80
  walk(ast, (node) => {
81
- if (
82
- node.type === 'Property' &&
83
- node.key?.name === 'implementation' &&
84
- node.value
85
- ) {
81
+ if (node.type === 'Property' && node.key?.name === 'run' && node.value) {
86
82
  bodies.push(node.value);
87
83
  }
88
84
  });
@@ -92,20 +88,20 @@ export const findImplementationBodies = (ast: AstNode): AstNode[] => {
92
88
  export interface TrailDefinition {
93
89
  /** Trail ID string, e.g. "entity.show" */
94
90
  readonly id: string;
95
- /** "trail" or "hike" */
91
+ /** "trail" or "event" */
96
92
  readonly kind: string;
97
- /** The config object argument (second arg to trail/hike call) */
93
+ /** The config object argument (second arg to trail() call) */
98
94
  readonly config: AstNode;
99
95
  /** Start offset of the call expression */
100
96
  readonly start: number;
101
97
  }
102
98
 
103
99
  /**
104
- * Find all `trail("id", { ... })` and `hike("id", { ... })` call sites.
100
+ * Find all `trail("id", { ... })` and `event("id", { ... })` call sites.
105
101
  *
106
102
  * Returns the trail ID, kind, and config object node for each definition.
107
103
  */
108
- const TRAIL_CALLEE_NAMES = new Set(['trail', 'hike']);
104
+ const TRAIL_CALLEE_NAMES = new Set(['trail', 'event']);
109
105
 
110
106
  const getTrailCalleeName = (node: AstNode): string | null => {
111
107
  if (node.type !== 'CallExpression') {
@@ -157,8 +153,8 @@ const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
157
153
  };
158
154
  };
159
155
 
160
- /** Check if a node is a call to `.implementation()` on some object. */
161
- export const isImplementationCall = (node: AstNode): boolean => {
156
+ /** Check if a node is a call to `.run()` on some object. */
157
+ export const isRunCall = (node: AstNode): boolean => {
162
158
  if (node.type !== 'CallExpression') {
163
159
  return false;
164
160
  }
@@ -175,7 +171,7 @@ export const isImplementationCall = (node: AstNode): boolean => {
175
171
  const prop = (callee as unknown as { property?: AstNode }).property;
176
172
  return (
177
173
  prop?.type === 'Identifier' &&
178
- (prop as unknown as { name: string }).name === 'implementation'
174
+ (prop as unknown as { name: string }).name === 'run'
179
175
  );
180
176
  };
181
177
 
@@ -123,7 +123,7 @@ const classifyImport = (
123
123
  */
124
124
  export const contextNoSurfaceTypes: WardenRule = {
125
125
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
126
- if (!/\b(?:trail|hike)\s*\(/.test(sourceCode)) {
126
+ if (!/\btrail\s*\(/.test(sourceCode)) {
127
127
  return [];
128
128
  }
129
129
 
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Finds implementations that return raw values instead of `Result`.
3
3
  *
4
- * Uses AST parsing to find `implementation:` bodies and check that
4
+ * Uses AST parsing to find `run:` bodies and check that
5
5
  * every return statement returns Result.ok(), Result.err(), ctx.follow(),
6
6
  * or a tracked Result-typed variable.
7
7
  */
8
8
 
9
9
  import {
10
- findImplementationBodies,
10
+ findRunBodies,
11
11
  findTrailDefinitions,
12
12
  offsetToLine,
13
13
  parse,
@@ -63,7 +63,7 @@ const isResultMemberCall = (callee: AstNode): boolean => {
63
63
  if (objName === 'ctx' && propName === 'follow') {
64
64
  return true;
65
65
  }
66
- return propName === 'implementation';
66
+ return propName === 'run';
67
67
  };
68
68
 
69
69
  // ---------------------------------------------------------------------------
@@ -158,6 +158,63 @@ const trackResultVariable = (node: AstNode, resultVars: Set<string>): void => {
158
158
  }
159
159
  };
160
160
 
161
+ // ---------------------------------------------------------------------------
162
+ // Shallow walk (stops at nested function boundaries)
163
+ // ---------------------------------------------------------------------------
164
+
165
+ const FUNCTION_BOUNDARY_TYPES = new Set([
166
+ 'ArrowFunctionExpression',
167
+ 'FunctionExpression',
168
+ 'FunctionDeclaration',
169
+ ]);
170
+
171
+ /** Check if a value is a function-boundary AST node that should not be recursed into. */
172
+ const isFunctionBoundary = (val: unknown): boolean =>
173
+ !!val &&
174
+ typeof val === 'object' &&
175
+ FUNCTION_BOUNDARY_TYPES.has((val as AstNode).type);
176
+
177
+ /** Recurse into a single AST property value, skipping function boundaries. */
178
+ const visitValue = (
179
+ val: unknown,
180
+ visit: (node: AstNode) => void,
181
+ recurse: (node: unknown, visit: (node: AstNode) => void) => void
182
+ ): void => {
183
+ if (Array.isArray(val)) {
184
+ for (const item of val) {
185
+ if (!isFunctionBoundary(item)) {
186
+ recurse(item, visit);
187
+ }
188
+ }
189
+ } else if (
190
+ val &&
191
+ typeof val === 'object' &&
192
+ (val as AstNode).type &&
193
+ !isFunctionBoundary(val)
194
+ ) {
195
+ recurse(val, visit);
196
+ }
197
+ };
198
+
199
+ /**
200
+ * Walk an AST node tree without recursing into nested function bodies.
201
+ *
202
+ * This ensures that return statements inside `.map()`, `.filter()`, `.then()`
203
+ * callbacks etc. are not mistakenly checked as implementation-level returns.
204
+ */
205
+ const walkShallow = (node: unknown, visit: (node: AstNode) => void): void => {
206
+ if (!node || typeof node !== 'object') {
207
+ return;
208
+ }
209
+ const n = node as AstNode;
210
+ if (n.type) {
211
+ visit(n);
212
+ }
213
+ for (const val of Object.values(n)) {
214
+ visitValue(val, visit, walkShallow);
215
+ }
216
+ };
217
+
161
218
  // ---------------------------------------------------------------------------
162
219
  // Return statement checking
163
220
  // ---------------------------------------------------------------------------
@@ -173,7 +230,7 @@ const checkReturnStatements = (
173
230
  ): void => {
174
231
  const resultVars = new Set<string>();
175
232
 
176
- walk(blockBody, (node) => {
233
+ walkShallow(blockBody, (node) => {
177
234
  if (node.type === 'VariableDeclarator') {
178
235
  trackResultVariable(node, resultVars);
179
236
  }
@@ -304,8 +361,8 @@ const checkAllDefinitions = (
304
361
  const helperNames = collectResultHelperNames(ast, sourceCode);
305
362
 
306
363
  for (const def of findTrailDefinitions(ast)) {
307
- const info = { id: def.id, label: def.kind === 'hike' ? 'Hike' : 'Trail' };
308
- for (const implValue of findImplementationBodies(def.config as AstNode)) {
364
+ const info = { id: def.id, label: 'Trail' };
365
+ for (const implValue of findRunBodies(def.config as AstNode)) {
309
366
  checkImplementation(
310
367
  implValue,
311
368
  info,
@@ -29,14 +29,7 @@ export { noThrowInDetourTarget } from './no-throw-in-detour-target.js';
29
29
  export { preferSchemaInference } from './prefer-schema-inference.js';
30
30
  export { validDescribeRefs } from './valid-describe-refs.js';
31
31
 
32
- /**
33
- * All built-in warden rules, keyed by rule name.
34
- *
35
- * Rules that duplicate validateTopo checks (follows-trails-exist,
36
- * no-recursive-follows, event-origins-exist, examples-match-schema,
37
- * require-output-schema) and follows-matches-calls (now covered by
38
- * testExamples follows coverage) have been removed.
39
- */
32
+ /** All built-in warden rules, keyed by rule name. */
40
33
  export const wardenRules: ReadonlyMap<string, WardenRule> = new Map<
41
34
  string,
42
35
  WardenRule
@@ -1,14 +1,15 @@
1
1
  /**
2
- * Detects hike implementations that call `.implementation()` directly.
2
+ * Detects trail implementations with `follow` that call `.run()` directly.
3
3
  *
4
- * Uses AST parsing to find hike definition bodies and check for
5
- * `.implementation()` call expressions.
4
+ * Uses AST parsing to find trail definitions that declare `follow` and check for
5
+ * `.run()` call expressions in their bodies.
6
6
  */
7
7
 
8
8
  import {
9
- findImplementationBodies,
9
+ findConfigProperty,
10
+ findRunBodies,
10
11
  findTrailDefinitions,
11
- isImplementationCall,
12
+ isRunCall,
12
13
  offsetToLine,
13
14
  parse,
14
15
  walk,
@@ -22,20 +23,20 @@ interface AstNode {
22
23
  readonly [key: string]: unknown;
23
24
  }
24
25
 
25
- const findImplCallsInHike = (
26
+ const findImplCallsInTrailWithFollow = (
26
27
  def: { readonly config: AstNode },
27
28
  filePath: string,
28
29
  sourceCode: string,
29
30
  diagnostics: WardenDiagnostic[]
30
31
  ): void => {
31
- for (const body of findImplementationBodies(def.config as AstNode)) {
32
+ for (const body of findRunBodies(def.config as AstNode)) {
32
33
  walk(body, (node) => {
33
- if (isImplementationCall(node as AstNode)) {
34
+ if (isRunCall(node as AstNode)) {
34
35
  diagnostics.push({
35
36
  filePath,
36
37
  line: offsetToLine(sourceCode, node.start),
37
38
  message:
38
- 'Use ctx.follow("trailId", input) instead of direct .implementation() calls. ctx.follow() validates input and propagates tracing.',
39
+ 'Use ctx.follow("trailId", input) instead of direct .run() calls. ctx.follow() validates input and propagates tracing.',
39
40
  rule: 'no-direct-impl-in-route',
40
41
  severity: 'warn',
41
42
  });
@@ -44,12 +45,15 @@ const findImplCallsInHike = (
44
45
  }
45
46
  };
46
47
 
48
+ const hasFollowProperty = (config: AstNode): boolean =>
49
+ findConfigProperty(config as AstNode, 'follow') !== null;
50
+
47
51
  /**
48
- * Detects routes that call another trail's `.implementation()` directly.
52
+ * Detects trails with `follow` that call another trail's `.run()` directly.
49
53
  */
50
54
  export const noDirectImplInRoute: WardenRule = {
51
55
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
52
- if (!/\bhike\s*\(/.test(sourceCode)) {
56
+ if (!/\btrail\s*\(/.test(sourceCode)) {
53
57
  return [];
54
58
  }
55
59
 
@@ -59,18 +63,18 @@ export const noDirectImplInRoute: WardenRule = {
59
63
  }
60
64
 
61
65
  const diagnostics: WardenDiagnostic[] = [];
62
- const hikeDefs = findTrailDefinitions(ast as AstNode).filter(
63
- (d) => d.kind === 'hike'
66
+ const followDefs = findTrailDefinitions(ast as AstNode).filter((d) =>
67
+ hasFollowProperty(d.config as AstNode)
64
68
  );
65
69
 
66
- for (const def of hikeDefs) {
67
- findImplCallsInHike(def, filePath, sourceCode, diagnostics);
70
+ for (const def of followDefs) {
71
+ findImplCallsInTrailWithFollow(def, filePath, sourceCode, diagnostics);
68
72
  }
69
73
 
70
74
  return diagnostics;
71
75
  },
72
76
  description:
73
- 'Prefer ctx.follow() over direct .implementation() calls in route bodies.',
77
+ 'Prefer ctx.follow() over direct .run() calls in trail bodies with follow.',
74
78
  name: 'no-direct-impl-in-route',
75
79
 
76
80
  severity: 'warn',
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Flags direct `.implementation()` calls in application code.
2
+ * Flags direct `.run()` calls in application code.
3
3
  *
4
- * Uses AST parsing to find `.implementation()` call expressions,
4
+ * Uses AST parsing to find `.run()` call expressions,
5
5
  * ignoring occurrences in strings and comments.
6
6
  */
7
7
 
8
- import { isImplementationCall, offsetToLine, parse, walk } from './ast.js';
8
+ import { isRunCall, offsetToLine, parse, walk } from './ast.js';
9
9
  import { isFrameworkInternalFile, isTestFile } from './scan.js';
10
10
  import type { WardenDiagnostic, WardenRule } from './types.js';
11
11
 
12
12
  /**
13
- * Flags direct `.implementation()` calls in application code.
13
+ * Flags direct `.run()` calls in application code.
14
14
  */
15
15
  export const noDirectImplementationCall: WardenRule = {
16
16
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
@@ -26,12 +26,12 @@ export const noDirectImplementationCall: WardenRule = {
26
26
  const diagnostics: WardenDiagnostic[] = [];
27
27
 
28
28
  walk(ast, (node) => {
29
- if (isImplementationCall(node)) {
29
+ if (isRunCall(node)) {
30
30
  diagnostics.push({
31
31
  filePath,
32
32
  line: offsetToLine(sourceCode, node.start),
33
33
  message:
34
- 'Use ctx.follow("trailId", input) instead of direct .implementation() calls. Direct implementation access bypasses validation, tracing, and layers.',
34
+ 'Use ctx.follow("trailId", input) instead of direct .run() calls. Direct implementation access bypasses validation, tracing, and layers.',
35
35
  rule: 'no-direct-implementation-call',
36
36
  severity: 'warn',
37
37
  });
@@ -41,7 +41,7 @@ export const noDirectImplementationCall: WardenRule = {
41
41
  return diagnostics;
42
42
  },
43
43
  description:
44
- 'Disallow direct .implementation() calls in application code. Use ctx.follow() instead.',
44
+ 'Disallow direct .run() calls in application code. Use ctx.follow() instead.',
45
45
  name: 'no-direct-implementation-call',
46
46
  severity: 'warn',
47
47
  };
@@ -7,10 +7,10 @@ import {
7
7
 
8
8
  const RESULT_ACCESS_PATTERN =
9
9
  /\.(?:isOk|isErr|match|map)\s*\(|\.(?:value|error)\b/;
10
- const IMPLEMENTATION_CALL_PATTERN = /\.implementation\s*\(/;
10
+ const IMPLEMENTATION_CALL_PATTERN = /\.run\s*\(/;
11
11
 
12
12
  const isAwaitedImplementationCall = (line: string): boolean => {
13
- const callIndex = line.indexOf('.implementation(');
13
+ const callIndex = line.indexOf('.run(');
14
14
  if (callIndex === -1) {
15
15
  return false;
16
16
  }
@@ -39,7 +39,7 @@ interface PendingCall {
39
39
  }
40
40
 
41
41
  const MISSING_AWAIT_MESSAGE =
42
- 'Missing await: .implementation() returns Promise<Result> after normalization. Use `const result = await trail.implementation(input, ctx)`.';
42
+ 'Missing await: .run() returns Promise<Result> after normalization. Use `const result = await trail.run(input, ctx)`.';
43
43
 
44
44
  const createMissingAwaitDiagnostic = (
45
45
  filePath: string,
@@ -140,7 +140,7 @@ const scanSourceCode = (
140
140
  };
141
141
 
142
142
  /**
143
- * Flags code that assumes `.implementation()` returns a synchronous result.
143
+ * Flags code that assumes `.run()` returns a synchronous result.
144
144
  */
145
145
  export const noSyncResultAssumption: WardenRule = {
146
146
  check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
@@ -150,7 +150,7 @@ export const noSyncResultAssumption: WardenRule = {
150
150
  return scanSourceCode(stripQuotedContent(sourceCode), filePath);
151
151
  },
152
152
  description:
153
- 'Disallow treating .implementation() as synchronous after normalization. Always await the returned Promise<Result>.',
153
+ 'Disallow treating .run() as synchronous after normalization. Always await the returned Promise<Result>.',
154
154
  name: 'no-sync-result-assumption',
155
155
  severity: 'error',
156
156
  };
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import {
9
- findImplementationBodies,
9
+ findRunBodies,
10
10
  findTrailDefinitions,
11
11
  offsetToLine,
12
12
  parse,
@@ -66,7 +66,7 @@ const findThrowsInTargetedTrails = (
66
66
  continue;
67
67
  }
68
68
 
69
- for (const body of findImplementationBodies(def.config as AstNode)) {
69
+ for (const body of findRunBodies(def.config as AstNode)) {
70
70
  walk(body, (node) => {
71
71
  if (node.type === 'ThrowStatement') {
72
72
  diagnostics.push({
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Finds `throw` statements inside `implementation:` function bodies.
2
+ * Finds `throw` statements inside `run:` function bodies.
3
3
  *
4
4
  * Uses AST parsing for accurate detection — no false positives from
5
5
  * throw in comments, strings, or nested non-implementation functions.
6
6
  */
7
7
 
8
- import { findImplementationBodies, offsetToLine, parse, walk } from './ast.js';
8
+ import { findRunBodies, offsetToLine, parse, walk } from './ast.js';
9
9
  import type { WardenDiagnostic, WardenRule } from './types.js';
10
10
 
11
11
  export const noThrowInImplementation: WardenRule = {
@@ -17,7 +17,7 @@ export const noThrowInImplementation: WardenRule = {
17
17
 
18
18
  const diagnostics: WardenDiagnostic[] = [];
19
19
 
20
- for (const body of findImplementationBodies(ast)) {
20
+ for (const body of findRunBodies(ast)) {
21
21
  walk(body, (node) => {
22
22
  if (node.type === 'ThrowStatement') {
23
23
  diagnostics.push({