babelfhir-ts 1.3.4 β†’ 1.3.6

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.
package/README.md CHANGED
@@ -160,7 +160,7 @@ The second pipeline validates using the [official HL7 FHIR Validator](https://co
160
160
  ![PIXm](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-pixm.json)
161
161
  ![MHD](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-mhd.json)
162
162
 
163
- > Terminology validation requires a tx server. The pipeline uses `--tx-server https://tx.fhir.org/r4` during generation to expand ValueSets and produce valid codes.
163
+ > Terminology validation requires a tx server. The pipeline uses `--tx-server https://tx.fhir.org/{r4|r5}` (matching the package's FHIR version) during generation to expand ValueSets and produce valid codes.
164
164
 
165
165
  πŸ“Š **[Full Report](https://max-health-inc.github.io/BabelFHIR-TS/)**
166
166
  <!-- HL7-PARITY-BADGES:END -->
@@ -158,9 +158,14 @@ export function generateSliceElement(slice, elementType) {
158
158
  log.debug(`Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
159
159
  const metaProfile = profileUrl ? `, meta: { profile: ['${profileUrl}'] }` : '';
160
160
  let resourceType = 'Composition';
161
- if (child.baseTypeCode) {
161
+ // Prefer concrete baseTypeCode (skip generic 'Resource'), then slice name
162
+ // (always the target resource type, e.g., 'Composition'), then profile URL.
163
+ if (child.baseTypeCode && child.baseTypeCode !== 'Resource') {
162
164
  resourceType = capitalise(child.baseTypeCode);
163
165
  }
166
+ else if (/^[A-Z][a-zA-Z]+$/.test(slice.name || '')) {
167
+ resourceType = slice.name;
168
+ }
164
169
  else if (profileUrl) {
165
170
  const profileName = profileUrl.split('/').pop() || '';
166
171
  if (profileName.includes('-')) {
@@ -127,18 +127,24 @@ export function generateSliceValidations(ctx) {
127
127
  generateExtensionUrlSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, extensionUrl, fieldPathMap, arrayFieldPaths, sliceValidations);
128
128
  }
129
129
  else {
130
- sliceValidations.push(`
130
+ const profileMatch = detectProfileDiscriminator(slice, fields);
131
+ if (profileMatch) {
132
+ generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, profileMatch, sliceValidations, ctx);
133
+ }
134
+ else {
135
+ sliceValidations.push(`
131
136
  // Slice validation for ${relPath}:${sliceLabel} skipped: no pattern/value discriminator found
132
137
  // Discriminator types 'type', 'profile', 'exists', 'position' are not yet supported`);
133
- addDiagnostic({
134
- profileUrl: ctx.profileUrl || '',
135
- profileName: ctx.profileName || '',
136
- severity: 'warning',
137
- category: 'discriminator',
138
- elementPath: `${relPath}:${sliceLabel}`,
139
- message: `Slice "${sliceLabel}" on "${relPath}" has no pattern/value discriminator β€” only type/profile/exists/position which are not yet evaluable.`,
140
- impact: 'Strict validators may reject valid resources because they cannot match this slice discriminator.',
141
- });
138
+ addDiagnostic({
139
+ profileUrl: ctx.profileUrl || '',
140
+ profileName: ctx.profileName || '',
141
+ severity: 'warning',
142
+ category: 'discriminator',
143
+ elementPath: `${relPath}:${sliceLabel}`,
144
+ message: `Slice "${sliceLabel}" on "${relPath}" has no pattern/value discriminator β€” only type/profile/exists/position which are not yet evaluable.`,
145
+ impact: 'Strict validators may reject valid resources because they cannot match this slice discriminator.',
146
+ });
147
+ }
142
148
  }
143
149
  }
144
150
  }
@@ -382,3 +388,97 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
382
388
  }
383
389
  }
384
390
  }
391
+ /**
392
+ * Detect whether a slice uses a `profile` discriminator on a `.resource` child.
393
+ * Returns the expected resourceType and profile URLs if found, undefined otherwise.
394
+ *
395
+ * Heuristic: the child field `<sliceElementId>.resource` with `profileUrls`
396
+ * indicates a profile-based entry slice. The resource type is derived from
397
+ * the profile URL (last path segment, PascalCase) or the slice name itself
398
+ * when the profile URL belongs to a non-core IG.
399
+ */
400
+ function detectProfileDiscriminator(slice, fields) {
401
+ const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
402
+ const resourceChild = fields.find(f => {
403
+ const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
404
+ return fId === `${sliceElemId}.resource`;
405
+ });
406
+ if (!resourceChild?.profileUrls?.length)
407
+ return undefined;
408
+ const profileUrls = resourceChild.profileUrls;
409
+ // Derive base resource type from the profile URL: the last path segment of a
410
+ // core FHIR profile IS the resource type (e.g., .../StructureDefinition/Patient β†’ Patient).
411
+ // For IG-specific profiles (e.g., .../ch-core-composition), the slice name is
412
+ // typically the resource type (e.g., "Composition"), so prefer that.
413
+ const sliceName = slice.sliceName || '';
414
+ // Validate: slice name should start with uppercase and be a plausible resource type
415
+ if (/^[A-Z][a-zA-Z]+$/.test(sliceName)) {
416
+ return { resourceType: sliceName, profileUrls };
417
+ }
418
+ // Fallback: try to extract from profile URL
419
+ const profileUrl = profileUrls[0];
420
+ const lastSegment = profileUrl.split('/').pop() || '';
421
+ if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
422
+ return { resourceType: lastSegment, profileUrls };
423
+ }
424
+ return undefined;
425
+ }
426
+ /**
427
+ * Generate slice validation for profile-discriminated entry slices.
428
+ * Matches Bundle entries by `entry[i].resource.resourceType`, enforces min/max,
429
+ * and delegates to the profiled validator when the profile URL is in profileUrlToName.
430
+ */
431
+ function generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, match, out, ctx) {
432
+ const { resourceType, profileUrls } = match;
433
+ const max = typeof slice.max === 'number' && slice.max !== Infinity ? slice.max : undefined;
434
+ const parentPath = relPath.includes('.') ? relPath.split('.').slice(0, -1).join('.') : '';
435
+ const accessor = parentPath ? `(resource as any)?.${parentPath.replace(/\./g, '?.')}` : `resource.${relPath}`;
436
+ // For top-level entry slices, access resource.entry directly
437
+ const arrayAccessor = relPath === 'entry' || !relPath.includes('.') ? `resource.${relPath}` : accessor;
438
+ // Resolve profiled validator for delegation
439
+ let delegateInterfaceName;
440
+ if (ctx.profileUrlToName && ctx.validatorImports) {
441
+ for (const pUrl of profileUrls) {
442
+ const name = ctx.profileUrlToName.get(pUrl);
443
+ if (name && name !== ctx.profileName) {
444
+ delegateInterfaceName = name;
445
+ ctx.validatorImports.set(name, `./${name}.js`);
446
+ break;
447
+ }
448
+ }
449
+ }
450
+ // When delegating, annotate the array so .resource is accessible without casts
451
+ const entriesType = delegateInterfaceName ? ': Array<Record<string, any>>' : '';
452
+ let code = `
453
+ // Required slice validation for ${relPath}:${sliceLabel} (profile discriminator β†’ ${resourceType})
454
+ {
455
+ const _${varName}Entries${entriesType} = (Array.isArray(${arrayAccessor}) ? ${arrayAccessor} : []).filter(
456
+ (e: any) => e?.resource?.resourceType === '${resourceType}'
457
+ );`;
458
+ if (min > 0) {
459
+ code += `
460
+ if (_${varName}Entries.length < ${min}) {
461
+ errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
462
+ }`;
463
+ }
464
+ if (max !== undefined) {
465
+ code += `
466
+ if (_${varName}Entries.length > ${max}) {
467
+ errors.push("${errorPath}:${sliceLabel}: max allowed = ${max}, but found " + _${varName}Entries.length);
468
+ }`;
469
+ }
470
+ // Delegate to profiled validator for each matched entry
471
+ if (delegateInterfaceName) {
472
+ code += `
473
+ for (const _e of _${varName}Entries) {
474
+ if (_e.resource) {
475
+ const _sub = await validate${delegateInterfaceName}(_e.resource, options);
476
+ errors.push(..._sub.errors);
477
+ warnings.push(..._sub.warnings);
478
+ }
479
+ }`;
480
+ }
481
+ code += `
482
+ }`;
483
+ out.push(code);
484
+ }
@@ -49,7 +49,7 @@ export function buildBindingValidations(fields, fieldParentMap, arrayFieldPaths,
49
49
  // ── System-presence fallback ────────────────────────────────────────────────
50
50
  function emitSystemPresenceFallback(output, field, rel, isNested, arrayFieldPaths) {
51
51
  const type = field.type || field.baseTypeCode;
52
- if (type !== 'CodeableConcept' && type !== 'Coding')
52
+ if (type !== 'CodeableConcept' && type !== 'Coding' && type !== 'Quantity')
53
53
  return;
54
54
  const vsUri = (field.binding.uri || '').split('|')[0];
55
55
  const vsDisplayName = vsUri.split('/').pop() || vsUri;
@@ -87,7 +87,7 @@ function emitTopLevelSystemPresence(output, field, rel, type, vsDisplayName, vsU
87
87
  }`);
88
88
  }
89
89
  }
90
- else {
90
+ else if (type === 'Coding') {
91
91
  if (field.isArray) {
92
92
  output.push(`
93
93
  // Required binding system-presence fallback for ${rel} (Coding array, ValueSet unresolved)
@@ -111,11 +111,35 @@ function emitTopLevelSystemPresence(output, field, rel, type, vsDisplayName, vsU
111
111
  }`);
112
112
  }
113
113
  }
114
+ else if (type === 'Quantity') {
115
+ if (field.isArray) {
116
+ output.push(`
117
+ // Required binding system-presence fallback for ${rel} (Quantity array, ValueSet unresolved)
118
+ if (_bRes.${rel} && Array.isArray(_bRes.${rel})) {
119
+ for (const _spQ of _bRes.${rel}) {
120
+ const _q = _spQ as { code?: string; system?: string };
121
+ if (_q.code !== undefined && (_q.system === undefined || _q.system === '')) {
122
+ errors.push("The System URI could not be determined for the code '" + _q.code + "' in the ValueSet '${vsDisplayName}' (${vsUri})");
123
+ }
124
+ }
125
+ }`);
126
+ }
127
+ else {
128
+ output.push(`
129
+ // Required binding system-presence fallback for ${rel} (Quantity, ValueSet unresolved)
130
+ if (_bRes.${rel}) {
131
+ const _q = _bRes.${rel} as { code?: string; system?: string };
132
+ if (_q.code !== undefined && (_q.system === undefined || _q.system === '')) {
133
+ errors.push("The System URI could not be determined for the code '" + _q.code + "' in the ValueSet '${vsDisplayName}' (${vsUri})");
134
+ }
135
+ }`);
136
+ }
137
+ }
114
138
  }
115
139
  // ── System-only binding ─────────────────────────────────────────────────────
116
140
  function emitSystemOnlyBinding(output, field, rel, isNested, vsInfo, arrayFieldPaths) {
117
141
  const type = field.type || field.baseTypeCode;
118
- if (type !== 'CodeableConcept' && type !== 'Coding')
142
+ if (type !== 'CodeableConcept' && type !== 'Coding' && type !== 'Quantity')
119
143
  return;
120
144
  const sysVsUri = (field.binding.uri || '').split('|')[0];
121
145
  const systemsLiteral = JSON.stringify(vsInfo.systems);
@@ -188,6 +212,32 @@ function emitTopLevelSystemOnly(output, field, rel, type, vsDisplayName, sysVsUr
188
212
  }`);
189
213
  }
