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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -2
  3. package/dist/analyzer/class-analyzer.d.ts +12 -6
  4. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  5. package/dist/analyzer/program.d.ts +3 -2
  6. package/dist/analyzer/program.d.ts.map +1 -1
  7. package/dist/browser.cjs +485 -76
  8. package/dist/browser.cjs.map +1 -1
  9. package/dist/browser.js +486 -77
  10. package/dist/browser.js.map +1 -1
  11. package/dist/build-alpha.d.ts +18 -2
  12. package/dist/build-beta.d.ts +18 -2
  13. package/dist/build-internal.d.ts +18 -2
  14. package/dist/build.d.ts +18 -2
  15. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts +5 -2
  16. package/dist/canonicalize/chain-dsl-canonicalizer.d.ts.map +1 -1
  17. package/dist/canonicalize/tsdoc-canonicalizer.d.ts +5 -1
  18. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  19. package/dist/cli.cjs +1460 -143
  20. package/dist/cli.cjs.map +1 -1
  21. package/dist/cli.js +1458 -139
  22. package/dist/cli.js.map +1 -1
  23. package/dist/generators/class-schema.d.ts +6 -1
  24. package/dist/generators/class-schema.d.ts.map +1 -1
  25. package/dist/generators/method-schema.d.ts.map +1 -1
  26. package/dist/generators/mixed-authoring.d.ts.map +1 -1
  27. package/dist/index.cjs +1425 -141
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1425 -139
  32. package/dist/index.js.map +1 -1
  33. package/dist/internals.cjs +1416 -178
  34. package/dist/internals.cjs.map +1 -1
  35. package/dist/internals.js +1416 -176
  36. package/dist/internals.js.map +1 -1
  37. package/dist/json-schema/generator.d.ts +3 -1
  38. package/dist/json-schema/generator.d.ts.map +1 -1
  39. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  40. package/dist/metadata/collision-guards.d.ts +3 -0
  41. package/dist/metadata/collision-guards.d.ts.map +1 -0
  42. package/dist/metadata/index.d.ts +7 -0
  43. package/dist/metadata/index.d.ts.map +1 -0
  44. package/dist/metadata/policy.d.ts +11 -0
  45. package/dist/metadata/policy.d.ts.map +1 -0
  46. package/dist/metadata/resolve.d.ts +20 -0
  47. package/dist/metadata/resolve.d.ts.map +1 -0
  48. package/dist/ui-schema/generator.d.ts +11 -2
  49. package/dist/ui-schema/generator.d.ts.map +1 -1
  50. package/dist/ui-schema/ir-generator.d.ts +2 -1
  51. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  52. package/package.json +7 -6
package/dist/internals.js CHANGED
@@ -1,5 +1,281 @@
1
1
  // src/canonicalize/chain-dsl-canonicalizer.ts
