@derwinjs/db 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@derwinjs/db",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Prisma schema + migrations for Derwin's own Postgres. 14 models, project-namespaced. Per ADR-0005. Ships its own generated Prisma client (multi-platform binaries) for cross-consumer compatibility.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@prisma/client": "^5.22.0",
36
- "@derwinjs/core": "0.1.0",
37
- "@derwinjs/sdk": "0.1.0"
36
+ "@derwinjs/core": "0.1.1",
37
+ "@derwinjs/sdk": "0.1.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@vitest/coverage-v8": "^2.1.9",
@@ -14,6 +14,14 @@
14
14
  // pgvector convention: `embedding Bytes?` on RAGCorpus is the v1 storage type.
15
15
  // QAP-076 (Sprint 7 — RAG corpus add) switches it to a pgvector `vector(1536)`
16
16
  // column once the extension is enabled in QAP-036.
17
+ //
18
+ // Multi-schema: every model + enum is in the `derwin` Postgres schema (not
19
+ // `public`). This isolates Derwin's tables from each consumer's existing
20
+ // schema — e.g., Lifeline's pre-extraction `public.qa_fix_attempts` stays
21
+ // untouched while Derwin's tables live at `derwin.qa_fix_attempts`. Required
22
+ // for the multi-tenant boundary rule: each consumer = ONE Project row, but
23
+ // each consumer also has its own pre-existing schema in `public`. (Added in
24
+ // QAP-019G when extracting Derwin into Lifeline first revealed the conflict.)
17
25
 
18
26
  // Generator output is INSIDE the package so the generated client ships
19
27
  // in the published tarball alongside dist/. Consumer code does
@@ -33,14 +41,16 @@
33
41
  // tarball platform-dependent (whatever the publisher was running on).
34
42
  // Intel Mac users (rare) add `darwin` locally via a re-generate.
35
43
  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"]
44
+ provider = "prisma-client-js"
45
+ output = "../prisma-client"
46
+ binaryTargets = ["darwin-arm64", "rhel-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
47
+ previewFeatures = ["multiSchema"]
39
48
  }
40
49
 
41
50
  datasource db {
42
51
  provider = "postgresql"
43
52
  url = env("DATABASE_URL")
53
+ schemas = ["derwin"]
44
54
  }
45
55
 
46
56
  // ═══════════════════════════════════════════════════════════════════════════
@@ -96,6 +106,7 @@ model Project {
96
106
  uniformities QAUniformity[]
97
107
 
98
108
  @@map("projects")
109
+ @@schema("derwin")
99
110
  }
100
111
 
