@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.
Files changed (153) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +159 -0
  3. package/README.md +57 -77
  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/index.d.ts +4 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/rules/ast.d.ts +15 -8
  12. package/dist/rules/ast.d.ts.map +1 -1
  13. package/dist/rules/ast.js +99 -44
  14. package/dist/rules/ast.js.map +1 -1
  15. package/dist/rules/context-no-surface-types.js +1 -1
  16. package/dist/rules/context-no-surface-types.js.map +1 -1
  17. package/dist/rules/follow-declarations.d.ts +13 -0
  18. package/dist/rules/follow-declarations.d.ts.map +1 -0
  19. package/dist/rules/follow-declarations.js +264 -0
  20. package/dist/rules/follow-declarations.js.map +1 -0
  21. package/dist/rules/implementation-returns-result.d.ts +1 -1
  22. package/dist/rules/implementation-returns-result.d.ts.map +1 -1
  23. package/dist/rules/implementation-returns-result.js +52 -6
  24. package/dist/rules/implementation-returns-result.js.map +1 -1
  25. package/dist/rules/index.d.ts +2 -8
  26. package/dist/rules/index.d.ts.map +1 -1
  27. package/dist/rules/index.js +4 -8
  28. package/dist/rules/index.js.map +1 -1
  29. package/dist/rules/no-direct-impl-in-route.d.ts +4 -4
  30. package/dist/rules/no-direct-impl-in-route.d.ts.map +1 -1
  31. package/dist/rules/no-direct-impl-in-route.js +15 -14
  32. package/dist/rules/no-direct-impl-in-route.js.map +1 -1
  33. package/dist/rules/no-direct-implementation-call.d.ts +3 -3
  34. package/dist/rules/no-direct-implementation-call.js +7 -7
  35. package/dist/rules/no-direct-implementation-call.js.map +1 -1
  36. package/dist/rules/no-sync-result-assumption.d.ts +1 -1
  37. package/dist/rules/no-sync-result-assumption.js +5 -5
  38. package/dist/rules/no-sync-result-assumption.js.map +1 -1
  39. package/dist/rules/no-throw-in-detour-target.js +2 -2
  40. package/dist/rules/no-throw-in-detour-target.js.map +1 -1
  41. package/dist/rules/no-throw-in-implementation.d.ts +1 -1
  42. package/dist/rules/no-throw-in-implementation.js +3 -3
  43. package/dist/rules/no-throw-in-implementation.js.map +1 -1
  44. package/dist/rules/specs.d.ts +1 -1
  45. package/dist/rules/specs.d.ts.map +1 -1
  46. package/dist/rules/specs.js +2 -2
  47. package/dist/rules/specs.js.map +1 -1
  48. package/dist/trails/context-no-surface-types.trail.d.ts +13 -0
  49. package/dist/trails/context-no-surface-types.trail.d.ts.map +1 -0
  50. package/dist/trails/context-no-surface-types.trail.js +21 -0
  51. package/dist/trails/context-no-surface-types.trail.js.map +1 -0
  52. package/dist/trails/follow-declarations.trail.d.ts +13 -0
  53. package/dist/trails/follow-declarations.trail.d.ts.map +1 -0
  54. package/dist/trails/follow-declarations.trail.js +22 -0
  55. package/dist/trails/follow-declarations.trail.js.map +1 -0
  56. package/dist/trails/implementation-returns-result.trail.d.ts +13 -0
  57. package/dist/trails/implementation-returns-result.trail.d.ts.map +1 -0
  58. package/dist/trails/implementation-returns-result.trail.js +20 -0
  59. package/dist/trails/implementation-returns-result.trail.js.map +1 -0
  60. package/dist/trails/index.d.ts +14 -0
  61. package/dist/trails/index.d.ts.map +1 -0
  62. package/dist/trails/index.js +13 -0
  63. package/dist/trails/index.js.map +1 -0
  64. package/dist/trails/no-direct-impl-in-route.trail.d.ts +13 -0
  65. package/dist/trails/no-direct-impl-in-route.trail.d.ts.map +1 -0
  66. package/dist/trails/no-direct-impl-in-route.trail.js +22 -0
  67. package/dist/trails/no-direct-impl-in-route.trail.js.map +1 -0
  68. package/dist/trails/no-direct-implementation-call.trail.d.ts +13 -0
  69. package/dist/trails/no-direct-implementation-call.trail.d.ts.map +1 -0
  70. package/dist/trails/no-direct-implementation-call.trail.js +16 -0
  71. package/dist/trails/no-direct-implementation-call.trail.js.map +1 -0
  72. package/dist/trails/no-sync-result-assumption.trail.d.ts +13 -0
  73. package/dist/trails/no-sync-result-assumption.trail.d.ts.map +1 -0
  74. package/dist/trails/no-sync-result-assumption.trail.js +19 -0
  75. package/dist/trails/no-sync-result-assumption.trail.js.map +1 -0
  76. package/dist/trails/no-throw-in-detour-target.trail.d.ts +14 -0
  77. package/dist/trails/no-throw-in-detour-target.trail.d.ts.map +1 -0
  78. package/dist/trails/no-throw-in-detour-target.trail.js +20 -0
  79. package/dist/trails/no-throw-in-detour-target.trail.js.map +1 -0
  80. package/dist/trails/no-throw-in-implementation.trail.d.ts +13 -0
  81. package/dist/trails/no-throw-in-implementation.trail.d.ts.map +1 -0
  82. package/dist/trails/no-throw-in-implementation.trail.js +20 -0
  83. package/dist/trails/no-throw-in-implementation.trail.js.map +1 -0
  84. package/dist/trails/prefer-schema-inference.trail.d.ts +13 -0
  85. package/dist/trails/prefer-schema-inference.trail.d.ts.map +1 -0
  86. package/dist/trails/prefer-schema-inference.trail.js +21 -0
  87. package/dist/trails/prefer-schema-inference.trail.js.map +1 -0
  88. package/dist/trails/run.d.ts +16 -0
  89. package/dist/trails/run.d.ts.map +1 -0
  90. package/dist/trails/run.js +30 -0
  91. package/dist/trails/run.js.map +1 -0
  92. package/dist/trails/schema.d.ts +52 -0
  93. package/dist/trails/schema.d.ts.map +1 -0
  94. package/dist/trails/schema.js +38 -0
  95. package/dist/trails/schema.js.map +1 -0
  96. package/dist/trails/topo.d.ts +3 -0
  97. package/dist/trails/topo.d.ts.map +1 -0
  98. package/dist/trails/topo.js +5 -0
  99. package/dist/trails/topo.js.map +1 -0
  100. package/dist/trails/valid-describe-refs.trail.d.ts +14 -0
  101. package/dist/trails/valid-describe-refs.trail.d.ts.map +1 -0
  102. package/dist/trails/valid-describe-refs.trail.js +18 -0
  103. package/dist/trails/valid-describe-refs.trail.js.map +1 -0
  104. package/dist/trails/valid-detour-refs.trail.d.ts +14 -0
  105. package/dist/trails/valid-detour-refs.trail.d.ts.map +1 -0
  106. package/dist/trails/valid-detour-refs.trail.js +24 -0
  107. package/dist/trails/valid-detour-refs.trail.js.map +1 -0
  108. package/dist/trails/wrap-rule.d.ts +29 -0
  109. package/dist/trails/wrap-rule.d.ts.map +1 -0
  110. package/dist/trails/wrap-rule.js +43 -0
  111. package/dist/trails/wrap-rule.js.map +1 -0
  112. package/package.json +5 -4
  113. package/src/__tests__/cli.test.ts +7 -7
  114. package/src/__tests__/drift.test.ts +1 -1
  115. package/src/__tests__/follow-declarations.test.ts +303 -0
  116. package/src/__tests__/implementation-returns-result.test.ts +60 -6
  117. package/src/__tests__/no-direct-implementation-call.test.ts +8 -8
  118. package/src/__tests__/no-sync-result-assumption.test.ts +6 -6
  119. package/src/__tests__/no-throw-in-detour-target.test.ts +6 -6
  120. package/src/__tests__/prefer-schema-inference.test.ts +4 -4
  121. package/src/__tests__/rules.test.ts +59 -20
  122. package/src/__tests__/trails.test.ts +19 -0
  123. package/src/__tests__/valid-describe-refs.test.ts +4 -4
  124. package/src/cli.ts +1 -4
  125. package/src/index.ts +21 -0
  126. package/src/rules/ast.ts +126 -57
  127. package/src/rules/context-no-surface-types.ts +1 -1
  128. package/src/rules/follow-declarations.ts +380 -0
  129. package/src/rules/implementation-returns-result.ts +63 -6
  130. package/src/rules/index.ts +4 -8
  131. package/src/rules/no-direct-impl-in-route.ts +20 -16
  132. package/src/rules/no-direct-implementation-call.ts +7 -7
  133. package/src/rules/no-sync-result-assumption.ts +5 -5
  134. package/src/rules/no-throw-in-detour-target.ts +2 -2
  135. package/src/rules/no-throw-in-implementation.ts +3 -3
  136. package/src/rules/specs.ts +5 -5
  137. package/src/trails/context-no-surface-types.trail.ts +21 -0
  138. package/src/trails/follow-declarations.trail.ts +22 -0
  139. package/src/trails/implementation-returns-result.trail.ts +20 -0
  140. package/src/trails/index.ts +14 -0
  141. package/src/trails/no-direct-impl-in-route.trail.ts +22 -0
  142. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  143. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  144. package/src/trails/no-throw-in-detour-target.trail.ts +20 -0
  145. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  146. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  147. package/src/trails/run.ts +40 -0
  148. package/src/trails/schema.ts +46 -0
  149. package/src/trails/topo.ts +6 -0
  150. package/src/trails/valid-describe-refs.trail.ts +18 -0
  151. package/src/trails/valid-detour-refs.trail.ts +24 -0
  152. package/src/trails/wrap-rule.ts +84 -0
  153. package/tsconfig.tsbuildinfo +1 -1
