@ontrails/warden 1.0.0-beta.0 → 1.0.0-beta.10
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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +159 -0
- package/README.md +57 -77
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/rules/ast.d.ts +15 -8
- package/dist/rules/ast.d.ts.map +1 -1
- package/dist/rules/ast.js +99 -44
- package/dist/rules/ast.js.map +1 -1
- package/dist/rules/context-no-surface-types.js +1 -1
- package/dist/rules/context-no-surface-types.js.map +1 -1
- package/dist/rules/follow-declarations.d.ts +13 -0
- package/dist/rules/follow-declarations.d.ts.map +1 -0
- package/dist/rules/follow-declarations.js +264 -0
- package/dist/rules/follow-declarations.js.map +1 -0
- package/dist/rules/implementation-returns-result.d.ts +1 -1
- package/dist/rules/implementation-returns-result.d.ts.map +1 -1
- package/dist/rules/implementation-returns-result.js +52 -6
- package/dist/rules/implementation-returns-result.js.map +1 -1
- package/dist/rules/index.d.ts +2 -8
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +4 -8
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +4 -4
- package/dist/rules/no-direct-impl-in-route.d.ts.map +1 -1
- package/dist/rules/no-direct-impl-in-route.js +15 -14
- package/dist/rules/no-direct-impl-in-route.js.map +1 -1
- package/dist/rules/no-direct-implementation-call.d.ts +3 -3
- package/dist/rules/no-direct-implementation-call.js +7 -7
- package/dist/rules/no-direct-implementation-call.js.map +1 -1
- package/dist/rules/no-sync-result-assumption.d.ts +1 -1
- package/dist/rules/no-sync-result-assumption.js +5 -5
- package/dist/rules/no-sync-result-assumption.js.map +1 -1
- package/dist/rules/no-throw-in-detour-target.js +2 -2
- package/dist/rules/no-throw-in-detour-target.js.map +1 -1
- package/dist/rules/no-throw-in-implementation.d.ts +1 -1
- package/dist/rules/no-throw-in-implementation.js +3 -3
- package/dist/rules/no-throw-in-implementation.js.map +1 -1
- package/dist/rules/specs.d.ts +1 -1
- package/dist/rules/specs.d.ts.map +1 -1
- package/dist/rules/specs.js +2 -2
- package/dist/rules/specs.js.map +1 -1
- package/dist/trails/context-no-surface-types.trail.d.ts +13 -0
- package/dist/trails/context-no-surface-types.trail.d.ts.map +1 -0
- package/dist/trails/context-no-surface-types.trail.js +21 -0
- package/dist/trails/context-no-surface-types.trail.js.map +1 -0
- package/dist/trails/follow-declarations.trail.d.ts +13 -0
- package/dist/trails/follow-declarations.trail.d.ts.map +1 -0
- package/dist/trails/follow-declarations.trail.js +22 -0
- package/dist/trails/follow-declarations.trail.js.map +1 -0
- package/dist/trails/implementation-returns-result.trail.d.ts +13 -0
- package/dist/trails/implementation-returns-result.trail.d.ts.map +1 -0
- package/dist/trails/implementation-returns-result.trail.js +20 -0
- package/dist/trails/implementation-returns-result.trail.js.map +1 -0
- package/dist/trails/index.d.ts +14 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +13 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/no-direct-impl-in-route.trail.d.ts +13 -0
- package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +1 -0
- package/dist/trails/no-direct-impl-in-route.trail.js +22 -0
- package/dist/trails/no-direct-impl-in-route.trail.js.map +1 -0
- package/dist/trails/no-direct-implementation-call.trail.d.ts +13 -0
- package/dist/trails/no-direct-implementation-call.trail.d.ts.map +1 -0
- package/dist/trails/no-direct-implementation-call.trail.js +16 -0
- package/dist/trails/no-direct-implementation-call.trail.js.map +1 -0
- package/dist/trails/no-sync-result-assumption.trail.d.ts +13 -0
- package/dist/trails/no-sync-result-assumption.trail.d.ts.map +1 -0
- package/dist/trails/no-sync-result-assumption.trail.js +19 -0
- package/dist/trails/no-sync-result-assumption.trail.js.map +1 -0
- package/dist/trails/no-throw-in-detour-target.trail.d.ts +14 -0
- package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +1 -0
- package/dist/trails/no-throw-in-detour-target.trail.js +20 -0
- package/dist/trails/no-throw-in-detour-target.trail.js.map +1 -0
- package/dist/trails/no-throw-in-implementation.trail.d.ts +13 -0
- package/dist/trails/no-throw-in-implementation.trail.d.ts.map +1 -0
- package/dist/trails/no-throw-in-implementation.trail.js +20 -0
- package/dist/trails/no-throw-in-implementation.trail.js.map +1 -0
- package/dist/trails/prefer-schema-inference.trail.d.ts +13 -0
- package/dist/trails/prefer-schema-inference.trail.d.ts.map +1 -0
- package/dist/trails/prefer-schema-inference.trail.js +21 -0
- package/dist/trails/prefer-schema-inference.trail.js.map +1 -0
- package/dist/trails/run.d.ts +16 -0
- package/dist/trails/run.d.ts.map +1 -0
- package/dist/trails/run.js +30 -0
- package/dist/trails/run.js.map +1 -0
- package/dist/trails/schema.d.ts +52 -0
- package/dist/trails/schema.d.ts.map +1 -0
- package/dist/trails/schema.js +38 -0
- package/dist/trails/schema.js.map +1 -0
- package/dist/trails/topo.d.ts +3 -0
- package/dist/trails/topo.d.ts.map +1 -0
- package/dist/trails/topo.js +5 -0
- package/dist/trails/topo.js.map +1 -0
- package/dist/trails/valid-describe-refs.trail.d.ts +14 -0
- package/dist/trails/valid-describe-refs.trail.d.ts.map +1 -0
- package/dist/trails/valid-describe-refs.trail.js +18 -0
- package/dist/trails/valid-describe-refs.trail.js.map +1 -0
- package/dist/trails/valid-detour-refs.trail.d.ts +14 -0
- package/dist/trails/valid-detour-refs.trail.d.ts.map +1 -0
- package/dist/trails/valid-detour-refs.trail.js +24 -0
- package/dist/trails/valid-detour-refs.trail.js.map +1 -0
- package/dist/trails/wrap-rule.d.ts +29 -0
- package/dist/trails/wrap-rule.d.ts.map +1 -0
- package/dist/trails/wrap-rule.js +43 -0
- package/dist/trails/wrap-rule.js.map +1 -0
- package/package.json +5 -4
- package/src/__tests__/cli.test.ts +7 -7
- package/src/__tests__/drift.test.ts +1 -1
- package/src/__tests__/follow-declarations.test.ts +303 -0
- package/src/__tests__/implementation-returns-result.test.ts +60 -6
- package/src/__tests__/no-direct-implementation-call.test.ts +8 -8
- package/src/__tests__/no-sync-result-assumption.test.ts +6 -6
- package/src/__tests__/no-throw-in-detour-target.test.ts +6 -6
- package/src/__tests__/prefer-schema-inference.test.ts +4 -4
- package/src/__tests__/rules.test.ts +59 -20
- package/src/__tests__/trails.test.ts +19 -0
- package/src/__tests__/valid-describe-refs.test.ts +4 -4
- package/src/cli.ts +1 -4
- package/src/index.ts +21 -0
- package/src/rules/ast.ts +126 -57
- package/src/rules/context-no-surface-types.ts +1 -1
- package/src/rules/follow-declarations.ts +380 -0
- package/src/rules/implementation-returns-result.ts +63 -6
- package/src/rules/index.ts +4 -8
- package/src/rules/no-direct-impl-in-route.ts +20 -16
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-sync-result-assumption.ts +5 -5
- package/src/rules/no-throw-in-detour-target.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +3 -3
- package/src/rules/specs.ts +5 -5
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/follow-declarations.trail.ts +22 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/index.ts +14 -0
- package/src/trails/no-direct-impl-in-route.trail.ts +22 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-target.trail.ts +20 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/run.ts +40 -0
- package/src/trails/schema.ts +46 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-refs.trail.ts +24 -0
- package/src/trails/wrap-rule.ts +84 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -9,11 +9,11 @@ describe('no-throw-in-detour-target', () => {
|
|
|
9
9
|
const code = `
|
|
10
10
|
trail("entity.show", {
|
|
11
11
|
detours: { NotFoundError: ["entity.fallback"] },
|
|
12
|
-
|
|
12
|
+
run: async (input, ctx) => Result.ok({ id: "123" })
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
trail("entity.fallback", {
|
|
16
|
-
|
|
16
|
+
run: async (input, ctx) => {
|
|
17
17
|
throw new Error("boom");
|
|
18
18
|
}
|
|
19
19
|
})`;
|
|
@@ -28,7 +28,7 @@ trail("entity.fallback", {
|
|
|
28
28
|
test('allows throw in implementations that are not detour targets', () => {
|
|
29
29
|
const code = `
|
|
30
30
|
trail("entity.show", {
|
|
31
|
-
|
|
31
|
+
run: async (input, ctx) => {
|
|
32
32
|
throw new Error("boom");
|
|
33
33
|
}
|
|
34
34
|
})`;
|
|
@@ -42,11 +42,11 @@ trail("entity.show", {
|
|
|
42
42
|
const code = `
|
|
43
43
|
trail("entity.show", {
|
|
44
44
|
detours: { NotFoundError: ["entity.fallback"] },
|
|
45
|
-
|
|
45
|
+
run: async (input, ctx) => Result.ok({ id: "123" })
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
trail("entity.fallback", {
|
|
49
|
-
|
|
49
|
+
run: async (input, ctx) => { throw new Error("boom"); }
|
|
50
50
|
})`;
|
|
51
51
|
|
|
52
52
|
const diagnostics = noThrowInDetourTarget.check(code, TEST_FILE);
|
|
@@ -58,7 +58,7 @@ trail("entity.fallback", {
|
|
|
58
58
|
test('uses project context when the detour target is defined in another file', () => {
|
|
59
59
|
const code = `
|
|
60
60
|
trail("entity.fallback", {
|
|
61
|
-
|
|
61
|
+
run: async (input, ctx) => {
|
|
62
62
|
throw new Error("boom");
|
|
63
63
|
}
|
|
64
64
|
})`;
|
|
@@ -10,7 +10,7 @@ trail("entity.show", {
|
|
|
10
10
|
fields: {
|
|
11
11
|
firstName: { label: "First Name" },
|
|
12
12
|
},
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
run: async (input, ctx) => Result.ok(data)
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
trail("entity.show", {
|
|
127
127
|
detours: [{ target: "entity.edit" }],
|
|
128
|
-
|
|
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
|
-
|
|
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 .
|
|
181
|
+
test('warns on direct .run() call in trail with follow', () => {
|
|
155
182
|
const code = `
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const result = await entityCreate.
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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.
|
|
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
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { testAll } from '@ontrails/testing';
|
|
3
|
+
|
|
4
|
+
import { wardenTopo } from '../trails/topo.js';
|
|
5
|
+
|
|
6
|
+
// oxlint-disable-next-line jest/require-hook -- testAll generates describe/test blocks, not setup code
|
|
7
|
+
testAll(wardenTopo);
|
|
8
|
+
|
|
9
|
+
describe('wardenTopo', () => {
|
|
10
|
+
test('contains all 11 rule trails', () => {
|
|
11
|
+
expect(wardenTopo.count).toBe(11);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('all trail IDs follow warden.rule.* naming', () => {
|
|
15
|
+
for (const id of wardenTopo.ids()) {
|
|
16
|
+
expect(id).toMatch(/^warden\.rule\./);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/index.ts
CHANGED
|
@@ -45,3 +45,24 @@ export {
|
|
|
45
45
|
// Drift detection
|
|
46
46
|
export type { DriftResult } from './drift.js';
|
|
47
47
|
export { checkDrift } from './drift.js';
|
|
48
|
+
|
|
49
|
+
// Trail layer
|
|
50
|
+
export { wardenTopo } from './trails/topo.js';
|
|
51
|
+
export { runWardenTrails } from './trails/run.js';
|
|
52
|
+
export {
|
|
53
|
+
contextNoSurfaceTypesTrail,
|
|
54
|
+
diagnosticSchema,
|
|
55
|
+
followDeclarationsTrail,
|
|
56
|
+
implementationReturnsResultTrail,
|
|
57
|
+
noDirectImplInRouteTrail,
|
|
58
|
+
noDirectImplementationCallTrail,
|
|
59
|
+
noSyncResultAssumptionTrail,
|
|
60
|
+
noThrowInDetourTargetTrail,
|
|
61
|
+
noThrowInImplementationTrail,
|
|
62
|
+
preferSchemaInferenceTrail,
|
|
63
|
+
ruleInput,
|
|
64
|
+
ruleOutput,
|
|
65
|
+
validDescribeRefsTrail,
|
|
66
|
+
validDetourRefsTrail,
|
|
67
|
+
} from './trails/index.js';
|
|
68
|
+
export type { RuleInput, RuleOutput } from './trails/index.js';
|
package/src/rules/ast.ts
CHANGED
|
@@ -74,38 +74,52 @@ export const offsetToLine = (sourceCode: string, offset: number): number => {
|
|
|
74
74
|
return line;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Config property extraction helpers
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Find a Property node by key name inside an ObjectExpression config. */
|
|
82
|
+
export const findConfigProperty = (
|
|
83
|
+
config: AstNode,
|
|
84
|
+
propertyName: string
|
|
85
|
+
): AstNode | null => {
|
|
86
|
+
if (config.type !== 'ObjectExpression') {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const properties = config['properties'] as readonly AstNode[] | undefined;
|
|
90
|
+
if (!properties) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
for (const prop of properties) {
|
|
94
|
+
if (prop.type === 'Property' && prop.key?.name === propertyName) {
|
|
95
|
+
return prop;
|
|
87
96
|
}
|
|
88
|
-
}
|
|
89
|
-
return
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
90
99
|
};
|
|
91
100
|
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Trail definition extraction
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
92
105
|
export interface TrailDefinition {
|
|
93
106
|
/** Trail ID string, e.g. "entity.show" */
|
|
94
107
|
readonly id: string;
|
|
95
|
-
/** "trail" or "
|
|
108
|
+
/** "trail" or "event" */
|
|
96
109
|
readonly kind: string;
|
|
97
|
-
/** The config object argument (second arg to trail
|
|
110
|
+
/** The config object argument (second arg to trail() call) */
|
|
98
111
|
readonly config: AstNode;
|
|
99
112
|
/** Start offset of the call expression */
|
|
100
113
|
readonly start: number;
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
/**
|
|
104
|
-
* Find all `trail("id", { ... })
|
|
117
|
+
* Find all `trail("id", { ... })`, `trail({ id: "x", ... })`, and
|
|
118
|
+
* `event("id", { ... })` call sites.
|
|
105
119
|
*
|
|
106
120
|
* Returns the trail ID, kind, and config object node for each definition.
|
|
107
121
|
*/
|
|
108
|
-
const TRAIL_CALLEE_NAMES = new Set(['trail', '
|
|
122
|
+
const TRAIL_CALLEE_NAMES = new Set(['trail', 'event']);
|
|
109
123
|
|
|
110
124
|
const getTrailCalleeName = (node: AstNode): string | null => {
|
|
111
125
|
if (node.type !== 'CallExpression') {
|
|
@@ -119,18 +133,49 @@ const getTrailCalleeName = (node: AstNode): string | null => {
|
|
|
119
133
|
return name && TRAIL_CALLEE_NAMES.has(name) ? name : null;
|
|
120
134
|
};
|
|
121
135
|
|
|
136
|
+
/** Extract args from a trail() call, handling both two-arg and single-object forms. */
|
|
122
137
|
const extractTrailArgs = (
|
|
123
138
|
node: AstNode
|
|
124
|
-
): { idArg: AstNode; configArg: AstNode } | null => {
|
|
139
|
+
): { idArg: AstNode | null; configArg: AstNode } | null => {
|
|
125
140
|
const args = node['arguments'] as readonly AstNode[] | undefined;
|
|
126
|
-
if (!args || args.length
|
|
141
|
+
if (!args || args.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const [firstArg, secondArg] = args;
|
|
146
|
+
if (!firstArg) {
|
|
127
147
|
return null;
|
|
128
148
|
}
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
|
|
150
|
+
// Two-arg form: trail('id', { ... })
|
|
151
|
+
if (secondArg && firstArg.type !== 'ObjectExpression') {
|
|
152
|
+
return { configArg: secondArg, idArg: firstArg };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Single-object form: trail({ id: 'x', ... })
|
|
156
|
+
return firstArg.type === 'ObjectExpression'
|
|
157
|
+
? { configArg: firstArg, idArg: null }
|
|
158
|
+
: null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Extract the string value from an `id` property inside a config ObjectExpression. */
|
|
162
|
+
const extractIdFromConfig = (config: AstNode): string | null => {
|
|
163
|
+
const idProp = findConfigProperty(config, 'id');
|
|
164
|
+
if (!idProp || !idProp.value) {
|
|
131
165
|
return null;
|
|
132
166
|
}
|
|
133
|
-
|
|
167
|
+
const val = (idProp.value as unknown as { value?: unknown }).value;
|
|
168
|
+
return typeof val === 'string' ? val : null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const extractTrailId = (trailArgs: {
|
|
172
|
+
idArg: AstNode | null;
|
|
173
|
+
configArg: AstNode;
|
|
174
|
+
}): string | null => {
|
|
175
|
+
if (trailArgs.idArg) {
|
|
176
|
+
return (trailArgs.idArg as unknown as { value?: string }).value ?? null;
|
|
177
|
+
}
|
|
178
|
+
return extractIdFromConfig(trailArgs.configArg);
|
|
134
179
|
};
|
|
135
180
|
|
|
136
181
|
const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
|
|
@@ -144,7 +189,7 @@ const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
|
|
|
144
189
|
return null;
|
|
145
190
|
}
|
|
146
191
|
|
|
147
|
-
const trailId = (trailArgs
|
|
192
|
+
const trailId = extractTrailId(trailArgs);
|
|
148
193
|
if (!trailId) {
|
|
149
194
|
return null;
|
|
150
195
|
}
|
|
@@ -157,28 +202,6 @@ const extractTrailDefinition = (node: AstNode): TrailDefinition | null => {
|
|
|
157
202
|
};
|
|
158
203
|
};
|
|
159
204
|
|
|
160
|
-
/** Check if a node is a call to `.implementation()` on some object. */
|
|
161
|
-
export const isImplementationCall = (node: AstNode): boolean => {
|
|
162
|
-
if (node.type !== 'CallExpression') {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
const callee = node['callee'] as AstNode | undefined;
|
|
166
|
-
if (!callee) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
if (
|
|
170
|
-
callee.type !== 'StaticMemberExpression' &&
|
|
171
|
-
callee.type !== 'MemberExpression'
|
|
172
|
-
) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
const prop = (callee as unknown as { property?: AstNode }).property;
|
|
176
|
-
return (
|
|
177
|
-
prop?.type === 'Identifier' &&
|
|
178
|
-
(prop as unknown as { name: string }).name === 'implementation'
|
|
179
|
-
);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
205
|
export const findTrailDefinitions = (ast: AstNode): TrailDefinition[] => {
|
|
183
206
|
const definitions: TrailDefinition[] = [];
|
|
184
207
|
|
|
@@ -193,25 +216,71 @@ export const findTrailDefinitions = (ast: AstNode): TrailDefinition[] => {
|
|
|
193
216
|
};
|
|
194
217
|
|
|
195
218
|
// ---------------------------------------------------------------------------
|
|
196
|
-
//
|
|
219
|
+
// Run body extraction
|
|
197
220
|
// ---------------------------------------------------------------------------
|
|
198
221
|
|
|
199
|
-
/**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract top-level `run:` property values from an ObjectExpression's direct properties.
|
|
224
|
+
*
|
|
225
|
+
* Does not recurse into nested objects, so `metadata: { run: ... }` is ignored.
|
|
226
|
+
*/
|
|
227
|
+
const extractRunFromConfig = (config: AstNode): AstNode[] => {
|
|
228
|
+
const bodies: AstNode[] = [];
|
|
207
229
|
const properties = config['properties'] as readonly AstNode[] | undefined;
|
|
208
230
|
if (!properties) {
|
|
209
|
-
return
|
|
231
|
+
return bodies;
|
|
210
232
|
}
|
|
211
233
|
for (const prop of properties) {
|
|
212
|
-
if (prop.type === 'Property' && prop.key?.name ===
|
|
213
|
-
|
|
234
|
+
if (prop.type === 'Property' && prop.key?.name === 'run' && prop.value) {
|
|
235
|
+
bodies.push(prop.value);
|
|
214
236
|
}
|
|
215
237
|
}
|
|
216
|
-
return
|
|
238
|
+
return bodies;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find `run:` property values.
|
|
243
|
+
*
|
|
244
|
+
* When given an ObjectExpression (trail config), returns only its direct `run:`
|
|
245
|
+
* properties. When given a full AST, finds trail definitions first and extracts
|
|
246
|
+
* `run:` from each config — in both cases ignoring nested `run:` properties
|
|
247
|
+
* (e.g. `metadata: { run: ... }`).
|
|
248
|
+
*/
|
|
249
|
+
export const findRunBodies = (node: AstNode): AstNode[] => {
|
|
250
|
+
if (node.type === 'ObjectExpression') {
|
|
251
|
+
return extractRunFromConfig(node);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Full AST — find trail definitions and extract run from their configs
|
|
255
|
+
const bodies: AstNode[] = [];
|
|
256
|
+
for (const def of findTrailDefinitions(node)) {
|
|
257
|
+
bodies.push(...extractRunFromConfig(def.config));
|
|
258
|
+
}
|
|
259
|
+
return bodies;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Misc helpers
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/** Check if a node is a call to `.run()` on some object. */
|
|
267
|
+
export const isRunCall = (node: AstNode): boolean => {
|
|
268
|
+
if (node.type !== 'CallExpression') {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const callee = node['callee'] as AstNode | undefined;
|
|
272
|
+
if (!callee) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
callee.type !== 'StaticMemberExpression' &&
|
|
277
|
+
callee.type !== 'MemberExpression'
|
|
278
|
+
) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const prop = (callee as unknown as { property?: AstNode }).property;
|
|
282
|
+
return (
|
|
283
|
+
prop?.type === 'Identifier' &&
|
|
284
|
+
(prop as unknown as { name: string }).name === 'run'
|
|
285
|
+
);
|
|
217
286
|
};
|
|
@@ -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 (!/\
|
|
126
|
+
if (!/\btrail\s*\(/.test(sourceCode)) {
|
|
127
127
|
return [];
|
|
128
128
|
}
|
|
129
129
|
|