@formspec/build 0.1.0-alpha.27 → 0.1.0-alpha.29

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -2
  3. package/dist/analyzer/class-analyzer.d.ts +12 -6
  4. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  5. package/dist/analyzer/program.d.ts +3 -2
  6. package/dist/analyzer/program.d.ts.map +1 -1
  7. package/dist/browser.cjs +485 -76
  8. package/dist/browser.cjs.map +1 -1
  9. package/dist/browser.js +486 -77
  10. package/dist/browser.js.map +1 -1
  11. package/dist/build-alpha.d.ts +18 -2
  12. package/dist/build-beta.d.ts +18 -2
  13. package/dist/build-internal.d.ts +18 -2
  14. package/dist/build.d.ts +18 -2
  15. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts +5 -2
  16. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
  17. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +5 -1
  18. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  19. package/dist/cli.cjs +1460 -143
  20. package/dist/cli.cjs.map +1 -1
  21. package/dist/cli.js +1458 -139
  22. package/dist/cli.js.map +1 -1
  23. package/dist/generators/class-schema.d.ts +6 -1
  24. package/dist/generators/class-schema.d.ts.map +1 -1
  25. package/dist/generators/method-schema.d.ts.map +1 -1
  26. package/dist/generators/mixed-authoring.d.ts.map +1 -1
  27. package/dist/index.cjs +1425 -141
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1425 -139
  32. package/dist/index.js.map +1 -1
  33. package/dist/internals.cjs +1416 -178
  34. package/dist/internals.cjs.map +1 -1
  35. package/dist/internals.js +1416 -176
  36. package/dist/internals.js.map +1 -1
  37. package/dist/json-schema/generator.d.ts +3 -1
  38. package/dist/json-schema/generator.d.ts.map +1 -1
  39. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  40. package/dist/metadata/collision-guards.d.ts +3 -0
  41. package/dist/metadata/collision-guards.d.ts.map +1 -0
  42. package/dist/metadata/index.d.ts +7 -0
  43. package/dist/metadata/index.d.ts.map +1 -0
  44. package/dist/metadata/policy.d.ts +11 -0
  45. package/dist/metadata/policy.d.ts.map +1 -0
  46. package/dist/metadata/resolve.d.ts +20 -0
  47. package/dist/metadata/resolve.d.ts.map +1 -0
  48. package/dist/ui-schema/generator.d.ts +11 -2
  49. package/dist/ui-schema/generator.d.ts.map +1 -1
  50. package/dist/ui-schema/ir-generator.d.ts +2 -1
  51. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  52. package/package.json +7 -6
package/dist/index.cjs CHANGED
@@ -46,6 +46,315 @@ module.exports = __toCommonJS(index_exports);
46
46
 
47
47
  // src/canonicalize/chain-dsl-canonicalizer.ts
48
48
  var import_internals = require("@formspec/core/internals");