@@ -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({
@@ -18,7 +18,7 @@ export interface ObjectProperty extends ParsedEntry {
18
18
 
19
19
  export interface TrailLikeSpec {
20
20
  readonly id: string;
21
- readonly kind: 'hike' | 'trail';
21
+ readonly kind: 'event' | 'trail';
22
22
  readonly line: number;
23
23
  readonly properties: ReadonlyMap<string, ObjectProperty>;
24
24
  readonly specText: string;
@@ -31,7 +31,7 @@ export interface SchemaFieldInfo {
31
31
  readonly required: boolean;
32
32
  }
33
33
 
34
- const TRAIL_LIKE_PATTERN = /\b(trail|hike)\s*\(/g;
34
+ const TRAIL_LIKE_PATTERN = /\b(trail|event)\s*\(/g;
35
35
 
36
36
  const PROPERTY_PATTERN =
37
37
  /^(?:readonly\s+)?(?:(["'`])([^"'`]+)\1|([A-Za-z_$][\w$]*))\s*:\s*([\s\S]+)$/;
@@ -214,7 +214,7 @@ const resolveSpecId = (
214
214
 
215
215
  const buildTrailLikeSpec = (
216
216
  sourceCode: string,
217
- kind: 'hike' | 'trail',
217
+ kind: 'event' | 'trail',
218
218
  specArg: SplitEntry,
219
219
  specStart: number,
220
220
  id: string,
@@ -276,7 +276,7 @@ const resolveTrailLikeSpec = (
276
276
 
277
277
  const parseTrailLikeMatch = (
278
278
  sourceCode: string,
279
- kind: 'hike' | 'trail',
279
+ kind: 'event' | 'trail',
280
280
  callStart: number
281
281
  ): TrailLikeSpec | null => {
282
282
  const resolved = resolveTrailLikeSpec(sourceCode, callStart);
@@ -352,7 +352,7 @@ export const findTrailLikeSpecs = (
352
352
  continue;
353
353
  }
354
354
 
355
- const kind = match[1] === 'hike' ? 'hike' : 'trail';
355
+ const kind = match[1] === 'event' ? 'event' : 'trail';
356
356
  const spec = parseTrailLikeMatch(sourceCode, kind, callStart);
357
357
  if (spec !== null) {
358
358
  specs.push(spec);
@@ -0,0 +1,21 @@
1
+ import { contextNoSurfaceTypes } from '../rules/context-no-surface-types.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const contextNoSurfaceTypesTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `import { trail, Result } from "@ontrails/core";
11
+ trail("entity.show", {
12
+ run: async (input, ctx) => {
13
+ return Result.ok({ name: "test" });
14
+ }
15
+ })`,
16
+ },
17
+ name: 'Clean trail without surface imports',
18
+ },
19
+ ],
20
+ rule: contextNoSurfaceTypes,
21
+ });
@@ -0,0 +1,22 @@
1
+ import { followDeclarations } from '../rules/follow-declarations.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const followDeclarationsTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.onboard", {
11
+ follow: ["entity.create"],
12
+ run: async (input, ctx) => {
13
+ const result = await ctx.follow("entity.create", input);
14
+ return Result.ok(result);
15
+ }
16
+ })`,
17
+ },
18
+ name: 'Matched follow declarations and calls',
19
+ },
20
+ ],
21
+ rule: followDeclarations,
22
+ });
@@ -0,0 +1,20 @@
1
+ import { implementationReturnsResult } from '../rules/implementation-returns-result.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const implementationReturnsResultTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.show", {
11
+ run: async (input, ctx) => {
12
+ return Result.ok({ name: "test" });
13
+ }
14
+ })`,
15
+ },
16
+ name: 'Implementation returning Result.ok()',
17
+ },
18
+ ],
19
+ rule: implementationReturnsResult,
20
+ });
@@ -0,0 +1,14 @@
1
+ export { contextNoSurfaceTypesTrail } from './context-no-surface-types.trail.js';
2
+ export { followDeclarationsTrail } from './follow-declarations.trail.js';
3
+ export { implementationReturnsResultTrail } from './implementation-returns-result.trail.js';
4
+ export { noDirectImplInRouteTrail } from './no-direct-impl-in-route.trail.js';
5
+ export { noDirectImplementationCallTrail } from './no-direct-implementation-call.trail.js';
6
+ export { noSyncResultAssumptionTrail } from './no-sync-result-assumption.trail.js';
7
+ export { noThrowInDetourTargetTrail } from './no-throw-in-detour-target.trail.js';
8
+ export { noThrowInImplementationTrail } from './no-throw-in-implementation.trail.js';
9
+ export { preferSchemaInferenceTrail } from './prefer-schema-inference.trail.js';
10
+ export { validDescribeRefsTrail } from './valid-describe-refs.trail.js';
11
+ export { validDetourRefsTrail } from './valid-detour-refs.trail.js';
12
+
13
+ export { ruleInput, ruleOutput, diagnosticSchema } from './schema.js';
14
+ export type { RuleInput, RuleOutput } from './schema.js';
@@ -0,0 +1,22 @@
1
+ import { noDirectImplInRoute } from '../rules/no-direct-impl-in-route.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const noDirectImplInRouteTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.onboard", {
11
+ follow: ["entity.create"],
12
+ run: async (input, ctx) => {
13
+ const result = await ctx.follow("entity.create", input);
14
+ return Result.ok(result);
15
+ }
16
+ })`,
17
+ },
18
+ name: 'Trail with follow using ctx.follow()',
19
+ },
20
+ ],
21
+ rule: noDirectImplInRoute,
22
+ });
@@ -0,0 +1,16 @@
1
+ import { noDirectImplementationCall } from '../rules/no-direct-implementation-call.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const noDirectImplementationCallTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `const data = await ctx.follow("entity.show", { id: "1" });`,
11
+ },
12
+ name: 'Clean code using ctx.follow instead of .run()',
13
+ },
14
+ ],
15
+ rule: noDirectImplementationCall,
16
+ });
@@ -0,0 +1,19 @@
1
+ import { noSyncResultAssumption } from '../rules/no-sync-result-assumption.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const noSyncResultAssumptionTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `const result = await myTrail.run(input, ctx);
11
+ if (result.isOk()) {
12
+ console.log(result.value);
13
+ }`,
14
+ },
15
+ name: 'Properly awaited .run() call',
16
+ },
17
+ ],
18
+ rule: noSyncResultAssumption,
19
+ });
@@ -0,0 +1,20 @@
1
+ import { noThrowInDetourTarget } from '../rules/no-throw-in-detour-target.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const noThrowInDetourTargetTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.fallback", {
11
+ run: async (input, ctx) => {
12
+ return Result.ok({ recovered: true });
13
+ }
14
+ })`,
15
+ },
16
+ name: 'Detour target without throw',
17
+ },
18
+ ],
19
+ rule: noThrowInDetourTarget,
20
+ });
@@ -0,0 +1,20 @@
1
+ import { noThrowInImplementation } from '../rules/no-throw-in-implementation.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const noThrowInImplementationTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.show", {
11
+ run: async (input, ctx) => {
12
+ return Result.ok({ name: "test" });
13
+ }
14
+ })`,
15
+ },
16
+ name: 'Clean implementation without throw',
17
+ },
18
+ ],
19
+ rule: noThrowInImplementation,
20
+ });
@@ -0,0 +1,21 @@
1
+ import { preferSchemaInference } from '../rules/prefer-schema-inference.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const preferSchemaInferenceTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.show", {
11
+ input: z.object({ name: z.string() }),
12
+ run: async (input, ctx) => {
13
+ return Result.ok({ name: input.name });
14
+ }
15
+ })`,
16
+ },
17
+ name: 'Trail without redundant field overrides',
18
+ },
19
+ ],
20
+ rule: preferSchemaInference,
21
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Run all warden rule trails against a single source file.
3
+ *
4
+ * Returns a flat array of diagnostics from every rule.
5
+ */
6
+
7
+ import { dispatch } from '@ontrails/core';
8
+
9
+ import type { WardenDiagnostic } from '../rules/types.js';
10
+ import type { RuleOutput } from './schema.js';
11
+ import { wardenTopo } from './topo.js';
12
+
13
+ /**
14
+ * Dispatch all warden rule trails for a given file and collect diagnostics.
15
+ *
16
+ * Each rule trail runs independently. Errors from individual trails are
17
+ * silently skipped so that one broken rule does not block the rest.
18
+ */
19
+ export const runWardenTrails = async (
20
+ filePath: string,
21
+ sourceCode: string,
22
+ options?: { readonly knownTrailIds?: readonly string[] }
23
+ ): Promise<readonly WardenDiagnostic[]> => {
24
+ const allDiagnostics: WardenDiagnostic[] = [];
25
+
26
+ for (const id of wardenTopo.ids()) {
27
+ const input = options?.knownTrailIds
28
+ ? { filePath, knownTrailIds: options.knownTrailIds, sourceCode }
29
+ : { filePath, sourceCode };
30
+ const result = await dispatch(wardenTopo, id, input);
31
+ if (result.isOk()) {
32
+ const { diagnostics } = result.value as RuleOutput;
33
+ for (const d of diagnostics) {
34
+ allDiagnostics.push(d);
35
+ }
36
+ }
37
+ }
38
+
39
+ return allDiagnostics;
40
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared Zod schemas for warden rule trails.
3
+ *
4
+ * Every rule trail shares the same input (source file) and output
5
+ * (array of diagnostics) shape.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+
10
+ /** A single diagnostic emitted by a warden rule trail. */
11
+ export const diagnosticSchema = z.object({
12
+ filePath: z.string().describe('File path that was analyzed'),
13
+ line: z.number().describe('1-based line number'),
14
+ message: z.string().describe('Human-readable diagnostic message'),
15
+ rule: z.string().describe('Rule name'),
16
+ severity: z.enum(['error', 'warn']).describe('Diagnostic severity'),
17
+ });
18
+
19
+ /** Input accepted by every warden rule trail. */
20
+ export const ruleInput = z.object({
21
+ filePath: z.string().describe('Path to the source file'),
22
+ sourceCode: z.string().describe('Source code content'),
23
+ });
24
+
25
+ /**
26
+ * Extended input for project-aware warden rule trails.
27
+ *
28
+ * Adds `knownTrailIds` so the caller can supply cross-file context and avoid
29
+ * false positives for detour targets or `@see` references defined in other
30
+ * files.
31
+ */
32
+ export const projectAwareRuleInput = ruleInput.extend({
33
+ knownTrailIds: z
34
+ .array(z.string())
35
+ .optional()
36
+ .describe('Trail IDs known across the project'),
37
+ });
38
+
39
+ /** Output returned by every warden rule trail. */
40
+ export const ruleOutput = z.object({
41
+ diagnostics: z.array(diagnosticSchema).describe('Diagnostics found'),
42
+ });
43
+
44
+ export type RuleInput = z.infer<typeof ruleInput>;
45
+ export type ProjectAwareRuleInput = z.infer<typeof projectAwareRuleInput>;
46
+ export type RuleOutput = z.infer<typeof ruleOutput>;
@@ -0,0 +1,6 @@
1
+ import { topo } from '@ontrails/core';
2
+
3
+ import * as rules from './index.js';
4
+
5
+ /** Topo collecting all warden rule trails. */
6
+ export const wardenTopo = topo('warden', rules);
@@ -0,0 +1,18 @@
1
+ import { validDescribeRefs } from '../rules/valid-describe-refs.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const validDescribeRefsTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `const schema = z.object({
11
+ name: z.string().describe("User display name"),
12
+ });`,
13
+ },
14
+ name: 'Describe without @see refs',
15
+ },
16
+ ],
17
+ rule: validDescribeRefs,
18
+ });
@@ -0,0 +1,24 @@
1
+ import { validDetourRefs } from '../rules/valid-detour-refs.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const validDetourRefsTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ knownTrailIds: ['entity.fallback', 'entity.show'],
11
+ sourceCode: `trail("entity.fallback", {
12
+ run: async (input, ctx) => Result.ok(data)
13
+ })
14
+
15
+ trail("entity.show", {
16
+ detours: [{ target: "entity.fallback" }],
17
+ run: async (input, ctx) => Result.ok(data)
18
+ })`,
19
+ },
20
+ name: 'Valid detour target reference',
21
+ },
22
+ ],
23
+ rule: validDetourRefs,
24
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Factory that wraps a WardenRule as a trail.
3
+ *
4
+ * Keeps each rule trail file minimal — just the import + examples.
5
+ */
6
+
7
+ import { trail, Result } from '@ontrails/core';
8
+ import type { Trail } from '@ontrails/core';
9
+
10
+ import type { ProjectAwareWardenRule, WardenRule } from '../rules/types.js';
11
+ import { projectAwareRuleInput, ruleInput, ruleOutput } from './schema.js';
12
+ import type { ProjectAwareRuleInput, RuleInput, RuleOutput } from './schema.js';
13
+
14
+ interface WrapRuleOptions {
15
+ /** The existing warden rule to wrap. */
16
+ readonly rule: WardenRule;
17
+ /** Trail examples for testing and documentation. */
18
+ readonly examples: Trail<RuleInput, RuleOutput>['examples'];
19
+ }
20
+
21
+ interface WrapProjectAwareRuleOptions {
22
+ /** The existing project-aware warden rule to wrap. */
23
+ readonly rule: ProjectAwareWardenRule;
24
+ /** Trail examples for testing and documentation. */
25
+ readonly examples: Trail<ProjectAwareRuleInput, RuleOutput>['examples'];
26
+ }
27
+
28
+ /**
29
+ * Wrap an existing `WardenRule` as a trail with typed input/output.
30
+ *
31
+ * The trail ID follows the pattern `warden.rule.<rule-name>`.
32
+ */
33
+ export function wrapRule(
34
+ options: WrapProjectAwareRuleOptions
35
+ ): Trail<ProjectAwareRuleInput, RuleOutput>;
36
+ export function wrapRule(
37
+ options: WrapRuleOptions
38
+ ): Trail<RuleInput, RuleOutput>;
39
+ export function wrapRule(
40
+ options: WrapRuleOptions | WrapProjectAwareRuleOptions
41
+ ): Trail<RuleInput, RuleOutput> | Trail<ProjectAwareRuleInput, RuleOutput> {
42
+ const { rule, examples } = options;
43
+ const isProjectAware = 'checkWithContext' in rule;
44
+
45
+ if (isProjectAware) {
46
+ const projectAwareRule = rule as ProjectAwareWardenRule;
47
+ return trail(`warden.rule.${rule.name}`, {
48
+ description: rule.description,
49
+ examples: examples as Trail<
50
+ ProjectAwareRuleInput,
51
+ RuleOutput
52
+ >['examples'],
53
+ input: projectAwareRuleInput,
54
+ intent: 'read',
55
+ metadata: { category: 'governance', severity: rule.severity },
56
+ output: ruleOutput,
57
+ run: (input: ProjectAwareRuleInput) => {
58
+ const diagnostics = projectAwareRule.checkWithContext(
59
+ input.sourceCode,
60
+ input.filePath,
61
+ {
62
+ knownTrailIds: input.knownTrailIds
63
+ ? new Set(input.knownTrailIds)
64
+ : new Set<string>(),
65
+ }
66
+ );
67
+ return Result.ok({ diagnostics: [...diagnostics] });
68
+ },
69
+ });
70
+ }
71
+
72
+ return trail(`warden.rule.${rule.name}`, {
73
+ description: rule.description,
74
+ examples: examples as Trail<RuleInput, RuleOutput>['examples'],
75
+ input: ruleInput,
76
+ intent: 'read',
77
+ metadata: { category: 'governance', severity: rule.severity },
78
+ output: ruleOutput,
79
+ run: (input: RuleInput) => {
80
+ const diagnostics = rule.check(input.sourceCode, input.filePath);
81
+ return Result.ok({ diagnostics: [...diagnostics] });
82
+ },
83
+ });
84
+ }
@@ -1 +1 @@
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"}
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/follow-declarations.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","./src/trails/context-no-surface-types.trail.ts","./src/trails/follow-declarations.trail.ts","./src/trails/implementation-returns-result.trail.ts","./src/trails/index.ts","./src/trails/no-direct-impl-in-route.trail.ts","./src/trails/no-direct-implementation-call.trail.ts","./src/trails/no-sync-result-assumption.trail.ts","./src/trails/no-throw-in-detour-target.trail.ts","./src/trails/no-throw-in-implementation.trail.ts","./src/trails/prefer-schema-inference.trail.ts","./src/trails/run.ts","./src/trails/schema.ts","./src/trails/topo.ts","./src/trails/valid-describe-refs.trail.ts","./src/trails/valid-detour-refs.trail.ts","./src/trails/wrap-rule.ts"],"version":"5.9.3"}