2
- import { IR_VERSION } from "@formspec/core/internals";
2
+ import { IR_VERSION, _getFormSpecMetadataPolicy } from "@formspec/core/internals";
3
+
4
+ // src/metadata/policy.ts
5
+ var NOOP_INFLECT = () => "";
6
+ function normalizePluralization(input) {
7
+ if (input?.mode === "infer-if-missing") {
8
+ return {
9
+ mode: "infer-if-missing",
10
+ infer: () => "",
11
+ inflect: input.inflect
12
+ };
13
+ }
14
+ if (input?.mode === "require-explicit") {
15
+ return {
16
+ mode: "require-explicit",
17
+ infer: () => "",
18
+ inflect: NOOP_INFLECT
19
+ };
20
+ }
21
+ return {
22
+ mode: "disabled",
23
+ infer: () => "",
24
+ inflect: NOOP_INFLECT
25
+ };
26
+ }
27
+ function normalizeScalarPolicy(input) {
28
+ if (input?.mode === "infer-if-missing") {
29
+ return {
30
+ mode: "infer-if-missing",
31
+ infer: input.infer,
32
+ pluralization: normalizePluralization(input.pluralization)
33
+ };
34
+ }
35
+ if (input?.mode === "require-explicit") {
36
+ return {
37
+ mode: "require-explicit",
38
+ infer: () => "",
39
+ pluralization: normalizePluralization(input.pluralization)
40
+ };
41
+ }
42
+ return {
43
+ mode: "disabled",
44
+ infer: () => "",
45
+ pluralization: normalizePluralization(input?.pluralization)
46
+ };
47
+ }
48
+ function normalizeDeclarationPolicy(input) {
49
+ return {
50
+ apiName: normalizeScalarPolicy(input?.apiName),
51
+ displayName: normalizeScalarPolicy(input?.displayName)
52
+ };
53
+ }
54
+ function normalizeMetadataPolicy(input) {
55
+ return {
56
+ type: normalizeDeclarationPolicy(input?.type),
57
+ field: normalizeDeclarationPolicy(input?.field),
58
+ method: normalizeDeclarationPolicy(input?.method)
59
+ };
60
+ }
61
+ function getDeclarationMetadataPolicy(policy, declarationKind) {
62
+ return policy[declarationKind];
63
+ }
64
+ function makeMetadataContext(surface, declarationKind, logicalName, buildContext) {
65
+ return {
66
+ surface,
67
+ declarationKind,
68
+ logicalName,
69
+ ...buildContext !== void 0 && { buildContext }
70
+ };
71
+ }
72
+
73
+ // src/metadata/resolve.ts
74
+ function toExplicitScalar(value) {
75
+ return value !== void 0 && value.trim() !== "" ? { value, source: "explicit" } : void 0;
76
+ }
77
+ function toExplicitResolvedMetadata(explicit) {
78
+ if (explicit === void 0) {
79
+ return void 0;
80
+ }
81
+ const apiName = toExplicitScalar(explicit.apiName);
82
+ const displayName = toExplicitScalar(explicit.displayName);
83
+ const apiNamePlural = toExplicitScalar(explicit.apiNamePlural);
84
+ const displayNamePlural = toExplicitScalar(explicit.displayNamePlural);
85
+ const metadata = {
86
+ ...apiName !== void 0 && { apiName },
87
+ ...displayName !== void 0 && { displayName },
88
+ ...apiNamePlural !== void 0 && { apiNamePlural },
89
+ ...displayNamePlural !== void 0 && { displayNamePlural }
90
+ };
91
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
92
+ }
93
+ function resolveScalar(current, policy, context, metadataLabel) {
94
+ if (current !== void 0) {
95
+ return current;
96
+ }
97
+ if (policy.mode === "require-explicit") {
98
+ throw new Error(
99
+ `Metadata policy requires explicit ${metadataLabel} for ${context.declarationKind} "${context.logicalName}" on the ${context.surface} surface.`
100
+ );
101
+ }
102
+ if (policy.mode !== "infer-if-missing") {
103
+ return void 0;
104
+ }
105
+ const inferredValue = policy.infer(context);
106
+ return inferredValue.trim() !== "" ? { value: inferredValue, source: "inferred" } : void 0;
107
+ }
108
+ function resolvePlural(current, singular, policy, context, metadataLabel) {
109
+ if (current !== void 0) {
110
+ return current;
111
+ }
112
+ if (policy.mode === "require-explicit") {
113
+ throw new Error(
114
+ `Metadata policy requires explicit ${metadataLabel} for ${context.declarationKind} "${context.logicalName}" on the ${context.surface} surface.`
115
+ );
116
+ }
117
+ if (singular === void 0 || policy.mode !== "infer-if-missing") {
118
+ return void 0;
119
+ }
120
+ const pluralValue = policy.inflect({ ...context, singular: singular.value });
121
+ return pluralValue.trim() !== "" ? { value: pluralValue, source: "inferred" } : void 0;
122
+ }
123
+ function resolveResolvedMetadata(current, policy, context) {
124
+ const apiName = resolveScalar(current?.apiName, policy.apiName, context, "apiName");
125
+ const displayName = resolveScalar(
126
+ current?.displayName,
127
+ policy.displayName,
128
+ context,
129
+ "displayName"
130
+ );
131
+ const apiNamePlural = resolvePlural(
132
+ current?.apiNamePlural,
133
+ apiName,
134
+ policy.apiName.pluralization,
135
+ context,
136
+ "apiNamePlural"
137
+ );
138
+ const displayNamePlural = resolvePlural(
139
+ current?.displayNamePlural,
140
+ displayName,
141
+ policy.displayName.pluralization,
142
+ context,
143
+ "displayNamePlural"
144
+ );
145
+ if (apiName === void 0 && displayName === void 0 && apiNamePlural === void 0 && displayNamePlural === void 0) {
146
+ return void 0;
147
+ }
148
+ return {
149
+ ...apiName !== void 0 && { apiName },
150
+ ...displayName !== void 0 && { displayName },
151
+ ...apiNamePlural !== void 0 && { apiNamePlural },
152
+ ...displayNamePlural !== void 0 && { displayNamePlural }
153
+ };
154
+ }
155
+ function resolveTypeNodeMetadata(type, options) {
156
+ switch (type.kind) {
157
+ case "array":
158
+ return {
159
+ ...type,
160
+ items: resolveTypeNodeMetadata(type.items, options)
161
+ };
162
+ case "object":
163
+ return {
164
+ ...type,
165
+ properties: type.properties.map((property) => resolveObjectPropertyMetadata(property, options))
166
+ };
167
+ case "record":
168
+ return {
169
+ ...type,
170
+ valueType: resolveTypeNodeMetadata(type.valueType, options)
171
+ };
172
+ case "union":
173
+ return {
174
+ ...type,
175
+ members: type.members.map((member) => resolveTypeNodeMetadata(member, options))
176
+ };
177
+ case "reference":
178
+ case "primitive":
179
+ case "enum":
180
+ case "dynamic":
181
+ case "custom":
182
+ return type;
183
+ default: {
184
+ const _exhaustive = type;
185
+ return _exhaustive;
186
+ }
187
+ }
188
+ }
189
+ function resolveObjectPropertyMetadata(property, options) {
190
+ const metadata = resolveResolvedMetadata(property.metadata, options.policy.field, {
191
+ surface: options.surface,
192
+ declarationKind: "field",
193
+ logicalName: property.name,
194
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
195
+ });
196
+ return {
197
+ ...property,
198
+ ...metadata !== void 0 && { metadata },
199
+ type: resolveTypeNodeMetadata(property.type, options)
200
+ };
201
+ }
202
+ function resolveFieldMetadataNode(field, options) {
203
+ const metadata = resolveResolvedMetadata(field.metadata, options.policy.field, {
204
+ surface: options.surface,
205
+ declarationKind: "field",
206
+ logicalName: field.name,
207
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
208
+ });
209
+ return {
210
+ ...field,
211
+ ...metadata !== void 0 && { metadata },
212
+ type: resolveTypeNodeMetadata(field.type, options)
213
+ };
214
+ }
215
+ function resolveFormElementMetadata(element, options) {
216
+ switch (element.kind) {
217
+ case "field":
218
+ return resolveFieldMetadataNode(element, options);
219
+ case "group":
220
+ return {
221
+ ...element,
222
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
223
+ };
224
+ case "conditional":
225
+ return {
226
+ ...element,
227
+ elements: element.elements.map((child) => resolveFormElementMetadata(child, options))
228
+ };
229
+ default: {
230
+ const _exhaustive = element;
231
+ return _exhaustive;
232
+ }
233
+ }
234
+ }
235
+ function resolveTypeDefinitionMetadata(typeDefinition, options) {
236
+ const metadata = resolveResolvedMetadata(typeDefinition.metadata, options.policy.type, {
237
+ surface: options.surface,
238
+ declarationKind: "type",
239
+ logicalName: typeDefinition.name,
240
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
241
+ });
242
+ return {
243
+ ...typeDefinition,
244
+ ...metadata !== void 0 && { metadata },
245
+ type: resolveTypeNodeMetadata(typeDefinition.type, options)
246
+ };
247
+ }
248
+ function resolveMetadata(explicit, policy, context) {
249
+ return resolveResolvedMetadata(toExplicitResolvedMetadata(explicit), policy, context);
250
+ }
251
+ function getSerializedName(logicalName, metadata) {
252
+ return metadata?.apiName?.value ?? logicalName;
253
+ }
254
+ function getDisplayName(metadata) {
255
+ return metadata?.displayName?.value;
256
+ }
257
+ function resolveFormIRMetadata(ir, options) {
258
+ const rootLogicalName = options.rootLogicalName ?? ir.name ?? "FormSpec";
259
+ const metadata = resolveResolvedMetadata(ir.metadata, options.policy.type, {
260
+ surface: options.surface,
261
+ declarationKind: "type",
262
+ logicalName: rootLogicalName,
263
+ ...options.buildContext !== void 0 && { buildContext: options.buildContext }
264
+ });
265
+ return {
266
+ ...ir,
267
+ ...metadata !== void 0 && { metadata },
268
+ elements: ir.elements.map((element) => resolveFormElementMetadata(element, options)),
269
+ typeRegistry: Object.fromEntries(
270
+ Object.entries(ir.typeRegistry).map(([name, definition]) => [
271
+ name,
272
+ resolveTypeDefinitionMetadata(definition, options)
273
+ ])
274
+ )
275
+ };
276
+ }
277
+
278
+ // src/canonicalize/chain-dsl-canonicalizer.ts
3
279
  var CHAIN_DSL_PROVENANCE = {
4
280
  surface: "chain-dsl",
5
281
  file: "",
@@ -15,57 +291,60 @@ function isConditional(el) {
15
291
  function isField(el) {
16
292
  return el._type === "field";
17
293
  }
18
- function canonicalizeChainDSL(form) {
294
+ function canonicalizeChainDSL(form, options) {
295
+ const metadataPolicy = normalizeMetadataPolicy(
296
+ options?.metadata ?? _getFormSpecMetadataPolicy(form)
297
+ );
19
298
  return {
20
299
  kind: "form-ir",
21
300
  irVersion: IR_VERSION,
22
- elements: canonicalizeElements(form.elements),
301
+ elements: canonicalizeElements(form.elements, metadataPolicy),
23
302
  rootAnnotations: [],
24
303
  typeRegistry: {},
25
304
  provenance: CHAIN_DSL_PROVENANCE
26
305
  };
27
306
  }
28
- function canonicalizeElements(elements) {
29
- return elements.map(canonicalizeElement);
307
+ function canonicalizeElements(elements, metadataPolicy) {
308
+ return elements.map((element) => canonicalizeElement(element, metadataPolicy));
30
309
  }
31
- function canonicalizeElement(element) {
310
+ function canonicalizeElement(element, metadataPolicy) {
32
311
  if (isField(element)) {
33
- return canonicalizeField(element);
312
+ return canonicalizeField(element, metadataPolicy);
34
313
  }
35
314
  if (isGroup(element)) {
36
- return canonicalizeGroup(element);
315
+ return canonicalizeGroup(element, metadataPolicy);
37
316
  }
38
317
  if (isConditional(element)) {
39
- return canonicalizeConditional(element);
318
+ return canonicalizeConditional(element, metadataPolicy);
40
319
  }
41
320
  const _exhaustive = element;
42
321
  throw new Error(`Unknown element type: ${JSON.stringify(_exhaustive)}`);
43
322
  }
44
- function canonicalizeField(field) {
323
+ function canonicalizeField(field, metadataPolicy) {
45
324
  switch (field._field) {
46
325
  case "text":
47
- return canonicalizeTextField(field);
326
+ return canonicalizeTextField(field, metadataPolicy);
48
327
  case "number":
49
- return canonicalizeNumberField(field);
328
+ return canonicalizeNumberField(field, metadataPolicy);
50
329
  case "boolean":
51
- return canonicalizeBooleanField(field);
330
+ return canonicalizeBooleanField(field, metadataPolicy);
52
331
  case "enum":
53
- return canonicalizeStaticEnumField(field);
332
+ return canonicalizeStaticEnumField(field, metadataPolicy);
54
333
  case "dynamic_enum":
55
- return canonicalizeDynamicEnumField(field);
334
+ return canonicalizeDynamicEnumField(field, metadataPolicy);
56
335
  case "dynamic_schema":
57
- return canonicalizeDynamicSchemaField(field);
336
+ return canonicalizeDynamicSchemaField(field, metadataPolicy);
58
337
  case "array":
59
- return canonicalizeArrayField(field);
338
+ return canonicalizeArrayField(field, metadataPolicy);
60
339
  case "object":
61
- return canonicalizeObjectField(field);
340
+ return canonicalizeObjectField(field, metadataPolicy);
62
341
  default: {
63
342
  const _exhaustive = field;
64
343
  throw new Error(`Unknown field type: ${JSON.stringify(_exhaustive)}`);
65
344
  }
66
345
  }
67
346
  }
68
- function canonicalizeTextField(field) {
347
+ function canonicalizeTextField(field, metadataPolicy) {
69
348
  const type = { kind: "primitive", primitiveKind: "string" };
70
349
  const constraints = [];
71
350
  if (field.minLength !== void 0) {
@@ -97,13 +376,14 @@ function canonicalizeTextField(field) {
97
376
  }
98
377
  return buildFieldNode(
99
378
  field.name,
379
+ resolveFieldMetadata(field.name, field, metadataPolicy),
100
380
  type,
101
381
  field.required,
102
- buildAnnotations(field.label, field.placeholder),
382
+ buildAnnotations(getExplicitDisplayName(field), field.placeholder),
103
383
  constraints
104
384
  );
105
385
  }
106
- function canonicalizeNumberField(field) {
386
+ function canonicalizeNumberField(field, metadataPolicy) {
107
387
  const type = { kind: "primitive", primitiveKind: "number" };
108
388
  const constraints = [];
109
389
  if (field.min !== void 0) {
@@ -135,17 +415,24 @@ function canonicalizeNumberField(field) {
135
415
  }
136
416
  return buildFieldNode(
137
417
  field.name,
418
+ resolveFieldMetadata(field.name, field, metadataPolicy),
138
419
  type,
139
420
  field.required,
140
- buildAnnotations(field.label),
421
+ buildAnnotations(getExplicitDisplayName(field)),
141
422
  constraints
142
423
  );
143
424
  }
144
- function canonicalizeBooleanField(field) {
425
+ function canonicalizeBooleanField(field, metadataPolicy) {
145
426
  const type = { kind: "primitive", primitiveKind: "boolean" };
146
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
427
+ return buildFieldNode(
428
+ field.name,
429
+ resolveFieldMetadata(field.name, field, metadataPolicy),
430
+ type,
431
+ field.required,
432
+ buildAnnotations(getExplicitDisplayName(field))
433
+ );
147
434
  }
148
- function canonicalizeStaticEnumField(field) {
435
+ function canonicalizeStaticEnumField(field, metadataPolicy) {
149
436
  const members = field.options.map((opt) => {
150
437
  if (typeof opt === "string") {
151
438
  return { value: opt };
@@ -153,28 +440,46 @@ function canonicalizeStaticEnumField(field) {
153
440
  return { value: opt.id, displayName: opt.label };
154
441
  });
155
442
  const type = { kind: "enum", members };
156
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
443
+ return buildFieldNode(
444
+ field.name,
445
+ resolveFieldMetadata(field.name, field, metadataPolicy),
446
+ type,
447
+ field.required,
448
+ buildAnnotations(getExplicitDisplayName(field))
449
+ );
157
450
  }
158
- function canonicalizeDynamicEnumField(field) {
451
+ function canonicalizeDynamicEnumField(field, metadataPolicy) {
159
452
  const type = {
160
453
  kind: "dynamic",
161
454
  dynamicKind: "enum",
162
455
  sourceKey: field.source,
163
456
  parameterFields: field.params ? [...field.params] : []
164
457
  };
165
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
458
+ return buildFieldNode(
459
+ field.name,
460
+ resolveFieldMetadata(field.name, field, metadataPolicy),
461
+ type,
462
+ field.required,
463
+ buildAnnotations(getExplicitDisplayName(field))
464
+ );
166
465
  }
167
- function canonicalizeDynamicSchemaField(field) {
466
+ function canonicalizeDynamicSchemaField(field, metadataPolicy) {
168
467
  const type = {
169
468
  kind: "dynamic",
170
469
  dynamicKind: "schema",
171
470
  sourceKey: field.schemaSource,
172
471
  parameterFields: []
173
472
  };
174
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
473
+ return buildFieldNode(
474
+ field.name,
475
+ resolveFieldMetadata(field.name, field, metadataPolicy),
476
+ type,
477
+ field.required,
478
+ buildAnnotations(getExplicitDisplayName(field))
479
+ );
175
480
  }
176
- function canonicalizeArrayField(field) {
177
- const itemProperties = buildObjectProperties(field.items);
481
+ function canonicalizeArrayField(field, metadataPolicy) {
482
+ const itemProperties = buildObjectProperties(field.items, metadataPolicy);
178
483
  const itemsType = {
179
484
  kind: "object",
180
485
  properties: itemProperties,
@@ -202,37 +507,44 @@ function canonicalizeArrayField(field) {
202
507
  }
203
508
  return buildFieldNode(
204
509
  field.name,
510
+ resolveFieldMetadata(field.name, field, metadataPolicy),
205
511
  type,
206
512
  field.required,
207
- buildAnnotations(field.label),
513
+ buildAnnotations(getExplicitDisplayName(field)),
208
514
  constraints
209
515
  );
210
516
  }
211
- function canonicalizeObjectField(field) {
212
- const properties = buildObjectProperties(field.properties);
517
+ function canonicalizeObjectField(field, metadataPolicy) {
518
+ const properties = buildObjectProperties(field.properties, metadataPolicy);
213
519
  const type = {
214
520
  kind: "object",
215
521
  properties,
216
522
  additionalProperties: true
217
523
  };
218
- return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
524
+ return buildFieldNode(
525
+ field.name,
526
+ resolveFieldMetadata(field.name, field, metadataPolicy),
527
+ type,
528
+ field.required,
529
+ buildAnnotations(getExplicitDisplayName(field))
530
+ );
219
531
  }
220
- function canonicalizeGroup(g) {
532
+ function canonicalizeGroup(g, metadataPolicy) {
221
533
  return {
222
534
  kind: "group",
223
535
  label: g.label,
224
- elements: canonicalizeElements(g.elements),
536
+ elements: canonicalizeElements(g.elements, metadataPolicy),
225
537
  provenance: CHAIN_DSL_PROVENANCE
226
538
  };
227
539
  }
228
- function canonicalizeConditional(c) {
540
+ function canonicalizeConditional(c, metadataPolicy) {
229
541
  return {
230
542
  kind: "conditional",
231
543
  fieldName: c.field,
232
544
  // Conditional values from the chain DSL are JSON-serializable primitives
233
545
  // (strings, numbers, booleans) produced by the `is()` predicate helper.
234
546
  value: assertJsonValue(c.value),
235
- elements: canonicalizeElements(c.elements),
547
+ elements: canonicalizeElements(c.elements, metadataPolicy),
236
548
  provenance: CHAIN_DSL_PROVENANCE
237
549
  };
238
550
  }
@@ -252,10 +564,11 @@ function assertJsonValue(v) {
252
564
  }
253
565
  throw new TypeError(`Conditional value is not a valid JsonValue: ${typeof v}`);
254
566
  }
255
- function buildFieldNode(name, type, required, annotations, constraints = []) {
567
+ function buildFieldNode(name, metadata, type, required, annotations, constraints = []) {
256
568
  return {
257
569
  kind: "field",
258
570
  name,
571
+ ...metadata !== void 0 && { metadata },
259
572
  type,
260
573
  required: required === true,
261
574
  constraints,
@@ -285,13 +598,14 @@ function buildAnnotations(label, placeholder) {
285
598
  }
286
599
  return annotations;
287
600
  }
288
- function buildObjectProperties(elements, insideConditional = false) {
601
+ function buildObjectProperties(elements, metadataPolicy, insideConditional = false) {
289
602
  const properties = [];
290
603
  for (const el of elements) {
291
604
  if (isField(el)) {
292
- const fieldNode = canonicalizeField(el);
605
+ const fieldNode = canonicalizeField(el, metadataPolicy);
293
606
  properties.push({
294
607
  name: fieldNode.name,
608
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
295
609
  type: fieldNode.type,
296
610
  // Fields inside a conditional branch are always optional in the
297
611
  // data schema, regardless of their `required` flag — the condition
@@ -302,17 +616,34 @@ function buildObjectProperties(elements, insideConditional = false) {
302
616
  provenance: CHAIN_DSL_PROVENANCE
303
617
  });
304
618
  } else if (isGroup(el)) {
305
- properties.push(...buildObjectProperties(el.elements, insideConditional));
619
+ properties.push(...buildObjectProperties(el.elements, metadataPolicy, insideConditional));
306
620
  } else if (isConditional(el)) {
307
- properties.push(...buildObjectProperties(el.elements, true));
621
+ properties.push(...buildObjectProperties(el.elements, metadataPolicy, true));
308
622
  }
309
623
  }
310
624
  return properties;
311
625
  }
626
+ function getExplicitDisplayName(field) {
627
+ if (field.label !== void 0 && field.displayName !== void 0) {
628
+ throw new Error('Chain DSL fields cannot specify both "label" and "displayName".');
629
+ }
630
+ return field.displayName ?? field.label;
631
+ }
632
+ function resolveFieldMetadata(logicalName, field, metadataPolicy) {
633
+ const displayName = getExplicitDisplayName(field);
634
+ return resolveMetadata(
635
+ {
636
+ ...field.apiName !== void 0 && { apiName: field.apiName },
637
+ ...displayName !== void 0 && { displayName }
638
+ },
639
+ getDeclarationMetadataPolicy(metadataPolicy, "field"),
640
+ makeMetadataContext("chain-dsl", "field", logicalName)
641
+ );
642
+ }
312
643
 
313
644
  // src/canonicalize/tsdoc-canonicalizer.ts
314
645
  import { IR_VERSION as IR_VERSION2 } from "@formspec/core/internals";
315
- function canonicalizeTSDoc(analysis, source) {
646
+ function canonicalizeTSDoc(analysis, source, options) {
316
647
  const file = source?.file ?? "";
317
648
  const provenance = {
318
649
  surface: "tsdoc",
@@ -321,15 +652,21 @@ function canonicalizeTSDoc(analysis, source) {
321
652
  column: 0
322
653
  };
323
654
  const elements = assembleElements(analysis.fields, analysis.fieldLayouts, provenance);
324
- return {
655
+ const ir = {
325
656
  kind: "form-ir",
657
+ name: analysis.name,
326
658
  irVersion: IR_VERSION2,
327
659
  elements,
660
+ ...analysis.metadata !== void 0 && { metadata: analysis.metadata },
328
661
  typeRegistry: analysis.typeRegistry,
329
662
  ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { rootAnnotations: analysis.annotations },
330
663
  ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
331
664
  provenance
332
665
  };
666
+ return resolveFormIRMetadata(ir, {
667
+ policy: normalizeMetadataPolicy(options?.metadata),
668
+ surface: "tsdoc"
669
+ });
333
670
  }
334
671
  function assembleElements(fields, layouts, provenance) {
335
672
  const elements = [];
@@ -392,6 +729,9 @@ import * as path from "path";
392
729
 
393
730
  // src/analyzer/class-analyzer.ts
394
731
  import * as ts3 from "typescript";
732
+ import {
733
+ parseCommentBlock as parseCommentBlock2
734
+ } from "@formspec/analysis/internal";
395
735
 
396
736
  // src/analyzer/jsdoc-constraints.ts
397
737
  import * as ts2 from "typescript";
@@ -1294,7 +1634,76 @@ function makeParseOptions(extensionRegistry, fieldType, checker, subjectType, ho
1294
1634
  ...hostType !== void 0 && { hostType }
1295
1635
  };
1296
1636
  }
1297
- function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1637
+ function makeExplicitScalarMetadata(value) {
1638
+ return value === void 0 || value === "" ? void 0 : { value, source: "explicit" };
1639
+ }
1640
+ function extractExplicitMetadata(node) {
1641
+ let apiName;
1642
+ let displayName;
1643
+ let apiNamePlural;
1644
+ let displayNamePlural;
1645
+ for (const tag of getLeadingParsedTags(node)) {
1646
+ const value = tag.argumentText.trim();
1647
+ if (value === "") {
1648
+ continue;
1649
+ }
1650
+ if (tag.normalizedTagName === "apiName") {
1651
+ if (tag.target === null) {
1652
+ apiName ??= value;
1653
+ } else if (tag.target.kind === "variant") {
1654
+ if (tag.target.rawText === "singular") {
1655
+ apiName ??= value;
1656
+ } else if (tag.target.rawText === "plural") {
1657
+ apiNamePlural ??= value;
1658
+ }
1659
+ }
1660
+ continue;
1661
+ }
1662
+ if (tag.normalizedTagName === "displayName") {
1663
+ if (tag.target === null) {
1664
+ displayName ??= value;
1665
+ } else if (tag.target.kind === "variant") {
1666
+ if (tag.target.rawText === "singular") {
1667
+ displayName ??= value;
1668
+ } else if (tag.target.rawText === "plural") {
1669
+ displayNamePlural ??= value;
1670
+ }
1671
+ }
1672
+ }
1673
+ }
1674
+ const resolvedApiName = makeExplicitScalarMetadata(apiName);
1675
+ const resolvedDisplayName = makeExplicitScalarMetadata(displayName);
1676
+ const resolvedApiNamePlural = makeExplicitScalarMetadata(apiNamePlural);
1677
+ const resolvedDisplayNamePlural = makeExplicitScalarMetadata(displayNamePlural);
1678
+ const metadata = {
1679
+ ...resolvedApiName !== void 0 && { apiName: resolvedApiName },
1680
+ ...resolvedDisplayName !== void 0 && { displayName: resolvedDisplayName },
1681
+ ...resolvedApiNamePlural !== void 0 && { apiNamePlural: resolvedApiNamePlural },
1682
+ ...resolvedDisplayNamePlural !== void 0 && {
1683
+ displayNamePlural: resolvedDisplayNamePlural
1684
+ }
1685
+ };
1686
+ return Object.keys(metadata).length === 0 ? void 0 : metadata;
1687
+ }
1688
+ function resolveNodeMetadata(metadataPolicy, declarationKind, logicalName, node, buildContext) {
1689
+ const explicit = extractExplicitMetadata(node);
1690
+ return resolveMetadata(
1691
+ {
1692
+ ...explicit?.apiName !== void 0 && { apiName: explicit.apiName.value },
1693
+ ...explicit?.displayName !== void 0 && { displayName: explicit.displayName.value },
1694
+ ...explicit?.apiNamePlural !== void 0 && {
1695
+ apiNamePlural: explicit.apiNamePlural.value
1696
+ },
1697
+ ...explicit?.displayNamePlural !== void 0 && {
1698
+ displayNamePlural: explicit.displayNamePlural.value
1699
+ }
1700
+ },
1701
+ getDeclarationMetadataPolicy(metadataPolicy, declarationKind),
1702
+ makeMetadataContext("tsdoc", declarationKind, logicalName, buildContext)
1703
+ );
1704
+ }
1705
+ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry, metadataPolicy) {
1706
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
1298
1707
  const name = classDecl.name?.text ?? "AnonymousClass";
1299
1708
  const fields = [];
1300
1709
  const fieldLayouts = [];
@@ -1321,6 +1730,7 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1321
1730
  visiting,
1322
1731
  diagnostics,
1323
1732
  classType,
1733
+ normalizedMetadataPolicy,
1324
1734
  extensionRegistry
1325
1735
  );
1326
1736
  if (fieldNode) {
@@ -1339,9 +1749,25 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1339
1749
  }
1340
1750
  }
1341
1751
  }
1752
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1753
+ fields,
1754
+ classDecl,
1755
+ classType,
1756
+ checker,
1757
+ file,
1758
+ diagnostics,
1759
+ normalizedMetadataPolicy
1760
+ );
1761
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, classDecl, {
1762
+ checker,
1763
+ declaration: classDecl,
1764
+ subjectType: classType,
1765
+ hostType: classType
1766
+ });
1342
1767
  return {
1343
1768
  name,
1344
- fields,
1769
+ ...metadata !== void 0 && { metadata },
1770
+ fields: specializedFields,
1345
1771
  fieldLayouts,
1346
1772
  typeRegistry,
1347
1773
  ...annotations.length > 0 && { annotations },
@@ -1350,7 +1776,8 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1350
1776
  staticMethods
1351
1777
  };
1352
1778
  }
1353
- function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry) {
1779
+ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegistry, metadataPolicy) {
1780
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
1354
1781
  const name = interfaceDecl.name.text;
1355
1782
  const fields = [];
1356
1783
  const typeRegistry = {};
@@ -1374,6 +1801,78 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1374
1801
  visiting,
1375
1802
  diagnostics,
1376
1803
  interfaceType,
1804
+ normalizedMetadataPolicy,
1805
+ extensionRegistry
1806
+ );
1807
+ if (fieldNode) {
1808
+ fields.push(fieldNode);
1809
+ }
1810
+ }
1811
+ }
1812
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1813
+ fields,
1814
+ interfaceDecl,
1815
+ interfaceType,
1816
+ checker,
1817
+ file,
1818
+ diagnostics,
1819
+ normalizedMetadataPolicy
1820
+ );
1821
+ const fieldLayouts = specializedFields.map(() => ({}));
1822
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, interfaceDecl, {
1823
+ checker,
1824
+ declaration: interfaceDecl,
1825
+ subjectType: interfaceType,
1826
+ hostType: interfaceType
1827
+ });
1828
+ return {
1829
+ name,
1830
+ ...metadata !== void 0 && { metadata },
1831
+ fields: specializedFields,
1832
+ fieldLayouts,
1833
+ typeRegistry,
1834
+ ...annotations.length > 0 && { annotations },
1835
+ ...diagnostics.length > 0 && { diagnostics },
1836
+ instanceMethods: [],
1837
+ staticMethods: []
1838
+ };
1839
+ }
1840
+ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry, metadataPolicy) {
1841
+ if (!ts3.isTypeLiteralNode(typeAlias.type)) {
1842
+ const sourceFile = typeAlias.getSourceFile();
1843
+ const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
1844
+ const kindDesc = ts3.SyntaxKind[typeAlias.type.kind] ?? "unknown";
1845
+ return {
1846
+ ok: false,
1847
+ error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
1848
+ };
1849
+ }
1850
+ const typeLiteral = typeAlias.type;
1851
+ const normalizedMetadataPolicy = normalizeMetadataPolicy(metadataPolicy);
1852
+ const name = typeAlias.name.text;
1853
+ const fields = [];
1854
+ const typeRegistry = {};
1855
+ const diagnostics = [];
1856
+ const aliasType = checker.getTypeAtLocation(typeAlias);
1857
+ const typeAliasDoc = extractJSDocParseResult(
1858
+ typeAlias,
1859
+ file,
1860
+ makeParseOptions(extensionRegistry, void 0, checker, aliasType, aliasType)
1861
+ );
1862
+ const annotations = [...typeAliasDoc.annotations];
1863
+ diagnostics.push(...typeAliasDoc.diagnostics);
1864
+ const visiting = /* @__PURE__ */ new Set();
1865
+ for (const member of typeLiteral.members) {
1866
+ if (ts3.isPropertySignature(member)) {
1867
+ const fieldNode = analyzeInterfacePropertyToIR(
1868
+ member,
1869
+ checker,
1870
+ file,
1871
+ typeRegistry,
1872
+ visiting,
1873
+ diagnostics,
1874
+ aliasType,
1875
+ normalizedMetadataPolicy,
1377
1876
  extensionRegistry
1378
1877
  );
1379
1878
  if (fieldNode) {
@@ -1381,73 +1880,474 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1381
1880
  }
1382
1881
  }
1383
1882
  }
1384
- const fieldLayouts = fields.map(() => ({}));
1385
- return {
1386
- name,
1387
- fields,
1388
- fieldLayouts,
1389
- typeRegistry,
1390
- ...annotations.length > 0 && { annotations },
1391
- ...diagnostics.length > 0 && { diagnostics },
1392
- instanceMethods: [],
1393
- staticMethods: []
1394
- };
1883
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1884
+ fields,
1885
+ typeAlias,
1886
+ aliasType,
1887
+ checker,
1888
+ file,
1889
+ diagnostics,
1890
+ normalizedMetadataPolicy
1891
+ );
1892
+ const metadata = resolveNodeMetadata(normalizedMetadataPolicy, "type", name, typeAlias, {
1893
+ checker,
1894
+ declaration: typeAlias,
1895
+ subjectType: aliasType,
1896
+ hostType: aliasType
1897
+ });
1898
+ return {
1899
+ ok: true,
1900
+ analysis: {
1901
+ name,
1902
+ ...metadata !== void 0 && { metadata },
1903
+ fields: specializedFields,
1904
+ fieldLayouts: specializedFields.map(() => ({})),
1905
+ typeRegistry,
1906
+ ...annotations.length > 0 && { annotations },
1907
+ ...diagnostics.length > 0 && { diagnostics },
1908
+ instanceMethods: [],
1909
+ staticMethods: []
1910
+ }
1911
+ };
1912
+ }
1913
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
1914
+ return {
1915
+ code,
1916
+ message,
1917
+ severity: "error",
1918
+ primaryLocation,
1919
+ relatedLocations
1920
+ };
1921
+ }
1922
+ function getLeadingParsedTags(node) {
1923
+ const sourceFile = node.getSourceFile();
1924
+ const sourceText = sourceFile.getFullText();
1925
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
1926
+ if (commentRanges === void 0) {
1927
+ return [];
1928
+ }
1929
+ const parsedTags = [];
1930
+ for (const range of commentRanges) {
1931
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
1932
+ continue;
1933
+ }
1934
+ const commentText = sourceText.slice(range.pos, range.end);
1935
+ if (!commentText.startsWith("/**")) {
1936
+ continue;
1937
+ }
1938
+ parsedTags.push(...parseCommentBlock2(commentText, { offset: range.pos }).tags);
1939
+ }
1940
+ return parsedTags;
1941
+ }
1942
+ function resolveDiscriminatorProperty(node, checker, fieldName) {
1943
+ const subjectType = checker.getTypeAtLocation(node);
1944
+ const propertySymbol = subjectType.getProperty(fieldName);
1945
+ if (propertySymbol === void 0) {
1946
+ return null;
1947
+ }
1948
+ const declaration = propertySymbol.valueDeclaration ?? propertySymbol.declarations?.find(
1949
+ (candidate) => ts3.isPropertyDeclaration(candidate) || ts3.isPropertySignature(candidate)
1950
+ ) ?? propertySymbol.declarations?.[0];
1951
+ return {
1952
+ declaration,
1953
+ type: checker.getTypeOfSymbolAtLocation(propertySymbol, declaration ?? node),
1954
+ optional: !!(propertySymbol.flags & ts3.SymbolFlags.Optional) || declaration !== void 0 && "questionToken" in declaration && declaration.questionToken !== void 0
1955
+ };
1956
+ }
1957
+ function isLocalTypeParameterName(node, typeParameterName) {
1958
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
1959
+ }
1960
+ function isNullishSemanticType(type) {
1961
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
1962
+ return true;
1963
+ }
1964
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
1965
+ }
1966
+ function isStringLikeSemanticType(type) {
1967
+ if (type.flags & ts3.TypeFlags.StringLike) {
1968
+ return true;
1969
+ }
1970
+ if (type.isUnion()) {
1971
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
1972
+ }
1973
+ return false;
1974
+ }
1975
+ function extractDiscriminatorDirective(node, file, diagnostics) {
1976
+ const discriminatorTags = getLeadingParsedTags(node).filter(
1977
+ (tag) => tag.normalizedTagName === "discriminator"
1978
+ );
1979
+ if (discriminatorTags.length === 0) {
1980
+ return null;
1981
+ }
1982
+ const [firstTag, ...duplicateTags] = discriminatorTags;
1983
+ for (const _duplicateTag of duplicateTags) {
1984
+ diagnostics.push(
1985
+ makeAnalysisDiagnostic(
1986
+ "DUPLICATE_TAG",
1987
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
1988
+ provenanceForNode(node, file)
1989
+ )
1990
+ );
1991
+ }
1992
+ if (firstTag === void 0) {
1993
+ return null;
1994
+ }
1995
+ const firstTarget = firstTag.target;
1996
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
1997
+ diagnostics.push(
1998
+ makeAnalysisDiagnostic(
1999
+ "INVALID_TAG_ARGUMENT",
2000
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
2001
+ provenanceForNode(node, file)
2002
+ )
2003
+ );
2004
+ return null;
2005
+ }
2006
+ if (firstTarget.path.segments.length !== 1) {
2007
+ diagnostics.push(
2008
+ makeAnalysisDiagnostic(
2009
+ "INVALID_TAG_ARGUMENT",
2010
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
2011
+ provenanceForNode(node, file)
2012
+ )
2013
+ );
2014
+ return null;
2015
+ }
2016
+ const typeParameterName = firstTag.argumentText.trim();
2017
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
2018
+ diagnostics.push(
2019
+ makeAnalysisDiagnostic(
2020
+ "INVALID_TAG_ARGUMENT",
2021
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
2022
+ provenanceForNode(node, file)
2023
+ )
2024
+ );
2025
+ return null;
2026
+ }
2027
+ return {
2028
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
2029
+ typeParameterName,
2030
+ provenance: provenanceForNode(node, file)
2031
+ };
2032
+ }
2033
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
2034
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
2035
+ if (directive === null) {
2036
+ return null;
2037
+ }
2038
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
2039
+ diagnostics.push(
2040
+ makeAnalysisDiagnostic(
2041
+ "INVALID_TAG_ARGUMENT",
2042
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
2043
+ directive.provenance
2044
+ )
2045
+ );
2046
+ return null;
2047
+ }
2048
+ const property = resolveDiscriminatorProperty(node, checker, directive.fieldName);
2049
+ if (property === null) {
2050
+ diagnostics.push(
2051
+ makeAnalysisDiagnostic(
2052
+ "UNKNOWN_PATH_TARGET",
2053
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
2054
+ directive.provenance
2055
+ )
2056
+ );
2057
+ return null;
2058
+ }
2059
+ if (property.optional) {
2060
+ diagnostics.push(
2061
+ makeAnalysisDiagnostic(
2062
+ "TYPE_MISMATCH",
2063
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
2064
+ directive.provenance,
2065
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
2066
+ )
2067
+ );
2068
+ return null;
2069
+ }
2070
+ if (isNullishSemanticType(property.type)) {
2071
+ diagnostics.push(
2072
+ makeAnalysisDiagnostic(
2073
+ "TYPE_MISMATCH",
2074
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
2075
+ directive.provenance,
2076
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
2077
+ )
2078
+ );
2079
+ return null;
2080
+ }
2081
+ if (!isStringLikeSemanticType(property.type)) {
2082
+ diagnostics.push(
2083
+ makeAnalysisDiagnostic(
2084
+ "TYPE_MISMATCH",
2085
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
2086
+ directive.provenance,
2087
+ property.declaration !== void 0 ? [provenanceForNode(property.declaration, file)] : []
2088
+ )
2089
+ );
2090
+ return null;
2091
+ }
2092
+ return directive;
2093
+ }
2094
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
2095
+ const typeParameterIndex = node.typeParameters?.findIndex(
2096
+ (typeParameter) => typeParameter.name.text === typeParameterName
2097
+ ) ?? -1;
2098
+ if (typeParameterIndex < 0) {
2099
+ return null;
2100
+ }
2101
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
2102
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
2103
+ return referenceTypeArguments[typeParameterIndex] ?? null;
2104
+ }
2105
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
2106
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
2107
+ }
2108
+ function resolveLiteralDiscriminatorPropertyValue(boundType, fieldName, checker, provenance, diagnostics) {
2109
+ const propertySymbol = boundType.getProperty(fieldName);
2110
+ if (propertySymbol === void 0) {
2111
+ return void 0;
2112
+ }
2113
+ const declaration = propertySymbol.valueDeclaration ?? propertySymbol.declarations?.[0];
2114
+ const anchorNode = declaration ?? boundType.symbol.declarations?.[0] ?? null;
2115
+ const resolvedAnchorNode = anchorNode ?? resolveNamedDiscriminatorDeclaration(boundType, checker);
2116
+ if (resolvedAnchorNode === null) {
2117
+ return void 0;
2118
+ }
2119
+ const propertyType = checker.getTypeOfSymbolAtLocation(
2120
+ propertySymbol,
2121
+ resolvedAnchorNode
2122
+ );
2123
+ if (propertyType.isStringLiteral()) {
2124
+ return propertyType.value;
2125
+ }
2126
+ if (propertyType.isUnion()) {
2127
+ const nonNullMembers = propertyType.types.filter(
2128
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
2129
+ );
2130
+ if (nonNullMembers.length > 0 && nonNullMembers.every((member) => member.isStringLiteral())) {
2131
+ diagnostics.push(
2132
+ makeAnalysisDiagnostic(
2133
+ "INVALID_TAG_ARGUMENT",
2134
+ "Discriminator resolution for union-valued identity properties is out of scope for v1.",
2135
+ provenance
2136
+ )
2137
+ );
2138
+ return null;
2139
+ }
2140
+ }
2141
+ return void 0;
2142
+ }
2143
+ function resolveDiscriminatorApiName(boundType, checker, metadataPolicy) {
2144
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
2145
+ if (declaration === null) {
2146
+ return void 0;
2147
+ }
2148
+ const metadata = resolveNodeMetadata(
2149
+ metadataPolicy,
2150
+ "type",
2151
+ getDiscriminatorLogicalName(boundType, declaration, checker),
2152
+ declaration,
2153
+ {
2154
+ checker,
2155
+ declaration,
2156
+ subjectType: boundType
2157
+ }
2158
+ );
2159
+ return metadata?.apiName;
2160
+ }
2161
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
2162
+ if (seen.has(type)) {
2163
+ return null;
2164
+ }
2165
+ seen.add(type);
2166
+ const symbol = type.aliasSymbol ?? type.getSymbol();
2167
+ if (symbol !== void 0) {
2168
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
2169
+ const targetSymbol = aliased ?? symbol;
2170
+ const declaration = targetSymbol.declarations?.find(
2171
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
2172
+ );
2173
+ if (declaration !== void 0) {
2174
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
2175
+ return resolveNamedDiscriminatorDeclaration(
2176
+ checker.getTypeFromTypeNode(declaration.type),
2177
+ checker,
2178
+ seen
2179
+ );
2180
+ }
2181
+ return declaration;
2182
+ }
2183
+ }
2184
+ return null;
1395
2185
  }
1396
- function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry) {
1397
- if (!ts3.isTypeLiteralNode(typeAlias.type)) {
1398
- const sourceFile = typeAlias.getSourceFile();
1399
- const { line } = sourceFile.getLineAndCharacterOfPosition(typeAlias.getStart());
1400
- const kindDesc = ts3.SyntaxKind[typeAlias.type.kind] ?? "unknown";
1401
- return {
1402
- ok: false,
1403
- error: `Type alias "${typeAlias.name.text}" at line ${String(line + 1)} is not an object type literal (found ${kindDesc})`
1404
- };
2186
+ function resolveDiscriminatorValue(boundType, fieldName, checker, provenance, diagnostics, metadataPolicy) {
2187
+ if (boundType === null) {
2188
+ diagnostics.push(
2189
+ makeAnalysisDiagnostic(
2190
+ "INVALID_TAG_ARGUMENT",
2191
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
2192
+ provenance
2193
+ )
2194
+ );
2195
+ return null;
1405
2196
  }
1406
- const name = typeAlias.name.text;
1407
- const fields = [];
1408
- const typeRegistry = {};
1409
- const diagnostics = [];
1410
- const aliasType = checker.getTypeAtLocation(typeAlias);
1411
- const typeAliasDoc = extractJSDocParseResult(
1412
- typeAlias,
1413
- file,
1414
- makeParseOptions(extensionRegistry, void 0, checker, aliasType, aliasType)
2197
+ if (boundType.isStringLiteral()) {
2198
+ return boundType.value;
2199
+ }
2200
+ if (boundType.isUnion()) {
2201
+ const nonNullMembers = boundType.types.filter(
2202
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
2203
+ );
2204
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
2205
+ diagnostics.push(
2206
+ makeAnalysisDiagnostic(
2207
+ "INVALID_TAG_ARGUMENT",
2208
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
2209
+ provenance
2210
+ )
2211
+ );
2212
+ return null;
2213
+ }
2214
+ }
2215
+ const literalIdentityValue = resolveLiteralDiscriminatorPropertyValue(
2216
+ boundType,
2217
+ fieldName,
2218
+ checker,
2219
+ provenance,
2220
+ diagnostics
1415
2221
  );
1416
- const annotations = [...typeAliasDoc.annotations];
1417
- diagnostics.push(...typeAliasDoc.diagnostics);
1418
- const visiting = /* @__PURE__ */ new Set();
1419
- for (const member of typeAlias.type.members) {
1420
- if (ts3.isPropertySignature(member)) {
1421
- const fieldNode = analyzeInterfacePropertyToIR(
1422
- member,
2222
+ if (literalIdentityValue !== void 0) {
2223
+ return literalIdentityValue;
2224
+ }
2225
+ const apiName = resolveDiscriminatorApiName(boundType, checker, metadataPolicy);
2226
+ if (apiName?.source === "explicit") {
2227
+ return apiName.value;
2228
+ }
2229
+ if (apiName?.source === "inferred") {
2230
+ return apiName.value;
2231
+ }
2232
+ diagnostics.push(
2233
+ makeAnalysisDiagnostic(
2234
+ "INVALID_TAG_ARGUMENT",
2235
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
2236
+ provenance
2237
+ )
2238
+ );
2239
+ return null;
2240
+ }
2241
+ function getDeclarationName(node) {
2242
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
2243
+ return node.name?.text ?? "anonymous";
2244
+ }
2245
+ return "anonymous";
2246
+ }
2247
+ function getResolvedTypeArguments(type) {
2248
+ return (isTypeReference(type) ? type.typeArguments : void 0) ?? type.aliasTypeArguments ?? [];
2249
+ }
2250
+ function getDiscriminatorLogicalName(type, declaration, checker) {
2251
+ const baseName = getDeclarationName(declaration);
2252
+ const typeArguments = getResolvedTypeArguments(type);
2253
+ return typeArguments.length === 0 ? baseName : buildInstantiatedReferenceName(baseName, typeArguments, checker);
2254
+ }
2255
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics, metadataPolicy) {
2256
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2257
+ if (directive === null) {
2258
+ return [...fields];
2259
+ }
2260
+ const discriminatorValue = resolveDiscriminatorValue(
2261
+ getConcreteTypeArgumentForDiscriminator(
2262
+ node,
2263
+ subjectType,
2264
+ checker,
2265
+ directive.typeParameterName
2266
+ ),
2267
+ directive.fieldName,
2268
+ checker,
2269
+ directive.provenance,
2270
+ diagnostics,
2271
+ metadataPolicy
2272
+ );
2273
+ if (discriminatorValue === null) {
2274
+ return [...fields];
2275
+ }
2276
+ return fields.map(
2277
+ (field) => field.name === directive.fieldName ? {
2278
+ ...field,
2279
+ type: {
2280
+ kind: "enum",
2281
+ members: [{ value: discriminatorValue }]
2282
+ }
2283
+ } : field
2284
+ );
2285
+ }
2286
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
2287
+ const renderedArguments = typeArguments.map(
2288
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
2289
+ ).filter((value) => value !== "");
2290
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
2291
+ }
2292
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy, extensionRegistry, diagnostics) {
2293
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2294
+ if (typeNode === void 0) {
2295
+ return [];
2296
+ }
2297
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2298
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
2299
+ return [];
2300
+ }
2301
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
2302
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
2303
+ return {
2304
+ tsType: argumentType,
2305
+ typeNode: resolveTypeNode(
2306
+ argumentType,
1423
2307
  checker,
1424
2308
  file,
1425
2309
  typeRegistry,
1426
2310
  visiting,
1427
- diagnostics,
1428
- aliasType,
1429
- extensionRegistry
1430
- );
1431
- if (fieldNode) {
1432
- fields.push(fieldNode);
1433
- }
1434
- }
2311
+ argumentNode,
2312
+ metadataPolicy,
2313
+ extensionRegistry,
2314
+ diagnostics
2315
+ )
2316
+ };
2317
+ });
2318
+ }
2319
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics, metadataPolicy) {
2320
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2321
+ if (directive === null) {
2322
+ return properties;
1435
2323
  }
1436
- return {
1437
- ok: true,
1438
- analysis: {
1439
- name,
1440
- fields,
1441
- fieldLayouts: fields.map(() => ({})),
1442
- typeRegistry,
1443
- ...annotations.length > 0 && { annotations },
1444
- ...diagnostics.length > 0 && { diagnostics },
1445
- instanceMethods: [],
1446
- staticMethods: []
1447
- }
1448
- };
2324
+ const discriminatorValue = resolveDiscriminatorValue(
2325
+ getConcreteTypeArgumentForDiscriminator(
2326
+ node,
2327
+ subjectType,
2328
+ checker,
2329
+ directive.typeParameterName
2330
+ ),
2331
+ directive.fieldName,
2332
+ checker,
2333
+ directive.provenance,
2334
+ diagnostics,
2335
+ metadataPolicy
2336
+ );
2337
+ if (discriminatorValue === null) {
2338
+ return properties;
2339
+ }
2340
+ return properties.map(
2341
+ (property) => property.name === directive.fieldName ? {
2342
+ ...property,
2343
+ type: {
2344
+ kind: "enum",
2345
+ members: [{ value: discriminatorValue }]
2346
+ }
2347
+ } : property
2348
+ );
1449
2349
  }