49
+
50
+ // src/metadata/policy.ts
51
+ var NOOP_INFLECT = () => "";
52
+ function normalizePluralization(input) {
53
+ if (input?.mode === "infer-if-missing") {
54
+ return {
55
+ mode: "infer-if-missing",
56
+ infer: () => "",
57
+ inflect: input.inflect
58
+ };
59
+ }
60
+ if (input?.mode === "require-explicit") {
61
+ return {
62
+ mode: "require-explicit",
63
+ infer: () => "",
64
+ inflect: NOOP_INFLECT
65
+ };
66
+ }
67
+ return {
68
+ mode: "disabled",
69
+ infer: () => "",
70
+ inflect: NOOP_INFLECT
71
+ };
72
+ }
73
+ function normalizeScalarPolicy(input) {
74
+ if (input?.mode === "infer-if-missing") {
75
+ return {
76
+ mode: "infer-if-missing",
77
+ infer: input.infer,
78
+ pluralization: normalizePluralization(input.pluralization)
79
+ };
80
+ }
81
+ if (input?.mode === "require-explicit") {
82
+ return {
83
+ mode: "require-explicit",
84
+ infer: () => "",
85
+ pluralization: normalizePluralization(input.pluralization)
86
+ };
87
+ }
88
+ return {
89
+ mode: "disabled",
90
+ infer: () => "",
91
+ pluralization: normalizePluralization(input?.pluralization)
92
+ };
93
+ }
94
+ function normalizeDeclarationPolicy(input) {
95
+ return {
96
+ apiName: normalizeScalarPolicy(input?.apiName),
97
+ displayName: normalizeScalarPolicy(input?.displayName)
98
+ };
99
+ }
100
+ function normalizeMetadataPolicy(input) {
101
+ return {
102
+ type: normalizeDeclarationPolicy(input?.type),
103
+ field: normalizeDeclarationPolicy(input?.field),
104
+ method: normalizeDeclarationPolicy(input?.method)
105
+ };
106
+ }
107
+ function getDeclarationMetadataPolicy(policy, declarationKind) {
108
+ return policy[declarationKind];
109
+ }
110
+ function makeMetadataContext(surface, declarationKind, logicalName, buildContext) {
111
+ return {
112
+ surface,
113
+ declarationKind,
114
+ logicalName,
115
+ ...buildContext !== void 0 && { buildContext }
116
+ };
117
+ }
118
+
119
+ // src/metadata/resolve.ts
120
+ function toExplicitScalar(value) {
121
+ return value !== void 0 && value.trim() !== "" ? { value, source: "explicit" } : void 0;
122
+ }
123
+ function toExplicitResolvedMetadata(explicit) {
124
+ if (explicit === void 0) {
125
+ return void 0;
126
+ }
127
+ const apiName = toExplicitScalar(explicit.apiName);
128
+ const displayName = toExplicitScalar(explicit.displayName);
129
+ const apiNamePlural = toExplicitScalar(explicit.apiNamePlural);
130
+ const displayNamePlural = toExplicitScalar(explicit.displayNamePlural);
131
+ const metadata = {
132
+ ...apiName !== void 0 && { apiName },
133
+ ...displayName !== void 0 && { displayName },
134
+ ...apiNamePlural !== void 0 && { apiNamePlural },
135
+ ...displayNamePlural !== void 0 && { displayNamePlural }
136
+ };
137
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
138
+ }
139
+ function resolveScalar(current, policy, context, metadataLabel) {
140
+ if (current !== void 0) {
141
+ return current;
142
+ }
143
+ if (policy.mode === "require-explicit") {
144
+ throw new Error(
145
+ `Metadata policy requires explicit ${metadataLabel} for ${context.declarationKind} "${context.logicalName}" on the ${context.surface} surface.`
146
+ );
147
+ }
148
+ if (policy.mode !== "infer-if-missing") {
149
+ return void 0;
150
+ }
151
+ const inferredValue = policy.infer(context);
152
+ return inferredValue.trim() !== "" ? { value: inferredValue, source: "inferred" } : void 0;
153
+ }
154
+ function resolvePlural(current, singular, policy, context, metadataLabel) {
155
+ if (current !== void 0) {
156
+ return current;
157
+ }
158
+ if (policy.mode === "require-explicit") {
159
+ throw new Error(
160
+ `Metadata policy requires explicit ${metadataLabel} for ${context.declarationKind} "${context.logicalName}" on the ${context.surface} surface.`
161
+ );
162
+ }
163
+ if (singular === void 0 || policy.mode !== "infer-if-missing") {
164
+ return void 0;
165
+ }
166
+ const pluralValue = policy.inflect({ ...context, singular: singular.value });
167
+ return pluralValue.trim() !== "" ? { value: pluralValue, source: "inferred" } : void 0;
168
+ }
169
+ function resolveResolvedMetadata(current, policy, context) {
170
+ const apiName = resolveScalar(current?.apiName, policy.apiName, context, "apiName");
171
+ const displayName = resolveScalar(
172
+ current?.displayName,
173
+ policy.displayName,
174
+ context,
175
+ "displayName"
176
+ );
177
+ const apiNamePlural = resolvePlural(
178
+ current?.apiNamePlural,
179
+ apiName,
180
+ policy.apiName.pluralization,
181
+ context,
182
+ "apiNamePlural"
183
+ );
184
+ const displayNamePlural = resolvePlural(
185
+ current?.displayNamePlural,
186
+ displayName,
187
+ policy.displayName.pluralization,
188
+ context,
189
+ "displayNamePlural"
190
+ );
191
+ if (apiName === void 0 && displayName === void 0 && apiNamePlural === void 0 && displayNamePlural === void 0) {
192
+ return void 0;
193
+ }
194
+ return {
195
+ ...apiName !== void 0 && { apiName },
196
+ ...displayName !== void 0 && { displayName },
197
+ ...apiNamePlural !== void 0 && { apiNamePlural },
198
+ ...displayNamePlural !== void 0 && { displayNamePlural }
199
+ };
200
+ }
201
+ function pickResolvedMetadataValue(baseValue, overlayValue) {
202
+ if (overlayValue?.source === "explicit") {
203
+ return overlayValue;
204
+ }
205
+ if (baseValue?.source === "explicit") {
206
+ return baseValue;
207
+ }
208
+ return baseValue ?? overlayValue;
209
+ }
210
+ function resolveTypeNodeMetadata(type, options) {
211
+ switch (type.kind) {
212
+ case "array":
213
+ return {
214
+ ...type,
215
+ items: resolveTypeNodeMetadata(type.items, options)
216
+ };
217
+ case "object":
218
+ return {
219
+ ...type,
220
+ properties: type.properties.map((property) => resolveObjectPropertyMetadata(property, options))
221
+ };
222
+ case "record":
223
+ return {
224
+ ...type,
225
+ valueType: resolveTypeNodeMetadata(type.valueType, options)
226
+ };
227
+ case "union":
228
+ return {
229
+ ...type,
230
+ members: type.members.map((member) => resolveTypeNodeMetadata(member, options))
231
+ };
232
+ case "reference":
233
+ case "primitive":
234
+ case "enum":
235
+ case "dynamic":
236
+ case "custom":
237
+ return type;
238
+ default: {
239
+ const _exhaustive = type;
240
+ return _exhaustive;
241
+ }
242
+ }
243
+ }
244
+ function resolveObjectPropertyMetadata(property, options) {
245
+ const metadata = resolveResolvedMetadata(property.metadata, options.policy.field, {
246
+ surface: options.surface,
247
+ declarationKind: "field",
248
+ logicalName: property.name,
249
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
250
+ });
251
+ return {
252
+ ...property,
253
+ ...metadata !== void 0 && { metadata },
254
+ type: resolveTypeNodeMetadata(property.type, options)
255
+ };
256
+ }
257
+ function resolveFieldMetadataNode(field, options) {
258
+ const metadata = resolveResolvedMetadata(field.metadata, options.policy.field, {
259
+ surface: options.surface,
260
+ declarationKind: "field",
261
+ logicalName: field.name,
262
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
263
+ });
264
+ return {
265
+ ...field,
266
+ ...metadata !== void 0 && { metadata },
267
+ type: resolveTypeNodeMetadata(field.type, options)
268
+ };
269
+ }
270
+ function resolveFormElementMetadata(element, options) {
271
+ switch (element.kind) {
272
+ case "field":
273
+ return resolveFieldMetadataNode(element, options);
274
+ case "group":
275
+ return {
276
+ ...element,
277
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
278
+ };
279
+ case "conditional":
280
+ return {
281
+ ...element,
282
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
283
+ };
284
+ default: {
285
+ const _exhaustive = element;
286
+ return _exhaustive;
287
+ }
288
+ }
289
+ }
290
+ function resolveTypeDefinitionMetadata(typeDefinition, options) {
291
+ const metadata = resolveResolvedMetadata(typeDefinition.metadata, options.policy.type, {
292
+ surface: options.surface,
293
+ declarationKind: "type",
294
+ logicalName: typeDefinition.name,
295
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
296
+ });
297
+ return {
298
+ ...typeDefinition,
299
+ ...metadata !== void 0 && { metadata },
300
+ type: resolveTypeNodeMetadata(typeDefinition.type, options)
301
+ };
302
+ }
303
+ function resolveMetadata(explicit, policy, context) {
304
+ return resolveResolvedMetadata(toExplicitResolvedMetadata(explicit), policy, context);
305
+ }
306
+ function mergeResolvedMetadata(baseMetadata, overlayMetadata) {
307
+ const apiName = pickResolvedMetadataValue(baseMetadata?.apiName, overlayMetadata?.apiName);
308
+ const displayName = pickResolvedMetadataValue(
309
+ baseMetadata?.displayName,
310
+ overlayMetadata?.displayName
311
+ );
312
+ const apiNamePlural = pickResolvedMetadataValue(
313
+ baseMetadata?.apiNamePlural,
314
+ overlayMetadata?.apiNamePlural
315
+ );
316
+ const displayNamePlural = pickResolvedMetadataValue(
317
+ baseMetadata?.displayNamePlural,
318
+ overlayMetadata?.displayNamePlural
319
+ );
320
+ if (apiName === void 0 && displayName === void 0 && apiNamePlural === void 0 && displayNamePlural === void 0) {
321
+ return void 0;
322
+ }
323
+ return {
324
+ ...apiName !== void 0 && { apiName },
325
+ ...displayName !== void 0 && { displayName },
326
+ ...apiNamePlural !== void 0 && { apiNamePlural },
327
+ ...displayNamePlural !== void 0 && { displayNamePlural }
328
+ };
329
+ }
330
+ function getSerializedName(logicalName, metadata) {
331
+ return metadata?.apiName?.value ?? logicalName;
332
+ }
333
+ function getDisplayName(metadata) {
334
+ return metadata?.displayName?.value;
335
+ }
336
+ function resolveFormIRMetadata(ir, options) {
337
+ const rootLogicalName = options.rootLogicalName ?? ir.name ?? "FormSpec";
338
+ const metadata = resolveResolvedMetadata(ir.metadata, options.policy.type, {
339
+ surface: options.surface,
340
+ declarationKind: "type",
341
+ logicalName: rootLogicalName,
342
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
343
+ });
344
+ return {
345
+ ...ir,
346
+ ...metadata !== void 0 && { metadata },
347
+ elements: ir.elements.map((element) => resolveFormElementMetadata(element, options)),
348
+ typeRegistry: Object.fromEntries(
349
+ Object.entries(ir.typeRegistry).map(([name, definition]) => [
350
+ name,
351
+ resolveTypeDefinitionMetadata(definition, options)
352
+ ])
353
+ )
354
+ };
355
+ }
356
+
357
+ // src/canonicalize/chain-dsl-canonicalizer.ts
49
358
  var CHAIN_DSL_PROVENANCE = {
50
359
  surface: "chain-dsl",
51
360
  file: "",
@@ -61,57 +370,60 @@ function isConditional(el) {
61
370
  function isField(el) {
62
371
  return el._type === "field";
63
372
  }
64
- function canonicalizeChainDSL(form) {
373
+ function canonicalizeChainDSL(form, options) {
374
+ const metadataPolicy = normalizeMetadataPolicy(
375
+ options?.metadata ?? (0, import_internals._getFormSpecMetadataPolicy)(form)
376
+ );
65
377
  return {
66
378
  kind: "form-ir",
67
379
  irVersion: import_internals.IR_VERSION,
68
- elements: canonicalizeElements(form.elements),
380
+ elements: canonicalizeElements(form.elements, metadataPolicy),
69
381
  rootAnnotations: [],
70
382
  typeRegistry: {},
71
383
  provenance: CHAIN_DSL_PROVENANCE
72
384
  };
73
385
  }
74
- function canonicalizeElements(elements) {
75
- return elements.map(canonicalizeElement);
386
+ function canonicalizeElements(elements, metadataPolicy) {
387
+ return elements.map((element) => canonicalizeElement(element, metadataPolicy));
76
388
  }
77
- function canonicalizeElement(element) {
389
+ function canonicalizeElement(element, metadataPolicy) {
78
390
  if (isField(element)) {
79
- return canonicalizeField(element);
391
+ return canonicalizeField(element, metadataPolicy);
80
392
  }
81
393
  if (isGroup(element)) {
82
- return canonicalizeGroup(element);
394
+ return canonicalizeGroup(element, metadataPolicy);
83
395
  }
84
396
  if (isConditional(element)) {
85
- return canonicalizeConditional(element);
397
+ return canonicalizeConditional(element, metadataPolicy);
86
398
  }
87
399
  const _exhaustive = element;
88
400
  throw new Error(`Unknown element type: ${JSON.stringify(_exhaustive)}`);
89
401
  }
90
- function canonicalizeField(field) {
402
+ function canonicalizeField(field, metadataPolicy) {
91
403
  switch (field._field) {
92
404
  case "text":
93
- return canonicalizeTextField(field);
405
+ return canonicalizeTextField(field, metadataPolicy);
94
406
  case "number":
95
- return canonicalizeNumberField(field);
407
+ return canonicalizeNumberField(field, metadataPolicy);
96
408
  case "boolean":
97
- return canonicalizeBooleanField(field);
409
+ return canonicalizeBooleanField(field, metadataPolicy);
98
410
  case "enum":
99
- return canonicalizeStaticEnumField(field);
411
+ return canonicalizeStaticEnumField(field, metadataPolicy);
100
412
  case "dynamic_enum":
101
- return canonicalizeDynamicEnumField(field);
413
+ return canonicalizeDynamicEnumField(field, metadataPolicy);
102
414
  case "dynamic_schema":
103
- return canonicalizeDynamicSchemaField(field);
415
+ return canonicalizeDynamicSchemaField(field, metadataPolicy);
104
416
  case "array":
105
- return canonicalizeArrayField(field);
417
+ return canonicalizeArrayField(field, metadataPolicy);
106
418
  case "object":
107
- return canonicalizeObjectField(field);
419
+ return canonicalizeObjectField(field, metadataPolicy);
108
420
  default: {
109
421
  const _exhaustive = field;
110
422
  throw new Error(`Unknown field type: ${JSON.stringify(_exhaustive)}`);
111
423
  }
112
424
  }
113
425
  }
114
- function canonicalizeTextField(field) {
426
+ function canonicalizeTextField(field, metadataPolicy) {
115
427
  const type = { kind: "primitive", primitiveKind: "string" };
116
428
  const constraints = [];
117
429
  if (field.minLength !== void 0) {
@@ -143,13 +455,14 @@ function canonicalizeTextField(field) {
143
455
  }
144
456
  return buildFieldNode(
145
457
  field.name,
458
+ resolveFieldMetadata(field.name, field, metadataPolicy),
146
459
  type,
147
460
  field.required,
148
- buildAnnotations(field.label, field.placeholder),
461
+ buildAnnotations(getExplicitDisplayName(field), field.placeholder),
149
462
  constraints
150
463
  );
151
464
  }
152
- function canonicalizeNumberField(field) {
465
+ function canonicalizeNumberField(field, metadataPolicy) {
153
466
  const type = { kind: "primitive", primitiveKind: "number" };
154
467
  const constraints = [];
155
468
  if (field.min !== void 0) {
@@ -181,17 +494,24 @@ function canonicalizeNumberField(field) {
181
494
  }
182
495
  return buildFieldNode(
183
496
  field.name,
497
+ resolveFieldMetadata(field.name, field, metadataPolicy),
184
498
  type,
185
499
  field.required,
186
- buildAnnotations(field.label),
500
+ buildAnnotations(getExplicitDisplayName(field)),
187
501
  constraints
188
502
  );
189
503
  }
190
- function canonicalizeBooleanField(field) {
504
+ function canonicalizeBooleanField(field, metadataPolicy) {
191
505
  const type = { kind: "primitive", primitiveKind: "boolean" };
192
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
506
+ return buildFieldNode(
507
+ field.name,
508
+ resolveFieldMetadata(field.name, field, metadataPolicy),
509
+ type,
510
+ field.required,
511
+ buildAnnotations(getExplicitDisplayName(field))
512
+ );
193
513
  }
194
- function canonicalizeStaticEnumField(field) {
514
+ function canonicalizeStaticEnumField(field, metadataPolicy) {
195
515
  const members = field.options.map((opt) => {
196
516
  if (typeof opt === "string") {
197
517
  return { value: opt };
@@ -199,28 +519,46 @@ function canonicalizeStaticEnumField(field) {
199
519
  return { value: opt.id, displayName: opt.label };
200
520
  });
201
521
  const type = { kind: "enum", members };
202
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
522
+ return buildFieldNode(
523
+ field.name,
524
+ resolveFieldMetadata(field.name, field, metadataPolicy),
525
+ type,
526
+ field.required,
527
+ buildAnnotations(getExplicitDisplayName(field))
528
+ );
203
529
  }
204
- function canonicalizeDynamicEnumField(field) {
530
+ function canonicalizeDynamicEnumField(field, metadataPolicy) {
205
531
  const type = {
206
532
  kind: "dynamic",
207
533
  dynamicKind: "enum",
208
534
  sourceKey: field.source,
209
535
  parameterFields: field.params ? [...field.params] : []
210
536
  };
211
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
537
+ return buildFieldNode(
538
+ field.name,
539
+ resolveFieldMetadata(field.name, field, metadataPolicy),
540
+ type,
541
+ field.required,
542
+ buildAnnotations(getExplicitDisplayName(field))
543
+ );
212
544
  }
213
- function canonicalizeDynamicSchemaField(field) {
545
+ function canonicalizeDynamicSchemaField(field, metadataPolicy) {
214
546
  const type = {
215
547
  kind: "dynamic",
216
548
  dynamicKind: "schema",
217
549
  sourceKey: field.schemaSource,
218
550
  parameterFields: []
219
551
  };
220
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
552
+ return buildFieldNode(
553
+ field.name,
554
+ resolveFieldMetadata(field.name, field, metadataPolicy),
555
+ type,
556
+ field.required,
557
+ buildAnnotations(getExplicitDisplayName(field))
558
+ );
221
559
  }
222
- function canonicalizeArrayField(field) {
223
- const itemProperties = buildObjectProperties(field.items);
560
+ function canonicalizeArrayField(field, metadataPolicy) {
561
+ const itemProperties = buildObjectProperties(field.items, metadataPolicy);
224
562
  const itemsType = {
225
563
  kind: "object",
226
564
  properties: itemProperties,
@@ -248,37 +586,44 @@ function canonicalizeArrayField(field) {
248
586
  }
249
587
  return buildFieldNode(
250
588
  field.name,
589
+ resolveFieldMetadata(field.name, field, metadataPolicy),
251
590
  type,
252
591
  field.required,
253
- buildAnnotations(field.label),
592
+ buildAnnotations(getExplicitDisplayName(field)),
254
593
  constraints
255
594
  );
256
595
  }
257
- function canonicalizeObjectField(field) {
258
- const properties = buildObjectProperties(field.properties);
596
+ function canonicalizeObjectField(field, metadataPolicy) {
597
+ const properties = buildObjectProperties(field.properties, metadataPolicy);
259
598
  const type = {
260
599
  kind: "object",
261
600
  properties,
262
601
  additionalProperties: true
263
602
  };
264
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
603
+ return buildFieldNode(
604
+ field.name,
605
+ resolveFieldMetadata(field.name, field, metadataPolicy),
606
+ type,
607
+ field.required,
608
+ buildAnnotations(getExplicitDisplayName(field))
609
+ );
265
610
  }
266
- function canonicalizeGroup(g) {
611
+ function canonicalizeGroup(g, metadataPolicy) {
267
612
  return {
268
613
  kind: "group",
269
614
  label: g.label,
270
- elements: canonicalizeElements(g.elements),
615
+ elements: canonicalizeElements(g.elements, metadataPolicy),
271
616
  provenance: CHAIN_DSL_PROVENANCE
272
617
  };
273
618
  }
274
- function canonicalizeConditional(c) {
619
+ function canonicalizeConditional(c, metadataPolicy) {
275
620
  return {
276
621
  kind: "conditional",
277
622
  fieldName: c.field,
278
623
  // Conditional values from the chain DSL are JSON-serializable primitives
279
624
  // (strings, numbers, booleans) produced by the `is()` predicate helper.
280
625
  value: assertJsonValue(c.value),
281
- elements: canonicalizeElements(c.elements),
626
+ elements: canonicalizeElements(c.elements, metadataPolicy),
282
627
  provenance: CHAIN_DSL_PROVENANCE
283
628
  };
284
629
  }
@@ -298,10 +643,11 @@ function assertJsonValue(v) {
298
643
  }
299
644
  throw new TypeError(`Conditional value is not a valid JsonValue: ${typeof v}`);
300
645
  }
301
- function buildFieldNode(name, type, required, annotations, constraints = []) {
646
+ function buildFieldNode(name, metadata, type, required, annotations, constraints = []) {
302
647
  return {
303
648
  kind: "field",
304
649
  name,
650
+ ...metadata !== void 0 && { metadata },
305
651
  type,
306
652
  required: required === true,
307
653
  constraints,
@@ -331,13 +677,14 @@ function buildAnnotations(label, placeholder) {
331
677
  }
332
678
  return annotations;
333
679
  }
334
- function buildObjectProperties(elements, insideConditional = false) {
680
+ function buildObjectProperties(elements, metadataPolicy, insideConditional = false) {
335
681
  const properties = [];
336
682
  for (const el of elements) {
337
683
  if (isField(el)) {
338
- const fieldNode = canonicalizeField(el);
684
+ const fieldNode = canonicalizeField(el, metadataPolicy);
339
685
  properties.push({
340
686
  name: fieldNode.name,
687
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
341
688
  type: fieldNode.type,
342
689
  // Fields inside a conditional branch are always optional in the
343
690
  // data schema, regardless of their `required` flag — the condition
@@ -348,17 +695,34 @@ function buildObjectProperties(elements, insideConditional = false) {
348
695
  provenance: CHAIN_DSL_PROVENANCE
349
696
  });
350
697
  } else if (isGroup(el)) {
351
- properties.push(...buildObjectProperties(el.elements, insideConditional));
698
+ properties.push(...buildObjectProperties(el.elements, metadataPolicy, insideConditional));
352
699
  } else if (isConditional(el)) {
353
- properties.push(...buildObjectProperties(el.elements, true));
700
+ properties.push(...buildObjectProperties(el.elements, metadataPolicy, true));
354
701
  }
355
702
  }
356
703
  return properties;
357
704
  }
705
+ function getExplicitDisplayName(field) {
706
+ if (field.label !== void 0 && field.displayName !== void 0) {
707
+ throw new Error('Chain DSL fields cannot specify both "label" and "displayName".');
708
+ }
709
+ return field.displayName ?? field.label;
710
+ }
711
+ function resolveFieldMetadata(logicalName, field, metadataPolicy) {
712
+ const displayName = getExplicitDisplayName(field);
713
+ return resolveMetadata(
714
+ {
715
+ ...field.apiName !== void 0 && { apiName: field.apiName },
716
+ ...displayName !== void 0 && { displayName }
717
+ },
718
+ getDeclarationMetadataPolicy(metadataPolicy, "field"),
719
+ makeMetadataContext("chain-dsl", "field", logicalName)
720
+ );
721
+ }
358
722
 
359
723
  // src/canonicalize/tsdoc-canonicalizer.ts
360
724
  var import_internals2 = require("@formspec/core/internals");
361
- function canonicalizeTSDoc(analysis, source) {
725
+ function canonicalizeTSDoc(analysis, source, options) {
362
726
  const file = source?.file ?? "";
363
727
  const provenance = {
364
728
  surface: "tsdoc",
@@ -367,15 +731,21 @@ function canonicalizeTSDoc(analysis, source) {
367
731
  column: 0
368
732
  };
369
733
  const elements = assembleElements(analysis.fields, analysis.fieldLayouts, provenance);
370
- return {
734
+ const ir = {
371
735
  kind: "form-ir",
736
+ name: analysis.name,
372
737
  irVersion: import_internals2.IR_VERSION,
373
738
  elements,
739
+ ...analysis.metadata !== void 0 && { metadata: analysis.metadata },
374
740
  typeRegistry: analysis.typeRegistry,
375
741
  ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { rootAnnotations: analysis.annotations },
376
742
  ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
377
743
  provenance
378
744
  };
745
+ return resolveFormIRMetadata(ir, {
746
+ policy: normalizeMetadataPolicy(options?.metadata),
747
+ surface: "tsdoc"
748
+ });
379
749
  }
380
750
  function assembleElements(fields, layouts, provenance) {
381
751
  const elements = [];
@@ -432,6 +802,120 @@ function wrapInConditional(field, layout, provenance) {
432
802
  return conditional;
433
803
  }
434
804
 
805
+ // src/metadata/collision-guards.ts
806
+ function assertUniqueSerializedNames(entries, scope) {
807
+ const seen = /* @__PURE__ */ new Map();
808
+ for (const entry of entries) {
809
+ const previous = seen.get(entry.serializedName);
810
+ if (previous !== void 0) {
811
+ if (previous.logicalName === entry.logicalName && previous.category === entry.category) {
812
+ continue;
813
+ }
814
+ throw new Error(
815
+ `Serialized name collision in ${scope}: ${previous.category} "${previous.logicalName}" and ${entry.category} "${entry.logicalName}" both resolve to "${entry.serializedName}".`
816
+ );
817
+ }
818
+ seen.set(entry.serializedName, entry);
819
+ }
820
+ }
821
+ function collectFlattenedFields(elements) {
822
+ const fields = [];
823
+ for (const element of elements) {
824
+ switch (element.kind) {
825
+ case "field":
826
+ fields.push(element);
827
+ break;
828
+ case "group":
829
+ case "conditional":
830
+ fields.push(...collectFlattenedFields(element.elements));
831
+ break;
832
+ default: {
833
+ const exhaustive = element;
834
+ void exhaustive;
835
+ }
836
+ }
837
+ }
838
+ return fields;
839
+ }
840
+ function validateObjectProperties(properties, scope) {
841
+ assertUniqueSerializedNames(
842
+ properties.map((property) => ({
843
+ logicalName: property.name,
844
+ serializedName: getSerializedName(property.name, property.metadata),
845
+ category: "object property"
846
+ })),
847
+ scope
848
+ );
849
+ for (const property of properties) {
850
+ validateTypeNode(
851
+ property.type,
852
+ `${scope}.${getSerializedName(property.name, property.metadata)}`
853
+ );
854
+ }
855
+ }
856
+ function validateTypeNode(type, scope) {
857
+ switch (type.kind) {
858
+ case "array":
859
+ validateTypeNode(type.items, `${scope}[]`);
860
+ break;
861
+ case "object":
862
+ validateObjectProperties(type.properties, scope);
863
+ break;
864
+ case "record":
865
+ validateTypeNode(type.valueType, `${scope}.*`);
866
+ break;
867
+ case "union":
868
+ type.members.forEach((member, index) => {
869
+ validateTypeNode(member, `${scope}|${String(index)}`);
870
+ });
871
+ break;
872
+ case "reference":
873
+ case "primitive":
874
+ case "enum":
875
+ case "dynamic":
876
+ case "custom":
877
+ break;
878
+ default: {
879
+ const exhaustive = type;
880
+ void exhaustive;
881
+ }
882
+ }
883
+ }
884
+ function validateTypeDefinitions(typeRegistry) {
885
+ const definitions = Object.values(typeRegistry);
886
+ assertUniqueSerializedNames(
887
+ definitions.map((definition) => ({
888
+ logicalName: definition.name,
889
+ serializedName: getSerializedName(definition.name, definition.metadata),
890
+ category: "type definition"
891
+ })),
892
+ "$defs"
893
+ );
894
+ for (const definition of definitions) {
895
+ validateTypeDefinition(definition);
896
+ }
897
+ }
898
+ function validateTypeDefinition(definition) {
899
+ validateTypeNode(
900
+ definition.type,
901
+ `type "${getSerializedName(definition.name, definition.metadata)}"`
902
+ );
903
+ }
904
+ function assertNoSerializedNameCollisions(ir) {
905
+ assertUniqueSerializedNames(
906
+ collectFlattenedFields(ir.elements).map((field) => ({
907
+ logicalName: field.name,
908
+ serializedName: getSerializedName(field.name, field.metadata),
909
+ category: "field"
910
+ })),
911
+ "form root"
912
+ );
913
+ for (const field of collectFlattenedFields(ir.elements)) {
914
+ validateTypeNode(field.type, `field "${getSerializedName(field.name, field.metadata)}"`);
915
+ }
916
+ validateTypeDefinitions(ir.typeRegistry);
917
+ }
918
+
435
919
  // src/json-schema/ir-generator.ts
436
920
  function makeContext(options) {
437
921
  const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
@@ -442,19 +926,33 @@ function makeContext(options) {
442
926
  }
443
927
  return {
444
928
  defs: {},
929
+ typeNameMap: {},
930
+ typeRegistry: {},
445
931
  extensionRegistry: options?.extensionRegistry,
446
932
  vendorPrefix
447
933
  };
448
934
  }
449
935
  function generateJsonSchemaFromIR(ir, options) {
450
- const ctx = makeContext(options);
936
+ assertNoSerializedNameCollisions(ir);
937
+ const ctx = {
938
+ ...makeContext(options),
939
+ typeRegistry: ir.typeRegistry,
940
+ typeNameMap: Object.fromEntries(
941
+ Object.entries(ir.typeRegistry).map(([name, typeDef]) => [
942
+ name,
943
+ getSerializedName(name, typeDef.metadata)
944
+ ])
945
+ )
946
+ };
451
947
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
452
- ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
948
+ const schemaName = ctx.typeNameMap[name] ?? name;
949
+ ctx.defs[schemaName] = generateTypeNode(typeDef.type, ctx);
950
+ applyResolvedMetadata(ctx.defs[schemaName], typeDef.metadata);
453
951
  if (typeDef.constraints && typeDef.constraints.length > 0) {
454
- applyConstraints(ctx.defs[name], typeDef.constraints, ctx);
952
+ applyConstraints(ctx.defs[schemaName], typeDef.constraints, ctx);
455
953
  }
456
954
  if (typeDef.annotations && typeDef.annotations.length > 0) {
457
- applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
955
+ applyAnnotations(ctx.defs[schemaName], typeDef.annotations, ctx);
458
956
  }
459
957
  }
460
958
  const properties = {};
@@ -467,6 +965,7 @@ function generateJsonSchemaFromIR(ir, options) {
467
965
  properties,
468
966
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
469
967
  };
968
+ applyResolvedMetadata(result, ir.metadata);
470
969
  if (ir.annotations && ir.annotations.length > 0) {
471
970
  applyAnnotations(result, ir.annotations, ctx);
472
971
  }
@@ -479,9 +978,9 @@ function collectFields(elements, properties, required, ctx) {
479
978
  for (const element of elements) {
480
979
  switch (element.kind) {
481
980
  case "field":
482
- properties[element.name] = generateFieldSchema(element, ctx);
981
+ properties[getSerializedName(element.name, element.metadata)] = generateFieldSchema(element, ctx);
483
982
  if (element.required) {
484
- required.push(element.name);
983
+ required.push(getSerializedName(element.name, element.metadata));
485
984
  }
486
985
  break;
487
986
  case "group":
@@ -525,6 +1024,7 @@ function generateFieldSchema(field, ctx) {
525
1024
  rootAnnotations.push(annotation);
526
1025
  }
527
1026
  }
1027
+ applyResolvedMetadata(schema, field.metadata);
528
1028
  applyAnnotations(schema, rootAnnotations, ctx);
529
1029
  if (itemStringSchema !== void 0) {
530
1030
  applyAnnotations(itemStringSchema, itemAnnotations, ctx);
@@ -532,7 +1032,7 @@ function generateFieldSchema(field, ctx) {
532
1032
  if (pathConstraints.length === 0) {
533
1033
  return schema;
534
1034
  }
535
- return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1035
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx, field.type);
536
1036
  }
537
1037
  function isStringItemConstraint(constraint) {
538
1038
  switch (constraint.constraintKind) {
@@ -544,9 +1044,11 @@ function isStringItemConstraint(constraint) {
544
1044
  return false;
545
1045
  }
546
1046
  }
547
- function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1047
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx, typeNode) {
548
1048
  if (schema.type === "array" && schema.items) {
549
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
1049
+ const referencedType = typeNode?.kind === "reference" ? resolveReferencedType(typeNode, ctx) : void 0;
1050
+ const nestedType = typeNode?.kind === "array" ? typeNode.items : referencedType?.kind === "array" ? referencedType.items : void 0;
1051
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx, nestedType);
550
1052
  return schema;
551
1053
  }
552
1054
  const byTarget = /* @__PURE__ */ new Map();
@@ -561,7 +1063,7 @@ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
561
1063
  for (const [target, constraints] of byTarget) {
562
1064
  const subSchema = {};
563
1065
  applyConstraints(subSchema, constraints, ctx);
564
- propertyOverrides[target] = subSchema;
1066
+ propertyOverrides[resolveSerializedPropertyName(target, typeNode, ctx)] = subSchema;
565
1067
  }
566
1068
  if (schema.$ref) {
567
1069
  const { $ref, ...rest } = schema;
@@ -609,7 +1111,7 @@ function generateTypeNode(type, ctx) {
609
1111
  case "union":
610
1112
  return generateUnionType(type, ctx);
611
1113
  case "reference":
612
- return generateReferenceType(type);
1114
+ return generateReferenceType(type, ctx);
613
1115
  case "dynamic":
614
1116
  return generateDynamicType(type);
615
1117
  case "custom":
@@ -650,9 +1152,10 @@ function generateObjectType(type, ctx) {
650
1152
  const properties = {};
651
1153
  const required = [];
652
1154
  for (const prop of type.properties) {
653
- properties[prop.name] = generatePropertySchema(prop, ctx);
1155
+ const propertyName = getSerializedName(prop.name, prop.metadata);
1156
+ properties[propertyName] = generatePropertySchema(prop, ctx);
654
1157
  if (!prop.optional) {
655
- required.push(prop.name);
1158
+ required.push(propertyName);
656
1159
  }
657
1160
  }
658
1161
  const schema = { type: "object", properties };
@@ -673,6 +1176,7 @@ function generateRecordType(type, ctx) {
673
1176
  function generatePropertySchema(prop, ctx) {
674
1177
  const schema = generateTypeNode(prop.type, ctx);
675
1178
  applyConstraints(schema, prop.constraints, ctx);
1179
+ applyResolvedMetadata(schema, prop.metadata);
676
1180
  applyAnnotations(schema, prop.annotations, ctx);
677
1181
  return schema;
678
1182
  }
@@ -701,8 +1205,28 @@ function isNullableUnion(type) {
701
1205
  ).length;
702
1206
  return nullCount === 1;
703
1207
  }
704
- function generateReferenceType(type) {
705
- return { $ref: `#/$defs/${type.name}` };
1208
+ function generateReferenceType(type, ctx) {
1209
+ return { $ref: `#/$defs/${ctx.typeNameMap[type.name] ?? type.name}` };
1210
+ }
1211
+ function applyResolvedMetadata(schema, metadata) {
1212
+ const displayName = getDisplayName(metadata);
1213
+ if (displayName !== void 0) {
1214
+ schema.title = displayName;
1215
+ }
1216
+ }
1217
+ function resolveReferencedType(type, ctx) {
1218
+ return ctx.typeRegistry[type.name]?.type;
1219
+ }
1220
+ function resolveSerializedPropertyName(logicalName, typeNode, ctx) {
1221
+ if (typeNode?.kind === "object") {
1222
+ const property = typeNode.properties.find((candidate) => candidate.name === logicalName);
1223
+ return property === void 0 ? logicalName : getSerializedName(property.name, property.metadata);
1224
+ }
1225
+ if (typeNode?.kind === "reference") {
1226
+ const referencedType = resolveReferencedType(typeNode, ctx);
1227
+ return referencedType === void 0 ? logicalName : resolveSerializedPropertyName(logicalName, referencedType, ctx);
1228
+ }
1229
+ return logicalName;
706
1230
  }
707
1231
  function generateDynamicType(type) {
708
1232
  if (type.dynamicKind === "enum") {
@@ -782,7 +1306,7 @@ function applyAnnotations(schema, annotations, ctx) {
782
1306
  for (const annotation of annotations) {
783
1307
  switch (annotation.annotationKind) {
784
1308
  case "displayName":
785
- schema.title = annotation.value;
1309
+ schema.title ??= annotation.value;
786
1310
  break;
787
1311
  case "description":
788
1312
  schema.description = annotation.value;
@@ -869,7 +1393,10 @@ function assignVendorPrefixedExtensionKeywords(schema, extensionSchema, vendorPr
869
1393
 
870
1394
  // src/json-schema/generator.ts
871
1395
  function generateJsonSchema(form, options) {
872
- const ir = canonicalizeChainDSL(form);
1396
+ const ir = canonicalizeChainDSL(
1397
+ form,
1398
+ options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
1399
+ );
873
1400
  const internalOptions = options?.vendorPrefix === void 0 ? void 0 : { vendorPrefix: options.vendorPrefix };
874
1401
  return generateJsonSchemaFromIR(ir, internalOptions);
875
1402
  }
@@ -1039,13 +1566,21 @@ function combineRules(parentRule, childRule) {
1039
1566
  }
1040
1567
  };
1041
1568
  }
1042
- function fieldNodeToControl(field, parentRule) {
1043
- const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
1569
+ function getFieldDisplayName(field) {
1570
+ const resolvedDisplayName = getDisplayName(field.metadata);
1571
+ if (resolvedDisplayName !== void 0) {
1572
+ return resolvedDisplayName;
1573
+ }
1574
+ return field.annotations.find((annotation) => annotation.annotationKind === "displayName")?.value;
1575
+ }
1576
+ function fieldNodeToControl(field, fieldNameMap, parentRule) {
1044
1577
  const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1578
+ const serializedName = fieldNameMap.get(field.name) ?? getSerializedName(field.name, field.metadata);
1579
+ const displayName = getFieldDisplayName(field);
1045
1580
  const control = {
1046
1581
  type: "Control",
1047
- scope: fieldToScope(field.name),
1048
- ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
1582
+ scope: fieldToScope(serializedName),
1583
+ ...displayName !== void 0 && { label: displayName },
1049
1584
  ...placeholderAnnotation !== void 0 && {
1050
1585
  options: { placeholder: placeholderAnnotation.value }
1051
1586
  },
@@ -1053,30 +1588,30 @@ function fieldNodeToControl(field, parentRule) {
1053
1588
  };
1054
1589
  return control;
1055
1590
  }
1056
- function groupNodeToLayout(group, parentRule) {
1591
+ function groupNodeToLayout(group, fieldNameMap, parentRule) {
1057
1592
  return {
1058
1593
  type: "Group",
1059
1594
  label: group.label,
1060
- elements: irElementsToUiSchema(group.elements, parentRule),
1595
+ elements: irElementsToUiSchema(group.elements, fieldNameMap, parentRule),
1061
1596
  ...parentRule !== void 0 && { rule: parentRule }
1062
1597
  };
1063
1598
  }
1064
- function irElementsToUiSchema(elements, parentRule) {
1599
+ function irElementsToUiSchema(elements, fieldNameMap, parentRule) {
1065
1600
  const result = [];
1066
1601
  for (const element of elements) {
1067
1602
  switch (element.kind) {
1068
1603
  case "field": {
1069
- result.push(fieldNodeToControl(element, parentRule));
1604
+ result.push(fieldNodeToControl(element, fieldNameMap, parentRule));
1070
1605
  break;
1071
1606
  }
1072
1607
  case "group": {
1073
- result.push(groupNodeToLayout(element, parentRule));
1608
+ result.push(groupNodeToLayout(element, fieldNameMap, parentRule));
1074
1609
  break;
1075
1610
  }
1076
1611
  case "conditional": {
1077
- const newRule = createShowRule(element.fieldName, element.value);
1612
+ const newRule = createShowRule(fieldNameMap.get(element.fieldName) ?? element.fieldName, element.value);
1078
1613
  const combinedRule = parentRule !== void 0 ? combineRules(parentRule, newRule) : newRule;
1079
- const childElements = irElementsToUiSchema(element.elements, combinedRule);
1614
+ const childElements = irElementsToUiSchema(element.elements, fieldNameMap, combinedRule);
1080
1615
  result.push(...childElements);
1081
1616
  break;
1082
1617
  }
@@ -1090,16 +1625,42 @@ function irElementsToUiSchema(elements, parentRule) {
1090
1625
  return result;
1091
1626
  }
1092
1627
  function generateUiSchemaFromIR(ir) {
1628
+ assertNoSerializedNameCollisions(ir);
1629
+ const fieldNameMap = collectFieldNameMap(ir.elements);
1093
1630
  const result = {
1094
1631
  type: "VerticalLayout",
1095
- elements: irElementsToUiSchema(ir.elements)
1632
+ elements: irElementsToUiSchema(ir.elements, fieldNameMap)
1096
1633
  };
1097
1634
  return parseOrThrow(uiSchema, result, "UI Schema");
1098
1635
  }
1636
+ function collectFieldNameMap(elements) {
1637
+ const map = /* @__PURE__ */ new Map();
1638
+ for (const element of elements) {
1639
+ switch (element.kind) {
1640
+ case "field":
1641
+ map.set(element.name, getSerializedName(element.name, element.metadata));
1642
+ break;
1643
+ case "group":
1644
+ case "conditional":
1645
+ for (const [key, value] of collectFieldNameMap(element.elements)) {
1646
+ map.set(key, value);
1647
+ }
1648
+ break;
1649
+ default: {
1650
+ const _exhaustive = element;
1651
+ void _exhaustive;
1652
+ }
1653
+ }
1654
+ }
1655
+ return map;
1656
+ }
1099
1657
 
1100
1658
  // src/ui-schema/generator.ts
1101
- function generateUiSchema(form) {
1102
- const ir = canonicalizeChainDSL(form);
1659
+ function generateUiSchema(form, options) {
1660
+ const ir = canonicalizeChainDSL(
1661
+ form,
1662
+ options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
1663
+ );
1103
1664
  return generateUiSchemaFromIR(ir);
1104
1665
  }
1105
1666
 
@@ -1258,6 +1819,7 @@ var path = __toESM(require("path"), 1);
1258
1819
 
1259
1820
  // src/analyzer/class-analyzer.ts
1260
1821
  var ts3 = __toESM(require("typescript"), 1);
1822
+ var import_internal2 = require("@formspec/analysis/internal");
1261
1823
 
1262
1824
  // src/analyzer/jsdoc-constraints.ts
1263
1825
  var ts2 = __toESM(require("typescript"), 1);
@@ -2135,7 +2697,76 @@ function makeParseOptions(extensionRegistry, fieldType, checker, subjectType, ho
2135
2697
  ...hostType !== void 0 && { hostType }
2136
2698
  };
2137
2699
  }
2138
- function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
2700
+ function makeExplicitScalarMetadata(value) {
2701
+ return value === void 0 || value === "" ? void 0 : { value, source: "explicit" };
2702
+ }
2703
+ function extractExplicitMetadata(node) {
2704
+ let apiName;
2705
+ let displayName;
2706
+ let apiNamePlural;
2707
+ let displayNamePlural;
2708
+ for (const tag of getLeadingParsedTags(node)) {
2709
+ const value = tag.argumentText.trim();
2710
+ if (value === "") {
2711
+ continue;
2712
+ }
2713
+ if (tag.normalizedTagName === "apiName") {
2714
+ if (tag.target === null) {
2715
+ apiName ??= value;
2716
+ } else if (tag.target.kind === "variant") {
2717
+ if (tag.target.rawText === "singular") {
2718
+ apiName ??= value;
2719
+ } else if (tag.target.rawText === "plural") {
2720
+ apiNamePlural ??= value;
2721
+ }
2722
+ }
2723
+ continue;
2724
+ }
2725
+ if (tag.normalizedTagName === "displayName") {
2726
+ if (tag.target === null) {
2727
+ displayName ??= value;
2728
+ } else if (tag.target.kind === "variant") {
2729
+ if (tag.target.rawText === "singular") {
2730
+ displayName ??= value;
2731
+ } else if (tag.target.rawText === "plural") {
2732
+ displayNamePlural ??= value;
2733
+ }
2734
+ }
2735
+ }
2736
+ }
2737
+ const resolvedApiName = makeExplicitScalarMetadata(apiName);
2738
+ const resolvedDisplayName = makeExplicitScalarMetadata(displayName);
2739
+ const resolvedApiNamePlural = makeExplicitScalarMetadata(apiNamePlural);
2740
+ const resolvedDisplayNamePlural = makeExplicitScalarMetadata(displayNamePlural);
2741
+ const metadata = {
2742
+ ...resolvedApiName !== void 0 && { apiName: resolvedApiName },
2743
+ ...resolvedDisplayName !== void 0 && { displayName: resolvedDisplayName },
2744
+ ...resolvedApiNamePlural !== void 0 && { apiNamePlural: resolvedApiNamePlural },
2745
+ ...resolvedDisplayNamePlural !== void 0 && {
2746
+ displayNamePlural: resolvedDisplayNamePlural
2747
+ }
2748
+ };
2749
+ return Object.keys(metadata).length === 0 ? void 0 : metadata;
2750
+ }
2751
+ function resolveNodeMetadata(metadataPolicy, declarationKind, logicalName, node, buildContext) {
2752
+ const explicit = extractExplicitMetadata(node);
2753
+ return resolveMetadata(
2754
+ {
2755
+ ...explicit?.apiName !== void 0 && { apiName: explicit.apiName.value },
2756
+ ...explicit?.displayName !== void 0 && { displayName: explicit.displayName.value },
2757
+ ...explicit?.apiNamePlural !== void 0 && {
2758
+ apiNamePlural: explicit.apiNamePlural.value
2759
+ },
2760
+ ...explicit?.displayNamePlural !== void 0 && {
2761
+ displayNamePlural: explicit.displayNamePlural.value
2762
+ }
2763
+ },
2764
+ getDeclarationMetadataPolicy(metadataPolicy, declarationKind),
2765
+ makeMetadataContext("tsdoc", declarationKind, logicalName, buildContext)
2766
+ );
2767
+ }
2768
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry, metadataPolicy) {
2769
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
2139
2770
  const name = classDecl.name?.text ?? "AnonymousClass";
2140
2771
  const fields = [];
2141
2772
  const fieldLayouts = [];
@@ -2162,6 +2793,7 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
2162
2793
  visiting,
2163
2794
  diagnostics,
2164
2795
  classType,
2796
+ normalizedMetadataPolicy,
2165
2797
  extensionRegistry
2166
2798
  );
2167
2799
  if (fieldNode) {
@@ -2180,9 +2812,25 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
2180
2812
  }
2181
2813
  }
2182
2814
  }
2815
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2816
+ fields,
2817
+ classDecl,
2818
+ classType,
2819
+ checker,
2820
+ file,
2821
+ diagnostics,
2822
+ normalizedMetadataPolicy
2823
+ );
2824
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, classDecl, {
2825
+ checker,
2826
+ declaration: classDecl,
2827
+ subjectType: classType,
2828
+ hostType: classType
2829
+ });
2183
2830
  return {
2184
2831
  name,
2185
- fields,
2832
+ ...metadata !== void 0 && { metadata },
2833
+ fields: specializedFields,
2186
2834
  fieldLayouts,
2187
2835
  typeRegistry,
2188
2836
  ...annotations.length > 0 && { annotations },
@@ -2191,7 +2839,8 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
2191
2839
  staticMethods
2192
2840
  };
2193
2841
  }
2194
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
2842
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry, metadataPolicy) {
2843
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
2195
2844
  const name = interfaceDecl.name.text;
2196
2845
  const fields = [];
2197
2846
  const typeRegistry = {};
@@ -2215,6 +2864,7 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
2215
2864
  visiting,
2216
2865
  diagnostics,
2217
2866
  interfaceType,
2867
+ normalizedMetadataPolicy,
2218
2868
  extensionRegistry
2219
2869
  );
2220
2870
  if (fieldNode) {
@@ -2222,10 +2872,26 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
2222
2872
  }
2223
2873
  }
2224
2874
  }
2225
- const fieldLayouts = fields.map(() => ({}));
2875
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2876
+ fields,
2877
+ interfaceDecl,
2878
+ interfaceType,
2879
+ checker,
2880
+ file,
2881
+ diagnostics,
2882
+ normalizedMetadataPolicy
2883
+ );
2884
+ const fieldLayouts = specializedFields.map(() => ({}));
2885
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, interfaceDecl, {
2886
+ checker,
2887
+ declaration: interfaceDecl,
2888
+ subjectType: interfaceType,
2889
+ hostType: interfaceType
2890
+ });
2226
2891
  return {
2227
2892
  name,
2228
- fields,
2893
+ ...metadata !== void 0 && { metadata },
2894
+ fields: specializedFields,
2229
2895
  fieldLayouts,
2230
2896
  typeRegistry,
2231
2897
  ...annotations.length > 0 && { annotations },
@@ -2234,7 +2900,7 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
2234
2900
  staticMethods: []
2235
2901
  };
2236
2902
  }
2237
- function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
2903
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry, metadataPolicy) {
2238
2904
  if (!ts3.isTypeLiteralNode(typeAlias.type)) {
2239
2905
  const sourceFile = typeAlias.getSourceFile();
2240
2906
  const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
@@ -2244,6 +2910,8 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2244
2910
  error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
2245
2911
  };
2246
2912
  }
2913
+ const typeLiteral = typeAlias.type;
2914
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
2247
2915
  const name = typeAlias.name.text;
2248
2916
  const fields = [];
2249
2917
  const typeRegistry = {};
@@ -2257,7 +2925,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2257
2925
  const annotations = [...typeAliasDoc.annotations];
2258
2926
  diagnostics.push(...typeAliasDoc.diagnostics);
2259
2927
  const visiting = /* @__PURE__ */ new Set();
2260
- for (const member of typeAlias.type.members) {
2928
+ for (const member of typeLiteral.members) {
2261
2929
  if (ts3.isPropertySignature(member)) {
2262
2930
  const fieldNode = analyzeInterfacePropertyToIR(
2263
2931
  member,
@@ -2267,6 +2935,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2267
2935
  visiting,
2268
2936
  diagnostics,
2269
2937
  aliasType,
2938
+ normalizedMetadataPolicy,
2270
2939
  extensionRegistry
2271
2940
  );
2272
2941
  if (fieldNode) {
@@ -2274,12 +2943,28 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2274
2943
  }
2275
2944
  }
2276
2945
  }
2946
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2947
+ fields,
2948
+ typeAlias,
2949
+ aliasType,
2950
+ checker,
2951
+ file,
2952
+ diagnostics,
2953
+ normalizedMetadataPolicy
2954
+ );
2955
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, typeAlias, {
2956
+ checker,
2957
+ declaration: typeAlias,
2958
+ subjectType: aliasType,
2959
+ hostType: aliasType
2960
+ });
2277
2961
  return {
2278
2962
  ok: true,
2279
2963
  analysis: {
2280
2964
  name,
2281
- fields,
2282
- fieldLayouts: fields.map(() => ({})),
2965
+ ...metadata !== void 0 && { metadata },
2966
+ fields: specializedFields,
2967
+ fieldLayouts: specializedFields.map(() => ({})),
2283
2968
  typeRegistry,
2284
2969
  ...annotations.length > 0 && { annotations },
2285
2970
  ...diagnostics.length > 0 && { diagnostics },
@@ -2288,7 +2973,444 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2288
2973
  }
2289
2974
  };
2290
2975
  }
2291
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2976
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
2977
+ return {
2978
+ code,
2979
+ message,
2980
+ severity: "error",
2981
+ primaryLocation,
2982
+ relatedLocations
2983
+ };
2984
+ }
2985
+ function getLeadingParsedTags(node) {
2986
+ const sourceFile = node.getSourceFile();
2987
+ const sourceText = sourceFile.getFullText();
2988
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
2989
+ if (commentRanges === void 0) {
2990
+ return [];
2991
+ }
2992
+ const parsedTags = [];
2993
+ for (const range of commentRanges) {
2994
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
2995
+ continue;
2996
+ }
2997
+ const commentText = sourceText.slice(range.pos, range.end);
2998
+ if (!commentText.startsWith("/**")) {
2999
+ continue;
3000
+ }
3001
+ parsedTags.push(...(0, import_internal2.parseCommentBlock)(commentText, { offset: range.pos }).tags);
3002
+ }
3003
+ return parsedTags;
3004
+ }
3005
+ function resolveDiscriminatorProperty(node, checker, fieldName) {
3006
+ const subjectType = checker.getTypeAtLocation(node);
3007
+ const propertySymbol = subjectType.getProperty(fieldName);
3008
+ if (propertySymbol === void 0) {
3009
+ return null;
3010
+ }
3011
+ const declaration = propertySymbol.valueDeclaration ?? propertySymbol.declarations?.find(
3012
+ (candidate) => ts3.isPropertyDeclaration(candidate) || ts3.isPropertySignature(candidate)
3013
+ ) ?? propertySymbol.declarations?.[0];
3014
+ return {
3015
+ declaration,
3016
+ type: checker.getTypeOfSymbolAtLocation(propertySymbol, declaration ?? node),
3017
+ optional: !!(propertySymbol.flags & ts3.SymbolFlags.Optional) || declaration !== void 0 && "questionToken" in declaration && declaration.questionToken !== void 0
3018
+ };
3019
+ }
3020
+ function isLocalTypeParameterName(node, typeParameterName) {
3021
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
3022
+ }
3023
+ function isNullishSemanticType(type) {
3024
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
3025
+ return true;
3026
+ }
3027
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
3028
+ }
3029
+ function isStringLikeSemanticType(type) {
3030
+ if (type.flags & ts3.TypeFlags.StringLike) {
3031
+ return true;
3032
+ }
3033
+ if (type.isUnion()) {
3034
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
3035
+ }
3036
+ return false;
3037
+ }
3038
+ function extractDiscriminatorDirective(node, file, diagnostics) {
3039
+ const discriminatorTags = getLeadingParsedTags(node).filter(
3040
+ (tag) => tag.normalizedTagName === "discriminator"
3041
+ );
3042
+ if (discriminatorTags.length === 0) {
3043
+ return null;
3044
+ }
3045
+ const [firstTag, ...duplicateTags] = discriminatorTags;
3046
+ for (const _duplicateTag of duplicateTags) {
3047
+ diagnostics.push(
3048
+ makeAnalysisDiagnostic(
3049
+ "DUPLICATE_TAG",
3050
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
3051
+ provenanceForNode(node, file)
3052
+ )
3053
+ );
3054
+ }
3055
+ if (firstTag === void 0) {
3056
+ return null;
3057
+ }
3058
+ const firstTarget = firstTag.target;
3059
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
3060
+ diagnostics.push(
3061
+ makeAnalysisDiagnostic(
3062
+ "INVALID_TAG_ARGUMENT",
3063
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
3064
+ provenanceForNode(node, file)
3065
+ )
3066
+ );
3067
+ return null;
3068
+ }
3069
+ if (firstTarget.path.segments.length !== 1) {
3070
+ diagnostics.push(
3071
+ makeAnalysisDiagnostic(
3072
+ "INVALID_TAG_ARGUMENT",
3073
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
3074
+ provenanceForNode(node, file)
3075
+ )
3076
+ );
3077
+ return null;
3078
+ }
3079
+ const typeParameterName = firstTag.argumentText.trim();
3080
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
3081
+ diagnostics.push(
3082
+ makeAnalysisDiagnostic(
3083
+ "INVALID_TAG_ARGUMENT",
3084
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
3085
+ provenanceForNode(node, file)
3086
+ )
3087
+ );
3088
+ return null;
3089
+ }
3090
+ return {
3091
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
3092
+ typeParameterName,
3093
+ provenance: provenanceForNode(node, file)
3094
+ };
3095
+ }
3096
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
3097
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
3098
+ if (directive === null) {
3099
+ return null;
3100
+ }
3101
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
3102
+ diagnostics.push(
3103
+ makeAnalysisDiagnostic(
3104
+ "INVALID_TAG_ARGUMENT",
3105
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
3106
+ directive.provenance
3107
+ )
3108
+ );
3109
+ return null;
3110
+ }
3111
+ const property = resolveDiscriminatorProperty(node, checker, directive.fieldName);
3112
+ if (property === null) {
3113
+ diagnostics.push(
3114
+ makeAnalysisDiagnostic(
3115
+ "UNKNOWN_PATH_TARGET",
3116
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
3117
+ directive.provenance
3118
+ )
3119
+ );
3120
+ return null;
3121
+ }
3122
+ if (property.optional) {
3123
+ diagnostics.push(
3124
+ makeAnalysisDiagnostic(
3125
+ "TYPE_MISMATCH",
3126
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
3127
+ directive.provenance,
3128
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
3129
+ )
3130
+ );
3131
+ return null;
3132
+ }
3133
+ if (isNullishSemanticType(property.type)) {
3134
+ diagnostics.push(
3135
+ makeAnalysisDiagnostic(
3136
+ "TYPE_MISMATCH",
3137
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
3138
+ directive.provenance,
3139
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
3140
+ )
3141
+ );
3142
+ return null;
3143
+ }
3144
+ if (!isStringLikeSemanticType(property.type)) {
3145
+ diagnostics.push(
3146
+ makeAnalysisDiagnostic(
3147
+ "TYPE_MISMATCH",
3148
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
3149
+ directive.provenance,
3150
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
3151
+ )
3152
+ );
3153
+ return null;
3154
+ }
3155
+ return directive;
3156
+ }
3157
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
3158
+ const typeParameterIndex = node.typeParameters?.findIndex(
3159
+ (typeParameter) => typeParameter.name.text === typeParameterName
3160
+ ) ?? -1;
3161
+ if (typeParameterIndex < 0) {
3162
+ return null;
3163
+ }
3164
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
3165
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
3166
+ return referenceTypeArguments[typeParameterIndex] ?? null;
3167
+ }
3168
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
3169
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
3170
+ }
3171
+ function resolveLiteralDiscriminatorPropertyValue(boundType, fieldName, checker, provenance, diagnostics) {
3172
+ const propertySymbol = boundType.getProperty(fieldName);
3173
+ if (propertySymbol === void 0) {
3174
+ return void 0;
3175
+ }
3176
+ const declaration = propertySymbol.valueDeclaration ?? propertySymbol.declarations?.[0];
3177
+ const anchorNode = declaration ?? boundType.symbol.declarations?.[0] ?? null;
3178
+ const resolvedAnchorNode = anchorNode ?? resolveNamedDiscriminatorDeclaration(boundType, checker);
3179
+ if (resolvedAnchorNode === null) {
3180
+ return void 0;
3181
+ }
3182
+ const propertyType = checker.getTypeOfSymbolAtLocation(
3183
+ propertySymbol,
3184
+ resolvedAnchorNode
3185
+ );
3186
+ if (propertyType.isStringLiteral()) {
3187
+ return propertyType.value;
3188
+ }
3189
+ if (propertyType.isUnion()) {
3190
+ const nonNullMembers = propertyType.types.filter(
3191
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
3192
+ );
3193
+ if (nonNullMembers.length > 0 && nonNullMembers.every((member) => member.isStringLiteral())) {
3194
+ diagnostics.push(
3195
+ makeAnalysisDiagnostic(
3196
+ "INVALID_TAG_ARGUMENT",
3197
+ "Discriminator resolution for union-valued identity properties is out of scope for v1.",
3198
+ provenance
3199
+ )
3200
+ );
3201
+ return null;
3202
+ }
3203
+ }
3204
+ return void 0;
3205
+ }
3206
+ function resolveDiscriminatorApiName(boundType, checker, metadataPolicy) {
3207
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
3208
+ if (declaration === null) {
3209
+ return void 0;
3210
+ }
3211
+ const metadata = resolveNodeMetadata(
3212
+ metadataPolicy,
3213
+ "type",
3214
+ getDiscriminatorLogicalName(boundType, declaration, checker),
3215
+ declaration,
3216
+ {
3217
+ checker,
3218
+ declaration,
3219
+ subjectType: boundType
3220
+ }
3221
+ );
3222
+ return metadata?.apiName;
3223
+ }
3224
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
3225
+ if (seen.has(type)) {
3226
+ return null;
3227
+ }
3228
+ seen.add(type);
3229
+ const symbol = type.aliasSymbol ?? type.getSymbol();
3230
+ if (symbol !== void 0) {
3231
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
3232
+ const targetSymbol = aliased ?? symbol;
3233
+ const declaration = targetSymbol.declarations?.find(
3234
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
3235
+ );
3236
+ if (declaration !== void 0) {
3237
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
3238
+ return resolveNamedDiscriminatorDeclaration(
3239
+ checker.getTypeFromTypeNode(declaration.type),
3240
+ checker,
3241
+ seen
3242
+ );
3243
+ }
3244
+ return declaration;
3245
+ }
3246
+ }
3247
+ return null;
3248
+ }
3249
+ function resolveDiscriminatorValue(boundType, fieldName, checker, provenance, diagnostics, metadataPolicy) {
3250
+ if (boundType === null) {
3251
+ diagnostics.push(
3252
+ makeAnalysisDiagnostic(
3253
+ "INVALID_TAG_ARGUMENT",
3254
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
3255
+ provenance
3256
+ )
3257
+ );
3258
+ return null;
3259
+ }
3260
+ if (boundType.isStringLiteral()) {
3261
+ return boundType.value;
3262
+ }
3263
+ if (boundType.isUnion()) {
3264
+ const nonNullMembers = boundType.types.filter(
3265
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
3266
+ );
3267
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
3268
+ diagnostics.push(
3269
+ makeAnalysisDiagnostic(
3270
+ "INVALID_TAG_ARGUMENT",
3271
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
3272
+ provenance
3273
+ )
3274
+ );
3275
+ return null;
3276
+ }
3277
+ }
3278
+ const literalIdentityValue = resolveLiteralDiscriminatorPropertyValue(
3279
+ boundType,
3280
+ fieldName,
3281
+ checker,
3282
+ provenance,
3283
+ diagnostics
3284
+ );
3285
+ if (literalIdentityValue !== void 0) {
3286
+ return literalIdentityValue;
3287
+ }
3288
+ const apiName = resolveDiscriminatorApiName(boundType, checker, metadataPolicy);
3289
+ if (apiName?.source === "explicit") {
3290
+ return apiName.value;
3291
+ }
3292
+ if (apiName?.source === "inferred") {
3293
+ return apiName.value;
3294
+ }
3295
+ diagnostics.push(
3296
+ makeAnalysisDiagnostic(
3297
+ "INVALID_TAG_ARGUMENT",
3298
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
3299
+ provenance
3300
+ )
3301
+ );
3302
+ return null;
3303
+ }
3304
+ function getDeclarationName(node) {
3305
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
3306
+ return node.name?.text ?? "anonymous";
3307
+ }
3308
+ return "anonymous";
3309
+ }
3310
+ function getResolvedTypeArguments(type) {
3311
+ return (isTypeReference(type) ? type.typeArguments : void 0) ?? type.aliasTypeArguments ?? [];
3312
+ }
3313
+ function getDiscriminatorLogicalName(type, declaration, checker) {
3314
+ const baseName = getDeclarationName(declaration);
3315
+ const typeArguments = getResolvedTypeArguments(type);
3316
+ return typeArguments.length === 0 ? baseName : buildInstantiatedReferenceName(baseName, typeArguments, checker);
3317
+ }
3318
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics, metadataPolicy) {
3319
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
3320
+ if (directive === null) {
3321
+ return [...fields];
3322
+ }
3323
+ const discriminatorValue = resolveDiscriminatorValue(
3324
+ getConcreteTypeArgumentForDiscriminator(
3325
+ node,
3326
+ subjectType,
3327
+ checker,
3328
+ directive.typeParameterName
3329
+ ),
3330
+ directive.fieldName,
3331
+ checker,
3332
+ directive.provenance,
3333
+ diagnostics,
3334
+ metadataPolicy
3335
+ );
3336
+ if (discriminatorValue === null) {
3337
+ return [...fields];
3338
+ }
3339
+ return fields.map(
3340
+ (field) => field.name === directive.fieldName ? {
3341
+ ...field,
3342
+ type: {
3343
+ kind: "enum",
3344
+ members: [{ value: discriminatorValue }]
3345
+ }
3346
+ } : field
3347
+ );
3348
+ }
3349
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
3350
+ const renderedArguments = typeArguments.map(
3351
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
3352
+ ).filter((value) => value !== "");
3353
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
3354
+ }
3355
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy, extensionRegistry, diagnostics) {
3356
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
3357
+ if (typeNode === void 0) {
3358
+ return [];
3359
+ }
3360
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
3361
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
3362
+ return [];
3363
+ }
3364
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
3365
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
3366
+ return {
3367
+ tsType: argumentType,
3368
+ typeNode: resolveTypeNode(
3369
+ argumentType,
3370
+ checker,
3371
+ file,
3372
+ typeRegistry,
3373
+ visiting,
3374
+ argumentNode,
3375
+ metadataPolicy,
3376
+ extensionRegistry,
3377
+ diagnostics
3378
+ )
3379
+ };
3380
+ });
3381
+ }
3382
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics, metadataPolicy) {
3383
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
3384
+ if (directive === null) {
3385
+ return properties;
3386
+ }
3387
+ const discriminatorValue = resolveDiscriminatorValue(
3388
+ getConcreteTypeArgumentForDiscriminator(
3389
+ node,
3390
+ subjectType,
3391
+ checker,
3392
+ directive.typeParameterName
3393
+ ),
3394
+ directive.fieldName,
3395
+ checker,
3396
+ directive.provenance,
3397
+ diagnostics,
3398
+ metadataPolicy
3399
+ );
3400
+ if (discriminatorValue === null) {
3401
+ return properties;
3402
+ }
3403
+ return properties.map(
3404
+ (property) => property.name === directive.fieldName ? {
3405
+ ...property,
3406
+ type: {
3407
+ kind: "enum",
3408
+ members: [{ value: discriminatorValue }]
3409
+ }
3410
+ } : property
3411
+ );
3412
+ }
3413
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
2292
3414
  if (!ts3.isIdentifier(prop.name)) {
2293
3415
  return null;
2294
3416
  }
@@ -2303,6 +3425,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
2303
3425
  typeRegistry,
2304
3426
  visiting,
2305
3427
  prop,
3428
+ metadataPolicy,
2306
3429
  extensionRegistry,
2307
3430
  diagnostics
2308
3431
  );
@@ -2326,9 +3449,16 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
2326
3449
  annotations.push(defaultAnnotation);
2327
3450
  }
2328
3451
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
3452
+ const metadata = resolveNodeMetadata(metadataPolicy, "field", name, prop, {
3453
+ checker,
3454
+ declaration: prop,
3455
+ subjectType: tsType,
3456
+ hostType
3457
+ });
2329
3458
  return {
2330
3459
  kind: "field",
2331
3460
  name,
3461
+ ...metadata !== void 0 && { metadata },
2332
3462
  type,
2333
3463
  required: !optional,
2334
3464
  constraints,
@@ -2336,7 +3466,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
2336
3466
  provenance
2337
3467
  };
2338
3468
  }
2339
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
3469
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
2340
3470
  if (!ts3.isIdentifier(prop.name)) {
2341
3471
  return null;
2342
3472
  }
@@ -2351,6 +3481,7 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
2351
3481
  typeRegistry,
2352
3482
  visiting,
2353
3483
  prop,
3484
+ metadataPolicy,
2354
3485
  extensionRegistry,
2355
3486
  diagnostics
2356
3487
  );
@@ -2370,9 +3501,16 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
2370
3501
  let annotations = [];
2371
3502
  annotations.push(...docResult.annotations);
2372
3503
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
3504
+ const metadata = resolveNodeMetadata(metadataPolicy, "field", name, prop, {
3505
+ checker,
3506
+ declaration: prop,
3507
+ subjectType: tsType,
3508
+ hostType
3509
+ });
2373
3510
  return {
2374
3511
  kind: "field",
2375
3512
  name,
3513
+ ...metadata !== void 0 && { metadata },
2376
3514
  type,
2377
3515
  required: !optional,
2378
3516
  constraints,
@@ -2497,7 +3635,7 @@ function getTypeNodeRegistrationName(typeNode) {
2497
3635
  }
2498
3636
  return null;
2499
3637
  }
2500
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
3638
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2501
3639
  const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
2502
3640
  if (customType) {
2503
3641
  return customType;
@@ -2509,6 +3647,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2509
3647
  typeRegistry,
2510
3648
  visiting,
2511
3649
  sourceNode,
3650
+ metadataPolicy,
2512
3651
  extensionRegistry,
2513
3652
  diagnostics
2514
3653
  );
@@ -2553,6 +3692,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2553
3692
  typeRegistry,
2554
3693
  visiting,
2555
3694
  sourceNode,
3695
+ metadataPolicy,
2556
3696
  extensionRegistry,
2557
3697
  diagnostics
2558
3698
  );
@@ -2565,6 +3705,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2565
3705
  typeRegistry,
2566
3706
  visiting,
2567
3707
  sourceNode,
3708
+ metadataPolicy,
2568
3709
  extensionRegistry,
2569
3710
  diagnostics
2570
3711
  );
@@ -2576,13 +3717,15 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2576
3717
  file,
2577
3718
  typeRegistry,
2578
3719
  visiting,
3720
+ sourceNode,
3721
+ metadataPolicy,
2579
3722
  extensionRegistry,
2580
3723
  diagnostics
2581
3724
  );
2582
3725
  }
2583
3726
  return { kind: "primitive", primitiveKind: "string" };
2584
3727
  }
2585
- function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
3728
+ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2586
3729
  if (!(type.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null))) {
2587
3730
  return null;
2588
3731
  }
