@atomic-ehr/fhirpath 0.0.3 → 0.0.4

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 (67) hide show
  1. package/dist/index.browser.d.ts +22 -0
  2. package/dist/index.browser.js +15758 -0
  3. package/dist/index.browser.js.map +1 -0
  4. package/dist/index.node.d.ts +24 -0
  5. package/dist/{index.js → index.node.js} +5450 -3809
  6. package/dist/index.node.js.map +1 -0
  7. package/dist/{index.d.ts → model-provider.common-oir-zg7r.d.ts} +81 -74
  8. package/package.json +10 -5
  9. package/src/analyzer.ts +46 -9
  10. package/src/complex-types/quantity-value.ts +131 -9
  11. package/src/complex-types/temporal.ts +45 -6
  12. package/src/errors.ts +4 -0
  13. package/src/index.browser.ts +4 -0
  14. package/src/{index.ts → index.common.ts} +18 -14
  15. package/src/index.node.ts +4 -0
  16. package/src/interpreter/navigator.ts +12 -0
  17. package/src/interpreter/runtime-context.ts +60 -25
  18. package/src/interpreter.ts +118 -33
  19. package/src/lexer.ts +4 -1
  20. package/src/model-provider.browser.ts +35 -0
  21. package/src/{model-provider.ts → model-provider.common.ts} +29 -26
  22. package/src/model-provider.node.ts +41 -0
  23. package/src/operations/allTrue-function.ts +6 -10
  24. package/src/operations/and-operator.ts +2 -2
  25. package/src/operations/as-function.ts +41 -0
  26. package/src/operations/combine-operator.ts +17 -4
  27. package/src/operations/comparison.ts +73 -21
  28. package/src/operations/convertsToQuantity-function.ts +56 -7
  29. package/src/operations/decode-function.ts +114 -0
  30. package/src/operations/divide-operator.ts +3 -3
  31. package/src/operations/encode-function.ts +110 -0
  32. package/src/operations/escape-function.ts +114 -0
  33. package/src/operations/exp-function.ts +65 -0
  34. package/src/operations/extension-function.ts +88 -0
  35. package/src/operations/greater-operator.ts +5 -24
  36. package/src/operations/greater-or-equal-operator.ts +5 -24
  37. package/src/operations/hasValue-function.ts +84 -0
  38. package/src/operations/iif-function.ts +7 -1
  39. package/src/operations/implies-operator.ts +1 -0
  40. package/src/operations/index.ts +11 -0
  41. package/src/operations/is-function.ts +11 -0
  42. package/src/operations/is-operator.ts +187 -5
  43. package/src/operations/less-operator.ts +6 -24
  44. package/src/operations/less-or-equal-operator.ts +5 -24
  45. package/src/operations/less-than.ts +7 -12
  46. package/src/operations/ln-function.ts +62 -0
  47. package/src/operations/log-function.ts +113 -0
  48. package/src/operations/lowBoundary-function.ts +14 -0
  49. package/src/operations/minus-operator.ts +8 -1
  50. package/src/operations/mod-operator.ts +7 -1
  51. package/src/operations/not-function.ts +9 -2
  52. package/src/operations/ofType-function.ts +35 -0
  53. package/src/operations/plus-operator.ts +46 -3
  54. package/src/operations/precision-function.ts +146 -0
  55. package/src/operations/replace-function.ts +19 -19
  56. package/src/operations/replaceMatches-function.ts +5 -0
  57. package/src/operations/sort-function.ts +209 -0
  58. package/src/operations/take-function.ts +1 -1
  59. package/src/operations/toQuantity-function.ts +0 -1
  60. package/src/operations/toString-function.ts +76 -12
  61. package/src/operations/trace-function.ts +20 -3
  62. package/src/operations/unescape-function.ts +119 -0
  63. package/src/operations/where-function.ts +3 -1
  64. package/src/parser.ts +14 -2
  65. package/src/types.ts +7 -5
  66. package/src/utils/decimal.ts +76 -0
  67. package/dist/index.js.map +0 -1
