2026-04-10 — EHR pipeline hardening, GDPR cascade fix, voice guardrails, Lab Results UI¶
Author: Srikanth Donthi (CPO/CTO) + Claude Code session Theme: Critical data correctness fixes across the EHR rebuild pipeline (asyncpg syntax, cross-case document scoping, FHIR deduplication), a production GDPR erasure bug patched, voice guardrail tightening, and a full Lab Results + Documents tab on the frontend — plus a 21-finding code review sweep with 17 resolved on the same day
Headline numbers¶
- 20 PRs merged (12 backend, 8 frontend)
- 21 code review findings (4 P0, 6 P1, 7 P2 fixed on the day; 4 deferred)
- 3 new EHR rebuild tests added
- 23 new lab flag unit tests on the frontend
- 45/45 EHR drawer tests passing after full V2 rewrite
Shipped to production¶
Backend (curaway-ai/curaway-backend)¶
| PR | Title | What it does |
|---|---|---|
| #134 | fix(ehr): asyncpg IN-clause syntax | WHERE id IN :ids → WHERE id = ANY(:ids). Asyncpg does not support SQLAlchemy-style IN binding; every EHR rebuild that referenced multiple document IDs was crashing with a parameter binding error. This was blocking all EHR rebuilds. |
| #136 | fix(ehr): cross-case document scoping | EHR snapshot now includes documents referenced by FHIR resources — not just documents directly linked to the current case. Observation source now surfaces the originating document filename. A documents list is written into the EHR snapshot at rebuild time. |
| #135 | fix(voice): first-message formatting rules + forbidden phrases | Added explicit first-message formatting rules to the voice prompt. Banned "happy to help" and its variants from all patient-facing responses — these hollow openers are now in config/voice_rules.yaml and enforced at runtime. |
| #137 | fix(critical): GDPR cascade — double-consumed iterator + bare excepts | .scalars().all() was being called on the same result set twice; FHIR resources survived erasure because the second iteration yielded nothing. Fixed iterator consumption order. Also narrowed 2 bare except: pass blocks to typed exceptions so silent failures can no longer swallow real errors. |
| #138 | fix(p1): FHIR dedup scoped to same resource_type | Deduplication was matching across resource types (e.g., a Condition could suppress an Observation). Scoped the dedup query to resource_type to prevent data loss. Also: extracted_data JSON string handling hardened, first-message prompt clarified for mixed emotional + procedural messages. SQL cleanup. |
| #139 | fix(p2): import order, audit event best-effort, EHR rebuild tests | Import order in internal.py corrected (Ruff compliance). Audit event write in documents.py moved to best-effort (non-fatal) so a failed audit log cannot block a document operation. 3 new EHR rebuild tests covering the asyncpg fix and cross-case scoping. |
| #140 | fix(compliance): DataForwardingAudit wired into GDPR cascade | DataForwardingAudit records were not deleted during GDPR erasure — a compliance gap. Now included in the cascade. Also added tenant_id filter on document lookup that was missing, tightening tenant isolation. |
| #142 | fix(voice): medical_advice guardrail opener | Removed "That's a really important question" as the opener on the medical_advice guardrail response path — a hollow acknowledgment that violates voice rules. Guardrail now redirects with substance rather than a filler phrase. |
Frontend (curaway-ai/curaway-frontend)¶
| PR | Title | What it does |
|---|---|---|
| #25 | feat(ehr): Lab Results tab | New Lab Results tab in the EHR drawer. 4-column table (Test / Value / Reference Range / Source). Color-coded rows by flag severity (high/low/critical). Reference ranges displayed inline. |
| #26 | feat(ehr): Documents tab + source display fix | New Documents tab listing all documents in the EHR snapshot with filename, type, and upload date. Source display on Observations fixed to show the actual document filename surfaced by PR #136. |
| #27 | fix(ehr): getLabFlag handles >/< prefixes, unisex hemoglobin range | getLabFlag() was returning normal for values reported as ">12.5" or "<0.5" because the numeric parse failed on the prefix. Prefix stripped before comparison. Hemoglobin reference range now uses a unisex range (not male-only) to avoid false flags on female patients. |
| #28 | fix(p2): mobile file picker, source break-words, 23 lab flag tests | Mobile file picker tap target enlarged to meet touch accessibility minimums. Long document source strings (filenames) now use break-words CSS to prevent overflow. 23 new Vitest unit tests for getLabFlag covering prefixed values, edge cases, and the unisex hemoglobin range. |
| #29 | fix(test): EHR drawer tests rewritten for V2 — 45/45 passing | The V1 drawer test suite was testing removed components. Full rewrite targeting the V2 FullEHRDrawer and its Lab Results + Documents tabs. All 45 tests pass. |
Code review sweep¶
21 findings surfaced during the session review pass:
| Severity | Count | Disposition |
|---|---|---|
| P0 (data loss / compliance) | 4 | All fixed (#137, #138, #140) |
| P1 (correctness) | 6 | All fixed (#134, #136, #138, #139, #142) |
| P2 (quality / lint) | 7 | All fixed (#135, #139, #28) |
| Deferred | 4 | Tracked below |
Documented and parked for later¶
Deferred from the code review sweep¶
- EHR rebuild backfill job — existing cases with no
documentslist in their snapshot will show an empty Documents tab until their next rebuild. A one-shot QStash job to triggerehr_rebuildfor all cases withehr_completestatus would resolve this. Deferred: low urgency, rebuilds on next chat turn anyway. DataForwardingAuditerasure certificate — the deletion cascade now removes the records, but the GDPR deletion certificate does not yet report theDataForwardingAuditrow count alongside the other affected table counts. Minor compliance gap in the audit trail output only.- Lab Results empty state — when
ehr_snapshot.observationsis empty the tab renders nothing. Should show a "No lab results recorded yet" placeholder to avoid a blank tab experience. - Voice rules CI coverage for new forbidden phrases —
test_voice_compliance.pydoes not yet enumerate the "happy to help" family in its forbidden-phrase fixture list. The runtime validator catches violations in production; the CI test does not catch them in source code scans.
Verification still needed¶
Not blocking, but worth confirming in the next session:
- Run
pytest tests/test_ehr_rebuild.py -vin Railway to confirm the 3 new rebuild tests are green (skipped locally — no venv) - Trigger a manual EHR rebuild on an existing case and confirm the Documents tab populates with the correct filenames
- Submit a GDPR erasure request for a test patient and verify the deletion
certificate row counts include
data_forwarding_audit - Check Langfuse for any remaining
FHIR_VALIDATION_*errors that may have been masked by theexcept: passblocks now converted to typed exceptions - Manually test the Lab Results tab on a case with mixed
>/<prefixed lab values (e.g.,">5.0"eGFR) to confirmgetLabFlagcolor-coding is correct after the prefix-strip fix