babelfhir-ts 1.5.19 → 1.5.21

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 (38) hide show
  1. package/out/src/cli/updateCommand.js +19 -12
  2. package/out/src/generator/core/constants.js +19 -0
  3. package/out/src/generator/core/extensionSdReader.js +99 -0
  4. package/out/src/generator/core/utils.js +97 -3
  5. package/out/src/generator/emitters/class/classTemplate.js +8 -2
  6. package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
  7. package/out/src/generator/emitters/client/clientGenerator.js +22 -6
  8. package/out/src/generator/emitters/client/smartAuthGenerator.js +4 -1
  9. package/out/src/generator/emitters/interface/backboneSliceTyping.js +15 -4
  10. package/out/src/generator/emitters/interface/importManager.js +28 -2
  11. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +21 -16
  12. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +16 -0
  13. package/out/src/generator/emitters/interface/postProcessExtensions.js +1 -0
  14. package/out/src/generator/emitters/validator/closedSlicingValidation.js +146 -0
  15. package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
  16. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +200 -35
  17. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +130 -299
  18. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +89 -16
  19. package/out/src/generator/emitters/validator/subExtensionScope.js +100 -0
  20. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +113 -237
  21. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +13 -4
  22. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +104 -63
  23. package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
  24. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +492 -46
  25. package/out/src/generator/emitters/validator/validatorGenerator.js +170 -169
  26. package/out/src/generator/emitters/validator/validatorRuntime.js +279 -0
  27. package/out/src/generator/emitters/validator/validatorTemplates.js +186 -54
  28. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +29 -19
  29. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +16 -1
  30. package/out/src/generator/index.js +21 -11
  31. package/out/src/generator/parser/packageParser.js +1 -8
  32. package/out/src/generator/parser/sdParser.js +1 -0
  33. package/out/src/generator/sdProcessor.js +303 -30
  34. package/out/src/generator/sdProcessorHelpers.js +1 -1
  35. package/out/src/generator/sdProcessorRequiredFields.js +120 -0
  36. package/out/src/main.js +37 -11
  37. package/package.json +3 -1
  38. package/parity-matrix.json +18 -18
@@ -114,21 +114,28 @@ async function promptInstallRedirect(packageFilter, confirm) {
114
114
  return { redirectInstall: packageFilter };
115
115
  return {};
116
116
  }