101
112
  enum ProjectType {
@@ -106,6 +117,8 @@ enum ProjectType {
106
117
  ANALYTICS
107
118
  INFRA
108
119
  CONTENT
120
+
121
+ @@schema("derwin")
109
122
  }
110
123
 
111
124
  enum ProjectMode {
@@ -113,6 +126,8 @@ enum ProjectMode {
113
126
  TICKET_ONLY // write tickets, no fix authoring
114
127
  AUTHOR // tickets + fix authoring, all to PR
115
128
  AUTO // full autonomy per risk tier policies
129
+
130
+ @@schema("derwin")
116
131
  }
117
132
 
118
133
  // ═══════════════════════════════════════════════════════════════════════════
@@ -142,6 +157,7 @@ model ProjectProfile {
142
157
  evolutionLog ProfileEvolutionLog[]
143
158
 
144
159
  @@map("project_profiles")
160
+ @@schema("derwin")
145
161
  }
146
162
 
147
163
  model IngestedDoc {
@@ -157,6 +173,7 @@ model IngestedDoc {
157
173
  @@unique([projectId, sourcePath, contentHash])
158
174
  @@index([projectId, sourcePath])
159
175
  @@map("ingested_docs")
176
+ @@schema("derwin")
160
177
  }
161
178
 
162
179
  model ProfileEvolutionLog {
@@ -172,6 +189,7 @@ model ProfileEvolutionLog {
172
189
 
173
190
  @@index([profileId, appliedAt(sort: Desc)])
174
191
  @@map("profile_evolution_log")
192
+ @@schema("derwin")
175
193
  }
176
194
 
177
195
  // ═══════════════════════════════════════════════════════════════════════════
@@ -201,6 +219,7 @@ model QARun {
201
219
 
202
220
  @@index([projectId, startedAt(sort: Desc)])
203
221
  @@map("qa_runs")
222
+ @@schema("derwin")
204
223
  }
205
224
 
206
225
  enum RunTrigger {
@@ -209,6 +228,8 @@ enum RunTrigger {
209
228
  WEBHOOK
210
229
  CONTINUOUS // for routes that run as a daemon (telemetry mining etc.)
211
230
  AGENT // QAP-019F (Group D-3): standalone QA agent (browser tool) ingestion
231
+
232
+ @@schema("derwin")
212
233
  }
213
234
 
214
235
  enum RunStatus {
@@ -216,6 +237,8 @@ enum RunStatus {
216
237
  COMPLETED
217
238
  FAILED
218
239
  CANCELLED
240
+
241
+ @@schema("derwin")
219
242
  }
220
243
 
221
244
  // A RawSignal is what a Discovery route emits. It hasn't been investigated /
@@ -240,6 +263,7 @@ model RawSignal {
240
263
  @@index([qaRunId])
241
264
  @@index([routeName, capturedAt(sort: Desc)])
242
265
  @@map("raw_signals")
266
+ @@schema("derwin")
243
267
  }
244
268
 
245
269
  // ═══════════════════════════════════════════════════════════════════════════
@@ -315,6 +339,7 @@ model QATicket {
315
339
  @@index([projectId, finalBucket])
316
340
  @@index([projectId, createdAt(sort: Desc)])
317
341
  @@map("qa_tickets")
342
+ @@schema("derwin")
318
343
  }
319
344
 
320
345
  enum SurfaceCategory {
@@ -326,6 +351,8 @@ enum SurfaceCategory {
326
351
  COMPLIANCE_AUDIT
327
352
  MULTI_TENANT_SAFETY
328
353
  OPERATIONAL_HEALTH
354
+
355
+ @@schema("derwin")
329
356
  }
330
357
 
331
358
  enum Severity {
@@ -333,6 +360,8 @@ enum Severity {
333
360
  HIGH
334
361
  MEDIUM
335
362
  LOW
363
+
364
+ @@schema("derwin")
336
365
  }
337
366
 
338
367
  enum RiskTier {
@@ -340,6 +369,8 @@ enum RiskTier {
340
369
  MEDIUM
341
370
  HIGH
342
371
  NEVER
372
+
373
+ @@schema("derwin")
343
374
  }
344
375
 
345
376
  enum TicketStatus {
@@ -352,12 +383,16 @@ enum TicketStatus {
352
383
  CLOSED_WONTFIX
353
384
  CLOSED_RESOLVED
354
385
  ESCALATED
386
+
387
+ @@schema("derwin")
355
388
  }
356
389
 
357
390
  enum ReviewBucket {
358
391
  PASS
359
392
  FAIL
360
393
  ESCALATION
394
+
395
+ @@schema("derwin")
361
396
  }
362
397
 
363
398
  // ═══════════════════════════════════════════════════════════════════════════
@@ -408,6 +443,7 @@ model QAFixAttempt {
408
443
  @@index([qaTicketId, attemptNumber])
409
444
  @@index([projectId, attemptedAt(sort: Desc)])
410
445
  @@map("qa_fix_attempts")
446
+ @@schema("derwin")
411
447
  }
412
448
 
413
449
  enum AttemptStatus {
@@ -420,6 +456,8 @@ enum AttemptStatus {
420
456
  REJECTED
421
457
  REGRESSED_REVERTED
422
458
  FAILED
459
+
460
+ @@schema("derwin")
423
461
  }
424
462
 
425
463
  // ═══════════════════════════════════════════════════════════════════════════
@@ -450,6 +488,7 @@ model ClassificationTrust {
450
488
 
451
489
  @@unique([projectId, classification, surface])
452
490
  @@map("classification_trust")
491
+ @@schema("derwin")
453
492
  }
454
493
 
455
494
  // Successful past fixes used as in-context examples for new dispatches.
@@ -470,6 +509,7 @@ model RAGCorpus {
470
509
 
471
510
  @@index([projectId, classification, surface])
472
511
  @@map("rag_corpus")
512
+ @@schema("derwin")
473
513
  }
474
514
 
475
515
  // ═══════════════════════════════════════════════════════════════════════════
@@ -505,6 +545,7 @@ model AuditArtifact {
505
545
  @@index([qaTicketId])
506
546
  @@index([projectId, capturedAt(sort: Desc)])
507
547
  @@map("audit_artifacts")
548
+ @@schema("derwin")
508
549
  }
509
550
 
510
551
  enum ArtifactType {
@@ -520,6 +561,8 @@ enum ArtifactType {
520
561
  REASONING_TRACE
521
562
  REVIEWER_OUTPUT
522
563
  REPLAY_BUNDLE
564
+
565
+ @@schema("derwin")
523
566
  }
524
567
 
525
568
  enum ArtifactStage {
@@ -530,6 +573,8 @@ enum ArtifactStage {
530
573
  PRE_MERGE_VERIFICATION
531
574
  POST_MERGE_VERIFICATION
532
575
  ARCHIVAL
576
+
577
+ @@schema("derwin")
533
578
  }
534
579
 
535
580
  // ═══════════════════════════════════════════════════════════════════════════
@@ -571,6 +616,7 @@ model Policy {
571
616
 
572
617
  @@index([projectId])
573
618
  @@map("policies")
619
+ @@schema("derwin")
574
620
  }
575
621
 
576
622
  enum PolicyType {
@@ -578,6 +624,8 @@ enum PolicyType {
578
624
  CONCURRENCY
579
625
  TRUST_THRESHOLD
580
626
  ESCALATION
627
+
628
+ @@schema("derwin")
581
629
  }
582
630
 
583
631
  model ProjectModeLog {
@@ -593,6 +641,7 @@ model ProjectModeLog {
593
641
 
594
642
  @@index([projectId, changedAt(sort: Desc)])
595
643
  @@map("project_mode_log")
644
+ @@schema("derwin")
596
645
  }
597
646
 
598
647
  // ═══════════════════════════════════════════════════════════════════════════
@@ -615,6 +664,7 @@ model SpendLedger {
615
664
  @@index([projectId, occurredAt(sort: Desc)])
616
665
  @@index([projectId, vendor, occurredAt])
617
666
  @@map("spend_ledger")
667
+ @@schema("derwin")
618
668
  }
619
669
 
620
670
  // ═══════════════════════════════════════════════════════════════════════════
@@ -661,12 +711,15 @@ model QAPattern {
661
711
  @@index([projectId, lastSeenAt(sort: Desc)])
662
712
  @@index([status])
663
713
  @@map("qa_patterns")
714
+ @@schema("derwin")
664
715
  }
665
716
 
666
717
  enum QAPatternStatus {
667
718
  OPEN
668
719
  RESOLVED
669
720
  ARCHIVED
721
+
722
+ @@schema("derwin")
670
723
  }
671
724
 
672
725
  enum QAFailureClass {
@@ -676,6 +729,8 @@ enum QAFailureClass {
676
729
  PRODUCT_BUG
677
730
  SCHEMA_DRIFT
678
731
  UNKNOWN
732
+
733
+ @@schema("derwin")
679
734
  }
680
735
 
681
736
  // QARevert logs a manual or auto-revert of a fix-forward commit. The route
@@ -713,6 +768,7 @@ model QARevert {
713
768
  @@index([projectId, revertedAt(sort: Desc)])
714
769
  @@index([ticketId])
715
770
  @@map("qa_reverts")
771
+ @@schema("derwin")
716
772
  }
717
773
 
718
774
  // QAUniformity stores per-page rule outcomes from the invariant crawler
@@ -739,10 +795,13 @@ model QAUniformity {
739
795
  @@index([projectId, ruleName, status])
740
796
  @@index([projectId, pagePath])
741
797
  @@map("qa_uniformities")
798
+ @@schema("derwin")
742
799
  }
743
800
 
744
801
  enum QAUniformityStatus {
745
802
  PASSED
746
803
  FAILED
747
804
  SKIPPED
805
+
806
+ @@schema("derwin")
748
807
  }
@@ -581,7 +581,9 @@ const config = {
581
581
  "value": "linux-arm64-openssl-3.0.x"
582
582
  }
583
583
  ],
584
- "previewFeatures": [],
584
+ "previewFeatures": [
585
+ "multiSchema"
586
+ ],
585
587
  "sourceFilePath": "/home/runner/work/derwin/derwin/packages/db/prisma/schema.prisma",
586
588
  "isCustomOutput": true
587
589
  },
@@ -605,8 +607,8 @@ const config = {
605
607
  }
606
608
  }
607
609
  },
608
- "inlineSchema": "// Derwin platform schema — Postgres + Prisma.\n//\n// 14 models, project-namespaced. Per ADR-0005 (Prisma + Postgres + project-\n// namespaced schema). Reference: technical spec §3.\n//\n// Multi-tenancy convention: every project-scoped table has a `projectId String`\n// FK to Project. Cross-project leakage is the leading regression risk; queries\n// MUST filter on projectId. Per ADR-0005, the trust-mitigation gates are:\n// 1. A lint or static check that flags Prisma queries missing projectId\n// filters on tables that have one (added in Sprint 8 / risk-tier work).\n// 2. A multi-project broken-fixture E2E that asserts isolation\n// (added in Sprint 13 / QAP-130).\n//\n// pgvector convention: `embedding Bytes?` on RAGCorpus is the v1 storage type.\n// QAP-076 (Sprint 7 — RAG corpus add) switches it to a pgvector `vector(1536)`\n// column once the extension is enabled in QAP-036.\n\n// Generator output is INSIDE the package so the generated client ships\n// in the published tarball alongside dist/. Consumer code does\n// `import { PrismaClient } from '@derwinjs/db'` (or '@derwinjs/db/client') —\n// see packages/db/src/index.ts re-exports. This is required for\n// multi-tenant correctness: each Derwin consumer (Lifeline, Side Piece,\n// Bolt, ...) already has its own @prisma/client generated against its\n// own schema; @derwinjs/db can't piggyback on the consumer's @prisma/client\n// or types break (Lifeline's PrismaClient doesn't have qAUniformity etc).\n//\n// binaryTargets ships explicit cross-platform binaries so the published\n// tarball works regardless of where the publish workflow ran:\n// - darwin-arm64: Apple Silicon Mac dev (the Derwin team's primary local)\n// - rhel-openssl-3.0.x: x86 Linux on OpenSSL 3 (Vercel default, most CI)\n// - linux-arm64-openssl-3.0.x: ARM Linux (AWS Graviton, etc.)\n// \"native\" is intentionally omitted — relying on it makes the published\n// tarball platform-dependent (whatever the publisher was running on).\n// Intel Mac users (rare) add `darwin` locally via a re-generate.\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../prisma-client\"\n binaryTargets = [\"darwin-arm64\", \"rhel-openssl-3.0.x\", \"linux-arm64-openssl-3.0.x\"]\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Multi-tenant root\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel Project {\n id String @id @default(cuid())\n name String\n slug String @unique\n type ProjectType\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Operating mode (Observe / TicketOnly / Author / Auto) — see product brief §11.2\n mode ProjectMode @default(OBSERVE)\n modeChangedAt DateTime @default(now())\n modeChangedBy String?\n\n // Per-project quotas + budgets\n monthlyBudgetCents Int @default(10000) // $100 default\n dailyDispatchLimit Int @default(50)\n\n // GitHub integration (QAP-019C / Group D-1, 2026-05-04).\n //\n // `repoFullName` is the canonical \"<owner>/<repo>\" identifier (e.g.,\n // \"RoryLYN/derwin\"). The github-webhook route uses it to resolve a\n // payload's `repository.full_name` → projectId. Optional + unique\n // because: not every Derwin project has a GitHub repo (Sprint 4+ may\n // onboard products that don't run on GitHub); but a given repo can map\n // to at most one Derwin project (otherwise webhook payloads would be\n // ambiguous).\n //\n // `webhookSecret` is the per-project HMAC-SHA256 secret GitHub uses to\n // sign webhook payloads. Optional so projects without GitHub integration\n // carry no secret. The webhook route passes this to\n // `QAAuthAdapter.verifyWebhook(headers, body, secret)` to authenticate\n // payloads before any DB writes.\n repoFullName String? @unique\n webhookSecret String?\n\n // Relations\n profile ProjectProfile?\n runs QARun[]\n tickets QATicket[]\n fixAttempts QAFixAttempt[]\n auditArtifacts AuditArtifact[]\n policies Policy[]\n trustScores ClassificationTrust[]\n modeLog ProjectModeLog[]\n patterns QAPattern[]\n reverts QARevert[]\n uniformities QAUniformity[]\n\n @@map(\"projects\")\n}\n\nenum ProjectType {\n SAAS\n ECOMMERCE\n MARKETING\n ERP\n ANALYTICS\n INFRA\n CONTENT\n}\n\nenum ProjectMode {\n OBSERVE // run but write nothing\n TICKET_ONLY // write tickets, no fix authoring\n AUTHOR // tickets + fix authoring, all to PR\n AUTO // full autonomy per risk tier policies\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer A: Project Profile (the Knowledge Base)\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel ProjectProfile {\n id String @id @default(cuid())\n projectId String @unique\n type ProjectType\n\n domainOntology Json // entities[], actions[], roles[]\n riskProfile Json // 8-vector weights across surface categories\n criticalFlows Json // named user journeys with steps + assertions\n glossary Json // term → definition; disambiguates \"ticket\"/\"tenant\"/etc.\n dependencies Json // third-party services that need integration testing\n complianceTags String[] @default([])\n\n ingestedDocsHash String // hash of (doc, version) tuples for change detection\n\n lastIngestedAt DateTime\n lastEvolvedAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n evolutionLog ProfileEvolutionLog[]\n\n @@map(\"project_profiles\")\n}\n\nmodel IngestedDoc {\n id String @id @default(cuid())\n projectId String\n source String // \"filesystem\" | \"notion\" | \"google-docs\" | etc.\n sourcePath String // canonical path/URL of the doc\n contentHash String\n contentSize Int\n ingestedAt DateTime @default(now())\n ontologyDelta Json? // what this doc contributed to the Profile\n\n @@unique([projectId, sourcePath, contentHash])\n @@index([projectId, sourcePath])\n @@map(\"ingested_docs\")\n}\n\nmodel ProfileEvolutionLog {\n id String @id @default(cuid())\n profileId String\n triggerType String // \"FIX_OUTCOME\" | \"SPRINT_INGEST\" | \"DEPLOY\" | \"OPERATOR_OVERRIDE\"\n delta Json // what changed\n rationale String @db.Text\n appliedAt DateTime @default(now())\n appliedBy String // \"agent\" | userId\n\n profile ProjectProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)\n\n @@index([profileId, appliedAt(sort: Desc)])\n @@map(\"profile_evolution_log\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer B/C: Discovery + Execution\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel QARun {\n id String @id @default(cuid())\n projectId String\n triggeredBy String // \"cron\" | \"operator:<userId>\" | \"webhook\" | \"<routeName>\"\n triggerType RunTrigger\n scope Json // what surfaces, which routes, which subset\n startedAt DateTime @default(now())\n completedAt DateTime?\n status RunStatus @default(IN_PROGRESS)\n\n // Outcome counts (denormalized for dashboard reads)\n signalsRaised Int @default(0)\n ticketsCreated Int @default(0)\n attemptsLaunched Int @default(0)\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n signals RawSignal[]\n tickets QATicket[]\n reverts QARevert[]\n uniformities QAUniformity[]\n\n @@index([projectId, startedAt(sort: Desc)])\n @@map(\"qa_runs\")\n}\n\nenum RunTrigger {\n CRON\n OPERATOR_ON_DEMAND\n WEBHOOK\n CONTINUOUS // for routes that run as a daemon (telemetry mining etc.)\n AGENT // QAP-019F (Group D-3): standalone QA agent (browser tool) ingestion\n}\n\nenum RunStatus {\n IN_PROGRESS\n COMPLETED\n FAILED\n CANCELLED\n}\n\n// A RawSignal is what a Discovery route emits. It hasn't been investigated /\n// classified / ticketed yet — Triage takes signals and produces tickets.\nmodel RawSignal {\n id String @id @default(cuid())\n qaRunId String\n routeName String // which Discovery route fired\n signalType String // \"test_failure\" | \"telemetry_anomaly\" | \"rage_click_cluster\" | etc.\n rawData Json // route-specific payload (test results, metric snapshot, etc.)\n rawArtifactRefs Json // pointers to S3 artifacts (videos, screenshots)\n\n // Investigation + classification happen later; null until Triage processes.\n qaTicketId String? @unique\n investigatedAt DateTime?\n\n capturedAt DateTime @default(now())\n\n qaRun QARun @relation(fields: [qaRunId], references: [id], onDelete: Cascade)\n ticket QATicket? @relation(fields: [qaTicketId], references: [id])\n\n @@index([qaRunId])\n @@index([routeName, capturedAt(sort: Desc)])\n @@map(\"raw_signals\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer D: Triage — the structured QA ticket\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel QATicket {\n id String @id @default(cuid())\n projectId String\n qaRunId String\n\n // Identity / linkage\n externalRef String? // if mirrored to Linear/Jira/Lifeline-native, the ID there (single primary cross-system ref; legacy from v0.4)\n externalSystem String? // \"linear\" | \"jira\" | \"github-issues\" | \"lifeline-native\"\n originatingTicketRef String? // the FEATURE ticket that introduced the affected code, if known\n\n // Cross-link stubs posted to consumer ticket systems per ADR-0008 (architecture\n // pivot 2026-05-01). Array of { system: 'github', repo: string, issueNumber: number,\n // url: string, state: 'open' | 'closed' }. Multiple stubs supported (e.g., post\n // to both GitHub Issues and Linear). QAP-018 (Sprint 2) populates this on\n // createQATicket via the LifelineTicketAdapter.\n crossLinkRefs Json @default(\"[]\")\n\n // Canonical deep-link URL for this ticket in the Derwin-owned dashboard per\n // ADR-0008. Populated by QAP-018B's createQATicket from DERWIN_DASHBOARD_URL\n // env + ticket id. The cross-link stubs in crossLinkRefs[] embed this URL in\n // their bodies so operators triaging in the consumer ticket system can click\n // through to the rich Derwin record.\n dashboardUrl String?\n\n // Classification\n surface SurfaceCategory\n classification String // SELECTOR | PRODUCT_BUG | TIMING | etc., extensible\n severity Severity\n riskTier RiskTier\n status TicketStatus @default(OPEN)\n\n // Content (from the structured ticket format in product brief §6.3)\n title String\n reproSteps Json // array of step objects\n suspectedRootCause String @db.Text\n blastRadius Json // affected files / tests / pages\n\n // Author/dispatch decision\n proposedFixApproach String @db.Text\n autoMergeEligible Boolean @default(false)\n autoMergeRationale String? @db.Text\n\n // Final bucket assignment (populated after lifecycle completes)\n finalBucket ReviewBucket?\n bucketReason String? @db.Text\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Optional audit field — operator userId or system actor name (e.g.,\n // \"qa-auto-fix-webhook\") that resolved this ticket. Set on status\n // transitions to RESOLVED, REJECTED, or REGRESSED_REVERTED. QAP-019C\n // (Group D-1, 2026-05-04). Mirrors Lifeline's QARecommendation.resolvedBy\n // column. Optional because not every status transition carries a\n // resolution actor.\n resolvedBy String?\n closedAt DateTime?\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n qaRun QARun @relation(fields: [qaRunId], references: [id], onDelete: Cascade)\n signals RawSignal[]\n attempts QAFixAttempt[]\n artifacts AuditArtifact[]\n reverts QARevert[]\n\n @@index([projectId, status])\n @@index([projectId, finalBucket])\n @@index([projectId, createdAt(sort: Desc)])\n @@map(\"qa_tickets\")\n}\n\nenum SurfaceCategory {\n CODE_HEALTH\n FUNCTIONAL_CORRECTNESS\n UI_UX_INTEGRITY\n PERF_RESILIENCE\n SECURITY\n COMPLIANCE_AUDIT\n MULTI_TENANT_SAFETY\n OPERATIONAL_HEALTH\n}\n\nenum Severity {\n CRITICAL\n HIGH\n MEDIUM\n LOW\n}\n\nenum RiskTier {\n LOW\n MEDIUM\n HIGH\n NEVER\n}\n\nenum TicketStatus {\n OPEN\n INVESTIGATING\n AUTHORING\n PR_OPEN\n MERGED\n REGRESSED_REVERTED\n CLOSED_WONTFIX\n CLOSED_RESOLVED\n ESCALATED\n}\n\nenum ReviewBucket {\n PASS\n FAIL\n ESCALATION\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer E: Auto-fix attempts\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel QAFixAttempt {\n id String @id @default(cuid())\n qaTicketId String\n projectId String\n attemptNumber Int // 1..N for iterative retry\n\n // Prompt + diff\n promptText String @db.Text\n diff String? @db.Text\n diffSizeBytes Int?\n modelName String?\n modelVersion String?\n inputTokens Int?\n outputTokens Int?\n costCents Int?\n\n // Pre-merge verification\n preVerifyResult Json? // sandbox run results\n preVerifyPassed Boolean?\n\n // Dispatch outcome\n branchName String?\n prUrl String?\n prNumber Int?\n dispatchStatus AttemptStatus @default(AUTHORING)\n\n // Post-merge verification (set after merge)\n mergedAt DateTime?\n postVerifyResult Json?\n regressionDetected Boolean @default(false)\n autoRevertedAt DateTime?\n humanEditLines Int?\n fixSuccessScore Float? // -1.0 to 1.0\n\n attemptedAt DateTime @default(now())\n closedAt DateTime?\n\n ticket QATicket @relation(fields: [qaTicketId], references: [id], onDelete: Cascade)\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n artifacts AuditArtifact[]\n\n @@index([qaTicketId, attemptNumber])\n @@index([projectId, attemptedAt(sort: Desc)])\n @@map(\"qa_fix_attempts\")\n}\n\nenum AttemptStatus {\n AUTHORING\n PRE_VERIFY_FAILED\n PRE_VERIFY_PASSED\n PR_OPENED\n AUTO_MERGED\n HUMAN_MERGED\n REJECTED\n REGRESSED_REVERTED\n FAILED\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer F: Trust scores + drift + RAG corpus\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel ClassificationTrust {\n id String @id @default(cuid())\n projectId String\n classification String // matches QATicket.classification\n surface SurfaceCategory\n\n // Rolling stats (30-day window)\n attemptsLast30d Int @default(0)\n mergedClean Int @default(0)\n mergedWithEdits Int @default(0)\n regressed Int @default(0)\n reverted Int @default(0)\n\n // Computed\n successScore Float @default(0) // -1.0 to 1.0\n trustPercent Int @default(0) // 0 to 100, operator-facing\n autoMergeEligible Boolean @default(false)\n\n lastComputedAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@unique([projectId, classification, surface])\n @@map(\"classification_trust\")\n}\n\n// Successful past fixes used as in-context examples for new dispatches.\n// Embedding column is Bytes for v1; QAP-076 (Sprint 7) switches to pgvector.\nmodel RAGCorpus {\n id String @id @default(cuid())\n projectId String\n classification String\n surface SurfaceCategory\n\n promptText String @db.Text // the prompt that produced the fix\n diff String @db.Text // the unified diff that worked\n embedding Bytes? // for semantic search\n\n fixAttemptId String // back-reference to the source attempt\n outcomeQuality Float // priority weight; declines if later usage shows problems\n addedAt DateTime @default(now())\n\n @@index([projectId, classification, surface])\n @@map(\"rag_corpus\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Audit artifacts — proof-of-work, not \"trust me\"\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel AuditArtifact {\n id String @id @default(cuid())\n projectId String\n qaTicketId String?\n qaFixAttemptId String?\n\n artifactType ArtifactType\n stage ArtifactStage // which lifecycle stage produced this\n\n // Storage references (S3-style)\n storageBackend String // \"s3\" | \"supabase-storage\" | \"filesystem\"\n storageKey String // canonical key/path\n contentType String\n sizeBytes Int\n contentHash String\n\n // Metadata\n meta Json // route-specific (e.g., screenshot timestamp, video duration)\n retentionUntil DateTime? // computed from project compliance mode\n\n capturedAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n ticket QATicket? @relation(fields: [qaTicketId], references: [id])\n fixAttempt QAFixAttempt? @relation(fields: [qaFixAttemptId], references: [id])\n\n @@index([qaTicketId])\n @@index([projectId, capturedAt(sort: Desc)])\n @@map(\"audit_artifacts\")\n}\n\nenum ArtifactType {\n SCREENSHOT\n VIDEO\n DOM_SNAPSHOT\n CONSOLE_LOG\n NETWORK_LOG\n TELEMETRY_SLICE\n CODE_SNAPSHOT\n PROMPT_TEXT\n DIFF\n REASONING_TRACE\n REVIEWER_OUTPUT\n REPLAY_BUNDLE\n}\n\nenum ArtifactStage {\n DETECTION\n INVESTIGATION\n TICKET_CREATION\n AUTHORING\n PRE_MERGE_VERIFICATION\n POST_MERGE_VERIFICATION\n ARCHIVAL\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Policies + safety (Sprint 8 — QAP-080..089 — fills these out)\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel Policy {\n id String @id @default(cuid())\n projectId String\n policyType PolicyType\n\n // Path tier rules: globs → tier\n pathRules Json // [{ glob: \"src/auth/**\", tier: \"HIGH\" }, …]\n classificationOverrides Json // { \"PRODUCT_BUG\": \"MEDIUM\", … }\n\n // Trust thresholds\n autoMergeTrustThreshold Int @default(70)\n autoMergeMediumThreshold Int @default(85)\n\n // Concurrency\n dailyDispatchLimit Int @default(50)\n // Fix-attempt retry limit per ticket. Default 1 per ADR-0006 — try once,\n // route to FAIL on failure (conservative trust posture). Configurable per\n // project; recommended range 1–4. Higher values raise auto-fix throughput\n // on iterative-success classifications at the cost of Anthropic spend and\n // delayed operator awareness of classifier weakness.\n perTicketAttemptLimit Int @default(1)\n\n // Time-of-day\n freezeWindowsCron String[] @default([])\n\n // Escalation triggers\n escalationTriggers Json\n\n updatedAt DateTime @updatedAt\n updatedBy String\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@index([projectId])\n @@map(\"policies\")\n}\n\nenum PolicyType {\n RISK_TIER\n CONCURRENCY\n TRUST_THRESHOLD\n ESCALATION\n}\n\nmodel ProjectModeLog {\n id String @id @default(cuid())\n projectId String\n fromMode ProjectMode\n toMode ProjectMode\n reason String @db.Text\n changedBy String // \"auto-promotion\" | userId\n changedAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@index([projectId, changedAt(sort: Desc)])\n @@map(\"project_mode_log\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Cost + telemetry\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel SpendLedger {\n id String @id @default(cuid())\n projectId String\n qaFixAttemptId String?\n\n // Per-event spend\n vendor String // \"anthropic\" | \"openai\" | \"github-actions\" | \"supabase-storage\"\n eventType String // \"claude_dispatch\" | \"claude_review\" | \"embedding\" | \"storage_write\"\n costCents Int // signed; can be credit\n meta Json\n\n occurredAt DateTime @default(now())\n\n @@index([projectId, occurredAt(sort: Desc)])\n @@index([projectId, vendor, occurredAt])\n @@map(\"spend_ledger\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer F: Learning — patterns + reverts (QAP-019D + QAP-019E, Group D-2)\n// ═══════════════════════════════════════════════════════════════════════════\n\n// QAPattern is a learned failure signature — the orchestrator's Triage stage\n// (Sprint 6) bumps occurrenceCount + lastSeenAt when a new RawSignal matches\n// a known signature. Lifeline-shape preserved verbatim per Group D-2 plan\n// decision #1: re-ranker fields (fixSuccessScore / fixOutcomeSampleSize /\n// lastWeightedAt) stay even though Derwin's re-ranker today reads\n// classification-trust-store. Preserves optionality for Sprint 4+ consumers\n// and removes shim translation work in QAP-019G.\n//\n// (signature, projectId) is unique — the same failure signature in different\n// projects is two distinct rows. Lifeline-style.\nmodel QAPattern {\n id String @id @default(cuid())\n projectId String\n signature String\n classification QAFailureClass\n firstSeenAt DateTime @default(now())\n lastSeenAt DateTime @default(now())\n occurrenceCount Int @default(1)\n affectedTickets String[] @default([])\n trendDirection String @default(\"stable\") // improving | stable | degrading\n status QAPatternStatus @default(OPEN)\n resolvedAt DateTime?\n promotedToLintAt DateTime?\n promotedLintPR String?\n archivedAt DateTime?\n\n // Re-ranker fields (Lifeline-shape preserved, Decision 1).\n fixSuccessScore Float?\n fixOutcomeSampleSize Int @default(0)\n lastWeightedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@unique([projectId, signature])\n @@index([projectId, lastSeenAt(sort: Desc)])\n @@index([status])\n @@map(\"qa_patterns\")\n}\n\nenum QAPatternStatus {\n OPEN\n RESOLVED\n ARCHIVED\n}\n\nenum QAFailureClass {\n SELECTOR\n TIMING\n TEST_DATA\n PRODUCT_BUG\n SCHEMA_DRIFT\n UNKNOWN\n}\n\n// QARevert logs a manual or auto-revert of a fix-forward commit. The route\n// handler does NOT execute `git revert` — it only records the operator's\n// action. Restore is the inverse: a fix-forward commit lands and the operator\n// flips restoredAt + restoredBy + restoredBySha.\n//\n// Decision 2: keep qaRunId FK only. Lifeline's qaResultId pointed at the\n// QARunResult table, which Derwin doesn't have under Option β (results are\n// RawSignal records). Reverts are at the ticket+commit level, not the\n// per-test-result level.\nmodel QARevert {\n id String @id @default(cuid())\n projectId String\n ticketId String\n revertedFromSha String\n revertedToSha String?\n revertedAt DateTime @default(now())\n revertedBy String\n reason String @db.Text\n qaRunId String?\n\n // Restore audit trail — set when the fix-forward commit ships.\n restoredAt DateTime?\n restoredBy String?\n restoredBySha String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n ticket QATicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)\n run QARun? @relation(fields: [qaRunId], references: [id], onDelete: SetNull)\n\n @@index([projectId, revertedAt(sort: Desc)])\n @@index([ticketId])\n @@map(\"qa_reverts\")\n}\n\n// QAUniformity stores per-page rule outcomes from the invariant crawler\n// (uniformity audit). Lifeline calls this `QAUniformityAudit`; renamed to\n// `QAUniformity` per Group D-3 plan Decision 5 (drop `Audit` suffix to match\n// other Derwin model names). Bulk-inserted by /api/qa/uniformity POST and\n// aggregated by GET. Optionally tied to a QARun (most ingestions are run-\n// scoped; manual operator runs may not have a qaRunId).\nmodel QAUniformity {\n id String @id @default(cuid())\n projectId String\n qaRunId String?\n pagePath String\n ruleName String\n ruleCategory String\n status QAUniformityStatus\n violationDetail String? @db.Text\n createdAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n run QARun? @relation(fields: [qaRunId], references: [id], onDelete: SetNull)\n\n @@index([projectId, createdAt(sort: Desc)])\n @@index([projectId, ruleName, status])\n @@index([projectId, pagePath])\n @@map(\"qa_uniformities\")\n}\n\nenum QAUniformityStatus {\n PASSED\n FAILED\n SKIPPED\n}\n",
609
- "inlineSchemaHash": "bcf38b11fdcdf6bb00af9a2f237413ff4bd4b7872c491bbc83e479b76227a654",
610
+ "inlineSchema": "// Derwin platform schema — Postgres + Prisma.\n//\n// 14 models, project-namespaced. Per ADR-0005 (Prisma + Postgres + project-\n// namespaced schema). Reference: technical spec §3.\n//\n// Multi-tenancy convention: every project-scoped table has a `projectId String`\n// FK to Project. Cross-project leakage is the leading regression risk; queries\n// MUST filter on projectId. Per ADR-0005, the trust-mitigation gates are:\n// 1. A lint or static check that flags Prisma queries missing projectId\n// filters on tables that have one (added in Sprint 8 / risk-tier work).\n// 2. A multi-project broken-fixture E2E that asserts isolation\n// (added in Sprint 13 / QAP-130).\n//\n// pgvector convention: `embedding Bytes?` on RAGCorpus is the v1 storage type.\n// QAP-076 (Sprint 7 — RAG corpus add) switches it to a pgvector `vector(1536)`\n// column once the extension is enabled in QAP-036.\n//\n// Multi-schema: every model + enum is in the `derwin` Postgres schema (not\n// `public`). This isolates Derwin's tables from each consumer's existing\n// schema — e.g., Lifeline's pre-extraction `public.qa_fix_attempts` stays\n// untouched while Derwin's tables live at `derwin.qa_fix_attempts`. Required\n// for the multi-tenant boundary rule: each consumer = ONE Project row, but\n// each consumer also has its own pre-existing schema in `public`. (Added in\n// QAP-019G when extracting Derwin into Lifeline first revealed the conflict.)\n\n// Generator output is INSIDE the package so the generated client ships\n// in the published tarball alongside dist/. Consumer code does\n// `import { PrismaClient } from '@derwinjs/db'` (or '@derwinjs/db/client') —\n// see packages/db/src/index.ts re-exports. This is required for\n// multi-tenant correctness: each Derwin consumer (Lifeline, Side Piece,\n// Bolt, ...) already has its own @prisma/client generated against its\n// own schema; @derwinjs/db can't piggyback on the consumer's @prisma/client\n// or types break (Lifeline's PrismaClient doesn't have qAUniformity etc).\n//\n// binaryTargets ships explicit cross-platform binaries so the published\n// tarball works regardless of where the publish workflow ran:\n// - darwin-arm64: Apple Silicon Mac dev (the Derwin team's primary local)\n// - rhel-openssl-3.0.x: x86 Linux on OpenSSL 3 (Vercel default, most CI)\n// - linux-arm64-openssl-3.0.x: ARM Linux (AWS Graviton, etc.)\n// \"native\" is intentionally omitted — relying on it makes the published\n// tarball platform-dependent (whatever the publisher was running on).\n// Intel Mac users (rare) add `darwin` locally via a re-generate.\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../prisma-client\"\n binaryTargets = [\"darwin-arm64\", \"rhel-openssl-3.0.x\", \"linux-arm64-openssl-3.0.x\"]\n previewFeatures = [\"multiSchema\"]\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n schemas = [\"derwin\"]\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Multi-tenant root\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel Project {\n id String @id @default(cuid())\n name String\n slug String @unique\n type ProjectType\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Operating mode (Observe / TicketOnly / Author / Auto) — see product brief §11.2\n mode ProjectMode @default(OBSERVE)\n modeChangedAt DateTime @default(now())\n modeChangedBy String?\n\n // Per-project quotas + budgets\n monthlyBudgetCents Int @default(10000) // $100 default\n dailyDispatchLimit Int @default(50)\n\n // GitHub integration (QAP-019C / Group D-1, 2026-05-04).\n //\n // `repoFullName` is the canonical \"<owner>/<repo>\" identifier (e.g.,\n // \"RoryLYN/derwin\"). The github-webhook route uses it to resolve a\n // payload's `repository.full_name` → projectId. Optional + unique\n // because: not every Derwin project has a GitHub repo (Sprint 4+ may\n // onboard products that don't run on GitHub); but a given repo can map\n // to at most one Derwin project (otherwise webhook payloads would be\n // ambiguous).\n //\n // `webhookSecret` is the per-project HMAC-SHA256 secret GitHub uses to\n // sign webhook payloads. Optional so projects without GitHub integration\n // carry no secret. The webhook route passes this to\n // `QAAuthAdapter.verifyWebhook(headers, body, secret)` to authenticate\n // payloads before any DB writes.\n repoFullName String? @unique\n webhookSecret String?\n\n // Relations\n profile ProjectProfile?\n runs QARun[]\n tickets QATicket[]\n fixAttempts QAFixAttempt[]\n auditArtifacts AuditArtifact[]\n policies Policy[]\n trustScores ClassificationTrust[]\n modeLog ProjectModeLog[]\n patterns QAPattern[]\n reverts QARevert[]\n uniformities QAUniformity[]\n\n @@map(\"projects\")\n @@schema(\"derwin\")\n}\n\nenum ProjectType {\n SAAS\n ECOMMERCE\n MARKETING\n ERP\n ANALYTICS\n INFRA\n CONTENT\n\n @@schema(\"derwin\")\n}\n\nenum ProjectMode {\n OBSERVE // run but write nothing\n TICKET_ONLY // write tickets, no fix authoring\n AUTHOR // tickets + fix authoring, all to PR\n AUTO // full autonomy per risk tier policies\n\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer A: Project Profile (the Knowledge Base)\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel ProjectProfile {\n id String @id @default(cuid())\n projectId String @unique\n type ProjectType\n\n domainOntology Json // entities[], actions[], roles[]\n riskProfile Json // 8-vector weights across surface categories\n criticalFlows Json // named user journeys with steps + assertions\n glossary Json // term → definition; disambiguates \"ticket\"/\"tenant\"/etc.\n dependencies Json // third-party services that need integration testing\n complianceTags String[] @default([])\n\n ingestedDocsHash String // hash of (doc, version) tuples for change detection\n\n lastIngestedAt DateTime\n lastEvolvedAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n evolutionLog ProfileEvolutionLog[]\n\n @@map(\"project_profiles\")\n @@schema(\"derwin\")\n}\n\nmodel IngestedDoc {\n id String @id @default(cuid())\n projectId String\n source String // \"filesystem\" | \"notion\" | \"google-docs\" | etc.\n sourcePath String // canonical path/URL of the doc\n contentHash String\n contentSize Int\n ingestedAt DateTime @default(now())\n ontologyDelta Json? // what this doc contributed to the Profile\n\n @@unique([projectId, sourcePath, contentHash])\n @@index([projectId, sourcePath])\n @@map(\"ingested_docs\")\n @@schema(\"derwin\")\n}\n\nmodel ProfileEvolutionLog {\n id String @id @default(cuid())\n profileId String\n triggerType String // \"FIX_OUTCOME\" | \"SPRINT_INGEST\" | \"DEPLOY\" | \"OPERATOR_OVERRIDE\"\n delta Json // what changed\n rationale String @db.Text\n appliedAt DateTime @default(now())\n appliedBy String // \"agent\" | userId\n\n profile ProjectProfile @relation(fields: [profileId], references: [id], onDelete: Cascade)\n\n @@index([profileId, appliedAt(sort: Desc)])\n @@map(\"profile_evolution_log\")\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer B/C: Discovery + Execution\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel QARun {\n id String @id @default(cuid())\n projectId String\n triggeredBy String // \"cron\" | \"operator:<userId>\" | \"webhook\" | \"<routeName>\"\n triggerType RunTrigger\n scope Json // what surfaces, which routes, which subset\n startedAt DateTime @default(now())\n completedAt DateTime?\n status RunStatus @default(IN_PROGRESS)\n\n // Outcome counts (denormalized for dashboard reads)\n signalsRaised Int @default(0)\n ticketsCreated Int @default(0)\n attemptsLaunched Int @default(0)\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n signals RawSignal[]\n tickets QATicket[]\n reverts QARevert[]\n uniformities QAUniformity[]\n\n @@index([projectId, startedAt(sort: Desc)])\n @@map(\"qa_runs\")\n @@schema(\"derwin\")\n}\n\nenum RunTrigger {\n CRON\n OPERATOR_ON_DEMAND\n WEBHOOK\n CONTINUOUS // for routes that run as a daemon (telemetry mining etc.)\n AGENT // QAP-019F (Group D-3): standalone QA agent (browser tool) ingestion\n\n @@schema(\"derwin\")\n}\n\nenum RunStatus {\n IN_PROGRESS\n COMPLETED\n FAILED\n CANCELLED\n\n @@schema(\"derwin\")\n}\n\n// A RawSignal is what a Discovery route emits. It hasn't been investigated /\n// classified / ticketed yet — Triage takes signals and produces tickets.\nmodel RawSignal {\n id String @id @default(cuid())\n qaRunId String\n routeName String // which Discovery route fired\n signalType String // \"test_failure\" | \"telemetry_anomaly\" | \"rage_click_cluster\" | etc.\n rawData Json // route-specific payload (test results, metric snapshot, etc.)\n rawArtifactRefs Json // pointers to S3 artifacts (videos, screenshots)\n\n // Investigation + classification happen later; null until Triage processes.\n qaTicketId String? @unique\n investigatedAt DateTime?\n\n capturedAt DateTime @default(now())\n\n qaRun QARun @relation(fields: [qaRunId], references: [id], onDelete: Cascade)\n ticket QATicket? @relation(fields: [qaTicketId], references: [id])\n\n @@index([qaRunId])\n @@index([routeName, capturedAt(sort: Desc)])\n @@map(\"raw_signals\")\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer D: Triage — the structured QA ticket\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel QATicket {\n id String @id @default(cuid())\n projectId String\n qaRunId String\n\n // Identity / linkage\n externalRef String? // if mirrored to Linear/Jira/Lifeline-native, the ID there (single primary cross-system ref; legacy from v0.4)\n externalSystem String? // \"linear\" | \"jira\" | \"github-issues\" | \"lifeline-native\"\n originatingTicketRef String? // the FEATURE ticket that introduced the affected code, if known\n\n // Cross-link stubs posted to consumer ticket systems per ADR-0008 (architecture\n // pivot 2026-05-01). Array of { system: 'github', repo: string, issueNumber: number,\n // url: string, state: 'open' | 'closed' }. Multiple stubs supported (e.g., post\n // to both GitHub Issues and Linear). QAP-018 (Sprint 2) populates this on\n // createQATicket via the LifelineTicketAdapter.\n crossLinkRefs Json @default(\"[]\")\n\n // Canonical deep-link URL for this ticket in the Derwin-owned dashboard per\n // ADR-0008. Populated by QAP-018B's createQATicket from DERWIN_DASHBOARD_URL\n // env + ticket id. The cross-link stubs in crossLinkRefs[] embed this URL in\n // their bodies so operators triaging in the consumer ticket system can click\n // through to the rich Derwin record.\n dashboardUrl String?\n\n // Classification\n surface SurfaceCategory\n classification String // SELECTOR | PRODUCT_BUG | TIMING | etc., extensible\n severity Severity\n riskTier RiskTier\n status TicketStatus @default(OPEN)\n\n // Content (from the structured ticket format in product brief §6.3)\n title String\n reproSteps Json // array of step objects\n suspectedRootCause String @db.Text\n blastRadius Json // affected files / tests / pages\n\n // Author/dispatch decision\n proposedFixApproach String @db.Text\n autoMergeEligible Boolean @default(false)\n autoMergeRationale String? @db.Text\n\n // Final bucket assignment (populated after lifecycle completes)\n finalBucket ReviewBucket?\n bucketReason String? @db.Text\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Optional audit field — operator userId or system actor name (e.g.,\n // \"qa-auto-fix-webhook\") that resolved this ticket. Set on status\n // transitions to RESOLVED, REJECTED, or REGRESSED_REVERTED. QAP-019C\n // (Group D-1, 2026-05-04). Mirrors Lifeline's QARecommendation.resolvedBy\n // column. Optional because not every status transition carries a\n // resolution actor.\n resolvedBy String?\n closedAt DateTime?\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n qaRun QARun @relation(fields: [qaRunId], references: [id], onDelete: Cascade)\n signals RawSignal[]\n attempts QAFixAttempt[]\n artifacts AuditArtifact[]\n reverts QARevert[]\n\n @@index([projectId, status])\n @@index([projectId, finalBucket])\n @@index([projectId, createdAt(sort: Desc)])\n @@map(\"qa_tickets\")\n @@schema(\"derwin\")\n}\n\nenum SurfaceCategory {\n CODE_HEALTH\n FUNCTIONAL_CORRECTNESS\n UI_UX_INTEGRITY\n PERF_RESILIENCE\n SECURITY\n COMPLIANCE_AUDIT\n MULTI_TENANT_SAFETY\n OPERATIONAL_HEALTH\n\n @@schema(\"derwin\")\n}\n\nenum Severity {\n CRITICAL\n HIGH\n MEDIUM\n LOW\n\n @@schema(\"derwin\")\n}\n\nenum RiskTier {\n LOW\n MEDIUM\n HIGH\n NEVER\n\n @@schema(\"derwin\")\n}\n\nenum TicketStatus {\n OPEN\n INVESTIGATING\n AUTHORING\n PR_OPEN\n MERGED\n REGRESSED_REVERTED\n CLOSED_WONTFIX\n CLOSED_RESOLVED\n ESCALATED\n\n @@schema(\"derwin\")\n}\n\nenum ReviewBucket {\n PASS\n FAIL\n ESCALATION\n\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer E: Auto-fix attempts\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel QAFixAttempt {\n id String @id @default(cuid())\n qaTicketId String\n projectId String\n attemptNumber Int // 1..N for iterative retry\n\n // Prompt + diff\n promptText String @db.Text\n diff String? @db.Text\n diffSizeBytes Int?\n modelName String?\n modelVersion String?\n inputTokens Int?\n outputTokens Int?\n costCents Int?\n\n // Pre-merge verification\n preVerifyResult Json? // sandbox run results\n preVerifyPassed Boolean?\n\n // Dispatch outcome\n branchName String?\n prUrl String?\n prNumber Int?\n dispatchStatus AttemptStatus @default(AUTHORING)\n\n // Post-merge verification (set after merge)\n mergedAt DateTime?\n postVerifyResult Json?\n regressionDetected Boolean @default(false)\n autoRevertedAt DateTime?\n humanEditLines Int?\n fixSuccessScore Float? // -1.0 to 1.0\n\n attemptedAt DateTime @default(now())\n closedAt DateTime?\n\n ticket QATicket @relation(fields: [qaTicketId], references: [id], onDelete: Cascade)\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n artifacts AuditArtifact[]\n\n @@index([qaTicketId, attemptNumber])\n @@index([projectId, attemptedAt(sort: Desc)])\n @@map(\"qa_fix_attempts\")\n @@schema(\"derwin\")\n}\n\nenum AttemptStatus {\n AUTHORING\n PRE_VERIFY_FAILED\n PRE_VERIFY_PASSED\n PR_OPENED\n AUTO_MERGED\n HUMAN_MERGED\n REJECTED\n REGRESSED_REVERTED\n FAILED\n\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer F: Trust scores + drift + RAG corpus\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel ClassificationTrust {\n id String @id @default(cuid())\n projectId String\n classification String // matches QATicket.classification\n surface SurfaceCategory\n\n // Rolling stats (30-day window)\n attemptsLast30d Int @default(0)\n mergedClean Int @default(0)\n mergedWithEdits Int @default(0)\n regressed Int @default(0)\n reverted Int @default(0)\n\n // Computed\n successScore Float @default(0) // -1.0 to 1.0\n trustPercent Int @default(0) // 0 to 100, operator-facing\n autoMergeEligible Boolean @default(false)\n\n lastComputedAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@unique([projectId, classification, surface])\n @@map(\"classification_trust\")\n @@schema(\"derwin\")\n}\n\n// Successful past fixes used as in-context examples for new dispatches.\n// Embedding column is Bytes for v1; QAP-076 (Sprint 7) switches to pgvector.\nmodel RAGCorpus {\n id String @id @default(cuid())\n projectId String\n classification String\n surface SurfaceCategory\n\n promptText String @db.Text // the prompt that produced the fix\n diff String @db.Text // the unified diff that worked\n embedding Bytes? // for semantic search\n\n fixAttemptId String // back-reference to the source attempt\n outcomeQuality Float // priority weight; declines if later usage shows problems\n addedAt DateTime @default(now())\n\n @@index([projectId, classification, surface])\n @@map(\"rag_corpus\")\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Audit artifacts — proof-of-work, not \"trust me\"\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel AuditArtifact {\n id String @id @default(cuid())\n projectId String\n qaTicketId String?\n qaFixAttemptId String?\n\n artifactType ArtifactType\n stage ArtifactStage // which lifecycle stage produced this\n\n // Storage references (S3-style)\n storageBackend String // \"s3\" | \"supabase-storage\" | \"filesystem\"\n storageKey String // canonical key/path\n contentType String\n sizeBytes Int\n contentHash String\n\n // Metadata\n meta Json // route-specific (e.g., screenshot timestamp, video duration)\n retentionUntil DateTime? // computed from project compliance mode\n\n capturedAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n ticket QATicket? @relation(fields: [qaTicketId], references: [id])\n fixAttempt QAFixAttempt? @relation(fields: [qaFixAttemptId], references: [id])\n\n @@index([qaTicketId])\n @@index([projectId, capturedAt(sort: Desc)])\n @@map(\"audit_artifacts\")\n @@schema(\"derwin\")\n}\n\nenum ArtifactType {\n SCREENSHOT\n VIDEO\n DOM_SNAPSHOT\n CONSOLE_LOG\n NETWORK_LOG\n TELEMETRY_SLICE\n CODE_SNAPSHOT\n PROMPT_TEXT\n DIFF\n REASONING_TRACE\n REVIEWER_OUTPUT\n REPLAY_BUNDLE\n\n @@schema(\"derwin\")\n}\n\nenum ArtifactStage {\n DETECTION\n INVESTIGATION\n TICKET_CREATION\n AUTHORING\n PRE_MERGE_VERIFICATION\n POST_MERGE_VERIFICATION\n ARCHIVAL\n\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Policies + safety (Sprint 8 — QAP-080..089 — fills these out)\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel Policy {\n id String @id @default(cuid())\n projectId String\n policyType PolicyType\n\n // Path tier rules: globs → tier\n pathRules Json // [{ glob: \"src/auth/**\", tier: \"HIGH\" }, …]\n classificationOverrides Json // { \"PRODUCT_BUG\": \"MEDIUM\", … }\n\n // Trust thresholds\n autoMergeTrustThreshold Int @default(70)\n autoMergeMediumThreshold Int @default(85)\n\n // Concurrency\n dailyDispatchLimit Int @default(50)\n // Fix-attempt retry limit per ticket. Default 1 per ADR-0006 — try once,\n // route to FAIL on failure (conservative trust posture). Configurable per\n // project; recommended range 1–4. Higher values raise auto-fix throughput\n // on iterative-success classifications at the cost of Anthropic spend and\n // delayed operator awareness of classifier weakness.\n perTicketAttemptLimit Int @default(1)\n\n // Time-of-day\n freezeWindowsCron String[] @default([])\n\n // Escalation triggers\n escalationTriggers Json\n\n updatedAt DateTime @updatedAt\n updatedBy String\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@index([projectId])\n @@map(\"policies\")\n @@schema(\"derwin\")\n}\n\nenum PolicyType {\n RISK_TIER\n CONCURRENCY\n TRUST_THRESHOLD\n ESCALATION\n\n @@schema(\"derwin\")\n}\n\nmodel ProjectModeLog {\n id String @id @default(cuid())\n projectId String\n fromMode ProjectMode\n toMode ProjectMode\n reason String @db.Text\n changedBy String // \"auto-promotion\" | userId\n changedAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@index([projectId, changedAt(sort: Desc)])\n @@map(\"project_mode_log\")\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Cost + telemetry\n// ═══════════════════════════════════════════════════════════════════════════\n\nmodel SpendLedger {\n id String @id @default(cuid())\n projectId String\n qaFixAttemptId String?\n\n // Per-event spend\n vendor String // \"anthropic\" | \"openai\" | \"github-actions\" | \"supabase-storage\"\n eventType String // \"claude_dispatch\" | \"claude_review\" | \"embedding\" | \"storage_write\"\n costCents Int // signed; can be credit\n meta Json\n\n occurredAt DateTime @default(now())\n\n @@index([projectId, occurredAt(sort: Desc)])\n @@index([projectId, vendor, occurredAt])\n @@map(\"spend_ledger\")\n @@schema(\"derwin\")\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Layer F: Learning — patterns + reverts (QAP-019D + QAP-019E, Group D-2)\n// ═══════════════════════════════════════════════════════════════════════════\n\n// QAPattern is a learned failure signature — the orchestrator's Triage stage\n// (Sprint 6) bumps occurrenceCount + lastSeenAt when a new RawSignal matches\n// a known signature. Lifeline-shape preserved verbatim per Group D-2 plan\n// decision #1: re-ranker fields (fixSuccessScore / fixOutcomeSampleSize /\n// lastWeightedAt) stay even though Derwin's re-ranker today reads\n// classification-trust-store. Preserves optionality for Sprint 4+ consumers\n// and removes shim translation work in QAP-019G.\n//\n// (signature, projectId) is unique — the same failure signature in different\n// projects is two distinct rows. Lifeline-style.\nmodel QAPattern {\n id String @id @default(cuid())\n projectId String\n signature String\n classification QAFailureClass\n firstSeenAt DateTime @default(now())\n lastSeenAt DateTime @default(now())\n occurrenceCount Int @default(1)\n affectedTickets String[] @default([])\n trendDirection String @default(\"stable\") // improving | stable | degrading\n status QAPatternStatus @default(OPEN)\n resolvedAt DateTime?\n promotedToLintAt DateTime?\n promotedLintPR String?\n archivedAt DateTime?\n\n // Re-ranker fields (Lifeline-shape preserved, Decision 1).\n fixSuccessScore Float?\n fixOutcomeSampleSize Int @default(0)\n lastWeightedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n\n @@unique([projectId, signature])\n @@index([projectId, lastSeenAt(sort: Desc)])\n @@index([status])\n @@map(\"qa_patterns\")\n @@schema(\"derwin\")\n}\n\nenum QAPatternStatus {\n OPEN\n RESOLVED\n ARCHIVED\n\n @@schema(\"derwin\")\n}\n\nenum QAFailureClass {\n SELECTOR\n TIMING\n TEST_DATA\n PRODUCT_BUG\n SCHEMA_DRIFT\n UNKNOWN\n\n @@schema(\"derwin\")\n}\n\n// QARevert logs a manual or auto-revert of a fix-forward commit. The route\n// handler does NOT execute `git revert` — it only records the operator's\n// action. Restore is the inverse: a fix-forward commit lands and the operator\n// flips restoredAt + restoredBy + restoredBySha.\n//\n// Decision 2: keep qaRunId FK only. Lifeline's qaResultId pointed at the\n// QARunResult table, which Derwin doesn't have under Option β (results are\n// RawSignal records). Reverts are at the ticket+commit level, not the\n// per-test-result level.\nmodel QARevert {\n id String @id @default(cuid())\n projectId String\n ticketId String\n revertedFromSha String\n revertedToSha String?\n revertedAt DateTime @default(now())\n revertedBy String\n reason String @db.Text\n qaRunId String?\n\n // Restore audit trail — set when the fix-forward commit ships.\n restoredAt DateTime?\n restoredBy String?\n restoredBySha String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n ticket QATicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)\n run QARun? @relation(fields: [qaRunId], references: [id], onDelete: SetNull)\n\n @@index([projectId, revertedAt(sort: Desc)])\n @@index([ticketId])\n @@map(\"qa_reverts\")\n @@schema(\"derwin\")\n}\n\n// QAUniformity stores per-page rule outcomes from the invariant crawler\n// (uniformity audit). Lifeline calls this `QAUniformityAudit`; renamed to\n// `QAUniformity` per Group D-3 plan Decision 5 (drop `Audit` suffix to match\n// other Derwin model names). Bulk-inserted by /api/qa/uniformity POST and\n// aggregated by GET. Optionally tied to a QARun (most ingestions are run-\n// scoped; manual operator runs may not have a qaRunId).\nmodel QAUniformity {\n id String @id @default(cuid())\n projectId String\n qaRunId String?\n pagePath String\n ruleName String\n ruleCategory String\n status QAUniformityStatus\n violationDetail String? @db.Text\n createdAt DateTime @default(now())\n\n project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)\n run QARun? @relation(fields: [qaRunId], references: [id], onDelete: SetNull)\n\n @@index([projectId, createdAt(sort: Desc)])\n @@index([projectId, ruleName, status])\n @@index([projectId, pagePath])\n @@map(\"qa_uniformities\")\n @@schema(\"derwin\")\n}\n\nenum QAUniformityStatus {\n PASSED\n FAILED\n SKIPPED\n\n @@schema(\"derwin\")\n}\n",
611
+ "inlineSchemaHash": "4fa036f75087a439bd3685ce630eb418553b67a25b7abe2526c98f4e56e239e9",
610
612
  "copyEngine": true
611
613
  }
612
614
  config.dirname = '/'