@ontrails/warden 0.2.0

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 (188) hide show
  1. package/CHANGELOG.md +1556 -0
  2. package/README.md +200 -0
  3. package/bin/warden.ts +51 -0
  4. package/package.json +51 -0
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +2159 -0
  7. package/src/command.ts +1167 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +260 -0
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +178 -0
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +295 -0
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/regrade-history.ts +188 -0
  18. package/src/regrade-receipt.ts +484 -0
  19. package/src/resolve.ts +531 -0
  20. package/src/rules/activation-orphan.ts +97 -0
  21. package/src/rules/captured-kernel.ts +375 -0
  22. package/src/rules/circular-refs.ts +150 -0
  23. package/src/rules/cli-command-route-coherence.ts +177 -0
  24. package/src/rules/composes-declarations.ts +839 -0
  25. package/src/rules/context-no-surface-types.ts +214 -0
  26. package/src/rules/dead-internal-trail.ts +161 -0
  27. package/src/rules/dead-public-trail.ts +258 -0
  28. package/src/rules/draft-file-marking.ts +155 -0
  29. package/src/rules/draft-visible-debt.ts +83 -0
  30. package/src/rules/duplicate-exported-symbol.ts +172 -0
  31. package/src/rules/duplicate-public-contract.ts +137 -0
  32. package/src/rules/entity-exists.ts +254 -0
  33. package/src/rules/entity-ids.ts +15 -0
  34. package/src/rules/error-mapping-completeness.ts +290 -0
  35. package/src/rules/example-valid.ts +395 -0
  36. package/src/rules/fires-declarations.ts +740 -0
  37. package/src/rules/governed-symbol-residue.ts +535 -0
  38. package/src/rules/governed-vocabulary-permutation-watch.ts +77 -0
  39. package/src/rules/implementation-returns-result.ts +1733 -0
  40. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  41. package/src/rules/incomplete-crud.ts +583 -0
  42. package/src/rules/index.ts +328 -0
  43. package/src/rules/intent-propagation.ts +125 -0
  44. package/src/rules/layer-field-name-drift.ts +102 -0
  45. package/src/rules/library-render-coherence.ts +97 -0
  46. package/src/rules/metadata.ts +929 -0
  47. package/src/rules/missing-reconcile.ts +97 -0
  48. package/src/rules/missing-visibility.ts +111 -0
  49. package/src/rules/no-destructured-compose.ts +196 -0
  50. package/src/rules/no-dev-permit-in-source.ts +99 -0
  51. package/src/rules/no-direct-implementation-call.ts +52 -0
  52. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  53. package/src/rules/no-legacy-layer-imports.ts +211 -0
  54. package/src/rules/no-native-error-result.ts +118 -0
  55. package/src/rules/no-redundant-result-error-wrap.ts +382 -0
  56. package/src/rules/no-retired-cross-vocabulary.ts +203 -0
  57. package/src/rules/no-sync-result-assumption.ts +1199 -0
  58. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  59. package/src/rules/no-throw-in-implementation.ts +48 -0
  60. package/src/rules/no-top-level-surface.ts +371 -0
  61. package/src/rules/on-references-exist.ts +194 -0
  62. package/src/rules/orphaned-signal.ts +149 -0
  63. package/src/rules/owner-render-parity.ts +143 -0
  64. package/src/rules/permit-governance.ts +25 -0
  65. package/src/rules/prefer-schema-inference.ts +141 -0
  66. package/src/rules/public-export-example-coverage.ts +561 -0
  67. package/src/rules/public-internal-deep-imports.ts +454 -0
  68. package/src/rules/public-output-schema.ts +29 -0
  69. package/src/rules/public-union-output-discriminants.ts +150 -0
  70. package/src/rules/read-intent-fires.ts +188 -0
  71. package/src/rules/reference-exists.ts +97 -0
  72. package/src/rules/registry-names.ts +171 -0
  73. package/src/rules/resolved-import-boundary.ts +146 -0
  74. package/src/rules/resource-declarations.ts +697 -0
  75. package/src/rules/resource-exists.ts +181 -0
  76. package/src/rules/resource-id-grammar.ts +65 -0
  77. package/src/rules/resource-mock-coverage.ts +115 -0
  78. package/src/rules/retired-vocabulary.ts +1407 -0
  79. package/src/rules/scan.ts +59 -0
  80. package/src/rules/scheduled-destroy-intent.ts +44 -0
  81. package/src/rules/signal-graph-coaching.ts +220 -0
  82. package/src/rules/source/composition.ts +165 -0
  83. package/src/rules/source/drafts.ts +164 -0
  84. package/src/rules/source/entities.ts +618 -0
  85. package/src/rules/source/pragmas.ts +45 -0
  86. package/src/rules/source/resources.ts +64 -0
  87. package/src/rules/source/signals.ts +397 -0
  88. package/src/rules/source/stores.ts +310 -0
  89. package/src/rules/specs.ts +388 -0
  90. package/src/rules/static-resource-accessor-preference.ts +654 -0
  91. package/src/rules/structure.ts +234 -0
  92. package/src/rules/surface-overlay-coherence.ts +262 -0
  93. package/src/rules/surface-trailhead-coherence.ts +366 -0
  94. package/src/rules/trail-fork-coaching.ts +625 -0
  95. package/src/rules/trail-versioning-source.ts +1076 -0
  96. package/src/rules/trail-versioning-topo.ts +172 -0
  97. package/src/rules/trailhead-override-divergence.ts +356 -0
  98. package/src/rules/types.ts +464 -0
  99. package/src/rules/unmaterialized-activation-source.ts +85 -0
  100. package/src/rules/unreachable-detour-shadowing.ts +343 -0
  101. package/src/rules/valid-describe-refs.ts +224 -0
  102. package/src/rules/valid-detour-contract.ts +78 -0
  103. package/src/rules/warden-export-symmetry.ts +540 -0
  104. package/src/rules/warden-rules-use-ast.ts +1109 -0
  105. package/src/rules/webhook-route-collision.ts +306 -0
  106. package/src/rules/workspace-lock-ownership.ts +34 -0
  107. package/src/trails/activation-orphan.trail.ts +84 -0
  108. package/src/trails/captured-kernel.trail.ts +108 -0
  109. package/src/trails/circular-refs.trail.ts +29 -0
  110. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  111. package/src/trails/composes-declarations.trail.ts +22 -0
  112. package/src/trails/context-no-surface-types.trail.ts +21 -0
  113. package/src/trails/dead-internal-trail.trail.ts +26 -0
  114. package/src/trails/dead-public-trail.trail.ts +31 -0
  115. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  116. package/src/trails/draft-file-marking.trail.ts +16 -0
  117. package/src/trails/draft-visible-debt.trail.ts +16 -0
  118. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  119. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  120. package/src/trails/entity-exists.trail.ts +21 -0
  121. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  122. package/src/trails/example-valid.trail.ts +25 -0
  123. package/src/trails/fires-declarations.trail.ts +23 -0
  124. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  125. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  126. package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
  127. package/src/trails/implementation-returns-result.trail.ts +20 -0
  128. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  129. package/src/trails/incomplete-crud.trail.ts +39 -0
  130. package/src/trails/index.ts +91 -0
  131. package/src/trails/intent-propagation.trail.ts +30 -0
  132. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  133. package/src/trails/library-render-coherence.trail.ts +43 -0
  134. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  135. package/src/trails/missing-reconcile.trail.ts +33 -0
  136. package/src/trails/missing-visibility.trail.ts +22 -0
  137. package/src/trails/no-destructured-compose.trail.ts +44 -0
  138. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  139. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  140. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  141. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  142. package/src/trails/no-native-error-result.trail.ts +18 -0
  143. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  144. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  145. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  146. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  147. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  148. package/src/trails/no-top-level-surface.trail.ts +43 -0
  149. package/src/trails/on-references-exist.trail.ts +21 -0
  150. package/src/trails/orphaned-signal.trail.ts +36 -0
  151. package/src/trails/owner-render-parity.trail.ts +26 -0
  152. package/src/trails/pending-force.trail.ts +21 -0
  153. package/src/trails/permit-governance.trail.ts +51 -0
  154. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  155. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  156. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  157. package/src/trails/public-output-schema.trail.ts +55 -0
  158. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  159. package/src/trails/read-intent-fires.trail.ts +20 -0
  160. package/src/trails/reference-exists.trail.ts +25 -0
  161. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  162. package/src/trails/resource-declarations.trail.ts +25 -0
  163. package/src/trails/resource-exists.trail.ts +27 -0
  164. package/src/trails/resource-id-grammar.trail.ts +39 -0
  165. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  166. package/src/trails/run.ts +205 -0
  167. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  168. package/src/trails/schema.ts +295 -0
  169. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  170. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  171. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  172. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  173. package/src/trails/topo.ts +6 -0
  174. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  175. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  176. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  177. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  178. package/src/trails/valid-describe-refs.trail.ts +18 -0
  179. package/src/trails/valid-detour-contract.trail.ts +71 -0
  180. package/src/trails/version-gap.trail.ts +35 -0
  181. package/src/trails/version-pinned-compose.trail.ts +23 -0
  182. package/src/trails/version-without-examples.trail.ts +38 -0
  183. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  184. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  185. package/src/trails/webhook-route-collision.trail.ts +50 -0
  186. package/src/trails/workspace-lock-ownership.trail.ts +16 -0
  187. package/src/trails/wrap-rule.ts +269 -0
  188. package/src/workspaces.ts +264 -0
