Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/deploy-maui-mieweb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ jobs:
node scripts/vendor-official-measure.mjs --measure CMS138FHIRTobaccoScrnCessation --catalog-id cms138 --strip-elm-annotations --complete-terminology
node scripts/vendor-official-measure.mjs --measure CMS130FHIRColorectalCancerScrn --catalog-id cms130 --strip-elm-annotations --complete-terminology
node scripts/vendor-official-measure.mjs --measure CMS165FHIRControllingHighBP --catalog-id cms165 --strip-elm-annotations --complete-terminology
node scripts/vendor-official-measure.mjs --measure CMS137FHIRSUDTxInitEngagement --catalog-id cms137 --strip-elm-annotations --complete-terminology
# The vendored artifact must not have changed — only the gitignored sidecar is new. A non-empty
# diff means the committed bundle does not match its pin, which must never reach a deploy image.
- name: The committed artifact is reproducible from its pin
Expand Down Expand Up @@ -276,7 +277,12 @@ jobs:
{key: "WORKWELL_INSTANCE", value: "maui"},
{key: "WORKWELL_SCHEDULER_ENABLED", value: "true"},
{key: "WORKWELL_VSAC_API_KEY", value: $vsac_api_key},
{key: "WORKWELL_OFFICIAL_MEASURES", value: "cms122,cms125"},
# THE FLIP (ADR-045; ADR-078, owner decision 2026-09-08): the sandbox routes the whole ACO
# computable set. cms2/cms130/cms137 have their second-engine evidence; cms165 runs on the
# profiles the corpus stamps (trustMetaProfile per measure, ADR-076 d1) and its WebChart
# ingest caveat governs the PHI phase, not the sandbox. reconcile-maui-mieweb.yml MUST match.
# No apostrophes in this block: it sits inside a single-quoted jq program.
{key: "WORKWELL_OFFICIAL_MEASURES", value: "cms122,cms125,cms2,cms130,cms165,cms137"},
{key: "WORKWELL_MAUI_CORPUS_SIZE", value: "20000"},
{key: "WORKWELL_RUN_CHUNK_SIZE", value: "500"},
{key: "WORKWELL_SCHEDULER_ANCHOR_HOUR_UTC", value: "12"},
Expand Down
29 changes: 24 additions & 5 deletions .github/workflows/flip-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ on:
type: string
default: "2026-12-31"
routed:
description: "The deployment's CURRENT WORKWELL_OFFICIAL_MEASURES (the gate appends the measure under test)."
description: "The deployment's CURRENT WORKWELL_OFFICIAL_MEASURES (the gate appends the measure under test). Paste the value deploy-maui-mieweb.yml ships."
required: true
type: string
default: "cms122,cms125"
default: "cms122,cms125,cms2,cms130,cms165,cms137"

permissions:
contents: read
Expand Down Expand Up @@ -91,26 +91,45 @@ jobs:
fi
# The vendored artifact must not have changed — only the gitignored sidecar is new.
- name: The committed artifact is reproducible from its pin
working-directory: .
run: git diff --exit-code backend-ts/measures/official
run: git diff --exit-code measures/official

