@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.
Files changed (245) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/dist/ask/index.d.ts +6 -0
  5. package/dist/ask/index.js +28 -0
  6. package/dist/ask/question-schema.d.ts +121 -0
  7. package/dist/ask/question-schema.js +167 -0
  8. package/dist/ask/report.d.ts +88 -0
  9. package/dist/ask/report.js +170 -0
  10. package/dist/ask/scorer.d.ts +104 -0
  11. package/dist/ask/scorer.js +164 -0
  12. package/dist/ask-run/ask-command.d.ts +62 -0
  13. package/dist/ask-run/ask-command.js +254 -0
  14. package/dist/ask-run/host/app.d.ts +65 -0
  15. package/dist/ask-run/host/app.js +146 -0
  16. package/dist/ask-run/host/db.d.ts +38 -0
  17. package/dist/ask-run/host/db.js +126 -0
  18. package/dist/ask-run/host/ports.d.ts +35 -0
  19. package/dist/ask-run/host/ports.js +113 -0
  20. package/dist/ask-run/index.d.ts +13 -0
  21. package/dist/ask-run/index.js +35 -0
  22. package/dist/ask-run/replay.d.ts +128 -0
  23. package/dist/ask-run/replay.js +212 -0
  24. package/dist/ask-run/run-ask-eval.d.ts +125 -0
  25. package/dist/ask-run/run-ask-eval.js +193 -0
  26. package/dist/ask-run/seed-golden-graph.d.ts +64 -0
  27. package/dist/ask-run/seed-golden-graph.js +495 -0
  28. package/dist/cli/index.d.ts +3 -0
  29. package/dist/cli/index.js +12 -0
  30. package/dist/cli/kg-command.d.ts +66 -0
  31. package/dist/cli/kg-command.js +238 -0
  32. package/dist/cli/main.d.ts +18 -0
  33. package/dist/cli/main.js +69 -0
  34. package/dist/fixtures/domains.d.ts +16 -0
  35. package/dist/fixtures/domains.js +33 -0
  36. package/dist/fixtures/fixture-schema.d.ts +323 -0
  37. package/dist/fixtures/fixture-schema.js +199 -0
  38. package/dist/fixtures/index.d.ts +16 -0
  39. package/dist/fixtures/index.js +40 -0
  40. package/dist/fixtures/load.d.ts +73 -0
  41. package/dist/fixtures/load.js +137 -0
  42. package/dist/fixtures/questions.d.ts +15 -0
  43. package/dist/fixtures/questions.js +26 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +36 -0
  46. package/dist/kg/gold-runner.d.ts +13 -0
  47. package/dist/kg/gold-runner.js +57 -0
  48. package/dist/kg/index.d.ts +17 -0
  49. package/dist/kg/index.js +65 -0
  50. package/dist/kg/internals.d.ts +7 -0
  51. package/dist/kg/internals.js +17 -0
  52. package/dist/kg/prediction-schema.d.ts +245 -0
  53. package/dist/kg/prediction-schema.js +102 -0
  54. package/dist/kg/predictions.d.ts +11 -0
  55. package/dist/kg/predictions.js +49 -0
  56. package/dist/kg/report.d.ts +84 -0
  57. package/dist/kg/report.js +125 -0
  58. package/dist/kg/run-kg-eval.d.ts +77 -0
  59. package/dist/kg/run-kg-eval.js +137 -0
  60. package/dist/kg/runner.d.ts +72 -0
  61. package/dist/kg/runner.js +51 -0
  62. package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
  63. package/dist/kg/runners/extract-resolve-runner.js +199 -0
  64. package/dist/kg/runners/extract-runner.d.ts +65 -0
  65. package/dist/kg/runners/extract-runner.js +243 -0
  66. package/dist/kg/scorer.d.ts +226 -0
  67. package/dist/kg/scorer.js +463 -0
  68. package/dist/kg/targets.d.ts +17 -0
  69. package/dist/kg/targets.js +16 -0
  70. package/dist/kg/text.d.ts +45 -0
  71. package/dist/kg/text.js +100 -0
  72. package/dist/live/adapters.d.ts +16 -0
  73. package/dist/live/adapters.js +58 -0
  74. package/dist/live/env.d.ts +39 -0
  75. package/dist/live/env.js +56 -0
  76. package/dist/live/index.d.ts +6 -0
  77. package/dist/live/index.js +16 -0
  78. package/dist/live/live-ai.d.ts +104 -0
  79. package/dist/live/live-ai.js +157 -0
  80. package/dist/parity/constants.d.ts +79 -0
  81. package/dist/parity/constants.js +104 -0
  82. package/dist/parity/index.d.ts +20 -0
  83. package/dist/parity/index.js +38 -0
  84. package/dist/parity/internals.d.ts +8 -0
  85. package/dist/parity/internals.js +16 -0
  86. package/dist/parity/scenario.d.ts +45 -0
  87. package/dist/parity/scenario.js +245 -0
  88. package/dist/parity/script.d.ts +44 -0
  89. package/dist/parity/script.js +136 -0
  90. package/dist/parity/snapshot.d.ts +43 -0
  91. package/dist/parity/snapshot.js +71 -0
  92. package/dist/real-data/index.d.ts +2 -0
  93. package/dist/real-data/index.js +12 -0
  94. package/dist/real-data/real-data.d.ts +76 -0
  95. package/dist/real-data/real-data.js +156 -0
  96. package/fixtures/kg-golden/README.md +136 -0
  97. package/fixtures/kg-golden/ask-questions.json +458 -0
  98. package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
  99. package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
  100. package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
  101. package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
  102. package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
  103. package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
  104. package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
  105. package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
  106. package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
  107. package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
  108. package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
  109. package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
  110. package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
  111. package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
  112. package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
  113. package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
  114. package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
  115. package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
  116. package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
  117. package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
  118. package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
  119. package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
  120. package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
  121. package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
  122. package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
  123. package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
  124. package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
  125. package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
  126. package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
  127. package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
  128. package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
  129. package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
  130. package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
  131. package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
  132. package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
  133. package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
  134. package/host/generated/client.d.ts +1 -0
  135. package/host/generated/client.js +5 -0
  136. package/host/generated/default.d.ts +1 -0
  137. package/host/generated/default.js +5 -0
  138. package/host/generated/edge.d.ts +1 -0
  139. package/host/generated/edge.js +1658 -0
  140. package/host/generated/index-browser.js +1684 -0
  141. package/host/generated/index.d.ts +192335 -0
  142. package/host/generated/index.js +1658 -0
  143. package/host/generated/package.json +139 -0
  144. package/host/generated/query_compiler_fast_bg.js +2 -0
  145. package/host/generated/query_compiler_fast_bg.wasm +0 -0
  146. package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
  147. package/host/generated/runtime/client.d.ts +3433 -0
  148. package/host/generated/runtime/client.js +110 -0
  149. package/host/generated/runtime/index-browser.d.ts +90 -0
  150. package/host/generated/runtime/index-browser.js +6 -0
  151. package/host/generated/runtime/wasm-compiler-edge.js +100 -0
  152. package/host/generated/schema.prisma +6312 -0
  153. package/host/generated/wasm-edge-light-loader.mjs +5 -0
  154. package/host/generated/wasm-worker-loader.mjs +5 -0
  155. package/host/prisma/fragments/kvox.ask.prisma +126 -0
  156. package/host/prisma/fragments/kvox.graph.prisma +1073 -0
  157. package/host/prisma/fragments/kvox.notes.prisma +787 -0
  158. package/host/prisma/fragments/kvox.search.prisma +211 -0
  159. package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
  160. package/host/prisma/kvox.lock +101 -0
  161. package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
  162. package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
  163. package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
  164. package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
  165. package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
  166. package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
  167. package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
  168. package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
  169. package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
  170. package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
  171. package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
  172. package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
  173. package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
  174. package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
  175. package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
  176. package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
  177. package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
  178. package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
  179. package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
  180. package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
  181. package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
  182. package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
  183. package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
  184. package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
  185. package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
  186. package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
  187. package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
  188. package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
  189. package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
  190. package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
  191. package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
  192. package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
  193. package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
  194. package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
  195. package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
  196. package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
  197. package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
  198. package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
  199. package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
  200. package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
  201. package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
  202. package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
  203. package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
  204. package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
  205. package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
  206. package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
  207. package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
  208. package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
  209. package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
  210. package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
  211. package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
  212. package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
  213. package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
  214. package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
  215. package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
  216. package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
  217. package/host/prisma/platform.lock +319 -0
  218. package/host/prisma/schema/app.kvox.ask.prisma +110 -0
  219. package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
  220. package/host/prisma/schema/app.kvox.notes.prisma +746 -0
  221. package/host/prisma/schema/app.kvox.search.prisma +208 -0
  222. package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
  223. package/host/prisma/schema/platform.ai.prisma +419 -0
  224. package/host/prisma/schema/platform.android-app.prisma +46 -0
  225. package/host/prisma/schema/platform.base.prisma +15 -0
  226. package/host/prisma/schema/platform.credentials.prisma +156 -0
  227. package/host/prisma/schema/platform.db-backup.prisma +359 -0
  228. package/host/prisma/schema/platform.identity.prisma +638 -0
  229. package/host/prisma/schema/platform.jobs.prisma +824 -0
  230. package/host/prisma/schema/platform.notifications.prisma +641 -0
  231. package/host/prisma/schema/platform.settings.prisma +53 -0
  232. package/host/prisma/schema/platform.sharing.prisma +208 -0
  233. package/host/prisma/schema/platform.storage.prisma +118 -0
  234. package/package.json +112 -1
  235. package/parity/requests.ask.json +2681 -0
  236. package/parity/requests.m01.json +1554 -0
  237. package/parity/requests.m02.json +1554 -0
  238. package/parity/requests.m03.json +1554 -0
  239. package/parity/requests.m04.json +1554 -0
  240. package/parity/requests.m05.json +1554 -0
  241. package/parity/requests.m06.json +1554 -0
  242. package/parity/requests.m07.json +1554 -0
  243. package/parity/requests.m08.json +1554 -0
  244. package/parity/requests.m09.json +1554 -0
  245. 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
+ }