@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
@@ -53,6 +53,282 @@ module.exports = __toCommonJS(internals_exports);
53
53
 
54
54
  // src/canonicalize/chain-dsl-canonicalizer.ts
55
55
  var import_internals = require("@formspec/core/internals");
56
+
57
+ // src/metadata/policy.ts
58
+ var NOOP_INFLECT = () => "";
59
+ function normalizePluralization(input) {
60
+ if (input?.mode === "infer-if-missing") {
61
+ return {
62
+ mode: "infer-if-missing",
63
+ infer: () => "",
64
+ inflect: input.inflect
65
+ };
66
+ }
67
+ if (input?.mode === "require-explicit") {
68
+ return {
69
+ mode: "require-explicit",
70
+ infer: () => "",
71
+ inflect: NOOP_INFLECT
72
+ };
73
+ }
74
+ return {
75
+ mode: "disabled",
76
+ infer: () => "",
77
+ inflect: NOOP_INFLECT
78
+ };
79
+ }
80
+ function normalizeScalarPolicy(input) {
81
+ if (input?.mode === "infer-if-missing") {
82
+ return {
83
+ mode: "infer-if-missing",
84
+ infer: input.infer,
85
+ pluralization: normalizePluralization(input.pluralization)
86
+ };
87
+ }
88
+ if (input?.mode === "require-explicit") {
89
+ return {
90
+ mode: "require-explicit",
91
+ infer: () => "",
92
+ pluralization: normalizePluralization(input.pluralization)
93
+ };
94
+ }
95
+ return {
96
+ mode: "disabled",
97
+ infer: () => "",
98
+ pluralization: normalizePluralization(input?.pluralization)
99
+ };
100
+ }
101
+ function normalizeDeclarationPolicy(input) {
102
+ return {
103
+ apiName: normalizeScalarPolicy(input?.apiName),
104
+ displayName: normalizeScalarPolicy(input?.displayName)
105
+ };
106
+ }
107
+ function normalizeMetadataPolicy(input) {
108
+ return {
109
+ type: normalizeDeclarationPolicy(input?.type),
110
+ field: normalizeDeclarationPolicy(input?.field),
111
+ method: normalizeDeclarationPolicy(input?.method)
112
+ };
113
+ }
114
+ function getDeclarationMetadataPolicy(policy, declarationKind) {
115
+ return policy[declarationKind];
116
+ }
117
+ function makeMetadataContext(surface, declarationKind, logicalName, buildContext) {
118
+ return {
119
+ surface,
120
+ declarationKind,
121
+ logicalName,
122
+ ...buildContext !== void 0 && { buildContext }
123
+ };
124
+ }
125
+
126
+ // src/metadata/resolve.ts
127
+ function toExplicitScalar(value) {
128
+ return value !== void 0 && value.trim() !== "" ? { value, source: "explicit" } : void 0;
129
+ }
130
+ function toExplicitResolvedMetadata(explicit) {
131
+ if (explicit === void 0) {
132
+ return void 0;
133
+ }
134
+ const apiName = toExplicitScalar(explicit.apiName);
135
+ const displayName = toExplicitScalar(explicit.displayName);
136
+ const apiNamePlural = toExplicitScalar(explicit.apiNamePlural);
137
+ const displayNamePlural = toExplicitScalar(explicit.displayNamePlural);
138
+ const metadata = {
139
+ ...apiName !== void 0 && { apiName },
140
+ ...displayName !== void 0 && { displayName },
141
+ ...apiNamePlural !== void 0 && { apiNamePlural },
142
+ ...displayNamePlural !== void 0 && { displayNamePlural }
143
+ };
144
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
145
+ }
146
+ function resolveScalar(current, policy, context, metadataLabel) {
147
+ if (current !== void 0) {
148
+ return current;
149
+ }
150
+ if (policy.mode === "require-explicit") {
151
+ throw new Error(
152
+ `Metadata policy requires explicit ${metadataLabel} for ${context.declarationKind} "${context.logicalName}" on the ${context.surface} surface.`
153
+ );
154
+ }
155
+ if (policy.mode !== "infer-if-missing") {
156
+ return void 0;
157
+ }
158
+ const inferredValue = policy.infer(context);
159
+ return inferredValue.trim() !== "" ? { value: inferredValue, source: "inferred" } : void 0;
160
+ }
161
+ function resolvePlural(current, singular, policy, context, metadataLabel) {
162
+ if (current !== void 0) {
163
+ return current;
164
+ }
165
+ if (policy.mode === "require-explicit") {
166
+ throw new Error(
167
+ `Metadata policy requires explicit ${metadataLabel} for ${context.declarationKind} "${context.logicalName}" on the ${context.surface} surface.`
168
+ );
169
+ }
170
+ if (singular === void 0 || policy.mode !== "infer-if-missing") {
171
+ return void 0;
172
+ }
173
+ const pluralValue = policy.inflect({ ...context, singular: singular.value });
174
+ return pluralValue.trim() !== "" ? { value: pluralValue, source: "inferred" } : void 0;
175
+ }
176
+ function resolveResolvedMetadata(current, policy, context) {
177
+ const apiName = resolveScalar(current?.apiName, policy.apiName, context, "apiName");
178
+ const displayName = resolveScalar(
179
+ current?.displayName,
180
+ policy.displayName,
181
+ context,
182
+ "displayName"
183
+ );
184
+ const apiNamePlural = resolvePlural(
185
+ current?.apiNamePlural,
186
+ apiName,
187
+ policy.apiName.pluralization,
188
+ context,
189
+ "apiNamePlural"
190
+ );
191
+ const displayNamePlural = resolvePlural(
192
+ current?.displayNamePlural,
193
+ displayName,
194
+ policy.displayName.pluralization,
195
+ context,
196
+ "displayNamePlural"
197
+ );
198
+ if (apiName === void 0 && displayName === void 0 && apiNamePlural === void 0 && displayNamePlural === void 0) {
199
+ return void 0;
200
+ }
201
+ return {
202
+ ...apiName !== void 0 && { apiName },
203
+ ...displayName !== void 0 && { displayName },
204
+ ...apiNamePlural !== void 0 && { apiNamePlural },
205
+ ...displayNamePlural !== void 0 && { displayNamePlural }
206
+ };
207
+ }
208
+ function resolveTypeNodeMetadata(type, options) {
209
+ switch (type.kind) {
210
+ case "array":
211
+ return {
212
+ ...type,
213
+ items: resolveTypeNodeMetadata(type.items, options)
214
+ };
215
+ case "object":
216
+ return {
217
+ ...type,
218
+ properties: type.properties.map((property) => resolveObjectPropertyMetadata(property, options))
219
+ };
220
+ case "record":
221
+ return {
222
+ ...type,
223
+ valueType: resolveTypeNodeMetadata(type.valueType, options)
224
+ };
225
+ case "union":
226
+ return {
227
+ ...type,
228
+ members: type.members.map((member) => resolveTypeNodeMetadata(member, options))
229
+ };
230
+ case "reference":
231
+ case "primitive":
232
+ case "enum":
233
+ case "dynamic":
234
+ case "custom":
235
+ return type;
236
+ default: {
237
+ const _exhaustive = type;
238
+ return _exhaustive;
239
+ }
240
+ }
241
+ }
242
+ function resolveObjectPropertyMetadata(property, options) {
243
+ const metadata = resolveResolvedMetadata(property.metadata, options.policy.field, {
244
+ surface: options.surface,
245
+ declarationKind: "field",
246
+ logicalName: property.name,
247
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
248
+ });
249
+ return {
250
+ ...property,
251
+ ...metadata !== void 0 && { metadata },
252
+ type: resolveTypeNodeMetadata(property.type, options)
253
+ };
254
+ }
255
+ function resolveFieldMetadataNode(field, options) {
256
+ const metadata = resolveResolvedMetadata(field.metadata, options.policy.field, {
257
+ surface: options.surface,
258
+ declarationKind: "field",
259
+ logicalName: field.name,
260
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
261
+ });
262
+ return {
263
+ ...field,
264
+ ...metadata !== void 0 && { metadata },
265
+ type: resolveTypeNodeMetadata(field.type, options)
266
+ };
267
+ }
268
+ function resolveFormElementMetadata(element, options) {
269
+ switch (element.kind) {
270
+ case "field":
271
+ return resolveFieldMetadataNode(element, options);
272
+ case "group":
273
+ return {
274
+ ...element,
275
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
276
+ };
277
+ case "conditional":
278
+ return {
279
+ ...element,
280
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
281
+ };
282
+ default: {
283
+ const _exhaustive = element;
284
+ return _exhaustive;
285
+ }
286
+ }
287
+ }
288
+ function resolveTypeDefinitionMetadata(typeDefinition, options) {
289
+ const metadata = resolveResolvedMetadata(typeDefinition.metadata, options.policy.type, {
290
+ surface: options.surface,
291
+ declarationKind: "type",
292
+ logicalName: typeDefinition.name,
293
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
294
+ });
295
+ return {
296
+ ...typeDefinition,
297
+ ...metadata !== void 0 && { metadata },
298
+ type: resolveTypeNodeMetadata(typeDefinition.type, options)
299
+ };
300
+ }
301
+ function resolveMetadata(explicit, policy, context) {
302
+ return resolveResolvedMetadata(toExplicitResolvedMetadata(explicit), policy, context);
303
+ }
304
+ function getSerializedName(logicalName, metadata) {
305
+ return metadata?.apiName?.value ?? logicalName;
306
+ }
307
+ function getDisplayName(metadata) {
308
+ return metadata?.displayName?.value;
309
+ }
310
+ function resolveFormIRMetadata(ir, options) {
311
+ const rootLogicalName = options.rootLogicalName ?? ir.name ?? "FormSpec";
312
+ const metadata = resolveResolvedMetadata(ir.metadata, options.policy.type, {
313
+ surface: options.surface,
314
+ declarationKind: "type",
315
+ logicalName: rootLogicalName,
316
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
317
+ });
318
+ return {
319
+ ...ir,
320
+ ...metadata !== void 0 && { metadata },
321
+ elements: ir.elements.map((element) => resolveFormElementMetadata(element, options)),
322
+ typeRegistry: Object.fromEntries(
323
+ Object.entries(ir.typeRegistry).map(([name, definition]) => [
324
+ name,
325
+ resolveTypeDefinitionMetadata(definition, options)
326
+ ])
327
+ )
328
+ };
329
+ }
330
+
331
+ // src/canonicalize/chain-dsl-canonicalizer.ts
56
332
  var CHAIN_DSL_PROVENANCE = {
57
333
  surface: "chain-dsl",
58
334
  file: "",
@@ -68,57 +344,60 @@ function isConditional(el) {
68
344
  function isField(el) {
69
345
  return el._type === "field";
70
346
  }
71
- function canonicalizeChainDSL(form) {
347
+ function canonicalizeChainDSL(form, options) {
348
+ const metadataPolicy = normalizeMetadataPolicy(
349
+ options?.metadata ?? (0, import_internals._getFormSpecMetadataPolicy)(form)
350
+ );
72
351
  return {
73
352
  kind: "form-ir",
74
353
  irVersion: import_internals.IR_VERSION,
75
- elements: canonicalizeElements(form.elements),
354
+ elements: canonicalizeElements(form.elements, metadataPolicy),
76
355
  rootAnnotations: [],
77
356
  typeRegistry: {},
78
357
  provenance: CHAIN_DSL_PROVENANCE
79
358
  };
80
359
  }
81
- function canonicalizeElements(elements) {
82
- return elements.map(canonicalizeElement);
360
+ function canonicalizeElements(elements, metadataPolicy) {
361
+ return elements.map((element) => canonicalizeElement(element, metadataPolicy));
83
362
  }
84
- function canonicalizeElement(element) {
363
+ function canonicalizeElement(element, metadataPolicy) {
85
364
  if (isField(element)) {
86
- return canonicalizeField(element);
365
+ return canonicalizeField(element, metadataPolicy);
87
366
  }
88
367
  if (isGroup(element)) {
89
- return canonicalizeGroup(element);
368
+ return canonicalizeGroup(element, metadataPolicy);
90
369
  }
91
370
  if (isConditional(element)) {
92
- return canonicalizeConditional(element);
371
+ return canonicalizeConditional(element, metadataPolicy);
93
372
  }
94
373
  const _exhaustive = element;
95
374
  throw new Error(`Unknown element type: ${JSON.stringify(_exhaustive)}`);
96
375
  }
97
- function canonicalizeField(field) {
376
+ function canonicalizeField(field, metadataPolicy) {
98
377
  switch (field._field) {
99
378
  case "text":
100
- return canonicalizeTextField(field);
379
+ return canonicalizeTextField(field, metadataPolicy);
101
380
  case "number":
102
- return canonicalizeNumberField(field);
381
+ return canonicalizeNumberField(field, metadataPolicy);
103
382
  case "boolean":
104
- return canonicalizeBooleanField(field);
383
+ return canonicalizeBooleanField(field, metadataPolicy);
105
384
  case "enum":
106
- return canonicalizeStaticEnumField(field);
385
+ return canonicalizeStaticEnumField(field, metadataPolicy);
107
386
  case "dynamic_enum":
108
- return canonicalizeDynamicEnumField(field);
387
+ return canonicalizeDynamicEnumField(field, metadataPolicy);
109
388
  case "dynamic_schema":
110
- return canonicalizeDynamicSchemaField(field);
389
+ return canonicalizeDynamicSchemaField(field, metadataPolicy);
111
390
  case "array":
112
- return canonicalizeArrayField(field);
391
+ return canonicalizeArrayField(field, metadataPolicy);
113
392
  case "object":
114
- return canonicalizeObjectField(field);
393
+ return canonicalizeObjectField(field, metadataPolicy);
115
394
  default: {
116
395
  const _exhaustive = field;
117
396
  throw new Error(`Unknown field type: ${JSON.stringify(_exhaustive)}`);
118
397
  }
119
398
  }
120
399
  }
121
- function canonicalizeTextField(field) {
400
+ function canonicalizeTextField(field, metadataPolicy) {
122
401
  const type = { kind: "primitive", primitiveKind: "string" };
123
402
  const constraints = [];
124
403
  if (field.minLength !== void 0) {
@@ -150,13 +429,14 @@ function canonicalizeTextField(field) {
150
429
  }
151
430
  return buildFieldNode(
152
431
  field.name,
432
+ resolveFieldMetadata(field.name, field, metadataPolicy),
153
433
  type,
154
434
  field.required,
155
- buildAnnotations(field.label, field.placeholder),
435
+ buildAnnotations(getExplicitDisplayName(field), field.placeholder),
156
436
  constraints
157
437
  );
158
438
  }
159
- function canonicalizeNumberField(field) {
439
+ function canonicalizeNumberField(field, metadataPolicy) {
160
440
  const type = { kind: "primitive", primitiveKind: "number" };
161
441
  const constraints = [];
162
442
  if (field.min !== void 0) {
@@ -188,17 +468,24 @@ function canonicalizeNumberField(field) {
188
468
  }
189
469
  return buildFieldNode(
190
470
  field.name,
471
+ resolveFieldMetadata(field.name, field, metadataPolicy),
191
472
  type,
192
473
  field.required,
193
- buildAnnotations(field.label),
474
+ buildAnnotations(getExplicitDisplayName(field)),
194
475
  constraints
195
476
  );
196
477
  }
197
- function canonicalizeBooleanField(field) {
478
+ function canonicalizeBooleanField(field, metadataPolicy) {
198
479
  const type = { kind: "primitive", primitiveKind: "boolean" };
199
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
480
+ return buildFieldNode(
481
+ field.name,
482
+ resolveFieldMetadata(field.name, field, metadataPolicy),
483
+ type,
484
+ field.required,
485
+ buildAnnotations(getExplicitDisplayName(field))
486
+ );
200
487
  }
201
- function canonicalizeStaticEnumField(field) {
488
+ function canonicalizeStaticEnumField(field, metadataPolicy) {
202
489
  const members = field.options.map((opt) => {
203
490
  if (typeof opt === "string") {
204
491
  return { value: opt };
@@ -206,28 +493,46 @@ function canonicalizeStaticEnumField(field) {
206
493
  return { value: opt.id, displayName: opt.label };
207
494
  });
208
495
  const type = { kind: "enum", members };
209
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
496
+ return buildFieldNode(
497
+ field.name,
498
+ resolveFieldMetadata(field.name, field, metadataPolicy),
499
+ type,
500
+ field.required,
501
+ buildAnnotations(getExplicitDisplayName(field))
502
+ );
210
503
  }
211
- function canonicalizeDynamicEnumField(field) {
504
+ function canonicalizeDynamicEnumField(field, metadataPolicy) {
212
505
  const type = {
213
506
  kind: "dynamic",
214
507
  dynamicKind: "enum",
215
508
  sourceKey: field.source,
216
509
  parameterFields: field.params ? [...field.params] : []
217
510
  };
218
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
511
+ return buildFieldNode(
512
+ field.name,
513
+ resolveFieldMetadata(field.name, field, metadataPolicy),
514
+ type,
515
+ field.required,
516
+ buildAnnotations(getExplicitDisplayName(field))
517
+ );
219
518
  }
220
- function canonicalizeDynamicSchemaField(field) {
519
+ function canonicalizeDynamicSchemaField(field, metadataPolicy) {
221
520
  const type = {
222
521
  kind: "dynamic",
223
522
  dynamicKind: "schema",
224
523
  sourceKey: field.schemaSource,
225
524
  parameterFields: []
226
525
  };
227
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
526
+ return buildFieldNode(
527
+ field.name,
528
+ resolveFieldMetadata(field.name, field, metadataPolicy),
529
+ type,
530
+ field.required,
531
+ buildAnnotations(getExplicitDisplayName(field))
532
+ );
228
533
  }
229
- function canonicalizeArrayField(field) {
230
- const itemProperties = buildObjectProperties(field.items);
534
+ function canonicalizeArrayField(field, metadataPolicy) {
535
+ const itemProperties = buildObjectProperties(field.items, metadataPolicy);
231
536
  const itemsType = {
232
537
  kind: "object",
233
538
  properties: itemProperties,
@@ -255,37 +560,44 @@ function canonicalizeArrayField(field) {
255
560
  }
256
561
  return buildFieldNode(
257
562
  field.name,
563
+ resolveFieldMetadata(field.name, field, metadataPolicy),
258
564
  type,
259
565
  field.required,
260
- buildAnnotations(field.label),
566
+ buildAnnotations(getExplicitDisplayName(field)),
261
567
  constraints
262
568
  );
263
569
  }
264
- function canonicalizeObjectField(field) {
265
- const properties = buildObjectProperties(field.properties);
570
+ function canonicalizeObjectField(field, metadataPolicy) {
571
+ const properties = buildObjectProperties(field.properties, metadataPolicy);
266
572
  const type = {
267
573
  kind: "object",
268
574
  properties,
269
575
  additionalProperties: true
270
576
  };
271
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
577
+ return buildFieldNode(
578
+ field.name,
579
+ resolveFieldMetadata(field.name, field, metadataPolicy),
580
+ type,
581
+ field.required,
582
+ buildAnnotations(getExplicitDisplayName(field))
583
+ );
272
584
  }
273
- function canonicalizeGroup(g) {
585
+ function canonicalizeGroup(g, metadataPolicy) {
274
586
  return {
275
587
  kind: "group",
276
588
  label: g.label,
277
- elements: canonicalizeElements(g.elements),
589
+ elements: canonicalizeElements(g.elements, metadataPolicy),
278
590
  provenance: CHAIN_DSL_PROVENANCE
279
591
  };
280
592
  }
281
- function canonicalizeConditional(c) {
593
+ function canonicalizeConditional(c, metadataPolicy) {
282
594
  return {
283
595
  kind: "conditional",
284
596
  fieldName: c.field,
285
597
  // Conditional values from the chain DSL are JSON-serializable primitives
286
598
  // (strings, numbers, booleans) produced by the `is()` predicate helper.
287
599
  value: assertJsonValue(c.value),
288
- elements: canonicalizeElements(c.elements),
600
+ elements: canonicalizeElements(c.elements, metadataPolicy),
289
601
  provenance: CHAIN_DSL_PROVENANCE
290
602
  };
291
603
  }
@@ -305,10 +617,11 @@ function assertJsonValue(v) {
305
617
  }
306
618
  throw new TypeError(`Conditional value is not a valid JsonValue: ${typeof v}`);
307
619
  }
308
- function buildFieldNode(name, type, required, annotations, constraints = []) {
620
+ function buildFieldNode(name, metadata, type, required, annotations, constraints = []) {
309
621
  return {
310
622
  kind: "field",
311
623
  name,
624
+ ...metadata !== void 0 && { metadata },
312
625
  type,
313
626
  required: required === true,
314
627
  constraints,
@@ -338,13 +651,14 @@ function buildAnnotations(label, placeholder) {
338
651
  }
339
652
  return annotations;
340
653
  }
341
- function buildObjectProperties(elements, insideConditional = false) {
654
+ function buildObjectProperties(elements, metadataPolicy, insideConditional = false) {
342
655
  const properties = [];
343
656
  for (const el of elements) {
344
657
  if (isField(el)) {
345
- const fieldNode = canonicalizeField(el);
658
+ const fieldNode = canonicalizeField(el, metadataPolicy);
346
659
  properties.push({
347
660
  name: fieldNode.name,
661
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
348
662
  type: fieldNode.type,
349
663
  // Fields inside a conditional branch are always optional in the
350
664
  // data schema, regardless of their `required` flag — the condition
@@ -355,17 +669,34 @@ function buildObjectProperties(elements, insideConditional = false) {
355
669
  provenance: CHAIN_DSL_PROVENANCE
356
670
  });
357
671
  } else if (isGroup(el)) {
358
- properties.push(...buildObjectProperties(el.elements, insideConditional));
672
+ properties.push(...buildObjectProperties(el.elements, metadataPolicy, insideConditional));
359
673
  } else if (isConditional(el)) {
360
- properties.push(...buildObjectProperties(el.elements, true));
674
+ properties.push(...buildObjectProperties(el.elements, metadataPolicy, true));
361
675
  }
362
676
  }
363
677
  return properties;
364
678
  }
679
+ function getExplicitDisplayName(field) {
680
+ if (field.label !== void 0 && field.displayName !== void 0) {
681
+ throw new Error('Chain DSL fields cannot specify both "label" and "displayName".');
682
+ }
683
+ return field.displayName ?? field.label;
684
+ }
685
+ function resolveFieldMetadata(logicalName, field, metadataPolicy) {
686
+ const displayName = getExplicitDisplayName(field);
687
+ return resolveMetadata(
688
+ {
689
+ ...field.apiName !== void 0 && { apiName: field.apiName },
690
+ ...displayName !== void 0 && { displayName }
691
+ },
692
+ getDeclarationMetadataPolicy(metadataPolicy, "field"),
693
+ makeMetadataContext("chain-dsl", "field", logicalName)
694
+ );
695
+ }
365
696
 
366
697
  // src/canonicalize/tsdoc-canonicalizer.ts
367
698
  var import_internals2 = require("@formspec/core/internals");
368
- function canonicalizeTSDoc(analysis, source) {
699
+ function canonicalizeTSDoc(analysis, source, options) {
369
700
  const file = source?.file ?? "";
370
701
  const provenance = {
371
702
  surface: "tsdoc",
@@ -374,15 +705,21 @@ function canonicalizeTSDoc(analysis, source) {
374
705
  column: 0
375
706
  };
376
707
  const elements = assembleElements(analysis.fields, analysis.fieldLayouts, provenance);
377
- return {
708
+ const ir = {
378
709
  kind: "form-ir",
710
+ name: analysis.name,
379
711
  irVersion: import_internals2.IR_VERSION,
380
712
  elements,
713
+ ...analysis.metadata !== void 0 && { metadata: analysis.metadata },
381
714
  typeRegistry: analysis.typeRegistry,
382
715
  ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { rootAnnotations: analysis.annotations },
383
716
  ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
384
717
  provenance
385
718
  };
719
+ return resolveFormIRMetadata(ir, {
720
+ policy: normalizeMetadataPolicy(options?.metadata),
721
+ surface: "tsdoc"
722
+ });
386
723
  }
387
724
  function assembleElements(fields, layouts, provenance) {
388
725
  const elements = [];
@@ -445,6 +782,7 @@ var path = __toESM(require("path"), 1);
445
782
 
446
783
  // src/analyzer/class-analyzer.ts
447
784
  var ts3 = __toESM(require("typescript"), 1);
785
+ var import_internal2 = require("@formspec/analysis/internal");
448
786
 
449
787
  // src/analyzer/jsdoc-constraints.ts
450
788
  var ts2 = __toESM(require("typescript"), 1);
@@ -1322,7 +1660,76 @@ function makeParseOptions(extensionRegistry, fieldType, checker, subjectType, ho
1322
1660
  ...hostType !== void 0 && { hostType }
1323
1661
  };
1324
1662
  }
1325
- function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1663
+ function makeExplicitScalarMetadata(value) {
1664
+ return value === void 0 || value === "" ? void 0 : { value, source: "explicit" };
1665
+ }
1666
+ function extractExplicitMetadata(node) {
1667
+ let apiName;
1668
+ let displayName;
1669
+ let apiNamePlural;
1670
+ let displayNamePlural;
1671
+ for (const tag of getLeadingParsedTags(node)) {
1672
+ const value = tag.argumentText.trim();
1673
+ if (value === "") {
1674
+ continue;
1675
+ }
1676
+ if (tag.normalizedTagName === "apiName") {
1677
+ if (tag.target === null) {
1678
+ apiName ??= value;
1679
+ } else if (tag.target.kind === "variant") {
1680
+ if (tag.target.rawText === "singular") {
1681
+ apiName ??= value;
1682
+ } else if (tag.target.rawText === "plural") {
1683
+ apiNamePlural ??= value;
1684
+ }
1685
+ }
1686
+ continue;
1687
+ }
1688
+ if (tag.normalizedTagName === "displayName") {
1689
+ if (tag.target === null) {
1690
+ displayName ??= value;
1691
+ } else if (tag.target.kind === "variant") {
1692
+ if (tag.target.rawText === "singular") {
1693
+ displayName ??= value;
1694
+ } else if (tag.target.rawText === "plural") {
1695
+ displayNamePlural ??= value;
1696
+ }
1697
+ }
1698
+ }
1699
+ }
1700
+ const resolvedApiName = makeExplicitScalarMetadata(apiName);
1701
+ const resolvedDisplayName = makeExplicitScalarMetadata(displayName);
1702
+ const resolvedApiNamePlural = makeExplicitScalarMetadata(apiNamePlural);
1703
+ const resolvedDisplayNamePlural = makeExplicitScalarMetadata(displayNamePlural);
1704
+ const metadata = {
1705
+ ...resolvedApiName !== void 0 && { apiName: resolvedApiName },
1706
+ ...resolvedDisplayName !== void 0 && { displayName: resolvedDisplayName },
1707
+ ...resolvedApiNamePlural !== void 0 && { apiNamePlural: resolvedApiNamePlural },
1708
+ ...resolvedDisplayNamePlural !== void 0 && {
1709
+ displayNamePlural: resolvedDisplayNamePlural
1710
+ }
1711
+ };
1712
+ return Object.keys(metadata).length === 0 ? void 0 : metadata;
1713
+ }
1714
+ function resolveNodeMetadata(metadataPolicy, declarationKind, logicalName, node, buildContext) {
1715
+ const explicit = extractExplicitMetadata(node);
1716
+ return resolveMetadata(
1717
+ {
1718
+ ...explicit?.apiName !== void 0 && { apiName: explicit.apiName.value },
1719
+ ...explicit?.displayName !== void 0 && { displayName: explicit.displayName.value },
1720
+ ...explicit?.apiNamePlural !== void 0 && {
1721
+ apiNamePlural: explicit.apiNamePlural.value
1722
+ },
1723
+ ...explicit?.displayNamePlural !== void 0 && {
1724
+ displayNamePlural: explicit.displayNamePlural.value
1725
+ }
1726
+ },
1727
+ getDeclarationMetadataPolicy(metadataPolicy, declarationKind),
1728
+ makeMetadataContext("tsdoc", declarationKind, logicalName, buildContext)
1729
+ );
1730
+ }
1731
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry, metadataPolicy) {
1732
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
1326
1733
  const name = classDecl.name?.text ?? "AnonymousClass";
1327
1734
  const fields = [];
1328
1735
  const fieldLayouts = [];
@@ -1349,6 +1756,7 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1349
1756
  visiting,
1350
1757
  diagnostics,
1351
1758
  classType,
1759
+ normalizedMetadataPolicy,
1352
1760
  extensionRegistry
1353
1761
  );
1354
1762
  if (fieldNode) {
@@ -1367,9 +1775,25 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1367
1775
  }
1368
1776
  }
1369
1777
  }
1778
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1779
+ fields,
1780
+ classDecl,
1781
+ classType,
1782
+ checker,
1783
+ file,
1784
+ diagnostics,
1785
+ normalizedMetadataPolicy
1786
+ );
1787
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, classDecl, {
1788
+ checker,
1789
+ declaration: classDecl,
1790
+ subjectType: classType,
1791
+ hostType: classType
1792
+ });
1370
1793
  return {
1371
1794
  name,
1372
- fields,
1795
+ ...metadata !== void 0 && { metadata },
1796
+ fields: specializedFields,
1373
1797
  fieldLayouts,
1374
1798
  typeRegistry,
1375
1799
  ...annotations.length > 0 && { annotations },
@@ -1378,7 +1802,8 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1378
1802
  staticMethods
1379
1803
  };
1380
1804
  }
1381
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
1805
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry, metadataPolicy) {
1806
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
1382
1807
  const name = interfaceDecl.name.text;
1383
1808
  const fields = [];
1384
1809
  const typeRegistry = {};
@@ -1402,6 +1827,78 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1402
1827
  visiting,
1403
1828
  diagnostics,
1404
1829
  interfaceType,
1830
+ normalizedMetadataPolicy,
1831
+ extensionRegistry
1832
+ );
1833
+ if (fieldNode) {
1834
+ fields.push(fieldNode);
1835
+ }
1836
+ }
1837
+ }
1838
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1839
+ fields,
1840
+ interfaceDecl,
1841
+ interfaceType,
1842
+ checker,
1843
+ file,
1844
+ diagnostics,
1845
+ normalizedMetadataPolicy
1846
+ );
1847
+ const fieldLayouts = specializedFields.map(() => ({}));
1848
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, interfaceDecl, {
1849
+ checker,
1850
+ declaration: interfaceDecl,
1851
+ subjectType: interfaceType,
1852
+ hostType: interfaceType
1853
+ });
1854
+ return {
1855
+ name,
1856
+ ...metadata !== void 0 && { metadata },
1857
+ fields: specializedFields,
1858
+ fieldLayouts,
1859
+ typeRegistry,
1860
+ ...annotations.length > 0 && { annotations },
1861
+ ...diagnostics.length > 0 && { diagnostics },
1862
+ instanceMethods: [],
1863
+ staticMethods: []
1864
+ };
1865
+ }
1866
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry, metadataPolicy) {
1867
+ if (!ts3.isTypeLiteralNode(typeAlias.type)) {
1868
+ const sourceFile = typeAlias.getSourceFile();
1869
+ const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
1870
+ const kindDesc = ts3.SyntaxKind[typeAlias.type.kind] ?? "unknown";
1871
+ return {
1872
+ ok: false,
1873
+ error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
1874
+ };
1875
+ }
1876
+ const typeLiteral = typeAlias.type;
1877
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
1878
+ const name = typeAlias.name.text;
1879
+ const fields = [];
1880
+ const typeRegistry = {};
1881
+ const diagnostics = [];
1882
+ const aliasType = checker.getTypeAtLocation(typeAlias);
1883
+ const typeAliasDoc = extractJSDocParseResult(
1884
+ typeAlias,
1885
+ file,
1886
+ makeParseOptions(extensionRegistry, void 0, checker, aliasType, aliasType)
1887
+ );
1888
+ const annotations = [...typeAliasDoc.annotations];
1889
+ diagnostics.push(...typeAliasDoc.diagnostics);
1890
+ const visiting = /* @__PURE__ */ new Set();
1891
+ for (const member of typeLiteral.members) {
1892
+ if (ts3.isPropertySignature(member)) {
1893
+ const fieldNode = analyzeInterfacePropertyToIR(
1894
+ member,
1895
+ checker,
1896
+ file,
1897
+ typeRegistry,
1898
+ visiting,
1899
+ diagnostics,
1900
+ aliasType,
1901
+ normalizedMetadataPolicy,
1405
1902
  extensionRegistry
1406
1903
  );
1407
1904
  if (fieldNode) {
@@ -1409,73 +1906,474 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1409
1906
  }
1410
1907
  }
1411
1908
  }
1412
- const fieldLayouts = fields.map(() => ({}));
1413
- return {
1414
- name,
1415
- fields,
1416
- fieldLayouts,
1417
- typeRegistry,
1418
- ...annotations.length > 0 && { annotations },
1419
- ...diagnostics.length > 0 && { diagnostics },
1420
- instanceMethods: [],
1421
- staticMethods: []
1422
- };
1909
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1910
+ fields,
1911
+ typeAlias,
1912
+ aliasType,
1913
+ checker,
1914
+ file,
1915
+ diagnostics,
1916
+ normalizedMetadataPolicy
1917
+ );
1918
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, typeAlias, {
1919
+ checker,
1920
+ declaration: typeAlias,
1921
+ subjectType: aliasType,
1922
+ hostType: aliasType
1923
+ });
1924
+ return {
1925
+ ok: true,
1926
+ analysis: {
1927
+ name,
1928
+ ...metadata !== void 0 && { metadata },
1929
+ fields: specializedFields,
1930
+ fieldLayouts: specializedFields.map(() => ({})),
1931
+ typeRegistry,
1932
+ ...annotations.length > 0 && { annotations },
1933
+ ...diagnostics.length > 0 && { diagnostics },
1934
+ instanceMethods: [],
1935
+ staticMethods: []
1936
+ }
1937
+ };
1938
+ }
1939
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
1940
+ return {
1941
+ code,
1942
+ message,
1943
+ severity: "error",
1944
+ primaryLocation,
1945
+ relatedLocations
1946
+ };
1947
+ }
1948
+ function getLeadingParsedTags(node) {
1949
+ const sourceFile = node.getSourceFile();
1950
+ const sourceText = sourceFile.getFullText();
1951
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
1952
+ if (commentRanges === void 0) {
1953
+ return [];
1954
+ }
1955
+ const parsedTags = [];
1956
+ for (const range of commentRanges) {
1957
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
1958
+ continue;
1959
+ }
1960
+ const commentText = sourceText.slice(range.pos, range.end);
1961
+ if (!commentText.startsWith("/**")) {
1962
+ continue;
1963
+ }
1964
+ parsedTags.push(...(0, import_internal2.parseCommentBlock)(commentText, { offset: range.pos }).tags);
1965
+ }
1966
+ return parsedTags;
1967
+ }
1968
+ function resolveDiscriminatorProperty(node, checker, fieldName) {
1969
+ const subjectType = checker.getTypeAtLocation(node);
1970
+ const propertySymbol = subjectType.getProperty(fieldName);
1971
+ if (propertySymbol === void 0) {
1972
+ return null;
1973
+ }
1974
+ const declaration = propertySymbol.valueDeclaration ?? propertySymbol.declarations?.find(
1975
+ (candidate) => ts3.isPropertyDeclaration(candidate) || ts3.isPropertySignature(candidate)
1976
+ ) ?? propertySymbol.declarations?.[0];
1977
+ return {
1978
+ declaration,
1979
+ type: checker.getTypeOfSymbolAtLocation(propertySymbol, declaration ?? node),
1980
+ optional: !!(propertySymbol.flags & ts3.SymbolFlags.Optional) || declaration !== void 0 && "questionToken" in declaration && declaration.questionToken !== void 0
1981
+ };
1982
+ }
1983
+ function isLocalTypeParameterName(node, typeParameterName) {
1984
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
1985
+ }
1986
+ function isNullishSemanticType(type) {
1987
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
1988
+ return true;
1989
+ }
1990
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
1991
+ }
1992
+ function isStringLikeSemanticType(type) {
1993
+ if (type.flags & ts3.TypeFlags.StringLike) {
1994
+ return true;
1995
+ }
1996
+ if (type.isUnion()) {
1997
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
1998
+ }
1999
+ return false;
2000
+ }
2001
+ function extractDiscriminatorDirective(node, file, diagnostics) {
2002
+ const discriminatorTags = getLeadingParsedTags(node).filter(
2003
+ (tag) => tag.normalizedTagName === "discriminator"
2004
+ );
2005
+ if (discriminatorTags.length === 0) {
2006
+ return null;
2007
+ }
2008
+ const [firstTag, ...duplicateTags] = discriminatorTags;
2009
+ for (const _duplicateTag of duplicateTags) {
2010
+ diagnostics.push(
2011
+ makeAnalysisDiagnostic(
2012
+ "DUPLICATE_TAG",
2013
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
2014
+ provenanceForNode(node, file)
2015
+ )
2016
+ );
2017
+ }
2018
+ if (firstTag === void 0) {
2019
+ return null;
2020
+ }
2021
+ const firstTarget = firstTag.target;
2022
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
2023
+ diagnostics.push(
2024
+ makeAnalysisDiagnostic(
2025
+ "INVALID_TAG_ARGUMENT",
2026
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
2027
+ provenanceForNode(node, file)
2028
+ )
2029
+ );
2030
+ return null;
2031
+ }
2032
+ if (firstTarget.path.segments.length !== 1) {
2033
+ diagnostics.push(
2034
+ makeAnalysisDiagnostic(
2035
+ "INVALID_TAG_ARGUMENT",
2036
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
2037
+ provenanceForNode(node, file)
2038
+ )
2039
+ );
2040
+ return null;
2041
+ }
2042
+ const typeParameterName = firstTag.argumentText.trim();
2043
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
2044
+ diagnostics.push(
2045
+ makeAnalysisDiagnostic(
2046
+ "INVALID_TAG_ARGUMENT",
2047
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
2048
+ provenanceForNode(node, file)
2049
+ )
2050
+ );
2051
+ return null;
2052
+ }
2053
+ return {
2054
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
2055
+ typeParameterName,
2056
+ provenance: provenanceForNode(node, file)
2057
+ };
2058
+ }
2059
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
2060
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
2061
+ if (directive === null) {
2062
+ return null;
2063
+ }
2064
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
2065
+ diagnostics.push(
2066
+ makeAnalysisDiagnostic(
2067
+ "INVALID_TAG_ARGUMENT",
2068
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
2069
+ directive.provenance
2070
+ )
2071
+ );
2072
+ return null;
2073
+ }
2074
+ const property = resolveDiscriminatorProperty(node, checker, directive.fieldName);
2075
+ if (property === null) {
2076
+ diagnostics.push(
2077
+ makeAnalysisDiagnostic(
2078
+ "UNKNOWN_PATH_TARGET",
2079
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
2080
+ directive.provenance
2081
+ )
2082
+ );
2083
+ return null;
2084
+ }
2085
+ if (property.optional) {
2086
+ diagnostics.push(
2087
+ makeAnalysisDiagnostic(
2088
+ "TYPE_MISMATCH",
2089
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
2090
+ directive.provenance,
2091
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
2092
+ )
2093
+ );
2094
+ return null;
2095
+ }
2096
+ if (isNullishSemanticType(property.type)) {
2097
+ diagnostics.push(
2098
+ makeAnalysisDiagnostic(
2099
+ "TYPE_MISMATCH",
2100
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
2101
+ directive.provenance,
2102
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
2103
+ )
2104
+ );
2105
+ return null;
2106
+ }
2107
+ if (!isStringLikeSemanticType(property.type)) {
2108
+ diagnostics.push(
2109
+ makeAnalysisDiagnostic(
2110
+ "TYPE_MISMATCH",
2111
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
2112
+ directive.provenance,
2113
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
2114
+ )
2115
+ );
2116
+ return null;
2117
+ }
2118
+ return directive;
2119
+ }
2120
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
2121
+ const typeParameterIndex = node.typeParameters?.findIndex(
2122
+ (typeParameter) => typeParameter.name.text === typeParameterName
2123
+ ) ?? -1;
2124
+ if (typeParameterIndex < 0) {
2125
+ return null;
2126
+ }
2127
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
2128
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
2129
+ return referenceTypeArguments[typeParameterIndex] ?? null;
2130
+ }
2131
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
2132
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
2133
+ }
2134
+ function resolveLiteralDiscriminatorPropertyValue(boundType, fieldName, checker, provenance, diagnostics) {
2135
+ const propertySymbol = boundType.getProperty(fieldName);
2136
+ if (propertySymbol === void 0) {
2137
+ return void 0;
2138
+ }
2139
+ const declaration = propertySymbol.valueDeclaration ?? propertySymbol.declarations?.[0];
2140
+ const anchorNode = declaration ?? boundType.symbol.declarations?.[0] ?? null;
2141
+ const resolvedAnchorNode = anchorNode ?? resolveNamedDiscriminatorDeclaration(boundType, checker);
2142
+ if (resolvedAnchorNode === null) {
2143
+ return void 0;
2144
+ }
2145
+ const propertyType = checker.getTypeOfSymbolAtLocation(
2146
+ propertySymbol,
2147
+ resolvedAnchorNode
2148
+ );
2149
+ if (propertyType.isStringLiteral()) {
2150
+ return propertyType.value;
2151
+ }
2152
+ if (propertyType.isUnion()) {
2153
+ const nonNullMembers = propertyType.types.filter(
2154
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
2155
+ );
2156
+ if (nonNullMembers.length > 0 && nonNullMembers.every((member) => member.isStringLiteral())) {
2157
+ diagnostics.push(
2158
+ makeAnalysisDiagnostic(
2159
+ "INVALID_TAG_ARGUMENT",
2160
+ "Discriminator resolution for union-valued identity properties is out of scope for v1.",
2161
+ provenance
2162
+ )
2163
+ );
2164
+ return null;
2165
+ }
2166
+ }
2167
+ return void 0;
2168
+ }
2169
+ function resolveDiscriminatorApiName(boundType, checker, metadataPolicy) {
2170
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
2171
+ if (declaration === null) {
2172
+ return void 0;
2173
+ }
2174
+ const metadata = resolveNodeMetadata(
2175
+ metadataPolicy,
2176
+ "type",
2177
+ getDiscriminatorLogicalName(boundType, declaration, checker),
2178
+ declaration,
2179
+ {
2180
+ checker,
2181
+ declaration,
2182
+ subjectType: boundType
2183
+ }
2184
+ );
2185
+ return metadata?.apiName;
2186
+ }
2187
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
2188
+ if (seen.has(type)) {
2189
+ return null;
2190
+ }
2191
+ seen.add(type);
2192
+ const symbol = type.aliasSymbol ?? type.getSymbol();
2193
+ if (symbol !== void 0) {
2194
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
2195
+ const targetSymbol = aliased ?? symbol;
2196
+ const declaration = targetSymbol.declarations?.find(
2197
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
2198
+ );
2199
+ if (declaration !== void 0) {
2200
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
2201
+ return resolveNamedDiscriminatorDeclaration(
2202
+ checker.getTypeFromTypeNode(declaration.type),
2203
+ checker,
2204
+ seen
2205
+ );
2206
+ }
2207
+ return declaration;
2208
+ }
2209
+ }
2210
+ return null;
1423
2211
  }
1424
- function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
1425
- if (!ts3.isTypeLiteralNode(typeAlias.type)) {
1426
- const sourceFile = typeAlias.getSourceFile();
1427
- const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
1428
- const kindDesc = ts3.SyntaxKind[typeAlias.type.kind] ?? "unknown";
1429
- return {
1430
- ok: false,
1431
- error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
1432
- };
2212
+ function resolveDiscriminatorValue(boundType, fieldName, checker, provenance, diagnostics, metadataPolicy) {
2213
+ if (boundType === null) {
2214
+ diagnostics.push(
2215
+ makeAnalysisDiagnostic(
2216
+ "INVALID_TAG_ARGUMENT",
2217
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
2218
+ provenance
2219
+ )
2220
+ );
2221
+ return null;
1433
2222
  }
1434
- const name = typeAlias.name.text;
1435
- const fields = [];
1436
- const typeRegistry = {};
1437
- const diagnostics = [];
1438
- const aliasType = checker.getTypeAtLocation(typeAlias);
1439
- const typeAliasDoc = extractJSDocParseResult(
1440
- typeAlias,
1441
- file,
1442
- makeParseOptions(extensionRegistry, void 0, checker, aliasType, aliasType)
2223
+ if (boundType.isStringLiteral()) {
2224
+ return boundType.value;
2225
+ }
2226
+ if (boundType.isUnion()) {
2227
+ const nonNullMembers = boundType.types.filter(
2228
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
2229
+ );
2230
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
2231
+ diagnostics.push(
2232
+ makeAnalysisDiagnostic(
2233
+ "INVALID_TAG_ARGUMENT",
2234
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
2235
+ provenance
2236
+ )
2237
+ );
2238
+ return null;
2239
+ }
2240
+ }
2241
+ const literalIdentityValue = resolveLiteralDiscriminatorPropertyValue(
2242
+ boundType,
2243
+ fieldName,
2244
+ checker,
2245
+ provenance,
2246
+ diagnostics
1443
2247
  );
1444
- const annotations = [...typeAliasDoc.annotations];
1445
- diagnostics.push(...typeAliasDoc.diagnostics);
1446
- const visiting = /* @__PURE__ */ new Set();
1447
- for (const member of typeAlias.type.members) {
1448
- if (ts3.isPropertySignature(member)) {
1449
- const fieldNode = analyzeInterfacePropertyToIR(
1450
- member,
2248
+ if (literalIdentityValue !== void 0) {
2249
+ return literalIdentityValue;
2250
+ }
2251
+ const apiName = resolveDiscriminatorApiName(boundType, checker, metadataPolicy);
2252
+ if (apiName?.source === "explicit") {
2253
+ return apiName.value;
2254
+ }
2255
+ if (apiName?.source === "inferred") {
2256
+ return apiName.value;
2257
+ }
2258
+ diagnostics.push(
2259
+ makeAnalysisDiagnostic(
2260
+ "INVALID_TAG_ARGUMENT",
2261
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
2262
+ provenance
2263
+ )
2264
+ );
2265
+ return null;
2266
+ }
2267
+ function getDeclarationName(node) {
2268
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
2269
+ return node.name?.text ?? "anonymous";
2270
+ }
2271
+ return "anonymous";
2272
+ }
2273
+ function getResolvedTypeArguments(type) {
2274
+ return (isTypeReference(type) ? type.typeArguments : void 0) ?? type.aliasTypeArguments ?? [];
2275
+ }
2276
+ function getDiscriminatorLogicalName(type, declaration, checker) {
2277
+ const baseName = getDeclarationName(declaration);
2278
+ const typeArguments = getResolvedTypeArguments(type);
2279
+ return typeArguments.length === 0 ? baseName : buildInstantiatedReferenceName(baseName, typeArguments, checker);
2280
+ }
2281
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics, metadataPolicy) {
2282
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2283
+ if (directive === null) {
2284
+ return [...fields];
2285
+ }
2286
+ const discriminatorValue = resolveDiscriminatorValue(
2287
+ getConcreteTypeArgumentForDiscriminator(
2288
+ node,
2289
+ subjectType,
2290
+ checker,
2291
+ directive.typeParameterName
2292
+ ),
2293
+ directive.fieldName,
2294
+ checker,
2295
+ directive.provenance,
2296
+ diagnostics,
2297
+ metadataPolicy
2298
+ );
2299
+ if (discriminatorValue === null) {
2300
+ return [...fields];
2301
+ }
2302
+ return fields.map(
2303
+ (field) => field.name === directive.fieldName ? {
2304
+ ...field,
2305
+ type: {
2306
+ kind: "enum",
2307
+ members: [{ value: discriminatorValue }]
2308
+ }
2309
+ } : field
2310
+ );
2311
+ }
2312
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
2313
+ const renderedArguments = typeArguments.map(
2314
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
2315
+ ).filter((value) => value !== "");
2316
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
2317
+ }
2318
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy, extensionRegistry, diagnostics) {
2319
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2320
+ if (typeNode === void 0) {
2321
+ return [];
2322
+ }
2323
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2324
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
2325
+ return [];
2326
+ }
2327
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
2328
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
2329
+ return {
2330
+ tsType: argumentType,
2331
+ typeNode: resolveTypeNode(
2332
+ argumentType,
1451
2333
  checker,
1452
2334
  file,
1453
2335
  typeRegistry,
1454
2336
  visiting,
1455
- diagnostics,
1456
- aliasType,
1457
- extensionRegistry
1458
- );
1459
- if (fieldNode) {
1460
- fields.push(fieldNode);
1461
- }
1462
- }
2337
+ argumentNode,
2338
+ metadataPolicy,
2339
+ extensionRegistry,
2340
+ diagnostics
2341
+ )
2342
+ };
2343
+ });
2344
+ }
2345
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics, metadataPolicy) {
2346
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2347
+ if (directive === null) {
2348
+ return properties;
1463
2349
  }
1464
- return {
1465
- ok: true,
1466
- analysis: {
1467
- name,
1468
- fields,
1469
- fieldLayouts: fields.map(() => ({})),
1470
- typeRegistry,
1471
- ...annotations.length > 0 && { annotations },
1472
- ...diagnostics.length > 0 && { diagnostics },
1473
- instanceMethods: [],
1474
- staticMethods: []
1475
- }
1476
- };
2350
+ const discriminatorValue = resolveDiscriminatorValue(
2351
+ getConcreteTypeArgumentForDiscriminator(
2352
+ node,
2353
+ subjectType,
2354
+ checker,
2355
+ directive.typeParameterName
2356
+ ),
2357
+ directive.fieldName,
2358
+ checker,
2359
+ directive.provenance,
2360
+ diagnostics,
2361
+ metadataPolicy
2362
+ );
2363
+ if (discriminatorValue === null) {
2364
+ return properties;
2365
+ }
2366
+ return properties.map(
2367
+ (property) => property.name === directive.fieldName ? {
2368
+ ...property,
2369
+ type: {
2370
+ kind: "enum",
2371
+ members: [{ value: discriminatorValue }]
2372
+ }
2373
+ } : property
2374
+ );
1477
2375
  }
