@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,359 @@
1
+ // GENERATED by platform db compose — do not edit
2
+ // source: package:db-backup.prisma
3
+
4
+ // =============================================================================
5
+ // Database Backup Runs (issue #281, epic #254)
6
+ // =============================================================================
7
+ //
8
+ // One row per attempt to dump this deployment's PostgreSQL database into
9
+ // object storage — and, from Phase 7 onwards, per attempt to restore FROM one.
10
+ // `DatabaseBackupRunnerService` (`src/db-backup/db-backup-runner.service.ts`)
11
+ // is the only writer of the backup half; the columns in the restore-audit
12
+ // group are declared here now and written by #285.
13
+ //
14
+ // -----------------------------------------------------------------------------
15
+ // ### Why this WAS not a queue job, and why it now is (issue #351, epic #345)
16
+ // -----------------------------------------------------------------------------
17
+ //
18
+ // ⚠ THIS SECTION USED TO SAY "DO NOT MIGRATE THIS ONTO THE QUEUE". It has
19
+ // been migrated, deliberately, and the history is kept because the three
20
+ // objections were correct and a reader needs to know WHERE EACH IS NOW
21
+ // ANSWERED rather than assume they were waved away.
22
+ //
23
+ // 1. `jobs.stuckThresholdMinutes` DEFAULTS TO 30 MINUTES. A dump of a real
24
+ // production database routinely runs longer than that. The lease reaper
25
+ // (`JobStuckResetTask`) would see a `running` job whose lease had
26
+ // expired, conclude its executor had died, and RESET IT TO `pending` —
27
+ // while `pg_dump` was still streaming. The next claim starts a SECOND
28
+ // `pg_dump`, writing to the SAME derived storage key as the first, and
29
+ // two processes interleave their output into one object.
30
+ // → ANSWERED BY #346's PER-TYPE PROFILES. `db.backup.run` declares
31
+ // `maxRuntimeMs: 6h`, and the claim's lease is DERIVED from that ceiling
32
+ // (`resolveJobLeaseMs`), never declared beside it — so a lease shorter
33
+ // than the permitted runtime is unrepresentable rather than merely
34
+ // avoided.
35
+ // 2. THE IN-PROCESS WORKER HAS NO LEASE-RENEWAL PATH. `JobWorker` claimed
36
+ // with a lease and never extended it, so the reaper's verdict above was
37
+ // not a rare race but unconditional for any long job.
38
+ // → ANSWERED BY #347. The worker renews on a ticker (lease ÷ 3) for
39
+ // exactly as long as `process()` runs.
40
+ // 3. A JOB HAS `attempts` AND A RETRY BUDGET. Automatically re-running a
41
+ // failed multi-gigabyte dump is exactly the wrong behaviour: it burns
42
+ // hours of I/O on a database that is probably already unwell, and the
43
+ // correct retry for a backup is "the next scheduled one", tomorrow.
44
+ // → ANSWERED BY `maxAttempts: 1` on the same profile. The policy did not
45
+ // change; what changed is that `JobStuckService`'s give-up phase now
46
+ // ENFORCES it, instead of it holding by the accident of there being no
47
+ // queue to disagree.
48
+ //
49
+ // This table still has its OWN heartbeat (`lastHeartbeatAt`, written every
50
+ // ~20s with live `bytesWritten`), its OWN staleness policy
51
+ // (`databaseBackup.runStaleMinutes`) and its OWN terminal states, and all
52
+ // three are still load-bearing — but their jobs have narrowed. The heartbeat
53
+ // is now PROGRESS REPORTING for the UI (the lease is the liveness signal),
54
+ // and the stale sweep is the backstop for the one path that still has no job:
55
+ // the `pre_restore` safety dump, which `DatabaseRestoreService` takes through
56
+ // `startBackup` because a restore must not wait on a worker slot.
57
+ //
58
+ // ⚠ THE TWO ROWS ARE NOT REDUNDANT. The `jobs` row answers who asked, when,
59
+ // which executor, which attempt and what error; this row answers what was
60
+ // PRODUCED — key, bucket, bytes, checksum, `verifiedAt`, the db/app/migration
61
+ // audit and the restore audit. They also have independent lifetimes: a job is
62
+ // purged by `job.history.purge` on a retention schedule that has nothing to do
63
+ // with how long the archive is kept, which is exactly why `jobId` is
64
+ // `onDelete: SetNull` and not `Cascade`.
65
+ //
66
+ // -----------------------------------------------------------------------------
67
+ // THE SINGLE-ACTIVE-RUN GUARD IS A PARTIAL UNIQUE INDEX, NOT APPLICATION CODE
68
+ // -----------------------------------------------------------------------------
69
+ //
70
+ // `database_backup_runs_active_uniq_idx` (hand-written in
71
+ // `prisma/migrations/20260907120000_add_database_backup_runs/migration.sql`,
72
+ // TIGHTENED by
73
+ // `prisma/migrations/<add_backup_run_job_link>/migration.sql`, epic #345,
74
+ // issue #351 — see that migration's own header for why) is a UNIQUE index on
75
+ // the CONSTANT EXPRESSION `(true)` restricted to `status IN
76
+ // ('pending','running')`. It is what makes "at most one backup at a time"
77
+ // true ACROSS REPLICAS.
78
+ //
79
+ // REJECTED: a `findFirst({ where: { status: 'running' } })` before the insert.
80
+ // It is a check-then-act race, and it is racy exactly when it matters — a
81
+ // scheduled tick on replica A and an admin's manual click on replica B, in
82
+ // the same second. Both would see no active run, both would insert, and two
83
+ // `pg_dump` processes would stream into two objects while the settings say
84
+ // one backup a night. Only the database can make "is one already active"
85
+ // atomic with the insert that would violate it.
86
+ //
87
+ // The loser's Prisma `P2002` becomes a typed `DatabaseBackupAlreadyRunningError`
88
+ // carrying the winner's run id, which #282/#283 turn into a 409.
89
+ //
90
+ // ⚠ PRISMA CANNOT EXPRESS THIS INDEX — INTENTIONAL SCHEMA DRIFT. The schema
91
+ // language has no syntax for a partial index OR an expression index, exactly
92
+ // as recorded above the `Job` model for `jobs_active_dedup_uniq_idx` and its
93
+ // two siblings. So `prisma migrate dev`/`diff` will not regenerate it and, if
94
+ // anyone diffs this schema against a live database, will want to DROP it.
95
+ // That drift is deliberate and permanent: do not "fix" it, and do not try to
96
+ // force it into `@@unique` syntax that does not exist. The migration file
97
+ // carries the same warning next to the statement itself.
98
+ //
99
+ // ⚠ WHAT THE INDEX ADMITS, PRECISELY, AND WHY THAT CHANGED. The ORIGINAL index
100
+ // keyed on the `status` COLUMN, filtered to two values — that admits at most
101
+ // one `pending` row AND at most one `running` row AT THE SAME TIME, i.e. TWO
102
+ // active runs, not one. That was harmless only as long as
103
+ // `DatabaseBackupRunnerService` inserted its claim directly as `running` and
104
+ // never wrote `pending`. #351 makes the backup a queue job: `POST
105
+ // /api/admin/db-backup/runs` now enqueues the job and creates the run row in
106
+ // `pending` BEFORE any worker has claimed it (so the endpoint can return a run
107
+ // id immediately and `GET /runs/{id}` keeps working), and the handler flips it
108
+ // to `running` once claimed. The moment `pending` rows are real, the
109
+ // column-keyed index stops meaning "at most one active run" and starts
110
+ // meaning "at most two". Keying on `(true)` instead makes every active row —
111
+ // whichever of the two statuses it holds — index to the SAME key, so at most
112
+ // one can exist across both statuses COMBINED. This is the exact tightening
113
+ // this comment always said a future path needing both states populated at
114
+ // once would require ("tighten this index to a constant expression, never
115
+ // move the guard into a service") — it has now happened.
116
+ //
117
+ // -----------------------------------------------------------------------------
118
+ // WHY THE RESTORE AUDIT LIVES ON THE BACKUP'S OWN ROW
119
+ // -----------------------------------------------------------------------------
120
+ //
121
+ // `restoreStatus`, `restoreError`, `restoredAt`, `restoredById`,
122
+ // `restoreScratchDb`, `restoreOldDb`, `swappedAt` and `preRestoreBackupId` are
123
+ // all nullable and all belong to Phase 7. They are columns on THIS table
124
+ // rather than a `database_restore_runs` table because A RESTORE IS ALWAYS
125
+ // DEFINED IN TERMS OF EXACTLY ONE BACKUP: there is no restore without an
126
+ // archive to restore, no archive is restored twice concurrently, and every
127
+ // question anyone asks about a restore ("which dump was this?", "how big was
128
+ // it?", "what migration was the schema on?") is answered by the backup's own
129
+ // columns. A separate table would buy a join on a strict 1:1 relationship and
130
+ // would let the two rows disagree about which archive was replayed.
131
+ //
132
+ // The cost is honest: a backup restored twice (a restore that failed, then
133
+ // retried) overwrites the first attempt's audit fields. That is the correct
134
+ // trade — the interesting record is the state of the LAST restore, and the
135
+ // full history of attempts is in `audit_events`.
136
+ //
137
+ // `preRestoreBackupId` is a SELF-FK: before a restore swaps a database away,
138
+ // #285 takes a `pre_restore` backup, and this column points the restore at the
139
+ // safety net it took. `onDelete: SetNull`, so pruning that safety backup by
140
+ // retention never deletes the restore record that referenced it.
141
+ model DatabaseBackupRun {
142
+ id String @id @default(uuid()) @db.Uuid
143
+
144
+ // The `jobs` row that drove this run, once the dump became a queue job
145
+ // (`db.backup.run`, epic #345, issue #351). Three properties, each
146
+ // load-bearing:
147
+ //
148
+ // 1. `@unique` is what makes a SECOND run row for one job
149
+ // UNREPRESENTABLE. #348 lets a handler derive its own node upload
150
+ // key, and a node may legitimately ask for that key twice (a
151
+ // timed-out transfer, a lost response, a restarted process still
152
+ // holding the lease). The backup's `deriveOutputKey` re-reads by
153
+ // `jobId` and returns the existing key; the unique index is what
154
+ // GUARANTEES there is at most one to find, rather than that being a
155
+ // convention the service has to remember.
156
+ //
157
+ // 2. Nullable, ON PURPOSE. Not every run has a job: the `pre_restore`
158
+ // dump `DatabaseRestoreService` takes goes through the runner
159
+ // directly, with no job in between, and rows predating this
160
+ // migration have no job at all. A required column would either break
161
+ // the restore path or need a backfill that invents job rows for
162
+ // history.
163
+ //
164
+ // 3. `onDelete: SetNull`, NOT `Cascade`. `job.history.purge` deletes
165
+ // `jobs` rows on a retention schedule that is completely independent
166
+ // of the archive retention. Cascade would let a routine history purge
167
+ // delete the record of a backup whose archive is still in the bucket
168
+ // and still restorable — the exact "deleting history must not delete
169
+ // history" hazard `JobStatsRollup` already exists to avoid on the
170
+ // stats side. Losing the LINK is acceptable; losing the RUN ROW is
171
+ // not.
172
+ jobId String? @unique @map("job_id") @db.Uuid
173
+ job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
174
+
175
+ // `pending` = claimed, dump not yet spawned. `running` = a `pg_dump` is
176
+ // streaming. `completed` = uploaded AND verified (see `verifiedAt`).
177
+ // `failed` = terminal, `lastError` says why. `stale` = the run's heartbeat
178
+ // stopped for longer than `databaseBackup.runStaleMinutes` and #282's sweep
179
+ // gave up on it — DISTINCT from `failed` on purpose, because nothing
180
+ // observed this run fail; the process holding it disappeared, and an
181
+ // operator reading the list needs to be able to tell "the dump errored"
182
+ // from "the container went away mid-dump".
183
+ status DatabaseBackupStatus @default(pending)
184
+
185
+ // `manual` = an administrator clicked. `scheduled` = #282's cron.
186
+ // `pre_restore` = the safety backup #285 takes immediately before swapping
187
+ // a restored database into place. No default: a run with no known origin is
188
+ // a run nobody can attribute, and every caller knows which it is.
189
+ trigger DatabaseBackupTrigger
190
+
191
+ // NULL until the dump is claimed / until it settles. `lastHeartbeatAt` is
192
+ // now PROGRESS REPORTING for the UI rather than this table's liveness
193
+ // signal — the job's LEASE is the liveness signal since #351/#352 — see the
194
+ // "Why this WAS not a queue job, and why it now is" block above.
195
+ startedAt DateTime? @map("started_at") @db.Timestamptz
196
+ finishedAt DateTime? @map("finished_at") @db.Timestamptz
197
+ lastHeartbeatAt DateTime? @map("last_heartbeat_at") @db.Timestamptz
198
+
199
+ // BigInt, both of them, and NOT `Int`. A PostgreSQL dump crossing 2 GiB is
200
+ // ordinary, and `Int` is a signed 32-bit column that overflows at 2147483647
201
+ // bytes — the failure would be a write error on the LARGEST backups, i.e.
202
+ // exactly the deployments this feature exists for, and it would surface as
203
+ // a failed run rather than as a wrong number.
204
+ //
205
+ // ⚠ A `BigInt` field is the JS `bigint` primitive, which `JSON.stringify`
206
+ // REFUSES to serialise. #283's response DTO must convert these two to
207
+ // `string` (or `Number`) explicitly — see the note above `JobStatsRollup`
208
+ // for the same hazard, decided the other way there because that column is a
209
+ // running average and this one is a byte count that must stay exact.
210
+ //
211
+ // `bytesWritten` is LIVE PROGRESS, rewritten by the heartbeat while the dump
212
+ // streams; `sizeBytes` is the final size, written once at completion. They
213
+ // converge on success and deliberately do not on a failure, where the
214
+ // difference is "how far did it get".
215
+ bytesWritten BigInt @default(0) @map("bytes_written")
216
+ sizeBytes BigInt @default(0) @map("size_bytes")
217
+
218
+ // The storage triple: WHICH provider, WHICH key, WHICH bucket. All three
219
+ // are recorded rather than derived on read, because all three can change
220
+ // under a deployment (a bucket rename, a provider swap) and a backup that
221
+ // cannot say where it was written is a backup nobody can restore. The key
222
+ // is chosen by the SERVER (`buildBackupStorageKey`), never by a caller.
223
+ storageProvider String @map("storage_provider")
224
+ storageKey String @map("storage_key")
225
+ bucket String
226
+
227
+ // `pg_dump`'s archive format — `custom` (`-Fc`) for everything this
228
+ // repository writes. Stored so a future format change does not make old
229
+ // archives unreadable-by-inspection: `pg_restore` needs to know, and
230
+ // guessing from the key's extension is not knowing.
231
+ format String
232
+
233
+ // sha256 of the archive, computed IN THE SAME SINGLE PASS as `sizeBytes`
234
+ // while the bytes stream past on their way to storage — the archive is
235
+ // never buffered and never read twice. NULL until the upload completes.
236
+ checksumSha256 String? @map("checksum_sha256")
237
+
238
+ // The audit quartet: what produced this archive. All four nullable because
239
+ // each is read best-effort and NONE of them may fail a backup — a run that
240
+ // could not read the server version is still a valid backup.
241
+ //
242
+ // `dbVersion` — the PostgreSQL server version this was dumped from.
243
+ // A restore onto an older major will not work, and this
244
+ // is the only place that fact is recorded.
245
+ // `appVersion` — the application build (`resolveApiVersion()`).
246
+ // `migrationName` — the newest applied `_prisma_migrations` row. THE most
247
+ // important of the four for a restore: it says which
248
+ // schema the archive contains, so an operator can tell
249
+ // before replaying it whether the running code will
250
+ // understand what comes back.
251
+ // `pgDumpVersion` — the `pg_dump` client that WROTE the archive (#352,
252
+ // epic #345). It was a trio until the dump could run on
253
+ // a worker node: on that path the client is a binary on
254
+ // a machine this deployment may not own and cannot
255
+ // inspect, so unless the node reports it, nothing
256
+ // anywhere records which `pg_dump` produced the file —
257
+ // and a `pg_restore` older than the `pg_dump` that wrote
258
+ // an archive simply cannot read it. The server path
259
+ // fills the same column from its own image's client, so
260
+ // the row means one thing whichever executor ran.
261
+ dbVersion String? @map("db_version")
262
+ appVersion String? @map("app_version")
263
+ migrationName String? @map("migration_name")
264
+ pgDumpVersion String? @map("pg_dump_version")
265
+
266
+ // Set when the UPLOADED OBJECT was streamed back out of storage and read by
267
+ // `pg_restore --list` with a non-empty table of contents. A run is never
268
+ // `completed` without it: the checksum proves what we sent, this proves what
269
+ // arrived is a readable archive.
270
+ verifiedAt DateTime? @map("verified_at") @db.Timestamptz
271
+
272
+ lastError String? @map("last_error")
273
+
274
+ // NULL for a `scheduled` run — nobody asked for it, a timer did. `SetNull`
275
+ // rather than `Cascade`, matching `AuditEvent.actorUserId` and
276
+ // `Credential.updatedByUser`: this is a HISTORY RECORD, and deleting an
277
+ // administrator's account must never delete the record that a backup was
278
+ // taken. Contrast `WorkerNode.createdBy`, which cascades because a node is
279
+ // live inventory rather than history.
280
+ createdById String? @map("created_by_id") @db.Uuid
281
+ createdBy User? @relation("UserDatabaseBackupRuns", fields: [createdById], references: [id], onDelete: SetNull)
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Restore audit (Phase 7). Declared by #281, written by #285's
285
+ // `DatabaseRestoreService`. See the "WHY THE RESTORE AUDIT LIVES ON THE
286
+ // BACKUP'S OWN ROW" block above.
287
+ // ---------------------------------------------------------------------------
288
+
289
+ // A plain string, not an enum, for the same reason `Job.executor` is one: a
290
+ // small closed set owned by code rather than by this table's own state
291
+ // machine, and adding a state to it must not cost a migration. NULL means
292
+ // "this backup has never been restored".
293
+ //
294
+ // `restoring` -> `verifying` -> `swapping` -> `completed` | `failed`, plus
295
+ // `rolled_back` for a restore that was swapped in and then undone. That last
296
+ // one is the argument for the column being a string: it was added by #285
297
+ // without a migration. The set lives in `RESTORE_STATUSES`
298
+ // (`src/db-backup/database-restore.service.ts`).
299
+ restoreStatus String? @map("restore_status")
300
+ restoreError String? @map("restore_error")
301
+
302
+ restoredAt DateTime? @map("restored_at") @db.Timestamptz
303
+ restoredById String? @map("restored_by_id") @db.Uuid
304
+ restoredBy User? @relation("UserDatabaseRestores", fields: [restoredById], references: [id], onDelete: SetNull)
305
+
306
+ // The names of the two databases a swap-style restore creates and displaces:
307
+ // the scratch database the archive was replayed into, and the original that
308
+ // was renamed out of the way. Recorded as NAMES because after the swap they
309
+ // are the only handle an operator has on either — `databaseBackup
310
+ // .restoreRollbackMode` decides whether the displaced one survives, and
311
+ // `oldDatabaseRetentionHours` decides for how long.
312
+ restoreScratchDb String? @map("restore_scratch_db")
313
+ restoreOldDb String? @map("restore_old_db")
314
+ swappedAt DateTime? @map("swapped_at") @db.Timestamptz
315
+
316
+ // The `pre_restore` backup taken immediately before this row's archive was
317
+ // swapped in — the way back. `SetNull` so retention pruning that safety
318
+ // backup never deletes the restore record pointing at it.
319
+ preRestoreBackupId String? @map("pre_restore_backup_id") @db.Uuid
320
+ preRestoreBackup DatabaseBackupRun? @relation("PreRestoreBackup", fields: [preRestoreBackupId], references: [id], onDelete: SetNull)
321
+ // The back-reference Prisma requires for the self-relation above. Nothing
322
+ // reads it today; it exists because a named relation needs both sides.
323
+ restoresUsingThis DatabaseBackupRun[] @relation("PreRestoreBackup")
324
+
325
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
326
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
327
+
328
+ // The queries #282 and #283 issue, and nothing speculative:
329
+ //
330
+ // - the admin list and the retention sweep are both "newest first", which
331
+ // is `createdAt DESC`;
332
+ // - `startedAt DESC` answers "when did backups actually run", which is a
333
+ // different question from "when were they requested" for any run that
334
+ // waited or never started at all;
335
+ // - `[status]` backs the stale sweep (`status = 'running'`) and the
336
+ // active-run lookup that turns a P2002 into an id;
337
+ // - `[status, createdAt DESC]` is the retention query — "the completed
338
+ // runs, newest first, keep N" — which neither single-column index
339
+ // answers without a sort.
340
+ @@index([createdAt(sort: Desc)])
341
+ @@index([startedAt(sort: Desc)])
342
+ @@index([status])
343
+ @@index([status, createdAt(sort: Desc)])
344
+ @@map("database_backup_runs")
345
+ }
346
+
347
+ enum DatabaseBackupStatus {
348
+ pending
349
+ running
350
+ completed
351
+ failed
352
+ stale
353
+ }
354
+
355
+ enum DatabaseBackupTrigger {
356
+ manual
357
+ scheduled
358
+ pre_restore
359
+ }