@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,464 @@
1
+ import type {
2
+ DiagnosticSeverity,
3
+ Intent,
4
+ RuleDiagnosticBase,
5
+ ScanTargets,
6
+ SurfaceBindings,
7
+ Topo,
8
+ } from '@ontrails/core';
9
+ import type {
10
+ TopoGraph,
11
+ UnownedWorkspaceLockObservation,
12
+ } from '@ontrails/topography';
13
+
14
+ import type { WardenDepth } from '../config.js';
15
+ import type { WardenImportResolution } from '../resolve.js';
16
+ import type { WardenPublicWorkspace } from '../workspaces.js';
17
+ import type { GovernedVocabularyHistoryProvenance } from './retired-vocabulary.js';
18
+
19
+ /**
20
+ * Severity level for warden diagnostics.
21
+ */
22
+ export type WardenSeverity = DiagnosticSeverity;
23
+
24
+ /**
25
+ * Execution tier for a Warden rule.
26
+ *
27
+ * Tier names describe the narrowest runtime shape that can answer the rule's
28
+ * question. They do not change ownership: source-static rules can still be
29
+ * durable public Warden doctrine.
30
+ */
31
+ export type WardenRuleTier =
32
+ | 'advisory'
33
+ | 'drift'
34
+ | 'project-static'
35
+ | 'source-static'
36
+ | 'topo-aware';
37
+
38
+ /**
39
+ * Context where a Warden rule applies.
40
+ */
41
+ export type WardenRuleScope =
42
+ | 'advisory'
43
+ | 'extension'
44
+ | 'external'
45
+ | 'internal'
46
+ | 'repo-local'
47
+ | 'temporary';
48
+
49
+ /**
50
+ * Lifecycle state for a Warden rule.
51
+ */
52
+ export type WardenRuleLifecycleState = 'deprecated' | 'durable' | 'temporary';
53
+
54
+ /**
55
+ * Queryable concern dimension for Warden rule metadata.
56
+ */
57
+ export type WardenRuleConcern =
58
+ | 'composition'
59
+ | 'general'
60
+ | 'lifecycle'
61
+ | 'meta'
62
+ | 'permits'
63
+ | 'resources'
64
+ | 'results'
65
+ | 'signals';
66
+
67
+ /**
68
+ * Lifecycle metadata for a Warden rule.
69
+ */
70
+ export interface WardenRuleLifecycle {
71
+ /** Current lifecycle state. */
72
+ readonly state: WardenRuleLifecycleState;
73
+ /** Required for temporary or deprecated rules. */
74
+ readonly retireWhen?: string | undefined;
75
+ }
76
+
77
+ /**
78
+ * Documentation or reference target for Warden remediation guidance.
79
+ */
80
+ export interface WardenGuidanceLink {
81
+ /** Human-readable link label. */
82
+ readonly label: string;
83
+ /** Repository-relative documentation path, when the target is in-tree. */
84
+ readonly path?: string | undefined;
85
+ /** External documentation URL, when the target is outside the repo. */
86
+ readonly url?: string | undefined;
87
+ }
88
+
89
+ /**
90
+ * Structured remediation guidance that can be rendered for humans or rendered
91
+ * into agent-facing manifests without scraping diagnostic prose.
92
+ */
93
+ export interface WardenGuidance {
94
+ /** Concise next step for the finding or rule. */
95
+ readonly summary: string;
96
+ /** Ordered remediation steps, when the rule benefits from more detail. */
97
+ readonly steps?: readonly string[] | undefined;
98
+ /** Reference docs that explain the invariant. */
99
+ readonly docs?: readonly WardenGuidanceLink[] | undefined;
100
+ /** Example commands. These are guidance examples, not autofix contracts. */
101
+ readonly commands?: readonly string[] | undefined;
102
+ /** Related rule identifiers that help agents navigate nearby doctrine. */
103
+ readonly relatedRules?: readonly string[] | undefined;
104
+ }
105
+
106
+ /**
107
+ * Transform class a fix belongs to.
108
+ *
109
+ * Names the kind of mechanical change so agents, the guide, and downstream
110
+ * regrades can route by class. `term-rewrite` is the durable name for retired
111
+ * vocabulary renames (`vocab-cutover` is historical wording only).
112
+ * `export-restructure` names structural export rewrites — the finding is not
113
+ * a rename but a reshaping of a module export (for example wrapping a legacy
114
+ * CLI alias map into a `surfaceOverlay({ cli })` entry inside
115
+ * `trailsOverlays`), so downstream Regrade routes it to a structural
116
+ * transform class instead of a span replacement.
117
+ */
118
+ export type WardenFixClass = 'export-restructure' | 'term-rewrite';
119
+
120
+ /**
121
+ * How safe a fix is to apply without human review.
122
+ *
123
+ * - `safe`: a deterministic, scope-correct source edit `warden --fix` may apply
124
+ * automatically.
125
+ * - `review`: the change is understood but needs human judgement (ambiguous
126
+ * span, removal with no mechanical replacement, semantic follow-up). The
127
+ * finding stays reported; `warden --fix` never applies it.
128
+ */
129
+ export type WardenFixSafety = 'review' | 'safe';
130
+
131
+ /**
132
+ * A concrete, half-open source edit `[start, end)` replaced by `replacement`.
133
+ *
134
+ * Offsets are JavaScript string indices into the exact analyzed source text,
135
+ * matching `String.prototype.slice()` and the `offsetToLine` helper the rules
136
+ * already use. Carrying an explicit span (not just a line) lets `warden --fix`
137
+ * apply edits deterministically and
138
+ * last-to-first without re-parsing.
139
+ */
140
+ export interface WardenFixEdit {
141
+ /** Inclusive start offset into the source. */
142
+ readonly start: number;
143
+ /** Exclusive end offset into the source. */
144
+ readonly end: number;
145
+ /** Text that replaces the `[start, end)` span. */
146
+ readonly replacement: string;
147
+ }
148
+
149
+ /**
150
+ * Source targets a fix class can inspect when rendered into downstream tools.
151
+ *
152
+ * Warden itself decides which committed files it scans. This metadata is for
153
+ * consumers such as Regrade that need to derive a narrower collection before
154
+ * invoking the rule against an explicit downstream root.
155
+ */
156
+ export type WardenFixScanTargets = ScanTargets & {
157
+ /**
158
+ * @deprecated Compatibility bridge for Warden-backed Regrade classes that
159
+ * predate PathScope. Prefer Regrade collection scope for new callers.
160
+ */
161
+ readonly ignoredDirectories?: readonly string[];
162
+ };
163
+
164
+ /**
165
+ * Per-finding fix metadata attached to a diagnostic.
166
+ *
167
+ * Authored on the diagnostic at construction because only the rule that matched
168
+ * knows the concrete span. `warden --fix` applies `edits` only when
169
+ * `safety` is `safe`; `review` fixes stay reported with their guidance so a
170
+ * human (or a downstream regrade) can resolve them.
171
+ */
172
+ export interface WardenFix {
173
+ /** Transform class this fix belongs to. */
174
+ readonly class: WardenFixClass;
175
+ /** Whether `warden --fix` may apply this automatically. */
176
+ readonly safety: WardenFixSafety;
177
+ /** Source edits to apply, when `safety` is `safe`. Empty for review-only. */
178
+ readonly edits?: readonly WardenFixEdit[] | undefined;
179
+ /** Why the fix is needed and what it changes, for humans and migration notes. */
180
+ readonly reason: string;
181
+ /** Optional pointer to a fixture or example demonstrating the fix. */
182
+ readonly fixture?: string | undefined;
183
+ }
184
+
185
+ /**
186
+ * Per-rule fix capability, rendered into the guide/manifest.
187
+ *
188
+ * Declares that a rule can emit {@link WardenFix} metadata and the default
189
+ * safety for its fixes, so `warden --help`, the guide, and agent surfaces can
190
+ * advertise fix availability without a finding in hand. Concrete edits still
191
+ * live on each diagnostic's {@link WardenFix}.
192
+ */
193
+ export interface WardenFixCapability {
194
+ /** Transform class the rule's fixes belong to. */
195
+ readonly class: WardenFixClass;
196
+ /** Default safety for fixes this rule emits. */
197
+ readonly safety: WardenFixSafety;
198
+ /** Downstream scan targets for tools that render this fix capability. */
199
+ readonly scanTargets?: WardenFixScanTargets | undefined;
200
+ }
201
+
202
+ /**
203
+ * Stable metadata used to classify Warden rules before dispatch filtering.
204
+ */
205
+ export interface WardenRuleMetadata {
206
+ /** Cumulative Warden depth where this rule first becomes relevant. */
207
+ readonly depth: WardenDepth;
208
+ /** Queryable rule concern for agent-facing surfaces. */
209
+ readonly concern: WardenRuleConcern;
210
+ /** One-line invariant the rule protects. */
211
+ readonly invariant: string;
212
+ /** Rule lifecycle. */
213
+ readonly lifecycle: WardenRuleLifecycle;
214
+ /** Where the rule applies. */
215
+ readonly scope: WardenRuleScope;
216
+ /** Narrowest Warden tier that can answer the rule. */
217
+ readonly tier: WardenRuleTier;
218
+ /** Structured remediation guidance for diagnostics emitted by this rule. */
219
+ readonly guidance?: WardenGuidance | undefined;
220
+ /** Declares that this rule can emit fix metadata, for guide rendering. */
221
+ readonly fix?: WardenFixCapability | undefined;
222
+ }
223
+
224
+ /**
225
+ * A single diagnostic reported by a warden rule.
226
+ */
227
+ export interface WardenDiagnostic extends RuleDiagnosticBase {
228
+ /** Rule identifier, e.g. "no-throw-in-implementation" */
229
+ readonly rule: string;
230
+ /** Optional rule-local diagnostic code for checks with multiple stable findings. */
231
+ readonly code?: string | undefined;
232
+ /** Severity level */
233
+ readonly severity: WardenSeverity;
234
+ /** Human-readable message describing the violation */
235
+ readonly message: string;
236
+ /** 1-based line number where the violation was detected */
237
+ readonly line: number;
238
+ /** File path that was analyzed */
239
+ readonly filePath: string;
240
+ /** Topo/app identity for diagnostics emitted during multi-topo runs. */
241
+ readonly topoName?: string | undefined;
242
+ /** Optional finding-level guidance. Defaults from rule metadata when absent. */
243
+ readonly guidance?: WardenGuidance | undefined;
244
+ /** Optional structured fix for this finding. `warden --fix` applies safe edits. */
245
+ readonly fix?: WardenFix | undefined;
246
+ }
247
+
248
+ /** Public exported symbol observed from a first-party package export target. */
249
+ export interface WardenExportedSymbolDefinition {
250
+ /** Exported binding name. */
251
+ readonly name: string;
252
+ /** Export declaration kind that introduced the binding. */
253
+ readonly kind:
254
+ | 'class'
255
+ | 'const'
256
+ | 'enum'
257
+ | 'export'
258
+ | 'function'
259
+ | 'interface'
260
+ | 'type';
261
+ /** Absolute or runner-provided file path containing the definition. */
262
+ readonly filePath: string;
263
+ /** 1-based source line where the exported definition starts. */
264
+ readonly line: number;
265
+ /** First-party workspace package that owns the definition. */
266
+ readonly workspaceName: string;
267
+ /** Filesystem root of the owning workspace. */
268
+ readonly workspaceRoot: string;
269
+ }
270
+
271
+ /**
272
+ * A warden rule analyzes one source file and returns diagnostics.
273
+ *
274
+ * Rules should prefer structured AST helpers when they inspect TypeScript
275
+ * syntax. Simple string checks remain acceptable when the authored rule is
276
+ * explicitly about text that is not parseable syntax, such as generated output.
277
+ */
278
+ export interface WardenRule {
279
+ /** Unique rule identifier */
280
+ readonly name: string;
281
+ /** Default severity */
282
+ readonly severity: WardenSeverity;
283
+ /** Human-readable description of what the rule enforces */
284
+ readonly description: string;
285
+ /** Optional inline classification. Built-ins are classified by registry. */
286
+ readonly metadata?: WardenRuleMetadata | undefined;
287
+ /** Source file kinds this rule explicitly supports beyond TypeScript. */
288
+ readonly sourceKinds?:
289
+ | readonly ('documentation' | 'text' | 'typescript')[]
290
+ | undefined;
291
+ /** Run the rule against source code and return any diagnostics */
292
+ readonly check: (
293
+ sourceCode: string,
294
+ filePath: string
295
+ ) => readonly WardenDiagnostic[];
296
+ }
297
+
298
+ /**
299
+ * One app's authored `surfaces` overlay `mcp` bindings plus the trail ids
300
+ * that scope which source files the bindings can be compared against.
301
+ */
302
+ export interface AuthoredMcpSurfaceBindingSet {
303
+ /** Stable app/topo label the bindings were authored for. */
304
+ readonly appName: string;
305
+ /** The `mcp` bindings from the app's `surfaces` overlay. */
306
+ readonly bindings: SurfaceBindings;
307
+ /** Trail ids registered in the owning app's topo. */
308
+ readonly trailIds: readonly string[];
309
+ }
310
+
311
+ /**
312
+ * Options for compose-file rules that need knowledge of all trail IDs in a project.
313
+ */
314
+ export interface ProjectContext {
315
+ /** Observation-only lock artifacts not owned by configured workspace apps. */
316
+ readonly unownedWorkspaceLocks?:
317
+ | readonly UnownedWorkspaceLockObservation[]
318
+ | undefined;
319
+ /** Whether this project owns the governed transition registry and must prove completed migrations. */
320
+ readonly governedVocabularyHistoryRequired?: boolean | undefined;
321
+ /** Validated committed Regrade history evidence keyed by governed transition. */
322
+ readonly governedVocabularyHistoryByTransitionId?:
323
+ | ReadonlyMap<string, GovernedVocabularyHistoryEvidence>
324
+ | undefined;
325
+ /** Invalid committed history artifacts observed while building context. */
326
+ readonly governedVocabularyHistoryIssues?:
327
+ | readonly GovernedVocabularyHistoryIssue[]
328
+ | undefined;
329
+ /**
330
+ * Per-app authored `mcp` surface bindings, resolved from the project's
331
+ * topo targets (the serialized graph overlays when available, else the
332
+ * app-module overlay registrations). Each set carries the owning app's
333
+ * trail ids so source rules can attribute a call-site surface option to
334
+ * the app whose overlay authored the default before comparing.
335
+ */
336
+ readonly authoredMcpSurfaceBindingSets?:
337
+ | readonly AuthoredMcpSurfaceBindingSet[]
338
+ | undefined;
339
+ /** All known entity names in the project. */
340
+ readonly knownEntityIds?: ReadonlySet<string>;
341
+ /** Store table IDs used with the CRUD factory across the project. */
342
+ readonly crudTableIds?: ReadonlySet<string>;
343
+ /** All known trail IDs in the project */
344
+ readonly knownTrailIds: ReadonlySet<string>;
345
+ /** Trail IDs registered in configured app topo targets. */
346
+ readonly topoTrailIds?: ReadonlySet<string>;
347
+ /** Declared entity references keyed by source entity name. */
348
+ readonly entityReferencesByName?: ReadonlyMap<string, readonly string[]>;
349
+ /** All known resource IDs in the project */
350
+ readonly knownResourceIds?: ReadonlySet<string>;
351
+ /** All known signal IDs in the project */
352
+ readonly knownSignalIds?: ReadonlySet<string>;
353
+ /** All trail IDs referenced by declared composes arrays across the project. */
354
+ readonly composeTargetTrailIds?: ReadonlySet<string>;
355
+ /** Signal IDs referenced by trail `on` arrays across the project. */
356
+ readonly onTargetSignalIds?: ReadonlySet<string>;
357
+ /** Store table IDs used with reconcile trails across the project. */
358
+ readonly reconcileTableIds?: ReadonlySet<string>;
359
+ /** Resolved import facts keyed by importer file path across the project. */
360
+ readonly importResolutionsByFile?: ReadonlyMap<
361
+ string,
362
+ readonly WardenImportResolution[]
363
+ >;
364
+ /** Resolved docs/specifier facts keyed by documentation file path. */
365
+ readonly documentedImportResolutionsByFile?: ReadonlyMap<
366
+ string,
367
+ readonly WardenImportResolution[]
368
+ >;
369
+ /** Non-private published @ontrails workspaces discovered from the root manifest. */
370
+ readonly publicWorkspaces?: ReadonlyMap<string, WardenPublicWorkspace>;
371
+ /** Public package export definitions grouped by exported name. */
372
+ readonly exportedSymbolDefinitionsByName?: ReadonlyMap<
373
+ string,
374
+ readonly WardenExportedSymbolDefinition[]
375
+ >;
376
+ /** Normalized trail intents by trail ID across the project. */
377
+ readonly trailIntentsById?: ReadonlyMap<string, Intent>;
378
+ /**
379
+ * CRUD operation coverage per entity aggregated across the project.
380
+ *
381
+ * Keys are stable entity IDs (authored entity names, `imported:<local>`
382
+ * sentinels for entities imported from another module, or store-table IDs
383
+ * produced by `deriveStoreTableId`). Values are the set of CRUD operations
384
+ * (`create`, `read`, `update`, `delete`, `list`) observed for that entity.
385
+ *
386
+ * Enables compose-file completeness evaluation so one-file-per-operation
387
+ * layouts (e.g. separate `create.ts`, `read.ts`) do not trip file-scoped
388
+ * coverage warnings.
389
+ */
390
+ readonly crudCoverageByEntity?: ReadonlyMap<string, ReadonlySet<string>>;
391
+ }
392
+
393
+ export interface GovernedVocabularyHistoryEvidence {
394
+ readonly caseSensitive: boolean;
395
+ readonly id: string;
396
+ readonly latestFormJudgments: readonly GovernedVocabularyHistoryFormJudgment[];
397
+ readonly path: string;
398
+ readonly provenance?: GovernedVocabularyHistoryProvenance;
399
+ readonly runCount: number;
400
+ readonly transitionId: string;
401
+ }
402
+
403
+ /** One compact vocabulary-form judgment preserved in committed Regrade history. */
404
+ export interface GovernedVocabularyHistoryFormJudgment {
405
+ readonly disposition: string;
406
+ readonly form: string;
407
+ readonly reason?: string | undefined;
408
+ readonly representative?:
409
+ | {
410
+ readonly line: number;
411
+ readonly path: string;
412
+ }
413
+ | undefined;
414
+ readonly target?: string | undefined;
415
+ }
416
+
417
+ export interface GovernedVocabularyHistoryIssue {
418
+ readonly message: string;
419
+ readonly path: string;
420
+ readonly transitionId?: string;
421
+ }
422
+
423
+ /**
424
+ * A project-aware rule that requires knowledge of all trail IDs.
425
+ */
426
+ export interface ProjectAwareWardenRule extends WardenRule {
427
+ /** Run project-wide validation once, independently of any source file. */
428
+ readonly checkProject?: (
429
+ context: ProjectContext
430
+ ) => readonly WardenDiagnostic[];
431
+ /** Run the rule with project-level context */
432
+ readonly checkWithContext: (
433
+ sourceCode: string,
434
+ filePath: string,
435
+ context: ProjectContext
436
+ ) => readonly WardenDiagnostic[];
437
+ }
438
+
439
+ /**
440
+ * A topo-aware warden rule inspects the compiled runtime trail graph —
441
+ * actual `Trail` objects with resolved types, accessor shapes, detour
442
+ * declarations, `pattern` field values, and other runtime-only data that
443
+ * is unavailable to AST-based rules.
444
+ *
445
+ * Unlike `WardenRule` and `ProjectAwareWardenRule`, which analyze source
446
+ * code on a per-file basis, a `TopoAwareWardenRule` runs once per topo
447
+ * and returns diagnostics spanning the whole graph. A rule file must
448
+ * implement exactly one of the three rule kinds.
449
+ */
450
+ export interface TopoAwareWardenRule {
451
+ /** Unique rule identifier */
452
+ readonly name: string;
453
+ /** Default severity */
454
+ readonly severity: WardenSeverity;
455
+ /** Human-readable description of what the rule enforces */
456
+ readonly description: string;
457
+ /** Optional inline classification. Built-ins are classified by registry. */
458
+ readonly metadata?: WardenRuleMetadata | undefined;
459
+ /** Run the rule against the resolved topo and return any diagnostics */
460
+ readonly checkTopo: (
461
+ topo: Topo,
462
+ context?: { readonly graph?: TopoGraph | undefined } | undefined
463
+ ) => readonly WardenDiagnostic[] | Promise<readonly WardenDiagnostic[]>;
464
+ }
@@ -0,0 +1,85 @@
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
+ 'queue',
10
+ 'schedule',
11
+ 'signal',
12
+ 'webhook',
13
+ ]);
14
+
15
+ const PENDING_SOURCE_KINDS = new Set<ActivationSourceKind>();
16
+
17
+ interface SourceConsumers {
18
+ readonly id: string;
19
+ readonly kind: ActivationSourceKind;
20
+ readonly key: string;
21
+ readonly trailIds: readonly string[];
22
+ }
23
+
24
+ const sourceKey = (kind: ActivationSourceKind, id: string): string =>
25
+ `${kind}:${id}`;
26
+
27
+ const sortedUnique = (values: Iterable<string>): readonly string[] =>
28
+ [...new Set(values)].toSorted();
29
+
30
+ const collectSourceConsumers = (topo: Topo): readonly SourceConsumers[] => {
31
+ const consumersBySource = new Map<
32
+ string,
33
+ {
34
+ readonly id: string;
35
+ readonly kind: ActivationSourceKind;
36
+ readonly trailIds: Set<string>;
37
+ }
38
+ >();
39
+
40
+ for (const trail of topo.list()) {
41
+ for (const activation of trail.activationSources) {
42
+ const key = sourceKey(activation.source.kind, activation.source.id);
43
+ const current =
44
+ consumersBySource.get(key) ??
45
+ ({
46
+ id: activation.source.id,
47
+ kind: activation.source.kind,
48
+ trailIds: new Set<string>(),
49
+ } as const);
50
+ current.trailIds.add(trail.id);
51
+ consumersBySource.set(key, current);
52
+ }
53
+ }
54
+
55
+ return [...consumersBySource.entries()]
56
+ .map(([key, source]) => ({
57
+ id: source.id,
58
+ key,
59
+ kind: source.kind,
60
+ trailIds: sortedUnique(source.trailIds),
61
+ }))
62
+ .toSorted((a, b) => a.key.localeCompare(b.key));
63
+ };
64
+
65
+ const isUnmaterialized = (kind: ActivationSourceKind): boolean =>
66
+ PENDING_SOURCE_KINDS.has(kind) && !MATERIALIZED_SOURCE_KINDS.has(kind);
67
+
68
+ const buildDiagnostic = (source: SourceConsumers): WardenDiagnostic => ({
69
+ filePath: TOPO_FILE,
70
+ line: 1,
71
+ 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.`,
72
+ rule: RULE_NAME,
73
+ severity: 'warn',
74
+ });
75
+
76
+ export const unmaterializedActivationSource: TopoAwareWardenRule = {
77
+ checkTopo: (topo) =>
78
+ collectSourceConsumers(topo)
79
+ .filter((source) => isUnmaterialized(source.kind))
80
+ .map((source) => buildDiagnostic(source)),
81
+ description:
82
+ 'Warn when declared activation sources do not have an available runtime materializer.',
83
+ name: RULE_NAME,
84
+ severity: 'warn',
85
+ };