1478
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2376
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
1479
2377
  if (!ts3.isIdentifier(prop.name)) {
1480
2378
  return null;
1481
2379
  }
@@ -1490,6 +2388,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
1490
2388
  typeRegistry,
1491
2389
  visiting,
1492
2390
  prop,
2391
+ metadataPolicy,
1493
2392
  extensionRegistry,
1494
2393
  diagnostics
1495
2394
  );
@@ -1513,9 +2412,16 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
1513
2412
  annotations.push(defaultAnnotation);
1514
2413
  }
1515
2414
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
2415
+ const metadata = resolveNodeMetadata(metadataPolicy, "field", name, prop, {
2416
+ checker,
2417
+ declaration: prop,
2418
+ subjectType: tsType,
2419
+ hostType
2420
+ });
1516
2421
  return {
1517
2422
  kind: "field",
1518
2423
  name,
2424
+ ...metadata !== void 0 && { metadata },
1519
2425
  type,
1520
2426
  required: !optional,
1521
2427
  constraints,
@@ -1523,7 +2429,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
1523
2429
  provenance
1524
2430
  };
1525
2431
  }
1526
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2432
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
1527
2433
  if (!ts3.isIdentifier(prop.name)) {
1528
2434
  return null;
1529
2435
  }
@@ -1538,6 +2444,7 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1538
2444
  typeRegistry,