117
- /** Default Y/N prompt reading from stdin. */
118
- function defaultConfirm(question) {
119
- return new Promise((resolve) => {
120
- process.stdout.write(question);
121
- const rl = (async () => {
122
- const { createInterface } = await import('readline');
123
- return createInterface({ input: process.stdin, output: process.stdout });
124
- })();
125
- rl.then(r => {
126
- r.question('', (answer) => {
127
- r.close();
117
+ /**
118
+ * Default Y/N prompt reading from stdin.
119
+ *
120
+ * The readline import is awaited rather than `.then`-ed inside a `new Promise`:
121
+ * that form dropped a rejection on the floor, and since nothing ever called
122
+ * `resolve`, a failed import left the caller awaiting a promise that could not
123
+ * settle. Declining is the safe answer when the prompt cannot be shown.
124
+ */
125
+ async function defaultConfirm(question) {
126
+ process.stdout.write(question);
127
+ const { createInterface } = await import('readline');
128
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
129
+ try {
130
+ return await new Promise((resolve) => {
131
+ rl.question('', (answer) => {
128
132
  resolve(!answer || answer.toLowerCase().startsWith('y'));
129
133
  });
130
134
  });
131
- });
135
+ }
136
+ finally {
137
+ rl.close();
138
+ }
132
139
  }
133
140
  // ── Discovery ───────────────────────────────────────────────────────────────
134
141
  /** Find all lib/ directories starting from startDir. Non-recursive returns only startDir/lib. */
@@ -57,6 +57,25 @@ export const FETCH_TIMEOUT_MS = 15_000;
57
57
  export const TX_TIMEOUT_MS = 30_000;
58
58
  /** Max filename length for terminology expansion cache files */
59
59
  export const MAX_CACHE_FILENAME_LENGTH = 200;
60
+ /**
61
+ * FHIR package registries in priority order.
62
+ *
63
+ * packages.fhir.org is the canonical source; packages.simplifier.net is the
64
+ * fallback and carries some national IGs the canonical registry does not mirror.
65
+ * Both speak the npm metadata shape, so `GET <registry>/<packageId>` returns
66
+ * `dist-tags` and `versions`.
67
+ */
68
+ /**
69
+ * Attempts per registry when downloading a package tarball.
70
+ *
71
+ * Registry blips are the single largest cause of spurious parity failures: a run
72
+ * pulls ~30 packages and a lone `fetch failed` aborted the whole generation.
73
+ */
74
+ export const PACKAGE_DOWNLOAD_ATTEMPTS = 3;
75
+ export const FHIR_REGISTRIES = [
76
+ 'https://packages.fhir.org',
77
+ 'https://packages.simplifier.net',
78
+ ];
60
79
  // ─── FHIR StructureDefinition URL prefixes ───────────────────────────────────
61
80
  export const FHIR_SD_URL_PREFIX = 'http://hl7.org/fhir/StructureDefinition/';
62
81
  export const FHIR_SD_URL_PREFIX_HTTPS = 'https://hl7.org/fhir/StructureDefinition/';
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Reading what an Extension StructureDefinition declares about its own contents.
3
+ *
4
+ * A complex extension carries its requirements in its own SD. A profile that
5
+ * references one re-declares the sub-extension paths as mustSupport markers but
6
+ * copies none of the constraints, so anything read only from the profile's field
7
+ * model misses them entirely — KBV_PR_ERP_PracticeSupply's
8
+ * `extension:Kostentraeger.extension:Kostentraegertyp.value[x]:valueCoding` has no
9
+ * binding, while KBV_EX_ERP_PracticeSupply_Payor binds it required to
10
+ * KBV_VS_FOR_Payor_type.
11
+ */
12
+ /** `Extension.extension:<name>` with no further path below it. */
13
+ const SUB_SLICE_ID = /^Extension\.extension:([^.:]+)$/;
14
+ /**
15
+ * The elements to read constraints from.
16
+ *
17
+ * Prefer the snapshot, but fall back to the differential: several published
18
+ * extension SDs ship without one (KBV_EX_ERP_PracticeSupply_Payor among them), and
19
+ * requiring a snapshot meant they resolved to no metadata at all.
20
+ */
21
+ export function extensionElements(sd) {
22
+ const els = sd?.snapshot?.element ?? sd?.differential?.element;
23
+ return els?.length ? els : undefined;
24
+ }
25
+ /** Read a binding off an element, normalising the ValueSet reference to a bare uri. */
26
+ export function readBinding(el) {
27
+ const binding = el?.binding;
28
+ if (!binding?.strength || !binding.valueSet)
29
+ return undefined;
30
+ const raw = typeof binding.valueSet === 'string'
31
+ ? binding.valueSet
32
+ : binding.valueSet?.reference;
33
+ if (!raw)
34
+ return undefined;
35
+ return { strength: binding.strength, uri: raw.split('|')[0] };
36
+ }
37
+ /** Type codes declared on an element. */
38
+ function typeCodes(el) {
39
+ return (el?.type ?? []).map(t => t.code).filter((c) => !!c);
40
+ }
41
+ /**
42
+ * `min` on the unsliced `Extension.extension` element.
43
+ *
44
+ * This is what HL7 reports as "Extension.extension: minimum required = N", and it is
45
+ * not the sum of the slice minimums — the SD states it directly.
46
+ */
47
+ export function readSubExtensionMin(els) {
48
+ const el = els.find(e => (e.id ?? e.path) === 'Extension.extension' && !e.sliceName);
49
+ return typeof el?.min === 'number' ? el.min : undefined;
50
+ }
51
+ /**
52
+ * Collect the sub-extension slices a complex extension declares.
53
+ *
54
+ * A slice with no resolvable `url` is dropped rather than reported under its slice
55
+ * name: the url is how a member is recognised at runtime, and without it any check
56
+ * would be guessing which member to apply itself to.
57
+ */
58
+ export function collectSubExtensionSlices(els) {
59
+ const byId = new Map();
60
+ for (const el of els) {
61
+ const id = el.id ?? el.path;
62
+ if (id)
63
+ byId.set(id, el);
64
+ }
65
+ const out = [];
66
+ for (const el of els) {
67
+ const id = el.id ?? el.path;
68
+ const match = id ? SUB_SLICE_ID.exec(id) : null;
69
+ if (!match)
70
+ continue;
71
+ const sliceName = el.sliceName ?? match[1];
72
+ const urlEl = byId.get(`Extension.extension:${sliceName}.url`);
73
+ const url = typeof urlEl?.fixedUri === 'string' ? urlEl.fixedUri : undefined;
74
+ if (!url)
75
+ continue;
76
+ const valueEl = byId.get(`Extension.extension:${sliceName}.value[x]`);
77
+ let valueTypes = typeCodes(valueEl);
78
+ let binding = readBinding(valueEl);
79
+ // The binding usually sits on the typed slice (…value[x]:valueCoding) rather than
80
+ // on value[x] itself, which is where the Payor extension puts it.
81
+ for (const code of valueTypes.length ? valueTypes : ['']) {
82
+ const suffix = code ? code.charAt(0).toUpperCase() + code.slice(1) : '';
83
+ const typedEl = suffix ? byId.get(`Extension.extension:${sliceName}.value[x]:value${suffix}`) : undefined;
84
+ if (!typedEl)
85
+ continue;
86
+ binding = binding ?? readBinding(typedEl);
87
+ if (valueTypes.length === 0)
88
+ valueTypes = typeCodes(typedEl);
89
+ }
90
+ out.push({
91
+ sliceName,
92
+ url,
93
+ min: typeof el.min === 'number' ? el.min : 0,
94
+ valueTypes,
95
+ binding,
96
+ });
97
+ }
98
+ return out;
99
+ }
@@ -24,6 +24,38 @@ export function capitalize(str) {
24
24
  return str;
25
25
  return str.charAt(0).toUpperCase() + str.slice(1);
26
26
  }
27
+ /**
28
+ * The property name a choice element takes for one of its permitted types.
29
+ *
30
+ * A choice element has no property of its own: FHIR declares one property per
31
+ * permitted type, named for the type code capitalized (`value[x]` as a
32
+ * SampledData is `valueSampledData`, as a `code` primitive it is `valueCode`).
33
+ *
34
+ * Pass the *type code*, never a profile name. R4 declares
35
+ * `Coverage.costToBeneficiary.value[x]` as a Quantity profiled to SimpleQuantity,
36
+ * and the property is `valueQuantity` — `valueSimpleQuantity` does not exist on
37
+ * any resource, so emitting it produces data every validator rejects.
38
+ *
39
+ * @param baseName the choice element's name with `[x]` already removed
40
+ */
41
+ export function choicePropertyName(baseName, typeCode) {
42
+ return baseName + capitalize(typeCode);
43
+ }
44
+ /**
45
+ * The single type a choice element has been narrowed to, or undefined while it
46
+ * still permits more than one.
47
+ *
48
+ * A still-open choice has no one property name, so callers must leave the base
49
+ * definition's expanded properties in force rather than pick a type.
50
+ */
51
+ export function narrowedChoiceType(field) {
52
+ if (field.typeOptions && field.typeOptions.length > 1)
53
+ return undefined;
54
+ const type = field.typeOptions?.[0]?.code || field.type;
55
+ if (!type || type === 'any' || type === 'unknown')
56
+ return undefined;
57
+ return type;
58
+ }
27
59
  /**
28
60
  * Extract base FHIR types from all interface declarations in a file.
29
61
  * Handles:
@@ -149,6 +181,48 @@ export function alignFields(newFields, oldFields) {
149
181
  // Return the original oldFields and the aligned newFields
150
182
  return { alignedNewFields, oldFields };
151
183
  }
184
+ /** A type the parser could not pin down from the element itself. */
185
+ function isUnstatedType(type) {
186
+ return !type || type === 'any';
187
+ }
188
+ /**
189
+ * Carry forward what a differential element left unstated.
190
+ *
191
+ * A differential says only what it changes, so an element that constrains
192
+ * cardinality alone carries no `type` and an element that constrains `min` alone
193
+ * carries no `max`. Both are inherited rather than defaulted away.
194
+ */
195
+ function inheritUnstatedFromBase(field, base) {
196
+ if (!base)
197
+ return field;
198
+ const merged = { ...field };
199
+ if (isUnstatedType(merged.type) && !isUnstatedType(base.type)) {
200
+ merged.type = base.type;
201
+ }
202
+ if (!merged.baseTypeCode && base.baseTypeCode) {
203
+ merged.baseTypeCode = base.baseTypeCode;
204
+ }
205
+ // `max` absent means the differential did not narrow the upper bound, so the
206
+ // base's bound — and with it whether the element repeats — still holds.
207
+ if (merged.max === undefined && base.max !== undefined) {
208
+ merged.max = base.max;
209
+ merged.isArray = base.isArray;
210
+ }
211
+ if (!merged.typeOptions?.length && base.typeOptions?.length) {
212
+ merged.typeOptions = base.typeOptions;
213
+ }
214
+ // An extension slice is recognised at runtime by the canonical its profile
215
+ // names. A derived profile that only tightens cardinality does not restate it —
216
+ // KBV_PR_ERP_Medication_PZN redeclares extension:Kategorie and extension:Normgroesse
217
+ // with no type.profile, while KBV_PR_Base_Medication gives them
218
+ // KBV_EX_Base_Medication_Type and .../normgroesse. Dropping the base's left two
219
+ // of four slices unidentifiable, so the closed-slicing check on
220
+ // Medication.extension could not build its allowed set and stayed silent.
221
+ if (!merged.profileUrls?.length && base.profileUrls?.length) {
222
+ merged.profileUrls = base.profileUrls;
223
+ }
224
+ return merged;
225
+ }
152
226
  export function mergeFields(newFields, oldFields) {
153
227
  const fieldMap = new Map();
154
228
  // Create unique key for field - use elementId if available (includes slice names for child elements)
@@ -176,9 +250,22 @@ export function mergeFields(newFields, oldFields) {
176
250
  }
177
251
  fieldMap.set(key, field);
178
252
  }
179
- // Overwrite or add newFields to the map
253
+ // Overwrite or add newFields to the map.
254
+ //
255
+ // A differential element states only what it *changes*. Replacing the base
256
+ // field wholesale therefore threw away everything it stayed silent about:
257
+ // KBV constrains Medication.amount.numerator.value to max=0 and says nothing
258
+ // about its type, so the merged field read `any` and every type-aware check
259
+ // skipped it — HL7 reported "The value 'Example value' is not a valid decimal"
260
+ // where we said nothing. The same silence on `max` left Medication.ingredient
261
+ // with isArray=false despite being 0..* in the base, so paths beneath it were
262
+ // never walked.
263
+ //
264
+ // Only genuinely absent properties are filled in; anything the differential
265
+ // states still wins.
180
266
  for (const field of newFields) {
181
- fieldMap.set(getFieldKey(field), field);
267
+ const key = getFieldKey(field);
268
+ fieldMap.set(key, inheritUnstatedFromBase(field, fieldMap.get(key)));
182
269
  }
183
270
  // Return the merged fields as an array
184
271
  return Array.from(fieldMap.values());
@@ -220,9 +307,16 @@ function caseAwareWriteFile(filePath, content) {
220
307
  }
221
308
  /**
222
309
  * Extracts the base resource name from a baseDefinition URL.
310
+ *
311
+ * The version suffix has to go first. A canonical may be pinned
312
+ * (`http://hl7.org/fhir/StructureDefinition/Base|4.0.1`, as IPS 2.0.1's logical
313
+ * models write it), and keeping it yielded the base name `Base_4_0_1` after
314
+ * sanitising. That defeated the `baseResource === 'Base'` check that suppresses
315
+ * extending the abstract root, so the emitter produced
316
+ * `export type Document = Base_4_0_1` plus an import of a type nothing generates.
223
317
  */
224
318
  export function getBaseResource(baseDefinition) {
225
- return baseDefinition?.split("/").pop() || "Resource";
319
+ return stripVersionFromCanonicalUrl(baseDefinition ?? '').split('/').pop() || 'Resource';
226
320
  }
227
321
  /**
228
322
  * Sanitizes a string so it can be used as a valid TypeScript identifier.
@@ -26,7 +26,7 @@ export function renderClassTemplate(params) {
26
26
  const helperImportList = Array.from(helperImports).join(', ');
27
27
  const hasVsBindings = Object.keys(valueSetBindings).length > 0;
28
28
  return `
29
- import { validate${interfaceName}, ${interfaceName} } from "./${interfaceName}.js";
29
+ import { validate${interfaceName}, type ${interfaceName} } from "./${interfaceName}.js";
30
30
  import type { ValidatorOptions } from "./ValidatorOptions.js";
31
31
  // Only import helpers actually referenced below (dynamic based on required fields)
32
32
  import { ${helperImportList} } from "./random/index.js";
@@ -55,7 +55,13 @@ function safeClone<T>(value: T): T {
55
55
 
56
56
  export class ${className} {
57
57
  ${generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder, codeableConceptBindings)}
58
- constructor(private resource: ${interfaceName}) {
58
+ private resource: ${interfaceName};
59
+
60
+ // Declared and assigned rather than written as a parameter property, which
61
+ // erasableSyntaxOnly bans: it emits real assignment code from what reads as a
62
+ // type annotation. Generated output is held to the same strictness as src/.
63
+ constructor(resource: ${interfaceName}) {
64
+ this.resource = resource;
59
65
  ${constructorMetaProfile}
60
66
  }
61
67
  /** Validate current resource instance. */
@@ -221,7 +221,9 @@ export function buildNestedRequirementsObject(nestedRequirements) {
221
221
  if (!nestedRequirements || nestedRequirements.length === 0) {
222
222
  return null;
223
223
  }
224
- // Collect all array segments from all requirements to know which paths need array wrapping
224
+ // Relative paths that are arrays, so nesting knows where to wrap. Keyed by the
225
+ // full relative path rather than by segment name: `name.part` is 0..* while the
226
+ // `part` string inside it is 1..1, and one shared verdict wrapped the string too.
225
227
  const allArraySegments = new Set();
226
228
  for (const req of nestedRequirements) {
227
229
  if (req.arraySegments) {
@@ -250,7 +252,7 @@ export function buildNestedRequirementsObject(nestedRequirements) {
250
252
  // Navigate/create nested objects for all but the last part
251
253
  for (let i = 0; i < parts.length - 1; i++) {
252
254
  const part = parts[i];
253
- const isArraySegment = allArraySegments.has(part);
255
+ const isArraySegment = allArraySegments.has(parts.slice(0, i + 1).join('.'));
254
256
  if (!(part in current)) {
255
257
  // Create either an array with one object or a plain object
256
258
  if (isArraySegment) {
@@ -346,7 +348,7 @@ export function buildNestedRequirementsObject(nestedRequirements) {
346
348
  }
347
349
  // If the final segment is an array segment, wrap the value in an array.
348
350
  // Also wrap if the field is inherently an array in FHIR base types
349
- const shouldWrap = (allArraySegments.has(lastPart) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
351
+ const shouldWrap = (allArraySegments.has(req.path) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
350
352
  && !Array.isArray(value);
351
353
  if (shouldWrap) {
352
354
  current[lastPart] = [value];
@@ -176,11 +176,16 @@ export type FetchFn = typeof globalThis.fetch;
176
176
  export class FhirResourceReader<T, S extends SearchParams = SearchParams> implements FhirResourceSearcher<T, S> {
177
177
  private readonly fetchFn: FetchFn;
178
178
 
179
+ public readonly baseUrl: string;
180
+ public readonly resourceType: string;
181
+
179
182
  constructor(
180
- public readonly baseUrl: string,
181
- public readonly resourceType: string,
183
+ baseUrl: string,
184
+ resourceType: string,
182
185
  fetchFn?: FetchFn,
183
186
  ) {
187
+ this.baseUrl = baseUrl;
188
+ this.resourceType = resourceType;
184
189
  this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
185
190
  }
186
191
 
@@ -321,11 +326,16 @@ import type { FetchFn } from "./resource-reader.js";
321
326
  export class FhirResourceWriterImpl<T> implements FhirResourceWriter<T> {
322
327
  private readonly fetchFn: FetchFn;
323
328
 
329
+ public readonly baseUrl: string;
330
+ public readonly resourceType: string;
331
+
324
332
  constructor(
325
- public readonly baseUrl: string,
326
- public readonly resourceType: string,
333
+ baseUrl: string,
334
+ resourceType: string,
327
335
  fetchFn?: FetchFn,
328
336
  ) {
337
+ this.baseUrl = baseUrl;
338
+ this.resourceType = resourceType;
329
339
  this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
330
340
  }
331
341
 
@@ -451,7 +461,11 @@ export type BundleEntry<T extends FhirResource = FhirResource> = {
451
461
  * FHIR Bundle Parser - provides type-safe parsing and filtering of FHIR Bundles
452
462
  */
453
463
  export class BundleParser {
454
- constructor(private readonly bundle: Bundle<FhirResource>) {}
464
+ private readonly bundle: Bundle<FhirResource>;
465
+
466
+ constructor(bundle: Bundle<FhirResource>) {
467
+ this.bundle = bundle;
468
+ }
455
469
 
456
470
  /**
457
471
  * Get all resources from the bundle
@@ -659,11 +673,13 @@ ${writerMethods}
659
673
  export class FhirClient {
660
674
  private readonly readClient: FhirReadClient;
661
675
  private readonly writeClient: FhirWriteClient;
676
+ public readonly baseUrl: string;
662
677
 
663
678
  constructor(
664
- public readonly baseUrl: string,
679
+ baseUrl: string,
665
680
  fetchFn?: FetchFn,
666
681
  ) {
682
+ this.baseUrl = baseUrl;
667
683
  this.readClient = new FhirReadClient(baseUrl, fetchFn);
668
684
  this.writeClient = new FhirWriteClient(baseUrl, fetchFn);
669
685
  }
@@ -167,7 +167,10 @@ export class SmartAuth {
167
167
  private readonly storage: Storage;
168
168
  private readonly keys: ReturnType<typeof storageKeys>;
169
169
 
170
- constructor(public readonly config: SmartConfig) {
170
+ public readonly config: SmartConfig;
171
+
172
+ constructor(config: SmartConfig) {
173
+ this.config = config;
171
174
  this.storage = config.storage ?? sessionStorage;
172
175
  this.keys = storageKeys(config.storagePrefix ?? "smart_");
173
176
  }
@@ -18,7 +18,7 @@
18
18
  * }
19
19
  * ```
20
20
  */
21
- import { capitalize, sanitizeIdentifier } from '../../core/utils.js';
21
+ import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier } from '../../core/utils.js';
22
22
  import { getRules } from '../../fhir/versionContext.js';
23
23
  import { logger } from '../../../logger.js';
24
24
  const log = logger.withTag('backbone-slices');
@@ -135,7 +135,7 @@ function collectSliceGroups(fields, baseFields, baseResource) {
135
135
  if (!isEligibleBackbonePath(field.baseTypeCode || field.type || '', path, baseResource))
136
136
  continue;
137
137
  const childFields = gatherChildFields(fields, path);
138
- const slicingRules = field.slicingRules || 'open';
138
+ const slicingRules = field.slicingRules ?? 'open';
139
139
  seenPaths.add(path);
140
140
  groups.push({ parentField: field, sliceFields, childFields, slicingRules });
141
141
  }
@@ -160,7 +160,7 @@ function collectSliceGroups(fields, baseFields, baseResource) {
160
160
  if (sliceFields.length === 0)
161
161
  continue;
162
162
  const childFields = gatherChildFields(fields, path);
163
- const slicingRules = parentField.slicingRules || 'open';
163
+ const slicingRules = parentField.slicingRules ?? 'open';
164
164
  seenPaths.add(path);
165
165
  groups.push({ parentField, sliceFields, childFields, slicingRules });
166
166
  }
@@ -263,7 +263,18 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
263
263
  // metadata and their inherited type (e.g. Quantity) is misleading
264
264
  if (choiceTypeParents.has(relativePath))
265
265
  continue;
266
- const propName = relativePath.replace(/\[x\]$/, '');
266
+ // A choice element has no property of its own, so it is named for the single
267
+ // type the slice narrowed it to. Stripping `[x]` produced `value`, `effective`
268
+ // and the like — properties no resource carries, so the slice demanded a
269
+ // property FHIR has no such thing as while leaving the real `valueSampledData`
270
+ // unconstrained. A choice still open here names nothing and is skipped.
271
+ let propName = relativePath;
272
+ if (propName.endsWith('[x]')) {
273
+ const narrowedTo = narrowedChoiceType(child);
274
+ if (!narrowedTo)
275
+ continue;
276
+ propName = choicePropertyName(propName.slice(0, -'[x]'.length), narrowedTo);
277
+ }
267
278
  if (child.fixedValue !== undefined) {
268
279
  // Fixed value constraint
269
280
  const fixedLiteral = JSON.stringify(child.fixedValue);
@@ -191,6 +191,33 @@ export class ImportManager {
191
191
  }
192
192
  return statements.join('\n');
193
193
  }
194
+ /**
195
+ * Whether `name` is referenced in `body`, counting the profile ids that
196
+ * resolve to it as references too.
197
+ *
198
+ * An external profile can be reached under more than one name: the id taken
199
+ * from its canonical URL (`AuDosage` from `.../au-dosage`) and the interface
200
+ * name it is finally generated under (`AUBaseDosage`, from the SD's name).
201
+ * `generateImportStatements` deduplicates those to the resolved name, but the
202
+ * interface body is still written with the id at that point — a later pass
203
+ * rewrites it. Matching only the resolved name therefore pruned an import that
204
+ * the rewritten body goes on to need, leaving a dangling reference (TS2304 on
205
+ * `AUBaseDosage` in au-core 2.0.0's AUCoreMedicationStatement, and the same
206
+ * shape in davinci-pas and davinci-dtr).
207
+ */
208
+ isReferencedAllowingAliases(name, body) {
209
+ if (new RegExp(`\\b${name}\\b`).test(body))
210
+ return true;
211
+ if (!this.profileIdToName)
212
+ return false;
213
+ for (const [alias, resolved] of this.profileIdToName) {
214
+ if (resolved !== name || alias === name)
215
+ continue;
216
+ if (new RegExp(`\\b${alias}\\b`).test(body))
217
+ return true;
218
+ }
219
+ return false;
220
+ }
194
221
  /**
195
222
  * Prunes unused imports from generated code.
196
223
  * Removes import statements for types that aren't actually used.
@@ -213,8 +240,7 @@ export class ImportManager {
213
240
  const used = [];
214
241
  const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
215
242
  for (const name of specifiers) {
216
- const re = new RegExp(`\\b${name}\\b`);
217
- if (re.test(output.replace(full, ''))) {
243
+ if (this.isReferencedAllowingAliases(name, output.replace(full, ''))) {
218
244
  used.push(name);
219
245
  }
220
246
  }
@@ -1,4 +1,4 @@
1
- import { capitalize, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
1
+ import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
2
2
  import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
3
3
  import { logger } from '../../../logger.js';
4
4
  import { getRules } from '../../fhir/versionContext.js';
@@ -214,25 +214,30 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
214
214
  });
215
215
  return;
216
216
  }
217
- if (fieldName === "value[x]")
218
- fieldName = "value";
219
- if (fieldName === "event[x]" && field.type === "any" && field.fixedValue === undefined)
220
- return;
221
- if (fieldName === "event[x]")
222
- fieldName = "event";
223
- if (fieldName === "deceased[x]")
224
- fieldName = "deceased";
217
+ // `extension[x]` is a parser artifact rather than a FHIR choice — Extension.extension
218
+ // is a plain repeating element — so it keeps its name.
225
219
  if (fieldName === "extension[x]")
226
220
  fieldName = "extension";
227
- if (fieldName === "multipleBirth[x]")
228
- fieldName = "multipleBirth";
229
221
  // Helper to retrieve original last segment before local renaming
230
222
  function fieldNameOriginal(fullName) { return fullName.split('.').pop() || fullName; }
231
- // Generic handling: for choice elements (ending in [x]) that we do NOT explicitly map above,
232
- // we skip emitting them so the base resource's expanded choice properties remain intact (e.g., onsetDateTime, onsetAge,...).
233
- if (/\[x\]$/.test(fieldNameOriginal(field.name)) && !['value[x]', 'event[x]', 'deceased[x]', 'extension[x]', 'multipleBirth[x]'].includes(fieldNameOriginal(field.name))) {
234
- debug('skip choice element to inherit base choice fields', field.name);
235
- return;
223
+ // Choice elements carry no property of their own: FHIR declares one property
224
+ // per permitted type. A profile that narrows the choice to a single type names
225
+ // that property, so the constraint lands on the property the resource actually
226
+ // carries; anything still open is skipped, leaving the base definition's
227
+ // expanded choice properties in force (onsetDateTime, onsetAge, …).
228
+ //
229
+ // value[x], event[x], deceased[x] and multipleBirth[x] used to be renamed to
230
+ // value/event/deceased/multipleBirth, none of which exist on any resource.
231
+ // A slice narrowing Observation.component.value[x] to SampledData declared
232
+ // `value: SampledData`, so a caller writing correct FHIR failed to typecheck
233
+ // for a missing `value` while `valueSampledData` went unconstrained.
234
+ if (/\[x\]$/.test(fieldNameOriginal(field.name))) {
235
+ const narrowedTo = narrowedChoiceType(field);
236
+ if (!narrowedTo) {
237
+ debug('skip choice element to inherit base choice fields', field.name);
238
+ return;
239
+ }
240
+ fieldName = choicePropertyName(fieldName.replace(/\[x\]$/, ''), narrowedTo);
236
241
  }
237
242
  if (isFhirType(capitalize(fieldName)) && field.type === "any" && field.fixedValue === undefined && !(fieldName === 'extension' && hasSamePathExtensionSlices)) {
238
243
  if (isAaaCodesContext)
@@ -13,6 +13,22 @@ export function canonicalToTypeName(name, profileUrlToName) {
13
13
  }
14
14
  t = t.split('/').pop() || t;
15
15
  }
16
+ // A bare profile id also has a registered interface name, and it is not
17
+ // derivable from the id: davinci-pas 2.2.1's `profile-identifier` is generated
18
+ // as `PASIdentifier` (from the SD's `name`), so PascalCasing the id produced
19
+ // `ProfileIdentifier` — a type that is referenced and imported but never
20
+ // emitted (TS2307/TS2304 across seven files). Resolve through the registry by
21
+ // matching the URL's last segment, and only when it is unambiguous.
22
+ if (profileUrlToName && !/^https?:\/\//.test(t)) {
23
+ const matches = new Set();
24
+ for (const [url, generatedName] of profileUrlToName) {
25
+ if ((url.split('/').pop() || '').toLowerCase() === t.toLowerCase()) {
26
+ matches.add(generatedName);
27
+ }
28
+ }
29
+ if (matches.size === 1)
30
+ return [...matches][0];
31
+ }
16
32
  // if already a valid identifier with capitalization, return as-is
17
33
  if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t) && /[A-Z]/.test(t[0]))
18
34
  return t;
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { capitalize, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
6
6
  import { logger } from '../../../logger.js';
7
+ import {} from './importManager.js';
7
8
  import { getRules } from '../../fhir/versionContext.js';
8
9
  export function postProcessExtensions(params) {
9
10
  const { interfaces, interfaceName, baseResource, newFields, baseFieldByLastSegment, importManager, existingProfiles, profileUrlToName, generatedAliasTypes, findInterfaceIndex, inferBackboneType, addTypeImport } = params;