@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21

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 (249) hide show
  1. package/CHANGELOG.md +497 -6
  2. package/README.md +77 -26
  3. package/bin/warden.ts +50 -0
  4. package/package.json +27 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +28 -0
  7. package/src/cli.ts +1374 -103
  8. package/src/command.ts +953 -0
  9. package/src/config.ts +184 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +106 -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 +206 -14
  16. package/src/project-context.ts +163 -0
  17. package/src/resolve.ts +530 -0
  18. package/src/rules/activation-orphan.ts +97 -0
  19. package/src/rules/ast.ts +3176 -85
  20. package/src/rules/circular-refs.ts +154 -0
  21. package/src/rules/composes-declarations.ts +704 -0
  22. package/src/rules/context-no-surface-types.ts +68 -8
  23. package/src/rules/contour-exists.ts +251 -0
  24. package/src/rules/contour-ids.ts +15 -0
  25. package/src/rules/dead-internal-trail.ts +154 -0
  26. package/src/rules/draft-file-marking.ts +160 -0
  27. package/src/rules/draft-visible-debt.ts +87 -0
  28. package/src/rules/error-mapping-completeness.ts +288 -0
  29. package/src/rules/example-valid.ts +401 -0
  30. package/src/rules/fires-declarations.ts +758 -0
  31. package/src/rules/implementation-returns-result.ts +1265 -95
  32. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  33. package/src/rules/incomplete-crud.ts +580 -0
  34. package/src/rules/index.ts +219 -18
  35. package/src/rules/intent-propagation.ts +127 -0
  36. package/src/rules/layer-field-name-drift.ts +96 -0
  37. package/src/rules/metadata.ts +654 -0
  38. package/src/rules/missing-reconcile.ts +98 -0
  39. package/src/rules/missing-visibility.ts +110 -0
  40. package/src/rules/no-destructured-compose.ts +192 -0
  41. package/src/rules/no-dev-permit-in-source.ts +99 -0
  42. package/src/rules/no-direct-implementation-call.ts +7 -7
  43. package/src/rules/no-legacy-layer-imports.ts +211 -0
  44. package/src/rules/no-native-error-result.ts +111 -0
  45. package/src/rules/no-redundant-result-error-wrap.ts +331 -0
  46. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  47. package/src/rules/no-sync-result-assumption.ts +1134 -99
  48. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  49. package/src/rules/no-throw-in-implementation.ts +10 -9
  50. package/src/rules/no-top-level-surface.ts +389 -0
  51. package/src/rules/on-references-exist.ts +194 -0
  52. package/src/rules/orphaned-signal.ts +150 -0
  53. package/src/rules/owner-projection-parity.ts +146 -0
  54. package/src/rules/permit-governance.ts +25 -0
  55. package/src/rules/public-export-example-coverage.ts +553 -0
  56. package/src/rules/public-internal-deep-imports.ts +517 -0
  57. package/src/rules/public-output-schema.ts +29 -0
  58. package/src/rules/public-union-output-discriminants.ts +150 -0
  59. package/src/rules/read-intent-fires.ts +187 -0
  60. package/src/rules/reference-exists.ts +98 -0
  61. package/src/rules/registry-names.ts +145 -0
  62. package/src/rules/resolved-import-boundary.ts +146 -0
  63. package/src/rules/resource-declarations.ts +704 -0
  64. package/src/rules/resource-exists.ts +179 -0
  65. package/src/rules/resource-id-grammar.ts +65 -0
  66. package/src/rules/resource-mock-coverage.ts +115 -0
  67. package/src/rules/scan.ts +38 -25
  68. package/src/rules/scheduled-destroy-intent.ts +44 -0
  69. package/src/rules/signal-graph-coaching.ts +191 -0
  70. package/src/rules/specs.ts +9 -5
  71. package/src/rules/static-resource-accessor-preference.ts +657 -0
  72. package/src/rules/surface-facet-coherence.ts +370 -0
  73. package/src/rules/trail-versioning-source.ts +1094 -0
  74. package/src/rules/trail-versioning-topo.ts +172 -0
  75. package/src/rules/types.ts +270 -6
  76. package/src/rules/unmaterialized-activation-source.ts +84 -0
  77. package/src/rules/unreachable-detour-shadowing.ts +344 -0
  78. package/src/rules/valid-describe-refs.ts +160 -32
  79. package/src/rules/valid-detour-contract.ts +78 -0
  80. package/src/rules/warden-export-symmetry.ts +533 -0
  81. package/src/rules/warden-rules-use-ast.ts +996 -0
  82. package/src/rules/webhook-route-collision.ts +243 -0
  83. package/src/trails/activation-orphan.trail.ts +84 -0
  84. package/src/trails/circular-refs.trail.ts +29 -0
  85. package/src/trails/composes-declarations.trail.ts +22 -0
  86. package/src/trails/context-no-surface-types.trail.ts +21 -0
  87. package/src/trails/contour-exists.trail.ts +21 -0
  88. package/src/trails/dead-internal-trail.trail.ts +26 -0
  89. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  90. package/src/trails/draft-file-marking.trail.ts +16 -0
  91. package/src/trails/draft-visible-debt.trail.ts +16 -0
  92. package/src/trails/error-mapping-completeness.trail.ts +29 -0
  93. package/src/trails/example-valid.trail.ts +25 -0
  94. package/src/trails/fires-declarations.trail.ts +23 -0
  95. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  96. package/src/trails/implementation-returns-result.trail.ts +20 -0
  97. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  98. package/src/trails/incomplete-crud.trail.ts +39 -0
  99. package/src/trails/index.ts +78 -0
  100. package/src/trails/intent-propagation.trail.ts +30 -0
  101. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  102. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  103. package/src/trails/missing-reconcile.trail.ts +33 -0
  104. package/src/trails/missing-visibility.trail.ts +22 -0
  105. package/src/trails/no-destructured-compose.trail.ts +44 -0
  106. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  107. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  108. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  109. package/src/trails/no-native-error-result.trail.ts +18 -0
  110. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  111. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  112. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  113. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  114. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  115. package/src/trails/no-top-level-surface.trail.ts +43 -0
  116. package/src/trails/on-references-exist.trail.ts +21 -0
  117. package/src/trails/orphaned-signal.trail.ts +36 -0
  118. package/src/trails/owner-projection-parity.trail.ts +26 -0
  119. package/src/trails/pending-force.trail.ts +21 -0
  120. package/src/trails/permit-governance.trail.ts +51 -0
  121. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  122. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  123. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  124. package/src/trails/public-output-schema.trail.ts +55 -0
  125. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  126. package/src/trails/read-intent-fires.trail.ts +20 -0
  127. package/src/trails/reference-exists.trail.ts +25 -0
  128. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  129. package/src/trails/resource-declarations.trail.ts +25 -0
  130. package/src/trails/resource-exists.trail.ts +27 -0
  131. package/src/trails/resource-id-grammar.trail.ts +39 -0
  132. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  133. package/src/trails/run.ts +162 -0
  134. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  135. package/src/trails/schema.ts +194 -0
  136. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  137. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  138. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  139. package/src/trails/topo.ts +6 -0
  140. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  141. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  142. package/src/trails/valid-describe-refs.trail.ts +18 -0
  143. package/src/trails/valid-detour-contract.trail.ts +71 -0
  144. package/src/trails/version-gap.trail.ts +35 -0
  145. package/src/trails/version-pinned-compose.trail.ts +23 -0
  146. package/src/trails/version-without-examples.trail.ts +38 -0
  147. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  148. package/src/trails/warden-rules-use-ast.trail.ts +45 -0
  149. package/src/trails/webhook-route-collision.trail.ts +50 -0
  150. package/src/trails/wrap-rule.ts +213 -0
  151. package/src/workspaces.ts +238 -0
  152. package/.turbo/turbo-build.log +0 -1
  153. package/.turbo/turbo-lint.log +0 -3
  154. package/.turbo/turbo-typecheck.log +0 -1
  155. package/dist/cli.d.ts +0 -46
  156. package/dist/cli.d.ts.map +0 -1
  157. package/dist/cli.js +0 -221
  158. package/dist/cli.js.map +0 -1
  159. package/dist/drift.d.ts +0 -26
  160. package/dist/drift.d.ts.map +0 -1
  161. package/dist/drift.js +0 -27
  162. package/dist/drift.js.map +0 -1
  163. package/dist/formatters.d.ts +0 -29
  164. package/dist/formatters.d.ts.map +0 -1
  165. package/dist/formatters.js +0 -87
  166. package/dist/formatters.js.map +0 -1
  167. package/dist/index.d.ts +0 -26
  168. package/dist/index.d.ts.map +0 -1
  169. package/dist/index.js +0 -26
  170. package/dist/index.js.map +0 -1
  171. package/dist/rules/ast.d.ts +0 -41
  172. package/dist/rules/ast.d.ts.map +0 -1
  173. package/dist/rules/ast.js +0 -163
  174. package/dist/rules/ast.js.map +0 -1
  175. package/dist/rules/context-no-surface-types.d.ts +0 -12
  176. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  177. package/dist/rules/context-no-surface-types.js +0 -96
  178. package/dist/rules/context-no-surface-types.js.map +0 -1
  179. package/dist/rules/implementation-returns-result.d.ts +0 -13
  180. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  181. package/dist/rules/implementation-returns-result.js +0 -231
  182. package/dist/rules/implementation-returns-result.js.map +0 -1
  183. package/dist/rules/index.d.ts +0 -22
  184. package/dist/rules/index.d.ts.map +0 -1
  185. package/dist/rules/index.js +0 -41
  186. package/dist/rules/index.js.map +0 -1
  187. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  188. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  189. package/dist/rules/no-direct-impl-in-route.js +0 -46
  190. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  191. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  192. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  193. package/dist/rules/no-direct-implementation-call.js +0 -39
  194. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  195. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  196. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  197. package/dist/rules/no-sync-result-assumption.js +0 -98
  198. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  199. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  200. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  201. package/dist/rules/no-throw-in-detour-target.js +0 -87
  202. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  203. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  204. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  205. package/dist/rules/no-throw-in-implementation.js +0 -34
  206. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  207. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  208. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  209. package/dist/rules/prefer-schema-inference.js +0 -86
  210. package/dist/rules/prefer-schema-inference.js.map +0 -1
  211. package/dist/rules/scan.d.ts +0 -8
  212. package/dist/rules/scan.d.ts.map +0 -1
  213. package/dist/rules/scan.js +0 -32
  214. package/dist/rules/scan.js.map +0 -1
  215. package/dist/rules/specs.d.ts +0 -29
  216. package/dist/rules/specs.d.ts.map +0 -1
  217. package/dist/rules/specs.js +0 -192
  218. package/dist/rules/specs.js.map +0 -1
  219. package/dist/rules/structure.d.ts +0 -13
  220. package/dist/rules/structure.d.ts.map +0 -1
  221. package/dist/rules/structure.js +0 -142
  222. package/dist/rules/structure.js.map +0 -1
  223. package/dist/rules/types.d.ts +0 -52
  224. package/dist/rules/types.d.ts.map +0 -1
  225. package/dist/rules/types.js +0 -2
  226. package/dist/rules/types.js.map +0 -1
  227. package/dist/rules/valid-describe-refs.d.ts +0 -7
  228. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  229. package/dist/rules/valid-describe-refs.js +0 -51
  230. package/dist/rules/valid-describe-refs.js.map +0 -1
  231. package/dist/rules/valid-detour-refs.d.ts +0 -6
  232. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  233. package/dist/rules/valid-detour-refs.js +0 -116
  234. package/dist/rules/valid-detour-refs.js.map +0 -1
  235. package/src/__tests__/cli.test.ts +0 -198
  236. package/src/__tests__/drift.test.ts +0 -74
  237. package/src/__tests__/formatters.test.ts +0 -157
  238. package/src/__tests__/implementation-returns-result.test.ts +0 -75
  239. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  240. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  241. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  242. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  243. package/src/__tests__/rules.test.ts +0 -188
  244. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  245. package/src/rules/no-direct-impl-in-route.ts +0 -77
  246. package/src/rules/no-throw-in-detour-target.ts +0 -150
  247. package/src/rules/valid-detour-refs.ts +0 -187
  248. package/tsconfig.json +0 -9
  249. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,187 @@