1539
2445
  visiting,
1540
2446
  prop,
2447
+ metadataPolicy,
1541
2448
  extensionRegistry,
1542
2449
  diagnostics
1543
2450
  );
@@ -1557,9 +2464,16 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1557
2464
  let annotations = [];
1558
2465
  annotations.push(...docResult.annotations);
1559
2466
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
2467
+ const metadata = resolveNodeMetadata(metadataPolicy, "field", name, prop, {
2468
+ checker,
2469
+ declaration: prop,
2470
+ subjectType: tsType,
2471
+ hostType
2472
+ });
1560
2473
  return {
1561
2474
  kind: "field",
1562
2475
  name,
2476
+ ...metadata !== void 0 && { metadata },
1563
2477
  type,
1564
2478
  required: !optional,
1565
2479
  constraints,
@@ -1684,7 +2598,7 @@ function getTypeNodeRegistrationName(typeNode) {
1684
2598
  }
1685
2599
  return null;
1686
2600
  }
1687
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2601
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1688
2602
  const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1689
2603
  if (customType) {
1690
2604
  return customType;
@@ -1696,6 +2610,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1696
2610
  typeRegistry,
1697
2611
  visiting,
1698
2612
  sourceNode,
2613
+ metadataPolicy,
1699
2614
  extensionRegistry,
1700
2615
  diagnostics
1701
2616
  );