@@ -190,13 +190,13 @@ declare enum PRECEDENCE {
190
190
  XOR = 30,
191
191
  AND = 40,
192
192
  IN_CONTAINS = 50,
193
+ AS_IS = 55,// as, is (moved to lower precedence)
193
194
  EQUALITY = 60,// =, !=, ~, !~
195
+ PIPE = 65,// | (moved to lower precedence)
194
196
  COMPARISON = 70,// <, >, <=, >=
195
- PIPE = 80,// |
196
197
  ADDITIVE = 90,// +, -
197
198
  MULTIPLICATIVE = 100,// *, /, div, mod
198
199
  UNARY = 110,// unary +, -, not
199
- AS_IS = 120,// as, is
200
200
  POSTFIX = 130,// []
201
201
  DOT = 140
202
202
  }
@@ -253,6 +253,7 @@ interface FunctionSignature {
253
253
  typeReference?: boolean;
254
254
  }>;
255
255
  result: TypeInfo | 'inputType' | 'inputTypeSingleton' | 'parameterType';
256
+ variadic?: boolean;
256
257
  }
257
258
  interface FunctionDefinition {
258
259
  name: string;
@@ -330,7 +331,7 @@ interface IdentifierNode extends BaseASTNode {
330
331
  interface LiteralNode extends BaseASTNode {
331
332
  type: NodeType.Literal;
332
333
  value: any;
333
- valueType: 'string' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
334
+ valueType: 'string' | 'number' | 'decimal' | 'boolean' | 'date' | 'time' | 'datetime' | 'null';
334
335
  }
335
336
  interface TemporalLiteralNode extends BaseASTNode {
336
337
  type: NodeType.TemporalLiteral;
@@ -396,8 +397,8 @@ interface RuntimeContext {
396
397
  currentNode?: ASTNode;
397
398
  modelProvider?: ModelProvider;
398
399
  }
399
- interface EvaluationResult {
400
- value: FHIRPathValue[];
400
+ interface EvaluationResult<T = any> {
401
+ value: T[];
401
402
  context: RuntimeContext;
402
403
  }
403
404
  declare enum DiagnosticSeverity {
@@ -613,7 +614,8 @@ declare class Interpreter {
613
614
  inputType?: TypeInfo;
614
615
  modelProvider?: ModelProvider;
615
616
  now?: Date;
616
- }): Promise<any[]>;
617
+ includeMetadata?: boolean;
618
+ }): Promise<EvaluationResult['value']>;
617
619
  private getBooleanKind;
618
620
  private boxBoolean;
619
621
  private evaluateTemporalLiteral;
@@ -743,72 +745,6 @@ declare class Analyzer {
743
745
  analyze(ast: ASTNode, userVariables?: Record<string, any>, inputType?: TypeInfo, options?: AnalyzerOptions): Promise<AnalysisResultWithCursor>;
744
746
  }
745
747
 
746
- interface FHIRModelContext {
747
- path: string;
748
- schemaHierarchy: FHIRSchema[];
749
- isUnion?: boolean;
750
- choices?: Array<{
751
- type: TypeName;
752
- code: string;
753
- choiceName?: string;
754
- schema?: FHIRSchema;
755
- }>;
756
- canonicalUrl?: string;
757
- version?: string;
758
- }
759
- interface FHIRModelProviderConfig {
760
- packages: Array<{
761
- name: string;
762
- version: string;
763
- }>;
764
- cacheDir?: string;
765
- registryUrl?: string;
766
- }
767
- /**
768
- * FHIR ModelProvider implementation
769
- *
770
- * Note: This provider requires async initialization before use.
771
- * Call initialize() before using the synchronous methods.
772
- *
773
- * For best performance, pre-load common types during initialization.
774
- */
775
- declare class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
776
- private config;
777
- private canonicalManager;
778
- private schemaCache;
779
- private hierarchyCache;
780
- private initialized;
781
- private complexTypesCache?;
782
- private primitiveTypesCache?;
783
- private resourceTypesCache?;
784
- private readonly typeMapping;
785
- private readonly primitiveTypeMapping;
786
- constructor(config?: FHIRModelProviderConfig);
787
- initialize(): Promise<void>;
788
- private buildCanonicalUrl;
789
- getSchema(typeName: string): Promise<FHIRSchema | undefined>;
790
- private getSchemaHierarchyAsync;
791
- private extractTypeName;
792
- private getSchemaHierarchyCached;
793
- private mapToFHIRPathType;
794
- private isChoiceType;
795
- private createUnionContext;
796
- getType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
797
- getElementType(parentType: TypeInfo<FHIRModelContext>, propertyName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
798
- ofType(type: TypeInfo<FHIRModelContext>, typeName: TypeName): TypeInfo<FHIRModelContext> | undefined;
799
- getElementNames(parentType: TypeInfo<FHIRModelContext>): string[];
800
- getChildrenType(parentType: TypeInfo<FHIRModelContext>): Promise<TypeInfo<FHIRModelContext> | undefined>;
801
- loadType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
802
- getElements(typeName: string): Promise<Array<{
803
- name: string;
804
- type: string;
805
- documentation?: string;
806
- }>>;
807
- getResourceTypes(): Promise<string[]>;
808
- getComplexTypes(): Promise<string[]>;
809
- getPrimitiveTypes(): Promise<string[]>;
810
- }
811
-
812
748
  interface ASTMetadata {
813
749
  complexity: number;
814
750
  depth: number;
@@ -895,6 +831,7 @@ declare const Errors: {
895
831
  incompatibleUnits(unit1: string, unit2: string, location?: Range): FHIRPathError;
896
832
  indexOutOfBounds(index: number, size: number, location?: Range): FHIRPathError;
897
833
  invalidOperation(details: string, location?: Range): FHIRPathError;
834
+ systemVariableOverride(name: string, location?: Range): FHIRPathError;
898
835
  invalidPrecision(operation: string, location?: Range): FHIRPathError;
899
836
  invalidStringOperation(operation: string, paramName: string, location?: Range): FHIRPathError;
900
837
  invalidNumericOperation(operation: string, paramName: string, expectedType: string, location?: Range): FHIRPathError;
@@ -995,13 +932,83 @@ interface EvaluateOptions {
995
932
  variables?: Record<string, unknown>;
996
933
  modelProvider?: ModelProvider;
997
934
  inputType?: TypeInfo;
935
+ includeMetadata?: boolean;
998
936
  }
999
- declare function evaluate(expression: string, options?: EvaluateOptions): Promise<any[]>;
937
+ declare function evaluate<T = any>(expression: string, options?: EvaluateOptions): Promise<EvaluationResult<T>['value']>;
1000
938
  declare function analyze(expression: string, options?: {
1001
939
  variables?: Record<string, unknown>;
1002
940
  modelProvider?: ModelProvider;
1003
941
  inputType?: TypeInfo;
1004
942
  errorRecovery?: boolean;
1005
943
  }): Promise<AnalysisResult>;
944
+ declare function getVersion(): string;
945
+
946
+ type Resource = {
947
+ url?: string;
948
+ version?: string;
949
+ id: string;
950
+ resourceType: string;
951
+ [key: string]: any;
952
+ };
953
+ interface FHIRModelContext {
954
+ path: string;
955
+ schemaHierarchy: FHIRSchema[];
956
+ isUnion?: boolean;
957
+ choices?: Array<{
958
+ type: TypeName;
959
+ code: string;
960
+ choiceName?: string;
961
+ schema?: FHIRSchema;
962
+ }>;
963
+ canonicalUrl?: string;
964
+ version?: string;
965
+ }
966
+ /**
967
+ * FHIR ModelProvider implementation
968
+ *
969
+ * Note: This provider requires async initialization before use.
970
+ * Call initialize() before using the synchronous methods.
971
+ *
972
+ * For best performance, pre-load common types during initialization.
973
+ */
974
+ declare class FHIRModelProviderBase implements ModelProvider<FHIRModelContext> {
975
+ private schemaCache;
976
+ private hierarchyCache;
977
+ private initialized;
978
+ private complexTypesCache?;
979
+ private primitiveTypesCache?;
980
+ private resourceTypesCache?;
981
+ private readonly typeMapping;
982
+ private readonly primitiveTypeMapping;
983
+ constructor();
984
+ prepare(): Promise<void>;
985
+ initialize(): Promise<void>;
986
+ resolve(_canonicalUrl: string): Promise<Resource | null>;
987
+ search(_params: {
988
+ kind: "primitive-type" | "complex-type" | "resource";
989
+ }): Promise<Resource[]>;
990
+ private buildCanonicalUrl;
991
+ getSchema(typeName: string): Promise<FHIRSchema | undefined>;
992
+ private getSchemaHierarchyAsync;
993
+ private extractTypeName;
994
+ private getSchemaHierarchyCached;
995
+ private mapToFHIRPathType;
996
+ private isChoiceType;
997
+ private createUnionContext;
998
+ getType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
999
+ getElementType(parentType: TypeInfo<FHIRModelContext>, propertyName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
1000
+ ofType(type: TypeInfo<FHIRModelContext>, typeName: TypeName): TypeInfo<FHIRModelContext> | undefined;
1001
+ getElementNames(parentType: TypeInfo<FHIRModelContext>): string[];
1002
+ getChildrenType(parentType: TypeInfo<FHIRModelContext>): Promise<TypeInfo<FHIRModelContext> | undefined>;
1003
+ loadType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined>;
1004
+ getElements(typeName: string): Promise<Array<{
1005
+ name: string;
1006
+ type: string;
1007
+ documentation?: string;
1008
+ }>>;
1009
+ getResourceTypes(): Promise<string[]>;
1010
+ getComplexTypes(): Promise<string[]>;
1011
+ getPrimitiveTypes(): Promise<string[]>;
1012
+ }
1006
1013
 
1007
- export { type ASTMetadata, type ASTNode, type AnalysisResult, Analyzer, type AnyCursorNode, type CompletionItem, CompletionKind, type CompletionOptions, type CursorArgumentNode, CursorContext, type CursorIdentifierNode, type CursorIndexNode, type CursorNode, type CursorOperatorNode, type CursorTypeNode, type Diagnostic, DiagnosticSeverity, ErrorCodes, Errors, type EvaluateOptions, type FHIRModelContext, FHIRModelProvider, type FHIRModelProviderConfig, FHIRPathError, type FunctionDefinition, type InspectOptions, type InspectResult, Interpreter, type ModelProvider as ModelTypeProvider, type OperatorDefinition, type ParseResult, Parser, Registry, type TypeInfo, type TypeName, analyze, evaluate, inspect, isCursorNode, parse, provideCompletions, registry };
1014
+ export { Analyzer as A, type CursorNode as B, CompletionKind as C, DiagnosticSeverity as D, type EvaluateOptions as E, FHIRModelProviderBase as F, type CursorOperatorNode as G, type CursorIdentifierNode as H, Interpreter as I, type CursorArgumentNode as J, type CursorIndexNode as K, type CursorTypeNode as L, type ModelProvider as M, type AnyCursorNode as N, type OperatorDefinition as O, Parser as P, type Resource as R, type TypeInfo as T, type FHIRModelContext as a, analyze as b, Registry as c, type ParseResult as d, evaluate as e, type Diagnostic as f, getVersion as g, type AnalysisResult as h, type ASTNode as i, type TypeName as j, type FunctionDefinition as k, type EvaluationResult as l, inspect as m, type InspectOptions as n, type InspectResult as o, parse as p, type ASTMetadata as q, registry as r, FHIRPathError as s, Errors as t, ErrorCodes as u, provideCompletions as v, type CompletionItem as w, type CompletionOptions as x, CursorContext as y, isCursorNode as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomic-ehr/fhirpath",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A TypeScript implementation of FHIRPath",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,8 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
12
+ "node": "./dist/index.node.js",
13
+ "default": "./dist/index.browser.js"
13
14
  }
14
15
  },
15
16
  "files": [
@@ -48,6 +49,8 @@
48
49
  "@types/lodash": "^4.17.20",
49
50
  "@types/node": "22.13.14",
50
51
  "@types/turndown": "^5.0.5",
52
+ "@types/xml2js": "^0.4.14",
53
+ "@xmldom/xmldom": "^0.8.11",
51
54
  "antlr4ts": "^0.5.0-alpha.4",
52
55
  "antlr4ts-cli": "^0.5.0-alpha.4",
53
56
  "js-yaml": "^4.1.0",
@@ -56,7 +59,8 @@
56
59
  "tsup": "8.5.0",
57
60
  "turndown": "^7.2.0",
58
61
  "typescript": "^5.8.3",
59
- "typescript-mcp": "^0.0.14"
62
+ "typescript-mcp": "^0.0.14",
63
+ "xml2js": "^0.6.2"
60
64
  },
61
65
  "peerDependencies": {
62
66
  "typescript": "^5"
@@ -64,6 +68,7 @@
64
68
  "dependencies": {
65
69
  "@atomic-ehr/fhir-canonical-manager": "^0.0.11",
66
70
  "@atomic-ehr/fhirschema": "^0.0.2",
67
- "@atomic-ehr/ucum": "^0.2.5"
71
+ "@atomic-ehr/ucum": "^0.2.5",
72
+ "fast-xml-parser": "^5.2.5"
68
73
  }
69
- }
74
+ }
package/src/analyzer.ts CHANGED
@@ -198,6 +198,19 @@ export class Analyzer {
198
198
 
199
199
  // Special handling for dot operator - flow context through
200
200
  if (node.operator === '.') {
201
+ // Check if this is actually a namespaced type in an 'is' expression
202
+ // Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
203
+ if (node.left.type === NodeType.MembershipTest && node.right.type === NodeType.Identifier) {
204
+ const membershipTest = node.left as MembershipTestNode;
205
+ const rightIdent = node.right as IdentifierNode;
206
+ // Reconstruct the correct MembershipTest with full type name
207
+ const correctedNode: MembershipTestNode = {
208
+ ...membershipTest,
209
+ targetType: `${membershipTest.targetType}.${rightIdent.name}`
210
+ };
211
+ return await this.analyzeMembershipTest(correctedNode, context);
212
+ }
213
+
201
214
  const leftResult = await this.analyzeNode(node.left, context);
202
215
  if (this.stoppedAtCursor) {
203
216
  return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
@@ -391,7 +404,7 @@ export class Analyzer {
391
404
  if (signature) {
392
405
  const params = signature.parameters || [];
393
406
  const requiredCount = params.filter(p => !p.optional).length;
394
- const maxCount = params.length;
407
+ const maxCount = signature.variadic ? Infinity : params.length;
395
408
  const actualCount = node.arguments.length;
396
409
 
397
410
  if (actualCount < requiredCount) {
@@ -468,6 +481,7 @@ export class Analyzer {
468
481
  if (param?.expression) {
469
482
  const itemType = { ...context.inputType, singleton: true };
470
483
  const exprContext = context
484
+ .withInputType(itemType) // Set input type to singleton for expression parameters
471
485
  .withSystemVariable('$this', itemType)
472
486
  .withSystemVariable('$index', { type: 'Integer', singleton: true });
473
487
  const argResult = await this.analyzeNode(arg, exprContext);
@@ -637,15 +651,29 @@ export class Analyzer {
637
651
 
638
652
  // Check if it's a user variable (starts with %)
639
653
  if (varName.startsWith('%')) {
640
- // Special handling for %context - it's a built-in environment variable
641
- // that always returns the original input to the evaluation engine
654
+ // Special handling for built-in environment variables
642
655
  if (varName === '%context') {
643
656
  // %context returns the root input type (the original input to evaluate())
644
657
  // In the analyzer, we track this as the initial input type
645
658
  return { type: context.inputType, diagnostics, context };
646
659
  }
647
660
 
648
- const name = varName.substring(1); // Remove % prefix
661
+ // Special handling for FHIR system variables (code system URLs)
662
+ if (varName === '%sct' || varName === '%loinc' || varName === '%ucum' ||
663
+ varName === '%vs-administrative-gender' || varName === '%`vs-administrative-gender`') {
664
+ // These are string constants
665
+ return { type: { type: 'String', singleton: true }, diagnostics, context };
666
+ }
667
+
668
+ // Handle environment variable syntax with backticks %`variable`
669
+ let name: string;
670
+ if (varName.startsWith('%`') && varName.endsWith('`')) {
671
+ // Remove %` from start and ` from end
672
+ name = varName.slice(2, -1);
673
+ } else {
674
+ // Remove % prefix
675
+ name = varName.substring(1);
676
+ }
649
677
  const varType = context.userVariables.get(name);
650
678
 
651
679
  if (!varType) {
@@ -745,10 +773,12 @@ export class Analyzer {
745
773
  type = { type: 'String', singleton: true };
746
774
  break;
747
775
  case 'number':
748
- // Check if integer or decimal
749
- type = Number.isInteger(node.value)
750
- ? { type: 'Integer', singleton: true }
751
- : { type: 'Decimal', singleton: true };
776
+ // Number without decimal point is integer
777
+ type = { type: 'Integer', singleton: true };
778
+ break;
779
+ case 'decimal':
780
+ // Number with decimal point is decimal (even if value is whole)
781
+ type = { type: 'Decimal', singleton: true };
752
782
  break;
753
783
  case 'boolean':
754
784
  type = { type: 'Boolean', singleton: true };
@@ -890,7 +920,14 @@ export class Analyzer {
890
920
 
891
921
  // ModelProvider requirement for non-primitive target types
892
922
  const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
893
- if (!context.modelProvider && !primitiveTypes.includes(node.targetType)) {
923
+
924
+ // Normalize System.X types to check if they're primitive
925
+ let targetType = node.targetType;
926
+ if (targetType.startsWith('System.')) {
927
+ targetType = targetType.substring(7); // Remove "System." prefix
928
+ }
929
+
930
+ if (!context.modelProvider && !primitiveTypes.includes(targetType)) {
894
931
  diagnostics.push(toDiagnostic(Errors.modelProviderRequired('is', node.range)));
895
932
  }
896
933
 
@@ -25,6 +25,29 @@ export const CALENDAR_DURATION_UNITS = new Set([
25
25
  'millisecond', 'milliseconds'
26
26
  ]);
27
27
 
28
+ /**
29
+ * Calendar to UCUM duration mappings
30
+ * Maps FHIRPath calendar duration units to their UCUM equivalents
31
+ */
32
+ export const CALENDAR_TO_UCUM: Record<string, string> = {
33
+ 'year': 'a',
34
+ 'years': 'a',
35
+ 'month': 'mo',
36
+ 'months': 'mo',
37
+ 'week': 'wk',
38
+ 'weeks': 'wk',
39
+ 'day': 'd',
40
+ 'days': 'd',
41
+ 'hour': 'h',
42
+ 'hours': 'h',
43
+ 'minute': 'min',
44
+ 'minutes': 'min',
45
+ 'second': 's',
46
+ 'seconds': 's',
47
+ 'millisecond': 'ms',
48
+ 'milliseconds': 'ms'
49
+ };
50
+
28
51
  /**
29
52
  * Create a quantity value
30
53
  */
@@ -234,19 +257,118 @@ export function divideQuantities(left: QuantityValue, right: QuantityValue): Qua
234
257
  * Returns -1 if left < right, 0 if equal, 1 if left > right, null if incomparable
235
258
  */
236
259
  export function compareQuantities(left: QuantityValue, right: QuantityValue): number | null {
237
- // Special case: comparing calendar durations with the same unit
238
- if (CALENDAR_DURATION_UNITS.has(left.unit) && left.unit === right.unit) {
239
- if (left.value < right.value) {
240
- return -1;
241
- } else if (left.value > right.value) {
242
- return 1;
243
- } else {
260
+ // Handle calendar to UCUM comparisons
261
+ const leftIsCalendar = CALENDAR_DURATION_UNITS.has(left.unit);
262
+ const rightIsCalendar = CALENDAR_DURATION_UNITS.has(right.unit);
263
+
264
+ // Both calendar units - only compare if they're compatible units
265
+ if (leftIsCalendar && rightIsCalendar) {
266
+ // Only allow conversion between specific compatible units
267
+ const areCompatible = (unit1: string, unit2: string): boolean => {
268
+ // Normalize to singular form
269
+ const normalize = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
270
+ const n1 = normalize(unit1);
271
+ const n2 = normalize(unit2);
272
+
273
+ // Same unit (singular/plural)
274
+ if (n1 === n2) return true;
275
+
276
+ // Week <-> days conversion
277
+ if ((n1 === 'week' && n2 === 'day') || (n1 === 'day' && n2 === 'week')) return true;
278
+
279
+ // No other conversions between calendar units
280
+ return false;
281
+ };
282
+
283
+ if (!areCompatible(left.unit, right.unit)) {
284
+ // Different calendar units that aren't compatible
285
+ return null;
286
+ }
287
+
288
+ // Handle week/day conversion
289
+ const normalizeToSingular = (u: string) => u.endsWith('s') ? u.slice(0, -1) : u;
290
+ const leftNorm = normalizeToSingular(left.unit);
291
+ const rightNorm = normalizeToSingular(right.unit);
292
+
293
+ if (leftNorm === rightNorm) {
294
+ // Same unit, just compare values
295
+ if (left.value < right.value) return -1;
296
+ if (left.value > right.value) return 1;
244
297
  return 0;
245
298
  }
299
+
300
+ // Week <-> day conversion
301
+ if ((leftNorm === 'week' && rightNorm === 'day') ||
302
+ (leftNorm === 'day' && rightNorm === 'week')) {
303
+ const leftInDays = leftNorm === 'week' ? left.value * 7 : left.value;
304
+ const rightInDays = rightNorm === 'week' ? right.value * 7 : right.value;
305
+
306
+ if (leftInDays < rightInDays) return -1;
307
+ if (leftInDays > rightInDays) return 1;
308
+ return 0;
309
+ }
310
+
311
+ // Shouldn't reach here
312
+ return null;
246
313
  }
247
314
 
248
- // Different calendar units cannot be compared
249
- if (CALENDAR_DURATION_UNITS.has(left.unit) || CALENDAR_DURATION_UNITS.has(right.unit)) {
315
+ // Calendar to UCUM comparison
316
+ if (leftIsCalendar && !rightIsCalendar) {
317
+ // Check for week/day special case
318
+ if ((left.unit === 'days' || left.unit === 'day') && right.unit === 'wk') {
319
+ // Convert days to weeks
320
+ const leftInWeeks = left.value / 7;
321
+ if (leftInWeeks < right.value) return -1;
322
+ if (leftInWeeks > right.value) return 1;
323
+ return 0;
324
+ }
325
+ if ((left.unit === 'weeks' || left.unit === 'week') && right.unit === 'd') {
326
+ // Convert weeks to days
327
+ const leftInDays = left.value * 7;
328
+ if (leftInDays < right.value) return -1;
329
+ if (leftInDays > right.value) return 1;
330
+ return 0;
331
+ }
332
+
333
+ // Direct mapping check
334
+ const leftUcumUnit = CALENDAR_TO_UCUM[left.unit];
335
+ if (leftUcumUnit === right.unit) {
336
+ // Direct mapping, compare values
337
+ if (left.value < right.value) return -1;
338
+ if (left.value > right.value) return 1;
339
+ return 0;
340
+ }
341
+ // No mapping, incomparable
342
+ return null;
343
+ }
344
+
345
+ // UCUM to calendar comparison
346
+ if (!leftIsCalendar && rightIsCalendar) {
347
+ // Check for week/day special case
348
+ if (left.unit === 'wk' && (right.unit === 'days' || right.unit === 'day')) {
349
+ // Convert weeks to days
350
+ const leftInDays = left.value * 7;
351
+ if (leftInDays < right.value) return -1;
352
+ if (leftInDays > right.value) return 1;
353
+ return 0;
354
+ }
355
+ if (left.unit === 'd' && (right.unit === 'weeks' || right.unit === 'week')) {
356
+ // Convert days to weeks
357
+ const leftInWeeks = left.value / 7;
358
+ if (leftInWeeks < right.value) return -1;
359
+ if (leftInWeeks > right.value) return 1;
360
+ return 0;
361
+ }
362
+
363
+ // Direct mapping check
364
+ const rightUcumUnit = CALENDAR_TO_UCUM[right.unit];
365
+ if (left.unit === rightUcumUnit) {
366
+ // Direct mapping, compare values
367
+ if (left.value < right.value) return -1;
368
+ if (left.value > right.value) return 1;
369
+ return 0;
370
+ }
371
+ // No mapping, incomparable
250
372
  return null;
251
373
  }
252
374
 
@@ -461,7 +461,33 @@ export function equivalent(a: TemporalValue, b: TemporalValue): boolean {
461
461
  }
462
462
 
463
463
  export function compare(a: TemporalValue, b: TemporalValue): -1 | 0 | 1 | null {
464
- // Different types can't be compared
464
+ // Special case: DateTime and Date can be compared when date portions differ
465
+ if (isFHIRDateTime(a) && isFHIRDate(b)) {
466
+ // Compare the date portion of DateTime with Date
467
+ if (a.year !== b.year) return a.year < b.year ? -1 : 1;
468
+ if (b.month !== undefined && a.month !== undefined) {
469
+ if (a.month !== b.month) return a.month < b.month ? -1 : 1;
470
+ }
471
+ if (b.day !== undefined && a.day !== undefined) {
472
+ if (a.day !== b.day) return a.day < b.day ? -1 : 1;
473
+ }
474
+ // When date portions are equal, Date and DateTime are incomparable
475
+ return null;
476
+ }
477
+ if (isFHIRDate(a) && isFHIRDateTime(b)) {
478
+ // Compare Date with the date portion of DateTime
479
+ if (a.year !== b.year) return a.year < b.year ? -1 : 1;
480
+ if (a.month !== undefined && b.month !== undefined) {
481
+ if (a.month !== b.month) return a.month < b.month ? -1 : 1;
482
+ }
483
+ if (a.day !== undefined && b.day !== undefined) {
484
+ if (a.day !== b.day) return a.day < b.day ? -1 : 1;
485
+ }
486
+ // When date portions are equal, Date and DateTime are incomparable
487
+ return null;
488
+ }
489
+
490
+ // Different types (except DateTime/Date) can't be compared
465
491
  if (a.kind !== b.kind) {
466
492
  return null;
467
493
  }
@@ -765,7 +791,7 @@ function parseDateLiteral(value: string): FHIRDate {
765
791
  }
766
792
 
767
793
  function parseTimeLiteral(value: string): FHIRTime {
768
- const match = value.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?)?$/);
794
+ const match = value.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?$/);
769
795
  if (!match) {
770
796
  throw new Error(`Invalid time literal: @T${value}`);
771
797
  }
@@ -773,7 +799,14 @@ function parseTimeLiteral(value: string): FHIRTime {
773
799
  const hour = parseInt(match[1]!, 10);
774
800
  const minute = match[2] ? parseInt(match[2]!, 10) : undefined;
775
801
  const second = match[3] ? parseInt(match[3]!, 10) : undefined;
776
- const millisecond = match[4] ? parseInt(match[4]!, 10) : undefined;
802
+ // Handle variable-length fractional seconds (pad or truncate to 3 digits)
803
+ let millisecond: number | undefined;
804
+ if (match[4]) {
805
+ const fraction = match[4];
806
+ // Pad to 3 digits if needed, truncate if longer
807
+ const padded = (fraction + '000').substring(0, 3);
808
+ millisecond = parseInt(padded, 10);
809
+ }
777
810
 
778
811
  return createTime(hour, minute, second, millisecond);
779
812
  }
@@ -826,13 +859,19 @@ function parseDateTimeLiteral(value: string): FHIRDateTime {
826
859
  }
827
860
  }
828
861
 
829
- // Parse time components
830
- const timeMatch = timeWithoutTz.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?)?$/);
862
+ // Parse time components - handle variable-length milliseconds
863
+ const timeMatch = timeWithoutTz.match(/^(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?$/);
831
864
  if (timeMatch) {
832
865
  hour = parseInt(timeMatch[1]!, 10);
833
866
  minute = timeMatch[2] ? parseInt(timeMatch[2]!, 10) : undefined;
834
867
  second = timeMatch[3] ? parseInt(timeMatch[3]!, 10) : undefined;
835
- millisecond = timeMatch[4] ? parseInt(timeMatch[4]!, 10) : undefined;
868
+ // Handle variable-length fractional seconds (pad or truncate to 3 digits)
869
+ if (timeMatch[4]) {
870
+ const fraction = timeMatch[4];
871
+ // Pad to 3 digits if needed, truncate if longer
872
+ const padded = (fraction + '000').substring(0, 3);
873
+ millisecond = parseInt(padded, 10);
874
+ }
836
875
  }
837
876
  }
838
877
 
package/src/errors.ts CHANGED
@@ -185,6 +185,10 @@ export const Errors = {
185
185
  invalidOperation(details: string, location?: Range): FHIRPathError {
186
186
  return new FHIRPathError(ErrorCodes.INVALID_OPERATION, `Invalid operation: ${details}`, location);
187
187
  },
188
+
189
+ systemVariableOverride(name: string, location?: Range): FHIRPathError {
190
+ return new FHIRPathError(ErrorCodes.INVALID_OPERATION, `Cannot override system variable '${name}'`, location);
191
+ },
188
192
 
189
193
  invalidPrecision(operation: string, location?: Range): FHIRPathError {
190
194
  return new FHIRPathError(ErrorCodes.INVALID_PRECISION, `${operation} precision must be a non-negative integer`, location);
@@ -0,0 +1,4 @@
1
+ export * from './index.common'
2
+
3
+ export { FHIRModelProvider } from './model-provider.browser';
4
+ export type { FHIRModelContext, Options, Resolver, Searcher } from './model-provider.browser';