TDD - Odyssey Phoenix Firm Order Memo Generator — UAT (R3)¶
status: draft
date: 2026-05-20
author: Mark Gleason (Odyssey tech lead)
prd: ./r3-prd.md
brief: ../../explanation/brief.md
epic: ../roadmap.md
parent-release: E-release-3
written-by: TechLead
source-spec: ../reference/Phoenix_Firm_Order_Memo_Spec.docx
STALE — needs revalidation before R3 work starts. This TDD was written against the pre-2026-05-26 R1 architecture (texts / snippets / inline_ routing on sections, separate
engine.pyrender dispatch). R1 was rearchitected to a gather → synthesize → render pipeline with prompts only; texts / snippets / risk_blocks moved to R2. What stays valid: Odyssey-specific tables (firm_order_memos,approval_records,readme_versions), the FastAPI routes, the Next.js app layout, the approval-detection algorithm (rapidfuzz on EFS comments + pdfplumber for typed PDF dates + lookup-table phrase selection —[T-decision-1]), the read-me-in-Postgres pattern ([T-decision-2]), the Phoenix self-host deployment shape, and Odyssey SSO auth. What needs revalidation:* how the Odyssey solution invokes DocGen primitives (it'll bedata_needs+ acontent_recipe=promptper paragraph in the new world, not inline_text/inline_prompt), the eval adapter wiring (R2-deferred from R1), and the Compose recipe (R2-deferred from R1). When R3 starts, rewrite the framework-integration sections against the then-current R1+R2 state; preserve everything else.Update (2026-06-10) — divergence, not just staleness. The framework shipped as a single-repo showcase, not the separate
odyssey-firm-order-memo/monorepo this TDD describes. The Phoenix memo is a seeded template inside thedemo/app +docgenpackage (see../reference/demo-app.mdfor the as-built shape); auth isaltaforge.authbearer tokens, not Odyssey SAML; and the framework capabilities live in R2 (../releases/r2-framework-production-features/r2-tdd.md— render modes[T2-render-modes], computed-fields[T2-computed], readiness gate[T2-readiness], per-doc model/temp[T2-llm-config], version history). This document is retained as the historical Odyssey production-solution design (sovereignty deployment, Odyssey-specific tables, SSO) for when that engagement is real — it is intentionally not rewritten to match the demo. What carried through verbatim: the deterministic approval-detection principle ([T-decision-1]) and the no-fabrication / eval intent, both embodied by the Phoenixcomputedlayer + P4/P5 grounding.Originally written by: TechLead (Mark Gleason). Reviewed by AltaML TechLead before the pod's architect agent picks it up.
How to read this¶
| Audience | Read | Skip |
|---|---|---|
| Engineering reviewer | TL;DR + Tech Stack + Data Model + Key Decisions + Reviewer Checklist | Detailed acceptance tests on first pass |
| Architect agent (issue decomposer) | Everything | - |
| Dev agent (implementer) | Everything; especially Tech Stack, Data Model, Component Structure, current phase's Acceptance Tests | Phases not currently in flight |
| Sponsor / business owner signing off | TL;DR + Phase Decomposition + Reviewer Checklist | Implementation detail |
Length budget: 5-15 pages.
Change History¶
| Date | Author | Change |
|---|---|---|
| 2026-05-20 | Mark Gleason | Initial draft |
| 2026-05-22 | Mark Ly | UI stack: docgen-ui builds on altaforge-ui (Radix Themes); Tailwind removed |
| 2026-05-22 | Mark Ly | Framework package boundary update: docgen-engine + docgen-tools merged into single docgen package; docgen-ui moves to own repo (altaml/altaforge-docgen-ui) consumed as submodule |
| 2026-05-23 | Mark Ly | Simplified: docgen-ui stays co-located in altaforge-docgen (no separate repo); Odyssey submodules altaml/altaforge-docgen and consumes both Python and React from one fetch |
| 2026-05-23 | Mark Ly | Rescoped to new R2 in 4-release plan. Odyssey UAT now lands in R2 (was R1 in old 5-release plan). Engineering content below is unchanged — same FastAPI backend, same Next.js UI, same Phoenix + EFS adapters, same Arize Phoenix self-host deployment. Cross-refs to the framework PRD/TDD now resolve to the R2 framework docs. |
TL;DR [T-tldr]¶
- Next.js + FastAPI Odyssey solution app built on DocGen v0 libraries (
docgenPython package +@altaml/docgen-uiReact package, both consumed by submodulingaltaml/altaforge-docgenand resolving via pnpm workspace during active co-dev); deployed inside Odyssey's cloud account. - Five DocGen Sections (one per firm-order-memo paragraph) wired to two Odyssey-specific data tools (
phoenix_tool,efs_tool) and a prompt template per paragraph drawn from the prompt read-me. - The prompt read-me is a Markdown file in Postgres (versioned by
organization_id, version_hash); the web UI exposes a Monaco-based editor; engine renders Jinja2 over it at generation time. - Approval-detection logic (the highest-risk area, PRD
[P-rule-1/2/3]) lives inefs_toolas pure Python — fuzzy regex match on EFS comments + typed date extraction. Persisted as anApprovalRecordrow per generation for debugging. - Arize Phoenix colocated with the deployment; all evals (PRD
P-eval-1throughP-eval-5) authored against the framework's eval adapter. - 4 phases: adapter scaffolding + read-me v0 → approval-detection + evals → web UI + read-me editor → Phoenix deployment + UAT.
System Overview [T-system]¶
Underwriter (Stamford)
┌────────────────────────────────────────────────┐
│ Browser (Chrome/Safari/Firefox/Edge) │
└──────────────────────┬─────────────────────────┘
│ HTTPS
▼
Odyssey cloud (sovereignty zone)
┌────────────────────────────────────────────────┐
│ Next.js UI (Odyssey-branded) │
│ • imports @altaml/docgen-ui │
│ • Monaco editor for prompt read-me │
│ │ │
│ ▼ (server actions) │
│ FastAPI backend (Odyssey solution layer) │
│ • POST /generate/{pid} │
│ • PATCH /readme │
│ • POST /sections/{id}/regenerate, /edit │
│ • POST /documents/{id}/export │
│ │ │
│ ▼ uses docgen │
│ docgen (in-process) │
│ • orchestrate(5 sections) │
│ • LiteLLM gateway → Claude (Odyssey-routed) │
│ • Arize adapter → Phoenix │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Postgres │ │ Arize │ │
│ │ (DocGen + │ │ Phoenix │ │
│ │ Odyssey │ │ (self-host) │ │
│ │ tables) │ └──────────────┘ │
│ └──────────────┘ │
│ │ │
│ ▼ (data tools) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ phoenix_tool │ │ efs_tool │ │
│ │ (Phoenix DB │ │ (EFS DB/API; │ │
│ │ or API; TBD)│ │ approval │ │
│ │ │ │ detection) │ │
│ └──────┬───────┘ └──────┬───────┘ │
└─────────┼─────────────────┼────────────────────┘
│ │
▼ ▼
Phoenix systems EFS systems (Odyssey-owned)
Everything in the dashed box runs in Odyssey's cloud account. No outbound traffic to AltaML cloud at runtime (per [P-sys] sovereignty).
Tech Stack [T-stack]¶
| Layer | Choice | Rationale (only if non-default) |
|---|---|---|
| Language (backend) | Python 3.11 | (default; matches framework) |
| Language (UI) | TypeScript 5 | (default) |
| UI framework | Next.js 14 (App Router) | (default for AltaML web work) |
| UI components | @altaml/docgen-ui + altaforge-ui (Radix Themes) + local Odyssey-branded components |
(default; framework UX components consumed directly) |
| Code editor (read-me) | Monaco Editor (@monaco-editor/react) |
Battle-tested for in-browser Markdown editing; underwriters comfortable with code-editor-like surface |
| Backend framework | FastAPI | (default) |
| Engine integration | docgen (Python import) |
Framework R1 library |
| Data tools | docgen.tools catalog + Odyssey-specific phoenix_tool, efs_tool (live in solution repo, implement docgen.tools.DataTool ABC) |
Per framework P-feat-6: domain-specific tools start solution-local |
| Prompt templating | Jinja2 (engine-supplied) | (default) |
| Approval-comment fuzzy match | rapidfuzz (Levenshtein distance) |
Tolerates typos per PRD [P-rule-1]; faster than fuzzywuzzy; pure Python |
| PDF date extraction | pdfplumber for text-layer; explicit reject (placeholder) for image-only PDFs |
Per PRD [P-rule-2] — never guess from handwritten dates; pdfplumber surfaces text-layer cleanly |
| LLM access | litellm via framework gateway → Claude 4.5 Sonnet |
(default; framework decision) |
| Temperature | 0 (mandatory per [P-sys] determinism) |
Spec §9.2 |
| DB | Postgres 15 (bundled in deployment Compose for v0; [T-q-1]) |
Per framework client-cloud recipe |
| ORM | SQLAlchemy 2.x (framework-supplied) + Odyssey-only tables via Alembic in solution repo | (default; matches framework) |
| Eval backend | Arize Phoenix (self-host, colocated) | Per framework [T-decision-3]; Odyssey is sovereignty-bound |
| Deployment | Docker Compose recipe from framework deployments/client-cloud/, extended with Odyssey solution containers |
Framework P-feat-9; Odyssey is the proving case |
| Region | Odyssey's cloud region (Odyssey IT picks) | Sovereignty |
| Auth | Odyssey SSO (SAML; details from Odyssey IT) | Odyssey-owned identity provider; outside DocGen scope per framework [T-auth] (n/a in R1) |
| Test runner | pytest + pytest-asyncio (backend), vitest (UI) | (default) |
| Browser support | Chrome, Safari, Firefox, Edge — latest 2 versions | Per PRD [P-sys] |
Data Model [T-entity-N]¶
Odyssey reuses DocGen's organizations (one row: odyssey), documents, sections, versions (R2). The Odyssey-specific tables sit alongside.
[T-entity-1] firm_order_memos ← references PRD [P-entity-1]¶
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
| id | text (uuid) | No | gen_random_uuid() |
PK |
| organization_id | text (uuid) | No | (always odyssey) |
FK → organizations.id |
| document_id | text (uuid) | No | - | FK → documents.id; one Document per memo |
| deal_pid | text | No | - | Phoenix deal identifier |
| program_name | text | Yes | - | Optional contextual input (PRD §5.2) |
| treaty_year | int | Yes | - | Optional contextual input |
| underwriter_name | text | Yes | - | Optional contextual input |
| readme_version_hash | text | No | - | SHA-256 of the prompt-read-me text used at generation; allows reproducing the prompt that produced this output |
| generated_at | timestamptz | No | now() |
Indexes: (organization_id, deal_pid) for lookups; (organization_id, generated_at DESC) for "recent memos" list.
[T-entity-2] approval_records ← references PRD [P-entity-2]¶
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
| id | text (uuid) | No | gen_random_uuid() |
PK |
| organization_id | text (uuid) | No | (always odyssey) |
FK → organizations.id |
| memo_id | text (uuid) | No | - | FK → firm_order_memos.id, cascade delete |
| artifact_type | text | No | - | email / pdf / none-found |
| approver_initials | text | Yes | - | e.g. JG, BQ, CO; null when none-found |
| artifact_date | date | Yes | - | The actual approval date per [P-rule-2]; null = placeholder needed |
| efs_file_ref | text | Yes | - | Opaque EFS file identifier for cross-reference |
| match_confidence | numeric(3,2) | Yes | - | Fuzzy-match confidence 0.00-1.00; null on exact match |
| match_reasoning | text | Yes | - | Why we picked this artifact (e.g., "Most recent matching {initials} approval; comment exact match BQ approval") |
| resolved_at | timestamptz | No | now() |
Indexes: (memo_id).
[T-entity-3] readme_versions¶
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
| id | text (uuid) | No | gen_random_uuid() |
PK |
| organization_id | text (uuid) | No | (always odyssey) |
FK → organizations.id |
| version_hash | text | No | - | SHA-256 of text; unique per organization |
| text | text | No | - | Full Markdown read-me content |
| created_by | text | No | - | Email of underwriter who saved this version |
| created_at | timestamptz | No | now() |
|
| is_current | boolean | No | false |
Exactly one row per organization has true; transactional swap on save |
Indexes: unique (organization_id, version_hash); partial unique (organization_id) WHERE is_current.
API Contracts [T-api-N]¶
FastAPI endpoints; all authenticated via Odyssey SSO (session-cookie based).
[T-api-1] POST /api/generate ← references PRD [P-feat-3], [P-flow-1]¶
Request:
{
"deal_pid": "PID-12345",
"program_name": "Program X",
"treaty_year": 2026,
"underwriter_name": "Matthew Nicolini"
}
Response 200:
{
"memo_id": "uuid",
"document_id": "uuid",
"approval_record": { "artifact_type": "pdf", "approver_initials": "BQ", "artifact_date": "2026-03-07", "match_confidence": 1.0 },
"sections": [
{ "id": "uuid", "ordinal": 1, "title": "Reconfirmation", "content": "...", "status": "generated" },
...
]
}
Errors:
- 404: PID not found in Phoenix
- 502: Phoenix or EFS unreachable
- 500: unexpected — payload { "error": "internal", "memo_id": "uuid" } so the underwriter can reference
Notes: Synchronous; ≤ 30s p95 (PRD [P-sys] latency). Trace emitted to Phoenix via framework adapter.
[T-api-2] POST /api/sections/{id}/regenerate ← references PRD [P-feat-7]¶
Wraps docgen.regenerate_section. Returns updated section.
[T-api-3] PATCH /api/sections/{id} ← references PRD [P-feat-7]¶
Body: { "content": "..." }. Wraps docgen.edit_section.
[T-api-4] GET /api/readme / PUT /api/readme ← references PRD [P-feat-8], [P-flow-2]¶
GET returns current read-me text + version_hash. PUT body { "text": "..." } creates a new readme_versions row, atomically flips is_current. Returns new hash.
[T-api-5] POST /api/documents/{id}/export.docx ← references PRD [P-feat-9]¶
Wraps docgen.export_docx. Returns the .docx as Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document.
Component / Module Structure [T-files]¶
odyssey-firm-order-memo/
apps/
backend/ # FastAPI app
pyproject.toml
src/firm_order_memo/
app.py # FastAPI entry; routes
config.py # Env: Phoenix/EFS endpoints, secrets
deps.py # FastAPI dependencies (DB session, organization)
api/
generate.py # POST /api/generate
sections.py # regenerate, edit
readme.py # GET/PUT readme
export.py # POST .docx export
tools/
phoenix_tool.py # docgen DataTool: reads Phoenix screens
efs_tool.py # docgen DataTool: reads EFS + approval detection
approval/
selector.py # [P-rule-1] artifact selection
date_extractor.py # [P-rule-2] date extraction
phrase.py # [P-rule-3] phrase selection
readme/
renderer.py # Jinja2 over the read-me Markdown
version_store.py # readme_versions table operations
evals/ # [P-eval-1..5] definitions
approval_source.py
approval_date.py
structural_conformance.py
no_fabrication.py
end_to_end.py
db/
models.py # FirmOrderMemo, ApprovalRecord, ReadmeVersion
migrations/ # Alembic
observability/
arize.py # Wraps framework adapter; sets eval_backend=phoenix
tests/
unit/
integration/
test_approval_detection.py # T-test-2.1, 2.2
e2e/
fixtures/ # Sample EFS comments, PDFs, emails
frontend/ # Next.js app
package.json
app/
layout.tsx
page.tsx # Deal input + generate
memo/[id]/page.tsx # Memo review/edit/regenerate/export
readme/page.tsx # Monaco editor for read-me
components/
ParagraphCard.tsx # Wraps @altaml/docgen-ui SectionCard with Odyssey labels
DealInput.tsx
ApprovalBadge.tsx # Shows resolved approval source/date or placeholder
ReadmeEditor.tsx # Monaco wrapper
lib/
api.ts # Typed client for /api/*
deployments/
odyssey-cloud/
docker-compose.yml # Extends framework recipe with backend + frontend + tools
.env.example
README.md # Runbook for AltaML DevOps deploying into Odyssey cloud
readme/
odyssey-firm-order-memo-prompt-readme.md # The initial read-me (Matthew co-authored)
Auth Model [T-auth]¶
- Provider: Odyssey SSO (SAML 2.0); IdP details from Odyssey IT.
- Session: server-side session in Postgres (cookie-based, HTTPS-only, SameSite=Lax). Sessions expire after 8 hours of inactivity.
- Authorization: all routes require an authenticated underwriter; no fine-grained RBAC in v1 (every authenticated user can generate, edit, export, and edit the read-me). Read-me edits log
created_byfor accountability. - Protected routes:
/api/*(all); UI routes redirect to SSO if unauthenticated.
Key Design Decisions [T-decision-N]¶
T-decision-1: Approval detection is pure Python in
efs_tool, not an LLM call Decision:approval.selector,approval.date_extractor,approval.phraseare deterministic Python — fuzzy regex over EFS comments,pdfplumberfor typed PDF dates, lookup table for the phrase. The LLM is only used for synthesis of the five paragraphs. Because: Per PRD[P-sys]no-fabrication contract — the approval source/date is the single most-watched correctness item ([P-metric-2]≥ 95%). LLM-based detection would introduce nondeterminism the spec explicitly forbids. Deterministic code is easier to evaluate, audit, and fix. Cost: harder to handle truly novel approval-comment formats — mitigated by P-eval-1 catching drift.T-decision-2: Prompt read-me in Postgres, not git Decision: The read-me lives as rows in
readme_versions. Underwriters edit via Monaco; save creates a new row + flipsis_current. Initial seed is checked into the repo (readme/odyssey-firm-order-memo-prompt-readme.md) for first deploy. Because: Per PRD[P-metric-3]— underwriters edit without engineering involvement; round-tripping through git would require GitHub access and merge conflict knowledge they shouldn't need. Postgres gives us auditable history, atomic save, andversion_hashtraceability intofirm_order_memos.readme_version_hash. Cost: lose IDE diff/PR review — acceptable because the read-me changes are wording, not structural.T-decision-3: Sync generation, no background queue Decision:
POST /api/generateruns end-to-end inside the request. No Celery. Because: Per PRD[P-sys]latency: ≤ 30s per deal. Single-deal-per-run is the v1 interaction (PRD[P-scope]out-of-scope: batch). Spec §9.2 corroborates. Framework engine is sync (framework[T-decision-1]). Queue would only matter at batch scale, which is explicitly v1+.T-decision-4: Persist
ApprovalRecordper generation, not just compute on the fly Decision: EveryPOST /api/generatewrites a row toapproval_recordswith the selected artifact, fuzzy-match confidence, and reasoning string. Because: Per PRD[P-risk-1]— convention adherence is fragile in production; we need a debug surface to investigate "why did Paragraph 1 say X for deal Y." Persisting the chosen artifact + reasoning lets us re-investigate without re-querying EFS (which may have changed). Storage cost is negligible (~1 row per generation, small payload).T-decision-5: PDF date extraction via
pdfplumber; image-only PDFs return null (placeholder) Decision:pdfplumberextracts the text layer. If the PDF has no text layer (scan/image),date_extractorreturnsnulland the UI shows{placeholder — underwriter to complete}per PRD[P-rule-2]worked example 2. Because: Per PRD[P-scope]out-of-scope item — OCR for handwritten/scanned PDFs is explicitly deferred. Per[P-eval-2]failure mode — fabricated dates are unacceptable. Returningnullis the safe path. Cost: degrades perceived value when image-only PDFs are common ([P-risk-2]); we measure during UAT and decide if OCR investigation is warranted.T-decision-6: All 5 evals live in the solution repo, not the framework Decision:
evals/directory inapps/backend/defines the eval suite, datasets intests/e2e/fixtures/. Run viadocgen eval(framework CLI). Because: Per framework PRD — solution-specific evals live in the solution, not the framework. Keeps the Odyssey eval suite versioned alongside the prompts and tools that affect its outputs. Run in CI on every PR to the read-me or tool code.
Phase Decomposition [T-phase-N]¶
[T-phase-1] Adapter scaffolding + read-me v0 + happy-path generation¶
Issues:
1. Repo setup (monorepo: apps/backend, apps/frontend); pyproject/package.json; CI skeleton
2. FastAPI app with /api/generate returning a hardcoded 5-section response (no real data yet)
3. Odyssey-only tables migration (firm_order_memos, approval_records, readme_versions)
4. phoenix_tool against a Phoenix sandbox with stubbed data; efs_tool against an EFS sandbox with stubbed data
5. Read-me v0 markdown checked in; rendered via Jinja2; produces a memo for a fixture deal
6. Wire DocGen engine, 5 Sections, sync to Postgres
Acceptance tests:
T-test-1.1: Given a Phoenix sandbox with one fixture deal and an EFS sandbox with one fixture approval email, when
POST /api/generateruns, then afirm_order_memosrow is created with fivesectionsfilled and anapproval_recordsrow. Maps to PRD[P-feat-3],[P-flow-1].T-test-1.2: Given the seeded read-me v0, when a memo is generated, then every section's content references the rules from the read-me (paragraph headings match; mandatory Paragraph 1 opening is present). Maps to
[P-feat-5].
[T-phase-2] Approval-detection logic + evals P-eval-1, P-eval-2, P-eval-4¶
Issues:
1. approval.selector — fuzzy regex on EFS comments with rapidfuzz; pick most-recent matching artifact ([P-rule-1])
2. approval.date_extractor — email send-date for emails; pdfplumber text-layer for PDFs; return null for image-only PDFs ([P-rule-2])
3. approval.phrase — lookup table mapping (artifact_type, date?) to opening sentence ([P-rule-3])
4. Wire into efs_tool; persist ApprovalRecord per generation
5. Eval datasets: ~30 EFS scenarios for P-eval-1; ~30 artifacts for P-eval-2; a fixture set for P-eval-4
6. Eval definitions in evals/ using framework adapter; run target: hosted-AX for dev runs, Phoenix once deployed
Acceptance tests:
T-test-2.1: Given an EFS deal with files
[email "JG approval" 2026-03-05, pdf "BQ approval" 2026-03-08, email "initial review" 2026-03-10], when the selector runs, then the chosen artifact is the PDF withapprover_initials="BQ". Maps to[P-rule-1]worked example.T-test-2.2: Given a PDF with typed date
2026-03-07near the signature, when the date extractor runs, thenapproval_date == "2026-03-07". Given an image-only PDF, when the date extractor runs, thenapproval_date IS NULL. Maps to[P-rule-2].T-test-2.3: Given
(artifact_type=pdf, date="2026-03-07"), when the phrase selector runs, then the opening sentence is exactly"Please reconfirm as agreed per our office meeting on 2026-03-07."and not"...per our meeting...". Maps to[P-rule-3]and spec §8.5.T-test-2.4: Given the P-eval-1 dataset (30 EFS scenarios) run through
docgen evalagainst the framework adapter, when the suite completes, then approval-source classification accuracy ≥ 95% on standard cases, with allnone-foundcases surfaced (not promoted to a guess). Maps to[P-eval-1].T-test-2.5: Given the P-eval-4 fixture set (deals + their generated memos) run through the LLM-as-judge no-fabrication scorer, when the suite completes, then zero fabricated approvals/dates/negotiations are detected. Maps to
[P-eval-4], spec §13.
[T-phase-3] Web UI + read-me editor¶
Issues:
1. Next.js app skeleton with Odyssey SSO integration
2. Deal-input page (/); on submit calls /api/generate, redirects to memo view
3. Memo view (/memo/[id]) — five ParagraphCards using @altaml/docgen-ui SectionCard; per-paragraph edit + regenerate + ApprovalBadge for Paragraph 1
4. Export button → POST /api/documents/{id}/export.docx; trigger browser download
5. Read-me page (/readme) — Monaco editor with Markdown syntax highlighting; Save calls PUT /api/readme; preview pane shows rendered Markdown
6. Smoke test against the phase-2 backend
Acceptance tests:
T-test-3.1: Given an underwriter logged in, when they enter a PID and click Generate, then within 30s the memo view shows five paragraphs with content (or clear failure states for sections whose tool failed). Maps to
[P-flow-1],[P-sys]latency.T-test-3.2: Given a generated memo, when the underwriter edits Paragraph 2's content inline and saves, then the section content updates and the section is marked
editedin the UI. When they click regenerate on Paragraph 4, then Paragraph 4 re-runs and the new content replaces the old. Maps to[P-flow-3],[P-feat-7].T-test-3.3: Given an underwriter on the
/readmepage, when they change a fixed phrase in Paragraph 2's rules and save, then a newreadme_versionsrow is created withis_current=trueand the previous version'sis_current=false. When they then generate a new memo, then Paragraph 2 reflects the new phrase. Maps to[P-flow-2],[P-metric-3].T-test-3.4: Given a generated memo on screen, when the underwriter clicks Export, then a .docx file downloads whose content is byte-equivalent to the on-screen text (modulo .docx-native rendering). Maps to
[P-flow-3],[P-feat-9].
[T-phase-4] Phoenix deployment in Odyssey cloud + production-data evals (P-eval-3, P-eval-5) + UAT¶
Issues:
1. Extend framework deployments/client-cloud/docker-compose.yml with Odyssey backend + frontend + tools
2. Deploy into a representative sandbox in Odyssey's cloud account
3. Wire phoenix_tool and efs_tool to production Phoenix/EFS read paths (DB / API / scrape per [P-q-4])
4. Validate Phoenix instance reachability from the engine; first eval run against Phoenix
5. Gather Matthew's gold-standard memos for the P-eval-5 dataset (~10-20 real deals)
6. UAT with Matthew per spec §12.1 cadence (Thursdays for 2 weeks)
7. Tighten read-me based on UAT findings
Acceptance tests:
T-test-4.1: Given the deployment running in the Odyssey sandbox, when
POST /api/generateruns against a real PID, then the response returns within 30s with a five-paragraph memo and a Phoenix trace exists for the generation. Maps to[P-flow-1],[P-sys], framework[T-test-4.1].T-test-4.2: Given the deployment, when network traffic is captured during a generation, then zero outbound traffic to Arize AX domains (
*.arize.com) or to AltaML cloud is observed; all eval/trace traffic stays inside Odyssey's cloud. Maps to[P-sys]sovereignty, framework[T-test-4.2].T-test-4.3: Given 50 representative deals generated through the system, when P-eval-3 (structural conformance) runs, then 100% of memos have all 5 paragraphs present and in order. Maps to
[P-eval-3].T-test-4.4: Given Matthew's gold-standard set for 10-20 real deals, when P-eval-5 (end-to-end acceptance) runs via LLM-as-judge + manual review, then the majority of generated memos are judged "acceptable with minor edits." Maps to
[P-eval-5],[P-metric-1].
Deployment [T-deploy]¶
Owned by AltaML DevOps + Odyssey IT collaboratively.
- Compose recipe:
deployments/odyssey-cloud/docker-compose.ymlextends the frameworkclient-cloudrecipe with Odyssey backend + frontend containers. - Environment: runs entirely inside Odyssey's cloud account (sovereignty); Odyssey IT provisions the VM/AKS-equivalent host; AltaML DevOps deploys and operates.
- Secrets: Odyssey's secret store (e.g., Azure Key Vault inside Odyssey's organization); env-mounted into the containers at start.
- Phoenix: colocated container (
arize-phoenix:latest); persistent volume for eval data. - Postgres: bundled in Compose for v0 (
[T-q-1]); migration path to managed Postgres if Odyssey IT prefers. - Migrations:
alembic upgrade headon backend container start (both framework migrations and Odyssey-specific migrations).
Env vars (passed by Odyssey IT / AltaML DevOps):
- LITELLM_BASE_URL, LITELLM_API_KEY (AltaML LiteLLM endpoint accessible from Odyssey cloud — coordinate networking)
- DATABASE_URL
- EVAL_BACKEND=phoenix, EVAL_BACKEND_URL=http://phoenix:6006
- PHOENIX_DB_DSN / PHOENIX_API_URL (per [P-q-4] once access mechanism is locked)
- EFS_DB_DSN / EFS_API_URL
- ODYSSEY_SSO_METADATA_URL, ODYSSEY_SSO_CERT
Open Questions [T-q-N]¶
- T-q-1: Bundle Postgres in Compose vs. require Odyssey-managed Postgres? Default: bundle for v0 to minimize Odyssey IT setup; revisit if Odyssey requires managed DB for backup reasons.
- T-q-2: LiteLLM endpoint accessibility from Odyssey cloud — does Odyssey allow outbound HTTPS to AltaML's LiteLLM, or do we need a private endpoint / VPN peering? Coordinate with Odyssey IT before phase 4.
- T-q-3: EFS PDF access pattern — does EFS return raw PDF bytes via API, or do we have to download files via SFTP/blob store? Drives
pdfplumberinvocation pattern. - T-q-4: Audit-log retention period — resolving PRD
[P-q-2]. Spec §9.2 + §10 frame this as operational logs for review and regression testing, not formal regulatory audit logs (no retention requirements, tamper-evidence, or compliance framework cited in the spec). Default: retainfirm_order_memos,approval_records, and Phoenix trace records for 90 days rolling window. That's enough for: (a) UAT-era regression testing against gold-standard outputs, (b) debugging "why did Paragraph 1 say X" within a quarter of the run, (c) bounded storage cost on the deployment Postgres. Revisit if Odyssey surfaces a regulatory retention requirement or if Matthew wants longer-term comparison across releases.
(Note: PRD [P-q-1] (input modality) resolved — web UI only for R1. [P-q-2] (audit-log retention) resolved here at T-q-4. [P-q-4] (Phoenix/EFS access mechanism) is the same as T-q-2 and T-q-3 — bubbles up to engineering coordination with Odyssey IT.)
Sign-off¶
| Role | Name | Date |
|---|---|---|
| Author | Mark Gleason | 2026-05-20 |
| Engineering reviewer (AltaML TechLead) | ||
| Business owner | Matthew Nicolini | |
| Approver | Mark Ly (AltaML CTO) |
Reviewer's checklist¶
- [x] TL;DR fits in ≤6 bullets and conveys the engineering shape
- [x] Every PRD
[P-feat-N]has a TDD home — see cross-ref below - [x] Every PRD
[P-metric-N]has a[T-test-N.M]—P-metric-1(acceptance) →T-test-4.4;P-metric-2(Paragraph 1 correctness) →T-test-2.4+T-test-2.5;P-metric-3(self-service) →T-test-3.3 - [x] Every
[T-test-N.M]annotates its parent inline - [x] Tech Stack table is complete; non-default choices (Monaco, rapidfuzz, pdfplumber) have a because
- [x] Every
[T-decision-N]has a because - [x] Every phase has at least one Given/When/Then acceptance test
- [x] Sections that don't apply are marked "n/a" — none (Odyssey solution has auth)
- [x] Open questions are truly unresolved
- [x] Cross-ref IDs (
T-*) assigned
Cross-ref check (PRD [P-feat-N] → TDD home):
- P-feat-1 (Phoenix adapter) → apps/backend/.../phoenix_tool.py, T-test-1.1
- P-feat-2 (EFS adapter) → apps/backend/.../efs_tool.py, approval/*, T-test-2.1 through T-test-2.3
- P-feat-3 (5-paragraph orchestration) → T-api-1, T-test-1.1
- P-feat-4 (Paragraph 1 phrase selector) → approval/phrase.py, T-test-2.3
- P-feat-5 (read-me v0) → readme/odyssey-firm-order-memo-prompt-readme.md + readme/renderer.py, T-test-1.2
- P-feat-6 (deal input + generate UI) → apps/frontend/app/page.tsx, T-test-3.1
- P-feat-7 (memo review UI) → apps/frontend/app/memo/[id]/page.tsx + ParagraphCard, T-test-3.2
- P-feat-8 (read-me editor) → apps/frontend/app/readme/page.tsx + ReadmeEditor, T-test-3.3
- P-feat-9 (.docx export) → T-api-5, T-test-3.4
- P-feat-10 (eval suite) → apps/backend/.../evals/, T-test-2.4, T-test-2.5, T-test-4.3, T-test-4.4
When approved, the architect agent decomposes each [T-phase-N] into atomic ≤30-min issues; each issue inherits its parent [T-test-N.M] as the assertion to verify.