@@ -1740,6 +2655,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1740
2655
  typeRegistry,
1741
2656
  visiting,
1742
2657
  sourceNode,
2658
+ metadataPolicy,
1743
2659
  extensionRegistry,
1744
2660
  diagnostics
1745
2661
  );
@@ -1752,6 +2668,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1752
2668
  typeRegistry,
1753
2669
  visiting,
1754
2670
  sourceNode,
2671
+ metadataPolicy,
1755
2672
  extensionRegistry,
1756
2673
  diagnostics
1757
2674
  );
@@ -1763,13 +2680,15 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1763
2680
  file,
1764
2681
  typeRegistry,
1765
2682
  visiting,
2683
+ sourceNode,
2684
+ metadataPolicy,
1766
2685
  extensionRegistry,
1767
2686
  diagnostics
1768
2687
  );
1769
2688
  }
1770
2689
  return { kind: "primitive", primitiveKind: "string" };
1771
2690
  }
1772
- function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2691
+ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1773
2692
  if (!(type.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null))) {
1774
2693
  return null;
1775
2694
  }
@@ -1789,14 +2708,21 @@ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiti
1789
2708
  file,
1790
2709
  makeParseOptions(extensionRegistry)
1791
2710
  );
2711
+ const metadata = resolveNodeMetadata(metadataPolicy, "type", aliasName, aliasDecl, {
2712
+ checker,
2713
+ declaration: aliasDecl,
2714
+ subjectType: aliasType
2715
+ });
1792
2716
  typeRegistry[aliasName] = {
1793
2717
  name: aliasName,
2718
+ ...metadata !== void 0 && { metadata },
1794
2719
  type: resolveAliasedPrimitiveTarget(
1795
2720
  aliasType,
1796
2721
  checker,
1797
2722
  file,
1798
2723
  typeRegistry,
1799
2724
  visiting,
2725
+ metadataPolicy,
1800
2726
  extensionRegistry,
1801
2727
  diagnostics
1802
2728
  ),
@@ -1825,7 +2751,7 @@ function shouldEmitPrimitiveAliasDefinition(typeNode, checker) {
1825
2751
  const resolved = checker.getTypeFromTypeNode(aliasDecl.type);
1826
2752
  return !!(resolved.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null));
1827
2753
  }
