Clinical portable transfer evidence¶
This is the bounded compatibility evidence for #2702. It covers the retained Rails serializer against fresh Rust, using synthetic records only. It is not a live-data migration procedure or proof of complete Rails database export.
Fixture provenance¶
The fixtures live in tests/fixtures/portable_rails/. They were produced by a
synthetic harness that loaded the retained Rails exporter, record/event/outcome
serializers, ETag and Encryptor without booting Rails, loading application models
or connecting to a database. Its exporter subclass supplied an in-memory record
graph and suppressed export audit writes. The production collection selection,
record serializers and ActiveSupport JSON/encryption ran unchanged.
The harness created an uninitialised configuration-only Rails application and
loaded the framework defaults selected in retained rails/config/application.rb.
It applied the same message serializer and metadata settings as the Rails
ActiveSupport initialisers. Standalone ActiveSupport defaults to Marshal;
using that default would have produced an incorrect fixture for this Rails
application. See the Rails configuration source
and ActiveSupport initialisers.
The fixtures are frozen: the retained Rails tree is being archived and the
generator is retired, so they are committed test data, not reproducible output. Their
recorded provenance is Ruby 4.0.7 with ActiveSupport 8.1.4 under Rails 8.0
framework defaults. The passphrase is the deliberately public synthetic test
value synthetic portable secret. Never use it for actual records.
rtk task test:integration TARGET=care_api FILTER=rails_serializer_
| Fixture | Actual retained Rails path | Contents / expected Rust handling |
|---|---|---|
migration-v1 |
Exporter#payload and Encryptor.encrypt |
Eight base collections; empty person-location links. |
migration-v2 |
Exporter#v2_payload and Encryptor.encrypt |
Base collections plus closed pause and saved not-taken outcome; no health events. |
household-v2 |
Exporter#household_payload and Encryptor.encrypt |
All eleven collections, including linked health history. The envelope wraps the household payload explicitly; this is not Exporter#call or direct ZIP restore. |
linked-migration-v2 |
Same v2 path with a synthetic person-location association | Rejected by Rust dry-run and apply before domain writes. |
historical-location-v2 |
Household v2 with stock moved since a historical take | Decrypts successfully; dry-run reports the location mismatch and apply rejects without domain writes. This is a retained v1/v2 compatibility gap, not discarded or rewritten history. |
Each row has a plaintext .json and Rails-produced .encrypted.json.
The additional prepared-history-v2 household fixture and HostedExport manifest
exercise the explicit preparation path, with
different current and historical stock labels and all eleven collections. Its
identities are separate from the rejection fixtures. Hosted manifests and archive
entry serialization use the retained pure builders without running database
selection or export authorisation.
The fixtures contain two storage labels, fractional medication/dosage stock,
one dependent adult, a schedule,
routine assignment, a historical take, a closed pause with unavailable original
actors, a not-taken outcome, notification preferences and a health event.
Accepted examples have no stock move. The separate historical-location fixture
keeps distinct current and taken-from labels and proves the current v2 rejection.
Collection and field matrix¶
All rows include portable_id, updated_at and etag. Portable identity is
preserved. Rust creates destination update/version metadata and ETags: those
are not cross-installation clinical provenance and must not be copied as local
concurrency tokens. Decimal strings are compared numerically, without losing
fractional stock; clinical timestamps are compared at microsecond precision.
The fields below are emitted by the retained serializers and accepted by the current import contract. Acceptance of a field name alone does not establish all values or export-selection completeness.
| Collection | Fields beyond common identity | Mapping / boundary |
|---|---|---|
people |
name, email, date_of_birth, person_type, has_capacity, location_portable_ids, notification_preference_portable_id |
New identity is linked to a fresh household participant. Empty obsolete links are accepted in v1/v2; non-empty links rejected. New dependents need an explicit current guardian declaration. No account or credential import. |
locations |
name, description |
Storage labels; no person-location membership is created. |
medications |
location_portable_id, name, friendly_name, category, description, dose_amount, dose_unit, default_schedule_type, current_supply, reorder_threshold, barcode, dmd_code, dmd_system, dmd_concept_class |
Location portable reference resolves locally; recorded stock is restored without administering a dose. |
dosage_options |
medication_portable_id, amount, unit, frequency, description, default_for_adults, default_for_children, default_max_daily_doses, default_min_hours_between_doses, default_dose_cycle, current_supply, reorder_threshold |
Parent portable reference resolves locally; stock and defaults retained. |
schedules |
source_dosage_option_portable_id, retired_at, person_portable_id, medication_portable_id, dose_amount, dose_unit, frequency, dose_cycle, max_daily_doses, min_hours_between_doses, schedule_type, schedule_config, start_date, end_date, active, notes |
References resolve within imported graph/destination; pause history determines final active state. This fixture exercises one daily configuration, not every scheduling variant. |
person_medications |
source_dosage_option_portable_id, retired_at, person_portable_id, medication_portable_id, dose_amount, dose_unit, dose_cycle, max_daily_doses, min_hours_between_doses, administration_kind, active, notes, position |
Routine assignment and source dosage reference retained; as-needed/retired variants need separate evidence. |
medication_takes |
client_uuid, source_type, source_portable_id, taken_at, dose_amount, dose_unit, taken_from_medication_portable_id, taken_from_location_portable_id |
Original source/time and same-location provenance restore without stock deduction. V1/v2 reject a taken-from label different from current medicine storage; Rust v3 allows that distinction. No automatic conversion is provided. |
notification_preferences |
person_portable_id, enabled, dose_due_enabled, missed_dose_enabled, low_stock_enabled, private_text_enabled, morning_time, afternoon_time, evening_time, night_time |
Preference flags/times and person link restored; restore creates no reminders or mail jobs. |
medication_pause_periods (v2+) |
source_type, source_portable_id, reason, note, started_at, ended_at, created_at, legacy_context, imported_context, recorded_by_person_portable_id, resumed_by_person_portable_id |
Original timing/context and unresolved actor portable references retained; imported_context becomes true. The importer has separate local audit attribution. |
dose_occurrences (v2+) |
source_type, source_portable_id, window_starts_on, window_ends_on, position, scheduled_at, outcome, reason, note, resolved_at, medication_take_portable_id |
Saved outcome identity, window and resolution retained. Newly imported outcomes attribute the local restore to the importer; no administration is replayed. |
health_events (v2+) |
person_portable_id, event_kind, severity, title, notes, started_on, ended_on, medication_portable_ids |
Household payload includes these; encrypted migration payload omits them. Do not claim a migration bundle is complete health-history backup. |
Acceptance and remaining work¶
The acceptance module is tests/care_api/portable_rails.rs, registered beneath
portable_imports. It uses the full Loco test application and restricted runtime
credentials with disposable PostgreSQL. It decrypts Rails envelopes with Rust,
checks dry-run counts and no domain effects, rejects missing guardian declarations,
applies with an explicit declaration, replays without one, and reads the saved
graph back through the normal sync route. It compares emitted clinical fields,
stock balances and relationships, checks no person-location memberships, and
checks no reminder, delivery or queue-job side effects.
Storage labels are checked through typed persisted rows, because clinical sync
does not include an unused storage label merely because it was restored.
The moved-stock fixture is separate negative evidence. In
src/models/care/portable_imports/preflight.rs, the non-v3 branch compares each
take's historical label with the medicine's current label. Both labels are
present and valid in this synthetic Rails graph, but v2 is refused. Keeping the
failure visible is not proof of supported transfer for that history.
Supported format markers are v1/v2/v3. The retained Rails exporter supports only v1/v2. V3 forbids the obsolete person-location field entirely. Do not relabel old payloads or strip relationships automatically. Existing focused Rust tests cover v3 export/import and obsolete-link rejection; they do not turn Rails v2 into v3.
#2702 remains open for the following evidence/decisions:
- A reviewed operator path for complete clinical transfer when Rails person-location links are non-empty, historical taken-from labels differ from current stock, or health events are absent from the chosen export. No automatic data removal or ZIP restoration is delivered here. The explicit household preparation path now addresses moved-stock history and included health events when obsolete links are empty; it deliberately rejects nonempty obsolete links and attachments.
- Database-backed Rails export selection, model validations, actual deployed Ruby/ Rails dependency versions and every supported schedule/history variant. The in-memory fixture establishes serializer/envelope interoperability only.
- Integration with the ongoing person-owned care-sharing and protected restore provenance work. This slice changes no restore, sharing or schema implementation.
- Full access-denial/conflict coverage across existing/self/guardian subjects, open/legacy/retired history and taken outcomes with linked takes. Existing Rust tests are useful evidence but are not the same as Rails-produced fixtures.
Fresh accounts and authentication are configured separately. No Rails database adoption, credential migration, reverse migration, live export or deployment is included. Profile JSON/ZIP downloads remain a separate contract under #2513.