babelfhir-ts 1.0.33 → 1.0.34

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.
@@ -1,3 +1,5 @@
1
+ import { logger } from '../logger.js';
2
+ const log = logger.withTag('classGenerator');
1
3
  // FHIR primitive types - these cannot have nested child properties (only _fieldName extensions)
2
4
  const FHIR_PRIMITIVE_TYPES = new Set([
3
5
  'string', 'boolean', 'integer', 'decimal', 'uri', 'url', 'canonical', 'base64Binary',
@@ -23,6 +25,36 @@ function primitivePlaceholder(type) {
23
25
  default: return `randomId()`; // fallback
24
26
  }
25
27
  }
28
+ /**
29
+ * Generate a sensible default value for a 'code' type field based on its name.
30
+ * Common FHIR code fields have known value sets - we pick a valid default.
31
+ */
32
+ function generateCodeDefault(fieldName) {
33
+ const name = fieldName.toLowerCase();
34
+ // Map common field names to sensible defaults
35
+ if (name === 'language')
36
+ return `'en'`; // ISO 639-1 language code
37
+ if (name === 'confidentiality')
38
+ return `'N'`; // HL7 v3 Confidentiality (Normal)
39
+ if (name === 'status')
40
+ return `'active'`;
41
+ if (name === 'mode')
42
+ return `'snapshot'`;
43
+ if (name === 'intent')
44
+ return `'proposal'`;
45
+ if (name === 'priority')
46
+ return `'routine'`;
47
+ if (name === 'gender')
48
+ return `'unknown'`;
49
+ if (name === 'use')
50
+ return `'usual'`;
51
+ if (name === 'type')
52
+ return `'document'`;
53
+ if (name === 'content')
54
+ return `'complete'`;
55
+ // Fallback to a simple code pattern
56
+ return `'unknown'`;
57
+ }
26
58
  const OBSERVATION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/observation-category';
27
59
  const CONDITION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/condition-category';
28
60
  const ROLE_CODE_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v3-RoleCode';
@@ -668,11 +700,22 @@ function generateSliceElement(slice, elementType) {
668
700
  }
669
701
  }
670
702
  }
703
+ // Handle CodeableConcept slices with binding codes but no patternConstraint
704
+ // This synthesizes a valid CodeableConcept from the first binding code
705
+ // Critical for slices like category:us-core which require codes from a ValueSet
706
+ else if (elementType === 'CodeableConcept' && slice.binding?.codes && slice.binding.codes.length > 0) {
707
+ const firstCode = slice.binding.codes[0];
708
+ const system = firstCode.system || 'http://terminology.hl7.org/CodeSystem/v3-NullFlavor';
709
+ const code = firstCode.code;
710
+ const display = firstCode.display || code;
711
+ return JSON.stringify({
712
+ coding: [{ system, code, display }]
713
+ });
714
+ }
671
715
  // Handle required children with their constraints
672
716
  // Only process direct children (single-segment paths) to avoid complex nested object generation