1828
- function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2754
+ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1829
2755
  const nestedAliasDecl = type.aliasSymbol?.declarations?.find(ts3.isTypeAliasDeclaration);
1830
2756
  if (nestedAliasDecl !== void 0) {
1831
2757
  return resolveAliasedPrimitiveTarget(
@@ -1834,6 +2760,7 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
1834
2760
  file,
1835
2761
  typeRegistry,
1836
2762
  visiting,
2763
+ metadataPolicy,
1837
2764
  extensionRegistry,
1838
2765
  diagnostics
1839
2766
  );
@@ -1845,11 +2772,12 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
1845
2772
  typeRegistry,
1846
2773
  visiting,
1847
2774
  void 0,
2775
+ metadataPolicy,
1848
2776
  extensionRegistry,
1849
2777
  diagnostics
1850
2778
  );
1851
2779
  }
1852
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2780
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1853
2781
  const typeName = getNamedTypeName(type);
1854
2782
  const namedDecl = getNamedTypeDeclaration(type);
1855
2783
  if (typeName && typeName in typeRegistry) {
@@ -1884,8 +2812,14 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1884
2812
  return result;
1885
2813
  }
1886
2814
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2815
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", typeName, namedDecl, {
2816
+ checker,
2817
+ declaration: namedDecl,
2818
+ subjectType: type
2819
+ }) : void 0;
1887
2820
  typeRegistry[typeName] = {
1888
2821
  name: typeName,
2822
+ ...metadata !== void 0 && { metadata },
1889
2823
  type: result,
1890
2824
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
1891
2825
  provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
@@ -1939,6 +2873,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1939
2873
  typeRegistry,
1940
2874
  visiting,
1941
2875
  nonNullMembers[0].sourceNode ?? sourceNode,
2876
+ metadataPolicy,
1942
2877
  extensionRegistry,
1943
2878
  diagnostics
1944
2879
  );
