## Outcome Read-only audit completed at immutable commit `3f69e46823269c97260976270e53acbcc45f00d4`. No files were modified. The minimal safe design is **additive**: leave `medication_administrations` and all legacy rows byte-for-byte unchanged, introduce typed medication/prescription/schedule/event tables, mechanically link legacy rows without parsing or reclassification, and move every future medication write through one preview-bound, idempotent worker path. --- # 1. Exact files and functions to change ## New files ### `scripts/health/dashboard_v5/sprint7c_f_medication_schema.py` Add: - `SCHEMA_VERSION = "dashboard_v5_7c_f_medication_v1"` - `MEDICATION_DDL` - `apply_schema(connection)` - `assert_schema(connection)` - schema/trigger/index inventory constants ### `scripts/health/dashboard_v5/medication_contract.py` Add the shared strict contract: - `validate_medication_preview_request(payload)` - `validate_medication_confirm_payload(payload)` - `canonical_medication_payload(payload)` - `medication_payload_hash(payload)` - `medication_preview_hash(preview)` - `normalize_route_for_new_input(value)` - closed enums for prescription status, event status, route, provenance, injection side/site - no legacy parsing helpers ### `scripts/health/migrate_sprint7c_f_medication_schema.py` Copy-first, idempotent migration modeled on: - `scripts/health/migrate_sprint6h_b_schema.py` - `scripts/health/migrate_patient_action_schema.py` Required functions: - `connect` - `check` - `freeze_legacy_projection` - `legacy_projection_digest` - `backup_database` - `migrate_copy` - `backfill_legacy_links` - `verify_idempotent` - `restore_proof` - `safe_migrate` - `main` ## Existing backend files ### `database/schema.sql` Append the new canonical tables, indexes, triggers, view, and migration marker schema. Do **not** alter or rebuild `medication_administrations`. ### `scripts/health/health_dashboard_action_worker.py` Change: - imports: add medication schema/contract functions - `validate_payload` - accept only medication contract v2 for new writes - retain v1 validation only for already-queued compatibility, if required - replace medication branch in `validate_patient_payload` - replace medication branch in `apply_patient_action` - change medication branch in `apply_capture_action` - update `apply_action` - update `write_action_receipt` - update `main` Add: - `build_medication_preview(connection, payload)` - `apply_medication_action(payload, action_id) -> str` - `_assert_medication_schema(connection)` - `_resolve_current_prescription` - `_resolve_schedule` - `_resolve_correction_target` - `_validate_medication_baseline` - `_insert_medication_event` Important: `apply_medication_action` must own one `BEGIN IMMEDIATE`; it must not call schema migration helpers or hidden commits. ### `scripts/health/health_dashboard_server.py` Change: - `validate_patient_action` - `_pending_action_identity` - `write_action_payload` - route dispatch around `MEDICATION_EVENT_ROUTE` Add: - `MEDICATION_PREVIEW_ROUTE`, preferably `/api/v1/medications/preview` - `MEDICATION_CONFIRM_ROUTE`, preferably `/health-actions/medication-confirm` - `validate_medication_preview_submission` - `validate_medication_confirm_submission` - read-only `medication_preview(database, request)` The server may read the DB and enqueue a validated confirmation, but must never write health tables directly. Pending identity must be: ```python ("medication_confirm", idempotency_key) ``` not medication/date/name. Same key plus changed payload must conflict. ### `scripts/health/dashboard_v5/read_api.py` Update all medication readers to use one shared effective resolver: - `_day_medications` - `_record_medications` - `_next_planned_medications` - `_record_summary` - capture plan reader near `/api/v1/capture/plans` - calendar medication branch - doctor-report medication section, if it consumes `_record_medications` Add: - `_canonical_medication_events` - `_legacy_medication_events` - `_effective_medication_events` - `_medication_catalog` - `_medication_schedules` - `_medication_preview` Rules: - canonical events are read from the new tables/view; - untouched legacy rows remain available through the legacy adapter; - a backfilled canonical shadow must not cause a legacy row to display twice; - new writes must not be projected back into `medication_administrations`. ### `scripts/health/dashboard_v5/data_provider.py` Replace direct legacy logic in: - `medication_data` with the shared effective medication resolver. Remove casefold/name/date cancellation inference for canonical rows. Keep it only in a clearly labeled legacy adapter where compatibility requires it. ### `scripts/health/dashboard_v5/capture_contract.py` Medication must stop using the generic `{name, amount, unit, status}` contract. Either: 1. preferred: the capture UI calls the new medication preview/confirm action directly; or 2. extend the payload with the complete v2 medication capability: ```text medication_id prescription_revision_id schedule_id planned_at planned_dose actual_dose route_original route_normalized injection_* lot_number corrects_event_id correction_reason target_business_revision preview_hash idempotency_key ``` Do not leave the current name-based capture writer as an alternate bypass. ## Existing frontend files ### `scripts/health/assets/health-assets/dashboard-v5-capture.js` Change: - `state.plans.medications` to use opaque `medication_id`, `prescription_revision_id`, and `schedule_id` - `administrationFields` - `planNames` — replace with plan objects, not names - `buildData` - submit handler - correction prefill - processing status polling Required UI distinctions: - planned dose and actual dose are separate - status is planned/administered/missed/cancelled - correction selects an exact event and requires a reason - correction chooses the corrected status; “corrected” is not the new event’s business status - original route and normalized route are separate - injection fields and lot are shown only as documentation fields - preview must be obtained before confirm - any form change invalidates the preview and disables confirm ### `scripts/health/dashboard_v5/render.py` Replace or remove the current legacy medication dialog around line 172. It currently allows a free-standing `event_type='corrected'` without target or reason. Ensure both the legacy dialog launcher and capture hub route through the same v2 medication contract. Do not retain a direct v1 POST bypass. ## Fixtures and tests ### `tests/fixtures/dashboard_v5_fixture.py` Add deterministic synthetic rows for: - active prescription with documented provenance - unknown prescription status/provenance - one schedule - one planned event - one administered event with planned/actual deviation - injection route/site/side/device/lot - one correction chain - one mechanically migrated legacy free-dose row Keep existing legacy medication fixture rows unchanged. ### New focused tests - `tests/test_dashboard_v5_sprint7c_f_medication_schema.py` - `tests/test_dashboard_v5_sprint7c_f_medication_actions.py` - `tests/test_dashboard_v5_sprint7c_f_medication_read.py` - `tests/browser/dashboard_v5_sprint7c_f_medications.spec.js` Update relevant existing regressions: - `tests/test_dashboard_v5_sprint6h_b.py` - `tests/test_dashboard_v5_capture_sprint5c.py` - record/day/calendar tests consuming medication output --- # 2. Proposed additive DDL Names may be shortened, but the following constraints should remain. ```sql CREATE TABLE IF NOT EXISTS medications ( id TEXT PRIMARY KEY CHECK(id GLOB 'med_[0-9a-f]*' AND length(id) = 28), name_original TEXT NOT NULL, name_normalized TEXT, provenance_type TEXT NOT NULL CHECK(provenance_type IN ( 'legacy_row','manual_documentation','reviewed_document','prescription_record' )), provenance_ref TEXT, created_at TEXT NOT NULL ); -- Do not add case-folded uniqueness: exact legacy spellings must not collapse. CREATE INDEX IF NOT EXISTS idx_medications_name_original ON medications(name_original); ``` ```sql CREATE TABLE IF NOT EXISTS medication_prescription_revisions ( id TEXT PRIMARY KEY, root_id TEXT NOT NULL, medication_id TEXT NOT NULL REFERENCES medications(id), business_revision INTEGER NOT NULL CHECK(business_revision >= 1), status TEXT NOT NULL CHECK(status IN ( 'unknown','active','paused','discontinued','expired' )), valid_from TEXT, valid_to TEXT, provenance_type TEXT NOT NULL CHECK(provenance_type IN ( 'legacy_unknown','manual_documentation','reviewed_document', 'prescription_record','clinician_documentation' )), provenance_ref TEXT, provenance_note TEXT, supersedes_revision_id TEXT REFERENCES medication_prescription_revisions(id), created_at TEXT NOT NULL, UNIQUE(root_id, business_revision), UNIQUE(id, medication_id) ); CREATE UNIQUE INDEX IF NOT EXISTS uq_medication_prescription_supersedes ON medication_prescription_revisions(supersedes_revision_id) WHERE supersedes_revision_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_medication_prescriptions_current ON medication_prescription_revisions(medication_id, root_id, business_revision DESC); ``` Do not model “current” as a mutable flag. Resolve the highest immutable revision per root. ```sql CREATE TABLE IF NOT EXISTS medication_schedules ( id TEXT PRIMARY KEY, root_id TEXT NOT NULL, medication_id TEXT NOT NULL, prescription_revision_id TEXT NOT NULL, business_revision INTEGER NOT NULL CHECK(business_revision >= 1), schedule_type TEXT NOT NULL CHECK(schedule_type IN ( 'one_time','regular','interval','unknown' )), start_date TEXT, end_date TEXT, weekdays TEXT, interval_days INTEGER CHECK(interval_days IS NULL OR interval_days BETWEEN 1 AND 365), planned_dose_original TEXT, planned_amount REAL CHECK(planned_amount IS NULL OR planned_amount >= 0), planned_unit TEXT, route_original TEXT, route_normalized TEXT CHECK( route_normalized IS NULL OR route_normalized IN ( 'oral','subcutaneous','intramuscular','intravenous', 'topical','inhaled','rectal','other','unknown' ) ), provenance_type TEXT NOT NULL, provenance_ref TEXT, supersedes_schedule_id TEXT REFERENCES medication_schedules(id), created_at TEXT NOT NULL, FOREIGN KEY(prescription_revision_id, medication_id) REFERENCES medication_prescription_revisions(id, medication_id), CHECK( (planned_amount IS NULL AND planned_unit IS NULL) OR (planned_amount IS NOT NULL AND planned_unit IS NOT NULL) ), UNIQUE(root_id, business_revision), UNIQUE(id, medication_id) ); CREATE UNIQUE INDEX IF NOT EXISTS uq_medication_schedule_supersedes ON medication_schedules(supersedes_schedule_id) WHERE supersedes_schedule_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_medication_schedule_medication ON medication_schedules(medication_id, start_date, end_date); ``` `medication_id` and `prescription_revision_id` are both enforced. An event may be unscheduled, but a non-null schedule must belong to the same medication. ```sql CREATE TABLE IF NOT EXISTS medication_dose_events ( id TEXT PRIMARY KEY, root_event_id TEXT NOT NULL, business_revision INTEGER NOT NULL CHECK(business_revision >= 1), medication_id TEXT NOT NULL REFERENCES medications(id), prescription_revision_id TEXT, schedule_id TEXT, event_status TEXT NOT NULL CHECK(event_status IN ( 'unknown','planned','administered','missed','cancelled' )), planned_at TEXT, occurred_at TEXT, planned_dose_original TEXT, planned_amount REAL CHECK(planned_amount IS NULL OR planned_amount >= 0), planned_unit TEXT, actual_dose_original TEXT, actual_amount REAL CHECK(actual_amount IS NULL OR actual_amount >= 0), actual_unit TEXT, route_original TEXT, route_normalized TEXT CHECK( route_normalized IS NULL OR route_normalized IN ( 'oral','subcutaneous','intramuscular','intravenous', 'topical','inhaled','rectal','other','unknown' ) ), injection_site_original TEXT, injection_site_normalized TEXT CHECK( injection_site_normalized IS NULL OR injection_site_normalized IN ( 'abdomen','thigh','upper_arm','buttock','other','unknown' ) ), injection_side TEXT CHECK( injection_side IS NULL OR injection_side IN ( 'left','right','midline','other','unknown' ) ), injection_device TEXT, injection_needle TEXT, lot_number TEXT, corrects_event_id TEXT REFERENCES medication_dose_events(id), correction_reason TEXT, provenance_type TEXT NOT NULL CHECK(provenance_type IN ( 'legacy_row','dashboard_manual','mobile_capture', 'reviewed_document','schedule_materialization' )), provenance_ref TEXT, legacy_administration_id INTEGER UNIQUE REFERENCES medication_administrations(id), created_at TEXT NOT NULL, FOREIGN KEY(prescription_revision_id, medication_id) REFERENCES medication_prescription_revisions(id, medication_id), FOREIGN KEY(schedule_id, medication_id) REFERENCES medication_schedules(id, medication_id), CHECK( (planned_amount IS NULL AND planned_unit IS NULL) OR (planned_amount IS NOT NULL AND planned_unit IS NOT NULL) ), CHECK( (actual_amount IS NULL AND actual_unit IS NULL) OR (actual_amount IS NOT NULL AND actual_unit IS NOT NULL) ), CHECK(event_status != 'planned' OR actual_amount IS NULL), CHECK(event_status NOT IN ('missed','cancelled') OR actual_amount IS NULL), CHECK( (corrects_event_id IS NULL AND correction_reason IS NULL) OR (corrects_event_id IS NOT NULL AND trim(COALESCE(correction_reason,'')) <> '') ), UNIQUE(root_event_id, business_revision) ); CREATE UNIQUE INDEX IF NOT EXISTS uq_medication_event_correction_target ON medication_dose_events(corrects_event_id) WHERE corrects_event_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_medication_events_time ON medication_dose_events(occurred_at, planned_at, medication_id); CREATE INDEX IF NOT EXISTS idx_medication_events_schedule ON medication_dose_events(schedule_id, planned_at); CREATE INDEX IF NOT EXISTS idx_medication_events_root_revision ON medication_dose_events(root_event_id, business_revision DESC); ``` Do **not** add a uniqueness constraint equivalent to legacy: ```sql UNIQUE(datum, medication_name, event_type) ``` A medication can legitimately have multiple doses on one day. ## Idempotency/action log ```sql CREATE TABLE IF NOT EXISTS medication_action_log ( idempotency_key TEXT PRIMARY KEY CHECK(length(idempotency_key) = 32), request_version INTEGER NOT NULL CHECK(request_version >= 1), action_hash TEXT NOT NULL CHECK(length(action_hash) = 64), preview_hash TEXT NOT NULL CHECK(length(preview_hash) = 64), event_id TEXT NOT NULL REFERENCES medication_dose_events(id), processed_at TEXT NOT NULL ); ``` Exact replay returns `event_id` with no write. Same key with another `action_hash`, `preview_hash`, or request version raises `conflicting idempotency key`. ## Corrected target status Do not update the corrected target. Derive effective status: ```sql CREATE VIEW IF NOT EXISTS medication_dose_events_effective AS SELECT e.*, CASE WHEN EXISTS ( SELECT 1 FROM medication_dose_events correction WHERE correction.corrects_event_id = e.id ) THEN 'corrected' ELSE e.event_status END AS effective_status FROM medication_dose_events e; ``` This preserves append-only history while displaying the target as corrected. ## Immutability triggers ```sql CREATE TRIGGER IF NOT EXISTS medication_events_no_update BEFORE UPDATE ON medication_dose_events BEGIN SELECT RAISE(ABORT, 'medication events are immutable'); END; CREATE TRIGGER IF NOT EXISTS medication_events_no_delete BEFORE DELETE ON medication_dose_events BEGIN SELECT RAISE(ABORT, 'medication events are immutable'); END; ``` Apply equivalent UPDATE/DELETE triggers to: - `medication_prescription_revisions` - `medication_schedules` - `medication_action_log` ## Correction validation trigger ```sql CREATE TRIGGER IF NOT EXISTS medication_event_validate_correction BEFORE INSERT ON medication_dose_events WHEN NEW.corrects_event_id IS NOT NULL BEGIN SELECT CASE WHEN NEW.corrects_event_id = NEW.id THEN RAISE(ABORT, 'self correction') END; SELECT CASE WHEN NOT EXISTS ( SELECT 1 FROM medication_dose_events t WHERE t.id = NEW.corrects_event_id ) THEN RAISE(ABORT, 'orphan correction') END; SELECT CASE WHEN EXISTS ( SELECT 1 FROM medication_dose_events t WHERE t.id = NEW.corrects_event_id AND ( t.medication_id <> NEW.medication_id OR t.root_event_id <> NEW.root_event_id OR NEW.business_revision <> t.business_revision + 1 ) ) THEN RAISE(ABORT, 'invalid correction lineage') END; SELECT CASE WHEN EXISTS ( SELECT 1 FROM medication_dose_events c WHERE c.corrects_event_id = NEW.corrects_event_id ) THEN RAISE(ABORT, 'event already corrected') END; SELECT CASE WHEN trim(COALESCE(NEW.correction_reason,'')) = '' THEN RAISE(ABORT, 'correction reason required') END; END; ``` Because a target must already exist and rows are immutable, correction edges always point backward. Combined with self/orphan rejection and one-child-per-target uniqueness, cycles cannot be introduced. Add a second trigger for ordinary revision 1: - `corrects_event_id IS NULL` - `business_revision = 1` - `root_event_id = id` The worker must revalidate all of this under `BEGIN IMMEDIATE`; triggers are the final backstop, not the sole validation layer. --- # 3. Legacy-preserving migration algorithm 1. **Preflight** - Verify exact Git SHA. - Run `PRAGMA integrity_check` and `foreign_key_check`. - Enumerate all old tables and columns. - Freeze a digest of every existing column in `medication_administrations`, ordered by `id`. - Record raw nulls, empty strings, free-dose strings, route strings, event types, and timestamps without normalization. 2. **Private backup** - Create target with `O_CREAT|O_EXCL|O_NOFOLLOW`, mode `0600`. - Use SQLite backup API. - Verify backup integrity and independent restore. 3. **Rehearse on an independent copy** - Apply DDL. - Apply it again and require no schema difference. - Never run migration through the network service or ordinary worker startup. 4. **Create medication identities mechanically** - One `medications` row per exact distinct `medication_name`, including whitespace/case differences. - `name_original = medication_name` exactly. - `name_normalized = NULL`. - `provenance_type = 'legacy_row'`. - Do not trim, casefold, merge, or infer products. 5. **Create unknown prescription revisions** - One root revision per migrated medication: - `status='unknown'` - `business_revision=1` - `provenance_type='legacy_unknown'` - Do not infer active/discontinued status from recent administrations. 6. **Backfill legacy event shadows** - One canonical event per legacy row, keyed deterministically from the legacy row ID and migration domain. - Copy: - `planned_dose_original = dose` or `actual_dose_original = dose` only as a raw field; safest default is a single `actual_dose_original`/`dose_original` compatibility field if semantics are not established. - `route_original = route` - `provenance_ref = "medication_administrations:"` - `legacy_administration_id = id` - Set structured amounts/units, normalized route, injection fields, lot, and schedule FK to `NULL`. - Do not split `"40 mg"`. - Do not reinterpret empty string as zero. - Do not infer scheduled dose from `scheduled_next_date`. - If legacy status is not an exact approved mapping, use `event_status='unknown'` and retain original `event_type` in a dedicated `legacy_event_type_original` column or private provenance payload. - No legacy row becomes a correction unless it already has authoritative target identity—which the current schema does not provide. 7. **No schedule inference** - Do not generate schedules from `scheduled_next_date`, repeated dates, name frequency, or dose text. - New schedule tables start empty unless an exact reviewed prescription/schedule source exists. 8. **Postconditions** - Old projection digest must equal pre-migration digest exactly. - Legacy table row count and every old value must match. - Number of `legacy_administration_id` links must equal legacy row count. - Structured numeric dose count for migrated rows must be zero. - Normalized route count for migrated rows must be zero. - Schedule count produced from legacy rows must be zero. - Run integrity/FK checks. 9. **Apply to productive DB only after rehearsal** - Stop the worker. - Apply once. - Apply again and prove no delta. - Deploy reader/writer code only after schema verification. - Restart worker and perform synthetic/copy-first preview, not a productive write. 10. **Reader transition** - Readers prefer canonical linked events. - Unlinked legacy rows continue through legacy adapter. - Never show both representations of the same `legacy_administration_id`. --- # 4. Preview/confirm binding Preview must include and hash: - contract version - action type - opaque medication ID - exact prescription revision ID and business revision - schedule ID and schedule business revision, or explicit null - target event ID/revision for correction - planned and actual dose fields - original and normalized route - injection fields and lot - provenance - correction reason - current dataset/schema version - idempotency key - policy/normalization version Confirm payload carries the exact `preview_hash`. Worker flow: 1. `BEGIN IMMEDIATE` 2. Check `medication_action_log` for replay. 3. Reload medication, prescription revision, schedule, and correction target. 4. Reconstruct the preview using the same function. 5. Require recomputed hash to equal submitted hash. 6. Revalidate prescription/schedule/medication consistency and target revision. 7. Insert immutable event. 8. Insert action log. 9. Commit. 10. Return stable event ID. Any form edit, changed schedule revision, changed prescription revision, corrected target, or stale target revision invalidates preview and requires a new preview. --- # 5. Focused test matrix ## Schema and migration 1. Empty legacy table migrates twice with no delta. 2. Legacy rows containing null, empty, whitespace, `"40 mg"`, `"1 Pen"`, and unknown routes remain byte-identical. 3. Free-dose strings produce no structured amount/unit. 4. Legacy route strings produce no normalized route. 5. Legacy `scheduled_next_date` creates no schedule. 6. Exact names differing only by case/space remain separate identities. 7. Old projection digest is identical before/after. 8. Backup restore has matching digest, integrity, and row counts. 9. FK check remains empty. 10. Migration marker exists exactly once. ## DDL invariants 11. Event with unknown medication FK fails. 12. Event with unknown schedule FK fails. 13. Schedule belonging to another medication fails. 14. Prescription revision belonging to another medication fails. 15. UPDATE/DELETE of event fails. 16. UPDATE/DELETE of prescription/schedule revision fails. 17. Duplicate `(root_event_id, business_revision)` fails. 18. Negative structured amount fails. 19. Amount without unit and unit without amount fail. 20. Planned/missed/cancelled event with actual amount fails. ## Corrections 21. Valid correction inserts a new revision and target view status becomes `corrected`. 22. Target raw event/status remains unchanged. 23. Blank correction reason fails. 24. Orphan target fails. 25. Self-correction fails. 26. Correction with another medication fails. 27. Revision jump or wrong root fails. 28. Second correction of the same target fails. 29. Attempted correction cycle fails/no rows added. 30. Correcting the current correction revision creates a valid linear chain only when explicitly supported. ## Idempotency and transactions 31. Exact replay returns same event ID and zero new rows. 32. Same idempotency key with changed dose fails with zero writes. 33. Same key with changed preview hash fails. 34. Crash simulation after event insert but before action-log insert rolls back both. 35. Crash after DB commit but before queue-file deletion replays with zero new rows. 36. Later-item failure in a batch rolls back all events/action logs. 37. Receipt generation failure does not cause a second business event. ## Preview binding 38. Unchanged preview confirms successfully. 39. Prescription revision added after preview causes stale conflict. 40. Schedule revised after preview causes stale conflict. 41. Target corrected after preview causes stale conflict. 42. Changing planned/actual amount, unit, route, injection site, lot, or correction reason invalidates preview. 43. Forged opaque ID plus valid-looking hash fails. 44. Confirm without preview fails. 45. UI edit after preview disables confirm. ## Read/API/UI 46. Planned and actual dose render separately. 47. Unknown remains unknown, never zero/empty-plan. 48. Corrected target is displayed as corrected; replacement displays its actual business status. 49. Legacy and canonical shadow do not double-render. 50. Multiple same-medication doses on one day render independently. 51. Injection fields and lot render only when documented. 52. Original route remains visible; normalized route is separately labeled. 53. Mobile correction requires target and non-empty reason. 54. Both the old medication dialog and capture hub use the same v2 endpoint. 55. No alternate v1 route can bypass preview, FKs, correction rules, or idempotency. --- # 6. P0 pitfalls - **P0 — current medication writes are not idempotent.** `apply_patient_action()` has no DB action log. A crash after commit but before queue-file deletion can replay. - **P0 — current “corrected” is a standalone status.** Neither `validate_patient_payload()` nor `apply_patient_action()` requires a target or correction reason. - **P0 — mobile capture correction does not correct the medication business row.** It versions `capture_entries`, then inserts another independent legacy `medication_administrations` row. - **P0 — legacy uniqueness is clinically unsafe for the new model.** `UNIQUE(datum, medication_name, event_type)` prevents multiple legitimate same-day administrations and is not an idempotency key. - **P0 — current known-medication identity is exact display text from event history.** It is not a medication/prescription identity and has no provenance/status contract. - **P0 — capture currently combines amount and unit into free text.** This loses planned-vs-actual semantics and makes later parsing tempting; future writes must use typed fields while preserving the original representation. - **P0 — dual write paths would bypass invariants.** The legacy dialog and capture hub must converge on one v2 worker function. - **P0 — do not “fix” targets by UPDATE.** Corrected status must be derived from immutable correction lineage. - **P0 — do not migrate by interpreting legacy event strings.** Unknown remains unknown; free dose, route, status, and schedule fields must not be inferred. - **P0 — migration must not run lazily inside the worker.** Worker code should assert the schema and fail closed. # 7. P1 pitfalls - **P1 — existing readers casefold and classify raw event types.** Keep that only inside a legacy compatibility adapter; canonical rows must use constrained status values directly. - **P1 — `scheduled_next_date` is overloaded.** It is not sufficient evidence of a recurring schedule or prescribed dose. - **P1 — prescription “current” flags can drift.** Use immutable revisions and deterministic latest-revision selection. - **P1 — normalization can erase evidence.** Preserve `route_original`; normalize only explicit new input with a versioned allowlist. - **P1 — nullable schedule is necessary.** Requiring a fabricated schedule for an unscheduled or legacy administration would invent provenance. - **P1 — action hash must exclude telemetry but include every business field.** - **P1 — queue receipt identity alone is insufficient.** DB action-log identity is the durable replay boundary. - **P1 — fixture builders currently seed only the legacy table.** Without mixed legacy/canonical fixtures, duplicate rendering and fallback bugs will remain invisible. - **P1 — canonical readers must be shared.** Day, calendar, record, summary, doctor report, and capture plans must not each invent different correction or planned-dose semantics.