Skip to content

feat(lists): the ACO's attributed patient list — import, filter, and the measurement-year report - #574

Merged
Taleef7 merged 8 commits into
mainfrom
feat/aco-attributed-list
Sep 17, 2026
Merged

Taleef7 merged 8 commits into
mainfrom
feat/aco-attributed-list

Conversation

@Taleef7

@Taleef7 Taleef7 commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

Closes #557. ADR-082.

The 2026-09-09 working session produced one concrete ask: hand WorkWell the list of patients the ACO attributes to the group, run the measures over exactly that subset, and return numerator, denominator and exclusions with the patient-level result and its date.

Everything else this deployment answers is "the patients in our directory". This is "the patients somebody else says are ours", and on an MSSP attribution those are different populations — the practice sees patients the ACO does not attribute, and the ACO attributes patients the practice has not seen this year. Nothing in the system could express the difference.

One PR, not the planned two

The approved plan split this into PR 3A (lists + import + listId) and 3B (report + CSV + UI). It ships as one. A list without a report is not a finished unit for the ACO, and the standing instruction is fewer and larger PRs. The split's actual purpose — a dedicated review round on the security-sensitive import surface — is preserved: it is priority section 1 of the review brief every lane ran, above the report and the UI.

A list is immutable, and that is the design

A report is a function of (list revision, run ids). If a list could be edited underneath a report, every number the ACO had already filed would become unverifiable — there would be no way to answer "what was the list when you computed this?"

So there is no UPDATE and no DELETE anywhere in SubjectListStore, a re-import of the same name allocates revision + 1, and a manual resolution of a non-match will also be a new revision (the rule ADR-022 applies to identity matching). The guarantee is the tested absence of a mutator in the interface, routes and UI — not a database trigger, and the ADR says so.

That immutability is also what makes the ?listId= memo safe, and it is the first thing that breaks if the rule is ever relaxed.

The sandbox data boundary