@@ -1956,6 +2891,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1956
2891
  typeRegistry,
1957
2892
  visiting,
1958
2893
  memberSourceNode ?? sourceNode,
2894
+ metadataPolicy,
1959
2895
  extensionRegistry,
1960
2896
  diagnostics
1961
2897
  )
@@ -1965,7 +2901,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1965
2901
  }
1966
2902
  return registerNamed({ kind: "union", members });
1967
2903
  }
1968
- function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2904
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1969
2905
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1970
2906
  const elementType = typeArgs?.[0];
1971
2907
  const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
@@ -1976,12 +2912,13 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNod
1976
2912
  typeRegistry,
1977
2913
  visiting,
1978
2914
  elementSourceNode,
2915
+ metadataPolicy,
1979
2916
  extensionRegistry,
1980
2917
  diagnostics
1981
2918
  ) : { kind: "primitive", primitiveKind: "string" };
1982
2919
  return { kind: "array", items };
1983
2920
  }
1984
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2921
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1985
2922
  if (type.getProperties().length > 0) {
1986
2923
  return null;
1987
2924
  }
@@ -1996,6 +2933,7 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, exten
1996
2933
  typeRegistry,
1997
2934
  visiting,
1998
2935
  void 0,
2936
+ metadataPolicy,
1999
2937
  extensionRegistry,
2000
2938
  diagnostics
2001
2939
  );
@@ -2026,35 +2964,76 @@ function typeNodeContainsReference(type, targetName) {
2026
2964
  }
2027
2965
  }
2028
2966
  }
2029
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2967
+ function shouldEmitResolvedObjectProperty(property, declaration) {
2968
+ if (property.name.startsWith("__@")) {
2969
+ return false;
2970
+ }
2971
+ if (declaration !== void 0 && "name" in declaration && declaration.name !== void 0) {
2972
+ const name = declaration.name;
2973
+ if (ts3.isComputedPropertyName(name) || ts3.isPrivateIdentifier(name)) {
2974
+ return false;
2975
+ }
2976
+ if (!ts3.isIdentifier(name) && !ts3.isStringLiteral(name) && !ts3.isNumericLiteral(name)) {
2977
+ return false;
2978
+ }
2979
+ }
2980
+ return true;
2981
+ }
2982
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2983
+ const collectedDiagnostics = diagnostics ?? [];
2030
2984
  const typeName = getNamedTypeName(type);
2031
2985
  const namedTypeName = typeName ?? void 0;
2032
2986
  const namedDecl = getNamedTypeDeclaration(type);
2033
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2987
+ const referenceTypeArguments = extractReferenceTypeArguments(
2988
+ type,
2989
+ checker,
2990
+ file,
2991
+ typeRegistry,
2992
+ visiting,
2993
+ sourceNode,
2994
+ metadataPolicy,
2995
+ extensionRegistry,
2996
+ collectedDiagnostics
2997
+ );
2998
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
2999
+ namedTypeName,
3000
+ referenceTypeArguments.map((argument) => argument.tsType),
3001
+ checker
3002
+ ) : void 0;
3003
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
3004
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2034
3005
  const clearNamedTypeRegistration = () => {
2035
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
3006
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2036
3007
  return;
2037
3008
  }
2038
- Reflect.deleteProperty(typeRegistry, namedTypeName);
3009
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2039
3010
  };
2040
3011
  if (visiting.has(type)) {
2041
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2042
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3012
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3013
+ return {
3014
+ kind: "reference",
3015
+ name: registryTypeName,
3016
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3017
+ };
2043
3018
  }
2044
3019
  return { kind: "object", properties: [], additionalProperties: false };
2045
3020
  }
2046
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2047
- typeRegistry[namedTypeName] = {
2048
- name: namedTypeName,
3021
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
3022
+ typeRegistry[registryTypeName] = {
3023
+ name: registryTypeName,
2049
3024
  type: RESOLVING_TYPE_PLACEHOLDER,
2050
3025
  provenance: provenanceForDeclaration(namedDecl, file)
2051
3026
  };
2052
3027
  }
2053
3028
  visiting.add(type);
2054
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2055
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
3029
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
3030
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2056
3031
  visiting.delete(type);
2057
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3032
+ return {
3033
+ kind: "reference",
3034
+ name: registryTypeName,
3035
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3036
+ };
2058
3037
  }
2059
3038
  }
2060
3039
  const recordNode = tryResolveRecordType(
@@ -2063,25 +3042,36 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2063
3042
  file,
2064
3043
  typeRegistry,
2065
3044
  visiting,
3045
+ metadataPolicy,
2066
3046
  extensionRegistry,
2067
- diagnostics
3047
+ collectedDiagnostics
2068
3048
  );
2069
3049
  if (recordNode) {
2070
3050
  visiting.delete(type);
2071
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2072
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
3051
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3052
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2073
3053
  if (!isRecursiveRecord) {
2074
3054
  clearNamedTypeRegistration();
2075
3055
  return recordNode;
2076
3056
  }
2077
3057
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2078
- typeRegistry[namedTypeName] = {
2079
- name: namedTypeName,
3058
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", registryTypeName, namedDecl, {
3059
+ checker,
3060
+ declaration: namedDecl,
3061
+ subjectType: type
3062
+ }) : void 0;
3063
+ typeRegistry[registryTypeName] = {
3064
+ name: registryTypeName,
3065
+ ...metadata !== void 0 && { metadata },
2080
3066
  type: recordNode,
2081
3067
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2082
3068
  provenance: provenanceForDeclaration(namedDecl, file)
2083
3069
  };
2084
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3070
+ return {
3071
+ kind: "reference",
3072
+ name: registryTypeName,
3073
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3074
+ };
2085
3075
  }
2086
3076
  return recordNode;
2087
3077
  }
@@ -2092,12 +3082,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2092
3082
  file,
2093
3083
  typeRegistry,
2094
3084
  visiting,
2095
- diagnostics ?? [],
3085
+ metadataPolicy,
3086
+ collectedDiagnostics,
2096
3087
  extensionRegistry
2097
3088
  );
2098
3089
  for (const prop of type.getProperties()) {
2099
3090
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
2100
3091
  if (!declaration) continue;
3092
+ if (!shouldEmitResolvedObjectProperty(prop, declaration)) continue;
2101
3093
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
2102
3094
  const optional = !!(prop.flags & ts3.SymbolFlags.Optional);
2103
3095
  const propTypeNode = resolveTypeNode(
@@ -2107,12 +3099,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2107
3099
  typeRegistry,
2108
3100
  visiting,
2109
3101
  declaration,
3102
+ metadataPolicy,
2110
3103
  extensionRegistry,
2111
- diagnostics
3104
+ collectedDiagnostics
2112
3105
  );
2113
3106
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2114
3107
  properties.push({
2115
3108
  name: prop.name,
3109
+ ...fieldNodeInfo?.metadata !== void 0 && { metadata: fieldNodeInfo.metadata },
2116
3110
  type: propTypeNode,
2117
3111
  optional,
2118
3112
  constraints: fieldNodeInfo?.constraints ?? [],
@@ -2123,22 +3117,40 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2123
3117
  visiting.delete(type);
2124
3118
  const objectNode = {
2125
3119
  kind: "object",
2126
- properties,
3120
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
3121
+ properties,
3122
+ namedDecl,
3123
+ type,
3124
+ checker,
3125
+ file,
3126
+ collectedDiagnostics,
3127
+ metadataPolicy
3128
+ ) : properties,
2127
3129
  additionalProperties: true
2128
3130
  };
2129
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
3131
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2130
3132
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2131
- typeRegistry[namedTypeName] = {
2132
- name: namedTypeName,
3133
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", registryTypeName, namedDecl, {
3134
+ checker,
3135
+ declaration: namedDecl,
3136
+ subjectType: type
3137
+ }) : void 0;
3138
+ typeRegistry[registryTypeName] = {
3139
+ name: registryTypeName,
3140
+ ...metadata !== void 0 && { metadata },
2133
3141
  type: objectNode,
2134
3142
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2135
3143
  provenance: provenanceForDeclaration(namedDecl, file)
2136
3144
  };
2137
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3145
+ return {
3146
+ kind: "reference",
3147
+ name: registryTypeName,
3148
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3149
+ };
2138
3150
  }
2139
3151
  return objectNode;
2140
3152
  }
2141
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, diagnostics, extensionRegistry) {
3153
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, metadataPolicy, diagnostics, extensionRegistry) {
2142
3154
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
2143
3155
  (s) => s?.declarations != null && s.declarations.length > 0
2144
3156
  );
@@ -2159,10 +3171,12 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2159
3171
  visiting,
2160
3172
  diagnostics,
2161
3173
  hostType,
3174
+ metadataPolicy,
2162
3175
  extensionRegistry
2163
3176
  );
