@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,254 @@
1
+ import {
2
+ buildUserNamespaceContext,
3
+ collectEntityDefinitionIds,
4
+ collectImportAliasMap,
5
+ collectNamedEntityIds,
6
+ deriveEntityIdentifierName,
7
+ isUserNamespaceReceiverAllowed,
8
+ } from './source/entities.js';
9
+ import {
10
+ extractFirstStringArg,
11
+ findConfigProperty,
12
+ findTrailDefinitions,
13
+ getNodeCallee,
14
+ getNodeObject,
15
+ getNodeProperty,
16
+ identifierName,
17
+ isMemberAccessNonComputed,
18
+ offsetToLine,
19
+ parse,
20
+ } from '@ontrails/source';
21
+ import type { AstNode, TrailDefinition } from '@ontrails/source';
22
+ import type { UserNamespaceContext } from './source/entities.js';
23
+ import { mergeKnownEntityIds } from './entity-ids.js';
24
+ import { isTestFile } from './scan.js';
25
+ import type {
26
+ ProjectAwareWardenRule,
27
+ ProjectContext,
28
+ WardenDiagnostic,
29
+ } from './types.js';
30
+
31
+ const isEntityCall = (node: AstNode): boolean =>
32
+ node.type === 'CallExpression' &&
33
+ identifierName(getNodeCallee(node)) === 'entity';
34
+
35
+ const getEntityElements = (config: AstNode): readonly AstNode[] => {
36
+ const entitiesProp = findConfigProperty(config, 'entities');
37
+ if (!entitiesProp) {
38
+ return [];
39
+ }
40
+
41
+ const arrayNode = entitiesProp.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 `entities.user` to its entity name. When `userNamespace` carries a
54
+ * scope-aware `safeMemberStarts` set, the member access must appear in it —
55
+ * rejecting cases where `entities` is shadowed by a local binding such as a
56
+ * function parameter or `const entities = ...`. Without the set, falls back
57
+ * to the bare name check for backward compatibility.
58
+ */
59
+ const resolveNamespaceMemberEntityName = (
60
+ element: AstNode,
61
+ userNamespace: UserNamespaceContext
62
+ ): string | null => {
63
+ if (!isMemberAccessNonComputed(element)) {
64
+ return null;
65
+ }
66
+ const object = getNodeObject(element);
67
+ const property = getNodeProperty(element);
68
+ const receiver = object ? identifierName(object) : null;
69
+ if (
70
+ !receiver ||
71
+ !isUserNamespaceReceiverAllowed(receiver, element.start, userNamespace)
72
+ ) {
73
+ return null;
74
+ }
75
+ return property ? identifierName(property) : null;
76
+ };
77
+
78
+ const resolveDeclaredEntityName = (
79
+ element: AstNode,
80
+ entityIdsByName: ReadonlyMap<string, string>,
81
+ knownEntityIds?: ReadonlySet<string>,
82
+ importAliases?: ReadonlyMap<string, string>,
83
+ userNamespace?: UserNamespaceContext
84
+ ): string | null => {
85
+ if (element.type === 'Identifier') {
86
+ const name = identifierName(element);
87
+ return name
88
+ ? deriveEntityIdentifierName(
89
+ name,
90
+ entityIdsByName,
91
+ knownEntityIds,
92
+ importAliases
93
+ )
94
+ : null;
95
+ }
96
+
97
+ if (userNamespace && userNamespace.bindings.size > 0) {
98
+ const namespaceTarget = resolveNamespaceMemberEntityName(
99
+ element,
100
+ userNamespace
101
+ );
102
+ if (namespaceTarget) {
103
+ return namespaceTarget;
104
+ }
105
+ }
106
+
107
+ return isEntityCall(element) ? extractFirstStringArg(element) : null;
108
+ };
109
+
110
+ const extractDeclaredEntityNames = (
111
+ config: AstNode,
112
+ entityIdsByName: ReadonlyMap<string, string>,
113
+ knownEntityIds?: ReadonlySet<string>,
114
+ importAliases?: ReadonlyMap<string, string>,
115
+ userNamespace?: UserNamespaceContext
116
+ ): readonly string[] => [
117
+ ...new Set(
118
+ getEntityElements(config).flatMap((element) => {
119
+ const entityName = resolveDeclaredEntityName(
120
+ element,
121
+ entityIdsByName,
122
+ knownEntityIds,
123
+ importAliases,
124
+ userNamespace
125
+ );
126
+ return entityName ? [entityName] : [];
127
+ })
128
+ ),
129
+ ];
130
+
131
+ const buildMissingEntityDiagnostic = (
132
+ trailId: string,
133
+ entityName: string,
134
+ filePath: string,
135
+ line: number
136
+ ): WardenDiagnostic => ({
137
+ filePath,
138
+ line,
139
+ message: `Trail "${trailId}" declares entity "${entityName}" which is not defined in the project. Define it with entity('${entityName}', ...) and include it in the topo, or fix the entities entry if this is a typo.`,
140
+ rule: 'entity-exists',
141
+ severity: 'error',
142
+ });
143
+
144
+ const buildDiagnosticsForDefinition = (
145
+ definition: TrailDefinition,
146
+ sourceCode: string,
147
+ filePath: string,
148
+ knownEntityIds: ReadonlySet<string>,
149
+ entityIdsByName: ReadonlyMap<string, string>,
150
+ importAliases: ReadonlyMap<string, string>,
151
+ userNamespace: UserNamespaceContext
152
+ ): readonly WardenDiagnostic[] => {
153
+ if (definition.kind !== 'trail') {
154
+ return [];
155
+ }
156
+
157
+ const line = offsetToLine(sourceCode, definition.start);
158
+ return extractDeclaredEntityNames(
159
+ definition.config,
160
+ entityIdsByName,
161
+ knownEntityIds,
162
+ importAliases,
163
+ userNamespace
164
+ ).flatMap((entityName) =>
165
+ knownEntityIds.has(entityName)
166
+ ? []
167
+ : [
168
+ buildMissingEntityDiagnostic(
169
+ definition.id,
170
+ entityName,
171
+ filePath,
172
+ line
173
+ ),
174
+ ]
175
+ );
176
+ };
177
+
178
+ const buildEntityDiagnostics = (
179
+ ast: AstNode,
180
+ sourceCode: string,
181
+ filePath: string,
182
+ knownEntityIds: ReadonlySet<string>
183
+ ): readonly WardenDiagnostic[] => {
184
+ const entityIdsByName = collectNamedEntityIds(ast);
185
+ const importAliases = collectImportAliasMap(ast);
186
+ const userNamespace = buildUserNamespaceContext(ast);
187
+
188
+ return findTrailDefinitions(ast).flatMap((definition) =>
189
+ buildDiagnosticsForDefinition(
190
+ definition,
191
+ sourceCode,
192
+ filePath,
193
+ knownEntityIds,
194
+ entityIdsByName,
195
+ importAliases,
196
+ userNamespace
197
+ )
198
+ );
199
+ };
200
+
201
+ const checkEntityDeclarations = (
202
+ ast: AstNode,
203
+ sourceCode: string,
204
+ filePath: string,
205
+ knownEntityIds: ReadonlySet<string>
206
+ ): readonly WardenDiagnostic[] => {
207
+ if (isTestFile(filePath)) {
208
+ return [];
209
+ }
210
+
211
+ return buildEntityDiagnostics(ast, sourceCode, filePath, knownEntityIds);
212
+ };
213
+
214
+ /**
215
+ * Checks that every entity declared in a trail `entities` array resolves to a
216
+ * known entity definition.
217
+ */
218
+ export const entityExists: ProjectAwareWardenRule = {
219
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
220
+ const ast = parse(filePath, sourceCode);
221
+ if (!ast) {
222
+ return [];
223
+ }
224
+
225
+ return checkEntityDeclarations(
226
+ ast,
227
+ sourceCode,
228
+ filePath,
229
+ collectEntityDefinitionIds(ast)
230
+ );
231
+ },
232
+ checkWithContext(
233
+ sourceCode: string,
234
+ filePath: string,
235
+ context: ProjectContext
236
+ ): readonly WardenDiagnostic[] {
237
+ const ast = parse(filePath, sourceCode);
238
+ if (!ast) {
239
+ return [];
240
+ }
241
+
242
+ const localEntityIds = collectEntityDefinitionIds(ast);
243
+ return checkEntityDeclarations(
244
+ ast,
245
+ sourceCode,
246
+ filePath,
247
+ mergeKnownEntityIds(localEntityIds, context.knownEntityIds)
248
+ );
249
+ },
250
+ description:
251
+ 'Ensure every entity declared on a trail resolves to a known entity definition.',
252
+ name: 'entity-exists',
253
+ severity: 'error',
254
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Merge a file's locally-defined entity IDs with the project-wide set.
3
+ *
4
+ * Rules that run with a `ProjectContext` need to treat both local and
5
+ * project-wide entity definitions as "known" so that declarations and
6
+ * references resolve correctly. When no project context is available — e.g.
7
+ * single-file lint runs via `check` — the local set is returned as-is.
8
+ */
9
+ export const mergeKnownEntityIds = (
10
+ localEntityIds: ReadonlySet<string>,
11
+ projectEntityIds?: ReadonlySet<string>
12
+ ): ReadonlySet<string> =>
13
+ projectEntityIds
14
+ ? new Set([...projectEntityIds, ...localEntityIds])
15
+ : localEntityIds;
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Validates that registered surface error mappers cover every error category.
3
+ *
4
+ * Scans `createSurfaceErrorMapper(...)` calls, then resolves simple object
5
+ * literals, identifier bindings, and object-property references in the same
6
+ * file so incomplete mapper registrations are caught before they ship.
7
+ */
8
+
9
+ import { codesByCategory, errorClasses } from '@ontrails/core';
10
+ import type { ErrorCategory } from '@ontrails/core';
11
+
12
+ import {
13
+ getNodeKey,
14
+ getNodeValueNode,
15
+ getStringValue,
16
+ identifierName,
17
+ isStringLiteral,
18
+ offsetToLine,
19
+ parse,
20
+ walk,
21
+ } from '@ontrails/source';
22
+ import type { AstNode } from '@ontrails/source';
23
+ import { isTestFile } from './scan.js';
24
+ import type { WardenDiagnostic, WardenRule } from './types.js';
25
+
26
+ const MEMBER_EXPRESSION_TYPES = new Set([
27
+ 'MemberExpression',
28
+ 'StaticMemberExpression',
29
+ ]);
30
+
31
+ const MAPPER_FACTORY_NAMES = new Set(['createSurfaceErrorMapper']);
32
+
33
+ const mappedErrorClassCategories = new Set(
34
+ errorClasses.flatMap((entry) =>
35
+ entry.category === 'dynamic' ? [] : [entry.category]
36
+ )
37
+ );
38
+
39
+ const requiredErrorCategories = (
40
+ Object.keys(codesByCategory) as ErrorCategory[]
41
+ ).filter((category) => mappedErrorClassCategories.has(category));
42
+
43
+ const getPropertyName = (node: AstNode | undefined): string | null => {
44
+ if (!node) {
45
+ return null;
46
+ }
47
+
48
+ return (
49
+ identifierName(node) ??
50
+ (isStringLiteral(node) ? getStringValue(node) : null)
51
+ );
52
+ };
53
+
54
+ const collectObjectBindings = (ast: AstNode): ReadonlyMap<string, AstNode> => {
55
+ const bindings = new Map<string, AstNode>();
56
+
57
+ walk(ast, (node) => {
58
+ if (node.type !== 'VariableDeclarator') {
59
+ return;
60
+ }
61
+
62
+ const { id, init } = node as { id?: AstNode; init?: AstNode };
63
+ const bindingName = identifierName(id);
64
+
65
+ if (bindingName && init?.type === 'ObjectExpression') {
66
+ bindings.set(bindingName, init);
67
+ }
68
+ });
69
+
70
+ return bindings;
71
+ };
72
+
73
+ const getObjectProperties = (objectNode: AstNode): readonly AstNode[] =>
74
+ objectNode.type === 'ObjectExpression'
75
+ ? ((objectNode['properties'] as readonly AstNode[] | undefined) ?? [])
76
+ : [];
77
+
78
+ const findObjectPropertyValue = (
79
+ objectNode: AstNode,
80
+ propertyName: string
81
+ ): AstNode | null => {
82
+ for (const property of getObjectProperties(objectNode)) {
83
+ if (property.type !== 'Property') {
84
+ continue;
85
+ }
86
+
87
+ const key = getPropertyName(getNodeKey(property));
88
+ if (key === propertyName) {
89
+ return getNodeValueNode(property) ?? null;
90
+ }
91
+ }
92
+
93
+ return null;
94
+ };
95
+
96
+ const resolveIdentifierObject = (
97
+ node: AstNode,
98
+ bindings: ReadonlyMap<string, AstNode>
99
+ ): AstNode | null =>
100
+ bindings.get((node as { name?: string }).name ?? '') ?? null;
101
+
102
+ const resolveMemberObject = (
103
+ node: AstNode,
104
+ bindings: ReadonlyMap<string, AstNode>,
105
+ depth: number,
106
+ resolve: (
107
+ node: AstNode | undefined,
108
+ bindings: ReadonlyMap<string, AstNode>,
109
+ depth?: number
110
+ ) => AstNode | null
111
+ ): AstNode | null => {
112
+ const { object, property } = node as { object?: AstNode; property?: AstNode };
113
+ const propertyName = getPropertyName(property);
114
+ if (!propertyName) {
115
+ return null;
116
+ }
117
+
118
+ const objectNode = resolve(object, bindings, depth + 1);
119
+ return objectNode
120
+ ? resolve(
121
+ findObjectPropertyValue(objectNode, propertyName) ?? undefined,
122
+ bindings,
123
+ depth + 1
124
+ )
125
+ : null;
126
+ };
127
+
128
+ const resolveObjectExpression = function resolveObjectExpression(
129
+ node: AstNode | undefined,
130
+ bindings: ReadonlyMap<string, AstNode>,
131
+ depth = 0
132
+ ): AstNode | null {
133
+ if (!node || depth > 4) {
134
+ return null;
135
+ }
136
+
137
+ if (node.type === 'ObjectExpression') {
138
+ return node;
139
+ }
140
+
141
+ if (node.type === 'Identifier') {
142
+ return resolveIdentifierObject(node, bindings);
143
+ }
144
+
145
+ return MEMBER_EXPRESSION_TYPES.has(node.type)
146
+ ? resolveMemberObject(node, bindings, depth, resolveObjectExpression)
147
+ : null;
148
+ };
149
+
150
+ const addMappedCategory = (
151
+ categories: Set<string>,
152
+ property: AstNode
153
+ ): boolean => {
154
+ if (property.type === 'SpreadElement') {
155
+ return false;
156
+ }
157
+
158
+ if (property.type !== 'Property') {
159
+ return true;
160
+ }
161
+
162
+ const key = getPropertyName(getNodeKey(property));
163
+ if (key) {
164
+ categories.add(key);
165
+ }
166
+
167
+ return true;
168
+ };
169
+
170
+ const collectMappedCategories = (
171
+ mapperObject: AstNode
172
+ ): ReadonlySet<string> | null => {
173
+ if (mapperObject.type !== 'ObjectExpression') {
174
+ return null;
175
+ }
176
+
177
+ const categories = new Set<string>();
178
+ for (const property of getObjectProperties(mapperObject)) {
179
+ if (!addMappedCategory(categories, property)) {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ return categories;
185
+ };
186
+
187
+ const createDiagnostic = (
188
+ filePath: string,
189
+ line: number,
190
+ missingCategories: readonly string[]
191
+ ): WardenDiagnostic => ({
192
+ filePath,
193
+ line,
194
+ message: `Surface error mapper is missing mappings for: ${missingCategories.join(', ')}. Registered createSurfaceErrorMapper() calls must cover every ErrorCategory.`,
195
+ rule: 'error-mapping-completeness',
196
+ severity: 'error',
197
+ });
198
+
199
+ const getCallArgs = (node: AstNode): readonly AstNode[] =>
200
+ (node as { arguments?: readonly AstNode[] }).arguments ?? [];
201
+
202
+ const getCallCallee = (node: AstNode): AstNode | undefined =>
203
+ (node as { callee?: AstNode }).callee;
204
+
205
+ const isMapperFactoryCall = (node: AstNode): boolean =>
206
+ node.type === 'CallExpression' &&
207
+ MAPPER_FACTORY_NAMES.has(identifierName(getCallCallee(node)) ?? '');
208
+
209
+ const findMissingCategories = (
210
+ mappedCategories: ReadonlySet<string>
211
+ ): readonly string[] =>
212
+ requiredErrorCategories.filter((category) => !mappedCategories.has(category));
213
+
214
+ const resolveMappedCategories = (
215
+ node: AstNode,
216
+ bindings: ReadonlyMap<string, AstNode>
217
+ ): ReadonlySet<string> | null => {
218
+ const [firstArg] = getCallArgs(node);
219
+ const mapperObject = resolveObjectExpression(firstArg, bindings);
220
+ return mapperObject ? collectMappedCategories(mapperObject) : null;
221
+ };
222
+
223
+ const inspectMapperCall = (
224
+ node: AstNode,
225
+ bindings: ReadonlyMap<string, AstNode>,
226
+ filePath: string,
227
+ sourceCode: string
228
+ ): WardenDiagnostic | null => {
229
+ if (!isMapperFactoryCall(node)) {
230
+ return null;
231
+ }
232
+
233
+ const mappedCategories = resolveMappedCategories(node, bindings);
234
+ if (!mappedCategories) {
235
+ return null;
236
+ }
237
+
238
+ const missingCategories = findMissingCategories(mappedCategories);
239
+ if (missingCategories.length === 0) {
240
+ return null;
241
+ }
242
+
243
+ return createDiagnostic(
244
+ filePath,
245
+ offsetToLine(sourceCode, node.start),
246
+ missingCategories
247
+ );
248
+ };
249
+
250
+ /**
251
+ * Flags registered surface error mapper calls that omit error categories.
252
+ */
253
+ export const errorMappingCompleteness: WardenRule = {
254
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
255
+ if (
256
+ isTestFile(filePath) ||
257
+ ![...MAPPER_FACTORY_NAMES].some((factoryName) =>
258
+ sourceCode.includes(factoryName)
259
+ )
260
+ ) {
261
+ return [];
262
+ }
263
+
264
+ const ast = parse(filePath, sourceCode);
265
+ if (!ast) {
266
+ return [];
267
+ }
268
+
269
+ const bindings = collectObjectBindings(ast);
270
+ const diagnostics: WardenDiagnostic[] = [];
271
+
272
+ walk(ast, (node) => {
273
+ const diagnostic = inspectMapperCall(
274
+ node,
275
+ bindings,
276
+ filePath,
277
+ sourceCode
278
+ );
279
+ if (diagnostic) {
280
+ diagnostics.push(diagnostic);
281
+ }
282
+ });
283
+
284
+ return diagnostics;
285
+ },
286
+ description:
287
+ 'Require registered surface error mappers to cover every ErrorCategory.',
288
+ name: 'error-mapping-completeness',
289
+ severity: 'error',
290
+ };