@@ -2602,14 +3745,21 @@ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiti
2602
3745
  file,
2603
3746
  makeParseOptions(extensionRegistry)
2604
3747
  );
3748
+ const metadata = resolveNodeMetadata(metadataPolicy, "type", aliasName, aliasDecl, {
3749
+ checker,
3750
+ declaration: aliasDecl,
3751
+ subjectType: aliasType
3752
+ });
2605
3753
  typeRegistry[aliasName] = {
2606
3754
  name: aliasName,
3755
+ ...metadata !== void 0 && { metadata },
2607
3756
  type: resolveAliasedPrimitiveTarget(
2608
3757
  aliasType,
2609
3758
  checker,
2610
3759
  file,
2611
3760
  typeRegistry,
2612
3761
  visiting,
3762
+ metadataPolicy,
2613
3763
  extensionRegistry,
2614
3764
  diagnostics
2615
3765
  ),
@@ -2638,7 +3788,7 @@ function shouldEmitPrimitiveAliasDefinition(typeNode, checker) {
2638
3788
  const resolved = checker.getTypeFromTypeNode(aliasDecl.type);
2639
3789
  return !!(resolved.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null));
2640
3790
  }
2641
- function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
3791
+ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2642
3792
  const nestedAliasDecl = type.aliasSymbol?.declarations?.find(ts3.isTypeAliasDeclaration);
