@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,395 @@
1
+ import { entity } from '@ontrails/core';
2
+ import { z } from 'zod';
3
+
4
+ import {
5
+ extractStringLiteral,
6
+ findConfigProperty,
7
+ findEntityDefinitions,
8
+ getNodeExpression,
9
+ getNodeObject,
10
+ getNodeProperty,
11
+ getStringValue,
12
+ identifierName,
13
+ offsetToLine,
14
+ parse,
15
+ } from '@ontrails/source';
16
+ import type { AstNode, EntityDefinition } from '@ontrails/source';
17
+ import { isTestFile } from './scan.js';
18
+ import type { WardenDiagnostic, WardenRule } from './types.js';
19
+
20
+ class UnsupportedEntityEvaluationError extends Error {}
21
+
22
+ type EntityEvaluationEnvironment = ReadonlyMap<string, unknown>;
23
+
24
+ const buildInvalidExampleDiagnostic = (
25
+ entityName: string,
26
+ message: string,
27
+ filePath: string,
28
+ line: number
29
+ ): WardenDiagnostic => ({
30
+ filePath,
31
+ line,
32
+ message: `Entity "${entityName}" has invalid examples: ${message}`,
33
+ rule: 'example-valid',
34
+ severity: 'error',
35
+ });
36
+
37
+ const getPropertyName = (node: unknown): string | null => {
38
+ if (typeof node !== 'object' || node === null) {
39
+ return null;
40
+ }
41
+
42
+ const { name } = node as { readonly name?: unknown };
43
+ if (typeof name === 'string') {
44
+ return name;
45
+ }
46
+
47
+ return extractStringLiteral(node as AstNode);
48
+ };
49
+
50
+ const requireNode = (node: AstNode | undefined): AstNode => {
51
+ if (!node) {
52
+ throw new UnsupportedEntityEvaluationError(
53
+ 'Missing node in entity evaluation.'
54
+ );
55
+ }
56
+
57
+ return node;
58
+ };
59
+
60
+ type EntityNodeEvaluator = (
61
+ node: AstNode,
62
+ env: EntityEvaluationEnvironment
63
+ ) => unknown;
64
+
65
+ // The evaluator table is declared before `evaluateNode` so the recursive
66
+ // dispatch can read it at call time without any forward references. Entries
67
+ // are registered below once each per-type evaluator is defined.
68
+ const entityNodeEvaluators = new Map<string, EntityNodeEvaluator>();
69
+
70
+ const evaluateNode: EntityNodeEvaluator = (
71
+ node: AstNode,
72
+ env: EntityEvaluationEnvironment
73
+ ): unknown => {
74
+ const evaluator = entityNodeEvaluators.get(node.type);
75
+ if (evaluator) {
76
+ return evaluator(node, env);
77
+ }
78
+
79
+ throw new UnsupportedEntityEvaluationError(
80
+ `Unsupported AST node "${node.type}" in entity evaluation.`
81
+ );
82
+ };
83
+
84
+ const evaluateArrayExpression: EntityNodeEvaluator = (
85
+ node: AstNode,
86
+ env: EntityEvaluationEnvironment
87
+ ): readonly unknown[] => {
88
+ const elements = node['elements'] as readonly AstNode[] | undefined;
89
+ return (elements ?? []).map((element) => evaluateNode(element, env));
90
+ };
91
+
92
+ const evaluateLiteralExpression: EntityNodeEvaluator = (
93
+ node: AstNode
94
+ ): unknown => getStringValue(node) ?? node.value;
95
+
96
+ const evaluateIdentifierExpression: EntityNodeEvaluator = (
97
+ node: AstNode,
98
+ env: EntityEvaluationEnvironment
99
+ ): unknown => {
100
+ const name = identifierName(node);
101
+ if (name === 'undefined') {
102
+ return undefined;
103
+ }
104
+
105
+ if (name === 'z') {
106
+ return z;
107
+ }
108
+
109
+ if (!name || !env.has(name)) {
110
+ throw new UnsupportedEntityEvaluationError(
111
+ `Unknown identifier "${name ?? '<unknown>'}" in entity evaluation.`
112
+ );
113
+ }
114
+
115
+ return env.get(name);
116
+ };
117
+
118
+ const evaluateWrappedExpression: EntityNodeEvaluator = (
119
+ node: AstNode,
120
+ env: EntityEvaluationEnvironment
121
+ ): unknown => evaluateNode(requireNode(getNodeExpression(node)), env);
122
+
123
+ const evaluateNullExpression: EntityNodeEvaluator = (): null => null;
124
+
125
+ const evaluateObjectExpression: EntityNodeEvaluator = (
126
+ node: AstNode,
127
+ env: EntityEvaluationEnvironment
128
+ ): Record<string, unknown> => {
129
+ const properties = node['properties'] as readonly AstNode[] | undefined;
130
+ const value: Record<string, unknown> = {};
131
+
132
+ for (const property of properties ?? []) {
133
+ if (property.type !== 'Property') {
134
+ throw new UnsupportedEntityEvaluationError(
135
+ `Unsupported object property type "${property.type}".`
136
+ );
137
+ }
138
+
139
+ const propertyName = getPropertyName(property.key);
140
+ if (!propertyName) {
141
+ throw new UnsupportedEntityEvaluationError(
142
+ 'Unsupported object property key in entity evaluation.'
143
+ );
144
+ }
145
+
146
+ value[propertyName] = evaluateNode(
147
+ requireNode(property.value as AstNode | undefined),
148
+ env
149
+ );
150
+ }
151
+
152
+ return value;
153
+ };
154
+
155
+ const evaluateCallArguments = (
156
+ node: AstNode,
157
+ env: EntityEvaluationEnvironment
158
+ ): readonly unknown[] =>
159
+ ((node['arguments'] as readonly AstNode[] | undefined) ?? []).map((arg) =>
160
+ evaluateNode(arg, env)
161
+ );
162
+
163
+ const resolveEntityFallbackOptions = (
164
+ shape: unknown
165
+ ): Parameters<typeof entity>[2] | null =>
166
+ typeof shape === 'object' &&
167
+ shape !== null &&
168
+ Object.hasOwn(shape, 'id') &&
169
+ (shape as z.ZodRawShape)['id'] !== undefined
170
+ ? ({ identity: 'id' } as Parameters<typeof entity>[2])
171
+ : null;
172
+
173
+ const resolveEntityOptions = (
174
+ shape: unknown,
175
+ options: unknown
176
+ ): Parameters<typeof entity>[2] => {
177
+ if (options === undefined) {
178
+ const fallbackOptions = resolveEntityFallbackOptions(shape);
179
+ if (fallbackOptions !== null) {
180
+ return fallbackOptions;
181
+ }
182
+
183
+ throw new UnsupportedEntityEvaluationError(
184
+ 'Entity evaluator requires literal options, or an `id` field when options are omitted.'
185
+ );
186
+ }
187
+
188
+ if (typeof options !== 'object' || options === null) {
189
+ throw new UnsupportedEntityEvaluationError(
190
+ 'Entity evaluator requires literal options when provided.'
191
+ );
192
+ }
193
+
194
+ return options as Parameters<typeof entity>[2];
195
+ };
196
+
197
+ const evaluateEntityCall = (args: readonly unknown[]): unknown => {
198
+ const [name, shape, options] = args;
199
+ if (typeof name !== 'string') {
200
+ throw new UnsupportedEntityEvaluationError(
201
+ 'Entity evaluator requires a literal name.'
202
+ );
203
+ }
204
+
205
+ return entity(
206
+ name,
207
+ shape as z.ZodRawShape,
208
+ resolveEntityOptions(shape, options)
209
+ );
210
+ };
211
+
212
+ const evaluateMemberCall = (
213
+ callee: AstNode,
214
+ args: readonly unknown[],
215
+ env: EntityEvaluationEnvironment
216
+ ): unknown => {
217
+ const receiver = evaluateNode(requireNode(getNodeObject(callee)), env);
218
+ const propertyName = getPropertyName(getNodeProperty(callee));
219
+ if (!propertyName) {
220
+ throw new UnsupportedEntityEvaluationError(
221
+ 'Unsupported member property in entity evaluation.'
222
+ );
223
+ }
224
+
225
+ const method = (receiver as Record<string, unknown>)[propertyName];
226
+ if (typeof method !== 'function') {
227
+ throw new UnsupportedEntityEvaluationError(
228
+ `Entity evaluator could not call "${propertyName}".`
229
+ );
230
+ }
231
+
232
+ return Reflect.apply(method, receiver, args);
233
+ };
234
+
235
+ const evaluateCallExpression: EntityNodeEvaluator = (
236
+ node: AstNode,
237
+ env: EntityEvaluationEnvironment
238
+ ): unknown => {
239
+ const args = evaluateCallArguments(node, env);
240
+ const callee = requireNode(node['callee'] as AstNode | undefined);
241
+
242
+ if (callee.type === 'Identifier') {
243
+ const calleeName = identifierName(callee);
244
+ if (calleeName !== 'entity') {
245
+ throw new UnsupportedEntityEvaluationError(
246
+ `Unsupported entity evaluator call "${calleeName ?? '<unknown>'}".`
247
+ );
248
+ }
249
+
250
+ return evaluateEntityCall(args);
251
+ }
252
+
253
+ if (
254
+ callee.type !== 'MemberExpression' &&
255
+ callee.type !== 'StaticMemberExpression'
256
+ ) {
257
+ throw new UnsupportedEntityEvaluationError(
258
+ `Unsupported callee type "${callee.type}".`
259
+ );
260
+ }
261
+
262
+ return evaluateMemberCall(callee, args, env);
263
+ };
264
+
265
+ const EVALUATOR_REGISTRATIONS: readonly (readonly [
266
+ string,
267
+ EntityNodeEvaluator,
268
+ ])[] = [
269
+ ['ArrayExpression', evaluateArrayExpression],
270
+ ['BooleanLiteral', evaluateLiteralExpression],
271
+ ['Literal', evaluateLiteralExpression],
272
+ ['NumericLiteral', evaluateLiteralExpression],
273
+ ['StringLiteral', evaluateLiteralExpression],
274
+ ['CallExpression', evaluateCallExpression],
275
+ ['Identifier', evaluateIdentifierExpression],
276
+ ['NullLiteral', evaluateNullExpression],
277
+ ['ObjectExpression', evaluateObjectExpression],
278
+ ['ParenthesizedExpression', evaluateWrappedExpression],
279
+ ['TSAsExpression', evaluateWrappedExpression],
280
+ ['TSSatisfiesExpression', evaluateWrappedExpression],
281
+ ];
282
+
283
+ for (const [type, evaluator] of EVALUATOR_REGISTRATIONS) {
284
+ entityNodeEvaluators.set(type, evaluator);
285
+ }
286
+
287
+ const hasExamples = (definition: EntityDefinition): boolean =>
288
+ definition.options !== null &&
289
+ findConfigProperty(definition.options, 'examples') !== null;
290
+
291
+ const evaluateEntityDefinition = (
292
+ definition: EntityDefinition,
293
+ env: EntityEvaluationEnvironment
294
+ ): unknown => evaluateNode(definition.call, env);
295
+
296
+ const buildEntityExampleErrorDiagnostic = (
297
+ definition: EntityDefinition,
298
+ error: unknown,
299
+ sourceCode: string,
300
+ filePath: string
301
+ ): WardenDiagnostic | null => {
302
+ if (
303
+ error instanceof UnsupportedEntityEvaluationError ||
304
+ !hasExamples(definition) ||
305
+ !(error instanceof Error)
306
+ ) {
307
+ return null;
308
+ }
309
+
310
+ return buildInvalidExampleDiagnostic(
311
+ definition.name,
312
+ error.message,
313
+ filePath,
314
+ offsetToLine(sourceCode, definition.start)
315
+ );
316
+ };
317
+
318
+ const evaluateEntityExamples = (
319
+ definition: EntityDefinition,
320
+ env: Map<string, unknown>,
321
+ sourceCode: string,
322
+ filePath: string
323
+ ): WardenDiagnostic | null => {
324
+ try {
325
+ const value = evaluateEntityDefinition(definition, env);
326
+ if (definition.bindingName) {
327
+ env.set(definition.bindingName, value);
328
+ }
329
+
330
+ return null;
331
+ } catch (error) {
332
+ return buildEntityExampleErrorDiagnostic(
333
+ definition,
334
+ error,
335
+ sourceCode,
336
+ filePath
337
+ );
338
+ }
339
+ };
340
+
341
+ const collectEntityExampleDiagnostics = (
342
+ definitions: readonly EntityDefinition[],
343
+ sourceCode: string,
344
+ filePath: string
345
+ ): readonly WardenDiagnostic[] => {
346
+ const diagnostics: WardenDiagnostic[] = [];
347
+ const env = new Map<string, unknown>();
348
+
349
+ for (const definition of definitions) {
350
+ const diagnostic = evaluateEntityExamples(
351
+ definition,
352
+ env,
353
+ sourceCode,
354
+ filePath
355
+ );
356
+ if (diagnostic) {
357
+ diagnostics.push(diagnostic);
358
+ }
359
+ }
360
+
361
+ return diagnostics;
362
+ };
363
+
364
+ const checkEntityExamples = (
365
+ sourceCode: string,
366
+ filePath: string
367
+ ): readonly WardenDiagnostic[] => {
368
+ if (isTestFile(filePath)) {
369
+ return [];
370
+ }
371
+
372
+ const ast = parse(filePath, sourceCode);
373
+ if (!ast) {
374
+ return [];
375
+ }
376
+
377
+ return collectEntityExampleDiagnostics(
378
+ findEntityDefinitions(ast),
379
+ sourceCode,
380
+ filePath
381
+ );
382
+ };
383
+
384
+ /**
385
+ * Checks that entity examples validate against their schema.
386
+ */
387
+ export const exampleValid: WardenRule = {
388
+ check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
389
+ return checkEntityExamples(sourceCode, filePath);
390
+ },
391
+ description:
392
+ 'Ensure every entity example validates against the declared entity schema.',
393
+ name: 'example-valid',
394
+ severity: 'error',
395
+ };