1450
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2350
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
1451
2351
  if (!ts3.isIdentifier(prop.name)) {
1452
2352
  return null;
1453
2353
  }
@@ -1462,6 +2362,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
1462
2362
  typeRegistry,
1463
2363
  visiting,
1464
2364
  prop,
2365
+ metadataPolicy,
1465
2366
  extensionRegistry,
1466
2367
  diagnostics
1467
2368
  );
@@ -1485,9 +2386,16 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
1485
2386
  annotations.push(defaultAnnotation);
1486
2387
  }
1487
2388
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
2389
+ const metadata = resolveNodeMetadata(metadataPolicy, "field", name, prop, {
2390
+ checker,
2391
+ declaration: prop,
2392
+ subjectType: tsType,
2393
+ hostType
2394
+ });
1488
2395
  return {
1489
2396
  kind: "field",
1490
2397
  name,
2398
+ ...metadata !== void 0 && { metadata },
1491
2399
  type,
1492
2400
  required: !optional,
1493
2401
  constraints,
@@ -1495,7 +2403,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnosti
1495
2403
  provenance
1496
2404
  };
1497
2405
  }
1498
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2406
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, metadataPolicy, extensionRegistry) {
1499
2407
  if (!ts3.isIdentifier(prop.name)) {
1500
2408
  return null;
1501
2409
  }
@@ -1510,6 +2418,7 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1510
2418
  typeRegistry,
1511
2419
  visiting,
1512
2420
  prop,
2421
+ metadataPolicy,
1513
2422
  extensionRegistry,
1514
2423
  diagnostics
1515
2424
  );