2643
3793
  if (nestedAliasDecl !== void 0) {
2644
3794
  return resolveAliasedPrimitiveTarget(
@@ -2647,6 +3797,7 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
2647
3797
  file,
2648
3798
  typeRegistry,
2649
3799
  visiting,
3800
+ metadataPolicy,
2650
3801
  extensionRegistry,
2651
3802
  diagnostics
2652
3803
  );
@@ -2658,11 +3809,12 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
2658
3809
  typeRegistry,
2659
3810
  visiting,
2660
3811
  void 0,
3812
+ metadataPolicy,
2661
3813
  extensionRegistry,
2662
3814
  diagnostics
2663
3815
  );
2664
3816
  }
2665
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
3817
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2666
3818
  const typeName = getNamedTypeName(type);
2667
3819
  const namedDecl = getNamedTypeDeclaration(type);
2668
3820
  if (typeName && typeName in typeRegistry) {
@@ -2697,8 +3849,14 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2697
3849
  return result;
2698
3850
  }
2699
3851
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
3852
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", typeName, namedDecl, {
3853
+ checker,
3854
+ declaration: namedDecl,
3855
+ subjectType: type
3856
+ }) : void 0;
2700
3857
  typeRegistry[typeName] = {
2701
3858
  name: typeName,
3859
+ ...metadata !== void 0 && { metadata },
2702
3860
  type: result,
2703
3861
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2704
3862
  provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
@@ -2752,6 +3910,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2752
3910
  typeRegistry,
2753
3911
  visiting,
2754
3912
  nonNullMembers[0].sourceNode ?? sourceNode,
3913
+ metadataPolicy,
2755
3914
  extensionRegistry,
2756
3915
  diagnostics
2757
3916
  );
