@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.41

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 (279) hide show
  1. package/CHANGELOG.md +835 -13
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +27 -6
  5. package/src/adapter-check.ts +136 -0
  6. package/src/cli.ts +1570 -105
  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 +247 -15
  15. package/src/project-context.ts +446 -0
  16. package/src/project-rules.ts +290 -0
  17. package/src/resolve.ts +531 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/captured-kernel.ts +375 -0
  20. package/src/rules/circular-refs.ts +150 -0
  21. package/src/rules/cli-command-route-coherence.ts +177 -0
  22. package/src/rules/composes-declarations.ts +839 -0
  23. package/src/rules/context-no-surface-types.ts +79 -15
  24. package/src/rules/dead-internal-trail.ts +161 -0
  25. package/src/rules/dead-public-trail.ts +258 -0
  26. package/src/rules/draft-file-marking.ts +155 -0
  27. package/src/rules/draft-visible-debt.ts +82 -0
  28. package/src/rules/duplicate-exported-symbol.ts +211 -0
  29. package/src/rules/duplicate-public-contract.ts +137 -0
  30. package/src/rules/entity-exists.ts +254 -0
  31. package/src/rules/entity-ids.ts +15 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +740 -0
  35. package/src/rules/governed-symbol-residue.ts +438 -0
  36. package/src/rules/implementation-returns-result.ts +1409 -166
  37. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  38. package/src/rules/incomplete-crud.ts +583 -0
  39. package/src/rules/index.ts +277 -10
  40. package/src/rules/intent-propagation.ts +125 -0
  41. package/src/rules/layer-field-name-drift.ts +102 -0
  42. package/src/rules/library-projection-coherence.ts +100 -0
  43. package/src/rules/metadata.ts +871 -0
  44. package/src/rules/missing-reconcile.ts +97 -0
  45. package/src/rules/missing-visibility.ts +111 -0
  46. package/src/rules/no-destructured-compose.ts +196 -0
  47. package/src/rules/no-dev-permit-in-source.ts +99 -0
  48. package/src/rules/no-direct-implementation-call.ts +12 -7
  49. package/src/rules/no-legacy-cli-alias-export.ts +247 -0
  50. package/src/rules/no-legacy-layer-imports.ts +211 -0
  51. package/src/rules/no-native-error-result.ts +118 -0
  52. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  53. package/src/rules/no-retired-cross-vocabulary.ts +204 -0
  54. package/src/rules/no-sync-result-assumption.ts +1141 -98
  55. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  56. package/src/rules/no-throw-in-implementation.ts +15 -8
  57. package/src/rules/no-top-level-surface.ts +371 -0
  58. package/src/rules/on-references-exist.ts +194 -0
  59. package/src/rules/orphaned-signal.ts +149 -0
  60. package/src/rules/owner-projection-parity.ts +143 -0
  61. package/src/rules/permit-governance.ts +25 -0
  62. package/src/rules/public-export-example-coverage.ts +573 -0
  63. package/src/rules/public-internal-deep-imports.ts +456 -0
  64. package/src/rules/public-output-schema.ts +29 -0
  65. package/src/rules/public-union-output-discriminants.ts +150 -0
  66. package/src/rules/read-intent-fires.ts +188 -0
  67. package/src/rules/reference-exists.ts +97 -0
  68. package/src/rules/registry-names.ts +167 -0
  69. package/src/rules/resolved-import-boundary.ts +146 -0
  70. package/src/rules/resource-declarations.ts +697 -0
  71. package/src/rules/resource-exists.ts +181 -0
  72. package/src/rules/resource-id-grammar.ts +65 -0
  73. package/src/rules/resource-mock-coverage.ts +115 -0
  74. package/src/rules/retired-vocabulary.ts +633 -0
  75. package/src/rules/scan.ts +38 -25
  76. package/src/rules/scheduled-destroy-intent.ts +44 -0
  77. package/src/rules/signal-graph-coaching.ts +220 -0
  78. package/src/rules/source/composition.ts +165 -0
  79. package/src/rules/source/drafts.ts +164 -0
  80. package/src/rules/source/entities.ts +618 -0
  81. package/src/rules/source/pragmas.ts +45 -0
  82. package/src/rules/source/resources.ts +64 -0
  83. package/src/rules/source/signals.ts +397 -0
  84. package/src/rules/source/stores.ts +310 -0
  85. package/src/rules/specs.ts +9 -5
  86. package/src/rules/static-resource-accessor-preference.ts +653 -0
  87. package/src/rules/surface-overlay-coherence.ts +262 -0
  88. package/src/rules/surface-trailhead-coherence.ts +366 -0
  89. package/src/rules/trail-fork-coaching.ts +625 -0
  90. package/src/rules/trail-versioning-source.ts +1076 -0
  91. package/src/rules/trail-versioning-topo.ts +172 -0
  92. package/src/rules/trailhead-override-divergence.ts +356 -0
  93. package/src/rules/types.ts +354 -8
  94. package/src/rules/unmaterialized-activation-source.ts +85 -0
  95. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  96. package/src/rules/valid-describe-refs.ts +162 -32
  97. package/src/rules/valid-detour-contract.ts +78 -0
  98. package/src/rules/warden-export-symmetry.ts +540 -0
  99. package/src/rules/warden-rules-use-ast.ts +1109 -0
  100. package/src/rules/webhook-route-collision.ts +306 -0
  101. package/src/trails/activation-orphan.trail.ts +84 -0
  102. package/src/trails/captured-kernel.trail.ts +108 -0
  103. package/src/trails/circular-refs.trail.ts +29 -0
  104. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  105. package/src/trails/composes-declarations.trail.ts +22 -0
  106. package/src/trails/context-no-surface-types.trail.ts +21 -0
  107. package/src/trails/dead-internal-trail.trail.ts +26 -0
  108. package/src/trails/dead-public-trail.trail.ts +31 -0
  109. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  110. package/src/trails/draft-file-marking.trail.ts +16 -0
  111. package/src/trails/draft-visible-debt.trail.ts +16 -0
  112. package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
  113. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  114. package/src/trails/entity-exists.trail.ts +21 -0
  115. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  116. package/src/trails/example-valid.trail.ts +25 -0
  117. package/src/trails/fires-declarations.trail.ts +23 -0
  118. package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
  119. package/src/trails/governed-symbol-residue.trail.ts +24 -0
  120. package/src/trails/implementation-returns-result.trail.ts +20 -0
  121. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  122. package/src/trails/incomplete-crud.trail.ts +39 -0
  123. package/src/trails/index.ts +89 -0
  124. package/src/trails/intent-propagation.trail.ts +30 -0
  125. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  126. package/src/trails/library-projection-coherence.trail.ts +43 -0
  127. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  128. package/src/trails/missing-reconcile.trail.ts +33 -0
  129. package/src/trails/missing-visibility.trail.ts +22 -0
  130. package/src/trails/no-destructured-compose.trail.ts +44 -0
  131. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  132. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  133. package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
  134. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  135. package/src/trails/no-native-error-result.trail.ts +18 -0
  136. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  137. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  138. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  139. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  140. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  141. package/src/trails/no-top-level-surface.trail.ts +43 -0
  142. package/src/trails/on-references-exist.trail.ts +21 -0
  143. package/src/trails/orphaned-signal.trail.ts +36 -0
  144. package/src/trails/owner-projection-parity.trail.ts +26 -0
  145. package/src/trails/pending-force.trail.ts +21 -0
  146. package/src/trails/permit-governance.trail.ts +51 -0
  147. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  148. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  149. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  150. package/src/trails/public-output-schema.trail.ts +55 -0
  151. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  152. package/src/trails/read-intent-fires.trail.ts +20 -0
  153. package/src/trails/reference-exists.trail.ts +25 -0
  154. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  155. package/src/trails/resource-declarations.trail.ts +25 -0
  156. package/src/trails/resource-exists.trail.ts +27 -0
  157. package/src/trails/resource-id-grammar.trail.ts +39 -0
  158. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  159. package/src/trails/run.ts +160 -0
  160. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  161. package/src/trails/schema.ts +237 -0
  162. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  163. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  164. package/src/trails/surface-overlay-coherence.trail.ts +24 -0
  165. package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
  166. package/src/trails/topo.ts +6 -0
  167. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  168. package/src/trails/trailhead-override-divergence.trail.ts +47 -0
  169. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  170. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  171. package/src/trails/valid-describe-refs.trail.ts +18 -0
  172. package/src/trails/valid-detour-contract.trail.ts +71 -0
  173. package/src/trails/version-gap.trail.ts +35 -0
  174. package/src/trails/version-pinned-compose.trail.ts +23 -0
  175. package/src/trails/version-without-examples.trail.ts +38 -0
  176. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  177. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  178. package/src/trails/webhook-route-collision.trail.ts +50 -0
  179. package/src/trails/wrap-rule.ts +224 -0
  180. package/src/workspaces.ts +199 -0
  181. package/.turbo/turbo-build.log +0 -1
  182. package/.turbo/turbo-lint.log +0 -3
  183. package/.turbo/turbo-typecheck.log +0 -1
  184. package/dist/cli.d.ts +0 -46
  185. package/dist/cli.d.ts.map +0 -1
  186. package/dist/cli.js +0 -218
  187. package/dist/cli.js.map +0 -1
  188. package/dist/drift.d.ts +0 -26
  189. package/dist/drift.d.ts.map +0 -1
  190. package/dist/drift.js +0 -27
  191. package/dist/drift.js.map +0 -1
  192. package/dist/formatters.d.ts +0 -29
  193. package/dist/formatters.d.ts.map +0 -1
  194. package/dist/formatters.js +0 -87
  195. package/dist/formatters.js.map +0 -1
  196. package/dist/index.d.ts +0 -26
  197. package/dist/index.d.ts.map +0 -1
  198. package/dist/index.js +0 -26
  199. package/dist/index.js.map +0 -1
  200. package/dist/rules/ast.d.ts +0 -41
  201. package/dist/rules/ast.d.ts.map +0 -1
  202. package/dist/rules/ast.js +0 -161
  203. package/dist/rules/ast.js.map +0 -1
  204. package/dist/rules/context-no-surface-types.d.ts +0 -12
  205. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  206. package/dist/rules/context-no-surface-types.js +0 -96
  207. package/dist/rules/context-no-surface-types.js.map +0 -1
  208. package/dist/rules/implementation-returns-result.d.ts +0 -13
  209. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  210. package/dist/rules/implementation-returns-result.js +0 -277
  211. package/dist/rules/implementation-returns-result.js.map +0 -1
  212. package/dist/rules/index.d.ts +0 -15
  213. package/dist/rules/index.d.ts.map +0 -1
  214. package/dist/rules/index.js +0 -34
  215. package/dist/rules/index.js.map +0 -1
  216. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  217. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  218. package/dist/rules/no-direct-impl-in-route.js +0 -47
  219. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  220. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  221. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  222. package/dist/rules/no-direct-implementation-call.js +0 -39
  223. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  224. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  225. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  226. package/dist/rules/no-sync-result-assumption.js +0 -98
  227. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  228. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  229. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  230. package/dist/rules/no-throw-in-detour-target.js +0 -87
  231. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  232. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  233. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  234. package/dist/rules/no-throw-in-implementation.js +0 -34
  235. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  236. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  237. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  238. package/dist/rules/prefer-schema-inference.js +0 -86
  239. package/dist/rules/prefer-schema-inference.js.map +0 -1
  240. package/dist/rules/scan.d.ts +0 -8
  241. package/dist/rules/scan.d.ts.map +0 -1
  242. package/dist/rules/scan.js +0 -32
  243. package/dist/rules/scan.js.map +0 -1
  244. package/dist/rules/specs.d.ts +0 -29
  245. package/dist/rules/specs.d.ts.map +0 -1
  246. package/dist/rules/specs.js +0 -192
  247. package/dist/rules/specs.js.map +0 -1
  248. package/dist/rules/structure.d.ts +0 -13
  249. package/dist/rules/structure.d.ts.map +0 -1
  250. package/dist/rules/structure.js +0 -142
  251. package/dist/rules/structure.js.map +0 -1
  252. package/dist/rules/types.d.ts +0 -52
  253. package/dist/rules/types.d.ts.map +0 -1
  254. package/dist/rules/types.js +0 -2
  255. package/dist/rules/types.js.map +0 -1
  256. package/dist/rules/valid-describe-refs.d.ts +0 -7
  257. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  258. package/dist/rules/valid-describe-refs.js +0 -51
  259. package/dist/rules/valid-describe-refs.js.map +0 -1
  260. package/dist/rules/valid-detour-refs.d.ts +0 -6
  261. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  262. package/dist/rules/valid-detour-refs.js +0 -116
  263. package/dist/rules/valid-detour-refs.js.map +0 -1
  264. package/src/__tests__/cli.test.ts +0 -198
  265. package/src/__tests__/drift.test.ts +0 -74
  266. package/src/__tests__/formatters.test.ts +0 -157
  267. package/src/__tests__/implementation-returns-result.test.ts +0 -129
  268. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  269. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  270. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  271. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  272. package/src/__tests__/rules.test.ts +0 -227
  273. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  274. package/src/rules/ast.ts +0 -213
  275. package/src/rules/no-direct-impl-in-route.ts +0 -81
  276. package/src/rules/no-throw-in-detour-target.ts +0 -150
  277. package/src/rules/valid-detour-refs.ts +0 -187
  278. package/tsconfig.json +0 -9
  279. 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-projection-parity';
