@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,172 @@
1
+ import { deriveTopoGraph } from '@ontrails/topographer';
2
+ import type {
3
+ TopoGraph,
4
+ TopoGraphEntry,
5
+ TopoGraphForceEntry,
6
+ TopoGraphVersionEntry,
7
+ } from '@ontrails/topographer';
8
+ import type { Topo } from '@ontrails/core';
9
+
10
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
11
+
12
+ const TOPO_FILE = '<topo>';
13
+
14
+ const isArchived = (entry: TopoGraphVersionEntry): boolean =>
15
+ entry.status?.state === 'archived';
16
+
17
+ const isDeprecatedWithoutGuidance = (entry: TopoGraphVersionEntry): boolean =>
18
+ entry.status?.state === 'deprecated' &&
19
+ entry.status.successor === undefined &&
20
+ entry.status.note === undefined &&
21
+ (entry.status.migration === undefined || entry.status.migration.length === 0);
22
+
23
+ const versionNumbersFor = (entry: TopoGraphEntry): readonly number[] => [
24
+ ...(entry.version === undefined ? [] : [entry.version]),
25
+ ...Object.keys(entry.versions ?? {}).map(Number),
26
+ ];
27
+
28
+ const versionEntryName = (trailId: string, version: string): string =>
29
+ `${trailId}@${version}`;
30
+
31
+ const topoDiagnostic = (
32
+ rule: string,
33
+ severity: WardenDiagnostic['severity'],
34
+ message: string
35
+ ): WardenDiagnostic => ({
36
+ filePath: TOPO_FILE,
37
+ line: 1,
38
+ message,
39
+ rule,
40
+ severity,
41
+ });
42
+
43
+ const trailEntries = (graph: TopoGraph): readonly TopoGraphEntry[] =>
44
+ graph.entries.filter((entry) => entry.kind === 'trail');
45
+
46
+ const graphFor = (
47
+ topo: Topo,
48
+ context: Parameters<TopoAwareWardenRule['checkTopo']>[1]
49
+ ): TopoGraph => context?.graph ?? deriveTopoGraph(topo);
50
+
51
+ const forceKey = (force: TopoGraphForceEntry): string =>
52
+ JSON.stringify([
53
+ force.kind,
54
+ force.id,
55
+ force.change,
56
+ force.detail,
57
+ force.reason,
58
+ force.severity,
59
+ force.source,
60
+ ]);
61
+
62
+ const pendingForceDiagnostic = (force: TopoGraphForceEntry): WardenDiagnostic =>
63
+ topoDiagnostic(
64
+ 'pending-force',
65
+ 'warn',
66
+ `Trail "${force.id}" has a pending forced topo break (${force.change}: ${force.detail}). Resolve or document the force event before release.`
67
+ );
68
+
69
+ export const deprecationWithoutGuidance: TopoAwareWardenRule = {
70
+ checkTopo(topo, context) {
71
+ return trailEntries(graphFor(topo, context)).flatMap((entry) =>
72
+ Object.entries(entry.versions ?? {}).flatMap(([version, historical]) =>
73
+ isDeprecatedWithoutGuidance(historical)
74
+ ? [
75
+ topoDiagnostic(
76
+ 'deprecation-without-guidance',
77
+ 'error',
78
+ `Trail "${versionEntryName(entry.id, version)}" is deprecated without successor, migration, or note guidance. Add at least one guidance field to the version status.`
79
+ ),
80
+ ]
81
+ : []
82
+ )
83
+ );
84
+ },
85
+ description:
86
+ 'Require deprecated trail version entries to carry successor, migration, or note guidance.',
87
+ name: 'deprecation-without-guidance',
88
+ severity: 'error',
89
+ };
90
+
91
+ export const versionGap: TopoAwareWardenRule = {
92
+ checkTopo(topo, context) {
93
+ const diagnostics: WardenDiagnostic[] = [];
94
+ for (const entry of trailEntries(graphFor(topo, context))) {
95
+ if (entry.version === undefined) {
96
+ continue;
97
+ }
98
+ const historicalVersions = new Set(
99
+ Object.keys(entry.versions ?? {}).map(Number)
100
+ );
101
+ if (historicalVersions.has(entry.version)) {
102
+ diagnostics.push(
103
+ topoDiagnostic(
104
+ 'version-gap',
105
+ 'error',
106
+ `Trail "${entry.id}" declares current version ${entry.version} both as current and as a historical entry. Remove the duplicate historical entry.`
107
+ )
108
+ );
109
+ }
110
+
111
+ const versions = new Set(versionNumbersFor(entry));
112
+ for (let version = 1; version <= entry.version; version += 1) {
113
+ if (!versions.has(version)) {
114
+ diagnostics.push(
115
+ topoDiagnostic(
116
+ 'version-gap',
117
+ 'error',
118
+ `Trail "${entry.id}" has a gap before current version ${entry.version}: missing version ${version}. Historical version coverage must be contiguous, including archived entries.`
119
+ )
120
+ );
121
+ }
122
+ }
123
+ }
124
+ return diagnostics;
125
+ },
126
+ description:
127
+ 'Require trail version coverage to stay contiguous from v1 through the current version.',
128
+ name: 'version-gap',
129
+ severity: 'error',
130
+ };
131
+
132
+ export const versionWithoutExamples: TopoAwareWardenRule = {
133
+ checkTopo(topo, context) {
134
+ return trailEntries(graphFor(topo, context)).flatMap((entry) =>
135
+ Object.entries(entry.versions ?? {}).flatMap(([version, historical]) =>
136
+ !isArchived(historical) && (historical.exampleCount ?? 0) === 0
137
+ ? [
138
+ topoDiagnostic(
139
+ 'version-without-examples',
140
+ 'warn',
141
+ `Trail "${versionEntryName(entry.id, version)}" is a live historical version without examples. Add version-entry examples or archive the entry if it no longer participates in runtime negotiation.`
142
+ ),
143
+ ]
144
+ : []
145
+ )
146
+ );
147
+ },
148
+ description:
149
+ 'Warn when live historical trail version entries do not include examples.',
150
+ name: 'version-without-examples',
151
+ severity: 'warn',
152
+ };
153
+
154
+ export const pendingForce: TopoAwareWardenRule = {
155
+ checkTopo(topo, context) {
156
+ const graph = graphFor(topo, context);
157
+ const diagnostics = new Map<string, WardenDiagnostic>();
158
+ for (const entry of trailEntries(graph)) {
159
+ for (const force of entry.forces ?? []) {
160
+ diagnostics.set(forceKey(force), pendingForceDiagnostic(force));
161
+ }
162
+ }
163
+ for (const force of graph.forces ?? []) {
164
+ diagnostics.set(forceKey(force), pendingForceDiagnostic(force));
165
+ }
166
+ return [...diagnostics.values()];
167
+ },
168
+ description:
169
+ 'Warn when graph-only force audit events remain attached to trail entries or the topo graph.',
170
+ name: 'pending-force',
171
+ severity: 'warn',
172
+ };
@@ -1,14 +1,201 @@
1
+ import type { Intent, Topo } from '@ontrails/core';
2
+ import type { TopoGraph } from '@ontrails/topographer';
3
+
4
+ import type { WardenDepth } from '../config.js';
5
+ import type { WardenImportResolution } from '../resolve.js';
6
+ import type { WardenPublicWorkspace } from '../workspaces.js';
7
+
1
8
  /**
2
9
  * Severity level for warden diagnostics.
3
10
  */