@@ -0,0 +1,25 @@
1
+ import { surfaceTrailheadCoherence } from '../rules/surface-trailhead-coherence.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const surfaceTrailheadCoherenceTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'mcp-options.ts',
10
+ sourceCode: `export const trailheads = {
11
+ inspect: {
12
+ description: "Inspect topo state.",
13
+ trails: ["survey", "survey.brief"],
14
+ },
15
+ governance: {
16
+ description: "Run diagnostics.",
17
+ trails: ["warden", "doctor"],
18
+ },
19
+ };`,
20
+ },
21
+ name: 'Reviewable trailhead map',
22
+ },
23
+ ],
24
+ rule: surfaceTrailheadCoherence,
25
+ });
@@ -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,42 @@
1
+ import { trailForkCoaching } from '../rules/trail-fork-coaching.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const trailForkCoachingTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: {
8
+ diagnostics: [
9
+ {
10
+ filePath: 'users.ts',
11
+ line: 9,
12
+ message:
13
+ 'Trail "users.manage" branches on input.action ("create", "delete"). This may be a trail fork hidden as a surface accommodation. If branches change semantics (intent, permits, errors, outputs, lifecycle, or side effects) or structure (selected trail identity), split them into distinct trails, a composing trail, or a trailhead that preserves member identity.',
14
+ rule: 'trail-fork-coaching',
15
+ severity: 'warn',
16
+ },
17
+ ],
18
+ },
19
+ input: {
20
+ filePath: 'users.ts',
21
+ sourceCode: `import { Result, trail } from "@ontrails/core";
22
+ import { z } from "zod";
23
+
24
+ export const usersManage = trail("users.manage", {
25
+ input: z.object({
26
+ action: z.enum(["create", "delete"]),
27
+ }),
28
+ implementation: async (input) => {
29
+ switch (input.action) {
30
+ case "create":
31
+ return Result.ok({ created: true });
32
+ case "delete":
33
+ return Result.ok({ deleted: true });
34
+ }
35
+ },
36
+ });`,
37
+ },
38
+ name: 'Possible trail fork hidden behind action',
39
+ },
40
+ ],
41
+ rule: trailForkCoaching,
42
+ });
@@ -0,0 +1,47 @@
1
+ import { trailheadOverrideDivergence } from '../rules/trailhead-override-divergence.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const trailheadOverrideDivergenceTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ authoredMcpSurfaceBindingSets: [
10
+ {
11
+ appName: 'demo',
12
+ bindings: { inspect: ['survey', 'survey.brief'] },
13
+ trailIds: ['survey', 'survey.brief'],
14
+ },
15
+ ],
16
+ filePath: 'mcp-options.ts',
17
+ sourceCode: `export const trailheads = {
18
+ inspect: {
19
+ description: "Inspect topo state.",
20
+ trails: ["survey", "survey.brief"],
21
+ },
22
+ };`,
23
+ },
24
+ name: 'Aligned call-site override',
25
+ },
26
+ {
27
+ input: {
28
+ authoredMcpSurfaceBindingSets: [
29
+ {
30
+ appName: 'demo',
31
+ bindings: { inspect: ['survey', 'survey.brief'] },
32
+ trailIds: ['survey', 'survey.brief'],
33
+ },
34
+ ],
35
+ filePath: 'mcp-options.ts',
36
+ sourceCode: `export const trailheads = {
37
+ inspect: {
38
+ description: "Inspect topo state.",
39
+ trails: ["survey"],
40
+ },
41
+ };`,
42
+ },
43
+ name: 'Diverging member selectors are flagged',
44
+ },
45
+ ],
46
+ rule: trailheadOverrideDivergence,
47
+ });
@@ -0,0 +1,72 @@
1
+ import { Result, schedule, signal, topo, trail, webhook } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { unmaterializedActivationSource } from '../rules/unmaterialized-activation-source.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ const invoicePaidWebhook = webhook('webhook.invoice.paid', {
8
+ parse: z.object({ invoiceId: z.string() }),
9
+ path: '/webhooks/invoice/paid',
10
+ });
11
+
12
+ const webhookConsumer = trail('invoice.audit-webhook', {
13
+ implementation: () => Result.ok({ ok: true }),
14
+ input: z.object({ invoiceId: z.string() }),
15
+ on: [invoicePaidWebhook],
16
+ output: z.object({ ok: z.boolean() }),
17
+ });
18
+
19
+ const invoiceCreated = signal('invoice.created', {
20
+ from: ['invoice.create'],
21
+ payload: z.object({ invoiceId: z.string() }),
22
+ });
23
+
24
+ const signalProducer = trail('invoice.create', {
25
+ fires: [invoiceCreated],
26
+ implementation: async (_input, ctx) => {
27
+ await ctx.fire?.(invoiceCreated, { invoiceId: 'inv_1' });
28
+ return Result.ok({ invoiceId: 'inv_1' });
29
+ },
30
+ input: z.object({}),
31
+ output: z.object({ invoiceId: z.string() }),
32
+ });
33
+
34
+ const signalConsumer = trail('invoice.index', {
35
+ implementation: () => Result.ok({ ok: true }),
36
+ input: z.object({ invoiceId: z.string() }),
37
+ on: [invoiceCreated],
38
+ output: z.object({ ok: z.boolean() }),
39
+ });
40
+
41
+ const scheduleConsumer = trail('invoice.reconcile', {
42
+ implementation: () => Result.ok({ ok: true }),
43
+ input: z.object({}),
44
+ on: [schedule('schedule.invoice.reconcile', { cron: '0 * * * *' })],
45
+ output: z.object({ ok: z.boolean() }),
46
+ });
47
+
48
+ export const unmaterializedActivationSourceTrail = wrapTopoRule({
49
+ examples: [
50
+ {
51
+ expected: { diagnostics: [] },
52
+ input: {
53
+ topo: topo('trl-461-webhook-materialized', { webhookConsumer }),
54
+ },
55
+ name: 'Webhook activation sources are materialized by HTTP',
56
+ },
57
+ {
58
+ expected: { diagnostics: [] },
59
+ input: {
60
+ topo: topo('trl-496-materialized-sources', {
61
+ invoiceCreated,
62
+ scheduleConsumer,
63
+ signalConsumer,
64
+ signalProducer,
65
+ webhookConsumer,
66
+ }),
67
+ },
68
+ name: 'Signal, schedule, and webhook activation sources are materialized',
69
+ },
70
+ ],
71
+ rule: unmaterializedActivationSource,
72
+ });
@@ -0,0 +1,45 @@
1
+ import { unreachableDetourShadowing } from '../rules/unreachable-detour-shadowing.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const unreachableDetourShadowingTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `trail("entity.save", {
11
+ detours: [
12
+ { on: ConflictError, recover: async () => Result.ok({ winner: "specific" }) },
13
+ { on: TrailsError, recover: async () => Result.ok({ winner: "broad" }) },
14
+ ],
15
+ });`,
16
+ },
17
+ name: 'Specific detours can precede broader ones',
18
+ },
19
+ {
20
+ expected: {
21
+ diagnostics: [
22
+ {
23
+ filePath: 'shadowed.ts',
24
+ line: 4,
25
+ message:
26
+ 'Trail "entity.save" declares detour on "ConflictError" after earlier detour on "TrailsError". Because "TrailsError" matches "ConflictError" first, the later detour is unreachable. Move the "ConflictError" detour before "TrailsError" so the more specific recovery runs, or remove it if the broader detour should own this case.',
27
+ rule: 'unreachable-detour-shadowing',
28
+ severity: 'error',
29
+ },
30
+ ],
31
+ },
32
+ input: {
33
+ filePath: 'shadowed.ts',
34
+ sourceCode: `trail("entity.save", {
35
+ detours: [
36
+ { on: TrailsError, recover: async () => Result.ok({ winner: "broad" }) },
37
+ { on: ConflictError, recover: async () => Result.ok({ winner: "specific" }) },
38
+ ],
39
+ });`,
40
+ },
41
+ name: 'Broader detours declared first shadow later specific ones',
42
+ },
43
+ ],
44
+ rule: unreachableDetourShadowing,
45
+ });
@@ -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,71 @@
1
+ import { ConflictError, Result, topo, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { validDetourContract } from '../rules/valid-detour-contract.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ const validTrail = trail('entity.save', {
8
+ detours: [
9
+ {
10
+ on: ConflictError,
11
+ recover: async () => {
12
+ const result = await Promise.resolve(Result.ok({ ok: true }));
13
+ return result;
14
+ },
15
+ },
16
+ ],
17
+ implementation: () => Result.ok({ ok: true }),
18
+ input: z.object({}),
19
+ output: z.object({ ok: z.boolean() }),
20
+ });
21
+
22
+ const invalidContractTrail = {
23
+ ...validTrail,
24
+ detours: [
25
+ {
26
+ on: 'ConflictError',
27
+ recover: 'not a function',
28
+ },
29
+ ],
30
+ } as unknown as typeof validTrail;
31
+
32
+ export const validDetourContractTrail = wrapTopoRule({
33
+ examples: [
34
+ {
35
+ expected: { diagnostics: [] },
36
+ input: {
37
+ topo: topo('trl-380-valid-detour-contract', { validTrail }),
38
+ },
39
+ name: 'Detours with an error constructor and recover function stay clean',
40
+ },
41
+ {
42
+ expected: {
43
+ diagnostics: [
44
+ {
45
+ filePath: '<topo>',
46
+ line: 1,
47
+ message:
48
+ 'Trail "entity.save" detour[0] must declare an error constructor in on:. Received ConflictError. Pass the error class itself, such as on: TimeoutError — not an error instance, a string name, or an error category.',
49
+ rule: 'valid-detour-contract',
50
+ severity: 'error',
51
+ },
52
+ {
53
+ filePath: '<topo>',
54
+ line: 1,
55
+ message:
56
+ 'Trail "entity.save" detour[0] must declare a callable recover function. Expected recover: (attempt, ctx) => Promise<Result<...>>; inspect attempt.error for the matched error and return Result.err(...) for unrecoverable cases.',
57
+ rule: 'valid-detour-contract',
58
+ severity: 'error',
59
+ },
60
+ ],
61
+ },
62
+ input: {
63
+ topo: topo('trl-380-invalid-detour-contract', {
64
+ invalidContractTrail,
65
+ } as Record<string, unknown>),
66
+ },
67
+ name: 'Malformed detour contracts emit diagnostics',
68
+ },
69
+ ],
70
+ rule: validDetourContract,
71
+ });
@@ -0,0 +1,35 @@
1
+ import { Result, topo, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { versionGap } from '../rules/trail-versioning-topo.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ const versionedTrail = trail('version.gap.clean', {
8
+ implementation: () => Result.ok({ ok: true }),
9
+ input: z.object({}),
10
+ output: z.object({ ok: z.boolean() }),
11
+ version: 2,
12
+ versions: {
13
+ 1: {
14
+ input: z.object({}),
15
+ output: z.object({ ok: z.boolean() }),
16
+ transpose: {
17
+ input: () => ({}),
18
+ output: ({ output }) => output,
19
+ },
20
+ },
21
+ },
22
+ });
23
+
24
+ export const versionGapTrail = wrapTopoRule({
25
+ examples: [
26
+ {
27
+ expected: { diagnostics: [] },
28
+ input: {
29
+ topo: topo('version-gap-clean', { versionedTrail }),
30
+ },
31
+ name: 'Contiguous versions pass',
32
+ },
33
+ ],
34
+ rule: versionGap,
35
+ });
@@ -0,0 +1,23 @@
1
+ import { versionPinnedCompose } from '../rules/trail-versioning-source.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const versionPinnedComposeTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'src/trails/current.ts',
10
+ sourceCode: `
11
+ trail('current.parent', {
12
+ implementation: async (_input, ctx) => {
13
+ await ctx.compose('current.child', {});
14
+ return Result.ok({});
15
+ },
16
+ });
17
+ `,
18
+ },
19
+ name: 'Current composition has no version-pin warning',
20
+ },
21
+ ],
22
+ rule: versionPinnedCompose,
23
+ });
@@ -0,0 +1,38 @@
1
+ import { Result, topo, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { versionWithoutExamples } from '../rules/trail-versioning-topo.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ const archivedWithoutExamples = trail('version.examples.archived', {
8
+ implementation: () => Result.ok({ ok: true }),
9
+ input: z.object({}),
10
+ output: z.object({ ok: z.boolean() }),
11
+ version: 2,
12
+ versions: {
13
+ 1: {
14
+ input: z.object({}),
15
+ output: z.object({ ok: z.boolean() }),
16
+ status: { state: 'archived' },
17
+ transpose: {
18
+ input: () => ({}),
19
+ output: ({ output }) => output,
20
+ },
21
+ },
22
+ },
23
+ });
24
+
25
+ export const versionWithoutExamplesTrail = wrapTopoRule({
26
+ examples: [
27
+ {
28
+ expected: { diagnostics: [] },
29
+ input: {
30
+ topo: topo('version-without-examples-clean', {
31
+ archivedWithoutExamples,
32
+ }),
33
+ },
34
+ name: 'Archived entries are exempt from example warnings',
35
+ },
36
+ ],
37
+ rule: versionWithoutExamples,
38
+ });
@@ -0,0 +1,16 @@
1
+ import { wardenExportSymmetry } from '../rules/warden-export-symmetry.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const wardenExportSymmetryTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'packages/other-pkg/src/index.ts',
10
+ sourceCode: `export { somethingElse } from './other.js';\n`,
11
+ },
12
+ name: 'Ignores files outside the warden barrel',
13
+ },
14
+ ],
15
+ rule: wardenExportSymmetry,
16
+ });
@@ -0,0 +1,64 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { wardenRulesUseAst } from '../rules/warden-rules-use-ast.js';
3
+ import { wrapRule } from './wrap-rule.js';
4
+
5
+ /**
6
+ * Resolve a filePath inside this package's `src/rules/` directory so the
7
+ * positive example fires the path-anchored scope check. Anchoring via
8
+ * `import.meta.url` keeps the example robust to the cwd under which tests run.
9
+ */
10
+ const fakeRulePath = fileURLToPath(
11
+ new URL('../rules/fake-rule.ts', import.meta.url)
12
+ );
13
+
14
+ export const wardenRulesUseAstTrail = wrapRule({
15
+ examples: [
16
+ {
17
+ expected: { diagnostics: [] },
18
+ input: {
19
+ filePath: 'packages/other-pkg/src/index.ts',
20
+ sourceCode: `const lines = sourceCode.split('\\n');\n`,
21
+ },
22
+ name: 'Ignores files outside the warden rules directory',
23
+ },
24
+ {
25
+ expected: {
26
+ diagnostics: [
27
+ {
28
+ filePath: fakeRulePath,
29
+ line: 1,
30
+ message:
31
+ 'warden-rules-use-ast: sourceCode.split(...) treats source text as a string. Warden rules must inspect source code via @ontrails/source helpers, not regex-scan raw source text. Use findStringLiterals, findTrailDefinitions, findConfigProperty, or a similar AST walker. Raw-text scanning produces false positives on string literals, template payloads, and docstrings — see TRL-335, ADR-0036.',
32
+ rule: 'warden-rules-use-ast',
33
+ severity: 'error',
34
+ },
35
+ ],
36
+ },
37
+ input: {
38
+ filePath: fakeRulePath,
39
+ sourceCode: `export const r = { check(sourceCode: string) { return sourceCode.split('\\n'); } };\n`,
40
+ },
41
+ name: 'Flags sourceCode.split(...) in a rule file',
42
+ },
43
+ {
44
+ expected: {
45
+ diagnostics: [
46
+ {
47
+ filePath: fakeRulePath,
48
+ line: 1,
49
+ message:
50
+ 'warden-rules-use-ast: raw AST node-field cast should use typed helpers from the shared source helpers (callee -> getNodeCallee). Raw node-field casts drift from the shared source helper surface.',
51
+ rule: 'warden-rules-use-ast',
52
+ severity: 'warn',
53
+ },
54
+ ],
55
+ },
56
+ input: {
57
+ filePath: fakeRulePath,
58
+ sourceCode: `export const r = { check() { const callee = (node as unknown as { callee?: AstNode }).callee; return callee ? [] : []; } };\n`,
59
+ },
60
+ name: 'Warns on hand-cast AST node fields',
61
+ },
62
+ ],
63
+ rule: wardenRulesUseAst,
64
+ });
@@ -0,0 +1,50 @@
1
+ import { Result, topo, trail, webhook } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { webhookRouteCollision } from '../rules/webhook-route-collision.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ const paymentWebhook = webhook('webhook.payment.received', {
8
+ parse: z.object({ paymentId: z.string() }),
9
+ path: '/webhooks/payment',
10
+ });
11
+
12
+ const paymentReceiver = trail('payment.receive', {
13
+ implementation: () => Result.ok({ ok: true }),
14
+ input: z.object({ paymentId: z.string() }),
15
+ on: [paymentWebhook],
16
+ output: z.object({ ok: z.boolean() }),
17
+ });
18
+
19
+ const directRoute = trail('webhooks.payment', {
20
+ implementation: () => Result.ok({ ok: true }),
21
+ input: z.object({}),
22
+ output: z.object({ ok: z.boolean() }),
23
+ });
24
+
25
+ export const webhookRouteCollisionTrail = wrapTopoRule({
26
+ examples: [
27
+ {
28
+ expected: {
29
+ diagnostics: [
30
+ {
31
+ filePath: '<topo>',
32
+ line: 1,
33
+ message:
34
+ 'HTTP webhook route collision on POST /webhooks/payment: derived trail route "webhooks.payment", webhook source "webhook.payment.received" on trail "payment.receive". Give each webhook source a distinct method/path pair; if a derived trail route is part of the collision, change the webhook path or rename the trail so its derived route differs.',
35
+ rule: 'webhook-route-collision',
36
+ severity: 'error',
37
+ },
38
+ ],
39
+ },
40
+ input: {
41
+ topo: topo('trl-461-webhook-route-collision', {
42
+ directRoute,
43
+ paymentReceiver,
44
+ }),
45
+ },
46
+ name: 'Webhook route colliding with a derived direct HTTP route',
47
+ },
48
+ ],
49
+ rule: webhookRouteCollision,
50
+ });
@@ -0,0 +1,16 @@
1
+ import { workspaceLockOwnership } from '../rules/workspace-lock-ownership.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const workspaceLockOwnershipTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'src/app.ts',
10
+ sourceCode: '',
11
+ },
12
+ name: 'File-scoped wrapper defers project ownership checks',
13
+ },
14
+ ],
15
+ rule: workspaceLockOwnership,
16
+ });