File size: 11,339 Bytes
612e777 5a79992 612e777 5a79992 612e777 5a79992 612e777 5a79992 612e777 5a79992 612e777 5a79992 612e777 5a79992 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | <article class="doc" id="doc-reference-data-model" aria-hidden="true"><div class="strip"><span class="path">reference/data-model.md</span><span class="tag">Engineering, reference</span><span class="meta">~3 min read</span></div>
<h1>6. Canonical Data Model</h1>
<h2 id="doc-reference-data-model--h1">6.1 Conventions</h2>
<ul>
<li>Primary keys: UUIDv7 (<code>id</code>). Every PHI-bearing table carries <code>tenant_id</code> (FK, indexed first in every composite index), cross-tenant queries are impossible by construction; a SQLAlchemy session-level tenant filter is applied on every request.</li>
<li>Timestamps: <code>created_at</code>/<code>updated_at</code> UTC (timestamptz); clinical times keep source timezone in a companion <code>*_tz</code> column when supplied.</li>
<li>Nothing is hard-deleted. Lifecycle is expressed in <code>status</code> columns; caregiver revocation, consent withdrawal, and supersession are state changes with audit events.</li>
<li>Optimistic concurrency: <code>version</code> (int) on <code>episode</code>, <code>task_instance</code>, <code>medication_plan_item</code>, <code>brief_snapshot</code>; writers must supply expected version.</li>
<li>Money/none in MVP; quantities as <code>numeric(12,4)</code>; enums are Postgres enums migrated via Alembic.</li>
</ul>
<h2 id="doc-reference-data-model--h2">6.2 Entity Catalog</h2>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-md"/><col class="col-md"/><col class="col-lg"/></colgroup>
<thead>
<tr>
<th>Entity</th>
<th>Purpose</th>
<th>Key Fields (beyond id/tenant/timestamps)</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Entity">tenant / cohort</td>
<td data-label="Purpose">Customer + clinical population config container.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">name, <code>config_version</code>, cohort criteria JSON, episode_template_id</td>
</tr>
<tr>
<td data-label="Entity">patient</td>
<td data-label="Purpose">Demographics + identity linkage.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">mrn (per tenant), fhir_patient_id, name, dob, phones[], language, tz, accessibility_prefs</td>
</tr>
<tr>
<td data-label="Entity">episode</td>
<td data-label="Purpose">One 30-day transition instance.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">patient_id, cohort_id, state, <code>version</code>, index_encounter_id, discharge_at, day30_at, activation_at, closed_at, close_reason</td>
</tr>
<tr>
<td data-label="Entity">consent_record</td>
<td data-label="Purpose">Versioned consent artifacts.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, type(program|caregiver|comm_channel), scope JSON, granted_by, granted_at, withdrawn_at, doc_version</td>
</tr>
<tr>
<td data-label="Entity">caregiver + caregiver_grant</td>
<td data-label="Purpose">Separate identity + scoped access.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">caregiver: name, phone, relationship; grant: episode_id, scope JSON, status(active|revoked), revoked_at</td>
</tr>
<tr>
<td data-label="Entity">clinician + access_grant</td>
<td data-label="Purpose">External clinician + purpose-limited grant.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">clinician: npi, name, email, org; grant: episode_id, purpose, expires_at, device_fingerprints[], status, step_up_at</td>
</tr>
<tr>
<td data-label="Entity">source_document</td>
<td data-label="Purpose">Ingested artifact + fingerprint.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, type(avs|dc_summary|post_visit_note|upload), fhir_ref, s3_key, sha256, source_level(1-9), finality(final|amended|prelim), supersedes_id, received_at</td>
</tr>
<tr>
<td data-label="Entity">extracted_fact</td>
<td data-label="Purpose">LLM/parsed proposition pre/post gates.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">source_document_id, kind, payload JSON (schema_version), span_start/end, span_text, gate_status(g1..g7|published|rejected|pending_review), confidence, model_run_id</td>
</tr>
<tr>
<td data-label="Entity">task_instance</td>
<td data-label="Purpose">Universal task (all patient/clinician actions).</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, category(medication|appointment|lab|education|safety|admin), priority(critical|important|routine), state (11), due_at, source_fact_id, supersedes_id, <code>version</code></td>
</tr>
<tr>
<td data-label="Entity">medication_plan_item</td>
<td data-label="Purpose">Instruction authority dimension.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, rxnorm, name, dose, route, freq, change_type(new|changed|continued|stopped|uncertain), status(proposed|active|superseded|stopped), source_fact_id, confirmed_by/at</td>
</tr>
<tr>
<td data-label="Entity">medication_status_event</td>
<td data-label="Purpose">Evidence dimensions (transmission/access/use).</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">plan_item_id, state(ordered|sent|available|obtained|started|taking|stopped|barrier), evidence_class(patient_report|pharmacy_confirmed|clinician_confirmed|ehr), reported_by, occurred_at, barrier_code(cost|stock|transport|confusion|side_effect|other)</td>
</tr>
<tr>
<td data-label="Entity">appointment_requirement / appointment</td>
<td data-label="Purpose">Need vs. booked visit.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">req: specialty, window_start/end, status(open|satisfied|waived); appt: req_id nullable, source(ehr|patient_report|assisted), starts_at, provider, status(booked|completed|no_show|cancelled), verified bool</td>
</tr>
<tr>
<td data-label="Entity">notification_message</td>
<td data-label="Purpose">Every outbound touch.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, task_id, channel(push|sms|voice|email), template_id+version, lang, scheduled_at, sent_at, delivery_status, suppress_reason</td>
</tr>
<tr>
<td data-label="Entity">agent_action</td>
<td data-label="Purpose">Evidence record of every agent step.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">agent(engagement|appointment|med_access|records|brief|outcome), tool, params_hash, params JSON (redacted), result(success|fail|blocked), evidence JSON, tokens_cost, envelope_version, killed_by</td>
</tr>
<tr>
<td data-label="Entity">brief_snapshot</td>
<td data-label="Purpose">Immutable clinician brief.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, appointment_id, content JSON, episode_version_at_build, s3_pdf_key, generated_at, opened_at[], stale_superseded_by</td>
</tr>
<tr>
<td data-label="Entity">outcome_event</td>
<td data-label="Purpose">ED/readmission signals.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">episode_id, type(ed|readmit), source(adt|claims|hie|patient_report), facility, occurred_at, within_30d bool, completeness_note</td>
</tr>
<tr>
<td data-label="Entity">audit_event</td>
<td data-label="Purpose">Append-only ledger (own schema).</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">actor_type/id, action, object_type/id, purpose, context JSON, ip, trace_id, occurred_at, monthly partitions</td>
</tr>
<tr>
<td data-label="Entity">config_version / template / model_registry</td>
<td data-label="Purpose">Versioned governance artifacts.</td>
<td data-label="Key Fields (beyond id/tenant/timestamps)">published_by/at, diff JSON, approval_ref; model_registry: model_id, prompt_version, schema_version, safety_policy_version, status(active|rollback)</td>
</tr>
</tbody>
</table></div>
<h2 id="doc-reference-data-model--h3">6.3 Core Table Details</h2>
<h3 id="doc-reference-data-model--h4">task_instance (authoritative for everything the patient sees)</h3>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-xs"/><col class="col-lg"/></colgroup>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Rule</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Field">state</td>
<td data-label="Type">enum(11)</td>
<td data-label="Rule">proposed, required, scheduled, in_progress, completed, blocked, pending_clinical_decision, declined, superseded, cancelled, unresolved_at_close. Transitions only via §7.1.</td>
</tr>
<tr>
<td data-label="Field">priority</td>
<td data-label="Type">enum</td>
<td data-label="Rule">critical | important | routine, set by deterministic rules on <code>category</code>+<code>source_fact</code> (e.g., new/changed cardiac med → critical); drives notification tier and Today ordering.</td>
</tr>
<tr>
<td data-label="Field">due_at / window</td>
<td data-label="Type">timestamptz / daterange</td>
<td data-label="Rule">From extracted timeframe; <code>null</code> means episode-long.</td>
</tr>
<tr>
<td data-label="Field">patient_response</td>
<td data-label="Type">enum</td>
<td data-label="Rule">done | not_yet | cannot_do | not_sure | none, PAT-005; <code>cannot_do</code> requires <code>barrier_code</code>, opens the reason-specific automation path.</td>
</tr>
<tr>
<td data-label="Field">supersedes_id</td>
<td data-label="Type">uuid</td>
<td data-label="Rule">Set when a post-visit update replaces a task; original keeps state <code>superseded</code> and stays queryable (audit invariant).</td>
</tr>
<tr>
<td data-label="Field">idempotency guard</td>
<td data-label="Type">unique(episode_id, natural_key)</td>
<td data-label="Rule">natural_key = category+source span hash, prevents duplicate tasks on reprocessing.</td>
</tr>
</tbody>
</table></div>
<h3 id="doc-reference-data-model--h5">medication dimensions (MED-003/006 invariants)</h3>
<p>The four dimensions are physically separated: <strong>Instruction Authority</strong> lives on <code>medication_plan_item</code>; <strong>Transmission</strong>, <strong>Fulfillment/Access</strong>, and <strong>Use</strong> are <code>medication_status_event</code> rows. Enforced invariants: (1) a status event can never mutate a plan item; (2) <code>evidence_class=patient_report</code> can never be rendered with verified styling, the API serializes <code>verified:false</code> and the design system binds badge color to that flag; (3) plan-item changes require a <code>source_fact_id</code> whose gate_status is <code>published</code> <strong>and</strong> source_level ≤7, or an explicit clinician confirmation (MED-010, VIS-007).</p>
<h2 id="doc-reference-data-model--h6">6.4 Indexing & Partitioning (day-one)</h2>
<ul>
<li>Hot paths: <code>task_instance (tenant_id, episode_id, state, due_at)</code>; <code>notification_message (tenant_id, scheduled_at) WHERE sent_at IS NULL</code>; <code>extracted_fact (source_document_id, gate_status)</code>; <code>episode (tenant_id, state, day30_at)</code>.</li>
<li><code>audit_event</code> monthly range partitions + BRIN on occurred_at; archived partitions detached after S3 WORM export (never dropped inside 6 y).</li>
<li>Transactional <strong>outbox</strong> table <code>event_outbox (id, aggregate, event_type, payload, created_at, processed_at)</code> written in the same transaction as any state change; notify-worker polls every 5 s, this is the mechanism behind every “within 60 seconds” requirement (PAT-003, ENG-004).</li>
</ul>
<div class="pn"><a class="pn-prev" href="#"></a><a class="pn-next" href="#"></a></div></article>
|