2164
3177
  if (fieldNode) {
2165
3178
  map.set(fieldNode.name, {
3179
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
2166
3180
  constraints: [...fieldNode.constraints],
2167
3181
  annotations: [...fieldNode.annotations],
2168
3182
  provenance: fieldNode.provenance
@@ -2180,6 +3194,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2180
3194
  file,
2181
3195
  typeRegistry,
2182
3196
  visiting,
3197
+ metadataPolicy,
2183
3198
  checker.getTypeAtLocation(interfaceDecl),
2184
3199
  diagnostics,
2185
3200
  extensionRegistry
@@ -2193,6 +3208,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2193
3208
  file,
2194
3209
  typeRegistry,
2195
3210
  visiting,
3211
+ metadataPolicy,
2196
3212
  checker.getTypeAtLocation(typeAliasDecl),
2197
3213
  diagnostics,
2198
3214
  extensionRegistry
@@ -2244,7 +3260,7 @@ function isNullishTypeNode(typeNode) {
2244
3260
  }
2245
3261
  return ts3.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts3.SyntaxKind.NullKeyword || typeNode.literal.kind === ts3.SyntaxKind.UndefinedKeyword);
2246
3262
  }
2247
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, hostType, diagnostics, extensionRegistry) {
3263
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, metadataPolicy, hostType, diagnostics, extensionRegistry) {
2248
3264
  const map = /* @__PURE__ */ new Map();
2249
3265
  for (const member of members) {
2250
3266
  if (ts3.isPropertySignature(member)) {
@@ -2256,10 +3272,12 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, h
2256
3272
  visiting,
2257
3273
  diagnostics,
2258
3274
  hostType,
3275
+ metadataPolicy,
2259
3276
  extensionRegistry
2260
3277
  );
2261
3278
  if (fieldNode) {
2262
3279
  map.set(fieldNode.name, {
3280
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
2263
3281
  constraints: [...fieldNode.constraints],
2264
3282
  annotations: [...fieldNode.annotations],
2265
3283
  provenance: fieldNode.provenance
@@ -2290,6 +3308,7 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegis
2290
3308
  {},
2291
3309
  /* @__PURE__ */ new Set(),
2292
3310
  aliasDecl.type,
3311
+ void 0,
2293
3312
  extensionRegistry
2294
3313
  );
2295
3314
  const constraints = extractJSDocConstraintNodes(
@@ -2475,15 +3494,27 @@ function findInterfaceByName(sourceFile, interfaceName) {
2475
3494
  function findTypeAliasByName(sourceFile, aliasName) {
2476
3495
  return findNodeByName(sourceFile, aliasName, ts4.isTypeAliasDeclaration, (n) => n.name.text);
2477
3496
  }
2478
- function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry) {
3497
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry, metadataPolicy) {
2479
3498
  const analysisFilePath = path.resolve(filePath);
2480
3499
  const classDecl = findClassByName(ctx.sourceFile, typeName);
2481
3500
  if (classDecl !== null) {
2482
- return analyzeClassToIR(classDecl, ctx.checker, analysisFilePath, extensionRegistry);
3501
+ return analyzeClassToIR(
3502
+ classDecl,
3503
+ ctx.checker,
3504
+ analysisFilePath,
3505
+ extensionRegistry,
3506
+ metadataPolicy
3507
+ );
2483
3508
  }
2484
3509
  const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
2485
3510
  if (interfaceDecl !== null) {
2486
- return analyzeInterfaceToIR(interfaceDecl, ctx.checker, analysisFilePath, extensionRegistry);
3511
+ return analyzeInterfaceToIR(
3512
+ interfaceDecl,
3513
+ ctx.checker,
3514
+ analysisFilePath,
3515
+ extensionRegistry,
3516
+ metadataPolicy
3517
+ );
2487
3518
  }
2488
3519
  const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
2489
3520
  if (typeAlias !== null) {
@@ -2491,7 +3522,8 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
2491
3522
  typeAlias,
2492
3523
  ctx.checker,
2493
3524
  analysisFilePath,
2494
- extensionRegistry
3525
+ extensionRegistry,
3526
+ metadataPolicy
2495
3527
  );
2496
3528
  if (result.ok) {
2497
3529
  return result.analysis;
@@ -2506,6 +3538,120 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
2506
3538
  // src/generators/class-schema.ts
2507
3539
  var ts5 = require("typescript");
2508
3540
 
3541
+ // src/metadata/collision-guards.ts
3542
+ function assertUniqueSerializedNames(entries, scope) {
3543
+ const seen = /* @__PURE__ */ new Map();
3544
+ for (const entry of entries) {
3545
+ const previous = seen.get(entry.serializedName);
3546
+ if (previous !== void 0) {
3547
+ if (previous.logicalName === entry.logicalName && previous.category === entry.category) {
3548
+ continue;
3549
+ }
3550
+ throw new Error(
3551
+ `Serialized name collision in ${scope}: ${previous.category} "${previous.logicalName}" and ${entry.category} "${entry.logicalName}" both resolve to "${entry.serializedName}".`
3552
+ );
3553
+ }
3554
+ seen.set(entry.serializedName, entry);
3555
+ }
3556
+ }
3557
+ function collectFlattenedFields(elements) {
3558
+ const fields = [];
3559
+ for (const element of elements) {
3560
+ switch (element.kind) {
3561
+ case "field":
3562
+ fields.push(element);
3563
+ break;
3564
+ case "group":
3565
+ case "conditional":
3566
+ fields.push(...collectFlattenedFields(element.elements));
3567
+ break;
3568
+ default: {
3569
+ const exhaustive = element;
3570
+ void exhaustive;
3571
+ }
3572
+ }
3573
+ }
3574
+ return fields;
3575
+ }
3576
+ function validateObjectProperties(properties, scope) {
3577
+ assertUniqueSerializedNames(
3578
+ properties.map((property) => ({
3579
+ logicalName: property.name,
3580
+ serializedName: getSerializedName(property.name, property.metadata),
3581
+ category: "object property"
3582
+ })),
3583
+ scope
3584
+ );
3585
+ for (const property of properties) {
3586
+ validateTypeNode(
3587
+ property.type,
3588
+ `${scope}.${getSerializedName(property.name, property.metadata)}`
3589
+ );
3590
+ }
3591
+ }
3592
+ function validateTypeNode(type, scope) {
3593
+ switch (type.kind) {
3594
+ case "array":
3595
+ validateTypeNode(type.items, `${scope}[]`);
3596
+ break;
3597
+ case "object":
3598
+ validateObjectProperties(type.properties, scope);
3599
+ break;
3600
+ case "record":
3601
+ validateTypeNode(type.valueType, `${scope}.*`);
3602
+ break;
3603
+ case "union":
3604
+ type.members.forEach((member, index) => {
3605
+ validateTypeNode(member, `${scope}|${String(index)}`);
3606
+ });
3607
+ break;
3608
+ case "reference":
3609
+ case "primitive":
3610
+ case "enum":
3611
+ case "dynamic":
3612
+ case "custom":
3613
+ break;
3614
+ default: {
3615
+ const exhaustive = type;
3616
+ void exhaustive;
3617
+ }
3618
+ }
3619
+ }
3620
+ function validateTypeDefinitions(typeRegistry) {
3621
+ const definitions = Object.values(typeRegistry);
3622
+ assertUniqueSerializedNames(
3623
+ definitions.map((definition) => ({
3624
+ logicalName: definition.name,
3625
+ serializedName: getSerializedName(definition.name, definition.metadata),
3626
+ category: "type definition"
3627
+ })),
3628
+ "$defs"
3629
+ );
3630
+ for (const definition of definitions) {
3631
+ validateTypeDefinition(definition);
3632
+ }
3633
+ }
3634
+ function validateTypeDefinition(definition) {
3635
+ validateTypeNode(
3636
+ definition.type,
3637
+ `type "${getSerializedName(definition.name, definition.metadata)}"`
3638
+ );
3639
+ }
3640
+ function assertNoSerializedNameCollisions(ir) {
3641
+ assertUniqueSerializedNames(
3642
+ collectFlattenedFields(ir.elements).map((field) => ({
3643
+ logicalName: field.name,
3644
+ serializedName: getSerializedName(field.name, field.metadata),
3645
+ category: "field"
3646
+ })),
3647
+ "form root"
3648
+ );
3649
+ for (const field of collectFlattenedFields(ir.elements)) {
3650
+ validateTypeNode(field.type, `field "${getSerializedName(field.name, field.metadata)}"`);
3651
+ }
3652
+ validateTypeDefinitions(ir.typeRegistry);
3653
+ }
3654
+
2509
3655
  // src/json-schema/ir-generator.ts
2510
3656
  function makeContext(options) {
2511
3657
  const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
@@ -2516,19 +3662,33 @@ function makeContext(options) {
2516
3662
  }
2517
3663
  return {
2518
3664
  defs: {},
3665
+ typeNameMap: {},
3666
+ typeRegistry: {},
2519
3667
  extensionRegistry: options?.extensionRegistry,
2520
3668
  vendorPrefix
2521
3669
  };
2522
3670
  }
2523
3671
  function generateJsonSchemaFromIR(ir, options) {
2524
- const ctx = makeContext(options);
3672
+ assertNoSerializedNameCollisions(ir);
3673
+ const ctx = {
3674
+ ...makeContext(options),
3675
+ typeRegistry: ir.typeRegistry,
3676
+ typeNameMap: Object.fromEntries(
3677
+ Object.entries(ir.typeRegistry).map(([name, typeDef]) => [
3678
+ name,
3679
+ getSerializedName(name, typeDef.metadata)
3680
+ ])
3681
+ )
3682
+ };
2525
3683
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
2526
- ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
3684
+ const schemaName = ctx.typeNameMap[name] ?? name;
3685
+ ctx.defs[schemaName] = generateTypeNode(typeDef.type, ctx);
3686
+ applyResolvedMetadata(ctx.defs[schemaName], typeDef.metadata);
2527
3687
  if (typeDef.constraints && typeDef.constraints.length > 0) {
2528
- applyConstraints(ctx.defs[name], typeDef.constraints, ctx);
3688
+ applyConstraints(ctx.defs[schemaName], typeDef.constraints, ctx);
2529
3689
  }
2530
3690
  if (typeDef.annotations && typeDef.annotations.length > 0) {
2531
- applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
3691
+ applyAnnotations(ctx.defs[schemaName], typeDef.annotations, ctx);
2532
3692
  }
2533
3693
  }
2534
3694
  const properties = {};
@@ -2541,6 +3701,7 @@ function generateJsonSchemaFromIR(ir, options) {
2541
3701
  properties,
2542
3702
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
2543
3703
  };
3704
+ applyResolvedMetadata(result, ir.metadata);
2544
3705
  if (ir.annotations && ir.annotations.length > 0) {
2545
3706
  applyAnnotations(result, ir.annotations, ctx);
2546
3707
  }
@@ -2553,9 +3714,9 @@ function collectFields(elements, properties, required, ctx) {
2553
3714
  for (const element of elements) {
2554
3715
  switch (element.kind) {
2555
3716
  case "field":
2556
- properties[element.name] = generateFieldSchema(element, ctx);
3717
+ properties[getSerializedName(element.name, element.metadata)] = generateFieldSchema(element, ctx);
2557
3718
  if (element.required) {
2558
- required.push(element.name);
3719
+ required.push(getSerializedName(element.name, element.metadata));
2559
3720
  }
2560
3721
  break;
2561
3722
  case "group":
@@ -2599,6 +3760,7 @@ function generateFieldSchema(field, ctx) {
2599
3760
  rootAnnotations.push(annotation);
2600
3761
  }
2601
3762
  }
3763
+ applyResolvedMetadata(schema, field.metadata);
2602
3764
  applyAnnotations(schema, rootAnnotations, ctx);
2603
3765
  if (itemStringSchema !== void 0) {
2604
3766
  applyAnnotations(itemStringSchema, itemAnnotations, ctx);
@@ -2606,7 +3768,7 @@ function generateFieldSchema(field, ctx) {
2606
3768
  if (pathConstraints.length === 0) {
2607
3769
  return schema;
2608
3770
  }
2609
- return applyPathTargetedConstraints(schema, pathConstraints, ctx);
3771
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx, field.type);
2610
3772
  }
2611
3773
  function isStringItemConstraint(constraint) {
2612
3774
  switch (constraint.constraintKind) {
@@ -2618,9 +3780,11 @@ function isStringItemConstraint(constraint) {
2618
3780
  return false;
2619
3781
  }
2620
3782
  }
2621
- function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
3783
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx, typeNode) {
2622
3784
  if (schema.type === "array" && schema.items) {
2623
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
3785
+ const referencedType = typeNode?.kind === "reference" ? resolveReferencedType(typeNode, ctx) : void 0;
3786
+ const nestedType = typeNode?.kind === "array" ? typeNode.items : referencedType?.kind === "array" ? referencedType.items : void 0;
3787
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx, nestedType);
2624
3788
  return schema;
2625
3789
  }
2626
3790
  const byTarget = /* @__PURE__ */ new Map();
@@ -2635,7 +3799,7 @@ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
2635
3799
  for (const [target, constraints] of byTarget) {
2636
3800
  const subSchema = {};
2637
3801
  applyConstraints(subSchema, constraints, ctx);
2638
- propertyOverrides[target] = subSchema;
3802
+ propertyOverrides[resolveSerializedPropertyName(target, typeNode, ctx)] = subSchema;
2639
3803
  }
2640
3804
  if (schema.$ref) {
2641
3805
  const { $ref, ...rest } = schema;
@@ -2683,7 +3847,7 @@ function generateTypeNode(type, ctx) {
2683
3847
  case "union":
2684
3848
  return generateUnionType(type, ctx);
2685
3849
  case "reference":
2686
- return generateReferenceType(type);
3850
+ return generateReferenceType(type, ctx);
2687
3851
  case "dynamic":
2688
3852
  return generateDynamicType(type);
2689
3853
  case "custom":
@@ -2724,9 +3888,10 @@ function generateObjectType(type, ctx) {
2724
3888
  const properties = {};
2725
3889
  const required = [];
2726
3890
  for (const prop of type.properties) {
2727
- properties[prop.name] = generatePropertySchema(prop, ctx);
3891
+ const propertyName = getSerializedName(prop.name, prop.metadata);
3892
+ properties[propertyName] = generatePropertySchema(prop, ctx);
2728
3893
  if (!prop.optional) {
2729
- required.push(prop.name);
3894
+ required.push(propertyName);
2730
3895
  }
2731
3896
  }
2732
3897
  const schema = { type: "object", properties };
@@ -2747,6 +3912,7 @@ function generateRecordType(type, ctx) {
2747
3912
  function generatePropertySchema(prop, ctx) {
2748
3913
  const schema = generateTypeNode(prop.type, ctx);
2749
3914
  applyConstraints(schema, prop.constraints, ctx);
3915
+ applyResolvedMetadata(schema, prop.metadata);
2750
3916
  applyAnnotations(schema, prop.annotations, ctx);
2751
3917
  return schema;
2752
3918
  }
@@ -2775,8 +3941,28 @@ function isNullableUnion(type) {
2775
3941
  ).length;
2776
3942
  return nullCount === 1;
2777
3943
  }
2778
- function generateReferenceType(type) {
2779
- return { $ref: `#/$defs/${type.name}` };
3944
+ function generateReferenceType(type, ctx) {
3945
+ return { $ref: `#/$defs/${ctx.typeNameMap[type.name] ?? type.name}` };
3946
+ }
3947
+ function applyResolvedMetadata(schema, metadata) {
3948
+ const displayName = getDisplayName(metadata);
3949
+ if (displayName !== void 0) {
3950
+ schema.title = displayName;
3951
+ }
3952
+ }
3953
+ function resolveReferencedType(type, ctx) {
3954
+ return ctx.typeRegistry[type.name]?.type;
3955
+ }
3956
+ function resolveSerializedPropertyName(logicalName, typeNode, ctx) {
3957
+ if (typeNode?.kind === "object") {
3958
+ const property = typeNode.properties.find((candidate) => candidate.name === logicalName);
3959
+ return property === void 0 ? logicalName : getSerializedName(property.name, property.metadata);
3960
+ }
3961
+ if (typeNode?.kind === "reference") {
3962
+ const referencedType = resolveReferencedType(typeNode, ctx);
3963
+ return referencedType === void 0 ? logicalName : resolveSerializedPropertyName(logicalName, referencedType, ctx);
3964
+ }
3965
+ return logicalName;
2780
3966
  }
2781
3967
  function generateDynamicType(type) {
2782
3968
  if (type.dynamicKind === "enum") {
@@ -2856,7 +4042,7 @@ function applyAnnotations(schema, annotations, ctx) {
2856
4042
  for (const annotation of annotations) {
2857
4043
  switch (annotation.annotationKind) {
2858
4044
  case "displayName":
2859
- schema.title = annotation.value;
4045
+ schema.title ??= annotation.value;
2860
4046
  break;
2861
4047
  case "description":
2862
4048
  schema.description = annotation.value;
@@ -3106,13 +4292,21 @@ function combineRules(parentRule, childRule) {
3106
4292
  }
3107
4293
  };
3108
4294
  }
3109
- function fieldNodeToControl(field, parentRule) {
3110
- const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
4295
+ function getFieldDisplayName(field) {
4296
+ const resolvedDisplayName = getDisplayName(field.metadata);
4297
+ if (resolvedDisplayName !== void 0) {
4298
+ return resolvedDisplayName;
4299
+ }
4300
+ return field.annotations.find((annotation) => annotation.annotationKind === "displayName")?.value;
4301
+ }
4302
+ function fieldNodeToControl(field, fieldNameMap, parentRule) {
3111
4303
  const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
4304
+ const serializedName = fieldNameMap.get(field.name) ?? getSerializedName(field.name, field.metadata);
4305
+ const displayName = getFieldDisplayName(field);
3112
4306
  const control = {
3113
4307
  type: "Control",
3114
- scope: fieldToScope(field.name),
3115
- ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
4308
+ scope: fieldToScope(serializedName),
4309
+ ...displayName !== void 0 && { label: displayName },
3116
4310
  ...placeholderAnnotation !== void 0 && {
3117
4311
  options: { placeholder: placeholderAnnotation.value }
3118
4312
  },
@@ -3120,30 +4314,30 @@ function fieldNodeToControl(field, parentRule) {
3120
4314
  };
3121
4315
  return control;
3122
4316
  }
3123
- function groupNodeToLayout(group, parentRule) {
4317
+ function groupNodeToLayout(group, fieldNameMap, parentRule) {
3124
4318
  return {
3125
4319
  type: "Group",
3126
4320
  label: group.label,
3127
- elements: irElementsToUiSchema(group.elements, parentRule),
4321
+ elements: irElementsToUiSchema(group.elements, fieldNameMap, parentRule),
3128
4322
  ...parentRule !== void 0 && { rule: parentRule }
3129
4323
  };
3130
4324
  }
3131
- function irElementsToUiSchema(elements, parentRule) {
4325
+ function irElementsToUiSchema(elements, fieldNameMap, parentRule) {
3132
4326
  const result = [];
3133
4327
  for (const element of elements) {
3134
4328
  switch (element.kind) {
3135
4329
  case "field": {
3136
- result.push(fieldNodeToControl(element, parentRule));
4330
+ result.push(fieldNodeToControl(element, fieldNameMap, parentRule));
3137
4331
  break;
3138
4332
  }
3139
4333
  case "group": {
3140
- result.push(groupNodeToLayout(element, parentRule));
4334
+ result.push(groupNodeToLayout(element, fieldNameMap, parentRule));
3141
4335
  break;
3142
4336
  }
3143
4337
  case "conditional": {
3144
- const newRule = createShowRule(element.fieldName, element.value);
4338
+ const newRule = createShowRule(fieldNameMap.get(element.fieldName) ?? element.fieldName, element.value);
3145
4339
  const combinedRule = parentRule !== void 0 ? combineRules(parentRule, newRule) : newRule;
3146
- const childElements = irElementsToUiSchema(element.elements, combinedRule);
4340
+ const childElements = irElementsToUiSchema(element.elements, fieldNameMap, combinedRule);
3147
4341
  result.push(...childElements);
3148
4342
  break;
3149
4343
  }
@@ -3157,17 +4351,40 @@ function irElementsToUiSchema(elements, parentRule) {
3157
4351
  return result;
3158
4352
  }
3159
4353
  function generateUiSchemaFromIR(ir) {
4354
+ assertNoSerializedNameCollisions(ir);
4355
+ const fieldNameMap = collectFieldNameMap(ir.elements);
3160
4356
  const result = {
3161
4357
  type: "VerticalLayout",
3162
- elements: irElementsToUiSchema(ir.elements)
4358
+ elements: irElementsToUiSchema(ir.elements, fieldNameMap)
3163
4359
  };
3164
4360
  return parseOrThrow(uiSchema, result, "UI Schema");
3165
4361
  }
4362
+ function collectFieldNameMap(elements) {
4363
+ const map = /* @__PURE__ */ new Map();
4364
+ for (const element of elements) {
4365
+ switch (element.kind) {
4366
+ case "field":
4367
+ map.set(element.name, getSerializedName(element.name, element.metadata));
4368
+ break;
4369
+ case "group":
4370
+ case "conditional":
4371
+ for (const [key, value] of collectFieldNameMap(element.elements)) {
4372
+ map.set(key, value);
4373
+ }
4374
+ break;
4375
+ default: {
4376
+ const _exhaustive = element;
4377
+ void _exhaustive;
4378
+ }
4379
+ }
4380
+ }
4381
+ return map;
4382
+ }
3166
4383
 
3167
4384
  // src/validate/constraint-validator.ts
3168
- var import_internal2 = require("@formspec/analysis/internal");
4385
+ var import_internal3 = require("@formspec/analysis/internal");
3169
4386
  function validateFieldNode(ctx, field) {
3170
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
4387
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3171
4388
  field.name,
3172
4389
  field.type,
3173
4390
  field.constraints,
@@ -3185,7 +4402,7 @@ function validateFieldNode(ctx, field) {
3185
4402
  }
3186
4403
  function validateObjectProperty(ctx, parentName, property) {
3187
4404
  const qualifiedName = `${parentName}.${property.name}`;
3188
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
4405
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3189
4406
  qualifiedName,
3190
4407
  property.type,
3191
4408
  property.constraints,
@@ -3245,7 +4462,11 @@ function generateClassSchemas(analysis, source, options) {
3245
4462
  if (errorDiagnostics !== void 0 && errorDiagnostics.length > 0) {
3246
4463
  throw new Error(formatValidationError(errorDiagnostics));
3247
4464
  }
3248
- const ir = canonicalizeTSDoc(analysis, source);
4465
+ const ir = canonicalizeTSDoc(
4466
+ analysis,
4467
+ source,
4468
+ options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
4469
+ );
3249
4470
  const validationResult = validateIR(ir, {
3250
4471
  ...options?.extensionRegistry !== void 0 && {
3251
4472
  extensionRegistry: options.extensionRegistry
@@ -3358,7 +4579,24 @@ var import_internals5 = require("@formspec/core/internals");
3358
4579
  function typeToJsonSchema(type, checker) {
3359
4580
  const typeRegistry = {};
3360
4581
  const visiting = /* @__PURE__ */ new Set();
3361
- const typeNode = resolveTypeNode(type, checker, "", typeRegistry, visiting);
4582
+ const diagnostics = [];
4583
+ const typeNode = resolveTypeNode(
4584
+ type,
4585
+ checker,
4586
+ "",
4587
+ typeRegistry,
4588
+ visiting,
4589
+ void 0,
4590
+ void 0,
4591
+ void 0,
4592
+ diagnostics
4593
+ );
4594
+ if (diagnostics.length > 0) {
4595
+ const diagnosticDetails = diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("; ");
4596
+ throw new Error(
4597
+ `FormSpec validation failed while resolving method schema types. ${diagnosticDetails}`
4598
+ );
4599
+ }
3362
4600
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
3363
4601
  const ir = {
3364
4602
  kind: "form-ir",