@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,362 @@
1
+ -- =============================================================================
2
+ -- Semantic search: pgvector storage for chunk embeddings (issue #181,
3
+ -- epic #165)
4
+ -- =============================================================================
5
+ --
6
+ -- Layer 1 (epic #164, the sibling migration `20260915120000_add_search_vectors`)
7
+ -- gave this application literal, lexical search over transcript/note content
8
+ -- via generated `tsvector` columns. This migration is layer 2: MEANING-based
9
+ -- search over the same content, via vectors an AI embedding model produces
10
+ -- and pgvector indexes for nearest-neighbour lookup. Nothing here replaces
11
+ -- layer 1 — a real search feature runs both and blends the results — and
12
+ -- nothing here runs any embedding model itself; this migration is storage
13
+ -- only. The pipeline that populates it (chunking, calling a provider,
14
+ -- writing rows here) is later issues in epic #165.
15
+ --
16
+ -- -----------------------------------------------------------------------------
17
+ -- `CREATE EXTENSION IF NOT EXISTS vector` IS REFUSED LOUDLY, NOT SILENTLY,
18
+ -- LONG BEFORE THIS MIGRATION EVER RUNS.
19
+ -- -----------------------------------------------------------------------------
20
+ --
21
+ -- This statement requires the `vector` extension's shared library to be
22
+ -- present on the PostgreSQL SERVER — not something any migration tool can
23
+ -- install by itself, and not something every managed-Postgres offering ships
24
+ -- by default. Issue #179 puts the check where a failure is actionable: the
25
+ -- deploy CLI's `database-vector-extension` preflight probes the target server
26
+ -- BEFORE any deploy proceeds, and refuses the deploy outright with a clear
27
+ -- operator-facing message if `vector` cannot be provided. By the time this
28
+ -- migration runs, that question has already been answered — this
29
+ -- `CREATE EXTENSION IF NOT EXISTS` is a re-affirmation of a fact the
30
+ -- preflight already established, not a probe of its own, and `IF NOT EXISTS`
31
+ -- only guards against re-running this migration idempotently, never against
32
+ -- the extension genuinely being unavailable.
33
+ --
34
+ -- THE REJECTED ALTERNATIVE: wrapping this statement in a `DO $$ ... $$` block
35
+ -- that catches the failure and skips creating the extension (and, by
36
+ -- extension, every table below that depends on it) when `vector` is absent.
37
+ -- That would trade one loud, pre-deploy refusal for permanent, silent,
38
+ -- PER-DEPLOYMENT schema drift: some installations would have semantic search
39
+ -- storage and some would not, indistinguishably, with nothing in the schema
40
+ -- itself recording which — and every query this feature ever issues would
41
+ -- have to probe `pg_extension` at runtime, forever, to find out which world
42
+ -- it is running in. A deployment that cannot provide `vector` should never
43
+ -- reach this migration in the first place; #179's preflight is what makes
44
+ -- that true, and this migration is written on the assumption that it did its
45
+ -- job.
46
+ --
47
+ -- -----------------------------------------------------------------------------
48
+ -- THREE TABLES
49
+ -- -----------------------------------------------------------------------------
50
+ --
51
+ -- `search_chunks` — one row per chunk of a document's text, independent of
52
+ -- any particular embedding model. `search_embeddings` — one row per
53
+ -- (chunk, model) vector, so multiple models (or a re-embed under a changed
54
+ -- model) can coexist without destroying history. `search_index_state` — one
55
+ -- row per DOCUMENT (not chunk), the legibility surface an admin/owner reads
56
+ -- to answer "is this searchable, and if not, why not".
57
+ --
58
+ -- -----------------------------------------------------------------------------
59
+ -- 1. `vector(1536)` IS A CONTRACT, NOT A DEFAULT.
60
+ -- -----------------------------------------------------------------------------
61
+ --
62
+ -- Postgres's `vector(n)` type is FIXED-WIDTH: a value produced by a model
63
+ -- with a different output dimensionality cannot be stored in this column at
64
+ -- all — not truncated, not padded, rejected outright at INSERT time. 1536 is
65
+ -- not picked because it is a good default; it is picked because it is the
66
+ -- width this deployment's embedding provider is configured to produce, and
67
+ -- the column is exactly as wide as that fact, no wider. `AiProvider`
68
+ -- (`src/ai/providers/ai-provider.interface.ts`) grows a sibling
69
+ -- `EMBEDDING_DIMENSIONS` constant as part of this same issue's line of work,
70
+ -- and `AiProviderRegistry` refuses to activate a provider whose embedding
71
+ -- model's width disagrees with it — AT APPLICATION BOOT, where an operator
72
+ -- reads a clear startup error, not inside a `search.embed` job at 3am where
73
+ -- the failure is a stack trace in a queue row nobody is watching. Say this
74
+ -- plainly: THIS COLUMN AND THAT CONSTANT ARE THE TWO HALVES OF ONE CONTRACT
75
+ -- and must always change together. Changing one without the other is not two
76
+ -- smaller changes, it is one change done incompletely — the database will
77
+ -- either reject every write (constant raised, column not) or silently accept
78
+ -- a narrower vector padded/misread by nothing in particular (this cannot
79
+ -- actually happen — pgvector rejects a width mismatch — but the point
80
+ -- stands: there is no partial, working state between them).
81
+ --
82
+ -- `search_embeddings.dimensions` is stored ANYWAY, despite the column type
83
+ -- already enforcing the width — not as a second constraint (Postgres already
84
+ -- enforces the real one) but as a RECORDED FACT about the row, independent of
85
+ -- whatever the column's declared width happens to be on the day the row is
86
+ -- read. The day a future issue widens this contract (a better model, a
87
+ -- migration to `vector(3072)` or similar), old rows and new rows will briefly
88
+ -- coexist under two different embedding models, and `dimensions` is what lets
89
+ -- a query or a backfill script tell them apart without re-deriving the width
90
+ -- from `octet_length`/array introspection every time.
91
+ --
92
+ -- -----------------------------------------------------------------------------
93
+ -- 2. `search_chunks.document_id` CARRIES NO FOREIGN KEY, IN EITHER DIRECTION —
94
+ -- THE SAME PRECEDENT `jobs.subject_type`/`subject_id` ALREADY SET.
95
+ -- -----------------------------------------------------------------------------
96
+ --
97
+ -- `document_type` (`'transcript'` | `'note'`) plus `document_id` is a
98
+ -- polymorphic reference, exactly the shape `jobs.subject_type`/`subject_id`
99
+ -- already uses in this schema (see the block comment above the `Job` model)
100
+ -- and for the identical reason: the set of document types this table indexes
101
+ -- is open-ended and code-owned, not a fixed pair of foreign keys Prisma could
102
+ -- express as two nullable relations. A fork adding a third searchable
103
+ -- document type costs this table zero migrations.
104
+ --
105
+ -- THE COST, STATED PLAINLY: an orphan row is POSSIBLE. Nothing at the
106
+ -- database level stops a `search_chunks` row from outliving the transcript
107
+ -- or note it was chunked from, the way a real foreign key would. THE
108
+ -- PROMISE, STATED EQUALLY PLAINLY: sweeping those rows is not a gap left for
109
+ -- someone to discover later, it is a responsibility handed to a named owner —
110
+ -- the deleting document's OWN purge handler. `transcript.purge` and
111
+ -- `note.purge` (both already existing, both already the sole path that
112
+ -- permanently removes a transcript or note) are wired by a later issue in
113
+ -- this epic to also delete their document's `search_chunks` rows (which
114
+ -- cascade to `search_embeddings` — see below) and its `search_index_state`
115
+ -- row, in the same job that already owns deleting everything else that
116
+ -- document's identity touches. This migration does not implement that sweep;
117
+ -- it states, here, that the sweep is owed and by whom.
118
+ --
119
+ -- -----------------------------------------------------------------------------
120
+ -- 3. HNSW, NOT IVFFLAT — CHOSEN FOR HOW EACH INDEX TYPE BEHAVES AGAINST A
121
+ -- TABLE THAT STARTS EMPTY.
122
+ -- -----------------------------------------------------------------------------
123
+ --
124
+ -- IVFFlat partitions the vector space into a fixed number of lists chosen at
125
+ -- BUILD time from the data the index is built against — building it against
126
+ -- an empty (or thinly populated) table produces a poor, effectively random
127
+ -- partitioning that keeps degrading as the real corpus grows past what the
128
+ -- index was originally trained on, and recovering good recall means
129
+ -- rebuilding the whole index from scratch against the grown corpus. HNSW
130
+ -- builds a NAVIGABLE GRAPH INCREMENTALLY, one vector at a time, and needs no
131
+ -- representative sample up front and no retraining as the corpus grows — the
132
+ -- graph a thousand vectors join is structurally the same kind of graph a
133
+ -- million will. THE INDEX BELOW IS THEREFORE CREATED ON A TABLE WITH ZERO
134
+ -- ROWS IN IT, because this migration runs before any document has ever been
135
+ -- embedded — and that is exactly fine for HNSW, and would have been a real
136
+ -- problem for IVFFlat, which is the whole reason this migration picks the
137
+ -- one it does.
138
+ --
139
+ -- -----------------------------------------------------------------------------
140
+ -- 4. `vector_cosine_ops` — MATCHED, ON PURPOSE, TO THE `<=>` OPERATOR THE
141
+ -- RANKING QUERY USES.
142
+ -- -----------------------------------------------------------------------------
143
+ --
144
+ -- This deployment's embedding provider returns NORMALISED vectors, so cosine
145
+ -- distance is what actually separates a relevant chunk from an irrelevant
146
+ -- one, and `<=>` (cosine distance) is the operator the ranking query is
147
+ -- written against. An HNSW index's operator class must MATCH the distance
148
+ -- operator a query uses, or pgvector cannot use the index for that query at
149
+ -- all — not "uses it less efficiently," SILENTLY FALLS BACK TO A SEQUENTIAL
150
+ -- SCAN, with no error, no warning, and a query plan that only reveals the
151
+ -- mismatch to someone who thinks to run `EXPLAIN`. `vector_cosine_ops` below
152
+ -- is not a stylistic pick among equivalent options; it is the specific
153
+ -- opclass that keeps this index reachable by `ORDER BY embedding <=>
154
+ -- $1 LIMIT n`, the query shape semantic search actually runs.
155
+ --
156
+ -- -----------------------------------------------------------------------------
157
+ -- 5. EMBEDDINGS ARE NOT A COLUMN ON THE DOCUMENT ROW.
158
+ -- -----------------------------------------------------------------------------
159
+ --
160
+ -- A `transcript`/`note` is a single row; its searchable content is many
161
+ -- chunks of text with independent character offsets. Storing "the"
162
+ -- embedding on the document row would force one of two bad shapes: either
163
+ -- one vector standing in for a whole document (useless for surfacing WHICH
164
+ -- part matched, which is what a search result's snippet needs), or an array
165
+ -- column of vectors with no per-chunk offsets to build that snippet from
166
+ -- and no way to re-embed one changed paragraph without rewriting the whole
167
+ -- array. A SEPARATE TABLE keyed on `(document_type, document_id, ordinal)`
168
+ -- makes re-indexing an edited document a per-chunk operation and gives every
169
+ -- match a `char_start`/`char_end` a snippet can be built from directly.
170
+ --
171
+ -- -----------------------------------------------------------------------------
172
+ -- 6. `content_hash` IS THE WHOLE POINT OF CONTENT ADDRESSING HERE.
173
+ -- -----------------------------------------------------------------------------
174
+ --
175
+ -- `content_hash` is `sha256` of a chunk's FINAL text. Re-indexing an edited
176
+ -- document re-chunks the whole thing, but only the chunks whose hash actually
177
+ -- changed need a fresh call to the embedding provider — an unchanged
178
+ -- paragraph's chunk keeps its existing embedding, no matter how far its
179
+ -- `ordinal` moved. The bill for every one of those provider calls lands on
180
+ -- the DOCUMENT OWNER'S OWN vendor account (the same strict bring-your-own-key
181
+ -- posture `docs/specs/notes.md` §9 already establishes for note generation),
182
+ -- so making a re-embed cheap is not merely an efficiency nicety, it is
183
+ -- directly what a re-index costs its owner in real vendor spend.
184
+ -- `UNIQUE (chunk_id, model)` is the constraint that makes this cheap in
185
+ -- practice: a repeat "does this (chunk, model) already have an embedding"
186
+ -- check is a single indexed lookup, not a table scan, and the pipeline this
187
+ -- migration exists to support relies on that lookup being cheap every time
188
+ -- it runs.
189
+ --
190
+ -- -----------------------------------------------------------------------------
191
+ -- STATUS: A REAL PRISMA ENUM. `document_type` AND `reason` STAY PLAIN TEXT.
192
+ -- THIS ASYMMETRY IS DELIBERATE, NOT AN OVERSIGHT.
193
+ -- -----------------------------------------------------------------------------
194
+ --
195
+ -- `search_index_state.status` (`pending`/`indexing`/`indexed`/`failed`/
196
+ -- `skipped`) is a CLOSED set this application itself owns end to end: every
197
+ -- value is written by code inside this repository, nothing external ever
198
+ -- proposes a new one, and a Prisma enum gives the compiler (not a runtime
199
+ -- check) the guarantee that every `.status` comparison in TypeScript is
200
+ -- exhaustive. `document_type` stays plain TEXT for the identical reason
201
+ -- `jobs.subject_type` does (see point 2 above and the `Job` model's own block
202
+ -- comment): the SET OF DOCUMENT TYPES is polymorphic and open to a fork's own
203
+ -- tables, which a Prisma enum — a closed set baked into the generated client
204
+ -- — is structurally the wrong tool to describe. `reason` (`ai_key_missing`,
205
+ -- `ai_not_configured`, `dimension_mismatch`, …) stays plain TEXT because it is
206
+ -- a DIAGNOSTIC STRING, not a state machine: it exists to be read by a human
207
+ -- debugging why one document did not index, it will grow new values as new
208
+ -- failure modes are discovered, and a `NULL` in this column for anything
209
+ -- other than `indexed` is itself informative (a `failed` row with no
210
+ -- `reason` is a bug in the code that set it). Three columns immediately
211
+ -- beside each other, three different answers to "should this be an enum",
212
+ -- stated here so the asymmetry reads as intentional rather than as three
213
+ -- decisions nobody thought about together.
214
+ --
215
+ -- -----------------------------------------------------------------------------
216
+ -- INTENTIONAL SCHEMA DRIFT — THE FIFTH OCCURRENCE OF THIS EXACT PATTERN.
217
+ -- -----------------------------------------------------------------------------
218
+ --
219
+ -- Prisma's schema DSL has no way to express `vector(1536)` as a column type
220
+ -- and no way to express `USING hnsw` as an index access method. In
221
+ -- `schema.prisma`, `search_embeddings.embedding` is declared
222
+ -- `Unsupported("vector(1536)")`, and the `search_embeddings_embedding_hnsw_idx`
223
+ -- index below exists ONLY in this file — `prisma migrate dev`/`diff` will
224
+ -- never regenerate it and must never be asked to "reconcile" it away. This is
225
+ -- the FIFTH occurrence of hand-written, Prisma-inexpressible schema in this
226
+ -- codebase, following (1) `jobs_active_dedup_uniq_idx` (a partial unique
227
+ -- index — `jobs` migration), (2) `database_backup_runs_active_uniq_idx` (the
228
+ -- same partial-unique pattern — `database_backup_runs` migration), (3)
229
+ -- `transcript_speakers_transcript_id_label_key` (a partial unique index over
230
+ -- labelled rows only — `transcripts` migration), and (4) the three generated
231
+ -- `tsvector` columns plus their GIN indexes in this migration's own sibling,
232
+ -- `20260915120000_add_search_vectors`. Each prior occurrence is Prisma
233
+ -- lacking DDL for a SQL feature this application genuinely needs; this one is
234
+ -- no different, and none of the five should be "fixed" by inventing a DSL
235
+ -- workaround that only obscures what is actually running against the
236
+ -- database.
237
+ --
238
+ -- -----------------------------------------------------------------------------
239
+ -- ⚠ A NOTE ON THE HNSW BUILD COST, IN THE SAME SPIRIT AS THIS MIGRATION'S
240
+ -- SIBLING'S `ACCESS EXCLUSIVE` WARNING.
241
+ -- -----------------------------------------------------------------------------
242
+ --
243
+ -- The index below is built against an empty table, so THIS MIGRATION is
244
+ -- instant — there is nothing yet to graph. That will not stay true forever:
245
+ -- rebuilding (or `REINDEX`ing) an HNSW index once `search_embeddings` holds a
246
+ -- real corpus is genuinely expensive CPU and memory work, proportional to the
247
+ -- number of rows and the graph's `m`/`ef_construction` parameters, and it is
248
+ -- governed by `maintenance_work_mem` — too small a setting during a later
249
+ -- rebuild materially slows the build and can spill to disk. An operator
250
+ -- planning a manual `REINDEX` against a populated table should raise
251
+ -- `maintenance_work_mem` for that session first, the same operational
252
+ -- awareness a large `ALTER TABLE` anywhere else in this codebase already
253
+ -- demands.
254
+ -- =============================================================================
255
+
256
+ CREATE EXTENSION IF NOT EXISTS vector;
257
+
258
+ -- CreateEnum
259
+ CREATE TYPE "SearchIndexStatus" AS ENUM ('pending', 'indexing', 'indexed', 'failed', 'skipped');
260
+
261
+ -- CreateTable: search_chunks — one row per chunk of a document's text. See
262
+ -- the header above (point 2) for why document_type/document_id carry no
263
+ -- foreign key, and point 5 for why chunks are not a column on the document.
264
+ CREATE TABLE "search_chunks" (
265
+ "id" UUID NOT NULL,
266
+ "document_type" TEXT NOT NULL,
267
+ "document_id" UUID NOT NULL,
268
+ "ordinal" INTEGER NOT NULL,
269
+ "text" TEXT NOT NULL,
270
+ "content_hash" TEXT NOT NULL,
271
+ "char_start" INTEGER NOT NULL,
272
+ "char_end" INTEGER NOT NULL,
273
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
274
+
275
+ CONSTRAINT "search_chunks_pkey" PRIMARY KEY ("id")
276
+ );
277
+
278
+ -- CreateIndex
279
+ CREATE UNIQUE INDEX "search_chunks_document_type_document_id_ordinal_key"
280
+ ON "search_chunks"("document_type", "document_id", "ordinal");
281
+
282
+ -- CreateIndex
283
+ CREATE INDEX "search_chunks_document_type_document_id_idx"
284
+ ON "search_chunks"("document_type", "document_id");
285
+
286
+ -- CreateIndex: content-addressing lookup (header point 6) — "does a chunk
287
+ -- with this exact text already exist" is a lookup this index makes cheap.
288
+ CREATE INDEX "search_chunks_content_hash_idx"
289
+ ON "search_chunks"("content_hash");
290
+
291
+ -- CreateTable: search_embeddings — one (chunk, model) vector. See the header
292
+ -- (points 1, 3, 4, 6) for the column width contract, the HNSW choice, the
293
+ -- cosine opclass, and why UNIQUE(chunk_id, model) is what makes a repeat
294
+ -- embed cheap.
295
+ CREATE TABLE "search_embeddings" (
296
+ "id" UUID NOT NULL,
297
+ "chunk_id" UUID NOT NULL,
298
+ "model" TEXT NOT NULL,
299
+ "dimensions" INTEGER NOT NULL,
300
+ "embedding" vector(1536) NOT NULL,
301
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
302
+
303
+ CONSTRAINT "search_embeddings_pkey" PRIMARY KEY ("id")
304
+ );
305
+
306
+ -- AddForeignKey: the one real FK in this migration — chunk_id points INSIDE
307
+ -- this same module's own table, unlike document_id above which points
308
+ -- sideways into another module's. See header point 2.
309
+ ALTER TABLE "search_embeddings"
310
+ ADD CONSTRAINT "search_embeddings_chunk_id_fkey"
311
+ FOREIGN KEY ("chunk_id") REFERENCES "search_chunks"("id")
312
+ ON DELETE CASCADE ON UPDATE CASCADE;
313
+
314
+ -- CreateIndex
315
+ CREATE UNIQUE INDEX "search_embeddings_chunk_id_model_key"
316
+ ON "search_embeddings"("chunk_id", "model");
317
+
318
+ -- CreateIndex: HAND-WRITTEN, Prisma-inexpressible. See the header's
319
+ -- "INTENTIONAL SCHEMA DRIFT" section (occurrence 5 of 5) and point 3 (why
320
+ -- HNSW, built empty, is correct here) and point 4 (why the opclass must be
321
+ -- vector_cosine_ops to match the `<=>` operator the ranking query uses).
322
+ CREATE INDEX "search_embeddings_embedding_hnsw_idx"
323
+ ON "search_embeddings" USING hnsw ("embedding" vector_cosine_ops);
324
+
325
+ -- CreateTable: search_index_state — one row per DOCUMENT, the legibility
326
+ -- surface. owner_id is a REAL foreign key (it points at `users`, this
327
+ -- module's neighbour, the same way every other per-user table in this
328
+ -- schema does) and CASCADES: an index-state row for a document nobody owns
329
+ -- any more means nothing. See the header's "STATUS" section for why status
330
+ -- is an enum while document_type/reason stay plain text.
331
+ CREATE TABLE "search_index_state" (
332
+ "id" UUID NOT NULL,
333
+ "document_type" TEXT NOT NULL,
334
+ "document_id" UUID NOT NULL,
335
+ "owner_id" UUID NOT NULL,
336
+ "status" "SearchIndexStatus" NOT NULL DEFAULT 'pending',
337
+ "reason" TEXT,
338
+ "chunk_count" INTEGER NOT NULL DEFAULT 0,
339
+ "model" TEXT,
340
+ "content_fingerprint" TEXT,
341
+ "last_error" TEXT,
342
+ "indexed_at" TIMESTAMPTZ,
343
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
344
+ "updated_at" TIMESTAMPTZ NOT NULL,
345
+
346
+ CONSTRAINT "search_index_state_pkey" PRIMARY KEY ("id")
347
+ );
348
+
349
+ -- AddForeignKey
350
+ ALTER TABLE "search_index_state"
351
+ ADD CONSTRAINT "search_index_state_owner_id_fkey"
352
+ FOREIGN KEY ("owner_id") REFERENCES "users"("id")
353
+ ON DELETE CASCADE ON UPDATE CASCADE;
354
+
355
+ -- CreateIndex
356
+ CREATE UNIQUE INDEX "search_index_state_document_type_document_id_key"
357
+ ON "search_index_state"("document_type", "document_id");
358
+
359
+ -- CreateIndex: the unindexedCount query — "how many of this owner's
360
+ -- documents are in state X".
361
+ CREATE INDEX "search_index_state_owner_id_status_idx"
362
+ ON "search_index_state"("owner_id", "status");
@@ -0,0 +1,30 @@
1
+ -- =============================================================================
2
+ -- `notes.title_source` (issue #180, epic #163)
3
+ -- =============================================================================
4
+ -- A title had no provenance until this column: nothing distinguished a name
5
+ -- the user typed from one the product guessed on the note's behalf. Without
6
+ -- it, a `regenerate` or a future bulk-retitle pass has no way to know a
7
+ -- note's owner already renamed it, and would silently overwrite a
8
+ -- human-chosen title with an AI guess — that is the failure mode this column
9
+ -- exists to make impossible rather than merely avoided.
10
+ --
11
+ -- Three values: `ai` (the model proposed it), `user` (a person typed it —
12
+ -- STICKY, the one value the AI titling path must never overwrite), and
13
+ -- `template` (nobody has chosen one yet; the note is still wearing the name
14
+ -- of the template/source it came from).
15
+ --
16
+ -- BACKFILL IS FREE. The column default applies to every existing row via a
17
+ -- single metadata change (`ADD COLUMN ... NOT NULL DEFAULT 'template'` needs
18
+ -- no table rewrite for a fixed non-null default on modern Postgres), and
19
+ -- `template` is the truthful value for every one of them: `notes.service.ts`
20
+ -- has always fallen back to the source template's `name` when generating a
21
+ -- note's initial title, so there is no row in this table today whose title
22
+ -- came from anywhere else. No data migration, no UPDATE pass, nothing to get
23
+ -- wrong.
24
+ -- =============================================================================
25
+
26
+ -- CreateEnum
27
+ CREATE TYPE "NoteTitleSource" AS ENUM ('ai', 'user', 'template');
28
+
29
+ -- AlterTable
30
+ ALTER TABLE "notes" ADD COLUMN "title_source" "NoteTitleSource" NOT NULL DEFAULT 'template';
@@ -0,0 +1,24 @@
1
+ -- =============================================================================
2
+ -- `note_generations` context snapshot (issue #307)
3
+ -- =============================================================================
4
+ -- A snapshot of WHAT WAS SENT to the AI provider — the exact
5
+ -- `assemblePrompt()` output (apps/api/src/notes/generation/prompt.ts) —
6
+ -- written BEFORE the provider call so a failed generation still records it.
7
+ --
8
+ -- `system_prompt`/`user_content` are the two halves of the assembled prompt.
9
+ -- `source_version` is the transcript `currentVersion` / source note
10
+ -- `currentVersion` actually materialized for the prompt; NULL for a document
11
+ -- source, which carries no version. `context_captured_at` is when the
12
+ -- snapshot was taken, not when the generation itself completed.
13
+ --
14
+ -- NULL across all four columns means the generation predates issue #307 —
15
+ -- deliberately no backfill, because re-materializing the source today would
16
+ -- fabricate history: the source transcript/note may have been corrected
17
+ -- since, and the template may have been edited since.
18
+ -- =============================================================================
19
+
20
+ -- AlterTable
21
+ ALTER TABLE "note_generations" ADD COLUMN "system_prompt" TEXT,
22
+ ADD COLUMN "user_content" TEXT,
23
+ ADD COLUMN "source_version" INTEGER,
24
+ ADD COLUMN "context_captured_at" TIMESTAMPTZ;
@@ -0,0 +1,35 @@
1
+ -- =============================================================================
2
+ -- Per-user hidden note templates: a LISTING preference, never access control
3
+ -- (#310, epic #306)
4
+ -- =============================================================================
5
+ -- One join table. See the block comment above `UserHiddenNoteTemplate` in
6
+ -- prisma/schema.prisma for why this is a join table rather than a
7
+ -- `user_settings` namespace (FK integrity in both directions, a plain
8
+ -- indexed `WHERE user_id = ...` query, no cap, no six-file settings-parity
9
+ -- cost) and rather than reusing `note_templates.is_archived` (archiving is a
10
+ -- property of the template, shared by every viewer; hiding is a property of
11
+ -- the (user, template) pair, and must never affect anyone else's picker).
12
+ --
13
+ -- ⚠ Hiding a template never gates access: create, regenerate and preview all
14
+ -- keep accepting a hidden template by id exactly as before this migration.
15
+ -- =============================================================================
16
+
17
+ -- CreateTable
18
+ CREATE TABLE "user_hidden_note_templates" (
19
+ "user_id" UUID NOT NULL,
20
+ "template_id" UUID NOT NULL,
21
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
+
23
+ CONSTRAINT "user_hidden_note_templates_pkey" PRIMARY KEY ("user_id","template_id")
24
+ );
25
+
26
+ -- CreateIndex
27
+ CREATE INDEX "user_hidden_note_templates_template_id_idx" ON "user_hidden_note_templates"("template_id");
28
+
29
+ -- AddForeignKey
30
+ -- Cascade: the preference is meaningless once its owner is gone.
31
+ ALTER TABLE "user_hidden_note_templates" ADD CONSTRAINT "user_hidden_note_templates_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
32
+
33
+ -- AddForeignKey
34
+ -- Cascade: hiding a deleted template has nothing left to hide.
35
+ ALTER TABLE "user_hidden_note_templates" ADD CONSTRAINT "user_hidden_note_templates_template_id_fkey" FOREIGN KEY ("template_id") REFERENCES "note_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,6 @@
1
+ -- Issue #323: a map `speakerId -> displayName` recording the first time a
2
+ -- user named an AI-detected speaker. This is an identification (metadata
3
+ -- about the recording, like `title`), not a content correction, so it is
4
+ -- deliberately not versioned in `transcript_versions` — see the block
5
+ -- comment above `Transcript.speakerIdentities` in schema.prisma.
6
+ ALTER TABLE "transcripts" ADD COLUMN "speaker_identities" JSONB NOT NULL DEFAULT '{}';
@@ -0,0 +1,95 @@
1
+ -- =============================================================================
2
+ -- AI-assisted transcript name checks (issue #328, epic #326)
3
+ -- =============================================================================
4
+ -- Two tables. See the block comment above `TranscriptNameCheck` in
5
+ -- schema.prisma for the full reasoning: a `TranscriptNameCheck` is one AI run
6
+ -- and a `TranscriptNameSuggestion` is one proposed edit it produced. Neither
7
+ -- is a second source of truth for transcript content — accepting a
8
+ -- suggestion is recorded as an ordinary `segment.update_text` op through the
9
+ -- existing `/operations` path and `TranscriptVersion` history, never a
10
+ -- direct write to `transcript_segments.text` from this migration's tables.
11
+
12
+ -- CreateEnum
13
+ CREATE TYPE "TranscriptNameCheckMode" AS ENUM ('standard', 'thorough');
14
+
15
+ -- CreateEnum
16
+ CREATE TYPE "TranscriptNameCheckStatus" AS ENUM ('pending', 'running', 'ready', 'failed');
17
+
18
+ -- CreateEnum
19
+ CREATE TYPE "TranscriptNameSuggestionStatus" AS ENUM ('pending', 'accepted', 'rejected', 'stale');
20
+
21
+ -- CreateTable
22
+ CREATE TABLE "transcript_name_checks" (
23
+ "id" UUID NOT NULL,
24
+ "transcript_id" UUID NOT NULL,
25
+ "requested_by_id" UUID,
26
+ "mode" "TranscriptNameCheckMode" NOT NULL,
27
+ "status" "TranscriptNameCheckStatus" NOT NULL DEFAULT 'pending',
28
+ "based_on_version" INTEGER NOT NULL,
29
+ "terms" JSONB NOT NULL,
30
+ "provider_id" TEXT,
31
+ "model" TEXT,
32
+ "candidate_count" INTEGER NOT NULL DEFAULT 0,
33
+ "suggestion_count" INTEGER NOT NULL DEFAULT 0,
34
+ "input_tokens" INTEGER NOT NULL DEFAULT 0,
35
+ "output_tokens" INTEGER NOT NULL DEFAULT 0,
36
+ "error_class" TEXT,
37
+ "error" TEXT,
38
+ "job_id" UUID,
39
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
40
+ "started_at" TIMESTAMPTZ,
41
+ "completed_at" TIMESTAMPTZ,
42
+
43
+ CONSTRAINT "transcript_name_checks_pkey" PRIMARY KEY ("id")
44
+ );
45
+
46
+ -- CreateTable
47
+ CREATE TABLE "transcript_name_suggestions" (
48
+ "id" UUID NOT NULL,
49
+ "check_id" UUID NOT NULL,
50
+ "segment_id" UUID NOT NULL,
51
+ "segment_rev" INTEGER NOT NULL,
52
+ "start" INTEGER NOT NULL,
53
+ "end" INTEGER NOT NULL,
54
+ "original" TEXT NOT NULL,
55
+ "replacement" TEXT NOT NULL,
56
+ "confidence" DOUBLE PRECISION,
57
+ "reason" TEXT,
58
+ "source" TEXT NOT NULL,
59
+ "status" "TranscriptNameSuggestionStatus" NOT NULL DEFAULT 'pending',
60
+ "decided_at" TIMESTAMPTZ,
61
+ "decided_by_id" UUID,
62
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
63
+
64
+ CONSTRAINT "transcript_name_suggestions_pkey" PRIMARY KEY ("id")
65
+ );
66
+
67
+ -- CreateIndex
68
+ CREATE UNIQUE INDEX "transcript_name_checks_job_id_key" ON "transcript_name_checks"("job_id");
69
+
70
+ -- CreateIndex
71
+ CREATE INDEX "transcript_name_checks_transcript_id_created_at_idx" ON "transcript_name_checks"("transcript_id", "created_at" DESC);
72
+
73
+ -- CreateIndex
74
+ CREATE INDEX "transcript_name_suggestions_check_id_status_idx" ON "transcript_name_suggestions"("check_id", "status");
75
+
76
+ -- CreateIndex
77
+ CREATE INDEX "transcript_name_suggestions_segment_id_idx" ON "transcript_name_suggestions"("segment_id");
78
+
79
+ -- AddForeignKey
80
+ ALTER TABLE "transcript_name_checks" ADD CONSTRAINT "transcript_name_checks_transcript_id_fkey" FOREIGN KEY ("transcript_id") REFERENCES "transcripts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
81
+
82
+ -- AddForeignKey
83
+ ALTER TABLE "transcript_name_checks" ADD CONSTRAINT "transcript_name_checks_requested_by_id_fkey" FOREIGN KEY ("requested_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
84
+
85
+ -- AddForeignKey
86
+ ALTER TABLE "transcript_name_checks" ADD CONSTRAINT "transcript_name_checks_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "jobs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
87
+
88
+ -- AddForeignKey
89
+ ALTER TABLE "transcript_name_suggestions" ADD CONSTRAINT "transcript_name_suggestions_check_id_fkey" FOREIGN KEY ("check_id") REFERENCES "transcript_name_checks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
90
+
91
+ -- AddForeignKey
92
+ ALTER TABLE "transcript_name_suggestions" ADD CONSTRAINT "transcript_name_suggestions_segment_id_fkey" FOREIGN KEY ("segment_id") REFERENCES "transcript_segments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
93
+
94
+ -- AddForeignKey
95
+ ALTER TABLE "transcript_name_suggestions" ADD CONSTRAINT "transcript_name_suggestions_decided_by_id_fkey" FOREIGN KEY ("decided_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,27 @@
1
+ -- =============================================================================
2
+ -- `body_format` on `note_templates` and `notes` (issue #334)
3
+ -- =============================================================================
4
+ -- The markdown-vs-plain-text SYNTAX a note body is written in — a separate
5
+ -- axis from `output_format`, which names the document TYPE (meeting notes,
6
+ -- summary, …) rather than how its text is styled. Plain TEXT column, not an
7
+ -- enum, same reasoning as `output_format`: validated by Zod in the API
8
+ -- ('markdown' | 'plain_text'), not by a database enum.
9
+ --
10
+ -- `note_templates.body_format` is the author's choice, read by
11
+ -- `assemblePrompt` alongside the other structured fields (spec §2.x).
12
+ -- `notes.body_format` is SNAPSHOTTED from the generating template's
13
+ -- `body_format` at generation time, never a live read of the template — so a
14
+ -- later edit to a template's body format never changes how an
15
+ -- already-generated note renders, the identical snapshot reasoning
16
+ -- `note_generations`'s context columns (#307) already establish for prompts.
17
+ --
18
+ -- Defaulted to 'markdown' for both existing rows and new ones: every
19
+ -- built-in and custom template predating this issue already asks the AI to
20
+ -- write markdown in practice, and every existing note's body is markdown.
21
+ -- =============================================================================
22
+
23
+ -- AlterTable
24
+ ALTER TABLE "note_templates" ADD COLUMN "body_format" TEXT NOT NULL DEFAULT 'markdown';
25
+
26
+ -- AlterTable
27
+ ALTER TABLE "notes" ADD COLUMN "body_format" TEXT NOT NULL DEFAULT 'markdown';
@@ -0,0 +1,23 @@
1
+ -- =============================================================================
2
+ -- `body_format` on `note_versions` (issue #337)
3
+ -- =============================================================================
4
+ -- Per-version tracking of the same markdown-vs-plain-text syntax axis
5
+ -- `note_templates.body_format`/`notes.body_format` (#334) already carry —
6
+ -- but snapshotted onto each individual version rather than only the note's
7
+ -- current one, so `GET /api/notes/:id/versions/:version` and a restore can
8
+ -- each answer "what syntax was THIS version written in" without consulting
9
+ -- the note's live (and possibly since-changed) format. Plain TEXT column,
10
+ -- not an enum: validated by Zod in the API ('markdown' | 'plain_text'), same
11
+ -- reasoning as every other body-format column in this schema.
12
+ --
13
+ -- Defaulted to 'markdown' for both existing rows and new ones. This is a
14
+ -- correct backfill, not merely a convenient one: a version written before
15
+ -- per-version tracking existed has no better answer available — the
16
+ -- generating template/note's format at the time was never recorded per
17
+ -- version — and 'plain_text' only existed as an option for a few hours
18
+ -- before this issue landed, so no pre-existing version could actually be
19
+ -- plain text in practice.
20
+ -- =============================================================================
21
+
22
+ -- AlterTable
23
+ ALTER TABLE "note_versions" ADD COLUMN "body_format" TEXT NOT NULL DEFAULT 'markdown';