Skip to content

CDS Hooks and compliance reads scan a subject's whole outcome history; add a per-measure store query #470

Description

@Taleef7

listOutcomesForEmployee(subjectId, 100000) returns every outcome row a subject has ever accrued and each store implementation JSON.parses that row's evidence_json. Five callers do this; four are batch or operator-facing, and one is now on an interactive, point-of-care path.

caller path
mcp/tools.ts:348 check_compliance
routes/compliance-api.ts:271 GET /api/v1/compliance/... (ADR-061 mode=latest)
routes/identity.ts:102 cross-system identity
run/employee-profile.ts:127 employee profile
routes/cds-hooks.ts:153 POST /cds-services/{id} — a CDS client renders this while a clinician waits

Why the window is large, and why shrinking it is the wrong fix

The 100,000 is deliberate and documented at compliance-api.ts:262-270: outcomes has no uniqueness on (subject, measure, period), so every run inserts a fresh row — a nightly ALL_PROGRAMS run over ~16 measures writes ~16 rows per subject per night. A small window makes a valid older outcome fall outside it and read as "no run has covered this subject", which for the CDS surface is the exact confusion noEvaluationCard exists to prevent. So the lever is not the limit.

The fix

A per-measure query in the store, so the work is bounded by the measure count rather than by history:

  • OutcomeStore.listLatestOutcomesPerMeasure(subjectId) in src/stores/outcome-store.ts
  • Postgres ceiling: DISTINCT ON (measure_id) ... ORDER BY measure_id, evaluated_at DESC
  • SQLite floor: a window function or a GROUP BY with a correlated max
  • A case in src/stores/store-contract.ts, so both implementations are held to the same behaviour — including the part the current code does in TypeScript: only rows whose run is COMPLETED or PARTIAL_FAILURE count, so the query must join runs rather than return the newest row regardless of run status

Then move cds-hooks.ts onto it, and consider compliance-api.ts and check_compliance, which want the same shape for one measure.

Scope note

Not a correctness bug and not introduced by #469 — the CDS route copied an existing constant deliberately. What #469 introduced is a latency budget for it. Nothing fires the hook today (no WebChart client, no configured CDS client), so this is unobserved: worth measuring against a realistic history before optimising, since the "~11k rows after two years" figure is extrapolation, not measurement.

Stated as a limit in docs/CDS_HOOKS.mdLimits, stated so an integrator reading the contract is not surprised.

Found by code review of #469.

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