@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/ask/index.d.ts +6 -0
- package/dist/ask/index.js +28 -0
- package/dist/ask/question-schema.d.ts +121 -0
- package/dist/ask/question-schema.js +167 -0
- package/dist/ask/report.d.ts +88 -0
- package/dist/ask/report.js +170 -0
- package/dist/ask/scorer.d.ts +104 -0
- package/dist/ask/scorer.js +164 -0
- package/dist/ask-run/ask-command.d.ts +62 -0
- package/dist/ask-run/ask-command.js +254 -0
- package/dist/ask-run/host/app.d.ts +65 -0
- package/dist/ask-run/host/app.js +146 -0
- package/dist/ask-run/host/db.d.ts +38 -0
- package/dist/ask-run/host/db.js +126 -0
- package/dist/ask-run/host/ports.d.ts +35 -0
- package/dist/ask-run/host/ports.js +113 -0
- package/dist/ask-run/index.d.ts +13 -0
- package/dist/ask-run/index.js +35 -0
- package/dist/ask-run/replay.d.ts +128 -0
- package/dist/ask-run/replay.js +212 -0
- package/dist/ask-run/run-ask-eval.d.ts +125 -0
- package/dist/ask-run/run-ask-eval.js +193 -0
- package/dist/ask-run/seed-golden-graph.d.ts +64 -0
- package/dist/ask-run/seed-golden-graph.js +495 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/kg-command.d.ts +66 -0
- package/dist/cli/kg-command.js +238 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +69 -0
- package/dist/fixtures/domains.d.ts +16 -0
- package/dist/fixtures/domains.js +33 -0
- package/dist/fixtures/fixture-schema.d.ts +323 -0
- package/dist/fixtures/fixture-schema.js +199 -0
- package/dist/fixtures/index.d.ts +16 -0
- package/dist/fixtures/index.js +40 -0
- package/dist/fixtures/load.d.ts +73 -0
- package/dist/fixtures/load.js +137 -0
- package/dist/fixtures/questions.d.ts +15 -0
- package/dist/fixtures/questions.js +26 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +36 -0
- package/dist/kg/gold-runner.d.ts +13 -0
- package/dist/kg/gold-runner.js +57 -0
- package/dist/kg/index.d.ts +17 -0
- package/dist/kg/index.js +65 -0
- package/dist/kg/internals.d.ts +7 -0
- package/dist/kg/internals.js +17 -0
- package/dist/kg/prediction-schema.d.ts +245 -0
- package/dist/kg/prediction-schema.js +102 -0
- package/dist/kg/predictions.d.ts +11 -0
- package/dist/kg/predictions.js +49 -0
- package/dist/kg/report.d.ts +84 -0
- package/dist/kg/report.js +125 -0
- package/dist/kg/run-kg-eval.d.ts +77 -0
- package/dist/kg/run-kg-eval.js +137 -0
- package/dist/kg/runner.d.ts +72 -0
- package/dist/kg/runner.js +51 -0
- package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
- package/dist/kg/runners/extract-resolve-runner.js +199 -0
- package/dist/kg/runners/extract-runner.d.ts +65 -0
- package/dist/kg/runners/extract-runner.js +243 -0
- package/dist/kg/scorer.d.ts +226 -0
- package/dist/kg/scorer.js +463 -0
- package/dist/kg/targets.d.ts +17 -0
- package/dist/kg/targets.js +16 -0
- package/dist/kg/text.d.ts +45 -0
- package/dist/kg/text.js +100 -0
- package/dist/live/adapters.d.ts +16 -0
- package/dist/live/adapters.js +58 -0
- package/dist/live/env.d.ts +39 -0
- package/dist/live/env.js +56 -0
- package/dist/live/index.d.ts +6 -0
- package/dist/live/index.js +16 -0
- package/dist/live/live-ai.d.ts +104 -0
- package/dist/live/live-ai.js +157 -0
- package/dist/parity/constants.d.ts +79 -0
- package/dist/parity/constants.js +104 -0
- package/dist/parity/index.d.ts +20 -0
- package/dist/parity/index.js +38 -0
- package/dist/parity/internals.d.ts +8 -0
- package/dist/parity/internals.js +16 -0
- package/dist/parity/scenario.d.ts +45 -0
- package/dist/parity/scenario.js +245 -0
- package/dist/parity/script.d.ts +44 -0
- package/dist/parity/script.js +136 -0
- package/dist/parity/snapshot.d.ts +43 -0
- package/dist/parity/snapshot.js +71 -0
- package/dist/real-data/index.d.ts +2 -0
- package/dist/real-data/index.js +12 -0
- package/dist/real-data/real-data.d.ts +76 -0
- package/dist/real-data/real-data.js +156 -0
- package/fixtures/kg-golden/README.md +136 -0
- package/fixtures/kg-golden/ask-questions.json +458 -0
- package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
- package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
- package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
- package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
- package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
- package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
- package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
- package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
- package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
- package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
- package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
- package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
- package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
- package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
- package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
- package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
- package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
- package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
- package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
- package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
- package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
- package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
- package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
- package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
- package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
- package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
- package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
- package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
- package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
- package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
- package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
- package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
- package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
- package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
- package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
- package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
- package/host/generated/client.d.ts +1 -0
- package/host/generated/client.js +5 -0
- package/host/generated/default.d.ts +1 -0
- package/host/generated/default.js +5 -0
- package/host/generated/edge.d.ts +1 -0
- package/host/generated/edge.js +1658 -0
- package/host/generated/index-browser.js +1684 -0
- package/host/generated/index.d.ts +192335 -0
- package/host/generated/index.js +1658 -0
- package/host/generated/package.json +139 -0
- package/host/generated/query_compiler_fast_bg.js +2 -0
- package/host/generated/query_compiler_fast_bg.wasm +0 -0
- package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/host/generated/runtime/client.d.ts +3433 -0
- package/host/generated/runtime/client.js +110 -0
- package/host/generated/runtime/index-browser.d.ts +90 -0
- package/host/generated/runtime/index-browser.js +6 -0
- package/host/generated/runtime/wasm-compiler-edge.js +100 -0
- package/host/generated/schema.prisma +6312 -0
- package/host/generated/wasm-edge-light-loader.mjs +5 -0
- package/host/generated/wasm-worker-loader.mjs +5 -0
- package/host/prisma/fragments/kvox.ask.prisma +126 -0
- package/host/prisma/fragments/kvox.graph.prisma +1073 -0
- package/host/prisma/fragments/kvox.notes.prisma +787 -0
- package/host/prisma/fragments/kvox.search.prisma +211 -0
- package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
- package/host/prisma/kvox.lock +101 -0
- package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
- package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
- package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
- package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
- package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
- package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
- package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
- package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
- package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
- package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
- package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
- package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
- package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
- package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
- package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
- package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
- package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
- package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
- package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
- package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
- package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
- package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
- package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
- package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
- package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
- package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
- package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
- package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
- package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
- package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
- package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
- package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
- package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
- package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
- package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
- package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
- package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
- package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
- package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
- package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
- package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
- package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
- package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
- package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
- package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
- package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
- package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
- package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
- package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
- package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
- package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
- package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
- package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
- package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
- package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
- package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
- package/host/prisma/platform.lock +319 -0
- package/host/prisma/schema/app.kvox.ask.prisma +110 -0
- package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
- package/host/prisma/schema/app.kvox.notes.prisma +746 -0
- package/host/prisma/schema/app.kvox.search.prisma +208 -0
- package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
- package/host/prisma/schema/platform.ai.prisma +419 -0
- package/host/prisma/schema/platform.android-app.prisma +46 -0
- package/host/prisma/schema/platform.base.prisma +15 -0
- package/host/prisma/schema/platform.credentials.prisma +156 -0
- package/host/prisma/schema/platform.db-backup.prisma +359 -0
- package/host/prisma/schema/platform.identity.prisma +638 -0
- package/host/prisma/schema/platform.jobs.prisma +824 -0
- package/host/prisma/schema/platform.notifications.prisma +641 -0
- package/host/prisma/schema/platform.settings.prisma +53 -0
- package/host/prisma/schema/platform.sharing.prisma +208 -0
- package/host/prisma/schema/platform.storage.prisma +118 -0
- package/package.json +112 -1
- package/parity/requests.ask.json +2681 -0
- package/parity/requests.m01.json +1554 -0
- package/parity/requests.m02.json +1554 -0
- package/parity/requests.m03.json +1554 -0
- package/parity/requests.m04.json +1554 -0
- package/parity/requests.m05.json +1554 -0
- package/parity/requests.m06.json +1554 -0
- package/parity/requests.m07.json +1554 -0
- package/parity/requests.m08.json +1554 -0
- package/parity/requests.m09.json +1554 -0
- package/parity/requests.m10.json +1554 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Worker Nodes (issue #267, epic #254)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- Adds `worker_nodes` and `node_credentials`, and wires the FK on
|
|
5
|
+
-- `jobs.claimed_by_node_id` that was deliberately left unconstrained by
|
|
6
|
+
-- 20260906120000_add_jobs (see that migration's own header comment, and the
|
|
7
|
+
-- block comment above the `Job` model and the `WorkerNode` model in
|
|
8
|
+
-- prisma/schema.prisma for the full reasoning behind every choice below —
|
|
9
|
+
-- this file intentionally does not restate it at length).
|
|
10
|
+
-- =============================================================================
|
|
11
|
+
|
|
12
|
+
-- CreateEnum
|
|
13
|
+
CREATE TYPE "NodeStatus" AS ENUM ('online', 'draining', 'offline', 'disabled');
|
|
14
|
+
|
|
15
|
+
-- CreateTable
|
|
16
|
+
CREATE TABLE "worker_nodes" (
|
|
17
|
+
"id" UUID NOT NULL,
|
|
18
|
+
"name" TEXT NOT NULL,
|
|
19
|
+
"hostname" TEXT NOT NULL,
|
|
20
|
+
"platform" TEXT NOT NULL,
|
|
21
|
+
"cli_version" TEXT NOT NULL,
|
|
22
|
+
"eligible_types" TEXT[],
|
|
23
|
+
"concurrency" INTEGER NOT NULL,
|
|
24
|
+
"status" "NodeStatus" NOT NULL DEFAULT 'offline',
|
|
25
|
+
"capabilities" JSONB,
|
|
26
|
+
"registered_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
+
"last_heartbeat_at" TIMESTAMPTZ,
|
|
28
|
+
"created_by_id" UUID NOT NULL,
|
|
29
|
+
|
|
30
|
+
CONSTRAINT "worker_nodes_pkey" PRIMARY KEY ("id")
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- CreateTable
|
|
34
|
+
CREATE TABLE "node_credentials" (
|
|
35
|
+
"id" UUID NOT NULL,
|
|
36
|
+
"user_id" UUID NOT NULL,
|
|
37
|
+
"name" TEXT NOT NULL,
|
|
38
|
+
"token_hash" TEXT NOT NULL,
|
|
39
|
+
"token_prefix" TEXT NOT NULL,
|
|
40
|
+
"expires_at" TIMESTAMPTZ,
|
|
41
|
+
"last_used_at" TIMESTAMPTZ,
|
|
42
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
43
|
+
"revoked_at" TIMESTAMPTZ,
|
|
44
|
+
|
|
45
|
+
CONSTRAINT "node_credentials_pkey" PRIMARY KEY ("id")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- CreateIndex
|
|
49
|
+
-- The register-or-reattach anchor (#268's own words: "existing exactly this
|
|
50
|
+
-- way") — see the block comment above the `WorkerNode` model.
|
|
51
|
+
CREATE UNIQUE INDEX "worker_nodes_created_by_id_name_key" ON "worker_nodes"("created_by_id", "name");
|
|
52
|
+
|
|
53
|
+
-- CreateIndex
|
|
54
|
+
-- "Every online/draining/offline/disabled node" — the fleet page's primary
|
|
55
|
+
-- filter, and what #270's lifecycle cron scans.
|
|
56
|
+
CREATE INDEX "worker_nodes_status_idx" ON "worker_nodes"("status");
|
|
57
|
+
|
|
58
|
+
-- CreateIndex
|
|
59
|
+
-- "This user's own nodes" — backs `GET /nodes` and `assertOwnership` (#268).
|
|
60
|
+
CREATE INDEX "worker_nodes_created_by_id_idx" ON "worker_nodes"("created_by_id");
|
|
61
|
+
|
|
62
|
+
-- CreateIndex
|
|
63
|
+
CREATE UNIQUE INDEX "node_credentials_token_hash_key" ON "node_credentials"("token_hash");
|
|
64
|
+
|
|
65
|
+
-- CreateIndex
|
|
66
|
+
-- "This user's own credentials" — backs `GET /api/node-credentials`, the
|
|
67
|
+
-- same query shape as `personal_access_tokens_user_id_idx`.
|
|
68
|
+
CREATE INDEX "node_credentials_user_id_idx" ON "node_credentials"("user_id");
|
|
69
|
+
|
|
70
|
+
-- AddForeignKey
|
|
71
|
+
-- onDelete: Cascade — a WorkerNode is a live registration with no standalone
|
|
72
|
+
-- value once its owner is gone (its NodeCredential rows are already gone via
|
|
73
|
+
-- their own Cascade below, so an orphaned node could never authenticate
|
|
74
|
+
-- again). See the block comment above `WorkerNode.createdBy` in
|
|
75
|
+
-- prisma/schema.prisma for the full reasoning, including why this is NOT the
|
|
76
|
+
-- SetNull audit-trail pattern used elsewhere in this schema (AuditEvent,
|
|
77
|
+
-- Credential, NotificationDelivery).
|
|
78
|
+
ALTER TABLE "worker_nodes" ADD CONSTRAINT "worker_nodes_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
79
|
+
|
|
80
|
+
-- AddForeignKey
|
|
81
|
+
-- onDelete: Cascade — mirrors `personal_access_tokens_user_id_fkey` exactly
|
|
82
|
+
-- (per issue #267): a node credential is a live authentication artifact with
|
|
83
|
+
-- no standalone value once its owning account is deleted.
|
|
84
|
+
ALTER TABLE "node_credentials" ADD CONSTRAINT "node_credentials_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
85
|
+
|
|
86
|
+
-- AddForeignKey
|
|
87
|
+
-- The FK deferred by 20260906120000_add_jobs. onDelete: SetNull — deleting a
|
|
88
|
+
-- WorkerNode must release the jobs it was holding, never delete job history.
|
|
89
|
+
-- See the "claimedByNodeId" block comment above the `Job` model.
|
|
90
|
+
ALTER TABLE "jobs" ADD CONSTRAINT "jobs_claimed_by_node_id_fkey" FOREIGN KEY ("claimed_by_node_id") REFERENCES "worker_nodes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "DatabaseBackupStatus" AS ENUM ('pending', 'running', 'completed', 'failed', 'stale');
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "DatabaseBackupTrigger" AS ENUM ('manual', 'scheduled', 'pre_restore');
|
|
6
|
+
|
|
7
|
+
-- CreateTable
|
|
8
|
+
CREATE TABLE "database_backup_runs" (
|
|
9
|
+
"id" UUID NOT NULL,
|
|
10
|
+
"status" "DatabaseBackupStatus" NOT NULL DEFAULT 'pending',
|
|
11
|
+
"trigger" "DatabaseBackupTrigger" NOT NULL,
|
|
12
|
+
"started_at" TIMESTAMPTZ,
|
|
13
|
+
"finished_at" TIMESTAMPTZ,
|
|
14
|
+
"last_heartbeat_at" TIMESTAMPTZ,
|
|
15
|
+
"bytes_written" BIGINT NOT NULL DEFAULT 0,
|
|
16
|
+
"size_bytes" BIGINT NOT NULL DEFAULT 0,
|
|
17
|
+
"storage_provider" TEXT NOT NULL,
|
|
18
|
+
"storage_key" TEXT NOT NULL,
|
|
19
|
+
"bucket" TEXT NOT NULL,
|
|
20
|
+
"format" TEXT NOT NULL,
|
|
21
|
+
"checksum_sha256" TEXT,
|
|
22
|
+
"db_version" TEXT,
|
|
23
|
+
"app_version" TEXT,
|
|
24
|
+
"migration_name" TEXT,
|
|
25
|
+
"verified_at" TIMESTAMPTZ,
|
|
26
|
+
"last_error" TEXT,
|
|
27
|
+
"created_by_id" UUID,
|
|
28
|
+
"restore_status" TEXT,
|
|
29
|
+
"restore_error" TEXT,
|
|
30
|
+
"restored_at" TIMESTAMPTZ,
|
|
31
|
+
"restored_by_id" UUID,
|
|
32
|
+
"restore_scratch_db" TEXT,
|
|
33
|
+
"restore_old_db" TEXT,
|
|
34
|
+
"swapped_at" TIMESTAMPTZ,
|
|
35
|
+
"pre_restore_backup_id" UUID,
|
|
36
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
38
|
+
|
|
39
|
+
CONSTRAINT "database_backup_runs_pkey" PRIMARY KEY ("id")
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
-- CreateIndex
|
|
43
|
+
CREATE INDEX "database_backup_runs_created_at_idx" ON "database_backup_runs"("created_at" DESC);
|
|
44
|
+
|
|
45
|
+
-- CreateIndex
|
|
46
|
+
CREATE INDEX "database_backup_runs_started_at_idx" ON "database_backup_runs"("started_at" DESC);
|
|
47
|
+
|
|
48
|
+
-- CreateIndex
|
|
49
|
+
CREATE INDEX "database_backup_runs_status_idx" ON "database_backup_runs"("status");
|
|
50
|
+
|
|
51
|
+
-- CreateIndex
|
|
52
|
+
CREATE INDEX "database_backup_runs_status_created_at_idx" ON "database_backup_runs"("status", "created_at" DESC);
|
|
53
|
+
|
|
54
|
+
-- AddForeignKey
|
|
55
|
+
ALTER TABLE "database_backup_runs" ADD CONSTRAINT "database_backup_runs_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
56
|
+
|
|
57
|
+
-- AddForeignKey
|
|
58
|
+
ALTER TABLE "database_backup_runs" ADD CONSTRAINT "database_backup_runs_restored_by_id_fkey" FOREIGN KEY ("restored_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
59
|
+
|
|
60
|
+
-- AddForeignKey
|
|
61
|
+
ALTER TABLE "database_backup_runs" ADD CONSTRAINT "database_backup_runs_pre_restore_backup_id_fkey" FOREIGN KEY ("pre_restore_backup_id") REFERENCES "database_backup_runs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
62
|
+
|
|
63
|
+
-- =============================================================================
|
|
64
|
+
-- HAND-WRITTEN INDEX — INTENTIONAL SCHEMA DRIFT (issue #281, epic #254)
|
|
65
|
+
-- =============================================================================
|
|
66
|
+
-- The index below is NOT emitted by `prisma migrate dev`/`diff` and never will
|
|
67
|
+
-- be: it needs a partial-index `WHERE` clause, which the Prisma schema language
|
|
68
|
+
-- has no syntax for. It is written here by hand, and the block comment above
|
|
69
|
+
-- the `DatabaseBackupRun` model in prisma/schema.prisma records the same fact
|
|
70
|
+
-- for anyone reading the schema without this file open.
|
|
71
|
+
--
|
|
72
|
+
-- ⚠ `prisma migrate dev` WILL WANT TO DROP IT on the next diff, exactly as it
|
|
73
|
+
-- would for the three partial indexes in 20260906120000_add_jobs. Do not
|
|
74
|
+
-- "reconcile" this migration by accepting that drop, and do not expect
|
|
75
|
+
-- `prisma migrate diff` against a live database to come back clean. This drift
|
|
76
|
+
-- is deliberate and permanent for as long as Prisma's DSL lacks partial
|
|
77
|
+
-- indexes. If a future `migrate dev` run emits a `DROP INDEX
|
|
78
|
+
-- "database_backup_runs_active_uniq_idx"`, delete that line from the generated
|
|
79
|
+
-- migration before committing it.
|
|
80
|
+
-- =============================================================================
|
|
81
|
+
|
|
82
|
+
-- THE single-active-run guard. It is what makes "at most one backup at a time"
|
|
83
|
+
-- true ACROSS REPLICAS, rather than true only within one process: a scheduled
|
|
84
|
+
-- tick on one container and an administrator's click on another cannot both
|
|
85
|
+
-- insert, so two `pg_dump` processes can never stream into the derived storage
|
|
86
|
+
-- key at once. `DatabaseBackupRunnerService.startBackup` inserts optimistically
|
|
87
|
+
-- and turns the loser's P2002 into a typed
|
|
88
|
+
-- `DatabaseBackupAlreadyRunningError` carrying the winner's id; there is
|
|
89
|
+
-- deliberately no `findFirst` pre-check anywhere in that path, because
|
|
90
|
+
-- check-then-act is racy exactly when this constraint matters.
|
|
91
|
+
--
|
|
92
|
+
-- No cast is needed on the literals: Postgres coerces an unadorned string
|
|
93
|
+
-- literal to the column's enum type in an `IN` list, the same way
|
|
94
|
+
-- "jobs_active_dedup_uniq_idx" does for "JobStatus".
|
|
95
|
+
--
|
|
96
|
+
-- ⚠ WHAT THIS ADMITS, PRECISELY: a UNIQUE index on ("status") filtered to two
|
|
97
|
+
-- values permits at most one 'pending' row AND at most one 'running' row. The
|
|
98
|
+
-- runner only ever inserts 'running', so today the ceiling is exactly one
|
|
99
|
+
-- active run; 'pending' is covered so that a future path which does insert one
|
|
100
|
+
-- is still arbitrated here rather than in application code. A future design
|
|
101
|
+
-- that genuinely needs both states populated at once must TIGHTEN this index
|
|
102
|
+
-- (to a constant expression) — never relax the guard into a service.
|
|
103
|
+
CREATE UNIQUE INDEX "database_backup_runs_active_uniq_idx"
|
|
104
|
+
ON "database_backup_runs" ("status") WHERE "status" IN ('pending','running');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Notification Broadcasts (issue #320, epic #319)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- Adds `notification_broadcasts`. See the block comment above the
|
|
5
|
+
-- `NotificationBroadcast` model in prisma/schema.prisma for the full
|
|
6
|
+
-- reasoning behind every choice below (why a table at all rather than
|
|
7
|
+
-- `Job.payload`, why `channels` is a plain string array, why `status` carries
|
|
8
|
+
-- a currently-unreachable `draft` member, why `cursor_user_id` pairs with
|
|
9
|
+
-- `audience_cutoff` to freeze and replay the fan-out's audience, why
|
|
10
|
+
-- `created_by_id` is SetNull, and the rejected per-recipient join table) —
|
|
11
|
+
-- this file intentionally does not restate it at length.
|
|
12
|
+
-- =============================================================================
|
|
13
|
+
|
|
14
|
+
-- CreateEnum
|
|
15
|
+
CREATE TYPE "NotificationBroadcastStatus" AS ENUM ('draft', 'scheduled', 'sending', 'sent', 'canceled', 'failed');
|
|
16
|
+
|
|
17
|
+
-- CreateTable
|
|
18
|
+
CREATE TABLE "notification_broadcasts" (
|
|
19
|
+
"id" UUID NOT NULL,
|
|
20
|
+
"title" TEXT NOT NULL,
|
|
21
|
+
"body" TEXT NOT NULL,
|
|
22
|
+
"link" TEXT,
|
|
23
|
+
"cta_label" TEXT,
|
|
24
|
+
"event_key" TEXT NOT NULL,
|
|
25
|
+
"channels" TEXT[],
|
|
26
|
+
"status" "NotificationBroadcastStatus" NOT NULL DEFAULT 'scheduled',
|
|
27
|
+
"scheduled_for" TIMESTAMPTZ,
|
|
28
|
+
"started_at" TIMESTAMPTZ,
|
|
29
|
+
"finished_at" TIMESTAMPTZ,
|
|
30
|
+
"canceled_at" TIMESTAMPTZ,
|
|
31
|
+
"audience_cutoff" TIMESTAMPTZ,
|
|
32
|
+
"cursor_user_id" UUID,
|
|
33
|
+
"recipients_targeted" INTEGER,
|
|
34
|
+
"recipients_dispatched" INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
"last_error" TEXT,
|
|
36
|
+
"created_by_id" UUID,
|
|
37
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
38
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
39
|
+
|
|
40
|
+
CONSTRAINT "notification_broadcasts_pkey" PRIMARY KEY ("id")
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- CreateIndex
|
|
44
|
+
-- "What is pending" — the scheduler's own query.
|
|
45
|
+
CREATE INDEX "notification_broadcasts_status_scheduled_for_idx" ON "notification_broadcasts"("status", "scheduled_for");
|
|
46
|
+
|
|
47
|
+
-- CreateIndex
|
|
48
|
+
-- The admin list page's default order, "most recently composed first".
|
|
49
|
+
CREATE INDEX "notification_broadcasts_created_at_idx" ON "notification_broadcasts"("created_at" DESC);
|
|
50
|
+
|
|
51
|
+
-- AddForeignKey
|
|
52
|
+
-- onDelete: SetNull — same audit-trail reasoning as
|
|
53
|
+
-- `audit_events_actor_user_id_fkey`: deleting the admin who composed a
|
|
54
|
+
-- broadcast must not delete the record of what they announced. Takes a
|
|
55
|
+
-- brief lock on `users` while the constraint is validated, as any new FK to
|
|
56
|
+
-- that table does.
|
|
57
|
+
ALTER TABLE "notification_broadcasts" ADD CONSTRAINT "notification_broadcasts_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Backup Run <-> Job link, and tightening the single-active-run guard
|
|
3
|
+
-- (issue #351, epic #345)
|
|
4
|
+
-- =============================================================================
|
|
5
|
+
-- Two independent changes land together because the second is only safe once
|
|
6
|
+
-- the first is possible to reason about (see the second block below for why
|
|
7
|
+
-- they must not be split across two migrations).
|
|
8
|
+
--
|
|
9
|
+
-- 1. `database_backup_runs.job_id`: the FK from a backup run back to the
|
|
10
|
+
-- queue job that drove it, once the dump becomes a job (`db.backup.run`).
|
|
11
|
+
-- Nullable, no default, no backfill — see the block comment above
|
|
12
|
+
-- `DatabaseBackupRun` in prisma/schema.prisma ("jobId") for the full
|
|
13
|
+
-- reasoning behind `@unique` / nullable / `SetNull`, which this migration
|
|
14
|
+
-- intentionally does not restate.
|
|
15
|
+
--
|
|
16
|
+
-- 2. Tightening `database_backup_runs_active_uniq_idx` from a UNIQUE index on
|
|
17
|
+
-- the `status` COLUMN to a UNIQUE index on the CONSTANT EXPRESSION `(true)`
|
|
18
|
+
-- — both filtered to `status IN ('pending','running')`. See the block
|
|
19
|
+
-- comment above `DatabaseBackupRun` in prisma/schema.prisma
|
|
20
|
+
-- ("THE SINGLE-ACTIVE-RUN GUARD...") for the full story; the short version:
|
|
21
|
+
-- the old, column-keyed index admits one `pending` row AND one `running`
|
|
22
|
+
-- row AT THE SAME TIME (two active runs), which was harmless only because
|
|
23
|
+
-- nothing ever wrote `pending`. #351's service half makes
|
|
24
|
+
-- `POST /api/admin/db-backup/runs` enqueue the job and insert the run row
|
|
25
|
+
-- as `pending` before any worker claims it, so `pending` rows become real.
|
|
26
|
+
-- Keying on `(true)` instead makes every active row — whichever status it
|
|
27
|
+
-- holds — collide on the same key, so at most one can exist across both
|
|
28
|
+
-- statuses combined.
|
|
29
|
+
-- =============================================================================
|
|
30
|
+
|
|
31
|
+
-- AlterTable
|
|
32
|
+
ALTER TABLE "database_backup_runs" ADD COLUMN "job_id" UUID;
|
|
33
|
+
|
|
34
|
+
-- CreateIndex
|
|
35
|
+
CREATE UNIQUE INDEX "database_backup_runs_job_id_key" ON "database_backup_runs"("job_id");
|
|
36
|
+
|
|
37
|
+
-- AddForeignKey
|
|
38
|
+
-- onDelete: SetNull, NOT Cascade — `job.history.purge` deletes `jobs` rows on
|
|
39
|
+
-- a retention schedule independent of the archive's own retention. Cascade
|
|
40
|
+
-- would let a routine job-history purge delete the record of a still-restorable
|
|
41
|
+
-- backup. See the block comment above `DatabaseBackupRun.jobId` in
|
|
42
|
+
-- prisma/schema.prisma.
|
|
43
|
+
ALTER TABLE "database_backup_runs" ADD CONSTRAINT "database_backup_runs_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "jobs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
44
|
+
|
|
45
|
+
-- =============================================================================
|
|
46
|
+
-- HAND-WRITTEN INDEX CHANGE — INTENTIONAL SCHEMA DRIFT (issue #351, epic #345)
|
|
47
|
+
-- =============================================================================
|
|
48
|
+
-- `database_backup_runs_active_uniq_idx` is NOT emitted by `prisma migrate
|
|
49
|
+
-- dev`/`diff` and never will be: Prisma's schema language has no syntax for a
|
|
50
|
+
-- partial index OR an expression index, so both the original (partial, on
|
|
51
|
+
-- `status`) and this tightened form (partial, on the expression `(true)`) are
|
|
52
|
+
-- written here by hand — exactly the drift `20260907120000_add_database_backup
|
|
53
|
+
-- _runs/migration.sql` already carries, now one step further. The block
|
|
54
|
+
-- comment above the `DatabaseBackupRun` model in prisma/schema.prisma records
|
|
55
|
+
-- the same fact for anyone reading the schema without this file open.
|
|
56
|
+
--
|
|
57
|
+
-- ⚠ `prisma migrate dev` WILL WANT TO DROP THIS INDEX on the next diff, the
|
|
58
|
+
-- same as its predecessor. Do not "reconcile" this migration by accepting
|
|
59
|
+
-- that drop, and do not expect `prisma migrate diff` against a live database
|
|
60
|
+
-- to come back clean. This drift is deliberate and permanent for as long as
|
|
61
|
+
-- Prisma's DSL lacks expression indexes.
|
|
62
|
+
-- =============================================================================
|
|
63
|
+
|
|
64
|
+
-- The ORIGINAL guard admitted at most one 'pending' row AND at most one
|
|
65
|
+
-- 'running' row AT THE SAME TIME — two active runs, not one — because the
|
|
66
|
+
-- index was keyed on the `status` column itself: a 'pending' row and a
|
|
67
|
+
-- 'running' row have different key values, so both could satisfy the same
|
|
68
|
+
-- unique index simultaneously. That was harmless only because nothing ever
|
|
69
|
+
-- wrote 'pending'. #351 makes the dump a queue job, and the run row is now
|
|
70
|
+
-- created 'pending' before any worker has claimed it, so 'pending' rows are
|
|
71
|
+
-- no longer hypothetical.
|
|
72
|
+
DROP INDEX "database_backup_runs_active_uniq_idx";
|
|
73
|
+
|
|
74
|
+
-- THE tightened single-active-run guard. Keying on the constant expression
|
|
75
|
+
-- `(true)` — rather than on `status` — makes every row matching the WHERE
|
|
76
|
+
-- predicate (any 'pending' or 'running' row) index to the exact same key, so
|
|
77
|
+
-- Postgres now enforces "at most one active row, full stop" across BOTH
|
|
78
|
+
-- statuses combined, not "at most one of each". A 'completed', 'failed' or
|
|
79
|
+
-- 'stale' row does not match the predicate at all and is never constrained by
|
|
80
|
+
-- this index, so any number of them may coexist.
|
|
81
|
+
--
|
|
82
|
+
-- `DatabaseBackupRunnerService.startBackup` still inserts optimistically and
|
|
83
|
+
-- turns the loser's P2002 into a typed `DatabaseBackupAlreadyRunningError`
|
|
84
|
+
-- carrying the winner's id; there is deliberately no `findFirst` pre-check,
|
|
85
|
+
-- because check-then-act is racy exactly when this constraint matters.
|
|
86
|
+
CREATE UNIQUE INDEX "database_backup_runs_active_uniq_idx"
|
|
87
|
+
ON "database_backup_runs" ((true)) WHERE "status" IN ('pending','running');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- Per-job secret broker: the handle ledger (issue #349, epic #345)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- One table, and the interesting thing about it is a column that is NOT here.
|
|
5
|
+
--
|
|
6
|
+
-- A worker node holds no credentials by design (docs/specs/worker-nodes.md §8).
|
|
7
|
+
-- Epic #345 needs it to run work that genuinely requires one, so the server
|
|
8
|
+
-- mints a credential per job, hands it over exactly once, and destroys it when
|
|
9
|
+
-- the job settles. `job_node_secrets` records THAT A GRANT EXISTED — which node,
|
|
10
|
+
-- of what kind, from when until when, revoked or not — so "did a node ever hold
|
|
11
|
+
-- a credential to this database, and when" is answerable.
|
|
12
|
+
--
|
|
13
|
+
-- ⚠ THERE IS NO COLUMN CAPABLE OF HOLDING SECRET MATERIAL, AND ADDING ONE IS
|
|
14
|
+
-- THE ONE CHANGE THIS TABLE MUST NEVER RECEIVE. Not `password`, not
|
|
15
|
+
-- `encrypted_material`, not "just a base64 blob for debugging". `handle` is an
|
|
16
|
+
-- identifier for the grant (a role name) and is what `JobSecretBroker.revoke`
|
|
17
|
+
-- takes; the material is serialised into one HTTP response and dropped.
|
|
18
|
+
-- Reusing SECRETS_ENCRYPTION_KEY here was considered and rejected: a credential
|
|
19
|
+
-- that can be re-read is a credential that can be stolen twice, and nothing
|
|
20
|
+
-- needs to re-read this one. Storing it — even encrypted — would put a live
|
|
21
|
+
-- database password into this table, into every pg_dump of it, and into every
|
|
22
|
+
-- copy of that dump. See the block comment above `JobNodeSecret` in
|
|
23
|
+
-- prisma/schema.prisma for the full argument.
|
|
24
|
+
--
|
|
25
|
+
-- NO FOREIGN KEYS, deliberately. `job_id` and `node_id` are plain UUIDs. A
|
|
26
|
+
-- `jobs` row is deleted on a retention schedule (`job.history.purge`) that has
|
|
27
|
+
-- nothing to do with a grant's lifetime, and a cascade from that purge would
|
|
28
|
+
-- erase the audit record this table exists to keep; a grant must also stay
|
|
29
|
+
-- revocable after its job row is gone, which is why the sweeper resolves a
|
|
30
|
+
-- broker by `kind` rather than by the job's `type`.
|
|
31
|
+
-- =============================================================================
|
|
32
|
+
|
|
33
|
+
-- CreateTable
|
|
34
|
+
CREATE TABLE "job_node_secrets" (
|
|
35
|
+
"id" UUID NOT NULL,
|
|
36
|
+
"job_id" UUID NOT NULL,
|
|
37
|
+
"node_id" UUID NOT NULL,
|
|
38
|
+
"kind" TEXT NOT NULL,
|
|
39
|
+
-- The grant's identifier. NEVER the password — see the header.
|
|
40
|
+
"handle" TEXT NOT NULL,
|
|
41
|
+
"issued_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
"expires_at" TIMESTAMPTZ NOT NULL,
|
|
43
|
+
"revoked_at" TIMESTAMPTZ,
|
|
44
|
+
|
|
45
|
+
CONSTRAINT "job_node_secrets_pkey" PRIMARY KEY ("id")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- CreateIndex
|
|
49
|
+
-- ONE CREDENTIAL PER JOB, EVER. A node asks for its secret more than once as a
|
|
50
|
+
-- matter of course (a restarted process still holding the lease, a lost
|
|
51
|
+
-- response, a retry), and every one of those calls must extend the SAME grant.
|
|
52
|
+
-- The revocation paths know exactly one handle per (job, kind); a sibling grant
|
|
53
|
+
-- would be one nothing ever destroys. This index makes the second row
|
|
54
|
+
-- unrepresentable rather than leaving it to the service to remember.
|
|
55
|
+
CREATE UNIQUE INDEX "job_node_secrets_job_id_kind_key" ON "job_node_secrets"("job_id", "kind");
|
|
56
|
+
|
|
57
|
+
-- CreateIndex
|
|
58
|
+
-- The sweeper's scan: unrevoked grants whose own clock has lapsed. See
|
|
59
|
+
-- `NodeSecretBrokerService.sweep` for why the sweeper is not redundant with the
|
|
60
|
+
-- settle-event revoker — three real cases emit no event at all.
|
|
61
|
+
CREATE INDEX "job_node_secrets_expires_at_idx" ON "job_node_secrets"("expires_at");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- `database_backup_runs.pg_dump_version` (issue #352, epic #345)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- The fourth member of this table's audit block: WHICH `pg_dump` wrote the
|
|
5
|
+
-- archive.
|
|
6
|
+
--
|
|
7
|
+
-- It was worth nothing while the dump could only run in the API process — the
|
|
8
|
+
-- client is whatever `apps/api/Dockerfile` installed, the same for every run,
|
|
9
|
+
-- and the node-credential preflight already reports it. #352 makes the dump
|
|
10
|
+
-- claimable by a worker node, and on that path the binary lives on a machine
|
|
11
|
+
-- this deployment may not own and cannot inspect after the fact. Without this
|
|
12
|
+
-- column, nothing anywhere records which client produced the file, and
|
|
13
|
+
-- "`pg_restore` cannot read an archive written by a NEWER `pg_dump`" becomes a
|
|
14
|
+
-- fact an operator discovers during a restore rather than one they can check
|
|
15
|
+
-- before starting it (see docs/runbooks/postgres-client-version.md).
|
|
16
|
+
--
|
|
17
|
+
-- Nullable, no default, NO BACKFILL. Every existing row was written by a
|
|
18
|
+
-- client nobody recorded, and inventing this deployment's CURRENT client
|
|
19
|
+
-- version for them would be a lie that reads exactly like evidence. NULL means
|
|
20
|
+
-- "not recorded", which is the truth for every run taken before this migration
|
|
21
|
+
-- and for any run whose `pg_dump --version` could not be read — the same
|
|
22
|
+
-- "warn and proceed" rule the other three audit columns already follow.
|
|
23
|
+
-- =============================================================================
|
|
24
|
+
|
|
25
|
+
-- AlterTable
|
|
26
|
+
ALTER TABLE "database_backup_runs" ADD COLUMN "pg_dump_version" TEXT;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- jobs.claim_token — per-claim identity, closing the two-replica lease hole
|
|
3
|
+
-- (issue #361)
|
|
4
|
+
-- =============================================================================
|
|
5
|
+
-- `heldLeaseWhere` (`src/jobs/job-lease.service.ts`) answers "does the caller
|
|
6
|
+
-- still hold this row" as `{ id, status: 'running', leaseExpiresAt: { gt: now
|
|
7
|
+
-- }, claimedByNodeId }`. That predicate identifies a CLAIMANT KIND — "a node"
|
|
8
|
+
-- (a real `WorkerNode.id`) vs. "the server" (`claimedByNodeId: null`) — not a
|
|
9
|
+
-- particular claim. Every API replica claims with `claimedByNodeId: null`, so
|
|
10
|
+
-- two server replicas produce the exact same `heldLeaseWhere` predicate for
|
|
11
|
+
-- the same job. Concretely: replica A claims a job, then stalls long enough
|
|
12
|
+
-- for the lease reaper to reclaim it as abandoned; replica B claims the
|
|
13
|
+
-- now-pending job and starts running it; replica A, still alive and unaware
|
|
14
|
+
-- it lost the row, calls `renew` — which matches `heldLeaseWhere` (status is
|
|
15
|
+
-- `running`, `claimedByNodeId` is `null`, and the lease B just set is still
|
|
16
|
+
-- in the future) and happily extends B's lease. A never learns it lost the
|
|
17
|
+
-- job, and now believes it owns work it does not.
|
|
18
|
+
--
|
|
19
|
+
-- The fix is a column that identifies ONE CLAIM, not one KIND of claimant:
|
|
20
|
+
-- `claim_token`, minted fresh (`gen_random_uuid()`) by the claim statement
|
|
21
|
+
-- itself and folded into `heldLeaseWhere` alongside `claimedByNodeId` (wiring
|
|
22
|
+
-- lands in a later slice — this migration and its schema.prisma companion are
|
|
23
|
+
-- schema-only, per the issue's stated scope). Once every renew/settle/reap
|
|
24
|
+
-- statement also matches on `claim_token`, replica A's renewal after losing
|
|
25
|
+
-- the row targets a token that is no longer on the row (B's claim overwrote
|
|
26
|
+
-- it with a new one) and the `UPDATE` matches zero rows, exactly like a node
|
|
27
|
+
-- losing a lease already does today.
|
|
28
|
+
--
|
|
29
|
+
-- -----------------------------------------------------------------------------
|
|
30
|
+
-- Why NULLABLE, with no DEFAULT
|
|
31
|
+
-- -----------------------------------------------------------------------------
|
|
32
|
+
-- The invariant this codebase will maintain from here on is `claim_token IS
|
|
33
|
+
-- NOT NULL` IFF the row is currently held under a claim. A `pending` job has
|
|
34
|
+
-- no claimant at all, so writing it a token would assert a claim that does
|
|
35
|
+
-- not exist — a lie the column has no business telling. `DEFAULT
|
|
36
|
+
-- gen_random_uuid()` would do exactly that: every row, including ones freshly
|
|
37
|
+
-- enqueued and never claimed, would carry a token that means nothing,
|
|
38
|
+
-- indistinguishable from a real one to anybody reading the column.
|
|
39
|
+
--
|
|
40
|
+
-- No backfill, and none is wanted. Every row that exists at deploy time is
|
|
41
|
+
-- either not currently claimed (`pending`, `succeeded`, `failed` — `NULL` is
|
|
42
|
+
-- simply correct) or `running` under a claim taken by a REPLICA RUNNING THE
|
|
43
|
+
-- OLD CODE, which never wrote a token and has no token to reconcile against.
|
|
44
|
+
-- `NULL` is still correct there, but the reason an old replica keeps renewing
|
|
45
|
+
-- successfully has nothing to do with NULL comparison semantics — it is
|
|
46
|
+
-- simpler than that: an old replica is running the OLD `heldLeaseWhere`,
|
|
47
|
+
-- which has no `claim_token` clause at all, so it never passes a token,
|
|
48
|
+
-- remembered or otherwise, and this column is invisible to its predicate.
|
|
49
|
+
-- The later slice's `heldLeaseWhere(jobId, { nodeId?, claimToken? })` treats
|
|
50
|
+
-- `claimToken` the same three-valued way `nodeId` already works: a string
|
|
51
|
+
-- constrains the match to that one claim, `undefined` omits the clause
|
|
52
|
+
-- entirely (what the node control plane and this migration-era code both do),
|
|
53
|
+
-- and `null` is passed straight through to Prisma and renders as an actual
|
|
54
|
+
-- `claim_token IS NULL` check — matching only a row that carries no token.
|
|
55
|
+
--
|
|
56
|
+
-- That makes the rolling-deploy story a narrowing, not a full close, and only
|
|
57
|
+
-- in one direction: a NEW replica's claim writes a real token, so no other
|
|
58
|
+
-- NEW replica can renew it out from under the claimant — the hole
|
|
59
|
+
-- is closed between new replicas from the moment this ships. An OLD
|
|
60
|
+
-- replica's `renew` still ignores `claim_token` altogether, so an old
|
|
61
|
+
-- replica that lost a row to the reaper can still extend a NEW replica's
|
|
62
|
+
-- lease for as long as the old replica keeps ticking — the same failure mode
|
|
63
|
+
-- this column exists to fix, just now confined to "one side of the pair is
|
|
64
|
+
-- still running pre-#361 code" instead of "always". That window is bounded
|
|
65
|
+
-- and ordinary: it closes for good once the last old replica has rolled off,
|
|
66
|
+
-- the same way any predicate change behaves during a rolling deploy, and is
|
|
67
|
+
-- not a reason to change this design.
|
|
68
|
+
--
|
|
69
|
+
-- -----------------------------------------------------------------------------
|
|
70
|
+
-- Why no index
|
|
71
|
+
-- -----------------------------------------------------------------------------
|
|
72
|
+
-- `claim_token` is never queried on its own — every read of it is one more
|
|
73
|
+
-- predicate bolted onto a single-row lookup already keyed by `jobs`'s primary
|
|
74
|
+
-- key (`WHERE id = $1 AND claim_token = $2 AND ...`), the same shape
|
|
75
|
+
-- `heldLeaseWhere` already has for `claimedByNodeId` and `leaseExpiresAt`,
|
|
76
|
+
-- neither of which is indexed on its own either. An index earns its keep by
|
|
77
|
+
-- narrowing a scan that would otherwise be large; there is no query here that
|
|
78
|
+
-- scans `jobs` by `claim_token` and needs narrowing. Adding one would be pure
|
|
79
|
+
-- write overhead on the hottest path this table has — every claim, every
|
|
80
|
+
-- renewal, every settle — for a lookup nothing performs.
|
|
81
|
+
--
|
|
82
|
+
-- -----------------------------------------------------------------------------
|
|
83
|
+
-- Online-safety
|
|
84
|
+
-- -----------------------------------------------------------------------------
|
|
85
|
+
-- `ADD COLUMN` of a NULLABLE column with NO DEFAULT is a metadata-only change
|
|
86
|
+
-- on PostgreSQL 11+: no table rewrite, no full-table scan to populate a
|
|
87
|
+
-- default, just a brief ACCESS EXCLUSIVE lock to update the catalog. Safe to
|
|
88
|
+
-- run against a live, populated `jobs` table with no maintenance window.
|
|
89
|
+
-- =============================================================================
|
|
90
|
+
|
|
91
|
+
-- AlterTable
|
|
92
|
+
ALTER TABLE "jobs" ADD COLUMN "claim_token" UUID;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "ai_models" (
|
|
3
|
+
"id" UUID NOT NULL,
|
|
4
|
+
"provider" TEXT NOT NULL,
|
|
5
|
+
"model_id" TEXT NOT NULL,
|
|
6
|
+
"display_name" TEXT,
|
|
7
|
+
"capabilities" JSONB NOT NULL,
|
|
8
|
+
"capability_source" TEXT NOT NULL DEFAULT 'unclassified',
|
|
9
|
+
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
|
10
|
+
"context_window" INTEGER,
|
|
11
|
+
"max_output_tokens" INTEGER,
|
|
12
|
+
"discovered_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
13
|
+
"last_seen_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
"deprecated_at" TIMESTAMPTZ,
|
|
15
|
+
"updated_by_user_id" UUID,
|
|
16
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
17
|
+
|
|
18
|
+
CONSTRAINT "ai_models_pkey" PRIMARY KEY ("id")
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- CreateTable
|
|
22
|
+
CREATE TABLE "user_ai_keys" (
|
|
23
|
+
"id" UUID NOT NULL,
|
|
24
|
+
"user_id" UUID NOT NULL,
|
|
25
|
+
"provider" TEXT NOT NULL,
|
|
26
|
+
"secret" TEXT NOT NULL,
|
|
27
|
+
"hint" TEXT,
|
|
28
|
+
"verified_at" TIMESTAMPTZ,
|
|
29
|
+
"last_error_code" TEXT,
|
|
30
|
+
"reachable_model_ids" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
31
|
+
"reachable_checked_at" TIMESTAMPTZ,
|
|
32
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
33
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
34
|
+
|
|
35
|
+
CONSTRAINT "user_ai_keys_pkey" PRIMARY KEY ("id")
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- CreateTable
|
|
39
|
+
CREATE TABLE "ai_runs" (
|
|
40
|
+
"id" UUID NOT NULL,
|
|
41
|
+
"user_id" UUID,
|
|
42
|
+
"job_id" UUID,
|
|
43
|
+
"status" TEXT NOT NULL DEFAULT 'pending',
|
|
44
|
+
"provider" TEXT NOT NULL,
|
|
45
|
+
"model_id" TEXT NOT NULL,
|
|
46
|
+
"request" JSONB NOT NULL,
|
|
47
|
+
"output" JSONB,
|
|
48
|
+
"error_code" TEXT,
|
|
49
|
+
"error_message" TEXT,
|
|
50
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
51
|
+
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
52
|
+
"completed_at" TIMESTAMPTZ,
|
|
53
|
+
|
|
54
|
+
CONSTRAINT "ai_runs_pkey" PRIMARY KEY ("id")
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- CreateTable
|
|
58
|
+
CREATE TABLE "ai_usage_events" (
|
|
59
|
+
"id" UUID NOT NULL,
|
|
60
|
+
"user_id" UUID,
|
|
61
|
+
"provider" TEXT NOT NULL,
|
|
62
|
+
"model_id" TEXT NOT NULL,
|
|
63
|
+
"operation" TEXT NOT NULL,
|
|
64
|
+
"key_source" TEXT NOT NULL,
|
|
65
|
+
"input_tokens" INTEGER,
|
|
66
|
+
"output_tokens" INTEGER,
|
|
67
|
+
"reasoning_tokens" INTEGER,
|
|
68
|
+
"cached_input_tokens" INTEGER,
|
|
69
|
+
"units" JSONB,
|
|
70
|
+
"latency_ms" INTEGER NOT NULL,
|
|
71
|
+
"status" TEXT NOT NULL,
|
|
72
|
+
"error_code" TEXT,
|
|
73
|
+
"provider_request_id" TEXT,
|
|
74
|
+
"job_id" UUID,
|
|
75
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
76
|
+
|
|
77
|
+
CONSTRAINT "ai_usage_events_pkey" PRIMARY KEY ("id")
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- CreateIndex
|
|
81
|
+
CREATE INDEX "ai_models_provider_enabled_idx" ON "ai_models"("provider", "enabled");
|
|
82
|
+
|
|
83
|
+
-- CreateIndex
|
|
84
|
+
CREATE UNIQUE INDEX "ai_models_provider_model_id_key" ON "ai_models"("provider", "model_id");
|
|
85
|
+
|
|
86
|
+
-- CreateIndex
|
|
87
|
+
CREATE UNIQUE INDEX "user_ai_keys_user_id_provider_key" ON "user_ai_keys"("user_id", "provider");
|
|
88
|
+
|
|
89
|
+
-- CreateIndex
|
|
90
|
+
CREATE INDEX "ai_runs_user_id_created_at_idx" ON "ai_runs"("user_id", "created_at");
|
|
91
|
+
|
|
92
|
+
-- CreateIndex
|
|
93
|
+
CREATE INDEX "ai_usage_events_user_id_created_at_idx" ON "ai_usage_events"("user_id", "created_at");
|
|
94
|
+
|
|
95
|
+
-- CreateIndex
|
|
96
|
+
CREATE INDEX "ai_usage_events_created_at_idx" ON "ai_usage_events"("created_at");
|
|
97
|
+
|
|
98
|
+
-- AddForeignKey
|
|
99
|
+
ALTER TABLE "ai_models" ADD CONSTRAINT "ai_models_updated_by_user_id_fkey" FOREIGN KEY ("updated_by_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
100
|
+
|
|
101
|
+
-- AddForeignKey
|
|
102
|
+
ALTER TABLE "user_ai_keys" ADD CONSTRAINT "user_ai_keys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
103
|
+
|
|
104
|
+
-- AddForeignKey
|
|
105
|
+
ALTER TABLE "ai_runs" ADD CONSTRAINT "ai_runs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
106
|
+
|
|
107
|
+
-- AddForeignKey
|
|
108
|
+
ALTER TABLE "ai_usage_events" ADD CONSTRAINT "ai_usage_events_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|