feat(documents): add document_id chat discriminator + verbatim index column (mig 0230)

This commit is contained in:
Marc-André Bombeck committed 2026-07-09 02:34:06 +02:00
1 parent 2b9d5a41c8
commit d4d04c75f4
2 files changed
+66 -1

No files matched your search

@@ -0,0 +1,38 @@
-- Document vault P4: chat about a document.
--
-- Two additive, nullable columns — no backfill, applies clean on a 0229
-- database and on a fresh database:
--
-- 1. `coach_conversations.document_id` — the discriminator that turns a Coach
-- conversation into a DOCUMENT chat. NULL = a normal Coach conversation
-- (health-record surface). Non-NULL = a chat scoped to exactly one stored
-- document, whose only context is that document's text (no health snapshot,
-- no tools). FK cascades: deleting the document removes its chats. A
-- composite index covers both the Coach rail scan (`document_id IS NULL`,
-- newest-first) and the document sheet scan (`document_id = <id>`,
-- newest-first).
--
-- 2. `document_content_index.verbatim_text_encrypted` — AES-256-GCM ciphertext
-- of the VERBATIM extracted text (byte-capped, not lowercased/de-accented),
-- stored additionally alongside the normalised `text_encrypted` so a chat
-- can cite the document faithfully. Nullable: rows indexed before P4 carry
-- NULL until re-indexed, and the chat route falls back to `text_encrypted`.
-- AlterTable
ALTER TABLE "coach_conversations"
ADD COLUMN "document_id" TEXT;
-- AlterTable
ALTER TABLE "document_content_index"
ADD COLUMN "verbatim_text_encrypted" BYTEA;
-- CreateIndex — covers the Coach rail (document_id IS NULL) and the document
-- sheet (document_id = <id>) newest-first scans with one composite.
CREATE INDEX "coach_conversations_user_id_document_id_updated_at_idx"
ON "coach_conversations" ("user_id", "document_id", "updated_at");
-- AddForeignKey — a deleted document takes its chats with it.
ALTER TABLE "coach_conversations"
ADD CONSTRAINT "coach_conversations_document_id_fkey"
FOREIGN KEY ("document_id") REFERENCES "inbound_documents" ("id")
ON DELETE CASCADE ON UPDATE CASCADE;
+28 -1
View File
@@ -4463,10 +4463,24 @@ model CoachConversation {
summaryUpdatedAt DateTime? @map("summary_updated_at")
summaryTurnCount Int @default(0) @map("summary_turn_count")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// v1.27.33 (Document vault P4 — chat about a document) — the discriminator
// that turns a Coach conversation into a DOCUMENT chat. NULL = a normal Coach
// conversation (health-record surface, snapshot + tools). Non-NULL = a chat
// scoped to exactly one stored document: its only context is that document's
// text, no health snapshot, no tools. The two never mix — the Coach rail
// filters `documentId: null` and the document sheet filters `documentId: <id>`.
// Cascades on document delete so a discarded document takes its chats with it.
documentId String? @map("document_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
document InboundDocument? @relation(fields: [documentId], references: [id], onDelete: Cascade)
messages CoachMessage[]
@@index([userId, updatedAt])
// v1.27.33 — the document sheet lists a document's chats newest-first; the
// Coach rail lists `documentId IS NULL` newest-first. This composite covers
// both scans without a separate index per surface.
@@index([userId, documentId, updatedAt])
@@map("coach_conversations")
}
@@ -5847,6 +5861,10 @@ model InboundDocument {
/// 1:1 blind content-search index (encrypted extracted text + HMAC token
/// array). Absent until the document is indexed; cascades on delete.
contentIndex DocumentContentIndex?
/// v1.27.33 (Document vault P4) — "chat about a document" threads. Each is a
/// `CoachConversation` with `documentId` set to this document; cascades on
/// delete so a discarded document takes its chats with it.
chats CoachConversation[]
/// share-link memberships (m:n via `ClinicianShareLinkDocument`).
/// Cascades: deleting this document removes it from every share it was on.
shareLinks ClinicianShareLinkDocument[]
@@ -5960,6 +5978,15 @@ model DocumentContentIndex {
/// with the Coach columns. The plaintext is recoverable server-side so
/// rotation can re-tokenise; the list query never selects it.
textEncrypted Bytes @map("text_encrypted")
/// v1.27.33 (Document vault P4 — chat about a document) — AES-256-GCM
/// ciphertext of the VERBATIM extracted text (byte-capped, NOT lowercased or
/// de-accented), stored additionally alongside `textEncrypted` so a document
/// chat can cite the document faithfully (exact casing, section names, units)
/// rather than from the normalised search text. Same `encrypt()`-string-as-
/// UTF-8 Bytes shape as every other PHI column. Nullable: rows indexed before
/// P4 carry NULL until re-indexed, and the chat route falls back to the
/// normalised `textEncrypted` when it is absent. The list query never selects it.
verbatimTextEncrypted Bytes? @map("verbatim_text_encrypted")
/// Deduped HMAC-SHA256 (truncated hex) of the normalised tokens — the blind
/// index. One-way: opaque without the server-held index subkey. GIN-indexed
/// (raw SQL in the migration; Prisma cannot express a GIN index).