@derwinjs/db 0.1.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 (102) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +79 -0
  3. package/dist/agent-findings-ingestor.d.ts +40 -0
  4. package/dist/agent-findings-ingestor.d.ts.map +1 -0
  5. package/dist/agent-findings-ingestor.js +154 -0
  6. package/dist/agent-findings-ingestor.js.map +1 -0
  7. package/dist/classification-trust-store.d.ts +31 -0
  8. package/dist/classification-trust-store.d.ts.map +1 -0
  9. package/dist/classification-trust-store.js +154 -0
  10. package/dist/classification-trust-store.js.map +1 -0
  11. package/dist/coverage-gap-reporter.d.ts +35 -0
  12. package/dist/coverage-gap-reporter.d.ts.map +1 -0
  13. package/dist/coverage-gap-reporter.js +84 -0
  14. package/dist/coverage-gap-reporter.js.map +1 -0
  15. package/dist/fix-policy.d.ts +46 -0
  16. package/dist/fix-policy.d.ts.map +1 -0
  17. package/dist/fix-policy.js +162 -0
  18. package/dist/fix-policy.js.map +1 -0
  19. package/dist/index.d.ts +27 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +53 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/learning-health-reporter.d.ts +37 -0
  24. package/dist/learning-health-reporter.d.ts.map +1 -0
  25. package/dist/learning-health-reporter.js +141 -0
  26. package/dist/learning-health-reporter.js.map +1 -0
  27. package/dist/prisma.d.ts +31 -0
  28. package/dist/prisma.d.ts.map +1 -0
  29. package/dist/prisma.js +31 -0
  30. package/dist/prisma.js.map +1 -0
  31. package/dist/qa-fix-attempt-store.d.ts +28 -0
  32. package/dist/qa-fix-attempt-store.d.ts.map +1 -0
  33. package/dist/qa-fix-attempt-store.js +258 -0
  34. package/dist/qa-fix-attempt-store.js.map +1 -0
  35. package/dist/qa-pattern-store.d.ts +32 -0
  36. package/dist/qa-pattern-store.d.ts.map +1 -0
  37. package/dist/qa-pattern-store.js +123 -0
  38. package/dist/qa-pattern-store.js.map +1 -0
  39. package/dist/qa-revert-store.d.ts +24 -0
  40. package/dist/qa-revert-store.d.ts.map +1 -0
  41. package/dist/qa-revert-store.js +139 -0
  42. package/dist/qa-revert-store.js.map +1 -0
  43. package/dist/qa-run-store.d.ts +46 -0
  44. package/dist/qa-run-store.d.ts.map +1 -0
  45. package/dist/qa-run-store.js +201 -0
  46. package/dist/qa-run-store.js.map +1 -0
  47. package/dist/qa-ticket-store.d.ts +35 -0
  48. package/dist/qa-ticket-store.d.ts.map +1 -0
  49. package/dist/qa-ticket-store.js +293 -0
  50. package/dist/qa-ticket-store.js.map +1 -0
  51. package/dist/qa-uniformity-store.d.ts +41 -0
  52. package/dist/qa-uniformity-store.d.ts.map +1 -0
  53. package/dist/qa-uniformity-store.js +288 -0
  54. package/dist/qa-uniformity-store.js.map +1 -0
  55. package/dist/scripts/smoke-auto-fix.d.ts +37 -0
  56. package/dist/scripts/smoke-auto-fix.d.ts.map +1 -0
  57. package/dist/scripts/smoke-auto-fix.js +270 -0
  58. package/dist/scripts/smoke-auto-fix.js.map +1 -0
  59. package/dist/scripts/smoke-learning-loop.d.ts +21 -0
  60. package/dist/scripts/smoke-learning-loop.d.ts.map +1 -0
  61. package/dist/scripts/smoke-learning-loop.js +375 -0
  62. package/dist/scripts/smoke-learning-loop.js.map +1 -0
  63. package/dist/scripts/smoke-orchestration.d.ts +35 -0
  64. package/dist/scripts/smoke-orchestration.d.ts.map +1 -0
  65. package/dist/scripts/smoke-orchestration.js +215 -0
  66. package/dist/scripts/smoke-orchestration.js.map +1 -0
  67. package/dist/scripts/smoke-qa-ticket-store.d.ts +18 -0
  68. package/dist/scripts/smoke-qa-ticket-store.d.ts.map +1 -0
  69. package/dist/scripts/smoke-qa-ticket-store.js +233 -0
  70. package/dist/scripts/smoke-qa-ticket-store.js.map +1 -0
  71. package/package.json +69 -0
  72. package/prisma/migrations/20260501165631_init/migration.sql +407 -0
  73. package/prisma/migrations/20260503051425_0002_qap018b_qaticket_crosslink_fields/migration.sql +6 -0
  74. package/prisma/migrations/20260504231316_add_project_repofullname_and_webhooksecret/migration.sql +12 -0
  75. package/prisma/migrations/20260504232851_add_qaticket_resolvedby/migration.sql +2 -0
  76. package/prisma/migrations/20260505042646_add_qapattern_qarevert/migration.sql +77 -0
  77. package/prisma/migrations/20260505055826_add_qauniformity_and_agent_trigger/migration.sql +35 -0
  78. package/prisma/migrations/migration_lock.toml +3 -0
  79. package/prisma/schema.prisma +748 -0
  80. package/prisma/seed.ts +181 -0
  81. package/prisma-client/default.d.ts +1 -0
  82. package/prisma-client/default.js +1 -0
  83. package/prisma-client/edge.d.ts +1 -0
  84. package/prisma-client/edge.js +631 -0
  85. package/prisma-client/index-browser.js +615 -0
  86. package/prisma-client/index.d.ts +34509 -0
  87. package/prisma-client/index.js +660 -0
  88. package/prisma-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  89. package/prisma-client/libquery_engine-linux-arm64-openssl-3.0.x.so.node +0 -0
  90. package/prisma-client/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  91. package/prisma-client/package.json +97 -0
  92. package/prisma-client/runtime/edge-esm.js +31 -0
  93. package/prisma-client/runtime/edge.js +31 -0
  94. package/prisma-client/runtime/index-browser.d.ts +365 -0
  95. package/prisma-client/runtime/index-browser.js +13 -0
  96. package/prisma-client/runtime/library.d.ts +3403 -0
  97. package/prisma-client/runtime/library.js +143 -0
  98. package/prisma-client/runtime/react-native.js +80 -0
  99. package/prisma-client/runtime/wasm.js +32 -0
  100. package/prisma-client/schema.prisma +748 -0
  101. package/prisma-client/wasm.d.ts +1 -0
  102. package/prisma-client/wasm.js +615 -0
