@ontrails/warden 1.0.0-beta.5 → 1.0.0-beta.50

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 (289) hide show
  1. package/CHANGELOG.md +929 -15
  2. package/README.md +133 -27
  3. package/bin/warden.ts +51 -0
  4. package/package.json +32 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1690 -114
  7. package/src/command.ts +986 -0
  8. package/src/config.ts +193 -0
  9. package/src/draft.ts +22 -0
  10. package/src/drift.ts +233 -23
  11. package/src/fix.ts +126 -0
  12. package/src/formatters.ts +78 -13
  13. package/src/guide.ts +245 -0
  14. package/src/index.ts +260 -15
  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 +79 -15
  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 +82 -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 +1496 -163
  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 +288 -13
  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 +908 -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 +12 -7
  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 +1141 -98
  58. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  59. package/src/rules/no-throw-in-implementation.ts +15 -8
  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/public-export-example-coverage.ts +561 -0
  66. package/src/rules/public-internal-deep-imports.ts +454 -0
  67. package/src/rules/public-output-schema.ts +29 -0
  68. package/src/rules/public-union-output-discriminants.ts +150 -0
  69. package/src/rules/read-intent-fires.ts +188 -0
  70. package/src/rules/reference-exists.ts +97 -0
  71. package/src/rules/registry-names.ts +169 -0
  72. package/src/rules/resolved-import-boundary.ts +146 -0
  73. package/src/rules/resource-declarations.ts +697 -0
  74. package/src/rules/resource-exists.ts +181 -0
  75. package/src/rules/resource-id-grammar.ts +65 -0
  76. package/src/rules/resource-mock-coverage.ts +115 -0
  77. package/src/rules/retired-vocabulary.ts +1407 -0
  78. package/src/rules/scan.ts +38 -25
  79. package/src/rules/scheduled-destroy-intent.ts +44 -0
  80. package/src/rules/signal-graph-coaching.ts +220 -0
  81. package/src/rules/source/composition.ts +165 -0
  82. package/src/rules/source/drafts.ts +164 -0
  83. package/src/rules/source/entities.ts +618 -0
  84. package/src/rules/source/pragmas.ts +45 -0
  85. package/src/rules/source/resources.ts +64 -0
  86. package/src/rules/source/signals.ts +397 -0
  87. package/src/rules/source/stores.ts +310 -0
  88. package/src/rules/specs.ts +9 -5
  89. package/src/rules/static-resource-accessor-preference.ts +653 -0
  90. package/src/rules/surface-overlay-coherence.ts +262 -0
  91. package/src/rules/surface-trailhead-coherence.ts +366 -0
  92. package/src/rules/trail-fork-coaching.ts +625 -0
  93. package/src/rules/trail-versioning-source.ts +1076 -0
  94. package/src/rules/trail-versioning-topo.ts +172 -0
  95. package/src/rules/trailhead-override-divergence.ts +356 -0
  96. package/src/rules/types.ts +403 -8
  97. package/src/rules/unmaterialized-activation-source.ts +85 -0
  98. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  99. package/src/rules/valid-describe-refs.ts +162 -32
  100. package/src/rules/valid-detour-contract.ts +78 -0
  101. package/src/rules/warden-export-symmetry.ts +540 -0
  102. package/src/rules/warden-rules-use-ast.ts +1109 -0
  103. package/src/rules/webhook-route-collision.ts +306 -0
  104. package/src/trails/activation-orphan.trail.ts +84 -0
  105. package/src/trails/captured-kernel.trail.ts +108 -0
  106. package/src/trails/circular-refs.trail.ts +29 -0
  107. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  108. package/src/trails/composes-declarations.trail.ts +22 -0
  109. package/src/trails/context-no-surface-types.trail.ts +21 -0
  110. package/src/trails/dead-internal-trail.trail.ts +26 -0
  111. package/src/trails/dead-public-trail.trail.ts +31 -0
  112. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  113. package/src/trails/draft-file-marking.trail.ts +16 -0
  114. package/src/trails/draft-visible-debt.trail.ts +16 -0
  115. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  116. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  117. package/src/trails/entity-exists.trail.ts +21 -0
  118. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  119. package/src/trails/example-valid.trail.ts +25 -0
  120. package/src/trails/fires-declarations.trail.ts +23 -0
  121. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  122. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  123. package/src/trails/governed-vocabulary-permutation-watch.trail.ts +16 -0
  124. package/src/trails/implementation-returns-result.trail.ts +20 -0
  125. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  126. package/src/trails/incomplete-crud.trail.ts +39 -0
  127. package/src/trails/index.ts +90 -0
  128. package/src/trails/intent-propagation.trail.ts +30 -0
  129. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  130. package/src/trails/library-render-coherence.trail.ts +43 -0
  131. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  132. package/src/trails/missing-reconcile.trail.ts +33 -0
  133. package/src/trails/missing-visibility.trail.ts +22 -0
  134. package/src/trails/no-destructured-compose.trail.ts +44 -0
  135. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  136. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  137. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  138. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  139. package/src/trails/no-native-error-result.trail.ts +18 -0
  140. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  141. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  142. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  143. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  144. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  145. package/src/trails/no-top-level-surface.trail.ts +43 -0
  146. package/src/trails/on-references-exist.trail.ts +21 -0
  147. package/src/trails/orphaned-signal.trail.ts +36 -0
  148. package/src/trails/owner-render-parity.trail.ts +26 -0
  149. package/src/trails/pending-force.trail.ts +21 -0
  150. package/src/trails/permit-governance.trail.ts +51 -0
  151. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  152. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  153. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  154. package/src/trails/public-output-schema.trail.ts +55 -0
  155. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  156. package/src/trails/read-intent-fires.trail.ts +20 -0
  157. package/src/trails/reference-exists.trail.ts +25 -0
  158. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  159. package/src/trails/resource-declarations.trail.ts +25 -0
  160. package/src/trails/resource-exists.trail.ts +27 -0
  161. package/src/trails/resource-id-grammar.trail.ts +39 -0
  162. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  163. package/src/trails/run.ts +202 -0
  164. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  165. package/src/trails/schema.ts +281 -0
  166. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  167. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  168. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  169. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  170. package/src/trails/topo.ts +6 -0
  171. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  172. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  173. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  174. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  175. package/src/trails/valid-describe-refs.trail.ts +18 -0
  176. package/src/trails/valid-detour-contract.trail.ts +71 -0
  177. package/src/trails/version-gap.trail.ts +35 -0
  178. package/src/trails/version-pinned-compose.trail.ts +23 -0
  179. package/src/trails/version-without-examples.trail.ts +38 -0
  180. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  181. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  182. package/src/trails/webhook-route-collision.trail.ts +50 -0
  183. package/src/trails/wrap-rule.ts +266 -0
  184. package/src/workspaces.ts +264 -0
  185. package/.turbo/turbo-build.log +0 -1
  186. package/.turbo/turbo-lint.log +0 -3
  187. package/.turbo/turbo-typecheck.log +0 -1
  188. package/dist/cli.d.ts +0 -46
  189. package/dist/cli.d.ts.map +0 -1
  190. package/dist/cli.js +0 -218
  191. package/dist/cli.js.map +0 -1
  192. package/dist/drift.d.ts +0 -26
  193. package/dist/drift.d.ts.map +0 -1
  194. package/dist/drift.js +0 -27
  195. package/dist/drift.js.map +0 -1
  196. package/dist/formatters.d.ts +0 -29
  197. package/dist/formatters.d.ts.map +0 -1
  198. package/dist/formatters.js +0 -87
  199. package/dist/formatters.js.map +0 -1
  200. package/dist/index.d.ts +0 -26
  201. package/dist/index.d.ts.map +0 -1
  202. package/dist/index.js +0 -26
  203. package/dist/index.js.map +0 -1
  204. package/dist/rules/ast.d.ts +0 -41
  205. package/dist/rules/ast.d.ts.map +0 -1
  206. package/dist/rules/ast.js +0 -161
  207. package/dist/rules/ast.js.map +0 -1
  208. package/dist/rules/context-no-surface-types.d.ts +0 -12
  209. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  210. package/dist/rules/context-no-surface-types.js +0 -96
  211. package/dist/rules/context-no-surface-types.js.map +0 -1
  212. package/dist/rules/follow-declarations.d.ts +0 -13
  213. package/dist/rules/follow-declarations.d.ts.map +0 -1
  214. package/dist/rules/follow-declarations.js +0 -195
  215. package/dist/rules/follow-declarations.js.map +0 -1
  216. package/dist/rules/implementation-returns-result.d.ts +0 -13
  217. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  218. package/dist/rules/implementation-returns-result.js +0 -277
  219. package/dist/rules/implementation-returns-result.js.map +0 -1
  220. package/dist/rules/index.d.ts +0 -16
  221. package/dist/rules/index.d.ts.map +0 -1
  222. package/dist/rules/index.js +0 -37
  223. package/dist/rules/index.js.map +0 -1
  224. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  225. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  226. package/dist/rules/no-direct-impl-in-route.js +0 -47
  227. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  228. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  229. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  230. package/dist/rules/no-direct-implementation-call.js +0 -39
  231. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  232. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  233. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  234. package/dist/rules/no-sync-result-assumption.js +0 -98
  235. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  236. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  237. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  238. package/dist/rules/no-throw-in-detour-target.js +0 -87
  239. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  240. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  241. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  242. package/dist/rules/no-throw-in-implementation.js +0 -34
  243. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  244. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  245. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  246. package/dist/rules/prefer-schema-inference.js +0 -86
  247. package/dist/rules/prefer-schema-inference.js.map +0 -1
  248. package/dist/rules/scan.d.ts +0 -8
  249. package/dist/rules/scan.d.ts.map +0 -1
  250. package/dist/rules/scan.js +0 -32
  251. package/dist/rules/scan.js.map +0 -1
  252. package/dist/rules/specs.d.ts +0 -29
  253. package/dist/rules/specs.d.ts.map +0 -1
  254. package/dist/rules/specs.js +0 -192
  255. package/dist/rules/specs.js.map +0 -1
  256. package/dist/rules/structure.d.ts +0 -13
  257. package/dist/rules/structure.d.ts.map +0 -1
  258. package/dist/rules/structure.js +0 -142
  259. package/dist/rules/structure.js.map +0 -1
  260. package/dist/rules/types.d.ts +0 -52
  261. package/dist/rules/types.d.ts.map +0 -1
  262. package/dist/rules/types.js +0 -2
  263. package/dist/rules/types.js.map +0 -1
  264. package/dist/rules/valid-describe-refs.d.ts +0 -7
  265. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  266. package/dist/rules/valid-describe-refs.js +0 -51
  267. package/dist/rules/valid-describe-refs.js.map +0 -1
  268. package/dist/rules/valid-detour-refs.d.ts +0 -6
  269. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  270. package/dist/rules/valid-detour-refs.js +0 -116
  271. package/dist/rules/valid-detour-refs.js.map +0 -1
  272. package/src/__tests__/cli.test.ts +0 -198
  273. package/src/__tests__/drift.test.ts +0 -74
  274. package/src/__tests__/follow-declarations.test.ts +0 -151
  275. package/src/__tests__/formatters.test.ts +0 -157
  276. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  277. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  278. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  279. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  280. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  281. package/src/__tests__/rules.test.ts +0 -227
  282. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  283. package/src/rules/ast.ts +0 -213
  284. package/src/rules/follow-declarations.ts +0 -283
  285. package/src/rules/no-direct-impl-in-route.ts +0 -81
  286. package/src/rules/no-throw-in-detour-target.ts +0 -150
  287. package/src/rules/valid-detour-refs.ts +0 -187
  288. package/tsconfig.json +0 -9
  289. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Validates that every signal id declared in a trail's `on:` array resolves
