@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/ask/index.d.ts +6 -0
- package/dist/ask/index.js +28 -0
- package/dist/ask/question-schema.d.ts +121 -0
- package/dist/ask/question-schema.js +167 -0
- package/dist/ask/report.d.ts +88 -0
- package/dist/ask/report.js +170 -0
- package/dist/ask/scorer.d.ts +104 -0
- package/dist/ask/scorer.js +164 -0
- package/dist/ask-run/ask-command.d.ts +62 -0
- package/dist/ask-run/ask-command.js +254 -0
- package/dist/ask-run/host/app.d.ts +65 -0
- package/dist/ask-run/host/app.js +146 -0
- package/dist/ask-run/host/db.d.ts +38 -0
- package/dist/ask-run/host/db.js +126 -0
- package/dist/ask-run/host/ports.d.ts +35 -0
- package/dist/ask-run/host/ports.js +113 -0
- package/dist/ask-run/index.d.ts +13 -0
- package/dist/ask-run/index.js +35 -0
- package/dist/ask-run/replay.d.ts +128 -0
- package/dist/ask-run/replay.js +212 -0
- package/dist/ask-run/run-ask-eval.d.ts +125 -0
- package/dist/ask-run/run-ask-eval.js +193 -0
- package/dist/ask-run/seed-golden-graph.d.ts +64 -0
- package/dist/ask-run/seed-golden-graph.js +495 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/kg-command.d.ts +66 -0
- package/dist/cli/kg-command.js +238 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +69 -0
- package/dist/fixtures/domains.d.ts +16 -0
- package/dist/fixtures/domains.js +33 -0
- package/dist/fixtures/fixture-schema.d.ts +323 -0
- package/dist/fixtures/fixture-schema.js +199 -0
- package/dist/fixtures/index.d.ts +16 -0
- package/dist/fixtures/index.js +40 -0
- package/dist/fixtures/load.d.ts +73 -0
- package/dist/fixtures/load.js +137 -0
- package/dist/fixtures/questions.d.ts +15 -0
- package/dist/fixtures/questions.js +26 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +36 -0
- package/dist/kg/gold-runner.d.ts +13 -0
- package/dist/kg/gold-runner.js +57 -0
- package/dist/kg/index.d.ts +17 -0
- package/dist/kg/index.js +65 -0
- package/dist/kg/internals.d.ts +7 -0
- package/dist/kg/internals.js +17 -0
- package/dist/kg/prediction-schema.d.ts +245 -0
- package/dist/kg/prediction-schema.js +102 -0
- package/dist/kg/predictions.d.ts +11 -0
- package/dist/kg/predictions.js +49 -0
- package/dist/kg/report.d.ts +84 -0
- package/dist/kg/report.js +125 -0
- package/dist/kg/run-kg-eval.d.ts +77 -0
- package/dist/kg/run-kg-eval.js +137 -0
- package/dist/kg/runner.d.ts +72 -0
- package/dist/kg/runner.js +51 -0
- package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
- package/dist/kg/runners/extract-resolve-runner.js +199 -0
- package/dist/kg/runners/extract-runner.d.ts +65 -0
- package/dist/kg/runners/extract-runner.js +243 -0
- package/dist/kg/scorer.d.ts +226 -0
- package/dist/kg/scorer.js +463 -0
- package/dist/kg/targets.d.ts +17 -0
- package/dist/kg/targets.js +16 -0
- package/dist/kg/text.d.ts +45 -0
- package/dist/kg/text.js +100 -0
- package/dist/live/adapters.d.ts +16 -0
- package/dist/live/adapters.js +58 -0
- package/dist/live/env.d.ts +39 -0
- package/dist/live/env.js +56 -0
- package/dist/live/index.d.ts +6 -0
- package/dist/live/index.js +16 -0
- package/dist/live/live-ai.d.ts +104 -0
- package/dist/live/live-ai.js +157 -0
- package/dist/parity/constants.d.ts +79 -0
- package/dist/parity/constants.js +104 -0
- package/dist/parity/index.d.ts +20 -0
- package/dist/parity/index.js +38 -0
- package/dist/parity/internals.d.ts +8 -0
- package/dist/parity/internals.js +16 -0
- package/dist/parity/scenario.d.ts +45 -0
- package/dist/parity/scenario.js +245 -0
- package/dist/parity/script.d.ts +44 -0
- package/dist/parity/script.js +136 -0
- package/dist/parity/snapshot.d.ts +43 -0
- package/dist/parity/snapshot.js +71 -0
- package/dist/real-data/index.d.ts +2 -0
- package/dist/real-data/index.js +12 -0
- package/dist/real-data/real-data.d.ts +76 -0
- package/dist/real-data/real-data.js +156 -0
- package/fixtures/kg-golden/README.md +136 -0
- package/fixtures/kg-golden/ask-questions.json +458 -0
- package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
- package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
- package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
- package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
- package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
- package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
- package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
- package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
- package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
- package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
- package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
- package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
- package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
- package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
- package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
- package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
- package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
- package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
- package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
- package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
- package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
- package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
- package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
- package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
- package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
- package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
- package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
- package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
- package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
- package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
- package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
- package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
- package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
- package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
- package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
- package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
- package/host/generated/client.d.ts +1 -0
- package/host/generated/client.js +5 -0
- package/host/generated/default.d.ts +1 -0
- package/host/generated/default.js +5 -0
- package/host/generated/edge.d.ts +1 -0
- package/host/generated/edge.js +1658 -0
- package/host/generated/index-browser.js +1684 -0
- package/host/generated/index.d.ts +192335 -0
- package/host/generated/index.js +1658 -0
- package/host/generated/package.json +139 -0
- package/host/generated/query_compiler_fast_bg.js +2 -0
- package/host/generated/query_compiler_fast_bg.wasm +0 -0
- package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/host/generated/runtime/client.d.ts +3433 -0
- package/host/generated/runtime/client.js +110 -0
- package/host/generated/runtime/index-browser.d.ts +90 -0
- package/host/generated/runtime/index-browser.js +6 -0
- package/host/generated/runtime/wasm-compiler-edge.js +100 -0
- package/host/generated/schema.prisma +6312 -0
- package/host/generated/wasm-edge-light-loader.mjs +5 -0
- package/host/generated/wasm-worker-loader.mjs +5 -0
- package/host/prisma/fragments/kvox.ask.prisma +126 -0
- package/host/prisma/fragments/kvox.graph.prisma +1073 -0
- package/host/prisma/fragments/kvox.notes.prisma +787 -0
- package/host/prisma/fragments/kvox.search.prisma +211 -0
- package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
- package/host/prisma/kvox.lock +101 -0
- package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
- package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
- package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
- package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
- package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
- package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
- package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
- package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
- package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
- package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
- package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
- package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
- package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
- package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
- package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
- package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
- package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
- package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
- package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
- package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
- package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
- package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
- package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
- package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
- package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
- package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
- package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
- package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
- package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
- package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
- package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
- package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
- package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
- package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
- package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
- package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
- package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
- package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
- package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
- package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
- package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
- package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
- package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
- package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
- package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
- package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
- package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
- package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
- package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
- package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
- package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
- package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
- package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
- package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
- package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
- package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
- package/host/prisma/platform.lock +319 -0
- package/host/prisma/schema/app.kvox.ask.prisma +110 -0
- package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
- package/host/prisma/schema/app.kvox.notes.prisma +746 -0
- package/host/prisma/schema/app.kvox.search.prisma +208 -0
- package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
- package/host/prisma/schema/platform.ai.prisma +419 -0
- package/host/prisma/schema/platform.android-app.prisma +46 -0
- package/host/prisma/schema/platform.base.prisma +15 -0
- package/host/prisma/schema/platform.credentials.prisma +156 -0
- package/host/prisma/schema/platform.db-backup.prisma +359 -0
- package/host/prisma/schema/platform.identity.prisma +638 -0
- package/host/prisma/schema/platform.jobs.prisma +824 -0
- package/host/prisma/schema/platform.notifications.prisma +641 -0
- package/host/prisma/schema/platform.settings.prisma +53 -0
- package/host/prisma/schema/platform.sharing.prisma +208 -0
- package/host/prisma/schema/platform.storage.prisma +118 -0
- package/package.json +112 -1
- package/parity/requests.ask.json +2681 -0
- package/parity/requests.m01.json +1554 -0
- package/parity/requests.m02.json +1554 -0
- package/parity/requests.m03.json +1554 -0
- package/parity/requests.m04.json +1554 -0
- package/parity/requests.m05.json +1554 -0
- package/parity/requests.m06.json +1554 -0
- package/parity/requests.m07.json +1554 -0
- package/parity/requests.m08.json +1554 -0
- package/parity/requests.m09.json +1554 -0
- package/parity/requests.m10.json +1554 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
// GENERATED by kvox-db sync — do not edit
|
|
2
|
+
// source: @marinoscar/kvox-db:schema/kvox.notes.prisma
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Notes (issue #48, epic #45) — docs/specs/notes.md §4
|
|
6
|
+
// =============================================================================
|
|
7
|
+
//
|
|
8
|
+
// Five tables — `notes`, `note_templates`, `note_generations`, `note_versions`,
|
|
9
|
+
// `note_exports` — for the AI-generated, user-correctable documents this epic
|
|
10
|
+
// adds on top of a transcript, another note, or an uploaded document. Nothing
|
|
11
|
+
// about generation, the stream, prompt assembly or export lands with this
|
|
12
|
+
// issue — only the schema and RBAC, ahead of the code that reads and writes
|
|
13
|
+
// it, matching the same "schema ships whole, code follows" precedent
|
|
14
|
+
// `jobs`/`worker_nodes` and `transcripts` (issue #24) already set. Every
|
|
15
|
+
// column, enum member and index below is named for and justified by
|
|
16
|
+
// docs/specs/notes.md §1, §4, §6 and §7 — read those sections for the full
|
|
17
|
+
// reasoning; this comment restates only what has to live beside the code to
|
|
18
|
+
// stay correct.
|
|
19
|
+
//
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
// owner_id CASCADES; the three source pointers and template_id RESTRICT or
|
|
22
|
+
// SetNull — the exact `Transcript` shape (schema.prisma's own block comment
|
|
23
|
+
// above), with one genuinely new wrinkle: a SELF-relation
|
|
24
|
+
// -----------------------------------------------------------------------------
|
|
25
|
+
// `owner_id` points UP the ownership chain, identically to `transcripts
|
|
26
|
+
// .owner_id`: a note has no meaning and this application's access model has
|
|
27
|
+
// no path to it once its owner is gone — there is deliberately no
|
|
28
|
+
// `notes:read_any` (spec §6.3) — so Cascade is the only outcome consistent
|
|
29
|
+
// with "no read-any, ever."
|
|
30
|
+
//
|
|
31
|
+
// The three source columns point SIDEWAYS and RESTRICT, but spec §4.1 is
|
|
32
|
+
// explicit that the *consequence* of that Restrict is not uniform across the
|
|
33
|
+
// three, and this schema does not paper over the difference:
|
|
34
|
+
//
|
|
35
|
+
// - `source_object_id` -> `storage_objects` is Restrict for the same
|
|
36
|
+
// belt-and-suspenders reason every `storage_objects` FK on `transcripts`
|
|
37
|
+
// is: the uploaded document is `managed_by: 'notes'`, so the generic
|
|
38
|
+
// storage `DELETE` already 409s on it before this FK is ever tested.
|
|
39
|
+
// - `source_transcript_id` -> `transcripts` and `source_note_id` -> `notes`
|
|
40
|
+
// (a SELF-relation — the one structural difference from every Restrict FK
|
|
41
|
+
// `transcripts` declares) point at rows that ARE independently,
|
|
42
|
+
// legitimately deletable through their own existing endpoints, which
|
|
43
|
+
// neither know nor care whether a note was ever derived from them.
|
|
44
|
+
// Restrict therefore has a real, user-visible consequence here:
|
|
45
|
+
// `DELETE /api/transcripts/:id` and (once #53 lands) `DELETE
|
|
46
|
+
// /api/notes/:id` on a note cited as another note's source must both gain
|
|
47
|
+
// a pre-check that 409s naming the blocking note id(s) BEFORE the row
|
|
48
|
+
// reaches this constraint, exactly as `transcripts.service.ts` already
|
|
49
|
+
// does for storage objects. This migration adds only the constraint; the
|
|
50
|
+
// one-line guard for the transcript side is backend work for the PR that
|
|
51
|
+
// lands transcripts.service.ts's next change, and the note-on-note guard
|
|
52
|
+
// is #53's own responsibility once `notes.service.ts` exists to hold it.
|
|
53
|
+
//
|
|
54
|
+
// `template_id` is the one FK in this feature that is `SetNull`, not
|
|
55
|
+
// `Restrict`, and that asymmetry is deliberate (spec §4.1's own closing
|
|
56
|
+
// paragraph): a template is a reusable RECIPE, never EVIDENCE — VISION.md's
|
|
57
|
+
// Trust-and-Provenance thesis is about a note staying connected to the
|
|
58
|
+
// INFORMATION it transformed, never about which instructions did the
|
|
59
|
+
// transforming. Making it Restrict would mean a custom template could never
|
|
60
|
+
// be deleted once it had produced even one note, an ordinary thing a user
|
|
61
|
+
// wants to do (spec §7.3's duplicate-and-edit flow exists precisely so this
|
|
62
|
+
// is safe to allow). Losing the *reference* on delete costs nothing durable:
|
|
63
|
+
// `note_generations.templateNameSnapshot` below keeps "which recipe produced
|
|
64
|
+
// this" as a plain string forever, independent of whether the template row
|
|
65
|
+
// itself survives.
|
|
66
|
+
//
|
|
67
|
+
// -----------------------------------------------------------------------------
|
|
68
|
+
// `note_generations` is deliberately SELF-SUFFICIENT — everything
|
|
69
|
+
// `assemblePrompt` needs is denormalized onto the row, because a template
|
|
70
|
+
// PREVIEW generation has no `note_id` to join back through
|
|
71
|
+
// -----------------------------------------------------------------------------
|
|
72
|
+
// `template_name_snapshot`, `context_text`, `source_type` and the three
|
|
73
|
+
// (deliberately PLAIN, non-FK) `source_*_id` columns, `provider_id` and
|
|
74
|
+
// `model` are all copies taken at the moment a generation is created, not
|
|
75
|
+
// joins. Two things follow from that, both worth stating explicitly rather
|
|
76
|
+
// than leaving implicit:
|
|
77
|
+
//
|
|
78
|
+
// - The three `source_*_id` columns here carry NO foreign key at all,
|
|
79
|
+
// unlike their namesakes on `notes` above. They are a denormalized RECORD
|
|
80
|
+
// of what was resolved when this generation started, not a reference this
|
|
81
|
+
// schema has to keep valid — a `kind: 'preview'` row has no `note_id` and
|
|
82
|
+
// is hard-deleted by its own TTL sweep regardless of what its source
|
|
83
|
+
// later does, so there is nothing for a Restrict to protect here that the
|
|
84
|
+
// owning `notes` row (for `create`/`regenerate`) is not already
|
|
85
|
+
// protecting on its own account.
|
|
86
|
+
// - `job_id` DOES EXIST ON THIS TABLE, `@unique`/nullable/`SetNull`,
|
|
87
|
+
// mirroring `DatabaseBackupRun.jobId` and `TranscriptExport.jobId` for
|
|
88
|
+
// the identical stated reason (issue #48): this row's own lifetime —
|
|
89
|
+
// read by the SSE endpoint and the note detail page for as long as
|
|
90
|
+
// generation is in flight or its content is still being displayed — is
|
|
91
|
+
// independent of `job.history.purge`'s retention schedule for the
|
|
92
|
+
// underlying `jobs` row. Losing the LINK on a history purge must never
|
|
93
|
+
// mean losing the GENERATION (its buffered `content` is what a
|
|
94
|
+
// `note_versions.ai_generated` row was stamped from). `SetNull` rather
|
|
95
|
+
// than `Restrict` for the same reason every other `jobId` in this schema
|
|
96
|
+
// is `SetNull`: a purged job's row disappearing is routine housekeeping,
|
|
97
|
+
// not a reason to block it.
|
|
98
|
+
//
|
|
99
|
+
// `error_class` is a proper enum (`NoteGenerationErrorClass`), unlike
|
|
100
|
+
// `Job.type`/`StorageObject.managedBy`'s deliberately open, code-owned plain
|
|
101
|
+
// strings: spec §2.2's taxonomy is a small, CLOSED, product-defined set (not
|
|
102
|
+
// a registry of pluggable handlers), the same reasoning every other
|
|
103
|
+
// small-closed-set field in this schema (`TranscriptExportStatus`,
|
|
104
|
+
// `DatabaseBackupTrigger`) is an enum rather than a string.
|
|
105
|
+
//
|
|
106
|
+
// -----------------------------------------------------------------------------
|
|
107
|
+
// `note_versions` stores a FULL BODY SNAPSHOT per row, not an operation log —
|
|
108
|
+
// the decision issue #46/#48 flag explicitly as a genuinely different answer
|
|
109
|
+
// from `transcript_versions`, restated here because it is the one place a
|
|
110
|
+
// reader of this schema without the spec open would reasonably ask "why does
|
|
111
|
+
// this feature not reuse the op-log machinery next door"
|
|
112
|
+
// -----------------------------------------------------------------------------
|
|
113
|
+
// `docs/specs/transcription.md` §11's own rejected-alternatives entry gives
|
|
114
|
+
// the arithmetic that ruled a full snapshot OUT for transcripts: roughly 7 MB
|
|
115
|
+
// per save for a 10-hour, 90,000-word recording. A note is a page or two of
|
|
116
|
+
// prose — a few kilobytes — so that arithmetic simply does not apply, and the
|
|
117
|
+
// machinery an op log needs to earn its cost (pure reducers, `materialize()`,
|
|
118
|
+
// gap-based ordinals, per-entity `rev`) has no equivalent structure here: a
|
|
119
|
+
// note has no segments, no speakers, and no concurrent-editor reconciliation
|
|
120
|
+
// to do (spec §5's stream is the only writer while generating). Storing the
|
|
121
|
+
// whole markdown string per save is not a shortcut around building that
|
|
122
|
+
// machinery — for a document this small, the op log is what would have been
|
|
123
|
+
// over-engineering.
|
|
124
|
+
//
|
|
125
|
+
// -----------------------------------------------------------------------------
|
|
126
|
+
// `note_exports` mirrors `transcript_exports` FIELD FOR FIELD (spec §4.6),
|
|
127
|
+
// including the one place this migration follows the REAL, already-shipped
|
|
128
|
+
// `TranscriptExport` implementation over spec §4.6's own shorthand prose
|
|
129
|
+
// -----------------------------------------------------------------------------
|
|
130
|
+
// Spec §4.6 describes the reuse-by-content-hash lookup with the English word
|
|
131
|
+
// "unique". The actual `transcript_exports` table it says this mirrors does
|
|
132
|
+
// NOT enforce that with a `@@unique` — see this schema's own block comment
|
|
133
|
+
// above `TranscriptExport`: "an expired or failed row may briefly coexist
|
|
134
|
+
// with a fresh one sharing the same key before housekeeping removes the
|
|
135
|
+
// stale one." A literal `@@unique` here would make that ordinary coexistence
|
|
136
|
+
// a write failure. This migration reuses `transcript_exports_lookup_idx`'s
|
|
137
|
+
// actual shape — a plain, non-unique `@@index` — because "mirrors
|
|
138
|
+
// `transcript_exports` field for field" (issue #48's own proposed solution)
|
|
139
|
+
// is the more specific, more load-bearing instruction, and because matching
|
|
140
|
+
// the table this feature explicitly exists to let #54 "reuse #28's registry
|
|
141
|
+
// unchanged" against means matching what that registry's code actually
|
|
142
|
+
// queries, not a one-word paraphrase of it.
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
enum NoteStatus {
|
|
146
|
+
draft
|
|
147
|
+
generating
|
|
148
|
+
ready
|
|
149
|
+
failed
|
|
150
|
+
deleting
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
enum NoteSourceType {
|
|
154
|
+
transcript
|
|
155
|
+
note
|
|
156
|
+
document
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
enum NoteGenerationKind {
|
|
160
|
+
create
|
|
161
|
+
regenerate
|
|
162
|
+
preview
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
enum NoteGenerationStatus {
|
|
166
|
+
pending
|
|
167
|
+
streaming
|
|
168
|
+
succeeded
|
|
169
|
+
failed
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
enum NoteGenerationErrorClass {
|
|
173
|
+
auth
|
|
174
|
+
refusal
|
|
175
|
+
rate_limit
|
|
176
|
+
other
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
enum NoteVersionKind {
|
|
180
|
+
ai_generated
|
|
181
|
+
edit
|
|
182
|
+
restore
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
enum NoteExportStatus {
|
|
186
|
+
pending
|
|
187
|
+
ready
|
|
188
|
+
failed
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
enum NoteTitleSource {
|
|
192
|
+
ai
|
|
193
|
+
user
|
|
194
|
+
template
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
model Note {
|
|
198
|
+
id String @id @default(uuid()) @db.Uuid
|
|
199
|
+
|
|
200
|
+
ownerId String @map("owner_id") @db.Uuid
|
|
201
|
+
title String
|
|
202
|
+
|
|
203
|
+
// WHO chose the current `title`, not just what it says (issue #180, epic
|
|
204
|
+
// #163). A title had no provenance before this column: nothing could tell
|
|
205
|
+
// a name the user typed apart from one the product guessed, so a
|
|
206
|
+
// `regenerate` or a future bulk-retitle pass had no way to know a note's
|
|
207
|
+
// owner had already renamed it — the exact failure mode this column exists
|
|
208
|
+
// to rule out is silently overwriting a human-chosen title with an AI
|
|
209
|
+
// guess. `template` is the default because that is literally what every
|
|
210
|
+
// row in this table has been until now: `notes.service.ts` has always
|
|
211
|
+
// fallen back to the source template's `name` when nothing more specific
|
|
212
|
+
// was available, so `template` is the truthful value for the entire
|
|
213
|
+
// existing dataset, not a placeholder invented for this migration.
|
|
214
|
+
// `user` is STICKY — once a person edits the title directly it becomes
|
|
215
|
+
// `user` and the AI titling path (present or future) must never overwrite
|
|
216
|
+
// it or flip it back to `ai`/`template`; that check belongs to the service
|
|
217
|
+
// layer, this column only makes the state it must check representable.
|
|
218
|
+
titleSource NoteTitleSource @default(template) @map("title_source")
|
|
219
|
+
|
|
220
|
+
// The LIVE working copy of the note's markdown body (issue #48). @db.Text,
|
|
221
|
+
// defaulting to an empty string so a freshly created `draft` note (no
|
|
222
|
+
// version yet — `currentVersion` starts at 0) has something to read rather
|
|
223
|
+
// than a nullable hole every reader would have to special-case.
|
|
224
|
+
//
|
|
225
|
+
// THE INVARIANT THIS COLUMN EXISTS UNDER: `notes.body` always equals the
|
|
226
|
+
// `note_versions` row at `note_versions.version = notes.current_version`.
|
|
227
|
+
// `note_versions.body` is the immutable, append-only SNAPSHOT — one row
|
|
228
|
+
// per save, never rewritten, the full history #53's version list and
|
|
229
|
+
// restore endpoint read from. `notes.body` is the single denormalized
|
|
230
|
+
// COPY of whichever snapshot is current, kept in lockstep by the same
|
|
231
|
+
// write path that bumps `currentVersion` (spec §1.3): every write that
|
|
232
|
+
// appends a `note_versions` row — AI generation completing, a `PATCH`
|
|
233
|
+
// editing the body, a restore — updates both columns together, in the
|
|
234
|
+
// same transaction, or not at all. This is what lets `GET /api/notes/:id`
|
|
235
|
+
// (#53) answer "what does this note say right now" with a single-row read
|
|
236
|
+
// and no join, while `GET /api/notes/:id/versions` still has the full,
|
|
237
|
+
// independently-readable history to page through. A `notes.body` that
|
|
238
|
+
// ever disagreed with its own `currentVersion`'s snapshot would be a bug
|
|
239
|
+
// in that write path, not an expected state.
|
|
240
|
+
body String @default("") @db.Text
|
|
241
|
+
|
|
242
|
+
// The markdown-vs-plain-text syntax `body` is written in, SNAPSHOTTED from
|
|
243
|
+
// the generating template's `bodyFormat` at generation time (never a live
|
|
244
|
+
// read of the template) — so a later edit to the template's body format
|
|
245
|
+
// never changes how an already-generated note renders. Plain String, not
|
|
246
|
+
// an enum, mirroring `NoteTemplate.bodyFormat` exactly.
|
|
247
|
+
bodyFormat String @default("markdown") @map("body_format")
|
|
248
|
+
|
|
249
|
+
status NoteStatus @default(draft)
|
|
250
|
+
|
|
251
|
+
// Bumped only by the write path that commits a new `note_versions` row
|
|
252
|
+
// (spec §1.3) — the same "current pointer, not a computed MAX()"
|
|
253
|
+
// convention `transcripts.currentVersion` already uses.
|
|
254
|
+
currentVersion Int @default(0) @map("current_version")
|
|
255
|
+
|
|
256
|
+
// Which registered `AiProvider` (issue #47) and which of its models
|
|
257
|
+
// produced the CURRENT body — denormalized onto the note itself (issue
|
|
258
|
+
// #48) so a note detail page can state "generated by GPT-4o" without
|
|
259
|
+
// joining to `note_generations`, whose rows `notes.housekeeping` is free
|
|
260
|
+
// to sweep (preview generations expire; nothing guarantees a `create`/
|
|
261
|
+
// `regenerate` generation outlives the note it produced forever). Plain
|
|
262
|
+
// strings, not an enum/FK — the provider registry is code-owned and open,
|
|
263
|
+
// the same `Transcript.provider`/`NoteGeneration.providerId` reasoning
|
|
264
|
+
// applied here. Both null until the first successful generation; a body
|
|
265
|
+
// edited by a human afterward does NOT clear either — they answer "what
|
|
266
|
+
// produced this note originally," not "who wrote the current body."
|
|
267
|
+
provider String?
|
|
268
|
+
model String?
|
|
269
|
+
|
|
270
|
+
// The generation to watch (spec §1.3) — set the moment a create/regenerate
|
|
271
|
+
// request enqueues a job, never cleared afterward, so a `ready` note's
|
|
272
|
+
// pointer always names the generation that produced its current content.
|
|
273
|
+
// SetNull rather than Restrict: losing this pointer loses only "which
|
|
274
|
+
// generation is live right now," never the generation row itself or any
|
|
275
|
+
// `note_versions` row it already produced.
|
|
276
|
+
currentGenerationId String? @map("current_generation_id") @db.Uuid
|
|
277
|
+
|
|
278
|
+
// Exactly one of the three below is set, matching `sourceType` — enforced
|
|
279
|
+
// by the service layer (`notes.service.ts`, #53), not by the database:
|
|
280
|
+
// Postgres has no portable "exactly one of these three columns is
|
|
281
|
+
// non-null" constraint short of a CHECK this schema does not add, the same
|
|
282
|
+
// posture `transcripts` takes toward its own multi-column invariants.
|
|
283
|
+
sourceType NoteSourceType @map("source_type")
|
|
284
|
+
sourceTranscriptId String? @map("source_transcript_id") @db.Uuid
|
|
285
|
+
sourceNoteId String? @map("source_note_id") @db.Uuid
|
|
286
|
+
sourceObjectId String? @map("source_object_id") @db.Uuid
|
|
287
|
+
|
|
288
|
+
// SetNull, not Restrict — see the block comment above the enums.
|
|
289
|
+
templateId String? @map("template_id") @db.Uuid
|
|
290
|
+
|
|
291
|
+
// The optional free-text Context (spec §3.1) — short, user-authored,
|
|
292
|
+
// placed ahead of the source in every assembled prompt.
|
|
293
|
+
contextText String? @map("context_text")
|
|
294
|
+
|
|
295
|
+
failureReason String? @map("failure_reason")
|
|
296
|
+
|
|
297
|
+
// GENERATED ALWAYS AS (setweight(to_tsvector('english'::regconfig,
|
|
298
|
+
// coalesce(title, '')), 'A') || setweight(to_tsvector('english'::regconfig,
|
|
299
|
+
// coalesce(body, '')), 'B')) STORED, plus a GIN index — issue #174, epic
|
|
300
|
+
// #164. Weighted A/B, unlike `TranscriptSegment.searchVector`: `title` is
|
|
301
|
+
// human-authored (the user wrote it) and `body` is AI-generated (or later
|
|
302
|
+
// user-edited), so a title hit is stronger evidence and `ts_rank`/
|
|
303
|
+
// `ts_rank_cd` can reflect that. Hand-written in
|
|
304
|
+
// `20260915120000_add_search_vectors/migration.sql`; see that migration's
|
|
305
|
+
// header for the full argument, including why the two-argument
|
|
306
|
+
// `to_tsvector` form is required and why a generated column rather than a
|
|
307
|
+
// trigger (restore-safety — a trigger does not fire under
|
|
308
|
+
// `session_replication_role = replica`). `Unsupported` and
|
|
309
|
+
// nullable-optional for the same reason `Transcript.titleSearchVector`
|
|
310
|
+
// is: never selectable or writable through the Prisma client — every read
|
|
311
|
+
// goes through raw SQL.
|
|
312
|
+
searchVector Unsupported("tsvector")? @map("search_vector")
|
|
313
|
+
|
|
314
|
+
deletedAt DateTime? @map("deleted_at") @db.Timestamptz
|
|
315
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
316
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
317
|
+
|
|
318
|
+
owner User @relation("NoteOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
319
|
+
sourceTranscript Transcript? @relation("NoteSourceTranscript", fields: [sourceTranscriptId], references: [id], onDelete: Restrict)
|
|
320
|
+
// Self-relation — see the block comment above the enums for why this one
|
|
321
|
+
// Restrict FK has a real, user-visible delete-blocking consequence.
|
|
322
|
+
sourceNote Note? @relation("NoteSourceNote", fields: [sourceNoteId], references: [id], onDelete: Restrict)
|
|
323
|
+
sourceObject StorageObject? @relation("NoteSourceObject", fields: [sourceObjectId], references: [id], onDelete: Restrict)
|
|
324
|
+
template NoteTemplate? @relation("NoteTemplateSelection", fields: [templateId], references: [id], onDelete: SetNull)
|
|
325
|
+
currentGeneration NoteGeneration? @relation("NoteCurrentGeneration", fields: [currentGenerationId], references: [id], onDelete: SetNull)
|
|
326
|
+
|
|
327
|
+
// The other side of the self-relation above: every note that named THIS
|
|
328
|
+
// note as its source. What #53's own delete guard queries before allowing
|
|
329
|
+
// `DELETE /api/notes/:id` to proceed.
|
|
330
|
+
derivedNotes Note[] @relation("NoteSourceNote")
|
|
331
|
+
generations NoteGeneration[] @relation("NoteGenerations")
|
|
332
|
+
versions NoteVersion[]
|
|
333
|
+
exports NoteExport[]
|
|
334
|
+
|
|
335
|
+
// Connected Knowledge (epic #344, issue #351). `kgEvidence`/`kgProposals`
|
|
336
|
+
// are SetNull (a citation, and an extraction run's own source pointer,
|
|
337
|
+
// both survive losing the note they named — see the block comments above
|
|
338
|
+
// `KgEvidence`/`KgProposal`); `kgMentions` Cascades (see the block
|
|
339
|
+
// comment above `KgMention`).
|
|
340
|
+
kgEvidence KgEvidence[]
|
|
341
|
+
kgMentions KgMention[]
|
|
342
|
+
kgProposals KgProposal[]
|
|
343
|
+
|
|
344
|
+
// "This owner's notes, newest first" — the notes library list view.
|
|
345
|
+
@@index([ownerId, updatedAt(sort: Desc)])
|
|
346
|
+
// "Every note in state X" — `notes.housekeeping`'s scans (#53) and any
|
|
347
|
+
// future admin-adjacent health check, matching `transcripts_status_idx`.
|
|
348
|
+
@@index([status])
|
|
349
|
+
@@map("notes")
|
|
350
|
+
}
|
|
351
|
+
// -----------------------------------------------------------------------------
|
|
352
|
+
// `note_templates.owner_id IS NULL` means BUILT-IN — readable by every user,
|
|
353
|
+
// editable by nobody through the API (spec §7.1-7.2) — the same "null means
|
|
354
|
+
// something specific and permanent" convention `transcript_speakers.label`
|
|
355
|
+
// established for a different column entirely. There is deliberately NO
|
|
356
|
+
// `is_built_in` boolean (spec §4.3): a second, independently-settable flag
|
|
357
|
+
// that could disagree with `owner_id IS NULL` is exactly the failure mode
|
|
358
|
+
// `job-handler.interface.ts`'s "presence is the declaration" rule already
|
|
359
|
+
// forbids for node eligibility.
|
|
360
|
+
//
|
|
361
|
+
// `instructions` PLUS six structured fields, not "one field, not several
|
|
362
|
+
// structured knobs" (issue #48's actual, explicit column list —
|
|
363
|
+
// `outputFormat`, `structure`, `tone`, `length`, `model`, `isArchived` — all
|
|
364
|
+
// six). `instructions` stays the free-text prompt body the system role
|
|
365
|
+
// carries verbatim; the structured fields sit BESIDE it, not composed into
|
|
366
|
+
// it, because #56's template editor round-trips them as real controls (an
|
|
367
|
+
// Output format select, an editable ordered list for Structure, Tone and
|
|
368
|
+
// Length pickers, an optional Model override) — a user who edits a template
|
|
369
|
+
// and reopens it must get their structured choices back, which is impossible
|
|
370
|
+
// if they were only ever flattened into prose on save with nothing left to
|
|
371
|
+
// read them back out of. `assemblePrompt` (#49) reads all of `instructions`
|
|
372
|
+
// AND the structured fields when building a generation's prompt; neither
|
|
373
|
+
// replaces the other.
|
|
374
|
+
//
|
|
375
|
+
// `structure` is `Json @db.JsonB` — an ordered array of section/heading
|
|
376
|
+
// strings (e.g. `["Overview", "Key Points", "Decisions"]`), the shape #56's
|
|
377
|
+
// editable ordered list edits directly. `outputFormat` is required (every
|
|
378
|
+
// template, built-in or custom, names one); `tone`, `length` and `model` are
|
|
379
|
+
// optional — a template author may leave any of them unset and let
|
|
380
|
+
// `assemblePrompt` fall back to a deployment default. `isArchived` defaults
|
|
381
|
+
// to `false`: an archived custom template is hidden from the picker without
|
|
382
|
+
// being deleted (and without orphaning notes/generations that already
|
|
383
|
+
// reference it via `SetNull`, see below).
|
|
384
|
+
//
|
|
385
|
+
// `@@unique([ownerId, name])` (issue #48): a user may not have two templates
|
|
386
|
+
// sharing a name. PostgreSQL's standard NULLS-DISTINCT behaviour means this
|
|
387
|
+
// is silently a no-op among built-ins — every built-in's `ownerId` is
|
|
388
|
+
// permanently `NULL`, and NULL is never equal to NULL for uniqueness
|
|
389
|
+
// purposes, so any number of built-in rows may coexist regardless of name.
|
|
390
|
+
// The constraint only ever fires among a single owner's own rows.
|
|
391
|
+
// -----------------------------------------------------------------------------
|
|
392
|
+
model NoteTemplate {
|
|
393
|
+
id String @id @default(uuid()) @db.Uuid
|
|
394
|
+
ownerId String? @map("owner_id") @db.Uuid
|
|
395
|
+
|
|
396
|
+
name String
|
|
397
|
+
description String
|
|
398
|
+
// The prompt body the system role carries verbatim (spec §3.1), read
|
|
399
|
+
// together with the structured fields below by `assemblePrompt` (#49).
|
|
400
|
+
// @db.Text: free-text prompt instructions can run well past what a
|
|
401
|
+
// VARCHAR would comfortably hold.
|
|
402
|
+
instructions String @db.Text
|
|
403
|
+
|
|
404
|
+
// Structured fields #56's template editor presents as real controls, and
|
|
405
|
+
// reads back unchanged on reopen — see the block comment above the enums
|
|
406
|
+
// for why these six are stored beside `instructions` rather than composed
|
|
407
|
+
// into it.
|
|
408
|
+
outputFormat String @map("output_format")
|
|
409
|
+
// The markdown-vs-plain-text SYNTAX the AI is asked to write in — a
|
|
410
|
+
// separate axis from `outputFormat` above, which names the document TYPE
|
|
411
|
+
// (meeting notes, summary, …) rather than how its text is styled. Plain
|
|
412
|
+
// String, not an enum: same reasoning as `outputFormat` — validated by
|
|
413
|
+
// Zod in the API ('markdown' | 'plain_text'), not by a database enum.
|
|
414
|
+
bodyFormat String @default("markdown") @map("body_format")
|
|
415
|
+
// Ordered array of section/heading strings, e.g. `["Overview", "Key
|
|
416
|
+
// Points", "Decisions"]` — the shape #56's editable ordered list edits.
|
|
417
|
+
structure Json @db.JsonB
|
|
418
|
+
tone String?
|
|
419
|
+
length String?
|
|
420
|
+
// Optional per-template model override, populated in the web editor from
|
|
421
|
+
// `GET /api/ai/config`'s permitted list so a user can never select a
|
|
422
|
+
// model the deployment forbids. Plain string, not an enum/FK — same
|
|
423
|
+
// "code-owned, open registry" reasoning as `NoteGeneration.model`.
|
|
424
|
+
model String?
|
|
425
|
+
// Hides a custom template from the picker without deleting it (and
|
|
426
|
+
// without orphaning anything that already references it — `notes
|
|
427
|
+
// .templateId`/`note_generations.templateId` are both `SetNull`, so
|
|
428
|
+
// archiving never breaks a note or generation that used this template).
|
|
429
|
+
// Never meaningfully set on a built-in row today, but not schema-
|
|
430
|
+
// prevented from being so in the future.
|
|
431
|
+
isArchived Boolean @default(false) @map("is_archived")
|
|
432
|
+
|
|
433
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
434
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
435
|
+
|
|
436
|
+
// Cascade: a user's own custom template has no meaning once its owner is
|
|
437
|
+
// gone, the identical "belongs entirely to this account" shape
|
|
438
|
+
// `user_settings`/`personal_access_tokens` already use. Never fires for a
|
|
439
|
+
// built-in row, whose `owner_id` is permanently NULL.
|
|
440
|
+
owner User? @relation("NoteTemplateOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
441
|
+
|
|
442
|
+
notes Note[] @relation("NoteTemplateSelection")
|
|
443
|
+
generations NoteGeneration[] @relation("NoteGenerationTemplateSelection")
|
|
444
|
+
|
|
445
|
+
// Who has hidden THIS template from their own picker (#310, epic #306) —
|
|
446
|
+
// the built-in-safe complement to `isArchived` above. See the block
|
|
447
|
+
// comment above `UserHiddenNoteTemplate`.
|
|
448
|
+
hiddenBy UserHiddenNoteTemplate[]
|
|
449
|
+
|
|
450
|
+
// A user may not have two templates sharing a name — see the block
|
|
451
|
+
// comment above the enums for why PostgreSQL's NULLS-DISTINCT behaviour
|
|
452
|
+
// leaves built-ins (permanently null `ownerId`) entirely unaffected by
|
|
453
|
+
// this constraint.
|
|
454
|
+
@@unique([ownerId, name])
|
|
455
|
+
@@map("note_templates")
|
|
456
|
+
}
|
|
457
|
+
// =============================================================================
|
|
458
|
+
// `user_hidden_note_templates`: a per-user LISTING preference, never access
|
|
459
|
+
// control (#310, epic #306)
|
|
460
|
+
// =============================================================================
|
|
461
|
+
//
|
|
462
|
+
// A join table, deliberately, over the two alternatives this could have been:
|
|
463
|
+
//
|
|
464
|
+
// 1. A `user_settings` namespace (an array of hidden template ids in the
|
|
465
|
+
// caller's own JSONB blob). Rejected on three counts: a JSONB array has
|
|
466
|
+
// no foreign key, so a deleted template leaves a dangling id nothing ever
|
|
467
|
+
// cleans up — the exact failure mode `user_settings`'s existing namespaces
|
|
468
|
+
// (`notifications`, `onboarding`, ...) never need to worry about, because
|
|
469
|
+
// none of them names another table's row by id; "is this specific
|
|
470
|
+
// template hidden" and "list every template I've hidden" both become an
|
|
471
|
+
// application-level filter over a blob instead of a `WHERE` clause an
|
|
472
|
+
// index can serve; and there is no natural cap, so a user who hides
|
|
473
|
+
// hundreds of templates over time grows one JSONB column without bound.
|
|
474
|
+
// It would also cost the six-file parity `user-settings-parity.spec.ts`
|
|
475
|
+
// guards for every namespace, for a preference that isn't shaped like the
|
|
476
|
+
// rest of that document at all — it names rows in another table, not a
|
|
477
|
+
// scalar or a small enum a client renders as a toggle.
|
|
478
|
+
// 2. `note_templates.is_archived`, reused per-caller. Rejected outright:
|
|
479
|
+
// `is_archived` is a property of the TEMPLATE, shared by every user who
|
|
480
|
+
// can see it (a built-in row is everyone's row), so "archived" can only
|
|
481
|
+
// ever mean one thing for all viewers at once. Hiding is a property of
|
|
482
|
+
// the (user, template) PAIR — my hiding a built-in template from my own
|
|
483
|
+
// picker must never affect anyone else's.
|
|
484
|
+
//
|
|
485
|
+
// A join table gives real FK integrity in both directions (see the two
|
|
486
|
+
// `onDelete: Cascade`s below — hiding a template is meaningless once either
|
|
487
|
+
// side is gone), a plain indexed `WHERE user_id = ...` for the picker's
|
|
488
|
+
// exclusion query, no cap, and zero settings-parity cost.
|
|
489
|
+
//
|
|
490
|
+
// ⚠ HIDING IS A LISTING PREFERENCE ONLY, NEVER ACCESS CONTROL. A row here
|
|
491
|
+
// must never be consulted by `POST /api/notes` (create), `POST
|
|
492
|
+
// /api/notes/:id/regenerate`, or `POST /api/note-templates/preview` — all
|
|
493
|
+
// three continue to accept a hidden template by id exactly as before. This
|
|
494
|
+
// table only narrows what a picker LISTS, the identical scope
|
|
495
|
+
// `note_templates.is_archived` already keeps for the built-in-vs-custom
|
|
496
|
+
// axis: archiving/hiding decide what is offered, never what is honoured.
|
|
497
|
+
// =============================================================================
|
|
498
|
+
model UserHiddenNoteTemplate {
|
|
499
|
+
userId String @map("user_id") @db.Uuid
|
|
500
|
+
templateId String @map("template_id") @db.Uuid
|
|
501
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
502
|
+
|
|
503
|
+
// Both cascade: a hidden-template row has no meaning once either the user
|
|
504
|
+
// or the template it names is gone.
|
|
505
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
506
|
+
template NoteTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
|
507
|
+
|
|
508
|
+
@@id([userId, templateId])
|
|
509
|
+
@@index([templateId])
|
|
510
|
+
@@map("user_hidden_note_templates")
|
|
511
|
+
}
|
|
512
|
+
// -----------------------------------------------------------------------------
|
|
513
|
+
// `note_generations` is the durable buffer spec §5.1 streams from — see the
|
|
514
|
+
// block comment above the enums for why it denormalizes its inputs and why
|
|
515
|
+
// it deliberately carries no `job_id`.
|
|
516
|
+
// -----------------------------------------------------------------------------
|
|
517
|
+
model NoteGeneration {
|
|
518
|
+
id String @id @default(uuid()) @db.Uuid
|
|
519
|
+
|
|
520
|
+
// NULL for a template preview (spec §4.4) — a preview has no note to
|
|
521
|
+
// belong to and is hard-deleted by its own `expiresAt` sweep, never a
|
|
522
|
+
// `note_versions` row.
|
|
523
|
+
noteId String? @map("note_id") @db.Uuid
|
|
524
|
+
kind NoteGenerationKind
|
|
525
|
+
|
|
526
|
+
status NoteGenerationStatus @default(pending)
|
|
527
|
+
errorClass NoteGenerationErrorClass? @map("error_class")
|
|
528
|
+
errorDetail String? @map("error_detail")
|
|
529
|
+
|
|
530
|
+
// Denormalized inputs — see the block comment above the enums. templateId
|
|
531
|
+
// is nullable/SetNull for the same reason `notes.templateId` is (a
|
|
532
|
+
// deleted template loses only the reference); templateNameSnapshot is
|
|
533
|
+
// NOT NULL because a generation is never created without a template
|
|
534
|
+
// having been resolved first, and the snapshot is what survives the
|
|
535
|
+
// template's own later deletion.
|
|
536
|
+
templateId String? @map("template_id") @db.Uuid
|
|
537
|
+
templateNameSnapshot String @map("template_name_snapshot")
|
|
538
|
+
contextText String? @map("context_text")
|
|
539
|
+
sourceType NoteSourceType @map("source_type")
|
|
540
|
+
sourceTranscriptId String? @map("source_transcript_id") @db.Uuid
|
|
541
|
+
sourceNoteId String? @map("source_note_id") @db.Uuid
|
|
542
|
+
sourceObjectId String? @map("source_object_id") @db.Uuid
|
|
543
|
+
// Which registered `AiProvider` (issue #47) this generation was submitted
|
|
544
|
+
// to, and which of its models. Plain strings, not an enum/FK — the
|
|
545
|
+
// provider registry is code-owned and open, the same `Transcript.provider`
|
|
546
|
+
// reasoning applied to a second, independent registry.
|
|
547
|
+
providerId String @map("provider_id")
|
|
548
|
+
model String
|
|
549
|
+
|
|
550
|
+
// The stream itself (spec §5.1). Append-only, flushed on a short interval
|
|
551
|
+
// together with `lastEventId` in the SAME UPDATE, so the SSE `id:` sequence
|
|
552
|
+
// is gapless and stable by construction — there is no separate counter
|
|
553
|
+
// that could disagree with what actually landed in `content`.
|
|
554
|
+
content String @default("") @db.Text
|
|
555
|
+
lastEventId Int @default(0) @map("last_event_id")
|
|
556
|
+
|
|
557
|
+
promptTokens Int? @map("prompt_tokens")
|
|
558
|
+
completionTokens Int? @map("completion_tokens")
|
|
559
|
+
|
|
560
|
+
// A snapshot of WHAT WAS SENT to the AI provider — the exact
|
|
561
|
+
// `assemblePrompt()` output (apps/api/src/notes/generation/prompt.ts) —
|
|
562
|
+
// written BEFORE the provider call so a failed generation still records
|
|
563
|
+
// it (issue #307). `sourceVersion` is the transcript `currentVersion` /
|
|
564
|
+
// source note `currentVersion` actually materialized into the prompt;
|
|
565
|
+
// NULL for a document source, which carries no version. NULL across all
|
|
566
|
+
// four columns means the generation predates issue #307 — deliberately
|
|
567
|
+
// no backfill, because re-materializing the source today would fabricate
|
|
568
|
+
// history: the source transcript/note may have been corrected since, and
|
|
569
|
+
// the template may have been edited since.
|
|
570
|
+
systemPrompt String? @map("system_prompt") @db.Text
|
|
571
|
+
userContent String? @map("user_content") @db.Text
|
|
572
|
+
sourceVersion Int? @map("source_version")
|
|
573
|
+
contextCapturedAt DateTime? @map("context_captured_at") @db.Timestamptz
|
|
574
|
+
|
|
575
|
+
// The `note.generate` job that produced (or is producing) this row, when
|
|
576
|
+
// one exists. `@unique`/nullable/`SetNull`, mirroring `DatabaseBackupRun
|
|
577
|
+
// .jobId` and `TranscriptExport.jobId` (issue #48): this row's own
|
|
578
|
+
// lifetime — read by the SSE stream and the note detail page for as long
|
|
579
|
+
// as generation is in flight, and kept afterward as the AI-generated
|
|
580
|
+
// version's provenance — is independent of `job.history.purge`'s
|
|
581
|
+
// retention schedule for the underlying `jobs` row. See the block comment
|
|
582
|
+
// above the enums for the full reasoning.
|
|
583
|
+
jobId String? @unique @map("job_id") @db.Uuid
|
|
584
|
+
|
|
585
|
+
// Only ever set for `kind: 'preview'` — what `notes.housekeeping`'s TTL
|
|
586
|
+
// sweep hard-deletes past (spec §4.4). A `create`/`regenerate` generation
|
|
587
|
+
// has no expiry of its own; it lives as long as its `note_versions` row
|
|
588
|
+
// needs it for provenance.
|
|
589
|
+
expiresAt DateTime? @map("expires_at") @db.Timestamptz
|
|
590
|
+
startedAt DateTime? @map("started_at") @db.Timestamptz
|
|
591
|
+
completedAt DateTime? @map("completed_at") @db.Timestamptz
|
|
592
|
+
// NOTE: deliberately no `updatedAt` here, matching docs/specs/notes.md
|
|
593
|
+
// §4.4's Lifecycle group exactly — this row is already flushed on a
|
|
594
|
+
// 250ms interval while streaming (spec §5.1); a second auto-maintained
|
|
595
|
+
// timestamp on the same hot write path would cost index/row-version
|
|
596
|
+
// churn spec §4.4 does not ask for and nothing downstream reads.
|
|
597
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
598
|
+
|
|
599
|
+
note Note? @relation("NoteGenerations", fields: [noteId], references: [id], onDelete: Cascade)
|
|
600
|
+
template NoteTemplate? @relation("NoteGenerationTemplateSelection", fields: [templateId], references: [id], onDelete: SetNull)
|
|
601
|
+
job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
|
|
602
|
+
|
|
603
|
+
// The inverse of `Note.currentGenerationId` above — structurally a list
|
|
604
|
+
// because the FK carries no `@unique`, even though at most one note will
|
|
605
|
+
// ever actually point here in practice (the note this generation belongs
|
|
606
|
+
// to, if any).
|
|
607
|
+
currentForNotes Note[] @relation("NoteCurrentGeneration")
|
|
608
|
+
versions NoteVersion[] @relation("NoteVersionGeneration")
|
|
609
|
+
|
|
610
|
+
@@map("note_generations")
|
|
611
|
+
}
|
|
612
|
+
// -----------------------------------------------------------------------------
|
|
613
|
+
// `note_versions` mirrors `transcript_versions` in spirit only — see the
|
|
614
|
+
// block comment above the enums for why a full body snapshot replaces the op
|
|
615
|
+
// log entirely rather than reusing it.
|
|
616
|
+
// -----------------------------------------------------------------------------
|
|
617
|
+
model NoteVersion {
|
|
618
|
+
id String @id @default(uuid()) @db.Uuid
|
|
619
|
+
noteId String @map("note_id") @db.Uuid
|
|
620
|
+
version Int
|
|
621
|
+
kind NoteVersionKind
|
|
622
|
+
|
|
623
|
+
// The full markdown, as stored — see the block comment above the enums.
|
|
624
|
+
body String @db.Text
|
|
625
|
+
|
|
626
|
+
// The syntax THIS version's body is written in ('markdown'|'plain_text',
|
|
627
|
+
// validated by Zod in the API, not by a database enum) — issue #337.
|
|
628
|
+
// Immutable once written: the source of truth for rendering/exporting
|
|
629
|
+
// that version, not a live read of the note's or template's current
|
|
630
|
+
// format. Restoring a version brings its format back onto the note,
|
|
631
|
+
// exactly as it brings the body back.
|
|
632
|
+
bodyFormat String @default("markdown") @map("body_format")
|
|
633
|
+
|
|
634
|
+
// A short, optional human- or AI-authored description of what changed in
|
|
635
|
+
// this version (issue #48) — e.g. "fixed the attendee list" for an
|
|
636
|
+
// `edit`, or left unset for most `ai_generated`/`restore` rows. Purely a
|
|
637
|
+
// display convenience for the version history list (#53); nothing in this
|
|
638
|
+
// schema derives it and nothing requires it to be set.
|
|
639
|
+
summary String?
|
|
640
|
+
|
|
641
|
+
// NULL means "the AI" — the identical convention `transcript_versions
|
|
642
|
+
// .authorId` already establishes, not a missing value. SetNull: a version
|
|
643
|
+
// is permanent history and must survive its human author's own account
|
|
644
|
+
// being deleted.
|
|
645
|
+
authorId String? @map("author_id") @db.Uuid
|
|
646
|
+
|
|
647
|
+
// Which generation produced this version, for `kind: 'ai_generated'`; NULL
|
|
648
|
+
// for `edit`/`restore`. SetNull for the identical reason `authorId` is —
|
|
649
|
+
// the version's `body` already carries everything durable; losing the
|
|
650
|
+
// pointer back to the generation that produced it loses only "which
|
|
651
|
+
// streaming run wrote this," never the content itself.
|
|
652
|
+
generationId String? @map("generation_id") @db.Uuid
|
|
653
|
+
|
|
654
|
+
// The version number this row restored FROM, when `kind: 'restore'` — a
|
|
655
|
+
// plain version number, not a self-relation FK, matching `transcripts
|
|
656
|
+
// .restoredFromVersion` exactly and for the identical reason: it names
|
|
657
|
+
// another row's `(noteId, version)` pair, not a column any FK can target.
|
|
658
|
+
restoredFromVersion Int? @map("restored_from_version")
|
|
659
|
+
// Idempotency key for the save endpoint (#53) — the same purpose as
|
|
660
|
+
// `transcript_versions.clientBatchId`, and the same free NULLS-DISTINCT
|
|
661
|
+
// behaviour below rather than a hand-written partial index.
|
|
662
|
+
clientBatchId String? @map("client_batch_id")
|
|
663
|
+
|
|
664
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
665
|
+
|
|
666
|
+
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
|
667
|
+
author User? @relation("NoteVersionAuthor", fields: [authorId], references: [id], onDelete: SetNull)
|
|
668
|
+
generation NoteGeneration? @relation("NoteVersionGeneration", fields: [generationId], references: [id], onDelete: SetNull)
|
|
669
|
+
|
|
670
|
+
// The version sequence itself.
|
|
671
|
+
@@unique([noteId, version])
|
|
672
|
+
// The idempotent-retry key. Plain `@@unique`, not hand-written: Postgres's
|
|
673
|
+
// standard NULLS DISTINCT behaviour already gives the wanted semantics for
|
|
674
|
+
// this nullable column (any number of null-`clientBatchId` rows coexist).
|
|
675
|
+
@@unique([noteId, clientBatchId])
|
|
676
|
+
@@map("note_versions")
|
|
677
|
+
}
|
|
678
|
+
// -----------------------------------------------------------------------------
|
|
679
|
+
// `note_exports` mirrors `transcript_exports` FIELD FOR FIELD — see the block
|
|
680
|
+
// comment above the enums for the one place ("@@index", not "@@unique") this
|
|
681
|
+
// migration follows that table's real, already-shipped shape over spec
|
|
682
|
+
// §4.6's own shorthand prose.
|
|
683
|
+
// -----------------------------------------------------------------------------
|
|
684
|
+
model NoteExport {
|
|
685
|
+
id String @id @default(uuid()) @db.Uuid
|
|
686
|
+
noteId String @map("note_id") @db.Uuid
|
|
687
|
+
version Int
|
|
688
|
+
|
|
689
|
+
// The exporter's own registry key (`markdown`, `pdf`, `docx` — spec §8.1,
|
|
690
|
+
// §8.3). Plain string, not an enum, for the same self-registering-registry
|
|
691
|
+
// reason `TranscriptExport.format` is: adding an exporter must cost zero
|
|
692
|
+
// migrations to this table.
|
|
693
|
+
format String
|
|
694
|
+
options Json @db.JsonB
|
|
695
|
+
optionsHash String @map("options_hash")
|
|
696
|
+
status NoteExportStatus @default(pending)
|
|
697
|
+
|
|
698
|
+
// Restrict, matching every `storage_objects` FK on `transcripts`: only
|
|
699
|
+
// `notes.housekeeping`'s expiry sweep (spec §8.4) may remove the rendered
|
|
700
|
+
// file, and only after deleting this row first.
|
|
701
|
+
objectId String? @map("object_id") @db.Uuid
|
|
702
|
+
// The `note.export` job that rendered this row, when one did.
|
|
703
|
+
// `@unique`/SetNull, mirroring `TranscriptExport.jobId` exactly: a second
|
|
704
|
+
// render for the SAME job is unrepresentable, and this row's own 7-day
|
|
705
|
+
// retention is independent of `job.history.purge`'s schedule for the
|
|
706
|
+
// underlying `jobs` row.
|
|
707
|
+
jobId String? @unique @map("job_id") @db.Uuid
|
|
708
|
+
|
|
709
|
+
// Who asked for this export. NOT NULL and Cascade, matching
|
|
710
|
+
// `TranscriptExport.requestedById`: an export is a disposable,
|
|
711
|
+
// byte-for-byte reproducible artifact with its own 7-day expiry already
|
|
712
|
+
// built in — nothing is lost by deleting a pending/ready row alongside the
|
|
713
|
+
// account that requested it.
|
|
714
|
+
requestedById String @map("requested_by_id") @db.Uuid
|
|
715
|
+
|
|
716
|
+
error String?
|
|
717
|
+
expiresAt DateTime @map("expires_at") @db.Timestamptz
|
|
718
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
719
|
+
|
|
720
|
+
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
|
721
|
+
object StorageObject? @relation("NoteExportObject", fields: [objectId], references: [id], onDelete: Restrict)
|
|
722
|
+
job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
|
|
723
|
+
requestedBy User @relation("NoteExportRequestedBy", fields: [requestedById], references: [id], onDelete: Cascade)
|
|
724
|
+
|
|
725
|
+
// The reuse-by-content-hash lookup (spec §8.4) — deliberately a plain,
|
|
726
|
+
// NON-unique index. See the block comment above the enums for why this
|
|
727
|
+
// migration follows `transcript_exports_lookup_idx`'s real shape rather
|
|
728
|
+
// than spec §4.6's shorthand "unique" wording: an expired or failed row
|
|
729
|
+
// may briefly coexist with a fresh one sharing the same key before
|
|
730
|
+
// housekeeping removes the stale one.
|
|
731
|
+
//
|
|
732
|
+
// `map:` pins an explicit name for the identical reason
|
|
733
|
+
// `transcript_exports_lookup_idx` needed one: Prisma's own default name
|
|
734
|
+
// for this four-column index is one byte over PostgreSQL's 63-byte
|
|
735
|
+
// identifier limit.
|
|
736
|
+
@@index([noteId, version, format, optionsHash], map: "note_exports_lookup_idx")
|
|
737
|
+
@@map("note_exports")
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Back-relations onto the platform's User (an `extend model` block holds back-relation fields only).
|
|
741
|
+
extend model User {
|
|
742
|
+
// Notes (issue #48, epic #45). Four relations to the note tables, split by
|
|
743
|
+
// role for the identical reason the six transcript relations above are:
|
|
744
|
+
// "owns this note", "owns this custom template", "wrote this version" and
|
|
745
|
+
// "asked for this export" are four different acts. See the block comment
|
|
746
|
+
// above the `Note` model for the full reasoning behind each `onDelete`.
|
|
747
|
+
notes Note[] @relation("NoteOwner")
|
|
748
|
+
noteTemplatesOwned NoteTemplate[] @relation("NoteTemplateOwner")
|
|
749
|
+
noteVersionsAuthored NoteVersion[] @relation("NoteVersionAuthor")
|
|
750
|
+
noteExportsRequested NoteExport[] @relation("NoteExportRequestedBy")
|
|
751
|
+
|
|
752
|
+
// Per-user "hide this template from my picker" preference (#310, epic
|
|
753
|
+
// #306). CASCADES — the preference is meaningless once its owner is gone.
|
|
754
|
+
// See the block comment above `UserHiddenNoteTemplate` for why this is a
|
|
755
|
+
// join table rather than a `user_settings` namespace.
|
|
756
|
+
hiddenNoteTemplates UserHiddenNoteTemplate[]
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Back-relations onto the platform's StorageObject (an `extend model` block holds back-relation fields only).
|
|
760
|
+
extend model StorageObject {
|
|
761
|
+
// Two back-relations from notes (issue #48, epic #45), the identical
|
|
762
|
+
// Restrict-FK reasoning as the five transcript ones above: only
|
|
763
|
+
// `note.purge`/`notes.housekeeping` deleting the referencing row first may
|
|
764
|
+
// ever remove one of these objects.
|
|
765
|
+
notesAsSourceObject Note[] @relation("NoteSourceObject")
|
|
766
|
+
noteExportFiles NoteExport[] @relation("NoteExportObject")
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Back-relations onto the platform's Job (an `extend model` block holds back-relation fields only).
|
|
770
|
+
extend model Job {
|
|
771
|
+
// The `note_exports` row this job rendered, when `type` is `'note.export'`
|
|
772
|
+
// (issue #48, epic #45). Identical one-to-one shape as `transcriptExport`
|
|
773
|
+
// immediately above, for the identical reason: `NoteExport.jobId` is
|
|
774
|
+
// `@unique`/nullable/`SetNull` because export rows have their own 7-day
|
|
775
|
+
// retention (`notes.housekeeping`), independent of `job.history.purge`'s
|
|
776
|
+
// schedule for this row.
|
|
777
|
+
noteExport NoteExport?
|
|
778
|
+
|
|
779
|
+
// The `note_generations` row this job is producing (or produced), when
|
|
780
|
+
// `type` is `'note.generate'` (issue #48, epic #45). Same one-to-one
|
|
781
|
+
// shape as `noteExport`/`transcriptExport`/`backupRun` above, for the
|
|
782
|
+
// same reason: `NoteGeneration.jobId` is `@unique`/nullable/`SetNull`
|
|
783
|
+
// because a generation's own lifetime (read by the SSE stream and the
|
|
784
|
+
// note detail page) is independent of `job.history.purge`'s schedule for
|
|
785
|
+
// this row.
|
|
786
|
+
noteGeneration NoteGeneration?
|
|
787
|
+
}
|