@@ -0,0 +1,748 @@
1
+ // Derwin platform schema — Postgres + Prisma.
2
+ //
3
+ // 14 models, project-namespaced. Per ADR-0005 (Prisma + Postgres + project-
4
+ // namespaced schema). Reference: technical spec §3.
5
+ //
6
+ // Multi-tenancy convention: every project-scoped table has a `projectId String`
7
+ // FK to Project. Cross-project leakage is the leading regression risk; queries
8
+ // MUST filter on projectId. Per ADR-0005, the trust-mitigation gates are:
9
+ // 1. A lint or static check that flags Prisma queries missing projectId
10
+ // filters on tables that have one (added in Sprint 8 / risk-tier work).
11
+ // 2. A multi-project broken-fixture E2E that asserts isolation
12
+ // (added in Sprint 13 / QAP-130).
13
+ //
14
+ // pgvector convention: `embedding Bytes?` on RAGCorpus is the v1 storage type.
15
+ // QAP-076 (Sprint 7 — RAG corpus add) switches it to a pgvector `vector(1536)`
16
+ // column once the extension is enabled in QAP-036.
17
+
18
+ // Generator output is INSIDE the package so the generated client ships
19
+ // in the published tarball alongside dist/. Consumer code does
20
+ // `import { PrismaClient } from '@derwinjs/db'` (or '@derwinjs/db/client') —
21
+ // see packages/db/src/index.ts re-exports. This is required for
22
+ // multi-tenant correctness: each Derwin consumer (Lifeline, Side Piece,
23
+ // Bolt, ...) already has its own @prisma/client generated against its
24
+ // own schema; @derwinjs/db can't piggyback on the consumer's @prisma/client
25
+ // or types break (Lifeline's PrismaClient doesn't have qAUniformity etc).
26
+ //
27
+ // binaryTargets ships explicit cross-platform binaries so the published
28
+ // tarball works regardless of where the publish workflow ran:
29
+ // - darwin-arm64: Apple Silicon Mac dev (the Derwin team's primary local)
30
+ // - rhel-openssl-3.0.x: x86 Linux on OpenSSL 3 (Vercel default, most CI)
31
+ // - linux-arm64-openssl-3.0.x: ARM Linux (AWS Graviton, etc.)
32
+ // "native" is intentionally omitted — relying on it makes the published
33
+ // tarball platform-dependent (whatever the publisher was running on).
34
+ // Intel Mac users (rare) add `darwin` locally via a re-generate.
35
+ generator client {
36
+ provider = "prisma-client-js"
37
+ output = "../prisma-client"
38
+ binaryTargets = ["darwin-arm64", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
39
+ }
40
+
41
+ datasource db {
42
+ provider = "postgresql"
43
+ url = env("DATABASE_URL")
44
+ }
45
+
46
+ // ═══════════════════════════════════════════════════════════════════════════
47
+ // Multi-tenant root
48
+ // ═══════════════════════════════════════════════════════════════════════════
49
+
50
+ model Project {
51
+ id String @id @default(cuid())
52
+ name String
53
+ slug String @unique
54
+ type ProjectType
55
+ createdAt DateTime @default(now())
56
+ updatedAt DateTime @updatedAt
57
+
58
+ // Operating mode (Observe / TicketOnly / Author / Auto) — see product brief §11.2
59
+ mode ProjectMode @default(OBSERVE)
60
+ modeChangedAt DateTime @default(now())
61
+ modeChangedBy String?
62
+
63
+ // Per-project quotas + budgets
64
+ monthlyBudgetCents Int @default(10000) // $100 default
65
+ dailyDispatchLimit Int @default(50)
66
+
67
+ // GitHub integration (QAP-019C / Group D-1, 2026-05-04).
68
+ //
69
+ // `repoFullName` is the canonical "<owner>/<repo>" identifier (e.g.,
70
+ // "RoryLYN/derwin"). The github-webhook route uses it to resolve a
71
+ // payload's `repository.full_name` → projectId. Optional + unique
72
+ // because: not every Derwin project has a GitHub repo (Sprint 4+ may
73
+ // onboard products that don't run on GitHub); but a given repo can map
74
+ // to at most one Derwin project (otherwise webhook payloads would be
75
+ // ambiguous).
76
+ //
77
+ // `webhookSecret` is the per-project HMAC-SHA256 secret GitHub uses to
78
+ // sign webhook payloads. Optional so projects without GitHub integration
79
+ // carry no secret. The webhook route passes this to
80
+ // `QAAuthAdapter.verifyWebhook(headers, body, secret)` to authenticate
81
+ // payloads before any DB writes.
82
+ repoFullName String? @unique
83
+ webhookSecret String?
84
+
85
+ // Relations
86
+ profile ProjectProfile?
87
+ runs QARun[]
88
+ tickets QATicket[]
89
+ fixAttempts QAFixAttempt[]
90
+ auditArtifacts AuditArtifact[]
91
+ policies Policy[]
92
+ trustScores ClassificationTrust[]
93
+ modeLog ProjectModeLog[]
94
+ patterns QAPattern[]
95
+ reverts QARevert[]
96
+ uniformities QAUniformity[]
97
+
98
+ @@map("projects")
99
+ }
100
+
101
+ enum ProjectType {
102
+ SAAS
103
+ ECOMMERCE
104
+ MARKETING
105
+ ERP
106
+ ANALYTICS
107
+ INFRA
108
+ CONTENT
109
+ }
110
+
111
+ enum ProjectMode {
112
+ OBSERVE // run but write nothing
113
+ TICKET_ONLY // write tickets, no fix authoring
114
+ AUTHOR // tickets + fix authoring, all to PR
115
+ AUTO // full autonomy per risk tier policies
116
+ }
117
+
118
+ // ═══════════════════════════════════════════════════════════════════════════
119
+ // Layer A: Project Profile (the Knowledge Base)
120
+ // ═══════════════════════════════════════════════════════════════════════════
121
+
122
+ model ProjectProfile {
123
+ id String @id @default(cuid())
124
+ projectId String @unique
125
+ type ProjectType
126
+
127
+ domainOntology Json // entities[], actions[], roles[]
128
+ riskProfile Json // 8-vector weights across surface categories
129
+ criticalFlows Json // named user journeys with steps + assertions
130
+ glossary Json // term → definition; disambiguates "ticket"/"tenant"/etc.
131
+ dependencies Json // third-party services that need integration testing
132
+ complianceTags String[] @default([])
133
+
134
+ ingestedDocsHash String // hash of (doc, version) tuples for change detection
135
+
136
+ lastIngestedAt DateTime
137
+ lastEvolvedAt DateTime
138
+ createdAt DateTime @default(now())
139
+ updatedAt DateTime @updatedAt
140
+
141
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
142
+ evolutionLog ProfileEvolutionLog[]
143
+
144
+ @@map("project_profiles")
145
+ }
146
+
147
+ model IngestedDoc {
148
+ id String @id @default(cuid())
149
+ projectId String
150
+ source String // "filesystem" | "notion" | "google-docs" | etc.
151
+ sourcePath String // canonical path/URL of the doc
152
+ contentHash String
153
+ contentSize Int
154
+ ingestedAt DateTime @default(now())
155
+ ontologyDelta Json? // what this doc contributed to the Profile
156
+
157
+ @@unique([projectId, sourcePath, contentHash])
158
+ @@index([projectId, sourcePath])
159
+ @@map("ingested_docs")
160
+ }
161
+
162
+ model ProfileEvolutionLog {
163
+ id String @id @default(cuid())
164
+ profileId String
165
+ triggerType String // "FIX_OUTCOME" | "SPRINT_INGEST" | "DEPLOY" | "OPERATOR_OVERRIDE"
166
+ delta Json // what changed
167
+ rationale String @db.Text
168
+ appliedAt DateTime @default(now())
169
+ appliedBy String // "agent" | userId
170
+
171
+ profile ProjectProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)
172
+
173
+ @@index([profileId, appliedAt(sort: Desc)])
174
+ @@map("profile_evolution_log")
175
+ }
176
+
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+ // Layer B/C: Discovery + Execution
179
+ // ═══════════════════════════════════════════════════════════════════════════
180
+
181
+ model QARun {
182
+ id String @id @default(cuid())
183
+ projectId String
184
+ triggeredBy String // "cron" | "operator:<userId>" | "webhook" | "<routeName>"
185
+ triggerType RunTrigger
186
+ scope Json // what surfaces, which routes, which subset
187
+ startedAt DateTime @default(now())
188
+ completedAt DateTime?
189
+ status RunStatus @default(IN_PROGRESS)
190
+
191
+ // Outcome counts (denormalized for dashboard reads)
192
+ signalsRaised Int @default(0)
193
+ ticketsCreated Int @default(0)
194
+ attemptsLaunched Int @default(0)
195
+
196
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
197
+ signals RawSignal[]
198
+ tickets QATicket[]
199
+ reverts QARevert[]
200
+ uniformities QAUniformity[]
201
+
202
+ @@index([projectId, startedAt(sort: Desc)])
203
+ @@map("qa_runs")
204
+ }
205
+
206
+ enum RunTrigger {
207
+ CRON
208
+ OPERATOR_ON_DEMAND
209
+ WEBHOOK
210
+ CONTINUOUS // for routes that run as a daemon (telemetry mining etc.)
211
+ AGENT // QAP-019F (Group D-3): standalone QA agent (browser tool) ingestion
212
+ }
213
+
214
+ enum RunStatus {
215
+ IN_PROGRESS
216
+ COMPLETED
217
+ FAILED
218
+ CANCELLED
219
+ }
220
+
221
+ // A RawSignal is what a Discovery route emits. It hasn't been investigated /
222
+ // classified / ticketed yet — Triage takes signals and produces tickets.
223
+ model RawSignal {
224
+ id String @id @default(cuid())
225
+ qaRunId String
226
+ routeName String // which Discovery route fired
227
+ signalType String // "test_failure" | "telemetry_anomaly" | "rage_click_cluster" | etc.
228
+ rawData Json // route-specific payload (test results, metric snapshot, etc.)
229
+ rawArtifactRefs Json // pointers to S3 artifacts (videos, screenshots)
230
+
231
+ // Investigation + classification happen later; null until Triage processes.
232
+ qaTicketId String? @unique
233
+ investigatedAt DateTime?
234
+
235
+ capturedAt DateTime @default(now())
236
+
237
+ qaRun QARun @relation(fields: [qaRunId], references: [id], onDelete: Cascade)
238
+ ticket QATicket? @relation(fields: [qaTicketId], references: [id])
239
+
240
+ @@index([qaRunId])
241
+ @@index([routeName, capturedAt(sort: Desc)])
242
+ @@map("raw_signals")
243
+ }
244
+
245
+ // ═══════════════════════════════════════════════════════════════════════════
246
+ // Layer D: Triage — the structured QA ticket
247
+ // ═══════════════════════════════════════════════════════════════════════════
248
+
249
+ model QATicket {
250
+ id String @id @default(cuid())
251
+ projectId String
252
+ qaRunId String
253
+
254
+ // Identity / linkage
255
+ externalRef String? // if mirrored to Linear/Jira/Lifeline-native, the ID there (single primary cross-system ref; legacy from v0.4)
256
+ externalSystem String? // "linear" | "jira" | "github-issues" | "lifeline-native"
257
+ originatingTicketRef String? // the FEATURE ticket that introduced the affected code, if known
258
+
259
+ // Cross-link stubs posted to consumer ticket systems per ADR-0008 (architecture
260
+ // pivot 2026-05-01). Array of { system: 'github', repo: string, issueNumber: number,
261
+ // url: string, state: 'open' | 'closed' }. Multiple stubs supported (e.g., post
262
+ // to both GitHub Issues and Linear). QAP-018 (Sprint 2) populates this on
263
+ // createQATicket via the LifelineTicketAdapter.
264
+ crossLinkRefs Json @default("[]")
265
+
266
+ // Canonical deep-link URL for this ticket in the Derwin-owned dashboard per
267
+ // ADR-0008. Populated by QAP-018B's createQATicket from DERWIN_DASHBOARD_URL
268
+ // env + ticket id. The cross-link stubs in crossLinkRefs[] embed this URL in
269
+ // their bodies so operators triaging in the consumer ticket system can click
270
+ // through to the rich Derwin record.
271
+ dashboardUrl String?
272
+
273
+ // Classification
274
+ surface SurfaceCategory
275
+ classification String // SELECTOR | PRODUCT_BUG | TIMING | etc., extensible
276
+ severity Severity
277
+ riskTier RiskTier
278
+ status TicketStatus @default(OPEN)
279
+
280
+ // Content (from the structured ticket format in product brief §6.3)
281
+ title String
282
+ reproSteps Json // array of step objects
283
+ suspectedRootCause String @db.Text
284
+ blastRadius Json // affected files / tests / pages
285
+
286
+ // Author/dispatch decision
287
+ proposedFixApproach String @db.Text
288
+ autoMergeEligible Boolean @default(false)
289
+ autoMergeRationale String? @db.Text
290
+
291
+ // Final bucket assignment (populated after lifecycle completes)
292
+ finalBucket ReviewBucket?
293
+ bucketReason String? @db.Text
294
+
295
+ createdAt DateTime @default(now())
296
+ updatedAt DateTime @updatedAt
297
+
298
+ // Optional audit field — operator userId or system actor name (e.g.,
299
+ // "qa-auto-fix-webhook") that resolved this ticket. Set on status
300
+ // transitions to RESOLVED, REJECTED, or REGRESSED_REVERTED. QAP-019C
301
+ // (Group D-1, 2026-05-04). Mirrors Lifeline's QARecommendation.resolvedBy
302
+ // column. Optional because not every status transition carries a
303
+ // resolution actor.
304
+ resolvedBy String?
305
+ closedAt DateTime?
306
+
307
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
308
+ qaRun QARun @relation(fields: [qaRunId], references: [id], onDelete: Cascade)
309
+ signals RawSignal[]
310
+ attempts QAFixAttempt[]
311
+ artifacts AuditArtifact[]
312
+ reverts QARevert[]
313
+
314
+ @@index([projectId, status])
315
+ @@index([projectId, finalBucket])
316
+ @@index([projectId, createdAt(sort: Desc)])
317
+ @@map("qa_tickets")
318
+ }
319
+
320
+ enum SurfaceCategory {
321
+ CODE_HEALTH
322
+ FUNCTIONAL_CORRECTNESS
323
+ UI_UX_INTEGRITY
324
+ PERF_RESILIENCE
325
+ SECURITY
326
+ COMPLIANCE_AUDIT
327
+ MULTI_TENANT_SAFETY
328
+ OPERATIONAL_HEALTH
329
+ }
330
+
331
+ enum Severity {
332
+ CRITICAL
333
+ HIGH
334
+ MEDIUM
335
+ LOW
336
+ }
337
+
338
+ enum RiskTier {
339
+ LOW
340
+ MEDIUM
341
+ HIGH
342
+ NEVER
343
+ }
344
+
345
+ enum TicketStatus {
346
+ OPEN
347
+ INVESTIGATING
348
+ AUTHORING
349
+ PR_OPEN
350
+ MERGED
351
+ REGRESSED_REVERTED
352
+ CLOSED_WONTFIX
353
+ CLOSED_RESOLVED
354
+ ESCALATED
355
+ }
356
+
357
+ enum ReviewBucket {
358
+ PASS
359
+ FAIL
360
+ ESCALATION
361
+ }
362
+
363
+ // ═══════════════════════════════════════════════════════════════════════════
364
+ // Layer E: Auto-fix attempts
365
+ // ═══════════════════════════════════════════════════════════════════════════
366
+
367
+ model QAFixAttempt {
368
+ id String @id @default(cuid())
369
+ qaTicketId String
370
+ projectId String
371
+ attemptNumber Int // 1..N for iterative retry
372
+
373
+ // Prompt + diff
374
+ promptText String @db.Text
375
+ diff String? @db.Text
376
+ diffSizeBytes Int?
377
+ modelName String?
378
+ modelVersion String?
379
+ inputTokens Int?
380
+ outputTokens Int?
381
+ costCents Int?
382
+
383
+ // Pre-merge verification
384
+ preVerifyResult Json? // sandbox run results
385
+ preVerifyPassed Boolean?
386
+
387
+ // Dispatch outcome
388
+ branchName String?
389
+ prUrl String?
390
+ prNumber Int?
391
+ dispatchStatus AttemptStatus @default(AUTHORING)
392
+
393
+ // Post-merge verification (set after merge)
394
+ mergedAt DateTime?
395
+ postVerifyResult Json?
396
+ regressionDetected Boolean @default(false)
397
+ autoRevertedAt DateTime?
398
+ humanEditLines Int?
399
+ fixSuccessScore Float? // -1.0 to 1.0
400
+
401
+ attemptedAt DateTime @default(now())
402
+ closedAt DateTime?
403
+
404
+ ticket QATicket @relation(fields: [qaTicketId], references: [id], onDelete: Cascade)
405
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
406
+ artifacts AuditArtifact[]
407
+
408
+ @@index([qaTicketId, attemptNumber])
409
+ @@index([projectId, attemptedAt(sort: Desc)])
410
+ @@map("qa_fix_attempts")
411
+ }
412
+
413
+ enum AttemptStatus {
414
+ AUTHORING
415
+ PRE_VERIFY_FAILED
416
+ PRE_VERIFY_PASSED
417
+ PR_OPENED
418
+ AUTO_MERGED
419
+ HUMAN_MERGED
420
+ REJECTED
421
+ REGRESSED_REVERTED
422
+ FAILED
423
+ }
424
+
425
+ // ═══════════════════════════════════════════════════════════════════════════
426
+ // Layer F: Trust scores + drift + RAG corpus
427
+ // ═══════════════════════════════════════════════════════════════════════════
428
+
429
+ model ClassificationTrust {
430
+ id String @id @default(cuid())
431
+ projectId String
432
+ classification String // matches QATicket.classification
433
+ surface SurfaceCategory
434
+
435
+ // Rolling stats (30-day window)
436
+ attemptsLast30d Int @default(0)
437
+ mergedClean Int @default(0)
438
+ mergedWithEdits Int @default(0)
439
+ regressed Int @default(0)
440
+ reverted Int @default(0)
441
+
442
+ // Computed
443
+ successScore Float @default(0) // -1.0 to 1.0
444
+ trustPercent Int @default(0) // 0 to 100, operator-facing
445
+ autoMergeEligible Boolean @default(false)
446
+
447
+ lastComputedAt DateTime @default(now())
448
+
449
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
450
+
451
+ @@unique([projectId, classification, surface])
452
+ @@map("classification_trust")
453
+ }
454
+
455
+ // Successful past fixes used as in-context examples for new dispatches.
456
+ // Embedding column is Bytes for v1; QAP-076 (Sprint 7) switches to pgvector.
457
+ model RAGCorpus {
458
+ id String @id @default(cuid())
459
+ projectId String
460
+ classification String
461
+ surface SurfaceCategory
462
+
463
+ promptText String @db.Text // the prompt that produced the fix
464
+ diff String @db.Text // the unified diff that worked
465
+ embedding Bytes? // for semantic search
466
+
467
+ fixAttemptId String // back-reference to the source attempt
468
+ outcomeQuality Float // priority weight; declines if later usage shows problems
469
+ addedAt DateTime @default(now())
470
+
471
+ @@index([projectId, classification, surface])
472
+ @@map("rag_corpus")
473
+ }
474
+
475
+ // ═══════════════════════════════════════════════════════════════════════════
476
+ // Audit artifacts — proof-of-work, not "trust me"
477
+ // ═══════════════════════════════════════════════════════════════════════════
478
+
479
+ model AuditArtifact {
480
+ id String @id @default(cuid())
481
+ projectId String
482
+ qaTicketId String?
483
+ qaFixAttemptId String?
484
+
485
+ artifactType ArtifactType
486
+ stage ArtifactStage // which lifecycle stage produced this
487
+
488
+ // Storage references (S3-style)
489
+ storageBackend String // "s3" | "supabase-storage" | "filesystem"
490
+ storageKey String // canonical key/path
491
+ contentType String
492
+ sizeBytes Int
493
+ contentHash String
494
+
495
+ // Metadata
496
+ meta Json // route-specific (e.g., screenshot timestamp, video duration)
497
+ retentionUntil DateTime? // computed from project compliance mode
498
+
499
+ capturedAt DateTime @default(now())
500
+
501
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
502
+ ticket QATicket? @relation(fields: [qaTicketId], references: [id])
503
+ fixAttempt QAFixAttempt? @relation(fields: [qaFixAttemptId], references: [id])
504
+
505
+ @@index([qaTicketId])
506
+ @@index([projectId, capturedAt(sort: Desc)])
507
+ @@map("audit_artifacts")
508
+ }
509
+
510
+ enum ArtifactType {
511
+ SCREENSHOT
512
+ VIDEO
513
+ DOM_SNAPSHOT
514
+ CONSOLE_LOG
515
+ NETWORK_LOG
516
+ TELEMETRY_SLICE
517
+ CODE_SNAPSHOT
518
+ PROMPT_TEXT
519
+ DIFF
520
+ REASONING_TRACE
521
+ REVIEWER_OUTPUT
522
+ REPLAY_BUNDLE
523
+ }
524
+
525
+ enum ArtifactStage {
526
+ DETECTION
527
+ INVESTIGATION
528
+ TICKET_CREATION
529
+ AUTHORING
530
+ PRE_MERGE_VERIFICATION
531
+ POST_MERGE_VERIFICATION
532
+ ARCHIVAL
533
+ }
534
+
535
+ // ═══════════════════════════════════════════════════════════════════════════
536
+ // Policies + safety (Sprint 8 — QAP-080..089 — fills these out)
537
+ // ═══════════════════════════════════════════════════════════════════════════
538
+
539
+ model Policy {
540
+ id String @id @default(cuid())
541
+ projectId String
542
+ policyType PolicyType
543
+
544
+ // Path tier rules: globs → tier
545
+ pathRules Json // [{ glob: "src/auth/**", tier: "HIGH" }, …]
546
+ classificationOverrides Json // { "PRODUCT_BUG": "MEDIUM", … }
547
+
548
+ // Trust thresholds
549
+ autoMergeTrustThreshold Int @default(70)
550
+ autoMergeMediumThreshold Int @default(85)
551
+
552
+ // Concurrency
553
+ dailyDispatchLimit Int @default(50)
554
+ // Fix-attempt retry limit per ticket. Default 1 per ADR-0006 — try once,
555
+ // route to FAIL on failure (conservative trust posture). Configurable per
556
+ // project; recommended range 1–4. Higher values raise auto-fix throughput
557
+ // on iterative-success classifications at the cost of Anthropic spend and
558
+ // delayed operator awareness of classifier weakness.
559
+ perTicketAttemptLimit Int @default(1)
560
+
561
+ // Time-of-day
562
+ freezeWindowsCron String[] @default([])
563
+
564
+ // Escalation triggers
565
+ escalationTriggers Json
566
+
567
+ updatedAt DateTime @updatedAt
568
+ updatedBy String
569
+
570
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
571
+
572
+ @@index([projectId])
573
+ @@map("policies")
574
+ }
575
+
576
+ enum PolicyType {
577
+ RISK_TIER
578
+ CONCURRENCY
579
+ TRUST_THRESHOLD
580
+ ESCALATION
581
+ }
582
+
583
+ model ProjectModeLog {
584
+ id String @id @default(cuid())
585
+ projectId String
586
+ fromMode ProjectMode
587
+ toMode ProjectMode
588
+ reason String @db.Text
589
+ changedBy String // "auto-promotion" | userId
590
+ changedAt DateTime @default(now())
591
+
592
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
593
+
594
+ @@index([projectId, changedAt(sort: Desc)])
595
+ @@map("project_mode_log")
596
+ }
597
+
598
+ // ═══════════════════════════════════════════════════════════════════════════
599
+ // Cost + telemetry
600
+ // ═══════════════════════════════════════════════════════════════════════════
601
+
602
+ model SpendLedger {
603
+ id String @id @default(cuid())
604
+ projectId String
605
+ qaFixAttemptId String?
606
+
607
+ // Per-event spend
608
+ vendor String // "anthropic" | "openai" | "github-actions" | "supabase-storage"
609
+ eventType String // "claude_dispatch" | "claude_review" | "embedding" | "storage_write"
610
+ costCents Int // signed; can be credit
611
+ meta Json
612
+
613
+ occurredAt DateTime @default(now())
614
+
615
+ @@index([projectId, occurredAt(sort: Desc)])
616
+ @@index([projectId, vendor, occurredAt])
617
+ @@map("spend_ledger")
618
+ }
619
+
620
+ // ═══════════════════════════════════════════════════════════════════════════
621
+ // Layer F: Learning — patterns + reverts (QAP-019D + QAP-019E, Group D-2)
622
+ // ═══════════════════════════════════════════════════════════════════════════
623
+
624
+ // QAPattern is a learned failure signature — the orchestrator's Triage stage
625
+ // (Sprint 6) bumps occurrenceCount + lastSeenAt when a new RawSignal matches
626
+ // a known signature. Lifeline-shape preserved verbatim per Group D-2 plan
627
+ // decision #1: re-ranker fields (fixSuccessScore / fixOutcomeSampleSize /
628
+ // lastWeightedAt) stay even though Derwin's re-ranker today reads
629
+ // classification-trust-store. Preserves optionality for Sprint 4+ consumers
630
+ // and removes shim translation work in QAP-019G.
631
+ //
632
+ // (signature, projectId) is unique — the same failure signature in different
633
+ // projects is two distinct rows. Lifeline-style.
634
+ model QAPattern {
635
+ id String @id @default(cuid())
636
+ projectId String
637
+ signature String
638
+ classification QAFailureClass
639
+ firstSeenAt DateTime @default(now())
640
+ lastSeenAt DateTime @default(now())
641
+ occurrenceCount Int @default(1)
642
+ affectedTickets String[] @default([])
643
+ trendDirection String @default("stable") // improving | stable | degrading
644
+ status QAPatternStatus @default(OPEN)
645
+ resolvedAt DateTime?
646
+ promotedToLintAt DateTime?
647
+ promotedLintPR String?
648
+ archivedAt DateTime?
649
+
650
+ // Re-ranker fields (Lifeline-shape preserved, Decision 1).
651
+ fixSuccessScore Float?
652
+ fixOutcomeSampleSize Int @default(0)
653
+ lastWeightedAt DateTime?
654
+
655
+ createdAt DateTime @default(now())
656
+ updatedAt DateTime @updatedAt
657
+
658
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
659
+
660
+ @@unique([projectId, signature])
661
+ @@index([projectId, lastSeenAt(sort: Desc)])
662
+ @@index([status])
663
+ @@map("qa_patterns")
664
+ }
665
+
666
+ enum QAPatternStatus {
667
+ OPEN
668
+ RESOLVED
669
+ ARCHIVED
670
+ }
671
+
672
+ enum QAFailureClass {
673
+ SELECTOR
674
+ TIMING
675
+ TEST_DATA
676
+ PRODUCT_BUG
677
+ SCHEMA_DRIFT
678
+ UNKNOWN
679
+ }
680
+
681
+ // QARevert logs a manual or auto-revert of a fix-forward commit. The route
682
+ // handler does NOT execute `git revert` — it only records the operator's
683
+ // action. Restore is the inverse: a fix-forward commit lands and the operator
684
+ // flips restoredAt + restoredBy + restoredBySha.
685
+ //
686
+ // Decision 2: keep qaRunId FK only. Lifeline's qaResultId pointed at the
687
+ // QARunResult table, which Derwin doesn't have under Option β (results are
688
+ // RawSignal records). Reverts are at the ticket+commit level, not the
689
+ // per-test-result level.
690
+ model QARevert {
691
+ id String @id @default(cuid())
692
+ projectId String
693
+ ticketId String
694
+ revertedFromSha String
695
+ revertedToSha String?
696
+ revertedAt DateTime @default(now())
697
+ revertedBy String
698
+ reason String @db.Text
699
+ qaRunId String?
700
+
701
+ // Restore audit trail — set when the fix-forward commit ships.
702
+ restoredAt DateTime?
703
+ restoredBy String?
704
+ restoredBySha String?
705
+
706
+ createdAt DateTime @default(now())
707
+ updatedAt DateTime @updatedAt
708
+
709
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
710
+ ticket QATicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
711
+ run QARun? @relation(fields: [qaRunId], references: [id], onDelete: SetNull)
712
+
713
+ @@index([projectId, revertedAt(sort: Desc)])
714
+ @@index([ticketId])
715
+ @@map("qa_reverts")
716
+ }
717
+
718
+ // QAUniformity stores per-page rule outcomes from the invariant crawler
719
+ // (uniformity audit). Lifeline calls this `QAUniformityAudit`; renamed to
720
+ // `QAUniformity` per Group D-3 plan Decision 5 (drop `Audit` suffix to match
721
+ // other Derwin model names). Bulk-inserted by /api/qa/uniformity POST and
722
+ // aggregated by GET. Optionally tied to a QARun (most ingestions are run-
723
+ // scoped; manual operator runs may not have a qaRunId).
724
+ model QAUniformity {
725
+ id String @id @default(cuid())
726
+ projectId String
727
+ qaRunId String?
728
+ pagePath String
729
+ ruleName String
730
+ ruleCategory String
731
+ status QAUniformityStatus
732
+ violationDetail String? @db.Text
733
+ createdAt DateTime @default(now())
734
+
735
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
736
+ run QARun? @relation(fields: [qaRunId], references: [id], onDelete: SetNull)
737
+
738
+ @@index([projectId, createdAt(sort: Desc)])
739
+ @@index([projectId, ruleName, status])
740
+ @@index([projectId, pagePath])
741
+ @@map("qa_uniformities")
742
+ }
743
+
744
+ enum QAUniformityStatus {
745
+ PASSED
746
+ FAILED
747
+ SKIPPED
748
+ }