--- name: pathling-fhirpath-review description: > Review a FHIRPath implementation change in Pathling against a correctness rubric covering collection semantics, empty propagation, column cardinality, type coercion, error-vs-empty behaviour, spec fidelity, and test coverage. Use this skill when reviewing a branch, diff, or PR that adds or changes a FHIRPath function, operator, or evaluation behaviour, or when the user asks for a correctness review of FHIRPath work. Trigger on phrases like "review this FHIRPath change", "review the diff", "check this implementation", "is this correct", or a review request naming a FHIRPath function or operator. --- # Pathling FHIRPath review A correctness rubric for FHIRPath implementation changes. It exists so a reviewer checks the things that actually break in this codebase, rather than producing generic code-review output. Review is most useful in a **fresh context** that sees the diff and this rubric but not the reasoning that produced the change. When invoked as part of a larger workflow, dispatch a subagent with the diff range and this rubric rather than reviewing inline. ## Establish the range ```bash BASE_SHA=$(git merge-base origin/main HEAD) git diff --stat $BASE_SHA..HEAD git diff $BASE_SHA..HEAD ``` Read the linked issue and the governing spec section before judging behaviour. Use the `fhirpath-spec` skill for spec lookups — the spec decides, not intuition. When this rubric is being applied inside a dispatched subagent — the usual case, and what the paragraph above recommends — there is no user to answer a question, so pass `--unattended` to `fhirpath-spec`. Without it, a missing reference-implementation pin leaves that skill waiting on an answer that cannot arrive. ## Rubric Work through these in order. The first five are where FHIRPath implementations actually go wrong. ### 1. Collection semantics Every FHIRPath expression evaluates to a collection. Check each of the three input shapes is handled as the spec requires: - **Empty** — does `{}` propagate, or does the spec require a value (`count()` → `0`, `empty()` → `true`)? Empty-propagation-by-default is right for most functions and wrong for existence and aggregate functions. - **Singleton** — the common case, usually correct. - **Multi-item** — where a function expects a single item, the spec's singleton evaluation rules apply: one item is used, empty returns empty, and **multiple items are an error**. Silently taking the first item is a bug. ### 2. Column cardinality In the Spark layer a singular FHIR element is a **scalar** column and a non-singular element is an **array** column. Code that works on one can fail on the other. Confirm the change handles both, and that tests cover both — a test suite using only literals exercises neither. ### 3. Null versus empty Spark nulls and FHIRPath's empty collection are related but not identical. Check that a null column value produces empty rather than propagating a null into a result struct, and that a typed-empty field behaves the same as an absent one where the spec says it should. ### 4. Type coercion and promotion - Integer to Decimal promotion in mixed arithmetic and comparison - String conversions that must return empty rather than throw on unparseable input - Date/time **partial precision**: differing precision alone does not make a comparison empty. Components are compared in order, and a difference found before the coarser operand runs out of precision gives a definite answer — `@2018-03-01 > @2018-01-01` is true, `@2012-01 = @2013` is false. The result is empty only when the precisions *differ* and the values are equal up to the coarser one, as in `@2018-03 > @2018-03-01` or `@2012-01 = @2012` — equal values at matching precision are simply equal, so `@2012 = @2012` is true. Applies to `=` as well as `<`/`>`; both directions of this have caused regressions before - Quantity units — calendar durations and UCUM units are not interchangeable above seconds ### 5. Error versus empty The spec distinguishes "return empty" from "signal an error", and they are easy to conflate. `toInteger()` on a non-numeric string returns empty; `single()` on a multi-item collection errors. Check each failure path against the spec text, and check the tests assert the right one — `testEmpty` and `testError` are not interchangeable. ### 6. Spec fidelity - Behaviour matches the spec section, including its examples - Javadoc carries an `@see` link to the governing spec section, as the existing providers do - Any deliberate divergence is called out in a comment with its reason, not left silent ### 7. Registration and annotations - `@FhirPathFunction` on the method; a new provider class also needs a `MethodDefinedFunction.mapOf` line in `StaticFunctionRegistry` - `@SqlOnFhirConformance(Profile.…)` where the function maps to a SQL-on-FHIR profile feature — check against sibling functions rather than guessing - `@Nonnull` / `@Nullable` on parameters, returns, and fields - Substantial logic lives in a `*Logic` helper (as `ConversionFunctions` delegates to `ConversionLogic`), not inline in the provider method ### 8. Project conventions Only flag these where they are actually violated — do not restate them as advice. - No inner classes, records, or enums; each type gets its own file - `final` on variables, parameters, and methods that do not change - Functional style with `Optional`/`Stream` over imperative null checks and loops, but broken after 3–4 chained calls for readability - Comments are complete sentences ending in a period, explaining intent rather than restating code - No `System.out` / `System.err` - No TODOs ### 9. Test coverage - One `@FhirPathTest` method per function, in a `*DslTest` class named by capability — except where the DSL's one-subject-per-method constraint forces a split by subject (e.g. `ExistenceFunctionsDslTest.testCount()` plus `testCountOnFhirResource()`); don't flag that as a violation - Spec examples present as test cases - Both cardinalities covered (scalar field and array field) - Empty covered in its distinct forms: `{}` literal, typed-empty field, computed empty (`where(false)`) - Error conditions asserted with `testError` - Tests assert spec-derived expectations, not observed implementation output See the `fhirpath-test-designer` skill for the dimension matrix and the DSL surface. ### 10. Exclusion baseline If the change implements a feature, the YAML exclusion baseline should have been swept — obsolete exclusions removed, over-broad matchers narrowed, mislabelled types corrected. The build catches exclusions that became obsolete, but not exclusions that are now over-broad. See the `pathling-yaml-exclusions` skill. ## Severity | Severity | Meaning | |---|---| | **Critical** | Wrong results, spec violation, crash, or a regression in existing behaviour | | **Important** | A real gap that will bite: an unhandled input shape, a missing test for behaviour the change claims to support, an annotation or registration omission | | **Minor** | Naming, structure, or convention issues with no behavioural consequence | ## What not to flag A reviewer asked to find problems will always find some, and chasing all of them produces defensive over-engineering. Stay on correctness and requirement gaps. - Style choices consistent with the surrounding code - Speculative generality — "this won't scale to a case nobody has asked for" - Performance micro-optimisation without a measurement - Requests for comments where the code is already clear - Rewrites of untouched code that merely happens to be nearby - Anything that does not survive a second read of the cited lines ## Output Report findings grouped by severity, each with a `file:line` citation, what is wrong, and the input that would demonstrate it. Then a single verdict line: ``` Ready to merge: Yes | No ``` State a finding once. If you are unsure whether something is a defect, say so and explain the uncertainty rather than promoting it to a finding or dropping it silently.