@@ -2769,6 +3928,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2769
3928
  typeRegistry,
2770
3929
  visiting,
2771
3930
  memberSourceNode ?? sourceNode,
3931
+ metadataPolicy,
2772
3932
  extensionRegistry,
2773
3933
  diagnostics
2774
3934
  )
@@ -2778,7 +3938,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2778
3938
  }
2779
3939
  return registerNamed({ kind: "union", members });
2780
3940
  }
2781
- function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
3941
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2782
3942
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
2783
3943
  const elementType = typeArgs?.[0];
2784
3944
  const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
@@ -2789,12 +3949,13 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNod
2789
3949
  typeRegistry,
2790
3950
  visiting,
2791
3951
  elementSourceNode,
3952
+ metadataPolicy,
2792
3953
  extensionRegistry,
2793
3954
  diagnostics
2794
3955
  ) : { kind: "primitive", primitiveKind: "string" };
2795
3956
  return { kind: "array", items };
2796
3957
  }
2797
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
3958
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2798
3959
  if (type.getProperties().length > 0) {
2799
3960
  return null;
2800
3961
  }
@@ -2809,6 +3970,7 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, exten
2809
3970
  typeRegistry,
2810
3971
  visiting,
2811
3972
  void 0,
3973
+ metadataPolicy,
2812
3974
  extensionRegistry,
