@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,726 @@
|
|
|
1
|
+
// GENERATED by platform db compose — do not edit
|
|
2
|
+
// source: app:kvox.transcripts.prisma
|
|
3
|
+
|
|
4
|
+
// GENERATED by kvox-db sync — do not edit
|
|
5
|
+
// source: @marinoscar/kvox-db:schema/kvox.transcripts.prisma
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Transcripts (issue #24, epic #19) — docs/specs/transcription.md §3
|
|
9
|
+
// =============================================================================
|
|
10
|
+
//
|
|
11
|
+
// Six tables, all keyed to one `transcripts` row per uploaded recording. This
|
|
12
|
+
// issue adds only the schema — the upload/pipeline/editing/sharing/export
|
|
13
|
+
// logic that reads and writes it all arrives in issues #25–#29. What is here
|
|
14
|
+
// is deliberately complete on its own, mirroring the `jobs`/`worker_nodes`
|
|
15
|
+
// precedent of "the schema ships whole, ahead of the code that will use it,
|
|
16
|
+
// so nothing downstream needs a follow-up migration to these tables
|
|
17
|
+
// themselves." Every column, enum member and index below is named for and
|
|
18
|
+
// justified by docs/specs/transcription.md §1, §3, §4, §5, §6, §8 and §10 —
|
|
19
|
+
// read those sections for the full reasoning; this comment restates only
|
|
20
|
+
// what has to live beside the code to stay correct.
|
|
21
|
+
//
|
|
22
|
+
// -----------------------------------------------------------------------------
|
|
23
|
+
// Three status enums, not one — restated from spec §1.4
|
|
24
|
+
// -----------------------------------------------------------------------------
|
|
25
|
+
// `transcripts.status` (`TranscriptStatus`) is what every list view and every
|
|
26
|
+
// permission check reads. `transcriptionStatus` and `playbackStatus` are
|
|
27
|
+
// detail fields for the pipeline stepper (issue #30): the transcode and the
|
|
28
|
+
// transcription round trip run CONCURRENTLY, so a single top-level enum would
|
|
29
|
+
// need one member per combination of the two sub-pipelines' states and would
|
|
30
|
+
// still need a second field to say which failure belongs to which
|
|
31
|
+
// sub-pipeline. This is the same design `worker_nodes` already uses for its
|
|
32
|
+
// own health (`status` for stated intent, `lastHeartbeatAt` for computed
|
|
33
|
+
// liveness): small, independently updatable pieces of state, not one field
|
|
34
|
+
// pretending to be several.
|
|
35
|
+
//
|
|
36
|
+
// -----------------------------------------------------------------------------
|
|
37
|
+
// owner_id CASCADES; source_object_id (and every other storage_objects FK
|
|
38
|
+
// below) RESTRICTS — the same column shape, the opposite `onDelete`, because
|
|
39
|
+
// they answer opposite ownership questions
|
|
40
|
+
// -----------------------------------------------------------------------------
|
|
41
|
+
// `owner_id` points UP the ownership chain: a `transcripts` row's only reason
|
|
42
|
+
// to exist is that exactly one user owns it. This is the same "belongs
|
|
43
|
+
// entirely to this user" shape `user_settings`, `refresh_tokens`,
|
|
44
|
+
// `personal_access_tokens` and `device_codes` already use Cascade for — NOT
|
|
45
|
+
// the audit/history shape `AuditEvent.actorUserId`, `Credential
|
|
46
|
+
// .updatedByUserId` or `DatabaseBackupRun.createdById` use SetNull for. The
|
|
47
|
+
// difference is what the row IS: an audit row is a record of an act someone
|
|
48
|
+
// performed against shared infrastructure and must survive the actor's
|
|
49
|
+
// deletion to keep meaning; a transcript is not a record OF anything, it IS
|
|
50
|
+
// the private content itself, and this application's own access model (§6.1)
|
|
51
|
+
// has no path to it once its owner is gone — there is deliberately no
|
|
52
|
+
// `transcripts:read_any` (§6.2), so an owner-less transcript would be a row
|
|
53
|
+
// nobody, including an admin, could ever read, share, edit or delete again.
|
|
54
|
+
// Cascading it away when the account goes is the only outcome consistent
|
|
55
|
+
// with "no read-any, ever."
|
|
56
|
+
//
|
|
57
|
+
// `source_object_id` / `playback_object_id` / `raw_result_object_id` (below)
|
|
58
|
+
// and `transcript_versions.snapshot_object_id` / `transcript_exports
|
|
59
|
+
// .object_id` all point SIDEWAYS, across a module boundary, into a DIFFERENT
|
|
60
|
+
// feature's table (`storage_objects`, issue #21) that this feature does not
|
|
61
|
+
// own. Restrict here protects the referenced object FROM this table's own
|
|
62
|
+
// module boundary being crossed by someone else: a `storage_objects` row a
|
|
63
|
+
// transcript still needs may never be deleted out from under it by the
|
|
64
|
+
// generic storage endpoints or an unrelated cleanup sweep (the exact
|
|
65
|
+
// `managed_by` discipline spec §9.3 already gives every managed object). The
|
|
66
|
+
// only path that may ever remove one of these objects is `transcript.purge`
|
|
67
|
+
// (spec §1.5.7, §10) deleting the referencing row FIRST — which a Restrict
|
|
68
|
+
// FK permits freely, because Restrict blocks deleting the REFERENCED
|
|
69
|
+
// `storage_objects` row while a reference exists, not deleting the
|
|
70
|
+
// REFERENCING transcript row itself.
|
|
71
|
+
//
|
|
72
|
+
// ⚠ ONE CONSEQUENCE OF THAT PAIRING, STATED HONESTLY RATHER THAN PAPERED
|
|
73
|
+
// OVER: `owner_id`'s Cascade fires at the Postgres level on a raw user
|
|
74
|
+
// deletion and does not, and cannot, invoke `transcript.purge`'s storage and
|
|
75
|
+
// provider cleanup — a cascading DELETE only removes SQL rows, and Restrict
|
|
76
|
+
// on the sideways FKs means Postgres will not even attempt to touch the
|
|
77
|
+
// `storage_objects` rows those columns pointed at (a Restrict's ON DELETE
|
|
78
|
+
// action fires only when deleting the REFERENCED row, never when the
|
|
79
|
+
// referencing row disappears by any means, cascade included). A user
|
|
80
|
+
// deleted by a path other than the transcript's own `DELETE
|
|
81
|
+
// /api/transcripts/:id` → `transcript.purge` route therefore leaves that
|
|
82
|
+
// transcript's audio, rendition, raw result and export files as orphaned
|
|
83
|
+
// `storage_objects` rows — reclaimable, but not reclaimed by this cascade.
|
|
84
|
+
// This mirrors the same asymmetry already accepted for `WorkerNode
|
|
85
|
+
// .createdBy`'s Cascade (deleting the SQL row is not the same act as
|
|
86
|
+
// deleting the credential's real-world effects) and is exactly why spec §10
|
|
87
|
+
// frames `transcript.purge` as the only complete-deletion path — a future
|
|
88
|
+
// issue that adds account deletion as a first-class flow should route it
|
|
89
|
+
// through that job rather than a bare `prisma.user.delete()`. This is a
|
|
90
|
+
// decision made explicitly for issue #24, not an oversight; #25–#29 are the
|
|
91
|
+
// right place to close it if account deletion becomes a real user-facing
|
|
92
|
+
// flow.
|
|
93
|
+
//
|
|
94
|
+
// -----------------------------------------------------------------------------
|
|
95
|
+
// speaker_id on transcript_segments — also Restrict, for a same-feature
|
|
96
|
+
// reason rather than a cross-module one
|
|
97
|
+
// -----------------------------------------------------------------------------
|
|
98
|
+
// Unlike the storage FKs above, this Restrict is WITHIN the transcript
|
|
99
|
+
// feature: a speaker may not be deleted while any segment still names it.
|
|
100
|
+
// `speaker.merge` (spec §4.1) is the only op that ever deletes a speaker, and
|
|
101
|
+
// it re-points every one of that speaker's segments to the merge target
|
|
102
|
+
// FIRST, in the same transaction, before deleting the source speaker rows —
|
|
103
|
+
// exactly the shape Restrict requires and enforces. A hypothetical direct
|
|
104
|
+
// "delete this speaker" operation with segments still attached is therefore
|
|
105
|
+
// unrepresentable at the database level, not merely disallowed by service
|
|
106
|
+
// code that could have a bug.
|
|
107
|
+
//
|
|
108
|
+
// -----------------------------------------------------------------------------
|
|
109
|
+
// Hand-written, Prisma-inexpressible constraints — INTENTIONAL SCHEMA DRIFT,
|
|
110
|
+
// the same pattern `jobs_active_dedup_uniq_idx` / `database_backup_runs
|
|
111
|
+
// _active_uniq_idx` already establish
|
|
112
|
+
// -----------------------------------------------------------------------------
|
|
113
|
+
// `transcript_speakers_transcript_id_label_key`, declared only in this
|
|
114
|
+
// migration's `migration.sql`, not as a `@@unique` here: a provider-supplied
|
|
115
|
+
// `label` (e.g. `"A"`) must be unique per transcript, but a user-created
|
|
116
|
+
// speaker (`speaker.create`, spec §4.1) legitimately has none, and Prisma's
|
|
117
|
+
// `@@index`/`@@unique` DSL has no syntax for the partial index's `WHERE
|
|
118
|
+
// "label" IS NOT NULL` clause that scopes the constraint to labelled rows.
|
|
119
|
+
// See that migration file's own header for the full accounting. Every OTHER
|
|
120
|
+
// uniqueness constraint below (`transcript_versions` on `(transcriptId,
|
|
121
|
+
// version)` and `(transcriptId, clientBatchId)`) is fully expressible as a
|
|
122
|
+
// plain `@@unique` and is declared that way — Postgres's standard "NULLS DISTINCT" behaviour already
|
|
123
|
+
// gives `clientBatchId` exactly the semantics spec §5 wants (many versions
|
|
124
|
+
// with a null `clientBatchId`, at most one per transcript for any given
|
|
125
|
+
// non-null value) with no partial index required.
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
enum TranscriptStatus {
|
|
129
|
+
uploading
|
|
130
|
+
processing
|
|
131
|
+
ready
|
|
132
|
+
failed
|
|
133
|
+
deleting
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
enum TranscriptionStatus {
|
|
137
|
+
waiting_input
|
|
138
|
+
queued
|
|
139
|
+
submitting
|
|
140
|
+
submitted
|
|
141
|
+
processing
|
|
142
|
+
completed
|
|
143
|
+
failed
|
|
144
|
+
cancelled
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
enum PlaybackStatus {
|
|
148
|
+
pending
|
|
149
|
+
processing
|
|
150
|
+
ready
|
|
151
|
+
failed
|
|
152
|
+
not_needed
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
model Transcript {
|
|
156
|
+
id String @id @default(uuid()) @db.Uuid
|
|
157
|
+
|
|
158
|
+
ownerId String @map("owner_id") @db.Uuid
|
|
159
|
+
// The organization this transcript belongs to (issue #485): the scope the
|
|
160
|
+
// platform sharing slice decides access in (`grants.org_id` must equal it).
|
|
161
|
+
// Single-organization deployment, so every row carries the default
|
|
162
|
+
// organization. `transcripts` is deliberately NOT under row-level security:
|
|
163
|
+
// `AccessPolicy` and the conformance suite need only this column (spike in
|
|
164
|
+
// the PR for #485, `registerResourceType` in `app-registrations/sharing.ts`).
|
|
165
|
+
orgId String @map("org_id") @db.Uuid
|
|
166
|
+
title String
|
|
167
|
+
language String?
|
|
168
|
+
|
|
169
|
+
status TranscriptStatus @default(uploading)
|
|
170
|
+
transcriptionStatus TranscriptionStatus @default(waiting_input) @map("transcription_status")
|
|
171
|
+
playbackStatus PlaybackStatus @default(pending) @map("playback_status")
|
|
172
|
+
|
|
173
|
+
// The storage triple. See the block comment above for why every one of
|
|
174
|
+
// these three is `onDelete: Restrict`, and why that is the opposite choice
|
|
175
|
+
// from `ownerId` just above despite the same `String @db.Uuid` shape.
|
|
176
|
+
sourceObjectId String @map("source_object_id") @db.Uuid
|
|
177
|
+
playbackObjectId String? @map("playback_object_id") @db.Uuid
|
|
178
|
+
rawResultObjectId String? @map("raw_result_object_id") @db.Uuid
|
|
179
|
+
|
|
180
|
+
// Which `TranscriptionProvider` (spec §2.1) this transcript was submitted
|
|
181
|
+
// to, and that provider's own job id once submitted. Plain strings, not an
|
|
182
|
+
// enum/FK — the provider registry is code-owned and open (a future
|
|
183
|
+
// provider is one new class, per spec §2.1), the same reasoning `Job.type`
|
|
184
|
+
// and `storage_objects.managed_by` already use for an open, code-owned set.
|
|
185
|
+
provider String
|
|
186
|
+
providerJobId String? @map("provider_job_id")
|
|
187
|
+
// `{ speakersExpected?, language? }` (spec §2.1/§3.1) — the per-transcript
|
|
188
|
+
// overrides a submitter chose, opaque JSONB for the same reason every
|
|
189
|
+
// other provider-shaped payload in this schema (`Job.payload`) is: a
|
|
190
|
+
// provider-specific option set must not force a migration per provider.
|
|
191
|
+
providerOptions Json? @map("provider_options") @db.JsonB
|
|
192
|
+
|
|
193
|
+
submittedAt DateTime? @map("submitted_at") @db.Timestamptz
|
|
194
|
+
lastPolledAt DateTime? @map("last_polled_at") @db.Timestamptz
|
|
195
|
+
completedAt DateTime? @map("completed_at") @db.Timestamptz
|
|
196
|
+
// Set once `provider.deleteRemote` succeeds (spec §1.5.2–4, §10) — either
|
|
197
|
+
// right after `transcription.ingest` commits (the default,
|
|
198
|
+
// `deleteRemoteAfterIngest: true`) or by `transcript.purge` if it had not
|
|
199
|
+
// happened yet. NULL means the provider may still hold a copy.
|
|
200
|
+
remoteDeletedAt DateTime? @map("remote_deleted_at") @db.Timestamptz
|
|
201
|
+
// Set when `DELETE /api/transcripts/:id` moves this row to `deleting`
|
|
202
|
+
// (spec §1.1) — i.e. WHEN REMOVAL WAS REQUESTED, not when it finished:
|
|
203
|
+
// spec §10 is explicit that `transcript.purge` deletes the SQL row once it
|
|
204
|
+
// is done ("only then deletes the SQL rows"), and the `TranscriptStatus`
|
|
205
|
+
// enum has no terminal `deleted` member for a finished purge to move to.
|
|
206
|
+
// This column therefore answers "how long has this been in `deleting`" —
|
|
207
|
+
// the same question `database_backup_runs.lastHeartbeatAt` answers for a
|
|
208
|
+
// running dump — for as long as the row exists to ask it of; it never
|
|
209
|
+
// survives to describe a completed deletion, because nothing does.
|
|
210
|
+
deletedAt DateTime? @map("deleted_at") @db.Timestamptz
|
|
211
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
212
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
213
|
+
// When the recording was made — defaults to the upload instant; the Meeting
|
|
214
|
+
// date the knowledge graph reads (docs/specs/ontology.md §5.4). Issue #352:
|
|
215
|
+
// backfilled from `created_at`, and `TranscriptsService.create` stamps it
|
|
216
|
+
// with the same `Date` instance as `createdAt`. Editable by the owner or an
|
|
217
|
+
// editor through `PATCH /api/transcripts/:id`; not versioned, like `title`.
|
|
218
|
+
recordedAt DateTime @default(now()) @map("recorded_at") @db.Timestamptz
|
|
219
|
+
|
|
220
|
+
durationMs Int? @map("duration_ms")
|
|
221
|
+
failureReason String? @map("failure_reason")
|
|
222
|
+
// Bumped by the SAME conditional `UPDATE ... WHERE current_version = $n`
|
|
223
|
+
// spec §5 describes for the batch-save race — 0 until `transcription
|
|
224
|
+
// .ingest` writes version 1 inside its own transaction (spec §1.5.4).
|
|
225
|
+
currentVersion Int @default(0) @map("current_version")
|
|
226
|
+
// Denormalized off `transcript_speakers`/`transcript_segments` so a list
|
|
227
|
+
// view never joins and counts on every row, matching the "cheap to read
|
|
228
|
+
// often, recomputed by the one write path that changes the true count"
|
|
229
|
+
// shape `worker_nodes` and `job_stats_rollup` already use for their own
|
|
230
|
+
// derived numbers elsewhere in this schema.
|
|
231
|
+
speakerCount Int @default(0) @map("speaker_count")
|
|
232
|
+
wordCount Int @default(0) @map("word_count")
|
|
233
|
+
|
|
234
|
+
// A map `speakerId → displayName` recording the first time a user named an
|
|
235
|
+
// AI-detected speaker (e.g. "Speaker A" -> "Oscar") — issue #323. That
|
|
236
|
+
// naming is an IDENTIFICATION, metadata about the recording like `title`,
|
|
237
|
+
// not a correction to its content, so it is deliberately NOT versioned:
|
|
238
|
+
// `materialize()` overlays this map onto any speaker still at its ingest
|
|
239
|
+
// default name (`Speaker <label>`) at ANY version, the same way a rename
|
|
240
|
+
// applies uniformly regardless of which version is being viewed. Keys are
|
|
241
|
+
// speaker ids with no FK — an entry for a speaker later merged or deleted
|
|
242
|
+
// is simply ignored on overlay, which is harmless and needs no cleanup.
|
|
243
|
+
speakerIdentities Json @default("{}") @map("speaker_identities") @db.JsonB
|
|
244
|
+
|
|
245
|
+
// GENERATED ALWAYS AS (to_tsvector('english'::regconfig, coalesce(title,
|
|
246
|
+
// ''))) STORED, plus a GIN index — issue #174, epic #164. Hand-written in
|
|
247
|
+
// `20260915120000_add_search_vectors/migration.sql` (Prisma has no DSL for
|
|
248
|
+
// a generated column's expression or an index's access method); see that
|
|
249
|
+
// migration's header for the full argument, including why it is its OWN
|
|
250
|
+
// column rather than folded into `TranscriptSegment.searchVector` (a
|
|
251
|
+
// title match must surface a transcript with no matching segment at all)
|
|
252
|
+
// and why the two-argument `to_tsvector` form is required (IMMUTABLE,
|
|
253
|
+
// unlike the one-argument form). `Unsupported` and nullable-optional even
|
|
254
|
+
// though the database column is generation-populated on every row —
|
|
255
|
+
// Prisma has no syntax for "the database writes this," and an
|
|
256
|
+
// `Unsupported` field is never selectable or writable through the client
|
|
257
|
+
// regardless: every read of this column goes through raw SQL, by design.
|
|
258
|
+
titleSearchVector Unsupported("tsvector")? @map("title_search_vector")
|
|
259
|
+
|
|
260
|
+
owner User @relation("TranscriptOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
261
|
+
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
262
|
+
sourceObject StorageObject @relation("TranscriptSourceObject", fields: [sourceObjectId], references: [id], onDelete: Restrict)
|
|
263
|
+
playbackObject StorageObject? @relation("TranscriptPlaybackObject", fields: [playbackObjectId], references: [id], onDelete: Restrict)
|
|
264
|
+
rawResultObject StorageObject? @relation("TranscriptRawResultObject", fields: [rawResultObjectId], references: [id], onDelete: Restrict)
|
|
265
|
+
|
|
266
|
+
speakers TranscriptSpeaker[]
|
|
267
|
+
segments TranscriptSegment[]
|
|
268
|
+
versions TranscriptVersion[]
|
|
269
|
+
exports TranscriptExport[]
|
|
270
|
+
// Every note that named this transcript as its source (issue #48, epic
|
|
271
|
+
// #45) — Restrict on `notes.source_transcript_id`, so a transcript with a
|
|
272
|
+
// dependent note cannot be deleted until `notes.service.ts`'s own delete
|
|
273
|
+
// guard clears it. See the block comment above the `Note` model.
|
|
274
|
+
notesAsSource Note[] @relation("NoteSourceTranscript")
|
|
275
|
+
// AI name-check runs against this transcript (issue #328, epic #326). See
|
|
276
|
+
// the block comment above the `TranscriptNameCheck` model.
|
|
277
|
+
nameChecks TranscriptNameCheck[]
|
|
278
|
+
|
|
279
|
+
// Connected Knowledge (epic #344, issue #351). Both SetNull: an evidence
|
|
280
|
+
// row is a citation that survives losing what it points to (`quote` is
|
|
281
|
+
// what keeps it readable — see the block comment above `KgEvidence`), and
|
|
282
|
+
// a mention Cascades instead (see the block comment above `KgMention`).
|
|
283
|
+
kgEvidence KgEvidence[]
|
|
284
|
+
kgMentions KgMention[]
|
|
285
|
+
|
|
286
|
+
// "This owner's transcripts, newest first" — the transcript list view.
|
|
287
|
+
@@index([ownerId, updatedAt(sort: Desc)])
|
|
288
|
+
// "This owner's transcripts by when they were recorded" (issue #352).
|
|
289
|
+
@@index([ownerId, recordedAt(sort: Desc)])
|
|
290
|
+
// "Every transcript in state X" — `transcripts.housekeeping`'s scans
|
|
291
|
+
// (spec §1.5.8) and any future admin-adjacent health check.
|
|
292
|
+
@@index([status])
|
|
293
|
+
// The organization column's foreign key (issue #485).
|
|
294
|
+
@@index([orgId])
|
|
295
|
+
@@map("transcripts")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
model TranscriptSpeaker {
|
|
299
|
+
id String @id @default(uuid()) @db.Uuid
|
|
300
|
+
transcriptId String @map("transcript_id") @db.Uuid
|
|
301
|
+
|
|
302
|
+
// The provider's own diarization label (e.g. `"A"`). NULL for a speaker a
|
|
303
|
+
// user created directly (`speaker.create`, spec §4.1), which has none.
|
|
304
|
+
// Unique per transcript AMONG LABELLED ROWS ONLY — see the block comment
|
|
305
|
+
// above the enums for why that constraint is hand-written in
|
|
306
|
+
// `migration.sql` rather than a `@@unique` here.
|
|
307
|
+
label String?
|
|
308
|
+
displayName String @map("display_name")
|
|
309
|
+
colorIndex Int @map("color_index")
|
|
310
|
+
// Optimistic-concurrency counter (spec §5). Starts at 1 — the row's first
|
|
311
|
+
// write (ingest, or `speaker.create`) is already its first version.
|
|
312
|
+
rev Int @default(1)
|
|
313
|
+
|
|
314
|
+
transcript Transcript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
|
|
315
|
+
segments TranscriptSegment[]
|
|
316
|
+
|
|
317
|
+
// Connected Knowledge (epic #344, issue #351). The `IDENTIFIED_AS` edge a
|
|
318
|
+
// diarized speaker gets linked to a real entity through — Cascade, so a
|
|
319
|
+
// deleted (merged-away) speaker's link goes with it. See the block
|
|
320
|
+
// comment above `KgRelation` for why this is a real FK to a speaker
|
|
321
|
+
// rather than a polymorphic `from_kind`/`from_id` pair.
|
|
322
|
+
kgRelationsAsSpeaker KgRelation[]
|
|
323
|
+
|
|
324
|
+
@@map("transcript_speakers")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
enum WordsAlignment {
|
|
328
|
+
exact
|
|
329
|
+
interpolated
|
|
330
|
+
none
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
enum SegmentOrigin {
|
|
334
|
+
ai
|
|
335
|
+
user
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
model TranscriptSegment {
|
|
339
|
+
// STABLE ACROSS EDITS (spec §3.5, §4.1's `segment.split` test vector): a
|
|
340
|
+
// split keeps this id on the earlier half specifically so a bookmark, an
|
|
341
|
+
// export reference, or a concurrent editor's stale `rev` for this segment
|
|
342
|
+
// still names something real.
|
|
343
|
+
id String @id @default(uuid()) @db.Uuid
|
|
344
|
+
transcriptId String @map("transcript_id") @db.Uuid
|
|
345
|
+
// Restrict, not Cascade or SetNull — see the block comment above the
|
|
346
|
+
// enums: a speaker may not be deleted while a segment still names it;
|
|
347
|
+
// `speaker.merge` re-points every segment away first.
|
|
348
|
+
speakerId String @map("speaker_id") @db.Uuid
|
|
349
|
+
|
|
350
|
+
startMs Int @map("start_ms")
|
|
351
|
+
endMs Int @map("end_ms")
|
|
352
|
+
// Gap-based float, NOT a dense integer sequence (spec §3.4): a
|
|
353
|
+
// `segment.split` only needs the midpoint between its two neighbours and
|
|
354
|
+
// touches no other row, versus an integer sequence that would need to
|
|
355
|
+
// renumber every later segment on every single split.
|
|
356
|
+
ordinal Float
|
|
357
|
+
|
|
358
|
+
text String
|
|
359
|
+
// `[{t, s, e, c}]` — text, start, end, confidence per word (spec §3.3),
|
|
360
|
+
// kept terse because a 10-hour transcript's word array is the single
|
|
361
|
+
// largest thing in this schema. Opaque JSONB, not a child table: word rows
|
|
362
|
+
// are never queried or updated independently of their segment.
|
|
363
|
+
words Json @db.JsonB
|
|
364
|
+
// How much of `words[]` to trust after an edit (spec §3.5). `exact` is the
|
|
365
|
+
// default because a freshly ingested (`ai_original`) segment, and either
|
|
366
|
+
// half of a `segment.split`/`segment.join`, are always exact — nothing
|
|
367
|
+
// about dividing or concatenating an existing array invents a timing.
|
|
368
|
+
wordsAlignment WordsAlignment @default(exact) @map("words_alignment")
|
|
369
|
+
confidence Float?
|
|
370
|
+
|
|
371
|
+
// Whether this segment's TEXT originated from the provider or was created
|
|
372
|
+
// by a person. Defaults `ai` because every segment `transcription.ingest`
|
|
373
|
+
// writes is exactly that; nothing in spec §4.1's op table currently
|
|
374
|
+
// creates a segment from nothing (only `speaker.create` has a from-nothing
|
|
375
|
+
// shape), so `user` exists for a future op without one today.
|
|
376
|
+
origin SegmentOrigin @default(ai)
|
|
377
|
+
// Optimistic-concurrency counter (spec §5), same starting value and same
|
|
378
|
+
// reasoning as `TranscriptSpeaker.rev` above.
|
|
379
|
+
rev Int @default(1)
|
|
380
|
+
editedById String? @map("edited_by_id") @db.Uuid
|
|
381
|
+
editedAt DateTime? @map("edited_at") @db.Timestamptz
|
|
382
|
+
|
|
383
|
+
// GENERATED ALWAYS AS (to_tsvector('english'::regconfig, coalesce(text,
|
|
384
|
+
// ''))) STORED, plus a GIN index — issue #174, epic #164. Unweighted,
|
|
385
|
+
// unlike `Note.searchVector` below: a segment is one line of speech with
|
|
386
|
+
// no title of its own to weight against. Hand-written in
|
|
387
|
+
// `20260915120000_add_search_vectors/migration.sql`; see that migration's
|
|
388
|
+
// header for the full argument (why the two-argument `to_tsvector` form
|
|
389
|
+
// is required, why `'english'` is a stated v1 limitation, why a generated
|
|
390
|
+
// column rather than a trigger — a trigger does not fire under the
|
|
391
|
+
// `session_replication_role = replica` a database restore runs under).
|
|
392
|
+
// `Unsupported` and nullable-optional for the same reason
|
|
393
|
+
// `Transcript.titleSearchVector` is: Prisma cannot express "the database
|
|
394
|
+
// writes this," and this field is never selectable or writable through
|
|
395
|
+
// the client — every read goes through raw SQL.
|
|
396
|
+
searchVector Unsupported("tsvector")? @map("search_vector")
|
|
397
|
+
|
|
398
|
+
transcript Transcript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
|
|
399
|
+
speaker TranscriptSpeaker @relation(fields: [speakerId], references: [id], onDelete: Restrict)
|
|
400
|
+
// SetNull, matching `AuditEvent.actorUserId`/`Credential.updatedByUserId`:
|
|
401
|
+
// "who last hand-edited this line" is an audit fact about the segment, not
|
|
402
|
+
// something the segment's own existence depends on.
|
|
403
|
+
editedBy User? @relation("TranscriptSegmentEditor", fields: [editedById], references: [id], onDelete: SetNull)
|
|
404
|
+
|
|
405
|
+
// AI name-check suggestions proposed against this segment (issue #328,
|
|
406
|
+
// epic #326). See the block comment above the `TranscriptNameCheck` model.
|
|
407
|
+
nameSuggestions TranscriptNameSuggestion[]
|
|
408
|
+
|
|
409
|
+
// Connected Knowledge (epic #344, issue #351). SetNull — an evidence row
|
|
410
|
+
// is a citation that survives losing the segment it pointed to; `quote`
|
|
411
|
+
// is what keeps it readable afterwards. See the block comment above
|
|
412
|
+
// `KgEvidence`.
|
|
413
|
+
kgEvidence KgEvidence[]
|
|
414
|
+
|
|
415
|
+
// The compound order both a segment list and a time-window query
|
|
416
|
+
// (`GET /:id/words?fromMs&toMs`) need (spec §3.3).
|
|
417
|
+
@@index([transcriptId, startMs, ordinal])
|
|
418
|
+
@@map("transcript_segments")
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
enum TranscriptVersionKind {
|
|
422
|
+
ai_original
|
|
423
|
+
edit
|
|
424
|
+
restore
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
model TranscriptVersion {
|
|
428
|
+
id String @id @default(uuid()) @db.Uuid
|
|
429
|
+
transcriptId String @map("transcript_id") @db.Uuid
|
|
430
|
+
version Int
|
|
431
|
+
kind TranscriptVersionKind
|
|
432
|
+
|
|
433
|
+
// NULL means "the AI" — the ingest handler is the only writer that ever
|
|
434
|
+
// leaves this null (spec §4.5); every human-triggered version (`edit`,
|
|
435
|
+
// `restore`) always has a real author. SetNull, not Restrict/Cascade: a
|
|
436
|
+
// version is permanent history (spec §4.5 — "nothing in this design ever
|
|
437
|
+
// deletes a `transcript_versions` row"; only `transcript.purge` deleting
|
|
438
|
+
// the whole transcript does) and must survive the author's own account
|
|
439
|
+
// being deleted, the same audit posture as `DatabaseBackupRun.createdById`.
|
|
440
|
+
authorId String? @map("author_id") @db.Uuid
|
|
441
|
+
summary String?
|
|
442
|
+
// The concrete ops this version applied (spec §4.1) — e.g. the expanded
|
|
443
|
+
// `segment.update_text` ops a `transcript.find_replace` call becomes
|
|
444
|
+
// (spec §4.2), NEVER the abstract find/replace call itself, so replaying
|
|
445
|
+
// history stays correct even if this application's matching logic changes
|
|
446
|
+
// later.
|
|
447
|
+
ops Json @db.JsonB
|
|
448
|
+
|
|
449
|
+
// The version number this row restored FROM, when `kind: restore` (spec
|
|
450
|
+
// §4.5). A plain version number, not a self-relation FK: it names another
|
|
451
|
+
// row's `(transcriptId, version)` pair, which is not a column any FK can
|
|
452
|
+
// point at directly, and a restore's target version is immutable history
|
|
453
|
+
// that is never itself deleted (short of purging the whole transcript).
|
|
454
|
+
restoredFromVersion Int? @map("restored_from_version")
|
|
455
|
+
// The gzipped materialized state at this version, when this version was
|
|
456
|
+
// snapshotted (spec §4.3 — always for v1 and every restore, otherwise on a
|
|
457
|
+
// size-or-count trigger). NULL for a version materialized purely by
|
|
458
|
+
// replaying `ops` from the nearest earlier snapshot.
|
|
459
|
+
snapshotObjectId String? @map("snapshot_object_id") @db.Uuid
|
|
460
|
+
// Idempotency key for a retried save (spec §5): a client that saved but
|
|
461
|
+
// never saw the response retries with the SAME `clientBatchId`, and
|
|
462
|
+
// `@@unique([transcriptId, clientBatchId])` below is what lets the write
|
|
463
|
+
// path detect the retry and return the original version instead of
|
|
464
|
+
// creating a second one. NULL for a version this application created
|
|
465
|
+
// itself (ingest, restore) rather than in response to a client batch —
|
|
466
|
+
// Postgres's standard NULLS-DISTINCT behaviour is exactly what is wanted
|
|
467
|
+
// there: any number of null-`clientBatchId` versions may coexist.
|
|
468
|
+
clientBatchId String? @map("client_batch_id")
|
|
469
|
+
|
|
470
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
471
|
+
|
|
472
|
+
transcript Transcript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
|
|
473
|
+
author User? @relation("TranscriptVersionAuthor", fields: [authorId], references: [id], onDelete: SetNull)
|
|
474
|
+
// Restrict, matching every other storage_objects FK on this feature — see
|
|
475
|
+
// the block comment above the enums.
|
|
476
|
+
snapshotObject StorageObject? @relation("TranscriptVersionSnapshotObject", fields: [snapshotObjectId], references: [id], onDelete: Restrict)
|
|
477
|
+
|
|
478
|
+
// The version sequence itself, and what `materialize()` (spec §4.4) looks
|
|
479
|
+
// up by.
|
|
480
|
+
@@unique([transcriptId, version])
|
|
481
|
+
// The idempotent-retry key described above. Plain `@@unique`, not a
|
|
482
|
+
// hand-written partial index: Postgres's default NULLS DISTINCT behaviour
|
|
483
|
+
// already gives exactly the wanted semantics for a nullable column, unlike
|
|
484
|
+
// `transcript_speakers`' label uniqueness above.
|
|
485
|
+
@@unique([transcriptId, clientBatchId])
|
|
486
|
+
@@map("transcript_versions")
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
enum TranscriptExportStatus {
|
|
490
|
+
pending
|
|
491
|
+
ready
|
|
492
|
+
failed
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
model TranscriptExport {
|
|
496
|
+
id String @id @default(uuid()) @db.Uuid
|
|
497
|
+
transcriptId String @map("transcript_id") @db.Uuid
|
|
498
|
+
version Int
|
|
499
|
+
|
|
500
|
+
// The exporter's own registry key (`markdown`, `pdf`, or the permanent
|
|
501
|
+
// published JSON format spec §8.2 names). Plain string, not an enum, for
|
|
502
|
+
// the same self-registering-registry reason `Job.type` is: adding an
|
|
503
|
+
// exporter (spec §8.1 — "one new class registering itself") must cost
|
|
504
|
+
// zero migrations to this table.
|
|
505
|
+
format String
|
|
506
|
+
// The requester's export options, as passed to the exporter's own
|
|
507
|
+
// `optionsSchema` (spec §8.1) — opaque JSONB, since the option shape is
|
|
508
|
+
// defined by the exporter, not by this table.
|
|
509
|
+
options Json @db.JsonB
|
|
510
|
+
// sha256 (or similar) of `{ format, version, options }` (spec §8.5) — the
|
|
511
|
+
// content-addressed key `POST /:id/exports` looks up to decide whether an
|
|
512
|
+
// existing, unexpired export can be returned instead of rendering again.
|
|
513
|
+
optionsHash String @map("options_hash")
|
|
514
|
+
status TranscriptExportStatus @default(pending)
|
|
515
|
+
|
|
516
|
+
// The rendered file, once `status: ready`. Restrict, matching every other
|
|
517
|
+
// storage_objects FK on this feature (block comment above the enums) —
|
|
518
|
+
// only `transcripts.housekeeping`'s expiry sweep (spec §8.5) may remove
|
|
519
|
+
// it, and only after deleting this row first.
|
|
520
|
+
objectId String? @map("object_id") @db.Uuid
|
|
521
|
+
// The `transcript.export` job that rendered this row, when one did (spec
|
|
522
|
+
// §1.5.6). `@unique`/SetNull, mirroring `DatabaseBackupRun.jobId` exactly:
|
|
523
|
+
// a second render for the SAME job is unrepresentable, this row's own
|
|
524
|
+
// 7-day retention is independent of `job.history.purge`'s schedule for the
|
|
525
|
+
// underlying `jobs` row, and losing the link on a history purge must never
|
|
526
|
+
// mean losing the export.
|
|
527
|
+
jobId String? @unique @map("job_id") @db.Uuid
|
|
528
|
+
// Who asked for this export. NOT NULL and Cascade — unlike every SetNull
|
|
529
|
+
// audit column elsewhere in this schema, an export is a disposable,
|
|
530
|
+
// byte-for-byte reproducible artifact with a 7-day expiry already built in
|
|
531
|
+
// (spec §8.5): nothing is lost by deleting a pending/ready export row
|
|
532
|
+
// alongside the account that requested it, the same way nothing is lost
|
|
533
|
+
// by `transcripts.housekeeping` expiring it on its own schedule.
|
|
534
|
+
requestedById String @map("requested_by_id") @db.Uuid
|
|
535
|
+
|
|
536
|
+
error String?
|
|
537
|
+
// 7 days out from creation by default (spec §8.5) — what
|
|
538
|
+
// `transcripts.housekeeping`'s sweep compares against.
|
|
539
|
+
expiresAt DateTime @map("expires_at") @db.Timestamptz
|
|
540
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
541
|
+
|
|
542
|
+
transcript Transcript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
|
|
543
|
+
object StorageObject? @relation("TranscriptExportObject", fields: [objectId], references: [id], onDelete: Restrict)
|
|
544
|
+
job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
|
|
545
|
+
requestedBy User @relation("TranscriptExportRequestedBy", fields: [requestedById], references: [id], onDelete: Cascade)
|
|
546
|
+
|
|
547
|
+
// The reuse-by-content-hash lookup spec §8.5 names explicitly ("the index
|
|
548
|
+
// issue #24 declares for exactly this lookup"). Not unique: an expired or
|
|
549
|
+
// failed row may briefly coexist with a fresh one sharing the same key
|
|
550
|
+
// before housekeeping removes the stale one.
|
|
551
|
+
//
|
|
552
|
+
// `map:` pins an explicit name: Prisma's own default-generated name for
|
|
553
|
+
// this four-column index is 64 bytes, one over PostgreSQL's 63-byte
|
|
554
|
+
// identifier limit, which would otherwise leave the exact truncated name
|
|
555
|
+
// to guess at in the hand-written migration.sql this issue ships without a
|
|
556
|
+
// live database to diff against.
|
|
557
|
+
@@index([transcriptId, version, format, optionsHash], map: "transcript_exports_lookup_idx")
|
|
558
|
+
@@map("transcript_exports")
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// =============================================================================
|
|
562
|
+
// Transcript Name Checks (issue #328, epic #326)
|
|
563
|
+
// =============================================================================
|
|
564
|
+
//
|
|
565
|
+
// AI-assisted detection of misspelled/mis-transcribed proper names and terms
|
|
566
|
+
// across a transcript's segments. A `TranscriptNameCheck` is one AI run —
|
|
567
|
+
// the request, its cost accounting, and its terminal state — and a
|
|
568
|
+
// `TranscriptNameSuggestion` is one PROPOSED edit that run produced. Both
|
|
569
|
+
// tables are deliberately never a second source of truth for transcript
|
|
570
|
+
// content: accepting a suggestion turns it into an ordinary
|
|
571
|
+
// `segment.update_text` op recorded through the exact same `/operations`
|
|
572
|
+
// path and `TranscriptVersion` history every other correction uses (see the
|
|
573
|
+
// block comment above `TranscriptVersion`) — these rows never write
|
|
574
|
+
// `transcript_segments.text` directly, only propose a change to it.
|
|
575
|
+
// `segmentRev` on a suggestion pins the `TranscriptSegment.rev` its offsets
|
|
576
|
+
// were computed against, the identical optimistic-concurrency shape
|
|
577
|
+
// `TranscriptVersion`'s `rev` split already uses elsewhere in this schema,
|
|
578
|
+
// so a suggestion whose segment was edited out from under it is detectable
|
|
579
|
+
// at apply time rather than silently misapplied at the wrong offset —
|
|
580
|
+
// `status: stale` is that outcome, not a bug report.
|
|
581
|
+
|
|
582
|
+
enum TranscriptNameCheckMode {
|
|
583
|
+
standard
|
|
584
|
+
thorough
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
enum TranscriptNameCheckStatus {
|
|
588
|
+
pending
|
|
589
|
+
running
|
|
590
|
+
ready
|
|
591
|
+
failed
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
model TranscriptNameCheck {
|
|
595
|
+
id String @id @default(uuid()) @db.Uuid
|
|
596
|
+
transcriptId String @map("transcript_id") @db.Uuid
|
|
597
|
+
|
|
598
|
+
// Who asked for this run. SetNull, not Cascade/Restrict — unlike
|
|
599
|
+
// `TranscriptExport.requestedById` (a disposable, re-renderable artifact
|
|
600
|
+
// deleted with its requester), a name-check run and the suggestions it
|
|
601
|
+
// produced are worth keeping as transcript history even after the
|
|
602
|
+
// requesting account is gone, the same audit posture
|
|
603
|
+
// `TranscriptVersion.authorId` already takes.
|
|
604
|
+
requestedById String? @map("requested_by_id") @db.Uuid
|
|
605
|
+
|
|
606
|
+
// `standard` (segments only) vs `thorough` (a wider discovery pass) — a
|
|
607
|
+
// closed, spec-defined pair, worth a real enum rather than a string.
|
|
608
|
+
mode TranscriptNameCheckMode
|
|
609
|
+
status TranscriptNameCheckStatus @default(pending)
|
|
610
|
+
|
|
611
|
+
// The `Transcript.currentVersion` this run read segment text from,
|
|
612
|
+
// matching `TranscriptExport.version`'s reasoning: a run started against
|
|
613
|
+
// one version must not be silently reinterpreted against a later one once
|
|
614
|
+
// edits land mid-run.
|
|
615
|
+
basedOnVersion Int @map("based_on_version")
|
|
616
|
+
|
|
617
|
+
// The target names/terms this run checked for — a string array, opaque
|
|
618
|
+
// JSONB for the same reason `TranscriptSegment.words` and
|
|
619
|
+
// `Transcript.providerOptions` are: the caller-supplied or AI-discovered
|
|
620
|
+
// term list has no shape this table needs to enforce.
|
|
621
|
+
terms Json @db.JsonB
|
|
622
|
+
|
|
623
|
+
// Which AI provider/model produced this run's suggestions — plain
|
|
624
|
+
// strings, not an enum/FK, matching `Transcript.provider`'s own
|
|
625
|
+
// open-registry reasoning: a new provider must cost zero migrations here.
|
|
626
|
+
providerId String? @map("provider_id")
|
|
627
|
+
model String?
|
|
628
|
+
|
|
629
|
+
// Denormalized off `transcript_name_suggestions` so a list/detail read
|
|
630
|
+
// never joins and counts on every request, matching
|
|
631
|
+
// `Transcript.speakerCount`/`.wordCount`'s "cheap to read often,
|
|
632
|
+
// recomputed by the one write path that changes the true count" shape.
|
|
633
|
+
candidateCount Int @default(0) @map("candidate_count")
|
|
634
|
+
suggestionCount Int @default(0) @map("suggestion_count")
|
|
635
|
+
|
|
636
|
+
// Cost accounting for this run's own AI call(s), the same pair
|
|
637
|
+
// `NoteGeneration` tracks per generation.
|
|
638
|
+
inputTokens Int @default(0) @map("input_tokens")
|
|
639
|
+
outputTokens Int @default(0) @map("output_tokens")
|
|
640
|
+
|
|
641
|
+
// A closed-in-spirit taxonomy kept as a plain string rather than a second
|
|
642
|
+
// copy of `NoteGenerationErrorClass`'s enum, matching `Job.type`'s own
|
|
643
|
+
// "plain string, code-owned set" reasoning.
|
|
644
|
+
errorClass String? @map("error_class")
|
|
645
|
+
error String?
|
|
646
|
+
|
|
647
|
+
// The `transcript.name_check` job that ran this check, when one did. `@unique`/
|
|
648
|
+
// nullable/`SetNull`, mirroring `TranscriptExport.jobId`/
|
|
649
|
+
// `DatabaseBackupRun.jobId` exactly: this row's own lifetime (read by the
|
|
650
|
+
// review UI long after job history is purged) is independent of
|
|
651
|
+
// `job.history.purge`'s schedule for the underlying `jobs` row.
|
|
652
|
+
jobId String? @unique @map("job_id") @db.Uuid
|
|
653
|
+
|
|
654
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
655
|
+
startedAt DateTime? @map("started_at") @db.Timestamptz
|
|
656
|
+
completedAt DateTime? @map("completed_at") @db.Timestamptz
|
|
657
|
+
|
|
658
|
+
transcript Transcript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
|
|
659
|
+
requestedBy User? @relation("TranscriptNameCheckRequestedBy", fields: [requestedById], references: [id], onDelete: SetNull)
|
|
660
|
+
job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull)
|
|
661
|
+
|
|
662
|
+
suggestions TranscriptNameSuggestion[]
|
|
663
|
+
|
|
664
|
+
// "This transcript's name-check runs, newest first" — the run history
|
|
665
|
+
// list.
|
|
666
|
+
@@index([transcriptId, createdAt(sort: Desc)])
|
|
667
|
+
@@map("transcript_name_checks")
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
enum TranscriptNameSuggestionStatus {
|
|
671
|
+
pending
|
|
672
|
+
accepted
|
|
673
|
+
rejected
|
|
674
|
+
stale
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
model TranscriptNameSuggestion {
|
|
678
|
+
id String @id @default(uuid()) @db.Uuid
|
|
679
|
+
checkId String @map("check_id") @db.Uuid
|
|
680
|
+
|
|
681
|
+
// Cascade, matching `TranscriptSegment`'s own relation to `Transcript`: a
|
|
682
|
+
// suggestion against a segment that no longer exists (a later
|
|
683
|
+
// `segment.split`/`segment.join`/delete) has nothing left to apply to.
|
|
684
|
+
segmentId String @map("segment_id") @db.Uuid
|
|
685
|
+
// The `TranscriptSegment.rev` these offsets were computed against — see
|
|
686
|
+
// the block comment above for why a mismatch at apply time means `stale`,
|
|
687
|
+
// never a silently wrong offset.
|
|
688
|
+
segmentRev Int @map("segment_rev")
|
|
689
|
+
|
|
690
|
+
// UTF-16 offsets into `TranscriptSegment.text` at `segmentRev` — matching
|
|
691
|
+
// JavaScript string indexing end to end, so the editor, the API and this
|
|
692
|
+
// column all agree on one encoding.
|
|
693
|
+
start Int
|
|
694
|
+
end Int
|
|
695
|
+
|
|
696
|
+
original String
|
|
697
|
+
replacement String
|
|
698
|
+
|
|
699
|
+
confidence Float?
|
|
700
|
+
reason String?
|
|
701
|
+
// `'phonetic' | 'discovery'` today — plain string, not an enum, matching
|
|
702
|
+
// `Transcript.provider`'s own open-registry reasoning: which detection
|
|
703
|
+
// strategies exist is code, not schema.
|
|
704
|
+
source String
|
|
705
|
+
|
|
706
|
+
status TranscriptNameSuggestionStatus @default(pending)
|
|
707
|
+
decidedAt DateTime? @map("decided_at") @db.Timestamptz
|
|
708
|
+
// SetNull, matching `TranscriptSegment.editedById`: "who accepted or
|
|
709
|
+
// rejected this suggestion" is an audit fact about the suggestion, not
|
|
710
|
+
// something its own existence depends on.
|
|
711
|
+
decidedById String? @map("decided_by_id") @db.Uuid
|
|
712
|
+
|
|
713
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
714
|
+
|
|
715
|
+
check TranscriptNameCheck @relation(fields: [checkId], references: [id], onDelete: Cascade)
|
|
716
|
+
segment TranscriptSegment @relation(fields: [segmentId], references: [id], onDelete: Cascade)
|
|
717
|
+
decidedBy User? @relation("TranscriptNameSuggestionDecidedBy", fields: [decidedById], references: [id], onDelete: SetNull)
|
|
718
|
+
|
|
719
|
+
// "This run's suggestions, filtered by status" — the review UI's default
|
|
720
|
+
// list.
|
|
721
|
+
@@index([checkId, status])
|
|
722
|
+
// "Every suggestion touching this segment" — applying/staling suggestions
|
|
723
|
+
// when a segment is edited elsewhere.
|
|
724
|
+
@@index([segmentId])
|
|
725
|
+
@@map("transcript_name_suggestions")
|
|
726
|
+
}
|