Skip to content

Pre-validate measure libraries before per-subject CQL evaluation#1024

Merged
JPercival merged 1 commit into
mainfrom
ld-20260501-cql-missing-valueset
May 4, 2026
Merged

Pre-validate measure libraries before per-subject CQL evaluation#1024
JPercival merged 1 commit into
mainfrom
ld-20260501-cql-missing-valueset

Conversation

@lukedegruchy
Copy link
Copy Markdown
Contributor

@lukedegruchy lukedegruchy commented May 1, 2026

Measures whose CQL referenced an unresolvable ValueSet (or had other fatal compile errors) failed in two awkward ways. With a function-based component stratifier, FunctionEvaluationHandler.processNonSubValueStratifier observed missing expression results when the IP failed and threw "Expression result: Initial Population is missing" — masking the underlying CqlException. In a multi-measure evaluation that masking exception then escaped the per-library inner loop in MeasureEvaluationResultHandler and was caught by the outer subject-scoped try/catch, which wrote the error to every measure def — poisoning unrelated libraries that would otherwise have evaluated cleanly. This MR introduces an upfront library-validation pass and a complementary runtime safety net so library failures are isolated and the engine-level diagnostic surfaces uniformly across all stratifier shapes.

  1. Library pre-validation before per-subject evaluation: A new MeasureLibraryPreValidator resolves each library via LibraryManager.resolveLibrary(id, errors), surfaces any severity=Error compile diagnostics, and walks the compiled ELM valueSetDef references against the engine's TerminologyProvider. Libraries that fail are recorded with the engine-level diagnostic and removed from the per-subject loop entirely. Hard library-resolution failures (CqlIncludeException, etc.) are deliberately allowed to propagate so the existing throw-based contracts in InvalidMeasureTest.evaluateThrowsErrorWhenLibraryIsMissingContent are preserved.

  2. Per-library exception check before function-evaluation handler: In MeasureEvaluationResultHandler.getEvaluationResults, the per-library inner loop now reads evaluationResultsForMultiLib.getExceptionFor(libraryVersionedIdentifier) before calling FunctionEvaluationHandler.cqlFunctionEvaluation. Function evaluation is skipped when the base CQL eval already failed, so any per-subject exception that slips past pre-validation reaches the existing per-subject error path instead of being masked by the function handler — and crucially, no longer escapes to the outer subject-scoped catch where it would pollute sibling measure defs.

  3. Multi-measure isolation guarantee: When one library in a multi-measure evaluation has unresolvable references, only that library's measure(s) are flagged. Sibling measures continue to evaluate normally — including their function-based stratifiers — and produce status=COMPLETE reports without spurious contained OperationOutcome entries.

Closes: #1022

Measures whose CQL referenced a missing ValueSet (or had other fatal
compile errors) failed in two awkward ways. With a function-based
component stratifier, FunctionEvaluationHandler.processNonSubValueStratifier
observed missing expression results when the IP failed and threw
"Expression result: Initial Population is missing" — masking the
underlying CqlException. In multi-measure evaluations, that masking
exception then escaped the per-library loop in MeasureEvaluationResultHandler
and was caught by the outer subject-scoped try/catch, which wrote the
error to every measure def — poisoning unrelated libraries that would
otherwise have evaluated cleanly.

Two complementary layers:

- MeasureLibraryPreValidator resolves each library before the per-subject
  loop, collects fatal compile errors via LibraryManager.resolveLibrary's
  error-collecting overload, and walks the compiled ELM ValueSet refs
  against the terminology provider. Failed libraries drop from the
  evaluation set with the engine-level diagnostic ("Unable to locate
  ValueSet …", or the original compile message) recorded against their
  measure defs. Hard library-resolution failures still propagate so
  existing throw-based contracts in InvalidMeasureTest are preserved.

- MeasureEvaluationResultHandler.getEvaluationResults now checks
  getExceptionFor(libraryId) before invoking cqlFunctionEvaluation, so
  any per-subject CqlException that slips past pre-validation is
  surfaced through the existing per-subject error path instead of being
  masked by the function-evaluation handler.

Tests:
- MeasureLibraryPreValidatorTest exercises compile-error, missing-
  ValueSet, fall-through, short-circuit, and multi-library isolation
  cases against real MultiLibraryIdMeasureEngineDetails / MeasureDef /
  CompositeEvaluationResultsPerMeasure instances.
- MultiMeasureIsolationTest evaluates three measures (clean / broken /
  clean) and asserts the broken one's failure does not poison the
  siblings' MeasureReports.
- Four cohortEncounterMissingValueSet* integration tests live in
  InvalidMeasureTest covering no-stratifier, scalar, value-component,
  and function-component stratifier shapes — all surfacing the same
  "Unable to locate ValueSet" diagnostic.

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

github-actions Bot commented May 1, 2026

Formatting check succeeded!

@lukedegruchy lukedegruchy marked this pull request as ready for review May 1, 2026 20:43
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 1, 2026

@JPercival JPercival merged commit d340296 into main May 4, 2026
9 checks passed
@JPercival JPercival deleted the ld-20260501-cql-missing-valueset branch May 4, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants