@ontrails/warden 1.0.0-beta.3 → 1.0.0-beta.30

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 (260) hide show
  1. package/CHANGELOG.md +625 -9
  2. package/README.md +111 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +28 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +137 -0
  7. package/src/cli.ts +1415 -104
  8. package/src/command.ts +943 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +122 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +215 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/project-rules.ts +290 -0
  18. package/src/resolve.ts +531 -0
  19. package/src/rules/activation-orphan.ts +97 -0
  20. package/src/rules/ast.ts +4008 -86
  21. package/src/rules/circular-refs.ts +154 -0
  22. package/src/rules/cli-command-route-coherence.ts +177 -0
  23. package/src/rules/composes-declarations.ts +837 -0
  24. package/src/rules/context-no-surface-types.ts +78 -15
  25. package/src/rules/contour-exists.ts +251 -0
  26. package/src/rules/contour-ids.ts +15 -0
  27. package/src/rules/dead-internal-trail.ts +161 -0
  28. package/src/rules/dead-public-trail.ts +258 -0
  29. package/src/rules/draft-file-marking.ts +160 -0
  30. package/src/rules/draft-visible-debt.ts +87 -0
  31. package/src/rules/duplicate-public-contract.ts +91 -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 +735 -0
  35. package/src/rules/implementation-returns-result.ts +1411 -166
  36. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  37. package/src/rules/incomplete-crud.ts +581 -0
  38. package/src/rules/index.ts +234 -18
  39. package/src/rules/intent-propagation.ts +127 -0
  40. package/src/rules/layer-field-name-drift.ts +102 -0
  41. package/src/rules/library-projection-coherence.ts +100 -0
  42. package/src/rules/metadata.ts +755 -0
  43. package/src/rules/missing-reconcile.ts +98 -0
  44. package/src/rules/missing-visibility.ts +110 -0
  45. package/src/rules/no-destructured-compose.ts +194 -0
  46. package/src/rules/no-dev-permit-in-source.ts +99 -0
  47. package/src/rules/no-direct-implementation-call.ts +7 -7
  48. package/src/rules/no-legacy-layer-imports.ts +211 -0
  49. package/src/rules/no-native-error-result.ts +118 -0
  50. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  51. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  52. package/src/rules/no-sync-result-assumption.ts +1135 -98
  53. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  54. package/src/rules/no-throw-in-implementation.ts +10 -9
  55. package/src/rules/no-top-level-surface.ts +371 -0
  56. package/src/rules/on-references-exist.ts +194 -0
  57. package/src/rules/orphaned-signal.ts +150 -0
  58. package/src/rules/owner-projection-parity.ts +143 -0
  59. package/src/rules/permit-governance.ts +25 -0
  60. package/src/rules/public-export-example-coverage.ts +562 -0
  61. package/src/rules/public-internal-deep-imports.ts +517 -0
  62. package/src/rules/public-output-schema.ts +29 -0
  63. package/src/rules/public-union-output-discriminants.ts +150 -0
  64. package/src/rules/read-intent-fires.ts +187 -0
  65. package/src/rules/reference-exists.ts +98 -0
  66. package/src/rules/registry-names.ts +155 -0
  67. package/src/rules/resolved-import-boundary.ts +146 -0
  68. package/src/rules/resource-declarations.ts +693 -0
  69. package/src/rules/resource-exists.ts +179 -0
  70. package/src/rules/resource-id-grammar.ts +65 -0
  71. package/src/rules/resource-mock-coverage.ts +115 -0
  72. package/src/rules/scan.ts +38 -25
  73. package/src/rules/scheduled-destroy-intent.ts +44 -0
  74. package/src/rules/signal-graph-coaching.ts +191 -0
  75. package/src/rules/specs.ts +9 -5
  76. package/src/rules/static-resource-accessor-preference.ts +649 -0
  77. package/src/rules/surface-facet-coherence.ts +362 -0
  78. package/src/rules/trail-fork-coaching.ts +616 -0
  79. package/src/rules/trail-versioning-source.ts +1072 -0
  80. package/src/rules/trail-versioning-topo.ts +172 -0
  81. package/src/rules/types.ts +272 -6
  82. package/src/rules/unmaterialized-activation-source.ts +84 -0
  83. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  84. package/src/rules/valid-describe-refs.ts +162 -32
  85. package/src/rules/valid-detour-contract.ts +78 -0
  86. package/src/rules/warden-export-symmetry.ts +540 -0
  87. package/src/rules/warden-rules-use-ast.ts +1114 -0
  88. package/src/rules/webhook-route-collision.ts +243 -0
  89. package/src/trails/activation-orphan.trail.ts +84 -0
  90. package/src/trails/circular-refs.trail.ts +29 -0
  91. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  92. package/src/trails/composes-declarations.trail.ts +22 -0
  93. package/src/trails/context-no-surface-types.trail.ts +21 -0
  94. package/src/trails/contour-exists.trail.ts +21 -0
  95. package/src/trails/dead-internal-trail.trail.ts +26 -0
  96. package/src/trails/dead-public-trail.trail.ts +31 -0
  97. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  98. package/src/trails/draft-file-marking.trail.ts +16 -0
  99. package/src/trails/draft-visible-debt.trail.ts +16 -0
  100. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  101. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  102. package/src/trails/example-valid.trail.ts +25 -0
  103. package/src/trails/fires-declarations.trail.ts +23 -0
  104. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  105. package/src/trails/implementation-returns-result.trail.ts +20 -0
  106. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  107. package/src/trails/incomplete-crud.trail.ts +39 -0
  108. package/src/trails/index.ts +83 -0
  109. package/src/trails/intent-propagation.trail.ts +30 -0
  110. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  111. package/src/trails/library-projection-coherence.trail.ts +43 -0
  112. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  113. package/src/trails/missing-reconcile.trail.ts +33 -0
  114. package/src/trails/missing-visibility.trail.ts +22 -0
  115. package/src/trails/no-destructured-compose.trail.ts +44 -0
  116. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  117. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  118. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  119. package/src/trails/no-native-error-result.trail.ts +18 -0
  120. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  121. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  122. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  123. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  124. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  125. package/src/trails/no-top-level-surface.trail.ts +43 -0
  126. package/src/trails/on-references-exist.trail.ts +21 -0
  127. package/src/trails/orphaned-signal.trail.ts +36 -0
  128. package/src/trails/owner-projection-parity.trail.ts +26 -0
  129. package/src/trails/pending-force.trail.ts +21 -0
  130. package/src/trails/permit-governance.trail.ts +51 -0
  131. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  132. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  133. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  134. package/src/trails/public-output-schema.trail.ts +55 -0
  135. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  136. package/src/trails/read-intent-fires.trail.ts +20 -0
  137. package/src/trails/reference-exists.trail.ts +25 -0
  138. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  139. package/src/trails/resource-declarations.trail.ts +25 -0
  140. package/src/trails/resource-exists.trail.ts +27 -0
  141. package/src/trails/resource-id-grammar.trail.ts +39 -0
  142. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  143. package/src/trails/run.ts +162 -0
  144. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  145. package/src/trails/schema.ts +198 -0
  146. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  147. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  148. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  149. package/src/trails/topo.ts +6 -0
  150. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  151. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  152. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  153. package/src/trails/valid-describe-refs.trail.ts +18 -0
  154. package/src/trails/valid-detour-contract.trail.ts +71 -0
  155. package/src/trails/version-gap.trail.ts +35 -0
  156. package/src/trails/version-pinned-compose.trail.ts +23 -0
  157. package/src/trails/version-without-examples.trail.ts +38 -0
  158. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  159. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  160. package/src/trails/webhook-route-collision.trail.ts +50 -0
  161. package/src/trails/wrap-rule.ts +214 -0
  162. package/src/workspaces.ts +238 -0
  163. package/.turbo/turbo-build.log +0 -1
  164. package/.turbo/turbo-lint.log +0 -3
  165. package/.turbo/turbo-typecheck.log +0 -1
  166. package/dist/cli.d.ts +0 -46
  167. package/dist/cli.d.ts.map +0 -1
  168. package/dist/cli.js +0 -221
  169. package/dist/cli.js.map +0 -1
  170. package/dist/drift.d.ts +0 -26
  171. package/dist/drift.d.ts.map +0 -1
  172. package/dist/drift.js +0 -27
  173. package/dist/drift.js.map +0 -1
  174. package/dist/formatters.d.ts +0 -29
  175. package/dist/formatters.d.ts.map +0 -1
  176. package/dist/formatters.js +0 -87
  177. package/dist/formatters.js.map +0 -1
  178. package/dist/index.d.ts +0 -26
  179. package/dist/index.d.ts.map +0 -1
  180. package/dist/index.js +0 -26
  181. package/dist/index.js.map +0 -1
  182. package/dist/rules/ast.d.ts +0 -41
  183. package/dist/rules/ast.d.ts.map +0 -1
  184. package/dist/rules/ast.js +0 -163
  185. package/dist/rules/ast.js.map +0 -1
  186. package/dist/rules/context-no-surface-types.d.ts +0 -12
  187. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  188. package/dist/rules/context-no-surface-types.js +0 -96
  189. package/dist/rules/context-no-surface-types.js.map +0 -1
  190. package/dist/rules/implementation-returns-result.d.ts +0 -13
  191. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  192. package/dist/rules/implementation-returns-result.js +0 -277
  193. package/dist/rules/implementation-returns-result.js.map +0 -1
  194. package/dist/rules/index.d.ts +0 -22
  195. package/dist/rules/index.d.ts.map +0 -1
  196. package/dist/rules/index.js +0 -41
  197. package/dist/rules/index.js.map +0 -1
  198. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  199. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  200. package/dist/rules/no-direct-impl-in-route.js +0 -46
  201. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  202. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  203. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  204. package/dist/rules/no-direct-implementation-call.js +0 -39
  205. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  206. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  207. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  208. package/dist/rules/no-sync-result-assumption.js +0 -98
  209. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  210. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  211. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  212. package/dist/rules/no-throw-in-detour-target.js +0 -87
  213. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  214. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  215. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  216. package/dist/rules/no-throw-in-implementation.js +0 -34
  217. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  218. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  219. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  220. package/dist/rules/prefer-schema-inference.js +0 -86
  221. package/dist/rules/prefer-schema-inference.js.map +0 -1
  222. package/dist/rules/scan.d.ts +0 -8
  223. package/dist/rules/scan.d.ts.map +0 -1
  224. package/dist/rules/scan.js +0 -32
  225. package/dist/rules/scan.js.map +0 -1
  226. package/dist/rules/specs.d.ts +0 -29
  227. package/dist/rules/specs.d.ts.map +0 -1
  228. package/dist/rules/specs.js +0 -192
  229. package/dist/rules/specs.js.map +0 -1
  230. package/dist/rules/structure.d.ts +0 -13
  231. package/dist/rules/structure.d.ts.map +0 -1
  232. package/dist/rules/structure.js +0 -142
  233. package/dist/rules/structure.js.map +0 -1
  234. package/dist/rules/types.d.ts +0 -52
  235. package/dist/rules/types.d.ts.map +0 -1
  236. package/dist/rules/types.js +0 -2
  237. package/dist/rules/types.js.map +0 -1
  238. package/dist/rules/valid-describe-refs.d.ts +0 -7
  239. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  240. package/dist/rules/valid-describe-refs.js +0 -51
  241. package/dist/rules/valid-describe-refs.js.map +0 -1
  242. package/dist/rules/valid-detour-refs.d.ts +0 -6
  243. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  244. package/dist/rules/valid-detour-refs.js +0 -116
  245. package/dist/rules/valid-detour-refs.js.map +0 -1
  246. package/src/__tests__/cli.test.ts +0 -198
  247. package/src/__tests__/drift.test.ts +0 -74
  248. package/src/__tests__/formatters.test.ts +0 -157
  249. package/src/__tests__/implementation-returns-result.test.ts +0 -128
  250. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  251. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  252. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  253. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  254. package/src/__tests__/rules.test.ts +0 -215
  255. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  256. package/src/rules/no-direct-impl-in-route.ts +0 -77
  257. package/src/rules/no-throw-in-detour-target.ts +0 -150
  258. package/src/rules/valid-detour-refs.ts +0 -187
  259. package/tsconfig.json +0 -9
  260. 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 {
13
+ collectSignalDefinitionIds,
14
+ deriveConstString,
15
+ findConfigProperty,
16
+ findTrailDefinitions,
17
+ getStringValue,
18
+ identifierName,
19
+ isStringLiteral,
20
+ offsetToLine,
21
+ parse,
22
+ } from './ast.js';
23
+ import type { AstNode } from './ast.js';
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,150 @@
1
+ import {
2
+ collectCrudTableIds,
3
+ collectOnTargetSignalIds,
4
+ findStoreTableDefinitions,
5
+ offsetToLine,
6
+ parse,
7
+ } from './ast.js';
8
+ import type { AstNode } from './ast.js';
9
+ import { isTestFile } from './scan.js';
10
+ import type {
11
+ ProjectAwareWardenRule,
12
+ ProjectContext,
13
+ WardenDiagnostic,
14
+ } from './types.js';
15
+
16
+ const CHANGE_SIGNAL_OPERATIONS = ['created', 'updated', 'removed'] as const;
17
+
18
+ const buildOrphanedSignalDiagnostic = (
19
+ tableId: string,
20
+ missingSignalIds: readonly string[],
21
+ filePath: string,
22
+ line: number
23
+ ): WardenDiagnostic => ({
24
+ filePath,
25
+ line,
26
+ message: `Store table "${tableId}" derives change signals with no trail on: consumers: ${missingSignalIds.join(', ')}. Add trail on: consumers or remove the unused reactive pattern.`,
27
+ rule: 'orphaned-signal',
28
+ severity: 'warn',
29
+ });
30
+
31
+ const getMissingSignalIds = (
32
+ tableId: string,
33
+ onTargetSignalIds: ReadonlySet<string>
34
+ ): readonly string[] =>
35
+ CHANGE_SIGNAL_OPERATIONS.map((operation) => `${tableId}.${operation}`).filter(
36
+ (signalId) =>
37
+ !onTargetSignalIds.has(signalId) &&
38
+ // Bare-name fallback: string-literal `on:` consumers store the signal
39
+ // without the composite `${storeBinding}:` prefix.
40
+ !onTargetSignalIds.has(signalId.replace(/^[^:]+:/, ''))
41
+ );
42
+
43
+ /**
44
+ * Strip the `${storeBinding}:` prefix from a composite signal id for display.
45
+ * Keeps diagnostic messages readable while keeping keys composite internally.
46
+ */
47
+ const stripStoreBinding = (
48
+ signalId: string,
49
+ storeBinding: string | null
50
+ ): string => {
51
+ if (!storeBinding) {
52
+ return signalId;
53
+ }
54
+ const prefix = `${storeBinding}:`;
55
+ return signalId.startsWith(prefix) ? signalId.slice(prefix.length) : signalId;
56
+ };
57
+
58
+ const buildDefinitionDiagnostic = (
59
+ definition: ReturnType<typeof findStoreTableDefinitions>[number],
60
+ sourceCode: string,
61
+ filePath: string,
62
+ crudTableIds: ReadonlySet<string>,
63
+ onTargetSignalIds: ReadonlySet<string>
64
+ ): WardenDiagnostic | null => {
65
+ if (!crudTableIds.has(definition.key)) {
66
+ return null;
67
+ }
68
+
69
+ const missingSignalIds = getMissingSignalIds(
70
+ definition.key,
71
+ onTargetSignalIds
72
+ );
73
+ return missingSignalIds.length === 0
74
+ ? null
75
+ : buildOrphanedSignalDiagnostic(
76
+ definition.name,
77
+ missingSignalIds.map((id) =>
78
+ stripStoreBinding(id, definition.storeBinding)
79
+ ),
80
+ filePath,
81
+ offsetToLine(sourceCode, definition.start)
82
+ );
83
+ };
84
+
85
+ const checkOrphanedSignals = (
86
+ ast: AstNode | null,
87
+ sourceCode: string,
88
+ filePath: string,
89
+ crudTableIds: ReadonlySet<string>,
90
+ onTargetSignalIds: ReadonlySet<string>
91
+ ): readonly WardenDiagnostic[] => {
92
+ if (isTestFile(filePath) || !ast) {
93
+ return [];
94
+ }
95
+
96
+ const diagnostics: WardenDiagnostic[] = [];
97
+
98
+ for (const definition of findStoreTableDefinitions(ast)) {
99
+ const diagnostic = buildDefinitionDiagnostic(
100
+ definition,
101
+ sourceCode,
102
+ filePath,
103
+ crudTableIds,
104
+ onTargetSignalIds
105
+ );
106
+ if (diagnostic) {
107
+ diagnostics.push(diagnostic);
108
+ }
109
+ }
110
+
111
+ return diagnostics;
112
+ };
113
+
114
+ export const orphanedSignal: ProjectAwareWardenRule = {
115
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
116
+ const ast = parse(filePath, sourceCode);
117
+ return checkOrphanedSignals(
118
+ ast,
119
+ sourceCode,
120
+ filePath,
121
+ ast ? collectCrudTableIds(ast) : new Set<string>(),
122
+ ast ? collectOnTargetSignalIds(ast, sourceCode) : new Set<string>()
123
+ );
124
+ },
125
+ checkWithContext(
126
+ sourceCode: string,
127
+ filePath: string,
128
+ context: ProjectContext
129
+ ): readonly WardenDiagnostic[] {
130
+ const ast = parse(filePath, sourceCode);
131
+ const localCrudTableIds = ast
132
+ ? collectCrudTableIds(ast)
133
+ : new Set<string>();
134
+ const localOnTargetSignalIds = ast
135
+ ? collectOnTargetSignalIds(ast, sourceCode)
136
+ : new Set<string>();
137
+
138
+ return checkOrphanedSignals(
139
+ ast,
140
+ sourceCode,
141
+ filePath,
142
+ context.crudTableIds ?? localCrudTableIds,
143
+ context.onTargetSignalIds ?? localOnTargetSignalIds
144
+ );
145
+ },
146
+ description:
147
+ 'Warn when CRUD-backed store change signals are never consumed by trail on: declarations.',
148
+ name: 'orphaned-signal',
149
+ severity: 'warn',
150
+ };
@@ -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 './ast.js';
18
+ import type { AstNode } from './ast.js';
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 === 'error' ? 'error' : 'warn',
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
+ };