@@ -1529,9 +2438,16 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1529
2438
  let annotations = [];
1530
2439
  annotations.push(...docResult.annotations);
1531
2440
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
2441
+ const metadata = resolveNodeMetadata(metadataPolicy, "field", name, prop, {
2442
+ checker,
2443
+ declaration: prop,
2444
+ subjectType: tsType,
2445
+ hostType
2446
+ });
1532
2447
  return {
1533
2448
  kind: "field",
1534
2449
  name,
2450
+ ...metadata !== void 0 && { metadata },
1535
2451
  type,
1536
2452
  required: !optional,
1537
2453
  constraints,
@@ -1656,7 +2572,7 @@ function getTypeNodeRegistrationName(typeNode) {
1656
2572
  }
1657
2573
  return null;
1658
2574
  }
1659
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2575
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1660
2576
  const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1661
2577
  if (customType) {
1662
2578
  return customType;
@@ -1668,6 +2584,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1668
2584
  typeRegistry,
1669
2585
  visiting,
1670
2586
  sourceNode,
2587
+ metadataPolicy,
1671
2588
  extensionRegistry,
1672
2589
  diagnostics
1673
2590
  );
@@ -1712,6 +2629,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1712
2629
  typeRegistry,
1713
2630
  visiting,
1714
2631
  sourceNode,