2813
3975
  diagnostics
2814
3976
  );
@@ -2839,35 +4001,76 @@ function typeNodeContainsReference(type, targetName) {
2839
4001
  }
2840
4002
  }
2841
4003
  }
2842
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
4004
+ function shouldEmitResolvedObjectProperty(property, declaration) {
4005
+ if (property.name.startsWith("__@")) {
4006
+ return false;
4007
+ }
4008
+ if (declaration !== void 0 && "name" in declaration && declaration.name !== void 0) {
4009
+ const name = declaration.name;
4010
+ if (ts3.isComputedPropertyName(name) || ts3.isPrivateIdentifier(name)) {
4011
+ return false;
4012
+ }
4013
+ if (!ts3.isIdentifier(name) && !ts3.isStringLiteral(name) && !ts3.isNumericLiteral(name)) {
4014
+ return false;
4015
+ }
4016
+ }
4017
+ return true;
4018
+ }
4019
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
4020
+ const collectedDiagnostics = diagnostics ?? [];
2843
4021
  const typeName = getNamedTypeName(type);
2844
4022
  const namedTypeName = typeName ?? void 0;
2845
4023
  const namedDecl = getNamedTypeDeclaration(type);
2846
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
4024
+ const referenceTypeArguments = extractReferenceTypeArguments(
4025
+ type,
4026
+ checker,
4027
+ file,
4028
+ typeRegistry,
4029
+ visiting,
4030
+ sourceNode,
4031
+ metadataPolicy,
4032
+ extensionRegistry,
4033
+ collectedDiagnostics
4034
+ );
4035
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
4036
+ namedTypeName,
4037
+ referenceTypeArguments.map((argument) => argument.tsType),
4038
+ checker
4039
+ ) : void 0;
4040
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
4041
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2847
4042
  const clearNamedTypeRegistration = () => {
2848
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
4043
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2849
4044
  return;
2850
4045
  }
