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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +674 -9
  2. package/README.md +133 -26
  3. package/bin/warden.ts +51 -0
  4. package/package.json +28 -5
  5. package/src/adapter-check.ts +136 -0
  6. package/src/ast.ts +137 -0
  7. package/src/cli.ts +1451 -104
  8. package/src/command.ts +966 -0
  9. package/src/config.ts +193 -0
  10. package/src/draft.ts +22 -0
  11. package/src/drift.ts +122 -22
  12. package/src/fix.ts +120 -0
  13. package/src/formatters.ts +79 -9
  14. package/src/guide.ts +245 -0
  15. package/src/index.ts +217 -14
  16. package/src/project-context.ts +192 -0
  17. package/src/project-rules.ts +290 -0
  18. package/src/resolve.ts +531 -0
  19. package/src/rules/activation-orphan.ts +97 -0
  20. package/src/rules/ast.ts +4008 -86
  21. package/src/rules/circular-refs.ts +154 -0
  22. package/src/rules/cli-command-route-coherence.ts +177 -0
  23. package/src/rules/composes-declarations.ts +837 -0
  24. package/src/rules/context-no-surface-types.ts +78 -15
  25. package/src/rules/contour-exists.ts +251 -0
  26. package/src/rules/contour-ids.ts +15 -0
  27. package/src/rules/dead-internal-trail.ts +161 -0
  28. package/src/rules/dead-public-trail.ts +258 -0
  29. package/src/rules/draft-file-marking.ts +160 -0
  30. package/src/rules/draft-visible-debt.ts +87 -0
  31. package/src/rules/duplicate-public-contract.ts +91 -0
  32. package/src/rules/error-mapping-completeness.ts +290 -0
  33. package/src/rules/example-valid.ts +395 -0
  34. package/src/rules/fires-declarations.ts +735 -0
  35. package/src/rules/implementation-returns-result.ts +1409 -166
  36. package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
  37. package/src/rules/incomplete-crud.ts +581 -0
  38. package/src/rules/index.ts +235 -18
  39. package/src/rules/intent-propagation.ts +127 -0
  40. package/src/rules/layer-field-name-drift.ts +102 -0
  41. package/src/rules/library-projection-coherence.ts +100 -0
  42. package/src/rules/metadata.ts +755 -0
  43. package/src/rules/missing-reconcile.ts +98 -0
  44. package/src/rules/missing-visibility.ts +110 -0
  45. package/src/rules/no-destructured-compose.ts +194 -0
  46. package/src/rules/no-dev-permit-in-source.ts +99 -0
  47. package/src/rules/no-direct-implementation-call.ts +7 -7
  48. package/src/rules/no-legacy-layer-imports.ts +211 -0
  49. package/src/rules/no-native-error-result.ts +118 -0
  50. package/src/rules/no-redundant-result-error-wrap.ts +384 -0
  51. package/src/rules/no-retired-cross-vocabulary.ts +194 -0
  52. package/src/rules/no-sync-result-assumption.ts +1135 -98
  53. package/src/rules/no-throw-in-detour-recover.ts +225 -0
  54. package/src/rules/no-throw-in-implementation.ts +10 -9
  55. package/src/rules/no-top-level-surface.ts +371 -0
  56. package/src/rules/on-references-exist.ts +194 -0
  57. package/src/rules/orphaned-signal.ts +150 -0
  58. package/src/rules/owner-projection-parity.ts +143 -0
  59. package/src/rules/permit-governance.ts +25 -0
  60. package/src/rules/public-export-example-coverage.ts +562 -0
  61. package/src/rules/public-internal-deep-imports.ts +457 -0
  62. package/src/rules/public-output-schema.ts +29 -0
  63. package/src/rules/public-union-output-discriminants.ts +150 -0
  64. package/src/rules/read-intent-fires.ts +187 -0
  65. package/src/rules/reference-exists.ts +98 -0
  66. package/src/rules/registry-names.ts +155 -0
  67. package/src/rules/resolved-import-boundary.ts +146 -0
  68. package/src/rules/resource-declarations.ts +693 -0
  69. package/src/rules/resource-exists.ts +179 -0
  70. package/src/rules/resource-id-grammar.ts +65 -0
  71. package/src/rules/resource-mock-coverage.ts +115 -0
  72. package/src/rules/scan.ts +38 -25
  73. package/src/rules/scheduled-destroy-intent.ts +44 -0
  74. package/src/rules/signal-graph-coaching.ts +191 -0
  75. package/src/rules/specs.ts +9 -5
  76. package/src/rules/static-resource-accessor-preference.ts +649 -0
  77. package/src/rules/surface-facet-coherence.ts +362 -0
  78. package/src/rules/trail-fork-coaching.ts +616 -0
  79. package/src/rules/trail-versioning-source.ts +1072 -0
  80. package/src/rules/trail-versioning-topo.ts +172 -0
  81. package/src/rules/types.ts +297 -8
  82. package/src/rules/unmaterialized-activation-source.ts +84 -0
  83. package/src/rules/unreachable-detour-shadowing.ts +339 -0
  84. package/src/rules/valid-describe-refs.ts +162 -32
  85. package/src/rules/valid-detour-contract.ts +78 -0
  86. package/src/rules/warden-export-symmetry.ts +540 -0
  87. package/src/rules/warden-rules-use-ast.ts +1114 -0
  88. package/src/rules/webhook-route-collision.ts +243 -0
  89. package/src/trails/activation-orphan.trail.ts +84 -0
  90. package/src/trails/circular-refs.trail.ts +29 -0
  91. package/src/trails/cli-command-route-coherence.trail.ts +47 -0
  92. package/src/trails/composes-declarations.trail.ts +22 -0
  93. package/src/trails/context-no-surface-types.trail.ts +21 -0
  94. package/src/trails/contour-exists.trail.ts +21 -0
  95. package/src/trails/dead-internal-trail.trail.ts +26 -0
  96. package/src/trails/dead-public-trail.trail.ts +31 -0
  97. package/src/trails/deprecation-without-guidance.trail.ts +21 -0
  98. package/src/trails/draft-file-marking.trail.ts +16 -0
  99. package/src/trails/draft-visible-debt.trail.ts +16 -0
  100. package/src/trails/duplicate-public-contract.trail.ts +47 -0
  101. package/src/trails/error-mapping-completeness.trail.ts +30 -0
  102. package/src/trails/example-valid.trail.ts +25 -0
  103. package/src/trails/fires-declarations.trail.ts +23 -0
  104. package/src/trails/fork-without-preserved-blaze.trail.ts +31 -0
  105. package/src/trails/implementation-returns-result.trail.ts +20 -0
  106. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
  107. package/src/trails/incomplete-crud.trail.ts +39 -0
  108. package/src/trails/index.ts +83 -0
  109. package/src/trails/intent-propagation.trail.ts +30 -0
  110. package/src/trails/layer-field-name-drift.trail.ts +39 -0
  111. package/src/trails/library-projection-coherence.trail.ts +43 -0
  112. package/src/trails/marker-schema-unsupported.trail.ts +23 -0
  113. package/src/trails/missing-reconcile.trail.ts +33 -0
  114. package/src/trails/missing-visibility.trail.ts +22 -0
  115. package/src/trails/no-destructured-compose.trail.ts +44 -0
  116. package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
  117. package/src/trails/no-direct-implementation-call.trail.ts +16 -0
  118. package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
  119. package/src/trails/no-native-error-result.trail.ts +18 -0
  120. package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
  121. package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
  122. package/src/trails/no-sync-result-assumption.trail.ts +19 -0
  123. package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
  124. package/src/trails/no-throw-in-implementation.trail.ts +20 -0
  125. package/src/trails/no-top-level-surface.trail.ts +43 -0
  126. package/src/trails/on-references-exist.trail.ts +21 -0
  127. package/src/trails/orphaned-signal.trail.ts +36 -0
  128. package/src/trails/owner-projection-parity.trail.ts +26 -0
  129. package/src/trails/pending-force.trail.ts +21 -0
  130. package/src/trails/permit-governance.trail.ts +51 -0
  131. package/src/trails/prefer-schema-inference.trail.ts +21 -0
  132. package/src/trails/public-export-example-coverage.trail.ts +16 -0
  133. package/src/trails/public-internal-deep-imports.trail.ts +94 -0
  134. package/src/trails/public-output-schema.trail.ts +55 -0
  135. package/src/trails/public-union-output-discriminants.trail.ts +33 -0
  136. package/src/trails/read-intent-fires.trail.ts +20 -0
  137. package/src/trails/reference-exists.trail.ts +25 -0
  138. package/src/trails/resolved-import-boundary.trail.ts +109 -0
  139. package/src/trails/resource-declarations.trail.ts +25 -0
  140. package/src/trails/resource-exists.trail.ts +27 -0
  141. package/src/trails/resource-id-grammar.trail.ts +39 -0
  142. package/src/trails/resource-mock-coverage.trail.ts +40 -0
  143. package/src/trails/run.ts +162 -0
  144. package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
  145. package/src/trails/schema.ts +198 -0
  146. package/src/trails/signal-graph-coaching.trail.ts +77 -0
  147. package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
  148. package/src/trails/surface-facet-coherence.trail.ts +25 -0
  149. package/src/trails/topo.ts +6 -0
  150. package/src/trails/trail-fork-coaching.trail.ts +42 -0
  151. package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
  152. package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
  153. package/src/trails/valid-describe-refs.trail.ts +18 -0
  154. package/src/trails/valid-detour-contract.trail.ts +71 -0
  155. package/src/trails/version-gap.trail.ts +35 -0
  156. package/src/trails/version-pinned-compose.trail.ts +23 -0
  157. package/src/trails/version-without-examples.trail.ts +38 -0
  158. package/src/trails/warden-export-symmetry.trail.ts +16 -0
  159. package/src/trails/warden-rules-use-ast.trail.ts +64 -0
  160. package/src/trails/webhook-route-collision.trail.ts +50 -0
  161. package/src/trails/wrap-rule.ts +214 -0
  162. package/src/workspaces.ts +199 -0
  163. package/.turbo/turbo-build.log +0 -1
  164. package/.turbo/turbo-lint.log +0 -3
  165. package/.turbo/turbo-typecheck.log +0 -1
  166. package/dist/cli.d.ts +0 -46
  167. package/dist/cli.d.ts.map +0 -1
  168. package/dist/cli.js +0 -221
  169. package/dist/cli.js.map +0 -1
  170. package/dist/drift.d.ts +0 -26
  171. package/dist/drift.d.ts.map +0 -1
  172. package/dist/drift.js +0 -27
  173. package/dist/drift.js.map +0 -1
  174. package/dist/formatters.d.ts +0 -29
  175. package/dist/formatters.d.ts.map +0 -1
  176. package/dist/formatters.js +0 -87
  177. package/dist/formatters.js.map +0 -1
  178. package/dist/index.d.ts +0 -26
  179. package/dist/index.d.ts.map +0 -1
  180. package/dist/index.js +0 -26
  181. package/dist/index.js.map +0 -1
  182. package/dist/rules/ast.d.ts +0 -41
  183. package/dist/rules/ast.d.ts.map +0 -1
  184. package/dist/rules/ast.js +0 -163
  185. package/dist/rules/ast.js.map +0 -1
  186. package/dist/rules/context-no-surface-types.d.ts +0 -12
  187. package/dist/rules/context-no-surface-types.d.ts.map +0 -1
  188. package/dist/rules/context-no-surface-types.js +0 -96
  189. package/dist/rules/context-no-surface-types.js.map +0 -1
  190. package/dist/rules/implementation-returns-result.d.ts +0 -13
  191. package/dist/rules/implementation-returns-result.d.ts.map +0 -1
  192. package/dist/rules/implementation-returns-result.js +0 -277
  193. package/dist/rules/implementation-returns-result.js.map +0 -1
  194. package/dist/rules/index.d.ts +0 -22
  195. package/dist/rules/index.d.ts.map +0 -1
  196. package/dist/rules/index.js +0 -41
  197. package/dist/rules/index.js.map +0 -1
  198. package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
  199. package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
  200. package/dist/rules/no-direct-impl-in-route.js +0 -46
  201. package/dist/rules/no-direct-impl-in-route.js.map +0 -1
  202. package/dist/rules/no-direct-implementation-call.d.ts +0 -12
  203. package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
  204. package/dist/rules/no-direct-implementation-call.js +0 -39
  205. package/dist/rules/no-direct-implementation-call.js.map +0 -1
  206. package/dist/rules/no-sync-result-assumption.d.ts +0 -6
  207. package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
  208. package/dist/rules/no-sync-result-assumption.js +0 -98
  209. package/dist/rules/no-sync-result-assumption.js.map +0 -1
  210. package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
  211. package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
  212. package/dist/rules/no-throw-in-detour-target.js +0 -87
  213. package/dist/rules/no-throw-in-detour-target.js.map +0 -1
  214. package/dist/rules/no-throw-in-implementation.d.ts +0 -9
  215. package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
  216. package/dist/rules/no-throw-in-implementation.js +0 -34
  217. package/dist/rules/no-throw-in-implementation.js.map +0 -1
  218. package/dist/rules/prefer-schema-inference.d.ts +0 -7
  219. package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
  220. package/dist/rules/prefer-schema-inference.js +0 -86
  221. package/dist/rules/prefer-schema-inference.js.map +0 -1
  222. package/dist/rules/scan.d.ts +0 -8
  223. package/dist/rules/scan.d.ts.map +0 -1
  224. package/dist/rules/scan.js +0 -32
  225. package/dist/rules/scan.js.map +0 -1
  226. package/dist/rules/specs.d.ts +0 -29
  227. package/dist/rules/specs.d.ts.map +0 -1
  228. package/dist/rules/specs.js +0 -192
  229. package/dist/rules/specs.js.map +0 -1
  230. package/dist/rules/structure.d.ts +0 -13
  231. package/dist/rules/structure.d.ts.map +0 -1
  232. package/dist/rules/structure.js +0 -142
  233. package/dist/rules/structure.js.map +0 -1
  234. package/dist/rules/types.d.ts +0 -52
  235. package/dist/rules/types.d.ts.map +0 -1
  236. package/dist/rules/types.js +0 -2
  237. package/dist/rules/types.js.map +0 -1
  238. package/dist/rules/valid-describe-refs.d.ts +0 -7
  239. package/dist/rules/valid-describe-refs.d.ts.map +0 -1
  240. package/dist/rules/valid-describe-refs.js +0 -51
  241. package/dist/rules/valid-describe-refs.js.map +0 -1
  242. package/dist/rules/valid-detour-refs.d.ts +0 -6
  243. package/dist/rules/valid-detour-refs.d.ts.map +0 -1
  244. package/dist/rules/valid-detour-refs.js +0 -116
  245. package/dist/rules/valid-detour-refs.js.map +0 -1
  246. package/src/__tests__/cli.test.ts +0 -198
  247. package/src/__tests__/drift.test.ts +0 -74
  248. package/src/__tests__/formatters.test.ts +0 -157
  249. package/src/__tests__/implementation-returns-result.test.ts +0 -128
  250. package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
  251. package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
  252. package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
  253. package/src/__tests__/prefer-schema-inference.test.ts +0 -84
  254. package/src/__tests__/rules.test.ts +0 -215
  255. package/src/__tests__/valid-describe-refs.test.ts +0 -60
  256. package/src/rules/no-direct-impl-in-route.ts +0 -77
  257. package/src/rules/no-throw-in-detour-target.ts +0 -150
  258. package/src/rules/valid-detour-refs.ts +0 -187
  259. package/tsconfig.json +0 -9
  260. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,1072 @@