- name: Gate
shell: bash
env:
WORKWELL_INSTANCE: maui
WORKWELL_MAUI_CORPUS_SIZE: "20000"
WORKWELL_RUN_CHUNK_SIZE: "500"
WORKWELL_OFFICIAL_MEASURES: ${{ inputs.routed }}
run: |
# pipefail, or `tee` would mask the CLI's own exit code (2 on a usage error) and the failure
# would surface later as "ls: cannot access .flip-gate" with the real message buried in stderr.
set -o pipefail
pnpm flip-gate --measure "${{ inputs.measure }}" --evaluation-date "${{ inputs.evaluation_date }}" --subjects "${{ inputs.subjects }}" 2> gate-stderr.log | tee gate-report.txt
echo "::group::stderr (engine warnings, deduplicated)"
sort gate-stderr.log | uniq -c | sort -rn | head -40
echo "::endgroup::"
ls -la .flip-gate/
# The JSON is the evidence a flip PR attaches; the text report alone would let the upload
# below succeed with the evidence missing. Copied out of the dot-directory first: the first
# two runs of this workflow uploaded only gate-report.txt, because the artifact glob does not
# descend into `.flip-gate/`.
test -n "$(ls .flip-gate/*.json 2>/dev/null)" || { echo "::error::the gate wrote no .flip-gate/*.json"; exit 1; }
mkdir -p flip-gate-out && cp .flip-gate/*.json flip-gate-out/

- uses: actions/upload-artifact@v6
if: always()
with:
name: flip-gate-${{ inputs.measure }}-${{ inputs.evaluation_date }}
path: |
backend-ts/.flip-gate/*.json
backend-ts/flip-gate-out/*.json
backend-ts/gate-report.txt
if-no-files-found: error

# The CLI is DESCRIPTIVE and exits 0 on every verdict (ADR-072); in CI a green run over a "DO NOT
# FLIP" verdict would be read as evidence FOR. The report is uploaded either way; the job is red.
- name: The verdict is evidence FOR the flip
run: |
if grep -q "DO NOT FLIP" gate-report.txt; then
echo "::error::the gate's verdict is DO NOT FLIP — read gate-report.txt in the artifact"
exit 1
fi
2 changes: 1 addition & 1 deletion .github/workflows/reconcile-maui-mieweb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ jobs:
# and cms122/cms125 quietly revert to authored CQL with no signal anywhere. Caught by
# Codex on #356 before it could happen; `official-flip-config.test.ts` now asserts the two
# workflows agree, so this pair cannot drift again.
{key: "WORKWELL_OFFICIAL_MEASURES", value: "cms122,cms125"},
{key: "WORKWELL_OFFICIAL_MEASURES", value: "cms122,cms125,cms2,cms130,cms165,cms137"},
{key: "WORKWELL_MAUI_CORPUS_SIZE", value: "20000"},
{key: "WORKWELL_RUN_CHUNK_SIZE", value: "500"},
{key: "WORKWELL_SCHEDULER_ANCHOR_HOUR_UTC", value: "12"},
Expand Down
14 changes: 7 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,13 @@ plan, ADR-070 drives it, and the owner decisions are `LOCKED_DECISIONS.md` §4A
retelling. Naming policy: repo documents say "Maui" and "the pilot group" only.
- **MM-0 shipped** (#496–#500). **MM-1 is in progress:** U1 (#526, ADR-072 — the runnable rule, the
calendar measurement period, the flip gate) and U2 (#528, ADR-073/074/075 — the 20,000-patient
corpus, multi-rate execution, outcome retention) merged 2026-09-05/06. Maui routes cms122 + cms125;
cms2, cms130, cms165 and cms137 are `official-pending` until MM-1c's second-engine sweep and a
`flip-gate` run clear each one. **No known-unverified measure is routed to the pilot**, the flip is a
reviewed workflow edit (ADR-045), and **cms165 must not be routed** until real blood pressures are
profile-stamped at ingest (ADR-076 d1 made profile trust per-measure, which is half the fix;
`MEASURES.md` and issue #533 carry the other half). U3 (#529) and the MM-1 open-flag work of
2026-09-07 (ADR-076, issues #530–#537) are the newest entries — read `docs/JOURNAL.md`, not this line.
corpus, multi-rate execution, outcome retention) merged 2026-09-05/06. **Since 2026-09-08 (ADR-078,
owner decision) the Maui sandbox routes the ACO's whole computable set — cms122, cms125, cms2,
cms130, cms165 and cms137.** The flip is a reviewed workflow edit (ADR-045). Two caveats travel with
it and govern the PHI phase, not the sandbox: cms165 runs on the corpus's stamped profiles and needs
real blood pressures profile-stamped at ingest before real data (issue #533's ingest half); cms137
stays routed unless the final rule removes Quality ID 305. An out-of-population official outcome no
longer opens a case (ADR-078 d2). Read `docs/JOURNAL.md` for the newest entries, not this line.
- MM-2/3/4 are blocked on externals (ROADMAP §7). The milestones deliver a **sandbox**; the pilot's
production/PHI phase is a separate `PRODUCTION_READINESS`-gated decision nothing in M-M authorizes.
- M-C (packaging) is complete and published; M-E1 (occupational content) is deferred behind M-M, not
Expand Down
23 changes: 23 additions & 0 deletions backend-ts/src/case/case-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,26 @@ test("official-routed nextActionFor reads the official display table first", ()
else process.env.WORKWELL_OFFICIAL_MEASURES = prior;
}
});

test("planCaseUpsert: a subject OUTSIDE the initial population never opens a case, and closes an active one under its own reason (ADR-078)", () => {
const NOW = "2026-09-08T12:00:00.000Z";
const out = { outOfPopulation: true };
assert.deepEqual(planCaseUpsert(null, "MISSING_DATA", NOW, out), { op: "noop" }, "no case exists → nothing to open");
assert.deepEqual(
planCaseUpsert({ status: "OPEN", currentOutcomeStatus: "MISSING_DATA", closedBy: null }, "MISSING_DATA", NOW, out),
{ op: "update", disposition: "RESOLVED", status: "RESOLVED", closedAt: NOW, closedReason: "OUT_OF_POPULATION", closedBy: null },
"an active case is closed by the system, with the reason that says why",
);
assert.deepEqual(
planCaseUpsert({ status: "IN_PROGRESS", currentOutcomeStatus: "OVERDUE", closedBy: null }, "MISSING_DATA", NOW, out),
{ op: "update", disposition: "RESOLVED", status: "RESOLVED", closedAt: NOW, closedReason: "OUT_OF_POPULATION", closedBy: null },
"IN_PROGRESS too: an operator was working a patient the measure does not concern",
);
assert.deepEqual(
planCaseUpsert({ status: "RESOLVED", currentOutcomeStatus: "COMPLIANT", closedBy: "someone" }, "MISSING_DATA", NOW, out),
{ op: "noop" },
"a closed case stays closed — no closed_at drift",
);
// Without the flag the same status keeps opening cases: in-population MISSING_DATA is still a gap.
assert.equal(planCaseUpsert(null, "MISSING_DATA", NOW).op, "insert");
});
21 changes: 20 additions & 1 deletion backend-ts/src/case/case-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,26 @@ export function planNextAction(
}

/** Decide how a case should be upserted from one outcome, given the existing row (or null). Pure. */
export function planCaseUpsert(existing: ExistingCaseState | null, outcomeStatus: string, now: string): CaseUpsertPlan {
export function planCaseUpsert(
existing: ExistingCaseState | null,
outcomeStatus: string,
now: string,
opts: { outOfPopulation?: boolean } = {},
): CaseUpsertPlan {
// A subject the official logic evaluated and found OUTSIDE the measure's initial population has
// nothing to chase: MISSING_DATA there is a result, not a gap (ADR-078). Never opens a case; a case
// that exists (the subject was in the population last period, or was carded before this rule) is
// closed by the system under its own reason, so the worklist stops carrying a non-diabetic on a
// diabetes measure. Until 2026-09-08 this fan-out opened one MEDIUM case per out-of-population
// subject per measure — ADR-043 recorded it as operational noise; six routed measures over 20,000
// patients made it the worklist.
if (opts.outOfPopulation) {
if (!existing) return { op: "noop" };
if (existing.status === "OPEN" || existing.status === "IN_PROGRESS")
return { op: "update", disposition: "RESOLVED", status: "RESOLVED", closedAt: now, closedReason: "OUT_OF_POPULATION", closedBy: null };
return { op: "noop" };
}

const disposition = dispositionFor(outcomeStatus);

if (!existing) {
Expand Down
48 changes: 48 additions & 0 deletions backend-ts/src/case/case-rerun.rates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,51 @@ test("rerun-to-verify hands the action back to the SYSTEM, so the rate can keep
"a computed action must say so, or patchCase's OPERATOR default freezes it",
);
});

test("rerun-to-verify closes a case whose subject the OFFICIAL logic finds outside the initial population, as a system closure (ADR-078)", async () => {
const { rerunToVerify } = await import("./case-rerun.ts");
const OUTSIDE = {
expressionResults: [],
official: { ecqmId: "CMS137", populationResults: rate(false).map((p) => ({ ...p, result: false })), measurementPeriod: { start: "2026-01-01", end: "2026-12-31" } },
};
let patched: Record<string, unknown> | null = null;
const audits: Array<{ eventType: string; payload: Record<string, unknown> }> = [];
const noop = async () => undefined;
const deps = {
cases: {
getCase: async () => existing,
patchCase: async (_id: string, patch: Record<string, unknown>) => { patched = patch; return { ...existing, ...patch }; },
listCases: async () => [existing],
upsertFromOutcome: noop,
countByLastRun: async () => 1,
},
events: {
recordCaseEvent: noop,
appendAudit: async (input: { eventType: string; payload: Record<string, unknown> }) => { audits.push({ eventType: input.eventType, payload: input.payload }); },
caseTimeline: async () => [],
latestOutreachDeliveryStatus: async () => null,
},
outcomes: { recordOutcome: noop, listOutcomes: async () => [] },
runStore: { createRun: async () => ({ id: "run-rerun-oop" }), markRunning: noop, appendLog: noop, finalizeRun: noop, listRuns: async () => [] },
engine: {
evaluate: async () => ({ outcome: "MISSING_DATA", evidence: OUTSIDE, inInitialPopulation: false }),
logicVersionFor: () => "official-fqm:1.0.000:artifact:terminology",
},
} as unknown as RerunDeps;

await rerunToVerify(deps, existing.id, "tester");
const p = patched as Record<string, unknown> | null;
assert.ok(p, "the case was patched");
assert.equal(p!.status, "RESOLVED");
assert.equal(p!.closedReason, "OUT_OF_POPULATION");
assert.equal(p!.closedBy, null, "the system's determination, not the operator's closure — a later in-population outcome may reopen it");
assert.ok(String(p!.nextAction).includes("not in the measure's initial population"));
assert.ok(audits.some((a) => a.eventType === "CASE_RESOLVED" && a.payload.closedReason === "OUT_OF_POPULATION"), "the closure is audited with its reason");

// The AUTHORED engine's flag does not close anything through this path either.
patched = null;
audits.length = 0;
const authored = { ...deps, engine: { evaluate: async () => ({ outcome: "MISSING_DATA", evidence: { expressionResults: [] }, inInitialPopulation: false }), logicVersionFor: () => "sha256:authored" } } as unknown as RerunDeps;
await rerunToVerify(authored, existing.id, "tester");
assert.equal((patched as Record<string, unknown> | null)?.status, "OPEN", "an authored out-of-population subject keeps the case open, unchanged");
});
37 changes: 32 additions & 5 deletions backend-ts/src/case/case-rerun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { compositeBundleSource } from "../wiring/subject-bundle-source.ts";
import { priorityFor, nextActionFor } from "./case-logic.ts";
import { toCaseDetail, type CaseDetail } from "./case-detail-read-model.ts";
import { caseRerunMeasurementPeriod } from "../run/run-period.ts";
import { OFFICIAL_LOGIC_VERSION_PREFIX } from "../wiring/executor-router.ts";

export interface RerunDeps {
cases: CaseStore;
Expand Down Expand Up @@ -102,10 +103,19 @@ export async function rerunToVerify(deps: RerunDeps, caseId: string, actor: stri

let verifiedStatus: string;
let evidence: unknown;
// ADR-078: the OFFICIAL logic found the subject outside the initial population. The nightly run closes
// such a case under OUT_OF_POPULATION; a rerun-to-verify must reach the same answer, or the operator's
// own click leaves the case open until the next night (Gemini review). Authored measures set the flag
// too and are deliberately not read here — same gate as the pipeline.
let outOfPopulation = false;
try {
const result = await deps.engine.evaluate({ measureId: existing.measureId, patientBundle: bundle, evaluationDate: evalDate });
verifiedStatus = result.outcome;
evidence = result.evidence;
const logicVersionFor = (deps.engine as { logicVersionFor?: (measureId: string) => string | undefined }).logicVersionFor;
outOfPopulation =
result.inInitialPopulation === false &&
(logicVersionFor?.(existing.measureId)?.startsWith(OFFICIAL_LOGIC_VERSION_PREFIX) ?? false);
} catch (err) {
verifiedStatus = "MISSING_DATA";
evidence = { evaluationError: "engine failure", message: String((err as Error)?.message ?? err) };
Expand All @@ -121,12 +131,18 @@ export async function rerunToVerify(deps: RerunDeps, caseId: string, actor: stri
evidence,
});

const updatedCaseStatus = verificationCaseStatus(existing.status, verifiedStatus);
const nextAction = verificationNextAction(verifiedStatus, existing.measureId, evidence);
const closing = isClosing(verifiedStatus);
const updatedCaseStatus = outOfPopulation ? "RESOLVED" : verificationCaseStatus(existing.status, verifiedStatus);
const nextAction = outOfPopulation
? "No follow-up needed: not in the measure's initial population on verification rerun."
: verificationNextAction(verifiedStatus, existing.measureId, evidence);
const closing = isClosing(verifiedStatus) || outOfPopulation;
const closedAt = closing ? new Date().toISOString() : null;
const closedReason = verifiedStatus === "COMPLIANT" ? "RERUN_VERIFIED" : verifiedStatus === "EXCLUDED" ? "RERUN_EXCLUDED" : null;
const closedBy = closing ? actor : null;
const closedReason =
verifiedStatus === "COMPLIANT" ? "RERUN_VERIFIED" : verifiedStatus === "EXCLUDED" ? "RERUN_EXCLUDED" : outOfPopulation ? "OUT_OF_POPULATION" : null;
// An out-of-population closure is the SYSTEM's determination even when a person clicked rerun: it
// stays `closed_by = NULL` so a later in-population outcome reopens it, exactly as the nightly path
// closes it. A verified COMPLIANT/EXCLUDED is the operator's closure and keeps their name.
const closedBy = closing && !outOfPopulation ? actor : null;

const actionPayload = {
priorOutcomeStatus: existing.currentOutcomeStatus,
Expand Down Expand Up @@ -197,6 +213,17 @@ export async function rerunToVerify(deps: RerunDeps, caseId: string, actor: stri
refMeasureVersionId: existing.measureId,
payload: { ...verificationPayload, exclusionReason: "Excluded on verification rerun." },
});
} else if (outOfPopulation) {
await deps.events.appendAudit({
eventType: "CASE_RESOLVED",
entityType: "case",
entityId: caseId,
actor,
refRunId: run.id,
refCaseId: caseId,
refMeasureVersionId: existing.measureId,
payload: { ...verificationPayload, closedReason: "OUT_OF_POPULATION", summary: "Case closed by rerun-to-verify: the subject is outside the measure's initial population (ADR-078).", runId: run.id },
});
}

await deps.runStore.finalizeRun(run.id, hasEvaluationError ? "PARTIAL_FAILURE" : "COMPLETED");
Expand Down
Loading
Loading