673
- // DEBUG: Log requiredChildren for document bundle slices
674
- if (process.env.DEBUG_FHIR_GEN === 'true' && slice.name?.toLowerCase().includes('composition')) {
675
- console.log(`[generateSliceElement] Composition slice requiredChildren: ${JSON.stringify(slice.requiredChildren?.map(c => ({ path: c.path, isRequired: c.isRequired, profileUrls: c.profileUrls })))}`);
717
+ if (slice.name?.toLowerCase().includes('composition')) {
718
+ log.debug(`Composition slice requiredChildren: ${JSON.stringify(slice.requiredChildren?.map(c => ({ path: c.path, isRequired: c.isRequired, profileUrls: c.profileUrls })))}`);
676
719
  }
677
720
  if (slice.requiredChildren && slice.requiredChildren.length > 0) {
678
721
  for (const child of slice.requiredChildren) {
@@ -713,10 +756,7 @@ function generateSliceElement(slice, elementType) {
713
756
  const sliceName = slice.name?.toLowerCase() || '';
714
757
  const profileUrls = child.profileUrls || [];
715
758
  const profileUrl = profileUrls[0]; // Primary profile URL
716
- // DEBUG: Log profile detection for document bundles
717
- if (process.env.DEBUG_FHIR_GEN === 'true') {
718
- console.log(`[generateSliceElement] Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
719
- }
759
+ log.debug(`Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
720
760
  const metaProfile = profileUrl ? `, meta: { profile: ['${profileUrl}'] }` : '';
721
761
  // Extract resource type from profile URL if available (e.g., Composition-uv-ips -> Composition)
722
762
  // Also check child.type which may contain the base resource type
@@ -830,9 +870,8 @@ function generateSliceElement(slice, elementType) {
830
870
  const sliceProfileUrls = slice.profileUrls || resourceChild?.profileUrls || [];
831
871
  const primaryProfileUrl = sliceProfileUrls[0];
832
872
  const metaProfileExpr = primaryProfileUrl ? `, meta: { profile: ['${primaryProfileUrl}'] }` : '';
833
- // DEBUG: Log slice info for document bundles
834
- if (process.env.DEBUG_FHIR_GEN === 'true' && (sliceName.includes('composition') || sliceName.includes('document'))) {
835
- console.log(`[generateSliceElement] Composition slice - name: ${slice.name}, profileUrls: ${JSON.stringify(slice.profileUrls)}, resourceChild?.profileUrls: ${JSON.stringify(resourceChild?.profileUrls)}, primaryProfileUrl: ${primaryProfileUrl}`);
873
+ if (sliceName.includes('composition') || sliceName.includes('document')) {
874
+ log.debug(`Composition slice - name: ${slice.name}, profileUrls: ${JSON.stringify(slice.profileUrls)}, resourceChild?.profileUrls: ${JSON.stringify(resourceChild?.profileUrls)}, primaryProfileUrl: ${primaryProfileUrl}`);
836
875
  }
837
876
  if (sliceName.includes('composition') || sliceName.includes('document')) {
838
877
  // Composition slice - requires: text (narrative), identifier, author.display
@@ -883,6 +922,21 @@ function generateSliceElement(slice, elementType) {
883
922
  // Device slice
884
923
  return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'Device', id: randomId()${metaProfileExpr}, type: { coding: [{ system: 'http://snomed.info/sct', code: '49062001', display: 'Device' }] } } }`;
885
924
  }
925
+ // Handle non-Bundle element types - don't return fullUrl structure for simple types
926
+ if (elementType === 'CodeableConcept') {
927
+ // For CodeableConcept slices (like category:us-core), return a valid CodeableConcept
928
+ // The binding/pattern should ideally provide the code, but fallback to a generic structure
929
+ return `{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/data-absent-reason', code: 'unknown', display: 'Unknown' }] }`;
930
+ }
931
+ if (elementType === 'Coding') {
932
+ return `{ system: 'http://terminology.hl7.org/CodeSystem/data-absent-reason', code: 'unknown', display: 'Unknown' }`;
933
+ }
934
+ if (elementType === 'Identifier') {
935
+ return `{ system: 'http://example.org/ids', value: randomId() }`;
936
+ }
937
+ if (elementType === 'Reference') {
938
+ return `{ reference: 'Resource/' + randomId() }`;
939
+ }
886
940
  // For BackboneElement without specific type, return a minimal placeholder
887
941
  return `{ fullUrl: 'urn:uuid:' + randomId() }`;
888
942
  }
@@ -1232,8 +1286,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1232
1286
  requiredInits.push(`${key(rf.name)}: ${initExpr}`);
1233
1287
  continue;
1234
1288
  }
1235
- if (['string', 'number', 'boolean'].includes(rf.type)) {
1236
- const expr = primitivePlaceholder(rf.type);
1289
+ if (['string', 'number', 'boolean', 'code'].includes(rf.type)) {
1290
+ // For 'code' type without binding, generate based on field name to pick a sensible default
1291
+ const expr = rf.type === 'code' ? generateCodeDefault(rf.name) : primitivePlaceholder(rf.type);
1237
1292
  const initExpr = buildValueExpression(rf, expr);
1238
1293
  requiredInits.push(`${key(rf.name)}: ${initExpr}`);
1239
1294
  continue;
@@ -1310,6 +1365,35 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
1310
1365
  code = JSON.stringify(parentBindingCode.code);
1311
1366
  }
1312
1367
  }
1368
+ // Check requiredChildren from type profile for required fields like 'version' and 'system'
1369
+ // This handles cases like CodingOPS which requires version field
1370
+ // and CodingAlphaID which has a fixed system value
1371
+ if (slice.requiredChildren && slice.requiredChildren.length > 0) {
1372
+ for (const child of slice.requiredChildren) {
1373
+ if (child.path === 'version' && !version) {
1374
+ // Check for fixed or pattern value first
1375
+ if (child.fixedValue && typeof child.fixedValue === 'string') {
1376
+ version = child.fixedValue;
1377
+ }
1378
+ else if (child.patternConstraint && typeof child.patternConstraint === 'string') {
1379
+ version = child.patternConstraint;
1380
+ }
1381
+ else if (child.isRequired) {
1382
+ // Version is required by type profile but not set - use current year
1383
+ version = new Date().getFullYear().toString();
1384
+ }
1385
+ }
1386
+ if (child.path === 'system') {
1387
+ // Check for fixed or pattern value for system
1388
+ if (child.fixedValue && typeof child.fixedValue === 'string') {
1389
+ system = child.fixedValue;
1390
+ }
1391
+ else if (child.patternConstraint && typeof child.patternConstraint === 'string') {
1392
+ system = child.patternConstraint;
1393
+ }
1394
+ }
1395
+ }
1396
+ }
1313
1397
  codingProps.push(`system: ${JSON.stringify(system)}`);
1314
1398
  if (version) {
1315
1399
  codingProps.push(`version: ${JSON.stringify(version)}`);
@@ -75,12 +75,16 @@ export class ImportManager {
75
75
  return;
76
76
  }
77
77
  // Check if we already have an external profile registered with this URL
78
- // If so, use the existing name to avoid duplicates
78
+ // If so, use the existing name to avoid duplicates, but still register baseType info
79
79
  if (profileUrl) {
80
80
  for (const [existingName, existingMeta] of this.externalProfiles) {
81
81
  if (existingMeta.profileUrl === profileUrl) {
82
82
  const existingNormalized = toPascalCase(sanitizeIdentifier(existingName));
83
83
  this.customImports.add(existingNormalized);
84
+ // Still register baseType info even for duplicates (important for extension detection)
85
+ if (baseType) {
86
+ this.registerExternalProfile(typeName, profileUrl, baseType, isFhirBase);
87
+ }
84
88
  return;
85
89
  }
86
90
  }
@@ -139,9 +143,11 @@ export class ImportManager {
139
143
  const existing = meta.baseTypes.get(sanitizedBase);
140
144
  if (!existing) {
141
145
  meta.baseTypes.set(sanitizedBase, { isFhir: isFhirBase });
146
+ logger.debug(`[ImportManager] Registered baseType '${sanitizedBase}' (isFhir=${isFhirBase}) for external profile '${normalizedTypeName}' (URL: ${resolvedProfileUrl})`);
142
147
  }
143
148
  else if (isFhirBase && !existing.isFhir) {
144
149
  existing.isFhir = true;
150
+ logger.debug(`[ImportManager] Updated baseType '${sanitizedBase}' to isFhir=true for '${normalizedTypeName}'`);
145
151
  }
146
152
  }
147
153
  }
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { generateClass } from './classGenerator.js';
5
5
  import { generateInterfaces } from './interfaceGenerator.js';
6
6
  import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir, readValueSetCodesFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir } from './packageParser.js';
7
- import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions } from './sdParser.js';
7
+ import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions, clearLocalStructureDefinitions } from './sdParser.js';
8
8
  import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, ensureDirectoryExists, writeToFile, getBaseResource, downloadFile, sanitizeIdentifier, toPascalCase, capitalize } from './utils.js';
9
9
  import { generateValidateProfileFunction } from './validatorGenerator.js';
10
10
  import { generateValueSetTypeScript, generateValueSetRegistry } from './valueSetGenerator.js';
@@ -294,14 +294,35 @@ async function processStructureDefinition(sd, ctx) {
294
294
  failedFetchProfiles.push(profileInfo.profileUrl || normalizedTypeName);
295
295
  const baseTypeCandidates = Array.from(profileInfo.baseTypes.entries());
296
296
  const preferred = baseTypeCandidates.find(([, meta]) => meta.isFhir);
297
+ // Log detailed info about the external profile for debugging
298
+ log.info(`[ExternalProfile] ${normalizedTypeName}:`);
299
+ log.info(` - URL: ${profileInfo.profileUrl || 'N/A'}`);
300
+ log.info(` - baseTypes: ${baseTypeCandidates.map(([t, m]) => `${t}(isFhir=${m.isFhir})`).join(', ') || 'none'}`);
301
+ log.info(` - isExtensionProfile: ${profileInfo.isExtensionProfile ?? 'undefined'}`);
297
302
  // Smart detection: if the URL or name contains "extension", or baseTypes includes Extension, use Extension as base type
303
+ // Also check if the profile is used in an extension context (isExtensionProfile flag from interfaceGenerator)
298
304
  let baseType = preferred?.[0] || 'Element';
299
305
  const urlLower = (profileInfo.profileUrl || '').toLowerCase();
300
306
  const nameLower = normalizedTypeName.toLowerCase();
301
307
  const hasExtensionInBaseTypes = baseTypeCandidates.some(([type]) => type === 'Extension');
302
- if (urlLower.includes('extension') || nameLower.includes('extension') || hasExtensionInBaseTypes) {
308
+ const isUsedAsExtension = profileInfo.isExtensionProfile === true;
309
+ log.info(` - Initial baseType: ${baseType}`);
310
+ log.info(` - urlLower.includes('extension'): ${urlLower.includes('extension')}`);
311
+ log.info(` - nameLower.includes('extension'): ${nameLower.includes('extension')}`);
312
+ log.info(` - hasExtensionInBaseTypes: ${hasExtensionInBaseTypes}`);
313
+ log.info(` - isUsedAsExtension: ${isUsedAsExtension}`);
314
+ if (urlLower.includes('extension') || nameLower.includes('extension') || hasExtensionInBaseTypes || isUsedAsExtension) {
303
315
  baseType = 'Extension';
316
+ log.info(` - Overriding baseType to Extension`);
304
317
  }
318
+ // Safety: NEVER use 'Element' as base type for extension profiles - it causes TypeScript errors
319
+ // when the generated type is used in extension?: Extension[] arrays
320
+ if (baseType === 'Element') {
321
+ log.warn(`[ExternalProfile] Type alias for ${normalizedTypeName} is using 'Element' as base type. ` +
322
+ `If this type is used in extension arrays, it may cause TypeScript compilation errors. ` +
323
+ `URL: ${profileInfo.profileUrl || 'N/A'}, baseTypes: ${baseTypeCandidates.map(([t]) => t).join(', ') || 'none'}`);
324
+ }
325
+ log.info(` - Final baseType: ${baseType}`);
305
326
  const importLine = `import type { ${baseType} } from "fhir/r4";\n\n`;
306
327
  const typeAliasContent = `${importLine}// Type alias for external profile\n// Profile: ${profileInfo.profileUrl}\n// Note: Full StructureDefinition could not be fetched; using type alias based on ${baseType}\nexport type ${normalizedTypeName} = ${baseType};\n`;
307
328
  writeToFile(path.join(outputDir, `${normalizedTypeName}.ts`), typeAliasContent);
@@ -690,6 +711,75 @@ async function processStructureDefinition(sd, ctx) {
690
711
  key = parentPart.split(':')[0];
691
712
  }
692
713
  }
714
+ // For coding slices with profile URLs, resolve the profile to get required fields like 'version'
715
+ // This handles cases like coding:ICD-10-GM -> ISiKICD10GMCoding -> CodingICD10GM where version is required
716
+ if (lastPart === 'coding' && f.sliceName && f.profileUrls && f.profileUrls.length > 0) {
717
+ for (const profileUrl of f.profileUrls) {
718
+ // Check if already resolved
719
+ let resolved = resolvedProfilePatterns.get(profileUrl);
720
+ if (!resolved) {
721
+ try {
722
+ const profileSd = await fetchStructureDefinition(profileUrl);
723
+ if (profileSd) {
724
+ const patternData = {};
725
+ // Track required fields from this profile and its base
726
+ const requiredFields = [];
727
+ // Check elements for required fields
728
+ const checkElements = (elements) => {
729
+ if (!elements)
730
+ return;
731
+ for (const elem of elements) {
732
+ // Check for required version field (min >= 1)
733
+ if (elem.path?.endsWith('.version') && elem.min && elem.min >= 1) {
734
+ requiredFields.push('version');
735
+ }
736
+ // Check for fixedUri on system
737
+ if (elem.path?.endsWith('.system') && elem.fixedUri) {
738
+ patternData.system = elem.fixedUri;
739
+ }
740
+ }
741
+ };
742
+ // Check this profile's elements
743
+ checkElements(profileSd.snapshot?.element);
744
+ checkElements(profileSd.differential?.element);
745
+ // Also check the base profile if there is one (e.g., CodingICD10GM requires version)
746
+ if (profileSd.baseDefinition) {
747
+ try {
748
+ const baseSd = await fetchStructureDefinition(profileSd.baseDefinition);
749
+ if (baseSd) {
750
+ checkElements(baseSd.snapshot?.element);
751
+ checkElements(baseSd.differential?.element);
752
+ }
753
+ }
754
+ catch {
755
+ // Base profile resolution failed
756
+ }
757
+ }
758
+ // If version is required, add it to the pattern
759
+ if (requiredFields.includes('version')) {
760
+ // Use the current year as the default version (common for ICD-10 codes)
761
+ patternData.version = new Date().getFullYear().toString();
762
+ }
763
+ if (Object.keys(patternData).length > 0) {
764
+ resolvedProfilePatterns.set(profileUrl, patternData);
765
+ resolved = patternData;
766
+ }
767
+ }
768
+ }
769
+ catch {
770
+ // Profile resolution failed
771
+ }
772
+ }
773
+ // Merge resolved profile pattern with existing patternConstraint
774
+ if (resolved && typeof resolved === 'object') {
775
+ const existingPattern = (f.patternConstraint && typeof f.patternConstraint === 'object')
776
+ ? f.patternConstraint
777
+ : {};
778
+ // Merge: existing pattern takes precedence for system, resolved adds version
779
+ f.patternConstraint = { ...resolved, ...existingPattern };
780
+ }
781
+ }
782
+ }
693
783
  // For identifier slices with profile URLs, resolve the profile to get system constraint
694
784
  // This handles cases like identifier:Arztnummer -> identifier-lanr where the system
695
785
  // is defined in the referenced profile, not in the slicing profile itself
@@ -777,6 +867,35 @@ async function processStructureDefinition(sd, ctx) {
777
867
  }
778
868
  }
779
869
  }
870
+ // Also extract binding sample codes for nested primitive paths (like valueQuantity.code)
871
+ // This handles cases where the binding is on a nested field, not a slice
872
+ // IMPORTANT: Only apply to truly nested paths (2+ parts after removing resource type)
873
+ // Top-level slices like category:us-core should NOT be handled here - they're handled below
874
+ else if (f.binding?.sampleCode && typeof f.binding.sampleCode === 'object') {
875
+ const elementId = f.elementId || f.name;
876
+ const idParts = elementId.split('.');
877
+ // Remove the resource type prefix (first part)
878
+ if (idParts.length > 2) { // Changed from > 1 to > 2: need at least 3 parts (Resource.field.nested)
879
+ idParts.shift();
880
+ // Build nested key, extracting slice names where present
881
+ const nestedParts = [];
882
+ for (const part of idParts) {
883
+ if (part.includes(':')) {
884
+ nestedParts.push(part.split(':')[1]);
885
+ }
886
+ else if (!part.includes('[x]')) {
887
+ nestedParts.push(part);
888
+ }
889
+ }
890
+ if (nestedParts.length > 1) { // Changed from > 0 to > 1: need at least 2 nested parts for truly nested path
891
+ const nestedKey = nestedParts.join('.');
892
+ // Only add if we don't already have this key
893
+ if (!patternMap.has(nestedKey)) {
894
+ patternMap.set(nestedKey, f.binding.sampleCode.code);
895
+ }
896
+ }
897
+ }
898
+ }
780
899
  // Otherwise, if this is a slice with binding codes, synthesize a pattern
781
900
  else if (!patternMap.has(key) && f.sliceName && f.binding && f.binding.codes && f.binding.codes.length > 0) {
782
901
  // Create a CodeableConcept pattern from the first binding code
@@ -802,13 +921,38 @@ async function processStructureDefinition(sd, ctx) {
802
921
  const refReqKey = `_refReq_${parentFieldName}`;
803
922
  if (!patternMap.has(refReqKey)) {
804
923
  // Store as an object with the nested requirements paths
924
+ // For extension fields, store profile URL so enrichResource can generate correct extension
805
925
  const reqPaths = {};
806
926
  for (const req of requirements) {
807
- reqPaths[req.path] = true; // Mark each required path
927
+ if (req.path === 'extension' && req.profileUrl) {
928
+ // Store extension profile URL for primitive element extension generation
929
+ reqPaths[req.path] = { profile: req.profileUrl };
930
+ }
931
+ else {
932
+ reqPaths[req.path] = true; // Mark each required path
933
+ }
808
934
  }
809
935
  patternMap.set(refReqKey, reqPaths);
810
936
  }
811
937
  }
938
+ // Add type option markers for choice elements (like value[x]) constrained to specific types
939
+ // This enables enrichResource to use the correct value[x] variant (e.g., valueCodeableConcept instead of valueQuantity)
940
+ // IMPORTANT: Check candidateRequiredFields (all fields), not just requiredFields, because value[x] is typically optional (min=0)
941
+ for (const f of candidateRequiredFields) {
942
+ // Only consider choice type fields (ending with [x]) that have type constraints
943
+ if (f.name.endsWith('[x]') && f.typeOptions && f.typeOptions.length > 0) {
944
+ // Extract the field name from the path (e.g., "Observation.value[x]" -> "value[x]")
945
+ const parts = f.name.split('.');
946
+ if (parts.length === 2) {
947
+ const fieldName = parts[1];
948
+ const typeMarkerKey = `_choiceType_${fieldName}`;
949
+ if (!patternMap.has(typeMarkerKey)) {
950
+ // Store the allowed types so enrichResource can pick the right variant
951
+ patternMap.set(typeMarkerKey, f.typeOptions.map(t => t.code));
952
+ }
953
+ }
954
+ }
955
+ }
812
956
  const fieldPatterns = Array.from(patternMap.entries()).map(([fieldName, pattern]) => ({ fieldName, pattern }));
813
957
  // Merge patterns from fieldPatterns into requiredFields so applyRequiredFieldHeuristic can use them
814
958
  // IMPORTANT: Don't add patternConstraint if the field has requiredSlices - those contain the correct pattern info
@@ -846,6 +990,11 @@ async function processStructureDefinition(sd, ctx) {
846
990
  return interfaceName;
847
991
  }
848
992
  export async function generate(fhirSource, outputDir, flags) {
993
+ // Initialize logger at the start of generation
994
+ const logDest = flags?.logDestination || flags?.logLevel || 'console';
995
+ logger.setLevel(logDest, outputDir, flags?.logVerbosity || 'info');
996
+ // Clear local StructureDefinitions from any previous generation
997
+ clearLocalStructureDefinitions();
849
998
  logger.log(`Fetching StructureDefinitions from: ${fhirSource}`);
850
999
  let structureDefinitions = [];
851
1000
  let localPath = fhirSource;
@@ -929,12 +1078,23 @@ export async function generate(fhirSource, outputDir, flags) {
929
1078
  * folder, then repackage to the specified output archive (or alongside original).
930
1079
  */
931
1080
  export async function generateIntoPackage(packageArchivePath, outArchivePath, flags) {
1081
+ // Initialize logger at the start of generation
1082
+ const logDest = flags?.logDestination || flags?.logLevel || 'console';
1083
+ logger.setLevel(logDest, path.dirname(outArchivePath || packageArchivePath), flags?.logVerbosity || 'info');
1084
+ // Clear local StructureDefinitions from any previous generation
1085
+ clearLocalStructureDefinitions();
932
1086
  if (!packageArchivePath.endsWith('.tgz') && !packageArchivePath.endsWith('.zip'))
933
1087
  throw new Error('Only .tgz or .zip supported.');
934
1088
  const extractResult = await extractPackage(packageArchivePath);
935
1089
  const extractedRoot = extractResult.path;
936
1090
  const shouldCleanup = !extractResult.isCached; // Only cleanup temp directories, not cache
937
1091
  try {
1092
+ // Load dependencies from package.json BEFORE reading StructureDefinitions
1093
+ // This ensures base profiles (like coverage-de-basis) are available for resolution
1094
+ const { getPackageManager } = await import('./packageManager.js');
1095
+ const packageManager = getPackageManager();
1096
+ packageManager.loadDependenciesFromExtractedPackage(extractedRoot);
1097
+ logger.log(`Loaded package dependencies (${packageManager.getTotalStructureDefinitionCount()} total SDs available)`);
938
1098
  const structureDefinitions = readStructureDefinitionsFromDir(extractedRoot);
939
1099
  // Load ValueSets from package AND its dependencies (for binding resolution)
940
1100
  const valueSetCodesMap = readValueSetCodesWithDependencies(extractedRoot);
@@ -985,15 +1145,11 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
985
1145
  try {
986
1146
  if (fs.existsSync(vdir)) {
987
1147
  fs.rmSync(vdir, { recursive: true, force: true });
988
- if (process.env.DEBUG_FHIR_GEN === 'true') {
989
- log.debug(`Removed stale ValueSets directory: ${vdir}`);
990
- }
1148
+ log.debug(`Removed stale ValueSets directory: ${vdir}`);
991
1149
  }
992
1150
  }
993
1151
  catch (e) {
994
- if (process.env.DEBUG_FHIR_GEN === 'true') {
995
- log.warn(`Failed to remove stale ValueSets directory ${vdir}: ${e.message}`);
996
- }
1152
+ log.debug(`Failed to remove stale ValueSets directory ${vdir}: ${e.message}`);
997
1153
  }
998
1154
  }
999
1155
  // Build set of existing StructureDefinition names
@@ -1099,8 +1255,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
1099
1255
  fs.rmSync(extractedRoot, { recursive: true, force: true });
1100
1256
  }
1101
1257
  catch (err) {
1102
- if (process.env.DEBUG_FHIR_GEN === 'true')
1103
- log.warn(`Cleanup failed: ${err.message}`);
1258
+ log.debug(`Cleanup failed: ${err.message}`);
1104
1259
  }
1105
1260
  }
1106
1261
  }
@@ -1114,7 +1269,10 @@ function deriveOutputArchiveName(input) {
1114
1269
  */
1115
1270
  export async function generateForDirectory(inputDir, outputDir, flags) {
1116
1271
  // Initialize logger
1117
- logger.setLevel(flags?.logLevel || 'none', outputDir);
1272
+ const logDest = flags?.logDestination || flags?.logLevel || 'console';
1273
+ logger.setLevel(logDest, outputDir, flags?.logVerbosity || 'info');
1274
+ // Clear local StructureDefinitions from any previous generation
1275
+ clearLocalStructureDefinitions();
1118
1276
  if (!fs.existsSync(inputDir))
1119
1277
  throw new Error(`Input directory not found: ${inputDir}`);
1120
1278
  if (!fs.existsSync(outputDir))
@@ -1162,15 +1320,11 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
1162
1320
  try {
1163
1321
  if (fs.existsSync(valueSetOutputDir)) {
1164
1322
  fs.rmSync(valueSetOutputDir, { recursive: true, force: true });
1165
- if (process.env.DEBUG_FHIR_GEN === 'true') {
1166
- log.debug(`Removed stale ValueSets directory: ${valueSetOutputDir}`);
1167
- }
1323
+ log.debug(`Removed stale ValueSets directory: ${valueSetOutputDir}`);
1168
1324
  }
1169
1325
  }
1170
1326
  catch (e) {
1171
- if (process.env.DEBUG_FHIR_GEN === 'true') {
1172
- log.warn(`Failed to remove stale ValueSets directory ${valueSetOutputDir}: ${e.message}`);
1173
- }
1327
+ log.debug(`Failed to remove stale ValueSets directory ${valueSetOutputDir}: ${e.message}`);
1174
1328
  }
1175
1329
  }
1176
1330
  const entries = fs.readdirSync(inputDir);
@@ -1259,6 +1413,8 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
1259
1413
  }
1260
1414
  }
1261
1415
  export async function generateFromJsonFile(jsonFilePath, outputDir, flags) {
1416
+ // Clear local StructureDefinitions from any previous generation
1417
+ clearLocalStructureDefinitions();
1262
1418
  if (!fs.existsSync(jsonFilePath))
1263
1419
  throw new Error(`File not found: ${jsonFilePath}`);
1264
1420
  const raw = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'));
@@ -1274,6 +1430,8 @@ export async function generateFromJsonFile(jsonFilePath, outputDir, flags) {
1274
1430
  * @param flags - Generation options
1275
1431
  */
1276
1432
  export async function generateFromProfileUrl(profileUrl, outputDir, flags) {
1433
+ // Clear local StructureDefinitions from any previous generation
1434
+ clearLocalStructureDefinitions();
1277
1435
  log.info(`Fetching profile: ${profileUrl}`);
1278
1436
  // Load all cached packages to make profile resolution work
1279
1437
  const { getPackageManager } = await import('./packageManager.js');
@@ -7,8 +7,7 @@ import { r4Rules } from './fhirR4Rules.js';
7
7
  import { postProcessExtensions, safetyNetRootExtensions } from './postProcessExtensions.js';
8
8
  const log = logger.withTag('interfaces');
9
9
  export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap, profileIdToName, profileUrlToName) {
10
- const debug = (...args) => { if (process.env.DEBUG_FHIR_GEN === 'true')
11
- log.debug(...args); };
10
+ const debug = (...args) => log.debug(...args);
12
11
  const interfaces = [];
13
12
  const localInterfaceNames = new Set();
14
13
  const writtenLines = new Set();
@@ -25,6 +24,18 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
25
24
  }
26
25
  importManager.addType(typeName, profileUrl, baseType, isFhirBase);
27
26
  };
27
+ // Helper to determine if a field is an extension field and get the correct base type
28
+ // Extension fields should always use 'Extension' as base type for proper fallback aliases
29
+ const getBaseTypeForField = (fieldName, fallbackBaseType) => {
30
+ const fieldNameLower = fieldName.toLowerCase();
31
+ const isExtensionField = fieldNameLower.endsWith('.extension') ||
32
+ fieldNameLower.endsWith('.modifierextension') ||
33
+ fieldNameLower.includes('extension:') ||
34
+ fieldNameLower.includes('modifierextension:');
35
+ const baseTypeName = isExtensionField ? 'Extension' : fallbackBaseType;
36
+ const baseIsFhir = isExtensionField || (baseTypeName ? isFhirType(baseTypeName) : false);
37
+ return { baseTypeName, baseIsFhir };
38
+ };
28
39
  // Helper to find exact interface block index (avoid prefix collisions like Foo and FooBar)
29
40
  const findInterfaceIndex = (name) => {
30
41
  const re = new RegExp(`^export interface ${name}(?:[\n\r\t ]|extends)`);
@@ -171,16 +182,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
171
182
  if (typeName && !isFhirType(typeName) && !isPrimitiveType(typeName)) {
172
183
  // We don't have baseFld yet, so get it from the base fields
173
184
  const baseFldForProfile = baseFields.find(bf => bf.name === field.name);
174
- // If this is an extension field (field name ends with .extension or .modifierExtension,
175
- // or contains extension: slice notation), use Extension as the base type
176
- // so fallback aliases are compatible with extension arrays
177
- const fieldNameLower = field.name.toLowerCase();
178
- const isExtensionField = fieldNameLower.endsWith('.extension') ||
179
- fieldNameLower.endsWith('.modifierextension') ||
180
- fieldNameLower.includes('extension:') ||
181
- fieldNameLower.includes('modifierextension:');
182
- const baseTypeName = isExtensionField ? 'Extension' : (baseFldForProfile?.type || field.type);
183
- const baseIsFhir = isExtensionField || (baseTypeName ? isFhirType(baseTypeName) : false);
185
+ // Use helper to determine base type - extension fields get 'Extension' as base type
186
+ const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFldForProfile?.type || field.type);
184
187
  addTypeImport(typeName, profileUrl, baseTypeName, baseIsFhir);
185
188
  }
186
189
  }
@@ -917,8 +920,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
917
920
  const line = `${fieldName}${field.isOptional ? '?' : ''}: ${resolvedType}${isArrayFinal ? '[]' : ''};`;
918
921
  if (field.isProfiled && field.profileUrls?.[0] && field.type && !isPrimitiveType(field.type) && !isFhirType(field.type)) {
919
922
  const profileUrl = field.profileUrls[0];
920
- const baseTypeName = baseFld?.type;
921
- const baseIsFhir = baseTypeName ? isFhirType(baseTypeName) : false;
923
+ // Use helper to determine base type - extension fields get 'Extension' as base type
924
+ const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
922
925
  addTypeImport(field.type, profileUrl, baseTypeName, baseIsFhir);
923
926
  }
924
927
  // Register external profiles for any field with profile URLs, regardless of nesting
@@ -927,8 +930,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
927
930
  const profileUrl = field.profileUrls[0];
928
931
  // Strip version suffix from canonical URL before extracting name
929
932
  const strippedUrl = stripVersionFromCanonicalUrl(profileUrl);
930
- const baseTypeName = baseFld?.type;
931
- const baseIsFhir = baseTypeName ? isFhirType(baseTypeName) : false;
933
+ // Use helper to determine base type - extension fields get 'Extension' as base type
934
+ const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
932
935
  // Use profileUrlToName to get the actual interface name, fallback to URL-derived name
933
936
  const typeName = profileUrlToName?.get(strippedUrl) || profileUrlToName?.get(profileUrl) || toPascalCase(sanitizeIdentifier(strippedUrl.split('/').pop() || resolvedType));
934
937
  // Only register if it's not a primitive and not already a FHIR type
@@ -942,8 +945,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
942
945
  !isFhirType(resolvedType) &&
943
946
  !localInterfaceNames.has(resolvedType)) {
944
947
  const profileUrl = field.profileUrls?.[0] || (field.type && field.type.includes('http') ? field.type : undefined);
945
- const baseTypeName = baseFld?.type;
946
- const baseIsFhir = baseTypeName ? isFhirType(baseTypeName) : false;
948
+ // Use helper to determine base type - extension fields get 'Extension' as base type
949
+ const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
947
950
  addTypeImport(resolvedType, profileUrl, baseTypeName, baseIsFhir);
948
951
  }
949
952
  if (!isPrimitiveType(resolvedType)) {
@@ -983,8 +986,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
983
986
  else {
984
987
  // Pass profile metadata if available
985
988
  const profileUrl = field.profileUrls?.[0];
986
- const baseTypeName = baseFld?.type;
987
- const baseIsFhir = baseTypeName ? isFhirType(baseTypeName) : false;
989
+ // Use helper to determine base type - extension fields get 'Extension' as base type
990
+ const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
988
991
  if (profileUrl || baseTypeName) {
989
992
  addTypeImport(sanitizeIdentifier(resolvedType), profileUrl, baseTypeName, baseIsFhir);
990
993
  }