@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.2fee5d9.20250808110507

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +195 -77
  3. package/dist/index.js +2541 -981
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/analyzer.ts +536 -64
  7. package/src/boxing.ts +124 -0
  8. package/src/errors.ts +246 -0
  9. package/src/index.ts +57 -9
  10. package/src/inspect.ts +307 -174
  11. package/src/interpreter.ts +299 -48
  12. package/src/model-provider.ts +110 -8
  13. package/src/operations/abs-function.ts +19 -10
  14. package/src/operations/aggregate-function.ts +11 -3
  15. package/src/operations/all-function.ts +18 -8
  16. package/src/operations/allFalse-function.ts +9 -6
  17. package/src/operations/allTrue-function.ts +9 -6
  18. package/src/operations/and-operator.ts +20 -11
  19. package/src/operations/anyFalse-function.ts +6 -4
  20. package/src/operations/anyTrue-function.ts +6 -4
  21. package/src/operations/as-operator.ts +1 -0
  22. package/src/operations/ceiling-function.ts +9 -5
  23. package/src/operations/children-function.ts +94 -0
  24. package/src/operations/combine-function.ts +4 -2
  25. package/src/operations/combine-operator.ts +18 -4
  26. package/src/operations/contains-function.ts +20 -8
  27. package/src/operations/contains-operator.ts +14 -6
  28. package/src/operations/count-function.ts +2 -1
  29. package/src/operations/defineVariable-function.ts +5 -3
  30. package/src/operations/descendants-function.ts +62 -0
  31. package/src/operations/distinct-function.ts +16 -4
  32. package/src/operations/div-operator.ts +15 -2
  33. package/src/operations/divide-operator.ts +10 -5
  34. package/src/operations/dot-operator.ts +3 -1
  35. package/src/operations/empty-function.ts +2 -1
  36. package/src/operations/endsWith-function.ts +22 -10
  37. package/src/operations/equal-operator.ts +18 -6
  38. package/src/operations/equivalent-operator.ts +15 -10
  39. package/src/operations/exclude-function.ts +12 -10
  40. package/src/operations/exists-function.ts +14 -7
  41. package/src/operations/first-function.ts +7 -4
  42. package/src/operations/floor-function.ts +9 -5
  43. package/src/operations/greater-operator.ts +9 -4
  44. package/src/operations/greater-or-equal-operator.ts +9 -4
  45. package/src/operations/iif-function.ts +18 -16
  46. package/src/operations/implies-operator.ts +22 -7
  47. package/src/operations/in-operator.ts +17 -7
  48. package/src/operations/index.ts +3 -0
  49. package/src/operations/indexOf-function.ts +22 -10
  50. package/src/operations/intersect-function.ts +17 -20
  51. package/src/operations/is-operator.ts +63 -14
  52. package/src/operations/isDistinct-function.ts +12 -6
  53. package/src/operations/join-function.ts +15 -4
  54. package/src/operations/last-function.ts +7 -4
  55. package/src/operations/length-function.ts +12 -5
  56. package/src/operations/less-operator.ts +9 -4
  57. package/src/operations/less-or-equal-operator.ts +9 -4
  58. package/src/operations/less-than.ts +25 -1
  59. package/src/operations/lower-function.ts +12 -5
  60. package/src/operations/minus-operator.ts +9 -4
  61. package/src/operations/mod-operator.ts +19 -2
  62. package/src/operations/multiply-operator.ts +11 -6
  63. package/src/operations/not-equal-operator.ts +15 -6
  64. package/src/operations/not-equivalent-operator.ts +15 -10
  65. package/src/operations/not-function.ts +12 -4
  66. package/src/operations/ofType-function.ts +135 -0
  67. package/src/operations/or-operator.ts +20 -11
  68. package/src/operations/plus-operator.ts +18 -6
  69. package/src/operations/power-function.ts +21 -9
  70. package/src/operations/replace-function.ts +26 -9
  71. package/src/operations/round-function.ts +23 -8
  72. package/src/operations/select-function.ts +12 -6
  73. package/src/operations/single-function.ts +5 -3
  74. package/src/operations/skip-function.ts +12 -5
  75. package/src/operations/split-function.ts +24 -9
  76. package/src/operations/sqrt-function.ts +9 -5
  77. package/src/operations/startsWith-function.ts +20 -8
  78. package/src/operations/subsetOf-function.ts +14 -11
  79. package/src/operations/substring-function.ts +36 -19
  80. package/src/operations/supersetOf-function.ts +14 -11
  81. package/src/operations/tail-function.ts +3 -1
  82. package/src/operations/take-function.ts +12 -5
  83. package/src/operations/toBoolean-function.ts +18 -11
  84. package/src/operations/toDecimal-function.ts +13 -6
  85. package/src/operations/toInteger-function.ts +13 -6
  86. package/src/operations/toString-function.ts +17 -10
  87. package/src/operations/trace-function.ts +12 -5
  88. package/src/operations/trim-function.ts +11 -4
  89. package/src/operations/truncate-function.ts +9 -5
  90. package/src/operations/unary-minus-operator.ts +22 -12
  91. package/src/operations/unary-plus-operator.ts +1 -0
  92. package/src/operations/union-function.ts +19 -24
  93. package/src/operations/union-operator.ts +1 -0
  94. package/src/operations/upper-function.ts +12 -5
  95. package/src/operations/where-function.ts +15 -8
  96. package/src/operations/xor-operator.ts +8 -3
  97. package/src/parser.ts +391 -8
  98. package/src/quantity-value.ts +4 -8
  99. package/src/registry.ts +3 -3
  100. package/src/types.ts +10 -6
  101. package/src/parser-base.ts +0 -400
