@taodigital/database 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +21 -2
  2. package/dist/index.d.ts +58 -0
  3. package/dist/index.js +209 -0
  4. package/package.json +29 -4
  5. package/prisma/migrations/0000_init/migration.sql +220 -0
  6. package/prisma/migrations/0001_tenant_isolation/migration.sql +43 -0
  7. package/prisma/migrations/0002_app_runtime_role/migration.sql +14 -0
  8. package/prisma/migrations/0003_skill_taxonomy/migration.sql +30 -0
  9. package/prisma/migrations/0004_extended_models/migration.sql +215 -0
  10. package/prisma/migrations/0005_enhanced_test_artifacts/migration.sql +24 -0
  11. package/prisma/migrations/0006_tenant_settings/migration.sql +28 -0
  12. package/prisma/migrations/0007_project_slug/migration.sql +11 -0
  13. package/prisma/migrations/0008_project_crawl_config/migration.sql +3 -0
  14. package/prisma/migrations/0009_tenant_slug/migration.sql +11 -0
  15. package/prisma/migrations/0010_user_password/migration.sql +3 -0
  16. package/prisma/migrations/0011_oauth_connections/migration.sql +78 -0
  17. package/prisma/migrations/0012_agent_webhooks/migration.sql +46 -0
  18. package/prisma/migrations/0013_project_artifacts/migration.sql +27 -0
  19. package/prisma/migrations/0014_knowledge_chunks/migration.sql +37 -0
  20. package/prisma/migrations/20260626022056_platform_job_migration/migration.sql +44 -0
  21. package/prisma/migrations/20260626053339_kag_schema_update/migration.sql +27 -0
  22. package/prisma/migrations/20260626053340_restore_bm25_index/migration.sql +13 -0
  23. package/prisma/migrations/20260626063704_token_optimisation/migration.sql +86 -0
  24. package/prisma/migrations/20260626080000_job_name/migration.sql +2 -0
  25. package/prisma/migrations/20260626090000_rls_missing_tables/migration.sql +39 -0
  26. package/prisma/migrations/20260626100000_token_budget/migration.sql +21 -0
  27. package/prisma/migrations/20260626110000_screen_groups/migration.sql +42 -0
  28. package/prisma/migrations/20260626120000_artifact_intelligence/migration.sql +5 -0
  29. package/prisma/migrations/20260626130000_notifications/migration.sql +34 -0
  30. package/prisma/migrations/20260627100000_element_viewport/migration.sql +2 -0
  31. package/prisma/migrations/20260627100000_screenshot_cloud_url/migration.sql +2 -0
  32. package/prisma/migrations/20260627110000_screenshot_index/migration.sql +2 -0
  33. package/prisma/migrations/20260627140000_artifact_storage_key/migration.sql +3 -0
  34. package/prisma/migrations/20260627150000_screen_lifecycle/migration.sql +5 -0
  35. package/prisma/migrations/20260627160000_native_app_support/migration.sql +41 -0
  36. package/prisma/migrations/20260627200000_rls_missing_tables/migration.sql +57 -0
  37. package/prisma/migrations/20260627210000_llmcalllog_tasktype/migration.sql +1 -0
  38. package/prisma/migrations/20260628100000_element_enrichment/migration.sql +7 -0
  39. package/prisma/migrations/20260628110000_element_hash/migration.sql +6 -0
  40. package/prisma/migrations/20260628120000_screen_diff/migration.sql +20 -0
  41. package/prisma/migrations/20260628130000_probe_observations/migration.sql +19 -0
  42. package/prisma/migrations/20260629100000_visual_baseline_masking/migration.sql +1 -0
  43. package/prisma/migrations/20260629110000_testcase_viewports/migration.sql +1 -0
  44. package/prisma/migrations/20260630100000_visual_baseline_element_snapshot/migration.sql +1 -0
  45. package/prisma/migrations/20260630110000_git_branched_baselines/migration.sql +11 -0
  46. package/prisma/migrations/20260630120000_testcase_viewport_constraint/migration.sql +1 -0
  47. package/prisma/migrations/20260630130000_test_case_runs/migration.sql +22 -0
  48. package/prisma/migrations/20260630140000_perf_env_comments/migration.sql +68 -0
  49. package/prisma/migrations/20260630150000_testcase_gen_at/migration.sql +2 -0
  50. package/prisma/migrations/20260630160000_rls_catchup_late_tables/migration.sql +41 -0
  51. package/prisma/migrations/20260701100000_skill_file_diff_approval/migration.sql +9 -0
  52. package/prisma/migrations/20260701200000_workspace_connector/migration.sql +20 -0
  53. package/prisma/migrations/20260701220000_screen_a11y_security/migration.sql +2 -0
  54. package/prisma/migrations/20260702100000_ci_threshold_root_cause/migration.sql +2 -0
  55. package/prisma/migrations/20260702120000_medium_priority_gaps/migration.sql +28 -0
  56. package/prisma/migrations/20260702140000_testcase_v2_technique/migration.sql +7 -0
  57. package/prisma/migrations/20260702150000_add_app_field/migration.sql +40 -0
  58. package/prisma/migrations/20260702160000_add_requirement_model/migration.sql +62 -0
  59. package/prisma/migrations/20260702180000_add_invitation_model/migration.sql +53 -0
  60. package/prisma/migrations/20260702190000_suite_hierarchy_artifact_trace/migration.sql +28 -0
  61. package/prisma/migrations/20260702200000_project_member_access/migration.sql +31 -0
  62. package/prisma/migrations/20260703000000_add_system_config/migration.sql +11 -0
  63. package/prisma/migrations/20260703000000_custom_roles/migration.sql +29 -0
  64. package/prisma/migrations/20260703100000_platform_admin/migration.sql +11 -0
  65. package/prisma/migrations/20260703110000_add_extension_models/migration.sql +106 -0
  66. package/prisma/migrations/20260703120000_project_restricted_column/migration.sql +1 -0
  67. package/prisma/migrations/20260703200000_system_config_category_index/migration.sql +1 -0
  68. package/prisma/migrations/20260703210000_workspace_layer/migration.sql +24 -0
  69. package/prisma/migrations/20260703230000_workspace_agent_tokens/migration.sql +2 -0
  70. package/prisma/migrations/20260704000000_requirement_rls_policy/migration.sql +35 -0
  71. package/prisma/migrations/20260704100000_fix_oauth_auditlog_cascade/migration.sql +9 -0
  72. package/prisma/migrations/20260704110000_skillfile_approval_status/migration.sql +2 -0
  73. package/prisma/migrations/20260704120000_scenario_suspect_workspace/migration.sql +35 -0
  74. package/prisma/migrations/20260704130000_testcase_traceto_suspect_indexes/migration.sql +5 -0
  75. package/prisma/migrations/20260704140000_workspace_rls/migration.sql +19 -0
  76. package/prisma/migrations/20260704150000_project_cascade_deletes/migration.sql +125 -0
  77. package/prisma/migrations/20260705100000_platform_job_worker_id/migration.sql +12 -0
  78. package/prisma/migrations/20260705110000_screen_discovery_method/migration.sql +7 -0
  79. package/prisma/migrations/20260705120000_webhook_delivery/migration.sql +32 -0
  80. package/prisma/migrations/20260705130000_tenant_feature_flags/migration.sql +23 -0
  81. package/prisma/migrations/20260705140000_auth_sessions/migration.sql +30 -0
  82. package/prisma/migrations/20260705150000_fix_rls_wrong_session_var/migration.sql +51 -0
  83. package/prisma/migrations/20260705160000_testcase_flaky_quarantine/migration.sql +14 -0
  84. package/prisma/migrations/20260705170000_testcaserun_browser/migration.sql +6 -0
  85. package/prisma/migrations/20260705180000_allow_custom_roles/migration.sql +22 -0
  86. package/prisma/migrations/20260706050000_screen_markdown_extraction/migration.sql +8 -0
  87. package/prisma/migrations/20260706090000_add_lighthouse_to_performance_snapshot/migration.sql +4 -0
  88. package/prisma/migrations/20260706100000_add_lighthouse_bp_and_job_index/migration.sql +5 -0
  89. package/prisma/migrations/20260706110000_add_visual_diff_ai_summary/migration.sql +2 -0
  90. package/prisma/migrations/20260707120000_script_bundles/migration.sql +25 -0
  91. package/prisma/migrations/20260707130000_requirement_source_section/migration.sql +4 -0
  92. package/prisma/migrations/20260707140000_structured_skill_metadata/migration.sql +8 -0
  93. package/prisma/migrations/20260707150000_crawler_platform_primitives/migration.sql +112 -0
  94. package/prisma/migrations/20260707160000_screen_group_versioning/migration.sql +9 -0
  95. package/prisma/migrations/20260707170000_add_mfa_fields/migration.sql +24 -0
  96. package/prisma/migrations/20260707180000_add_missing_tables/migration.sql +158 -0
  97. package/prisma/migrations/20260707190000_add_missing_columns/migration.sql +15 -0
  98. package/prisma/migrations/20260707200000_add_plan_sessions/migration.sql +1 -0
  99. package/prisma/migrations/20260708100000_missing_columns_comprehensive/migration.sql +15 -0
  100. package/prisma/migrations/20260708200000_testcase_dataset_column/migration.sql +16 -0
  101. package/prisma/migrations/20260709100000_add_filename_to_native_app_build/migration.sql +1 -0
  102. package/prisma/migrations/20260709200000_cloud_url_visual_artifacts/migration.sql +6 -0
  103. package/prisma/migrations/20260714200000_add_cached_scenarios_to_artifact/migration.sql +4 -0
  104. package/prisma/migrations/20260715200000_screen_markdown_content/migration.sql +2 -0
  105. package/prisma/migrations/20260716100000_github_repo_coverage_tables/migration.sql +114 -0
  106. package/prisma/migrations/20260716200000_github_pr_script_validation/migration.sql +48 -0
  107. package/prisma/migrations/20260716210000_s3_content_keys/migration.sql +20 -0
  108. package/prisma/migrations/20260717220000_ide_push_token/migration.sql +21 -0
  109. package/prisma/migrations/20260717230000_ide_push_script_ids/migration.sql +2 -0
  110. package/prisma/migrations/20260730040000_add_step_evidence_and_validation/migration.sql +93 -0
  111. package/prisma/migrations/20260803000000_add_tenantId_to_test_data_row/migration.sql +40 -0
  112. package/prisma/migrations/20260804000000_add_project_restricted/migration.sql +6 -0
  113. package/prisma/migrations/20260804000000_add_user_avatar_url/migration.sql +4 -0
  114. package/prisma/migrations/20260805000000_add_auditlog_ipaddress/migration.sql +2 -0
  115. package/prisma/migrations/20260807000000_add_test_data_classification/migration.sql +18 -0
  116. package/prisma/migrations/20260807000001_add_test_case_notes/migration.sql +5 -0
  117. package/prisma/migrations/20260807000002_add_visual_baseline_browser_structured_steps/migration.sql +11 -0
  118. package/prisma/migrations/20260807100000_add_tenant_plan_config/migration.sql +14 -0
  119. package/prisma/migrations/20260808000000_agent_token_missing_columns/migration.sql +22 -0
  120. package/prisma/migrations/20260808000001_visual_diff_image_and_threshold/migration.sql +8 -0
  121. package/prisma/migrations/20260808000002_ssoconfig_email_domains/migration.sql +7 -0
  122. package/prisma/migrations/20260808000003_project_status_description_updatedat/migration.sql +7 -0
  123. package/prisma/migrations/20260814000000_workspace_require_mfa/migration.sql +2 -0
  124. package/prisma/migrations/20260814000001_add_screenshot_dpr/migration.sql +2 -0
  125. package/prisma/migrations/20260818000000_add_content_hash_to_test_case/migration.sql +6 -0
  126. package/prisma/migrations/20260818000001_add_grounding_hash_to_test_case/migration.sql +3 -0
  127. package/prisma/migrations/20260819000000_coverage_mapping_cross_encoder_fields/migration.sql +3 -0
  128. package/prisma/migrations/20260820000000_add_locators_json_to_test_case/migration.sql +6 -0
  129. package/prisma/migrations/20260820000001_add_test_case_version/migration.sql +27 -0
  130. package/prisma/migrations/20260826000000_add_capture_step_screenshots/migration.sql +1 -0
  131. package/prisma/migrations/20260903000000_add_platform_job_dlq/migration.sql +10 -0
  132. package/prisma/migrations/20260903000002_add_versioning_tables/migration.sql +38 -0
  133. package/prisma/migrations/20260903000003_add_learning_models/migration.sql +71 -0
  134. package/prisma/migrations/20260903000004_add_workspace_agent_prompts/migration.sql +33 -0
  135. package/prisma/migrations/20260903000005_add_config_history/migration.sql +70 -0
  136. package/prisma/migrations/20260903000006_add_skill_file_config/migration.sql +24 -0
  137. package/prisma/migrations/20260904000000_add_agent_prompt_platform_table/migration.sql +23 -0
  138. package/prisma/migrations/20260904180424_rbac_role_configs_admin_roles/migration.sql +606 -0
  139. package/prisma/migrations/20260907000000_fix_testcase_timestamps/migration.sql +13 -0
  140. package/prisma/migrations/20260907000001_add_artifact_imported_suite_id/migration.sql +1 -0
  141. package/prisma/migrations/20260907000002_add_user_login_tracking/migration.sql +9 -0
  142. package/prisma/migrations/20260907000003_add_artifact_import_tracking/migration.sql +9 -0
  143. package/prisma/migrations/20260907000004_add_artifact_version_chain/migration.sql +18 -0
  144. package/prisma/migrations/20260908000000_add_project_vsa_enabled/migration.sql +2 -0
  145. package/prisma/migrations/20260908000001_rbac_migration_catchup/migration.sql +314 -0
  146. package/prisma/migrations/20260908000002_fix_role_configs_empty_permissions/migration.sql +22 -0
  147. package/prisma/migrations/20260908000003_sync_builtin_role_permissions/migration.sql +38 -0
  148. package/prisma/migrations/20260909000000_add_filepath_to_testscript/migration.sql +5 -0
  149. package/prisma/migrations/20260909000001_add_alignment_result_table/migration.sql +31 -0
  150. package/prisma/migrations/20260910000000_add_admin_mfa/migration.sql +35 -0
  151. package/prisma/migrations/20260910000000_add_dom_snapshot_cloud_url/migration.sql +2 -0
  152. package/prisma/migrations/20260910000001_add_requires_auth_to_screen/migration.sql +1 -0
  153. package/prisma/migrations/20260910000002_add_screen_requires_auth/migration.sql +2 -0
  154. package/prisma/migrations/20260910000003_add_admin_auth_sessions/migration.sql +17 -0
  155. package/prisma/migrations/20260916000001_add_admin_last_login/migration.sql +1 -0
  156. package/prisma/migrations/20260916000002_add_admin_failed_login/migration.sql +2 -0
  157. package/prisma/migrations/20260916000003_seed_system_tenant/migration.sql +6 -0
  158. package/prisma/migrations/20260918000000_fix_permissions_comprehensive/migration.sql +63 -0
  159. package/prisma/migrations/20260924000000_add_screen_role_access/migration.sql +2 -0
  160. package/prisma/migrations/20260928000000_add_navigation_graph/migration.sql +41 -0
  161. package/prisma/migrations/20260929000000_artifact_content_hash_suite_unique/migration.sql +25 -0
  162. package/prisma/migrations/20260929000000_navigation_graph_unique_constraint/migration.sql +5 -0
  163. package/prisma/migrations/20260929000001_add_membership_status/migration.sql +7 -0
  164. package/prisma/migrations/20260929000002_add_sso_config_scim_token_hash/migration.sql +8 -0
  165. package/prisma/migrations/20261005000001_jobs_workers_hardening/migration.sql +63 -0
  166. package/prisma/migrations/20261009000001_add_suite_category/migration.sql +2 -0
  167. package/prisma/migrations/20261009000002_add_testcase_approved_fields/migration.sql +3 -0
  168. package/prisma/migrations/20261012_auto_version_history/migration.sql +111 -0
  169. package/prisma/migrations/20261015000001_traceability_alignment_gaps/migration.sql +11 -0
  170. package/prisma/migrations/20261028000001_context_intelligence_p1_p7/migration.sql +76 -0
  171. package/prisma/migrations/migration_lock.toml +3 -0
  172. package/prisma/schema.prisma +2901 -0
  173. package/prisma/seed.ts +1262 -0
@@ -0,0 +1,2901 @@
1
+ // ============================================================================
2
+ // THE DATA MODEL — "the sealed boxes"
3
+ // Tenant -> Workspace -> Project -> (Zeta Core, Screens, Test Suites...)
4
+ //
5
+ // Every record carries tenantId. Tenant isolation is ALSO enforced at the
6
+ // database layer via Row-Level Security (see migrations/0001_tenant_isolation).
7
+ // Never rely on application code alone to keep tenants apart.
8
+ // ============================================================================
9
+
10
+ generator client {
11
+ provider = "prisma-client-js"
12
+ }
13
+
14
+ datasource db {
15
+ provider = "postgresql"
16
+ url = env("DATABASE_URL")
17
+ directUrl = env("MIGRATION_DATABASE_URL")
18
+ }
19
+
20
+ // ---- Access layer -----------------------------------------------------------
21
+
22
+ model Tenant {
23
+ id String @id @default(cuid())
24
+ name String
25
+ slug String @unique @default("")
26
+ // "POOL" = shared infra with row-level filtering. "SILO" = dedicated (Phase 5).
27
+ isolation String
28
+ region String
29
+ createdAt DateTime @default(now())
30
+ workspaces Workspace[]
31
+ projects Project[]
32
+ memberships Membership[]
33
+ tokenBudget TokenBudget?
34
+ notifications Notification[]
35
+ notificationPreferences NotificationPreference[]
36
+ invitations Invitation[]
37
+ customRoles CustomRole[]
38
+ auditLogs AuditLog[]
39
+ ssoConfig SsoConfig?
40
+ slackConnection SlackConnection?
41
+ teamsConnection TeamsConnection?
42
+ siloConfig TenantSiloConfig?
43
+ plan TenantPlan?
44
+ accessReviews AccessReview[]
45
+ workspaceAgentPrompts WorkspaceAgentPrompt[]
46
+ }
47
+
48
+ model Workspace {
49
+ id String @id @default(cuid())
50
+ tenantId String
51
+ name String
52
+ slug String
53
+ settings Json? // domain config: { vertical, requirementTypes, testTechniques }
54
+ requireMfa Boolean @default(false)
55
+ createdAt DateTime @default(now())
56
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
57
+ projects Project[]
58
+
59
+ @@unique([tenantId, slug])
60
+ @@index([tenantId])
61
+ }
62
+
63
+ model User {
64
+ id String @id @default(cuid())
65
+ email String @unique
66
+ name String?
67
+ avatarUrl String?
68
+ passwordHash String?
69
+ mfaEnabled Boolean @default(false)
70
+ mfaSecret_enc String? // AES-256-GCM encrypted TOTP secret
71
+ mfaVerifiedAt DateTime? // when user completed first TOTP verification
72
+ lastLoginAt DateTime? // updated on every successful login
73
+ failedLoginCount Int @default(0) // consecutive failures since last success; reset on login
74
+ lastFailedLoginAt DateTime? // timestamp of most recent failed attempt
75
+ createdAt DateTime @default(now())
76
+ memberships Membership[]
77
+ mfaRecoveryCodes MfaRecoveryCode[]
78
+ notifications Notification[]
79
+ notificationPreferences NotificationPreference[]
80
+ auditLogs AuditLog[]
81
+ stepValidations StepValidation[]
82
+ createdAgentTokens AgentToken[]
83
+ comments Comment[]
84
+ userContext UserContext?
85
+ learningJourney LearningJourney?
86
+ }
87
+
88
+ // Which user has which ROLE inside which tenant.
89
+ model Membership {
90
+ id String @id @default(cuid())
91
+ userId String
92
+ tenantId String
93
+ // OWNER | ADMIN | QA_LEAD | TESTER | DEVELOPER | VIEWER
94
+ role String
95
+ // ACTIVE | SUSPENDED — set to SUSPENDED by SCIM deprovisioning (soft
96
+ // suspend, role preserved for reactivation) or an admin. Enforced in
97
+ // resolveContext() on every authenticated request, not just at login.
98
+ status String @default("ACTIVE")
99
+ user User @relation(fields: [userId], references: [id])
100
+ tenant Tenant @relation(fields: [tenantId], references: [id])
101
+
102
+ @@unique([userId, tenantId])
103
+ @@index([tenantId])
104
+ @@index([userId])
105
+ }
106
+
107
+ // Custom roles created by the tenant
108
+ model CustomRole {
109
+ id String @id @default(cuid())
110
+ tenantId String
111
+ name String
112
+ description String?
113
+ permissions Json @default("[]") // e.g. ["project:read", "project:write", "settings:manage"]
114
+ createdAt DateTime @default(now())
115
+ updatedAt DateTime @default(now())
116
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
117
+
118
+ @@unique([tenantId, name])
119
+ }
120
+
121
+ // Pending invites to a tenant
122
+ model Invitation {
123
+ id String @id @default(cuid())
124
+ tenantId String
125
+ email String
126
+ role String
127
+ tokenHash String @unique
128
+ invitedBy String?
129
+ status String @default("PENDING")
130
+ expiresAt DateTime
131
+ createdAt DateTime @default(now())
132
+ acceptedAt DateTime?
133
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
134
+
135
+ @@unique([tenantId, email])
136
+ @@index([tenantId, status])
137
+ }
138
+
139
+ // ---- A Project = one app being tested --------------------------------------
140
+
141
+ model Project {
142
+ id String @id @default(cuid())
143
+ tenantId String
144
+ name String
145
+ slug String @default("")
146
+ appUrl String?
147
+ figmaUrl String?
148
+ appType String?
149
+ crawlConfig Json? // { username?, password_enc?, totpSecret_enc? }
150
+ ciFailureThreshold Int?
151
+ slaThreshold Int?
152
+ visualDiffThreshold Float?
153
+ // Set to true when the project's knowledge outgrows direct loading and
154
+ // needs vector search (RAG). Default false = direct loading (the simple way).
155
+ description String?
156
+ status String @default("ACTIVE")
157
+ oversized Boolean @default(false)
158
+ restricted Boolean @default(false)
159
+ vsaEnabled Boolean @default(false)
160
+ workspaceId String?
161
+ // Context Intelligence (P1-P8)
162
+ projectKind String @default("WEB")
163
+ contextSources Json @default("{}")
164
+ contextScore Int @default(0)
165
+ contextPhase String @default("PENDING_CONTEXT")
166
+ contextSourceRequirements Json @default("{}")
167
+ approvalGateSummary Json?
168
+ createdAt DateTime @default(now())
169
+ updatedAt DateTime @updatedAt
170
+ tenant Tenant @relation(fields: [tenantId], references: [id])
171
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: SetNull)
172
+
173
+ screens Screen[]
174
+ screenGroups ScreenGroup[]
175
+ flows Flow[]
176
+ skillFiles SkillFile[]
177
+ artifacts ProjectArtifact[]
178
+ testSuites TestSuite[]
179
+ scriptBundles ScriptBundle[]
180
+ connectors Connector[]
181
+ oauthConnections OAuthConnection[]
182
+ agentTokens AgentToken[]
183
+ webhooks Webhook[]
184
+ auditLogs AuditLog[]
185
+ changeLogs ChangeLog[]
186
+ platformJobs PlatformJob[]
187
+ sessions UserSession[]
188
+ testRuns TestRun[]
189
+ appFields AppField[]
190
+ nativeAppBuilds NativeAppBuild[]
191
+ projectEnvironments ProjectEnvironment[]
192
+ projectMembers ProjectMember[]
193
+ requirements Requirement[]
194
+ extensionCaptures ExtensionCapture[]
195
+ extensionErrors ExtensionError[]
196
+ screenTransitions ScreenTransition[]
197
+ crawlEvidence CrawlEvidence[]
198
+ actionRecipes ActionRecipe[]
199
+ apiEndpointObservations ApiEndpointObservation[]
200
+ testDataSets TestDataSet[]
201
+ a11yViolations ScreenA11yViolation[]
202
+ // Framework Modernization — Phase 2
203
+ githubRepoConnections GitHubRepoConnection[]
204
+ coverageSnapshots CoverageSnapshot[]
205
+ idePushTokens IdePushToken[]
206
+ skillFileConfigs SkillFileConfig[]
207
+ discoveredLinks DiscoveredLink[]
208
+ virtualScreens VirtualScreen[]
209
+ contextEnhancements ContextEnhancement[]
210
+ contextSourceApprovals ContextSourceApproval[]
211
+ crawlDiffs CrawlDiff[]
212
+ bugSignals BugSignal[]
213
+ requirementCoverages RequirementCoverage[]
214
+ figmaComponents FigmaComponent[]
215
+ extractedElements ExtractedElement[]
216
+
217
+ @@unique([tenantId, slug])
218
+ @@index([tenantId])
219
+ @@index([workspaceId])
220
+ }
221
+
222
+ // ---- THE Zeta Core (a normal, structured, versioned knowledge store) -------
223
+
224
+ model ScreenGroup {
225
+ id String @id @default(cuid())
226
+ tenantId String
227
+ projectId String
228
+ name String
229
+ slug String
230
+ description String?
231
+ color String @default("#6FD0E8")
232
+ icon String @default("◈")
233
+ order Int @default(0)
234
+ crawlJobId String?
235
+ createdAt DateTime @default(now())
236
+ updatedAt DateTime @updatedAt
237
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
238
+ screens Screen[]
239
+
240
+ @@index([tenantId, projectId])
241
+ }
242
+
243
+ model Screen {
244
+ id String @id @default(cuid())
245
+ tenantId String
246
+ projectId String
247
+ name String
248
+ url String?
249
+ version Int @default(1)
250
+ groupId String?
251
+ group ScreenGroup? @relation(fields: [groupId], references: [id])
252
+ // How closely the live screen matches its Figma design (Phase 2). 0-100.
253
+ figmaMatch Int?
254
+ // Accessibility score 0-100 (Phase 3).
255
+ a11yScore Int?
256
+ lastSeenAt DateTime @default(now())
257
+ // Lifecycle fields — track screen presence across crawl runs
258
+ status String @default("ACTIVE") // ACTIVE | PENDING_REMOVAL
259
+ lastCrawlJobId String? // ID of the last PlatformJob that discovered this screen
260
+ removedAt DateTime?
261
+ removalReason String? // e.g. "Not found in crawl #N (was present in crawl #N-1)"
262
+ // How this screen was first discovered — set once at creation, never changed on
263
+ // re-crawl. Powers orphan-page detection: a screen with zero inbound KAG edges
264
+ // (Element.transitionsToScreenId) is only a meaningful "orphan" if it wasn't the
265
+ // crawl's own entry point.
266
+ // ENTRY_POINT | LINK_FOLLOW | SEED_URL | SKYVERN_AI | SITEMAP
267
+ discoveryMethod String @default("LINK_FOLLOW")
268
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
269
+ elements Element[]
270
+ screenshots Screenshot[]
271
+ baselines VisualBaseline[]
272
+ testCases TestCase[]
273
+
274
+ // Knowledge Graph (KAG): Elements on OTHER screens that transition to this screen
275
+ transitionedFrom Element[] @relation("ElementTransitions")
276
+
277
+ // Phase 2: element hash for stale-test detection
278
+ elementHash String?
279
+ elementHashChangedAt DateTime?
280
+
281
+ // Phase 3: perceptual screenshot diffs
282
+ screenDiffs ScreenDiff[]
283
+
284
+ // Phase 5: structured probe observations
285
+ probeObservations ProbeObservation[]
286
+ screenTransitionsFrom ScreenTransition[] @relation("ScreenTransitionFrom")
287
+ screenTransitionsTo ScreenTransition[] @relation("ScreenTransitionTo")
288
+ crawlEvidence CrawlEvidence[]
289
+ apiEndpointObservations ApiEndpointObservation[]
290
+
291
+ // Navigation graph: discovered link edges from/to this screen
292
+ discoveredLinksFrom DiscoveredLink[] @relation("DiscoveredLinkFromScreen")
293
+ discoveredLinksTo DiscoveredLink[] @relation("DiscoveredLinkToScreen")
294
+
295
+ domSnapshotPath String?
296
+ domSnapshotCloudUrl String?
297
+ // True when crawler detected this screen requires authentication but session was lost.
298
+ // DOM/elements captured for this screen may reflect an auth wall, not real content.
299
+ requiresAuth Boolean @default(false)
300
+
301
+ // Navigation graph metadata
302
+ depth Int? // BFS depth from entry point (0 = entry point)
303
+ parentUrl String? // URL of the screen that linked to this one first
304
+
305
+ // Role profiles that can access this screen — populated by multi-role crawl runs.
306
+ roleAccess String[] @default([])
307
+
308
+ // Markdown extraction (crawl4AI-style text output). "readability" = clean
309
+ // article content (nav/ads/chrome stripped); "full-page" = whole-page
310
+ // fallback for app UI screens Readability finds no article in.
311
+ markdownPath String?
312
+ markdownContent String?
313
+ markdownMethod String?
314
+ markdownWordCount Int?
315
+ // AI-generated one-sentence summary of what this screen does/contains
316
+ aiSummary String?
317
+
318
+ // Phase 4.4: axe-core accessibility violations
319
+ a11yViolations ScreenA11yViolation[]
320
+ virtualScreens VirtualScreen[]
321
+ figmaNodeId String?
322
+ figmaComponents FigmaComponent[]
323
+ crawlDiffsAsFrom CrawlDiff[] @relation("CrawlDiffFromScreen")
324
+ crawlDiffsAsTo CrawlDiff[] @relation("CrawlDiffToScreen")
325
+
326
+ @@index([tenantId, projectId])
327
+ @@index([tenantId, projectId, status])
328
+ @@index([tenantId, projectId, elementHashChangedAt])
329
+ @@index([status])
330
+ }
331
+
332
+ // Phase 4.4: Per-screen axe-core accessibility violations (WCAG 2.x)
333
+ model ScreenA11yViolation {
334
+ id String @id @default(cuid())
335
+ tenantId String
336
+ projectId String
337
+ screenId String
338
+ runId String?
339
+ impact String // critical | serious | moderate | minor
340
+ rule String // axe rule ID e.g. "color-contrast"
341
+ description String
342
+ nodeHtml String? // offending DOM snippet (truncated to 500 chars)
343
+ wcagTags Json @default("[]") // ["wcag2a", "wcag21aa"] etc
344
+ createdAt DateTime @default(now())
345
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
346
+ screen Screen @relation(fields: [screenId], references: [id], onDelete: Cascade)
347
+
348
+ @@index([tenantId, projectId])
349
+ @@index([screenId])
350
+ }
351
+
352
+ // Rich workflow graph edge discovered during crawl/probe/replay.
353
+ model ScreenTransition {
354
+ id String @id @default(cuid())
355
+ tenantId String
356
+ projectId String
357
+ fromScreenId String
358
+ toScreenId String?
359
+ action Json
360
+ confidence Float @default(0.5)
361
+ jobId String?
362
+ createdAt DateTime @default(now())
363
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
364
+ fromScreen Screen @relation("ScreenTransitionFrom", fields: [fromScreenId], references: [id], onDelete: Cascade)
365
+ toScreen Screen? @relation("ScreenTransitionTo", fields: [toScreenId], references: [id], onDelete: SetNull)
366
+
367
+ @@index([tenantId, projectId])
368
+ @@index([tenantId, projectId, jobId])
369
+ @@index([fromScreenId])
370
+ @@index([toScreenId])
371
+ }
372
+
373
+ // Durable evidence envelope emitted by the crawler for auditability and replay.
374
+ model CrawlEvidence {
375
+ id String @id @default(cuid())
376
+ tenantId String
377
+ projectId String
378
+ screenId String?
379
+ jobId String?
380
+ type String
381
+ payload Json
382
+ createdAt DateTime @default(now())
383
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
384
+ screen Screen? @relation(fields: [screenId], references: [id], onDelete: Cascade)
385
+
386
+ @@index([tenantId, projectId])
387
+ @@index([tenantId, projectId, jobId])
388
+ @@index([screenId])
389
+ @@index([type])
390
+ }
391
+
392
+ // Replay-first self-healing action recipe. Successful AI/manual actions become
393
+ // deterministic recipes before future crawls spend more model tokens.
394
+ model ActionRecipe {
395
+ id String @id @default(cuid())
396
+ tenantId String
397
+ projectId String
398
+ domain String
399
+ purpose String
400
+ fingerprint String
401
+ steps Json
402
+ successRate Float @default(0)
403
+ lastUsedAt DateTime?
404
+ createdAt DateTime @default(now())
405
+ updatedAt DateTime @updatedAt
406
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
407
+
408
+ @@unique([tenantId, projectId, purpose, fingerprint])
409
+ @@index([tenantId, projectId])
410
+ @@index([tenantId, projectId, domain])
411
+ }
412
+
413
+ // Represents a dynamic state (modal, panel, popup) within a base Screen, captured after an interaction
414
+ model VirtualScreen {
415
+ id String @id @default(cuid())
416
+ tenantId String
417
+ projectId String
418
+ parentScreenId String
419
+ stateId String // simHash of DOM after action
420
+ triggerAction String // e.g. "click Import Tests"
421
+ overlayType String // 'modal' | 'panel' | 'tooltip' | 'dropdown'
422
+ screenshotUrl String?
423
+ domSnapshotUrl String?
424
+ createdAt DateTime @default(now())
425
+
426
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
427
+ parentScreen Screen @relation(fields: [parentScreenId], references: [id], onDelete: Cascade)
428
+
429
+ @@unique([tenantId, projectId, parentScreenId, stateId])
430
+ @@index([tenantId, projectId])
431
+ @@index([parentScreenId])
432
+ }
433
+
434
+ // Network/API calls observed while rendering a screen. This connects UI screens
435
+ // to REST/GraphQL endpoints for API test generation and backend-state mapping.
436
+ model ApiEndpointObservation {
437
+ id String @id @default(cuid())
438
+ tenantId String
439
+ projectId String
440
+ screenId String?
441
+ method String
442
+ urlPattern String
443
+ statusCode Int?
444
+ requestKind String?
445
+ responseShape Json?
446
+ createdAt DateTime @default(now())
447
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
448
+ screen Screen? @relation(fields: [screenId], references: [id], onDelete: Cascade)
449
+
450
+ @@index([tenantId, projectId])
451
+ @@index([tenantId, projectId, method])
452
+ @@index([screenId])
453
+ }
454
+
455
+ model Element {
456
+ id String @id @default(cuid())
457
+ tenantId String
458
+ projectId String
459
+ // Nullable because extension-captured elements (context-menu/devtools/picker)
460
+ // don't correspond to a crawled Screen — only crawl-discovered elements set this.
461
+ screenId String?
462
+ // A stable, human-readable description, e.g. "Primary login submit button".
463
+ meaning String?
464
+ // The kind of thing it is: BUTTON | INPUT | LINK | MENU | CHECKBOX | ...
465
+ role String?
466
+ // What data this field expects, e.g. "valid_email", "us_phone", "date_ddmmyyyy".
467
+ expectedData String?
468
+ // Free-form notes learned over time, e.g. "rejects non-company.com domains".
469
+ notes String?
470
+ text String? // Inner text or placeholder
471
+ isInteractive Boolean? // True if it's an a/button/input, false if it's just layout
472
+
473
+ // Phase 1: enriched element metadata
474
+ boundingRect Json? // {x, y, width, height, top, left, right, bottom}
475
+ ariaState Json? // {expanded, checked, pressed, selected, invalid, required}
476
+ parentLandmark String? // "main" | "nav" | "aside" | "header" | "footer" | "modal"
477
+ viewport String @default("DESKTOP")
478
+
479
+ // Extension-capture fields (context-menu/devtools/picker sources — no crawled Screen).
480
+ selector String?
481
+ pageUrl String?
482
+ screenshotUrl String?
483
+ source String @default("crawl") // "crawl" | "context-menu" | "devtools" | "picker"
484
+ metadata Json?
485
+
486
+ // Knowledge Graph (KAG) Edge: If clicking this element leads to another screen.
487
+ transitionsToScreenId String?
488
+ transitionsToScreen Screen? @relation("ElementTransitions", fields: [transitionsToScreenId], references: [id])
489
+
490
+ screen Screen? @relation(fields: [screenId], references: [id])
491
+ fingerprint ElementFingerprint?
492
+
493
+ @@unique([tenantId, projectId, selector, pageUrl], name: "Element_ext_upsert_key")
494
+ @@index([tenantId, projectId])
495
+ @@index([tenantId, screenId])
496
+ @@index([tenantId, screenId, parentLandmark])
497
+ @@index([tenantId, screenId, viewport])
498
+ }
499
+
500
+ model Flow {
501
+ id String @id @default(cuid())
502
+ tenantId String
503
+ projectId String
504
+ name String
505
+ // Ordered list of screen ids that make up this user journey (JSON).
506
+ steps Json
507
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
508
+
509
+ @@index([tenantId, projectId])
510
+ }
511
+
512
+ model Screenshot {
513
+ id String @id @default(cuid())
514
+ tenantId String
515
+ projectId String
516
+ screenId String
517
+ // DESKTOP | MOBILE | TABLET
518
+ viewport String @default("DESKTOP")
519
+ storagePath String
520
+ cloudUrl String?
521
+ version Int @default(1)
522
+ capturedAt DateTime @default(now())
523
+ // FM-4: device pixel ratio captured at crawl time; null = unknown (pre-migration)
524
+ dpr Float?
525
+ screen Screen @relation(fields: [screenId], references: [id])
526
+ stepEvidence StepEvidence[]
527
+
528
+ @@index([tenantId, projectId])
529
+ @@index([tenantId, screenId])
530
+ @@index([screenId, version(sort: Desc)])
531
+ }
532
+
533
+ // A reusable, per-project instruction package the AI loads before a task.
534
+ //
535
+ // TAXONOMY KINDS (Application Intelligence):
536
+ // PLATFORM — tech stack: React 18, JWT auth, REST API, mobile-responsive
537
+ // FUNCTION — domain: "e-commerce checkout", "healthcare patient portal"
538
+ // DESIGN — visual system: color tokens, typography, spacing, dark/light mode
539
+ // THEME — cross-cutting conventions: "errors = toast top-right", "spinner on submit"
540
+ // COMPONENT — reusable UI patterns: "DataTable paginates at 25 rows, sortable cols"
541
+ // ELEMENT — specific element behavior: "email validates on blur, error inline"
542
+ //
543
+ // LEGACY KINDS (task-specific):
544
+ // TEST_CASE | TEST_DATA | TEST_SCRIPT | CRAWL | SCREEN_UNDERSTANDING
545
+ //
546
+ // RETRIEVAL TIERS:
547
+ // PLATFORM + FUNCTION + DESIGN + THEME → always injected (global, project-wide)
548
+ // COMPONENT + ELEMENT → injected selectively (filtered by screenIds/tags)
549
+ model SkillFile {
550
+ id String @id @default(cuid())
551
+ tenantId String
552
+ projectId String
553
+
554
+ // Taxonomy kind (see above)
555
+ kind String
556
+
557
+ // PROJECT = per-project (default)
558
+ // WORKSPACE = shared template across all projects in this tenant
559
+ scope String @default("PROJECT")
560
+
561
+ // 1-3 line plain-English description — shown in UI and used for keyword search
562
+ summary String?
563
+
564
+ // Searchable tags for selective retrieval: ["login-form", "email-validation"]
565
+ tags Json @default("[]")
566
+
567
+ // For COMPONENT/ELEMENT: which screenIds this knowledge covers ([] = all screens)
568
+ screenIds Json @default("[]")
569
+
570
+ // The full markdown instruction content — grows with human edits over time
571
+ content String
572
+
573
+ // Machine-readable extraction alongside markdown. Generators should prefer
574
+ // this when present; markdown remains the human review surface.
575
+ structuredData Json @default("{}")
576
+ provenance Json @default("[]")
577
+ confidence Float?
578
+
579
+ // UNKNOWN | PASS | WARN | FAIL
580
+ validationStatus String @default("UNKNOWN")
581
+ validationSummary String?
582
+
583
+ // CURRENT | STALE | APPROVED_STALE
584
+ staleStatus String @default("CURRENT")
585
+ staleReason String?
586
+
587
+ generatorVersion String?
588
+
589
+ version Int @default(1)
590
+ approvedBy String?
591
+ // PENDING | APPROVED | NEEDS_REVIEW
592
+ approvalStatus String @default("PENDING")
593
+
594
+ // Diff / review fields — populated when AI regenerates content
595
+ priorContent String?
596
+ priorVersion Int?
597
+ priorGeneratedAt DateTime?
598
+ changesSummary String?
599
+
600
+ // AUTO (AI-generated) | MANUAL (human-written) | SEEDED (platform template)
601
+ source String @default("AUTO")
602
+
603
+ updatedAt DateTime @default(now())
604
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
605
+
606
+ // Token Tracking
607
+ lastGenPromptTokens Int?
608
+ lastGenCompletionTokens Int?
609
+ lastGenCostUsd Float?
610
+ lastGenModel String?
611
+ lastGenAt DateTime?
612
+
613
+ @@unique([projectId, kind, scope])
614
+ @@index([tenantId, projectId])
615
+ @@index([tenantId, kind])
616
+ }
617
+
618
+ // ---- Per-project skill file kind configuration ----------------------------
619
+ // Platform admins can enable/disable generation for specific kinds,
620
+ // require manual approval, or set auto-gen schedules per project.
621
+
622
+ model SkillFileConfig {
623
+ id String @id @default(cuid())
624
+ projectId String
625
+ tenantId String
626
+ kind String // matches TAXONOMY_KINDS value
627
+ enabled Boolean @default(true)
628
+ requireApproval Boolean @default(false)
629
+ autoGenIntervalMins Int? // null = no scheduled auto-gen
630
+ generationStrategy String @default("AUTO") // AUTO | MANUAL | SEEDED
631
+ validationThresholds Json? // { minConfidence?: number, maxStaleAgeDays?: number }
632
+ adminNotes String? @db.Text
633
+ createdAt DateTime @default(now())
634
+ updatedAt DateTime @updatedAt
635
+
636
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
637
+
638
+ @@unique([projectId, kind])
639
+ @@index([tenantId, projectId])
640
+ }
641
+
642
+ // ---- User-uploaded knowledge artifacts ------------------------------------
643
+
644
+ model ProjectArtifact {
645
+ id String @id @default(cuid())
646
+ tenantId String
647
+ projectId String
648
+ name String // display name (user-editable)
649
+ originalName String // filename as uploaded
650
+ mimeType String
651
+ size Int // bytes of original file
652
+ content String @db.Text // converted markdown content
653
+ storageKey String? // object key in tenant S3/R2 (original file); null = not stored in cloud
654
+ // When tenant S3 is configured, extracted markdown is stored there too.
655
+ // Resolver: if contentKey is set, read from S3; otherwise use content field directly.
656
+ contentKey String?
657
+ kind String @default("DOCUMENT") // DOCUMENT | SPREADSHEET | TEST_CASES | SPEC | OTHER
658
+ uploadedBy String
659
+ uploadedAt DateTime @default(now())
660
+ summary String?
661
+ keywords String[] @default([])
662
+ docType String?
663
+ chunkCount Int @default(1)
664
+ cachedScenarios Json? // Cached extractScenarios result — array of ScenarioSummary objects
665
+ // Import lifecycle tracking
666
+ importStatus String? // null | 'IMPORTED' | 'FAILED'
667
+ importedAt DateTime?
668
+ importedTestCount Int?
669
+ importedSuiteId String? // suite the test cases were imported into; null if suite deleted
670
+ // Version chain — versionNum increments on re-upload; previousVersionId links to prior artifact
671
+ versionNum Int @default(1)
672
+ previousVersionId String?
673
+ contentHash String? // sha256 of original upload bytes; partial unique (projectId, contentHash) index in migration SQL
674
+ previousVersion ProjectArtifact? @relation("ArtifactVersionChain", fields: [previousVersionId], references: [id], onDelete: SetNull)
675
+ nextVersions ProjectArtifact[] @relation("ArtifactVersionChain")
676
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
677
+ testCases TestCase[]
678
+
679
+ @@index([tenantId, projectId])
680
+ }
681
+
682
+ // ---- Test artifacts --------------------------------------------------------
683
+
684
+ enum SuiteCategory {
685
+ REGRESSION
686
+ SMOKE
687
+ SANITY
688
+ API
689
+ SECURITY
690
+ INTEGRATION
691
+ E2E
692
+ CUSTOM
693
+ }
694
+
695
+ model TestSuite {
696
+ id String @id @default(cuid())
697
+ tenantId String
698
+ projectId String
699
+ name String
700
+ createdAt DateTime @default(now())
701
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
702
+ testCases TestCase[]
703
+ category SuiteCategory?
704
+
705
+ // Hierarchy — child suites are created per Section column on import
706
+ // Unique (tenantId, projectId, COALESCE(parentId,''), name) expression index lives in migration SQL.
707
+ parentId String?
708
+ parent TestSuite? @relation("SuiteHierarchy", fields: [parentId], references: [id], onDelete: Cascade)
709
+ children TestSuite[] @relation("SuiteHierarchy")
710
+
711
+ ownerUserId String?
712
+
713
+ // Token Tracking
714
+ genTotalTokens Int?
715
+ genTotalCostUsd Float?
716
+ genModel String?
717
+ genAt DateTime?
718
+ genScreenCount Int?
719
+ coverageReport Json?
720
+
721
+ @@index([tenantId, projectId])
722
+ @@index([parentId])
723
+ }
724
+
725
+ model TestCase {
726
+ id String @id @default(cuid())
727
+ tenantId String
728
+ projectId String
729
+ suiteId String
730
+ screenId String?
731
+ // Human-readable stable reference, e.g. "TC-LOGIN-014"
732
+ ref String?
733
+ title String
734
+ // POSITIVE | NEGATIVE | EDGE | ACCESSIBILITY | SECURITY
735
+ kind String
736
+ // CRITICAL | HIGH | MEDIUM | LOW
737
+ priority String @default("HIGH")
738
+ preconditions String?
739
+ steps Json
740
+ expectedResult String?
741
+ // The rendered Gherkin .feature text for this case
742
+ gherkin String?
743
+ // PENDING_REVIEW | APPROVED | RETIRED (human-in-the-loop)
744
+ status String @default("PENDING_REVIEW")
745
+ approvedBy String? // userId of reviewer who approved
746
+ approvedAt DateTime? // when APPROVED status was set
747
+ // AI_GENERATED | HUMAN_WRITTEN | IMPORTED
748
+ source String @default("AI_GENERATED")
749
+ // Which real element ids this test is grounded in (proves "no invention").
750
+ groundedOn Json
751
+ // Searchable tags e.g. ["@smoke", "@auth", "@regression"]
752
+ tags Json @default("[]")
753
+ // Links to external requirements/Figma/JIRA
754
+ caseRefs Json @default("[]")
755
+ // Viewport scopes for this test case. Empty = applies to all.
756
+ // Values: ["DESKTOP", "MOBILE_PORTRAIT", "TABLET"]
757
+ viewports Json @default("[]")
758
+ // Which runner viewport this test case is valid for. ALL = any runner.
759
+ // ALL | DESKTOP_ONLY | MOBILE_ONLY | TABLET_ONLY
760
+ viewportConstraint String @default("ALL")
761
+ screen Screen? @relation(fields: [screenId], references: [id])
762
+ suite TestSuite @relation(fields: [suiteId], references: [id])
763
+ testData TestData[]
764
+ scripts TestScript[]
765
+ humanReviews HumanReviewItem[]
766
+ results TestResult[]
767
+ testCaseRuns TestCaseRun[]
768
+ stepEvidence StepEvidence[]
769
+ stepValidations StepValidation[]
770
+
771
+ techniqueOfRecord String?
772
+ traceTo String?
773
+ // Additional requirement IDs this test also covers (many-to-many traceability).
774
+ // traceTo stays the primary link; this array holds secondary reqIds.
775
+ traceToExtra Json @default("[]")
776
+ testDataInline Json?
777
+ dataNote String? // LLM-generated guidance for the data-agent: field constraints, personas, boundary values
778
+ scriptNote String? // LLM-generated guidance for the script-agent: preconditions, dynamic extractions, parallelism
779
+ structuredSteps Json? // [{action, target?, data?}] — machine-readable step format for accurate data extraction
780
+
781
+ // Traceability — which artifact this test case was imported from
782
+ artifactId String?
783
+ artifact ProjectArtifact? @relation(fields: [artifactId], references: [id], onDelete: SetNull)
784
+
785
+ // Scenario traceability
786
+ scenarioId String?
787
+ scenario Scenario? @relation(fields: [scenarioId], references: [id], onDelete: SetNull)
788
+ // Suspect link — flagged when upstream requirement changes after this test was generated
789
+ suspect Boolean @default(false)
790
+ suspectReason String?
791
+
792
+ // Flakiness tracking — rolling stats over the last N TestCaseRun outcomes,
793
+ // recomputed by AppBrain.recordFlakinessOutcome() after each run. See
794
+ // docs/13-TEST-FLAKINESS-DETECTION.md "Future Work".
795
+ // 0.0-1.0 fraction of FAILED+FLAKY outcomes in the rolling window.
796
+ flakyScore Float @default(0)
797
+ // Current losing streak (consecutive FAILED/FLAKY runs), reset on a PASS.
798
+ consecutiveFlakes Int @default(0)
799
+ lastFlakyAt DateTime?
800
+ // Auto- or human-quarantined tests are blocked from running (POST .../run
801
+ // returns 409) unless the caller explicitly passes force:true. Never
802
+ // auto-cleared — requires an explicit PATCH .../quarantine to lift.
803
+ quarantined Boolean @default(false)
804
+ quarantinedAt DateTime?
805
+ quarantineReason String?
806
+
807
+ // Token Tracking
808
+ genPromptTokens Int?
809
+ genCompletionTokens Int?
810
+ genCostUsd Float?
811
+ genModel String?
812
+
813
+ // Data-driven execution — linked data set (Phase 4.3)
814
+ dataSetId String?
815
+ dataSet TestDataSet? @relation(fields: [dataSetId], references: [id], onDelete: SetNull)
816
+
817
+ // Framework Modernization — script coverage mapping (Phase 2)
818
+ coverageMapping CoverageMapping?
819
+ // Where a generated script should land in the connected repo (Phase 3)
820
+ repoTargetPath String?
821
+
822
+ // S3 content key pointers (DB→S3 pattern; doc 18).
823
+ // When set, the corresponding DB field is a stub and actual content is in tenant S3.
824
+ stepsKey String? // pointer for steps Json
825
+ gherkinKey String? // pointer for gherkin String
826
+
827
+ // SHA-256 of normalized(title+steps+expectedResult+preconditions) — used for content-hash dedup.
828
+ // Replaces title-only dedup so AI-paraphrased duplicates are caught cross-pass.
829
+ contentHash String?
830
+ // SHA-256 of sorted(groundedOn element IDs) + kind — structural dedup: same DOM flow + kind = same test.
831
+ groundingHash String?
832
+
833
+ // Extracted Playwright locator expressions — computed from Screen.elements (crawl path)
834
+ // or from on-demand page extraction (import path). Stored as PlaywrightLocator[] JSON.
835
+ // Shape: [{key, expression, elementType, confidence, source, description}]
836
+ locatorsJson Json?
837
+ // ISO timestamp of last locator extraction (used to show staleness in UI)
838
+ locatorsExtractedAt DateTime?
839
+ // Per-step screenshot/evidence trail from CAPTURE_STEPS jobs.
840
+ captureStepScreenshots Json?
841
+
842
+ versions TestCaseVersion[]
843
+ sourceEnhancementId String?
844
+ sourceEnhancement ContextEnhancement? @relation("TestCaseSourceEnhancement", fields: [sourceEnhancementId], references: [id], onDelete: SetNull)
845
+ contextQualityScore Int?
846
+ crawlDiffId String?
847
+ crawlDiff CrawlDiff? @relation("TestCaseCrawlDiff", fields: [crawlDiffId], references: [id], onDelete: SetNull)
848
+ bugSignalRefs BugSignal[] @relation("BugSignalAffectedTestCases")
849
+
850
+ createdAt DateTime @default(now())
851
+ updatedAt DateTime @updatedAt
852
+
853
+ @@index([tenantId, projectId])
854
+ @@index([tenantId, suiteId])
855
+ @@index([suiteId, contentHash])
856
+ @@index([suiteId, groundingHash])
857
+ @@index([screenId])
858
+ @@index([priority])
859
+ @@index([source])
860
+ @@index([artifactId])
861
+ @@index([scenarioId])
862
+ @@index([traceTo])
863
+ @@index([tenantId, projectId, suspect])
864
+ @@index([tenantId, projectId, quarantined])
865
+ @@index([dataSetId])
866
+ @@index([status, projectId])
867
+ @@index([sourceEnhancementId])
868
+ @@index([crawlDiffId])
869
+ }
870
+
871
+ // Immutable snapshot of a TestCase taken before each save (edit/review).
872
+ // versionNum increments per testCase starting at 1. The snapshot field holds
873
+ // the full TestCase row at time of capture for complete restore fidelity.
874
+ model TestCaseVersion {
875
+ id String @id @default(cuid())
876
+ testCaseId String
877
+ testCase TestCase @relation(fields: [testCaseId], references: [id], onDelete: Cascade)
878
+ versionNum Int
879
+ title String
880
+ steps Json?
881
+ preconditions String?
882
+ expectedResult String?
883
+ kind String?
884
+ priority String?
885
+ snapshot Json // full TestCase fields at time of snapshot
886
+ createdAt DateTime @default(now())
887
+ createdBy String? // userId
888
+ tenantId String
889
+
890
+ @@unique([testCaseId, versionNum])
891
+ @@index([tenantId, testCaseId])
892
+ }
893
+
894
+ model TestData {
895
+ id String @id @default(cuid())
896
+ tenantId String
897
+ projectId String
898
+ testCaseId String
899
+ // VALID | INVALID | EDGE | SYNTHETIC
900
+ kind String
901
+ // SYNTHETIC | FALLBACK | PRODUCTION | STAGING | SANDBOX | DUMMY | IMPORTED
902
+ source String @default("SYNTHETIC")
903
+ payload Json
904
+ // When set, this row is a historical version superseded by a later regeneration
905
+ supersededAt DateTime?
906
+ testCase TestCase @relation(fields: [testCaseId], references: [id])
907
+
908
+ @@index([tenantId, projectId])
909
+ @@index([tenantId, testCaseId])
910
+ @@index([tenantId, source])
911
+ @@index([tenantId, testCaseId, supersededAt])
912
+ }
913
+
914
+ model TestScript {
915
+ id String @id @default(cuid())
916
+ tenantId String
917
+ projectId String
918
+ testCaseId String
919
+ // The runnable, exportable code (e.g. Playwright).
920
+ language String @default("playwright-ts")
921
+ code String @db.Text
922
+ filePath String? // relative path in the repo, e.g. src/MySuite/locators/MyClass.locators.ts
923
+ // When tenant S3 is configured, code is stored there and this holds the object key.
924
+ // Resolver: if codeKey is set, read from S3; otherwise use code field directly.
925
+ codeKey String?
926
+ // PENDING | PASS | FAIL | SKIPPED
927
+ validationStatus String @default("PENDING")
928
+ validationErrors String? @db.Text
929
+ // Whether this script was pushed to a GitHub PR
930
+ pushedToPR Boolean @default(false)
931
+ pushedPRId String? // GitHubPR.id
932
+ scriptContextSnapshot Json?
933
+ testCase TestCase @relation(fields: [testCaseId], references: [id])
934
+
935
+ @@unique([testCaseId, language])
936
+ @@index([tenantId, projectId])
937
+ @@index([tenantId, testCaseId])
938
+ }
939
+
940
+ model ScriptBundle {
941
+ id String @id @default(cuid())
942
+ tenantId String
943
+ projectId String
944
+ // SUITE | PACK
945
+ scopeType String
946
+ // TestSuite.id for suites, pack id such as regression/smoke/sanity for packs.
947
+ scopeId String
948
+ language String @default("playwright-ts")
949
+ files Json
950
+ basePage String?
951
+ caseCount Int @default(0)
952
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
953
+ createdAt DateTime @default(now())
954
+ updatedAt DateTime @updatedAt
955
+
956
+ @@unique([tenantId, projectId, scopeType, scopeId, language])
957
+ @@index([tenantId, projectId])
958
+ @@index([tenantId, projectId, scopeType, scopeId])
959
+ }
960
+
961
+ // Stores script alignment verification results — score, aligned flag, and issues
962
+ // from the AI verifier. Uses a proper dedicated table instead of abusing TestScript.
963
+ model AlignmentResult {
964
+ id String @id @default(cuid())
965
+ tenantId String
966
+ projectId String
967
+ testCaseId String
968
+ language String
969
+ score Int @default(0)
970
+ aligned Boolean @default(false)
971
+ issues Json @default("[]")
972
+ // Deterministic step coverage (matched steps ÷ total steps), independent of the LLM score.
973
+ stepCoveragePct Float?
974
+ // { totalSteps, coveredSteps, coveragePct, assertionCoverage, perStep: [...] }
975
+ stepCoverage Json?
976
+ verifiedAt DateTime @default(now())
977
+ createdAt DateTime @default(now())
978
+ updatedAt DateTime @updatedAt
979
+
980
+ @@unique([testCaseId, language])
981
+ @@index([tenantId, projectId])
982
+ @@index([tenantId, testCaseId])
983
+ }
984
+
985
+ // Snapshot table — captures old TestScript code before each regeneration.
986
+ // The TestScript table always holds the latest version; history lives here.
987
+ model TestScriptVersion {
988
+ id String @id @default(cuid())
989
+ tenantId String
990
+ testCaseId String
991
+ language String
992
+ code String @db.Text
993
+ versionNum Int
994
+ createdAt DateTime @default(now())
995
+
996
+ @@index([testCaseId, language])
997
+ @@index([tenantId, testCaseId])
998
+ }
999
+
1000
+ // Snapshot table — captures SkillFile content (especially TEST_PLAN) before each update.
1001
+ model SkillFileVersion {
1002
+ id String @id @default(cuid())
1003
+ tenantId String
1004
+ projectId String
1005
+ skillFileId String
1006
+ kind String
1007
+ content String @db.Text
1008
+ versionNum Int
1009
+ createdAt DateTime @default(now())
1010
+
1011
+ @@index([skillFileId, versionNum])
1012
+ @@index([tenantId, projectId, kind])
1013
+ }
1014
+
1015
+ // ---- Workspace / Tenant Settings -------------------------------------------
1016
+
1017
+ // Workspace-level configuration per tenant — LLM provider, notifications, etc.
1018
+ // Sensitive fields (API keys) use the _enc suffix, encrypted with AES-256-GCM.
1019
+ model TenantSetting {
1020
+ id String @id @default(cuid())
1021
+ tenantId String
1022
+ // "LLM" | "NOTIFICATIONS" | "BRANDING"
1023
+ category String
1024
+ // JSON blob; sensitive fields end with _enc (same pattern as Connector)
1025
+ config Json @default("{}")
1026
+ dataRetentionDays Int @default(365)
1027
+ updatedAt DateTime @default(now())
1028
+
1029
+ @@unique([tenantId, category])
1030
+ @@index([tenantId])
1031
+ }
1032
+
1033
+ // Per-tenant override of a platform feature-flag default. Absence of a row for
1034
+ // a given (tenantId, key) means "use the default" (see FEATURE_FLAG_DEFAULTS
1035
+ // in @detiq/core) — this table only ever stores explicit overrides, not every
1036
+ // flag's state for every tenant.
1037
+ model TenantFeatureFlag {
1038
+ id String @id @default(cuid())
1039
+ tenantId String
1040
+ key String
1041
+ enabled Boolean
1042
+ updatedAt DateTime @default(now())
1043
+
1044
+ @@unique([tenantId, key])
1045
+ @@index([tenantId])
1046
+ }
1047
+
1048
+ model PlatformJob {
1049
+ id String @id @default(cuid())
1050
+ tenantId String
1051
+ projectId String
1052
+ type String @default("CRAWL") // CRAWL | GENERATE_TESTS | VISUAL_REGRESSION
1053
+ name String? // EventName_YYYY-MM-DD_timestamp for tracking
1054
+ // QUEUED | RUNNING | DONE | FAILED
1055
+ status String @default("QUEUED")
1056
+ config Json
1057
+ result Json?
1058
+ error String?
1059
+ failureType String? // FailureType enum: TRANSIENT | INFRASTRUCTURE | LOGICAL | FATAL
1060
+ progress Int @default(0)
1061
+ queuedAt DateTime @default(now())
1062
+ startedAt DateTime?
1063
+ finishedAt DateTime?
1064
+ // Identifies which worker process claimed this job (host:pid) — diagnostics +
1065
+ // lets the stale-job sweep tell a legitimately long-running job apart from
1066
+ // one whose worker died without updating status.
1067
+ workerId String?
1068
+ // Bumped on every progress update while RUNNING. The stale-job sweep compares
1069
+ // this (not startedAt) against its timeout, so a slow-but-alive job doesn't
1070
+ // get requeued and double-processed by another worker.
1071
+ heartbeatAt DateTime?
1072
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1073
+
1074
+ // Token Tracking
1075
+ totalPromptTokens Int?
1076
+ totalCompletionTokens Int?
1077
+ totalCacheReadTokens Int?
1078
+ totalEstimatedCostUsd Float?
1079
+ llmCallCount Int @default(0)
1080
+
1081
+ // Gap 30: Dead-letter queue / automatic retry
1082
+ retryCount Int @default(0)
1083
+ nextRetryAt DateTime?
1084
+ dlq Boolean @default(false)
1085
+ dlqReason String?
1086
+
1087
+ // Worker lease / fencing (GAP-03)
1088
+ leaseExpiresAt DateTime?
1089
+ fencingToken String?
1090
+
1091
+ // Durable checkpoint for CRAWL (GAP-04)
1092
+ checkpoint Json?
1093
+ checkpointAt DateTime?
1094
+
1095
+ // Parent-child relationship (GAP-06)
1096
+ parentJobId String?
1097
+ rootJobId String?
1098
+ parent PlatformJob? @relation("JobParent", fields: [parentJobId], references: [id])
1099
+ children PlatformJob[] @relation("JobParent")
1100
+
1101
+ // Current execution step as indexed column (GAP-09)
1102
+ currentStep String?
1103
+
1104
+ // Re-queue timestamp for accurate queue-wait time on retries (GAP-10/22)
1105
+ requeuedAt DateTime?
1106
+
1107
+ // DLQ lifecycle (GAP-13)
1108
+ dlqAt DateTime?
1109
+ dlqRedriveCount Int @default(0)
1110
+
1111
+ // User attribution (GAP-16)
1112
+ submittedBy String?
1113
+
1114
+ // Inline (API-in-process) vs Worker mode (GAP-21)
1115
+ inline Boolean @default(false)
1116
+
1117
+ // Priority — higher = sooner (GAP-23)
1118
+ priority Int @default(0)
1119
+
1120
+ // Attempts relation
1121
+ attempts JobAttempt[]
1122
+
1123
+ @@index([tenantId, projectId])
1124
+ @@index([tenantId, projectId, queuedAt])
1125
+ @@index([status])
1126
+ @@index([status, queuedAt])
1127
+ @@index([status, workerId])
1128
+ @@index([status, dlq, nextRetryAt])
1129
+ @@index([status, nextRetryAt])
1130
+ @@index([parentJobId])
1131
+ @@index([tenantId, status])
1132
+ @@index([priority, queuedAt])
1133
+ }
1134
+
1135
+ model JobAttempt {
1136
+ id String @id @default(cuid())
1137
+ jobId String
1138
+ tenantId String
1139
+ attemptNumber Int
1140
+ workerId String?
1141
+ fencingToken String?
1142
+ startedAt DateTime @default(now())
1143
+ finishedAt DateTime?
1144
+ status String // RUNNING | DONE | FAILED | CANCELLED
1145
+ error String?
1146
+ progress Int @default(0)
1147
+ job PlatformJob @relation(fields: [jobId], references: [id], onDelete: Cascade)
1148
+
1149
+ @@index([jobId])
1150
+ @@index([tenantId])
1151
+ @@index([jobId, attemptNumber])
1152
+ }
1153
+
1154
+ // ---- Self-Healing Element Fingerprints -------------------------------------
1155
+
1156
+ model ElementFingerprint {
1157
+ id String @id @default(cuid())
1158
+ tenantId String
1159
+ projectId String
1160
+ elementId String @unique
1161
+ // Multiple selectors ranked by stability: [{type, value, confidence}]
1162
+ selectors Json
1163
+ lastHealedAt DateTime?
1164
+ healCount Int @default(0)
1165
+ element Element @relation(fields: [elementId], references: [id])
1166
+
1167
+ @@index([tenantId, projectId])
1168
+ }
1169
+
1170
+ // ---- Session Recording (Record-and-Replay + Production Capture) ------------
1171
+
1172
+ model UserSession {
1173
+ id String @id @default(cuid())
1174
+ tenantId String
1175
+ projectId String
1176
+ // RECORDED | PRODUCTION
1177
+ source String
1178
+ startUrl String
1179
+ // Array of {type, target, value, timestamp, viewport}
1180
+ events Json
1181
+ duration Int?
1182
+ // PENDING | PROCESSING | CONVERTED | FAILED
1183
+ status String @default("PENDING")
1184
+ convertedSuiteId String?
1185
+ recordedAt DateTime @default(now())
1186
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1187
+
1188
+ @@index([tenantId, projectId])
1189
+ @@index([status])
1190
+ }
1191
+
1192
+ // ---- Visual Regression Testing ---------------------------------------------
1193
+
1194
+ model VisualBaseline {
1195
+ id String @id @default(cuid())
1196
+ tenantId String
1197
+ projectId String
1198
+ screenId String
1199
+ viewport String @default("DESKTOP")
1200
+ browser String @default("chromium")
1201
+ storagePath String
1202
+ cloudUrl String?
1203
+ approvedBy String?
1204
+ createdAt DateTime @default(now())
1205
+ gitBranch String @default("main")
1206
+ gitCommit String?
1207
+ // Masking zones — regions excluded from visual diff comparison.
1208
+ // Array of {x, y, width, height, label} rectangles (user-drawn exclusions for dynamic content).
1209
+ ignoredRegions Json @default("[]")
1210
+ // Snapshot of element bounding boxes at baseline creation time for layout drift detection.
1211
+ // Array of {meaning, role, boundingRect} objects.
1212
+ elementSnapshot Json?
1213
+ screen Screen @relation(fields: [screenId], references: [id])
1214
+ diffs VisualDiff[]
1215
+
1216
+ @@index([tenantId, projectId])
1217
+ @@unique([screenId, viewport, gitBranch, browser])
1218
+ }
1219
+
1220
+ model VisualDiff {
1221
+ id String @id @default(cuid())
1222
+ tenantId String
1223
+ baselineId String
1224
+ newScreenshotPath String
1225
+ newScreenshotCloudUrl String?
1226
+ diffImageCloudUrl String?
1227
+ // 0-100; 0 = identical
1228
+ diffScore Float
1229
+ // NONE | MINOR | SIGNIFICANT | CRITICAL
1230
+ changeType String
1231
+ // [{description, location, severity}]
1232
+ changes Json?
1233
+ // APPROVE | REVIEW | FLAG | BLOCK
1234
+ recommendation String?
1235
+ bugReportDraft Json?
1236
+ // AI-generated one-sentence summary of what changed (or "No meaningful changes detected")
1237
+ aiSummary String?
1238
+ // PENDING | APPROVED | FLAGGED | BLOCKED
1239
+ status String @default("PENDING")
1240
+ reviewedBy String?
1241
+ bugSignalVisualDiffId String?
1242
+ bugSignalRef BugSignal? @relation("BugSignalVisualDiffs", fields: [bugSignalVisualDiffId], references: [id], onDelete: SetNull)
1243
+ createdAt DateTime @default(now())
1244
+ baseline VisualBaseline @relation(fields: [baselineId], references: [id])
1245
+
1246
+ @@index([tenantId, baselineId])
1247
+ @@index([status])
1248
+ }
1249
+
1250
+ // ---- Human + AI Hybrid Review Queue ----------------------------------------
1251
+
1252
+ model HumanReviewItem {
1253
+ id String @id @default(cuid())
1254
+ tenantId String
1255
+ projectId String
1256
+ testCaseId String
1257
+ reason String
1258
+ confidence Float
1259
+ context Json?
1260
+ // PENDING | IN_REVIEW | APPROVED | REJECTED
1261
+ status String @default("PENDING")
1262
+ assignedTo String?
1263
+ resolution String?
1264
+ reviewedAt DateTime?
1265
+ createdAt DateTime @default(now())
1266
+ testCase TestCase @relation(fields: [testCaseId], references: [id])
1267
+
1268
+ @@index([tenantId, projectId])
1269
+ @@index([status])
1270
+ }
1271
+
1272
+ // ---- Per-step visual evidence — persisted AI+human element matching -----------
1273
+ // One row per (testCaseId, stepIndex). Populated on first evidence fetch and
1274
+ // can be corrected by a QA reviewer (source -> 'human_verified').
1275
+ // When the parent Screen is re-crawled and elementHash changes, source is set
1276
+ // to 'stale' so reviewers know to re-verify.
1277
+
1278
+ model StepEvidence {
1279
+ id String @id @default(cuid())
1280
+ tenantId String
1281
+ testCaseId String
1282
+ stepIndex Int // 0-based index matching TestCase.steps array
1283
+ stepText String // snapshot of step text when evidence was computed
1284
+
1285
+ // The screenshot that best captures the screen state for this step.
1286
+ // Null = use the Screen's latest screenshot (default).
1287
+ screenshotId String?
1288
+ screenshot Screenshot? @relation(fields: [screenshotId], references: [id], onDelete: SetNull)
1289
+
1290
+ // JSON array of Element.id strings that are relevant to this step
1291
+ elementIds Json @default("[]")
1292
+
1293
+ // Highest confidence score among matched elements (0.0-1.0)
1294
+ confidence Float @default(0)
1295
+
1296
+ // 'ai_heuristic' | 'ai_semantic' | 'human_verified' | 'stale'
1297
+ source String @default("ai_heuristic")
1298
+
1299
+ testCase TestCase @relation(fields: [testCaseId], references: [id], onDelete: Cascade)
1300
+ createdAt DateTime @default(now())
1301
+ updatedAt DateTime @updatedAt
1302
+
1303
+ @@unique([testCaseId, stepIndex])
1304
+ @@index([tenantId, testCaseId])
1305
+ }
1306
+
1307
+ // ---- Manual QA step validation — human-in-the-loop per-step annotation -------
1308
+ // Inspired by TestRail step results. A QA reviewer marks each step
1309
+ // PASS / FAIL / SKIP / BLOCKED and optionally uploads screenshot evidence.
1310
+
1311
+ model StepValidation {
1312
+ id String @id @default(cuid())
1313
+ tenantId String
1314
+ testCaseId String
1315
+ stepIndex Int // 0-based index matching TestCase.steps array
1316
+ // PASS | FAIL | SKIP | BLOCKED
1317
+ result String
1318
+ comment String?
1319
+ // URL of an uploaded screenshot or other attachment (S3 cloudUrl)
1320
+ attachmentUrl String?
1321
+ // Optional: ties this validation to a specific TestCaseRun
1322
+ testCaseRunId String?
1323
+ testCaseRun TestCaseRun? @relation(fields: [testCaseRunId], references: [id], onDelete: SetNull)
1324
+ reviewerId String?
1325
+ reviewer User? @relation(fields: [reviewerId], references: [id], onDelete: SetNull)
1326
+ createdAt DateTime @default(now())
1327
+ testCase TestCase @relation(fields: [testCaseId], references: [id], onDelete: Cascade)
1328
+
1329
+ @@index([tenantId, testCaseId])
1330
+ @@index([testCaseId, stepIndex])
1331
+ }
1332
+
1333
+ // ---- Per-Test-Case Execution Runs ------------------------------------------
1334
+
1335
+ model TestCaseRun {
1336
+ id String @id @default(cuid())
1337
+ testCaseId String
1338
+ tenantId String
1339
+ // PENDING | RUNNING | PASSED | FAILED | ERROR | FLAKY (maxRuns>1, outcomes diverged)
1340
+ status String @default("PENDING")
1341
+ // Array<ExecutionStep> generated by test-script-agent
1342
+ executionPlan Json?
1343
+ // Array<StepResult> from executor
1344
+ stepResults Json?
1345
+ totalDuration Int?
1346
+ // 0-indexed step number that failed
1347
+ errorStep Int?
1348
+ errorMessage String?
1349
+ screenshotPath String?
1350
+ viewport String @default("DESKTOP")
1351
+ // chromium | firefox | webkit — engine used for this run (executeTestPlan already
1352
+ // supports all 3; this just records/selects which one)
1353
+ browser String @default("chromium")
1354
+ createdAt DateTime @default(now())
1355
+ completedAt DateTime?
1356
+ testCase TestCase @relation(fields: [testCaseId], references: [id], onDelete: Cascade)
1357
+ stepValidations StepValidation[]
1358
+
1359
+ @@index([tenantId, testCaseId])
1360
+ @@index([tenantId, status, createdAt])
1361
+ }
1362
+
1363
+ // ---- Test Run Tracking -----------------------------------------------------
1364
+
1365
+ model TestRun {
1366
+ id String @id @default(cuid())
1367
+ tenantId String
1368
+ projectId String
1369
+ suiteId String?
1370
+ // MANUAL | SCHEDULED | GIT_PUSH | POST_CRAWL | GITHUB_ACTION
1371
+ trigger String @default("MANUAL")
1372
+ // RUNNING | COMPLETED | FAILED | CANCELLED
1373
+ status String @default("RUNNING")
1374
+ // {browser, viewport, baseUrl, env, browsers?: string[], shards?: number}
1375
+ config Json?
1376
+ // {total, passed, failed, skipped, duration}
1377
+ summary Json?
1378
+ // Parallel shard config — null means single-shard (legacy) run
1379
+ totalShards Int?
1380
+ // CI/CD integration fields
1381
+ githubRepo String?
1382
+ githubSha String?
1383
+ githubRef String?
1384
+ triggeredBy String?
1385
+ startedAt DateTime @default(now())
1386
+ finishedAt DateTime?
1387
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1388
+ results TestResult[]
1389
+ shards TestRunShard[]
1390
+
1391
+ @@index([tenantId, projectId])
1392
+ @@index([status])
1393
+ }
1394
+
1395
+ // One row per parallel shard within a TestRun.
1396
+ // Each shard runs a subset of test cases, optionally on a different browser.
1397
+ model TestRunShard {
1398
+ id String @id @default(cuid())
1399
+ runId String
1400
+ tenantId String
1401
+ shardIndex Int // 0-based index within the run
1402
+ browser String @default("chromium") // chromium | firefox | webkit
1403
+ // PENDING | RUNNING | COMPLETED | FAILED
1404
+ status String @default("PENDING")
1405
+ // {total, passed, failed, skipped, duration}
1406
+ summary Json?
1407
+ startedAt DateTime?
1408
+ finishedAt DateTime?
1409
+ run TestRun @relation(fields: [runId], references: [id], onDelete: Cascade)
1410
+
1411
+ @@unique([runId, shardIndex, browser])
1412
+ @@index([runId])
1413
+ @@index([tenantId])
1414
+ }
1415
+
1416
+ model TestResult {
1417
+ id String @id @default(cuid())
1418
+ tenantId String
1419
+ runId String
1420
+ testCaseId String
1421
+ // PASSED | FAILED | SKIPPED | ERROR
1422
+ status String
1423
+ duration Int?
1424
+ error String?
1425
+ screenshotPath String?
1426
+ logs Json?
1427
+ healedAt DateTime?
1428
+ // AI-generated root cause summary (Phase 4.1)
1429
+ failureAnalysis String?
1430
+ failureAnalysisAt DateTime?
1431
+ // Data-driven execution — which row produced this result (Phase 4.3)
1432
+ dataRowId String?
1433
+ dataRowIndex Int?
1434
+ bugSignalId String?
1435
+ bugSignalRef BugSignal? @relation("BugSignalTestResults", fields: [bugSignalId], references: [id], onDelete: SetNull)
1436
+ run TestRun @relation(fields: [runId], references: [id])
1437
+ testCase TestCase @relation(fields: [testCaseId], references: [id])
1438
+
1439
+ @@index([tenantId, runId])
1440
+ @@index([tenantId, runId, status])
1441
+ @@index([testCaseId])
1442
+ }
1443
+
1444
+ // ---- Connectors, audit, change tracking ------------------------------------
1445
+
1446
+ model Connector {
1447
+ id String @id @default(cuid())
1448
+ tenantId String
1449
+ projectId String
1450
+ // GITHUB | JIRA | FIGMA | CONFLUENCE | PAGERDUTY | SERVICENOW | SALESFORCE
1451
+ kind String
1452
+ // A reference/handle to where credentials live in the secrets manager.
1453
+ // NEVER store raw secrets in the database.
1454
+ secretRef String
1455
+ config Json?
1456
+ createdAt DateTime @default(now())
1457
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1458
+
1459
+ @@index([tenantId, projectId])
1460
+ }
1461
+
1462
+ // OAuth 2.0 connections — replaces manual token entry in Connector.
1463
+ // All token fields use the _enc suffix and are AES-256-GCM encrypted at rest.
1464
+ // Never return raw token values in API responses.
1465
+ model OAuthConnection {
1466
+ id String @id @default(cuid())
1467
+ tenantId String
1468
+ projectId String
1469
+
1470
+ // GITHUB | JIRA | FIGMA
1471
+ provider String
1472
+
1473
+ // Encrypted access token (AES-256-GCM, base64).
1474
+ // Null for GitHub App (uses IAT generated on-the-fly from private key).
1475
+ accessToken_enc String?
1476
+ // Encrypted refresh token. Null for GitHub App.
1477
+ // Jira: rotating — always store new one issued on refresh.
1478
+ refreshToken_enc String?
1479
+ // When the access token expires. Null = does not expire (rare).
1480
+ tokenExpiresAt DateTime?
1481
+ // When the refresh token expires (Jira ~180 days unused; GitHub 6 months).
1482
+ refreshTokenExpiresAt DateTime?
1483
+
1484
+ // CONNECTED | NEEDS_REAUTH | DISCONNECTED
1485
+ // Only set to NEEDS_REAUTH on definitive auth errors (400/401 with invalid_grant).
1486
+ // Proactive refresh failures use backoff, not status change.
1487
+ status String @default("CONNECTED")
1488
+
1489
+ // Human-readable identity — who/what authorized this connection.
1490
+ // e.g. "Acme Corp (org)" for GitHub App, "user@company.atlassian.net" for Jira.
1491
+ connectedAs String?
1492
+
1493
+ // GitHub App: numeric installation ID (needed to generate IAT)
1494
+ installationId String?
1495
+
1496
+ // Jira Cloud: cloudId from /oauth/token/accessible-resources
1497
+ // Required to construct API base URL: api.atlassian.com/ex/jira/{cloudId}/...
1498
+ cloudId String?
1499
+
1500
+ // Granted OAuth scopes (space-separated string or JSON array as string)
1501
+ scopes String?
1502
+
1503
+ // Encrypted PAT — fallback for Jira Data Center / on-prem (no 3LO available)
1504
+ pat_enc String?
1505
+ // Jira Data Center base URL (e.g. https://jira.internal.company.com)
1506
+ dcBaseUrl String?
1507
+
1508
+ createdAt DateTime @default(now())
1509
+ updatedAt DateTime @updatedAt
1510
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1511
+
1512
+ // One active connection per provider per project
1513
+ @@unique([projectId, provider])
1514
+ @@index([tenantId, projectId])
1515
+ @@index([tenantId, provider])
1516
+ @@index([status])
1517
+ }
1518
+
1519
+ // A real, revocable login session — the row's id IS the JWT's `jti` claim.
1520
+ // checked on every request (see resolveContext in packages/api). Deliberately
1521
+ // distinct from UserSession (that model is unrelated: recorded browser test
1522
+ // sessions for replay/conversion, nothing to do with authentication).
1523
+ // Admin-impersonation tokens get one too (minted via mintSessionToken, same
1524
+ // as a real login) so "Stop Impersonating" can actually revoke it server-side
1525
+ // instead of only clearing the admin's local browser storage — the 30-minute
1526
+ // natural expiry alone isn't enough for a privileged-access session that's
1527
+ // meant to be endable on demand. Not created for the transient "root" token
1528
+ // issued when a login can't yet pick a single tenant (that token alone can't
1529
+ // reach any tenant-scoped route, so there is nothing meaningful to revoke).
1530
+ model AuthSession {
1531
+ id String @id // JWT jti claim — set from crypto.randomUUID() in mintSessionToken()
1532
+ userId String
1533
+ tenantId String
1534
+ userAgent String?
1535
+ ipAddress String?
1536
+ createdAt DateTime @default(now())
1537
+ lastSeenAt DateTime @default(now())
1538
+ revokedAt DateTime?
1539
+
1540
+ @@index([userId])
1541
+ @@index([tenantId])
1542
+ }
1543
+
1544
+ // One-time backup codes for TOTP MFA recovery (each usable exactly once)
1545
+ model MfaRecoveryCode {
1546
+ id String @id @default(cuid())
1547
+ userId String
1548
+ codeHash String // bcrypt hash of the plain recovery code
1549
+ usedAt DateTime?
1550
+ createdAt DateTime @default(now())
1551
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1552
+
1553
+ @@index([userId])
1554
+ }
1555
+
1556
+ // SSO configuration per tenant (SAML 2.0 or OIDC)
1557
+ model SsoConfig {
1558
+ id String @id @default(cuid())
1559
+ tenantId String @unique
1560
+ provider String // "SAML" | "OIDC"
1561
+ // SAML fields
1562
+ entryPoint String? // IdP SSO URL
1563
+ issuer String? // IdP entity ID / OIDC issuer URL
1564
+ cert String? // IdP signing certificate (PEM)
1565
+ // OIDC fields
1566
+ clientId String?
1567
+ clientSecret_enc String? // AES-256-GCM encrypted
1568
+ // Common
1569
+ defaultRole String @default("TESTER")
1570
+ emailDomains String[] // permitted email domains for JIT provisioning; empty = allow all
1571
+ jitProvisioning Boolean @default(false) // auto-create users on first SSO login
1572
+ enabled Boolean @default(false)
1573
+ // When true, password login is rejected for members of this tenant who
1574
+ // have an active membership here — they must use SSO. Defaults to false
1575
+ // (opt-in) so configuring SSO doesn't silently lock out existing password
1576
+ // users. See docs/100-GAPS-RBAC-SECURITY-ENTERPRISE.md §1 #42 (SSO-02).
1577
+ ssoRequired Boolean @default(false)
1578
+ // SCIM 2.0 bearer token for this tenant, stored as a SHA-256 hash (same
1579
+ // pattern as AgentToken.tokenHash) — never the plaintext, which is shown
1580
+ // once at generation time. Nullable: SCIM is off until an admin generates
1581
+ // one. Previously this field didn't exist at all, so per-tenant SCIM could
1582
+ // never work — see docs/100-GAPS-RBAC-SECURITY-ENTERPRISE.md §3 NEW-1.
1583
+ scimTokenHash String? @unique
1584
+ createdAt DateTime @default(now())
1585
+ updatedAt DateTime @updatedAt
1586
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1587
+ }
1588
+
1589
+ // Audit trail for compliance (SOC 2 / EU AI Act) — Phase 3 expands this.
1590
+ model AuditLog {
1591
+ id String @id @default(cuid())
1592
+ tenantId String
1593
+ projectId String?
1594
+ actorId String?
1595
+ action String
1596
+ detail Json?
1597
+ ipAddress String?
1598
+ createdAt DateTime @default(now())
1599
+ project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
1600
+ user User? @relation(fields: [actorId], references: [id])
1601
+ tenant Tenant @relation(fields: [tenantId], references: [id])
1602
+
1603
+ @@index([tenantId, createdAt])
1604
+ @@index([tenantId, action])
1605
+ }
1606
+
1607
+ model AccessReview {
1608
+ id String @id @default(cuid())
1609
+ tenantId String
1610
+ reviewedBy String
1611
+ memberCount Int
1612
+ removedCount Int @default(0)
1613
+ completedAt DateTime @default(now())
1614
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1615
+
1616
+ @@index([tenantId])
1617
+ }
1618
+
1619
+ // Local agent pairing token — hashed, never stored plain.
1620
+ model AgentToken {
1621
+ id String @id @default(cuid())
1622
+ tenantId String
1623
+ projectId String?
1624
+ name String
1625
+ tokenHash String @unique
1626
+ lastSeenAt DateTime?
1627
+ revokedAt DateTime?
1628
+ createdAt DateTime @default(now())
1629
+ expiresAt DateTime?
1630
+ createdByUserId String?
1631
+ project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
1632
+ createdByUser User? @relation(fields: [createdByUserId], references: [id])
1633
+
1634
+ @@index([tenantId, projectId])
1635
+ }
1636
+
1637
+ // Outbound webhook — ZeTa posts events to this URL on crawl/test changes.
1638
+ model Webhook {
1639
+ id String @id @default(cuid())
1640
+ tenantId String
1641
+ projectId String
1642
+ url String
1643
+ // HMAC secret, AES-256-GCM encrypted at application layer.
1644
+ secret_enc String
1645
+ // JSON array of event names, e.g. ["crawl.done","test.generated"]
1646
+ events Json @default("[]")
1647
+ active Boolean @default(true)
1648
+ createdAt DateTime @default(now())
1649
+ updatedAt DateTime @updatedAt
1650
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1651
+ deliveries WebhookDelivery[]
1652
+
1653
+ @@index([tenantId, projectId])
1654
+ }
1655
+
1656
+ // One row per delivery attempt (dispatch retries on failure, so a single
1657
+ // logical event can produce several rows sharing the same event/payload with
1658
+ // increasing `attempt`). Storing the payload lets a failed delivery be
1659
+ // manually redelivered later with the exact original body.
1660
+ model WebhookDelivery {
1661
+ id String @id @default(cuid())
1662
+ webhookId String
1663
+ tenantId String
1664
+ event String
1665
+ payload Json
1666
+ attempt Int @default(1)
1667
+ status String // SUCCESS | FAILED
1668
+ httpStatus Int?
1669
+ error String?
1670
+ createdAt DateTime @default(now())
1671
+ webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
1672
+
1673
+ @@index([webhookId, createdAt])
1674
+ @@index([webhookId, status])
1675
+ }
1676
+
1677
+ // Phase 3: Perceptual screenshot diff — crawl-to-crawl visual change detection
1678
+ model ScreenDiff {
1679
+ id String @id @default(cuid())
1680
+ tenantId String
1681
+ projectId String
1682
+ screenId String
1683
+ fromVersion Int
1684
+ toVersion Int
1685
+ // (hammingDistance / 64) * 100, rounded to 1 decimal. 0–100.
1686
+ pHashDistance Float
1687
+ // "IDENTICAL" | "MINOR" | "MODERATE" | "SIGNIFICANT"
1688
+ changeLevel String
1689
+ createdAt DateTime @default(now())
1690
+ screen Screen @relation(fields: [screenId], references: [id])
1691
+
1692
+ @@unique([screenId, fromVersion, toVersion])
1693
+ @@index([tenantId, projectId])
1694
+ @@index([tenantId, screenId])
1695
+ }
1696
+
1697
+ // Phase 5: Structured probe observations — typed facts from interactive probe
1698
+ model ProbeObservation {
1699
+ id String @id @default(cuid())
1700
+ tenantId String
1701
+ projectId String
1702
+ screenId String
1703
+ elementMeaning String
1704
+ // VALIDATION_ERROR | MODAL | SUCCESS_TOAST | CONDITIONAL_REVEAL | LOADING_STATE
1705
+ observationType String
1706
+ observedText String
1707
+ // "empty_submit" | "invalid_format" | "button_click:{label}" | "select_change:{label}:{value}"
1708
+ triggeredBy String
1709
+ observedAt DateTime @default(now())
1710
+ screen Screen @relation(fields: [screenId], references: [id])
1711
+
1712
+ @@index([tenantId, projectId])
1713
+ @@index([tenantId, screenId])
1714
+ @@index([tenantId, screenId, observationType])
1715
+ }
1716
+
1717
+ // Records what changed between crawls (Differential Engine — Phase 6).
1718
+ model ChangeLog {
1719
+ id String @id @default(cuid())
1720
+ tenantId String
1721
+ projectId String
1722
+ // ADDED | REMOVED | MODIFIED
1723
+ changeType String
1724
+ summary String
1725
+ detail Json?
1726
+ createdAt DateTime @default(now())
1727
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1728
+
1729
+ @@index([tenantId, projectId])
1730
+ }
1731
+
1732
+ // Full-text search index — one row per heading-delimited section of a skill file or artifact.
1733
+ // The `tsv` tsvector column is GENERATED ALWAYS AS in Postgres (see migration 0014).
1734
+ // Prisma does not manage tsv — it is invisible to Prisma and updated automatically by Postgres.
1735
+ model KnowledgeChunk {
1736
+ id String @id @default(cuid())
1737
+ tenantId String
1738
+ projectId String? // null = global / research docs available to all projects
1739
+ sourceType String // SKILL_FILE | ARTIFACT | SCREEN
1740
+ sourceId String // ID of the parent SkillFile or ProjectArtifact
1741
+ imagePath String? // Base64 or local path for vision-grounded chunks
1742
+ heading String
1743
+ content String
1744
+ // tsv is a GENERATED ALWAYS AS STORED tsvector column managed entirely by Postgres.
1745
+ // Declared Unsupported so Prisma never tries to write it (and never drops it).
1746
+ tsv Unsupported("tsvector")?
1747
+ updatedAt DateTime @updatedAt
1748
+
1749
+ @@index([tenantId, projectId])
1750
+ @@index([sourceId])
1751
+ }
1752
+
1753
+ // ---- Token Optimisation & Ledger -------------------------------------------
1754
+
1755
+ model LLMCallLog {
1756
+ id String @id @default(cuid())
1757
+ tenantId String
1758
+ projectId String?
1759
+
1760
+ // What triggered this call
1761
+ jobId String? // FK to PlatformJob (if inside a job)
1762
+ agentType String // 'test_case' | 'skill_file' | 'flow' | 'nlp' | 'script' | 'data' | 'visual'
1763
+ label String // 'pass1_functional' | 'skill_file_TECH_STACK' | 'flow_journey'
1764
+ entityId String? // TestCase.id or SkillFile.id this call produced/contributed to
1765
+ entityType String? // 'test_case' | 'skill_file' | 'script' | 'test_data'
1766
+
1767
+ // Provider
1768
+ provider String
1769
+ model String
1770
+
1771
+ // Token counts
1772
+ promptTokens Int
1773
+ completionTokens Int
1774
+ cacheReadTokens Int @default(0)
1775
+ cacheWriteTokens Int @default(0)
1776
+ totalTokens Int
1777
+
1778
+ // Cost
1779
+ estimatedCostUsd Float @default(0)
1780
+
1781
+ // Response quality
1782
+ outputTokens Int? // alias completionTokens
1783
+ latencyMs Int?
1784
+
1785
+ taskType String?
1786
+ calledAt DateTime @default(now())
1787
+
1788
+ @@index([tenantId, projectId])
1789
+ @@index([tenantId, jobId])
1790
+ @@index([tenantId, agentType])
1791
+ @@index([entityId])
1792
+ @@index([calledAt])
1793
+ }
1794
+
1795
+ model TokenBudget {
1796
+ id String @id @default(cuid())
1797
+ tenantId String @unique
1798
+ monthlyTokenCap Int?
1799
+ monthlyUsdCap Float?
1800
+ alertAtPercent Int @default(80)
1801
+ currentMonthTokens Int @default(0)
1802
+ currentMonthUsd Float @default(0)
1803
+ lastResetAt DateTime @default(now())
1804
+ updatedAt DateTime @updatedAt
1805
+ tenant Tenant @relation(fields: [tenantId], references: [id])
1806
+ }
1807
+
1808
+ model Notification {
1809
+ id String @id @default(cuid())
1810
+ tenantId String
1811
+ userId String
1812
+ type String
1813
+ title String
1814
+ body String
1815
+ metadata Json?
1816
+ read Boolean @default(false)
1817
+ createdAt DateTime @default(now())
1818
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1819
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1820
+ @@index([tenantId, userId, read])
1821
+ @@index([tenantId, createdAt])
1822
+ }
1823
+
1824
+ model NotificationPreference {
1825
+ id String @id @default(cuid())
1826
+ tenantId String
1827
+ userId String
1828
+ eventType String
1829
+ inApp Boolean @default(true)
1830
+ email Boolean @default(true)
1831
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1832
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1833
+ @@unique([userId, tenantId, eventType])
1834
+ @@index([tenantId, userId])
1835
+ }
1836
+
1837
+ // ─── Platform System Configuration ───────────────────────────────────────────
1838
+ // Key-value store for platform-wide settings managed via admin panel.
1839
+ // Replaces env vars for runtime-changeable config (SMTP, LLM, OAuth app creds).
1840
+ // Sensitive values are AES-256-GCM encrypted before storage.
1841
+ // Env vars remain as fallback if no DB row exists.
1842
+
1843
+ model SystemConfig {
1844
+ key String @id
1845
+ value String
1846
+ category String
1847
+ sensitive Boolean @default(false)
1848
+ updatedAt DateTime @updatedAt
1849
+ updatedByAdminId String?
1850
+
1851
+ @@index([category])
1852
+ }
1853
+
1854
+ // ---- App Fields (form field schema per project) ----------------------------
1855
+
1856
+ model AppField {
1857
+ id String @id @default(cuid())
1858
+ tenantId String
1859
+ projectId String
1860
+ fieldKey String
1861
+ label String
1862
+ elementRole String
1863
+ dataBearing Boolean @default(true)
1864
+ dataType String
1865
+ required Boolean @default(false)
1866
+ minLength Int?
1867
+ maxLength Int?
1868
+ minValue Float?
1869
+ maxValue Float?
1870
+ precision Int?
1871
+ regex String?
1872
+ enumValues Json?
1873
+ piiClass String @default("unknown")
1874
+ exampleValue String?
1875
+ relationGroup String?
1876
+ notes String?
1877
+ status String @default("unconfirmed")
1878
+ createdAt DateTime @default(now())
1879
+ updatedAt DateTime @updatedAt
1880
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1881
+
1882
+ @@unique([tenantId, projectId, fieldKey])
1883
+ @@index([tenantId, projectId])
1884
+ }
1885
+
1886
+ // ---- Workspace Connectors (third-party service credentials per tenant) ------
1887
+
1888
+ model WorkspaceConnector {
1889
+ id String @id @default(cuid())
1890
+ tenantId String
1891
+ kind String
1892
+ secretRef String @default("local-aes256")
1893
+ config Json?
1894
+ testOk Boolean @default(false)
1895
+ testedAt DateTime?
1896
+ createdAt DateTime @default(now())
1897
+ updatedAt DateTime @updatedAt
1898
+
1899
+ @@unique([tenantId, kind])
1900
+ @@index([tenantId])
1901
+ }
1902
+
1903
+ // ---- Shared Run Tokens (shareable signed links to test run results) ---------
1904
+
1905
+ model SharedRunToken {
1906
+ id String @id
1907
+ tenantId String
1908
+ runId String
1909
+ token String @unique
1910
+ expiresAt DateTime
1911
+ createdAt DateTime @default(now())
1912
+
1913
+ @@index([tenantId])
1914
+ @@index([token])
1915
+ }
1916
+
1917
+ // ---- Native App Builds -----------------------------------------------------
1918
+
1919
+ model NativeAppBuild {
1920
+ id String @id @default(cuid())
1921
+ tenantId String
1922
+ projectId String
1923
+ platform String
1924
+ fileFormat String
1925
+ fileName String?
1926
+ s3Path String
1927
+ packageName String?
1928
+ version String?
1929
+ uploadedAt DateTime @default(now())
1930
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1931
+
1932
+ @@index([tenantId, projectId])
1933
+ }
1934
+
1935
+ // ---- Performance Snapshots -------------------------------------------------
1936
+
1937
+ model PerformanceSnapshot {
1938
+ id String @id @default(cuid())
1939
+ tenantId String
1940
+ screenId String
1941
+ crawlJobId String
1942
+ lcp Float?
1943
+ fcp Float?
1944
+ cls Float?
1945
+ ttfb Float?
1946
+ viewport String @default("DESKTOP")
1947
+ lighthousePerf Float?
1948
+ lighthouseA11y Float?
1949
+ lighthouseSEO Float?
1950
+ lighthouseBP Float?
1951
+ capturedAt DateTime @default(now())
1952
+
1953
+ @@index([tenantId, screenId])
1954
+ @@index([tenantId, crawlJobId])
1955
+ }
1956
+
1957
+ // ---- Project Environments --------------------------------------------------
1958
+
1959
+ model ProjectEnvironment {
1960
+ id String @id @default(cuid())
1961
+ tenantId String
1962
+ projectId String
1963
+ name String
1964
+ baseUrl String
1965
+ credentials_enc String?
1966
+ isDefault Boolean @default(false)
1967
+ createdAt DateTime @default(now())
1968
+ updatedAt DateTime @updatedAt
1969
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1970
+
1971
+ @@index([tenantId, projectId])
1972
+ }
1973
+
1974
+ // ---- Project Members -------------------------------------------------------
1975
+
1976
+ model ProjectMember {
1977
+ id String @id @default(cuid())
1978
+ tenantId String
1979
+ projectId String
1980
+ userId String
1981
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
1982
+
1983
+ @@unique([projectId, userId])
1984
+ @@index([tenantId, userId])
1985
+ }
1986
+
1987
+ // ---- Comments --------------------------------------------------------------
1988
+
1989
+ model Comment {
1990
+ id String @id @default(cuid())
1991
+ tenantId String
1992
+ parentType String
1993
+ parentId String
1994
+ userId String?
1995
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
1996
+ body String
1997
+ createdAt DateTime @default(now())
1998
+ updatedAt DateTime @updatedAt
1999
+
2000
+ @@index([tenantId, parentType, parentId])
2001
+ }
2002
+
2003
+ // ---- Requirements ----------------------------------------------------------
2004
+
2005
+ model Requirement {
2006
+ id String @id @default(cuid())
2007
+ tenantId String
2008
+ projectId String
2009
+ reqId String
2010
+ title String
2011
+ actor String?
2012
+ type String @default("functional")
2013
+ priority String @default("HIGH")
2014
+ acceptanceCriteria Json @default("[]")
2015
+ businessRules Json @default("[]")
2016
+ relatedScreenHints Json @default("[]")
2017
+ sourceSection String?
2018
+ derived Boolean @default(false)
2019
+ status String @default("pending")
2020
+ source String @default("AI_GENERATED")
2021
+ // External tracker metadata (Jira): { url, issueType, status, epic, labels, components, links, subtasks }
2022
+ externalMeta Json?
2023
+ // Append-only history of content changes. Each entry: { version, changedAt, changedFields, snapshot }
2024
+ changeLog Json @default("[]")
2025
+ createdAt DateTime @default(now())
2026
+ updatedAt DateTime @updatedAt
2027
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2028
+ scenarios Scenario[]
2029
+
2030
+ @@unique([tenantId, projectId, reqId])
2031
+ @@index([tenantId, projectId])
2032
+ @@index([tenantId, projectId, priority])
2033
+ @@index([tenantId, projectId, status])
2034
+ @@index([tenantId, projectId, sourceSection])
2035
+ }
2036
+
2037
+ // ---- Scenarios --------------------------------------------------------------
2038
+
2039
+ model Scenario {
2040
+ id String @id @default(cuid())
2041
+ tenantId String
2042
+ projectId String
2043
+ requirementId String
2044
+ title String
2045
+ description String?
2046
+ gherkin String @db.Text
2047
+ status String @default("draft") // draft | approved | rejected
2048
+ version Int @default(1)
2049
+ source String @default("AI_GENERATED")
2050
+ createdAt DateTime @default(now())
2051
+ updatedAt DateTime @updatedAt
2052
+
2053
+ requirement Requirement @relation(fields: [requirementId], references: [id], onDelete: Cascade)
2054
+ testCases TestCase[]
2055
+
2056
+ @@index([tenantId, projectId])
2057
+ @@index([requirementId])
2058
+ }
2059
+
2060
+ // ---- Extension Captures ----------------------------------------------------
2061
+
2062
+ model ExtensionCapture {
2063
+ id String @id @default(cuid())
2064
+ tenantId String
2065
+ projectId String
2066
+ kind String
2067
+ s3Url String?
2068
+ entryCount Int @default(0)
2069
+ capturedAt DateTime @default(now())
2070
+ createdAt DateTime @default(now())
2071
+ metadata Json?
2072
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2073
+ interactions ExtensionInteraction[]
2074
+
2075
+ @@index([tenantId, projectId])
2076
+ @@index([tenantId, projectId, kind])
2077
+ @@index([tenantId, projectId, capturedAt])
2078
+ }
2079
+
2080
+ model ExtensionInteraction {
2081
+ id String @id @default(cuid())
2082
+ tenantId String
2083
+ captureId String
2084
+ type String
2085
+ selector String?
2086
+ xpath String?
2087
+ tagName String?
2088
+ value String?
2089
+ url String?
2090
+ timestamp BigInt?
2091
+ metadata Json?
2092
+ capture ExtensionCapture @relation(fields: [captureId], references: [id], onDelete: Cascade)
2093
+
2094
+ @@index([tenantId, captureId])
2095
+ @@index([captureId])
2096
+ }
2097
+
2098
+ model ExtensionError {
2099
+ id String @id @default(cuid())
2100
+ tenantId String
2101
+ projectId String
2102
+ pageUrl String?
2103
+ kind String
2104
+ level String @default("error")
2105
+ text String
2106
+ url String?
2107
+ lineNumber Int?
2108
+ columnNumber Int?
2109
+ stackTrace Json?
2110
+ source String?
2111
+ capturedAt DateTime @default(now())
2112
+ createdAt DateTime @default(now())
2113
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2114
+
2115
+ @@index([tenantId, projectId])
2116
+ @@index([tenantId, projectId, pageUrl])
2117
+ @@index([tenantId, projectId, kind])
2118
+ @@index([tenantId, projectId, level])
2119
+ @@index([tenantId, projectId, capturedAt])
2120
+ }
2121
+
2122
+ // ---- Billing / Plan Enforcement --------------------------------------------
2123
+
2124
+ model TenantPlanConfig {
2125
+ tier String @id
2126
+ maxProjects Int
2127
+ maxMembers Int
2128
+ maxSessions Int
2129
+ maxCrawlsPerMonth Int
2130
+ maxRunsPerMonth Int
2131
+ maxShards Int
2132
+ maxTokensPerMonth Int
2133
+ createdAt DateTime @default(now())
2134
+ updatedAt DateTime @updatedAt
2135
+ }
2136
+
2137
+ model TenantPlan {
2138
+ tenantId String @id
2139
+ tier String @default("TRIAL") // TRIAL | STARTER | GROWTH | ENTERPRISE
2140
+ stripeCustomerId String?
2141
+ stripeSubscriptionId String?
2142
+ trialEndsAt DateTime?
2143
+ maxProjects Int
2144
+ maxMembers Int
2145
+ maxSessions Int
2146
+ maxCrawlsPerMonth Int
2147
+ maxRunsPerMonth Int
2148
+ maxShards Int
2149
+ maxTokensPerMonth Int
2150
+ currentCrawls Int @default(0)
2151
+ currentRuns Int @default(0)
2152
+ lastResetAt DateTime @default(now())
2153
+ updatedAt DateTime @updatedAt
2154
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2155
+ }
2156
+
2157
+ // ---- Platform Admin --------------------------------------------------------
2158
+
2159
+ model AdminUser {
2160
+ id String @id @default(cuid())
2161
+ email String @unique
2162
+ passwordHash String
2163
+ name String?
2164
+ role String @default("SUPER_ADMIN") // SUPER_ADMIN | SUPPORT
2165
+ mfaEnabled Boolean @default(false)
2166
+ mfaSecret_enc String?
2167
+ mfaVerifiedAt DateTime?
2168
+ lastLoginAt DateTime? // updated on every successful login (F-009)
2169
+ failedLoginCount Int @default(0) // consecutive failures since last success; reset on login (F-010)
2170
+ lastFailedLoginAt DateTime? // timestamp of most recent failed attempt (F-010)
2171
+ createdAt DateTime @default(now())
2172
+ updatedAt DateTime @default(now())
2173
+ mfaRecoveryCodes AdminMfaRecoveryCode[]
2174
+ mfaChallenges AdminMfaChallenge[]
2175
+ authSessions AdminAuthSession[]
2176
+ }
2177
+
2178
+ model AdminAuthSession {
2179
+ id String @id // JWT jti claim — set from crypto.randomUUID() in mintSessionToken()
2180
+ adminId String
2181
+ userAgent String?
2182
+ ipAddress String?
2183
+ createdAt DateTime @default(now())
2184
+ lastSeenAt DateTime @default(now())
2185
+ revokedAt DateTime?
2186
+ admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
2187
+
2188
+ @@index([adminId])
2189
+ @@index([lastSeenAt])
2190
+ }
2191
+
2192
+ model AdminMfaRecoveryCode {
2193
+ id String @id @default(cuid())
2194
+ adminId String
2195
+ codeHash String
2196
+ usedAt DateTime?
2197
+ createdAt DateTime @default(now())
2198
+ admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
2199
+
2200
+ @@index([adminId])
2201
+ }
2202
+
2203
+ model AdminMfaChallenge {
2204
+ id String @id
2205
+ adminId String
2206
+ purpose String
2207
+ expiresAt DateTime
2208
+ usedAt DateTime?
2209
+ createdAt DateTime @default(now())
2210
+ admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
2211
+
2212
+ @@index([adminId, purpose, usedAt])
2213
+ @@index([expiresAt])
2214
+ }
2215
+
2216
+ // ---- DB-driven built-in role permissions -----------------------------------
2217
+ // Stores the permission sets for the 6 built-in roles (OWNER, ADMIN, etc.).
2218
+ // Seeded on first deploy; editable by platform super-admins at runtime.
2219
+ // The middleware reads from here (with a 60s in-memory cache) instead of
2220
+ // the hardcoded ROLE_PERMISSIONS map in @detiq/core.
2221
+ model RoleConfig {
2222
+ roleName String @id // 'OWNER' | 'ADMIN' | 'QA_LEAD' | 'TESTER' | 'DEVELOPER' | 'VIEWER'
2223
+ rank Int @unique // 0=VIEWER … 5=OWNER — used for privilege-escalation checks
2224
+ permissions String[] // array of Permission strings
2225
+ updatedAt DateTime @updatedAt
2226
+
2227
+ @@map("role_configs")
2228
+ }
2229
+
2230
+ // ---- Slack Integration (OAuth Bot token per workspace) ----------------------
2231
+
2232
+ model SlackConnection {
2233
+ id String @id @default(cuid())
2234
+ tenantId String @unique
2235
+ teamId String
2236
+ teamName String
2237
+ channelId String
2238
+ channelName String
2239
+ accessToken_enc String // AES-256-GCM encrypted Bot token
2240
+ installedBy String // userId of the member who ran /slack/install
2241
+ createdAt DateTime @default(now())
2242
+ updatedAt DateTime @updatedAt
2243
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2244
+ }
2245
+
2246
+ // ---- Microsoft Teams Integration (Power Automate webhook) ------------------
2247
+
2248
+ model TeamsConnection {
2249
+ id String @id @default(cuid())
2250
+ tenantId String @unique
2251
+ webhookUrl String // Power Automate incoming webhook URL
2252
+ channelName String?
2253
+ installedBy String
2254
+ createdAt DateTime @default(now())
2255
+ updatedAt DateTime @updatedAt
2256
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2257
+ }
2258
+
2259
+ // ---- SILO Isolation Config (Phase 3.2) -------------------------------------
2260
+
2261
+ model TenantSiloConfig {
2262
+ tenantId String @id
2263
+ connectionString_enc String
2264
+ encryptionKeyRef String @default("default")
2265
+ provisionedAt DateTime @default(now())
2266
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2267
+ }
2268
+
2269
+ // ---- Data-Driven Test Execution (Phase 4.3) ---------------------------------
2270
+
2271
+ model TestDataSet {
2272
+ id String @id @default(cuid())
2273
+ tenantId String
2274
+ projectId String
2275
+ name String
2276
+ // SYNTHETIC | PRODUCTION | STAGING | SANDBOX | DUMMY | IMPORTED
2277
+ classification String @default("SYNTHETIC")
2278
+ // Free-form environment label e.g. "Production", "UAT", "QA-01"
2279
+ environment String?
2280
+ rows TestDataRow[]
2281
+ testCases TestCase[]
2282
+ createdAt DateTime @default(now())
2283
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2284
+
2285
+ @@index([tenantId, projectId])
2286
+ @@index([tenantId, classification])
2287
+ }
2288
+
2289
+ model TestDataRow {
2290
+ id String @id @default(cuid())
2291
+ tenantId String
2292
+ dataSetId String
2293
+ rowIndex Int
2294
+ data Json // { fieldName: value } — e.g. { "username": "alice", "password": "pass1" }
2295
+ dataSet TestDataSet @relation(fields: [dataSetId], references: [id], onDelete: Cascade)
2296
+
2297
+ @@unique([dataSetId, rowIndex])
2298
+ @@index([tenantId])
2299
+ }
2300
+
2301
+ // ---- Framework Modernization — Phase 2: GitHub Repo Integration -------------
2302
+
2303
+ // One row per connected GitHub automation repo per project.
2304
+ model GitHubRepoConnection {
2305
+ id String @id @default(cuid())
2306
+ tenantId String
2307
+ projectId String
2308
+ createdBy String // userId who connected the repo
2309
+ // "owner/repo"
2310
+ repoFullName String
2311
+ defaultBranch String @default("main")
2312
+ // AES-256-GCM: "<iv>:<authTag>:<ciphertext>"
2313
+ tokenEnc String
2314
+ // ACTIVE | NEEDS_REAUTH | ERROR | DISCONNECTED
2315
+ status String @default("ACTIVE")
2316
+ // PAT | GITHUB_APP | FINE_GRAINED_PAT
2317
+ authMethod String @default("PAT")
2318
+ // Detected from config files: playwright-ts | cypress-ts | pytest | junit | etc.
2319
+ detectedFramework String?
2320
+ configFilePath String? // e.g. "playwright.config.ts"
2321
+ testGlob String? // e.g. "**/*.spec.ts"
2322
+ // Regex to extract test IDs from test titles
2323
+ testIdPattern String @default("TC-[A-Z]+-\\d+")
2324
+ // JSON: extracted style conventions for aligned script generation (Phase 3)
2325
+ styleProfile Json?
2326
+ // Compressed JSON tree from last scan (stored as base64 gzip)
2327
+ treeSnapshot String? @db.Text
2328
+ // IDLE | SCANNING | DONE | FAILED
2329
+ scanStatus String @default("IDLE")
2330
+ scanError String?
2331
+ testFileCount Int @default(0)
2332
+ extractedIdCount Int @default(0)
2333
+ lastScannedAt DateTime?
2334
+ lastSyncAt DateTime?
2335
+ // JSON array of PageObjectSummary — populated during repo scan
2336
+ pageObjectInventory Json?
2337
+ // GitHub App installation ID (for GitHub App auth)
2338
+ installationId Int?
2339
+ createdAt DateTime @default(now())
2340
+ updatedAt DateTime @updatedAt
2341
+
2342
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2343
+ repoTestFiles RepoTestFile[]
2344
+ coverageMappings CoverageMapping[]
2345
+ gitHubPRs GitHubPR[]
2346
+
2347
+ @@index([tenantId, projectId])
2348
+ @@index([tenantId, status])
2349
+ }
2350
+
2351
+ // One row per test file discovered in the connected repo.
2352
+ model RepoTestFile {
2353
+ id String @id @default(cuid())
2354
+ tenantId String
2355
+ projectId String
2356
+ repoConnectionId String
2357
+ // Repo-root-relative path, forward slashes, no leading slash
2358
+ filePath String
2359
+ framework String // playwright-ts | cypress-ts | pytest | etc.
2360
+ // string[] of extracted test IDs / names
2361
+ extractedIds Json @default("[]")
2362
+ // First 2000 chars — used for style extraction
2363
+ rawSample String? @db.Text
2364
+ lastScannedAt DateTime @default(now())
2365
+
2366
+ repoConnection GitHubRepoConnection @relation(fields: [repoConnectionId], references: [id], onDelete: Cascade)
2367
+
2368
+ @@unique([repoConnectionId, filePath])
2369
+ @@index([tenantId, projectId])
2370
+ @@index([repoConnectionId])
2371
+ }
2372
+
2373
+ // One row per TestCase — tracks mapping to a repo script file.
2374
+ model CoverageMapping {
2375
+ id String @id @default(cuid())
2376
+ tenantId String
2377
+ projectId String
2378
+ testCaseId String @unique
2379
+ repoConnectionId String
2380
+ // MAPPED | UNMAPPED | AMBIGUOUS
2381
+ status String @default("UNMAPPED")
2382
+ matchedFilePath String? // relative path in repo
2383
+ scriptTestName String? // matched test name inside the file
2384
+ lineNumber Int?
2385
+ // 0.0–1.0
2386
+ confidence Float?
2387
+ requirementScore Float? // cross-encoder quality score: how well TC covers its linked requirement (0.0–1.0)
2388
+ suggestedReqId String? // for UNMAPPED TCs: best-match requirement ID from cross-encoder
2389
+ suggestedReqScore Float? // cross-encoder confidence for the suggestion (0.0–1.0)
2390
+ // EXACT | NORMALIZED | FUZZY | AI | MANUAL
2391
+ matchMethod String?
2392
+ manualOverride Boolean @default(false)
2393
+ confirmedBy String? // userId
2394
+ confirmedAt DateTime?
2395
+ createdAt DateTime @default(now())
2396
+ updatedAt DateTime @updatedAt
2397
+
2398
+ testCase TestCase @relation(fields: [testCaseId], references: [id], onDelete: Cascade)
2399
+ repoConnection GitHubRepoConnection @relation(fields: [repoConnectionId], references: [id], onDelete: Cascade)
2400
+
2401
+ @@index([tenantId, projectId])
2402
+ @@index([repoConnectionId])
2403
+ @@index([status, projectId])
2404
+ }
2405
+
2406
+ // Time-series snapshot of coverage % for the dashboard trend chart.
2407
+ model CoverageSnapshot {
2408
+ id String @id @default(cuid())
2409
+ tenantId String
2410
+ projectId String
2411
+ totalCases Int
2412
+ mappedCases Int
2413
+ unmappedCases Int
2414
+ ambiguousCases Int
2415
+ coveragePercent Float
2416
+ snapshotAt DateTime @default(now())
2417
+
2418
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2419
+
2420
+ @@index([tenantId, projectId, snapshotAt])
2421
+ }
2422
+
2423
+ // Tracks GitHub PRs created by ZeTa (one per automated push batch).
2424
+ model GitHubPR {
2425
+ id String @id @default(cuid())
2426
+ tenantId String
2427
+ projectId String
2428
+ repoConnectionId String
2429
+ prNumber Int
2430
+ prUrl String
2431
+ branch String
2432
+ headSha String
2433
+ // OPEN | MERGED | CLOSED
2434
+ status String @default("OPEN")
2435
+ // Which crawl job triggered this PR (optional)
2436
+ crawlJobId String?
2437
+ // How many scripts are in this PR
2438
+ scriptCount Int @default(0)
2439
+ // ZeTa-generated PR body text
2440
+ prBody String? @db.Text
2441
+ createdAt DateTime @default(now())
2442
+ updatedAt DateTime @updatedAt
2443
+
2444
+ repoConnection GitHubRepoConnection @relation(fields: [repoConnectionId], references: [id], onDelete: Cascade)
2445
+
2446
+ @@index([tenantId, projectId])
2447
+ @@index([repoConnectionId])
2448
+ @@index([tenantId, status])
2449
+ }
2450
+
2451
+ // ---- IDE Push Tokens (one-time, 60-second OTP for VS Code / JetBrains) ------
2452
+ // The web app mints a token, puts only its hash in DB, and hands the plain token
2453
+ // to the IDE extension via the vscode:// URI. The extension exchanges it here
2454
+ // (GET /ide-push/:token) which validates, marks as used (single-use), and returns
2455
+ // the script payload — without the long-lived AgentToken ever touching a URI.
2456
+ model IdePushToken {
2457
+ id String @id @default(cuid())
2458
+ tenantId String
2459
+ projectId String
2460
+ suiteId String?
2461
+ packId String? // for pack-based pushes (regression/smoke/sanity)
2462
+ scriptIds String? // JSON array of TestScript IDs for direct selection
2463
+ language String @default("playwright-ts")
2464
+ tokenHash String @unique
2465
+ expiresAt DateTime
2466
+ usedAt DateTime?
2467
+ createdAt DateTime @default(now())
2468
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2469
+
2470
+ @@index([tenantId, projectId])
2471
+ @@index([tokenHash])
2472
+ @@index([expiresAt])
2473
+ }
2474
+
2475
+ // ---- LLM pricing table (replaces hardcoded PRICING constant in token-pricing.ts) ---------
2476
+ // Platform admin can update prices via UI without a code deploy.
2477
+ // modelKey format: "provider/model" e.g. "anthropic/claude-sonnet-4-6".
2478
+ model LlmPricing {
2479
+ id String @id @default(cuid())
2480
+ modelKey String @unique
2481
+ inputPer1M Float
2482
+ outputPer1M Float
2483
+ cacheReadPer1M Float @default(0)
2484
+ cacheWritePer1M Float @default(0)
2485
+ effectiveDate DateTime @default(now())
2486
+ createdAt DateTime @default(now())
2487
+ updatedAt DateTime @updatedAt
2488
+
2489
+ @@index([modelKey])
2490
+ }
2491
+
2492
+ // ---- Database-seeded agent prompts (replaces hardcoded GLOBAL_RULES) ---------
2493
+ // Loaded by packages/core/src/agent-prompts.ts with 60s in-process cache.
2494
+ // agentKey: e.g. "test-case-global-rules", passKey: optional per-pass override key.
2495
+ model AgentPrompt {
2496
+ id String @id @default(cuid())
2497
+ name String
2498
+ agentKey String
2499
+ passKey String?
2500
+ body String @db.Text
2501
+ version Int @default(1)
2502
+ isActive Boolean @default(true)
2503
+ createdAt DateTime @default(now())
2504
+ updatedAt DateTime @updatedAt
2505
+
2506
+ @@unique([agentKey, passKey, isActive])
2507
+ @@index([agentKey, isActive])
2508
+ }
2509
+
2510
+ // Per-tenant override of platform-wide AgentPrompt.
2511
+ // Resolution order: WorkspaceAgentPrompt (tenant) → AgentPrompt (platform) → hardcoded fallback.
2512
+ model WorkspaceAgentPrompt {
2513
+ id String @id @default(cuid())
2514
+ tenantId String
2515
+ agentKey String
2516
+ passKey String?
2517
+ name String
2518
+ body String @db.Text
2519
+ isActive Boolean @default(true)
2520
+ createdAt DateTime @default(now())
2521
+ updatedAt DateTime @updatedAt
2522
+ updatedBy String?
2523
+
2524
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2525
+
2526
+ @@unique([tenantId, agentKey, passKey])
2527
+ @@index([tenantId, agentKey, isActive])
2528
+ }
2529
+
2530
+ // ---- Admin Config Change History -----------------------------------------
2531
+ // Immutable audit trail for every SystemConfig upsert and delete.
2532
+ // Sensitive field values stored as MASK ("••••••••") — never raw.
2533
+
2534
+ model SystemConfigHistory {
2535
+ id String @id @default(cuid())
2536
+ configKey String // e.g. "smtp.password"
2537
+ category String
2538
+ fieldKey String
2539
+ oldValue String? // null on first-ever create; MASK for sensitive
2540
+ newValue String? // null on delete; MASK for sensitive
2541
+ action String // "upsert" | "delete"
2542
+ sensitive Boolean @default(false)
2543
+ adminId String?
2544
+ adminEmail String?
2545
+ changedAt DateTime @default(now())
2546
+
2547
+ @@index([configKey, changedAt(sort: Desc)])
2548
+ @@index([category, changedAt(sort: Desc)])
2549
+ @@index([adminId])
2550
+ }
2551
+
2552
+ // ---- Platform Prompt Change History --------------------------------------
2553
+ // Captures every body edit to AgentPrompt platform records (version++ trigger).
2554
+
2555
+ model AgentPromptHistory {
2556
+ id String @id @default(cuid())
2557
+ agentPromptId String
2558
+ agentKey String
2559
+ passKey String?
2560
+ oldBody String @db.Text
2561
+ newBody String @db.Text
2562
+ version Int
2563
+ adminId String?
2564
+ adminEmail String?
2565
+ changedAt DateTime @default(now())
2566
+
2567
+ @@index([agentPromptId, changedAt(sort: Desc)])
2568
+ @@index([agentKey, changedAt(sort: Desc)])
2569
+ }
2570
+
2571
+ // ---- Tenant Prompt Override History --------------------------------------
2572
+ // Tracks every upsert/delete of WorkspaceAgentPrompt per tenant.
2573
+
2574
+ model WorkspaceAgentPromptHistory {
2575
+ id String @id @default(cuid())
2576
+ workspaceAgentPromptId String? // null when record was deleted
2577
+ tenantId String
2578
+ agentKey String
2579
+ passKey String?
2580
+ oldBody String? @db.Text // null = first creation
2581
+ newBody String? @db.Text // null = deletion
2582
+ action String // "upsert" | "delete"
2583
+ updatedBy String?
2584
+ changedAt DateTime @default(now())
2585
+
2586
+ @@index([tenantId, agentKey, changedAt(sort: Desc)])
2587
+ @@index([workspaceAgentPromptId, changedAt(sort: Desc)])
2588
+ }
2589
+
2590
+ // ==========================================
2591
+ // Learning / Onboarding Models
2592
+ // ==========================================
2593
+
2594
+ model UserContext {
2595
+ id String @id @default(cuid())
2596
+ userId String @unique
2597
+ dailyTasks String[] @default([])
2598
+ roles String[] @default([])
2599
+ responsibilities String[] @default([])
2600
+ toolsUsed String[] @default([])
2601
+ lastExtractedAt DateTime?
2602
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2603
+ }
2604
+
2605
+ model LearningJourney {
2606
+ id String @id @default(cuid())
2607
+ userId String @unique
2608
+ tenantId String
2609
+ level Int @default(1)
2610
+ status String @default("ACTIVE")
2611
+ tasks LearningTask[]
2612
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
2613
+
2614
+ @@index([tenantId])
2615
+ }
2616
+
2617
+ model LearningTask {
2618
+ id String @id @default(cuid())
2619
+ journeyId String
2620
+ title String
2621
+ description String @db.Text
2622
+ requiredArtifacts String[] @default([])
2623
+ status String @default("ASSIGNED")
2624
+ createdAt DateTime @default(now())
2625
+ submissions TaskSubmission[]
2626
+ journey LearningJourney @relation(fields: [journeyId], references: [id], onDelete: Cascade)
2627
+
2628
+ @@index([journeyId])
2629
+ }
2630
+
2631
+ model TaskSubmission {
2632
+ id String @id @default(cuid())
2633
+ taskId String
2634
+ textContent String? @db.Text
2635
+ contentUrl String?
2636
+ submittedAt DateTime @default(now())
2637
+ task LearningTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
2638
+
2639
+ @@index([taskId])
2640
+ }
2641
+
2642
+ // Navigation Knowledge Graph: hyperlink edges discovered between screens during crawl.
2643
+ // linkType: NAV_LINK | CONTENT_LINK | FORM_ACTION | BUTTON_LINK | DATA_LINK
2644
+ // source: DOM | AI | INTERACTIVE | SITEMAP
2645
+ model DiscoveredLink {
2646
+ id String @id @default(cuid())
2647
+ tenantId String
2648
+ projectId String
2649
+ fromScreenId String
2650
+ toScreenId String?
2651
+ href String
2652
+ text String?
2653
+ landmark String? // nav | header | footer | main | sidebar | aside
2654
+ linkType String @default("NAV_LINK")
2655
+ source String @default("DOM")
2656
+ confidence Float @default(1.0)
2657
+ jobId String?
2658
+ createdAt DateTime @default(now())
2659
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2660
+ fromScreen Screen @relation("DiscoveredLinkFromScreen", fields: [fromScreenId], references: [id], onDelete: Cascade)
2661
+ toScreen Screen? @relation("DiscoveredLinkToScreen", fields: [toScreenId], references: [id], onDelete: SetNull)
2662
+
2663
+ @@unique([fromScreenId, href])
2664
+ @@index([tenantId, projectId])
2665
+ @@index([tenantId, projectId, jobId])
2666
+ @@index([toScreenId])
2667
+ @@index([href])
2668
+ }
2669
+
2670
+ // ---- API Test Run Persistence (GAP-003) ------------------------------------
2671
+ // Persists newman / agent API test run results beyond the 5-min in-memory TTL.
2672
+
2673
+ model ApiTestRunHistory {
2674
+ id String @id @default(cuid())
2675
+ tenantId String
2676
+ projectId String
2677
+ status String // queued | running | passed | failed | error
2678
+ summary Json?
2679
+ source String? // newman | agent
2680
+ createdAt DateTime @default(now())
2681
+ completedAt DateTime?
2682
+ requests ApiTestRunRequest[]
2683
+
2684
+ @@index([tenantId, projectId])
2685
+ @@index([tenantId, createdAt])
2686
+ }
2687
+
2688
+ model ApiTestRunRequest {
2689
+ id String @id @default(cuid())
2690
+ runHistoryId String
2691
+ run ApiTestRunHistory @relation(fields: [runHistoryId], references: [id], onDelete: Cascade)
2692
+ requestName String
2693
+ status String // pending | running | passed | failed | skipped | error
2694
+ statusCode Int?
2695
+ durationMs Int?
2696
+ assertions Json?
2697
+ requestUrl String?
2698
+ error String?
2699
+
2700
+ @@index([runHistoryId])
2701
+ }
2702
+
2703
+ // ---- Custom Assertion Builder (GAP-009) ------------------------------------
2704
+ // Stores per-test-case custom assertions for the assertion builder UI.
2705
+
2706
+ model TestCaseAssertion {
2707
+ id String @id @default(cuid())
2708
+ testCaseId String
2709
+ field String
2710
+ operator String // eq | ne | contains | gt | lt | gte | lte | schema | regex | exists
2711
+ expectedValue String?
2712
+ createdAt DateTime @default(now())
2713
+
2714
+ @@index([testCaseId])
2715
+ }
2716
+
2717
+ // ---- CSV/JSON Data-Driven Testing — Skill File Link (GAP-017) -------------
2718
+ // Links a test case to a TEST_DATA skill file for multi-row iteration via
2719
+ // Newman --iteration-data (CSV/JSON). Separate from TestDataSet (DB rows)
2720
+ // to support file-based data sources without DB import.
2721
+
2722
+ model TestCaseDataLink {
2723
+ id String @id @default(cuid())
2724
+ testCaseId String @unique
2725
+ skillFileId String // ID of the TEST_DATA skill file
2726
+ dataFormat String @default("json") // json | csv
2727
+ iterateAll Boolean @default(true)
2728
+ createdAt DateTime @default(now())
2729
+
2730
+ @@index([testCaseId])
2731
+ }
2732
+
2733
+ // ---- Context Intelligence Models (P1-P7) ------------------------------------
2734
+
2735
+ model ContextEnhancement {
2736
+ id String @id @default(cuid())
2737
+ tenantId String
2738
+ projectId String
2739
+ sourceType String
2740
+ sourceRef String?
2741
+ crawlJobId String?
2742
+ status String @default("PENDING")
2743
+ proposedChanges Json @default("[]")
2744
+ appliedAt DateTime?
2745
+ appliedBy String?
2746
+ rejectedAt DateTime?
2747
+ rejectedBy String?
2748
+ approvalRequestedAt DateTime?
2749
+ approvalRequestedBy String?
2750
+ assignedReviewerId String?
2751
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2752
+ extractedElements ExtractedElement[]
2753
+ enrichedTestCases TestCase[] @relation("TestCaseSourceEnhancement")
2754
+ createdAt DateTime @default(now())
2755
+ updatedAt DateTime @updatedAt
2756
+
2757
+ @@index([tenantId, projectId])
2758
+ @@index([tenantId, projectId, status])
2759
+ @@index([tenantId, projectId, sourceType])
2760
+ @@index([sourceRef])
2761
+ }
2762
+
2763
+ model ExtractedElement {
2764
+ id String @id @default(cuid())
2765
+ tenantId String
2766
+ projectId String
2767
+ sourceEnhancementId String
2768
+ figmaComponentId String?
2769
+ name String?
2770
+ role String?
2771
+ selector String?
2772
+ confidence Float @default(0.5)
2773
+ boundingBox Json?
2774
+ extractedFrom String
2775
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2776
+ sourceEnhancement ContextEnhancement @relation(fields: [sourceEnhancementId], references: [id], onDelete: Cascade)
2777
+ figmaComponent FigmaComponent? @relation(fields: [figmaComponentId], references: [id], onDelete: SetNull)
2778
+ createdAt DateTime @default(now())
2779
+
2780
+ @@index([tenantId, projectId])
2781
+ @@index([sourceEnhancementId])
2782
+ }
2783
+
2784
+ model ContextSourceApproval {
2785
+ id String @id @default(cuid())
2786
+ tenantId String
2787
+ projectId String
2788
+ sourceType String
2789
+ approvedBy String?
2790
+ approvedAt DateTime?
2791
+ jobId String?
2792
+ diffCount Int @default(0)
2793
+ diffApprovedCount Int @default(0)
2794
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2795
+ createdAt DateTime @default(now())
2796
+ updatedAt DateTime @updatedAt
2797
+
2798
+ @@index([tenantId, projectId])
2799
+ @@index([tenantId, projectId, sourceType])
2800
+ }
2801
+
2802
+ model FigmaComponent {
2803
+ id String @id @default(cuid())
2804
+ tenantId String
2805
+ projectId String
2806
+ screenId String?
2807
+ figmaNodeId String
2808
+ name String
2809
+ componentType String @default("COMPONENT")
2810
+ description String?
2811
+ interactions Json?
2812
+ destinationScreenId String?
2813
+ suggestedLocator String?
2814
+ suggestedTestId String?
2815
+ boundingBox Json?
2816
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2817
+ screen Screen? @relation(fields: [screenId], references: [id], onDelete: SetNull)
2818
+ extractedElements ExtractedElement[]
2819
+ createdAt DateTime @default(now())
2820
+ updatedAt DateTime @updatedAt
2821
+
2822
+ @@index([tenantId, projectId])
2823
+ @@index([screenId])
2824
+ @@index([figmaNodeId])
2825
+ @@unique([projectId, figmaNodeId])
2826
+ }
2827
+
2828
+ model CrawlDiff {
2829
+ id String @id @default(cuid())
2830
+ tenantId String
2831
+ projectId String
2832
+ fromJobId String?
2833
+ toJobId String
2834
+ fromScreenId String?
2835
+ toScreenId String?
2836
+ diffType String
2837
+ severity String @default("MINOR")
2838
+ description String?
2839
+ beforeSnapshot Json?
2840
+ afterSnapshot Json?
2841
+ affectedTestCaseIds Json @default("[]")
2842
+ status String @default("PENDING")
2843
+ reviewedBy String?
2844
+ reviewedAt DateTime?
2845
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2846
+ fromScreen Screen? @relation("CrawlDiffFromScreen", fields: [fromScreenId], references: [id], onDelete: SetNull)
2847
+ toScreen Screen? @relation("CrawlDiffToScreen", fields: [toScreenId], references: [id], onDelete: SetNull)
2848
+ staleTestCases TestCase[] @relation("TestCaseCrawlDiff")
2849
+ createdAt DateTime @default(now())
2850
+ updatedAt DateTime @updatedAt
2851
+
2852
+ @@index([tenantId, projectId])
2853
+ @@index([tenantId, projectId, status])
2854
+ @@index([toJobId])
2855
+ @@index([fromJobId])
2856
+ @@index([severity, status])
2857
+ }
2858
+
2859
+ model BugSignal {
2860
+ id String @id @default(cuid())
2861
+ tenantId String
2862
+ projectId String
2863
+ signalType String
2864
+ severity String @default("MEDIUM")
2865
+ title String
2866
+ description String?
2867
+ sourceRef Json @default("{}")
2868
+ affectedTestCaseIds Json @default("[]")
2869
+ affectedRequirementIds Json @default("[]")
2870
+ status String @default("OPEN")
2871
+ resolution String?
2872
+ assignedTo String?
2873
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2874
+ testResults TestResult[] @relation("BugSignalTestResults")
2875
+ visualDiffs VisualDiff[] @relation("BugSignalVisualDiffs")
2876
+ relatedTestCases TestCase[] @relation("BugSignalAffectedTestCases")
2877
+ createdAt DateTime @default(now())
2878
+ updatedAt DateTime @updatedAt
2879
+
2880
+ @@index([tenantId, projectId])
2881
+ @@index([tenantId, projectId, status])
2882
+ @@index([tenantId, projectId, signalType])
2883
+ @@index([tenantId, projectId, severity])
2884
+ @@index([assignedTo])
2885
+ }
2886
+
2887
+ model RequirementCoverage {
2888
+ id String @id @default(cuid())
2889
+ tenantId String
2890
+ projectId String
2891
+ requirementId String
2892
+ testCaseIds Json @default("[]")
2893
+ coverageScore Float @default(0)
2894
+ uncoveredAcIds Json @default("[]")
2895
+ computedAt DateTime @default(now())
2896
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
2897
+
2898
+ @@unique([projectId, requirementId])
2899
+ @@index([tenantId, projectId])
2900
+ @@index([tenantId, projectId, coverageScore])
2901
+ }