babelfhir-ts 1.5.20 → 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.
@@ -149,6 +149,38 @@ export function buildInlineResourceTypeValidations(fields, profileUrl) {
149
149
  * Build deeply nested required field validations.
150
150
  * For fields like component.value[x].data (min=1) where the parent is an array.
151
151
  */
152
+ /**
153
+ * Rewrite an element id whose only slice qualifiers name choice-type variants into
154
+ * the concrete JSON path, or undefined when any qualifier discriminates a subset.
155
+ *
156
+ * `ingredient.item[x]:itemCodeableConcept.text` → `ingredient.itemCodeableConcept.text`.
157
+ * A qualifier is only accepted when its base ends in `[x]` and the slice name starts
158
+ * with the base property, which is the convention that makes the slice name the
159
+ * property name. `identifier:NPI.value` fails both tests and is still skipped: NPI
160
+ * is one identifier among several, so requiring its children of every identifier
161
+ * would report a conformant resource.
162
+ */
163
+ function leafFieldName(parts) {
164
+ return parts[parts.length - 1];
165
+ }
166
+ function resolveChoiceSliceSegments(elementIdRel) {
167
+ const segments = elementIdRel.split('.');
168
+ const out = [];
169
+ for (const segment of segments) {
170
+ if (!segment.includes(':')) {
171
+ out.push(segment.replace(/\[x\]$/, ''));
172
+ continue;
173
+ }
174
+ const [base, sliceName] = segment.split(':');
175
+ if (!base.endsWith('[x]') || !sliceName)
176
+ return undefined;
177
+ const baseProp = base.slice(0, -'[x]'.length);
178
+ if (!sliceName.startsWith(baseProp) || sliceName === baseProp)
179
+ return undefined;
180
+ out.push(sliceName);
181
+ }
182
+ return out.join('.');
183
+ }
152
184
  export function buildNestedRequiredValidations(fields, arrayFieldPaths) {
153
185
  const result = [];
154
186
  fields
@@ -162,15 +194,34 @@ export function buildNestedRequiredValidations(fields, arrayFieldPaths) {
162
194
  let rel = relFull.replace(/^[^.]+\./, '').replace(/\[x\]/g, '');
163
195
  if (!rel)
164
196
  return;
165
- // Skip slice-specific fields
197
+ // Skip slice-specific fields: a slice's requirements bind only its own
198
+ // members, and this builder emits an unconditional check.
199
+ //
200
+ // One exception, and it is not really a subset. A choice-type slice names a
201
+ // JSON property rather than discriminating within one: FHIR writes
202
+ // `item[x]:itemCodeableConcept` for the CodeableConcept variant of `item[x]`,
203
+ // and an instance carrying that variant writes `itemCodeableConcept`. So the
204
+ // slice *is* the property, and its required children apply whenever the
205
+ // property is present — which is what HL7 reports as
206
+ // "Medication.ingredient.item[x]:itemCodeableConcept.text: minimum required = 1"
207
+ // where we said nothing, because `field.name` flattens `item[x]` to `item`
208
+ // and loses which variant was meant.
166
209
  const elementId = field.elementId || relFull;
167
- if (elementId.includes(':'))
168
- return;
210
+ let choiceSliceResolved = false;
211
+ if (elementId.includes(':')) {
212
+ const resolved = resolveChoiceSliceSegments(elementId.replace(/^[^.]+\./, ''));
213
+ if (!resolved)
214
+ return;
215
+ rel = resolved;
216
+ choiceSliceResolved = true;
217
+ }
169
218
  const pathParts = rel.split('.');
170
219
  if (pathParts.length < 2)
171
220
  return;
172
- // Resolve choice type in path
173
- if (relFull.includes('[x]')) {
221
+ // Resolve choice type in path. Skipped when the element id already named the
222
+ // variant: this block reads the *leaf's* own type, which for a nested leaf
223
+ // (…item[x]:itemCodeableConcept.text) is the text's type, not the choice's.
224
+ if (relFull.includes('[x]') && !choiceSliceResolved) {
174
225
  // The JSON property name comes from the element's *own* type, not its parent's.
175
226
  // Reading the parent discarded the check whenever that parent was a container:
176
227
  // Claim.item.location[x] is typed CodeableConcept, so the property is
@@ -203,8 +254,27 @@ export function buildNestedRequiredValidations(fields, arrayFieldPaths) {
203
254
  // segment to be an array meant a path whose only array sits deeper
204
255
  // (Observation.code.coding.code — code is 0..1, coding is the array) bailed
205
256
  // out and emitted nothing, leaving HL7 to report the missing element alone.
206
- if (!topIsArray && !hasIntermediateArray)
257
+ if (!topIsArray && !hasIntermediateArray) {
258
+ // A choice-slice path is emitted even when no level is a known array.
259
+ // `Medication.ingredient` is 0..* in the base but the KBV differential
260
+ // constrains only its `min`, so the merged field carries no max and
261
+ // `isArray` reads false — the check bailed and HL7 reported
262
+ // "…itemCodeableConcept.text: minimum required = 1" alone. `elementsAt`
263
+ // flattens an array level and passes a single object through unchanged,
264
+ // so it is right either way and does not depend on that flag.
265
+ if (!choiceSliceResolved)
266
+ return;
267
+ const walk = resolvedParts.slice(0, -1).map(p => `'${p}'`).join(', ');
268
+ result.push(`
269
+ // Nested required field: ${rel} (min=1)
270
+ for (const _nrEl of elementsAt(resource, [${walk}])) {
271
+ const _nrLeaf = at(_nrEl, '${leafFieldName(resolvedParts)}');
272
+ if (_nrLeaf === undefined || _nrLeaf === null) {
273
+ errors.push("Missing required member: '${leafFieldName(resolvedParts)}'");
274
+ }
275
+ }`);
207
276
  return;
277
+ }
208
278
  const leafField = resolvedParts[resolvedParts.length - 1];
209
279
  // Check if the leaf field itself is an array — if so, don't flag Array values as absent
210
280
  const fullLeafPath = resolvedParts.join('.');
@@ -450,6 +520,20 @@ export function buildProhibitedFieldValidations(fields, arrayFieldPaths, fieldPa
450
520
  // Skip if ANY segment has a slice qualifier — the constraint is
451
521
  // slice-scoped (e.g. name:Geburtsname.given) and must not be emitted
452
522
  // as a global prohibition.
523
+ //
524
+ // TODO(parity): this correctly avoids a global prohibition but emits nothing
525
+ // at all, so slice-scoped max=0 goes unchecked. A complex extension sets its
526
+ // own value[x] to max=0 — sub-extensions or a value, never both — and we miss
527
+ // it: on DTRStdQuestionnaire, HL7 reports "Extension.value[x]: max allowed =
528
+ // 0, but found 1" and "Extension.extension: minimum required = 2, but only
529
+ // found 0" for item.extension:contextExpression while we report neither.
530
+ //
531
+ // Fixing it needs a slice-scoped emission, not a relaxed guard: iterate the
532
+ // extensions matching the slice's url and assert the absence of any value*
533
+ // property. It cannot name one property, because a forbidden choice narrows
534
+ // to no type — which is why the choice resolution elsewhere in this file
535
+ // skips it too. Removing this guard instead would reintroduce the regression
536
+ // where AuditEvent.agent:user.requestor was demanded of every agent.
453
537
  const segments = field.elementId.split('.');
454
538
  if (segments.some(seg => seg.includes(':')))
455
539
  return false;
@@ -570,6 +654,16 @@ const FHIR_PRIMITIVE_TYPES = new Set([
570
654
  'time', 'instant', 'code', 'id', 'oid', 'markdown', 'base64Binary',
571
655
  'uuid', 'positiveInt', 'unsignedInt', 'integer64', 'xhtml',
572
656
  ]);
657
+ /**
658
+ * FHIR primitives that serialize as a JSON number.
659
+ *
660
+ * integer64 is deliberately absent: R5 serializes it as a *string* to survive
661
+ * JavaScript's 53-bit safe integer range, so requiring a number would reject
662
+ * conformant data.
663
+ */
664
+ const NUMERIC_PRIMITIVES = new Set([
665
+ 'decimal', 'integer', 'positiveInt', 'unsignedInt',
666
+ ]);
573
667
  /** Check whether a FHIR type serializes as a JSON primitive (string/number/bool). */
574
668
  function isFhirPrimitive(fieldType) {
575
669
  if (FHIR_PRIMITIVE_TYPES.has(fieldType))
@@ -595,9 +689,72 @@ const CONTAINER_TYPES = new Set([
595
689
  *
596
690
  * Only top-level (depth-1) non-sliced fields are checked.
597
691
  */
692
+ /**
693
+ * A numeric or boolean primitive below a repeating element must still hold the right
694
+ * JSON kind.
695
+ *
696
+ * The flat pass deliberately skips any path with an array ancestor, because a path
697
+ * walk cannot descend one — so `ingredient.strength.numerator.value` went unchecked
698
+ * and HL7 reported "The value 'Example value' is not a valid decimal" alone. This
699
+ * emits the loop that pass declined to guess at.
700
+ *
701
+ * Both of HL7's headings are pushed: the value wording classifies as `unclassified`
702
+ * and the parse wording as `constraint`, so emitting only the first leaves
703
+ * `constraint` looking like an HL7-only finding.
704
+ */
705
+ function buildArrayNestedPrimitiveKindValidations(fields, arrayFieldPaths, result) {
706
+ const isArrayPath = (p) => arrayFieldPaths.has(p) || arrayFieldPaths.has(p.split(':')[0]);
707
+ const seen = new Set();
708
+ for (const field of fields) {
709
+ if (field.sliceName || field.isArray)
710
+ continue;
711
+ const rel = (field.name || '').replace(/^[^.]+\./, '');
712
+ if (!rel || !rel.includes('.') || rel.includes('[x]') || seen.has(rel))
713
+ continue;
714
+ const fieldType = field.type || field.baseTypeCode;
715
+ const jsonKind = NUMERIC_PRIMITIVES.has(fieldType || '') ? 'number'
716
+ : fieldType === 'boolean' ? 'boolean'
717
+ : undefined;
718
+ if (!jsonKind)
719
+ continue;
720
+ const segments = rel.split('.');
721
+ const parents = segments.slice(0, -1);
722
+ const leaf = segments[segments.length - 1];
723
+ // The flat pass already covers a path with no array on the way down.
724
+ if (!parents.some((_p, i) => isArrayPath(parents.slice(0, i + 1).join('.'))))
725
+ continue;
726
+ seen.add(rel);
727
+ let expr = 'resource';
728
+ const open = [];
729
+ let indent = ' ';
730
+ parents.forEach((segment, i) => {
731
+ if (isArrayPath(parents.slice(0, i + 1).join('.'))) {
732
+ const loopVar = `_pkEl${i}`;
733
+ open.push(`${indent}for (const ${loopVar} of arr(at(${expr}, '${segment}'))) {`);
734
+ indent += ' ';
735
+ expr = loopVar;
736
+ }
737
+ else {
738
+ expr = `at(${expr}, '${segment}')`;
739
+ }
740
+ });
741
+ const closes = open.map((_l, i) => ' '.repeat(open.length - i + 1) + '}').join('\n');
742
+ const body = `${indent}const _pkValue = at(${expr}, '${leaf}');
743
+ ${indent}if (_pkValue !== undefined && _pkValue !== null && typeof _pkValue !== '${jsonKind}') {
744
+ ${indent} errors.push("The value '" + String(_pkValue) + "' is not a valid ${fieldType}");
745
+ ${indent} errors.push("Error parsing JSON: the primitive value must be a ${jsonKind}");
746
+ ${indent}}`;
747
+ result.push(`
748
+ // Structural type check: ${rel} must be a JSON ${jsonKind} (${fieldType})
749
+ {
750
+ ${[...open, body, closes].filter(Boolean).join('\n')}
751
+ }`);
752
+ }
753
+ }
598
754
  export function buildStructuralTypeValidations(fields, arrayFieldPaths) {
599
755
  const result = [];
600
756
  buildNestedObjectTypeValidations(fields, arrayFieldPaths, result);
757
+ buildArrayNestedPrimitiveKindValidations(fields, arrayFieldPaths, result);
601
758
  for (const field of fields) {
602
759
  // Skip sliced fields — handled by slice validator
603
760
  if (field.sliceName)
@@ -610,13 +767,34 @@ export function buildStructuralTypeValidations(fields, arrayFieldPaths) {
610
767
  if (fullPath.includes('[x]'))
611
768
  continue;
612
769
  const rel = fullPath.replace(/^[^.]+\./, '');
613
- if (!rel || rel.includes('.'))
614
- continue; // only top-level fields
770
+ if (!rel)
771
+ continue;
772
+ // A nested primitive is still worth checking, and only the primitive kind check
773
+ // below handles one: HL7 reports "The value 'Example value' is not a valid
774
+ // decimal" on Medication.amount.numerator.value, three levels down, where we
775
+ // reported nothing. The array and object checks stay top-level only, since both
776
+ // would need iteration to descend correctly.
777
+ const isNested = rel.includes('.');
778
+ if (isNested) {
779
+ // Any array on the way down needs a loop, not a path walk. Skipped rather than
780
+ // guessed at, so a repeating ancestor never yields a check against the array
781
+ // object itself.
782
+ // arrayFieldPaths is keyed on the *relative* path. Building the key from the
783
+ // full path never matched, so Bundle.entry.search.score was emitted as
784
+ // at(resource, 'entry', 'search', 'score') — a walk into an array, which
785
+ // yields undefined and checks nothing.
786
+ const segments = rel.split('.');
787
+ const hasArrayAncestor = segments.some((_seg, i) => arrayFieldPaths.has(segments.slice(0, i + 1).join('.')));
788
+ if (hasArrayAncestor)
789
+ continue;
790
+ if (!NUMERIC_PRIMITIVES.has(field.type || '') && field.type !== 'boolean')
791
+ continue;
792
+ }
615
793
  const fieldType = field.type || field.baseTypeCode;
616
794
  if (!fieldType)
617
795
  continue;
618
796
  // ── Array structural check ─────────────────────────────────────────
619
- if (field.isArray) {
797
+ if (field.isArray && !isNested) {
620
798
  result.push(`
621
799
  // Structural type check: ${rel} must be a JSON Array
622
800
  {
@@ -625,10 +803,40 @@ export function buildStructuralTypeValidations(fields, arrayFieldPaths) {
625
803
  const _stType = at(resource, 'resourceType');
626
804
  errors.push("The property ${rel} must be a JSON Array, not an Object (at " + (typeof _stType === 'string' ? _stType : "Resource") + ")");
627
805
  }
806
+ }`);
807
+ }
808
+ // ── Primitive JSON kind check ──────────────────────────────────────
809
+ // A numeric or boolean FHIR primitive must not arrive as a string. We checked
810
+ // that complex types are objects but never that a decimal holds a number, so
811
+ // HL7 reported "The value 'Example value' is not a valid decimal" and "the
812
+ // primitive value must be a number" where we said nothing.
813
+ //
814
+ // Both headings, as HL7 reports them: the value wording classifies as
815
+ // `unclassified` and the parse wording as `constraint`, so emitting only the
816
+ // first left `constraint` attributed to HL7 alone for a defect we had detected.
817
+ //
818
+ // Only the kinds JSON distinguishes are checked. Date, code and the rest all
819
+ // serialize as strings, and their formats already have dedicated checks
820
+ // elsewhere; asserting `typeof === 'string'` on them would add nothing.
821
+ const jsonKind = NUMERIC_PRIMITIVES.has(fieldType) ? 'number'
822
+ : fieldType === 'boolean' ? 'boolean'
823
+ : undefined;
824
+ if (jsonKind && !field.isArray) {
825
+ // Segments, not a dotted key: at() walks its varargs, so at(resource, 'a.b')
826
+ // would look for a property literally named "a.b" and always find nothing.
827
+ const accessorArgs = rel.split('.').map(seg => `'${seg}'`).join(', ');
828
+ result.push(`
829
+ // Structural type check: ${rel} must be a JSON ${jsonKind} (${fieldType})
830
+ {
831
+ const _stValue = at(resource, ${accessorArgs});
832
+ if (_stValue !== undefined && _stValue !== null && typeof _stValue !== '${jsonKind}') {
833
+ errors.push("The value '" + String(_stValue) + "' is not a valid ${fieldType}");
834
+ errors.push("Error parsing JSON: the primitive value must be a ${jsonKind}");
835
+ }
628
836
  }`);
629
837
  }
630
838
  // ── Complex type as Object check ───────────────────────────────────
631
- if (!isFhirPrimitive(fieldType) && !CONTAINER_TYPES.has(fieldType)) {
839
+ if (!isNested && !isFhirPrimitive(fieldType) && !CONTAINER_TYPES.has(fieldType)) {
632
840
  // Complex types must be objects when present — not primitive strings/numbers
633
841
  if (!field.isArray) {
634
842
  result.push(`
@@ -1,6 +1,7 @@
1
1
  import { logger } from '../../../logger.js';
2
2
  import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
3
3
  import { generateSliceValidations } from './sliceValidatorGenerator.js';
4
+ import { generateClosedSlicingValidations } from './closedSlicingValidation.js';
4
5
  import { fhirpathModelSlug } from '../../fhir/versionContext.js';
5
6
  import { buildRequiredConstraints, buildMaxCardinalityConstraints, buildForbiddenFieldConstraintEntries, buildPatternValidations, buildChoiceTypeNarrowingValidations, } from './validatorConstraintBuilders.js';
6
7
  import { buildNestedRequiredValidations, buildFixedValueValidations, buildProhibitedFieldValidations, buildPrimitiveFormatValidations, buildStructuralTypeValidations, buildInlineResourceTypeValidations, INLINE_RESOURCE_SLOTS, } from './validatorFieldBuilders.js';
@@ -241,6 +242,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
241
242
  valueSetImports, profileUrl, profileName: interfaceName, baseResourceType,
242
243
  profileUrlToName, validatorImports, extensionMetadata, profileUrlToType, datatypeProfilePins,
243
244
  });
245
+ const closedSlicingValidations = generateClosedSlicingValidations(fields, profileUrl, baseResourceType);
244
246
  const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
245
247
  const structuralTypeValidations = [
246
248
  ...buildStructuralTypeValidations(fields, arrayFieldPaths),
@@ -479,7 +481,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
479
481
  })
480
482
  .join("\n");
481
483
  // ── Assemble final output ───────────────────────────────────────────────
482
- const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0
484
+ const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0 || closedSlicingValidations.length > 0
483
485
  || patternValidations.length > 0 || fixedValueValidations.length > 0
484
486
  || bindingValidations.length > 0 || prohibitedFieldValidations.length > 0
485
487
  || bundleRefValidation.length > 0 || primitiveFormatValidations.length > 0
@@ -561,7 +563,7 @@ ${body}
561
563
  const errors: string[] = [];
562
564
  const warnings: string[] = [];
563
565
  ${fhirpathOptionsBlock}
564
- ${bundleEntryResolution}${referenceTargetTypeValidation}${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${structuralTypeValidations.join('')}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${choiceNarrowingValidations.join('')}${sliceValidations.join('')}${datatypeProfileDelegations.join('')}${nonDomainTextCheck}${extensionStructuralValidation}${containedRefValidation}${bundledAggregationValidation}${bundleRefValidation}
566
+ ${bundleEntryResolution}${referenceTargetTypeValidation}${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${structuralTypeValidations.join('')}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${choiceNarrowingValidations.join('')}${sliceValidations.join('')}${closedSlicingValidations.join('')}${datatypeProfileDelegations.join('')}${nonDomainTextCheck}${extensionStructuralValidation}${containedRefValidation}${bundledAggregationValidation}${bundleRefValidation}
565
567
  return { errors, warnings };
566
568
  }`,
567
569
  valueSetImports
@@ -8,7 +8,7 @@
8
8
  * Runtime helpers every generated validator may import from ValidatorOptions.
9
9
  * Single source of truth for the emitted import list.
10
10
  */
11
- export const HELPER_NAMES = ['at', 'arr', 'node', 'elements', 'coding', 'codings', 'asResource', 'refsAt'];
11
+ export const HELPER_NAMES = ['at', 'arr', 'node', 'elements', 'elementsAt', 'coding', 'codings', 'asResource', 'refsAt'];
12
12
  /**
13
13
  * Returns the content of ValidatorOptions.ts — the shared runtime options type
14
14
  * plus the element-probing helpers that every generated validateX() imports.
@@ -131,13 +131,14 @@ export function elements(value: unknown): readonly ElementShape[] {
131
131
  }
132
132
 
133
133
  /**
134
- * Collect the literal references found at a property path.
134
+ * Collect every element sitting at a property path, flattening arrays at each hop.
135
135
  *
136
- * Every hop flattens arrays, so one path covers both single and repeating elements
137
- * (Composition.author is 1..*, Composition.subject 0..1). Only string \`reference\`
138
- * values are returned; a Reference carrying just an identifier names no target.
136
+ * One path therefore covers both single and repeating elements
137
+ * (Composition.author is 1..*, Composition.subject 0..1) and any mix of the two,
138
+ * which is what \`at()\` cannot do — it stops the moment the path meets an array.
139
+ * Absent and null intermediates simply contribute nothing.
139
140
  */
140
- export function refsAt(value: unknown, path: readonly string[]): string[] {
141
+ export function elementsAt(value: unknown, path: readonly string[]): unknown[] {
141
142
  let current: unknown[] = [value];
142
143
  for (const key of path) {
143
144
  const next: unknown[] = [];
@@ -148,9 +149,18 @@ export function refsAt(value: unknown, path: readonly string[]): string[] {
148
149
  }
149
150
  current = next;
150
151
  }
152
+ return current;
153
+ }
151
154
 
155
+ /**
156
+ * Collect the literal references found at a property path.
157
+ *
158
+ * Only string \`reference\` values are returned; a Reference carrying just an
159
+ * identifier names no target.
160
+ */
161
+ export function refsAt(value: unknown, path: readonly string[]): string[] {
152
162
  const found: string[] = [];
153
- for (const item of current) {
163
+ for (const item of elementsAt(value, path)) {
154
164
  const reference = at(item, 'reference');
155
165
  if (typeof reference === 'string') found.push(reference);
156
166
  }
@@ -223,16 +223,18 @@ async function generateIndexFile(dir) {
223
223
  const files = fs.readdirSync(dir, { withFileTypes: true });
224
224
  const exports = [];
225
225
  const exportedNames = new Set();
226
- // First pass: collect all files and their exported names
226
+ // First pass: collect all files and their exported names, keeping the
227
+ // declaration kind — `verbatimModuleSyntax` needs a re-exported type to go
228
+ // through `export type { … }`, so the barrel has to know which is which.
227
229
  const fileExports = new Map();
228
230
  for (const file of files) {
229
231
  if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts') && file.name !== 'index.ts' && file.name !== 'ValidatorOptions.ts') {
230
232
  const filePath = path.join(dir, file.name);
231
233
  const content = fs.readFileSync(filePath, 'utf-8');
232
- const names = new Set();
233
- const exportMatches = content.matchAll(/export\s+(?:interface|class|type|const|function)\s+(\w+)/g);
234
+ const names = new Map();
235
+ const exportMatches = content.matchAll(/export\s+(interface|class|type|const|function)\s+(\w+)/g);
234
236
  for (const match of exportMatches) {
235
- names.add(match[1]);
237
+ names.set(match[2], match[1] === 'interface' || match[1] === 'type' ? 'type' : 'value');
236
238
  }
237
239
  fileExports.set(file.name, names);
238
240
  }
@@ -241,11 +243,11 @@ async function generateIndexFile(dir) {
241
243
  for (const file of files) {
242
244
  if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts') && file.name !== 'index.ts' && file.name !== 'ValidatorOptions.ts') {
243
245
  const baseName = file.name.replace('.ts', '');
244
- const fileNames = fileExports.get(file.name) || new Set();
246
+ const fileNames = fileExports.get(file.name) || new Map();
245
247
  // Find which names from this file conflict with already-exported names
246
248
  const conflicting = new Set();
247
249
  const unique = new Set();
248
- for (const name of fileNames) {
250
+ for (const name of fileNames.keys()) {
249
251
  if (exportedNames.has(name)) {
250
252
  conflicting.add(name);
251
253
  }
@@ -254,17 +256,25 @@ async function generateIndexFile(dir) {
254
256
  }
255
257
  }
256
258
  if (conflicting.size === 0) {
257
- // No conflicts — use simple wildcard export
259
+ // No conflicts — use simple wildcard export. `export *` carries types
260
+ // without needing the `type` modifier.
258
261
  exports.push(`export * from './${baseName}.js';`);
259
262
  }
260
263
  else if (unique.size > 0) {
261
- // Has conflicts but also has unique names — use selective named export
262
- const names = [...unique].sort().join(', ');
263
- exports.push(`export { ${names} } from './${baseName}.js';`);
264
+ // Has conflicts but also has unique names — use selective named exports,
265
+ // split by kind so re-exported types carry the `type` modifier.
266
+ const typeNames = [...unique].filter(n => fileNames.get(n) === 'type').sort();
267
+ const valueNames = [...unique].filter(n => fileNames.get(n) !== 'type').sort();
268
+ if (valueNames.length > 0) {
269
+ exports.push(`export { ${valueNames.join(', ')} } from './${baseName}.js';`);
270
+ }
271
+ if (typeNames.length > 0) {
272
+ exports.push(`export type { ${typeNames.join(', ')} } from './${baseName}.js';`);
273
+ }
264
274
  }
265
275
  // If all names conflict (unique.size === 0), skip entirely
266
276
  // Track all names from this file as exported
267
- for (const name of fileNames) {
277
+ for (const name of fileNames.keys()) {
268
278
  exportedNames.add(name);
269
279
  }
270
280
  }
@@ -442,6 +442,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
442
442
  type,
443
443
  baseTypeCode, // Raw FHIR type code (e.g., "HumanName" even when type is "humanname-de-basis")
444
444
  sliceName: element.sliceName,
445
+ slicingRules: element.slicing?.rules,
445
446
  profileUrls: (element.type || []).flatMap(t => t.profile || []),
446
447
  binding: bindingMeta,
447
448
  isProfiled: hasProfile,