File size: 18,784 Bytes
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<article class="doc" id="doc-reference-platform" aria-hidden="true"><div class="strip"><span class="path">reference/platform.md</span><span class="tag">Engineering, reference</span><span class="meta">~7 min read</span></div>
<h1>Platform: Architecture, Stack, Cloud &amp; Actors</h1>
<h1>4. System Architecture</h1>
<p><img alt="Figure 1, MVP system architecture. One deployable API, one async plane, one canonical store." loading="lazy" src="assets/img/platform-architecture.png"/>
<em>Figure 1, MVP system architecture. One deployable API, one async plane, one canonical store.</em></p>
<h2 id="doc-reference-platform--h1">4.1 Module Inventory (Monolith Internal Boundaries)</h2>
<p>Each module is a Python package under <code>app/modules/</code> with a public interface (<code>api.py</code> router + <code>service.py</code> facade) and private internals. Cross-module calls go through service facades only; <code>import-linter</code> contracts in CI fail the build on any deeper import. Modules share the database but own their tables (write access enforced by convention + code review; the <code>audit</code> schema is enforced by a separate INSERT-only DB role).</p>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-lg"/><col class="col-auto"/></colgroup>
<thead>
<tr>
<th>Module</th>
<th>Responsibility</th>
<th>Key Requirements Owned</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Module"><code>identity</code></td>
<td data-label="Responsibility">Activation tokens, patient/caregiver/clinician sessions, grants, step-up, recovery, revocation lists.</td>
<td data-label="Key Requirements Owned">ENR-003/004/005, CLN-001, CGV-001/004</td>
</tr>
<tr>
<td data-label="Module"><code>consent</code></td>
<td data-label="Responsibility">Consent records, versions, scopes (program, caregiver, recording-optional), withdrawal cascade.</td>
<td data-label="Key Requirements Owned">SEC-010, CGV-007</td>
</tr>
<tr>
<td data-label="Module"><code>ingestion</code></td>
<td data-label="Responsibility">EHR adapter, ADT listener, FHIR client, document fingerprinting, source versioning.</td>
<td data-label="Key Requirements Owned">INT-001, DPC-001</td>
</tr>
<tr>
<td data-label="Module"><code>compiler</code></td>
<td data-label="Responsibility">Discharge plan compiler + seven-gate LLM pipeline; extracted-fact publication.</td>
<td data-label="Key Requirements Owned">DPC-003/004/007/008, DQS-004/011</td>
</tr>
<tr>
<td data-label="Module"><code>episode</code></td>
<td data-label="Responsibility">Episode &amp; Universal Task state machines, transitions, idempotency, prioritization engine.</td>
<td data-label="Key Requirements Owned">PAT-001/003/005, ENR-001/002</td>
</tr>
<tr>
<td data-label="Module"><code>medication</code></td>
<td data-label="Responsibility">Medication plan items, status events (8 states), barriers, verification labeling.</td>
<td data-label="Key Requirements Owned">MED-003/005/006/010</td>
</tr>
<tr>
<td data-label="Module"><code>scheduling</code></td>
<td data-label="Responsibility">Appointment requirements, appointments, matching, assisted-scheduling flows, result completion.</td>
<td data-label="Key Requirements Owned">APT-001/004/010</td>
</tr>
<tr>
<td data-label="Module"><code>notification</code></td>
<td data-label="Responsibility">Channel selection, escalation ladders, quiet hours, suppression outbox, templates, silent-deterioration detector.</td>
<td data-label="Key Requirements Owned">ENG-002/004/012, EDU-002/007</td>
</tr>
<tr>
<td data-label="Module"><code>brief</code></td>
<td data-label="Responsibility">Brief snapshots, precompute triggers, staleness checks, post-visit update loop.</td>
<td data-label="Key Requirements Owned">VIS-002/005/007, CLN-004/008</td>
</tr>
<tr>
<td data-label="Module"><code>agents</code></td>
<td data-label="Responsibility">Agent orchestrator, policy envelopes, tool registry, evidence records, kill switches.</td>
<td data-label="Key Requirements Owned">AUT-001/006/009</td>
</tr>
<tr>
<td data-label="Module"><code>admin</code></td>
<td data-label="Responsibility">Tenant/cohort/config management, versioned publication, RBAC administration.</td>
<td data-label="Key Requirements Owned">ADM-004 config side</td>
</tr>
<tr>
<td data-label="Module"><code>analytics</code></td>
<td data-label="Responsibility">Event projections, denominators, execution &amp; outcome reports, coverage flags.</td>
<td data-label="Key Requirements Owned">ADM-004/007</td>
</tr>
<tr>
<td data-label="Module"><code>audit</code></td>
<td data-label="Responsibility">Append-only audit writer + query API; S3 WORM archiver.</td>
<td data-label="Key Requirements Owned">SEC-003, DQS-001</td>
</tr>
</tbody>
</table></div>
<h2 id="doc-reference-platform--h2">4.2 Runtime Topology</h2>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-auto"/><col class="col-md"/></colgroup>
<thead>
<tr>
<th>Process</th>
<th>Scale (pilot)</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Process"><code>api</code></td>
<td data-label="Scale (pilot)">2–4 tasks, 1 vCPU/2 GB</td>
<td data-label="Notes">FastAPI + Uvicorn behind ALB; stateless; readiness = DB+Redis ping.</td>
</tr>
<tr>
<td data-label="Process"><code>worker-default</code></td>
<td data-label="Scale (pilot)">2 tasks</td>
<td data-label="Notes">Celery queues: <code>episode</code>, <code>ingest</code>, <code>analytics</code>. Prefetch 1, acks-late, visibility 300 s.</td>
</tr>
<tr>
<td data-label="Process"><code>worker-llm</code></td>
<td data-label="Scale (pilot)">1–2 tasks</td>
<td data-label="Notes">Queue <code>llm</code>; concurrency 4; per-task Bedrock budget caps (§11.7); isolates latency.</td>
</tr>
<tr>
<td data-label="Process"><code>worker-notify</code></td>
<td data-label="Scale (pilot)">2 tasks</td>
<td data-label="Notes">Queues <code>notify</code>, <code>outbox</code>; drains suppression outbox every 5 s to meet the 60 s SLA.</td>
</tr>
<tr>
<td data-label="Process"><code>beat</code></td>
<td data-label="Scale (pilot)">1 task (singleton)</td>
<td data-label="Notes">Celery beat: schedules in §16.5. Leader lock in Redis.</td>
</tr>
<tr>
<td data-label="Process"><code>migrator</code></td>
<td data-label="Scale (pilot)">on deploy</td>
<td data-label="Notes">Alembic <code>upgrade head</code> as ECS one-off task; deploy gate.</td>
</tr>
</tbody>
</table></div>
<h2 id="doc-reference-platform--h3">4.3 Technology Stack (pinned)</h2>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-md"/><col class="col-md"/></colgroup>
<thead>
<tr>
<th>Layer</th>
<th>Choice (version)</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Layer">Language / API</td>
<td data-label="Choice (version)">Python 3.12 · FastAPI 0.115 · Uvicorn 0.30</td>
<td data-label="Notes">OpenAPI generated; <code>/v1</code> prefix; Pydantic v2 models shared with workers.</td>
</tr>
<tr>
<td data-label="Layer">ORM / migrations</td>
<td data-label="Choice (version)">SQLAlchemy 2.0.32 · Alembic 1.13</td>
<td data-label="Notes">One migration lineage; no auto-generated DDL in prod without review.</td>
</tr>
<tr>
<td data-label="Layer">Async jobs</td>
<td data-label="Choice (version)">Celery 5.4 · Redis 7.2</td>
<td data-label="Notes">Priorities via dedicated queues, not priority flag.</td>
</tr>
<tr>
<td data-label="Layer">Frontend</td>
<td data-label="Choice (version)">React 18.3 · TypeScript 5.5 · Vite 5 · Workbox 7</td>
<td data-label="Notes">PWA manifest; bundle budget 250 KB gz initial for patient routes.</td>
</tr>
<tr>
<td data-label="Layer">UI kit</td>
<td data-label="Choice (version)">Radix UI + Tailwind 3.4</td>
<td data-label="Notes">44 px min touch targets; WCAG 2.2 AA tokens; EN/ES i18n via i18next.</td>
</tr>
<tr>
<td data-label="Layer">LLM</td>
<td data-label="Choice (version)">Bedrock: <code>anthropic.claude-sonnet</code> pinned model ID; Titan embed v2</td>
<td data-label="Notes">IDs live in model registry table (§11.6), never in code.</td>
</tr>
<tr>
<td data-label="Layer">Telephony</td>
<td data-label="Choice (version)">Twilio SMS + Voice (Studio flows)</td>
<td data-label="Notes">BAA signed; status callbacks → <code>/webhooks/twilio</code>.</td>
</tr>
<tr>
<td data-label="Layer">Email</td>
<td data-label="Choice (version)">AWS SES</td>
<td data-label="Notes">Transactional only; DKIM/DMARC enforced.</td>
</tr>
<tr>
<td data-label="Layer">Observability</td>
<td data-label="Choice (version)">OpenTelemetry SDK → ADOT collector → CloudWatch; Sentry (PHI-scrubbed)</td>
<td data-label="Notes">Trace ID propagated into Celery headers and audit rows.</td>
</tr>
<tr>
<td data-label="Layer">Testing</td>
<td data-label="Choice (version)">pytest 8 · Playwright 1.45 · schemathesis · Locust</td>
<td data-label="Notes">Layer mapping in §17.1.</td>
</tr>
</tbody>
</table></div>
<h2 id="doc-reference-platform--h4">4.4 Repository &amp; Conventions</h2>
<pre><code>travi ai agent/ # single monorepo
 app/ # FastAPI monolith
 modules/{identity,consent,ingestion,compiler,episode,medication,
 scheduling,notification,brief,agents,admin,analytics,audit}/
 api.py service.py models.py schemas.py tasks.py tests/
 core/ # settings, db, security, llm_gateway, ehr_adapter, outbox
 main.py
 web/ # React PWA (patient/caregiver/clinician routes)
 infra/ # Terraform: modules/ + envs/{dev,stage,prod}
 ops/ # runbooks, dashboards-as-code, oncall
 docs/adr/ # ADR-001..018 as markdown, this doc's source
 .github/workflows/ # ci.yml, deploy.yml, nightly-golden.yml
