typespec-http-zod 0.1.0 → 0.2.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/zod.js CHANGED
@@ -5,7 +5,7 @@ import { reportDiagnostic } from "./lib.js";
5
5
  /**
6
6
  * What a walk returns after reporting a refusal.
7
7
  *
8
- * ⚠️ **Reporting does not unwind, which is the point.** A throw abandoned the compile at the first
8
+ * **Reporting does not unwind, which is the point.** A throw abandoned the compile at the first
9
9
  * unrepresentable construct; reporting lets the walk carry on and name every one of them in a single
10
10
  * run. The walk still has to return a `string`, and `z.never()` is the honest placeholder: valid
11
11
  * Zod, rejects everything, and unreachable in practice because the error diagnostic has already
@@ -13,53 +13,53 @@ import { reportDiagnostic } from "./lib.js";
13
13
  */
14
14
  const UNREPRESENTABLE = "z.never()";
15
15
  /**
16
- * TypeSpec type → Zod source text.
16
+ * TypeSpec type -> Zod source text.
17
17
  *
18
- * **Deliberately partial, and it fails loudly rather than guessing.** A general TypeSpec→Zod
18
+ * **Deliberately partial, and it fails loudly rather than guessing.** A general TypeSpec->Zod
19
19
  * converter has to answer for every construct the language can express; this one only has to answer
20
20
  * for the constructs our own specs are allowed to use, which is what makes it a bounded problem
21
21
  * rather than an open-ended dependency (see the ADR on why `typespec-zod@0.0.0-68` is not in the
22
- * runtime path). Anything outside that set throws with the offending type named — an emitter that
22
+ * runtime path). Anything outside that set throws with the offending type named - an emitter that
23
23
  * silently produced `z.any()` for a construct it did not understand would push a hole all the way
24
24
  * into `respond()`, where the whole point is that drift is caught.
25
25
  */
26
26
  /**
27
- * External module → the constants the emitted schemas reference from it.
27
+ * External module -> the constants the emitted schemas reference from it.
28
28
  *
29
29
  * Collected during emission rather than pre-scanned: only the enums actually reachable from an
30
30
  * operation should produce an import, and reachability is exactly what the walk already computes.
31
31
  *
32
- * ⚠️ **Scoped to one emission, not module-global-and-cleared.** The previous shape was a module
32
+ * **Scoped to one emission, not module-global-and-cleared.** The previous shape was a module
33
33
  * `Map` emptied by whoever read it first, which is correct only while there is exactly one service
34
- * and one read. A second `@service` namespace — a public surface beside an internal one — would
34
+ * and one read. A second `@service` namespace - a public surface beside an internal one - would
35
35
  * have merged one surface's imports into the other's file: an unused import if lucky, a missing one
36
36
  * if not. Neither is discoverable while there is only one service, so it is fixed before there are
37
37
  * two rather than found by them. Same save/restore idiom as {@link withRefResolver}.
38
38
  */
39
39
  let externalImports;
40
40
  /**
41
- * Enum name → its declared members, for every vocabulary the surface actually reaches.
41
+ * Enum name -> its declared members, for every vocabulary the surface actually reaches.
42
42
  *
43
43
  * The spec is the single source for these. A consumer's shared package used to hold them as tuples
44
- * and the spec merely *described* them for the document's benefit — two lists that agreed only by
44
+ * and the spec merely *described* them for the document's benefit - two lists that agreed only by
45
45
  * inspection, and by the time this flipped, two of them did not: `PersonalAttribute` published four
46
46
  * of six values wrong (the charge-authentication answers an MR01 cannot be filed without) and
47
47
  * `SubmissionStatusCode` published all five wrong. Generating them removes the second list.
48
48
  */
49
49
  let vocabularies;
50
50
  /**
51
- * Whether a model is SEALED — closed to properties nobody declared.
51
+ * Whether a model is SEALED - closed to properties nobody declared.
52
52
  *
53
- * ⚠️ **A property of the model's own shape, not of where it is used.** This was previously "the
53
+ * **A property of the model's own shape, not of where it is used.** This was previously "the
54
54
  * model is reachable from a request body", computed by a whole-graph pre-pass. That rule is wrong in
55
55
  * a way that is invisible while it happens to agree: there is one declaration per model, so adding a
56
56
  * single operation that accepts `ProblemItem` in a body would flip `problemItemSchema` from
57
57
  * stripping to rejecting **in every response that uses it too**. Strictness would be decided by the
58
58
  * rest of the spec rather than by the model.
59
59
  *
60
- * ⚠️ **This mirrors `@typespec/openapi3`'s `shouldSealSchema` deliberately, line for line.** The
60
+ * **This mirrors `@typespec/openapi3`'s `shouldSealSchema` deliberately, line for line.** The
61
61
  * emitted validator and the published document have to answer the same question the same way, and
62
- * the only way to guarantee that is to ask it the same way — not to arrive at the same answer by a
62
+ * the only way to guarantee that is to ask it the same way - not to arrive at the same answer by a
63
63
  * different route and measure that they still agree. The exception for models with derived models is
64
64
  * theirs: sealing a base would make a subtype's own properties "unevaluated" against it.
65
65
  *
@@ -69,7 +69,7 @@ let vocabularies;
69
69
  * `seal-object-schemas` in `EmitterOptions`, defaulting to openapi3's own default.
70
70
  */
