|
| 1 | +# Federation verification procedure |
| 2 | + |
| 3 | +Manual verification procedure for the federated-terminology wire-up in |
| 4 | +`MeasureEvaluator`. The automated tests |
| 5 | +(`MeasureEvaluatorFederationTests`) protect against regressions with small |
| 6 | +in-code fixtures; this procedure verifies the same invariants against |
| 7 | +production-shaped IG bundles. |
| 8 | + |
| 9 | +## What is being verified |
| 10 | + |
| 11 | +Three invariants of the composition in |
| 12 | +`MeasureEvaluator.buildRepository()`: |
| 13 | + |
| 14 | +1. **Federation preserves results.** For a bundle that carries its own |
| 15 | + ValueSets, evaluating with federation on and off must produce the same |
| 16 | + MeasureReport. If they diverge, terminology is being resolved |
| 17 | + differently in the two paths — the wire-up has a bug. |
| 18 | +2. **Federation consults the bundle first.** When the bundle carries the |
| 19 | + ValueSets it needs, no remote calls should be made. If the remote is |
| 20 | + consulted at all, the federation is routing wholesale rather than |
| 21 | + falling through, and IG-embedded ValueSet versions would be silently |
| 22 | + ignored. |
| 23 | +3. **Federation falls through when the bundle is empty.** When ValueSets |
| 24 | + are stripped from the bundle, the remote must be consulted and the |
| 25 | + report must still match the reference result. Confirms the fallback |
| 26 | + half of the composition actually works. |
| 27 | + |
| 28 | +## Prerequisites |
| 29 | + |
| 30 | +- The `nhsn-measures` repository checked out and built. Bundles are read |
| 31 | + from its `bundles/` and example subject data from its `output/` |
| 32 | + directory. |
| 33 | +- `mvn` on `PATH`. The verifier runs against the test-scope classpath, so |
| 34 | + no separate WireMock or NHSN-specific dependency setup is needed — the |
| 35 | + project pom already includes them for test scope. |
| 36 | + |
| 37 | +## Invocation |
| 38 | + |
| 39 | +```bash |
| 40 | +cd Java |
| 41 | + |
| 42 | +mvn -pl measureeval exec:java -Dexec.classpathScope=test \ |
| 43 | + -Dexec.mainClass=com.lantanagroup.link.measureeval.audit.FederatedTerminologyVerifier \ |
| 44 | + -Dexec.args="\ |
| 45 | + --measure-bundle /path/to/nhsn-measures/bundles/measure/NHSNAcuteCareHospitalDailyInitialPopulation/NHSNAcuteCareHospitalDailyInitialPopulation-bundle.json \ |
| 46 | + --subjects-dir /path/to/nhsn-measures/output \ |
| 47 | + --period-start 2024-01-01 \ |
| 48 | + --period-end 2024-12-31" |
| 49 | +``` |
| 50 | + |
| 51 | +Flags: |
| 52 | + |
| 53 | +| Flag | Required | Default | Purpose | |
| 54 | +|------|----------|---------|---------| |
| 55 | +| `--measure-bundle <path>` | yes | — | Path to the measure bundle (Measure + Library + ValueSets). | |
| 56 | +| `--subjects-dir <dir>` | yes | — | Directory containing subject bundles. The verifier picks up JSON files whose name contains `subject`. | |
| 57 | +| `--ts-port <port>` | no | 8089 | Port for the embedded WireMock terminology server. Change if 8089 is in use. | |
| 58 | +| `--period-start <yyyy-mm-dd>` | no | 2024-01-01 | Measurement period start passed to CQL evaluation. | |
| 59 | +| `--period-end <yyyy-mm-dd>` | no | 2024-12-31 | Measurement period end. | |
| 60 | + |
| 61 | +## Expected output |
| 62 | + |
| 63 | +``` |
| 64 | +Measure bundle: /path/to/.../NHSNAcuteCareHospitalDailyInitialPopulation-bundle.json |
| 65 | +Subjects dir: /path/to/nhsn-measures/output |
| 66 | +Mock TS port: 8089 |
| 67 | +Period: 2024-01-01 .. 2024-12-31 |
| 68 | +
|
| 69 | +Measure bundle carries 34 ValueSet(s); stripped variant contains 4 entries |
| 70 | +Found 4 subject bundle(s): |
| 71 | + - Bundle-bundle-example-ach-daily-subject-influenzatherapeutic.json |
| 72 | + - Bundle-bundle-example-ach-daily-subject-initialpopulationpass.json |
| 73 | + - Bundle-bundle-example-ach-daily-subject-negativepcr.json |
| 74 | + - Bundle-bundle-example-ach-daily-subject-rsvlabbtg.json |
| 75 | +
|
| 76 | +Subject | A | B | C | Bcalls | Ccalls | Verdict |
| 77 | +----------------------------------------------|------|------|------|--------|--------|-------- |
| 78 | + Bundle-bundle-example-ach-daily-subject-... | 1 | 1 | 1 | 0 | 12 | ✓ PASS |
| 79 | + Bundle-bundle-example-ach-daily-subject-... | 1 | 1 | 1 | 0 | 12 | ✓ PASS |
| 80 | + Bundle-bundle-example-ach-daily-subject-... | 1 | 1 | 1 | 0 | 12 | ✓ PASS |
| 81 | + Bundle-bundle-example-ach-daily-subject-... | 0 | 0 | 0 | 0 | 12 | ✓ PASS |
| 82 | +
|
| 83 | +Overall: 4/4 PASS |
| 84 | +
|
| 85 | +Legend: |
| 86 | + A = initial-population, no federation, full bundle |
| 87 | + B = initial-population, federation on, full bundle (must equal A) |
| 88 | + C = initial-population, federation on, stripped bundle (must equal A) |
| 89 | + Bcalls= mock TS requests during scenario B (must be 0) |
| 90 | + Ccalls= mock TS requests during scenario C (must be > 0) |
| 91 | +``` |
| 92 | + |
| 93 | +## Interpreting the output |
| 94 | + |
| 95 | +### PASS |
| 96 | + |
| 97 | +All three columns (A, B, C) match; Bcalls is 0; Ccalls is greater than 0. |
| 98 | +This means: |
| 99 | + |
| 100 | +- The initial-population count is stable across the federated and |
| 101 | + non-federated paths for this measure and subject. |
| 102 | +- The bundle-first behavior held: when the bundle had the ValueSets, the |
| 103 | + remote TS was never touched. |
| 104 | +- The fallback behavior held: when the ValueSets were removed, the mock |
| 105 | + TS served them and the result came out the same. |
| 106 | + |
| 107 | +### FAIL: results diverge |
| 108 | + |
| 109 | +Columns A, B, C are not all equal. Someone in the chain is resolving |
| 110 | +terminology differently in the federated path. Common causes: |
| 111 | + |
| 112 | +- `Repositories.proxy(...)` invocation is wrong — check the arg order |
| 113 | + `(data, content, terminology)` in `MeasureEvaluator.buildRepository()`. |
| 114 | +- A CQF upgrade changed the semantics of `FederatedRepository` or |
| 115 | + `ProxyRepository` and our composition no longer behaves as before. |
| 116 | + Re-read the CQF `Repositories.proxy` bytecode notes in |
| 117 | + `MeasureEvaluator.buildRepository()`'s Javadoc. |
| 118 | + |
| 119 | +### FAIL: scenario B hit remote |
| 120 | + |
| 121 | +Bcalls is not 0. Federation is not consulting the bundle first — every |
| 122 | +terminology lookup is going straight to the remote TS, and any bundle- |
| 123 | +embedded ValueSet version is being silently ignored. This is the failure |
| 124 | +mode our composition was specifically designed to prevent, so a Bcalls |
| 125 | +count above 0 always warrants investigation. |
| 126 | + |
| 127 | +- Likeliest cause: `FederatedRepository` is being constructed with the |
| 128 | + arguments in the wrong order (remote first, bundle second). Check |
| 129 | + `buildRepository()`. |
| 130 | + |
| 131 | +### FAIL: scenario C did not use remote |
| 132 | + |
| 133 | +Ccalls is 0. Stripping the ValueSets from the bundle didn't force any |
| 134 | +remote calls, which means either: |
| 135 | + |
| 136 | +- The measure being evaluated doesn't actually reference the stripped |
| 137 | + ValueSets in its CQL — try a different measure. |
| 138 | +- CQF isn't reaching the remote tier at all — the composition is broken. |
| 139 | + |
| 140 | +## Cross-referencing against the shipped MeasureReport |
| 141 | + |
| 142 | +Each of the NHSN example subject bundles also contains a pre-computed |
| 143 | +`MeasureReport`. Reading the initial-population count from that reference |
| 144 | +report and comparing to columns A / B / C is a stronger check than |
| 145 | +comparing A / B / C against each other alone. If the verifier reports |
| 146 | +PASS but the initial-population value differs from the shipped |
| 147 | +MeasureReport, the numeric result is wrong regardless of federation |
| 148 | +behavior — that's a measure-evaluation bug, not a federation bug, but |
| 149 | +worth surfacing. |
| 150 | + |
| 151 | +Reading the shipped MeasureReport by hand: |
| 152 | + |
| 153 | +```bash |
| 154 | +jq '.entry[] | select(.resource.resourceType=="MeasureReport") |
| 155 | + | .resource.group[0].population[0].count' \ |
| 156 | + /path/to/nhsn-measures/output/Bundle-bundle-example-ach-daily-subject-initialpopulationpass.json |
| 157 | +``` |
| 158 | + |
| 159 | +Should return an integer that agrees with column A above. |
| 160 | + |
| 161 | +## When to re-run |
| 162 | + |
| 163 | +- After any change to `MeasureEvaluator.buildRepository()`. |
| 164 | +- After a `cqf-fhir` version bump — behavior of `FederatedRepository`, |
| 165 | + `ProxyRepository`, or `Repositories.proxy(...)` could shift. |
| 166 | +- Before shipping any change that touches `LinkConfig.fhirTerminologyServiceUrl`, |
| 167 | + the `remoteTerminologyClient` bean, or the `MeasureDefinitionController` |
| 168 | + wire-up. |
| 169 | + |
| 170 | +## Related |
| 171 | + |
| 172 | +- `Java/measureeval/src/test/.../MeasureEvaluatorFederationTests.java` |
| 173 | + — the CI-friendly regression tests. Same invariants, smaller fixtures. |
| 174 | +- `Java/measureeval/src/main/.../services/MeasureEvaluator.java` — |
| 175 | + `buildRepository()` and its Javadoc explain the composition choice. |
| 176 | +- `Java/validation/src/main/.../providers/REMOTE-TERM-COST-ANALYSIS.md` |
| 177 | + — cost profile and open items for the validation service's remote |
| 178 | + terminology client. |
0 commit comments