2632
+ metadataPolicy,
1715
2633
  extensionRegistry,
1716
2634
  diagnostics
1717
2635
  );
@@ -1724,6 +2642,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1724
2642
  typeRegistry,
1725
2643
  visiting,
1726
2644
  sourceNode,
2645
+ metadataPolicy,
1727
2646
  extensionRegistry,
1728
2647
  diagnostics
1729
2648
  );
@@ -1735,13 +2654,15 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1735
2654
  file,
1736
2655
  typeRegistry,
1737
2656
  visiting,
2657
+ sourceNode,
2658
+ metadataPolicy,
1738
2659
  extensionRegistry,
1739
2660
  diagnostics
1740
2661
  );
1741
2662
  }
1742
2663
  return { kind: "primitive", primitiveKind: "string" };
1743
2664
  }
1744
- function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2665
+ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1745
2666
  if (!(type.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null))) {
1746
2667
  return null;
1747
2668
  }
@@ -1761,14 +2682,21 @@ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiti
1761
2682
  file,
1762
2683
  makeParseOptions(extensionRegistry)
1763
2684
  );
2685
+ const metadata = resolveNodeMetadata(metadataPolicy, "type", aliasName, aliasDecl, {
2686
+ checker,
2687
+ declaration: aliasDecl,
2688
+ subjectType: aliasType
2689
+ });
1764
2690
  typeRegistry[aliasName] = {
1765
2691
  name: aliasName,
2692
+ ...metadata !== void 0 && { metadata },
1766
2693
  type: resolveAliasedPrimitiveTarget(
1767
2694
  aliasType,
1768
2695
  checker,
1769
2696
  file,
1770
2697
  typeRegistry,
1771
2698
  visiting,
2699
+ metadataPolicy,
1772
2700
  extensionRegistry,
1773
2701
  diagnostics
1774
2702
  ),
@@ -1797,7 +2725,7 @@ function shouldEmitPrimitiveAliasDefinition(typeNode, checker) {
1797
2725
  const resolved = checker.getTypeFromTypeNode(aliasDecl.type);
1798
2726
  return !!(resolved.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null));
1799
2727
  }