71
71
  function isSealed(model) {
72
- // `Record<never>` is closed whatever the option says — the spec has stated it outright, and
72
+ // `Record<never>` is closed whatever the option says - the spec has stated it outright, and
73
73
  // openapi3 seals it for the same reason.
74
74
  if (model.indexer !== undefined)
75
75
  return isNeverType(model.indexer.value);
@@ -80,11 +80,11 @@ function isSealed(model) {
80
80
  /**
81
81
  * Whether `@typespec/openapi3` is sealing object schemas for this compile.
82
82
  *
83
- * ⚠️ **This emitter cannot see the other emitter's options, so the consumer states it twice.**
83
+ * **This emitter cannot see the other emitter's options, so the consumer states it twice.**
84
84
  * Awkward, and the alternative is worse: assume sealing is on and a project running openapi3 with
85
85
  * its default gets validators that reject properties its published document permits. Whichever way
86
86
  * the two are configured, the conformance differential compares openness in both directions and
87
- * fails on a mismatch — so a project that sets one and forgets the other is told immediately rather
87
+ * fails on a mismatch - so a project that sets one and forgets the other is told immediately rather
88
88
  * than shipping a contradiction.
89
89
  */
90
90
  let sealObjectSchemas = false;
@@ -99,7 +99,7 @@ export function withSealedObjects(sealed, run) {
99
99
  }
100
100
  }
101
101
  /**
102
- * The visibility the current walk is emitting for — which decides what counts as HTTP metadata.
102
+ * The visibility the current walk is emitting for - which decides what counts as HTTP metadata.
103
103
  *
104
104
  * Visibility decides **which** metadata applies: `@statusCode` is applicable to a response and not
105
105
  * to a request, so the same model projects differently in the two directions. `createMetadataInfo`
@@ -107,7 +107,7 @@ export function withSealedObjects(sealed, run) {
107
107
  * from answering differently. Measured on `type/model/visibility`: openapi3 emits five components
108
108
  * for one model, and before this we emitted one carrying every lifecycle property at once.
109
109
  *
110
- * ⚠️ **Position is a SECOND axis, and this only handles the first.** `@header` is metadata at a
110
+ * **Position is a SECOND axis, and this only handles the first.** `@header` is metadata at a
111
111
  * payload root and ordinary data nested inside a `@body`; `isPayloadProperty` takes a third
112
112
  * argument, `ignoreMetadataAnnotations`, which openapi3 threads as emitter context
113
113
  * (`schema-emitter.js:210`) and this does not pass. A metadata-carrying model used in **both**
@@ -116,11 +116,11 @@ export function withSealedObjects(sealed, run) {
116
116
  * That shape is rare rather than theoretical: it raises `metadata-ignored` from `@typespec/http`,
117
117
  * which fires **zero** times across all 65 `@typespec/http-specs` scenarios,
118
118
  * and one variant of it makes openapi3 itself fail with `duplicate-type-name`. It is a real gap with
119
- * no known occurrence — recorded here rather than described as an incident, because it has not been
119
+ * no known occurrence - recorded here rather than described as an incident, because it has not been
120
120
  * one.
121
121
  *
122
122
  * Scoped rather than threaded through six signatures, matching the idiom the rest of this module
123
- * already uses — the walk is deeply recursive and a parameter would have to be carried by every
123
+ * already uses - the walk is deeply recursive and a parameter would have to be carried by every
124
124
  * function whether it cared or not.
125
125
  */
126
126
  let visibility = Visibility.Read;
@@ -138,7 +138,7 @@ export function withVisibility(program, next, run) {
138
138
  metadataInfo = previousInfo;
139
139
  }
140
140
  }