190
214
  }
215
+ else if (type === 'Quantity') {
216
+ if (field.isArray) {
217
+ output.push(`
218
+ // Required ValueSet system-level binding validation for ${rel} (Quantity array)
219
+ if (_bRes.${rel} && Array.isArray(_bRes.${rel})) {
220
+ for (const _sysQ of _bRes.${rel}) {
221
+ const _q = _sysQ as { code?: string; system?: string };
222
+ const _allowedSystems = ${systemsLiteral};
223
+ if (_q.code !== undefined && _q.system !== undefined && !_allowedSystems.includes(_q.system)) {
224
+ errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");
225
+ }
226
+ }
227
+ }`);
228
+ }
229
+ else {
230
+ output.push(`
231
+ // Required ValueSet system-level binding validation for ${rel} (Quantity)
232
+ if (_bRes.${rel}) {
233
+ const _q = _bRes.${rel} as { code?: string; system?: string };
234
+ const _allowedSystems = ${systemsLiteral};
235
+ if (_q.code !== undefined && _q.system !== undefined && !_allowedSystems.includes(_q.system)) {
236
+ errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");
237
+ }
238
+ }`);
239
+ }
240
+ }
191
241
  }
192
242
  // ── Resolved code-level binding ─────────────────────────────────────────────
193
243
  function emitResolvedBinding(output, field, rel, isNested, vsInfo, arrayFieldPaths) {
@@ -74,7 +74,7 @@ export function emitSystemPresenceLeaf(leafAcc, indent, type, vsDisplayName, vsU
74
74
  code += `\n${indent}}`;
75
75
  }
76
76
  }
77
- else {
77
+ else if (type === 'Coding') {
78
78
  if (isArray) {
79
79
  code += `\n${indent}if (${leafAcc} && Array.isArray(${leafAcc})) {`;
80
80
  indent += ' ';
@@ -99,6 +99,31 @@ export function emitSystemPresenceLeaf(leafAcc, indent, type, vsDisplayName, vsU
99
99
  code += `\n${indent}}`;
100
100
  }
101
101
  }
102
+ else if (type === 'Quantity') {
103
+ if (isArray) {
104
+ code += `\n${indent}if (${leafAcc} && Array.isArray(${leafAcc})) {`;
105
+ indent += ' ';
106
+ code += `\n${indent}for (const _spQ of ${leafAcc}) {`;
107
+ }
108
+ else {
109
+ code += `\n${indent}if (${leafAcc} !== undefined) {`;
110
+ indent += ' ';
111
+ code += `\n${indent}const _spQ = ${leafAcc};`;
112
+ }
113
+ indent += ' ';
114
+ code += `\n${indent}const _q = _spQ as { code?: string; system?: string };`;
115
+ code += `\n${indent}if (_q.code !== undefined && (_q.system === undefined || _q.system === '')) {`;
116
+ indent += ' ';
117
+ code += `\n${indent}errors.push("The System URI could not be determined for the code '" + _q.code + "' in the ValueSet '${vsDisplayName}' (${vsUri})");`;
118
+ indent = indent.slice(2);
119
+ code += `\n${indent}}`;
120
+ indent = indent.slice(2);
121
+ code += `\n${indent}}`;
122
+ if (isArray) {
123
+ indent = indent.slice(2);
124
+ code += `\n${indent}}`;
125
+ }
126
+ }
102
127
  return code;
103
128
  }
104
129
  export function emitSystemOnlyLeaf(leafAcc, indent, type, vsDisplayName, sysVsUri, systemsLiteral, isArray) {
@@ -177,6 +202,38 @@ export function emitSystemOnlyLeaf(leafAcc, indent, type, vsDisplayName, sysVsUr
177
202
  code += `\n${indent}}`;
178
203
  }
179
204
  }
205
+ else if (type === 'Quantity') {
206
+ if (isArray) {
207
+ code += `\n${indent}if (${leafAcc} && Array.isArray(${leafAcc})) {`;
208
+ indent += ' ';
209
+ code += `\n${indent}for (const _sysQ of ${leafAcc}) {`;
210
+ indent += ' ';
211
+ code += `\n${indent}const _q = _sysQ as { code?: string; system?: string };`;
212
+ code += `\n${indent}const _allowedSystems = ${systemsLiteral};`;
213
+ code += `\n${indent}if (_q.code !== undefined && _q.system !== undefined && !_allowedSystems.includes(_q.system)) {`;
214
+ indent += ' ';
215
+ code += `\n${indent}errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");`;
216
+ indent = indent.slice(2);
217
+ code += `\n${indent}}`;
218
+ indent = indent.slice(2);
219
+ code += `\n${indent}}`;
220
+ indent = indent.slice(2);
221
+ code += `\n${indent}}`;
222
+ }
223
+ else {
224
+ code += `\n${indent}{`;
225
+ indent += ' ';
226
+ code += `\n${indent}const _q = (${leafAcc} as any);`;
227
+ code += `\n${indent}const _allowedSystems = ${systemsLiteral};`;
228
+ code += `\n${indent}if (_q?.code !== undefined && _q?.system !== undefined && !_allowedSystems.includes(_q.system)) {`;
229
+ indent += ' ';
230
+ code += `\n${indent}errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");`;
231
+ indent = indent.slice(2);
232
+ code += `\n${indent}}`;
233
+ indent = indent.slice(2);
234
+ code += `\n${indent}}`;
235
+ }
236
+ }
180
237
  return code;
181
238
  }
