Skip to content

Fix multi-component stratifier alignment for list + scalar#1042

Merged
lukedegruchy merged 3 commits into
mainfrom
ld-20260629-stratifiers-multi-component-report-strata
Jun 1, 2026
Merged

Fix multi-component stratifier alignment for list + scalar#1042
lukedegruchy merged 3 commits into
mainfrom
ld-20260629-stratifiers-multi-component-report-strata

Conversation

@lukedegruchy
Copy link
Copy Markdown
Contributor

@lukedegruchy lukedegruchy commented May 29, 2026

This branch fixes a $evaluate-measure stratifier defect (CDO-715) in which a boolean-basis stratifier composed of one multi-value (list) component and one scalar component emitted disjoint single-component strata instead of strata containing a value from every declared component. The fix generalizes the existing "function row key" alignment pass — previously applied only to Map<?, ?> (function) component results — so it also collects alignment row keys from Iterable<?> (list) component results, then expands scalar components onto those keys. As a side effect, the empty-list / empty-map case now suppresses the affected subject's stratum contribution entirely when the stratifier as a whole produces alignment rows, matching the existing single-component empty-list semantics.

  1. Generalize collectFunctionRowKeyscollectAlignmentRowKeys, with parallel handling for Iterable<?> results that emits (subject | iterableElement(value, index)) row keys.
  2. Rename expandScalarToMatchFunctionRowKeysexpandScalarToAlignmentRowKeys and route scalar expansion through the unified alignment-keys map so list+scalar mixes align the same way function+scalar mixes already did.
  3. Add an empty-alignment-rows guard: when the stratifier overall has multi-value components but a particular subject produced none (empty Map / empty List), that subject now contributes zero strata instead of falling back to a subject-only scalar row.
  4. Extract per-entry alignment logic into a collectAlignmentRowKeysForEntry helper to keep cognitive complexity within Sonar limits after the new branch was added.
  5. Add a new R4 fixture (CohortBooleanMultiComponentListScalarStrat) plus single-subject and multi-subject tests covering the (encounter-status list × gender scalar) tuple expansion across the existing test patient set.

Worked example

CQL expressions (LibrarySimple.cql) — one returns a per-patient list, the other returns a per-patient scalar:

define "Distinct Encounter Statuses":
    distinct([Encounter] E return E.status)

define "Gender Stratification String":
    if Patient.gender = 'male' then 'male' else 'female'

For Patient/patient-9 (two Encounters with statuses finished and in-progress, gender male) these evaluate to:

"Distinct Encounter Statuses"   → { 'finished', 'in-progress' }   // List<String>
"Gender Stratification String"  → 'male'                          // String

Measure stratifier — two components, one referencing the list expression, one referencing the scalar:

"stratifier": [{
  "code": { "text": "Encounter Status and Gender" },
  "component": [
    {
      "code": { "text": "Encounter Status" },
      "criteria": { "language": "text/cql.identifier",
                    "expression": "Distinct Encounter Statuses" }
    },
    {
      "code": { "text": "Gender" },
      "criteria": { "language": "text/cql.identifier",
                    "expression": "Gender Stratification String" }
    }
  ]
}]

MeasureReport stratum — before the fix (broken: three single-component strata, gender never appears alongside encounter status):

"stratifier": [{
  "code": [{ "text": "Encounter Status and Gender" }],
  "stratum": [
    { "component": [{ "code": { "text": "Encounter Status" },
                      "value": { "text": "finished" } }],
      "population": [{ "count": 1 }] },
    { "component": [{ "code": { "text": "Encounter Status" },
                      "value": { "text": "in-progress" } }],
      "population": [{ "count": 1 }] },
    { "component": [{ "code": { "text": "Gender" },
                      "value": { "text": "male" } }],
      "population": [{ "count": 1 }] }
  ]
}]

MeasureReport stratum — after the fix (two strata, each carrying both components — one per (status, gender) tuple):

"stratifier": [{
  "code": [{ "text": "Encounter Status and Gender" }],
  "stratum": [
    { "component": [
        { "code": { "text": "Encounter Status" }, "value": { "text": "finished" } },
        { "code": { "text": "Gender" },           "value": { "text": "male"     } }
      ],
      "population": [{ "count": 1 }] },
    { "component": [
        { "code": { "text": "Encounter Status" }, "value": { "text": "in-progress" } },
        { "code": { "text": "Gender" },           "value": { "text": "male"        } }
      ],
      "population": [{ "count": 1 }] }
  ]
}]

A boolean (subject) basis stratifier with two components — one returning
a per-patient List and one returning a per-patient scalar — emitted one
stratum per component value with a single component, instead of one
stratum per unique (list-element, scalar) tuple with both components.

Root cause: collectFunctionRowKeys only collected alignment row keys from
function (Map) results, so a scalar component sharing a subject with an
iterable component fell back to a subject-only row key. The iterable's
composite per-element keys and the scalar's subject-only key never
intersected, producing disjoint single-component strata.

Generalize the collector (renamed collectAlignmentRowKeys) to also
gather row keys from iterable results, so scalars expand alongside list
elements via the existing expandScalarToAlignmentRowKeys path. Tighten
the scalar fallback: when the stratifier has multi-value components but
a given subject produced none (empty Map or List), the subject
contributes no stratum — matching the existing empty-list semantic.

Adds single-subject and multi-subject MeasureStratifierTest cases plus a
CohortBooleanMultiComponentListScalarStrat fixture that pairs the
existing "Distinct Encounter Statuses" list expression with the
"Gender Stratification String" scalar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Formatting check succeeded!

@lukedegruchy lukedegruchy changed the title Fix multi-component stratifier alignment for list + scalar (CDO-715) Fix multi-component stratifier alignment for list + scalar Jun 1, 2026
lukedegruchy and others added 2 commits June 1, 2026 14:36
Extract per-entry work into a helper method to flatten nesting and
remove duplicated qualifiedSubject/computeIfAbsent setup across the
Map and Iterable branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lukedegruchy lukedegruchy marked this pull request as ready for review June 1, 2026 18:44
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 1, 2026

@lukedegruchy lukedegruchy merged commit c516b6d into main Jun 1, 2026
9 checks passed
@lukedegruchy lukedegruchy deleted the ld-20260629-stratifiers-multi-component-report-strata branch June 1, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants