mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Replace the scalar document_id discriminator on a coach conversation with a CoachConversationDocument join table and a sticky documentScoped flag, so a conversation can be grounded on several stored documents at once while the prompt-injection fence stays intact across the data-model change. The fence is re-established at both enforcement points, fail-closed: - Client: resolveCoachSendTarget routes any turn on a fenced conversation (or a fresh chat with staged attachments) to the new /api/insights/chat/fenced endpoint; the fenced body is strict and never carries scope, guided-question, or prefill hints. - Server: the tool route fetches with documentScoped:false AND 404s when the fetched row still carries an attachment (flag/join drift), emitting a fence_drift audit alarm. Document text now enters an LLM prompt in exactly one code path — the fenced pipeline — with no tools and no health snapshot; a module-graph test pins that the tool loop is unreachable from it. The fenced turn fences each document separately with a server-built, scrubbed header line (title and filename are untrusted text), runs a per-document egress consent check on the single picked provider, and grounds numerically over the live attachment union only. Attachments are capped at five documents and 192 KB of combined text with honest truncation. Attach and detach ride dedicated endpoints: attaching validates the document is owned, live, indexed, and within cap, then flips the conversation fenced (the one legal, privilege-reducing transition, audit-logged); detaching removes the document but never clears the flag. The document-sheet chat keeps its public wire contract and shares the same fenced pipeline over the join. Migration 0245 adds the join table and the flag, backfills every legacy single-document chat into a one-attachment fenced conversation, and drops the scalar column in the same transaction — no dual-truth window.
prisma/
The database layer. HealthLog runs PostgreSQL 16 via Prisma 7. This directory holds the single schema file and the forward-only migration history; the generated client is emitted elsewhere.
Contents
schema.prisma— 61 models, one file.cuid()primary keys,snake_casecolumns via@map(...), encrypted columns mostly suffixed*Encrypted(search the file; there are 24 encrypted column references). Thegenerator clientblock emits to../src/generated/prisma(imported as@/generated/prisma).migrations/— numbered, forward-only SQL migrations (0001_…upward).migration_lock.tomlpins the provider topostgresql.
Migration workflow
# author a migration from a schema edit
pnpm dlx prisma migrate dev --name <short_snake_case_name>
# regenerate the client after any schema change
pnpm dlx prisma generate
Number a new migration with the next free prefix. The production entrypoint runs prisma migrate deploy on boot, so every migration must apply cleanly forward — there is no down-migration path. Always include the migration file in the same change as the schema edit.
Conventions
- Encryption is fail-closed:
*Encryptedcolumns are written through@/lib/crypto(AES-256-GCM, versioned key ids); never store plaintext in an encrypted column. Key rotation isscripts/rotate-encryption-key.ts+docs/ops/encryption-key-rotation.md. - The generated client under
src/generated/prismais build output — do not edit it. - Schema-touching changes that affect the API surface must regenerate
docs/api/openapi.yaml(see../CLAUDE.md).
Related: ../src/lib/README.md (db.ts, rollups/), ../docs/ops/ (migrations + backup runbooks).