22
+
23
+ const HTTP_METHOD_PROJECTION_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_PROJECTION_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 ProjectionMap {
48
+ readonly keys: ReadonlySet<string>;
49
+ readonly node: AstNode;
50
+ }
51
+
52
+ const findHttpMethodByIntentMap = (ast: AstNode): ProjectionMap | null => {
53
+ let found: ProjectionMap | 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 projection keys: ${extra.join(', ')}` : '',
93
+ ].filter(Boolean);
94
+
95
+ return [
96
+ 'owner-projection-parity: httpMethodByIntent must cover the core intentValues owner vocabulary.',
97
+ ...details,
98
+ ].join(' ');
99
+ };
100
+
101
+ export const ownerProjectionParity: 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 projection = findHttpMethodByIntentMap(ast);
113
+ const ownerKeys = new Set<string>(intentValues);
114
+ const projectionKeys = projection?.keys ?? new Set<string>();
115
+ const missing = [...ownerKeys]
116
+ .filter((key) => !projectionKeys.has(key))
117
+ .toSorted();
118
+ const extra = [...projectionKeys]
119
+ .filter((key) => !ownerKeys.has(key))
120
+ .toSorted();
121
+
122
+ if (projection && missing.length === 0 && extra.length === 0) {
123
+ return [];
124
+ }
125
+
126
+ const node = projection?.node ?? ast;
127
+ return [
128
+ {
129
+ filePath,
130
+ line: offsetToLine(sourceCode, node.start),
131
+ message: projection
132
+ ? buildMessage(missing, extra)
133
+ : 'owner-projection-parity: expected httpMethodByIntent to project core intentValues.',
134
+ rule: RULE_NAME,
135
+ severity: 'error',
136
+ },
137
+ ];
138
+ },
139
+ description:
140
+ 'Require owner-derived projection 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
+ };