typespec-http-zod 0.1.0 → 0.3.0

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.
package/dist/src/types.js CHANGED
@@ -1,13 +1,13 @@
1
- import { getDiscriminatedUnion, getEncode, resolveEncodedName, } from "@typespec/compiler";
1
+ import { getDiscriminatedUnion, getEncode, isNeverType, resolveEncodedName, } from "@typespec/compiler";
2
2
  import { reportDiagnostic } from "./lib.js";
3
3
  import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, objectKey, } from "./zod.js";
4
4
  /**
5
- * TypeSpec type → plain TypeScript type text.
5
+ * TypeSpec type -> plain TypeScript type text.
6
6
  *
7
7
  * **Why a second walker rather than `z.infer` of the emitted Zod.** The contract types are meant to
8
8
  * be framework-free: a codebase passes them between layers that have no business importing a
9
9
  * validation library, and layering rules commonly forbid exactly that. A type inferred from a schema
10
- * drags the schema — and `zod` — everywhere it goes, so the shared type has to be emitted as ordinary
10
+ * drags the schema - and `zod` - everywhere it goes, so the shared type has to be emitted as ordinary
11
11
  * TypeScript. That is the whole point: one declaration both sides are checked against, with neither
12
12
  * importing the other.
13
13
  *
@@ -38,7 +38,7 @@ const SCALARS = {
38
38
  decimal: "number",
39
39
  decimal128: "number",
40
40
  numeric: "number",
41
- /** Dates cross the wire as strings — `zod.ts` emits `z.string()` for all of these. */
41
+ /** Dates cross the wire as strings - `zod.ts` emits `z.string()` for all of these. */
42
42
  utcDateTime: "string",
43
43
  offsetDateTime: "string",
44
44
  plainDate: "string",
@@ -47,7 +47,7 @@ const SCALARS = {
47
47
  url: "string",
48
48
  };
49
49
  /**
50
- * What a walk returns after reporting a refusal — see `UNREPRESENTABLE` in `zod.ts`.
50
+ * What a walk returns after reporting a refusal - see `UNREPRESENTABLE` in `zod.ts`.
51
51
  *
52
52
  * `never` is the honest placeholder in TypeScript: nothing is assignable to it, so a contract type
53
53
  * built on one cannot be used by accident, and the error diagnostic has already failed the compile.
@@ -63,7 +63,7 @@ function refuse(program, type, why) {
63
63
  return UNREPRESENTABLE;
64
64
  }
65
65
  function scalarToTs(program, scalar) {
66
- // A scalar declaration can carry the encoding itself — see `scalarToZod`.
66
+ // A scalar declaration can carry the encoding itself - see `scalarToZod`.
67
67
  const encoded = getEncode(program, scalar)?.type;
68
68
  if (encoded !== undefined && encoded !== scalar)
69
69
  return scalarToTs(program, encoded);
@@ -75,12 +75,21 @@ function scalarToTs(program, scalar) {
75
75
  return mapped;
76
76
  current = current.baseScalar;
77
77
  }
78
- reportDiagnostic(program, {
79
- code: "unsupported-scalar",
80
- target: scalar,
81
- format: { artefact: "a TypeScript type", name: scalar.name },
82
- });
83
- return UNREPRESENTABLE;
78
+ /**
79
+ * **`unknown`, matching `z.unknown()` on the other side and `{}` in the document.**
80
+ *
81
+ * **Fixing only the Zod walk left the two artefacts describing different shapes.** `scalarToZod`
82
+ * was changed to emit `z.unknown()` for a scalar with no known base, because openapi3 publishes the
83
+ * empty schema and that asserts nothing. This walk still refused, so a consumer with
84
+ * `contracts-output-dir` set got `value: z.unknown()` in `schemas.gen.ts` and `value: never` in
85
+ * `requests.gen.ts` - plus the refusal that was supposed to be gone.
86
+ *
87
+ * That is the disagreement `wire-contract.gen.ts` exists to catch, reintroduced by the fix for the
88
+ * defect beside it. It went unseen for one commit because the verification compile left
89
+ * `contracts-output-dir` unset, so this walk never ran - the same shape as the `never` property,
90
+ * where the diagnostic came from the walk nobody was looking at.
91
+ */
92
+ return "unknown";
84
93
  }
