Skip to content

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 :idsWHERE 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

  1. EHR rebuild backfill job — existing cases with no documents list in their snapshot will show an empty Documents tab until their next rebuild. A one-shot QStash job to trigger ehr_rebuild for all cases with ehr_complete status would resolve this. Deferred: low urgency, rebuilds on next chat turn anyway.
  2. DataForwardingAudit erasure certificate — the deletion cascade now removes the records, but the GDPR deletion certificate does not yet report the DataForwardingAudit row count alongside the other affected table counts. Minor compliance gap in the audit trail output only.
  3. Lab Results empty state — when ehr_snapshot.observations is empty the tab renders nothing. Should show a "No lab results recorded yet" placeholder to avoid a blank tab experience.
  4. Voice rules CI coverage for new forbidden phrasestest_voice_compliance.py does 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:

  1. Run pytest tests/test_ehr_rebuild.py -v in Railway to confirm the 3 new rebuild tests are green (skipped locally — no venv)
  2. Trigger a manual EHR rebuild on an existing case and confirm the Documents tab populates with the correct filenames
  3. Submit a GDPR erasure request for a test patient and verify the deletion certificate row counts include data_forwarding_audit
  4. Check Langfuse for any remaining FHIR_VALIDATION_* errors that may have been masked by the except: pass blocks now converted to typed exceptions
  5. Manually test the Lab Results tab on a case with mixed >/< prefixed lab values (e.g., ">5.0" eGFR) to confirm getLabFlag color-coding is correct after the prefix-strip fix