</code></pre>
<ul>
<li><strong>Branching:</strong> trunk-based; short-lived feature branches; squash merge; Conventional Commits; release tags <code>v0.x.y</code>.</li>
<li><strong>Quality gates (CI-blocking):</strong> ruff + mypy --strict, eslint + tsc, import-linter contracts, pytest ≥85% line coverage on <code>modules/</code>, schemathesis against OpenAPI, Playwright smoke, Trivy image scan (no HIGH/CRITICAL), tfsec.</li>
<li><strong>Code review:</strong> 1 approval; 2 for <code>identity</code>, <code>compiler</code> gates, <code>audit</code>, Terraform, or any migration touching PHI tables.</li>
<li><strong>PHI hygiene:</strong> no PHI in logs, traces, Sentry, commit messages, or fixtures, synthetic data only outside prod (§15.6).</li>
</ul>
<h1>5. Environments, Cloud &amp; Delivery Infrastructure</h1>
<h2 id="doc-reference-platform--h5">5.1 Accounts &amp; Environments</h2>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-auto"/><col class="col-lg"/></colgroup>
<thead>
<tr>
<th>Env</th>
<th>AWS Account</th>
<th>Purpose / Data Policy</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Env">dev</td>
<td data-label="AWS Account"><code>tai-dev</code></td>
<td data-label="Purpose / Data Policy">Ephemeral stacks per PR optional; synthetic data only; relaxed sizes; nightly teardown of previews.</td>
</tr>
<tr>
<td data-label="Env">stage</td>
<td data-label="AWS Account"><code>tai-stage</code></td>
<td data-label="Purpose / Data Policy">Production-shaped; Epic <strong>sandbox</strong> connectivity; synthetic + partner test patients only; load &amp; DR drills here.</td>
</tr>
<tr>
<td data-label="Env">prod</td>
<td data-label="AWS Account"><code>tai-prod</code></td>
<td data-label="Purpose / Data Policy">Live PHI. Change only via pipeline; console read-only except break-glass role (audited).</td>
</tr>
</tbody>
</table></div>
<h2 id="doc-reference-platform--h6">5.2 Network &amp; Core Services</h2>
<ul>
<li><strong>VPC</strong> per env: 3 AZs; public subnets = ALB + NAT only; private subnets = ECS, RDS, ElastiCache; no public IPs on tasks; VPC endpoints for S3, ECR, Secrets Manager, Bedrock, CloudWatch.</li>
<li><strong>Hospital connectivity:</strong> site-to-site IPsec VPN (or partner-preferred private link) terminating in a dedicated <code>integration</code> subnet; ADT listener reachable only from partner interface-engine CIDR; all inbound mTLS where the engine supports it.</li>
<li><strong>Edge:</strong> CloudFront serves the static PWA bundle (no PHI cached; <code>Cache-Control: no-store</code> on all API responses); AWS WAF managed rules + rate rules (<code>/v1/patient/activate</code>: 10/min/IP) in front of ALB.</li>
<li><strong>Data:</strong> RDS PostgreSQL 16 Multi-AZ (db.m7g.large pilot), PITR 15-min granularity, 35-day backups; ElastiCache Redis 7 (cluster mode off, multi-AZ); S3 buckets <code>tai-{env}-sources</code>, <code>-briefs</code>, <code>-exports</code>, <code>-audit-archive</code> (Object Lock compliance 6 y on audit), all SSE-KMS with per-purpose CMKs, versioning + access logs on.</li>
<li><strong>Secrets:</strong> AWS Secrets Manager; 90-day rotation for DB/Twilio/Epic client credentials; no secrets in env files or Terraform state (state in S3 + DynamoDB lock, KMS-encrypted).</li>
<li><strong>IAM:</strong> task roles per process (api / workers / migrator); Bedrock invoke restricted to registry model IDs; deny <code>s3:DeleteObject</code> on audit archive to all runtime roles.</li>
</ul>
<h2 id="doc-reference-platform--h7">5.3 CI/CD</h2>
<ol>
<li>PR → CI gates (§4.4) on GitHub Actions; preview deploy to dev on label.</li>
<li>Merge to <code>main</code> → build multi-arch image → push ECR → auto-deploy <strong>stage</strong> (migrator → blue/green ECS via CodeDeploy, 10% canary 10 min, auto-rollback on alarm).</li>
<li>Manual approval (CTO or delegate) → <strong>prod</strong> deploy, same shape; release notes auto-generated from Conventional Commits.</li>
<li>Nightly: golden-scenario suite + extraction benchmark against pinned models; regression fails page the on-call and block the next prod deploy.</li>
</ol>
<h2 id="doc-reference-platform--h8">5.4 Resilience, DR &amp; Retention</h2>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-lg"/></colgroup>
<thead>
<tr>
<th>Item</th>
<th>Commitment / Mechanism</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Item">Availability target</td>
<td data-label="Commitment / Mechanism">≥99.9% monthly, Multi-AZ RDS/Redis, ≥2 tasks/service across AZs, ALB health checks, graceful drains.</td>
</tr>
<tr>
<td data-label="Item">RPO / RTO</td>
<td data-label="Commitment / Mechanism">RPO ≤15 min (PITR + Redis is rebuildable); RTO ≤4 h via Terraform re-provision runbook; quarterly restore test in stage is a release-blocking control.</td>
</tr>
<tr>
<td data-label="Item">Degradation modes</td>
<td data-label="Commitment / Mechanism">Bedrock down → pipeline pauses at Gate 2, tasks stay <code>pending_extraction</code>, ops alert (no unvalidated publishing). Twilio down → channel ladder skips to next channel; safety-tier failures page. Epic down → ingestion retries with backoff; episodes continue on last-known plan.</td>
</tr>
<tr>
<td data-label="Item">Data retention</td>
<td data-label="Commitment / Mechanism">PHI: contract term + 6 y (audit) unless partner policy stricter; Twilio/SES message bodies not stored at vendor beyond delivery (config); LLM: Bedrock zero-retention, no training use (SEC-010); voice recordings: not stored in MVP.</td>
</tr>
<tr>
<td data-label="Item">Environments parity</td>
<td data-label="Commitment / Mechanism">Stage = prod Terraform with size overrides only; drift detection nightly (<code>terraform plan</code> must be empty).</td>
</tr>
</tbody>
</table></div>
<h1>2. Scope &amp; Requirements Baseline</h1>
<p>The functional baseline is the Product Requirements Document. All requirement IDs referenced in this document (e.g., <code>MED-006</code>, <code>ENG-012</code>) are normative and their acceptance criteria are the test oracle. Section 20 provides the full requirement-to-component traceability matrix; no MVP requirement is unowned. Where the PRD says “configurable,” this specification names the configuration key, its default value, and its owner.</p>
<h2 id="doc-reference-platform--h9">2.1 Actors and Trust Levels</h2>
<div class="tbl-wrap"><table class="col-table compact"><colgroup><col class="col-auto"/><col class="col-md"/><col class="col-lg"/></colgroup>
<thead>
<tr>
<th>Actor</th>
<th>Authentication</th>
<th>Trust / Authorization Scope</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Actor">Patient</td>
<td data-label="Authentication">Activation token → session JWT (Section 9.1)</td>
<td data-label="Trust / Authorization Scope">Own episode only. Can report status, respond to tasks, manage consent &amp; caregivers.</td>
</tr>
<tr>
<td data-label="Actor">Caregiver</td>
<td data-label="Authentication">Own invitation token → own session JWT</td>
<td data-label="Trust / Authorization Scope">Patient-granted scope on one episode; read + report; never sees other episodes; revocable instantly.</td>
</tr>
<tr>
<td data-label="Actor">Clinician</td>
<td data-label="Authentication">Magic link + email OTP; step-up OTP for confirmations</td>
<td data-label="Trust / Authorization Scope">One patient, one episode, time-boxed grant. Read brief; confirm/clarify material items with step-up.</td>
</tr>
<tr>
<td data-label="Actor">Hospital admin</td>
<td data-label="Authentication">Design-partner IdP via OIDC SSO + MFA</td>
<td data-label="Trust / Authorization Scope">Tenant-scoped configuration, cohorts, reports, kill switches per RBAC matrix (Section 9.5).</td>
</tr>
<tr>
<td data-label="Actor">Travi AI Agent operator</td>
<td data-label="Authentication">Internal IdP (Google Workspace OIDC) + MFA</td>
<td data-label="Trust / Authorization Scope">Break-glass PHI access is separately authorized, reasoned, time-boxed, and audited.</td>
</tr>
<tr>
<td data-label="Actor">System agents</td>
<td data-label="Authentication">Service identity (IAM role) + policy envelope</td>
<td data-label="Trust / Authorization Scope">Least-privilege tool allowlists; can never transition clinical state directly (Section 12).</td>
</tr>
</tbody>
</table></div>
<h2 id="doc-reference-platform--h10">2.2 Configuration Ownership Model</h2>
<p>Every tunable named in this document lives in the <code>tenant_config</code> service (Section 16.4) with three tiers: <strong>platform defaults</strong> (owned by Travi AI Agent engineering, changeable by release), <strong>tenant policy</strong> (owned by the design partner, changeable via admin console with versioning per ADM requirements), and <strong>cohort overrides</strong> (owned by the clinical lead). Configuration changes never retroactively alter active episodes; a new <code>config_version</code> applies only to episodes created after publication, satisfying the enterprise immutability constraint.</p>
<div class="pn"><a class="pn-prev" href="#"></a><a class="pn-next" href="#"></a></div></article>