3
+ * to a known signal definition somewhere in the project.
4
+ *
5
+ * Mirrors `resource-exists` structurally — collects local signal definitions
6
+ * for the standalone `check()` path and accepts a project-wide
7
+ * `knownSignalIds` set via `checkWithContext()`.
8
+ */
9
+
10
+ import { isDraftId } from '@ontrails/core';
11
+
12
+ import { collectSignalDefinitionIds } from './source/signals.js';
13
+ import {
14
+ deriveConstString,
15
+ findConfigProperty,
16
+ findTrailDefinitions,
17
+ getStringValue,
18
+ identifierName,
19
+ isStringLiteral,
20
+ offsetToLine,
21
+ parse,
22
+ } from '@ontrails/source';
23
+ import type { AstNode } from '@ontrails/source';
24
+ import { isTestFile } from './scan.js';
25
+ import type {
26
+ ProjectAwareWardenRule,
27
+ ProjectContext,
28
+ WardenDiagnostic,
29
+ } from './types.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Declared `on:` extraction
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const getOnElements = (config: AstNode): readonly AstNode[] => {
36
+ const onProp = findConfigProperty(config, 'on');
37
+ if (!onProp) {
38
+ return [];
39
+ }
40
+
41
+ const arrayNode = onProp.value;
42
+ if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
43
+ return [];
44
+ }
45
+
46
+ const elements = (arrayNode as AstNode)['elements'] as
47
+ | readonly AstNode[]
48
+ | undefined;
49
+ return elements ?? [];
50
+ };
51
+
52
+ /**
53
+ * Resolve an `on:` array element to a signal id when possible.
54
+ *
55
+ * Handles string literals and `const NAME = 'id'` identifier references.
56
+ * Object-form entries (e.g. `on: [someSignal]` where `someSignal` is a
57
+ * `Signal` value) cannot be statically resolved here and are skipped — the
58
+ * runtime normalizes them inside `trail()`, so skipping is safe. The tradeoff
59
+ * is that typo'd Signal imports won't be caught at lint time; the TypeScript
60
+ * compiler catches those instead.
61
+ */
62
+ const extractOnElementId = (
63
+ element: AstNode,
64
+ sourceCode: string
65
+ ): string | null => {
66
+ if (element.type === 'Identifier') {
67
+ const name = identifierName(element);
68
+ return name ? deriveConstString(name, sourceCode) : null;
69
+ }
70
+
71
+ if (isStringLiteral(element)) {
72
+ return getStringValue(element);
73
+ }
74
+
75
+ return null;
76
+ };
77
+
78
+ const extractDeclaredOnIds = (
79
+ config: AstNode,
80
+ sourceCode: string
81
+ ): readonly string[] => [
82
+ ...new Set(
83
+ getOnElements(config).flatMap((element) => {
84
+ const id = extractOnElementId(element, sourceCode);
85
+ return id ? [id] : [];
86
+ })
87
+ ),
88
+ ];
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Diagnostics
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const buildMissingSignalDiagnostic = (
95
+ trailId: string,
96
+ signalId: string,
97
+ filePath: string,
98
+ line: number
99
+ ): WardenDiagnostic => ({
100
+ filePath,
101
+ line,
102
+ message: `Trail "${trailId}" declares on: "${signalId}" which is not a known signal in the project. Define it with signal('${signalId}', ...), include it in the topo, or fix the on: id if this is a typo.`,
103
+ rule: 'on-references-exist',
104
+ severity: 'error',
105
+ });
106
+
107
+ const reportMissingSignals = (
108
+ def: { id: string; config: AstNode; start: number },
109
+ sourceCode: string,
110
+ filePath: string,
111
+ knownSignalIds: ReadonlySet<string>,
112
+ diagnostics: WardenDiagnostic[]
113
+ ): void => {
114
+ const line = offsetToLine(sourceCode, def.start);
115
+ for (const signalId of extractDeclaredOnIds(def.config, sourceCode)) {
116
+ if (!knownSignalIds.has(signalId) && !isDraftId(signalId)) {
117
+ diagnostics.push(
118
+ buildMissingSignalDiagnostic(def.id, signalId, filePath, line)
119
+ );
120
+ }
121
+ }
122
+ };
123
+
124
+ const buildSignalDiagnostics = (
125
+ ast: AstNode,
126
+ sourceCode: string,
127
+ filePath: string,
128
+ knownSignalIds: ReadonlySet<string>
129
+ ): readonly WardenDiagnostic[] => {
130
+ const diagnostics: WardenDiagnostic[] = [];
131
+ for (const def of findTrailDefinitions(ast)) {
132
+ if (def.kind !== 'trail') {
133
+ continue;
134
+ }
135
+ reportMissingSignals(
136
+ def,
137
+ sourceCode,
138
+ filePath,
139
+ knownSignalIds,
140
+ diagnostics
141
+ );
142
+ }
143
+ return diagnostics;
144
+ };
145
+
146
+ const checkOnReferences = (
147
+ ast: AstNode | null,
148
+ sourceCode: string,
149
+ filePath: string,
150
+ knownSignalIds: ReadonlySet<string>
151
+ ): readonly WardenDiagnostic[] => {
152
+ if (isTestFile(filePath) || !ast) {
153
+ return [];
154
+ }
155
+ return buildSignalDiagnostics(ast, sourceCode, filePath, knownSignalIds);
156
+ };
157
+
158
+ /**
159
+ * Checks that every `on:` reference resolves to a known signal definition.
160
+ */
161
+ export const onReferencesExist: ProjectAwareWardenRule = {
162
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
163
+ const ast = parse(filePath, sourceCode);
164
+ if (!ast) {
165
+ return [];
166
+ }
167
+ return checkOnReferences(
168
+ ast,
169
+ sourceCode,
170
+ filePath,
171
+ collectSignalDefinitionIds(ast)
172
+ );
173
+ },
174
+ checkWithContext(
175
+ sourceCode: string,
176
+ filePath: string,
177
+ context: ProjectContext
178
+ ): readonly WardenDiagnostic[] {
179
+ const ast = parse(filePath, sourceCode);
180
+ const localSignalIds = ast
181
+ ? collectSignalDefinitionIds(ast)
182
+ : new Set<string>();
183
+ return checkOnReferences(
184
+ ast,
185
+ sourceCode,
186
+ filePath,
187
+ context.knownSignalIds ?? localSignalIds
188
+ );
189
+ },
190
+ description:
191
+ 'Ensure every signal id declared in a trail on: array resolves to a known signal definition.',
192
+ name: 'on-references-exist',
193
+ severity: 'error',
194
+ };
@@ -0,0 +1,149 @@
1
+ import { collectOnTargetSignalIds } from './source/signals.js';
2
+ import {
3
+ collectCrudTableIds,
4
+ findStoreTableDefinitions,
5
+ } from './source/stores.js';
6
+ import { offsetToLine, parse } from '@ontrails/source';
7
+ import type { AstNode } from '@ontrails/source';
8
+ import { isTestFile } from './scan.js';
9
+ import type {
10
+ ProjectAwareWardenRule,
11
+ ProjectContext,
12
+ WardenDiagnostic,
13
+ } from './types.js';
14
+
15
+ const CHANGE_SIGNAL_OPERATIONS = ['created', 'updated', 'removed'] as const;
16
+
17
+ const buildOrphanedSignalDiagnostic = (
18
+ tableId: string,
19
+ missingSignalIds: readonly string[],
20
+ filePath: string,
21
+ line: number
22
+ ): WardenDiagnostic => ({
23
+ filePath,
24
+ line,
25
+ message: `Store table "${tableId}" derives change signals with no trail on: consumers: ${missingSignalIds.join(', ')}. Add trail on: consumers or remove the unused reactive pattern.`,
26
+ rule: 'orphaned-signal',
27
+ severity: 'warn',
28
+ });
29
+
30
+ const getMissingSignalIds = (
31
+ tableId: string,
32
+ onTargetSignalIds: ReadonlySet<string>
33
+ ): readonly string[] =>
34
+ CHANGE_SIGNAL_OPERATIONS.map((operation) => `${tableId}.${operation}`).filter(
35
+ (signalId) =>
36
+ !onTargetSignalIds.has(signalId) &&
37
+ // Bare-name fallback: string-literal `on:` consumers store the signal
38
+ // without the composite `${storeBinding}:` prefix.
39
+ !onTargetSignalIds.has(signalId.replace(/^[^:]+:/, ''))
40
+ );
41
+
42
+ /**
43
+ * Strip the `${storeBinding}:` prefix from a composite signal id for display.
44
+ * Keeps diagnostic messages readable while keeping keys composite internally.
45
+ */
46
+ const stripStoreBinding = (
47
+ signalId: string,
48
+ storeBinding: string | null
49
+ ): string => {
50
+ if (!storeBinding) {
51
+ return signalId;
52
+ }
53
+ const prefix = `${storeBinding}:`;
54
+ return signalId.startsWith(prefix) ? signalId.slice(prefix.length) : signalId;
55
+ };
56
+
57
+ const buildDefinitionDiagnostic = (
58
+ definition: ReturnType<typeof findStoreTableDefinitions>[number],
59
+ sourceCode: string,
60
+ filePath: string,
61
+ crudTableIds: ReadonlySet<string>,
62
+ onTargetSignalIds: ReadonlySet<string>
63
+ ): WardenDiagnostic | null => {
64
+ if (!crudTableIds.has(definition.key)) {
65
+ return null;
66
+ }
67
+
68
+ const missingSignalIds = getMissingSignalIds(
69
+ definition.key,
70
+ onTargetSignalIds
71
+ );
72
+ return missingSignalIds.length === 0
73
+ ? null
74
+ : buildOrphanedSignalDiagnostic(
75
+ definition.name,
76
+ missingSignalIds.map((id) =>
77
+ stripStoreBinding(id, definition.storeBinding)
78
+ ),
79
+ filePath,
80
+ offsetToLine(sourceCode, definition.start)
81
+ );
82
+ };
83
+
84
+ const checkOrphanedSignals = (
85
+ ast: AstNode | null,
86
+ sourceCode: string,
87
+ filePath: string,
88
+ crudTableIds: ReadonlySet<string>,
89
+ onTargetSignalIds: ReadonlySet<string>
90
+ ): readonly WardenDiagnostic[] => {
91
+ if (isTestFile(filePath) || !ast) {
92
+ return [];
93
+ }
94
+
95
+ const diagnostics: WardenDiagnostic[] = [];
96
+
97
+ for (const definition of findStoreTableDefinitions(ast)) {
98
+ const diagnostic = buildDefinitionDiagnostic(
99
+ definition,
100
+ sourceCode,
101
+ filePath,
102
+ crudTableIds,
103
+ onTargetSignalIds
104
+ );
105
+ if (diagnostic) {
106
+ diagnostics.push(diagnostic);
107
+ }
108
+ }
109
+
110
+ return diagnostics;
111
+ };
112
+
113
+ export const orphanedSignal: ProjectAwareWardenRule = {
114
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
115
+ const ast = parse(filePath, sourceCode);
116
+ return checkOrphanedSignals(
117
+ ast,
118
+ sourceCode,
119
+ filePath,
120
+ ast ? collectCrudTableIds(ast) : new Set<string>(),
121
+ ast ? collectOnTargetSignalIds(ast, sourceCode) : new Set<string>()
122
+ );
123
+ },
124
+ checkWithContext(
125
+ sourceCode: string,
126
+ filePath: string,
127
+ context: ProjectContext
128
+ ): readonly WardenDiagnostic[] {
129
+ const ast = parse(filePath, sourceCode);
130
+ const localCrudTableIds = ast
131
+ ? collectCrudTableIds(ast)
132
+ : new Set<string>();
133
+ const localOnTargetSignalIds = ast
134
+ ? collectOnTargetSignalIds(ast, sourceCode)
135
+ : new Set<string>();
136
+
137
+ return checkOrphanedSignals(
138
+ ast,
139
+ sourceCode,
140
+ filePath,
141
+ context.crudTableIds ?? localCrudTableIds,
142
+ context.onTargetSignalIds ?? localOnTargetSignalIds
143
+ );
144
+ },
145
+ description:
146
+ 'Warn when CRUD-backed store change signals are never consumed by trail on: declarations.',
147
+ name: 'orphaned-signal',
148
+ severity: 'warn',
149
+ };
@@ -0,0 +1,143 @@
1
+ import { resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { intentValues } from '@ontrails/core';
5
+
6
+ import {
7
+ getNodeExpression,
8
+ getNodeId,
9
+ getNodeInit,
10
+ getNodeKey,
11
+ getNodeProperties,
12
+ getPropertyName,
13
+ identifierName,
14
+ offsetToLine,
15
+ parse,
16
+ walk,
17
+ } from '@ontrails/source';
18
+ import type { AstNode } from '@ontrails/source';
19
+ import type { WardenDiagnostic, WardenRule } from './types.js';
20
+
21
+ const RULE_NAME = 'owner-render-parity';
22
+
23
+ const HTTP_METHOD_DERIVATION_PATH = resolve(
24
+ fileURLToPath(new URL('../../../http/src/method.ts', import.meta.url))
25
+ );
26
+
27
+ const isTargetFile = (filePath: string): boolean =>
28
+ resolve(filePath) === HTTP_METHOD_DERIVATION_PATH;
29
+
30
+ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
31
+ let current = node;
32
+ while (
33
+ current &&
34
+ [
35
+ 'ParenthesizedExpression',
36
+ 'TSAsExpression',
37
+ 'TSNonNullExpression',
38
+ 'TSSatisfiesExpression',
39
+ 'TSTypeAssertion',
40
+ ].includes(current.type)
41
+ ) {
42
+ current = getNodeExpression(current);
43
+ }
44
+ return current;
45
+ };
46
+
47
+ interface IntentKeyMap {
48
+ readonly keys: ReadonlySet<string>;
49
+ readonly node: AstNode;
50
+ }
51
+
52
+ const findHttpMethodByIntentMap = (ast: AstNode): IntentKeyMap | null => {
53
+ let found: IntentKeyMap | null = null;
54
+
55
+ walk(ast, (node) => {
56
+ if (found || node.type !== 'VariableDeclarator') {
57
+ return;
58
+ }
59
+
60
+ const id = getNodeId(node);
61
+ const init = getNodeInit(node);
62
+ if (identifierName(id) !== 'httpMethodByIntent') {
63
+ return;
64
+ }
65
+
66
+ const objectExpression = unwrapExpression(init);
67
+ if (objectExpression?.type !== 'ObjectExpression') {
68
+ found = { keys: new Set(), node };
69
+ return;
70
+ }
71
+
72
+ const keys = new Set<string>();
73
+ for (const property of getNodeProperties(objectExpression) ?? []) {
74
+ if (property.type !== 'Property') {
75
+ continue;
76
+ }
77
+ const key = getPropertyName(getNodeKey(property));
78
+ if (key) {
79
+ keys.add(key);
80
+ }
81
+ }
82
+
83
+ found = { keys, node: objectExpression };
84
+ });
85
+
86
+ return found;
87
+ };
88
+
89
+ const buildMessage = (missing: string[], extra: string[]): string => {
90
+ const details = [
91
+ missing.length > 0 ? `missing owner intents: ${missing.join(', ')}` : '',
92
+ extra.length > 0 ? `unknown intentKeyMap keys: ${extra.join(', ')}` : '',
93
+ ].filter(Boolean);
94
+
95
+ return [
96
+ 'owner-render-parity: httpMethodByIntent must cover the core intentValues owner vocabulary.',
97
+ ...details,
98
+ ].join(' ');
99
+ };
100
+
101
+ export const ownerRenderParity: WardenRule = {
102
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
103
+ if (!isTargetFile(filePath)) {
104
+ return [];
105
+ }
106
+
107
+ const ast = parse(filePath, sourceCode);
108
+ if (!ast) {
109
+ return [];
110
+ }
111
+
112
+ const intentKeyMap = findHttpMethodByIntentMap(ast);
113
+ const ownerKeys = new Set<string>(intentValues);
114
+ const derivedKeys = intentKeyMap?.keys ?? new Set<string>();
115
+ const missing = [...ownerKeys]
116
+ .filter((key) => !derivedKeys.has(key))
117
+ .toSorted();
118
+ const extra = [...derivedKeys]
119
+ .filter((key) => !ownerKeys.has(key))
120
+ .toSorted();
121
+
122
+ if (intentKeyMap && missing.length === 0 && extra.length === 0) {
123
+ return [];
124
+ }
125
+
126
+ const node = intentKeyMap?.node ?? ast;
127
+ return [
128
+ {
129
+ filePath,
130
+ line: offsetToLine(sourceCode, node.start),
131
+ message: intentKeyMap
132
+ ? buildMessage(missing, extra)
133
+ : 'owner-render-parity: expected httpMethodByIntent to render core intentValues.',
134
+ rule: RULE_NAME,
135
+ severity: 'error',
136
+ },
137
+ ];
138
+ },
139
+ description:
140
+ 'Require owner-derived intentKeyMap maps to cover their authoritative owner vocabulary.',
141
+ name: RULE_NAME,
142
+ severity: 'error',
143
+ };
@@ -0,0 +1,25 @@
1
+ import type { Trail } from '@ontrails/core';
2
+ import { validatePermits } from '@ontrails/permits';
3
+
4
+ import type { TopoAwareWardenRule, WardenDiagnostic } from './types.js';
5
+
6
+ const toWardenDiagnostic = (
7
+ diagnostic: ReturnType<typeof validatePermits>[number]
8
+ ): WardenDiagnostic => ({
9
+ filePath: '<topo>',
10
+ line: 1,
11
+ message: diagnostic.message,
12
+ rule: `permit.${diagnostic.rule}`,
13
+ severity: diagnostic.severity,
14
+ });
15
+
16
+ export const permitGovernance: TopoAwareWardenRule = {
17
+ checkTopo: (topo) =>
18
+ validatePermits(
19
+ topo.list() as readonly Trail<unknown, unknown, unknown>[]
20
+ ).map(toWardenDiagnostic),
21
+ description:
22
+ 'Enforces permit declarations and scope hygiene across the compiled topo',
23
+ name: 'permit-governance',
24
+ severity: 'warn',
25
+ };