4
11
  export type WardenSeverity = 'error' | 'warn';
5
12
 
13
+ /**
14
+ * Execution tier for a Warden rule.
15
+ *
16
+ * Tier names describe the narrowest runtime shape that can answer the rule's
17
+ * question. They do not change ownership: source-static rules can still be
18
+ * durable public Warden doctrine.
19
+ */
20
+ export type WardenRuleTier =
21
+ | 'advisory'
22
+ | 'drift'
23
+ | 'project-static'
24
+ | 'source-static'
25
+ | 'topo-aware';
26
+
27
+ /**
28
+ * Context where a Warden rule applies.
29
+ */
30
+ export type WardenRuleScope =
31
+ | 'advisory'
32
+ | 'extension'
33
+ | 'external'
34
+ | 'internal'
35
+ | 'repo-local'
36
+ | 'temporary';
37
+
38
+ /**
39
+ * Lifecycle state for a Warden rule.
40
+ */
41
+ export type WardenRuleLifecycleState = 'deprecated' | 'durable' | 'temporary';
42
+
43
+ /**
44
+ * Queryable concern dimension for Warden rule metadata.
45
+ */
46
+ export type WardenRuleConcern =
47
+ | 'composition'
48
+ | 'general'
49
+ | 'lifecycle'
50
+ | 'meta'
51
+ | 'permits'
52
+ | 'resources'
53
+ | 'results'
54
+ | 'signals';
55
+
56
+ /**
57
+ * Lifecycle metadata for a Warden rule.
58
+ */
59
+ export interface WardenRuleLifecycle {
60
+ /** Current lifecycle state. */
61
+ readonly state: WardenRuleLifecycleState;
62
+ /** Required for temporary or deprecated rules. */
63
+ readonly retireWhen?: string | undefined;
64
+ }
65
+
66
+ /**
67
+ * Documentation or reference target for Warden remediation guidance.
68
+ */
69
+ export interface WardenGuidanceLink {
70
+ /** Human-readable link label. */
71
+ readonly label: string;
72
+ /** Repository-relative documentation path, when the target is in-tree. */
73
+ readonly path?: string | undefined;
74
+ /** External documentation URL, when the target is outside the repo. */
75
+ readonly url?: string | undefined;
76
+ }
77
+
78
+ /**
79
+ * Structured remediation guidance that can be rendered for humans or projected
80
+ * into agent-facing manifests without scraping diagnostic prose.
81
+ */
82
+ export interface WardenGuidance {
83
+ /** Concise next step for the finding or rule. */
84
+ readonly summary: string;
85
+ /** Ordered remediation steps, when the rule benefits from more detail. */
86
+ readonly steps?: readonly string[] | undefined;
87
+ /** Reference docs that explain the invariant. */
88
+ readonly docs?: readonly WardenGuidanceLink[] | undefined;
89
+ /** Example commands. These are guidance examples, not autofix contracts. */
90
+ readonly commands?: readonly string[] | undefined;
91
+ /** Related rule identifiers that help agents navigate nearby doctrine. */
92
+ readonly relatedRules?: readonly string[] | undefined;
93
+ }
94
+
95
+ /**
96
+ * Transform class a fix belongs to.
97
+ *
98
+ * Names the kind of mechanical change so agents, the guide, and downstream
99
+ * regrades can route by class. `term-rewrite` is the durable name for retired
100
+ * vocabulary renames (`vocab-cutover` is historical wording only).
101
+ */
102
+ export type WardenFixClass = 'term-rewrite';
103
+
104
+ /**
105
+ * How safe a fix is to apply without human review.
106
+ *
107
+ * - `safe`: a deterministic, scope-correct source edit `warden --fix` may apply
108
+ * automatically.
109
+ * - `review`: the change is understood but needs human judgement (ambiguous
110
+ * span, removal with no mechanical replacement, semantic follow-up). The
111
+ * finding stays reported; `warden --fix` never applies it.
112
+ */
113
+ export type WardenFixSafety = 'review' | 'safe';
114
+
115
+ /**
116
+ * A concrete, half-open source edit `[start, end)` replaced by `replacement`.
117
+ *
118
+ * Offsets are JavaScript string indices into the exact analyzed source text,
119
+ * matching `String.prototype.slice()` and the `offsetToLine` helper the rules
120
+ * already use. Carrying an explicit span (not just a line) lets `warden --fix`
121
+ * apply edits deterministically and
122
+ * last-to-first without re-parsing.
123
+ */
124
+ export interface WardenFixEdit {
125
+ /** Inclusive start offset into the source. */
126
+ readonly start: number;
127
+ /** Exclusive end offset into the source. */
128
+ readonly end: number;
129
+ /** Text that replaces the `[start, end)` span. */
130
+ readonly replacement: string;
131
+ }
132
+
133
+ /**
134
+ * Per-finding fix metadata attached to a diagnostic.
135
+ *
136
+ * Authored on the diagnostic at construction because only the rule that matched
137
+ * knows the concrete span. `warden --fix` applies `edits` only when
138
+ * `safety` is `safe`; `review` fixes stay reported with their guidance so a
139
+ * human (or a downstream regrade) can resolve them.
140
+ */
141
+ export interface WardenFix {
142
+ /** Transform class this fix belongs to. */
143
+ readonly class: WardenFixClass;
144
+ /** Whether `warden --fix` may apply this automatically. */
145
+ readonly safety: WardenFixSafety;
146
+ /** Source edits to apply, when `safety` is `safe`. Empty for review-only. */
147
+ readonly edits?: readonly WardenFixEdit[] | undefined;
148
+ /** Why the fix is needed and what it changes, for humans and migration notes. */
149
+ readonly reason: string;
150
+ /** Optional pointer to a fixture or example demonstrating the fix. */
151
+ readonly fixture?: string | undefined;
152
+ }
153
+
154
+ /**
155
+ * Per-rule fix capability, projected into the guide/manifest.
156
+ *
157
+ * Declares that a rule can emit {@link WardenFix} metadata and the default
158
+ * safety for its fixes, so `warden --help`, the guide, and agent surfaces can
159
+ * advertise fix availability without a finding in hand. Concrete edits still
160
+ * live on each diagnostic's {@link WardenFix}.
161
+ */
162
+ export interface WardenFixCapability {
163
+ /** Transform class the rule's fixes belong to. */
164
+ readonly class: WardenFixClass;
165
+ /** Default safety for fixes this rule emits. */
166
+ readonly safety: WardenFixSafety;
167
+ }
168
+
169
+ /**
170
+ * Stable metadata used to classify Warden rules before dispatch filtering.
171
+ */
172
+ export interface WardenRuleMetadata {
173
+ /** Cumulative Warden depth where this rule first becomes relevant. */
174
+ readonly depth: WardenDepth;
175
+ /** Queryable rule concern for agent-facing surfaces. */
176
+ readonly concern: WardenRuleConcern;
177
+ /** One-line invariant the rule protects. */
178
+ readonly invariant: string;
179
+ /** Rule lifecycle. */
180
+ readonly lifecycle: WardenRuleLifecycle;
181
+ /** Where the rule applies. */
182
+ readonly scope: WardenRuleScope;
183
+ /** Narrowest Warden tier that can answer the rule. */
184
+ readonly tier: WardenRuleTier;
185
+ /** Structured remediation guidance for diagnostics emitted by this rule. */
186
+ readonly guidance?: WardenGuidance | undefined;
187
+ /** Declares that this rule can emit fix metadata, for guide projection. */
188
+ readonly fix?: WardenFixCapability | undefined;
189
+ }
190
+
6
191
  /**
7
192
  * A single diagnostic reported by a warden rule.
8
193
  */