1800
- function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2728
+ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1801
2729
  const nestedAliasDecl = type.aliasSymbol?.declarations?.find(ts3.isTypeAliasDeclaration);
1802
2730
  if (nestedAliasDecl !== void 0) {
1803
2731
  return resolveAliasedPrimitiveTarget(
@@ -1806,6 +2734,7 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
1806
2734
  file,
1807
2735
  typeRegistry,
1808
2736
  visiting,
2737
+ metadataPolicy,
1809
2738
  extensionRegistry,
1810
2739
  diagnostics
1811
2740
  );
@@ -1817,11 +2746,12 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
1817
2746
  typeRegistry,
1818
2747
  visiting,
1819
2748
  void 0,
2749
+ metadataPolicy,
1820
2750
  extensionRegistry,
1821
2751
  diagnostics
1822
2752
  );
1823
2753
  }
1824
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2754
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1825
2755
  const typeName = getNamedTypeName(type);
1826
2756
  const namedDecl = getNamedTypeDeclaration(type);
1827
2757
  if (typeName && typeName in typeRegistry) {
@@ -1856,8 +2786,14 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1856
2786
  return result;
1857
2787
  }
1858
2788
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2789
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", typeName, namedDecl, {
2790
+ checker,
2791
+ declaration: namedDecl,
2792
+ subjectType: type
2793
+ }) : void 0;
1859
2794
  typeRegistry[typeName] = {
1860
2795
  name: typeName,
2796
+ ...metadata !== void 0 && { metadata },
1861
2797
  type: result,
1862
2798
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
1863
2799
  provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
@@ -1911,6 +2847,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1911
2847
  typeRegistry,
1912
2848
  visiting,
1913
2849
  nonNullMembers[0].sourceNode ?? sourceNode,
2850
+ metadataPolicy,
1914
2851
  extensionRegistry,
1915
2852
  diagnostics
1916
2853
  );
@@ -1928,6 +2865,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1928
2865
  typeRegistry,
1929
2866
  visiting,
1930
2867
  memberSourceNode ?? sourceNode,
2868
+ metadataPolicy,
1931
2869
  extensionRegistry,
1932
2870
  diagnostics
1933
2871
  )
@@ -1937,7 +2875,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1937
2875
  }
1938
2876
  return registerNamed({ kind: "union", members });
1939
2877
  }
1940
- function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2878
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1941
2879
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1942
2880
  const elementType = typeArgs?.[0];
1943
2881
  const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
@@ -1948,12 +2886,13 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNod
1948
2886
  typeRegistry,
1949
2887
  visiting,
1950
2888
  elementSourceNode,
2889
+ metadataPolicy,
1951
2890
  extensionRegistry,
1952
2891
  diagnostics
1953
2892
  ) : { kind: "primitive", primitiveKind: "string" };
1954
2893
  return { kind: "array", items };
1955
2894
  }
1956
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2895
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
1957
2896
  if (type.getProperties().length > 0) {
1958
2897
  return null;
1959
2898
  }
@@ -1968,6 +2907,7 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, exten
1968
2907
  typeRegistry,
1969
2908
  visiting,
1970
2909
  void 0,
2910
+ metadataPolicy,
1971
2911
  extensionRegistry,
1972
2912
  diagnostics
1973
2913
  );
@@ -1998,35 +2938,76 @@ function typeNodeContainsReference(type, targetName) {
1998
2938
  }
1999
2939
  }
2000
2940
  }
2001
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2941
+ function shouldEmitResolvedObjectProperty(property, declaration) {
2942
+ if (property.name.startsWith("__@")) {
2943
+ return false;
2944
+ }
2945
+ if (declaration !== void 0 && "name" in declaration && declaration.name !== void 0) {
2946
+ const name = declaration.name;
2947
+ if (ts3.isComputedPropertyName(name) || ts3.isPrivateIdentifier(name)) {
2948
+ return false;
2949
+ }
2950
+ if (!ts3.isIdentifier(name) && !ts3.isStringLiteral(name) && !ts3.isNumericLiteral(name)) {
2951
+ return false;
2952
+ }
2953
+ }
2954
+ return true;
2955
+ }
2956
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, metadataPolicy = normalizeMetadataPolicy(void 0), extensionRegistry, diagnostics) {
2957
+ const collectedDiagnostics = diagnostics ?? [];
2002
2958
  const typeName = getNamedTypeName(type);
2003
2959
  const namedTypeName = typeName ?? void 0;
2004
2960
  const namedDecl = getNamedTypeDeclaration(type);
2005
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2961
+ const referenceTypeArguments = extractReferenceTypeArguments(
2962
+ type,
2963
+ checker,
2964
+ file,
2965
+ typeRegistry,
2966
+ visiting,
2967
+ sourceNode,
2968
+ metadataPolicy,
2969
+ extensionRegistry,
2970
+ collectedDiagnostics
2971
+ );
2972
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
2973
+ namedTypeName,
2974
+ referenceTypeArguments.map((argument) => argument.tsType),
2975
+ checker
2976
+ ) : void 0;
2977
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
2978
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2006
2979
  const clearNamedTypeRegistration = () => {
2007
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
2980
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2008
2981
  return;
2009
2982
  }
2010
- Reflect.deleteProperty(typeRegistry, namedTypeName);
2983
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2011
2984
  };
2012
2985
  if (visiting.has(type)) {
2013
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2014
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2986
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2987
+ return {
2988
+ kind: "reference",
2989
+ name: registryTypeName,
2990
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2991
+ };
2015
2992
  }
2016
2993
  return { kind: "object", properties: [], additionalProperties: false };
2017
2994
  }
2018
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2019
- typeRegistry[namedTypeName] = {
2020
- name: namedTypeName,
2995
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
2996
+ typeRegistry[registryTypeName] = {
2997
+ name: registryTypeName,
2021
2998
  type: RESOLVING_TYPE_PLACEHOLDER,
2022
2999
  provenance: provenanceForDeclaration(namedDecl, file)
2023
3000
  };
2024
3001
  }
2025
3002
  visiting.add(type);
2026
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2027
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
3003
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
3004
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2028
3005
  visiting.delete(type);
2029
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3006
+ return {
3007
+ kind: "reference",
3008
+ name: registryTypeName,
3009
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3010
+ };
2030
3011
  }
2031
3012
  }
2032
3013
  const recordNode = tryResolveRecordType(
@@ -2035,25 +3016,36 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2035
3016
  file,
2036
3017
  typeRegistry,
2037
3018
  visiting,
3019
+ metadataPolicy,
2038
3020
  extensionRegistry,
2039
- diagnostics
3021
+ collectedDiagnostics
2040
3022
  );
2041
3023
  if (recordNode) {
2042
3024
  visiting.delete(type);
2043
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2044
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
3025
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3026
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2045
3027
  if (!isRecursiveRecord) {
2046
3028
  clearNamedTypeRegistration();
2047
3029
  return recordNode;
2048
3030
  }
2049
3031
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2050
- typeRegistry[namedTypeName] = {
2051
- name: namedTypeName,
3032
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", registryTypeName, namedDecl, {
3033
+ checker,
3034
+ declaration: namedDecl,
3035
+ subjectType: type
3036
+ }) : void 0;
3037
+ typeRegistry[registryTypeName] = {
3038
+ name: registryTypeName,
3039
+ ...metadata !== void 0 && { metadata },
2052
3040
  type: recordNode,
2053
3041
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2054
3042
  provenance: provenanceForDeclaration(namedDecl, file)
2055
3043
  };
2056
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3044
+ return {
3045
+ kind: "reference",
3046
+ name: registryTypeName,
3047
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3048
+ };
2057
3049
  }
2058
3050
  return recordNode;
2059
3051
  }
@@ -2064,12 +3056,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2064
3056
  file,
2065
3057
  typeRegistry,
2066
3058
  visiting,
2067
- diagnostics ?? [],
3059
+ metadataPolicy,
3060
+ collectedDiagnostics,
2068
3061
  extensionRegistry
2069
3062
  );
2070
3063
  for (const prop of type.getProperties()) {
2071
3064
  const declaration = prop.valueDeclaration ?? prop.declarations?.[0];
2072
3065
  if (!declaration) continue;
3066
+ if (!shouldEmitResolvedObjectProperty(prop, declaration)) continue;
2073
3067
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
2074
3068
  const optional = !!(prop.flags & ts3.SymbolFlags.Optional);
2075
3069
  const propTypeNode = resolveTypeNode(
@@ -2079,12 +3073,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2079
3073
  typeRegistry,
2080
3074
  visiting,
2081
3075
  declaration,
3076
+ metadataPolicy,
2082
3077
  extensionRegistry,
2083
- diagnostics
3078
+ collectedDiagnostics
2084
3079
  );
2085
3080
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2086
3081
  properties.push({
2087
3082
  name: prop.name,
3083
+ ...fieldNodeInfo?.metadata !== void 0 && { metadata: fieldNodeInfo.metadata },
2088
3084
  type: propTypeNode,
2089
3085
  optional,
2090
3086
  constraints: fieldNodeInfo?.constraints ?? [],
@@ -2095,22 +3091,40 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2095
3091
  visiting.delete(type);
2096
3092
  const objectNode = {
2097
3093
  kind: "object",
2098
- properties,
3094
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
3095
+ properties,
3096
+ namedDecl,
3097
+ type,
3098
+ checker,
3099
+ file,
3100
+ collectedDiagnostics,
3101
+ metadataPolicy
3102
+ ) : properties,
2099
3103
  additionalProperties: true
2100
3104
  };
2101
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
3105
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2102
3106
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2103
- typeRegistry[namedTypeName] = {
2104
- name: namedTypeName,
3107
+ const metadata = namedDecl !== void 0 ? resolveNodeMetadata(metadataPolicy, "type", registryTypeName, namedDecl, {
3108
+ checker,
3109
+ declaration: namedDecl,
3110
+ subjectType: type
3111
+ }) : void 0;
3112
+ typeRegistry[registryTypeName] = {
3113
+ name: registryTypeName,
3114
+ ...metadata !== void 0 && { metadata },
2105
3115
  type: objectNode,
2106
3116
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2107
3117
  provenance: provenanceForDeclaration(namedDecl, file)
2108
3118
  };
2109
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3119
+ return {
3120
+ kind: "reference",
3121
+ name: registryTypeName,
3122
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3123
+ };
2110
3124
  }
2111
3125
  return objectNode;
2112
3126
  }
2113
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, diagnostics, extensionRegistry) {
3127
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, metadataPolicy, diagnostics, extensionRegistry) {
2114
3128
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
2115
3129
  (s) => s?.declarations != null && s.declarations.length > 0
2116
3130
  );
@@ -2131,10 +3145,12 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2131
3145
  visiting,
2132
3146
  diagnostics,
2133
3147
  hostType,
3148
+ metadataPolicy,
2134
3149
  extensionRegistry
2135
3150
  );
