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

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