@@ -18,6 +18,8 @@ import { Registry } from './registry';
18
18
  import * as operations from './operations';
19
19
  import type { EvaluationResult, FunctionEvaluator, NodeEvaluator, OperationEvaluator, RuntimeContext } from './types';
20
20
  import { createQuantity } from './quantity-value';
21
+ import { box, unbox, ensureBoxed, type FHIRPathValue } from './boxing';
22
+ import { Errors } from './errors';
21
23
 
22
24
  /**
23
25
  * Runtime context manager that provides efficient prototype-based context operations
@@ -166,9 +168,11 @@ export class Interpreter {
166
168
  private nodeEvaluators: Record<NodeType, NodeEvaluator>;
167
169
  private operationEvaluators: Map<string, OperationEvaluator>;
168
170
  private functionEvaluators: Map<string, FunctionEvaluator>;
171
+ private modelProvider?: import('./types').ModelProvider<any>;
169
172
 
170
- constructor(registry?: Registry) {
173
+ constructor(registry?: Registry, modelProvider?: import('./types').ModelProvider<any>) {
171
174
  this.registry = registry || new Registry();
175
+ this.modelProvider = modelProvider;
172
176
  this.operationEvaluators = new Map();
173
177
  this.functionEvaluators = new Map();
174
178
 
@@ -225,45 +229,171 @@ export class Interpreter {
225
229
  input = input === null || input === undefined ? [] : [input];
226
230
  }
227
231
 
232
+ // Box the initial input values
233
+ const boxedInput = input.map(value => ensureBoxed(value));
234
+
235
+ // Set current node in context
236
+ const contextWithNode = RuntimeContextManager.copy(context);
237
+ contextWithNode.currentNode = node;
238
+
228
239
  // Dispatch to appropriate evaluator
229
240
  const evaluator = this.nodeEvaluators[node.type];
230
241
  if (!evaluator) {
231
- throw new Error(`Unknown node type: ${node.type}`);
242
+ throw Errors.unknownNodeType(node.type);
232
243
  }
233
244
 
234
- return evaluator(node, input, context);
245
+ return evaluator(node, boxedInput, contextWithNode);
235
246
  }
236
247
 
237
248
  private createInitialContext(input: any[]): RuntimeContext {
238
249
  const context = RuntimeContextManager.create(input);
239
250
  // Set $this to initial input
240
251
  context.variables['$this'] = input;
252
+ // Add model provider if available
253
+ if (this.modelProvider) {
254
+ context.modelProvider = this.modelProvider;
255
+ }
241
256
  return context;
242
257
  }
243
258
 
244
259
  // Literal node evaluator
245
- private evaluateLiteral(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
260
+ private evaluateLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
246
261
  const literal = node as LiteralNode;
262
+
263
+ // Box the literal value with appropriate type info
264
+ let typeInfo: import('./types').TypeInfo | undefined;
265
+ const value = literal.value;
266
+
267
+ if (typeof value === 'string') {
268
+ typeInfo = { type: 'String', singleton: true };
269
+ } else if (typeof value === 'number') {
270
+ typeInfo = Number.isInteger(value) ?
271
+ { type: 'Integer', singleton: true } :
272
+ { type: 'Decimal', singleton: true };
273
+ } else if (typeof value === 'boolean') {
274
+ typeInfo = { type: 'Boolean', singleton: true };
275
+ }
276
+
247
277
  return {
248
- value: [literal.value],
278
+ value: [box(literal.value, typeInfo)],
249
279
  context
250
280
  };
251
281
  }
252
282
 
253
283
  // Identifier node evaluator
254
- private evaluateIdentifier(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
284
+ private evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
255
285
  const identifier = node as IdentifierNode;
256
286
  const name = identifier.name;
257
287
 
258
- // Navigate property on each item in input
259
- const results: any[] = [];
260
- for (const item of input) {
261
- if (item && typeof item === 'object' && name in item) {
262
- const value = item[name];
263
- if (Array.isArray(value)) {
264
- results.push(...value);
265
- } else if (value !== null && value !== undefined) {
266
- results.push(value);
288
+ // Navigate property on each boxed item in input
289
+ const results: FHIRPathValue[] = [];
290
+
291
+ // Get the type info from the node (set by analyzer)
292
+ const nodeTypeInfo = node.typeInfo;
293
+
294
+ for (const boxedItem of input) {
295
+ const item = unbox(boxedItem);
296
+
297
+ // Special handling for primitive extension navigation
298
+ if (name === 'extension' && boxedItem.primitiveElement?.extension) {
299
+ // Navigation from a primitive value to its extensions
300
+ for (const ext of boxedItem.primitiveElement.extension) {
301
+ results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
302
+ }
303
+ continue;
304
+ }
305
+
306
+ if (item && typeof item === 'object') {
307
+ // Check if this is a choice type navigation
308
+ if (nodeTypeInfo?.modelContext && typeof nodeTypeInfo.modelContext === 'object' &&
309
+ 'isUnion' in nodeTypeInfo.modelContext &&
310
+ nodeTypeInfo.modelContext.isUnion && 'choices' in nodeTypeInfo.modelContext &&
311
+ Array.isArray(nodeTypeInfo.modelContext.choices)) {
312
+ // For choice types, look for any of the choice properties
313
+ for (const choice of nodeTypeInfo.modelContext.choices) {
314
+ const choiceName = choice.choiceName;
315
+ if (choiceName && choiceName in item) {
316
+ const value = item[choiceName];
317
+ const primitiveElementName = `_${choiceName}`;
318
+ const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
319
+
320
+ // Box with the specific choice type
321
+ const choiceTypeInfo = {
322
+ type: choice.type,
323
+ singleton: !Array.isArray(value),
324
+ modelContext: choice
325
+ };
326
+
327
+ if (Array.isArray(value)) {
328
+ for (const v of value) {
329
+ results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
330
+ }
331
+ } else if (value !== null && value !== undefined) {
332
+ results.push(box(value, choiceTypeInfo, primitiveElement));
333
+ }
334
+ }
335
+ }
336
+ } else if (name in item) {
337
+ // Regular property navigation
338
+ const value = item[name];
339
+ const primitiveElementName = `_${name}`;
340
+ const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
341
+
342
+ if (Array.isArray(value)) {
343
+ // Box each array element with type info
344
+ // For arrays, make the type singleton since each element is a single value
345
+ const elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
346
+ for (const v of value) {
347
+ // Special handling for FHIR resources - use their resourceType
348
+ // Do this if the property navigation has type 'Any' (polymorphic reference) or no type info
349
+ if (v && typeof v === 'object' && 'resourceType' in v && typeof v.resourceType === 'string') {
350
+ // Get full type info from model provider if available
351
+ let resourceTypeInfo;
352
+ if (context.modelProvider) {
353
+ resourceTypeInfo = context.modelProvider.getType(v.resourceType);
354
+ if (resourceTypeInfo) {
355
+ // Make it singleton since it's a single element in the array
356
+ resourceTypeInfo = { ...resourceTypeInfo, singleton: true };
357
+ }
358
+ }
359
+ if (!resourceTypeInfo) {
360
+ // Fallback to basic type info
361
+ resourceTypeInfo = {
362
+ type: v.resourceType as import('./types').TypeName,
363
+ singleton: true
364
+ };
365
+ }
366
+ results.push(box(v, resourceTypeInfo, primitiveElement));
367
+ } else {
368
+ results.push(box(v, elementTypeInfo, primitiveElement));
369
+ }
370
+ }
371
+ } else if (value !== null && value !== undefined) {
372
+ // Special handling for FHIR resources - use their resourceType
373
+ // Do this if the property navigation has type 'Any' (polymorphic reference) or no type info
374
+ if (value && typeof value === 'object' && 'resourceType' in value && typeof value.resourceType === 'string') {
375
+ // Get full type info from model provider if available
376
+ let resourceTypeInfo;
377
+ if (context.modelProvider) {
378
+ resourceTypeInfo = context.modelProvider.getType(value.resourceType);
379
+ if (resourceTypeInfo) {
380
+ // Preserve singleton status
381
+ resourceTypeInfo = { ...resourceTypeInfo, singleton: !Array.isArray(value) };
382
+ }
383
+ }
384
+ if (!resourceTypeInfo) {
385
+ // Fallback to basic type info
386
+ resourceTypeInfo = {
387
+ type: value.resourceType as import('./types').TypeName,
388
+ singleton: !Array.isArray(value)
389
+ };
390
+ }
391
+ results.push(box(value, resourceTypeInfo, primitiveElement));
392
+ } else {
393
+ // Box single value with primitive element if available
394
+ results.push(box(value, nodeTypeInfo, primitiveElement));
395
+ }
396
+ }
267
397
  }
268
398
  }
269
399
  }
@@ -275,14 +405,15 @@ export class Interpreter {
275
405
  }
276
406
 
277
407
  // TypeOrIdentifier node evaluator (handles Patient, Observation, etc.)
278
- private evaluateTypeOrIdentifier(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
408
+ private evaluateTypeOrIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
279
409
  const typeOrId = node as TypeOrIdentifierNode;
280
410
  const name = typeOrId.name;
281
411
 
282
412
  // First try as type filter
283
- const filtered = input.filter(item =>
284
- item && typeof item === 'object' && item.resourceType === name
285
- );
413
+ const filtered = input.filter(boxedItem => {
414
+ const item = unbox(boxedItem);
415
+ return item && typeof item === 'object' && item.resourceType === name;
416
+ });
286
417
 
287
418
  if (filtered.length > 0) {
288
419
  return { value: filtered, context };
@@ -293,7 +424,7 @@ export class Interpreter {
293
424
  }
294
425
 
295
426
  // Binary operator evaluator
296
- private evaluateBinary(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
427
+ private evaluateBinary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
297
428
  const binary = node as BinaryNode;
298
429
  const operator = binary.operator;
299
430
 
@@ -335,11 +466,11 @@ export class Interpreter {
335
466
  }
336
467
 
337
468
  // If no evaluator found, throw error
338
- throw new Error(`No evaluator found for binary operator: ${operator}`);
469
+ throw Errors.noEvaluatorFound('binary operator', operator);
339
470
  }
340
471
 
341
472
  // Unary operator evaluator
342
- private evaluateUnary(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
473
+ private evaluateUnary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
343
474
  const unary = node as UnaryNode;
344
475
  const operator = unary.operator;
345
476
 
@@ -358,11 +489,11 @@ export class Interpreter {
358
489
  }
359
490
 
360
491
  // If no evaluator found, throw error
361
- throw new Error(`No evaluator found for unary operator: ${operator}`);
492
+ throw Errors.noEvaluatorFound('unary operator', operator);
362
493
  }
363
494
 
364
495
  // Variable evaluator
365
- private evaluateVariable(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
496
+ private evaluateVariable(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
366
497
  const variable = node as VariableNode;
367
498
  const name = variable.name;
368
499
 
@@ -371,17 +502,19 @@ export class Interpreter {
371
502
  if (value !== undefined) {
372
503
  // Ensure value is always an array
373
504
  const arrayValue = Array.isArray(value) ? value : [value];
374
- return { value: arrayValue, context };
505
+ // Box each value in the array
506
+ const boxedValues = arrayValue.map(v => ensureBoxed(v));
507
+ return { value: boxedValues, context };
375
508
  }
376
509
 
377
510
  // According to FHIRPath spec: attempting to access an undefined environment variable will result in an error
378
- throw new Error(`Variable '${name}' is not defined in the current scope`);
511
+ throw Errors.variableNotDefined(name);
379
512
  }
380
513
 
381
514
  // Collection evaluator
382
- private evaluateCollection(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
515
+ private evaluateCollection(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
383
516
  const collection = node as CollectionNode;
384
- const results: any[] = [];
517
+ const results: FHIRPathValue[] = [];
385
518
 
386
519
  for (const element of collection.elements) {
387
520
  const result = this.evaluate(element, input, context);
@@ -403,11 +536,11 @@ export class Interpreter {
403
536
  }
404
537
 
405
538
  // No function found in registry
406
- throw new Error(`Unknown function: ${funcName}`);
539
+ throw Errors.unknownFunction(funcName);
407
540
  }
408
541
 
409
542
  // Index evaluator
410
- private evaluateIndex(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
543
+ private evaluateIndex(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
411
544
  const indexNode = node as IndexNode;
412
545
  const exprResult = this.evaluate(indexNode.expression, input, context);
413
546
  const indexResult = this.evaluate(indexNode.index, input, context);
@@ -416,48 +549,166 @@ export class Interpreter {
416
549
  return { value: [], context };
417
550
  }
418
551
 
419
- const index = indexResult.value[0];
420
- if (typeof index === 'number' && index >= 0 && index < exprResult.value.length) {
421
- return { value: [exprResult.value[index]], context };
552
+ const boxedIndex = indexResult.value[0];
553
+ if (boxedIndex) {
554
+ const index = unbox(boxedIndex);
555
+ if (typeof index === 'number' && index >= 0 && index < exprResult.value.length) {
556
+ const result = exprResult.value[index];
557
+ return { value: result ? [result] : [], context };
558
+ }
422
559
  }
423
560
 
424
561
  return { value: [], context };
425
562
  }
426
563
 
427
564
  // Type membership test (is operator)
428
- private evaluateMembershipTest(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
565
+ private evaluateMembershipTest(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
429
566
  const test = node as MembershipTestNode;
430
567
  const exprResult = this.evaluate(test.expression, input, context);
431
568
 
432
- // Simple type checking
433
- const results = exprResult.value.map(item => {
434
- switch (test.targetType) {
435
- case 'String': return typeof item === 'string';
436
- case 'Boolean': return typeof item === 'boolean';
437
- case 'Integer': return Number.isInteger(item);
438
- case 'Decimal': return typeof item === 'number';
439
- default: return false;
569
+ // If expression evaluates to empty, return empty
570
+ if (exprResult.value.length === 0) {
571
+ return { value: [], context };
572
+ }
573
+
574
+ // If we have type information from analyzer (with ModelProvider), use it
575
+ if (context.currentNode?.typeInfo?.modelContext) {
576
+ const modelContext = context.currentNode.typeInfo.modelContext as any;
577
+
578
+ // For union types, check if the target type is valid
579
+ if (modelContext.isUnion && modelContext.choices) {
580
+ const hasValidChoice = modelContext.choices.some((c: any) =>
581
+ c.type === test.targetType || c.elementType === test.targetType
582
+ );
583
+
584
+ if (!hasValidChoice) {
585
+ // Type system knows this will always be false
586
+ return {
587
+ value: exprResult.value.map(() => box(false, { type: 'Boolean', singleton: true })),
588
+ context
589
+ };
590
+ }
440
591
  }
592
+ }
593
+
594
+ // Type checking with subtype support via ModelProvider
595
+ const results = exprResult.value.map(boxedItem => {
596
+ const item = unbox(boxedItem);
597
+
598
+ // If we have a ModelProvider and typeInfo, use it for accurate subtype checking
599
+ if (context.modelProvider && boxedItem.typeInfo) {
600
+ const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, test.targetType as import('./types').TypeName);
601
+ return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
602
+ }
603
+
604
+ // For FHIR resources without typeInfo, try to get it from modelProvider
605
+ if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
606
+ const typeInfo = context.modelProvider.getType(item.resourceType);
607
+ if (typeInfo) {
608
+ const matchingType = context.modelProvider.ofType(typeInfo, test.targetType as import('./types').TypeName);
609
+ return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
610
+ }
611
+ // Fall back to exact match
612
+ return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
613
+ }
614
+
615
+ // Check for FHIR resource types (no ModelProvider available)
616
+ if (item && typeof item === 'object' && 'resourceType' in item) {
617
+ return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
618
+ }
619
+
620
+ // Check primitive types
621
+ const isMatch = (() => {
622
+ switch (test.targetType) {
623
+ case 'String': return typeof item === 'string';
624
+ case 'Boolean': return typeof item === 'boolean';
625
+ case 'Integer': return Number.isInteger(item);
626
+ case 'Decimal': return typeof item === 'number';
627
+ case 'Date':
628
+ case 'DateTime':
629
+ case 'Time':
630
+ // Simple check for date-like strings
631
+ return typeof item === 'string' && !isNaN(Date.parse(item));
632
+ default: return false;
633
+ }
634
+ })();
635
+ return box(isMatch, { type: 'Boolean', singleton: true });
441
636
  });
442
637
 
443
638
  return { value: results, context };
444
639
  }
445
640
 
446
641
  // Type cast (as operator)
447
- private evaluateTypeCast(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
642
+ private evaluateTypeCast(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
448
643
  const cast = node as TypeCastNode;
449
644
  const exprResult = this.evaluate(cast.expression, input, context);
450
645
 
451
- // For now, just return the values as-is
452
- // In a real implementation, would perform type conversion
453
- return { value: exprResult.value, context };
646
+ // If we have type information from analyzer (with ModelProvider), use it
647
+ if (context.currentNode?.typeInfo?.modelContext) {
648
+ const modelContext = context.currentNode.typeInfo.modelContext as any;
649
+
650
+ // For union types, check if the cast is valid
651
+ if (modelContext.isUnion && modelContext.choices) {
652
+ const validChoice = modelContext.choices.find((c: any) =>
653
+ c.type === cast.targetType || c.elementType === cast.targetType
654
+ );
655
+
656
+ if (!validChoice) {
657
+ // Invalid cast - return empty
658
+ return { value: [], context };
659
+ }
660
+ }
661
+ }
662
+
663
+ // Filter values that match the target type with subtype support
664
+ const filtered = exprResult.value.filter(boxedItem => {
665
+ const item = unbox(boxedItem);
666
+
667
+ // If we have a ModelProvider and typeInfo, use it for accurate subtype checking
668
+ if (context.modelProvider && boxedItem.typeInfo) {
669
+ const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, cast.targetType as import('./types').TypeName);
670
+ return matchingType !== undefined;
671
+ }
672
+
673
+ // For FHIR resources without typeInfo, try to get it from modelProvider
674
+ if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
675
+ const typeInfo = context.modelProvider.getType(item.resourceType);
676
+ if (typeInfo) {
677
+ const matchingType = context.modelProvider.ofType(typeInfo, cast.targetType as import('./types').TypeName);
678
+ return matchingType !== undefined;
679
+ }
680
+ // Fall back to exact match
681
+ return item.resourceType === cast.targetType;
682
+ }
683
+
684
+ // Check for FHIR resource types (no ModelProvider available)
685
+ if (item && typeof item === 'object' && 'resourceType' in item) {
686
+ return item.resourceType === cast.targetType;
687
+ }
688
+
689
+ // Check primitive types
690
+ switch (cast.targetType) {
691
+ case 'String': return typeof item === 'string';
692
+ case 'Boolean': return typeof item === 'boolean';
693
+ case 'Integer': return Number.isInteger(item);
694
+ case 'Decimal': return typeof item === 'number';
695
+ case 'Date':
696
+ case 'DateTime':
697
+ case 'Time':
698
+ // Simple check for date-like strings
699
+ return typeof item === 'string' && !isNaN(Date.parse(item));
700
+ default: return false;
701
+ }
702
+ });
703
+
704
+ return { value: filtered, context };
454
705
  }
455
706
 
456
- private evaluateQuantity(node: ASTNode, input: any[], context: RuntimeContext): EvaluationResult {
707
+ private evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
457
708
  const quantity = node as QuantityNode;
458
709
  const quantityValue = createQuantity(quantity.value, quantity.unit, quantity.isCalendarUnit);
459
710
  return {
460
- value: [quantityValue],
711
+ value: [box(quantityValue, { type: 'Quantity', singleton: true })],
461
712
  context
462
713
  };
463
714
  }
@@ -76,14 +76,54 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
76
76
  'Count': 'Quantity'
77
77
  };
78
78
 
79
- // Common FHIR types to preload
79
+ // Common FHIR types to preload - load all resource types and common data types
80
80
  private readonly commonTypes = [
81
- 'Patient', 'HumanName', 'Observation', 'CodeableConcept', 'Coding',
82
- 'Extension', 'Reference', 'Identifier', 'Period', 'ContactPoint',
83
- 'Address', 'Attachment', 'Meta', 'Narrative'
81
+ // Base resources
82
+ 'Resource', 'DomainResource',
83
+ // All FHIR R4 resources (alphabetical)
84
+ 'Account', 'ActivityDefinition', 'AdverseEvent', 'AllergyIntolerance', 'Appointment',
85
+ 'AppointmentResponse', 'AuditEvent', 'Basic', 'Binary', 'BiologicallyDerivedProduct',
86
+ 'BodyStructure', 'Bundle', 'CapabilityStatement', 'CarePlan', 'CareTeam',
87
+ 'CatalogEntry', 'ChargeItem', 'ChargeItemDefinition', 'Claim', 'ClaimResponse',
88
+ 'ClinicalImpression', 'CodeSystem', 'Communication', 'CommunicationRequest', 'CompartmentDefinition',
89
+ 'Composition', 'ConceptMap', 'Condition', 'Consent', 'Contract',
90
+ 'Coverage', 'CoverageEligibilityRequest', 'CoverageEligibilityResponse', 'DetectedIssue', 'Device',
91
+ 'DeviceDefinition', 'DeviceMetric', 'DeviceRequest', 'DeviceUseStatement', 'DiagnosticReport',
92
+ 'DocumentManifest', 'DocumentReference', 'EffectEvidenceSynthesis', 'Encounter', 'Endpoint',
93
+ 'EnrollmentRequest', 'EnrollmentResponse', 'EpisodeOfCare', 'EventDefinition', 'Evidence',
94
+ 'EvidenceVariable', 'ExampleScenario', 'ExplanationOfBenefit', 'FamilyMemberHistory', 'Flag',
95
+ 'Goal', 'GraphDefinition', 'Group', 'GuidanceResponse', 'HealthcareService',
96
+ 'ImagingStudy', 'Immunization', 'ImmunizationEvaluation', 'ImmunizationRecommendation', 'ImplementationGuide',
97
+ 'InsurancePlan', 'Invoice', 'Library', 'Linkage', 'List',
98
+ 'Location', 'Measure', 'MeasureReport', 'Media', 'Medication',
99
+ 'MedicationAdministration', 'MedicationDispense', 'MedicationKnowledge', 'MedicationRequest', 'MedicationStatement',
100
+ 'MedicinalProduct', 'MedicinalProductAuthorization', 'MedicinalProductContraindication', 'MedicinalProductIndication', 'MedicinalProductIngredient',
101
+ 'MedicinalProductInteraction', 'MedicinalProductManufactured', 'MedicinalProductPackaged', 'MedicinalProductPharmaceutical', 'MedicinalProductUndesirableEffect',
102
+ 'MessageDefinition', 'MessageHeader', 'MolecularSequence', 'NamingSystem', 'NutritionOrder',
103
+ 'Observation', 'ObservationDefinition', 'OperationDefinition', 'OperationOutcome', 'Organization',
104
+ 'OrganizationAffiliation', 'Parameters', 'Patient', 'PaymentNotice', 'PaymentReconciliation',
105
+ 'Person', 'PlanDefinition', 'Practitioner', 'PractitionerRole', 'Procedure',
106
+ 'Provenance', 'Questionnaire', 'QuestionnaireResponse', 'RelatedPerson', 'RequestGroup',
107
+ 'ResearchDefinition', 'ResearchElementDefinition', 'ResearchStudy', 'ResearchSubject', 'RiskAssessment',
108
+ 'RiskEvidenceSynthesis', 'Schedule', 'SearchParameter', 'ServiceRequest', 'Slot',
109
+ 'Specimen', 'SpecimenDefinition', 'StructureDefinition', 'StructureMap', 'Subscription',
110
+ 'Substance', 'SubstanceNucleicAcid', 'SubstancePolymer', 'SubstanceProtein', 'SubstanceReferenceInformation',
111
+ 'SubstanceSourceMaterial', 'SubstanceSpecification', 'SupplyDelivery', 'SupplyRequest', 'Task',
112
+ 'TerminologyCapabilities', 'TestReport', 'TestScript', 'ValueSet', 'VerificationResult',
113
+ 'VisionPrescription',
114
+ // Common data types
115
+ 'HumanName', 'CodeableConcept', 'Coding', 'Extension', 'Reference',
116
+ 'Identifier', 'Period', 'ContactPoint', 'Address', 'Attachment',
117
+ 'Meta', 'Narrative', 'Quantity', 'SimpleQuantity', 'Age', 'Distance',
118
+ 'Duration', 'Count', 'Money', 'Range', 'Ratio', 'SampledData',
119
+ 'Timing', 'Annotation', 'Signature', 'ContactDetail', 'Contributor',
120
+ 'DataRequirement', 'ParameterDefinition', 'RelatedArtifact', 'TriggerDefinition',
121
+ 'UsageContext', 'Dosage', 'ElementDefinition'
84
122
  ];
85
123
 
86
- constructor(private config: FHIRModelProviderConfig) {
124
+ constructor(private config: FHIRModelProviderConfig = {
125
+ packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
126
+ }) {
87
127
  const canonicalConfig: Config = {
88
128
  packages: config.packages.map(p => `${p.name}@${p.version}`),
89
129
  workingDir: config.cacheDir || './tmp/.fhir-cache'
@@ -203,7 +243,12 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
203
243
  }
204
244
 
205
245
  private mapToFHIRPathType(fhirType: string): TypeName {
206
- return this.typeMapping[fhirType] || 'Any';
246
+ // If it's a mapped type (primitive or special types), use the mapping
247
+ if (this.typeMapping[fhirType]) {
248
+ return this.typeMapping[fhirType];
249
+ }
250
+ // Otherwise, keep the FHIR type name (for complex types like CodeableConcept)
251
+ return fhirType as TypeName;
207
252
  }
208
253
 
209
254
  private isChoiceType(element: any): boolean {
@@ -373,8 +418,27 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
373
418
  return undefined;
374
419
  }
375
420
 
376
- // For non-union types, check direct match
377
- return type.type === typeName ? type : undefined;
421
+ // For non-union types, check if the type matches or is a subtype
422
+ // First check direct match on FHIRPath type
423
+ if (type.type === typeName) {
424
+ return type;
425
+ }
426
+
427
+ // Check if the type name matches
428
+ if (type.name === typeName) {
429
+ return type;
430
+ }
431
+
432
+ // Check if any of the schemas in the hierarchy match
433
+ if (context?.schemaHierarchy) {
434
+ for (const schema of context.schemaHierarchy) {
435
+ if (schema.type === typeName || schema.name === typeName) {
436
+ return type;
437
+ }
438
+ }
439
+ }
440
+
441
+ return undefined;
378
442
  }
379
443
 
380
444
  getElementNames(parentType: TypeInfo<FHIRModelContext>): string[] {
@@ -398,6 +462,44 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
398
462
 
399
463
  return Array.from(names);
400
464
  }
465
+
466
+ getChildrenType(parentType: TypeInfo<FHIRModelContext>): TypeInfo<FHIRModelContext> | undefined {
467
+ const elementNames = this.getElementNames(parentType);
468
+ if (elementNames.length === 0) return undefined;
469
+
470
+ // Collect all unique child types
471
+ const childTypes = new Map<string, TypeInfo<FHIRModelContext>>();
472
+
473
+ for (const elementName of elementNames) {
474
+ const elementType = this.getElementType(parentType, elementName);
475
+ if (elementType) {
476
+ // Use a combination of namespace and name as key to deduplicate
477
+ const key = `${elementType.namespace || ''}.${elementType.name || elementType.type}`;
478
+ childTypes.set(key, elementType);
479
+ }
480
+ }
481
+
482
+ if (childTypes.size === 0) return undefined;
483
+
484
+ // Create a union type representing all possible children
485
+ return {
486
+ type: 'Any',
487
+ namespace: parentType.namespace,
488
+ name: 'ChildrenUnion',
489
+ singleton: false, // children() always returns a collection
490
+ modelContext: {
491
+ path: `${parentType.modelContext?.path || ''}.children()`,
492
+ schemaHierarchy: [],
493
+ isUnion: true,
494
+ choices: Array.from(childTypes.values()).map(type => ({
495
+ type: type.type as TypeName,
496
+ code: type.name || type.type,
497
+ namespace: type.namespace,
498
+ modelContext: type.modelContext
499
+ }))
500
+ } as FHIRModelContext
501
+ };
502
+ }
401
503
 
402
504
  // Async helper methods for loading additional schemas
403
505
  async loadType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined> {