2136
3151
  if (fieldNode) {
2137
3152
  map.set(fieldNode.name, {
3153
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
2138
3154
  constraints: [...fieldNode.constraints],
2139
3155
  annotations: [...fieldNode.annotations],
2140
3156
  provenance: fieldNode.provenance
@@ -2152,6 +3168,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2152
3168
  file,
2153
3169
  typeRegistry,
2154
3170
  visiting,
3171
+ metadataPolicy,
2155
3172
  checker.getTypeAtLocation(interfaceDecl),
2156
3173
  diagnostics,
2157
3174
  extensionRegistry
@@ -2165,6 +3182,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2165
3182
  file,
2166
3183
  typeRegistry,
2167
3184
  visiting,
3185
+ metadataPolicy,
2168
3186
  checker.getTypeAtLocation(typeAliasDecl),
2169
3187
  diagnostics,
2170
3188
  extensionRegistry
@@ -2216,7 +3234,7 @@ function isNullishTypeNode(typeNode) {
2216
3234
  }
2217
3235
  return ts3.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts3.SyntaxKind.NullKeyword || typeNode.literal.kind === ts3.SyntaxKind.UndefinedKeyword);
2218
3236
  }
2219
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, hostType, diagnostics, extensionRegistry) {
3237
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, metadataPolicy, hostType, diagnostics, extensionRegistry) {
2220
3238
  const map = /* @__PURE__ */ new Map();
2221
3239
  for (const member of members) {
2222
3240
  if (ts3.isPropertySignature(member)) {
@@ -2228,10 +3246,12 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, h
2228
3246
  visiting,
2229
3247
  diagnostics,
2230
3248
  hostType,
3249
+ metadataPolicy,
2231
3250
  extensionRegistry
2232
3251
  );
2233
3252
  if (fieldNode) {
2234
3253
  map.set(fieldNode.name, {
3254
+ ...fieldNode.metadata !== void 0 && { metadata: fieldNode.metadata },
2235
3255
  constraints: [...fieldNode.constraints],
2236
3256
  annotations: [...fieldNode.annotations],
2237
3257
  provenance: fieldNode.provenance
@@ -2262,6 +3282,7 @@ function extractTypeAliasConstraintNodes(typeNode, checker, file, extensionRegis
2262
3282
  {},
2263
3283
  /* @__PURE__ */ new Set(),
2264
3284
  aliasDecl.type,
3285
+ void 0,
2265
3286
  extensionRegistry
2266
3287
  );
2267
3288
  const constraints = extractJSDocConstraintNodes(
@@ -2447,15 +3468,27 @@ function findInterfaceByName(sourceFile, interfaceName) {
2447
3468
  function findTypeAliasByName(sourceFile, aliasName) {
2448
3469
  return findNodeByName(sourceFile, aliasName, ts4.isTypeAliasDeclaration, (n) => n.name.text);
2449
3470
  }
2450
- function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry) {
3471
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry, metadataPolicy) {
2451
3472
  const analysisFilePath = path.resolve(filePath);
2452
3473
  const classDecl = findClassByName(ctx.sourceFile, typeName);
2453
3474
  if (classDecl !== null) {
2454
- return analyzeClassToIR(classDecl, ctx.checker, analysisFilePath, extensionRegistry);
3475
+ return analyzeClassToIR(
3476
+ classDecl,
3477
+ ctx.checker,
3478
+ analysisFilePath,
3479
+ extensionRegistry,
3480
+ metadataPolicy
3481
+ );
2455
3482
  }
2456
3483
  const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
2457
3484
  if (interfaceDecl !== null) {
2458
- return analyzeInterfaceToIR(interfaceDecl, ctx.checker, analysisFilePath, extensionRegistry);
3485
+ return analyzeInterfaceToIR(
3486
+ interfaceDecl,
3487
+ ctx.checker,
3488
+ analysisFilePath,
3489
+ extensionRegistry,
3490
+ metadataPolicy
3491
+ );
2459
3492
  }
2460
3493
  const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
2461
3494
  if (typeAlias !== null) {
@@ -2463,7 +3496,8 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
2463
3496
  typeAlias,
2464
3497
  ctx.checker,
2465
3498
  analysisFilePath,
2466
- extensionRegistry
3499
+ extensionRegistry,
3500
+ metadataPolicy
2467
3501
  );
2468
3502
  if (result.ok) {
2469
3503
  return result.analysis;
@@ -2478,6 +3512,120 @@ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensi
2478
3512
  // src/generators/class-schema.ts
2479
3513
  import "typescript";
2480
3514
 
3515
+ // src/metadata/collision-guards.ts
3516
+ function assertUniqueSerializedNames(entries, scope) {
3517
+ const seen = /* @__PURE__ */ new Map();
3518
+ for (const entry of entries) {
3519
+ const previous = seen.get(entry.serializedName);
3520
+ if (previous !== void 0) {
3521
+ if (previous.logicalName === entry.logicalName && previous.category === entry.category) {
3522
+ continue;
3523
+ }
3524
+ throw new Error(
3525
+ `Serialized name collision in ${scope}: ${previous.category} "${previous.logicalName}" and ${entry.category} "${entry.logicalName}" both resolve to "${entry.serializedName}".`
3526
+ );
3527
+ }
3528
+ seen.set(entry.serializedName, entry);
3529
+ }
3530
+ }
3531
+ function collectFlattenedFields(elements) {
3532
+ const fields = [];
3533
+ for (const element of elements) {
3534
+ switch (element.kind) {
3535
+ case "field":
3536
+ fields.push(element);
3537
+ break;
3538
+ case "group":
3539
+ case "conditional":
3540
+ fields.push(...collectFlattenedFields(element.elements));
3541
+ break;
3542
+ default: {
3543
+ const exhaustive = element;
3544
+ void exhaustive;
3545
+ }
3546
+ }
3547
+ }
3548
+ return fields;
3549
+ }
3550
+ function validateObjectProperties(properties, scope) {
3551
+ assertUniqueSerializedNames(
3552
+ properties.map((property) => ({
3553
+ logicalName: property.name,
3554
+ serializedName: getSerializedName(property.name, property.metadata),
3555
+ category: "object property"
3556
+ })),
3557
+ scope
3558
+ );
3559
+ for (const property of properties) {
3560
+ validateTypeNode(
3561
+ property.type,
3562
+ `${scope}.${getSerializedName(property.name, property.metadata)}`
3563
+ );
3564
+ }
3565
+ }
3566
+ function validateTypeNode(type, scope) {
3567
+ switch (type.kind) {
3568
+ case "array":
3569
+ validateTypeNode(type.items, `${scope}[]`);
3570
+ break;
3571
+ case "object":
3572
+ validateObjectProperties(type.properties, scope);
3573
+ break;
3574
+ case "record":
3575
+ validateTypeNode(type.valueType, `${scope}.*`);
3576
+ break;
3577
+ case "union":
3578
+ type.members.forEach((member, index) => {
3579
+ validateTypeNode(member, `${scope}|${String(index)}`);
3580
+ });
3581
+ break;
3582
+ case "reference":
3583
+ case "primitive":
3584
+ case "enum":
3585
+ case "dynamic":
3586
+ case "custom":
3587
+ break;
3588
+ default: {
3589
+ const exhaustive = type;
3590
+ void exhaustive;
3591
+ }
3592
+ }
3593
+ }
3594
+ function validateTypeDefinitions(typeRegistry) {
3595
+ const definitions = Object.values(typeRegistry);
3596
+ assertUniqueSerializedNames(
3597
+ definitions.map((definition) => ({
3598
+ logicalName: definition.name,
3599
+ serializedName: getSerializedName(definition.name, definition.metadata),
3600
+ category: "type definition"
3601
+ })),
3602
+ "$defs"
3603
+ );
3604
+ for (const definition of definitions) {
3605
+ validateTypeDefinition(definition);
3606
+ }
3607
+ }
3608
+ function validateTypeDefinition(definition) {
3609
+ validateTypeNode(
3610
+ definition.type,
3611
+ `type "${getSerializedName(definition.name, definition.metadata)}"`
3612
+ );
3613
+ }
3614
+ function assertNoSerializedNameCollisions(ir) {
3615
+ assertUniqueSerializedNames(
3616
+ collectFlattenedFields(ir.elements).map((field) => ({
3617
+ logicalName: field.name,
3618
+ serializedName: getSerializedName(field.name, field.metadata),
3619
+ category: "field"
3620
+ })),
3621
+ "form root"
3622
+ );
3623
+ for (const field of collectFlattenedFields(ir.elements)) {
3624
+ validateTypeNode(field.type, `field "${getSerializedName(field.name, field.metadata)}"`);
3625
+ }
3626
+ validateTypeDefinitions(ir.typeRegistry);
3627
+ }
3628
+
2481
3629
  // src/json-schema/ir-generator.ts
2482
3630
  function makeContext(options) {
2483
3631
  const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
@@ -2488,19 +3636,33 @@ function makeContext(options) {
2488
3636
  }
2489
3637
  return {
2490
3638
  defs: {},
3639
+ typeNameMap: {},
3640
+ typeRegistry: {},
2491
3641
  extensionRegistry: options?.extensionRegistry,
2492
3642
  vendorPrefix
2493
3643
  };
2494
3644
  }
2495
3645
  function generateJsonSchemaFromIR(ir, options) {
2496
- const ctx = makeContext(options);
3646
+ assertNoSerializedNameCollisions(ir);
3647
+ const ctx = {
3648
+ ...makeContext(options),
3649
+ typeRegistry: ir.typeRegistry,
3650
+ typeNameMap: Object.fromEntries(
3651
+ Object.entries(ir.typeRegistry).map(([name, typeDef]) => [
3652
+ name,
3653
+ getSerializedName(name, typeDef.metadata)
3654
+ ])
3655
+ )
3656
+ };
2497
3657
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
2498
- ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
3658
+ const schemaName = ctx.typeNameMap[name] ?? name;
3659
+ ctx.defs[schemaName] = generateTypeNode(typeDef.type, ctx);
3660
+ applyResolvedMetadata(ctx.defs[schemaName], typeDef.metadata);
2499
3661
  if (typeDef.constraints && typeDef.constraints.length > 0) {
2500
- applyConstraints(ctx.defs[name], typeDef.constraints, ctx);
3662
+ applyConstraints(ctx.defs[schemaName], typeDef.constraints, ctx);
2501
3663
  }
2502
3664
  if (typeDef.annotations && typeDef.annotations.length > 0) {
2503
- applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
3665
+ applyAnnotations(ctx.defs[schemaName], typeDef.annotations, ctx);
2504
3666
  }
2505
3667
  }
2506
3668
  const properties = {};
@@ -2513,6 +3675,7 @@ function generateJsonSchemaFromIR(ir, options) {
2513
3675
  properties,
2514
3676
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
2515
3677
  };
3678
+ applyResolvedMetadata(result, ir.metadata);
2516
3679
  if (ir.annotations && ir.annotations.length > 0) {
2517
3680
  applyAnnotations(result, ir.annotations, ctx);
2518
3681
  }
@@ -2525,9 +3688,9 @@ function collectFields(elements, properties, required, ctx) {
2525
3688
  for (const element of elements) {
2526
3689
  switch (element.kind) {
2527
3690
  case "field":
2528
- properties[element.name] = generateFieldSchema(element, ctx);
3691
+ properties[getSerializedName(element.name, element.metadata)] = generateFieldSchema(element, ctx);
2529
3692
  if (element.required) {
2530
- required.push(element.name);
3693
+ required.push(getSerializedName(element.name, element.metadata));
2531
3694
  }
2532
3695
  break;
2533
3696
  case "group":
@@ -2571,6 +3734,7 @@ function generateFieldSchema(field, ctx) {
2571
3734
  rootAnnotations.push(annotation);
2572
3735
  }
2573
3736
  }
3737
+ applyResolvedMetadata(schema, field.metadata);
2574
3738
  applyAnnotations(schema, rootAnnotations, ctx);
2575
3739
  if (itemStringSchema !== void 0) {
2576
3740
  applyAnnotations(itemStringSchema, itemAnnotations, ctx);
@@ -2578,7 +3742,7 @@ function generateFieldSchema(field, ctx) {
2578
3742
  if (pathConstraints.length === 0) {
2579
3743
  return schema;
2580
3744
  }
2581
- return applyPathTargetedConstraints(schema, pathConstraints, ctx);
3745
+ return applyPathTargetedConstraints(schema, pathConstraints, ctx, field.type);
2582
3746
  }
2583
3747
  function isStringItemConstraint(constraint) {
2584
3748
  switch (constraint.constraintKind) {
@@ -2590,9 +3754,11 @@ function isStringItemConstraint(constraint) {
2590
3754
  return false;
2591
3755
  }
2592
3756
  }
2593
- function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
3757
+ function applyPathTargetedConstraints(schema, pathConstraints, ctx, typeNode) {
2594
3758
  if (schema.type === "array" && schema.items) {
2595
- schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
3759
+ const referencedType = typeNode?.kind === "reference" ? resolveReferencedType(typeNode, ctx) : void 0;
3760
+ const nestedType = typeNode?.kind === "array" ? typeNode.items : referencedType?.kind === "array" ? referencedType.items : void 0;
3761
+ schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx, nestedType);
2596
3762
  return schema;
2597
3763
  }
2598
3764
  const byTarget = /* @__PURE__ */ new Map();
@@ -2607,7 +3773,7 @@ function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
2607
3773
  for (const [target, constraints] of byTarget) {
2608
3774
  const subSchema = {};
2609
3775
  applyConstraints(subSchema, constraints, ctx);
2610
- propertyOverrides[target] = subSchema;
3776
+ propertyOverrides[resolveSerializedPropertyName(target, typeNode, ctx)] = subSchema;
2611
3777
  }
2612
3778
  if (schema.$ref) {
2613
3779
  const { $ref, ...rest } = schema;
@@ -2655,7 +3821,7 @@ function generateTypeNode(type, ctx) {
2655
3821
  case "union":
2656
3822
  return generateUnionType(type, ctx);
2657
3823
  case "reference":
2658
- return generateReferenceType(type);
3824
+ return generateReferenceType(type, ctx);
2659
3825
  case "dynamic":
2660
3826
  return generateDynamicType(type);
2661
3827
  case "custom":
@@ -2696,9 +3862,10 @@ function generateObjectType(type, ctx) {
2696
3862
  const properties = {};
2697
3863
  const required = [];
2698
3864
  for (const prop of type.properties) {
2699
- properties[prop.name] = generatePropertySchema(prop, ctx);
3865
+ const propertyName = getSerializedName(prop.name, prop.metadata);
3866
+ properties[propertyName] = generatePropertySchema(prop, ctx);
2700
3867
  if (!prop.optional) {
2701
- required.push(prop.name);
3868
+ required.push(propertyName);
2702
3869
  }
2703
3870
  }
2704
3871
  const schema = { type: "object", properties };
@@ -2719,6 +3886,7 @@ function generateRecordType(type, ctx) {
2719
3886
  function generatePropertySchema(prop, ctx) {
2720
3887
  const schema = generateTypeNode(prop.type, ctx);
2721
3888
  applyConstraints(schema, prop.constraints, ctx);
3889
+ applyResolvedMetadata(schema, prop.metadata);
2722
3890
  applyAnnotations(schema, prop.annotations, ctx);
2723
3891
  return schema;
2724
3892
  }
@@ -2747,8 +3915,28 @@ function isNullableUnion(type) {
2747
3915
  ).length;
2748
3916
  return nullCount === 1;
2749
3917
  }
2750
- function generateReferenceType(type) {
2751
- return { $ref: `#/$defs/${type.name}` };
3918
+ function generateReferenceType(type, ctx) {
3919
+ return { $ref: `#/$defs/${ctx.typeNameMap[type.name] ?? type.name}` };
3920
+ }
3921
+ function applyResolvedMetadata(schema, metadata) {
3922
+ const displayName = getDisplayName(metadata);
3923
+ if (displayName !== void 0) {
3924
+ schema.title = displayName;
3925
+ }
3926
+ }
3927
+ function resolveReferencedType(type, ctx) {
3928
+ return ctx.typeRegistry[type.name]?.type;
3929
+ }
3930
+ function resolveSerializedPropertyName(logicalName, typeNode, ctx) {
3931
+ if (typeNode?.kind === "object") {
3932
+ const property = typeNode.properties.find((candidate) => candidate.name === logicalName);
3933
+ return property === void 0 ? logicalName : getSerializedName(property.name, property.metadata);
3934
+ }
3935
+ if (typeNode?.kind === "reference") {
3936
+ const referencedType = resolveReferencedType(typeNode, ctx);
3937
+ return referencedType === void 0 ? logicalName : resolveSerializedPropertyName(logicalName, referencedType, ctx);
3938
+ }
3939
+ return logicalName;
2752
3940
  }
2753
3941
  function generateDynamicType(type) {
2754
3942
  if (type.dynamicKind === "enum") {
@@ -2828,7 +4016,7 @@ function applyAnnotations(schema, annotations, ctx) {
2828
4016
  for (const annotation of annotations) {
2829
4017
  switch (annotation.annotationKind) {
2830
4018
  case "displayName":
2831
- schema.title = annotation.value;
4019
+ schema.title ??= annotation.value;
2832
4020
  break;
2833
4021
  case "description":
2834
4022
  schema.description = annotation.value;
@@ -3078,13 +4266,21 @@ function combineRules(parentRule, childRule) {
3078
4266
  }
3079
4267
  };
3080
4268
  }
3081
- function fieldNodeToControl(field, parentRule) {
3082
- const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
4269
+ function getFieldDisplayName(field) {
4270
+ const resolvedDisplayName = getDisplayName(field.metadata);
4271
+ if (resolvedDisplayName !== void 0) {
4272
+ return resolvedDisplayName;
4273
+ }
4274
+ return field.annotations.find((annotation) => annotation.annotationKind === "displayName")?.value;
4275
+ }
4276
+ function fieldNodeToControl(field, fieldNameMap, parentRule) {
3083
4277
  const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
4278
+ const serializedName = fieldNameMap.get(field.name) ?? getSerializedName(field.name, field.metadata);
4279
+ const displayName = getFieldDisplayName(field);
3084
4280
  const control = {
3085
4281
  type: "Control",
3086
- scope: fieldToScope(field.name),
3087
- ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
4282
+ scope: fieldToScope(serializedName),
4283
+ ...displayName !== void 0 && { label: displayName },
3088
4284
  ...placeholderAnnotation !== void 0 && {
3089
4285
  options: { placeholder: placeholderAnnotation.value }
3090
4286
  },
@@ -3092,30 +4288,30 @@ function fieldNodeToControl(field, parentRule) {
3092
4288
  };
3093
4289
  return control;
3094
4290
  }
3095
- function groupNodeToLayout(group, parentRule) {
4291
+ function groupNodeToLayout(group, fieldNameMap, parentRule) {
3096
4292
  return {
3097
4293
  type: "Group",
3098
4294
  label: group.label,
3099
- elements: irElementsToUiSchema(group.elements, parentRule),
4295
+ elements: irElementsToUiSchema(group.elements, fieldNameMap, parentRule),
3100
4296
  ...parentRule !== void 0 && { rule: parentRule }
3101
4297
  };
3102
4298
  }
3103
- function irElementsToUiSchema(elements, parentRule) {
4299
+ function irElementsToUiSchema(elements, fieldNameMap, parentRule) {
3104
4300
  const result = [];
3105
4301
  for (const element of elements) {
3106
4302
  switch (element.kind) {
3107
4303
  case "field": {
3108
- result.push(fieldNodeToControl(element, parentRule));
4304
+ result.push(fieldNodeToControl(element, fieldNameMap, parentRule));
3109
4305
  break;
3110
4306
  }
3111
4307
  case "group": {
3112
- result.push(groupNodeToLayout(element, parentRule));
4308
+ result.push(groupNodeToLayout(element, fieldNameMap, parentRule));
3113
4309
  break;
3114
4310
  }
3115
4311
  case "conditional": {
3116
- const newRule = createShowRule(element.fieldName, element.value);
4312
+ const newRule = createShowRule(fieldNameMap.get(element.fieldName) ?? element.fieldName, element.value);
3117
4313
  const combinedRule = parentRule !== void 0 ? combineRules(parentRule, newRule) : newRule;
3118
- const childElements = irElementsToUiSchema(element.elements, combinedRule);
4314
+ const childElements = irElementsToUiSchema(element.elements, fieldNameMap, combinedRule);
3119
4315
  result.push(...childElements);
3120
4316
  break;
3121
4317
  }
@@ -3129,12 +4325,35 @@ function irElementsToUiSchema(elements, parentRule) {
3129
4325
  return result;
3130
4326
  }
3131
4327
  function generateUiSchemaFromIR(ir) {
4328
+ assertNoSerializedNameCollisions(ir);
4329
+ const fieldNameMap = collectFieldNameMap(ir.elements);
3132
4330
  const result = {
3133
4331
  type: "VerticalLayout",
3134
- elements: irElementsToUiSchema(ir.elements)
4332
+ elements: irElementsToUiSchema(ir.elements, fieldNameMap)
3135
4333
  };
3136
4334
  return parseOrThrow(uiSchema, result, "UI Schema");
3137
4335
  }
4336
+ function collectFieldNameMap(elements) {
4337
+ const map = /* @__PURE__ */ new Map();
4338
+ for (const element of elements) {
4339
+ switch (element.kind) {
4340
+ case "field":
4341
+ map.set(element.name, getSerializedName(element.name, element.metadata));
4342
+ break;
4343
+ case "group":
4344
+ case "conditional":
4345
+ for (const [key, value] of collectFieldNameMap(element.elements)) {
4346
+ map.set(key, value);
4347
+ }
4348
+ break;
4349
+ default: {
4350
+ const _exhaustive = element;
4351
+ void _exhaustive;
4352
+ }
4353
+ }
4354
+ }
4355
+ return map;
4356
+ }
3138
4357
 
3139
4358
  // src/validate/constraint-validator.ts
3140
4359
  import {
@@ -3219,7 +4438,11 @@ function generateClassSchemas(analysis, source, options) {
3219
4438
  if (errorDiagnostics !== void 0 && errorDiagnostics.length > 0) {
3220
4439
  throw new Error(formatValidationError(errorDiagnostics));
3221
4440
  }
3222
- const ir = canonicalizeTSDoc(analysis, source);
4441
+ const ir = canonicalizeTSDoc(
4442
+ analysis,
4443
+ source,
4444
+ options?.metadata !== void 0 ? { metadata: options.metadata } : void 0
4445
+ );
3223
4446
  const validationResult = validateIR(ir, {
3224
4447
  ...options?.extensionRegistry !== void 0 && {
3225
4448
  extensionRegistry: options.extensionRegistry
@@ -3332,7 +4555,24 @@ import { IR_VERSION as IR_VERSION3 } from "@formspec/core/internals";
3332
4555
  function typeToJsonSchema(type, checker) {
3333
4556
  const typeRegistry = {};
3334
4557
  const visiting = /* @__PURE__ */ new Set();
3335
- const typeNode = resolveTypeNode(type, checker, "", typeRegistry, visiting);
4558
+ const diagnostics = [];
4559
+ const typeNode = resolveTypeNode(
4560
+ type,
4561
+ checker,
4562
+ "",
4563
+ typeRegistry,
4564
+ visiting,
4565
+ void 0,
4566
+ void 0,
4567
+ void 0,
4568
+ diagnostics
4569
+ );
4570
+ if (diagnostics.length > 0) {
4571
+ const diagnosticDetails = diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("; ");
4572
+ throw new Error(
4573
+ `FormSpec validation failed while resolving method schema types. ${diagnosticDetails}`
4574
+ );
4575
+ }
3336
4576
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
3337
4577
  const ir = {
3338
4578
  kind: "form-ir",