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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +497 -6
  2. package/README.md +77 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +27 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +28 -0
  7. package/src/cli.ts +1374 -103
  8. package/src/command.ts +953 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +106 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +206 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/resolve.ts +530 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/ast.ts +3176 -85
  20. package/src/rules/circular-refs.ts +154 -0
  21. package/src/rules/composes-declarations.ts +704 -0
  22. package/src/rules/context-no-surface-types.ts +68 -8
  23. package/src/rules/contour-exists.ts +251 -0
  24. package/src/rules/contour-ids.ts +15 -0
  25. package/src/rules/dead-internal-trail.ts +154 -0
  26. package/src/rules/draft-file-marking.ts +160 -0
  27. package/src/rules/draft-visible-debt.ts +87 -0
  28. package/src/rules/error-mapping-completeness.ts +288 -0
  29. package/src/rules/example-valid.ts +401 -0
  30. package/src/rules/fires-declarations.ts +758 -0
  31. package/src/rules/implementation-returns-result.ts +1265 -95
  32. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  33. package/src/rules/incomplete-crud.ts +580 -0
  34. package/src/rules/index.ts +219 -18
  35. package/src/rules/intent-propagation.ts +127 -0
  36. package/src/rules/layer-field-name-drift.ts +96 -0
  37. package/src/rules/metadata.ts +654 -0
  38. package/src/rules/missing-reconcile.ts +98 -0
  39. package/src/rules/missing-visibility.ts +110 -0
  40. package/src/rules/no-destructured-compose.ts +192 -0
  41. package/src/rules/no-dev-permit-in-source.ts +99 -0
  42. package/src/rules/no-direct-implementation-call.ts +7 -7
  43. package/src/rules/no-legacy-layer-imports.ts +211 -0
  44. package/src/rules/no-native-error-result.ts +111 -0
  45. package/src/rules/no-redundant-result-error-wrap.ts +331 -0
  46. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  47. package/src/rules/no-sync-result-assumption.ts +1134 -99
  48. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  49. package/src/rules/no-throw-in-implementation.ts +10 -9
  50. package/src/rules/no-top-level-surface.ts +389 -0
  51. package/src/rules/on-references-exist.ts +194 -0
  52. package/src/rules/orphaned-signal.ts +150 -0
  53. package/src/rules/owner-projection-parity.ts +146 -0
  54. package/src/rules/permit-governance.ts +25 -0
  55. package/src/rules/public-export-example-coverage.ts +553 -0
  56. package/src/rules/public-internal-deep-imports.ts +517 -0
  57. package/src/rules/public-output-schema.ts +29 -0
  58. package/src/rules/public-union-output-discriminants.ts +150 -0
  59. package/src/rules/read-intent-fires.ts +187 -0
  60. package/src/rules/reference-exists.ts +98 -0
  61. package/src/rules/registry-names.ts +145 -0
  62. package/src/rules/resolved-import-boundary.ts +146 -0
  63. package/src/rules/resource-declarations.ts +704 -0
  64. package/src/rules/resource-exists.ts +179 -0
  65. package/src/rules/resource-id-grammar.ts +65 -0
  66. package/src/rules/resource-mock-coverage.ts +115 -0
  67. package/src/rules/scan.ts +38 -25
  68. package/src/rules/scheduled-destroy-intent.ts +44 -0
  69. package/src/rules/signal-graph-coaching.ts +191 -0
  70. package/src/rules/specs.ts +9 -5
  71. package/src/rules/static-resource-accessor-preference.ts +657 -0
  72. package/src/rules/surface-facet-coherence.ts +370 -0
  73. package/src/rules/trail-versioning-source.ts +1094 -0
  74. package/src/rules/trail-versioning-topo.ts +172 -0
  75. package/src/rules/types.ts +270 -6
  76. package/src/rules/unmaterialized-activation-source.ts +84 -0
  77. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  78. package/src/rules/valid-describe-refs.ts +160 -32
  79. package/src/rules/valid-detour-contract.ts +78 -0
  80. package/src/rules/warden-export-symmetry.ts +533 -0
  81. package/src/rules/warden-rules-use-ast.ts +996 -0
  82. package/src/rules/webhook-route-collision.ts +243 -0
  83. package/src/trails/activation-orphan.trail.ts +84 -0
  84. package/src/trails/circular-refs.trail.ts +29 -0
  85. package/src/trails/composes-declarations.trail.ts +22 -0
  86. package/src/trails/context-no-surface-types.trail.ts +21 -0
  87. package/src/trails/contour-exists.trail.ts +21 -0
  88. package/src/trails/dead-internal-trail.trail.ts +26 -0
  89. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  90. package/src/trails/draft-file-marking.trail.ts +16 -0
  91. package/src/trails/draft-visible-debt.trail.ts +16 -0
  92. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  93. package/src/trails/example-valid.trail.ts +25 -0
  94. package/src/trails/fires-declarations.trail.ts +23 -0
  95. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  96. package/src/trails/implementation-returns-result.trail.ts +20 -0
  97. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  98. package/src/trails/incomplete-crud.trail.ts +39 -0
  99. package/src/trails/index.ts +78 -0
  100. package/src/trails/intent-propagation.trail.ts +30 -0
  101. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  102. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  103. package/src/trails/missing-reconcile.trail.ts +33 -0
  104. package/src/trails/missing-visibility.trail.ts +22 -0
  105. package/src/trails/no-destructured-compose.trail.ts +44 -0
  106. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  107. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  108. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  109. package/src/trails/no-native-error-result.trail.ts +18 -0
  110. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  111. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  112. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  113. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  114. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  115. package/src/trails/no-top-level-surface.trail.ts +43 -0
  116. package/src/trails/on-references-exist.trail.ts +21 -0
  117. package/src/trails/orphaned-signal.trail.ts +36 -0
  118. package/src/trails/owner-projection-parity.trail.ts +26 -0
  119. package/src/trails/pending-force.trail.ts +21 -0
  120. package/src/trails/permit-governance.trail.ts +51 -0
  121. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  122. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  123. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  124. package/src/trails/public-output-schema.trail.ts +55 -0
  125. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  126. package/src/trails/read-intent-fires.trail.ts +20 -0
  127. package/src/trails/reference-exists.trail.ts +25 -0
  128. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  129. package/src/trails/resource-declarations.trail.ts +25 -0
  130. package/src/trails/resource-exists.trail.ts +27 -0
  131. package/src/trails/resource-id-grammar.trail.ts +39 -0
  132. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  133. package/src/trails/run.ts +162 -0
  134. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  135. package/src/trails/schema.ts +194 -0
  136. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  137. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  138. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  139. package/src/trails/topo.ts +6 -0
  140. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  141. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  142. package/src/trails/valid-describe-refs.trail.ts +18 -0
  143. package/src/trails/valid-detour-contract.trail.ts +71 -0
  144. package/src/trails/version-gap.trail.ts +35 -0
  145. package/src/trails/version-pinned-compose.trail.ts +23 -0
  146. package/src/trails/version-without-examples.trail.ts +38 -0
  147. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  148. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  149. package/src/trails/webhook-route-collision.trail.ts +50 -0
  150. package/src/trails/wrap-rule.ts +213 -0
  151. package/src/workspaces.ts +238 -0
  152. package/.turbo/turbo-build.log +0 -1
  153. package/.turbo/turbo-lint.log +0 -3
  154. package/.turbo/turbo-typecheck.log +0 -1
  155. package/dist/cli.d.ts +0 -46
  156. package/dist/cli.d.ts.map +0 -1
  157. package/dist/cli.js +0 -221
  158. package/dist/cli.js.map +0 -1
  159. package/dist/drift.d.ts +0 -26
  160. package/dist/drift.d.ts.map +0 -1
  161. package/dist/drift.js +0 -27
  162. package/dist/drift.js.map +0 -1
  163. package/dist/formatters.d.ts +0 -29
  164. package/dist/formatters.d.ts.map +0 -1
  165. package/dist/formatters.js +0 -87
  166. package/dist/formatters.js.map +0 -1
  167. package/dist/index.d.ts +0 -26
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -26
  170. package/dist/index.js.map +0 -1
  171. package/dist/rules/ast.d.ts +0 -41
  172. package/dist/rules/ast.d.ts.map +0 -1
  173. package/dist/rules/ast.js +0 -163
  174. package/dist/rules/ast.js.map +0 -1
  175. package/dist/rules/context-no-surface-types.d.ts +0 -12
  176. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  177. package/dist/rules/context-no-surface-types.js +0 -96
  178. package/dist/rules/context-no-surface-types.js.map +0 -1
  179. package/dist/rules/implementation-returns-result.d.ts +0 -13
  180. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  181. package/dist/rules/implementation-returns-result.js +0 -231
  182. package/dist/rules/implementation-returns-result.js.map +0 -1
  183. package/dist/rules/index.d.ts +0 -22
  184. package/dist/rules/index.d.ts.map +0 -1
  185. package/dist/rules/index.js +0 -41
  186. package/dist/rules/index.js.map +0 -1
  187. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  188. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  189. package/dist/rules/no-direct-impl-in-route.js +0 -46
  190. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  191. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  192. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  193. package/dist/rules/no-direct-implementation-call.js +0 -39
  194. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  195. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  196. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  197. package/dist/rules/no-sync-result-assumption.js +0 -98
  198. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  199. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  200. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  201. package/dist/rules/no-throw-in-detour-target.js +0 -87
  202. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  203. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  204. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  205. package/dist/rules/no-throw-in-implementation.js +0 -34
  206. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  207. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  208. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  209. package/dist/rules/prefer-schema-inference.js +0 -86
  210. package/dist/rules/prefer-schema-inference.js.map +0 -1
  211. package/dist/rules/scan.d.ts +0 -8
  212. package/dist/rules/scan.d.ts.map +0 -1
  213. package/dist/rules/scan.js +0 -32
  214. package/dist/rules/scan.js.map +0 -1
  215. package/dist/rules/specs.d.ts +0 -29
  216. package/dist/rules/specs.d.ts.map +0 -1
  217. package/dist/rules/specs.js +0 -192
  218. package/dist/rules/specs.js.map +0 -1
  219. package/dist/rules/structure.d.ts +0 -13
  220. package/dist/rules/structure.d.ts.map +0 -1
  221. package/dist/rules/structure.js +0 -142
  222. package/dist/rules/structure.js.map +0 -1
  223. package/dist/rules/types.d.ts +0 -52
  224. package/dist/rules/types.d.ts.map +0 -1
  225. package/dist/rules/types.js +0 -2
  226. package/dist/rules/types.js.map +0 -1
  227. package/dist/rules/valid-describe-refs.d.ts +0 -7
  228. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  229. package/dist/rules/valid-describe-refs.js +0 -51
  230. package/dist/rules/valid-describe-refs.js.map +0 -1
  231. package/dist/rules/valid-detour-refs.d.ts +0 -6
  232. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  233. package/dist/rules/valid-detour-refs.js +0 -116
  234. package/dist/rules/valid-detour-refs.js.map +0 -1
  235. package/src/__tests__/cli.test.ts +0 -198
  236. package/src/__tests__/drift.test.ts +0 -74
  237. package/src/__tests__/formatters.test.ts +0 -157
  238. package/src/__tests__/implementation-returns-result.test.ts +0 -75
  239. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  240. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  241. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  242. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  243. package/src/__tests__/rules.test.ts +0 -188
  244. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  245. package/src/rules/no-direct-impl-in-route.ts +0 -77
  246. package/src/rules/no-throw-in-detour-target.ts +0 -150
  247. package/src/rules/valid-detour-refs.ts +0 -187
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,40 @@
1
+ import { resourceMockCoverage } from '../rules/resource-mock-coverage.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const resourceMockCoverageTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `const db = resource("db.main", {
11
+ create: () => Result.ok(openDatabase()),
12
+ mock: () => createInMemoryDb(),
13
+ });`,
14
+ },
15
+ name: 'Resource declaring a mock factory passes',
16
+ },
17
+ {
18
+ expected: {
19
+ diagnostics: [
20
+ {
21
+ filePath: 'missing-mock.ts',
22
+ line: 1,
23
+ message:
24
+ 'Resource "db.main" declares no mock factory. Add a mock() so testAll(app) runs without configuration, or declare unmockable: { reason } if it intentionally cannot be mocked.',
25
+ rule: 'resource-mock-coverage',
26
+ severity: 'warn',
27
+ },
28
+ ],
29
+ },
30
+ input: {
31
+ filePath: 'missing-mock.ts',
32
+ sourceCode: `const db = resource("db.main", {
33
+ create: () => Result.ok(openDatabase()),
34
+ });`,
35
+ },
36
+ name: 'Resource without a mock factory or unmockable reason is flagged',
37
+ },
38
+ ],
39
+ rule: resourceMockCoverage,
40
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Run file-scoped warden rule trails against a single source file.
3
+ *
4
+ * Returns a flat array of diagnostics from every source-aware rule. Built-in
5
+ * topo-aware rules are dispatched separately via `runTopoAwareWardenTrails()`
6
+ * so callers that loop files do not duplicate graph-level findings.
7
+ */
8
+
9
+ import type { Intent, Topo } from '@ontrails/core';
10
+ import { run } from '@ontrails/core';
11
+
12
+ import { wardenTopoRules } from '../rules/index.js';
13
+ import type { WardenDiagnostic } from '../rules/types.js';
14
+ import type { WardenImportResolution } from '../resolve.js';
15
+ import type { WardenPublicWorkspace } from '../workspaces.js';
16
+ import type { RuleOutput } from './schema.js';
17
+ import { wardenTopo } from './topo.js';
18
+
19
+ /**
20
+ * Run all file-scoped warden rule trails for a given file and collect diagnostics.
21
+ *
22
+ * Each rule trail runs independently. Errors from individual trails are
23
+ * silently skipped so that one broken rule does not block the rest.
24
+ */
25
+ const appendDiagnostics = (
26
+ target: WardenDiagnostic[],
27
+ diagnostics: readonly WardenDiagnostic[]
28
+ ): void => {
29
+ for (const diagnostic of diagnostics) {
30
+ target.push(diagnostic);
31
+ }
32
+ };
33
+
34
+ type TrailIntentMap = Readonly<Record<string, Intent>>;
35
+
36
+ interface ProjectRuleOptions {
37
+ readonly contourReferencesByName?: Readonly<
38
+ Record<string, readonly string[]>
39
+ >;
40
+ readonly composeTargetTrailIds?: readonly string[];
41
+ readonly crudTableIds?: readonly string[];
42
+ readonly crudCoverageByEntity?: Readonly<Record<string, readonly string[]>>;
43
+ readonly knownContourIds?: readonly string[];
44
+ readonly importResolutionsByFile?: Readonly<
45
+ Record<string, readonly WardenImportResolution[]>
46
+ >;
47
+ readonly documentedImportResolutionsByFile?: Readonly<
48
+ Record<string, readonly WardenImportResolution[]>
49
+ >;
50
+ readonly knownResourceIds?: readonly string[];
51
+ readonly knownSignalIds?: readonly string[];
52
+ readonly knownTrailIds?: readonly string[];
53
+ readonly onTargetSignalIds?: readonly string[];
54
+ readonly publicWorkspaces?: Readonly<Record<string, WardenPublicWorkspace>>;
55
+ readonly reconcileTableIds?: readonly string[];
56
+ readonly trailIntentsById?: TrailIntentMap;
57
+ }
58
+
59
+ const PROJECT_OPTION_KEYS = [
60
+ 'contourReferencesByName',
61
+ 'composeTargetTrailIds',
62
+ 'crudTableIds',
63
+ 'crudCoverageByEntity',
64
+ 'knownContourIds',
65
+ 'importResolutionsByFile',
66
+ 'documentedImportResolutionsByFile',
67
+ 'knownResourceIds',
68
+ 'knownSignalIds',
69
+ 'knownTrailIds',
70
+ 'onTargetSignalIds',
71
+ 'publicWorkspaces',
72
+ 'reconcileTableIds',
73
+ 'trailIntentsById',
74
+ ] as const satisfies readonly (keyof ProjectRuleOptions)[];
75
+
76
+ const hasProjectOptions = (options?: ProjectRuleOptions): boolean =>
77
+ Boolean(
78
+ options && PROJECT_OPTION_KEYS.some((key) => options[key] !== undefined)
79
+ );
80
+
81
+ const collectProjectOptions = (
82
+ options?: ProjectRuleOptions
83
+ ): ProjectRuleOptions => {
84
+ if (!options) {
85
+ return {};
86
+ }
87
+
88
+ return Object.fromEntries(
89
+ PROJECT_OPTION_KEYS.flatMap((key) => {
90
+ const value = options[key];
91
+ return value === undefined ? [] : [[key, value] as const];
92
+ })
93
+ ) as ProjectRuleOptions;
94
+ };
95
+
96
+ const buildRuleInput = (
97
+ filePath: string,
98
+ sourceCode: string,
99
+ options?: ProjectRuleOptions
100
+ ): {
101
+ readonly filePath: string;
102
+ readonly sourceCode: string;
103
+ } & ProjectRuleOptions => {
104
+ const base = { filePath, sourceCode };
105
+ if (!hasProjectOptions(options)) {
106
+ return base;
107
+ }
108
+
109
+ return { ...base, ...collectProjectOptions(options) };
110
+ };
111
+
112
+ const topoAwareTrailIds = new Set(
113
+ [...wardenTopoRules.keys()].map((ruleName) => `warden.rule.${ruleName}`)
114
+ );
115
+
116
+ export const runWardenTrails = async (
117
+ filePath: string,
118
+ sourceCode: string,
119
+ options?: ProjectRuleOptions
120
+ ): Promise<readonly WardenDiagnostic[]> => {
121
+ const allDiagnostics: WardenDiagnostic[] = [];
122
+ const input = buildRuleInput(filePath, sourceCode, options);
123
+
124
+ for (const id of wardenTopo.ids()) {
125
+ if (topoAwareTrailIds.has(id)) {
126
+ continue;
127
+ }
128
+ const result = await run(wardenTopo, id, input);
129
+ if (result.isOk()) {
130
+ appendDiagnostics(
131
+ allDiagnostics,
132
+ (result.value as RuleOutput).diagnostics
133
+ );
134
+ }
135
+ }
136
+
137
+ return allDiagnostics;
138
+ };
139
+
140
+ /**
141
+ * Run the built-in topo-aware warden rule trails once against a resolved topo.
142
+ *
143
+ * Unlike `runWardenTrails()`, which is file-scoped, topo-aware rules inspect
144
+ * the compiled graph and should only be dispatched once per topo.
145
+ */
146
+ export const runTopoAwareWardenTrails = async (
147
+ topo: Topo
148
+ ): Promise<readonly WardenDiagnostic[]> => {
149
+ const allDiagnostics: WardenDiagnostic[] = [];
150
+
151
+ for (const id of topoAwareTrailIds) {
152
+ const result = await run(wardenTopo, id, { topo });
153
+ if (result.isOk()) {
154
+ appendDiagnostics(
155
+ allDiagnostics,
156
+ (result.value as RuleOutput).diagnostics
157
+ );
158
+ }
159
+ }
160
+
161
+ return allDiagnostics;
162
+ };
@@ -0,0 +1,56 @@
1
+ import { Result, schedule, topo, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { scheduledDestroyIntent } from '../rules/scheduled-destroy-intent.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ export const scheduledDestroyTrail = trail('billing.purge-expired', {
8
+ blaze: () => Result.ok({ ok: true }),
9
+ input: z.object({}),
10
+ intent: 'destroy',
11
+ on: [
12
+ schedule('schedule.billing.purge-expired', {
13
+ cron: '0 2 * * *',
14
+ }),
15
+ ],
16
+ output: z.object({ ok: z.boolean() }),
17
+ permit: { scopes: ['billing:purge'] },
18
+ });
19
+
20
+ const scheduledWriteTrail = trail('billing.reconcile', {
21
+ blaze: () => Result.ok({ ok: true }),
22
+ input: z.object({}),
23
+ on: [schedule('schedule.billing.reconcile', { cron: '0 * * * *' })],
24
+ output: z.object({ ok: z.boolean() }),
25
+ });
26
+
27
+ export const scheduledDestroyIntentTrail = wrapTopoRule({
28
+ examples: [
29
+ {
30
+ expected: {
31
+ diagnostics: [
32
+ {
33
+ filePath: '<topo>',
34
+ line: 1,
35
+ message:
36
+ 'Trail "billing.purge-expired" declares intent: \'destroy\' and is activated by schedule source "schedule.billing.purge-expired". Scheduled destroy work should make cadence, permit scope, idempotency, and recovery explicit before it runs unattended.',
37
+ rule: 'scheduled-destroy-intent',
38
+ severity: 'warn',
39
+ },
40
+ ],
41
+ },
42
+ input: {
43
+ topo: topo('trl-457-scheduled-destroy', { scheduledDestroyTrail }),
44
+ },
45
+ name: 'Scheduled destroy trails emit coaching',
46
+ },
47
+ {
48
+ expected: { diagnostics: [] },
49
+ input: {
50
+ topo: topo('trl-457-scheduled-write', { scheduledWriteTrail }),
51
+ },
52
+ name: 'Scheduled write trails do not emit destroy coaching',
53
+ },
54
+ ],
55
+ rule: scheduledDestroyIntent,
56
+ });
@@ -0,0 +1,194 @@
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 { intentValues } from '@ontrails/core';
9
+ import type { Topo } from '@ontrails/core';
10
+ import type { TopoGraph } from '@ontrails/topographer';
11
+ import { z } from 'zod';
12
+ import { wardenImportResolutionErrorKinds } from '../resolve.js';
13
+ import { wardenFixClasses, wardenFixSafeties } from '../rules/metadata.js';
14
+
15
+ export const guidanceLinkSchema = z.object({
16
+ label: z.string(),
17
+ path: z.string().optional(),
18
+ url: z.string().optional(),
19
+ });
20
+
21
+ export const guidanceSchema = z.object({
22
+ commands: z.array(z.string()).readonly().optional(),
23
+ docs: z.array(guidanceLinkSchema).readonly().optional(),
24
+ relatedRules: z.array(z.string()).readonly().optional(),
25
+ steps: z.array(z.string()).readonly().optional(),
26
+ summary: z.string(),
27
+ });
28
+
29
+ export const fixEditSchema = z.object({
30
+ end: z.number(),
31
+ replacement: z.string(),
32
+ start: z.number(),
33
+ });
34
+
35
+ export const fixSchema = z.object({
36
+ class: z.enum(wardenFixClasses),
37
+ edits: z.array(fixEditSchema).readonly().optional(),
38
+ fixture: z.string().optional(),
39
+ reason: z.string(),
40
+ safety: z.enum(wardenFixSafeties),
41
+ });
42
+
43
+ /** A single diagnostic emitted by a warden rule trail. */
44
+ export const diagnosticSchema = z.object({
45
+ code: z.string().optional().describe('Optional rule-local diagnostic code'),
46
+ filePath: z.string().describe('File path that was analyzed'),
47
+ fix: fixSchema.optional().describe('Structured fix metadata'),
48
+ guidance: guidanceSchema
49
+ .optional()
50
+ .describe('Structured remediation guidance'),
51
+ line: z.number().describe('1-based line number'),
52
+ message: z.string().describe('Human-readable diagnostic message'),
53
+ rule: z.string().describe('Rule name'),
54
+ severity: z.enum(['error', 'warn']).describe('Diagnostic severity'),
55
+ });
56
+
57
+ /** Input accepted by every warden rule trail. */
58
+ export const ruleInput = z.object({
59
+ filePath: z.string().describe('Path to the source file'),
60
+ sourceCode: z.string().describe('Source code content'),
61
+ });
62
+
63
+ export const importResolutionSchema = z.object({
64
+ builtinModule: z.string().optional(),
65
+ crossesPackageBoundary: z.boolean(),
66
+ errorKind: z.enum(wardenImportResolutionErrorKinds).optional(),
67
+ errorMessage: z.string().optional(),
68
+ importSource: z.string(),
69
+ importerPath: z.string(),
70
+ isInternalTarget: z.boolean(),
71
+ line: z.number(),
72
+ packageName: z.string().optional(),
73
+ packageRoot: z.string().optional(),
74
+ resolvedPath: z.string().optional(),
75
+ usesPublicExport: z.boolean(),
76
+ });
77
+
78
+ export const publicWorkspaceSchema = z.object({
79
+ bin: z.record(z.string(), z.string()).optional(),
80
+ exportTargets: z.record(z.string(), z.string()).optional(),
81
+ files: z.array(z.string()).optional(),
82
+ hasExports: z.boolean(),
83
+ name: z.string(),
84
+ packageJsonPath: z.string(),
85
+ rootDir: z.string(),
86
+ });
87
+
88
+ /**
89
+ * Extended input for project-aware warden rule trails.
90
+ *
91
+ * Adds `knownTrailIds` so the caller can supply compose-file context and avoid
92
+ * false positives for `@see` references or compose-file contour relationships.
93
+ */
94
+ export const projectAwareRuleInput = ruleInput.extend({
95
+ composeTargetTrailIds: z
96
+ .array(z.string())
97
+ .optional()
98
+ .describe('Trail IDs referenced by composes arrays across the project'),
99
+ contourReferencesByName: z
100
+ .record(z.string(), z.array(z.string()))
101
+ .optional()
102
+ .describe('Declared contour references keyed by source contour name'),
103
+ crudCoverageByEntity: z
104
+ .record(z.string(), z.array(z.string()))
105
+ .optional()
106
+ .describe(
107
+ 'CRUD operation coverage per entity aggregated across the project'
108
+ ),
109
+ crudTableIds: z
110
+ .array(z.string())
111
+ .optional()
112
+ .describe('Store table IDs used with CRUD factories across the project'),
113
+ documentedImportResolutionsByFile: z
114
+ .record(z.string(), z.array(importResolutionSchema))
115
+ .optional()
116
+ .describe('Resolved docs/specifier facts keyed by documentation file path'),
117
+ importResolutionsByFile: z
118
+ .record(z.string(), z.array(importResolutionSchema))
119
+ .optional()
120
+ .describe('Resolved import facts keyed by importer file path'),
121
+ knownContourIds: z
122
+ .array(z.string())
123
+ .optional()
124
+ .describe('Contour names known across the project'),
125
+ knownResourceIds: z
126
+ .array(z.string())
127
+ .optional()
128
+ .describe('Resource IDs known across the project'),
129
+ knownSignalIds: z
130
+ .array(z.string())
131
+ .optional()
132
+ .describe('Signal IDs known across the project'),
133
+ knownTrailIds: z
134
+ .array(z.string())
135
+ .optional()
136
+ .describe('Trail IDs known across the project'),
137
+ onTargetSignalIds: z
138
+ .array(z.string())
139
+ .optional()
140
+ .describe('Signal IDs referenced by trail on arrays across the project'),
141
+ publicWorkspaces: z
142
+ .record(z.string(), publicWorkspaceSchema)
143
+ .optional()
144
+ .describe('Non-private published @ontrails workspaces by package name'),
145
+ reconcileTableIds: z
146
+ .array(z.string())
147
+ .optional()
148
+ .describe('Store table IDs used with reconcile trails across the project'),
149
+ trailIntentsById: z
150
+ .record(z.string(), z.enum(intentValues))
151
+ .optional()
152
+ .describe('Normalized trail intents keyed by trail ID'),
153
+ });
154
+
155
+ /**
156
+ * Input for topo-aware warden rule trails.
157
+ *
158
+ * The `Topo` graph is not a serializable value, so the schema accepts it
159
+ * as an opaque `z.custom`. Topo-aware rules are invoked from the warden
160
+ * runtime with a live, resolved topo reference — they are not expected
161
+ * to be called across a network boundary.
162
+ */
163
+ export const topoAwareRuleInput = z.object({
164
+ graph: z
165
+ .custom<TopoGraph>(
166
+ (value) =>
167
+ typeof value === 'object' && value !== null && 'entries' in value,
168
+ { message: 'Expected a serialized TopoGraph object' }
169
+ )
170
+ .optional()
171
+ .describe('Optional derived TopoGraph with graph-only audit annotations'),
172
+ topo: z
173
+ .custom<Topo>(
174
+ (value) =>
175
+ typeof value === 'object' &&
176
+ value !== null &&
177
+ 'trails' in value &&
178
+ 'resources' in value &&
179
+ 'contours' in value &&
180
+ 'signals' in value,
181
+ { message: 'Expected a resolved Topo instance' }
182
+ )
183
+ .describe('Resolved topo graph under inspection'),
184
+ });
185
+
186
+ /** Output returned by every warden rule trail. */
187
+ export const ruleOutput = z.object({
188
+ diagnostics: z.array(diagnosticSchema).describe('Diagnostics found'),
189
+ });
190
+
191
+ export type RuleInput = z.infer<typeof ruleInput>;
192
+ export type ProjectAwareRuleInput = z.infer<typeof projectAwareRuleInput>;
193
+ export type TopoAwareRuleInput = z.infer<typeof topoAwareRuleInput>;
194
+ export type RuleOutput = z.infer<typeof ruleOutput>;
@@ -0,0 +1,77 @@
1
+ import { Result, resource, signal, topo, trail } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import { signalGraphCoaching } from '../rules/signal-graph-coaching.js';
5
+ import { wrapTopoRule } from './wrap-rule.js';
6
+
7
+ const unusedSignal = signal('invoice.unused', {
8
+ payload: z.object({ invoiceId: z.string() }),
9
+ });
10
+
11
+ const producedSignal = signal('invoice.created', {
12
+ payload: z.object({ invoiceId: z.string() }),
13
+ });
14
+
15
+ const producerTrail = trail('invoice.create', {
16
+ blaze: async (_input, ctx) => {
17
+ await ctx.fire?.(producedSignal, { invoiceId: 'inv_1' });
18
+ return Result.ok({ invoiceId: 'inv_1' });
19
+ },
20
+ fires: [producedSignal],
21
+ input: z.object({}),
22
+ output: z.object({ invoiceId: z.string() }),
23
+ });
24
+
25
+ const resourceSignal = signal('store:invoice.created', {
26
+ payload: z.object({ invoiceId: z.string() }),
27
+ });
28
+
29
+ const invoiceStore = resource('store', {
30
+ create: () => Result.ok({ ok: true }),
31
+ signals: [resourceSignal],
32
+ });
33
+
34
+ export const signalGraphCoachingTrail = wrapTopoRule({
35
+ examples: [
36
+ {
37
+ expected: {
38
+ diagnostics: [
39
+ {
40
+ filePath: '<topo>',
41
+ line: 1,
42
+ message:
43
+ 'Signal "invoice.created" is produced by producer trail "invoice.create" but has no consumer trails. Add an on: consumer if the signal is meant to drive reactive work, or remove the unused fires:/producer declaration.',
44
+ rule: 'signal-graph-coaching',
45
+ severity: 'warn',
46
+ },
47
+ {
48
+ filePath: '<topo>',
49
+ line: 1,
50
+ message:
51
+ 'Signal "invoice.unused" is declared in the topo but has no producer trails, producer resources, or consumer trails. Add fires:/on: edges, attach producer metadata, or remove the unused signal contract.',
52
+ rule: 'signal-graph-coaching',
53
+ severity: 'warn',
54
+ },
55
+ {
56
+ filePath: '<topo>',
57
+ line: 1,
58
+ message:
59
+ 'Signal "store:invoice.created" is produced by producer resource "store" but has no consumer trails. Add an on: consumer if the signal is meant to drive reactive work, or remove the unused fires:/producer declaration.',
60
+ rule: 'signal-graph-coaching',
61
+ severity: 'warn',
62
+ },
63
+ ],
64
+ },
65
+ input: {
66
+ topo: topo('trl-447-signal-graph-coaching', {
67
+ invoiceStore,
68
+ producedSignal,
69
+ producerTrail,
70
+ unusedSignal,
71
+ }),
72
+ },
73
+ name: 'Declared and produced signals without consumers get coaching',
74
+ },
75
+ ],
76
+ rule: signalGraphCoaching,
77
+ });
@@ -0,0 +1,25 @@
1
+ import { staticResourceAccessorPreference } from '../rules/static-resource-accessor-preference.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const staticResourceAccessorPreferenceTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'clean.ts',
10
+ sourceCode: `const db = resource("db.main", {
11
+ create: () => Result.ok({ source: "factory" }),
12
+ });
13
+
14
+ trail("entity.show", {
15
+ resources: [db],
16
+ blaze: async (_input, ctx) => {
17
+ return Result.ok(db.from(ctx));
18
+ }
19
+ })`,
20
+ },
21
+ name: 'Static resource helper access',
22
+ },
23
+ ],
24
+ rule: staticResourceAccessorPreference,
25
+ });
@@ -0,0 +1,25 @@
1
+ import { surfaceFacetCoherence } from '../rules/surface-facet-coherence.js';
2
+ import { wrapRule } from './wrap-rule.js';
3
+
4
+ export const surfaceFacetCoherenceTrail = wrapRule({
5
+ examples: [
6
+ {
7
+ expected: { diagnostics: [] },
8
+ input: {
9
+ filePath: 'mcp-options.ts',
10
+ sourceCode: `export const facets = {
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 surface facet map',
22
+ },
23
+ ],
24
+ rule: surfaceFacetCoherence,
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,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
+ blaze: () => 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
+ blaze: async (_input, ctx) => {
26
+ await ctx.fire?.(invoiceCreated, { invoiceId: 'inv_1' });
27
+ return Result.ok({ invoiceId: 'inv_1' });
28
+ },
29
+ fires: [invoiceCreated],
30
+ input: z.object({}),
31
+ output: z.object({ invoiceId: z.string() }),
32
+ });
33
+
34
+ const signalConsumer = trail('invoice.index', {
35
+ blaze: () => 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
+ blaze: () => 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
+ });