---
name: pathling-yaml-exclusions
description: >
  Manage the YAML conformance-test exclusion baselines at
  fhirpath/src/test/resources/fhirpath-js/config.yaml and fhirpath-ptl/config.yaml. Use this skill
  when a newly implemented FHIRPath feature makes previously-excluded conformance cases pass, when
  the build fails with "Excluded test passed when expected outcome was ...", when auditing the
  baseline for stale or mislabelled entries, or when deciding how to record a case that Pathling
  cannot yet handle. Trigger on phrases like "exclusion", "excluded test", "config.yaml",
  "known failures", "conformance baseline", "YamlReferenceImplTest", "YamlFhirPathTest", or any
  request to remove, narrow, or reclassify a test exclusion.
---

# Pathling YAML test exclusions

Pathling runs two YAML conformance suites, each with its own exclusion baseline:

| Test class | Config | Corpus |
|---|---|---|
| `YamlReferenceImplTest` | `fhirpath-js/config.yaml` | The fhirpath.js reference test corpus |
| `YamlFhirPathTest` | `fhirpath-ptl/config.yaml` | Pathling's own cases |

An exclusion is not a mute button. It is an **assertion about how a case currently fails**.

## The baseline polices itself

Excluded cases are still executed. The runner asserts the case produces the recorded `outcome`,
and only then reports it as skipped. If an excluded case starts passing, the build fails with:

```
Excluded test passed when expected outcome was error
```

That is the machine-checkable done-signal for feature work: implement a feature, and every
exclusion it obsoletes turns the build red until it is cleaned up. You cannot silently leave a
stale exclusion behind.

| `outcome` | Meaning | Build fails when |
|---|---|---|
| `error` (default) | The case throws an exception | It passes, or fails an assertion instead of throwing |
| `failure` | The case runs but produces the wrong result | It passes, or throws instead of failing |
| `pass` | The case passes, but is excluded for another reason | It fails or throws |
| *explicitly null* (`outcome:`) | The case is **not run at all** | Never — unverified |

Prefer `error` or `failure`. An explicitly-null `outcome` opts the case out of verification
entirely, which is how baselines rot. Use it only when running the case is itself the problem
(a hang, an OOM), and say so in the `comment`.

`UnsupportedFhirPathFeatureError` is handled separately — such cases are skipped regardless of
`outcome`.

## Rule anatomy

```yaml
excludeSet:
  - title: Global exclusions        # required
    comment: |                      # optional
      Why this block exists.
    exclude:
      - title: Unsupported resources # required
        type: feature                # convention only — see taxonomy below
        id: "#2418"                  # issue this is tracked under
        outcome: failure             # defaults to "error"
        comment: |                   # why, and what would resolve it
          ...
        expression:                  # matcher — see below
          - "^StructureDefinition"
```

### Matchers

A rule may carry several matchers; they are **OR**-ed. A case is excluded by the **first** rule
whose matchers match it.

| Matcher | Semantics |
|---|---|
| `any: [...]` | **Substring** match against the case's expression **or** its description |
| `expression: [...]` | **Regex**, unanchored (find, not full match), against the expression only |
| `function: [...]` | Matches by function name |
| `spel: [...]` | SpEL predicate over the case |

Prefer `any` with a complete expression string for surgical exclusions — it is the easiest to
verify and the hardest to over-match. Reach for `expression` regexes only when a genuine family
of cases shares a shape, and remember they are unanchored: `"toQuantity"` also matches
`convertsToQuantity`.

### Fields that do not work

Two fields are parsed but never applied. Do not rely on them, and do not add new uses:

- **`glob` on an exclude block** — documented as scoping a block to certain test files, but never
  read. **Every block applies to every case file.** A rule you believe is scoped to one corpus
  file will silently mask matching cases everywhere.
- **`desc` on a rule** — declared as a matcher but never converted to a predicate. Use `any`,
  which already matches descriptions.

Separately, the system property `au.csiro.pathling.test.yaml.exclusionsOnly` is read and logged
but never applied — it does not filter anything. Ignore it.

`disabled: true` on a rule, by contrast, does work: it suppresses the rule entirely, so the cases
it was masking run unexcluded. See §3 below for using it, and the `disabledExclusions` property,
to test whether a rule is still earning its place.

## Type taxonomy

`type` is a free-form string with no validation. Current usage in `fhirpath-js/config.yaml`:

