Skip to content

Maui profile: four surfaces resolve names without filtering rows, and three enumerate the full measure catalog (MM-0 follow-up) #501

Description

@Taleef7

The Maui deployment profile (#500, MM-0) scopes the roster's population and columns, and isolates rows in the programs read models. Four surfaces are still name-resolving but not row-filtering, and one class of surface still enumerates the full measure catalog. All were found by review during #500 and deliberately deferred there rather than growing a PR that existed to answer two review comments.

None of these is reachable on a freshly provisioned Maui database, which contains no foreign rows. They become reachable the moment a Maui database is restored from, imported into, or re-pointed at a database holding TWH/IHN/seed:scale data — the same premise as the two P1s already fixed in #500.

1. Row-filtering gaps — foreign subject identifiers reach a client

These resolve names through the profile-scoped directory but never require a row's subject to resolve before including it. When the lookup misses they fall back to the raw subjectId, so the identifier itself is rendered — the same leak class as fb1fac0c (segment preview) and the programRiskOutlook gap fixed in 10816fe4.

  • backend-ts/src/export/export-csv.ts (~99, ~182, ~186) — the one I would not leave long. It writes foreign rows into a downloadable file, so the leak leaves the application entirely and persists wherever the CSV is sent. Highest priority of the five.
  • backend-ts/src/case/case-read-models.ts (~53, ~56) — emits employeeName: emp?.name ?? c.employeeId, so /api/cases on a scoped profile renders the other deployment's subject identifiers verbatim.
  • backend-ts/src/mcp/tools.ts (~329, ~420, ~424) — the read-only MCP tool surface, same shape.
  • backend-ts/src/run/read-models.ts (~201) — same shape.
  • backend-ts/src/segment/segment-seed.ts (~20-22) — builds ALL_SITES from the unscoped EMPLOYEES, so every TWH site name is seeded into Maui's segment rules and shown in Configure Groups. Note this one was left unscoped deliberately in feat(maui): the Maui sandbox — deployment profile, deploy workflow, sandbox accounts, and a guard that finds its own workflows (MM-0) #500: the seeded segment's site list is load-bearing for Maui applicability, so changing it needs the applicability path checked first, not just a swapped import.
  • backend-ts/src/program/program-read-models.ts programSites (~166-171) — builds the site-filter options from unfiltered rows, so a scoped profile can offer a site that matches no visible row. Cosmetic (an option matching nothing) rather than a data leak.

The pattern to follow is profileMatcher in program-read-models.ts: a predicate requiring the subject to resolve in the injected directory, applied before aggregation, and gated so it is a no-op on the default profile. That gate is load-bearing, not caution — a TWH database legitimately holds subjects absent from the synthetic catalog (QRDA Category I imports keyed by Cypress MRNs, ADR-051/056), and an ungated filter would silently drop exactly those. #500 has a test asserting the default profile still counts an unresolvable subject; each of these needs the same pair.

2. Measure-set scoping — Maui lists 11 measures that can never produce an outcome

These iterate every MEASURE_CATALOG measure with status === "Active" rather than the profile's runnable set, so Maui's surfaces list all 14 measures where only cms122, cms125 and hypertension can run.

  • backend-ts/src/program/program-read-models.ts programOverview (~253) — /api/programs returns 14 summaries, 11 permanently zero.
  • backend-ts/src/program/hierarchy-rollup.ts (~90-91) — same, for the drill-down.
  • backend-ts/src/routes/orders.ts (~42-58) — proposes orders across all Active measures, and does not apply a subject filter either, so it belongs to both halves of this issue.

This is the same defect class as the roster-column P1 fixed in 4b165b88, on a different surface. isRunnableMeasure from src/config/deployment-profile.ts is the predicate; it is all-true on the default profile, so each change is inert there.

3. A UI state that misattributes a deployment problem to a person

When a profile leaves no panel with a runnable, catalog-Active measure, availablePanels is [] and the roster serves zero columns. panels.ts logs a warning naming the cause, but that reaches an operator reading container logs, not the person looking at the screen:

  • frontend/app/(dashboard)/compliance/page.tsx renders a <select> with zero options and a value matching none of them.
  • frontend/features/employee/components/IndividualComplianceStatus.tsx renders "No evaluated measures for this patient yet" — a claim about the patient when the truth is a claim about the deployment.

This is the shape ADR-043 warns about (a silent empty result is the dangerous one) and the same defect review caught on the CDS cards in ADR-067. Fix: branch on availablePanels.length === 0 in both components and say that no panel is configured for this deployment.

Reachable in MM-1, which is why it is worth doing rather than filing as theoretical: cms2, cms130 and cms165 appear in no PANELS entry, so adding them to MAUI_MEASURE_IDS makes them runnable and invisible in the roster.

4. Smaller items from the same review

  • backend-ts/src/compliance/panels.ts — the console.warn for the no-available-panel case is untested and can be deleted silently. runProfileChild already captures the child's stderr; have it return that alongside the parsed stdout and assert on it.
  • backend-ts/src/compliance/roster-read-model.ts (~97) — RUNNABLE_PANELS[panel] ?? [] is unreachable (panel is PanelId, RUNNABLE_PANELS is a total Record). It reads as a guard against a state the types forbid — the vacuous shape this repo audits for.
  • panels.ts builds activeCatalogMeasures while roster-read-model.ts (~86) rebuilds the identical set for the segment branch. Export it from panels.ts so the two cannot drift the way availability and column derivation just did.
  • panels.ts's module-level active snapshot is only safe because MEASURE_CATALOG is never mutated at runtime (lifecycle mutations go to the measure store). Worth one line saying so — if that ever changes, the roster columns freeze for the process lifetime.
  • The runProfileChild helper is duplicated byte-for-byte in panels.test.ts and program-read-models.live-directory.test.ts. More substantively, those child test bodies are template strings, so tsc --noEmit never type-checks them — a ProgramDeps shape change surfaces as an opaque non-zero exit rather than a compile error.

Scope note

Adding a deployment-profile dimension to quality_snapshots keys is not in this issue. It is the real fix for the monthly-trend leak that #500 works around by falling back to the per-run path, and it is a schema change — owner-owned per CLAUDE.md, so it needs its own decision rather than being folded in here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions