-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathschema.prisma
More file actions
878 lines (772 loc) · 30.1 KB
/
Copy pathschema.prisma
File metadata and controls
878 lines (772 loc) · 30.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
// TODO: Standardize created_by/creator and updated_by/updator columns.
// TODO: Standardize case formatting. Decide between snake_case (bcgov recommended) or camelCase (prisma recommended).
generator client {
provider = "prisma-client-js"
// Output path will be specified per-app via prisma generate command
}
datasource db {
provider = "postgresql"
}
enum WorkflowKind {
primary
benchmark_candidate
}
model Document {
id String @id @default(cuid())
title String
original_filename String
file_path String
/// Normalized PDF blob key for OCR, in-app view, and workflows (always application/pdf).
normalized_file_path String?
file_type String
file_size Int
metadata Json?
source String
status DocumentStatus @default(pre_ocr)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
/// Set by the ephemeral-document cleanup janitor once the document's blobs
/// and Temporal execution record have been purged. Null = not yet purged.
purged_at DateTime?
apim_request_id String?
workflow_id String? // @deprecated: Use workflow_config_id and workflow_execution_id instead
workflow_config_id String? // Reference to WorkflowVersion.id (pinned graph config)
workflow_execution_id String? @unique // Temporal workflow execution ID
model_id String @default("prebuilt-layout")
ocr_result OcrResult?
review_sessions ReviewSession[]
lock DocumentLock?
groundTruthJob DatasetGroundTruthJob?
group_id String
group Group @relation(fields: [group_id], references: [id])
workflowVersion WorkflowVersion? @relation(fields: [workflow_config_id], references: [id], onDelete: SetNull)
@@index([group_id])
@@index([workflow_config_id])
// Supports the documents list endpoint's common access pattern:
// filter by group_id, order by created_at DESC, limit/offset. Lets Postgres
// serve the default sort from the index instead of sorting the group's rows.
@@index([group_id, created_at(sort: Desc)])
// Partial index `documents_purge_scan_idx` on (workflow_config_id, status)
// WHERE purged_at IS NULL supports the ephemeral-cleanup janitor. Prisma
// cannot express partial indexes, so it is managed via the raw SQL migration
// 20260624000000_add_documents_purge_index. Do not let `migrate dev` drop it.
//
// Trigram GIN indexes `documents_title_trgm_idx` /
// `documents_original_filename_trgm_idx` back the list endpoint's ILIKE
// search. Prisma cannot express GIN/trigram indexes, so they are managed via
// the raw SQL migration 20260626000000_add_documents_list_indexes. Do not let
// `migrate dev` drop them.
@@map("documents")
}
model LabelingDocument {
id String @id @default(cuid())
title String
original_filename String
file_path String
/// Normalized PDF blob key for OCR and in-app view (always application/pdf).
normalized_file_path String?
file_type String
file_size Int
metadata Json?
source String
status DocumentStatus @default(pre_ocr)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
apim_request_id String?
model_id String @default("prebuilt-layout")
ocr_result Json?
labeled_documents LabeledDocument[]
group_id String
group Group @relation(fields: [group_id], references: [id])
@@index([group_id])
@@map("labeling_documents")
}
model OcrResult {
id String @id @default(cuid())
document_id String @unique
document Document @relation(fields: [document_id], references: [id], onDelete: Cascade)
keyValuePairs Json?
/// Structured OCR output: { format: "text"|"markdown", text: string,
/// markdown?: string, pages: [{ pageNumber, content, lines }] }. Populated
/// for prebuilt read/layout/document models where there are no fields to extract.
content Json?
enrichment_summary Json?
processed_at DateTime @default(now())
@@map("ocr_results")
}
model ApiKey {
id String @id @default(cuid())
key_hash String @unique
key_prefix String
/// The user who most recently generated or regenerated this key. Recorded for
/// audit purposes only; not used for authentication identity resolution.
generating_user_id String
generating_user User @relation(fields: [generating_user_id], references: [id])
group_id String @unique
group Group @relation(fields: [group_id], references: [id])
created_at DateTime @default(now())
last_used DateTime?
actor_id String @unique
actor Actor @relation(fields: [actor_id], references: [id])
@@index([group_id])
@@map("api_keys")
}
model User {
id String @id
email String @unique
last_login_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
is_system_admin Boolean @default(false)
actor_id String @unique
actor Actor @relation(fields: [actor_id], references: [id])
apiKeys ApiKey[]
userGroups UserGroup[]
membershipRequests GroupMembershipRequest[] @relation(name: "MembershipRequestUser")
@@map("user")
}
model Actor {
id String @id @default(cuid())
apiKey ApiKey?
user User?
templateModels TemplateModel[]
classifierModelsCreated ClassifierModel[] @relation(name: "ClassifierCreatedByActor")
classifierModelsUpdated ClassifierModel[] @relation(name: "ClassifierUpdatedByActor")
createdGroups Group[] @relation(name: "GroupCreatedBy")
updatedGroups Group[] @relation(name: "GroupUpdatedBy")
deletedGroups Group[] @relation(name: "GroupDeletedBy")
reviewSessions ReviewSession[]
datasets Dataset[]
benchmarkProjects BenchmarkProject[]
benchmarkAuditLogs BenchmarkAuditLog[]
createdMembershipRequests GroupMembershipRequest[] @relation(name: "MembershipRequestCreatedBy")
updatedMembershipRequests GroupMembershipRequest[] @relation(name: "MembershipRequestUpdatedBy")
workflowLineages WorkflowLineage[]
@@map("actor")
}
/// Stable workflow identity (list in UI). Config lives on WorkflowVersion rows.
model WorkflowLineage {
id String @id @default(cuid())
name String
/// URL/CLI-friendly stable handle for the workflow, unique within the group.
/// Auto-derived from `name` on create (kebab-case, deduped). Immutable across renames.
slug String
description String?
actor_id String
actor Actor @relation(fields: [actor_id], references: [id])
group_id String
group Group @relation(fields: [group_id], references: [id])
workflow_kind WorkflowKind @default(primary)
source_workflow_id String? // Source/base lineage id (only for benchmark candidates)
sourceWorkflow WorkflowLineage? @relation("LineageSource", fields: [source_workflow_id], references: [id], onDelete: SetNull)
head_version_id String? @unique
headVersion WorkflowVersion? @relation("LineageHead", fields: [head_version_id], references: [id], onDelete: SetNull)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
versions WorkflowVersion[] @relation("LineageVersions")
benchmarkCandidateLineages WorkflowLineage[] @relation("LineageSource")
@@unique([group_id, slug])
@@index([group_id])
@@map("workflow_lineages")
}
/// Immutable graph config revision; revert = change pin to an older version id.
model WorkflowVersion {
id String @id @default(cuid())
lineage_id String
lineage WorkflowLineage @relation("LineageVersions", fields: [lineage_id], references: [id], onDelete: Cascade)
version_number Int
config Json // GraphWorkflowConfig
created_at DateTime @default(now())
headOfLineage WorkflowLineage? @relation("LineageHead")
benchmarkDefinitions BenchmarkDefinition[]
documents Document[]
groundTruthJobs DatasetGroundTruthJob[]
@@unique([lineage_id, version_number])
@@index([lineage_id])
@@map("workflow_versions")
}
// ========== TEMPLATE MODELS ==========
model TemplateModel {
id String @id @default(cuid())
name String
model_id String @unique
description String?
created_by String
creator Actor @relation(fields: [created_by], references: [id])
created_at DateTime @default(now())
updated_at DateTime @updatedAt
status TemplateModelStatus @default(draft)
field_schema FieldDefinition[]
documents LabeledDocument[]
training_jobs TrainingJob[]
trained_models TrainedModel[]
group_id String
group Group @relation(fields: [group_id], references: [id])
@@index([group_id])
@@map("template_models")
}
model FieldDefinition {
id String @id @default(cuid())
template_model_id String
template_model TemplateModel @relation(fields: [template_model_id], references: [id], onDelete: Cascade)
field_key String
field_type FieldType @default(string)
field_format String?
format_spec String? // JSON format spec for normalization/validation
display_order Int @default(0)
@@unique([template_model_id, field_key])
@@map("field_definitions")
}
model LabeledDocument {
id String @id @default(cuid())
template_model_id String
template_model TemplateModel @relation(fields: [template_model_id], references: [id], onDelete: Cascade)
labeling_document_id String
labeling_document LabelingDocument @relation(fields: [labeling_document_id], references: [id], onDelete: Cascade)
status LabelingStatus @default(unlabeled)
labels DocumentLabel[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([template_model_id, labeling_document_id])
@@map("labeled_documents")
}
model DocumentLabel {
id String @id @default(cuid())
labeled_doc_id String
labeled_doc LabeledDocument @relation(fields: [labeled_doc_id], references: [id], onDelete: Cascade)
field_key String
label_name String
value String?
page_number Int
bounding_box Json
created_at DateTime @default(now())
@@index([labeled_doc_id, field_key])
@@map("document_labels")
}
// ========== TRAINING MODELS ==========
model TrainingJob {
id String @id @default(cuid())
template_model_id String
template_model TemplateModel @relation(fields: [template_model_id], references: [id], onDelete: Cascade)
status TrainingStatus @default(PENDING)
container_name String
sas_url String?
blob_count Int @default(0)
/// Azure build mode used for this training run.
build_mode BuildMode @default(template)
/// Optional max training hours budget. Only meaningful for neural builds;
/// passed straight through to the Azure `maxTrainingHours` parameter.
max_training_hours Float?
/// The versioned Azure model ID this job targets (e.g. "km-invoice-v3").
/// Captured at startTraining time so the poller and downstream consumers
/// don't have to re-derive it from naming conventions.
target_model_id String?
/// The version number this job will produce. Persisted alongside
/// target_model_id for the same reason.
target_version Int?
operation_id String?
error_message String?
started_at DateTime @default(now())
completed_at DateTime?
trained_model TrainedModel?
@@index([template_model_id])
@@index([status])
@@map("training_jobs")
}
model TrainedModel {
id String @id @default(cuid())
template_model_id String
template_model TemplateModel @relation(fields: [template_model_id], references: [id], onDelete: Cascade)
training_job_id String @unique
training_job TrainingJob @relation(fields: [training_job_id], references: [id])
/// Versioned Azure model identifier. v1 keeps the bare TemplateModel.model_id
/// for backwards compatibility; v2+ append "-v<n>".
model_id String @unique
/// Sequential version per TemplateModel, starting at 1.
version Int @default(1)
/// Exactly one row per TemplateModel should be active. Documents and benchmarks
/// referencing TemplateModel.model_id resolve to whichever version is active.
is_active Boolean @default(false)
/// Tombstone for deleted versions. The Azure artifact is removed but the row
/// is kept so audit trails ("v2 — deleted on …") still resolve.
deleted_at DateTime?
/// Snapshot of the labeled documents + labels used to train this version.
/// Shape: { documents: Array<{ labelingDocumentId, originalFilename, labels: Array<{ fieldKey, labelName, value, pageNumber, boundingBox }> }> }
dataset_snapshot Json?
description String?
doc_types Json?
field_count Int @default(0)
/// Mirrors TrainingJob.build_mode at the time of success.
build_mode BuildMode @default(template)
/// Mirrors TrainingJob.max_training_hours at the time of success.
max_training_hours Float?
/// Read-only `trainingHours` from Azure GET /documentModels/{id} response,
/// captured by the poller on successful build. Always null for template.
actual_training_hours Float?
created_at DateTime @default(now())
@@unique([template_model_id, version])
@@index([template_model_id, is_active])
@@map("trained_models")
}
model ClassifierModel {
name String
description String
created_by String
updated_by String
creator Actor @relation(name: "ClassifierCreatedByActor", fields: [created_by], references: [id])
updator Actor @relation(name: "ClassifierUpdatedByActor", fields: [updated_by], references: [id])
group_id String
group Group @relation(fields: [group_id], references: [id])
created_at DateTime @default(now())
updated_at DateTime @updatedAt
last_used_at DateTime?
version Int @default(1)
source ClassifierSource
status ClassifierStatus @default(PRETRAINING)
operation_location String?
@@id([name, group_id])
@@index([group_id])
@@map("classifier_model")
}
model Group {
id String @id @default(cuid())
name String @unique
description String?
created_by String
created_at DateTime @default(now())
updated_by String?
updated_at DateTime @updatedAt
deleted_at DateTime?
deleted_by String?
creator Actor @relation(name: "GroupCreatedBy", fields: [created_by], references: [id])
updator Actor? @relation(name: "GroupUpdatedBy", fields: [updated_by], references: [id])
deleter Actor? @relation(name: "GroupDeletedBy", fields: [deleted_by], references: [id])
classifier_models ClassifierModel[]
user_groups UserGroup[]
membership_requests GroupMembershipRequest[]
documents Document[]
workflowLineages WorkflowLineage[]
template_models TemplateModel[]
labeling_documents LabelingDocument[]
api_keys ApiKey[]
datasets Dataset[]
benchmark_projects BenchmarkProject[]
confusionProfiles ConfusionProfile[]
reference_tables ReferenceTable[]
@@map("group")
}
model ConfusionProfile {
id String @id @default(cuid())
name String
description String?
matrix Json
metadata Json?
group_id String
group Group @relation(fields: [group_id], references: [id])
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([group_id])
@@map("confusion_profiles")
}
model UserGroup {
user_id String
user User? @relation(fields: [user_id], references: [id])
group_id String
group Group? @relation(fields: [group_id], references: [id])
role GroupRole @default(MEMBER)
created_at DateTime @default(now())
@@id([user_id, group_id])
@@index([group_id])
@@map("user_group")
}
model GroupMembershipRequest {
id String @id @default(cuid())
user_id String
user User @relation(name: "MembershipRequestUser", fields: [user_id], references: [id])
group_id String
group Group @relation(fields: [group_id], references: [id])
status GroupMembershipRequestStatus @default(PENDING)
reason String?
resolved_at DateTime?
created_at DateTime @default(now())
created_by String
creator Actor @relation(name: "MembershipRequestCreatedBy", fields: [created_by], references: [id])
updated_at DateTime @updatedAt
updated_by String
updator Actor @relation(name: "MembershipRequestUpdatedBy", fields: [updated_by], references: [id])
@@unique([group_id, user_id, status])
@@map("group_membership_request")
}
// ========== AUDIT ==========
// TODO: Should these have references to the other tables?
model AuditEvent {
id String @id @default(cuid())
occurred_at DateTime @default(now())
event_type String
actor_id String?
resource_type String
resource_id String
document_id String?
workflow_execution_id String?
group_id String?
request_id String?
payload Json?
@@index([occurred_at])
@@index([event_type])
@@index([resource_type])
@@index([document_id])
@@index([workflow_execution_id])
@@index([group_id])
@@map("audit_events")
}
// ========== HITL MODELS ==========
model ReviewSession {
id String @id @default(cuid())
document_id String
document Document @relation(fields: [document_id], references: [id], onDelete: Cascade)
actor_id String
actor Actor @relation(fields: [actor_id], references: [id])
status ReviewStatus @default(in_progress)
started_at DateTime @default(now())
completed_at DateTime?
corrections FieldCorrection[]
lock DocumentLock?
@@map("review_sessions")
}
model DocumentLock {
id String @id @default(cuid())
document_id String @unique
document Document @relation(fields: [document_id], references: [id], onDelete: Cascade)
reviewer_id String
session_id String @unique
session ReviewSession @relation(fields: [session_id], references: [id], onDelete: Cascade)
acquired_at DateTime @default(now())
last_heartbeat DateTime @default(now())
expires_at DateTime
@@index([expires_at])
@@map("document_locks")
}
model FieldCorrection {
id String @id @default(cuid())
session_id String
session ReviewSession @relation(fields: [session_id], references: [id], onDelete: Cascade)
field_key String
original_value String?
corrected_value String?
original_conf Float?
action CorrectionAction @default(confirmed)
created_at DateTime @default(now())
@@map("field_corrections")
}
// ========== ENUMS ==========
// TODO: Standarize these enums as constant casing
enum TemplateModelStatus {
draft
training
trained
failed
}
enum BuildMode {
template
neural
}
enum FieldType {
string
number
date
selectionMark
signature
}
enum LabelingStatus {
unlabeled
in_progress
labeled
}
enum ReviewStatus {
in_progress
approved
escalated
skipped
}
enum CorrectionAction {
confirmed
corrected
flagged
deleted
}
enum TrainingStatus {
PENDING
UPLOADING
UPLOADED
TRAINING
SUCCEEDED
FAILED
}
enum ClassifierSource {
AZURE
}
enum ClassifierStatus {
PRETRAINING
FAILED
TRAINING
READY
}
enum DocumentStatus {
pre_ocr
ongoing_ocr
/// OCR extraction complete; workflow may continue (e.g. HITL review).
extracted
/// OCR complete; document paused awaiting human review.
awaiting_review
/// Entire process complete (OCR + optional HITL review).
complete
failed
/// Original stored but image/PDF could not be normalized to a display/OCR PDF.
conversion_failed
}
// ========== BENCHMARKING MODELS ==========
model Dataset {
id String @id @default(uuid())
name String
description String?
metadata Json @default("{}")
storagePath String @default("")
createdBy String
creator Actor @relation(fields: [createdBy], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
group_id String
group Group @relation(fields: [group_id], references: [id])
versions DatasetVersion[]
@@unique([name, group_id])
@@index([group_id])
@@map("datasets")
}
// TODO: Does this need created/updated info?
model DatasetVersion {
id String @id @default(uuid())
datasetId String
dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade)
version String
name String?
storagePrefix String?
manifestPath String
documentCount Int
groundTruthSchema Json?
frozen Boolean @default(false)
createdAt DateTime @default(now())
splits Split[]
benchmarkDefinitions BenchmarkDefinition[]
groundTruthJobs DatasetGroundTruthJob[]
@@index([datasetId])
@@map("dataset_versions")
}
// TODO: Does this need created/updated info?
model Split {
id String @id @default(uuid())
datasetVersionId String
datasetVersion DatasetVersion @relation(fields: [datasetVersionId], references: [id], onDelete: Cascade)
name String
type SplitType
sampleIds Json
stratificationRules Json?
frozen Boolean @default(false)
createdAt DateTime @default(now())
benchmarkDefinitions BenchmarkDefinition[]
@@index([datasetVersionId])
@@map("splits")
}
// TODO: No updated by?
model BenchmarkProject {
id String @id @default(uuid())
name String
description String?
createdBy String
creator Actor @relation(fields: [createdBy], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
group_id String
group Group @relation(fields: [group_id], references: [id])
benchmarkDefinitions BenchmarkDefinition[]
benchmarkRuns BenchmarkRun[]
@@unique([name, group_id])
@@index([group_id])
@@map("benchmark_projects")
}
model BenchmarkDefinition {
id String @id @default(uuid())
projectId String
project BenchmarkProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
datasetVersionId String
datasetVersion DatasetVersion @relation(fields: [datasetVersionId], references: [id])
splitId String?
split Split? @relation(fields: [splitId], references: [id])
workflowVersionId String
workflowVersion WorkflowVersion @relation(fields: [workflowVersionId], references: [id])
workflowConfigHash String
workflowConfigOverrides Json? @default("{}") @map("workflow_config_overrides")
evaluatorType String
evaluatorConfig Json
runtimeSettings Json
immutable Boolean @default(false)
revision Int @default(1)
/// Structured debug log from the last OCR improvement pipeline run (overwritten each run)
pipelineDebugLog Json? @map("pipeline_debug_log")
scheduleEnabled Boolean @default(false)
scheduleCron String?
scheduleId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
benchmarkRuns BenchmarkRun[]
@@index([projectId])
@@index([datasetVersionId])
@@index([splitId])
@@index([workflowVersionId])
@@map("benchmark_definitions")
}
model BenchmarkRun {
id String @id @default(uuid())
definitionId String
definition BenchmarkDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade)
projectId String
project BenchmarkProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
status BenchmarkRunStatus @default(pending)
temporalWorkflowId String
workerImageDigest String?
workerGitSha String
startedAt DateTime?
completedAt DateTime?
metrics Json @default("{}")
params Json @default("{}")
tags Json @default("{}")
error String?
isBaseline Boolean @default(false)
baselineThresholds Json?
baselineComparison Json?
createdAt DateTime @default(now())
ocrCacheRows BenchmarkOcrCache[]
@@index([definitionId])
@@index([projectId])
@@map("benchmark_runs")
}
/// Cached Azure OCR poll response per benchmark run sample (for replay without re-calling Azure).
model BenchmarkOcrCache {
id String @id @default(uuid())
sourceRunId String
sourceRun BenchmarkRun @relation(fields: [sourceRunId], references: [id], onDelete: Cascade)
sampleId String
ocrResponse Json
createdAt DateTime @default(now())
@@unique([sourceRunId, sampleId])
@@index([sourceRunId])
@@map("benchmark_ocr_cache")
}
model BenchmarkAuditLog {
id String @id @default(uuid())
timestamp DateTime @default(now())
actor_id String
actor Actor @relation(fields: [actor_id], references: [id])
action AuditAction
entityType String
entityId String
metadata Json?
@@index([timestamp])
@@index([actor_id])
@@index([entityType, entityId])
@@map("benchmark_audit_logs")
}
// ========== BENCHMARKING ENUMS ==========
enum SplitType {
train
val
test
golden
}
enum BenchmarkRunStatus {
pending
running
completed
failed
cancelled
}
enum AuditAction {
dataset_created
version_published
run_started
run_completed
baseline_promoted
}
// ========== GROUND TRUTH GENERATION ==========
enum GroundTruthJobStatus {
pending
processing
awaiting_review
completed
failed
}
model DatasetGroundTruthJob {
id String @id @default(uuid())
datasetVersionId String
datasetVersion DatasetVersion @relation(fields: [datasetVersionId], references: [id], onDelete: Cascade)
sampleId String
documentId String? @unique
document Document? @relation(fields: [documentId], references: [id])
workflowVersionId String
workflowVersion WorkflowVersion @relation(fields: [workflowVersionId], references: [id], onDelete: Restrict)
temporalWorkflowId String?
status GroundTruthJobStatus @default(pending)
workflowConfigOverrides Json?
groundTruthPath String?
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// TODO: created_by and updated_by?
@@index([datasetVersionId])
@@index([documentId])
@@index([status])
@@index([workflowVersionId])
@@map("dataset_ground_truth_jobs")
}
enum GroupMembershipRequestStatus {
PENDING
APPROVED
DENIED
CANCELLED
}
enum GroupRole {
ADMIN
MEMBER
}
// ========== REFERENCE DATA TABLES ==========
model ReferenceTable {
id String @id @default(cuid())
group_id String
group Group @relation(fields: [group_id], references: [id], onDelete: Cascade)
table_id String // unique within group
label String
description String?
columns Json // ColumnDef[]
lookups Json // LookupDef[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
rows ReferenceTableRow[]
@@unique([group_id, table_id])
@@index([group_id])
@@map("reference_tables")
}
model ReferenceTableRow {
id String @id @default(cuid())
group_id String
table_id String
table ReferenceTable @relation(fields: [group_id, table_id], references: [group_id, table_id], onDelete: Cascade)
data Json
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([group_id, table_id])
@@map("reference_table_rows")
}