fhirpath 0.2.0.pre1

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.
@@ -0,0 +1,507 @@
1
+ # FHIRPath Ruby architecture
2
+
3
+ Status: implementation contract for the current pre-release slice
4
+ Date: 2026-09-04
5
+ Repository: `fhirpath-ruby`
6
+ Decision scope: architecture and compatibility contract only; this document does not claim that the scaffold implements FHIRPath.
7
+
8
+ ## 1. Executive decision
9
+
10
+ Build a Ruby-native FHIRPath engine around five stable boundaries:
11
+
12
+ 1. a source-span-preserving lexer and parser;
13
+ 2. an immutable, typed abstract syntax tree (AST);
14
+ 3. a collection-first evaluator with explicit empty/singleton semantics;
15
+ 4. FHIRPath value objects and a model/element adapter boundary; and
16
+ 5. an explicit evaluation context plus metadata-driven function registry.
17
+
18
+ The initial conformance target is FHIRPath 2.0.0, the normative R2 release, with a named, feature-gated subset of FHIRPath 3.0.0 STU3 added only after the normative core is tested. The HL7 grammar, specification, and shared test cases are the conformance authorities; `fhirpath-py` is the first compatibility reference, not the specification.[1][2][3]
19
+
20
+ The implementation must be Ruby-native. It may use a grammar generator or a hand-written parser, but it must not make the Python implementation's dictionary AST, mutable context hash, or generated parser artifacts part of the Ruby public contract.
21
+
22
+ ## 2. Design goals and non-goals
23
+
24
+ ### Goals
25
+
26
+ - Implement standard FHIRPath behavior independently of a particular FHIR server or Ruby FHIR model library.
27
+ - Preserve collection semantics through the evaluator rather than reducing every result to Ruby truthiness, `nil`, or a scalar.
28
+ - Make parsing, compilation, evaluation, model navigation, terminology, reference resolution, and custom functions separately testable.
29
+ - Produce deterministic, structured parse/evaluation errors with source spans.
30
+ - Permit repeated evaluation of an immutable compiled expression without reparsing or sharing per-evaluation state.
31
+ - Make the supported FHIRPath version and optional STU features observable through a capability object.
32
+ - Support plain Ruby hashes/objects before adding a FHIR-release-specific adapter.
33
+ - Keep enough internal type/path metadata to support FHIR choice elements, primitive extensions, and logical model paths when a FHIR adapter is present.
34
+
35
+ ### Non-goals for the first release
36
+
37
+ - A FHIR server, persistence layer, terminology server, or clinical-reasoning platform.
38
+ - A source-compatible port of `fhirpath-py`, `fhirpath.js`, Firely, or HAPI.
39
+ - Silent compatibility with every implementation-specific permissive mode.
40
+ - Network I/O from pure expression evaluation.
41
+ - Claiming complete FHIR R4/R5 model support before a versioned model provider and fixtures exist.
42
+ - Enabling trial-use FHIRPath 3.0 features merely because the parser can recognize their syntax.
43
+
44
+ ## 3. Conformance contract
45
+
46
+ ### 3.1 Version policy
47
+
48
+ `FHIRPath::Capability` is the runtime declaration of the implementation contract. It should expose at least:
49
+
50
+ ```ruby
51
+ FHIRPath::Capability.current
52
+ # => {
53
+ # fhirpath: "2.0.0",
54
+ # trial_use: ['stu3-aggregate-functions'],
55
+ # model_releases: ['R4'],
56
+ # host_features: []
57
+ # }
58
+ ```
59
+
60
+ The normative 2.0 feature set is the default. Each 3.0 STU feature must have:
61
+
62
+ - a capability name;
63
+ - a parser/evaluator test group;
64
+ - an explicit enablement option;
65
+ - a version or capability marker in conformance reports; and
66
+ - a documented reason it is not part of the normative default.
67
+
68
+ The declared exception to the gating model is the `stu3-aggregate-functions`
69
+ subset (`sum()`, `avg()`, `max()`, `min()`), which ships default-on in the
70
+ standard registry and is surfaced in `Capability#trial_use`; callers that need
71
+ a strict-2.0 declaration can construct `Capability.new(trial_use: [])`.
72
+ Registry-level enforcement of that strict declaration is documented as not yet
73
+ implemented. Examples of gated features include `Long`, instance
74
+ selectors/object construction, the general-purpose `aggregate()` function,
75
+ reflection, and other additions identified by the specification as trial use.
76
+ The grammar is not itself a promise of evaluator support.[1][2]
77
+
78
+ ### 3.2 Result policy
79
+
80
+ The evaluator core returns a `FHIRPath::Collection`, including for a singleton result. The collection is ordered, enumerable, and represents the FHIRPath empty collection with `empty? == true`; it is not a Ruby `nil` value. A convenience API returns the first item explicitly.
81
+
82
+ ```ruby
83
+ patient = {
84
+ "resourceType" => "Patient",
85
+ "name" => [
86
+ { "family" => "Lovelace", "given" => ["Ada", "Augusta"] }
87
+ ]
88
+ }
89
+
90
+ FHIRPath.evaluate(patient, "Patient.name.given").to_a
91
+ # => ["Ada", "Augusta"]
92
+
93
+ FHIRPath.evaluate_first(patient, "Patient.name.family")
94
+ # => "Lovelace"
95
+
96
+ FHIRPath.evaluate_first(patient, "Patient.telecom")
97
+ # => nil
98
+ ```
99
+
100
+ The default collection result is deliberately more explicit than a scalar convenience result. It also gives the engine a place to preserve item metadata, such as logical type, source path, and model element information, without changing the public meaning of an empty result.
101
+
102
+ ### 3.3 Public API proposal
103
+
104
+ The first stable API should be small:
105
+
106
+ ```ruby
107
+ module FHIRPath
108
+ def self.parse(expression, capability: Capability.current)
109
+ # => ParsedExpression, or raises ParseError
110
+ end
111
+
112
+ def self.compile(expression, model: nil, capability: Capability.current,
113
+ functions: FunctionRegistry.standard)
114
+ # => CompiledExpression
115
+ end
116
+
117
+ def self.evaluate(resource, expression, variables: {}, model: nil,
118
+ capability: Capability.current,
119
+ functions: FunctionRegistry.standard,
120
+ options: {})
121
+ # => Collection
122
+ end
123
+
124
+ def self.evaluate_first(resource, expression, variables: {}, model: nil,
125
+ capability: Capability.current,
126
+ functions: FunctionRegistry.standard,
127
+ options: {})
128
+ # => one item or nil
129
+ end
130
+ end
131
+ ```
132
+
133
+ A compiled expression is immutable and reusable:
134
+
135
+ ```ruby
136
+ program = FHIRPath.compile("Patient.name.where(use = 'official').given")
137
+
138
+ program.evaluate(patient).to_a
139
+ # => ["Ada", "Augusta"]
140
+
141
+ # Optional Ruby-callable compatibility convenience:
142
+ program.call(patient).to_a
143
+ ```
144
+
145
+ A caller can inspect the parsed AST for tooling without coupling to evaluator state:
146
+
147
+ ```ruby
148
+ parsed = FHIRPath.parse("Patient.name.given")
149
+ parsed.source # => the original expression
150
+ parsed.ast # => immutable FHIRPath::AST nodes
151
+ parsed.source_map # => node => source span
152
+ ```
153
+
154
+ External constants and host services are passed explicitly:
155
+
156
+ ```ruby
157
+ FHIRPath.evaluate(
158
+ patient,
159
+ "%subject.name.family",
160
+ variables: { "subject" => patient }
161
+ )
162
+ ```
163
+
164
+ The API must not mutate the caller's resource to add engine metadata. `fhirpath-py` currently prepares a context containing `dataRoot`, variables, model metadata, a user invocation table, and an optional trace callback, and its navigation wrappers retain path/type information internally.[4][5][7] Ruby should retain the useful separation while keeping wrappers and evaluation state immutable or per-evaluation.
165
+
166
+ ### 3.4 Error taxonomy
167
+
168
+ All public errors derive from `FHIRPath::Error` and carry a stable code, message, and optional source span:
169
+
170
+ - `ParseError`: invalid characters, malformed tokens, unexpected token, trailing input, or expression nesting exceeding the parser depth budget (code `nesting_depth_exceeded`);
171
+ - `EvaluationError`: a valid expression cannot be evaluated for the current focus or collection;
172
+ - `SingletonError`: an operation requiring one item received multiple items;
173
+ - `TypeError`: an argument or value is not compatible with the required FHIRPath type;
174
+ - `UnknownFunctionError`: no standard or explicitly registered function exists;
175
+ - `UnknownConstantError`: an external constant is not present in the context;
176
+ - `ModelError`: model navigation/type resolution failed;
177
+ - `HostError`: an injected host service failed; and
178
+ - `UnsupportedFeatureError`: a known but disabled or unimplemented capability.
179
+
180
+ Errors should expose machine-readable fields without promising exception-name compatibility with another implementation:
181
+
182
+ ```ruby
183
+ begin
184
+ FHIRPath.evaluate(patient, "Patient.name.given[bad]")
185
+ rescue FHIRPath::ParseError => error
186
+ error.code # => :unexpected_token
187
+ error.span # => FHIRPath::SourceSpan
188
+ error.expression # => original source
189
+ end
190
+ ```
191
+
192
+ The parser must require end-of-input. A parser that returns an AST for a valid prefix is unsafe for a conformance engine; both the Python parser pipeline and Firely compiler explicitly install/perform parse failure handling rather than treating malformed input as a successful expression.[4][11]
193
+
194
+ ## 4. Layered architecture
195
+
196
+ ```text
197
+ Public API
198
+ parse / compile / evaluate / evaluate_first
199
+ |
200
+ Capability + immutable configuration
201
+ |
202
+ Lexer -> Parser -> AST + SourceMap
203
+ |
204
+ Compiler / evaluator plan
205
+ |
206
+ EvaluationContext + Closure
207
+ |
208
+ Collection operations and function registry
209
+ | |
210
+ FHIRPath values Host/model adapter
211
+ and operators plain Ruby | FHIR R4/R5 | custom model
212
+ |
213
+ Collection result / errors / trace
214
+ ```
215
+
216
+ ### 4.1 Public boundary and configuration
217
+
218
+ `FHIRPath` owns the public API and no evaluator singleton. A call constructs or receives an immutable configuration containing:
219
+
220
+ - `Capability` (FHIRPath release and enabled features);
221
+ - `ModelProvider` (optional);
222
+ - a snapshot of `FunctionRegistry`;
223
+ - host service implementations;
224
+ - evaluation options (strictness, tracing, cancellation policy); and
225
+ - cache policy, if compilation caching is enabled by the host.
226
+
227
+ The configuration may be reused, but an `EvaluationContext` must be new for each evaluation. This prevents `$this`, `$index`, `$total`, variables, and trace state from crossing requests.
228
+
229
+ ### 4.2 Lexer and parser
230
+
231
+ The parser consumes the pinned HL7 grammar or a semantically equivalent Ruby-native grammar. It must recognize the selected release's syntax, preserve token offsets, and produce a complete parse or a structured `ParseError`.
232
+
233
+ The parser should not expose generator-specific parse-tree classes. If ANTLR is selected, generated files remain an internal build artifact behind a Ruby AST builder. If a hand-written parser is selected, it must still be tested against the same grammar alternatives and malformed-input corpus.
234
+
235
+ ### 4.3 AST
236
+
237
+ AST nodes are immutable value objects with a common shape:
238
+
239
+ ```ruby
240
+ FHIRPath::AST::MemberInvocation.new(
241
+ receiver: ..., name: "given", span: SourceSpan.new(12, 16)
242
+ )
243
+ ```
244
+
245
+ Recommended node families:
246
+
247
+ - `Expression` / `BinaryExpression` / `UnaryExpression`;
248
+ - `MemberInvocation` / `FunctionInvocation`;
249
+ - `Indexer`;
250
+ - `Identifier` / `ExternalConstant` / `TypeSpecifier`;
251
+ - Boolean, integer, decimal, string, date/time, quantity, and null/empty literals;
252
+ - collection and parenthesized terms; and
253
+ - feature-gated instance/object nodes.
254
+
255
+ The AST is a syntax representation, not an evaluation context. It must never store the current focus, index, total, variable values, network clients, or mutable caches.
256
+
257
+ ### 4.4 Compiler and evaluator
258
+
259
+ `Compiler` validates capability-dependent syntax/function availability and turns the immutable AST into an evaluator plan. The first implementation may interpret AST nodes directly; a later compiler may lower them to closures or instruction nodes. Both approaches must preserve the same semantics.
260
+
261
+ `Evaluator` receives:
262
+
263
+ ```ruby
264
+ EvaluationContext(
265
+ root: Collection,
266
+ focus: Collection,
267
+ variables: Variables,
268
+ model: ModelProvider,
269
+ host: HostServices,
270
+ functions: FunctionRegistry,
271
+ capability: Capability,
272
+ trace: TraceSink
273
+ )
274
+ ```
275
+
276
+ Every nested expression receives a derived context. A derived context changes focus and possibly `$index`/`$total`; it does not mutate the parent context in a way that can leak after the invocation returns.
277
+
278
+ ### 4.5 Collection and focus semantics
279
+
280
+ `Collection` is the semantic center of the engine. It must centralize:
281
+
282
+ - empty versus non-empty;
283
+ - singleton extraction and multiple-item rejection;
284
+ - flattening rules;
285
+ - order and duplicate handling;
286
+ - focus (`$this`), index (`$index`), and aggregate total (`$total`); and
287
+ - conversion to public Ruby values at the API boundary.
288
+
289
+ Do not scatter `Array(value)` coercions across functions. `fhirpath-py` centralizes related behavior in `arraify`, `is_empty`, `is_nullable`, `flatten`, and `get_data`, but its mutable Python lists and `None` conventions are implementation details to translate rather than expose.[5][7]
290
+
291
+ Ruby's `false`, `nil`, empty arrays, and truthy objects cannot stand in for FHIRPath's empty collection and three-valued Boolean semantics. `and`, `or`, `xor`, and `implies` must be implemented from explicit truth tables and tested independently of Ruby conditionals.[1]
292
+
293
+ ### 4.6 Value and type system
294
+
295
+ Use dedicated value objects where Ruby's built-ins lose FHIRPath information:
296
+
297
+ - `FHIRPath::Value::Integer` and `Long` (the latter gated until enabled);
298
+ - `FHIRPath::Value::Decimal` backed by exact decimal arithmetic;
299
+ - `FHIRPath::Value::String` if metadata or escaped-value provenance requires it;
300
+ - `FHIRPath::Value::Date`, `DateTime`, and `Time` preserving partial precision and timezone state;
301
+ - `FHIRPath::Value::Quantity` with numeric value and canonical unit boundary; and
302
+ - `FHIRPath::TypeInfo` for logical FHIRPath type, namespace, model path, and runtime type.
303
+
304
+ The public boundary may convert ordinary values where safe, but the evaluator must not default to binary floating point or Ruby temporal comparison. Quantity conversion should be delegated to a narrow unit service (UCUM-backed when enabled), with invalid/incomparable units represented as deterministic evaluation errors or empty results according to the standard operation.
305
+
306
+ `fhirpath-py` uses `FP_Type`, `FP_Quantity`, `FP_DateTime`, `FP_Time`, `ResourceNode`, and `TypeInfo` to preserve special values and navigation metadata.[7] The Ruby design keeps those responsibilities but uses namespaced immutable objects and an explicit unit-service interface.
307
+
308
+ ### 4.7 Function registry and delayed evaluation
309
+
310
+ Function dispatch is registry-driven, not Ruby method-name reflection. A `FunctionSpec` should declare:
311
+
312
+ ```ruby
313
+ FunctionSpec.new(
314
+ name: "where",
315
+ arity: 1,
316
+ parameters: [:expression],
317
+ receiver: :collection,
318
+ delayed: true,
319
+ nullable_input: false,
320
+ implementation: Filtering::Where
321
+ )
322
+ ```
323
+
324
+ Parameter kinds include `:any`, `:boolean`, `:integer`, `:number`, `:string`, `:type`, `:collection`, `:expression`, and `:root_expression`. Metadata should also state whether the argument is eager, delayed, variadic, nullable, or capability-gated.
325
+
326
+ `where`, `select`, `exists(criteria)`, `all`, `repeat`, `iif`, `coalesce`, and `aggregate` cannot receive already-evaluated arguments. They need a closure containing the AST and a derived focus/context. `fhirpath-py` represents this with `Expr`, `AnyAtRoot`, type specifiers, nullable/variadic metadata, and `make_param`; the registry is therefore a semantic contract, not merely a list of method names.[5][6]
327
+
328
+ Organize standard implementations by semantic family:
329
+
330
+ - `Existence`: `empty`, `exists`, `all`, `count`, truth aggregators;
331
+ - `Filtering`: `where`, `select`, `repeat`, `first`, `last`, `single`, `take`, `skip`, `tail`;
332
+ - `Combining`: union, combine, coalesce, exclude;
333
+ - `Equality` and `Logic`;
334
+ - `Conversion` and `Type`;
335
+ - `String`, `Math`, and `DateTime`;
336
+ - `Navigation`: children, descendants, extension, resolve boundary; and
337
+ - `Aggregate`: sum, min, max, avg, aggregate, with feature gates where required.
338
+
339
+ Standard functions are registered in an immutable default registry. Host functions require explicit registration and a namespace/name policy so an application extension cannot silently replace a standard function.
340
+
341
+ ### 4.8 Evaluation context and host services
342
+
343
+ `HostServices` is the only boundary through which evaluation may ask the host to do work outside the pure expression graph:
344
+
345
+ ```ruby
346
+ HostServices.new(
347
+ constants: ->(name, mode:, context:) { ... },
348
+ resolve_reference: ->(reference, containing:) { ... },
349
+ terminology: nil,
350
+ element_children: nil,
351
+ trace: nil
352
+ )
353
+ ```
354
+
355
+ Initial pure evaluation may provide constants and model navigation but no network services. Later services include:
356
+
357
+ - external constant resolution (`%name`);
358
+ - `resolve()` reference lookup;
359
+ - terminology membership/validation;
360
+ - child/descendant traversal for non-Hash models;
361
+ - tracing; and
362
+ - cancellation/async policy if a host genuinely needs I/O.
363
+
364
+ Firely exposes a typed evaluation context with terminology and element resolution, while HAPI exposes an evaluation-context interface for `resolveReference` and constant resolution.[12][14] `fhirpath.js` additionally documents async evaluation, terminology URLs, server hooks, and internal-type conversion options.[9] These are strong evidence for an injectable boundary, not reasons to make network access part of the core.
365
+
366
+ ### 4.9 Model provider and FHIR integration
367
+
368
+ The core model protocol should be small:
369
+
370
+ ```ruby
371
+ class ModelProvider
372
+ def root_type(resource); end
373
+ def property(element, logical_name); end
374
+ def children(element); end
375
+ def type_of(element); end
376
+ def choice_types(parent_type, logical_name); end
377
+ def primitive_extension(element, logical_name); end
378
+ end
379
+ ```
380
+
381
+ `PlainModel` implements predictable Hash/Array/object navigation. The bundled
382
+ `FHIRPath::FHIR::R4::ModelProvider` is dependency-free; other release-specific
383
+ providers remain separately loaded adapters.
384
+
385
+ FHIR model metadata is data, not evaluator code. It should describe logical paths, type names, parent relationships, choice variants, and primitive-extension fields. For `Observation.value`, an R4 provider must resolve `valueQuantity`, `valueString`, and other `value[x]` variants to the logical property rather than treating JSON key lookup as the FHIRPath model.
386
+
387
+ This boundary follows the observed contrast among implementations: `fhirpath-py` uses release-specific JSON maps and `ResourceNode` metadata, while `fhirpath.js` ships separate FHIR context packages.[7][9] HAPI isolates release behavior in version-specific adapters.[10]
388
+
389
+ ## 5. Comparison with other implementations
390
+
391
+ | Concern | `fhirpath-py` | HL7 `fhirpath.js` | Firely .NET SDK | HAPI FHIR | Ruby decision |
392
+ |---|---|---|---|---|---|
393
+ | Public API | `evaluate`, `compile`, typed first/array helpers; list-oriented output | `evaluate`, `compile`, internal-type conversion, model/options arguments | Compiler `Parse` and `Compile`, producing reusable delegates | `evaluate`, `evaluateFirst`, `parse`, opaque parsed expression | Keep `parse`, `compile`, `evaluate`, `evaluate_first`; return a collection by default and make scalar conversion explicit.[4][9][11] |
394
+ | Parser/AST | ANTLR grammar; listener builds generic dictionaries | ANTLR grammar and custom internal structures | Sprache parser; typed expression objects | Opaque parsed-expression handle | Hide parser technology behind immutable Ruby AST; preserve source spans.[2][4][11] |
395
+ | Evaluation | Dynamic node dispatch and invocation registry | Dynamic evaluator with internal FP types and async options | Typed expression tree compiled to `Invokee` delegates | Engine-specific compiled/parsed object | Use a registry plus visitor/plan; permit interpretation first and compilation optimization later.[5][6][11] |
396
+ | Delayed args | `Expr`, `AnyAtRoot`, nullable/variadic metadata | User invocation metadata includes expression/root argument kinds | Closures/`Invokee` and symbol table | Engine handles standard function semantics internally | Make delayed argument kind a first-class `FunctionSpec` field.[5][6][11] |
397
+ | Context/host | Context dict with root, variables, model, trace | Variables, model, terminology URLs, server/async hooks | `FhirEvaluationContext`, terminology, element resolver | `IFhirPathEvaluationContext` for constants/references | Explicit per-evaluation `EvaluationContext` and injectable `HostServices`; no global singleton.[4][9][12] |
398
+ | FHIR model | JSON maps plus `ResourceNode` | Separate FHIR context packages for releases | POCO/element model | Release-specific adapter | Core is model-independent; add versioned providers later.[7][9][13] |
399
+ | Errors | General exceptions and parser listener behavior | JavaScript exceptions and options | `FormatException` on parse failure | Java exceptions and opaque parsed expressions | Stable Ruby classes/codes/spans; do not promise foreign exception compatibility.[4][11][13] |
400
+ | Tests | YAML collector, resource fixtures, AST fixtures, pytest | Unit/type/E2E and release model tests | SDK/unit tests | Java engine/adapter tests | Official shared suite plus ported compatibility fixtures, differential tests, and property/edge tests.[3][8] |
401
+
402
+ `fhirpath-py` is the most useful first behavioral map because its source makes its context, evaluator, registry, special values, model maps, and fixture loader visible.[4][5][6]
403
+
404
+ Its node and fixture details are documented separately.[7][8] Firely is the strongest counterexample to a purely dynamic dictionary AST: its compiler separates parsing from typed expression compilation and symbol-table dispatch.[11]
405
+
406
+ HAPI demonstrates a small application-facing boundary in which parsing is reusable and implementation details remain opaque.[13][14]
407
+
408
+ `fhirpath.js` demonstrates packaging, async/terminology hooks, and the risk of stale implementation-status prose.[9][10]
409
+
410
+ ## 6. Mapping from `fhirpath-py` modules to Ruby components
411
+
412
+ | Python module or area | Observed responsibility | Ruby component | Compatibility treatment |
413
+ |---|---|---|---|
414
+ | `fhirpathpy/__init__.py` | Public `evaluate`, `compile`, parsed-path application, raw/result conversion | `lib/fhirpath.rb`, `FHIRPath::API`, `CompiledExpression` | Preserve the conceptual boundary; return `Collection` and expose `evaluate_first` rather than copying Python list/scalar conversion.[4] |
415
+ | `parser/FHIRPath.g4` | Grammar source | `grammar/fhirpath.g4` or `lib/fhirpath/grammar.rb` | Pin a grammar revision and generator/runtime if used; never expose generated parser classes.[2] |
416
+ | `parser/generated/*` | Generated lexer/parser | Internal build output | Do not port into the public gem; regenerate in CI or use an original Ruby parser. |
417
+ | `parser/__init__.py` | Lexer/token stream/parser setup and error listener | `FHIRPath::Lexer`, `FHIRPath::Parser`, `FHIRPath::ParseError` | Require complete input and preserve source spans.[4] |
418
+ | `parser/ASTPathListener.py` | Parse-tree callbacks into generic `{type, text, children}` dictionaries | `FHIRPath::AST::*`, `ASTBuilder`, `SourceMap` | Replace mutable generic dictionaries with immutable typed nodes.[4] |
419
+ | `engine/evaluators/*` | AST-node evaluator dispatch | `FHIRPath::Evaluator`, node visitors/handlers | Use exhaustive dispatch and `UnsupportedFeatureError` for known gaps. |
420
+ | `engine/__init__.py` | `do_eval`, `doInvoke`, infix calls, argument checking, delayed args | `Evaluator`, `Invocation`, `ArgumentBinder`, `EvaluationContext` | Keep explicit argument kinds and nested closures; do not use Ruby reflection as a substitute.[5] |
421
+ | `engine/invocations/__init__.py` | Standard function/operator registry and metadata | `FunctionRegistry`, `FunctionSpec`, semantic-family modules | Registry is immutable per configuration; distinguish standard, STU, and host functions.[6] |
422
+ | `engine/invocations/{family}.py` | Existence, filtering, equality, logic, math, strings, navigation, aggregate implementations | `FHIRPath::Functions::{Family}` | Port behavior through shared tests, not source code. |
423
+ | `engine/nodes.py` | Special values, quantities, temporal values, resource wrappers, type info | `Value::*`, `Quantity`, `Temporal`, `Element`, `TypeInfo` | Preserve semantic information; use Ruby-native value objects and explicit unit service.[7] |
424
+ | `engine/util.py` | Collection normalization, data unwrapping, primitive handling, user table adaptation | `Collection`, `ValueAdapter`, `ElementAdapter`, `ArgumentBinder` | Centralize empty/singleton semantics; never scatter coercion.[7] |
425
+ | `models/*` | Release-specific path/type/choice maps | `ModelProvider`, `FHIR::R4`, `FHIR::R5` adapters | Load versioned metadata as data; keep core independent.[7][9][10] |
426
+ | `tests/conftest.py` | YAML collection, resource fixtures, context/variables, result comparison | `test/support/yaml_suite_loader`, fixture adapters, conformance reporter | Preserve fixture provenance; replace string-only comparison with typed expected values/errors.[8] |
427
+
428
+ ## 7. Standards-required versus implementation-specific behavior
429
+
430
+ | Area | Standards-required behavior | Implementation-specific choice |
431
+ |---|---|---|
432
+ | Grammar | Accepted tokens, precedence, literals, operators, function syntax, and release-specific features must match the declared FHIRPath target.[1][2] | ANTLR versus hand-written parser; generated-file layout; AST class names. |
433
+ | Collections | Empty, singleton, multi-item, flattening, ordering, duplicates, and singleton coercion must follow FHIRPath semantics.[1] | `Collection` class shape, enumerable methods, and whether `to_a` allocates. |
434
+ | Boolean logic | Empty-aware logical truth tables and implication semantics.[1] | Internal truth-table helper names and storage representation. |
435
+ | Equality/equivalence | `=`/`!=` versus `~`/`!~`, collection behavior, precision, dates/times, quantities, and type rules.[1] | Value-object implementation and diagnostic wording. |
436
+ | Types and values | FHIRPath primitive/model types, conversions, date/time precision, quantity semantics, and type operators for the declared release.[1] | Decimal/temporal/UCUM libraries, internal wrappers, and public conversion policy. |
437
+ | Functions | Standard function names, arities, argument semantics, delayed evaluation, and results.[1] | Registry data structure, module grouping, extension-registration API. |
438
+ | Root/context | Evaluation is relative to a focus/root and supports standard variables and external constants according to the declared host/model contract.[1] | Ruby keyword names, context object classes, default variables, cache ownership. |
439
+ | FHIR navigation | A FHIR adapter must expose logical model properties, choice types, primitive extensions, and model types correctly for its release.[1] | Which FHIR releases/dependencies ship, metadata format, and adapter class names. |
440
+ | `resolve()`/terminology | Behavior is only available when the relevant host services and model contract are supplied.[1] | Whether services are synchronous, asynchronous, cancellable, local, remote, or disabled by default. |
441
+ | Parsing errors | Invalid/trailing input must not evaluate as a successful prefix. | Error classes, codes, spans, and message wording. |
442
+ | Compilation | Reusing a parsed/compiled expression must not alter its result semantics. | Whether compilation means an AST wrapper, closures, bytecode, or cached plan. |
443
+ | Tracing | If exposed, trace output must not change expression results. | Trace sink interface, event shape, redaction, and performance policy. |
444
+ | Conformance reporting | Reports must identify expression, fixture, expected/actual result or error, target release, and classification. | Report file format, CI integration, dashboard, and naming conventions. |
445
+
446
+ ## 8. Compatibility risks and mitigations
447
+
448
+ | Risk | Failure mode | Mitigation |
449
+ |---|---|---|
450
+ | Release skew | 2.0 normative, 3.0 STU, and continuous grammar are mixed without a declaration | Capability object, feature gates, pinned grammar/test snapshots, release-labelled reports.[1][2] |
451
+ | Ruby truthiness | `nil`, `false`, and arrays collapse empty and Boolean semantics | Dedicated `Collection` and explicit three-valued logic tests. |
452
+ | Eager macro arguments | `where`, `iif`, or `aggregate` evaluates the wrong focus or invalid branch | AST closures and `FunctionSpec` argument kinds; test nested `$this` and lazy branches.[5][6] |
453
+ | Precision loss | Float/Date/Time conversions change decimal, temporal, or quantity results | Dedicated Decimal/temporal/Quantity values and boundary conversion tests.[1][7] |
454
+ | FHIR choice fields | JSON lookup misses logical `value` or returns the wrong choice | Versioned model provider with choice maps and R4 fixtures.[7][9] |
455
+ | Mutable evaluation state | Compiled expressions leak `$this`, variables, traces, or caches across calls | Immutable AST/registry; fresh per-call context; concurrency tests. |
456
+ | Host I/O surprises | `resolve` or terminology calls leak data or make pure tests nondeterministic | Explicit `HostServices`, disabled by default, timeouts/cancellation, host-dependent classification. |
457
+ | Error incompatibility | Consumers depend on another engine's exception text/class | Stable Ruby error codes/spans; compatibility adapter only where needed. |
458
+ | Stale implementation prose | README feature claims lag actual code, especially in `fhirpath.js` | Pin source/package/test revisions and make code-level behavior plus shared tests authoritative.[9][10] |
459
+ | License contamination | Generated/copied code or runtime dependencies impose unnoticed obligations | Original Ruby implementation, dependency inventory, NOTICE policy, and release audit. |
460
+
461
+ ## 9. First vertical implementation slice
462
+
463
+ The first slice is intentionally narrow but end-to-end:
464
+
465
+ > Parse and evaluate path navigation, literals, indexers, `where`, `select`, `first`, `exists`, equality, and Boolean logic over plain Ruby Hash/Array/object data, with source locations and deterministic errors.
466
+
467
+ It should include:
468
+
469
+ 1. a pinned parser grammar subset with complete-input validation;
470
+ 2. immutable AST nodes and source spans;
471
+ 3. `Collection`, `EvaluationContext`, and nested focus closures;
472
+ 4. `PlainModel` navigation for Hash, Array, and simple Ruby objects;
473
+ 5. Boolean/string/integer/decimal literals needed by the slice;
474
+ 6. member navigation and indexer semantics;
475
+ 7. delayed `where` and `select` predicates;
476
+ 8. `first()` and `exists()` with and without criteria;
477
+ 9. equality plus `and`/`or`/`implies` behavior for empty and singleton operands;
478
+ 10. `FHIRPath.parse`, `compile`, `evaluate`, and `evaluate_first`; and
479
+ 11. focused tests for valid expressions, invalid/trailing input, empty navigation, singleton violations, nested focus, and deterministic error fields.
480
+
481
+ The slice is complete only when the same compiled expression can be evaluated against multiple independent resources without shared state. It must not add FHIR R4 metadata, terminology, `resolve()`, network I/O, or STU3-only syntax unless a test demonstrates that the slice cannot be expressed without it.
482
+
483
+ ## 10. Open decisions before implementation
484
+
485
+ 1. Choose ANTLR-generated parsing or a Ruby-native parser after a small parser/error-quality spike; either choice must implement the pinned HL7 grammar and complete-input rule.
486
+ 2. Confirm whether `Collection` remains the public result object or whether a compatibility layer also exposes a plain Array view.
487
+ 3. Select the first FHIR model dependency and release after the pure slice; R4 is the likely first adapter because the project’s surrounding interoperability work targets R4, but this is a project decision, not a core-language requirement.
488
+ 4. Select a Decimal/temporal/UCUM dependency policy and document license compatibility before adding it to the gem.
489
+ 5. Decide whether host-defined functions are namespaced and whether they can shadow standard functions; default recommendation is no shadowing.
490
+ 6. Define cache ownership and maximum compiled-expression lifetime before enabling a global cache.
491
+
492
+ ## Sources
493
+
494
+ [1] https://raw.githubusercontent.com/HL7/FHIRPath/master/input/pages/index.md — HL7 FHIRPath specification
495
+ [2] https://raw.githubusercontent.com/HL7/FHIRPath/master/input/images/fhirpath.g4 — HL7 FHIRPath grammar
496
+ [3] https://raw.githubusercontent.com/HL7/FHIRPath/master/input/pages/tests.md — HL7 FHIRPath tests page
497
+ [4] https://raw.githubusercontent.com/beda-software/fhirpath-py/master/fhirpathpy/__init__.py — fhirpath-py public API
498
+ [5] https://raw.githubusercontent.com/beda-software/fhirpath-py/master/fhirpathpy/engine/__init__.py — fhirpath-py engine
499
+ [6] https://raw.githubusercontent.com/beda-software/fhirpath-py/master/fhirpathpy/engine/invocations/__init__.py — fhirpath-py invocation registry
500
+ [7] https://raw.githubusercontent.com/beda-software/fhirpath-py/master/fhirpathpy/engine/nodes.py — fhirpath-py nodes and values
501
+ [8] https://raw.githubusercontent.com/beda-software/fhirpath-py/master/tests/conftest.py — fhirpath-py test harness
502
+ [9] https://raw.githubusercontent.com/HL7/fhirpath.js/master/README.md — HL7 fhirpath.js README
503
+ [10] https://raw.githubusercontent.com/HL7/fhirpath.js/master/package.json — HL7 fhirpath.js package metadata
504
+ [11] https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/develop/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs — Firely FhirPathCompiler
505
+ [12] https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/develop/src/Hl7.Fhir.Base/FhirPath/FhirEvaluationContext.cs — Firely FhirEvaluationContext
506
+ [13] https://raw.githubusercontent.com/hapifhir/hapi-fhir/master/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java — HAPI IFhirPath API
507
+ [14] https://raw.githubusercontent.com/hapifhir/hapi-fhir/master/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java — HAPI FHIRPath evaluation context
@@ -0,0 +1,67 @@
1
+ # Conformance and differential workflow
2
+
3
+ The project distinguishes specification conformance from compatibility evidence.
4
+
5
+ - The HL7 FHIRPath specification and official shared test cases are normative.
6
+ - Checked-in JSONL vectors are small, reviewable regression probes inspired by observed behavior in `fhirpath-py`.
7
+ - No Python runtime is required to run the Ruby vector harness.
8
+ - The repository ships a Ruby-only importer for pinned official XML subsets and the pinned `fhirpath-py` YAML case format; complete conformance remains deferred.
9
+
10
+ ## Run the checked-in vectors
11
+
12
+ From a clean checkout:
13
+
14
+ ```sh
15
+ bundle install
16
+ bundle exec ruby script/run_vectors.rb conformance/core.jsonl
17
+ ```
18
+
19
+ The same check is available as:
20
+
21
+ ```sh
22
+ bundle exec rake vectors
23
+ ```
24
+
25
+ The runner emits JSON containing `total`, counts for each classification, and per-case actual results/errors. Classifications are:
26
+
27
+ - `pass`: actual result or expected structured error matches;
28
+ - `defect`: behavior differs from the vector;
29
+ - `unsupported`: the Ruby engine raised `UnsupportedFeatureError`;
30
+ - `host-dependent`: evaluation needs an unavailable host service; and
31
+ - `not-run`: reserved for cases intentionally skipped by a future importer.
32
+
33
+ A vector has this shape:
34
+
35
+ ```json
36
+ {"id":"eq-string-001","target":"2.0.0","expression":"'ab c' ~ 'Ab C'","resource":{},"variables":{},"expected":[true],"origin":{"suite":"fhirpath-py","commit":"19f6316","case":"manual"}}
37
+ ```
38
+
39
+ For errors, use an expected object instead of treating any exception as success:
40
+
41
+ ```json
42
+ {"id":"bad-input-001","target":"2.0.0","expression":"1 ???","resource":{},"expected":[],"error":{"class":"FHIRPath::ParseError","code":"invalid_token"},"origin":{"suite":"manual","case":"parser"}}
43
+ ```
44
+
45
+ Values that JSON cannot represent should use explicit tagged values when that family is implemented, for example `{"$type":"decimal","value":"1.10"}`. Keep each vector's expression, target release, fixture, expected result/error, and provenance together.
46
+
47
+ ## Adding a vector
48
+
49
+ 1. Confirm the expected behavior against the HL7 specification.
50
+ 2. If using another implementation as a probe, record its exact commit and case in `origin`; never copy implementation internals into the gem.
51
+ 3. Add a focused Ruby test for the behavior.
52
+ 4. Add the JSONL vector and run the complete test, lint, build, and vector commands.
53
+ 5. Update `docs/feature-matrix.md`, README limitations, and `CHANGELOG.md` if the public scope changes.
54
+
55
+ ## Import a pinned suite subset
56
+
57
+ The importer accepts the checked-in manifest and a local checkout of its pinned source:
58
+
59
+ ```sh
60
+ bundle exec ruby script/import_vectors.rb /path/to/fhir-test-cases
61
+ ```
62
+
63
+ It emits one JSON record per selected XML case. XML fixtures are never converted heuristically: when a verified same-resource JSON fixture is available, that JSON is used while `input_fixture` retains the original XML path and `fixture_source` records the normalized source. Otherwise the record is retained as `not-run` with an explicit reason. Disabled cases are also retained as `not-run` and are never evaluated.
64
+
65
+ The same importer accepts a `fhirpath-py` YAML case file when initialized with its checkout as `source_root`. YAML is parsed with safe loading, group and `disable` state are preserved, expression lists become independent records, and each record keeps the suite commit and original fixture path. `error: true` means a FHIRPath error is expected; an unrelated Ruby `StandardError` remains a defect.
66
+
67
+ Every record includes `suite`, `suite_commit`, `expression`, `input_fixture`, `model`, `host_features`, `expected`, `target`, and provenance. Runner reports include per-capability totals for `pass`, `defect`, `unsupported`, `host-dependent`, and `not-run`. Unsupported and host-dependent cases remain evidence rather than passes; defects and unexplained skips block release checks.
@@ -0,0 +1,49 @@
1
+ # Feature and capability matrix
2
+
3
+ Status: `0.2.0.pre1`; target release: FHIRPath `2.0.0`; publication contract: [`support-matrix.md`](support-matrix.md)
4
+
5
+ This matrix is deliberately conservative. `Supported` means the behavior is exercised by the Ruby test suite or the checked-in vector corpus. `Deferred` means callers should expect a structured unsupported/unknown error. `Host-dependent` requires an adapter or injected service that is not shipped here.
6
+
7
+ | Area | Status | Evidence / boundary |
8
+ |---|---|---|
9
+ | `require "fhirpath"`, version | Supported | `test/fhirpath_test.rb` |
10
+ | Parse, immutable AST, source spans | Supported | foundation/parser tests |
11
+ | Complete-input validation | Supported | parser regression tests |
12
+ | String, Boolean, integer, decimal literals | Supported | foundation/core compatibility tests |
13
+ | Scientific notation | Supported | core compatibility tests |
14
+ | Empty and comma-separated collections | Supported | parser/evaluator tests |
15
+ | Hash/Array/plain object navigation | Supported | foundation tests; `PlainModel` |
16
+ | Unary/numeric arithmetic and string `+` | Supported | parity/core compatibility tests; `+` propagates empty operands; a zero divisor for `/`, `div`, `mod` yields an empty collection, while `+`, `-`, `*` operate on zero normally |
17
+ | Relational comparison | Supported | parity/core compatibility tests |
18
+ | Collection equality/equivalence | Supported | core compatibility tests and vectors |
19
+ | Finite JSON `Float` treated as `Decimal` | Supported | evaluator correctness tests; a finite `Float` (e.g. from `JSON.parse`) compares, equals, arithmetically combines, and satisfies `is Decimal`; `NaN`/`Infinity` are rejected as non-numeric |
20
+ | String `&` concatenation | Supported | core compatibility tests; empty operands are treated as `''` |
21
+ | Empty-aware Boolean operators | Supported | foundation/core compatibility tests |
22
+ | Union, `in`, `contains`, `is`, `as` | Supported | core compatibility tests and vectors; union eliminates duplicates from both operands using `=` equality in first-seen order; `in`/`contains` require a singleton operand and follow the empty-collection rules; `is`/`as` test built-in primitive types by runtime value and, when a model provider resolves the value, also test the FHIR logical type recorded by navigation (e.g. `Observation.value is Quantity`), with `as` passing the value through unchanged on a match and yielding the empty collection on a mismatch |
23
+ | Indexers | Supported | foundation/parity tests |
24
+ | `where`, `select`, `first`, `last`, `tail`, `take`, `skip`, `exists` | Supported | `test/subsetting_functions_test.rb` |
25
+ | Aggregate functions `count()`, `sum()`, `avg()`, `max()`, `min()` | Supported | `test/aggregate_functions_test.rb` and aggregate vectors. `count()` follows FHIRPath 2.0.0 (integer count; empty -> `[0]`). `sum`/`avg`/`max`/`min` are FHIRPath 3.0.0 STU3 aggregate additions (published 2026-07-28; absent from 2.0.0 and the 3.0.0 ballot) shipped in the standard registry: empty input -> empty; `sum()`/`avg()` accept numeric items only (`TypeError` code `expected_number` otherwise), sum mixed Integer/Decimal input through Decimal, and `avg()` converts Integer items to Decimal before dividing; `max()`/`min()` use comparison-operator semantics for numeric and string items (incompatible item types raise `TypeError` code `incompatible_comparison`); no input mutation. The STU3 subset is surfaced on the capability object: `Capability.current` keeps `fhirpath` `2.0.0` and declares marker `stu3-aggregate-functions` in `trial_use` (capability surface tests in the same file) |
26
+ | `empty`, `not`, `all`, Boolean aggregates | Supported | core compatibility tests |
27
+ | `$this`, `$index`, `$total` | Supported | parity tests |
28
+ | Explicit external constants | Supported | foundation/core compatibility tests; values may come from `variables:` or an explicitly injected `HostServices` constant provider |
29
+ | Missing external constant provider | Supported | `test/host_services_test.rb`; raises `UnknownConstantError` with code `:unknown_constant` and performs no fallback I/O |
30
+ | Constant-provider failures and redaction | Supported | `test/host_services_test.rb`; raises generic `HostError` without retaining constant-provider exceptions as public causes or exposing their detail in diagnostics |
31
+ | Host callback configuration/reentrancy | Supported | `test/host_services_test.rb`; `HostServices` is immutable and each evaluation receives a fresh context |
32
+ | Custom registered functions | Supported | API/foundation tests |
33
+ | Compiled-expression reuse | Supported | API/foundation tests |
34
+ | Stable structured engine errors | Supported | API/foundation/parser tests |
35
+ | Date/time literals and values | Deferred | no temporal value implementation |
36
+ | Quantity/UCUM | Deferred | no unit service or quantity implementation |
37
+ | Advanced conversion/math/string/regex | Deferred | not in standard registry |
38
+ | FHIR R4 model adapter (`model: :r4`) | Supported | `test/r4_model_test.rb`; dependency-free `FHIRPath::FHIR::R4::ModelProvider` |
39
+ | FHIR R4 `Observation.value[x]` logical navigation | Supported | R4 choice vectors; `valueQuantity` and `valueString` resolve through `value`, absent choice is empty |
40
+ | FHIR R4 logical-type `is`/`as` over resolved choice values | Supported | `test/r4_type_operator_test.rb` and R4 choice vectors; navigation records the resolved choice variant's FHIR logical type (`Quantity`, `string`, ...) and `is`/`as` test against it; empty-in/empty-out and PlainModel (no model metadata) behavior are covered; the type is recorded for collections produced directly by navigation (operators that rebuild collections, such as `union`, do not yet propagate it); resource-level type tests and `ofType()` remain deferred |
41
+ | FHIR R5 model adapter | Deferred | no R5 provider |
42
+ | Broader FHIR choice elements and primitive extensions | Host-dependent | first R4 slice only covers `Observation.value[x]` |
43
+ | `resolve()` and terminology | Host-dependent | requires injected host services |
44
+ | Official HL7 shared test suite | Deferred | importer is not yet bundled |
45
+ | FHIRPath 3.0 STU3 aggregate functions (`sum`, `avg`, `max`, `min`) | Supported | shipped by default as the first STU3-subset additions to the standard registry — a deliberate, documented exception (declared `stu3-aggregate-functions` in `Capability.current.trial_use` with `fhirpath` staying `2.0.0`; see `docs/api.md` and `docs/support-matrix.md`); semantics follow the aggregate row above |
46
+ | Other FHIRPath 3.0 STU3 features | Deferred | not enabled by default |
47
+ | Network I/O/global evaluator state | Not supported by design | pure evaluation boundary |
48
+
49
+ The matrix is a release-review aid, not a conformance percentage. A future release must update it together with tests, capability output, and the changelog.