182
239
  export function emitResolvedLeaf(leafAcc, indent, type, vsName, vsDisplayName, vsUri, isArray) {
@@ -27,8 +27,9 @@ export interface ValidatorOptions {
27
27
  }
28
28
  `;
29
29
  }
30
- export function generateValidateProfileFunction(interfaceName, fields, valueSets, profileUrl, baseResourceType) {
30
+ export function generateValidateProfileFunction(interfaceName, fields, valueSets, profileUrl, baseResourceType, profileUrlToName) {
31
31
  const valueSetImports = new Map();
32
+ const validatorImports = new Map();
32
33
  // ── Collect field-level constraints ──────────────────────────────────────
33
34
  const constraintsWithContext = [];
34
35
  fields.forEach((field) => {
@@ -143,6 +144,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
143
144
  const sliceValidations = generateSliceValidations({
144
145
  fields, fieldPathMap, arrayFieldPaths, getValueSetForBinding,
145
146
  valueSetImports, profileUrl, profileName: interfaceName, baseResourceType,
147
+ profileUrlToName, validatorImports,
146
148
  });
147
149
  const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
148
150
  const bundleRefValidation = generateBundleRefValidation(baseResourceType);
@@ -153,14 +155,23 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
153
155
  const filteredConstraints = uniqueConstraints.filter(c => {
154
156
  const expr = c.constraint.expression || "";
155
157
  const fieldPath = c.fieldPath || "";
158
+ // Filter out text.div-related expressions (narrative HTML), XPath-style
159
+ // variables ($A…$Z), and value[x] type-discriminator patterns.
156
160
  if (/(text\.div\b|\.all\(div\b|\$[A-Z]|value\.[A-Z])/.test(expr))
157
161
  return false;
162
+ // Filter $this on choice-type fields β€” fhirpath scoping differs from HL7.
158
163
  if (c.isChoiceType && /\$this\b/.test(expr))
159
164
  return false;
165
+ // For nested fields (depth > 1), filter only base FHIR invariants whose
166
+ // evaluation diverges between fhirpath.js and HL7:
167
+ // ele-1: hasValue() or (children().count() > id.count())
168
+ // ext-1: extension.exists() != value.exists()
169
+ // Profile-specific constraints at nested depth are allowed through β€”
170
+ // the .all() wrapping handles scoping correctly for these.
160
171
  if (fieldPath && fieldPath.split('.').length > 1) {
161
- const isSimpleExistenceCheck = /^[a-zA-Z.]+\.exists\(\)$/.test(expr);
162
- const isSimpleCountCheck = /^[a-zA-Z.]+\.count\(\)\s*[<>=]+\s*\d+$/.test(expr);
163
- if (!isSimpleExistenceCheck && !isSimpleCountCheck)
172
+ const isEle1 = /^hasValue\(\)\s+or\s+\(children\(\)\.count\(\)\s*>\s*id\.count\(\)\)$/.test(expr);
173
+ const isExt1 = /^extension\.exists\(\)\s*!=\s*value\.exists\(\)$/.test(expr);
174
+ if (isEle1 || isExt1)
164
175
  return false;
165
176
  }
166
177
  return true;
@@ -198,20 +209,30 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
198
209
  }
199
210
  }
200
211
  const needsTerminology = /\bmemberOf\b/.test(constraint.expression || '');
201
- const needsResolve = /\bresolve\b/.test(constraint.expression || '');
202
212
  let evaluateBlock = `
203
213
  const result${index} = await fhirpath.evaluate(resource, "${escapedExpression}", { resource }, fhirpath_model, fhirpathOptions);
204
214
  if (!result${index}.every(Boolean)) {
205
215
  ${constraint.severity === "error" ? "errors" : "warnings"}.push("Constraint violation: ${escapedHuman}");
206
216
  }`;
217
+ // Gate memberOf() behind terminologyUrl. For resolve(), use try/catch
218
+ // instead of a hard gate: FHIRPath `and` short-circuits, so expressions
219
+ // like `(A and resolve().is(X)) or (B and focus.exists().not())` can still
220
+ // evaluate the resolve-free branch when A is false. If resolve() IS
221
+ // reached without a fhirServerUrl, fhirpath.js throws β€” catch it and
222
+ // degrade to a warning instead of silently skipping the entire constraint.
223
+ const needsResolve = /\bresolve\b/.test(constraint.expression || '');
207
224
  if (needsTerminology) {
208
225
  evaluateBlock = `
209
226
  if (options?.terminologyUrl) {${evaluateBlock}
227
+ } else {
228
+ warnings.push("Constraint not evaluated (no terminologyUrl): ${escapedHuman}");
210
229
  }`;
211
230
  }
212
231
  else if (needsResolve) {
213
232
  evaluateBlock = `
214
- if (options?.fhirServerUrl) {${evaluateBlock}
233
+ try {${evaluateBlock}
234
+ } catch {
235
+ warnings.push("Constraint partially not evaluated (resolve requires fhirServerUrl): ${escapedHuman}");
215
236
  }`;
216
237
  }
217
238
  return evaluateBlock;
@@ -260,9 +281,15 @@ ${extensionStructuralValidation}${containedRefValidation}
260
281
  .map(([name, path]) => `import { isValid${name}Code } from '${path}';`)
261
282
  .join('\n') + '\n\n';
262
283
  }
284
+ let validatorImportStatements = '';
285
+ if (validatorImports.size > 0) {
286
+ validatorImportStatements = Array.from(validatorImports.entries())
287
+ .map(([name, importPath]) => `import { validate${name} } from '${importPath}';`)
288
+ .join('\n') + '\n\n';
289
+ }
263
290
  const fixedPatternValidations = patternValidations.map(v => v.replace(/if \(resource\.(\w+)\.(\w+)/g, (_match, parent, child) => `if (resource.${parent} && resource.${parent}.${child}`));
264
291
  return {
265
- code: `${fhirpathImport}${optionsImport}${valueSetImportStatements}export async function validate${interfaceName}(resource: ${interfaceName}, options?: ValidatorOptions): Promise<{ errors: string[]; warnings: string[] }> {
292
+ code: `${fhirpathImport}${optionsImport}${valueSetImportStatements}${validatorImportStatements}export async function validate${interfaceName}(resource: ${interfaceName}, options?: ValidatorOptions): Promise<{ errors: string[]; warnings: string[] }> {
266
293
  const errors: string[] = [];
267
294
  const warnings: string[] = [];
268
295
  ${fhirpathOptionsBlock}
@@ -104,10 +104,14 @@ export function generateExtensionStructuralValidation() {
104
104
  return `
105
105
  // Base FHIR structural validation: extension.url required, ext-1 constraint, empty objects
106
106
  {
107
- const _checkExtensions = (obj: unknown, path: string): void => {
107
+ const _checkExtensions = (obj: unknown, path: string, depth: number): void => {
108
108
  if (!obj || typeof obj !== 'object') return;
109
- if (Array.isArray(obj)) { obj.forEach((item, i) => _checkExtensions(item, path + '[' + i + ']')); return; }
109
+ if (Array.isArray(obj)) { obj.forEach((item, i) => _checkExtensions(item, path + '[' + i + ']', depth + 1)); return; }
110
110
  const rec = obj as Record<string, unknown>;
111
+ // Empty object check β€” HL7 validates all FHIR elements must have content
112
+ if (depth > 0 && Object.keys(rec).length === 0) {
113
+ errors.push('Object must have some content');
114
+ }
111
115
  // Validate extension arrays
112
116
  for (const extKey of ['extension', 'modifierExtension'] as const) {
113
117
  const exts = rec[extKey];
@@ -115,11 +119,6 @@ export function generateExtensionStructuralValidation() {
115
119
  for (const ext of exts) {
116
120
  if (ext && typeof ext === 'object' && !Array.isArray(ext)) {
117
121
  const e = ext as Record<string, unknown>;
118
- // Empty extension object check
119
- if (Object.keys(e).length === 0) {
120
- errors.push('Object must have some content');
121
- continue;
122
- }
123
122
  if (typeof e.url !== 'string' || !e.url) {
124
123
  errors.push('Extension.url is required in order to identify, use and validate the extension');
125
124
  }
@@ -136,11 +135,11 @@ export function generateExtensionStructuralValidation() {
136
135
  // Recurse into child objects (skip primitive values)
137
136
  for (const [_key, _val] of Object.entries(rec)) {
138
137
  if (_val && typeof _val === 'object') {
139
- _checkExtensions(_val, path + '.' + _key);
138
+ _checkExtensions(_val, path + '.' + _key, depth + 1);
140
139
  }
141
140
  }
142
141
  };
143
- _checkExtensions(resource, '');
142
+ _checkExtensions(resource, '', 0);
144
143
  }
145
144
  `;
146
145
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_description": "Curated sample codes for large external CodeSystems (LOINC, SNOMED CT, etc.) that cannot be shipped in full due to size or licensing. Used as fallback when the CodeSystem has content='not-present' to allow ValueSet resolution for core R4 ValueSets like observation-codes without requiring a terminology server.",
2
+ "_description": "Curated sample codes for large external CodeSystems (LOINC, SNOMED CT, etc.) and complete code sets for small core FHIR R4 CodeSystems that cannot be loaded from packages skipped by isLargeHl7Package (hl7.fhir.r4.core, hl7.terminology, etc.). Used as fallback to allow ValueSet resolution for core R4 ValueSets without requiring a terminology server.",
3
3
  "_valueSets": "Maps core R4 ValueSet canonical URLs to included CodeSystem URLs. Used when the dependency package containing the ValueSet is not cached locally.",
4
4
  "systems": {
5
5
  "http://loinc.org": [
@@ -91,6 +91,370 @@
91
91
  { "code": "failed", "display": "Failed" },
92
92
  { "code": "completed", "display": "Completed" },
93
93
  { "code": "entered-in-error", "display": "Entered in Error" }
94
+ ],
95
+ "http://hl7.org/fhir/event-status": [
96
+ { "code": "preparation", "display": "Preparation" },
97
+ { "code": "in-progress", "display": "In Progress" },
98
+ { "code": "not-done", "display": "Not Done" },
99
+ { "code": "on-hold", "display": "On Hold" },
100
+ { "code": "stopped", "display": "Stopped" },
101
+ { "code": "completed", "display": "Completed" },
102
+ { "code": "entered-in-error", "display": "Entered in Error" },
103
+ { "code": "unknown", "display": "Unknown" }
104
+ ],
105
+ "http://hl7.org/fhir/observation-status": [
106
+ { "code": "registered", "display": "Registered" },
107
+ { "code": "preliminary", "display": "Preliminary" },
108
+ { "code": "final", "display": "Final" },
109
+ { "code": "amended", "display": "Amended" },
110
+ { "code": "corrected", "display": "Corrected" },
111
+ { "code": "cancelled", "display": "Cancelled" },
112
+ { "code": "entered-in-error", "display": "Entered in Error" },
113
+ { "code": "unknown", "display": "Unknown" }
114
+ ],
115
+ "http://hl7.org/fhir/administrative-gender": [
116
+ { "code": "male", "display": "Male" },
117
+ { "code": "female", "display": "Female" },
118
+ { "code": "other", "display": "Other" },
119
+ { "code": "unknown", "display": "Unknown" }
120
+ ],
121
+ "http://hl7.org/fhir/encounter-status": [
122
+ { "code": "planned", "display": "Planned" },
123
+ { "code": "arrived", "display": "Arrived" },
124
+ { "code": "triaged", "display": "Triaged" },
125
+ { "code": "in-progress", "display": "In Progress" },
126
+ { "code": "onleave", "display": "On Leave" },
127
+ { "code": "finished", "display": "Finished" },
128
+ { "code": "cancelled", "display": "Cancelled" },
129
+ { "code": "entered-in-error", "display": "Entered in Error" },
130
+ { "code": "unknown", "display": "Unknown" }
131
+ ],
132
+ "http://hl7.org/fhir/request-status": [
133
+ { "code": "draft", "display": "Draft" },
134
+ { "code": "active", "display": "Active" },
135
+ { "code": "on-hold", "display": "On Hold" },
136
+ { "code": "revoked", "display": "Revoked" },
137
+ { "code": "completed", "display": "Completed" },
138
+ { "code": "entered-in-error", "display": "Entered in Error" },
139
+ { "code": "unknown", "display": "Unknown" }
140
+ ],
141
+ "http://hl7.org/fhir/narrative-status": [
142
+ { "code": "generated", "display": "Generated" },
143
+ { "code": "extensions", "display": "Extensions" },
144
+ { "code": "additional", "display": "Additional" },
145
+ { "code": "empty", "display": "Empty" }
146
+ ],
147
+ "http://hl7.org/fhir/composition-status": [
148
+ { "code": "preliminary", "display": "Preliminary" },
149
+ { "code": "final", "display": "Final" },
150
+ { "code": "amended", "display": "Amended" },
151
+ { "code": "entered-in-error", "display": "Entered in Error" }
152
+ ],
153
+ "http://hl7.org/fhir/document-reference-status": [
154
+ { "code": "current", "display": "Current" },
155
+ { "code": "superseded", "display": "Superseded" },
156
+ { "code": "entered-in-error", "display": "Entered in Error" }
157
+ ],
158
+ "http://hl7.org/fhir/list-status": [
159
+ { "code": "current", "display": "Current" },
160
+ { "code": "retired", "display": "Retired" },
161
+ { "code": "entered-in-error", "display": "Entered in Error" }
162
+ ],
163
+ "http://hl7.org/fhir/fm-status": [
164
+ { "code": "active", "display": "Active" },
165
+ { "code": "cancelled", "display": "Cancelled" },
166
+ { "code": "draft", "display": "Draft" },
167
+ { "code": "entered-in-error", "display": "Entered in Error" }
168
+ ],
169
+ "http://hl7.org/fhir/diagnostic-report-status": [
170
+ { "code": "registered", "display": "Registered" },
171
+ { "code": "partial", "display": "Partial" },
172
+ { "code": "preliminary", "display": "Preliminary" },
173
+ { "code": "final", "display": "Final" },
174
+ { "code": "amended", "display": "Amended" },
175
+ { "code": "corrected", "display": "Corrected" },
176
+ { "code": "appended", "display": "Appended" },
177
+ { "code": "cancelled", "display": "Cancelled" },
178
+ { "code": "entered-in-error", "display": "Entered in Error" },
179
+ { "code": "unknown", "display": "Unknown" }
180
+ ],
181
+ "http://hl7.org/fhir/bundle-type": [
182
+ { "code": "document", "display": "Document" },
183
+ { "code": "message", "display": "Message" },
184
+ { "code": "transaction", "display": "Transaction" },
185
+ { "code": "transaction-response", "display": "Transaction Response" },
186
+ { "code": "batch", "display": "Batch" },
187
+ { "code": "batch-response", "display": "Batch Response" },
188
+ { "code": "history", "display": "History List" },
189
+ { "code": "searchset", "display": "Search Results" },
190
+ { "code": "collection", "display": "Collection" }
191
+ ],
192
+ "http://hl7.org/fhir/allergy-intolerance-type": [
193
+ { "code": "allergy", "display": "Allergy" },
194
+ { "code": "intolerance", "display": "Intolerance" }
195
+ ],
196
+ "http://hl7.org/fhir/group-type": [
197
+ { "code": "person", "display": "Person" },
198
+ { "code": "animal", "display": "Animal" },
199
+ { "code": "practitioner", "display": "Practitioner" },
200
+ { "code": "device", "display": "Device" },
201
+ { "code": "medication", "display": "Medication" },
202
+ { "code": "substance", "display": "Substance" }
203
+ ],
204
+ "http://hl7.org/fhir/measure-report-status": [
205
+ { "code": "complete", "display": "Complete" },
206
+ { "code": "pending", "display": "Pending" },
207
+ { "code": "error", "display": "Error" }
208
+ ],
209
+ "http://hl7.org/fhir/measure-report-type": [
210
+ { "code": "individual", "display": "Individual" },
211
+ { "code": "subject-list", "display": "Subject List" },
212
+ { "code": "summary", "display": "Summary" },
213
+ { "code": "data-collection", "display": "Data Collection" }
214
+ ],
215
+ "http://hl7.org/fhir/goal-status": [
216
+ { "code": "proposed", "display": "Proposed" },
217
+ { "code": "planned", "display": "Planned" },
218
+ { "code": "accepted", "display": "Accepted" },
219
+ { "code": "active", "display": "Active" },
220
+ { "code": "on-hold", "display": "On Hold" },
221
+ { "code": "completed", "display": "Completed" },
222
+ { "code": "cancelled", "display": "Cancelled" },
223
+ { "code": "entered-in-error", "display": "Entered in Error" },
224
+ { "code": "rejected", "display": "Rejected" }
225
+ ],
226
+ "http://hl7.org/fhir/episode-of-care-status": [
227
+ { "code": "planned", "display": "Planned" },
228
+ { "code": "waitlist", "display": "Waitlist" },
229
+ { "code": "active", "display": "Active" },
230
+ { "code": "onhold", "display": "On Hold" },
231
+ { "code": "finished", "display": "Finished" },
232
+ { "code": "cancelled", "display": "Cancelled" },
233
+ { "code": "entered-in-error", "display": "Entered in Error" }
234
+ ],
235
+ "http://hl7.org/fhir/specimen-status": [
236
+ { "code": "available", "display": "Available" },
237
+ { "code": "unavailable", "display": "Unavailable" },
238
+ { "code": "unsatisfactory", "display": "Unsatisfactory" },
239
+ { "code": "entered-in-error", "display": "Entered in Error" }
240
+ ],
241
+ "http://hl7.org/fhir/flag-status": [
242
+ { "code": "active", "display": "Active" },
243
+ { "code": "inactive", "display": "Inactive" },
244
+ { "code": "entered-in-error", "display": "Entered in Error" }
245
+ ],
246
+ "http://hl7.org/fhir/address-use": [
247
+ { "code": "home", "display": "Home" },
248
+ { "code": "work", "display": "Work" },
249
+ { "code": "temp", "display": "Temp" },
250
+ { "code": "old", "display": "Old / Incorrect" },
251
+ { "code": "billing", "display": "Billing" }
252
+ ],
253
+ "http://hl7.org/fhir/address-type": [
254
+ { "code": "postal", "display": "Postal" },
255
+ { "code": "physical", "display": "Physical" },
256
+ { "code": "both", "display": "Postal & Physical" }
257
+ ],
258
+ "http://hl7.org/fhir/contact-point-system": [
259
+ { "code": "phone", "display": "Phone" },
260
+ { "code": "fax", "display": "Fax" },
261
+ { "code": "email", "display": "Email" },
262
+ { "code": "pager", "display": "Pager" },
263
+ { "code": "url", "display": "URL" },
264
+ { "code": "sms", "display": "SMS" },
265
+ { "code": "other", "display": "Other" }
266
+ ],
267
+ "http://hl7.org/fhir/contact-point-use": [
268
+ { "code": "home", "display": "Home" },
269
+ { "code": "work", "display": "Work" },
270
+ { "code": "temp", "display": "Temp" },
271
+ { "code": "old", "display": "Old" },
272
+ { "code": "mobile", "display": "Mobile" }
273
+ ],
274
+ "http://hl7.org/fhir/identifier-use": [
275
+ { "code": "usual", "display": "Usual" },
276
+ { "code": "official", "display": "Official" },
277
+ { "code": "temp", "display": "Temp" },
278
+ { "code": "secondary", "display": "Secondary" },
279
+ { "code": "old", "display": "Old" }
280
+ ],
281
+ "http://hl7.org/fhir/name-use": [
282
+ { "code": "usual", "display": "Usual" },
283
+ { "code": "official", "display": "Official" },
284
+ { "code": "temp", "display": "Temp" },
285
+ { "code": "nickname", "display": "Nickname" },
286
+ { "code": "anonymous", "display": "Anonymous" },
287
+ { "code": "old", "display": "Old" },
288
+ { "code": "maiden", "display": "Name changed for Marriage" }
289
+ ],
290
+ "http://hl7.org/fhir/link-type": [
291
+ { "code": "replaced-by", "display": "Replaced-by" },
292
+ { "code": "replaces", "display": "Replaces" },
293
+ { "code": "refer", "display": "Refer" },
294
+ { "code": "seealso", "display": "See also" }
295
+ ],
296
+ "http://hl7.org/fhir/account-status": [
297
+ { "code": "active", "display": "Active" },
298
+ { "code": "inactive", "display": "Inactive" },
299
+ { "code": "entered-in-error", "display": "Entered in Error" },
300
+ { "code": "on-hold", "display": "On Hold" },
301
+ { "code": "unknown", "display": "Unknown" }
302
+ ],
303
+ "http://hl7.org/fhir/consent-state-codes": [
304
+ { "code": "draft", "display": "Pending" },
305
+ { "code": "proposed", "display": "Proposed" },
306
+ { "code": "active", "display": "Active" },
307
+ { "code": "rejected", "display": "Rejected" },
308
+ { "code": "inactive", "display": "Inactive" },
309
+ { "code": "entered-in-error", "display": "Entered in Error" }
310
+ ],
311
+ "http://hl7.org/fhir/substance-status": [
312
+ { "code": "active", "display": "Active" },
313
+ { "code": "inactive", "display": "Inactive" },
314
+ { "code": "entered-in-error", "display": "Entered in Error" }
315
+ ],
316
+ "http://hl7.org/fhir/questionnaire-answers-status": [
317
+ { "code": "in-progress", "display": "In Progress" },
318
+ { "code": "completed", "display": "Completed" },
319
+ { "code": "amended", "display": "Amended" },
320
+ { "code": "entered-in-error", "display": "Entered in Error" },
321
+ { "code": "stopped", "display": "Stopped" }
322
+ ],
323
+ "http://hl7.org/fhir/item-type": [
324
+ { "code": "group", "display": "Group" },
325
+ { "code": "display", "display": "Display" },
326
+ { "code": "question", "display": "Question" },
327
+ { "code": "boolean", "display": "Boolean" },
328
+ { "code": "decimal", "display": "Decimal" },
329
+ { "code": "integer", "display": "Integer" },
330
+ { "code": "date", "display": "Date" },
331
+ { "code": "dateTime", "display": "Date Time" },
332
+ { "code": "time", "display": "Time" },
333
+ { "code": "string", "display": "String" },
334
+ { "code": "text", "display": "Text" },
335
+ { "code": "url", "display": "Url" },
336
+ { "code": "choice", "display": "Choice" },
337
+ { "code": "open-choice", "display": "Open Choice" },
338
+ { "code": "attachment", "display": "Attachment" },
339
+ { "code": "reference", "display": "Reference" },
340
+ { "code": "quantity", "display": "Quantity" }
341
+ ],
342
+ "http://hl7.org/fhir/http-verb": [
343
+ { "code": "GET", "display": "GET" },
344
+ { "code": "HEAD", "display": "HEAD" },
345
+ { "code": "POST", "display": "POST" },
346
+ { "code": "PUT", "display": "PUT" },
347
+ { "code": "DELETE", "display": "DELETE" },
348
+ { "code": "PATCH", "display": "PATCH" }
349
+ ],
350
+ "http://hl7.org/fhir/capability-statement-kind": [
351
+ { "code": "instance", "display": "Instance" },
352
+ { "code": "capability", "display": "Capability" },
353
+ { "code": "requirements", "display": "Requirements" }
354
+ ],
355
+ "http://hl7.org/fhir/restful-capability-mode": [
356
+ { "code": "client", "display": "Client" },
357
+ { "code": "server", "display": "Server" }
358
+ ],
359
+ "http://hl7.org/fhir/search-param-type": [
360
+ { "code": "number", "display": "Number" },
361
+ { "code": "date", "display": "Date/DateTime" },
362
+ { "code": "string", "display": "String" },
363
+ { "code": "token", "display": "Token" },
364
+ { "code": "reference", "display": "Reference" },
365
+ { "code": "composite", "display": "Composite" },
366
+ { "code": "quantity", "display": "Quantity" },
367
+ { "code": "uri", "display": "URI" },
368
+ { "code": "special", "display": "Special" }
369
+ ],
370
+ "http://hl7.org/fhir/filter-operator": [
371
+ { "code": "=", "display": "Equals" },
372
+ { "code": "is-a", "display": "Is A (by subsumption)" },
373
+ { "code": "descendent-of", "display": "Descendent Of (by subsumption)" },
374
+ { "code": "is-not-a", "display": "Not (Is A) (by subsumption)" },
375
+ { "code": "regex", "display": "Regular Expression" },
376
+ { "code": "in", "display": "Not in Set" },
377
+ { "code": "not-in", "display": "Not in Set" },
378
+ { "code": "generalizes", "display": "Generalizes (by Subsumption)" },
379
+ { "code": "exists", "display": "Exists" }
380
+ ],
381
+ "http://hl7.org/fhir/CodeSystem/medicationrequest-status": [
382
+ { "code": "active", "display": "Active" },
383
+ { "code": "on-hold", "display": "On Hold" },
384
+ { "code": "cancelled", "display": "Cancelled" },
385
+ { "code": "completed", "display": "Completed" },
386
+ { "code": "entered-in-error", "display": "Entered in Error" },
387
+ { "code": "stopped", "display": "Stopped" },
388
+ { "code": "draft", "display": "Draft" },
389
+ { "code": "unknown", "display": "Unknown" }
390
+ ],
391
+ "http://hl7.org/fhir/CodeSystem/medication-status": [
392
+ { "code": "active", "display": "Active" },
393
+ { "code": "inactive", "display": "Inactive" },
394
+ { "code": "entered-in-error", "display": "Entered in Error" }
395
+ ],
396
+ "http://hl7.org/fhir/CodeSystem/medication-admin-status": [
397
+ { "code": "in-progress", "display": "In Progress" },
398
+ { "code": "not-done", "display": "Not Done" },
399
+ { "code": "on-hold", "display": "On Hold" },
400
+ { "code": "completed", "display": "Completed" },
401
+ { "code": "entered-in-error", "display": "Entered in Error" },
402
+ { "code": "stopped", "display": "Stopped" },
403
+ { "code": "unknown", "display": "Unknown" }
404
+ ],
405
+ "http://hl7.org/fhir/CodeSystem/medicationdispense-status": [
406
+ { "code": "preparation", "display": "Preparation" },
407
+ { "code": "in-progress", "display": "In Progress" },
408
+ { "code": "cancelled", "display": "Cancelled" },
409
+ { "code": "on-hold", "display": "On Hold" },
410
+ { "code": "completed", "display": "Completed" },
411
+ { "code": "entered-in-error", "display": "Entered in Error" },
412
+ { "code": "stopped", "display": "Stopped" },
413
+ { "code": "declined", "display": "Declined" },
414
+ { "code": "unknown", "display": "Unknown" }
415
+ ],
416
+ "http://hl7.org/fhir/request-intent": [
417
+ { "code": "proposal", "display": "Proposal" },
418
+ { "code": "plan", "display": "Plan" },
419
+ { "code": "directive", "display": "Directive" },
420
+ { "code": "order", "display": "Order" },
421
+ { "code": "original-order", "display": "Original Order" },
422
+ { "code": "reflex-order", "display": "Reflex Order" },
423
+ { "code": "filler-order", "display": "Filler Order" },
424
+ { "code": "instance-order", "display": "Instance Order" },
425
+ { "code": "option", "display": "Option" }
426
+ ],
427
+ "http://hl7.org/fhir/request-priority": [
428
+ { "code": "routine", "display": "Routine" },
429
+ { "code": "urgent", "display": "Urgent" },
430
+ { "code": "asap", "display": "ASAP" },
431
+ { "code": "stat", "display": "STAT" }
432
+ ],
433
+ "http://hl7.org/fhir/care-plan-activity-status": [
434
+ { "code": "not-started", "display": "Not Started" },
435
+ { "code": "scheduled", "display": "Scheduled" },
436
+ { "code": "in-progress", "display": "In Progress" },
437
+ { "code": "on-hold", "display": "On Hold" },
438
+ { "code": "completed", "display": "Completed" },
439
+ { "code": "cancelled", "display": "Cancelled" },
440
+ { "code": "stopped", "display": "Stopped" },
441
+ { "code": "unknown", "display": "Unknown" },
442
+ { "code": "entered-in-error", "display": "Entered in Error" }
443
+ ],
444
+ "http://hl7.org/fhir/quantity-comparator": [
445
+ { "code": "<", "display": "Less than" },
446
+ { "code": "<=", "display": "Less or Equal to" },
447
+ { "code": ">=", "display": "Greater or Equal to" },
448
+ { "code": ">", "display": "Greater than" }
449
+ ],
450
+ "http://hl7.org/fhir/days-of-week": [
451
+ { "code": "mon", "display": "Monday" },
452
+ { "code": "tue", "display": "Tuesday" },
453
+ { "code": "wed", "display": "Wednesday" },
454
+ { "code": "thu", "display": "Thursday" },
455
+ { "code": "fri", "display": "Friday" },
456
+ { "code": "sat", "display": "Saturday" },
457
+ { "code": "sun", "display": "Sunday" }
94
458
  ]
95
459
  },
96
460
  "valueSets": {
@@ -103,6 +467,53 @@
103
467
  "http://hl7.org/fhir/ValueSet/namingsystem-type": ["http://hl7.org/fhir/namingsystem-type"],
104
468
  "http://hl7.org/fhir/ValueSet/namingsystem-identifier-type": ["http://hl7.org/fhir/namingsystem-identifier-type"],
105
469
  "http://hl7.org/fhir/ValueSet/publication-status": ["http://hl7.org/fhir/publication-status"],
106
- "http://hl7.org/fhir/ValueSet/task-status": ["http://hl7.org/fhir/task-status"]
470
+ "http://hl7.org/fhir/ValueSet/task-status": ["http://hl7.org/fhir/task-status"],
471
+ "http://hl7.org/fhir/ValueSet/event-status": ["http://hl7.org/fhir/event-status"],
472
+ "http://hl7.org/fhir/ValueSet/immunization-status": ["http://hl7.org/fhir/event-status"],
473
+ "http://hl7.org/fhir/ValueSet/observation-status": ["http://hl7.org/fhir/observation-status"],
474
+ "http://hl7.org/fhir/ValueSet/administrative-gender": ["http://hl7.org/fhir/administrative-gender"],
475
+ "http://hl7.org/fhir/ValueSet/encounter-status": ["http://hl7.org/fhir/encounter-status"],
476
+ "http://hl7.org/fhir/ValueSet/request-status": ["http://hl7.org/fhir/request-status"],
477
+ "http://hl7.org/fhir/ValueSet/narrative-status": ["http://hl7.org/fhir/narrative-status"],
478
+ "http://hl7.org/fhir/ValueSet/composition-status": ["http://hl7.org/fhir/composition-status"],
479
+ "http://hl7.org/fhir/ValueSet/document-reference-status": ["http://hl7.org/fhir/document-reference-status"],
480
+ "http://hl7.org/fhir/ValueSet/list-status": ["http://hl7.org/fhir/list-status"],
481
+ "http://hl7.org/fhir/ValueSet/fm-status": ["http://hl7.org/fhir/fm-status"],
482
+ "http://hl7.org/fhir/ValueSet/diagnostic-report-status": ["http://hl7.org/fhir/diagnostic-report-status"],
483
+ "http://hl7.org/fhir/ValueSet/bundle-type": ["http://hl7.org/fhir/bundle-type"],
484
+ "http://hl7.org/fhir/ValueSet/allergy-intolerance-type": ["http://hl7.org/fhir/allergy-intolerance-type"],
485
+ "http://hl7.org/fhir/ValueSet/group-type": ["http://hl7.org/fhir/group-type"],
486
+ "http://hl7.org/fhir/ValueSet/measure-report-status": ["http://hl7.org/fhir/measure-report-status"],
487
+ "http://hl7.org/fhir/ValueSet/measure-report-type": ["http://hl7.org/fhir/measure-report-type"],
488
+ "http://hl7.org/fhir/ValueSet/goal-status": ["http://hl7.org/fhir/goal-status"],
489
+ "http://hl7.org/fhir/ValueSet/episode-of-care-status": ["http://hl7.org/fhir/episode-of-care-status"],
490
+ "http://hl7.org/fhir/ValueSet/specimen-status": ["http://hl7.org/fhir/specimen-status"],
491
+ "http://hl7.org/fhir/ValueSet/flag-status": ["http://hl7.org/fhir/flag-status"],
492
+ "http://hl7.org/fhir/ValueSet/address-use": ["http://hl7.org/fhir/address-use"],
493
+ "http://hl7.org/fhir/ValueSet/address-type": ["http://hl7.org/fhir/address-type"],
494
+ "http://hl7.org/fhir/ValueSet/contact-point-system": ["http://hl7.org/fhir/contact-point-system"],
495
+ "http://hl7.org/fhir/ValueSet/contact-point-use": ["http://hl7.org/fhir/contact-point-use"],
496
+ "http://hl7.org/fhir/ValueSet/identifier-use": ["http://hl7.org/fhir/identifier-use"],
497
+ "http://hl7.org/fhir/ValueSet/name-use": ["http://hl7.org/fhir/name-use"],
498
+ "http://hl7.org/fhir/ValueSet/link-type": ["http://hl7.org/fhir/link-type"],
499
+ "http://hl7.org/fhir/ValueSet/account-status": ["http://hl7.org/fhir/account-status"],
500
+ "http://hl7.org/fhir/ValueSet/consent-state-codes": ["http://hl7.org/fhir/consent-state-codes"],
501
+ "http://hl7.org/fhir/ValueSet/substance-status": ["http://hl7.org/fhir/substance-status"],
502
+ "http://hl7.org/fhir/ValueSet/questionnaire-answers-status": ["http://hl7.org/fhir/questionnaire-answers-status"],
503
+ "http://hl7.org/fhir/ValueSet/item-type": ["http://hl7.org/fhir/item-type"],
504
+ "http://hl7.org/fhir/ValueSet/http-verb": ["http://hl7.org/fhir/http-verb"],
505
+ "http://hl7.org/fhir/ValueSet/capability-statement-kind": ["http://hl7.org/fhir/capability-statement-kind"],
506
+ "http://hl7.org/fhir/ValueSet/restful-capability-mode": ["http://hl7.org/fhir/restful-capability-mode"],
507
+ "http://hl7.org/fhir/ValueSet/search-param-type": ["http://hl7.org/fhir/search-param-type"],
508
+ "http://hl7.org/fhir/ValueSet/filter-operator": ["http://hl7.org/fhir/filter-operator"],
509
+ "http://hl7.org/fhir/ValueSet/medicationrequest-status": ["http://hl7.org/fhir/CodeSystem/medicationrequest-status"],
510
+ "http://hl7.org/fhir/ValueSet/medication-status": ["http://hl7.org/fhir/CodeSystem/medication-status"],
511
+ "http://hl7.org/fhir/ValueSet/medication-admin-status": ["http://hl7.org/fhir/CodeSystem/medication-admin-status"],
512
+ "http://hl7.org/fhir/ValueSet/medicationdispense-status": ["http://hl7.org/fhir/CodeSystem/medicationdispense-status"],
513
+ "http://hl7.org/fhir/ValueSet/request-intent": ["http://hl7.org/fhir/request-intent"],
514
+ "http://hl7.org/fhir/ValueSet/request-priority": ["http://hl7.org/fhir/request-priority"],
515
+ "http://hl7.org/fhir/ValueSet/care-plan-activity-status": ["http://hl7.org/fhir/care-plan-activity-status"],
516
+ "http://hl7.org/fhir/ValueSet/quantity-comparator": ["http://hl7.org/fhir/quantity-comparator"],
517
+ "http://hl7.org/fhir/ValueSet/days-of-week": ["http://hl7.org/fhir/days-of-week"]
107
518
  }
108
519
  }
@@ -356,7 +356,7 @@ export async function createPackageFromDir(sourceDir, outFile) {
356
356
  * a map of CodeSystem URL β†’ concept[]. This allows ValueSet resolution for
357
357
  * compose.include entries that reference a whole system without listing concepts.
358
358
  */
359
- export function readCodeSystemConceptsFromDir(extractedRoot) {
359
+ export function readCodeSystemConceptsFromDir(extractedRoot, maxConceptCount) {
360
360
  const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'CodeSystem-');
361
361
  const map = {};
362
362
  for (const file of jsonFiles) {
@@ -371,6 +371,9 @@ export function readCodeSystemConceptsFromDir(extractedRoot) {
371
371
  if (content.content !== 'complete')
372
372
  continue;
373
373
  const concepts = flattenCodeSystemConcepts(content.concept);
374
+ // When a cap is set (large-package selective loading), skip huge CodeSystems
375
+ if (maxConceptCount !== undefined && concepts.length > maxConceptCount)
376
+ continue;
374
377
  if (concepts.length > 0) {
375
378
  map[url] = concepts;
376
379
  }
@@ -570,6 +573,13 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
570
573
  * Core SDs are derived via `deriveVersionData()` / bundled base definitions.
571
574
  * Extensions and terminology resources are fetched on-demand as needed.
572
575
  */
576
+ /**
577
+ * Maximum number of flattened concepts a CodeSystem may have to be loaded from
578
+ * a large HL7 package during selective loading. 973 of the 1 054 complete
579
+ * CodeSystems in hl7.fhir.r4.core have ≀ 20 concepts; a cap of 500 captures
580
+ * every practically-used VS while keeping memory bounded.
581
+ */
582
+ const LARGE_PKG_MAX_CONCEPTS = 500;
573
583
  export function isLargeHl7Package(depName) {
574
584
  // hl7.terminology.r5, hl7.terminology.r4, etc. β€” large CodeSystem/ValueSet packages
575
585
  if (depName.startsWith('hl7.terminology'))
@@ -667,11 +677,20 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
667
677
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
668
678
  const deps = pkgJson.dependencies || {};
669
679
  for (const [depName, depVersion] of Object.entries(deps)) {
670
- if (isLargeHl7Package(depName))
671
- continue;
672
680
  const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
673
- if (fs.existsSync(depDir))
681
+ if (!fs.existsSync(depDir))
682
+ continue;
683
+ if (isLargeHl7Package(depName)) {
684
+ // Selectively load small CodeSystems from large packages
685
+ const smallCs = readCodeSystemConceptsFromDir(depDir, LARGE_PKG_MAX_CONCEPTS);
686
+ for (const [url, concepts] of Object.entries(smallCs)) {
687
+ if (!mergedCodeSystems[url])
688
+ mergedCodeSystems[url] = concepts;
689
+ }
690
+ }
691
+ else {
674
692
  collectCodeSystems(depDir);
693
+ }
675
694
  }
676
695
  }
677
696
  catch { /* skip */ }
@@ -715,11 +734,19 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
715
734
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
716
735
  const deps = pkgJson.dependencies || {};
717
736
  for (const [depName, depVersion] of Object.entries(deps)) {
718
- if (isLargeHl7Package(depName))
719
- continue;
720
737
  // Look for dependency in cache: name@version format
721
738
  const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
722
- if (fs.existsSync(depDir)) {
739
+ if (!fs.existsSync(depDir))
740
+ continue;
741
+ if (isLargeHl7Package(depName)) {
742
+ // Selectively load ValueSets from large packages (CodeSystems already loaded in pass 1)
743
+ const vsCodes = readValueSetCodesFromDir(depDir, mergedCodeSystems);
744
+ for (const [uri, codeBucket] of Object.entries(vsCodes)) {
745
+ if (!merged[uri])
746
+ merged[uri] = codeBucket;
747
+ }
748
+ }
749
+ else {
723
750
  log.info(`Loading ValueSets from dependency: ${depName}@${depVersion}`);
724
751
  loadPackageAndDeps(depDir);
725
752
  }
@@ -463,7 +463,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
463
463
  // Post-processing: Aggregate required slices onto parent array fields
464
464
  // This populates requiredSlices on array fields that have slices with min >= 1
465
465
  // Pass both newFields and baseFields so we can find parent fields from base profiles
466
- await aggregateRequiredSlices(newFields, baseFields);
466
+ await aggregateRequiredSlices(newFields, baseFields, valueSetCodesMap);
467
467
  return { newFields, oldFields: baseFields };
468
468
  }
469
469
  // NOTE: buildFhirChildTypeMap was removed - now using pre-generated fhirDataTypes.json instead
@@ -103,8 +103,9 @@ export function synthesizeSlicePatternsFromChildren(fields) {
103
103
  *
104
104
  * @param fields - The new fields from the differential (slices are defined here)
105
105
  * @param baseFields - Optional fields from base profiles (parent array fields may be here)
106
+ * @param valueSetCodesMap - Resolved ValueSet→codes map for IG-specific bindings
106
107
  */
107
- export async function aggregateRequiredSlices(fields, baseFields = []) {
108
+ export async function aggregateRequiredSlices(fields, baseFields = [], valueSetCodesMap) {
108
109
  const allFields = [...fields, ...baseFields];
109
110
  const slicesByParentPath = new Map();
110
111
  for (const field of fields) {
@@ -264,7 +265,14 @@ export async function aggregateRequiredSlices(fields, baseFields = []) {
264
265
  elemAny.patternString;
265
266
  const isChoiceElement = /^value\[x\]$/i.test(childPath);
266
267
  const bindingUri = elem.binding?.valueSet;
267
- const resolvedDefault = bindingUri ? resolveValueSetDefault(bindingUri) : undefined;
268
+ let resolvedDefault = bindingUri ? resolveValueSetDefault(bindingUri) : undefined;
269
+ if (!resolvedDefault && bindingUri && valueSetCodesMap) {
270
+ const bare = bindingUri.split('|')[0];
271
+ const codes = valueSetCodesMap[bare] ?? valueSetCodesMap[bindingUri];
272
+ if (codes?.length) {
273
+ resolvedDefault = { code: codes[0].code, system: codes[0].system };
274
+ }
275
+ }
268
276
  const isSimpleExtension = isChoiceElement && elem.type?.length &&
269
277
  typeProfile?.type === 'Extension' &&
270
278
  !profileElements.some(pe => pe.path === 'Extension.extension' && pe.sliceName);
@@ -19,6 +19,7 @@ export function parseValueSet(valueSet) {
19
19
  const concepts = [];
20
20
  // Collect system URIs from compose.include for system-level validation
21
21
  const systems = [];
22
+ let hasFilter = false;
22
23
  // Parse compose.include sections
23
24
  if (vs.compose?.include) {
24
25
  for (const include of vs.compose.include) {
@@ -36,18 +37,11 @@ export function parseValueSet(valueSet) {
36
37
  }
37
38
  }
38
39
  // Filter-based inclusion (e.g., all codes from a system)
40
+ // We can't enumerate filter codes at build time, but we keep any
41
+ // explicit concepts from other includes and still process expansion below.
39
42
  if (include.filter) {
40
- // For filter-based ValueSets, we can't enumerate codes at build time
41
- // Mark as large to skip union type generation
42
- log.debug(`ValueSet ${name} uses filters, skipping union type generation`);
43
- return {
44
- url: url || '',
45
- name: name || 'Unknown',
46
- concepts: [],
47
- size: Infinity,
48
- isSmall: false,
49
- systems: systems.length > 0 ? systems : undefined,
50
- };
43
+ hasFilter = true;
44
+ log.debug(`ValueSet ${name} uses filters, union type generation skipped`);
51
45
  }
52
46
  }
53
47
  }
@@ -61,8 +55,11 @@ export function parseValueSet(valueSet) {
61
55
  });
62
56
  }
63
57
  }
64
- const size = concepts.length;
65
- const isSmall = size > 0 && size <= ctx().valueSetThresholds.UNION_TYPE;
58
+ // When filters are present, the concept list may be incomplete.
59
+ // Use Infinity size to prevent union type generation, but still expose
60
+ // whatever concepts we did collect (explicit + expansion) for binding validation.
61
+ const size = hasFilter ? Infinity : concepts.length;
62
+ const isSmall = !hasFilter && concepts.length > 0 && concepts.length <= ctx().valueSetThresholds.UNION_TYPE;
66
63
  return {
67
64
  url: url || '',
68
65
  name: name || 'Unknown',
@@ -153,7 +153,7 @@ export async function processStructureDefinition(sd, ctx) {
153
153
  finalInterfaceContent = dedupedLines.join('\n');
154
154
  // ── Required fields ──────────────────────────────────────────────
155
155
  const merged = mergeFields(aligned.alignedNewFields, parsed.oldFields);
156
- const validateResult = generateValidateProfileFunction(interfaceName, merged, valueSets, sd.url, sd.type);
156
+ const validateResult = generateValidateProfileFunction(interfaceName, merged, valueSets, sd.url, sd.type, profileUrlToName);
157
157
  const validateFn = validateResult.code;
158
158
  const candidateRequiredFields = [...aligned.alignedNewFields, ...parsed.oldFields];
159
159
  const profileDiffFieldNames = new Set(aligned.alignedNewFields.map(f => f.name));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",