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

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 +837 -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,188 @@
1
+ import { buildSignalIdentifierResolver } from './source/signals.js';
2
+ import {
3
+ deriveConstString,
4
+ extractStringLiteral,
5
+ findConfigProperty,
6
+ findTrailDefinitions,
7
+ getNodeElements,
8
+ getNodeId,
9
+ getNodeInit,
10
+ getStringValue,
11
+ identifierName,
12
+ isStringLiteral,
13
+ offsetToLine,
14
+ parse,
15
+ walk,
16
+ } from '@ontrails/source';
17
+ import type { AstNode } from '@ontrails/source';
18
+ import type { SignalIdentifierResolver } from './source/signals.js';
19
+ import { isTestFile } from './scan.js';
20
+ import type { WardenDiagnostic, WardenRule } from './types.js';
21
+
22
+ interface DeclaredFireSummary {
23
+ readonly count: number;
24
+ readonly ids: readonly string[];
25
+ readonly line: number;
26
+ }
27
+
28
+ const isReadIntent = (config: AstNode): boolean => {
29
+ const intentProp = findConfigProperty(config, 'intent');
30
+ const intentValue = intentProp?.value as AstNode | undefined;
31
+ return isStringLiteral(intentValue) && getStringValue(intentValue) === 'read';
32
+ };
33
+
34
+ const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
35
+ const bindings = new Map<string, AstNode>();
36
+ walk(ast, (node) => {
37
+ if (node.type !== 'VariableDeclarator') {
38
+ return;
39
+ }
40
+
41
+ const id = getNodeId(node);
42
+ const init = getNodeInit(node);
43
+ const name = identifierName(id);
44
+ if (name && init?.type === 'ArrayExpression') {
45
+ bindings.set(name, init);
46
+ }
47
+ });
48
+ return bindings;
49
+ };
50
+
51
+ const getFiresArray = (
52
+ config: AstNode,
53
+ arrayBindings: ReadonlyMap<string, AstNode>
54
+ ): AstNode | null => {
55
+ const firesProp = findConfigProperty(config, 'fires');
56
+ const value = firesProp?.value as AstNode | undefined;
57
+ if (value?.type === 'ArrayExpression') {
58
+ return value;
59
+ }
60
+
61
+ const name = identifierName(value);
62
+ return name ? (arrayBindings.get(name) ?? null) : null;
63
+ };
64
+
65
+ const getFiresElements = (
66
+ config: AstNode,
67
+ arrayBindings: ReadonlyMap<string, AstNode>
68
+ ): readonly AstNode[] => {
69
+ const array = getFiresArray(config, arrayBindings);
70
+ if (!array) {
71
+ return [];
72
+ }
73
+
74
+ return getNodeElements(array).filter(
75
+ (element): element is AstNode => element !== null
76
+ );
77
+ };
78
+
79
+ const resolveFireElementId = (
80
+ element: AstNode,
81
+ sourceCode: string,
82
+ signalIds: SignalIdentifierResolver
83
+ ): string | null => {
84
+ const literalValue = extractStringLiteral(element);
85
+ if (literalValue !== null) {
86
+ return literalValue;
87
+ }
88
+
89
+ if (element.type !== 'Identifier') {
90
+ return null;
91
+ }
92
+
93
+ const resolved = signalIds.resolve(element);
94
+ if (resolved.kind === 'signal' || resolved.kind === 'string') {
95
+ return resolved.id;
96
+ }
97
+
98
+ const name = identifierName(element);
99
+ return name ? deriveConstString(name, sourceCode) : null;
100
+ };
101
+
102
+ const summarizeDeclaredFires = (
103
+ config: AstNode,
104
+ arrayBindings: ReadonlyMap<string, AstNode>,
105
+ sourceCode: string,
106
+ signalIds: SignalIdentifierResolver
107
+ ): DeclaredFireSummary | null => {
108
+ const firesProp = findConfigProperty(config, 'fires');
109
+ const elements = getFiresElements(config, arrayBindings);
110
+ if (elements.length === 0) {
111
+ return null;
112
+ }
113
+
114
+ const ids: string[] = [];
115
+ for (const element of elements) {
116
+ const resolved = resolveFireElementId(element, sourceCode, signalIds);
117
+ if (resolved) {
118
+ ids.push(resolved);
119
+ }
120
+ }
121
+
122
+ const [firstElement] = elements;
123
+ const lineNode = firesProp ?? firstElement;
124
+ return {
125
+ count: elements.length,
126
+ ids,
127
+ line: lineNode ? offsetToLine(sourceCode, lineNode.start) : 1,
128
+ };
129
+ };
130
+
131
+ const formatSignalList = (summary: DeclaredFireSummary): string => {
132
+ const named = summary.ids.map((id) => `"${id}"`);
133
+ const unresolvedCount = summary.count - summary.ids.length;
134
+ const unresolved =
135
+ unresolvedCount > 0
136
+ ? [
137
+ unresolvedCount === 1
138
+ ? '1 unresolved signal reference'
139
+ : `${unresolvedCount} unresolved signal references`,
140
+ ]
141
+ : [];
142
+ return [...named, ...unresolved].join(', ');
143
+ };
144
+
145
+ const buildDiagnostic = (
146
+ trailId: string,
147
+ summary: DeclaredFireSummary,
148
+ filePath: string
149
+ ): WardenDiagnostic => ({
150
+ filePath,
151
+ line: summary.line,
152
+ message: `Trail "${trailId}" declares intent: 'read' but also declares fires: [${formatSignalList(summary)}]. Read trails should remain side-effect-free; change the trail intent or move ctx.fire behavior to an appropriate write trail.`,
153
+ rule: 'read-intent-fires',
154
+ severity: 'warn',
155
+ });
156
+
157
+ export const readIntentFires: WardenRule = {
158
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
159
+ if (isTestFile(filePath)) {
160
+ return [];
161
+ }
162
+
163
+ const ast = parse(filePath, sourceCode);
164
+ if (!ast) {
165
+ return [];
166
+ }
167
+
168
+ const signalIds = buildSignalIdentifierResolver(ast);
169
+ const arrayBindings = collectArrayBindings(ast);
170
+ return findTrailDefinitions(ast).flatMap((def) => {
171
+ if (def.kind !== 'trail' || !isReadIntent(def.config)) {
172
+ return [];
173
+ }
174
+
175
+ const summary = summarizeDeclaredFires(
176
+ def.config,
177
+ arrayBindings,
178
+ sourceCode,
179
+ signalIds
180
+ );
181
+ return summary ? [buildDiagnostic(def.id, summary, filePath)] : [];
182
+ });
183
+ },
184
+ description:
185
+ 'Warn when read-intent trails declare signal fires side effects.',
186
+ name: 'read-intent-fires',
187
+ severity: 'warn',
188
+ };
@@ -0,0 +1,97 @@
1
+ import {
2
+ collectEntityDefinitionIds,
3
+ collectEntityReferenceSites,
4
+ } from './source/entities.js';
5
+ import { offsetToLine, parse } from '@ontrails/source';
6
+ import type { AstNode } from '@ontrails/source';
7
+ import { mergeKnownEntityIds } from './entity-ids.js';
8
+ import { isTestFile } from './scan.js';
9
+ import type {
10
+ ProjectAwareWardenRule,
11
+ ProjectContext,
12
+ WardenDiagnostic,
13
+ } from './types.js';
14
+
15
+ const buildMissingReferenceDiagnostic = (
16
+ sourceEntity: string,
17
+ field: string,
18
+ targetEntity: string,
19
+ filePath: string,
20
+ line: number
21
+ ): WardenDiagnostic => ({
22
+ filePath,
23
+ line,
24
+ message: `Entity "${sourceEntity}" field "${field}" references entity "${targetEntity}" which is not defined in the project. Define it with entity('${targetEntity}', ...) and include it in the topo, or fix the field reference if this is a typo.`,
25
+ rule: 'reference-exists',
26
+ severity: 'error',
27
+ });
28
+
29
+ const checkEntityReferences = (
30
+ ast: AstNode,
31
+ sourceCode: string,
32
+ filePath: string,
33
+ knownEntityIds: ReadonlySet<string>
34
+ ): readonly WardenDiagnostic[] => {
35
+ if (isTestFile(filePath)) {
36
+ return [];
37
+ }
38
+
39
+ return collectEntityReferenceSites(ast, knownEntityIds).flatMap(
40
+ (reference) => {
41
+ if (knownEntityIds.has(reference.target)) {
42
+ return [];
43
+ }
44
+
45
+ return [
46
+ buildMissingReferenceDiagnostic(
47
+ reference.source,
48
+ reference.field,
49
+ reference.target,
50
+ filePath,
51
+ offsetToLine(sourceCode, reference.start)
52
+ ),
53
+ ];
54
+ }
55
+ );
56
+ };
57
+
58
+ /**
59
+ * Checks that every entity `.id()` reference resolves to a known entity.
60
+ */
61
+ export const referenceExists: ProjectAwareWardenRule = {
62
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
63
+ const ast = parse(filePath, sourceCode);
64
+ if (!ast) {
65
+ return [];
66
+ }
67
+
68
+ return checkEntityReferences(
69
+ ast,
70
+ sourceCode,
71
+ filePath,
72
+ collectEntityDefinitionIds(ast)
73
+ );
74
+ },
75
+ checkWithContext(
76
+ sourceCode: string,
77
+ filePath: string,
78
+ context: ProjectContext
79
+ ): readonly WardenDiagnostic[] {
80
+ const ast = parse(filePath, sourceCode);
81
+ if (!ast) {
82
+ return [];
83
+ }
84
+
85
+ const localEntityIds = collectEntityDefinitionIds(ast);
86
+ return checkEntityReferences(
87
+ ast,
88
+ sourceCode,
89
+ filePath,
90
+ mergeKnownEntityIds(localEntityIds, context.knownEntityIds)
91
+ );
92
+ },
93
+ description:
94
+ 'Ensure every entity field declared via .id() resolves to a known entity.',
95
+ name: 'reference-exists',
96
+ severity: 'error',
97
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Registry name snapshot used by `warden-export-symmetry`.
3
+ *
4
+ * Imports each rule module directly to avoid a dependency cycle with
5
+ * `./index.ts`, which itself imports `warden-export-symmetry`. Keep this
6
+ * list in lockstep with `wardenRules` / `wardenTopoRules` in `./index.ts` —
7
+ * the `warden-export-symmetry` rule will fail the build if they drift.
8
+ */
9
+ import { activationOrphan } from './activation-orphan.js';
10
+ import { capturedKernel } from './captured-kernel.js';
11
+ import { cliCommandRouteCoherence } from './cli-command-route-coherence.js';
12
+ import { circularRefs } from './circular-refs.js';
13
+ import { entityExists } from './entity-exists.js';
14
+ import { contextNoSurfaceTypes } from './context-no-surface-types.js';
15
+ import { composesDeclarations } from './composes-declarations.js';
16
+ import { deadInternalTrail } from './dead-internal-trail.js';
17
+ import { deadPublicTrail } from './dead-public-trail.js';
18
+ import { draftFileMarking } from './draft-file-marking.js';
19
+ import { draftVisibleDebt } from './draft-visible-debt.js';
20
+ import { duplicateExportedSymbol } from './duplicate-exported-symbol.js';
21
+ import { duplicatePublicContract } from './duplicate-public-contract.js';
22
+ import { errorMappingCompleteness } from './error-mapping-completeness.js';
23
+ import { exampleValid } from './example-valid.js';
24
+ import { firesDeclarations } from './fires-declarations.js';
25
+ import { governedSymbolResidue } from './governed-symbol-residue.js';
26
+ import { implementationReturnsResult } from './implementation-returns-result.js';
27
+ import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
28
+ import { incompleteCrud } from './incomplete-crud.js';
29
+ import { intentPropagation } from './intent-propagation.js';
30
+ import { layerFieldNameDrift } from './layer-field-name-drift.js';
31
+ import { libraryProjectionCoherence } from './library-projection-coherence.js';
32
+ import { missingReconcile } from './missing-reconcile.js';
33
+ import { missingVisibility } from './missing-visibility.js';
34
+ import { noDevPermitInSource } from './no-dev-permit-in-source.js';
35
+ import { noDestructuredCompose } from './no-destructured-compose.js';
36
+ import { noLegacyCliAliasExport } from './no-legacy-cli-alias-export.js';
37
+ import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
38
+ import { noDirectImplementationCall } from './no-direct-implementation-call.js';
39
+ import { noNativeErrorResult } from './no-native-error-result.js';
40
+ import { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
41
+ import { noRetiredCrossVocabulary } from './no-retired-cross-vocabulary.js';
42
+ import { noSyncResultAssumption } from './no-sync-result-assumption.js';
43
+ import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
44
+ import { noThrowInImplementation } from './no-throw-in-implementation.js';
45
+ import { noTopLevelSurface } from './no-top-level-surface.js';
46
+ import { onReferencesExist } from './on-references-exist.js';
47
+ import { orphanedSignal } from './orphaned-signal.js';
48
+ import { ownerProjectionParity } from './owner-projection-parity.js';
49
+ import { permitGovernance } from './permit-governance.js';
50
+ import { preferSchemaInference } from './prefer-schema-inference.js';
51
+ import { publicExportExampleCoverage } from './public-export-example-coverage.js';
52
+ import { publicInternalDeepImports } from './public-internal-deep-imports.js';
53
+ import { publicOutputSchema } from './public-output-schema.js';
54
+ import { publicUnionOutputDiscriminants } from './public-union-output-discriminants.js';
55
+ import { readIntentFires } from './read-intent-fires.js';
56
+ import { referenceExists } from './reference-exists.js';
57
+ import { resolvedImportBoundary } from './resolved-import-boundary.js';
58
+ import { resourceDeclarations } from './resource-declarations.js';
59
+ import { resourceExists } from './resource-exists.js';
60
+ import { resourceIdGrammar } from './resource-id-grammar.js';
61
+ import { resourceMockCoverage } from './resource-mock-coverage.js';
62
+ import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
63
+ import { signalGraphCoaching } from './signal-graph-coaching.js';
64
+ import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
65
+ import { surfaceOverlayCoherence } from './surface-overlay-coherence.js';
66
+ import { surfaceTrailheadCoherence } from './surface-trailhead-coherence.js';
67
+ import { trailForkCoaching } from './trail-fork-coaching.js';
68
+ import { trailheadOverrideDivergence } from './trailhead-override-divergence.js';
69
+ import {
70
+ forkWithoutPreservedImplementation,
71
+ markerSchemaUnsupported,
72
+ versionPinnedCompose,
73
+ } from './trail-versioning-source.js';
74
+ import {
75
+ deprecationWithoutGuidance,
76
+ pendingForce,
77
+ versionGap,
78
+ versionWithoutExamples,
79
+ } from './trail-versioning-topo.js';
80
+ import { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
81
+ import { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
82
+ import { validDetourContract } from './valid-detour-contract.js';
83
+ import { validDescribeRefs } from './valid-describe-refs.js';
84
+ import { wardenRulesUseAst } from './warden-rules-use-ast.js';
85
+ import { webhookRouteCollision } from './webhook-route-collision.js';
86
+
87
+ /**
88
+ * All non-`warden-export-symmetry` rule identifiers registered in
89
+ * `wardenRules` / `wardenTopoRules`. Excludes the symmetry rule itself to
90
+ * avoid a self-referential check; the symmetry rule adds its own name back in
91
+ * when comparing against the public barrel.
92
+ */
93
+ export const registeredRuleNames: readonly string[] = [
94
+ activationOrphan.name,
95
+ capturedKernel.name,
96
+ cliCommandRouteCoherence.name,
97
+ circularRefs.name,
98
+ contextNoSurfaceTypes.name,
99
+ entityExists.name,
100
+ composesDeclarations.name,
101
+ deadInternalTrail.name,
102
+ deadPublicTrail.name,
103
+ deprecationWithoutGuidance.name,
104
+ duplicateExportedSymbol.name,
105
+ duplicatePublicContract.name,
106
+ draftFileMarking.name,
107
+ draftVisibleDebt.name,
108
+ errorMappingCompleteness.name,
109
+ exampleValid.name,
110
+ firesDeclarations.name,
111
+ governedSymbolResidue.name,
112
+ forkWithoutPreservedImplementation.name,
113
+ implementationReturnsResult.name,
114
+ incompleteAccessorForStandardOp.name,
115
+ incompleteCrud.name,
116
+ intentPropagation.name,
117
+ layerFieldNameDrift.name,
118
+ libraryProjectionCoherence.name,
119
+ markerSchemaUnsupported.name,
120
+ missingReconcile.name,
121
+ missingVisibility.name,
122
+ noDevPermitInSource.name,
123
+ noDestructuredCompose.name,
124
+ noLegacyCliAliasExport.name,
125
+ noLegacyLayerImports.name,
126
+ noDirectImplementationCall.name,
127
+ noNativeErrorResult.name,
128
+ noRedundantResultErrorWrap.name,
129
+ noRetiredCrossVocabulary.name,
130
+ noSyncResultAssumption.name,
131
+ noThrowInDetourRecover.name,
132
+ noThrowInImplementation.name,
133
+ noTopLevelSurface.name,
134
+ onReferencesExist.name,
135
+ orphanedSignal.name,
136
+ ownerProjectionParity.name,
137
+ pendingForce.name,
138
+ permitGovernance.name,
139
+ preferSchemaInference.name,
140
+ publicExportExampleCoverage.name,
141
+ publicInternalDeepImports.name,
142
+ publicOutputSchema.name,
143
+ publicUnionOutputDiscriminants.name,
144
+ readIntentFires.name,
145
+ referenceExists.name,
146
+ resolvedImportBoundary.name,
147
+ resourceDeclarations.name,
148
+ resourceExists.name,
149
+ resourceIdGrammar.name,
150
+ resourceMockCoverage.name,
151
+ scheduledDestroyIntent.name,
152
+ signalGraphCoaching.name,
153
+ staticResourceAccessorPreference.name,
154
+ surfaceOverlayCoherence.name,
155
+ surfaceTrailheadCoherence.name,
156
+ trailForkCoaching.name,
157
+ trailheadOverrideDivergence.name,
158
+ unmaterializedActivationSource.name,
159
+ unreachableDetourShadowing.name,
160
+ validDetourContract.name,
161
+ validDescribeRefs.name,
162
+ versionGap.name,
163
+ versionPinnedCompose.name,
164
+ versionWithoutExamples.name,
165
+ wardenRulesUseAst.name,
166
+ webhookRouteCollision.name,
167
+ ];
@@ -0,0 +1,146 @@
1
+ import { hasIgnoreCommentOnLine, splitSourceLines } from './source/pragmas.js';
2
+ import { isTestFile } from './scan.js';
3
+ import type { WardenImportResolution } from '../resolve.js';
4
+ import type {
5
+ ProjectAwareWardenRule,
6
+ ProjectContext,
7
+ WardenDiagnostic,
8
+ } from './types.js';
9
+
10
+ const RULE_NAME = 'resolved-import-boundary';
11
+
12
+ const normalizePath = (path: string): string => path.replaceAll('\\', '/');
13
+
14
+ const isLocalPathImport = (importSource: string): boolean =>
15
+ importSource.startsWith('.') || importSource.startsWith('/');
16
+
17
+ const isFixtureOrMigrationFile = (filePath: string): boolean => {
18
+ const normalized = normalizePath(filePath);
19
+ return /(?:^|\/)(?:__fixtures__|fixtures?|migrations?)(?:\/|$)/.test(
20
+ normalized
21
+ );
22
+ };
23
+
24
+ const isAllowlistedFile = (filePath: string): boolean =>
25
+ isTestFile(filePath) || isFixtureOrMigrationFile(filePath);
26
+
27
+ const resolutionLabel = (resolution: {
28
+ readonly importSource: string;
29
+ readonly packageName?: string | undefined;
30
+ }): string => resolution.packageName ?? resolution.importSource;
31
+
32
+ const publicSurfaceMessage = (resolution: {
33
+ readonly importSource: string;
34
+ readonly packageName?: string | undefined;
35
+ }): string =>
36
+ `Import "${resolution.importSource}" is not exported by ${resolutionLabel(
37
+ resolution
38
+ )}. Import the package root or an exported subpath instead.`;
39
+
40
+ const localPathBoundaryMessage = (resolution: {
41
+ readonly importSource: string;
42
+ readonly packageName?: string | undefined;
43
+ }): string =>
44
+ `Local import "${resolution.importSource}" composes into ${resolutionLabel(
45
+ resolution
46
+ )}. Import the target package public surface instead.`;
47
+
48
+ const internalTargetMessage = (resolution: {
49
+ readonly importSource: string;
50
+ readonly packageName?: string | undefined;
51
+ }): string =>
52
+ `Import "${resolution.importSource}" targets internal/private files in ${resolutionLabel(
53
+ resolution
54
+ )}. Import the target package public surface instead.`;
55
+
56
+ const diagnosticForResolution = (
57
+ filePath: string,
58
+ resolution: WardenImportResolution
59
+ ): WardenDiagnostic | null => {
60
+ if (!resolution.crossesPackageBoundary) {
61
+ return null;
62
+ }
63
+
64
+ if (resolution.isInternalTarget) {
65
+ return {
66
+ filePath,
67
+ line: resolution.line,
68
+ message: internalTargetMessage(resolution),
69
+ rule: RULE_NAME,
70
+ severity: 'error',
71
+ };
72
+ }
73
+
74
+ if (isLocalPathImport(resolution.importSource)) {
75
+ return {
76
+ filePath,
77
+ line: resolution.line,
78
+ message: localPathBoundaryMessage(resolution),
79
+ rule: RULE_NAME,
80
+ severity: 'error',
81
+ };
82
+ }
83
+
84
+ if (resolution.errorKind === 'package-path-not-exported') {
85
+ return {
86
+ filePath,
87
+ line: resolution.line,
88
+ message: publicSurfaceMessage(resolution),
89
+ rule: RULE_NAME,
90
+ severity: 'error',
91
+ };
92
+ }
93
+
94
+ if (
95
+ resolution.errorKind &&
96
+ resolution.errorKind !== 'builtin' &&
97
+ resolution.errorKind !== 'ignored'
98
+ ) {
99
+ return {
100
+ filePath,
101
+ line: resolution.line,
102
+ message: publicSurfaceMessage(resolution),
103
+ rule: RULE_NAME,
104
+ severity: 'error',
105
+ };
106
+ }
107
+
108
+ return null;
109
+ };
110
+
111
+ const importResolutionsForFile = (context: ProjectContext, filePath: string) =>
112
+ context.importResolutionsByFile?.get(filePath) ?? [];
113
+
114
+ export const resolvedImportBoundary: ProjectAwareWardenRule = {
115
+ check(): readonly WardenDiagnostic[] {
116
+ return [];
117
+ },
118
+ checkWithContext(
119
+ sourceCode: string,
120
+ filePath: string,
121
+ context: ProjectContext
122
+ ): readonly WardenDiagnostic[] {
123
+ if (isAllowlistedFile(filePath)) {
124
+ return [];
125
+ }
126
+
127
+ const lines = splitSourceLines(sourceCode);
128
+ const diagnostics: WardenDiagnostic[] = [];
129
+
130
+ for (const resolution of importResolutionsForFile(context, filePath)) {
131
+ if (hasIgnoreCommentOnLine(lines, resolution.line)) {
132
+ continue;
133
+ }
134
+ const diagnostic = diagnosticForResolution(filePath, resolution);
135
+ if (diagnostic) {
136
+ diagnostics.push(diagnostic);
137
+ }
138
+ }
139
+
140
+ return diagnostics;
141
+ },
142
+ description:
143
+ 'Ensure compose-package imports resolve through package-owned public exports.',
144
+ name: RULE_NAME,
145
+ severity: 'error',
146
+ };