@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,1073 @@
1
+ // GENERATED by kvox-db sync — do not edit
2
+ // source: @marinoscar/kvox-db:schema/kvox.graph.prisma
3
+
4
+ // =============================================================================
5
+ // Connected Knowledge (epic #344)
6
+ // =============================================================================
7
+ //
8
+ // The graph foundation: entities, relations, items (commitments, decisions,
9
+ // claims, person facts), the evidence that anchors every claim back to a
10
+ // transcript segment or note span, and the review-and-commit proposal model
11
+ // extraction runs through. Full design in `docs/specs/ontology.md`; §10 is
12
+ // this section's own summary, kept in sync with the column list below.
13
+ // SCHEMA ONLY in this migration — no service, no endpoint, no data. The
14
+ // evidence-invariant trigger is #355's own migration, landing with the code
15
+ // that satisfies it; `kg_graph_layouts` and `ask_*` are #371's and #376's
16
+ // own migrations.
17
+ //
18
+ // Five conventions every model below shares, stated once here rather than
19
+ // repeated per model:
20
+ //
21
+ // 1. `owner_id → users.id`, `onDelete: Cascade`, on every table but one. An
22
+ // entity/relation/item/evidence row/mention/proposal/merge/distinct-pair/
23
+ // attribute-def has no meaning and no permission path to read it once its
24
+ // owner is gone — the identical reasoning `notes.owner_id`/
25
+ // `transcripts.owner_id` already establish, and there is no
26
+ // `graph:read_any` for the identical reason there is no `notes:read_any`.
27
+ // `kg_entity_views` is the one exception: it carries `user_id`, because
28
+ // "which entities has THIS user looked at" is a per-viewer fact, not an
29
+ // ownership fact.
30
+ // 2. `type`/`kind` columns that name an ONTOLOGY key (`kg_entities.type`,
31
+ // `kg_relations.type`) are plain TEXT, never a Prisma enum — the same
32
+ // "small, code-owned, growing set" reasoning `jobs.type` and
33
+ // `storage_objects.managed_by` already establish elsewhere in this
34
+ // schema. Every enum below instead closes a set this SCHEMA itself
35
+ // enforces (a review lifecycle, a proposal's own status machine) — see
36
+ // §17.4 for the full type-vs-enum argument.
37
+ // 3. `props`/`stats`/`user_guidance` JSONB columns default `'{}'` where
38
+ // listed, exactly like `jobs.payload`/`kg_entities.props` elsewhere in
39
+ // this file — an empty object, never NULL, so a reader never has to
40
+ // special-case "no extra data yet" against "extra data exists but is
41
+ // empty."
42
+ // 4. `ontology_version` (text) on every row a §17.4 definition-file version
43
+ // could apply to (`kg_entities`, `kg_relations`, `kg_items`) — which
44
+ // version of the ontology's own type/attribute definitions this row was
45
+ // written against, so a later definition-file migration can tell an
46
+ // old row from a new one without re-deriving it.
47
+ // 5. `valid tstzrange` (on `kg_relations` and `kg_items`) is written and
48
+ // read entirely OUTSIDE Prisma's query API. Prisma cannot construct,
49
+ // bind, or select a `tstzrange` value — the column is declared
50
+ // `Unsupported("tstzrange")` below specifically so the generated client
51
+ // refuses to touch it at all, the same "declare it unsupported so a
52
+ // mistaken ORM write is impossible" shape `SearchEmbedding.embedding`
53
+ // already establishes for `vector(1536)`. Writers set `valid` with
54
+ // `$executeRaw` inside the SAME transaction as the `create`; readers
55
+ // select `lower(valid)`, `upper(valid)` and `lower_inc(valid)`/
56
+ // `upper_inc(valid)` via `$queryRaw`. #353 provides the `toPgRange`/
57
+ // `fromPgRange` helpers every future reader/writer uses instead of
58
+ // reinventing this.
59
+ //
60
+ // -----------------------------------------------------------------------------
61
+ // WHY THERE IS NO ANCHOR CHECK ON `kg_evidence`.
62
+ // -----------------------------------------------------------------------------
63
+ //
64
+ // Every FK `kg_evidence` holds into another module's table
65
+ // (`transcript_id`/`segment_id`/`note_id`/`import_object_id`) is `SetNull`,
66
+ // by design (§10) — an evidence row is a CITATION, and a citation survives
67
+ // losing the thing it pointed to; `quote` (NOT NULL) is stored precisely so
68
+ // the citation stays readable once every anchor has gone to NULL. A CHECK
69
+ // requiring at least one anchor to be non-NULL would make deleting a
70
+ // transcript FAIL the moment its own evidence rows lose their last pointer —
71
+ // exactly the failure `SetNull` exists to avoid. So there is deliberately no
72
+ // such CHECK: a fully-anchorless evidence row (every FK NULL, `quote` intact)
73
+ // is a valid, expected end state, not a bug to prevent.
74
+ //
75
+ // -----------------------------------------------------------------------------
76
+ // INTENTIONAL SCHEMA DRIFT — HAND-WRITTEN, PRISMA-INEXPRESSIBLE SQL LIVES
77
+ // ONLY IN `migration.sql`, THE SAME PATTERN `jobs_active_dedup_uniq_idx` AND
78
+ // `search_embeddings_embedding_hnsw_idx` ALREADY ESTABLISH.
79
+ // -----------------------------------------------------------------------------
80
+ //
81
+ // `migration.sql` for this section hand-writes:
82
+ // - `CREATE EXTENSION IF NOT EXISTS pg_trgm` — a new capability for this
83
+ // codebase (pgvector already is not new; `SearchEmbedding` already
84
+ // depends on it). Trusted on PG13+, so the migration role needs no
85
+ // superuser.
86
+ // - Two `gin (… gin_trgm_ops)` trigram indexes (fuzzy candidate generation,
87
+ // §7): `kg_entities_label_trgm_idx`, `kg_entity_aliases_normalized_trgm_idx`.
88
+ // - Two `gist (valid)` range indexes (§5.4's containment/overlap queries):
89
+ // `kg_relations_valid_gist_idx`, `kg_items_valid_gist_idx`.
90
+ // - Two `hnsw (embedding vector_cosine_ops)` indexes, matching
91
+ // `SearchEmbedding`'s own hand-written index discipline exactly:
92
+ // `kg_entities_embedding_hnsw_idx`, `kg_items_embedding_hnsw_idx`.
93
+ // - Five partial unique indexes and one CHECK-shaped guard, all below.
94
+ // - Eight CHECK constraints, all below.
95
+ // None of the above has a Prisma DSL equivalent; `prisma migrate dev`/`diff`
96
+ // will propose to drop every one of them and must never be asked to
97
+ // reconcile that "drift" away.
98
+ //
99
+ // -----------------------------------------------------------------------------
100
+ // FIVE PARTIAL UNIQUE INDEXES, EACH ENFORCED IN THE DATABASE, NEVER BY A
101
+ // `findFirst` BEFORE THE INSERT — THE `database_backup_runs_active_uniq_idx`
102
+ // REASONING, FIVE TIMES OVER.
103
+ // -----------------------------------------------------------------------------
104
+ //
105
+ // - `kg_items_live_statement_uniq_idx` — the "known, skipped" dedup guard
106
+ // (§8): a verbatim restatement of an already-`accepted`/`edited` item
107
+ // attaches new evidence instead of inserting a duplicate row. Scoped to
108
+ // `review_status IN ('accepted','edited')` and `subject_id IS NOT NULL`
109
+ // so a `rejected` row's hash never blocks a legitimate later insert.
110
+ // - `kg_proposals_note_draft_uniq_idx` — at most one OPEN draft per note.
111
+ // Whoever creates a new draft discards the previous one in the same
112
+ // transaction (#363); this index is what makes "at most one" true even
113
+ // under a race, not the service-layer discard alone.
114
+ // - `kg_proposals_note_extracting_uniq_idx` — at most one RUNNING
115
+ // extraction per note (#363's 409 `extraction_running`).
116
+ // - `kg_proposals_owner_import_extracting_uniq_idx` — at most one RUNNING
117
+ // import per OWNER (#387's), scoped by `owner_id` rather than `note_id`
118
+ // because an import proposal has no note at all.
119
+ // - `kg_relations_speaker_link_uniq_idx` — at most one `IDENTIFIED_AS`
120
+ // edge per diarized speaker (a speaker names at most one entity).
121
+ //
122
+ // -----------------------------------------------------------------------------
123
+ // EIGHT CHECK CONSTRAINTS.
124
+ // -----------------------------------------------------------------------------
125
+ //
126
+ // - `kg_distinct_pairs_order_chk` (`a_id < b_id`) — canonicalizes an
127
+ // unordered pair into exactly one storable row, so `(a, b)` and `(b, a)`
128
+ // can never both be inserted as two different "confirmed distinct" facts.
129
+ // - `kg_mentions_one_source_chk` (`num_nonnulls(note_id, transcript_id) =
130
+ // 1`) — a mention always comes from exactly one source document.
131
+ // - `kg_relations_one_source_chk` (`num_nonnulls(from_id, from_speaker_id)
132
+ // = 1`) — a relation's `from` side is EITHER a resolved entity OR a
133
+ // diarized speaker awaiting `IDENTIFIED_AS`, never both, never neither.
134
+ // - `kg_relations_speaker_type_chk` (`from_speaker_id IS NULL OR type =
135
+ // 'IDENTIFIED_AS'`) — `from_speaker_id` exists for exactly one
136
+ // relation type, and no other.
137
+ // - `kg_relations_valid_precision_chk` (`(valid IS NULL) =
138
+ // (valid_precision IS NULL) OR valid_precision IS NOT DISTINCT FROM
139
+ // 'unknown'` — `IS NOT DISTINCT FROM`, not `=`, since #398: with `=` a
140
+ // NULL precision made the CHECK evaluate NULL, which passes) — a
141
+ // temporal relation always states a precision, but `unknown` is
142
+ // representable even should `valid` itself later become NULL (e.g. a
143
+ // future correction that clears a range while a precision is still on
144
+ // record as having once been claimed).
145
+ // - `kg_items_sensitivity_chk` (`(kind = 'person_fact') = (sensitivity IS
146
+ // NOT NULL)`) — sensitivity is meaningful for exactly one item kind
147
+ // (§5.6), and this makes "a claim with a sensitivity" and "a person
148
+ // fact with none" both unrepresentable rather than merely discouraged.
149
+ // - `kg_items_subject_required_chk` (`kind NOT IN ('claim','person_fact')
150
+ // OR subject_id IS NOT NULL`) — a claim or a person fact is always
151
+ // ABOUT something; a commitment/decision may stand on its own.
152
+ // - `kg_evidence_char_range_chk` / `kg_evidence_ms_range_chk` — an end
153
+ // offset, when both ends are given, is never before its start.
154
+ // - `kg_proposal_items_kind_payload_chk` (`jsonb_typeof(payload) =
155
+ // 'object'`) — `payload` is always a JSON object, never a bare array or
156
+ // scalar, so every reader can assume key access without a type guard.
157
+ // - `kg_proposal_items_merge_into_chk` (`merge_into_id IS NULL OR decision
158
+ // = 'merge_into'`) — the override column and the decision it overrides
159
+ // can never disagree about which decision this row represents.
160
+ // `merge_into_id` is `SetNull`: forgetting or purging the target entity
161
+ // clears the override, and the row falls back to `pending` semantics at
162
+ // commit (#366 refuses a `merge_into` row without a target).
163
+ // - `kg_proposals_note_source_chk` (`CASE WHEN kind = 'extraction' THEN
164
+ // note_version IS NOT NULL ELSE note_id IS NULL AND note_version IS
165
+ // NULL END`) — keys on `note_version`, NEVER on `note_id`, and this is
166
+ // deliberate: `note_id` is inserted non-null for every extraction
167
+ // proposal and can only become NULL afterwards through the note's own
168
+ // `SetNull` FK once the note is hard-deleted. A CHECK keyed on `note_id`
169
+ // would then start failing for rows that were valid when written; a
170
+ // CHECK keyed on `note_version` (never nulled by any FK action) stays
171
+ // satisfied for the life of the row.
172
+ //
173
+ // -----------------------------------------------------------------------------
174
+ // FUTURE MIGRATIONS: when a later `prisma migrate dev` proposes `DROP INDEX`
175
+ // for any hand-written name above, DELETE those proposed lines. This is
176
+ // intentional drift, not decay to fix.
177
+ // =============================================================================
178
+
179
+ enum KgReviewStatus {
180
+ unreviewed
181
+ accepted
182
+ edited
183
+ rejected
184
+ merged
185
+ superseded
186
+
187
+ @@map("kg_review_status")
188
+ }
189
+
190
+ enum KgValidPrecision {
191
+ day
192
+ month
193
+ year
194
+ unknown
195
+
196
+ @@map("kg_valid_precision")
197
+ }
198
+
199
+ enum KgItemKind {
200
+ commitment
201
+ decision
202
+ claim
203
+ person_fact
204
+
205
+ @@map("kg_item_kind")
206
+ }
207
+
208
+ enum KgSensitivity {
209
+ business
210
+ personal
211
+ sensitive
212
+
213
+ @@map("kg_sensitivity")
214
+ }
215
+
216
+ enum KgAliasSource {
217
+ user
218
+ extraction
219
+ speaker_naming
220
+ import
221
+
222
+ @@map("kg_alias_source")
223
+ }
224
+
225
+ enum KgEvidenceSubjectKind {
226
+ entity
227
+ relation
228
+ item
229
+ proposal_item
230
+ import
231
+
232
+ @@map("kg_evidence_subject_kind")
233
+ }
234
+
235
+ enum KgMentionStatus {
236
+ suggested
237
+ linked
238
+ new
239
+ ignored
240
+
241
+ @@map("kg_mention_status")
242
+ }
243
+ // The COMPLETE proposal model, up front — every status and kind #363, #364,
244
+ // #366 and #387 need, so none of them ever needs `ALTER TYPE … ADD VALUE` or
245
+ // a proposal-table migration of its own. `extracting`/`resolution` are used
246
+ // by no code yet; #363 owns the prose for the `extracting` lifecycle and the
247
+ // `resolution` kind, this migration owns only the column list (issue #351).
248
+ enum KgProposalKind {
249
+ extraction
250
+ import
251
+ resolution
252
+
253
+ @@map("kg_proposal_kind")
254
+ }
255
+ // Lifecycle: `extracting → draft | failed`, `draft → committed | discarded`,
256
+ // `failed → discarded`, `committed → reverted`. Transitions are enforced by
257
+ // the SERVICES (#363/#366), not by a database trigger — this enum only
258
+ // closes the set of states a row may ever be in.
259
+ enum KgProposalStatus {
260
+ draft
261
+ extracting
262
+ committed
263
+ discarded
264
+ failed
265
+ reverted
266
+
267
+ @@map("kg_proposal_status")
268
+ }
269
+
270
+ enum KgProposalItemKind {
271
+ entity
272
+ relation
273
+ item
274
+ closing
275
+
276
+ @@map("kg_proposal_item_kind")
277
+ }
278
+
279
+ enum KgProposalItemDecision {
280
+ pending
281
+ accept
282
+ edit
283
+ reject
284
+ merge_into
285
+
286
+ @@map("kg_proposal_item_decision")
287
+ }
288
+
289
+ enum KgProposalItemOrigin {
290
+ ai
291
+ user
292
+
293
+ @@map("kg_proposal_item_origin")
294
+ }
295
+
296
+ enum KgAttributeKind {
297
+ text
298
+ number
299
+ date
300
+ boolean
301
+ select
302
+ multi_select
303
+ url
304
+ entity_ref
305
+
306
+ @@map("kg_attribute_kind")
307
+ }
308
+ // -----------------------------------------------------------------------------
309
+ // `kg_entities` — Person, Organization, Project, Meeting (`type` is a plain
310
+ // ontology key, §17.4 — never a Prisma enum; see the section header above).
311
+ // `owner_id` Cascade — see convention 1 above. `embedding vector(1536)`
312
+ // reuses the EXACT model/dimension contract `SearchEmbedding` already
313
+ // carries (see that model's own block comment for the full "why
314
+ // `Unsupported`, why raw SQL" argument) rather than inventing a second
315
+ // embedding convention; `embedding_hash` is the content hash #364's
316
+ // `kg.embed` keys re-embedding on, mirroring `SearchChunk.contentHash`.
317
+ // `merged_into_id` is a self-relation: once an entity is merged into
318
+ // another (§7), this points at the survivor and `review_status` moves to
319
+ // `merged` — SetNull so deleting the survivor un-merges rather than
320
+ // cascading a delete backward onto every entity it absorbed.
321
+ // `occurred_at` is set on `Meeting` rows only (the ontology's own type
322
+ // contract, not a database constraint — a fork's own type could reuse it
323
+ // differently).
324
+ // -----------------------------------------------------------------------------
325
+ model KgEntity {
326
+ id String @id @default(uuid()) @db.Uuid
327
+
328
+ ownerId String @map("owner_id") @db.Uuid
329
+ type String
330
+ label String
331
+ props Json @default("{}") @db.JsonB
332
+
333
+ // See the section header's convention 5 — `Unsupported`, written/read only
334
+ // through `$executeRaw`/`$queryRaw`, matching `SearchEmbedding.embedding`.
335
+ // Written as `Unsupported("vector(1536)")` with the parentheses as \u0028/\u0029: the
336
+ // composer's field reader (platform-db 0.1.0-next.5) stops a type at the first `)`.
337
+ // Prisma decodes the escapes, so the column type is still vector(1536).
338
+ embedding Unsupported("vector\u00281536\u0029")?
339
+ embeddingModel String? @map("embedding_model")
340
+ embeddingHash String? @map("embedding_hash")
341
+
342
+ reviewStatus KgReviewStatus @default(accepted) @map("review_status")
343
+ mergedIntoId String? @map("merged_into_id") @db.Uuid
344
+ occurredAt DateTime? @map("occurred_at") @db.Timestamptz
345
+
346
+ ontologyVersion String @map("ontology_version")
347
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
348
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
349
+
350
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
351
+ mergedInto KgEntity? @relation("KgEntityMergedInto", fields: [mergedIntoId], references: [id], onDelete: SetNull)
352
+
353
+ mergedFrom KgEntity[] @relation("KgEntityMergedInto")
354
+ aliases KgEntityAlias[]
355
+ relationsFrom KgRelation[] @relation("KgRelationFrom")
356
+ relationsTo KgRelation[] @relation("KgRelationTo")
357
+ itemsAsSubject KgItem[] @relation("KgItemSubject")
358
+ itemsAsMeeting KgItem[] @relation("KgItemMeeting")
359
+ itemsAsOwnerPerson KgItem[] @relation("KgItemOwnerPerson")
360
+ itemsAsCounterparty KgItem[] @relation("KgItemCounterparty")
361
+ mentions KgMention[]
362
+ proposalItemsMergedInto KgProposalItem[] @relation("KgProposalItemMergeInto")
363
+ mergesAsSurvivor KgMerge[] @relation("KgMergeSurvivor")
364
+ mergesAsMerged KgMerge[] @relation("KgMergeMerged")
365
+ distinctPairsAsA KgDistinctPair[] @relation("KgDistinctPairA")
366
+ distinctPairsAsB KgDistinctPair[] @relation("KgDistinctPairB")
367
+ digest KgEntityDigest?
368
+ views KgEntityView[]
369
+ // Ask conversations scoped to this entity (issue #376). SetNull on the
370
+ // conversation's side — forgetting or merging a person drops the scope
371
+ // link, never the user's own conversation.
372
+ askConversationsScoped AskConversation[]
373
+
374
+ // "This owner's entities of type X, by review state" and "this owner's
375
+ // entities, recently updated" — the two shapes an entity list/filter reads.
376
+ @@index([ownerId, type, reviewStatus])
377
+ @@index([ownerId, updatedAt(sort: Desc)])
378
+ // "Every entity that merged into this one" — the merge-reversal read path.
379
+ @@index([mergedIntoId])
380
+ // HAND-WRITTEN in migration.sql only: `kg_entities_label_trgm_idx` (gin,
381
+ // gin_trgm_ops) and `kg_entities_embedding_hnsw_idx` (hnsw,
382
+ // vector_cosine_ops). See the section header's "INTENTIONAL SCHEMA DRIFT"
383
+ // note — Prisma has no DSL for either.
384
+ @@map("kg_entities")
385
+ }
386
+ // -----------------------------------------------------------------------------
387
+ // `kg_entity_aliases` — a SEPARATE table, not an array column on
388
+ // `kg_entities`, specifically because an alias needs its own indexed
389
+ // exact/fuzzy lookup AND its own provenance (`source`) — an array column
390
+ // could hold the strings but not which of four very different origins each
391
+ // one came from (§10). `normalized` is produced by #355's `normalizeAlias()`
392
+ // (NFKC, lowercase, trimmed, whitespace collapsed) — this REPLACES `citext`,
393
+ // which is not installed and is not needed: a normalized `text` column
394
+ // produced by one pure function gives exact matching AND feeds `pg_trgm`
395
+ // directly, at the cost of nothing `citext` would have bought here.
396
+ // `entity_id`/`owner_id` both Cascade — an alias has no meaning once either
397
+ // its entity or its owner is gone.
398
+ // -----------------------------------------------------------------------------
399
+ model KgEntityAlias {
400
+ id String @id @default(uuid()) @db.Uuid
401
+ entityId String @map("entity_id") @db.Uuid
402
+ ownerId String @map("owner_id") @db.Uuid
403
+
404
+ alias String
405
+ // See the block comment above the model for the full `normalizeAlias()`
406
+ // argument — this is why there is no `citext` column here.
407
+ normalized String
408
+
409
+ source KgAliasSource
410
+
411
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
412
+
413
+ entity KgEntity @relation(fields: [entityId], references: [id], onDelete: Cascade)
414
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
415
+
416
+ // One alias per (entity, normalized form) — a repeat submission of the
417
+ // identically-normalized alias is a no-op, not a duplicate row.
418
+ @@unique([entityId, normalized])
419
+ @@index([ownerId, normalized])
420
+ @@index([entityId])
421
+ // HAND-WRITTEN in migration.sql only: `kg_entity_aliases_normalized_trgm_idx`
422
+ // (gin, gin_trgm_ops) — fuzzy candidate generation (§7). See the section
423
+ // header's "INTENTIONAL SCHEMA DRIFT" note.
424
+ @@map("kg_entity_aliases")
425
+ }
426
+ // -----------------------------------------------------------------------------
427
+ // `kg_relations` — `type` is one of §5.2's sixteen ontology edge types
428
+ // (plain text, never a Prisma enum — convention 2 above). No inverse row is
429
+ // EVER stored (§5.2): a "from" side is either a resolved `KgEntity`
430
+ // (`from_id`) or a diarized speaker awaiting identification
431
+ // (`from_speaker_id`) — `kg_relations_one_source_chk` makes exactly one of
432
+ // the two representable, and `kg_relations_speaker_type_chk` restricts
433
+ // `from_speaker_id` to the one relation type (`IDENTIFIED_AS`) whose source
434
+ // is a speaker rather than an entity. This is a contract addition beyond
435
+ // spec §10's original text (§5.1/§5.2): a polymorphic `from_kind`/`from_id`
436
+ // pair without FKs was rejected in favour of a REAL FK to
437
+ // `transcript_speakers`, so deleting a merged speaker's row cleans up the
438
+ // edge for free. `valid`/`valid_precision` are the §5.4 bitemporal pair —
439
+ // see the section header's convention 5 for how `valid` is actually
440
+ // written/read. `superseded_by_id` is a self-relation (§5.5's "closing a
441
+ // temporal edge" case), SetNull so deleting the successor un-supersedes
442
+ // rather than cascading backward.
443
+ // -----------------------------------------------------------------------------
444
+ model KgRelation {
445
+ id String @id @default(uuid()) @db.Uuid
446
+ ownerId String @map("owner_id") @db.Uuid
447
+ type String
448
+
449
+ // Endpoints — see the block comment above the model for the full
450
+ // exactly-one-of argument enforced by `kg_relations_one_source_chk`.
451
+ fromId String? @map("from_id") @db.Uuid
452
+ fromSpeakerId String? @map("from_speaker_id") @db.Uuid
453
+ toId String @map("to_id") @db.Uuid
454
+
455
+ props Json @default("{}") @db.JsonB
456
+
457
+ // §5.4's bitemporal pair. See the section header's convention 5 for why
458
+ // `valid` is `Unsupported` and written/read only via raw SQL.
459
+ valid Unsupported("tstzrange")?
460
+ validPrecision KgValidPrecision? @map("valid_precision")
461
+
462
+ reviewStatus KgReviewStatus @default(accepted) @map("review_status")
463
+ confidence Float?
464
+ supersededById String? @map("superseded_by_id") @db.Uuid
465
+
466
+ ontologyVersion String @map("ontology_version")
467
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
468
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
469
+
470
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
471
+ fromEntity KgEntity? @relation("KgRelationFrom", fields: [fromId], references: [id], onDelete: Cascade)
472
+ fromSpeaker TranscriptSpeaker? @relation(fields: [fromSpeakerId], references: [id], onDelete: Cascade)
473
+ toEntity KgEntity @relation("KgRelationTo", fields: [toId], references: [id], onDelete: Cascade)
474
+ supersededBy KgRelation? @relation("KgRelationSupersededBy", fields: [supersededById], references: [id], onDelete: SetNull)
475
+
476
+ supersedes KgRelation[] @relation("KgRelationSupersededBy")
477
+
478
+ // §5.2's "joins are symmetric" argument, made concrete: a relation is
479
+ // looked up from either endpoint equally often.
480
+ @@index([ownerId, fromId, type])
481
+ @@index([ownerId, toId, type])
482
+ @@index([fromSpeakerId])
483
+ @@index([supersededById])
484
+ @@index([ownerId, type, reviewStatus])
485
+ // HAND-WRITTEN in migration.sql only: `kg_relations_valid_gist_idx` (gist,
486
+ // §5.4's containment/overlap queries) and `kg_relations_speaker_link_uniq_idx`
487
+ // (a partial unique index — see the section header). See the section
488
+ // header's "INTENTIONAL SCHEMA DRIFT" note.
489
+ @@map("kg_relations")
490
+ }
491
+ // -----------------------------------------------------------------------------
492
+ // `kg_items` — Commitment, Decision, Claim, and PersonFact in ONE table,
493
+ // distinguished by `kind` (a real Prisma enum here, unlike `kg_entities
494
+ // .type`/`kg_relations.type` — see §10, this is the schema's own closed
495
+ // four-way split, not an open ontology key). `meeting_id` backs
496
+ // `CREATED_IN`/`DECIDED_IN` (a contract addition beyond the original §10
497
+ // text) — SetNull, so deleting the meeting entity leaves the item itself
498
+ // intact with a now-unknown venue rather than deleting the item.
499
+ // `owner_person_id`/`counterparty_id` Cascade (deleting the person this
500
+ // commitment is FOR/BETWEEN removes the commitment's meaning entirely, the
501
+ // same argument as `subject_id`); `subject_id` also Cascade. `sensitivity`
502
+ // is meaningful for exactly one `kind` (§5.6), enforced by
503
+ // `kg_items_sensitivity_chk`. `statement_hash` is the §7 dedup/suppression
504
+ // key `kg_items_live_statement_uniq_idx` enforces. `valid`/`valid_precision`
505
+ // mirror `kg_relations`'s own bitemporal pair (§5.4) so an item's own valid
506
+ // period is never a second stored shape from its edges' shape.
507
+ // -----------------------------------------------------------------------------
508
+ model KgItem {
509
+ id String @id @default(uuid()) @db.Uuid
510
+ ownerId String @map("owner_id") @db.Uuid
511
+ kind KgItemKind
512
+
513
+ subjectId String? @map("subject_id") @db.Uuid
514
+ meetingId String? @map("meeting_id") @db.Uuid
515
+ ownerPersonId String? @map("owner_person_id") @db.Uuid
516
+ counterpartyId String? @map("counterparty_id") @db.Uuid
517
+
518
+ title String?
519
+ // Commitment: `open | done | dropped | superseded`. Others: `active |
520
+ // superseded`. Plain text, not an enum — the same "small, code-owned set
521
+ // that varies per `kind`" reasoning `props`/§10 already give this table
522
+ // its single-table shape for.
523
+ statement String
524
+ status String
525
+ props Json @default("{}") @db.JsonB
526
+
527
+ occurredAt DateTime? @map("occurred_at") @db.Timestamptz
528
+ dueAt DateTime? @map("due_at") @db.Timestamptz
529
+
530
+ supersededById String? @map("superseded_by_id") @db.Uuid
531
+ sensitivity KgSensitivity?
532
+ // The §7 dedup/suppression key — see `kg_items_live_statement_uniq_idx`
533
+ // below (hand-written, since Prisma cannot express its `WHERE` clause).
534
+ statementHash String @map("statement_hash")
535
+
536
+ // §5.4's bitemporal pair, mirroring `kg_relations` exactly. See the
537
+ // section header's convention 5 for why `valid` is `Unsupported`.
538
+ valid Unsupported("tstzrange")?
539
+ validPrecision KgValidPrecision? @map("valid_precision")
540
+
541
+ // See the section header's convention 5 — same `Unsupported`/raw-SQL
542
+ // contract as `kg_entities.embedding` and `SearchEmbedding.embedding`.
543
+ // Written as `Unsupported("vector(1536)")` with the parentheses as \u0028/\u0029: the
544
+ // composer's field reader (platform-db 0.1.0-next.5) stops a type at the first `)`.
545
+ // Prisma decodes the escapes, so the column type is still vector(1536).
546
+ embedding Unsupported("vector\u00281536\u0029")?
547
+ embeddingModel String? @map("embedding_model")
548
+ embeddingHash String? @map("embedding_hash")
549
+
550
+ reviewStatus KgReviewStatus @default(accepted) @map("review_status")
551
+ confidence Float?
552
+
553
+ ontologyVersion String @map("ontology_version")
554
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
555
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
556
+
557
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
558
+ subject KgEntity? @relation("KgItemSubject", fields: [subjectId], references: [id], onDelete: Cascade)
559
+ meeting KgEntity? @relation("KgItemMeeting", fields: [meetingId], references: [id], onDelete: SetNull)
560
+ ownerPerson KgEntity? @relation("KgItemOwnerPerson", fields: [ownerPersonId], references: [id], onDelete: Cascade)
561
+ counterparty KgEntity? @relation("KgItemCounterparty", fields: [counterpartyId], references: [id], onDelete: Cascade)
562
+ supersededBy KgItem? @relation("KgItemSupersededBy", fields: [supersededById], references: [id], onDelete: SetNull)
563
+
564
+ supersedes KgItem[] @relation("KgItemSupersededBy")
565
+
566
+ // §9.1's "what's changed for this entity" brief query.
567
+ @@index([ownerId, subjectId, occurredAt(sort: Desc)])
568
+ @@index([ownerId, kind, status])
569
+ @@index([ownerId, statementHash])
570
+ @@index([ownerPersonId])
571
+ @@index([counterpartyId])
572
+ @@index([meetingId])
573
+ @@index([supersededById])
574
+ // HAND-WRITTEN in migration.sql only: `kg_items_valid_gist_idx` (gist),
575
+ // `kg_items_embedding_hnsw_idx` (hnsw, vector_cosine_ops), and
576
+ // `kg_items_live_statement_uniq_idx` (a partial unique index — the "known,
577
+ // skipped" §8 dedup guard). See the section header's "INTENTIONAL SCHEMA
578
+ // DRIFT" note.
579
+ @@map("kg_items")
580
+ }
581
+ // -----------------------------------------------------------------------------
582
+ // `kg_evidence` — the §5.3 evidence contract. `subject_kind`/`subject_id`
583
+ // are POLYMORPHIC, carrying NO foreign key — the identical `jobs.subject_
584
+ // type`/`subject_id` pattern this schema already uses, because the set of
585
+ // things evidence can anchor (an entity, a relation, an item, a proposal
586
+ // item, an import) is open-ended and code-owned. The transcript/note/import
587
+ // anchors ARE real foreign keys, and every one of them is `SetNull` — see
588
+ // the section header's "WHY THERE IS NO ANCHOR CHECK" note for the full
589
+ // argument; `quote` (NOT NULL) is what keeps a citation readable once every
590
+ // anchor has gone to NULL. `char_start`/`char_end` are offsets into the
591
+ // cited NOTE VERSION BODY when `note_id` is set, or into the cited
592
+ // SEGMENT'S TEXT when `segment_id` is set — NULL means the whole segment.
593
+ // On a NOTE citation, both NULL means "the note's Context field" (#440): the
594
+ // Context has no body offsets, so the quote alone carries the span.
595
+ // #363 relies on this exact convention; do not reinterpret it per-caller.
596
+ // -----------------------------------------------------------------------------
597
+ model KgEvidence {
598
+ id String @id @default(uuid()) @db.Uuid
599
+ ownerId String @map("owner_id") @db.Uuid
600
+ subjectKind KgEvidenceSubjectKind @map("subject_kind")
601
+ // Polymorphic, no FK — see the block comment above the model.
602
+ subjectId String @map("subject_id") @db.Uuid
603
+
604
+ // Transcript anchor. SetNull — see the section header's "WHY THERE IS NO
605
+ // ANCHOR CHECK" note.
606
+ transcriptId String? @map("transcript_id") @db.Uuid
607
+ segmentId String? @map("segment_id") @db.Uuid
608
+ segmentRev Int? @map("segment_rev")
609
+ startMs Int? @map("start_ms")
610
+ endMs Int? @map("end_ms")
611
+
612
+ // Note anchor. SetNull, same reasoning.
613
+ noteId String? @map("note_id") @db.Uuid
614
+ noteVersion Int? @map("note_version")
615
+
616
+ // Offsets into the cited NOTE VERSION BODY when `noteId` is set, or into
617
+ // the cited SEGMENT'S TEXT when `segmentId` is set. NULL means the whole
618
+ // segment; on a note citation, both NULL means the note's Context field
619
+ // (#440). See the block comment above the model — #363 reads this
620
+ // convention directly.
621
+ charStart Int? @map("char_start")
622
+ charEnd Int? @map("char_end")
623
+
624
+ // The text AT ANCHORING TIME (§5.3) — NOT NULL, deliberately: this is
625
+ // what keeps the citation readable once every anchor above has gone to
626
+ // NULL (see the section header's "WHY THERE IS NO ANCHOR CHECK" note).
627
+ quote String
628
+
629
+ // Import anchor. SetNull, same reasoning.
630
+ importObjectId String? @map("import_object_id") @db.Uuid
631
+ sourceIri String? @map("source_iri")
632
+
633
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
634
+
635
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
636
+ transcript Transcript? @relation(fields: [transcriptId], references: [id], onDelete: SetNull)
637
+ segment TranscriptSegment? @relation(fields: [segmentId], references: [id], onDelete: SetNull)
638
+ note Note? @relation(fields: [noteId], references: [id], onDelete: SetNull)
639
+ importObject StorageObject? @relation(fields: [importObjectId], references: [id], onDelete: SetNull)
640
+
641
+ @@index([subjectKind, subjectId])
642
+ @@index([ownerId, transcriptId])
643
+ @@index([segmentId])
644
+ @@index([ownerId, noteId])
645
+ @@map("kg_evidence")
646
+ }
647
+ // -----------------------------------------------------------------------------
648
+ // `kg_mentions` — the coarse `MENTIONS` shortcut (§5.2): what an entity
649
+ // page's "notes about Joe" section reads, distinct from `kg_evidence`'s
650
+ // precise per-claim citation. Exactly one source document per row
651
+ // (`kg_mentions_one_source_chk`). `entity_id`/`note_id`/`transcript_id` ALL
652
+ // Cascade — a mention has no meaning once either side is gone, the opposite
653
+ // of `kg_evidence`'s pointer relationship above.
654
+ // -----------------------------------------------------------------------------
655
+ model KgMention {
656
+ id String @id @default(uuid()) @db.Uuid
657
+ ownerId String @map("owner_id") @db.Uuid
658
+
659
+ entityId String @map("entity_id") @db.Uuid
660
+
661
+ noteId String? @map("note_id") @db.Uuid
662
+ transcriptId String? @map("transcript_id") @db.Uuid
663
+
664
+ // `{charStart,charEnd}` or `{segmentId}` — which shape depends on which of
665
+ // `noteId`/`transcriptId` is set.
666
+ span Json? @db.JsonB
667
+ status KgMentionStatus @default(linked)
668
+ score Float?
669
+
670
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
671
+
672
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
673
+ entity KgEntity @relation(fields: [entityId], references: [id], onDelete: Cascade)
674
+ note Note? @relation(fields: [noteId], references: [id], onDelete: Cascade)
675
+ transcript Transcript? @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
676
+
677
+ @@index([ownerId, entityId])
678
+ @@index([noteId])
679
+ @@index([transcriptId])
680
+ // HAND-WRITTEN in migration.sql only: `kg_mentions_one_source_chk`. See
681
+ // the section header's "EIGHT CHECK CONSTRAINTS" note.
682
+ @@map("kg_mentions")
683
+ }
684
+ // -----------------------------------------------------------------------------
685
+ // `kg_proposals` — one row per extraction, import, or resolution run
686
+ // (§8, §18.3; the complete kind/status enums are declared above THIS
687
+ // migration, issue #351, so #363/#364/#366/#387 never need `ALTER TYPE`).
688
+ // Only `kind = 'extraction'` has a note — `kg_proposals_note_source_chk`
689
+ // requires `note_version` for `extraction` and requires BOTH `note_id` AND
690
+ // `note_version` NULL for `import`/`resolution`. The CHECK keys on
691
+ // `note_version`, never `note_id`: `note_id` is inserted non-null for every
692
+ // extraction and can only become NULL afterwards via the note's own
693
+ // `SetNull` FK once the note is hard-deleted, so a CHECK on `note_id` would
694
+ // start failing for rows that were valid when written. `commit_log` is
695
+ // #366's internal undo record — the commit writes it, the revert reads it —
696
+ // and it is NEVER serialized to clients; `stats` is the display copy.
697
+ // `job_id` mirrors `TranscriptExport.jobId` exactly (`@unique`, nullable,
698
+ // SetNull): a proposal's own review lifetime is independent of
699
+ // `job.history.purge`'s schedule for the job row.
700
+ // -----------------------------------------------------------------------------
701
+ model KgProposal {
702
+ id String @id @default(uuid()) @db.Uuid
703
+ ownerId String @map("owner_id") @db.Uuid
704
+ kind KgProposalKind
705
+ status KgProposalStatus @default(draft)
706
+
707
+ // Only `kind = 'extraction'` has a note. See the block comment above the
708
+ // model for why `kg_proposals_note_source_chk` keys on `noteVersion`.
709
+ noteId String? @map("note_id") @db.Uuid
710
+ noteVersion Int? @map("note_version")
711
+
712
+ // Generation context — which task-model resolution actually ran (§20),
713
+ // recorded rather than re-derived, so an administrator changing the
714
+ // default tomorrow never rewrites what an already-committed proposal used
715
+ // yesterday.
716
+ model String?
717
+ provider String?
718
+ systemPrompt String? @map("system_prompt")
719
+ userContent String? @map("user_content")
720
+ userGuidance Json? @map("user_guidance") @db.JsonB
721
+
722
+ stats Json @default("{}") @db.JsonB
723
+
724
+ committedAt DateTime? @map("committed_at") @db.Timestamptz
725
+ revertedAt DateTime? @map("reverted_at") @db.Timestamptz
726
+ jobId String? @unique @map("job_id") @db.Uuid
727
+
728
+ // #366's internal undo record — the commit writes it, the revert reads
729
+ // it. NEVER serialized to clients; `stats` above is the display copy.
730
+ commitLog Json? @map("commit_log") @db.JsonB
731
+
732
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
733
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
734
+
735
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
736
+ note Note? @relation(fields: [noteId], references: [id], onDelete: SetNull)
737
+ job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
738
+
739
+ items KgProposalItem[]
740
+
741
+ @@index([ownerId, status, createdAt(sort: Desc)])
742
+ @@index([noteId, createdAt(sort: Desc)])
743
+ // HAND-WRITTEN in migration.sql only: `kg_proposals_note_draft_uniq_idx`,
744
+ // `kg_proposals_note_extracting_uniq_idx`,
745
+ // `kg_proposals_owner_import_extracting_uniq_idx` (three partial unique
746
+ // indexes) and `kg_proposals_note_source_chk`. See the section header's
747
+ // "FIVE PARTIAL UNIQUE INDEXES" / "EIGHT CHECK CONSTRAINTS" notes.
748
+ @@map("kg_proposals")
749
+ }
750
+ // -----------------------------------------------------------------------------
751
+ // `kg_proposal_items` — one row per proposed entity, relation, item, or
752
+ // closing edit (`closing` is §7's "closing a temporal edge is a proposal
753
+ // row" case). `merge_into_id`/`distinct_from` are #366's reviewer-override
754
+ // columns, declared up front by this migration (issue #351) so #366 needs
755
+ // no schema change of its own: `merge_into_id` is set IFF `decision =
756
+ // 'merge_into'` (`kg_proposal_items_merge_into_chk`), SetNull so losing the
757
+ // target entity clears the override rather than blocking the delete;
758
+ // `distinct_from` records the "not the same as" candidates a reviewer ruled
759
+ // out, as plain uuids (no FK — these are recorded facts about a review
760
+ // decision, not live references another table's delete must cascade
761
+ // through). `committed_ref_id` is polymorphic (no FK), the row this item
762
+ // became once committed — #366's revert reads this to know exactly what to
763
+ // undo.
764
+ // -----------------------------------------------------------------------------
765
+ model KgProposalItem {
766
+ id String @id @default(uuid()) @db.Uuid
767
+ proposalId String @map("proposal_id") @db.Uuid
768
+ kind KgProposalItemKind
769
+
770
+ payload Json @db.JsonB
771
+ resolution Json? @db.JsonB
772
+ editedPayload Json? @map("edited_payload") @db.JsonB
773
+
774
+ decision KgProposalItemDecision @default(pending)
775
+ // Values from #363's `PROPOSAL_ITEM_FLAGS` — plain text[], not an enum:
776
+ // the review panel's pre-check flags are a growing, code-owned set, the
777
+ // same reasoning `kg_entities.type` stays plain text.
778
+ flags String[] @default([])
779
+ origin KgProposalItemOrigin @default(ai)
780
+
781
+ // #366's reviewer-override columns. See the block comment above the model
782
+ // for the exactly-one-decision argument `kg_proposal_items_merge_into_chk`
783
+ // enforces, and why `mergeIntoId` is SetNull.
784
+ mergeIntoId String? @map("merge_into_id") @db.Uuid
785
+ distinctFrom String[] @default([]) @map("distinct_from")
786
+
787
+ // Polymorphic, no FK — the `kg_entities`/`kg_relations`/`kg_items` row
788
+ // this item became once committed.
789
+ committedRefId String? @map("committed_ref_id") @db.Uuid
790
+ sortOrder Int @default(0) @map("sort_order")
791
+
792
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
793
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
794
+
795
+ proposal KgProposal @relation(fields: [proposalId], references: [id], onDelete: Cascade)
796
+ mergeInto KgEntity? @relation("KgProposalItemMergeInto", fields: [mergeIntoId], references: [id], onDelete: SetNull)
797
+
798
+ @@index([proposalId, kind, sortOrder])
799
+ // HAND-WRITTEN in migration.sql only: `kg_proposal_items_kind_payload_chk`
800
+ // and `kg_proposal_items_merge_into_chk`. See the section header's "EIGHT
801
+ // CHECK CONSTRAINTS" note.
802
+ @@map("kg_proposal_items")
803
+ }
804
+ // One row per merge (§7), with the full reversal payload — what #366 (and
805
+ // an entity page's "undo merge" action) replays to restore the merged
806
+ // entity. `survivor_id`/`merged_id` both Cascade: a merge record has no
807
+ // meaning once either entity it names is gone.
808
+ model KgMerge {
809
+ id String @id @default(uuid()) @db.Uuid
810
+ ownerId String @map("owner_id") @db.Uuid
811
+ survivorId String @map("survivor_id") @db.Uuid
812
+ mergedId String @map("merged_id") @db.Uuid
813
+
814
+ reversal Json @db.JsonB
815
+ reversedAt DateTime? @map("reversed_at") @db.Timestamptz
816
+
817
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
818
+
819
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
820
+ survivor KgEntity @relation("KgMergeSurvivor", fields: [survivorId], references: [id], onDelete: Cascade)
821
+ merged KgEntity @relation("KgMergeMerged", fields: [mergedId], references: [id], onDelete: Cascade)
822
+
823
+ @@index([ownerId, createdAt(sort: Desc)])
824
+ @@index([survivorId])
825
+ @@index([mergedId])
826
+ @@map("kg_merges")
827
+ }
828
+ // Confirmed-NOT-the-same pairs (§7), skipped by every future
829
+ // candidate-generation pass. `a_id < b_id` is enforced by
830
+ // `kg_distinct_pairs_order_chk` (hand-written — Prisma cannot express a
831
+ // cross-column CHECK), so `(a, b)` and `(b, a)` can never both exist as two
832
+ // different rows for what is the same unordered fact. Both entity FKs
833
+ // Cascade.
834
+ model KgDistinctPair {
835
+ ownerId String @map("owner_id") @db.Uuid
836
+ aId String @map("a_id") @db.Uuid
837
+ bId String @map("b_id") @db.Uuid
838
+
839
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
840
+
841
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
842
+ entityA KgEntity @relation("KgDistinctPairA", fields: [aId], references: [id], onDelete: Cascade)
843
+ entityB KgEntity @relation("KgDistinctPairB", fields: [bId], references: [id], onDelete: Cascade)
844
+
845
+ @@id([ownerId, aId, bId])
846
+ // HAND-WRITTEN in migration.sql only: `kg_distinct_pairs_order_chk`. See
847
+ // the section header's "EIGHT CHECK CONSTRAINTS" note.
848
+ @@map("kg_distinct_pairs")
849
+ }
850
+ // One row per user-defined attribute (§17.3) — the schema half of the
851
+ // ontology's user-extensibility story. `owner_id` Cascade, the same
852
+ // reasoning every other `kg_*` table's `owner_id` follows.
853
+ model KgAttributeDef {
854
+ id String @id @default(uuid()) @db.Uuid
855
+ ownerId String @map("owner_id") @db.Uuid
856
+
857
+ entityType String @map("entity_type")
858
+ key String
859
+ label String
860
+ kind KgAttributeKind
861
+ options Json? @db.JsonB
862
+
863
+ extractable Boolean @default(false)
864
+ extractionHint String? @map("extraction_hint")
865
+
866
+ sensitivity KgSensitivity?
867
+ sortOrder Int @default(0) @map("sort_order")
868
+ deprecatedAt DateTime? @map("deprecated_at") @db.Timestamptz
869
+
870
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
871
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
872
+
873
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
874
+
875
+ @@unique([ownerId, entityType, key])
876
+ @@index([ownerId, entityType, sortOrder])
877
+ @@map("kg_attribute_defs")
878
+ }
879
+ // §9.2's precomputed entity brief — "precompute once, read cheaply," the
880
+ // same economy `kg_graph_layouts` (its own, later migration) applies to a
881
+ // whole-graph layout. `entity_id` is the PRIMARY KEY (not a separate `id`):
882
+ // there is exactly one digest per entity, always replaced in place, never
883
+ // versioned.
884
+ model KgEntityDigest {
885
+ entityId String @id @map("entity_id") @db.Uuid
886
+ ownerId String @map("owner_id") @db.Uuid
887
+
888
+ summary String
889
+ citations Json @db.JsonB
890
+ coversUntil DateTime @map("covers_until") @db.Timestamptz
891
+ model String
892
+ generatedAt DateTime @map("generated_at") @db.Timestamptz
893
+
894
+ entity KgEntity @relation(fields: [entityId], references: [id], onDelete: Cascade)
895
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
896
+
897
+ @@index([ownerId])
898
+ @@map("kg_entity_digests")
899
+ }
900
+ // (user_id, entity_id, last_viewed_at) — §9.2's "recently viewed" list. The
901
+ // ONE table in this section keyed on `user_id`, not `owner_id` — see
902
+ // convention 1 above: which entities a viewer has looked at is a per-viewer
903
+ // fact, not an ownership fact (a shared/admin view is out of scope today,
904
+ // but the column name is deliberately not `owner_id` so it never implies
905
+ // one). A plain `@@unique`, exactly as issue #351's canonical column list
906
+ // specifies — NOT the partial unique §10's prose mentions elsewhere;
907
+ // nothing about this row is conditional.
908
+ model KgEntityView {
909
+ id String @id @default(uuid()) @db.Uuid
910
+ userId String @map("user_id") @db.Uuid
911
+ entityId String @map("entity_id") @db.Uuid
912
+ lastViewedAt DateTime @map("last_viewed_at") @db.Timestamptz
913
+
914
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
915
+ entity KgEntity @relation(fields: [entityId], references: [id], onDelete: Cascade)
916
+
917
+ @@unique([userId, entityId])
918
+ @@map("kg_entity_views")
919
+ }
920
+ // =============================================================================
921
+ // `kg_exports` — RDF exports of one owner's graph (issue #386, epic #349;
922
+ // docs/specs/ontology.md §18.2)
923
+ // =============================================================================
924
+ //
925
+ // Mirrors `note_exports`: one row per requested render, produced by the
926
+ // `kg.export` job into a `managed_by: 'graph'` storage object, expiring 7 days
927
+ // after it was requested (the daily sweep deletes the row and the file).
928
+ //
929
+ // CONTENT-ADDRESSED on `graph_fingerprint` — a SHA-256 over the ontology
930
+ // version, the format, and the owner's graph "shape" (max updated_at plus row
931
+ // counts of the kg_* tables). `POST /api/graph/exports` reuses an unexpired,
932
+ // non-failed row with the same `(owner, format, fingerprint)`, so asking twice
933
+ // for an unchanged graph renders once. A plain, NON-unique index — an expired
934
+ // or failed row may briefly coexist with a fresh one sharing the key, exactly
935
+ // as `note_exports_lookup_idx` allows.
936
+ //
937
+ // `owner_id` Cascade; `object_id` Restrict (the file goes only after the row
938
+ // lets go of it); `job_id` `@unique`/SetNull (the `NoteExport.jobId` shape).
939
+ // No hand-written SQL: every constraint here is Prisma-expressible.
940
+ // =============================================================================
941
+
942
+ enum KgExportFormat {
943
+ jsonld
944
+ turtle
945
+ nquads
946
+
947
+ @@map("kg_export_format")
948
+ }
949
+
950
+ enum KgExportStatus {
951
+ pending
952
+ running
953
+ ready
954
+ failed
955
+
956
+ @@map("kg_export_status")
957
+ }
958
+
959
+ model KgExport {
960
+ id String @id @default(uuid()) @db.Uuid
961
+ ownerId String @map("owner_id") @db.Uuid
962
+ format KgExportFormat
963
+ status KgExportStatus @default(pending)
964
+ graphFingerprint String @map("graph_fingerprint")
965
+ ontologyVersion String @map("ontology_version")
966
+ objectId String? @map("object_id") @db.Uuid
967
+ // `{ entities, relations, items, evidence, excludedSensitive, bytes }` — counts only.
968
+ stats Json @default("{}") @db.JsonB
969
+ // Short and generic; never row data.
970
+ errorMessage String? @map("error_message")
971
+ jobId String? @unique @map("job_id") @db.Uuid
972
+ expiresAt DateTime @map("expires_at") @db.Timestamptz(6)
973
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
974
+
975
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
976
+ object StorageObject? @relation(fields: [objectId], references: [id], onDelete: Restrict)
977
+ job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
978
+
979
+ @@index([ownerId, format, graphFingerprint])
980
+ @@index([expiresAt])
981
+ @@map("kg_exports")
982
+ }
983
+ /// Latest whole-graph layout per owner (spec §22, issue #371). A cache of derived data — never
984
+ /// a source of truth; labels are NOT stored (read live, so a renamed or forgotten entity never
985
+ /// shows a stale label). `clusters`/`positions` are versioned JSON blobs (`{ version: 1, ... }`)
986
+ /// rather than typed columns, produced by the pure `computeLayout()` function and consumed only
987
+ /// by `GraphOverviewService`, which joins live entity rows back in at read time.
988
+ model KgGraphLayout {
989
+ id String @id @default(uuid()) @db.Uuid
990
+ ownerId String @map("owner_id") @db.Uuid
991
+
992
+ computedAt DateTime @map("computed_at") @db.Timestamptz(6)
993
+
994
+ nodeCount Int @map("node_count")
995
+ edgeCount Int @map("edge_count")
996
+
997
+ clusters Json @db.JsonB
998
+ positions Json @db.JsonB
999
+
1000
+ ontologyVersion String @map("ontology_version")
1001
+
1002
+ // max(updated_at) over the owner's kg_entities/kg_relations read by this run — the staleness
1003
+ // check `GET /api/graph/overview` compares against a fresh count, never recomputing itself.
1004
+ sourceUpdatedAt DateTime? @map("source_updated_at") @db.Timestamptz(6)
1005
+
1006
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
1007
+
1008
+ @@index([ownerId, computedAt(sort: Desc)])
1009
+ @@map("kg_graph_layouts")
1010
+ }
1011
+
1012
+ // Back-relations onto the platform's User (an `extend model` block holds back-relation fields only).
1013
+ extend model User {
1014
+ // Connected Knowledge (epic #344, issue #351). Every `kg_*` table carries
1015
+ // its own `owner_id` and CASCADES — an entity, relation, item, piece of
1016
+ // evidence, mention, proposal, merge, distinct-pair or attribute
1017
+ // definition has no meaning and no permission path to read it once its
1018
+ // owner is gone, the identical reasoning `notes.owner_id`/
1019
+ // `transcripts.owner_id` already establish (§10, and there is no
1020
+ // `graph:read_any` for the same reason there is no `notes:read_any`).
1021
+ // `kgEntityViews` is the one exception: it is keyed on `user_id`, not
1022
+ // `owner_id`, because "which entities has THIS user looked at" is a
1023
+ // per-viewer fact, not an ownership fact — see the block comment above
1024
+ // `KgEntityView`.
1025
+ kgEntities KgEntity[]
1026
+ kgEntityAliases KgEntityAlias[]
1027
+ kgRelations KgRelation[]
1028
+ kgItems KgItem[]
1029
+ kgEvidence KgEvidence[]
1030
+ kgMentions KgMention[]
1031
+ kgProposals KgProposal[]
1032
+ kgMerges KgMerge[]
1033
+ kgDistinctPairs KgDistinctPair[]
1034
+ kgAttributeDefs KgAttributeDef[]
1035
+ kgEntityDigests KgEntityDigest[]
1036
+ kgEntityViews KgEntityView[]
1037
+ kgGraphLayouts KgGraphLayout[]
1038
+
1039
+ // #386 — RDF exports of the owner's graph. CASCADES: a disposable,
1040
+ // reproducible artefact with its own 7-day expiry, like `note_exports`.
1041
+ kgExports KgExport[]
1042
+ }
1043
+
1044
+ // Back-relations onto the platform's StorageObject (an `extend model` block holds back-relation fields only).
1045
+ extend model StorageObject {
1046
+ // Connected Knowledge (epic #344, issue #351). SetNull — an import's
1047
+ // evidence row is a citation into the imported file, and survives losing
1048
+ // the object the same way a transcript/note evidence pointer does. See
1049
+ // the block comment above `KgEvidence`.
1050
+ kgEvidenceImports KgEvidence[]
1051
+
1052
+ // #386 — a rendered graph export (`managed_by: 'graph'`). Restrict, the
1053
+ // `noteExportFiles` reasoning: only the export sweep / `kg.purge`, clearing
1054
+ // the `kg_exports` reference first, may remove the file.
1055
+ kgExportFiles KgExport[]
1056
+ }
1057
+
1058
+ // Back-relations onto the platform's Job (an `extend model` block holds back-relation fields only).
1059
+ extend model Job {
1060
+ // The `kg_proposals` row this job is extracting (or extracted) into, when
1061
+ // `type` is `'kg.extract'` (epic #344, issue #351). Same one-to-one shape
1062
+ // as `transcriptNameCheck`/`noteGeneration`/`noteExport`/`transcriptExport`/
1063
+ // `backupRun` above, for the same reason: `KgProposal.jobId` is
1064
+ // `@unique`/nullable/`SetNull` because a proposal's own review lifetime is
1065
+ // independent of `job.history.purge`'s schedule for this row.
1066
+ kgProposal KgProposal?
1067
+
1068
+ // The `kg_exports` row this job rendered, when `type` is `'kg.export'`
1069
+ // (issue #386). The `noteExport` shape exactly: `KgExport.jobId` is
1070
+ // `@unique`/nullable/`SetNull` because an export's own 7-day retention is
1071
+ // independent of `job.history.purge`'s schedule for this row.
1072
+ kgExport KgExport?
1073
+ }