@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.42

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