1
+ import {
2
+ buildSignalIdentifierResolver,
3
+ deriveConstString,
4
+ extractStringLiteral,
5
+ findConfigProperty,
6
+ findTrailDefinitions,
7
+ getStringValue,
8
+ identifierName,
9
+ isStringLiteral,
10
+ offsetToLine,
11
+ parse,
12
+ walk,
13
+ } from './ast.js';
14
+ import type { AstNode, SignalIdentifierResolver } from './ast.js';
15
+ import { isTestFile } from './scan.js';
16
+ import type { WardenDiagnostic, WardenRule } from './types.js';
17
+
18
+ interface DeclaredFireSummary {
19
+ readonly count: number;
20
+ readonly ids: readonly string[];
21
+ readonly line: number;
22
+ }
23
+
24
+ const isReadIntent = (config: AstNode): boolean => {
25
+ const intentProp = findConfigProperty(config, 'intent');
26
+ const intentValue = intentProp?.value as AstNode | undefined;
27
+ return isStringLiteral(intentValue) && getStringValue(intentValue) === 'read';
28
+ };
29
+
30
+ const collectArrayBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
31
+ const bindings = new Map<string, AstNode>();
32
+ walk(ast, (node) => {
33
+ if (node.type !== 'VariableDeclarator') {
34
+ return;
35
+ }
36
+
37
+ const { id, init } = node as unknown as {
38
+ readonly id?: AstNode;
39
+ readonly init?: AstNode;
40
+ };
41
+ const name = identifierName(id);
42
+ if (name && init?.type === 'ArrayExpression') {
43
+ bindings.set(name, init);
44
+ }
45
+ });
46
+ return bindings;
47
+ };
48
+
49
+ const getFiresArray = (
50
+ config: AstNode,
51
+ arrayBindings: ReadonlyMap<string, AstNode>
52
+ ): AstNode | null => {
53
+ const firesProp = findConfigProperty(config, 'fires');
54
+ const value = firesProp?.value as AstNode | undefined;
55
+ if (value?.type === 'ArrayExpression') {
56
+ return value;
57
+ }
58
+
59
+ const name = identifierName(value);
60
+ return name ? (arrayBindings.get(name) ?? null) : null;
61
+ };
62
+
63
+ const getFiresElements = (
64
+ config: AstNode,
65
+ arrayBindings: ReadonlyMap<string, AstNode>
66
+ ): readonly AstNode[] => {
67
+ const array = getFiresArray(config, arrayBindings);
68
+ if (!array) {
69
+ return [];
70
+ }
71
+
72
+ return (
73
+ (array as unknown as { readonly elements?: readonly (AstNode | null)[] })
74
+ .elements ?? []
75
+ ).filter((element): element is AstNode => element !== null);
76
+ };
77
+
78
+ const resolveFireElementId = (
79
+ element: AstNode,
80
+ sourceCode: string,
81
+ signalIds: SignalIdentifierResolver
82
+ ): string | null => {
83
+ const literalValue = extractStringLiteral(element);
84
+ if (literalValue !== null) {
85
+ return literalValue;
86
+ }
87
+
88
+ if (element.type !== 'Identifier') {
89
+ return null;
90
+ }
91
+
92
+ const resolved = signalIds.resolve(element);
93
+ if (resolved.kind === 'signal' || resolved.kind === 'string') {
94
+ return resolved.id;
95
+ }
96
+
97
+ const name = identifierName(element);
98
+ return name ? deriveConstString(name, sourceCode) : null;
99
+ };
100
+
101
+ const summarizeDeclaredFires = (
102
+ config: AstNode,
103
+ arrayBindings: ReadonlyMap<string, AstNode>,
104
+ sourceCode: string,
105
+ signalIds: SignalIdentifierResolver
106
+ ): DeclaredFireSummary | null => {
107
+ const firesProp = findConfigProperty(config, 'fires');
108
+ const elements = getFiresElements(config, arrayBindings);
109
+ if (elements.length === 0) {
110
+ return null;
111
+ }
112
+
113
+ const ids: string[] = [];
114
+ for (const element of elements) {
115
+ const resolved = resolveFireElementId(element, sourceCode, signalIds);
116
+ if (resolved) {
117
+ ids.push(resolved);
118
+ }
119
+ }
120
+
121
+ const [firstElement] = elements;
122
+ const lineNode = firesProp ?? firstElement;
123
+ return {
124
+ count: elements.length,
125
+ ids,
126
+ line: lineNode ? offsetToLine(sourceCode, lineNode.start) : 1,
127
+ };
128
+ };
129
+
130
+ const formatSignalList = (summary: DeclaredFireSummary): string => {
131
+ const named = summary.ids.map((id) => `"${id}"`);
132
+ const unresolvedCount = summary.count - summary.ids.length;
133
+ const unresolved =
134
+ unresolvedCount > 0
135
+ ? [
136
+ unresolvedCount === 1
137
+ ? '1 unresolved signal reference'
138
+ : `${unresolvedCount} unresolved signal references`,
139
+ ]
140
+ : [];
141
+ return [...named, ...unresolved].join(', ');
142
+ };
143
+
144
+ const buildDiagnostic = (
145
+ trailId: string,
146
+ summary: DeclaredFireSummary,
147
+ filePath: string
148
+ ): WardenDiagnostic => ({
149
+ filePath,
150
+ line: summary.line,
151
+ 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.`,
152
+ rule: 'read-intent-fires',
153
+ severity: 'warn',
154
+ });
155
+
156
+ export const readIntentFires: WardenRule = {
157
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
158
+ if (isTestFile(filePath)) {
159
+ return [];
160
+ }
161
+
162
+ const ast = parse(filePath, sourceCode);
163
+ if (!ast) {
164
+ return [];
165
+ }
166
+
167
+ const signalIds = buildSignalIdentifierResolver(ast);
168
+ const arrayBindings = collectArrayBindings(ast);
169
+ return findTrailDefinitions(ast).flatMap((def) => {
170
+ if (def.kind !== 'trail' || !isReadIntent(def.config)) {
171
+ return [];
172
+ }
173
+
174
+ const summary = summarizeDeclaredFires(
175
+ def.config,
176
+ arrayBindings,
177
+ sourceCode,
178
+ signalIds
179
+ );
180
+ return summary ? [buildDiagnostic(def.id, summary, filePath)] : [];
181
+ });
182
+ },
183
+ description:
184
+ 'Warn when read-intent trails declare signal fires side effects.',
185
+ name: 'read-intent-fires',
186
+ severity: 'warn',
187
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ collectContourDefinitionIds,
3
+ collectContourReferenceSites,
4
+ offsetToLine,
5
+ parse,
6
+ } from './ast.js';
7
+ import type { AstNode } from './ast.js';
8
+ import { mergeKnownContourIds } from './contour-ids.js';
9
+ import { isTestFile } from './scan.js';
10
+ import type {
11
+ ProjectAwareWardenRule,
12
+ ProjectContext,
13
+ WardenDiagnostic,
14
+ } from './types.js';
15
+
16
+ const buildMissingReferenceDiagnostic = (
17
+ sourceContour: string,
18
+ field: string,
19
+ targetContour: string,
20
+ filePath: string,
21
+ line: number
22
+ ): WardenDiagnostic => ({
23
+ filePath,
24
+ line,
25
+ message: `Contour "${sourceContour}" field "${field}" references contour "${targetContour}" which is not defined in the project. Define it with contour('${targetContour}', ...) and include it in the topo, or fix the field reference if this is a typo.`,
26
+ rule: 'reference-exists',
27
+ severity: 'error',
28
+ });
29
+
30
+ const checkContourReferences = (
31
+ ast: AstNode,
32
+ sourceCode: string,
33
+ filePath: string,
34
+ knownContourIds: ReadonlySet<string>
35
+ ): readonly WardenDiagnostic[] => {
36
+ if (isTestFile(filePath)) {
37
+ return [];
38
+ }
39
+
40
+ return collectContourReferenceSites(ast, knownContourIds).flatMap(
41
+ (reference) => {
42
+ if (knownContourIds.has(reference.target)) {
43
+ return [];
44
+ }
45
+
46
+ return [
47
+ buildMissingReferenceDiagnostic(
48
+ reference.source,
49
+ reference.field,
50
+ reference.target,
51
+ filePath,
52
+ offsetToLine(sourceCode, reference.start)
53
+ ),
54
+ ];
55
+ }
56
+ );
57
+ };
58
+
59
+ /**
60
+ * Checks that every contour `.id()` reference resolves to a known contour.
61
+ */
62
+ export const referenceExists: ProjectAwareWardenRule = {
63
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
64
+ const ast = parse(filePath, sourceCode);
65
+ if (!ast) {
66
+ return [];
67
+ }
68
+
69
+ return checkContourReferences(
70
+ ast,
71
+ sourceCode,
72
+ filePath,
73
+ collectContourDefinitionIds(ast)
74
+ );
75
+ },
76
+ checkWithContext(
77
+ sourceCode: string,
78
+ filePath: string,
79
+ context: ProjectContext
80
+ ): readonly WardenDiagnostic[] {
81
+ const ast = parse(filePath, sourceCode);
82
+ if (!ast) {
83
+ return [];
84
+ }
85
+
86
+ const localContourIds = collectContourDefinitionIds(ast);
87
+ return checkContourReferences(
88
+ ast,
89
+ sourceCode,
90
+ filePath,
91
+ mergeKnownContourIds(localContourIds, context.knownContourIds)
92
+ );
93
+ },
94
+ description:
95
+ 'Ensure every contour field declared via .id() resolves to a known contour.',
96
+ name: 'reference-exists',
97
+ severity: 'error',
98
+ };
@@ -0,0 +1,145 @@
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 { circularRefs } from './circular-refs.js';
11
+ import { contourExists } from './contour-exists.js';
12
+ import { contextNoSurfaceTypes } from './context-no-surface-types.js';
13
+ import { composesDeclarations } from './composes-declarations.js';
14
+ import { deadInternalTrail } from './dead-internal-trail.js';
15
+ import { draftFileMarking } from './draft-file-marking.js';
16
+ import { draftVisibleDebt } from './draft-visible-debt.js';
17
+ import { errorMappingCompleteness } from './error-mapping-completeness.js';
18
+ import { exampleValid } from './example-valid.js';
19
+ import { firesDeclarations } from './fires-declarations.js';
20
+ import { implementationReturnsResult } from './implementation-returns-result.js';
21
+ import { incompleteAccessorForStandardOp } from './incomplete-accessor-for-standard-op.js';
22
+ import { incompleteCrud } from './incomplete-crud.js';
23
+ import { intentPropagation } from './intent-propagation.js';
24
+ import { layerFieldNameDrift } from './layer-field-name-drift.js';
25
+ import { missingReconcile } from './missing-reconcile.js';
26
+ import { missingVisibility } from './missing-visibility.js';
27
+ import { noDevPermitInSource } from './no-dev-permit-in-source.js';
28
+ import { noDestructuredCompose } from './no-destructured-compose.js';
29
+ import { noLegacyLayerImports } from './no-legacy-layer-imports.js';
30
+ import { noDirectImplementationCall } from './no-direct-implementation-call.js';
31
+ import { noNativeErrorResult } from './no-native-error-result.js';
32
+ import { noRedundantResultErrorWrap } from './no-redundant-result-error-wrap.js';
33
+ import { noRetiredCrossVocabulary } from './no-retired-cross-vocabulary.js';
34
+ import { noSyncResultAssumption } from './no-sync-result-assumption.js';
35
+ import { noThrowInDetourRecover } from './no-throw-in-detour-recover.js';
36
+ import { noThrowInImplementation } from './no-throw-in-implementation.js';
37
+ import { noTopLevelSurface } from './no-top-level-surface.js';
38
+ import { onReferencesExist } from './on-references-exist.js';
39
+ import { orphanedSignal } from './orphaned-signal.js';
40
+ import { ownerProjectionParity } from './owner-projection-parity.js';
41
+ import { permitGovernance } from './permit-governance.js';
42
+ import { preferSchemaInference } from './prefer-schema-inference.js';
43
+ import { publicExportExampleCoverage } from './public-export-example-coverage.js';
44
+ import { publicInternalDeepImports } from './public-internal-deep-imports.js';
45
+ import { publicOutputSchema } from './public-output-schema.js';
46
+ import { publicUnionOutputDiscriminants } from './public-union-output-discriminants.js';
47
+ import { readIntentFires } from './read-intent-fires.js';
48
+ import { referenceExists } from './reference-exists.js';
49
+ import { resolvedImportBoundary } from './resolved-import-boundary.js';
50
+ import { resourceDeclarations } from './resource-declarations.js';
51
+ import { resourceExists } from './resource-exists.js';
52
+ import { resourceIdGrammar } from './resource-id-grammar.js';
53
+ import { resourceMockCoverage } from './resource-mock-coverage.js';
54
+ import { scheduledDestroyIntent } from './scheduled-destroy-intent.js';
55
+ import { signalGraphCoaching } from './signal-graph-coaching.js';
56
+ import { staticResourceAccessorPreference } from './static-resource-accessor-preference.js';
57
+ import { surfaceFacetCoherence } from './surface-facet-coherence.js';
58
+ import {
59
+ forkWithoutPreservedBlaze,
60
+ markerSchemaUnsupported,
61
+ versionPinnedCompose,
62
+ } from './trail-versioning-source.js';
63
+ import {
64
+ deprecationWithoutGuidance,
65
+ pendingForce,
66
+ versionGap,
67
+ versionWithoutExamples,
68
+ } from './trail-versioning-topo.js';
69
+ import { unmaterializedActivationSource } from './unmaterialized-activation-source.js';
70
+ import { unreachableDetourShadowing } from './unreachable-detour-shadowing.js';
71
+ import { validDetourContract } from './valid-detour-contract.js';
72
+ import { validDescribeRefs } from './valid-describe-refs.js';
73
+ import { wardenRulesUseAst } from './warden-rules-use-ast.js';
74
+ import { webhookRouteCollision } from './webhook-route-collision.js';
75
+
76
+ /**
77
+ * All non-`warden-export-symmetry` rule identifiers registered in
78
+ * `wardenRules` / `wardenTopoRules`. Excludes the symmetry rule itself to
79
+ * avoid a self-referential check; the symmetry rule adds its own name back in
80
+ * when comparing against the public barrel.
81
+ */
82
+ export const registeredRuleNames: readonly string[] = [
83
+ activationOrphan.name,
84
+ circularRefs.name,
85
+ contextNoSurfaceTypes.name,
86
+ contourExists.name,
87
+ composesDeclarations.name,
88
+ deadInternalTrail.name,
89
+ deprecationWithoutGuidance.name,
90
+ draftFileMarking.name,
91
+ draftVisibleDebt.name,
92
+ errorMappingCompleteness.name,
93
+ exampleValid.name,
94
+ firesDeclarations.name,
95
+ forkWithoutPreservedBlaze.name,
96
+ implementationReturnsResult.name,
97
+ incompleteAccessorForStandardOp.name,
98
+ incompleteCrud.name,
99
+ intentPropagation.name,
100
+ layerFieldNameDrift.name,
101
+ markerSchemaUnsupported.name,
102
+ missingReconcile.name,
103
+ missingVisibility.name,
104
+ noDevPermitInSource.name,
105
+ noDestructuredCompose.name,
106
+ noLegacyLayerImports.name,
107
+ noDirectImplementationCall.name,
108
+ noNativeErrorResult.name,
109
+ noRedundantResultErrorWrap.name,
110
+ noRetiredCrossVocabulary.name,
111
+ noSyncResultAssumption.name,
112
+ noThrowInDetourRecover.name,
113
+ noThrowInImplementation.name,
114
+ noTopLevelSurface.name,
115
+ onReferencesExist.name,
116
+ orphanedSignal.name,
117
+ ownerProjectionParity.name,
118
+ pendingForce.name,
119
+ permitGovernance.name,
120
+ preferSchemaInference.name,
121
+ publicExportExampleCoverage.name,
122
+ publicInternalDeepImports.name,
123
+ publicOutputSchema.name,
124
+ publicUnionOutputDiscriminants.name,
125
+ readIntentFires.name,
126
+ referenceExists.name,
127
+ resolvedImportBoundary.name,
128
+ resourceDeclarations.name,
129
+ resourceExists.name,
130
+ resourceIdGrammar.name,
131
+ resourceMockCoverage.name,
132
+ scheduledDestroyIntent.name,
133
+ signalGraphCoaching.name,
134
+ staticResourceAccessorPreference.name,
135
+ surfaceFacetCoherence.name,
136
+ unmaterializedActivationSource.name,
137
+ unreachableDetourShadowing.name,
138
+ validDetourContract.name,
139
+ validDescribeRefs.name,
140
+ versionGap.name,
141
+ versionPinnedCompose.name,
142
+ versionWithoutExamples.name,
143
+ wardenRulesUseAst.name,
144
+ webhookRouteCollision.name,
145
+ ];
@@ -0,0 +1,146 @@
1
+ import { hasIgnoreCommentOnLine, splitSourceLines } from './ast.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
+ };