141
- /** Whether a property is optional AT THE CURRENT POSITION — PATCH makes update fields optional. */
141
+ /** Whether a property is optional AT THE CURRENT POSITION - PATCH makes update fields optional. */
142
142
  function isOptionalAt(program, property) {
143
143
  metadataInfo ??= createMetadataInfo(program);
144
144
  return metadataInfo.isOptional(property, visibility);
@@ -153,12 +153,12 @@ export function isTransformedBy(program, model, at) {
153
153
  metadataInfo ??= createMetadataInfo(program);
154
154
  return metadataInfo.isTransformed(model, at);
155
155
  }
156
- /** The visibility the current walk is emitting for — the registry keys declarations on it. */
156
+ /** The visibility the current walk is emitting for - the registry keys declarations on it. */
157
157
  export function currentVisibility() {
158
158
  return visibility;
159
159
  }
160
160
  /**
161
- * openapi3's own rule for which derived models count — `@typespec/openapi3`'s `util.ts`.
161
+ * openapi3's own rule for which derived models count - `@typespec/openapi3`'s `util.ts`.
162
162
  *
163
163
  * A template declaration is not a real subtype, and neither is an uninstantiated template; both
164
164
  * would otherwise stop a base from being sealed for no reason a reader could see.
@@ -176,8 +176,8 @@ function noteVocabulary(name, values) {
176
176
  /**
177
177
  * Record every enum a service DECLARES, not merely every one its success bodies happen to reach.
178
178
  *
179
- * ⚠️ **A vocabulary is a fact about the contract, not about a code path.** Collecting them during the
180
- * Zod walk meant an enum reachable only through an ERROR body was never emitted — which is exactly
179
+ * **A vocabulary is a fact about the contract, not about a code path.** Collecting them during the
180
+ * Zod walk meant an enum reachable only through an ERROR body was never emitted - which is exactly
181
181
  * what happened to one surface's error-code enum: declared in the spec, published on every
182
182
  * operation's failure responses, and absent from the generated vocabularies, so the one list a
183
183
  * consumer branches on had to be hand-written beside the generated ones. Walking declarations removes
@@ -191,16 +191,16 @@ export function noteDeclaredVocabularies(enums) {
191
191
  /**
192
192
  * Record a named model's PROPERTY NAMES as a runtime vocabulary.
193
193
  *
194
- * ⚠️ **This exists because "a `Record` keyed by an enum" is inexpressible, and the workaround was
194
+ * **This exists because "a `Record` keyed by an enum" is inexpressible, and the workaround was
195
195
  * about to cost a duplicated list.** `ProfileAttributes` is 150-odd known keys; naming them in the
196
196
  * spec is what makes the published document state the constraint instead of hiding it behind a
197
197
  * hand-written predicate. But a *type* cannot be iterated, and more than one layer usually needs
198
- * the set at RUN time — so without this the keys would live in the spec AND as a hand-maintained
199
- * tuple in a shared package — one vocabulary stated twice, which is precisely the
198
+ * the set at RUN time - so without this the keys would live in the spec AND as a hand-maintained
199
+ * tuple in a shared package - one vocabulary stated twice, which is precisely the
200
200
  * drift the deleted predicate existed to prevent.
201
201
  *
202
- * ⚠️ **Selected by an emitter OPTION, deliberately not by a decorator.** A decorator would put a
203
- * mark in the spec that no document carries — the exact objection that deleted the last four. This
202
+ * **Selected by an emitter OPTION, deliberately not by a decorator.** A decorator would put a
203
+ * mark in the spec that no document carries - the exact objection that deleted the last four. This
204
204
  * changes no schema, states nothing in the contract and enforces nothing; it decides which
205
205
  * convenience artefact gets written beside the types, which is what an emitter option is for.
206
206
  *
@@ -228,16 +228,16 @@ export function noteExternalImport(module, name) {
228
228
  externalImports.set(module, names);
229
229
  }
230
230
  /**
231
- * Which package the generated Zod imports its vocabularies from — `undefined` when there is none.
231
+ * Which package the generated Zod imports its vocabularies from - `undefined` when there is none.
232
232
  *
233
- * ⚠️ **Not a constant, and no longer defaulted.** Each `@service` publishes its own contracts
233
+ * **Not a constant, and no longer defaulted.** Each `@service` publishes its own contracts
234
234
  * package, so a hardcoded specifier makes the second service's schemas import names that package
235
235
  * does not export. Scoped the same way the strict-model set is, so a nested emission cannot leak
236
236
  * into its parent.
237
237
  *
238
- * ⚠️ **It used to default to one repository's own package name.** A
238
+ * **It used to default to one repository's own package name.** A
239
239
  * consumer who configured nothing got validators importing `SPEC_VOCABULARIES` from a package they
240
- * had never heard of — output that looks right and does not resolve. There is no honest default for
240
+ * had never heard of - output that looks right and does not resolve. There is no honest default for
241
241
  * "the caller's own package", so the absence is the answer: with no package named, an enum is
242
242
  * emitted inline and the output depends on nothing.
243
243
  */
@@ -331,24 +331,38 @@ function scalarToZod(program, scalar) {
331
331
  while (current !== undefined) {
332
332
  const mapped = SCALARS[current.name];
333
333
  // Constraints are read from the scalar the property NAMED, not from the primitive it resolves
334
- // to, so `@pattern(…) scalar TrimmedString extends string` carries its rules to every use.
334
+ // to, so `@pattern(...) scalar TrimmedString extends string` carries its rules to every use.
335
335
  if (mapped !== undefined)
336
336
  return applyConstraints(program, mapped, scalar);
337
337
  current = current.baseScalar;
338
338
  }
339
- reportDiagnostic(program, {
340
- code: "unsupported-scalar",
341
- target: scalar,
342
- format: { artefact: "Zod", name: scalar.name },
343
- });
344
- return UNREPRESENTABLE;
339
+ /**
340
+ * **A scalar with no known base is `z.unknown()`, and it used to be a refusal.**
341
+ *
342
+ * **`z.never()` was the exact inversion of what the document says.** `scalar Mystery;` is
343
+ * published by `@typespec/openapi3` as `"Mystery": {}` - the empty schema, which under JSON Schema
344
+ * asserts nothing and therefore accepts **everything**. This emitter refused the compile and, on the
345
+ * way, wrote `value: z.never()`, which accepts **nothing**. Measured against the emitted schema:
346
+ * `"hello"`, `42` and `null` were all rejected, so a service built on it would answer 400 to every
347
+ * request the document calls valid.
348
+ *
349
+ * Refusing was the wrong answer twice over: the document represents the spec perfectly, so under the
350
+ * governing rule this emitter must too - the same source has to be representable by both or by
351
+ * neither. And `z.unknown()` is not a guess, it is the precise reading of `{}`: a validator that
352
+ * enforces nothing is what a schema asserting nothing means.
353
+ *
354
+ * No diagnostic, because openapi3 raises none. Copying its rule includes copying its silence; a
355
+ * warning here would report a problem the published contract does not have. This is the same
356
+ * treatment a raw binary body already gets, for the same reason.
357
+ */
358
+ return applyConstraints(program, "z.unknown()", scalar);
345
359
  }
346
360
  /**
347
361
  * A TypeSpec `enum` becomes a Zod enum over its member values, resolved through the generated
348
362
  * vocabulary constant so the document and the runtime cannot describe different sets.
349
363
  *
350
- * ⚠️ **`@externalValues` used to be able to point that arrow the other way** — the spec deferring to
351
- * a hand-written TypeScript tuple — and while it did, the members here described only the
364
+ * **`@externalValues` used to be able to point that arrow the other way** - the spec deferring to
365
+ * a hand-written TypeScript tuple - and while it did, the members here described only the
352
366
  * *document*. It is gone, and the tuples are generated from the spec.
353
367
  */
354
368
  function enumToZod(_program, target) {
@@ -359,7 +373,7 @@ function enumToZod(_program, target) {
359
373
  /**
360
374
  * Every string vocabulary resolves through the generated constant rather than being restated.
361
375
  *
362
- * The members are declared once, in the spec, and the vocabularies artefact is generated from them —
376
+ * The members are declared once, in the spec, and the vocabularies artefact is generated from them -
363
377
  * so the document and the runtime cannot describe different vocabularies. This replaces the
364
378
  * `@externalValues` decorator, which pointed the arrow the other way: the spec deferred to a
365
379
  * hand-written TypeScript constant, which is the opposite of a spec being a source of truth.
@@ -368,7 +382,7 @@ function enumToZod(_program, target) {
368
382
  /**
369
383
  * Inline unless the consumer named a package to share the tuple through.
370
384
  *
371
- * The indirection exists so a SECOND package — another layer, a client — can branch on the same
385
+ * The indirection exists so a SECOND package - another layer, a client - can branch on the same
372
386
  * values without restating them. A consumer who has no such package wants a self-contained
373
387
  * validator, and emitting a reference to one they never configured is the only way to get output
374
388
  * that cannot resolve.
@@ -390,7 +404,7 @@ function enumToZod(_program, target) {
390
404
  /**
391
405
  * Register a declaration the SPEC never wrote, under a name the document publishes.
392
406
  *
393
- * A `@discriminated` union with an envelope has no TypeSpec type for its wrapper — openapi3
407
+ * A `@discriminated` union with an envelope has no TypeSpec type for its wrapper - openapi3
394
408
  * synthesises one (`tk.model.create`, `schema-emitter.js:436`) and publishes it as
395
409
  * `PetWithEnvelopeCat`. The registry keys declarations on a `Type`, and there is no type to key on,
396
410
  * so this is the seam that lets one be declared by name instead. Falls back to inlining when no
@@ -418,17 +432,17 @@ function declaresDiscriminator(program, variant, property) {
418
432
  /**
419
433
  * One variant of a `@discriminated` union, as it appears ON THE WIRE.
420
434
  *
421
- * ⚠️ **The union is not a choice between its variant types — it is a choice between wrappings of
435
+ * **The union is not a choice between its variant types - it is a choice between wrappings of
422
436
  * them**, and this emitter used to ignore the wrapping entirely. `z.discriminatedUnion("kind",
423
437
  * [catSchema, dogSchema])` over models that carry no `kind` at all **matches nothing**: every
424
438
  * request to a polymorphic endpoint failed, and the differential could not see it because it read a
425
439
  * discriminated component as an object.
426
440
  *
427
- * `envelope: "object"` (the default) wraps the variant — `{kind: "cat", value: {…Cat}}` — and
441
+ * `envelope: "object"` (the default) wraps the variant - `{kind: "cat", value: {...Cat}}` - and
428
442
  * openapi3 publishes each wrapper as a component named `union.type.name + capitalize(variantName)`.
429
443
  * Mirrored exactly, so the identifiers line up with the component names.
430
444
  *
431
- * `envelope: "none"` injects the discriminator into the variant — `{kind: "cat", …Cat}` — and is
445
+ * `envelope: "none"` injects the discriminator into the variant - `{kind: "cat", ...Cat}` - and is
432
446
  * emitted **only when the variant declares that property itself**. Otherwise the document does not
433
447
  * publish it and the union is refused; see `undeclared-discriminator`.
434
448
  */
@@ -452,19 +466,19 @@ function unionToZod(program, union) {
452
466
  * Both accept the same values, but a plain union reports "no variant matched" and lists every
453
467
  * failure, while a discriminated one reads the tag and reports the single rule that was actually
454
468
  * broken. These unions exist to carry cross-field rules that used to hide in a hand-written
455
- * predicate — publishing the rule and then reporting it unreadably would only move the problem.
469
+ * predicate - publishing the rule and then reporting it unreadably would only move the problem.
456
470
  */
457
471
  const [discriminated] = getDiscriminatedUnion(program, union);
458
472
  if (discriminated !== undefined) {
459
473
  const options = discriminated.options;
460
474
  if (discriminated.defaultVariant !== undefined) {
461
475
  // "Anything else" has no discriminator value to switch on. Refused by name rather than
462
- // silently dropped — no corpus scenario exercises it, so guessing would be untested code.
476
+ // silently dropped - no corpus scenario exercises it, so guessing would be untested code.
463
477
  return refuse(program, union, "a discriminated union with a default variant");
464
478
  }
465
479
  /**
466
- * ⚠️ **Refused, not compensated for.** With no envelope the discriminator lives inside the
467
- * variant, and openapi3 publishes the variant unchanged — so unless the variant declares the
480
+ * **Refused, not compensated for.** With no envelope the discriminator lives inside the
481
+ * variant, and openapi3 publishes the variant unchanged - so unless the variant declares the
468
482
  * property, the document names a discriminator it never requires, which OpenAPI 3.1 forbids.
469
483
  * Injecting it here would enforce a rule the contract does not state: correct against the wire,
470
484
  * derived from nothing anybody published, and indistinguishable from a bug to every other
@@ -512,25 +526,25 @@ function unionToZod(program, union) {
512
526
  /**
513
527
  * TypeSpec models arrays and dictionaries as a `Model` carrying an **indexer**.
514
528
  *
515
- * ⚠️ **Keyed on the indexer, not on the model's name.** This used to test `name === "Record"`, which
516
- * silently lost the indexer of any model *derived* from one — `model ProfileAttributes is
529
+ * **Keyed on the indexer, not on the model's name.** This used to test `name === "Record"`, which
530
+ * silently lost the indexer of any model *derived* from one - `model ProfileAttributes is
517
531
  * Record<string | float64 | boolean>` emitted `z.object({})`, a schema that accepts an object and
518
532
  * then strips every key in it. For the profile attributes that is silent data loss on a write, and
519
533
  * nothing downstream could have told the difference between "the caller sent nothing" and "we threw
520
534
  * it away". A named model is still declared and referenced normally; only its *body* comes from the
521
535
  * indexer.
522
536
  */
523
- /** `unknown` — the indexer value of a `...Record<unknown>` spread, which is plain permissiveness. */
537
+ /** `unknown` - the indexer value of a `...Record<unknown>` spread, which is plain permissiveness. */
524
538
  function isUnknownType(type) {
525
539
  return type.kind === "Intrinsic" && type.name === "unknown";
526
540
  }
527
541
  /**
528
542
  * Every property the model declares, **including the ones it inherits**.
529
543
  *
530
- * ⚠️ **`model.properties` holds only a model's OWN properties, and `baseModel` appeared nowhere in
544
+ * **`model.properties` holds only a model's OWN properties, and `baseModel` appeared nowhere in
531
545
  * this emitter.** `model Extension extends Element` reaches the document as `allOf: [{$ref: Element}]`
532
- * beside the derived model's own properties, and the sealing keyword openapi3 uses —
533
- * `unevaluatedProperties` — is chosen precisely because it sees through that. So the document said
546
+ * beside the derived model's own properties, and the sealing keyword openapi3 uses -
547
+ * `unevaluatedProperties` - is chosen precisely because it sees through that. So the document said
534
548
  * `Extension` carries `level` **and** the inherited `extension`, while this emitter produced
535
549
  * `z.object({level}).strict()`: the inherited property gone, and the model closed against it. The
536
550
  * conformance scenario's own documented request body would have been rejected with a 400 naming a key
@@ -538,12 +552,12 @@ function isUnknownType(type) {
538
552
  *
539
553
  * Invisible for two reasons at once. The spec it was first built against declared no `extends` at
540
554
  * all, and the differential
541
- * skipped every schema carrying an `allOf` — so 28 of 226 components were not divergent, they were
555
+ * skipped every schema carrying an `allOf` - so 28 of 226 components were not divergent, they were
542
556
  * never compared.
543
557
  *
544
558
  * Flattened rather than composed. `z.object(...).extend(...)` would mirror the document's structure
545
559
  * more literally and cannot be used here: `.extend()` reads `shape` eagerly, so a base whose property
546
- * refers back to the derived model — `type/model/inheritance/recursive`, the exact case — would throw
560
+ * refers back to the derived model - `type/model/inheritance/recursive`, the exact case - would throw
547
561
  * during module initialisation. Flattening asks nothing of the base but its properties.
548
562
  */
549
563
  export function inheritedAndOwnProperties(model) {
@@ -559,7 +573,7 @@ export function inheritedAndOwnProperties(model) {
559
573
  }
560
574
  return [...byName.values()];
561
575
  }
562
- /** The indexer in force, which a model can inherit — `model X extends Record<unknown>`. */
576
+ /** The indexer in force, which a model can inherit - `model X extends Record<unknown>`. */
563
577
  export function effectiveIndexer(model) {
564
578
  for (let link = model; link !== undefined; link = link.baseModel) {
565
579
  if (link.indexer !== undefined)
@@ -571,7 +585,7 @@ export function effectiveIndexer(model) {
571
585
  * The subtypes a `@discriminator` base stands for, in declaration order.
572
586
  *
573
587
  * Filtered by openapi3's own {@link includeDerivedModel} so a template declaration is not mistaken
574
- * for a real subtype — the same rule that decides whether a base may be sealed.
588
+ * for a real subtype - the same rule that decides whether a base may be sealed.
575
589
  */
576
590
  export function discriminatedSubtypes(program, model) {
577
591
  if (getDiscriminator(program, model) === undefined)
@@ -580,18 +594,18 @@ export function discriminatedSubtypes(program, model) {
580
594
  }
581
595
  function modelToZod(program, model) {
582
596
  /**
583
- * ⚠️ **A `@discriminator` base is a CHOICE between its subtypes, not a shape of its own.**
597
+ * **A `@discriminator` base is a CHOICE between its subtypes, not a shape of its own.**
584
598
  *
585
599
  * `@discriminator("kind") model Bird { kind: string; wingspan: int32 }` with four models extending
586
600
  * it reaches OpenAPI as a component carrying `discriminator: {propertyName, mapping}`, and the
587
601
  * mapping is an instruction: validate the body against whichever subtype the discriminator names.
588
602
  * Emitting the base's own properties instead produced `z.object({kind: z.string(), wingspan})`,
589
- * which accepts `{kind: "eagle"}` while ignoring everything `Eagle` actually declares — a
603
+ * which accepts `{kind: "eagle"}` while ignoring everything `Eagle` actually declares - a
590
604
  * polymorphic endpoint validating none of its polymorphism.
591
605
  *
592
606
  * `z.discriminatedUnion` is the faithful encoding and composes both ways this corpus needs.
593
607
  * Measured on Zod 4.4.3: a union may be an OPTION of another union, which is how
594
- * `Fish → Shark → SawShark` works; and an option carrying a getter that refers back to the union
608
+ * `Fish -> Shark -> SawShark` works; and an option carrying a getter that refers back to the union
595
609
  * being defined does **not** throw, because the discriminator is read without enumerating the rest
596
610
  * of the shape. `Eagle.friends?: Bird[]` is exactly that, and it decides whether polymorphism and
597
611
  * recursion can coexist at all.
@@ -609,10 +623,10 @@ function modelToZod(program, model) {
609
623
  return applyConstraints(program, `z.array(${typeToZod(program, indexer.value)})`, model);
610
624
  }
611
625
  /**
612
- * ⚠️ **A string indexer only means "dictionary" when the model declares NO properties.**
626
+ * **A string indexer only means "dictionary" when the model declares NO properties.**
613
627
  *
614
- * `model X { id: string; ...Record<unknown> }` — the standard TypeSpec way to say "these fields,
615
- * plus anything else", and what `@loose` was replaced by — carries both. Returning
628
+ * `model X { id: string; ...Record<unknown> }` - the standard TypeSpec way to say "these fields,
629
+ * plus anything else", and what `@loose` was replaced by - carries both. Returning
616
630
  * `z.record(...)` for it emits a schema that accepts an object and then **strips every declared
617
631
  * field**, so `respond()`'s drift detection degrades to "is it an object" and the pinned fields
618
632
  * silently stop being checked. This function's own history is the other half of the same mistake:
@@ -625,8 +639,8 @@ function modelToZod(program, model) {
625
639
  * Permissiveness, read from the INDEXER and from nothing else.
626
640
  *
627
641
  * `...Record<unknown>` is the standard TypeSpec construct and the one `@typespec/openapi3` can
628
- * see, so it is the only source. There was a second — `@loose`, a decorator of ours, invisible to
629
- * the document — and it is gone: a model that accepted unknown properties while the published
642
+ * see, so it is the only source. There was a second - `@loose`, a decorator of ours, invisible to
643
+ * the document - and it is gone: a model that accepted unknown properties while the published
630
644
  * schema said otherwise is a runtime disagreeing with its own contract.
631
645
  *
632
646
  * An indexer over something other than `unknown` is a typed catchall, where `.loose()` would drop
@@ -648,29 +662,45 @@ function modelToZod(program, model) {
648
662
  */
649
663
  const strict = suffix === "" && isSealed(model) ? ".strict()" : "";
650
664
  /**
651
- * ⚠️ **HTTP metadata is not payload.** A `@header` or `@statusCode` property describes the envelope,
652
- * so it never appears in the body — `@typespec/openapi3` strips it from the schema it emits, and a
665
+ * **HTTP metadata is not payload.** A `@header` or `@statusCode` property describes the envelope,
666
+ * so it never appears in the body - `@typespec/openapi3` strips it from the schema it emits, and a
653
667
  * validator that kept it would demand a field the wire never carries.
654
668
  *
655
669
  * Found the moment a model carried its own content-type: RFC 9457 puts
656
670
  * `@header("content-type") contentType: "application/problem+json"` on the problem model, the
657
671
  * document correctly described a body without it, and this emitter produced
658
- * `contentType: z.literal(…)` as a required member. Two artefacts from one source, disagreeing
659
- * about the shape of a body — the exact contradiction ADR 0005 exists to make impossible.
672
+ * `contentType: z.literal(...)` as a required member. Two artefacts from one source, disagreeing
673
+ * about the shape of a body - the exact contradiction ADR 0005 exists to make impossible.
660
674
  */
661
675
  /**
662
- * ⚠️ **A property that refers back into an unfinished declaration is emitted as a GETTER.**
676
+ * **A property that refers back into an unfinished declaration is emitted as a GETTER.**
663
677
  *
664
678
  * `model InnerModel { children?: InnerModel[] }` is a plain recursive tree, and openapi3 publishes
665
679
  * it as a self-`$ref`. Emitting `children: z.array(innerModelSchema)` reads the `const` inside its
666
680
  * own initialiser, so the module throws on import; deferring the reference behind a getter is the
667
681
  * idiom Zod documents for exactly this, and it is the only one that keeps `z.infer` inferring the
668
682
  * recursive type rather than collapsing to `any`. `z.lazy(() => innerModelSchema)` runs correctly
669
- * but does not typecheck — measured: `TS7022`, "implicitly has type 'any' because it does not have
683
+ * but does not typecheck - measured: `TS7022`, "implicitly has type 'any' because it does not have
670
684
  * a type annotation and is referenced directly or indirectly in its own initializer".
671
685
  */
672
686
  const members = declared
673
687
  .filter((property) => isPayloadProperty(program, property))
688
+ /**
689
+ * **A `never` property is DROPPED, exactly as the document drops it.** `model N { value:
690
+ * never; other: string }` is published by `@typespec/openapi3` as `{other}` with
691
+ * `required: ["other"]` - `value` is absent entirely, because a property that can hold no value
692
+ * is not part of the payload.
693
+ *
694
+ * This emitter kept it and wrote `value: z.never()`, which made the model unsatisfiable: measured,
695
+ * `{"other":"x"}` - the exact body the document describes - was REJECTED with "expected never,
696
+ * received undefined". So the validator generated from a document rejected every request that
697
+ * document calls valid, which is the divergence class this package exists to prevent.
698
+ *
699
+ * Dropping it makes the two artefacts agree and leaves nothing to refuse. `never` in a position
700
+ * that is not a property - a body, a variant - is still refused by `typeToZodBody`, because there
701
+ * it is not something the document quietly omits.
702
+ */
703
+ .filter((property) => !isNeverType(property.type))
674
704
  .map((property) => {
675
705
  const key = propertyKey(program, property);
676
706
  const { value, deferred } = captureBackEdges(() => propertyToZod(program, property));
@@ -683,46 +713,45 @@ function modelToZod(program, model) {
683
713
  /**
684
714
  * A model-level `@refine` goes through the same path as every other constraint, so the predicate's
685
715
  * import is recorded exactly once, in one place. Handling it separately here is what dropped the
686
- * import the first time — the emitted schema referenced a name it never imported, and only the
716
+ * import the first time - the emitted schema referenced a name it never imported, and only the
687
717
  * compile check in `test/emit.test.ts` noticed.
688
718
  *
689
719
  * `.refine()` returns an effect rather than an object, so `applyConstraints` appending it after
690
720
  * `.loose()` is also the only order that works.
691
721
  */
692
722
  /**
693
- * ⚠️ **A deferred property forces the CONSTRUCTOR form of the object.**
723
+ * **A deferred property forces the CONSTRUCTOR form of the object.**
694
724
  *
695
725
  * Measured on Zod 4.4.3: `.strict()`, `.loose()` and `.catchall()` all read `shape` eagerly, so
696
726
  * each one fires the getter during module initialisation and throws `Cannot access 'X' before
697
- * initialization` — the very thing the getter exists to avoid. `z.strictObject` and
727
+ * initialization` - the very thing the getter exists to avoid. `z.strictObject` and
698
728
  * `z.looseObject` take the same shape without reading it. The suffix form stays everywhere else,
699
729
  * so a model that does not recurse emits exactly the text it emitted before.
700
730
  *
701
- * A **typed** catchall has no constructor form to move to, so there is nowhere safe to put the
702
- * getter and the cycle is genuinely unrepresentable here — that is a refusal, and it is named.
731
+ * **A TYPED catchall has no constructor form, and that used to be a refusal.** There is nowhere
732
+ * safe to put the getter - `.catchall()` reads `shape` eagerly - so the cycle was reported as
733
+ * `circular-model`. `z.lazy()` is the place to put it: wrapping the whole expression defers the
734
+ * getter past module initialisation, and by the time the schema is forced every declaration exists.
735
+ * Measured on 4.4.3: `z.lazy(() => z.object({ get kid() {...} }).catchall(z.number()))` parses a
736
+ * nested value and still applies the catchall.
703
737
  */
704
738
  const lazily = members.some((member) => member.deferred);
705
739
  const constructible = suffix === "" || suffix === ".loose()";
706
- if (lazily && !constructible) {
707
- reportDiagnostic(program, {
708
- code: "circular-model",
709
- target: model,
710
- format: { name: model.name === "" ? "an anonymous model" : model.name },
711
- });
712
- }
713
740
  const shape = entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
714
- const body = lazily && constructible
715
- ? `${suffix === ".loose()" ? "z.looseObject" : strict === ".strict()" ? "z.strictObject" : "z.object"}(${shape})`
741
+ const body = lazily
742
+ ? constructible
743
+ ? `${suffix === ".loose()" ? "z.looseObject" : strict === ".strict()" ? "z.strictObject" : "z.object"}(${shape})`
744
+ : `z.lazy(() => z.object(${shape})${suffix}${strict})`
716
745
  : `z.object(${shape})${suffix}${strict}`;
717
746
  return applyConstraints(program, body, model);
718
747
  }
719
748
  /**
720
- * A name as an object key — quoted when it is not a legal JavaScript identifier.
749
+ * A name as an object key - quoted when it is not a legal JavaScript identifier.
721
750
  *
722
- * ⚠️ **A TypeSpec property can be named anything**, and the corpus proves it: `parameters/spread`
751
+ * **A TypeSpec property can be named anything**, and the corpus proves it: `parameters/spread`
723
752
  * declares `` `x-ms-test-header`: string ``. A parameter walk assumed the property name was always
724
- * safe — its docblock even argued that using the property name rather than the wire name was what
725
- * avoided this — and emitted `x-ms-test-header: z.string()`, a file that does not parse.
753
+ * safe - its docblock even argued that using the property name rather than the wire name was what
754
+ * avoided this - and emitted `x-ms-test-header: z.string()`, a file that does not parse.
726
755
  *
727
756
  * Exported so there is one answer to this question. There were two sites and only one of them knew.
728
757
  */
@@ -732,24 +761,24 @@ export function objectKey(name) {
732
761
  /**
733
762
  * The name a property carries **on the wire**, which is not always the name the spec gave it.
734
763
  *
735
- * ⚠️ **`@encodedName` is a first-party decorator, and a decorator is a declaration rather than a
764
+ * **`@encodedName` is a first-party decorator, and a decorator is a declaration rather than a
736
765
  * behaviour.** It attaches metadata to the type graph and does nothing on its own; every emitter has
737
- * to go and ask. `@typespec/openapi3` asks — `resolveEncodedName` at `schema-emitter.js:214` — so the
766
+ * to go and ask. `@typespec/openapi3` asks - `resolveEncodedName` at `schema-emitter.js:214` - so the
738
767
  * document publishes the wire name. This emitter did not, so the validator was keyed on the TypeSpec
739
768
  * name and a request carrying exactly what the contract required failed twice over: the declared
740
769
  * property was missing, and the one actually sent was an unrecognised key a sealed model rejects.
741
770
  *
742
771
  * The corpus scenario for this is called "Testing that you send the right JSON name on the wire",
743
- * and it sat in the green column throughout — compiling cleanly while emitting the wrong key.
772
+ * and it sat in the green column throughout - compiling cleanly while emitting the wrong key.
744
773
  *
745
774
  * **The domain therefore sees the wire name too**, because `types.ts` resolves it identically. A
746
775
  * validator that renamed on the way through would be a transform, which JSON Schema cannot express
747
- * and no document could state — the same reason `@trimmed` was removed.
776
+ * and no document could state - the same reason `@trimmed` was removed.
748
777
  */
749
778
  function propertyKey(program, property) {
750
779
  return objectKey(rawPropertyKey(program, property));
751
780
  }
752
- /** The same wire name, unquoted — for a vocabulary tuple, where the value is data rather than code. */
781
+ /** The same wire name, unquoted - for a vocabulary tuple, where the value is data rather than code. */
753
782
  function rawPropertyKey(program, property) {
754
783
  return resolveEncodedName(program, property, JSON_MEDIA_TYPE);
755
784
  }
@@ -758,13 +787,13 @@ function rawPropertyKey(program, property) {
758
787
  *
759
788
  * Fixed rather than threaded: everything this emitter produces validates a JSON body, and
760
789
  * `@encodedName` is per-media-type. A surface serving XML as well would need this carried from the
761
- * operation's content type, as openapi3 does — and would need far more than a name to be correct.
790
+ * operation's content type, as openapi3 does - and would need far more than a name to be correct.
762
791
  */
763
792
  const JSON_MEDIA_TYPE = "application/json";
764
793
  /**
765
794
  * The type a value takes **on the wire**, which `@encode` can make different from its declared one.
766
795
  *
767
- * ⚠️ **A first-party decorator, and a decorator is a declaration rather than a behaviour** — every
796
+ * **A first-party decorator, and a decorator is a declaration rather than a behaviour** - every
768
797
  * emitter has to go and ask. `@typespec/openapi3` asks, so the document publishes the encoded type;
769
798
  * this emitter did not, and validated the DECLARED one. Measured across the corpus, 30 properties
770
799
  * where the two disagreed, and each disagreement rejects a body the contract calls valid:
@@ -774,10 +803,10 @@ const JSON_MEDIA_TYPE = "application/json";
774
803
  * `@encode(string) int32` document `string` we emitted a number
775
804
  * `@encode(unixTimestamp) utcDateTime` document `integer` we emitted a string
776
805
  *
777
- * Exactly the shape of the `@encodedName` defect, found by exactly the same kind of arm — there, the
806
+ * Exactly the shape of the `@encodedName` defect, found by exactly the same kind of arm - there, the
778
807
  * key a property goes by; here, the type it goes as.
779
808
  *
780
- * ⚠️ **An array encoding on a QUERY parameter is a style, not a type change — and this docblock used
809
+ * **An array encoding on a QUERY parameter is a style, not a type change - and this docblock used
781
810
  * to say the opposite.** It claimed the array encodings needed no special case "because a
782
811
  * comma-delimited list IS a string on the wire". True of the wire, and the wrong conclusion: the
783
812
  * document keeps `type: array` and publishes `style: pipeDelimited` beside it, because the parameter
@@ -792,8 +821,8 @@ const JSON_MEDIA_TYPE = "application/json";
792
821
  * }
793
822
  * ```
794
823
  *
795
- * Everywhere else the encoding really does change the type — `@encode(unixTimestamp) utcDateTime` is
796
- * an integer — so this narrows to exactly the case the reference implementation narrows to. The
824
+ * Everywhere else the encoding really does change the type - `@encode(unixTimestamp) utcDateTime` is
825
+ * an integer - so this narrows to exactly the case the reference implementation narrows to. The
797
826
  * flattening is undone at the parameter, by {@link COLLECTION_DELIMITERS}.
798
827
  */
799
828
  const PARAMETER_STYLE_ENCODINGS = new Set([
@@ -815,9 +844,9 @@ function encodedTypeOf(program, target) {
815
844
  }
816
845
  export function propertyToZod(program, property) {
817
846
  /**
818
- * ⚠️ **Constraints go INSIDE `.nullable()`, not after it.**
847
+ * **Constraints go INSIDE `.nullable()`, not after it.**
819
848
  *
820
- * `z.string().nullable().min(1)` does not compile — `.min` is not on `ZodNullable` — so a property
849
+ * `z.string().nullable().min(1)` does not compile - `.min` is not on `ZodNullable` - so a property
821
850
  * typed `string | null` with `@minLength(1)` has to emit `z.string().min(1).nullable()`. The
822
851
  * nullability is peeled off here, the constraints applied to what is underneath, and the wrapper
823
852
  * put back. Found by a consumer's own typecheck on a nullable-and-constrained property.
@@ -826,7 +855,7 @@ export function propertyToZod(program, property) {
826
855
  * to the value.
827
856
  */
828
857
  const nullable = nullableInnerOf(property.type);
829
- // `@encode` on the property wins over its declared type — see `encodedTypeOf`. The nullable
858
+ // `@encode` on the property wins over its declared type - see `encodedTypeOf`. The nullable
830
859
  // wrapper is kept: `@encode` changes what the value IS, not whether it may be absent.
831
860
  const encoded = encodedTypeOf(program, property);
832
861
  const declared = encoded ?? nullable ?? property.type;
@@ -834,70 +863,95 @@ export function propertyToZod(program, property) {
834
863
  ? applyConstraints(program, typeToZod(program, declared), property)
835
864
  : `${applyConstraints(program, typeToZod(program, declared), property)}.nullable()`;
836
865
  /**
837
- * A TypeSpec default (`basis: string = "s1159"`) becomes `.default(…)`, which in Zod also makes
866
+ * A TypeSpec default (`basis: string = "s1159"`) becomes `.default(...)`, which in Zod also makes
838
867
  * the field optional on the way in. Dropping it turned an optional field with a fallback into a
839
- * required one — callers omitting it would start getting a 400 for a request that used to work.
868
+ * required one - callers omitting it would start getting a 400 for a request that used to work.
840
869
  */
841
870
  const defaulted = defaultOf(program, property);
842
871
  if (defaulted !== undefined)
843
872
  return `${base}.default(${defaulted})`;
844
873
  /**
845
- * ⚠️ **Optionality is a function of the position too, not only of the `?` in the spec.**
874
+ * **Optionality is a function of the position too, not only of the `?` in the spec.**
846
875
  *
847
876
  * TypeSpec's PATCH semantics make a property optional in an update request even where the model
848
- * declares it required — an update sends the fields it is changing. The document says so
877
+ * declares it required - an update sends the fields it is changing. The document says so
849
878
  * (`VisibilityModelUpdate` requires nothing) and a validator that still demanded the property
850
879
  * would reject every partial update the contract invites. `metadataInfo.isOptional` is the same
851
880
  * predicate `@typespec/openapi3` uses to decide the `required` list.
852
881
  */
853
882
  return property.optional || isOptionalAt(program, property) ? `${base}.optional()` : base;
854
883
  }
855
- /** A property's declared default, as a JS literal, or `undefined` when it has none. */
856
- function defaultOf(program, property) {
857
- const value = property.defaultValue;
858
- if (value === undefined)
859
- return undefined;
884
+ /**
885
+ * A default value as a JS literal, or `undefined` where this emitter cannot render one.
886
+ *
887
+ * **Composite defaults used to be REFUSED, and the refusal was wrong on the emitter's own
888
+ * governing rule.** `#["a", "b"]` and `#{ x: 1 }` were reported as `unsupported-default` with the
889
+ * reason "a populated literal default would need each element rendered, and no schema here has one".
890
+ * That was a statement about this function rather than about what can be represented: `.default()`
891
+ * takes any JS value, and `@typespec/openapi3` publishes these exactly -
892
+ * `default: ["a","b"]`, `default: {"x":1,"label":"hi"}`, and nested arrays too, measured from one
893
+ * compile of a spec carrying all three. So the document could say it, Zod could enforce it, and only
894
+ * this emitter refused - which is the same spec being representable by one emitter and not the other,
895
+ * the one thing a differential between them cannot tolerate.
896
+ *
897
+ * **The refusal also emitted `.default(z.never())`**, because `UNREPRESENTABLE` is a schema
898
+ * expression and this position wants a VALUE. Had the diagnostic ever been downgraded to a warning,
899
+ * that would have compiled and set every such default to a Zod object.
900
+ *
901
+ * A value is pure data - strings, numbers, booleans, null, and arrays and objects of those - so it
902
+ * renders as JSON. Anything else stays a refusal, and `ScalarValue` is the live case: a default like
903
+ * `utcDateTime.fromISO(...)` is a constructor call rather than a literal.
904
+ */
905
+ function renderValue(value) {
860
906
  if (value.valueKind === "StringValue")
861
907
  return JSON.stringify(value.value);
862
908
  if (value.valueKind === "NumericValue")
863
909
  return String(value.value.asNumber());
864
910
  if (value.valueKind === "BooleanValue")
865
911
  return String(value.value);
912
+ if (value.valueKind === "NullValue")
913
+ return "null";
866
914
  if (value.valueKind === "EnumValue") {
867
915
  const member = value.value;
868
916
  return JSON.stringify(member.value ?? member.name);
869
917
  }
870
- // `attendees?: string[] = #[]` — an empty-collection default, emitted as `.default([])`.
871
- // Only empty ones: a populated literal default would need each element rendered, and no schema here
872
- // has one, so it stays unsupported rather than half-implemented.
873
918
  if (value.valueKind === "ArrayValue") {
874
- const values = value.values ?? [];
875
- if (values.length === 0)
876
- return "[]";
877
- reportDiagnostic(program, {
878
- code: "unsupported-default",
879
- target: property,
880
- format: { why: "a non-empty array" },
881
- });
882
- return UNREPRESENTABLE;
919
+ const rendered = value.values.map(renderValue);
920
+ // One unrenderable element makes the whole literal unrenderable - a partial array would be a
921
+ // different default from the one the spec declares, which is worse than refusing.
922
+ return rendered.some((entry) => entry === undefined) ? undefined : `[${rendered.join(", ")}]`;
883
923
  }
884
924
  if (value.valueKind === "ObjectValue") {
885
- const properties = value.properties;
886
- if (properties === undefined || properties.size === 0)
887
- return "{}";
888
- reportDiagnostic(program, {
889
- code: "unsupported-default",
890
- target: property,
891
- format: { why: "a non-empty object" },
892
- });
893
- return UNREPRESENTABLE;
925
+ const entries = [];
926
+ for (const [name, descriptor] of value.properties) {
927
+ const rendered = renderValue(descriptor.value);
928
+ if (rendered === undefined)
929
+ return undefined;
930
+ entries.push(`${JSON.stringify(name)}: ${rendered}`);
931
+ }
932
+ return `{${entries.join(", ")}}`;
894
933
  }
934
+ return undefined;
935
+ }
936
+ /** A property's declared default, as a JS literal, or `undefined` when it has none. */
937
+ function defaultOf(program, property) {
938
+ const value = property.defaultValue;
939
+ if (value === undefined)
940
+ return undefined;
941
+ const rendered = renderValue(value);
942
+ if (rendered !== undefined)
943
+ return rendered;
895
944
  reportDiagnostic(program, {
896
945
  code: "unsupported-default",
897
946
  target: property,
898
947
  format: { why: `kind "${value.valueKind}"` },
899
948
  });
900
- return UNREPRESENTABLE;
949
+ /**
950
+ * **No `.default(...)` at all, rather than a default this emitter invented.** Returning
951
+ * `UNREPRESENTABLE` here put a schema expression where a value belongs. The property keeps its
952
+ * declared shape and loses only the fallback, which the diagnostic names.
953
+ */
954
+ return undefined;
901
955
  }
902
956
  /** The non-null half of a two-variant `T | null` union, or `undefined` if it is not one. */
903
957
  function nullableInnerOf(type) {
@@ -931,8 +985,8 @@ export function withRefResolver(resolver, run) {
931
985
  /**
932
986
  * How many references the current walk has made **back into a declaration still being rendered**.
933
987
  *
934
- * A recursive model is an ordinary construct — `@typespec/openapi3` emits a self-`$ref` for
935
- * `model InnerModel { children?: InnerModel[] }` and the document is valid — so the reference has to
988
+ * A recursive model is an ordinary construct - `@typespec/openapi3` emits a self-`$ref` for
989
+ * `model InnerModel { children?: InnerModel[] }` and the document is valid - so the reference has to
936
990
  * be emitted, not refused. But a `const` cannot read itself while it is being initialised, so the
937
991
  * reference must be made **lazy**, and only the enclosing object knows where to put the getter that
938
992
  * does it. The registry counts the back edges; {@link captureBackEdges} is how an object claims them.
@@ -945,8 +999,8 @@ export function noteBackEdge() {
945
999
  /**
946
1000
  * Render `run()`, reporting whether it reached back into a declaration still being rendered.
947
1001
  *
948
- * ⚠️ **The count is restored on the way out, and that is the point.** A caller that wraps the
949
- * reference in a getter has *settled* the back edge — the reference is now lazy and the enclosing
1002
+ * **The count is restored on the way out, and that is the point.** A caller that wraps the
1003
+ * reference in a getter has *settled* the back edge - the reference is now lazy and the enclosing
950
1004
  * object is safe. Letting it propagate would make every ancestor defer as well, and would make the
951
1005
  * registry report a cycle that has already been dealt with.
952
1006
  */
@@ -968,7 +1022,7 @@ export function typeToZod(program, type) {
968
1022
  *
969
1023
  * The registry renders a declaration's *body* with this: asking the resolver first would hand back
970
1024
  * the identifier the declaration is about to bind, emitting `const xSchema = xSchema`. Nested
971
- * occurrences still go through {@link typeToZod} and do resolve — which is what makes a recursive
1025
+ * occurrences still go through {@link typeToZod} and do resolve - which is what makes a recursive
972
1026
  * model reference itself instead of inlining itself forever.
973
1027
  */
974
1028
  export function typeToZodBody(program, type) {
@@ -990,13 +1044,13 @@ export function typeToZodBody(program, type) {
990
1044
  case "EnumMember":
991
1045
  return `z.literal(${JSON.stringify(type.value ?? type.name)})`;
992
1046
  /**
993
- * ⚠️ **A named union VARIANT is a type you can refer to, and it is not the union.**
1047
+ * **A named union VARIANT is a type you can refer to, and it is not the union.**
994
1048
  *
995
1049
  * `union DogKind { string, Golden: "golden" }` is TypeSpec's extensible-enum idiom, and
996
1050
  * `kind: DogKind.Golden` resolves to the variant rather than to its value. Unreachable until
997
1051
  * discriminated subtypes started being declared, at which point the whole
998
1052
  * `type/model/inheritance/enum-discriminator` scenario stopped compiling with
999
- * "no rule for this kind" — a dormant seam, not a new defect.
1053
+ * "no rule for this kind" - a dormant seam, not a new defect.
1000
1054
  */
1001
1055
  case "UnionVariant":
1002
1056
  return typeToZod(program, type.type);