1
+ import {
2
+ findBlazeBodies,
3
+ findConfigProperty,
4
+ findTrailDefinitions,
5
+ getNodeArgument,
6
+ getNodeArguments,
7
+ getNodeCallee,
8
+ getNodeElements,
9
+ getNodeExpression,
10
+ getNodeId,
11
+ getNodeInit,
12
+ getNodeKey,
13
+ getNodeLeft,
14
+ getNodeName,
15
+ getNodeObject,
16
+ getNodeParams,
17
+ getNodeProperties,
18
+ getNodeProperty,
19
+ getNodeValue,
20
+ getNodeValueNode,
21
+ isMemberAccessNonComputed,
22
+ offsetToLine,
23
+ parse,
24
+ walk,
25
+ walkScope,
26
+ } from './ast.js';
27
+ import type { AstNode } from './ast.js';
28
+ import type { WardenDiagnostic, WardenRule } from './types.js';
29
+
30
+ const VERSION_PINNED_COMPOSE = 'version-pinned-compose';
31
+ const FORK_WITHOUT_PRESERVED_BLAZE = 'fork-without-preserved-blaze';
32
+ const MARKER_SCHEMA_UNSUPPORTED = 'marker-schema-unsupported';
33
+
34
+ interface SchemaBindingRecord {
35
+ readonly initializer: AstNode | undefined;
36
+ readonly scopeEnd: number;
37
+ readonly scopeStart: number;
38
+ readonly start: number;
39
+ }
40
+
41
+ type SchemaBindings = ReadonlyMap<string, readonly SchemaBindingRecord[]>;
42
+
43
+ // Zod schema constructors and modifiers outside the marker subset that the
44
+ // runtime guard in packages/core/src/version-marker.ts rejects. This deny-list
45
+ // is best-effort source-static coverage; the runtime allow-list remains the
46
+ // authoritative gate. Entries are evidence-verified against the runtime guard.
47
+ const unsupportedSchemaCalls = new Set([
48
+ 'and',
49
+ 'any',
50
+ 'base64',
51
+ 'base64url',
52
+ 'bigint',
53
+ 'catch',
54
+ 'catchall',
55
+ 'check',
56
+ 'cidrv4',
57
+ 'cidrv6',
58
+ 'codec',
59
+ 'cuid',
60
+ 'cuid2',
61
+ 'custom',
62
+ 'date',
63
+ 'datetime',
64
+ 'default',
65
+ 'duration',
66
+ 'e164',
67
+ 'email',
68
+ 'emoji',
69
+ 'endsWith',
70
+ 'file',
71
+ 'finite',
72
+ 'function',
73
+ 'gt',
74
+ 'gte',
75
+ 'guid',
76
+ 'hash',
77
+ 'includes',
78
+ 'instanceof',
79
+ 'int',
80
+ 'intersection',
81
+ 'ipv4',
82
+ 'ipv6',
83
+ 'json',
84
+ 'jwt',
85
+ 'ksuid',
86
+ 'lazy',
87
+ 'length',
88
+ 'loose',
89
+ 'looseObject',
90
+ 'looseRecord',
91
+ 'lowercase',
92
+ 'lt',
93
+ 'lte',
94
+ 'map',
95
+ 'max',
96
+ 'min',
97
+ 'multipleOf',
98
+ 'nan',
99
+ 'nanoid',
100
+ 'negative',
101
+ 'never',
102
+ 'nonempty',
103
+ 'nonnegative',
104
+ 'nonoptional',
105
+ 'nonpositive',
106
+ 'normalize',
107
+ 'null',
108
+ 'overwrite',
109
+ 'partialRecord',
110
+ 'passthrough',
111
+ 'pipe',
112
+ 'positive',
113
+ 'prefault',
114
+ 'preprocess',
115
+ 'promise',
116
+ 'record',
117
+ 'refine',
118
+ 'regex',
119
+ 'required',
120
+ 'safe',
121
+ 'set',
122
+ 'slugify',
123
+ 'startsWith',
124
+ 'step',
125
+ 'strict',
126
+ 'strictObject',
127
+ 'stringbool',
128
+ 'superRefine',
129
+ 'symbol',
130
+ 'templateLiteral',
131
+ 'time',
132
+ 'toLowerCase',
133
+ 'toUpperCase',
134
+ 'transform',
135
+ 'trim',
136
+ 'tuple',
137
+ 'ulid',
138
+ 'undefined',
139
+ 'unknown',
140
+ 'uppercase',
141
+ 'url',
142
+ 'uuid',
143
+ 'uuidv4',
144
+ 'uuidv6',
145
+ 'uuidv7',
146
+ 'void',
147
+ 'xid',
148
+ ]);
149
+
150
+ const diagnostic = (
151
+ rule: string,
152
+ severity: WardenDiagnostic['severity'],
153
+ filePath: string,
154
+ sourceCode: string,
155
+ node: AstNode,
156
+ message: string
157
+ ): WardenDiagnostic => ({
158
+ filePath,
159
+ line: offsetToLine(sourceCode, node.start),
160
+ message,
161
+ rule,
162
+ severity,
163
+ });
164
+
165
+ const staticPropertyKeyName = (node: AstNode | undefined): string | null => {
166
+ if (node?.type === 'Identifier') {
167
+ return getNodeName(node) ?? null;
168
+ }
169
+ if (
170
+ node?.type === 'Literal' ||
171
+ node?.type === 'StringLiteral' ||
172
+ node?.type === 'NumericLiteral'
173
+ ) {
174
+ const value = getNodeValue(node);
175
+ return typeof value === 'string' || typeof value === 'number'
176
+ ? String(value)
177
+ : null;
178
+ }
179
+ return null;
180
+ };
181
+
182
+ const objectProperties = (node: AstNode | undefined): readonly AstNode[] =>
183
+ node?.type === 'ObjectExpression' ? (getNodeProperties(node) ?? []) : [];
184
+
185
+ const propertyName = (node: AstNode): string | null =>
186
+ node.type === 'Property' ? staticPropertyKeyName(getNodeKey(node)) : null;
187
+
188
+ const hasProperty = (node: AstNode, name: string): boolean =>
189
+ objectProperties(node).some((property) => propertyName(property) === name);
190
+
191
+ const propertyValue = (property: AstNode | null): AstNode | undefined =>
192
+ property?.type === 'Property'
193
+ ? (getNodeValueNode(property) ?? undefined)
194
+ : undefined;
195
+
196
+ const trailIsVersioned = (config: AstNode): boolean =>
197
+ findConfigProperty(config, 'version') !== null ||
198
+ findConfigProperty(config, 'versions') !== null;
199
+
200
+ const versionEntries = (config: AstNode): readonly AstNode[] => {
201
+ const versions = propertyValue(findConfigProperty(config, 'versions'));
202
+ return objectProperties(versions)
203
+ .map((property) => propertyValue(property))
204
+ .filter((entry): entry is AstNode => entry?.type === 'ObjectExpression');
205
+ };
206
+
207
+ const identifierName = (node: AstNode | undefined): string | undefined =>
208
+ node?.type === 'Identifier' ? getNodeName(node) : undefined;
209
+
210
+ const schemaBindingInitializer = (
211
+ schemaBindings: SchemaBindings,
212
+ name: string,
213
+ referenceStart: number
214
+ ): AstNode | undefined => {
215
+ let resolved: SchemaBindingRecord | undefined;
216
+ for (const record of schemaBindings.get(name) ?? []) {
217
+ if (record.start >= referenceStart) {
218
+ break;
219
+ }
220
+ if (
221
+ record.scopeStart > referenceStart ||
222
+ record.scopeEnd < referenceStart
223
+ ) {
224
+ continue;
225
+ }
226
+ resolved = record;
227
+ }
228
+ return resolved?.initializer;
229
+ };
230
+
231
+ const memberObject = (node: AstNode | undefined): AstNode | undefined =>
232
+ node !== undefined && isMemberAccessNonComputed(node)
233
+ ? getNodeObject(node)
234
+ : undefined;
235
+
236
+ const memberPropertyName = (node: AstNode | undefined): string | undefined =>
237
+ node !== undefined && isMemberAccessNonComputed(node)
238
+ ? identifierName(getNodeProperty(node))
239
+ : undefined;
240
+
241
+ const callCallee = (node: AstNode): AstNode | undefined =>
242
+ node.type === 'CallExpression' ? getNodeCallee(node) : undefined;
243
+
244
+ const isZodSchemaReceiver = (
245
+ node: AstNode | undefined,
246
+ schemaBindings: SchemaBindings = new Map(),
247
+ referenceStart = node?.start ?? Number.POSITIVE_INFINITY
248
+ ): boolean => {
249
+ if (!node) {
250
+ return false;
251
+ }
252
+ const name = identifierName(node);
253
+ if (
254
+ name === 'z' ||
255
+ (name !== undefined &&
256
+ schemaBindingInitializer(schemaBindings, name, referenceStart) !==
257
+ undefined)
258
+ ) {
259
+ return true;
260
+ }
261
+ if (node.type === 'CallExpression') {
262
+ return isZodSchemaReceiver(
263
+ memberObject(callCallee(node)),
264
+ schemaBindings,
265
+ referenceStart
266
+ );
267
+ }
268
+ return isZodSchemaReceiver(
269
+ memberObject(node),
270
+ schemaBindings,
271
+ referenceStart
272
+ );
273
+ };
274
+
275
+ const isZodSchemaCallee = (
276
+ node: AstNode | undefined,
277
+ schemaBindings: SchemaBindings = new Map()
278
+ ): boolean =>
279
+ node !== undefined && isZodSchemaReceiver(memberObject(node), schemaBindings);
280
+
281
+ const callArguments = (node: AstNode): readonly AstNode[] =>
282
+ node.type === 'CallExpression' ? (getNodeArguments(node) ?? []) : [];
283
+
284
+ const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
285
+ let current = node;
286
+ while (
287
+ current?.type === 'TSAsExpression' ||
288
+ current?.type === 'TSSatisfiesExpression' ||
289
+ current?.type === 'TSNonNullExpression'
290
+ ) {
291
+ current = getNodeExpression(current);
292
+ }
293
+ return current;
294
+ };
295
+
296
+ const arrayExpressionLength = (node: AstNode | undefined): number => {
297
+ const expression = unwrapExpression(node);
298
+ return expression?.type === 'ArrayExpression'
299
+ ? (getNodeElements(expression) ?? []).length
300
+ : 0;
301
+ };
302
+
303
+ const isNumberProperty = (
304
+ node: AstNode | undefined,
305
+ names: ReadonlySet<string>
306
+ ): boolean =>
307
+ node !== undefined &&
308
+ isMemberAccessNonComputed(node) &&
309
+ identifierName(memberObject(node)) === 'Number' &&
310
+ names.has(memberPropertyName(node) ?? '');
311
+
312
+ const nonFiniteNumberProperties = new Set([
313
+ 'NaN',
314
+ 'NEGATIVE_INFINITY',
315
+ 'POSITIVE_INFINITY',
316
+ ]);
317
+
318
+ const literalExpressionIsJsonLossy = (expression: AstNode): boolean => {
319
+ if (
320
+ expression.type === 'BigIntLiteral' ||
321
+ expression.type === 'RegExpLiteral'
322
+ ) {
323
+ return true;
324
+ }
325
+ if (expression['bigint'] !== undefined || expression['regex'] !== undefined) {
326
+ return true;
327
+ }
328
+ const value = getNodeValue(expression);
329
+ if (value instanceof RegExp) {
330
+ return true;
331
+ }
332
+ return typeof value === 'number' && !Number.isFinite(value);
333
+ };
334
+
335
+ const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
336
+ const expression = unwrapExpression(node);
337
+ if (!expression) {
338
+ return true;
339
+ }
340
+
341
+ const name = identifierName(expression);
342
+ if (name === 'NaN' || name === 'Infinity' || name === 'undefined') {
343
+ return true;
344
+ }
345
+
346
+ if (isNumberProperty(expression, nonFiniteNumberProperties)) {
347
+ return true;
348
+ }
349
+
350
+ if (expression.type === 'UnaryExpression') {
351
+ return expressionIsJsonLossy(getNodeArgument(expression));
352
+ }
353
+
354
+ if (expression.type === 'Literal' || expression.type === 'NumericLiteral') {
355
+ return literalExpressionIsJsonLossy(expression);
356
+ }
357
+
358
+ if (expression.type === 'ArrayExpression') {
359
+ const elements = getNodeElements(expression);
360
+ return elements.some((element) =>
361
+ expressionIsJsonLossy(element ?? undefined)
362
+ );
363
+ }
364
+
365
+ if (expression.type === 'ObjectExpression') {
366
+ return objectProperties(expression).some((property) => {
367
+ if (property.type !== 'Property') {
368
+ return false;
369
+ }
370
+ return expressionIsJsonLossy(propertyValue(property));
371
+ });
372
+ }
373
+
374
+ return false;
375
+ };
376
+
377
+ const expressionIsReferenceValued = (node: AstNode | undefined): boolean => {
378
+ const expression = unwrapExpression(node);
379
+ return (
380
+ expression?.type === 'ArrayExpression' ||
381
+ expression?.type === 'ObjectExpression'
382
+ );
383
+ };
384
+
385
+ const isMultiValueLiteralCall = (
386
+ node: AstNode,
387
+ schemaBindings: SchemaBindings
388
+ ): boolean => {
389
+ const callee = callCallee(node);
390
+ return (
391
+ node.type === 'CallExpression' &&
392
+ memberPropertyName(callee) === 'literal' &&
393
+ isZodSchemaCallee(callee, schemaBindings) &&
394
+ arrayExpressionLength(callArguments(node)[0]) > 1
395
+ );
396
+ };
397
+
398
+ const isJsonLossyLiteralCall = (
399
+ node: AstNode,
400
+ schemaBindings: SchemaBindings
401
+ ): boolean => {
402
+ const callee = callCallee(node);
403
+ return (
404
+ node.type === 'CallExpression' &&
405
+ memberPropertyName(callee) === 'literal' &&
406
+ isZodSchemaCallee(callee, schemaBindings) &&
407
+ expressionIsJsonLossy(callArguments(node)[0])
408
+ );
409
+ };
410
+
411
+ const isReferenceValuedLiteralCall = (
412
+ node: AstNode,
413
+ schemaBindings: SchemaBindings
414
+ ): boolean => {
415
+ const callee = callCallee(node);
416
+ if (
417
+ node.type !== 'CallExpression' ||
418
+ memberPropertyName(callee) !== 'literal' ||
419
+ !isZodSchemaCallee(callee, schemaBindings)
420
+ ) {
421
+ return false;
422
+ }
423
+
424
+ const [rawValue] = callArguments(node);
425
+ const value = unwrapExpression(rawValue);
426
+ if (value?.type === 'ObjectExpression') {
427
+ return true;
428
+ }
429
+ if (value?.type !== 'ArrayExpression') {
430
+ return false;
431
+ }
432
+ return (getNodeElements(value) ?? []).some((element) =>
433
+ expressionIsReferenceValued(element ?? undefined)
434
+ );
435
+ };
436
+
437
+ const isJsonLossyEnumCall = (
438
+ node: AstNode,
439
+ schemaBindings: SchemaBindings
440
+ ): boolean => {
441
+ const callee = callCallee(node);
442
+ if (
443
+ node.type !== 'CallExpression' ||
444
+ memberPropertyName(callee) !== 'enum' ||
445
+ !isZodSchemaCallee(callee, schemaBindings)
446
+ ) {
447
+ return false;
448
+ }
449
+
450
+ const [rawOptions] = callArguments(node);
451
+ const options = unwrapExpression(rawOptions);
452
+ if (options?.type === 'ArrayExpression') {
453
+ return expressionIsJsonLossy(options);
454
+ }
455
+ if (options?.type !== 'ObjectExpression') {
456
+ return false;
457
+ }
458
+ return objectProperties(options).some((property) => {
459
+ if (property.type !== 'Property') {
460
+ return false;
461
+ }
462
+ return expressionIsJsonLossy(propertyValue(property));
463
+ });
464
+ };
465
+
466
+ const isReferenceValuedEnumCall = (
467
+ node: AstNode,
468
+ schemaBindings: SchemaBindings
469
+ ): boolean => {
470
+ const callee = callCallee(node);
471
+ if (
472
+ node.type !== 'CallExpression' ||
473
+ memberPropertyName(callee) !== 'enum' ||
474
+ !isZodSchemaCallee(callee, schemaBindings)
475
+ ) {
476
+ return false;
477
+ }
478
+
479
+ const [rawOptions] = callArguments(node);
480
+ const options = unwrapExpression(rawOptions);
481
+ if (options?.type === 'ArrayExpression') {
482
+ return getNodeElements(options).some((element) =>
483
+ expressionIsReferenceValued(element ?? undefined)
484
+ );
485
+ }
486
+ if (options?.type !== 'ObjectExpression') {
487
+ return false;
488
+ }
489
+ return objectProperties(options).some((property) => {
490
+ if (property.type !== 'Property') {
491
+ return false;
492
+ }
493
+ return expressionIsReferenceValued(propertyValue(property));
494
+ });
495
+ };
496
+
497
+ const markerWrapperCanHideOptional = new Set(['nullable', 'readonly']);
498
+
499
+ const isOptionalWrapperCall = (
500
+ node: AstNode,
501
+ schemaBindings: SchemaBindings
502
+ ): boolean => {
503
+ const callee = callCallee(node);
504
+ return (
505
+ node.type === 'CallExpression' &&
506
+ memberPropertyName(callee) === 'optional' &&
507
+ isZodSchemaCallee(callee, schemaBindings)
508
+ );
509
+ };
510
+
511
+ const callChainHasOptionalWrapper = (
512
+ node: AstNode | undefined,
513
+ schemaBindings: SchemaBindings
514
+ ): boolean => {
515
+ const seen = new Set<number>();
516
+ const visit = (current: AstNode | undefined): boolean => {
517
+ const expression = unwrapExpression(current);
518
+ if (expression === undefined || seen.has(expression.start)) {
519
+ return false;
520
+ }
521
+ seen.add(expression.start);
522
+
523
+ const name = identifierName(expression);
524
+ if (name !== undefined) {
525
+ return visit(
526
+ schemaBindingInitializer(schemaBindings, name, expression.start)
527
+ );
528
+ }
529
+
530
+ if (expression.type === 'CallExpression') {
531
+ const callee = callCallee(expression);
532
+ if (
533
+ memberPropertyName(callee) === 'optional' &&
534
+ isZodSchemaCallee(callee, schemaBindings)
535
+ ) {
536
+ return true;
537
+ }
538
+ return visit(memberObject(callee));
539
+ }
540
+ if (!isMemberAccessNonComputed(expression)) {
541
+ return false;
542
+ }
543
+ return visit(memberObject(expression));
544
+ };
545
+ return visit(node);
546
+ };
547
+
548
+ const isHiddenOptionalWrapperCall = (
549
+ node: AstNode,
550
+ schemaBindings: SchemaBindings
551
+ ): boolean => {
552
+ const callee = callCallee(node);
553
+ return (
554
+ node.type === 'CallExpression' &&
555
+ markerWrapperCanHideOptional.has(memberPropertyName(callee) ?? '') &&
556
+ isZodSchemaCallee(callee, schemaBindings) &&
557
+ callChainHasOptionalWrapper(memberObject(callee), schemaBindings)
558
+ );
559
+ };
560
+
561
+ const nestedSchemaArguments = (node: AstNode): readonly AstNode[] => {
562
+ if (node.type !== 'CallExpression') {
563
+ return [];
564
+ }
565
+ const name = memberPropertyName(callCallee(node));
566
+ if (name === 'array') {
567
+ return callArguments(node).slice(0, 1);
568
+ }
569
+ if (name === 'or') {
570
+ return callArguments(node).slice(0, 1);
571
+ }
572
+ if (name !== 'union') {
573
+ return [];
574
+ }
575
+ const [rawOptions] = callArguments(node);
576
+ const options = unwrapExpression(rawOptions);
577
+ return options?.type === 'ArrayExpression'
578
+ ? getNodeElements(options).filter(
579
+ (element): element is AstNode => element !== null
580
+ )
581
+ : [];
582
+ };
583
+
584
+ const collectUnsupportedOptionalWrapperStarts = (
585
+ node: AstNode,
586
+ schemaBindings: SchemaBindings,
587
+ unsupported: Set<number>,
588
+ options: { readonly optionalWrapperAllowed?: boolean } = {}
589
+ ): void => {
590
+ const expression = unwrapExpression(node);
591
+ if (expression === undefined) {
592
+ return;
593
+ }
594
+ const name = identifierName(expression);
595
+ if (name !== undefined) {
596
+ const initializer = schemaBindingInitializer(
597
+ schemaBindings,
598
+ name,
599
+ expression.start
600
+ );
601
+ if (initializer !== undefined) {
602
+ collectUnsupportedOptionalWrapperStarts(
603
+ initializer,
604
+ schemaBindings,
605
+ unsupported,
606
+ options
607
+ );
608
+ }
609
+ return;
610
+ }
611
+ if (isOptionalWrapperCall(expression, schemaBindings)) {
612
+ if (options.optionalWrapperAllowed !== true) {
613
+ unsupported.add(expression.start);
614
+ }
615
+ const inner = memberObject(callCallee(expression));
616
+ if (inner) {
617
+ collectUnsupportedOptionalWrapperStarts(
618
+ inner,
619
+ schemaBindings,
620
+ unsupported
621
+ );
622
+ }
623
+ return;
624
+ }
625
+ if (expression.type === 'ObjectExpression') {
626
+ for (const property of objectProperties(expression)) {
627
+ collectUnsupportedOptionalWrapperStarts(
628
+ propertyValue(property) ?? property,
629
+ schemaBindings,
630
+ unsupported,
631
+ { optionalWrapperAllowed: true }
632
+ );
633
+ }
634
+ return;
635
+ }
636
+ if (
637
+ expression.type === 'CallExpression' &&
638
+ memberPropertyName(callCallee(expression)) === 'object'
639
+ ) {
640
+ const [rawShape] = callArguments(expression);
641
+ const shape = unwrapExpression(rawShape);
642
+ if (shape?.type === 'ObjectExpression') {
643
+ for (const property of objectProperties(shape)) {
644
+ collectUnsupportedOptionalWrapperStarts(
645
+ propertyValue(property) ?? property,
646
+ schemaBindings,
647
+ unsupported,
648
+ { optionalWrapperAllowed: true }
649
+ );
650
+ }
651
+ }
652
+ return;
653
+ }
654
+ for (const argument of nestedSchemaArguments(expression)) {
655
+ collectUnsupportedOptionalWrapperStarts(
656
+ argument,
657
+ schemaBindings,
658
+ unsupported
659
+ );
660
+ }
661
+ };
662
+
663
+ const isMemberCallNamed = (
664
+ node: AstNode,
665
+ names: ReadonlySet<string>,
666
+ schemaBindings: SchemaBindings
667
+ ): boolean => {
668
+ if (node.type !== 'CallExpression') {
669
+ return false;
670
+ }
671
+ const callee = callCallee(node);
672
+ if (!callee) {
673
+ return false;
674
+ }
675
+ if (!isZodSchemaCallee(callee, schemaBindings)) {
676
+ return false;
677
+ }
678
+ return names.has(memberPropertyName(callee) ?? '');
679
+ };
680
+
681
+ const bindingName = (node: AstNode): string | undefined =>
682
+ node.type === 'VariableDeclarator'
683
+ ? identifierName(getNodeId(node))
684
+ : undefined;
685
+
686
+ const lexicalScopeTypes = new Set([
687
+ 'ArrowFunctionExpression',
688
+ 'BlockStatement',
689
+ 'FunctionDeclaration',
690
+ 'FunctionExpression',
691
+ 'Program',
692
+ 'StaticBlock',
693
+ ]);
694
+
695
+ const addPatternBindingNames = (
696
+ node: AstNode | undefined,
697
+ into: Set<string>
698
+ ) => {
699
+ if (!node) {
700
+ return;
701
+ }
702
+ if (node.type === 'Identifier') {
703
+ const name = identifierName(node);
704
+ if (name !== undefined) {
705
+ into.add(name);
706
+ }
707
+ return;
708
+ }
709
+ if (node.type === 'AssignmentPattern') {
710
+ addPatternBindingNames(getNodeLeft(node), into);
711
+ return;
712
+ }
713
+ if (node.type === 'RestElement') {
714
+ addPatternBindingNames(getNodeArgument(node), into);
715
+ return;
716
+ }
717
+ if (node.type === 'ArrayPattern') {
718
+ const elements = getNodeElements(node);
719
+ for (const element of elements) {
720
+ addPatternBindingNames(element ?? undefined, into);
721
+ }
722
+ return;
723
+ }
724
+ if (node.type !== 'ObjectPattern') {
725
+ return;
726
+ }
727
+ const properties = getNodeProperties(node) ?? [];
728
+ for (const property of properties) {
729
+ if (property.type === 'RestElement') {
730
+ addPatternBindingNames(property, into);
731
+ continue;
732
+ }
733
+ addPatternBindingNames(getNodeValueNode(property), into);
734
+ }
735
+ };
736
+
737
+ const parameterBindingNames = (node: AstNode): readonly string[] => {
738
+ if (!lexicalScopeTypes.has(node.type) || node.type === 'BlockStatement') {
739
+ return [];
740
+ }
741
+ const names = new Set<string>();
742
+ const params = getNodeParams(node) ?? [];
743
+ for (const param of params) {
744
+ addPatternBindingNames(param, names);
745
+ }
746
+ return [...names];
747
+ };
748
+
749
+ const variableInitializer = (node: AstNode): AstNode | undefined =>
750
+ node.type === 'VariableDeclarator'
751
+ ? (getNodeInit(node) ?? undefined)
752
+ : undefined;
753
+
754
+ const isZodSchemaExpression = (
755
+ node: AstNode | undefined,
756
+ schemaBindings: SchemaBindings
757
+ ): boolean =>
758
+ node?.type === 'CallExpression' &&
759
+ isZodSchemaCallee(callCallee(node), schemaBindings);
760
+
761
+ const schemaBindingExpressionInitializer = (
762
+ node: AstNode | undefined,
763
+ schemaBindings: SchemaBindings
764
+ ): AstNode | undefined => {
765
+ if (isZodSchemaExpression(node, schemaBindings)) {
766
+ return node;
767
+ }
768
+ const name = identifierName(node);
769
+ return name === undefined || node === undefined
770
+ ? undefined
771
+ : schemaBindingInitializer(schemaBindings, name, node.start);
772
+ };
773
+
774
+ const astChildNodes = (node: AstNode): readonly AstNode[] => {
775
+ const children: AstNode[] = [];
776
+ for (const value of Object.values(node)) {
777
+ if (Array.isArray(value)) {
778
+ children.push(
779
+ ...value.filter(
780
+ (entry): entry is AstNode =>
781
+ typeof entry === 'object' &&
782
+ entry !== null &&
783
+ typeof (entry as AstNode).type === 'string'
784
+ )
785
+ );
786
+ continue;
787
+ }
788
+ if (
789
+ typeof value === 'object' &&
790
+ value !== null &&
791
+ typeof (value as AstNode).type === 'string'
792
+ ) {
793
+ children.push(value as AstNode);
794
+ }
795
+ }
796
+ return children;
797
+ };
798
+
799
+ const collectZodSchemaBindings = (ast: AstNode): SchemaBindings => {
800
+ const bindings = new Map<string, SchemaBindingRecord[]>();
801
+
802
+ const visit = (
803
+ node: AstNode,
804
+ scope: { readonly end: number; readonly start: number }
805
+ ): void => {
806
+ const nextScope = lexicalScopeTypes.has(node.type)
807
+ ? { end: node.end, start: node.start }
808
+ : scope;
809
+ const name = bindingName(node);
810
+ const initializer = variableInitializer(node);
811
+ for (const parameterName of parameterBindingNames(node)) {
812
+ const records = bindings.get(parameterName) ?? [];
813
+ records.push({
814
+ initializer: undefined,
815
+ scopeEnd: nextScope.end,
816
+ scopeStart: nextScope.start,
817
+ start: node.start,
818
+ });
819
+ bindings.set(parameterName, records);
820
+ }
821
+ if (name !== undefined) {
822
+ const records = bindings.get(name) ?? [];
823
+ records.push({
824
+ initializer: schemaBindingExpressionInitializer(initializer, bindings),
825
+ scopeEnd: nextScope.end,
826
+ scopeStart: nextScope.start,
827
+ start: node.start,
828
+ });
829
+ bindings.set(name, records);
830
+ }
831
+
832
+ for (const child of astChildNodes(node)) {
833
+ visit(child, nextScope);
834
+ }
835
+ };
836
+
837
+ visit(ast, { end: ast.end, start: ast.start });
838
+ return bindings;
839
+ };
840
+
841
+ /**
842
+ * Detect coerced primitive schema calls such as `z.coerce.number()`. The final
843
+ * callee property is a supported primitive name, so the deny-list never matches;
844
+ * the coercion lives on the intermediate `.coerce` member. The runtime marker
845
+ * guard rejects `def.coerce === true`, so Warden must flag the same shape.
846
+ */
847
+ const isCoerceMarkerCall = (node: AstNode): boolean => {
848
+ if (node.type !== 'CallExpression') {
849
+ return false;
850
+ }
851
+ const callee = callCallee(node);
852
+ if (!callee || !isMemberAccessNonComputed(callee)) {
853
+ return false;
854
+ }
855
+ const object = memberObject(callee);
856
+ return (
857
+ object !== undefined &&
858
+ isMemberAccessNonComputed(object) &&
859
+ memberPropertyName(object) === 'coerce' &&
860
+ isZodSchemaReceiver(memberObject(object))
861
+ );
862
+ };
863
+
864
+ const hasVersionOption = (node: AstNode | undefined): boolean =>
865
+ node?.type === 'ObjectExpression' && hasProperty(node, 'version');
866
+
867
+ const composeCallHasVersionPin = (node: AstNode): boolean => {
868
+ if (node.type !== 'CallExpression') {
869
+ return false;
870
+ }
871
+ const args = getNodeArguments(node);
872
+ const callee = getNodeCallee(node);
873
+ if (!callee || !args) {
874
+ return false;
875
+ }
876
+
877
+ const isComposeIdentifier =
878
+ callee.type === 'Identifier' && getNodeName(callee) === 'compose';
879
+ const property = getNodeProperty(callee);
880
+ const isComposeMember =
881
+ isMemberAccessNonComputed(callee) &&
882
+ property?.type === 'Identifier' &&
883
+ getNodeName(property) === 'compose';
884
+
885
+ return (isComposeIdentifier || isComposeMember) && hasVersionOption(args[2]);
886
+ };
887
+
888
+ export const versionPinnedCompose: WardenRule = {
889
+ check(sourceCode, filePath) {
890
+ const ast = parse(filePath, sourceCode);
891
+ if (!ast) {
892
+ return [];
893
+ }
894
+
895
+ const diagnostics: WardenDiagnostic[] = [];
896
+ for (const blaze of findBlazeBodies(ast)) {
897
+ walk(blaze, (node) => {
898
+ if (!composeCallHasVersionPin(node)) {
899
+ return;
900
+ }
901
+ diagnostics.push(
902
+ diagnostic(
903
+ VERSION_PINNED_COMPOSE,
904
+ 'warn',
905
+ filePath,
906
+ sourceCode,
907
+ node,
908
+ 'ctx.compose() version pins are temporary migration debt. Prefer keeping composition current, or document why this pin can be removed later.'
909
+ )
910
+ );
911
+ });
912
+ }
913
+ return diagnostics;
914
+ },
915
+ description:
916
+ 'Warn when ctx.compose() calls pin a specific trail version instead of composing with the current trail.',
917
+ name: VERSION_PINNED_COMPOSE,
918
+ severity: 'warn',
919
+ };
920
+
921
+ export const forkWithoutPreservedBlaze: WardenRule = {
922
+ check(sourceCode, filePath) {
923
+ const ast = parse(filePath, sourceCode);
924
+ if (!ast) {
925
+ return [];
926
+ }
927
+
928
+ const diagnostics: WardenDiagnostic[] = [];
929
+ for (const definition of findTrailDefinitions(ast)) {
930
+ if (definition.kind !== 'trail') {
931
+ continue;
932
+ }
933
+ for (const entry of versionEntries(definition.config)) {
934
+ if (hasProperty(entry, 'transpose') || hasProperty(entry, 'blaze')) {
935
+ continue;
936
+ }
937
+ diagnostics.push(
938
+ diagnostic(
939
+ FORK_WITHOUT_PRESERVED_BLAZE,
940
+ 'error',
941
+ filePath,
942
+ sourceCode,
943
+ entry,
944
+ `Trail "${definition.id}" has a historical version entry without transpose or blaze. Add transpose for a revision entry, or preserve the historical blaze for a fork entry.`
945
+ )
946
+ );
947
+ }
948
+ }
949
+ return diagnostics;
950
+ },
951
+ description:
952
+ 'Require historical fork version entries to preserve a blaze, while revision entries declare transpose.',
953
+ name: FORK_WITHOUT_PRESERVED_BLAZE,
954
+ severity: 'error',
955
+ };
956
+
957
+ const directSchemaNodesForTrail = (config: AstNode): readonly AstNode[] => {
958
+ const nodes: AstNode[] = [];
959
+ for (const key of ['input', 'output']) {
960
+ const value = propertyValue(findConfigProperty(config, key));
961
+ if (value) {
962
+ nodes.push(value);
963
+ }
964
+ }
965
+ for (const entry of versionEntries(config)) {
966
+ for (const key of ['input', 'output']) {
967
+ const value = propertyValue(findConfigProperty(entry, key));
968
+ if (value) {
969
+ nodes.push(value);
970
+ }
971
+ }
972
+ }
973
+ return nodes;
974
+ };
975
+
976
+ const schemaNodesForTrail = (
977
+ config: AstNode,
978
+ schemaBindings: SchemaBindings
979
+ ): readonly AstNode[] => {
980
+ const nodes: AstNode[] = [...directSchemaNodesForTrail(config)];
981
+ const seenBindings = new Set<string>();
982
+ let index = 0;
983
+ while (index < nodes.length) {
984
+ const node = nodes[index];
985
+ index += 1;
986
+ if (node === undefined) {
987
+ continue;
988
+ }
989
+ walkScope(node, (candidate) => {
990
+ const name = identifierName(candidate);
991
+ if (name === undefined || seenBindings.has(name)) {
992
+ return;
993
+ }
994
+ const initializer = schemaBindingInitializer(
995
+ schemaBindings,
996
+ name,
997
+ candidate.start
998
+ );
999
+ if (initializer === undefined) {
1000
+ return;
1001
+ }
1002
+ seenBindings.add(name);
1003
+ nodes.push(initializer);
1004
+ });
1005
+ }
1006
+ return nodes;
1007
+ };
1008
+
1009
+ export const markerSchemaUnsupported: WardenRule = {
1010
+ check(sourceCode, filePath) {
1011
+ const ast = parse(filePath, sourceCode);
1012
+ if (!ast) {
1013
+ return [];
1014
+ }
1015
+
1016
+ const diagnostics: WardenDiagnostic[] = [];
1017
+ const schemaBindings = collectZodSchemaBindings(ast);
1018
+ for (const definition of findTrailDefinitions(ast)) {
1019
+ if (definition.kind !== 'trail' || !trailIsVersioned(definition.config)) {
1020
+ continue;
1021
+ }
1022
+ const seenDiagnostics = new Set<number>();
1023
+ const unsupportedOptionalWrapperStarts = new Set<number>();
1024
+ for (const schema of directSchemaNodesForTrail(definition.config)) {
1025
+ collectUnsupportedOptionalWrapperStarts(
1026
+ schema,
1027
+ schemaBindings,
1028
+ unsupportedOptionalWrapperStarts
1029
+ );
1030
+ }
1031
+ for (const schema of schemaNodesForTrail(
1032
+ definition.config,
1033
+ schemaBindings
1034
+ )) {
1035
+ walkScope(schema, (node) => {
1036
+ if (
1037
+ !unsupportedOptionalWrapperStarts.has(node.start) &&
1038
+ !isMemberCallNamed(node, unsupportedSchemaCalls, schemaBindings) &&
1039
+ !isCoerceMarkerCall(node) &&
1040
+ !isMultiValueLiteralCall(node, schemaBindings) &&
1041
+ !isJsonLossyLiteralCall(node, schemaBindings) &&
1042
+ !isJsonLossyEnumCall(node, schemaBindings) &&
1043
+ !isReferenceValuedLiteralCall(node, schemaBindings) &&
1044
+ !isReferenceValuedEnumCall(node, schemaBindings) &&
1045
+ !isHiddenOptionalWrapperCall(node, schemaBindings)
1046
+ ) {
1047
+ return;
1048
+ }
1049
+ if (seenDiagnostics.has(node.start)) {
1050
+ return;
1051
+ }
1052
+ seenDiagnostics.add(node.start);
1053
+ diagnostics.push(
1054
+ diagnostic(
1055
+ MARKER_SCHEMA_UNSUPPORTED,
1056
+ 'error',
1057
+ filePath,
1058
+ sourceCode,
1059
+ node,
1060
+ `Trail "${definition.id}" uses a schema construct outside the supported version-marker subset. Use explicit object, primitive, enum, array, optional, nullable, and union schemas for versioned contracts.`
1061
+ )
1062
+ );
1063
+ });
1064
+ }
1065
+ }
1066
+ return diagnostics;
1067
+ },
1068
+ description:
1069
+ 'Reject versioned trail schema constructs that cannot be projected into stable marker contracts.',
1070
+ name: MARKER_SCHEMA_UNSUPPORTED,
1071
+ severity: 'error',
1072
+ };