2851
- Reflect.deleteProperty(typeRegistry, namedTypeName);
4046
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2852
4047
  };
2853
4048
  if (visiting.has(type)) {
2854
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2855
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
4049
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
4050
+ return {
4051
+ kind: "reference",
4052
+ name: registryTypeName,
4053
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
4054
+ };
2856
4055
  }
2857
4056
  return { kind: "object", properties: [], additionalProperties: false };
2858
4057
  }
2859
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2860
- typeRegistry[namedTypeName] = {
2861
- name: namedTypeName,
4058
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
4059
+ typeRegistry[registryTypeName] = {
4060
+ name: registryTypeName,
2862
4061
  type: RESOLVING_TYPE_PLACEHOLDER,
2863
4062
  provenance: provenanceForDeclaration(namedDecl, file)
2864
4063
  };
2865
4064
  }
2866
4065
  visiting.add(type);
2867
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2868
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
4066
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
4067
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2869
4068
  visiting.delete(type);
2870
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
4069
+ return {
4070
+ kind: "reference",
4071
+ name: registryTypeName,
4072
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
4073
+ };
2871
4074
  }
2872
4075
  }
2873
4076
  const recordNode = tryResolveRecordType(
@@ -2876,25 +4079,36 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2876
4079
  file,
2877
4080
  typeRegistry,
2878
4081
  visiting,
4082
+ metadataPolicy,
2879
4083
  extensionRegistry,
2880
- diagnostics
4084
+ collectedDiagnostics
2881
4085
  );
2882
4086
  if (recordNode) {
2883
4087
  visiting.delete(type);
2884
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2885
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
4088
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
4089
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2886
4090
  if (!isRecursiveRecord) {
2887
4091
  clearNamedTypeRegistration();
2888
4092
  return recordNode;
2889
4093
  }
2890
4094
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2891
- typeRegistry[namedTypeName] = {
2892
- name: namedTypeName,
4095
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", registryTypeName, namedDecl, {
4096
+ checker,
4097
+ declaration: namedDecl,
4098
+ subjectType: type
4099
+ }) : void 0;
4100
+ typeRegistry[registryTypeName] = {
4101
+ name: registryTypeName,
4102
+ ...metadata !== void 0 && { metadata },
2893
4103
  type: recordNode,
2894
4104
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2895
4105
  provenance: provenanceForDeclaration(namedDecl, file)
2896
4106
  };
2897
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
4107
+ return {
4108
+ kind: "reference",
4109
+ name: registryTypeName,
4110
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
4111
+ };
2898
4112
  }
2899
4113
  return recordNode;
2900
4114
  }
@@ -2905,12 +4119,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2905
4119
  file,
2906
4120
  typeRegistry,
2907
4121
  visiting,
2908
- diagnostics ?? [],
4122
+ metadataPolicy,
4123
+ collectedDiagnostics,
2909
4124
  extensionRegistry
2910
4125
  );
2911
4126
  for (const prop of type.getProperties()) {
2912
4127
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
2913
4128
  if (!declaration) continue;
4129
+ if (!shouldEmitResolvedObjectProperty(prop, declaration)) continue;
2914
4130
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
2915
4131
  const optional = !!(prop.flags & ts3.SymbolFlags.Optional);
2916
4132
  const propTypeNode = resolveTypeNode(
@@ -2920,12 +4136,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2920
4136
  typeRegistry,
2921
4137
  visiting,
2922
4138
  declaration,
4139
+ metadataPolicy,
2923
4140
  extensionRegistry,
2924
- diagnostics
4141
+ collectedDiagnostics
2925
4142
  );
2926
4143
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2927
4144
  properties.push({
2928
4145
  name: prop.name,
4146
+ ...fieldNodeInfo?.metadata !== void 0 && { metadata: fieldNodeInfo.metadata },
2929
4147
  type: propTypeNode,
2930
4148
  optional,
2931
4149
  constraints: fieldNodeInfo?.constraints ?? [],
@@ -2936,22 +4154,40 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2936
4154
  visiting.delete(type);
2937
4155
  const objectNode = {
2938
4156
  kind: "object",
2939
- properties,
4157
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
4158
+ properties,
4159
+ namedDecl,
4160
+ type,
4161
+ checker,
4162
+ file,
4163
+ collectedDiagnostics,
4164
+ metadataPolicy
4165
+ ) : properties,
2940
4166
  additionalProperties: true
2941
4167
  };
2942
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
4168
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2943
4169
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2944
- typeRegistry[namedTypeName] = {
2945
- name: namedTypeName,
4170
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", registryTypeName, namedDecl, {
4171
+ checker,
4172
+ declaration: namedDecl,
4173
+ subjectType: type
4174
+ }) : void 0;
4175
+ typeRegistry[registryTypeName] = {
4176
+ name: registryTypeName,
4177
+ ...metadata !== void 0 && { metadata },
2946
4178
  type: objectNode,
2947
4179
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2948
4180
  provenance: provenanceForDeclaration(namedDecl, file)
2949
4181
  };
2950
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
4182
+ return {
4183
+ kind: "reference",
4184
+ name: registryTypeName,
4185
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
4186
+ };
2951
4187
  }