85
94
  function enumToTs(target) {
86
95
  const values = [...target.members.values()].map((member) => member.value ?? member.name);
@@ -88,7 +97,7 @@ function enumToTs(target) {
88
97
  }
89
98
  function unionToTs(program, union) {
90
99
  /**
91
- * A `@discriminated` union is a choice between WRAPPINGS of its variants — see
100
+ * A `@discriminated` union is a choice between WRAPPINGS of its variants - see
92
101
  * `discriminatedVariant` in `zod.ts` for the two wire shapes and why `envelope: "none"` diverges
93
102
  * from the document. Emitted here too, or the contract type says `Cat | Dog` while the validator
94
103
  * requires `{kind, value}` and the assertion pairing them fails.
@@ -120,7 +129,7 @@ function unionToTs(program, union) {
120
129
  }
121
130
  function modelToTs(program, model) {
122
131
  /**
123
- * ⚠️ **A `@discriminator` base is a CHOICE between its subtypes, not a shape of its own — and this
132
+ * **A `@discriminator` base is a CHOICE between its subtypes, not a shape of its own - and this
124
133
  * walk did not know that.** `modelToZod` has had the rule since polymorphism was first supported;
125
134
  * here the base was walked as an ordinary model, so `@discriminator("kind") model Shape` produced
126
135
  * `interface Shape { kind: string; label: string }`.
@@ -129,9 +138,9 @@ function modelToTs(program, model) {
129
138
  * could narrow it; `kind: string` admitted every string the validator rejects; and the subtypes got
130
139
  * no declaration at all, because nothing reached them.
131
140
  *
132
- * ⚠️ **It survived because the two walks are only ever compared where a spec declares polymorphism
141
+ * **It survived because the two walks are only ever compared where a spec declares polymorphism
133
142
  * AND something compiles the emitted assertions.** `wire-contract.gen.ts` exists precisely to catch
134
- * the emitter disagreeing with itself — one TypeSpec, walked twice — and this is the second defect
143
+ * the emitter disagreeing with itself - one TypeSpec, walked twice - and this is the second defect
135
144
  * of that class it has found. The first was a vocabulary inlined by one walk and referenced by the
136
145
  * other.
137
146
  */
@@ -141,7 +150,7 @@ function modelToTs(program, model) {
141
150
  }
142
151
  const indexer = effectiveIndexer(model);
143
152
  const indexerKey = indexer?.key.name;
144
- // Inherited properties included — see `inheritedAndOwnProperties`. Both artefacts describe the
153
+ // Inherited properties included - see `inheritedAndOwnProperties`. Both artefacts describe the
145
154
  // same model, so a base whose properties reach one and not the other is a contradiction we ship.
146
155
  const declared = inheritedAndOwnProperties(model);
147
156
  if (indexerKey === "integer" && indexer !== undefined) {
@@ -150,45 +159,70 @@ function modelToTs(program, model) {
150
159
  return /[|&\s]/.test(element) ? `(${element})[]` : `${element}[]`;
151
160
  }
152
161
  /**
153
- * ⚠️ **A string indexer only means "dictionary" when the model declares NO properties.**
162
+ * **A string indexer only means "dictionary" when the model declares NO properties.**
154
163
  *
155
164
  * The same trap `modelToZod` had, and it bites harder here: every permissive response model is
156
- * `{ …pinned fields; ...Record<unknown> }`, so reading the indexer first would type all 97 of them
157
- * as a bare `Record<string, unknown>` — a type that satisfies any assertion about what the response
165
+ * `{ ...pinned fields; ...Record<unknown> }`, so reading the indexer first would type all 97 of them
166
+ * as a bare `Record<string, unknown>` - a type that satisfies any assertion about what the response
158
167
  * contains, which is precisely the check this file exists to feed.
159
168
  */
160
169
  if (indexerKey === "string" && indexer !== undefined && declared.length === 0) {
161
170
  return `Record<string, ${typeToTs(program, indexer.value)}>`;
162
171
  }
163
- const entries = declared.map((property) => `\t${propertyToTs(program, property)}`);
164
172
  /**
165
- * ⚠️ **The indexer is deliberately NOT intersected in.**
173
+ * **A `never` property is dropped here too, and this walk is where the refusal actually came
174
+ * from.** `modelToZod` filters it because `@typespec/openapi3` omits such a property from the
175
+ * document entirely - `model N { value: never; other: string }` publishes as `{other}` with
176
+ * `required: ["other"]`. This walk had no property filter at all, so it reached `typeToTs`, hit the
177
+ * `Intrinsic` case and reported `unsupported-type` from a type with no source node - which is why
178
+ * the diagnostic pointed at `<unknown location>` and why filtering the Zod side alone did not
179
+ * silence it.
166
180
  *
167
- * `{ …pinned } & Record<string, unknown>` looks like the honest rendering of a permissive model,
181
+ * Two walks over one TypeSpec have to agree about which properties exist, or `wire-contract.gen.ts`
182
+ * pairs a validator against a type describing a different shape. That assertion is the only thing
183
+ * that catches this emitter disagreeing with itself, and it cannot catch a disagreement that
184
+ * aborts the compile first.
185
+ */
186
+ const entries = declared
187
+ .filter((property) => !isNeverType(property.type))
188
+ .map((property) => `\t${propertyToTs(program, property)}`);
189
+ /**
190
+ * **The indexer is deliberately NOT intersected in.**
191
+ *
192
+ * `{ ...pinned } & Record<string, unknown>` looks like the honest rendering of a permissive model,
168
193
  * and it makes the type unsatisfiable: a plain interface has no index signature, so no domain
169
194
  * result can be assignable to it and the assertion this type feeds fails for **every** operation.
170
195
  * A gate that fails for everything is as useless as one that passes for everything.
171
196
  *
172
- * Permissiveness is a statement about the VALIDATOR — what it lets through — not about what a
197
+ * Permissiveness is a statement about the VALIDATOR - what it lets through - not about what a
173
198
  * producer must contain. The contract type is the floor: these fields, at least.
174
199
  */
175
200
  return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
176
201
  }
177
- /** A property line, including its optionality marker. */
178
- export function propertyToTs(program, property) {
179
- // The WIRE name, resolved exactly as `zod.ts` resolves it — `wire-contract.gen.ts` asserts the
202
+ /**
203
+ * A property line, including its optionality marker.
204
+ *
205
+ * `wireName` overrides the key for an HTTP PARAMETER, whose wire name comes from `@header("x-thing")`
206
+ * or `@query("$select")` rather than from any JSON encoding. `resolveEncodedName` cannot see those,
207
+ * so a header renamed on the wire was keyed here by its TypeScript property name while the emitted
208
+ * validator keyed it by the wire name. A consumer checking its handlers against the generated input
209
+ * types then failed to typecheck against a server that was correct: `z.object({ "x-thing": ... })`
210
+ * on one side and `{ thing: string }` on the other.
211
+ */
212
+ export function propertyToTs(program, property, wireName) {
213
+ // The WIRE name, resolved exactly as `zod.ts` resolves it - `wire-contract.gen.ts` asserts the
180
214
  // emitted Zod infers this type, so the two must name every property identically or it fails.
181
- const key = objectKey(resolveEncodedName(program, property, "application/json"));
182
- // `@encode` changes the wire TYPE, resolved exactly as `zod.ts` resolves it — a property encoded as
215
+ const key = objectKey(wireName ?? resolveEncodedName(program, property, "application/json"));
216
+ // `@encode` changes the wire TYPE, resolved exactly as `zod.ts` resolves it - a property encoded as
183
217
  // a string on one side and an array on the other fails the emitted assertion, and deserves to.
184
218
  const encoded = getEncode(program, property)?.type;
185
219
  const value = typeToTs(program, encoded ?? property.type);
186
220
  /**
187
- * ⚠️ **A defaulted property is REQUIRED here.**
221
+ * **A defaulted property is REQUIRED here.**
188
222
  *
189
223
  * `zod.ts` emits `.default(v)`, which makes the field optional on the way *in* and guaranteed on
190
224
  * the way *out*. The assertion this type feeds compares against `z.infer`, which is the output
191
- * type — so by the time a value reaches the wire the default has fired and the field is present.
225
+ * type - so by the time a value reaches the wire the default has fired and the field is present.
192
226
  * Marking it optional would make a correct schema fail its own assertion.
193
227
  */
194
228
  if (property.defaultValue !== undefined)
@@ -215,7 +249,7 @@ export function typeToTs(program, type) {
215
249
  return typeToTsBody(program, type);
216
250
  }
217
251
  /**
218
- * The same walk, except that `type` is never resolved to its own name — see `SchemaRegistry.#inline`
252
+ * The same walk, except that `type` is never resolved to its own name - see `SchemaRegistry.#inline`
219
253
  * for why the "not itself" rule has to sit at the top of the walk rather than inside the resolver.
220
254
  */
221
255
  export function typeToTsBody(program, type) {
@@ -236,7 +270,7 @@ export function typeToTsBody(program, type) {
236
270
  return String(type.value);
237
271
  case "EnumMember":
238
272
  return JSON.stringify(type.value ?? type.name);
239
- // A named union variant is a referable type — `DogKind.Golden`. See the note in `zod.ts`.
273
+ // A named union variant is a referable type - `DogKind.Golden`. See the note in `zod.ts`.
240
274
  case "UnionVariant":
241
275
  return typeToTs(program, type.type);
242
276
  case "Intrinsic":
@@ -3,9 +3,9 @@ import { type Program } from "@typespec/compiler";
3
3
  /**
4
4
  * **Serve the version the service is on, not every version it has ever had.**
5
5
  *
6
- * ⚠️ **The emitter used to emit the UNION of all versions.** Measured on
6
+ * **The emitter used to emit the UNION of all versions.** Measured on
7
7
  * `versioning/removed`: v2 deletes `POST /v1` and `POST /interface-v1/v1`, and both were mounted
8
- * anyway — 582 routes against the 580 the document declares. A service built on that output serves
8
+ * anyway - 582 routes against the 580 the document declares. A service built on that output serves
9
9
  * endpoints its current version does not have, validates bodies against properties a later version
10
10
  * removed, and does all of it while the document says otherwise.
11
11
  *
@@ -13,7 +13,7 @@ import { type Program } from "@typespec/compiler";
13
13
  * `v1, v2preview, v2`, the documents sort alphabetically, and the differential compared us against
14
14
  * `openapi.v2preview.json`. Fixing WHICH document is read is what made this visible at all.
15
15
  *
16
- * ⚠️ **`@typespec/versioning` is an OPTIONAL peer, behind a guarded `import()`** — the same treatment
16
+ * **`@typespec/versioning` is an OPTIONAL peer, behind a guarded `import()`** - the same treatment
17
17
  * `@typespec/streams` gets, and for the same reason: a spec that declares no versions must not need
18
18
  * the package installed. `@typespec/openapi3` resolves it exactly this way (`versioning-module.js`),
19
19
  * so a service is versioned for us precisely when it is versioned for the document we are judged
@@ -44,14 +44,14 @@ export interface ServiceSnapshot {
44
44
  /**
45
45
  * Every service to emit, each already projected to its current version.
46
46
  *
47
- * ⚠️ **The LAST snapshot, not a merge and not a choice.** `getVersioningMutators` returns snapshots
48
- * in declared order — which is the only order that means anything, since a version name sorts
47
+ * **The LAST snapshot, not a merge and not a choice.** `getVersioningMutators` returns snapshots
48
+ * in declared order - which is the only order that means anything, since a version name sorts
49
49
  * however its author spelled it (`v2preview` sorts after `v2`). The last one is what the service
50
50
  * currently serves, and one schema set can only describe one version.
51
51
  *
52
52
  * Serving an older version would be a legitimate thing to want and is deliberately NOT offered here:
53
53
  * it needs an option, a name for the emitted directory, and an answer for what the contracts package
54
- * exports. Emitting the union — which is what happened before — is not that feature, it is the
54
+ * exports. Emitting the union - which is what happened before - is not that feature, it is the
55
55
  * absence of this one.
56
56
  */
57
57
  export declare function serviceSnapshots(program: Program, versioning: VersioningModule | undefined): ServiceSnapshot[];
@@ -12,14 +12,14 @@ export async function resolveVersioningModule() {
12
12
  /**
13
13
  * Every service to emit, each already projected to its current version.
14
14
  *
15
- * ⚠️ **The LAST snapshot, not a merge and not a choice.** `getVersioningMutators` returns snapshots
16
- * in declared order — which is the only order that means anything, since a version name sorts
15
+ * **The LAST snapshot, not a merge and not a choice.** `getVersioningMutators` returns snapshots
16
+ * in declared order - which is the only order that means anything, since a version name sorts
17
17
  * however its author spelled it (`v2preview` sorts after `v2`). The last one is what the service
18
18
  * currently serves, and one schema set can only describe one version.
19
19
  *
20
20
  * Serving an older version would be a legitimate thing to want and is deliberately NOT offered here:
21
21
  * it needs an option, a name for the emitted directory, and an answer for what the contracts package
22
- * exports. Emitting the union — which is what happened before — is not that feature, it is the
22
+ * exports. Emitting the union - which is what happened before - is not that feature, it is the
23
23
  * absence of this one.
24
24
  */
25
25
  export function serviceSnapshots(program, versioning) {
package/dist/src/zod.d.ts CHANGED
@@ -4,13 +4,13 @@ export declare function withSealedObjects<T>(sealed: boolean, run: () => T): T;
4
4
  export declare function withVisibility<T>(program: Program, next: Visibility, run: () => T): T;
5
5
  /** Whether this model is shaped differently at `visibility` than at the canonical `Read`. */
6
6
  export declare function isTransformedBy(program: Program, model: Model, at: Visibility): boolean;
7
- /** The visibility the current walk is emitting for — the registry keys declarations on it. */
7
+ /** The visibility the current walk is emitting for - the registry keys declarations on it. */
8
8
  export declare function currentVisibility(): Visibility;
9
9
  /**
10
10
  * Record every enum a service DECLARES, not merely every one its success bodies happen to reach.
11
11
  *
12
- * ⚠️ **A vocabulary is a fact about the contract, not about a code path.** Collecting them during the
13
- * Zod walk meant an enum reachable only through an ERROR body was never emitted — which is exactly
12
+ * **A vocabulary is a fact about the contract, not about a code path.** Collecting them during the
13
+ * Zod walk meant an enum reachable only through an ERROR body was never emitted - which is exactly
14
14
  * what happened to one surface's error-code enum: declared in the spec, published on every
15
15
  * operation's failure responses, and absent from the generated vocabularies, so the one list a
16
16
  * consumer branches on had to be hand-written beside the generated ones. Walking declarations removes
@@ -20,16 +20,16 @@ export declare function noteDeclaredVocabularies(enums: Iterable<Enum>): void;
20
20
  /**
21
21
  * Record a named model's PROPERTY NAMES as a runtime vocabulary.
22
22
  *
23
- * ⚠️ **This exists because "a `Record` keyed by an enum" is inexpressible, and the workaround was
23
+ * **This exists because "a `Record` keyed by an enum" is inexpressible, and the workaround was
24
24
  * about to cost a duplicated list.** `ProfileAttributes` is 150-odd known keys; naming them in the
25
25
  * spec is what makes the published document state the constraint instead of hiding it behind a
26
26
  * hand-written predicate. But a *type* cannot be iterated, and more than one layer usually needs
27
- * the set at RUN time — so without this the keys would live in the spec AND as a hand-maintained
28
- * tuple in a shared package — one vocabulary stated twice, which is precisely the
27
+ * the set at RUN time - so without this the keys would live in the spec AND as a hand-maintained
28
+ * tuple in a shared package - one vocabulary stated twice, which is precisely the
29
29
  * drift the deleted predicate existed to prevent.
30
30
  *
31
- * ⚠️ **Selected by an emitter OPTION, deliberately not by a decorator.** A decorator would put a
32
- * mark in the spec that no document carries — the exact objection that deleted the last four. This
31
+ * **Selected by an emitter OPTION, deliberately not by a decorator.** A decorator would put a
32
+ * mark in the spec that no document carries - the exact objection that deleted the last four. This
33
33
  * changes no schema, states nothing in the contract and enforces nothing; it decides which
34
34
  * convenience artefact gets written beside the types, which is what an emitter option is for.
35
35
  *
@@ -56,10 +56,10 @@ export declare function withSyntheticDeclarations<T>(hook: (name: string, build:
56
56
  /**
57
57
  * Every property the model declares, **including the ones it inherits**.
58
58
  *
59
- * ⚠️ **`model.properties` holds only a model's OWN properties, and `baseModel` appeared nowhere in
59
+ * **`model.properties` holds only a model's OWN properties, and `baseModel` appeared nowhere in
60
60
  * this emitter.** `model Extension extends Element` reaches the document as `allOf: [{$ref: Element}]`
61
- * beside the derived model's own properties, and the sealing keyword openapi3 uses —
62
- * `unevaluatedProperties` — is chosen precisely because it sees through that. So the document said
61
+ * beside the derived model's own properties, and the sealing keyword openapi3 uses -
62
+ * `unevaluatedProperties` - is chosen precisely because it sees through that. So the document said
63
63
  * `Extension` carries `level` **and** the inherited `extension`, while this emitter produced
64
64
  * `z.object({level}).strict()`: the inherited property gone, and the model closed against it. The
65
65
  * conformance scenario's own documented request body would have been rejected with a 400 naming a key
@@ -67,31 +67,31 @@ export declare function withSyntheticDeclarations<T>(hook: (name: string, build:
67
67
  *
68
68
  * Invisible for two reasons at once. The spec it was first built against declared no `extends` at
69
69
  * all, and the differential
70
- * skipped every schema carrying an `allOf` — so 28 of 226 components were not divergent, they were
70
+ * skipped every schema carrying an `allOf` - so 28 of 226 components were not divergent, they were
71
71
  * never compared.
72
72
  *
73
73
  * Flattened rather than composed. `z.object(...).extend(...)` would mirror the document's structure
74
74
  * more literally and cannot be used here: `.extend()` reads `shape` eagerly, so a base whose property
75
- * refers back to the derived model — `type/model/inheritance/recursive`, the exact case — would throw
75
+ * refers back to the derived model - `type/model/inheritance/recursive`, the exact case - would throw
76
76
  * during module initialisation. Flattening asks nothing of the base but its properties.
77
77
  */
78
78
  export declare function inheritedAndOwnProperties(model: Model): ModelProperty[];
79
- /** The indexer in force, which a model can inherit — `model X extends Record<unknown>`. */
79
+ /** The indexer in force, which a model can inherit - `model X extends Record<unknown>`. */
80
80
  export declare function effectiveIndexer(model: Model): Model["indexer"];
81
81
  /**
82
82
  * The subtypes a `@discriminator` base stands for, in declaration order.
83
83
  *
84
84
  * Filtered by openapi3's own {@link includeDerivedModel} so a template declaration is not mistaken
85
- * for a real subtype — the same rule that decides whether a base may be sealed.
85
+ * for a real subtype - the same rule that decides whether a base may be sealed.
86
86
  */
87
87
  export declare function discriminatedSubtypes(program: Program, model: Model): readonly Model[];
88
88
  /**
89
- * A name as an object key — quoted when it is not a legal JavaScript identifier.
89
+ * A name as an object key - quoted when it is not a legal JavaScript identifier.
90
90
  *
91
- * ⚠️ **A TypeSpec property can be named anything**, and the corpus proves it: `parameters/spread`
91
+ * **A TypeSpec property can be named anything**, and the corpus proves it: `parameters/spread`
92
92
  * declares `` `x-ms-test-header`: string ``. A parameter walk assumed the property name was always
93
- * safe — its docblock even argued that using the property name rather than the wire name was what
94
- * avoided this — and emitted `x-ms-test-header: z.string()`, a file that does not parse.
93
+ * safe - its docblock even argued that using the property name rather than the wire name was what
94
+ * avoided this - and emitted `x-ms-test-header: z.string()`, a file that does not parse.
95
95
  *
96
96
  * Exported so there is one answer to this question. There were two sites and only one of them knew.
97
97
  */
@@ -103,8 +103,8 @@ export declare function noteBackEdge(): void;
103
103
  /**
104
104
  * Render `run()`, reporting whether it reached back into a declaration still being rendered.
105
105
  *
106
- * ⚠️ **The count is restored on the way out, and that is the point.** A caller that wraps the
107
- * reference in a getter has *settled* the back edge — the reference is now lazy and the enclosing
106
+ * **The count is restored on the way out, and that is the point.** A caller that wraps the
107
+ * reference in a getter has *settled* the back edge - the reference is now lazy and the enclosing
108
108
  * object is safe. Letting it propagate would make every ancestor defer as well, and would make the
109
109
  * registry report a cycle that has already been dealt with.
110
110
  */
@@ -118,7 +118,7 @@ export declare function typeToZod(program: Program, type: Type): string;
118
118
  *
119
119
  * The registry renders a declaration's *body* with this: asking the resolver first would hand back
120
120
  * the identifier the declaration is about to bind, emitting `const xSchema = xSchema`. Nested
121
- * occurrences still go through {@link typeToZod} and do resolve — which is what makes a recursive
121
+ * occurrences still go through {@link typeToZod} and do resolve - which is what makes a recursive
122
122
  * model reference itself instead of inlining itself forever.
123
123
  */
124
124
  export declare function typeToZodBody(program: Program, type: Type): string;