M-M authorises a synthetic sandbox (LOCKED §4A.1). An import route that persists arbitrary identifiers — even as unresolved rows — is a path for a real attribution file to reach Neon, its backups and its exports before the environment split (#267), the auth fork (#265) and observability (#264) exist.

  • An identifier outside the deployment's own namespace refuses the WHOLE upload before anything is written, reporting a count and never the values. An error body is logged and kept by the browser; echoing them would persist them by another route.
  • Writing that namespace as one pattern would have been wrong twice. The Maui corpus is 48 hand-written fixtures with three-digit ids (pat-001) followed by generated five-digit ones, so a pattern built from the generated form alone would have refused the first 48 real patients in the sandbox. And the default deployment is a different synthetic roster entirely (emp-NNN), where the corpus pattern refuses everything. It is per profile now, with a route test on each asserting the other profile's ids are the outsiders.
  • It is a namespace test, not an existence test: pat-99999 conforms and is simply NOT_FOUND, which is what keeps the review queue reachable with synthetic-shaped ids.
  • A live-directory deployment cannot import at all, and the gate fires before the body is read. That directory is a worker-local last-known registry that fabricates a minimal profile for any wc| id, so matching against it would be silently incomplete — every row MATCHED and every denominator wrong — rather than merely unavailable.

Every method on /api/subject-lists/** is CASE_MANAGER/ADMIN, metadata included — deliberately not split into a GET rule and a write rule the way panels is. A member row is a raw patient identifier another system asserted, the list's existence says which patients an ACO claims, and the public /sandbox signs in as a read-only VIEWER that may browse every AUTHENTICATED GET (on Maui the clinician seat is a VIEWER too).

NOT_FOUND members are kept, and the database enforces it

An identifier the directory cannot resolve is the ACO and the practice disagreeing about who a patient is — a review-queue finding, not a row to drop, because dropping it removes the patient from the denominator and from the evidence they were ever claimed.

resolution and subject_id are coupled by a CHECK rather than a convention, and a partial unique index refuses a second MATCHED row for one subject, so an alias lands AMBIGUOUS instead of doubling that patient in every denominator the list feeds. Both are exercised against the real database, not only against the validator.

Nothing is visible until the import finished

Header IMPORTING → members in chunks → audit → COMPLETE, with every read filtering COMPLETE. "One transaction" was never available on the floor (D1 caps a batch at ~90 statements), so rather than let the ceiling be quietly safer, both backends rely on the flip and a crash leaves an invisible partial row instead of a short list masquerading as whole. A short list is the dangerous failure: it reads as a smaller ACO population under a real list's name.

The revision allocation is transactional, because it is the only part with a real race — an advisory xact lock on the ceiling (never the session form: Neon's pooler is PgBouncer in transaction mode), a single INSERT … SELECT MAX+1 with one retry on the floor.

The report

  • measurementYear is required and has no default. An officially routed run is scored over the calendar year containing its evaluation date (ADR-072), so "the latest numbers" answers a PY2027 question with PY2028's first nightly the moment January arrives — and looks exactly like a correct answer. The selector walks past a newer run from the wrong year.
  • Compaction refuses per MEASURE. ADR-077 refuses a report built over rows that may be incomplete, but the refusal belongs to the measure whose run aged out; withholding five complete measures because the sixth did would be a second wrong answer. Exposure is checked before the reads and again after them, because a pass beginning mid-report would have deleted rows the earlier pages already counted — and every derived row is computed before anything is serialised, since a streamed CSV cannot change its status after the first byte.
  • missingFromRun is reported beside the rates and never subtracted. A member the run never evaluated is a gap in the evidence, not an exclusion; folding them into a denominator would let a smaller run produce a higher score. Two reconciliations are stated in DATA_MODEL_CONTRACTS §6.6 and pinned by test.
  • The score stays numer / (denom − denex − denexcep) — what createRateAggregator already computes and what the eCQM proportion convention specifies. status=EXCLUDED is the workflow vocabulary; the two denominator* columns are the artifact's populations. The ACO's word "exclusions" covers both, so both are reported separately.

The CSV

The patient-level artifact, with a header pinned by test because the ACO's tooling reads it by name. Three row shapes: one per (measure, rate) when evaluated; one per measure for a member the run never saw, with every population cell the empty string rather than 0 (which reads as a scored result of zero); and exactly one row per unresolved identifier, after the evaluated rows — one per measure would multiply a single disagreement by six.

Text a person supplied goes through csvTextCell: a CSV of somebody's uploaded identifiers must not become code when the ACO opens it.

?listId= through the one predicate

Six surfaces — the roster, the cases route, the work list, both CSV exports and the MCP tool. It is a resolved membership, not a token the predicate parses: the parameter names an immutable list and the server turns it into subjects, so a client can ask for a list but cannot spell a membership. An unknown id is a 404 on every surface (LIST_NOT_FOUND on MCP). hasActiveSubjectFilters tests != null, not .size > 0 — a list none of whose identifiers resolved is an active filter matching nobody, and reading it as absent would answer "the ACO's population" with everybody.

The set is memoized in an 8-entry LRU. A 50,000-id Set rebuilt on every /worklist page load is the pool-pressure shape #560 removed from the case export.

The review round

Four lanes on the whole diff — GPT 5.6 Sol (xhigh), GLM 5.3 Flash, Gemini 3.8 Flash high and this
project's own whole-diff reviewer. Two findings were serious and were found independently by more
than one lane.

** re-opened the membership the CM/ADMIN gate exists to close.** Five of the six surfaces
that accept it are AUTHENTICATED, so a read-only VIEWER holding a list id could take the whole
membership out of — names, provider, payer,
per-measure status — which is strictly MORE than the members endpoint the gate protects. The id is not
a secret by construction: it is in the query string of every filtered screen, so it reaches shareable
URLs, browser history and access logs. Enforced once in the worker now, and the mutation that removes
it fails three tests.

The stated reconciliation was arithmetically false. 's is a
SUPERSET of its , so subtracting both double-counted every error — and a row that is
both out-of-population and an evaluation error made NEGATIVE. It held in the test only
because the fixture had all three counts at zero, which makes the assertion and
passes for any implementation. That was the missed mutation the brief asked the lanes to find. Each
seen subject is classified into exactly one bucket now, in a stated order, so the identity holds on a
PARTIAL_FAILURE run.

Also folded, each verified first: the default profile's namespace refused fifty legitimate
members of its own directory; the run search took the twelve most recent runs outright
(twelve DAYS on a nightly deployment, so a mid-January report for the closed year answered "no run");
the per-row flags built 300,000 aggregators at the cap; // were the one
unvalidated channel into Neon and into an audit table that is exported wholesale; and
did not filter COMPLETE; the duplicate collapse compared timestamps as strings; the CSV
carried the resolved subject id in the column; broke an identity the docs
state unconditionally; the 2 MB cap buffered the body first and had no test; and a test titled "the
body is never read" asserted only a status code.

One claim was checked and rejected: a lane reported that an unrecognised deployment profile would
fail OPEN and admit real identifiers. It would not — the fallback was the pattern, which
refuses an MRN — and the profile id is a closed union. The underlying point was taken anyway: the map
is explicit and an unrecorded profile is refused rather than lent somebody else's namespace.

Codex on the open PR — four more, two of them P1

The run selection still filtered by when a run STARTED. A manual run takes an arbitrary
evaluationDate, so a rerun-to-verify of a closed year begins in the following one and legitimately
scores the closed one. I had written that limit into ADR-082 as acceptable; it is not, because a
backdated rerun is a supported path rather than a hypothetical, and the report would answer "no run
for this year" with that run sitting in the table. RunStore.listPopulationRunsForPeriod (new, both
stores, with a contract test) filters on the run's own measurement_period_start — and removes the
25-run walk cap that made the start-date window necessary in the first place.

A measure with no usable run emitted no patient rows while its summary claimed N members were
missing from it. The CSV serialises rows alone, so the count was unreconstructable and the ACO could
not see who. It emits one row per matched member now; a COMPACTED measure stays the exception and
claims nothing per subject.

Two on the screen: the year select offered only past years, making PY2027 — the year the pilot
exists for — unreachable until the clock caught up; and changing the year left the computed table and
the Download button up while download read the NEW year.

That last one surfaced something worth recording: @mieweb/ui's Select renders a custom combobox
whose options are not in the DOM, so userEvent.selectOptions cannot drive it. The roster's page-size
control is a native <select> with an aria-label for exactly that reason, and both selects on this
page follow it now — a control with behaviour worth pinning has to be drivable by a test.

Verification

  • Backend typecheck clean; 2,742 tests, 2,718 pass, one failure — the standing corpus-membership stale sparse-checkout one, green in CI.
  • Frontend lint clean, 473 tests, build compiled.
  • Seven mutations, each caught by the test named for it: .size > 0 in the active-filter guard revives the empty-list hole; dropping the resolution from the cases CSV breaks all three cross-surface tests; dropping the post-read compaction check fails three report tests; taking the newest run regardless of year fails five; dropping the frontend row shape-guard fails the picker test; and removing the ?listId= authorization gate fails three worker tests.
  • The Postgres ceiling's store contract runs in CI only. Docker is down on this host and free RAM was 1.3 GB; the SQLite floor cannot catch Pg-only SQL, which is how two defects reached perf(run): reads proportionate to what they show, and the case pass batched #544. The CI postgres:16 service is what verifies the advisory lock, the partial unique index and the uuid[] binds — please check that job specifically.

Two source hazards worth the next person's time

A literal U+FEFF, and a literal tab + CR, ended up inside regexes in source because the shell tooling collapses  and \t to the characters they name before the file is written. Both were invisible on screen, one sat inside a character class where a stray CR would not have survived line-ending normalisation, and both were caught only by dumping bytes. The rule that already exists for sed on UTF-8 source extends to any scripted write: verify the bytes, not the rendering.

Docs

ADR-082 (seven decisions); DATA_MODEL_CONTRACTS §6.6 (the report's columns and its three row shapes) and §6.7 (?listId=), both appended so nothing in §6.1–§6.5 moves; DATA_MODEL §3.29; MCP.md; DEPLOY.md (nothing to run, no backfill, rollback-safe, request budgets, and the retained-report stopgap); ARCHITECTURE §3; guide ch.10; journal; roadmap MM-2 marked complete. MEASURES.md is not affected — no measure logic changes.

PRODUCTION_READINESS §4 gains the two PHI-phase items this defers: an authoritative subject resolver before the import can run on a live directory, and the per-run report archive the ACO's ~10-year audit window needs (with its design).

Open questions for the owner

  1. The four ACO inputs are still outstanding — identifier format, cadence, non-match handling, and whether Medicare Advantage is in scope. Each changes one seam (resolve(), a scheduler job, the members UI, a report-time payer intersection) rather than the schema, which is why the tables could be written first. The shipped defaults are exact trimmed externalId, manual upload, a review queue that is never auto-applied, and no payer filter.
  2. The retained-report stopgap is an ops habit, not a system. Until the archive ships, a report handed to the ACO has to be saved as a dated file in the R2 bucket or December's question about March has no artifact. Is that acceptable for the sandbox phase, or should the archive move earlier?
  3. Should /lists be visible to a VIEWER seat as a read-only surface? Today it is CM/ADMIN throughout, which is stricter than panels and was chosen deliberately.

Taleef added 7 commits September 15, 2026 20:50
…ble on both stores

The one concrete ask from the 2026-09-09 working session: hand WorkWell the list
of patients the ACO attributes to the group, run the measures over exactly that
subset, and get numerator/denominator/exclusions back with patient-level results.
Every other population question this deployment answers is "the patients in our
directory"; this one is "the patients somebody else says are ours".

Two tables, owner-authorized in session on 2026-09-15. IMMUTABLE is the design
rather than a nicety: a report is a function of (list revision, run ids), so a
list that could be edited underneath one would make every number the ACO had
already filed unverifiable. A re-import is a new row with revision+1 under the
same name, the store interface exposes no mutator, and a manual resolution of a
non-match is therefore also a new revision — the rule ADR-022 applies to identity
matching.

NOT_FOUND members are KEPT. An identifier the directory cannot resolve is the ACO
and the practice disagreeing about who a patient is, which is a finding for the
review queue; dropping it would remove the patient from the denominator and from
the evidence that they were ever claimed. `resolution` and `subject_id` are
coupled by a CHECK rather than by convention, and a partial unique index refuses
a second MATCHED row for one subject, so an alias lands AMBIGUOUS rather than
doubling a patient in every denominator the list feeds.

Nothing is visible until the whole import landed — header IMPORTING, members in
chunks, audit, then COMPLETE, with every read filtering COMPLETE. "One
transaction" was never available on the floor (D1 caps a batch at ~90
statements), so rather than let the ceiling be quietly safer, both backends rely
on the flip and a crash at any point leaves an invisible partial row instead of a
short list masquerading as whole. The revision allocation IS transactional,
because it is the only part with a real race: an advisory xact lock on the
ceiling (never the session form — Neon's pooler is PgBouncer in transaction
mode), a single INSERT…SELECT MAX+1 with one retry on the floor.

The import rules are pure and tested where they can fail. Two are invisible from
a route test: resolution never consults a lookup that FABRICATES profiles (the
live directory returns a minimal profile for any `wc|` string, so a resolver
built on it would auto-match identifiers that exist nowhere), and the alias
collapse cannot fire under today's exact-id matching but is the seam the
identifier format changes at.

The sandbox data boundary is per PROFILE, and writing it as one pattern was the
first thing that would have been wrong twice: the Maui corpus is 48 fixture
patients with three-digit ids followed by generated five-digit ones, so a gate
written for the generated form alone would refuse the first 48 real patients in
the sandbox; and the default deployment is a different synthetic roster
(emp-NNN), where the corpus pattern would refuse everything. It is a namespace
test, not an existence test — pat-99999 conforms and is simply NOT_FOUND, so the
review queue is exercised with synthetic-shaped ids.

Verification: 12 store-contract tests green on the SQLite floor; 13 import-rule
tests green. The Postgres ceiling contract runs in CI's postgres:16 service —
Docker is down on this host and the floor cannot catch Pg-only SQL, which is how
two defects reached #544.
…ace by it

The import route, its sandbox data boundary, and `?listId=` through the one
predicate six surfaces already share.

EVERY method on /api/subject-lists is CASE_MANAGER/ADMIN, metadata included —
deliberately not split into a GET rule and a write rule the way panels is. A
member row is a raw patient identifier another system asserted, and the list's
mere existence says which patients an ACO claims. The public /sandbox signs in as
a read-only VIEWER that may browse every AUTHENTICATED GET, and on Maui the
clinician seat is a VIEWER too, so leaving the reads to the /api/** catch-all
would have handed all of it to anyone who pressed "explore the sandbox".

The sandbox data boundary refuses the WHOLE upload before persistence when any
identifier is outside this deployment's own namespace, and reports the COUNT
rather than the values — an error body is logged and kept by the browser, so
echoing them would persist the very data the gate refuses. A live-directory
deployment cannot import at all: that directory is a worker-local last-known
registry that also fabricates profiles for `wc|` ids, so matching against it
would be silently incomplete — every row MATCHED and every denominator wrong —
rather than merely unavailable. The gate runs before the body is read.

The audit payload carries counts, provenance and revision and NO identifier; a
test asserts it, because audit_events is exported wholesale and an identifier
there would defeat the namespace gate by another route.

`?listId=` is a RESOLVED membership, never a token the predicate parses: the
parameter names an immutable list and the server turns it into subjects, so a
client cannot spell one. The set is memoized in an 8-entry LRU, which is safe
only because a list is immutable — a 50,000-id Set rebuilt on every /worklist
page load is the pool-pressure shape #560 removed from the case export.
`hasActiveSubjectFilters` tests `!= null`, not `.size > 0`: a list none of whose
identifiers resolved is a legitimate answer and an ACTIVE filter matching nobody,
and reading it as absent would answer "the ACO's population" with everybody.

Verification: 12 route tests, 2 Maui-profile gate tests (each profile's own
namespace is the admitted one — a single pattern would have refused the first 48
corpus patients on one profile and everything on the other), 6 cross-surface
tests, 1 authorization test; 108 existing route tests still green. Two mutations,
each caught: `.size > 0` in the active-filter guard revives the empty-list hole,
and dropping the resolution from the cases CSV breaks all three surface tests.
…tient-level evidence

What the ACO actually asked for on 2026-09-09: numerator, denominator and
exclusions over the patients they attribute to the group, with the per-patient
result and its date. Computed from the same evidence every other rate here comes
from — `createRateAggregator` over the outcomes' persisted population
memberships — fed only the rows whose subject is in the list.

Four things that were each a way to be quietly wrong:

`measurementYear` is REQUIRED and has no default. An officially routed run is
scored over the calendar year containing its evaluation date (ADR-072), so in
January 2028 the newest run is a PY2028 one and "the latest numbers" would answer
a PY2027 question with next year's first nightly — a wrong number that looks
exactly like a right one. The selector walks past a newer run from the wrong
year.

Compaction is checked PER MEASURE, before and after the reads. ADR-077 refuses a
report built over rows that may be incomplete, but the refusal belongs to the
measure whose run aged out; withholding five complete measures because the sixth
did would be a second wrong answer. The whole request is 409 only when every
selected run is exposed. The post-read check is what makes a mid-report
compaction pass a 409 rather than a truncated 200 — a streamed CSV cannot change
its status after the first byte, so every derived row is computed before anything
is serialised.

`missingFromRun` is reported BESIDE the rates and never subtracted. A member the
run never evaluated is a gap in the evidence, not an exclusion, and folding them
into a denominator would let a SMALLER run produce a HIGHER score. Two
reconciliations are pinned by test: matched = seen + missing, and seen = scored +
unmeasured + errors + outOfPopulation.

The CSV is the patient-level artifact, with its header pinned exactly because the
ACO's tooling reads it by name. Three row shapes: one per (measure, rate) when
evaluated; one per MEASURE for a member the run never saw, with every population
cell EMPTY rather than 0 (which reads as a scored result); and exactly ONE row
per unresolved identifier, after the evaluated rows — one per measure would
multiply one disagreement by six. Text a person supplied is neutralised against
spreadsheet formula injection, because a CSV of somebody's uploaded identifiers
must not become code when the ACO opens it. `subjectHeaders` is exported rather
than copied, so the subject columns follow the deployment's own term.

The report read is audited like COMPLIANCE_API_READ, naming the list, revision
and run ids — what the December question about March's numbers needs — and no
identifier.

Verification: 14 report tests, 5 new route tests, 17 route tests in total for the
surface. Backend 2,724 tests, 2,700 pass, one failure — the standing
corpus-membership stale sparse-checkout one, green in CI. Two mutations, each
caught: dropping the post-read compaction check fails 3 tests, and taking the
newest run regardless of year fails 5.
…nd the year's numbers

One page for the ask: upload the list, see what resolved and what did not, and
take the measurement year's numbers off it.

The unresolved members get a filter of their own rather than a count in a corner.
An identifier the directory cannot resolve is the ACO and the practice
disagreeing about who a patient is — a finding somebody has to work, and a list
that reported only its matched count would hide exactly the rows worth a phone
call.

The server's refusal is rendered VERBATIM. The sandbox-namespace gate answers
with a COUNT of identifiers outside the deployment's directory, and "Import
refused" alone would drop the one number the operator needs to go back to the
ACO with.

The report's measurement year is a select with no default, because ADR-072 scores
an officially routed run over its calendar year: "the latest numbers" would
answer a PY2027 question with PY2028's first nightly the moment January arrives,
and it would look right. A measure whose run predates a retention cutoff is named
with its reason beside the measures that did report — withholding the others
would be a second wrong answer, and showing that one's numbers would be the
first.

The lists hook carries the same hardening `use-panel-payers` and
`use-assignable-users` needed: a row that is not what it claims to be is dropped,
and a payload that arrived wholly unusable warns rather than rendering an empty
state that reads as "this deployment has no lists". Both fetch effects defer out
of the synchronous body, the idiom compliance/ and cases/ already use for
react-hooks/set-state-in-effect; the members table resets its page in the CHANGE
HANDLER and remounts per list via a key, so no effect writes state at all.

Verification: 5 page tests, frontend lint clean, 471 tests, build compiled. One
mutation, caught: dropping the row shape guard fails the picker test.
…hase items it defers

ADR-082 records seven decisions: a list is immutable (a report is a function of
list revision and run ids, so an editable list makes every filed number
unverifiable); an attribution is not an assignment and neither is a denominator;
NOT_FOUND members are kept and an alias is AMBIGUOUS rather than a silent
collapse; the sandbox data boundary, per profile and before persistence; the
report is for a measurement year and compaction refuses per measure;
missingFromRun is reported beside the rates and never subtracted; and the CSV is
the patient-level artifact with a pinned header.

DATA_MODEL_CONTRACTS gains §6.6 (the report's columns, its three row shapes, and
why the two non-evaluated ones carry the empty string rather than 0) and §6.7
(`?listId=` on the six filtered surfaces). Both are APPENDED — nothing in
§6.1–§6.5 moves.

DATA_MODEL §3.29 carries the two tables and the visible-state machine; MCP.md
documents the listId argument and states that it exposes nothing the tool's role
gate did not; ARCHITECTURE §3 places the report as the sixth population read
model and the only one whose population is asserted from outside; DEPLOY gains
the runbook — nothing to run, no backfill, rollback-safe, the request budgets,
and the retained-report stopgap that is the ACO's audit trail until the archive
ships.

PRODUCTION_READINESS §4 gains the two PHI-phase items this defers: an
authoritative subject resolver (the live directory fabricates profiles for
unknown `wc|` ids, so matching against it would be silently incomplete) and the
per-run report archive, with its design, for the ACO's audit window.

Guide chapter 10 gains the scenario in plain words, and the journal entry records
the two source hazards this session hit: a literal U+FEFF and a literal tab + CR
written into regexes by tooling that collapsed the escapes, both invisible on
screen and caught only by dumping bytes.
… read-only

Importing is a WRITE and belongs in `maui-writes`; this project runs first and
must be harmless when the thing it tests is broken, so every assertion is about a
surface that exists whether or not any list has been imported, and the negative
checks name a list id no import can have produced.

The one thing worth an e2e rather than a unit test: this route's READS are
CM/ADMIN, unlike every other directory surface on the deployment. A regression
that dropped them to the AUTHENTICATED catch-all would look entirely normal in a
unit test of the page, and would hand an ACO's attribution to the read-only
sandbox seat.
…nciliation that was false

Four review lanes on the whole diff. The two that matter were found
independently by more than one of them.

`?listId=` re-opened the membership the CM/ADMIN gate exists to close. Every
method on /api/subject-lists is CM/ADMIN because the list's existence says which
patients an ACO claims — but five of the six surfaces that accept `?listId=` are
AUTHENTICATED, so a read-only VIEWER holding a list id could take the whole
membership out of `GET /api/exports/cases?format=csv&listId=<uuid>`: names,
provider, payer, per-measure status. That is strictly MORE than the members
endpoint the gate protects, and the id is not a secret by construction — it sits
in the query string of every filtered screen, so it reaches shareable URLs,
browser history and access logs. A gate that reads as present and cannot fire for
the widest read is this repo's own vacuous-guard shape at the system level.
Enforced once in the worker, where authorization decisions live.

The stated reconciliation was arithmetically false. `createRateAggregator`'s
`unmeasured` is a SUPERSET of its `evaluationErrors` — it starts the count at the
error count — so subtracting both double-counted every error, and a row that was
both out-of-population and an evaluation error made `scoredSubjects` NEGATIVE.
The test that "pinned" it used a fixture with all three counts at zero, which
makes the assertion `2 === 2 + 0 + 0 + 0` and passes for any implementation. Each
seen subject is now classified into exactly ONE bucket in a stated order (error,
then out-of-population, then in-no-rate, then scored), so the identity holds on a
PARTIAL_FAILURE run — an ordinary night on the pilot.

Also, each verified before folding:

- The default profile's namespace refused fifty legitimate `ihn-emp-NNN` members
  of its own directory — the same defect as refusing Maui's 48 fixtures, in the
  other deployment. And an unrecognised profile is refused outright now rather
  than lent another profile's namespace.
- The run search took the twelve most recent runs outright, which on a nightly
  deployment is twelve DAYS: from mid-January a report for the closed year
  answered "no run" while that year's runs sat uncompacted.
  `listLatestPopulationRuns` walks at most 25 runs whatever count it is given, so
  scoping to the year's window was the only mechanism that could work.
- The per-row rate flags built a whole aggregator per row — 300,000 of them at
  the 50,000-member cap across six measures — where `membershipRatesFor` is the
  thing wanted, and it is read once per row for both the flags and the bucket.
- `name`/`source`/`note` were the one unvalidated channel into Neon, and
  name/source are copied into an audit payload that is exported wholesale. Capped
  at 200 by the route and by a CHECK in both schemas.
- `listMembers` and `countMembers` did not filter COMPLETE, safe only because
  every caller checked first — a claim about callers, not about the store.
- The duplicate collapse compared timestamps as STRINGS, so an offset-form
  `evaluatedAt` could pick the older clinical evaluation.
- The CSV carried the resolved subject id in the `rawIdentifier` column, which is
  right only while matching is exact and wrong the moment the format changes —
  which is the entire point of that column.
- `emptyEntry` broke `matchedSubjects = seen + missingFromRun` for exactly the
  entries a reader is most likely to check: the ones with no numbers.
- The 2 MB cap was measured after buffering the body, and had no test.
- A test titled "the body is never read" asserted only a status code.

One claim was checked and REJECTED: a lane reported that an unrecognised profile
would fail OPEN and admit real identifiers. It would not — the fallback was the
`emp-` pattern, which refuses an MRN — and the profile id is a closed union. The
underlying point was still worth taking.

Verification: backend typecheck clean, 2,738 tests, one failure (the standing
corpus-membership stale sparse-checkout, green in CI). Seven mutations, each
caught; the new one is removing the `?listId=` gate, which fails three worker
tests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1aa356dea2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +354 to +361
const candidates = await deps.outcomes.listLatestPopulationRuns(
[measureId],
{
excludeScale: true,
excludeTrendHistory: true,
from: `${measurementYear}-01-01`,
to: `${measurementYear}-12-31`,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Select runs by measurement period, not start year

When a historical measurement year is evaluated by a later rerun, this from/to window removes the run before its persisted measurement period is inspected. Manual runs explicitly accept an arbitrary evaluationDate (run-pipeline.ts:367-407), so a run started in 2028 for PY2027 is valid, but this report returns no_completed_population_run_for_year instead of its results, contrary to the contract that selects by the run's own period. The candidate lookup must include such reruns and reduce using measurementPeriodStart rather than startedAt.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d330049e. You are right and I had written the limit into ADR-082 as acceptable — which it is not, because a backdated rerun is a supported path rather than a hypothetical. RunStore.listPopulationRunsForPeriod (new, both stores + contract test) filters on the run's own measurement_period_start, so a run started in 2028 that scores PY2027 is selected. It also removes the reason the start-date window existed: listLatestPopulationRuns caps its walk at 25 runs whatever candidate count it is given, so a period-scoped read is simpler as well as correct. The contract test proves the later-started run is found, and that a CASE-scope rerun and an unfinished run are not.

Comment on lines +173 to +176
const winner = await selectRunForYear(deps, measureId, measurementYear);
if (!winner) {
entries.push(emptyEntry(measureId, matchedIds.size, "no_run", `no_completed_population_run_for_year`));
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit patient rows for measures without a run

When a requested year has no eligible run, this branch records every matched member as missingFromRun through emptyEntry and immediately continues without adding any MISSING_FROM_RUN rows. Because subjectListReportCsv serializes only report.rows, the downloaded patient-level artifact omits this measure entirely—or is header-only when no measures ran—while the JSON summary claims that N patients are missing, so its counts cannot be reconstructed from the CSV and recipients cannot identify the affected patients.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d330049e. A measure with no usable run now emits one MISSING_FROM_RUN row per matched member, so the count is reconstructable from the CSV and the ACO can see who. A COMPACTED measure stays the exception and claims nothing per subject — no rows and missingFromRun: 0 — because ADR-077 refuses numbers built over rows that may be incomplete, and "how many of your patients did this measure miss?" is such a number; DATA_MODEL_CONTRACTS §6.6 scopes the identities to non-compacted entries accordingly.

Comment thread frontend/app/(dashboard)/lists/page.tsx Outdated
Comment on lines +376 to +377
const thisYear = new Date().getUTCFullYear();
const years = [thisYear, thisYear - 1, thisYear - 2];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow selecting the pilot's upcoming performance year

In 2026 this hard-coded list offers only 2026, 2025, and 2024, even though the Maui target is PY2027 and the existing run form permits creating a run with a 2027 evaluation date. Such a run can be queried through the API but cannot be reported from this page until the system clock reaches 2027; use available run years, allow direct year entry, or include the upcoming pilot year.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d330049e. The select offers thisYear + 1 now, for exactly the reason you name: the pilot targets PY2027 while the clock says 2026, and a run can already be created with that evaluation date — so a list of past years alone made the one year this exists for unreachable from the page. Pinned by a test.

Comment thread frontend/app/(dashboard)/lists/page.tsx Outdated
Comment on lines +419 to +421
value={String(year)}
onValueChange={(v) => setYear(Number(v))}
options={years.map((y) => ({ value: String(y), label: String(y) }))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate the displayed report when the year changes

After computing one year, changing this selector leaves the old report and Download button visible while download immediately switches to the newly selected year. An operator can therefore see one year's table and download a different year's CSV; an in-flight compute can also resolve after a year change and repopulate the stale result. Clear or key the report by year and ignore responses whose requested year is no longer selected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d330049e. Changing the year clears the report and the refusal, so the table and the Download button cannot name different years; and a resolved compute whose year is no longer selected is discarded, read from a ref rather than the closure. Both are pinned by a test.

Finding this also exposed something worth recording: @mieweb/ui's Select renders a custom combobox whose options are not in the DOM, so userEvent.selectOptions cannot drive it — which is why the roster's page-size control is a native <select> with an aria-label. Both selects on this page follow that now, because a control with behaviour worth pinning has to be drivable by a test.

@Taleef7 Taleef7 self-assigned this Sep 16, 2026
…a measure that never ran

Four findings from Codex on the open PR, two of them P1.

The run selection filtered candidates by when a run STARTED. A manual run takes
an arbitrary `evaluationDate`, so a rerun-to-verify of a closed year begins in
the following one and legitimately scores the closed one — I had written that
limit into ADR-082 as acceptable, and it is not, because a backdated rerun is a
supported path rather than a hypothetical. The report would answer
`no_completed_population_run_for_year` with that run sitting in the table, which
is the shape of wrong answer this project refuses: it looks exactly like a right
one. `RunStore.listPopulationRunsForPeriod` filters on the run's own
`measurement_period_start`, which also removes the 25-run walk cap that made the
start-date window necessary in the first place, and is simpler than what it
replaced.

A measure with no usable run emitted no patient rows while its summary claimed N
members were missing from it. The CSV serialises `rows` alone, so the count was
unreconstructable from the artifact and the ACO could not see WHO. It emits one
MISSING_FROM_RUN row per matched member now. A COMPACTED measure stays the
exception and claims nothing per subject — no rows, `missingFromRun: 0` —
because ADR-077 refuses numbers built over rows that may be incomplete, and "how
many of your patients did this measure miss?" is such a number.

On the screen: the year select offered only past years, which made PY2027 — the
year the pilot exists for — unreachable until the clock caught up, even though a
run can already be created with that evaluation date. And changing the year left
the computed table and the Download button up while `download` read the NEW year,
so an operator could read one year's numbers and download another's; an in-flight
compute could also land after a year change and repopulate the stale table.

Both selects on the page are native `<select>` elements with `aria-label` now,
following the roster's page-size control: `@mieweb/ui`'s Select renders a custom
combobox whose options are not in the DOM, so `userEvent.selectOptions` cannot
drive it, and a control with behaviour worth pinning has to be drivable by a
test. Two sibling components were also keyed by the same list id.

Verification: backend typecheck clean, 2,742 tests, one failure (the standing
corpus-membership stale sparse-checkout, green in CI); frontend lint clean, 473
tests, build compiled. A new store-contract test proves the period read selects a
run started in a LATER year, and refuses a CASE-scope rerun and an unfinished run.
@Taleef7

Taleef7 commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

CI is green on d330049e, and the one verification gap is closed.

The Postgres ceiling contract ran, it did not self-skip. All 14 [postgres] subject lists: … tests passed against the CI postgres:16 service, plus [postgres] listPopulationRunsForPeriod selects by the run's PERIOD, not by when it started. That covers the three things the SQLite floor cannot check and that I could not run locally (Docker down, 1.3 GB free RAM): the pg_advisory_xact_lock revision allocation, the partial unique index that makes an alias AMBIGUOUS rather than a second MATCHED row, and the uuid[] array binds.

All four Codex findings are fixed and replied to in thread. The two P1s were both real and the first one corrected something I had written into ADR-082 as an acceptable limit — a rerun-to-verify of a closed year is a supported path, not a hypothetical, so selecting candidates by start date was a silent wrong answer rather than a stated trade-off.

Ready for review. The three open questions are at the bottom of the PR description; none of them blocks the merge.

@Taleef7
Taleef7 merged commit ef42449 into main Sep 17, 2026
23 checks passed
@Taleef7
Taleef7 deleted the feat/aco-attributed-list branch September 17, 2026 14:45
Taleef7 pushed a commit that referenced this pull request Sep 17, 2026
#574 landed the ACO attributed list, which adds six test files. The shard
split is computed from the live glob, so it absorbed them without a change:
260 files at 550/550/549 units. Measured individually they are 0.6-3.8s each,
so none earns a WEIGHTS entry and the balance is unaffected.

docs/JOURNAL.md conflicted because both sides added a top entry. Both are
kept, 2026-09-17 above 2026-09-16 — newest on top, as the file's convention
requires.
Taleef7 pushed a commit that referenced this pull request Sep 18, 2026
…e, by id

The post-merge verification ADR-083 called for. One synthetic case was closed as
admin on the sandbox (ffce1e30-5ffb-48cd-b9d6-862876d310fb — pat-19735, cms130,
cycle 2026-01-01) and asserted BY THAT ID on all four surfaces: the /cases
staff-closed row, the roster marker, the programs chip and the cases CSV. The
card states the reconciliation the change exists for: overdue 4,997 = open 4,996
+ closed-by-staff 1.

`e2e/tests/maui/staff-closed.spec.ts` pins it in the READ-ONLY Maui project and
is keyed on that id deliberately: "the tab returns a number" and "the chip
renders" are both satisfied by zero, so the feature could be wholly broken with
the suite green. Proven by mutation — pointing DESIGNATED at an id the sandbox
does not hold fails three of the five tests, and the two that survive are keyed
on the patient and on the chip-equals-tab identity rather than on the case id.
The fixture expires at cycle rollover, and the first test detects exactly that
and says to designate a fresh closure, so the expiry is a loud failure with
instructions rather than a quiet skip. 5/5 green against the sandbox.

After-numbers in the journal, measured warm and outside the recompute window.

Three record corrections found while re-reading the four source documents:

- The 2026-09-09 entry still read as though the ACO's one concrete ask were
  unbuilt. It shipped as #574 on 2026-09-17. A SINCE note now says so, because
  the journal is read newest-first and that paragraph is the one someone would
  act on.
- MEASURES.md gave the CMS catalog three different sizes in four places (44, 47,
  49). The catalog holds 49 CMS eCQM entries: 6 Active — cms122 and cms125 with
  authored CQL, cms2/cms130/cms165/cms137 official-only — and 43 Draft. The
  category table had also never been updated for cms137's routing (ADR-078).
- ROADMAP §7 gains item 18: at the 2026-08-27 session the per-patient compliance
  API was told to be killed and then walked back within the same minute, with a
  design rule attached. It was built to that rule and demoted rather than
  deleted, but no record of the follow-up conversation exists — so it is one
  confirmation to get before the surface is demoed, not a change.
Taleef7 added a commit that referenced this pull request Sep 23, 2026
…the measurement-year report (#574)

* feat(lists): the ACO's attributed list is an immutable, revisioned table on both stores

The one concrete ask from the 2026-09-09 working session: hand WorkWell the list
of patients the ACO attributes to the group, run the measures over exactly that
subset, and get numerator/denominator/exclusions back with patient-level results.
Every other population question this deployment answers is "the patients in our
directory"; this one is "the patients somebody else says are ours".

Two tables, owner-authorized in session on 2026-09-15. IMMUTABLE is the design
rather than a nicety: a report is a function of (list revision, run ids), so a
list that could be edited underneath one would make every number the ACO had
already filed unverifiable. A re-import is a new row with revision+1 under the
same name, the store interface exposes no mutator, and a manual resolution of a
non-match is therefore also a new revision — the rule ADR-022 applies to identity
matching.

NOT_FOUND members are KEPT. An identifier the directory cannot resolve is the ACO
and the practice disagreeing about who a patient is, which is a finding for the
review queue; dropping it would remove the patient from the denominator and from
the evidence that they were ever claimed. `resolution` and `subject_id` are
coupled by a CHECK rather than by convention, and a partial unique index refuses
a second MATCHED row for one subject, so an alias lands AMBIGUOUS rather than
doubling a patient in every denominator the list feeds.

Nothing is visible until the whole import landed — header IMPORTING, members in
chunks, audit, then COMPLETE, with every read filtering COMPLETE. "One
transaction" was never available on the floor (D1 caps a batch at ~90
statements), so rather than let the ceiling be quietly safer, both backends rely
on the flip and a crash at any point leaves an invisible partial row instead of a
short list masquerading as whole. The revision allocation IS transactional,
because it is the only part with a real race: an advisory xact lock on the
ceiling (never the session form — Neon's pooler is PgBouncer in transaction
mode), a single INSERT…SELECT MAX+1 with one retry on the floor.

The import rules are pure and tested where they can fail. Two are invisible from
a route test: resolution never consults a lookup that FABRICATES profiles (the
live directory returns a minimal profile for any `wc|` string, so a resolver
built on it would auto-match identifiers that exist nowhere), and the alias
collapse cannot fire under today's exact-id matching but is the seam the
identifier format changes at.

The sandbox data boundary is per PROFILE, and writing it as one pattern was the
first thing that would have been wrong twice: the Maui corpus is 48 fixture
patients with three-digit ids followed by generated five-digit ones, so a gate
written for the generated form alone would refuse the first 48 real patients in
the sandbox; and the default deployment is a different synthetic roster
(emp-NNN), where the corpus pattern would refuse everything. It is a namespace
test, not an existence test — pat-99999 conforms and is simply NOT_FOUND, so the
review queue is exercised with synthetic-shaped ids.

Verification: 12 store-contract tests green on the SQLite floor; 13 import-rule
tests green. The Postgres ceiling contract runs in CI's postgres:16 service —
Docker is down on this host and the floor cannot catch Pg-only SQL, which is how
two defects reached #544.

* feat(lists): import an attributed list, and filter every subject surface by it

The import route, its sandbox data boundary, and `?listId=` through the one
predicate six surfaces already share.

EVERY method on /api/subject-lists is CASE_MANAGER/ADMIN, metadata included —
deliberately not split into a GET rule and a write rule the way panels is. A
member row is a raw patient identifier another system asserted, and the list's
mere existence says which patients an ACO claims. The public /sandbox signs in as
a read-only VIEWER that may browse every AUTHENTICATED GET, and on Maui the
clinician seat is a VIEWER too, so leaving the reads to the /api/** catch-all
would have handed all of it to anyone who pressed "explore the sandbox".

The sandbox data boundary refuses the WHOLE upload before persistence when any
identifier is outside this deployment's own namespace, and reports the COUNT
rather than the values — an error body is logged and kept by the browser, so
echoing them would persist the very data the gate refuses. A live-directory
deployment cannot import at all: that directory is a worker-local last-known
registry that also fabricates profiles for `wc|` ids, so matching against it
would be silently incomplete — every row MATCHED and every denominator wrong —
rather than merely unavailable. The gate runs before the body is read.

The audit payload carries counts, provenance and revision and NO identifier; a
test asserts it, because audit_events is exported wholesale and an identifier
there would defeat the namespace gate by another route.

`?listId=` is a RESOLVED membership, never a token the predicate parses: the
parameter names an immutable list and the server turns it into subjects, so a
client cannot spell one. The set is memoized in an 8-entry LRU, which is safe
only because a list is immutable — a 50,000-id Set rebuilt on every /worklist
page load is the pool-pressure shape #560 removed from the case export.
`hasActiveSubjectFilters` tests `!= null`, not `.size > 0`: a list none of whose
identifiers resolved is a legitimate answer and an ACTIVE filter matching nobody,
and reading it as absent would answer "the ACO's population" with everybody.

Verification: 12 route tests, 2 Maui-profile gate tests (each profile's own
namespace is the admitted one — a single pattern would have refused the first 48
corpus patients on one profile and everything on the other), 6 cross-surface
tests, 1 authorization test; 108 existing route tests still green. Two mutations,
each caught: `.size > 0` in the active-filter guard revives the empty-list hole,
and dropping the resolution from the cases CSV breaks all three surface tests.

* feat(report): the attributed list's measurement-year numbers, with patient-level evidence

What the ACO actually asked for on 2026-09-09: numerator, denominator and
exclusions over the patients they attribute to the group, with the per-patient
result and its date. Computed from the same evidence every other rate here comes
from — `createRateAggregator` over the outcomes' persisted population
memberships — fed only the rows whose subject is in the list.

Four things that were each a way to be quietly wrong:

`measurementYear` is REQUIRED and has no default. An officially routed run is
scored over the calendar year containing its evaluation date (ADR-072), so in
January 2028 the newest run is a PY2028 one and "the latest numbers" would answer
a PY2027 question with next year's first nightly — a wrong number that looks
exactly like a right one. The selector walks past a newer run from the wrong
year.

Compaction is checked PER MEASURE, before and after the reads. ADR-077 refuses a
report built over rows that may be incomplete, but the refusal belongs to the
measure whose run aged out; withholding five complete measures because the sixth
did would be a second wrong answer. The whole request is 409 only when every
selected run is exposed. The post-read check is what makes a mid-report
compaction pass a 409 rather than a truncated 200 — a streamed CSV cannot change
its status after the first byte, so every derived row is computed before anything
is serialised.

`missingFromRun` is reported BESIDE the rates and never subtracted. A member the
run never evaluated is a gap in the evidence, not an exclusion, and folding them
into a denominator would let a SMALLER run produce a HIGHER score. Two
reconciliations are pinned by test: matched = seen + missing, and seen = scored +
unmeasured + errors + outOfPopulation.

The CSV is the patient-level artifact, with its header pinned exactly because the
ACO's tooling reads it by name. Three row shapes: one per (measure, rate) when
evaluated; one per MEASURE for a member the run never saw, with every population
cell EMPTY rather than 0 (which reads as a scored result); and exactly ONE row
per unresolved identifier, after the evaluated rows — one per measure would
multiply one disagreement by six. Text a person supplied is neutralised against
spreadsheet formula injection, because a CSV of somebody's uploaded identifiers
must not become code when the ACO opens it. `subjectHeaders` is exported rather
than copied, so the subject columns follow the deployment's own term.

The report read is audited like COMPLIANCE_API_READ, naming the list, revision
and run ids — what the December question about March's numbers needs — and no
identifier.

Verification: 14 report tests, 5 new route tests, 17 route tests in total for the
surface. Backend 2,724 tests, 2,700 pass, one failure — the standing
corpus-membership stale sparse-checkout one, green in CI. Two mutations, each
caught: dropping the post-read compaction check fails 3 tests, and taking the
newest run regardless of year fails 5.

* feat(lists): the attributed-list screen — import, the review queue, and the year's numbers

One page for the ask: upload the list, see what resolved and what did not, and
take the measurement year's numbers off it.

The unresolved members get a filter of their own rather than a count in a corner.
An identifier the directory cannot resolve is the ACO and the practice
disagreeing about who a patient is — a finding somebody has to work, and a list
that reported only its matched count would hide exactly the rows worth a phone
call.

The server's refusal is rendered VERBATIM. The sandbox-namespace gate answers
with a COUNT of identifiers outside the deployment's directory, and "Import
refused" alone would drop the one number the operator needs to go back to the
ACO with.

The report's measurement year is a select with no default, because ADR-072 scores
an officially routed run over its calendar year: "the latest numbers" would
answer a PY2027 question with PY2028's first nightly the moment January arrives,
and it would look right. A measure whose run predates a retention cutoff is named
with its reason beside the measures that did report — withholding the others
would be a second wrong answer, and showing that one's numbers would be the
first.

The lists hook carries the same hardening `use-panel-payers` and
`use-assignable-users` needed: a row that is not what it claims to be is dropped,
and a payload that arrived wholly unusable warns rather than rendering an empty
state that reads as "this deployment has no lists". Both fetch effects defer out
of the synchronous body, the idiom compliance/ and cases/ already use for
react-hooks/set-state-in-effect; the members table resets its page in the CHANGE
HANDLER and remounts per list via a key, so no effect writes state at all.

Verification: 5 page tests, frontend lint clean, 471 tests, build compiled. One
mutation, caught: dropping the row shape guard fails the picker test.

* docs(lists): ADR-082, the report's column contract, and the two PHI-phase items it defers

ADR-082 records seven decisions: a list is immutable (a report is a function of
list revision and run ids, so an editable list makes every filed number
unverifiable); an attribution is not an assignment and neither is a denominator;
NOT_FOUND members are kept and an alias is AMBIGUOUS rather than a silent
collapse; the sandbox data boundary, per profile and before persistence; the
report is for a measurement year and compaction refuses per measure;
missingFromRun is reported beside the rates and never subtracted; and the CSV is
the patient-level artifact with a pinned header.

DATA_MODEL_CONTRACTS gains §6.6 (the report's columns, its three row shapes, and
why the two non-evaluated ones carry the empty string rather than 0) and §6.7
(`?listId=` on the six filtered surfaces). Both are APPENDED — nothing in
§6.1–§6.5 moves.

DATA_MODEL §3.29 carries the two tables and the visible-state machine; MCP.md
documents the listId argument and states that it exposes nothing the tool's role
gate did not; ARCHITECTURE §3 places the report as the sixth population read
model and the only one whose population is asserted from outside; DEPLOY gains
the runbook — nothing to run, no backfill, rollback-safe, the request budgets,
and the retained-report stopgap that is the ACO's audit trail until the archive
ships.

PRODUCTION_READINESS §4 gains the two PHI-phase items this defers: an
authoritative subject resolver (the live directory fabricates profiles for
unknown `wc|` ids, so matching against it would be silently incomplete) and the
per-run report archive, with its design, for the ACO's audit window.

Guide chapter 10 gains the scenario in plain words, and the journal entry records
the two source hazards this session hit: a literal U+FEFF and a literal tab + CR
written into regexes by tooling that collapsed the escapes, both invisible on
screen and caught only by dumping bytes.

* test(e2e): the attributed-list screen and its authorization boundary, read-only

Importing is a WRITE and belongs in `maui-writes`; this project runs first and
must be harmless when the thing it tests is broken, so every assertion is about a
surface that exists whether or not any list has been imported, and the negative
checks name a list id no import can have produced.

The one thing worth an e2e rather than a unit test: this route's READS are
CM/ADMIN, unlike every other directory surface on the deployment. A regression
that dropped them to the AUTHENTICATED catch-all would look entirely normal in a
unit test of the page, and would hand an ACO's attribution to the read-only
sandbox seat.

* fix(lists): the review round — a gate that could not fire, and a reconciliation that was false

Four review lanes on the whole diff. The two that matter were found
independently by more than one of them.

`?listId=` re-opened the membership the CM/ADMIN gate exists to close. Every
method on /api/subject-lists is CM/ADMIN because the list's existence says which
patients an ACO claims — but five of the six surfaces that accept `?listId=` are
AUTHENTICATED, so a read-only VIEWER holding a list id could take the whole
membership out of `GET /api/exports/cases?format=csv&listId=<uuid>`: names,
provider, payer, per-measure status. That is strictly MORE than the members
endpoint the gate protects, and the id is not a secret by construction — it sits
in the query string of every filtered screen, so it reaches shareable URLs,
browser history and access logs. A gate that reads as present and cannot fire for
the widest read is this repo's own vacuous-guard shape at the system level.
Enforced once in the worker, where authorization decisions live.

The stated reconciliation was arithmetically false. `createRateAggregator`'s
`unmeasured` is a SUPERSET of its `evaluationErrors` — it starts the count at the
error count — so subtracting both double-counted every error, and a row that was
both out-of-population and an evaluation error made `scoredSubjects` NEGATIVE.
The test that "pinned" it used a fixture with all three counts at zero, which
makes the assertion `2 === 2 + 0 + 0 + 0` and passes for any implementation. Each
seen subject is now classified into exactly ONE bucket in a stated order (error,
then out-of-population, then in-no-rate, then scored), so the identity holds on a
PARTIAL_FAILURE run — an ordinary night on the pilot.

Also, each verified before folding:

- The default profile's namespace refused fifty legitimate `ihn-emp-NNN` members
  of its own directory — the same defect as refusing Maui's 48 fixtures, in the
  other deployment. And an unrecognised profile is refused outright now rather
  than lent another profile's namespace.
- The run search took the twelve most recent runs outright, which on a nightly
  deployment is twelve DAYS: from mid-January a report for the closed year
  answered "no run" while that year's runs sat uncompacted.
  `listLatestPopulationRuns` walks at most 25 runs whatever count it is given, so
  scoping to the year's window was the only mechanism that could work.
- The per-row rate flags built a whole aggregator per row — 300,000 of them at
  the 50,000-member cap across six measures — where `membershipRatesFor` is the
  thing wanted, and it is read once per row for both the flags and the bucket.
- `name`/`source`/`note` were the one unvalidated channel into Neon, and
  name/source are copied into an audit payload that is exported wholesale. Capped
  at 200 by the route and by a CHECK in both schemas.
- `listMembers` and `countMembers` did not filter COMPLETE, safe only because
  every caller checked first — a claim about callers, not about the store.
- The duplicate collapse compared timestamps as STRINGS, so an offset-form
  `evaluatedAt` could pick the older clinical evaluation.
- The CSV carried the resolved subject id in the `rawIdentifier` column, which is
  right only while matching is exact and wrong the moment the format changes —
  which is the entire point of that column.
- `emptyEntry` broke `matchedSubjects = seen + missingFromRun` for exactly the
  entries a reader is most likely to check: the ones with no numbers.
- The 2 MB cap was measured after buffering the body, and had no test.
- A test titled "the body is never read" asserted only a status code.

One claim was checked and REJECTED: a lane reported that an unrecognised profile
would fail OPEN and admit real identifiers. It would not — the fallback was the
`emp-` pattern, which refuses an MRN — and the profile id is a closed union. The
underlying point was still worth taking.

Verification: backend typecheck clean, 2,738 tests, one failure (the standing
corpus-membership stale sparse-checkout, green in CI). Seven mutations, each
caught; the new one is removing the `?listId=` gate, which fails three worker
tests.

* fix(report): select a run by the period it SCORES, and emit rows for a measure that never ran

Four findings from Codex on the open PR, two of them P1.

The run selection filtered candidates by when a run STARTED. A manual run takes
an arbitrary `evaluationDate`, so a rerun-to-verify of a closed year begins in
the following one and legitimately scores the closed one — I had written that
limit into ADR-082 as acceptable, and it is not, because a backdated rerun is a
supported path rather than a hypothetical. The report would answer
`no_completed_population_run_for_year` with that run sitting in the table, which
is the shape of wrong answer this project refuses: it looks exactly like a right
one. `RunStore.listPopulationRunsForPeriod` filters on the run's own
`measurement_period_start`, which also removes the 25-run walk cap that made the
start-date window necessary in the first place, and is simpler than what it
replaced.

A measure with no usable run emitted no patient rows while its summary claimed N
members were missing from it. The CSV serialises `rows` alone, so the count was
unreconstructable from the artifact and the ACO could not see WHO. It emits one
MISSING_FROM_RUN row per matched member now. A COMPACTED measure stays the
exception and claims nothing per subject — no rows, `missingFromRun: 0` —
because ADR-077 refuses numbers built over rows that may be incomplete, and "how
many of your patients did this measure miss?" is such a number.

On the screen: the year select offered only past years, which made PY2027 — the
year the pilot exists for — unreachable until the clock caught up, even though a
run can already be created with that evaluation date. And changing the year left
the computed table and the Download button up while `download` read the NEW year,
so an operator could read one year's numbers and download another's; an in-flight
compute could also land after a year change and repopulate the stale table.

Both selects on the page are native `<select>` elements with `aria-label` now,
following the roster's page-size control: `@mieweb/ui`'s Select renders a custom
combobox whose options are not in the DOM, so `userEvent.selectOptions` cannot
drive it, and a control with behaviour worth pinning has to be drivable by a
test. Two sibling components were also keyed by the same list id.

Verification: backend typecheck clean, 2,742 tests, one failure (the standing
corpus-membership stale sparse-checkout, green in CI); frontend lint clean, 473
tests, build compiled. A new store-contract test proves the period read selects a
run started in a LATER year, and refuses a CASE-scope rerun and an unfinished run.

---------

Co-authored-by: Taleef <taleef@gmail.com>
Taleef7 pushed a commit that referenced this pull request Sep 23, 2026
…e, by id

The post-merge verification ADR-083 called for. One synthetic case was closed as
admin on the sandbox (ffce1e30-5ffb-48cd-b9d6-862876d310fb — pat-19735, cms130,
cycle 2026-01-01) and asserted BY THAT ID on all four surfaces: the /cases
staff-closed row, the roster marker, the programs chip and the cases CSV. The
card states the reconciliation the change exists for: overdue 4,997 = open 4,996
+ closed-by-staff 1.

`e2e/tests/maui/staff-closed.spec.ts` pins it in the READ-ONLY Maui project and
is keyed on that id deliberately: "the tab returns a number" and "the chip
renders" are both satisfied by zero, so the feature could be wholly broken with
the suite green. Proven by mutation — pointing DESIGNATED at an id the sandbox
does not hold fails three of the five tests, and the two that survive are keyed
on the patient and on the chip-equals-tab identity rather than on the case id.
The fixture expires at cycle rollover, and the first test detects exactly that
and says to designate a fresh closure, so the expiry is a loud failure with
instructions rather than a quiet skip. 5/5 green against the sandbox.

After-numbers in the journal, measured warm and outside the recompute window.

Three record corrections found while re-reading the four source documents:

- The 2026-09-09 entry still read as though the ACO's one concrete ask were
  unbuilt. It shipped as #574 on 2026-09-17. A SINCE note now says so, because
  the journal is read newest-first and that paragraph is the one someone would
  act on.
- MEASURES.md gave the CMS catalog three different sizes in four places (44, 47,
  49). The catalog holds 49 CMS eCQM entries: 6 Active — cms122 and cms125 with
  authored CQL, cms2/cms130/cms165/cms137 official-only — and 43 Draft. The
  category table had also never been updated for cms137's routing (ADR-078).
- ROADMAP §7 gains item 18: at the 2026-08-27 session the per-patient compliance
  API was told to be killed and then walked back within the same minute, with a
  design rule attached. It was built to that rule and demoted rather than
  deleted, but no record of the follow-up conversation exists — so it is one
  confirmation to get before the surface is demoed, not a change.
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.

MM-2 PR 3: import the ACO's attributed patient list and report num/den/exclusions with patient-level evidence

1 participant