2952
4188
  return objectNode;
2953
4189
  }
2954
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, diagnostics, extensionRegistry) {
4190
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, metadataPolicy, diagnostics, extensionRegistry) {
2955
4191
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
2956
4192
  (s) => s?.declarations != null && s.declarations.length > 0
2957
4193
  );
@@ -2972,10 +4208,12 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2972
4208
  visiting,
2973
4209
  diagnostics,
2974
4210
  hostType,
4211
+ metadataPolicy,
2975
4212
  extensionRegistry
2976
4213
  );
2977
4214
  if (fieldNode) {
2978
4215
  map.set(fieldNode.name, {
4216
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
2979
4217
  constraints: [...fieldNode.constraints],
2980
4218
  annotations: [...fieldNode.annotations],
2981
4219
  provenance: fieldNode.provenance
@@ -2993,6 +4231,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2993
4231
  file,
2994
4232
  typeRegistry,
2995
4233
  visiting,
4234
+ metadataPolicy,
2996
4235
  checker.getTypeAtLocation(interfaceDecl),
2997
4236
  diagnostics,
2998
4237
  extensionRegistry
@@ -3006,6 +4245,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
3006
4245
  file,
3007
4246
  typeRegistry,
3008
4247
  visiting,
4248
+ metadataPolicy,
3009
4249
  checker.getTypeAtLocation(typeAliasDecl),
3010
4250
  diagnostics,
3011
4251
  extensionRegistry
@@ -3057,7 +4297,7 @@ function isNullishTypeNode(typeNode) {
3057
4297
  }
3058
4298
  return ts3.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts3.SyntaxKind.NullKeyword || typeNode.literal.kind === ts3.SyntaxKind.UndefinedKeyword);
3059
4299
  }
3060
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, hostType, diagnostics, extensionRegistry) {
4300
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, metadataPolicy, hostType, diagnostics, extensionRegistry) {
3061
4301
  const map = /* @__PURE__ */ new Map();
3062
4302
  for (const member of members) {
3063
4303
  if (ts3.isPropertySignature(member)) {
@@ -3069,10 +4309,12 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, h
3069
4309
  visiting,
3070
4310
  diagnostics,
3071
4311
  hostType,
4312
+ metadataPolicy,
3072
4313
  extensionRegistry
3073
4314
  );
3074
4315
  if (fieldNode) {
3075
4316
  map.set(fieldNode.name, {
4317
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
3076
4318
  constraints: [...fieldNode.constraints],
3077
4319
  annotations: [...fieldNode.annotations],
3078
4320
  provenance: fieldNode.provenance
@@ -3103,6 +4345,7 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegis
3103
4345
  {},
3104
4346
  /* @__PURE__ */ new Set(),
3105
4347
  aliasDecl.type,
4348
+ void 0,
3106
4349
  extensionRegistry
3107
4350
  );
3108
4351
  const constraints = extractJSDocConstraintNodes(
@@ -3288,19 +4531,37 @@ function findInterfaceByName(sourceFile, interfaceName) {
3288
4531
  function findTypeAliasByName(sourceFile, aliasName) {
3289
4532
  return findNodeByName(sourceFile, aliasName, ts4.isTypeAliasDeclaration, (n) => n.name.text);
3290
4533
  }
3291
- function analyzeNamedTypeToIR(filePath, typeName, extensionRegistry) {
4534
+ function analyzeNamedTypeToIR(filePath, typeName, extensionRegistry, metadataPolicy) {
3292
4535
  const ctx = createProgramContext(filePath);
3293
- return analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry);
4536
+ return analyzeNamedTypeToIRFromProgramContext(
4537
+ ctx,
4538
+ filePath,
4539
+ typeName,
4540
+ extensionRegistry,
4541
+ metadataPolicy
4542
+ );
3294
4543
  }
3295
- function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry) {
4544
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry, metadataPolicy) {
3296
4545
  const analysisFilePath = path.resolve(filePath);
3297
4546
  const classDecl = findClassByName(ctx.sourceFile, typeName);
3298
4547
  if (classDecl !== null) {
3299
- return analyzeClassToIR(classDecl, ctx.checker, analysisFilePath, extensionRegistry);
4548
+ return analyzeClassToIR(
4549
+ classDecl,
4550
+ ctx.checker,
4551
+ analysisFilePath,
4552
+ extensionRegistry,
4553
+ metadataPolicy
4554
+ );
3300
4555
  }
3301
4556
  const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
3302
4557
  if (interfaceDecl !== null) {
3303
- return analyzeInterfaceToIR(interfaceDecl, ctx.checker, analysisFilePath, extensionRegistry);
4558
+ return analyzeInterfaceToIR(
4559
+ interfaceDecl,
4560
+ ctx.checker,
4561
+ analysisFilePath,
4562
+ extensionRegistry,
4563
+ metadataPolicy
4564
+ );
3304
4565
  }
3305
4566
  const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
3306
4567
  if (typeAlias !== null) {
@@ -3308,7 +4569,8 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
3308
4569
  typeAlias,
3309
4570
  ctx.checker,
3310
4571
  analysisFilePath,
3311
- extensionRegistry
4572
+ extensionRegistry,
4573
+ metadataPolicy
3312
4574
  );
3313
4575
  if (result.ok) {
3314
4576
  return result.analysis;
@@ -3321,9 +4583,9 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
3321
4583
  }
3322
4584
 
3323
4585
  // src/validate/constraint-validator.ts
3324
- var import_internal2 = require("@formspec/analysis/internal");
4586
+ var import_internal3 = require("@formspec/analysis/internal");
3325
4587
  function validateFieldNode(ctx, field) {
3326
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
4588
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3327
4589
  field.name,
3328
4590
  field.type,
3329
4591
  field.constraints,
@@ -3341,7 +4603,7 @@ function validateFieldNode(ctx, field) {
3341
4603
  }
3342
4604
  function validateObjectProperty(ctx, parentName, property) {
3343
4605
  const qualifiedName = `${parentName}.${property.name}`;
3344
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
4606
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3345
4607
  qualifiedName,
3346
4608
  property.type,
3347
4609
  property.constraints,
@@ -3401,7 +4663,11 @@ function generateClassSchemas(analysis, source, options) {
3401
4663
  if (errorDiagnostics !== void 0 && errorDiagnostics.length > 0) {
3402
4664
  throw new Error(formatValidationError(errorDiagnostics));
3403
4665
  }
3404
- const ir = canonicalizeTSDoc(analysis, source);
4666
+ const ir = canonicalizeTSDoc(
4667
+ analysis,
4668
+ source,
4669
+ options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
4670
+ );
3405
4671
  const validationResult = validateIR(ir, {
3406
4672
  ...options?.extensionRegistry !== void 0 && {
3407
4673
  extensionRegistry: options.extensionRegistry
@@ -3438,13 +4704,15 @@ function generateSchemasFromClass(options) {
3438
4704
  classDecl,
3439
4705
  ctx.checker,
3440
4706
  options.filePath,
3441
- options.extensionRegistry
4707
+ options.extensionRegistry,
4708
+ options.metadata
3442
4709
  );
3443
4710
  return generateClassSchemas(
3444
4711
  analysis,
3445
4712
  { file: options.filePath },
3446
4713
  {
3447
4714
  extensionRegistry: options.extensionRegistry,
4715
+ metadata: options.metadata,
3448
4716
  vendorPrefix: options.vendorPrefix
3449
4717
  }
3450
4718
  );
@@ -3462,13 +4730,15 @@ function generateSchemasFromProgram(options) {
3462
4730
  ctx,
3463
4731
  options.filePath,
3464
4732
  options.typeName,
3465
- options.extensionRegistry
4733
+ options.extensionRegistry,
4734
+ options.metadata
3466
4735
  );
3467
4736
  return generateClassSchemas(
3468
4737
  analysis,
3469
4738
  { file: options.filePath },
3470
4739
  {
3471
4740
  extensionRegistry: options.extensionRegistry,
4741
+ metadata: options.metadata,
3472
4742
  vendorPrefix: options.vendorPrefix
3473
4743
  }
3474
4744
  );
@@ -3477,16 +4747,28 @@ function generateSchemasFromProgram(options) {
3477
4747
  // src/generators/mixed-authoring.ts
3478
4748
  function buildMixedAuthoringSchemas(options) {
3479
4749
  const { filePath, typeName, overlays, ...schemaOptions } = options;
3480
- const analysis = analyzeNamedTypeToIR(filePath, typeName, schemaOptions.extensionRegistry);
3481
- const composedAnalysis = composeAnalysisWithOverlays(analysis, overlays);
3482
- const ir = canonicalizeTSDoc(composedAnalysis, { file: filePath });
4750
+ const analysis = analyzeNamedTypeToIR(
4751
+ filePath,
4752
+ typeName,
4753
+ schemaOptions.extensionRegistry,
4754
+ schemaOptions.metadata
4755
+ );
4756
+ const composedAnalysis = composeAnalysisWithOverlays(analysis, overlays, schemaOptions.metadata);
4757
+ const ir = canonicalizeTSDoc(
4758
+ composedAnalysis,
4759
+ { file: filePath },
4760
+ schemaOptions.metadata !== void 0 ? { metadata: schemaOptions.metadata } : void 0
4761
+ );
3483
4762
  return {
3484
4763
  jsonSchema: generateJsonSchemaFromIR(ir, schemaOptions),
3485
4764
  uiSchema: generateUiSchemaFromIR(ir)
3486
4765
  };
3487
4766
  }
3488
- function composeAnalysisWithOverlays(analysis, overlays) {
3489
- const overlayIR = canonicalizeChainDSL(overlays);
4767
+ function composeAnalysisWithOverlays(analysis, overlays, metadata) {
4768
+ const overlayIR = canonicalizeChainDSL(
4769
+ overlays,
4770
+ metadata !== void 0 ? { metadata } : void 0
4771
+ );
3490
4772
  const overlayFields = collectOverlayFields(overlayIR.elements);
3491
4773
  if (overlayFields.length === 0) {
3492
4774
  return analysis;
@@ -3542,8 +4824,10 @@ function collectOverlayFields(elements) {
3542
4824
  }
3543
4825
  function mergeFieldOverlay(baseField, overlayField, typeRegistry) {
3544
4826
  assertSupportedOverlayField(baseField, overlayField);
4827
+ const metadata = mergeResolvedMetadata(baseField.metadata, overlayField.metadata);
3545
4828
  return {
3546
4829
  ...baseField,
4830
+ ...metadata !== void 0 && { metadata },
3547
4831
  type: mergeFieldType(baseField, overlayField, typeRegistry),
3548
4832
  annotations: mergeAnnotations(baseField.annotations, overlayField.annotations)
3549
4833
  };
@@ -3654,12 +4938,12 @@ function annotationKey(annotation) {
3654
4938
  function buildFormSchemas(form, options) {
3655
4939
  return {
3656
4940
  jsonSchema: generateJsonSchema(form, options),
3657
- uiSchema: generateUiSchema(form)
4941
+ uiSchema: generateUiSchema(form, options)
3658
4942
  };
3659
4943
  }
3660
4944
  function writeSchemas(form, options) {
3661
- const { outDir, name = "schema", indent = 2, vendorPrefix } = options;
3662
- const buildOptions = vendorPrefix === void 0 ? void 0 : { vendorPrefix };
4945
+ const { outDir, name = "schema", indent = 2, vendorPrefix, metadata } = options;
4946
+ const buildOptions = vendorPrefix === void 0 && metadata === void 0 ? void 0 : { vendorPrefix, metadata };
3663
4947
  const { jsonSchema, uiSchema: uiSchema2 } = buildFormSchemas(form, buildOptions);
3664
4948
  if (!fs.existsSync(outDir)) {
3665
4949
  fs.mkdirSync(outDir, { recursive: true });