9
194
  export interface WardenDiagnostic {
10
195
  /** Rule identifier, e.g. "no-throw-in-implementation" */
11
196
  readonly rule: string;
197
+ /** Optional rule-local diagnostic code for checks with multiple stable findings. */
198
+ readonly code?: string | undefined;
12
199
  /** Severity level */
13
200
  readonly severity: WardenSeverity;
14
201
  /** Human-readable message describing the violation */
@@ -17,13 +204,20 @@ export interface WardenDiagnostic {
17
204
  readonly line: number;
18
205
  /** File path that was analyzed */
19
206
  readonly filePath: string;
207
+ /** Topo/app identity for diagnostics emitted during multi-topo runs. */
208
+ readonly topoName?: string | undefined;
209
+ /** Optional finding-level guidance. Defaults from rule metadata when absent. */
210
+ readonly guidance?: WardenGuidance | undefined;
211
+ /** Optional structured fix for this finding. `warden --fix` applies safe edits. */
212
+ readonly fix?: WardenFix | undefined;
20
213
  }
21
214
 
22
215
  /**
23
- * A warden rule is a function that analyzes source code and returns diagnostics.
216
+ * A warden rule analyzes one source file and returns diagnostics.
24
217
  *
25
- * Rules use string/regex analysis (not full AST parsing) to detect patterns
26
- * that violate Trails conventions.
218
+ * Rules should prefer structured AST helpers when they inspect TypeScript
219
+ * syntax. Simple string checks remain acceptable when the authored rule is
220
+ * explicitly about text that is not parseable syntax, such as generated output.
27
221
  */
28
222
  export interface WardenRule {
29
223
  /** Unique rule identifier */
@@ -32,6 +226,8 @@ export interface WardenRule {
32
226
  readonly severity: WardenSeverity;
33
227
  /** Human-readable description of what the rule enforces */
34
228
  readonly description: string;
229
+ /** Optional inline classification. Built-ins are classified by registry. */
230
+ readonly metadata?: WardenRuleMetadata | undefined;
35
231
  /** Run the rule against source code and return any diagnostics */
36
232
  readonly check: (
37
233
  sourceCode: string,
@@ -40,13 +236,54 @@ export interface WardenRule {
40
236
  }
41
237
 
42
238
  /**
43
- * Options for cross-file rules that need knowledge of all trail IDs in a project.
239
+ * Options for compose-file rules that need knowledge of all trail IDs in a project.
44
240
  */
45
241
  export interface ProjectContext {
242
+ /** All known contour names in the project. */
243
+ readonly knownContourIds?: ReadonlySet<string>;
244
+ /** Store table IDs used with the CRUD factory across the project. */
245
+ readonly crudTableIds?: ReadonlySet<string>;
46
246
  /** All known trail IDs in the project */
47
247
  readonly knownTrailIds: ReadonlySet<string>;
48
- /** All trail IDs referenced as detour targets across the project */
49
- readonly detourTargetTrailIds?: ReadonlySet<string>;
248
+ /** Declared contour references keyed by source contour name. */
249
+ readonly contourReferencesByName?: ReadonlyMap<string, readonly string[]>;
250
+ /** All known resource IDs in the project */
251
+ readonly knownResourceIds?: ReadonlySet<string>;
252
+ /** All known signal IDs in the project */
253
+ readonly knownSignalIds?: ReadonlySet<string>;
254
+ /** All trail IDs referenced by declared composes arrays across the project. */
255
+ readonly composeTargetTrailIds?: ReadonlySet<string>;
256
+ /** Signal IDs referenced by trail `on` arrays across the project. */
257
+ readonly onTargetSignalIds?: ReadonlySet<string>;
258
+ /** Store table IDs used with reconcile trails across the project. */
259
+ readonly reconcileTableIds?: ReadonlySet<string>;
260
+ /** Resolved import facts keyed by importer file path across the project. */
261
+ readonly importResolutionsByFile?: ReadonlyMap<
262
+ string,
263
+ readonly WardenImportResolution[]
264
+ >;
265
+ /** Resolved docs/specifier facts keyed by documentation file path. */
266
+ readonly documentedImportResolutionsByFile?: ReadonlyMap<
267
+ string,
268
+ readonly WardenImportResolution[]
269
+ >;
270
+ /** Non-private published @ontrails workspaces discovered from the root manifest. */
271
+ readonly publicWorkspaces?: ReadonlyMap<string, WardenPublicWorkspace>;
272
+ /** Normalized trail intents by trail ID across the project. */
273
+ readonly trailIntentsById?: ReadonlyMap<string, Intent>;
274
+ /**
275
+ * CRUD operation coverage per entity aggregated across the project.
276
+ *
277
+ * Keys are stable entity IDs (authored contour names, `imported:<local>`
278
+ * sentinels for contours imported from another module, or store-table IDs
279
+ * produced by `deriveStoreTableId`). Values are the set of CRUD operations
280
+ * (`create`, `read`, `update`, `delete`, `list`) observed for that entity.
281
+ *
282
+ * Enables compose-file completeness evaluation so one-file-per-operation
283
+ * layouts (e.g. separate `create.ts`, `read.ts`) do not trip file-scoped
284
+ * coverage warnings.
285
+ */
286
+ readonly crudCoverageByEntity?: ReadonlyMap<string, ReadonlySet<string>>;
50
287
  }
51
288
 
52
289
  /**
@@ -60,3 +297,30 @@ export interface ProjectAwareWardenRule extends WardenRule {
60
297
  context: ProjectContext
61
298
  ) => readonly WardenDiagnostic[];
62
299
  }
300
+
301
+ /**
302
+ * A topo-aware warden rule inspects the compiled runtime trail graph —
303
+ * actual `Trail` objects with resolved types, accessor shapes, detour
304
+ * declarations, `pattern` field values, and other runtime-only data that
305
+ * is unavailable to AST-based rules.
306
+ *
307
+ * Unlike `WardenRule` and `ProjectAwareWardenRule`, which analyze source
308
+ * code on a per-file basis, a `TopoAwareWardenRule` runs once per topo
309
+ * and returns diagnostics spanning the whole graph. A rule file must
310
+ * implement exactly one of the three rule kinds.
311
+ */
312
+ export interface TopoAwareWardenRule {
313
+ /** Unique rule identifier */
314
+ readonly name: string;
315
+ /** Default severity */
316
+ readonly severity: WardenSeverity;
317
+ /** Human-readable description of what the rule enforces */
318
+ readonly description: string;
319
+ /** Optional inline classification. Built-ins are classified by registry. */
320
+ readonly metadata?: WardenRuleMetadata | undefined;
321
+ /** Run the rule against the resolved topo and return any diagnostics */
322
+ readonly checkTopo: (
323
+ topo: Topo,
324
+ context?: { readonly graph?: TopoGraph | undefined } | undefined
325
+ ) => readonly WardenDiagnostic[] | Promise<readonly WardenDiagnostic[]>;
326
+ }
@@ -0,0 +1,84 @@
1
+ import type { ActivationSourceKind, Topo } from '@ontrails/core';
2
+
3
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
4
+
5
+ const RULE_NAME = 'unmaterialized-activation-source';
6
+ const TOPO_FILE = '<topo>';
7
+
8
+ const MATERIALIZED_SOURCE_KINDS = new Set<ActivationSourceKind>([
9
+ 'schedule',
10
+ 'signal',
11
+ 'webhook',
12
+ ]);
13
+
14
+ const PENDING_SOURCE_KINDS = new Set<ActivationSourceKind>();
15
+
16
+ interface SourceConsumers {
17
+ readonly id: string;
18
+ readonly kind: ActivationSourceKind;
19
+ readonly key: string;
20
+ readonly trailIds: readonly string[];
21
+ }
22
+
23
+ const sourceKey = (kind: ActivationSourceKind, id: string): string =>
24
+ `${kind}:${id}`;
25
+
26
+ const sortedUnique = (values: Iterable<string>): readonly string[] =>
27
+ [...new Set(values)].toSorted();
28
+
29
+ const collectSourceConsumers = (topo: Topo): readonly SourceConsumers[] => {
30
+ const consumersBySource = new Map<
31
+ string,
32
+ {
33
+ readonly id: string;
34
+ readonly kind: ActivationSourceKind;
35
+ readonly trailIds: Set<string>;
36
+ }
37
+ >();
38
+
39
+ for (const trail of topo.list()) {
40
+ for (const activation of trail.activationSources) {
41
+ const key = sourceKey(activation.source.kind, activation.source.id);
42
+ const current =
43
+ consumersBySource.get(key) ??
44
+ ({
45
+ id: activation.source.id,
46
+ kind: activation.source.kind,
47
+ trailIds: new Set<string>(),
48
+ } as const);
49
+ current.trailIds.add(trail.id);
50
+ consumersBySource.set(key, current);
51
+ }
52
+ }
53
+
54
+ return [...consumersBySource.entries()]
55
+ .map(([key, source]) => ({
56
+ id: source.id,
57
+ key,
58
+ kind: source.kind,
59
+ trailIds: sortedUnique(source.trailIds),
60
+ }))
61
+ .toSorted((a, b) => a.key.localeCompare(b.key));
62
+ };
63
+
64
+ const isUnmaterialized = (kind: ActivationSourceKind): boolean =>
65
+ PENDING_SOURCE_KINDS.has(kind) && !MATERIALIZED_SOURCE_KINDS.has(kind);
66
+
67
+ const buildDiagnostic = (source: SourceConsumers): WardenDiagnostic => ({
68
+ filePath: TOPO_FILE,
69
+ line: 1,
70
+ message: `Activation source "${source.id}" of kind "${source.kind}" activates trail${source.trailIds.length === 1 ? '' : 's'} ${source.trailIds.map((id) => `"${id}"`).join(', ')} but no built-in materializer is available in this stack. Add the materializer before relying on runtime delivery, or defer the source declaration until the materializer lands.`,
71
+ rule: RULE_NAME,
72
+ severity: 'warn',
73
+ });
74
+
75
+ export const unmaterializedActivationSource: TopoAwareWardenRule = {
76
+ checkTopo: (topo) =>
77
+ collectSourceConsumers(topo)
78
+ .filter((source) => isUnmaterialized(source.kind))
79
+ .map((source) => buildDiagnostic(source)),
80
+ description:
81
+ 'Warn when declared activation sources do not have an available runtime materializer.',
82
+ name: RULE_NAME,
83
+ severity: 'warn',
84
+ };