| `type` | Count | Meaning |
|---|---|---|
| `feature` | 23 | Capability not yet implemented |
| `new-feature` | 9 | Synonym for `feature` — **being retired** |
| `bug` | 5 | Pathling defect; the case should pass |
| `wontfix` | 23 | Pathling deliberately diverges, or the case itself is invalid |

**Use `feature`, not `new-feature`.** When you touch a block containing `new-feature`, migrate it.

`wontfix` deserves scrutiny. It currently absorbs two different things: genuinely invalid test
cases, and unimplemented capabilities mislabelled as invalid. For example a `wontfix` block titled
*"Parse error — not valid FHIRPath syntax"* contains `('a'|'b'|'c').join(',')`,
`(1|2|3).sum()` and `(1|2|3).aggregate($this+$total, 0)` — all valid FHIRPath, and all really
`feature` gaps. If a `wontfix` entry you encounter is actually a capability gap, reclassify it and
give it an issue id.

## The sweep

Run this whenever a feature lands, and whenever the build reports an excluded test passing.

### 1. Find the rules in scope

Search by every handle the feature has — function name, operator, expression fragment, and the
issue number:

```bash
cd fhirpath/src/test/resources
rg -n 'toQuantity' fhirpath-js/config.yaml fhirpath-ptl/config.yaml
rg -n '#2391' fhirpath-js/config.yaml fhirpath-ptl/config.yaml
```

**An empty result is not evidence the feature is fully working.** Most open FHIRPath issues own no
exclusion at all — the corpus may simply not cover the feature. When nothing matches, say so
explicitly in your report rather than concluding there was nothing to do.

### 2. Decide, per rule

| Decision | When |
|---|---|
| **REMOVE** | Every case the rule matches now passes |
| **NARROW** | The matcher is over-broad — some cases pass now, others still legitimately fail. Rewrite it, preferring `any` with exact expressions, so it catches only the residual failures |
| **RECLASSIFY** | The residual failure is real but recorded under the wrong `type`, or the `outcome` changed (a case that used to throw now returns a wrong result → `error` becomes `failure`) |
| **KEEP** | The rule describes a real gap unrelated to this change |

### 3. Verify

```bash
mvn spotless:apply -pl fhirpath
mvn test -pl fhirpath -Dtest=YamlReferenceImplTest
mvn test -pl fhirpath -Dtest=YamlFhirPathTest
```

Both must be 0 failures, 0 errors. Two failure modes to read correctly:

- *"Excluded test passed when expected outcome was error"* → the rule is now obsolete for that
  case. REMOVE or NARROW it.
- A plain failure on a case you just un-excluded → the feature does not actually cover it. Restore
  the exclusion (NARROW it) and say which cases remain, or fix the implementation.

To check whether a specific rule is still needed without editing the file, disable it by id:

```bash
mvn test -pl fhirpath -Dtest=YamlReferenceImplTest \
  -Dau.csiro.pathling.test.yaml.disabledExclusions='#2391,#2418'
```

Cases the rule was masking now run for real. This is the cheapest way to answer "is this
exclusion still earning its place?"

### 4. Report

State per rule what changed and why: rule title, decision, and the evidence. Report the suite
output, not a claim that it is green. If the sweep found nothing in scope, report that too.

## Hygiene for any rule you add or modify

- `feature` / `bug` → carries `id: "#NNNN"` pointing at an **open** Pathling issue. Verify with
  `gh issue view NNNN --json state,title`. Several ids in the baseline point at closed issues
  (#2163, #2398, #2383) — fix those when you touch their block.
- `wontfix` → needs a `comment` justifying the divergence or explaining why the case is invalid.
  No id required, but confirm it is genuinely not a capability gap.
- Every rule needs a `title`, and a `comment` whenever the title alone does not explain the
  decision.
- The matcher must match at least one real case. A rule matching nothing is dead weight; the
  runner will not tell you, so check with `rg` against the case files under
  `fhirpath-js/cases/` or `fhirpath-ptl/cases/`.
- No two rules should match the same case — the first wins, and the second becomes invisible.
- Set `outcome` deliberately. Do not leave it null to make a stubborn case go away.

## Do not

- Add an exclusion to turn a red build green when the cause is a regression in your own change.
  An exclusion records a *pre-existing* gap.
- Widen an existing matcher to swallow a new failure. Add a separate, narrower rule instead, so
  the two gaps stay independently trackable.
- Rely on `glob`, `desc`, or `exclusionsOnly` (see above).
