@derwinjs/db 0.3.0 → 0.5.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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Sprint 4 Phase 5 smoke — exercises the ProjectProfileStore against the real
3
+ * local Postgres by upserting the canonical Lifeline Profile from JSON.
4
+ *
5
+ * Run: pnpm --filter @derwinjs/db smoke:seed-lifeline-profile
6
+ *
7
+ * Prereqs:
8
+ * - Local Postgres running on port 5433 (per Sprint 1 docker setup)
9
+ * - DATABASE_URL set in packages/db/.env
10
+ * - Migrations applied (Sprint 4 Phase 1 ProjectProfile model present)
11
+ * - Seed run at least once so the Lifeline project row exists
12
+ *
13
+ * What this proves:
14
+ * 1. lifeline-profile.json parses and conforms to PutProfileInput
15
+ * 2. createPrismaProjectProfileStore().putProfile round-trips entities,
16
+ * criticalFlows, glossary, dependencies, complianceTags
17
+ * 3. getProfile returns identical counts (no silent JSON column truncation)
18
+ *
19
+ * Idempotent: putProfile is upsert-shaped, so re-running just refreshes
20
+ * lastEvolvedAt/updatedAt without polluting the table. No cleanup needed.
21
+ *
22
+ * Prints "SPRINT4-PHASE5 PROFILE READY ✓" on success. Exits 1 on any failure.
23
+ */
24
+ import { readFileSync } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { PrismaClient } from '../prisma.js';
28
+ import { createPrismaProjectProfileStore } from '../project-profile-store.js';
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+ const PROFILE_JSON_PATH = join(__dirname, '..', '..', 'seed-data', 'lifeline-profile.json');
32
+ const prisma = new PrismaClient();
33
+ const store = createPrismaProjectProfileStore({ prisma });
34
+ async function main() {
35
+ // ─── Step 1: locate Lifeline project ────────────────────────────────
36
+ const lifeline = await prisma.project.findUnique({ where: { slug: 'lifeline' } });
37
+ if (!lifeline) {
38
+ throw new Error("Lifeline project (slug='lifeline') not found. Run `pnpm --filter @derwinjs/db db:seed` first.");
39
+ }
40
+ console.log(`✓ Found Lifeline project (id=${lifeline.id})`);
41
+ // ─── Step 2: load + parse the JSON ──────────────────────────────────
42
+ const raw = readFileSync(PROFILE_JSON_PATH, 'utf-8');
43
+ const parsed = JSON.parse(raw);
44
+ const lastIngestedAt = new Date(parsed.lastIngestedAt);
45
+ if (Number.isNaN(lastIngestedAt.getTime())) {
46
+ throw new Error(`lifeline-profile.json has invalid lastIngestedAt: ${parsed.lastIngestedAt}`);
47
+ }
48
+ console.log(`✓ Loaded lifeline-profile.json from ${PROFILE_JSON_PATH}`);
49
+ const expectedEntities = parsed.domainOntology.entities.length;
50
+ const expectedFlows = parsed.criticalFlows.length;
51
+ const expectedGlossary = Object.keys(parsed.glossary).length;
52
+ const expectedDeps = parsed.dependencies?.length ?? 0;
53
+ const expectedTags = parsed.complianceTags?.length ?? 0;
54
+ console.log(` source counts → entities=${String(expectedEntities)} flows=${String(expectedFlows)} glossary=${String(expectedGlossary)} deps=${String(expectedDeps)} tags=${String(expectedTags)}`);
55
+ // ─── Step 3: putProfile (upsert) ────────────────────────────────────
56
+ const input = {
57
+ projectId: lifeline.id,
58
+ type: parsed.type,
59
+ domainOntology: parsed.domainOntology,
60
+ riskProfile: parsed.riskProfile,
61
+ criticalFlows: parsed.criticalFlows,
62
+ glossary: parsed.glossary,
63
+ dependencies: parsed.dependencies ?? [],
64
+ complianceTags: parsed.complianceTags ?? [],
65
+ ingestedDocsHash: parsed.ingestedDocsHash,
66
+ lastIngestedAt,
67
+ };
68
+ const { profile, ref } = await store.putProfile(input);
69
+ console.log(`✓ putProfile → id=${ref.id} projectId=${ref.projectId}`);
70
+ assertProfileMatches(profile, input, {
71
+ expectedEntities,
72
+ expectedFlows,
73
+ expectedGlossary,
74
+ expectedDeps,
75
+ expectedTags,
76
+ });
77
+ // ─── Step 4: getProfile round-trip ──────────────────────────────────
78
+ const fetched = await store.getProfile(lifeline.id);
79
+ if (!fetched) {
80
+ throw new Error('getProfile returned null immediately after putProfile');
81
+ }
82
+ console.log(`✓ getProfile → returned profile for projectId=${lifeline.id}`);
83
+ assertProfileMatches(fetched, input, {
84
+ expectedEntities,
85
+ expectedFlows,
86
+ expectedGlossary,
87
+ expectedDeps,
88
+ expectedTags,
89
+ });
90
+ console.log(' round-trip counts match input (no JSON column truncation)');
91
+ // ─── Step 5: cross-tenant negative ──────────────────────────────────
92
+ const wrongTenant = await store.getProfile('not-a-real-project-id-99999');
93
+ if (wrongTenant !== null) {
94
+ throw new Error('Tenant isolation broken: getProfile with unknown projectId returned a row.');
95
+ }
96
+ console.log('✓ getProfile (unknown projectId) → null (tenant isolation OK)');
97
+ console.log('\nSPRINT4-PHASE5 PROFILE READY ✓');
98
+ }
99
+ function assertProfileMatches(profile, input, counts) {
100
+ if (profile.type !== input.type) {
101
+ throw new Error(`type mismatch: expected ${input.type}, got ${profile.type}`);
102
+ }
103
+ if (profile.domainOntology.entities.length !== counts.expectedEntities) {
104
+ throw new Error(`entities count mismatch: expected ${String(counts.expectedEntities)}, got ${String(profile.domainOntology.entities.length)}`);
105
+ }
106
+ if (profile.criticalFlows.length !== counts.expectedFlows) {
107
+ throw new Error(`criticalFlows count mismatch: expected ${String(counts.expectedFlows)}, got ${String(profile.criticalFlows.length)}`);
108
+ }
109
+ if (Object.keys(profile.glossary).length !== counts.expectedGlossary) {
110
+ throw new Error(`glossary terms count mismatch: expected ${String(counts.expectedGlossary)}, got ${String(Object.keys(profile.glossary).length)}`);
111
+ }
112
+ if (profile.dependencies.length !== counts.expectedDeps) {
113
+ throw new Error(`dependencies count mismatch: expected ${String(counts.expectedDeps)}, got ${String(profile.dependencies.length)}`);
114
+ }
115
+ if (profile.complianceTags.length !== counts.expectedTags) {
116
+ throw new Error(`complianceTags count mismatch: expected ${String(counts.expectedTags)}, got ${String(profile.complianceTags.length)}`);
117
+ }
118
+ if (profile.ingestedDocsHash !== input.ingestedDocsHash) {
119
+ throw new Error(`ingestedDocsHash mismatch: expected ${input.ingestedDocsHash}, got ${profile.ingestedDocsHash}`);
120
+ }
121
+ }
122
+ main()
123
+ .catch((err) => {
124
+ console.error('\n✗ Smoke failed:', err);
125
+ process.exitCode = 1;
126
+ })
127
+ .finally(async () => {
128
+ await prisma.$disconnect();
129
+ });
130
+ //# sourceMappingURL=smoke-seed-lifeline-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke-seed-lifeline-profile.js","sourceRoot":"","sources":["../../src/scripts/smoke-seed-lifeline-profile.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@derwinjs/db",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
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.3.0",
37
- "@derwinjs/sdk": "0.3.0"
36
+ "@derwinjs/core": "0.5.0",
37
+ "@derwinjs/sdk": "0.5.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@vitest/coverage-v8": "^2.1.9",
@@ -64,6 +64,7 @@
64
64
  "smoke:qa-ticket-store": "tsx src/scripts/smoke-qa-ticket-store.ts",
65
65
  "smoke:auto-fix": "tsx src/scripts/smoke-auto-fix.ts",
66
66
  "smoke:learning-loop": "tsx src/scripts/smoke-learning-loop.ts",
67
- "smoke:orchestration": "tsx src/scripts/smoke-orchestration.ts"
67
+ "smoke:orchestration": "tsx src/scripts/smoke-orchestration.ts",
68
+ "smoke:seed-lifeline-profile": "tsx src/scripts/smoke-seed-lifeline-profile.ts"
68
69
  }
69
70
  }
@@ -0,0 +1,12 @@
1
+ -- Sprint 5 Phase 1 — Enable pgvector extension + migrate RAGCorpus.embedding to vector(1536)
2
+ -- The corpus is empty at v0.4.0 — DROP+ADD is safe (no data loss).
3
+ -- Index uses HNSW with cosine ops for similarity search per pgvector docs.
4
+
5
+ CREATE EXTENSION IF NOT EXISTS vector;
6
+
7
+ ALTER TABLE "derwin"."rag_corpus" DROP COLUMN IF EXISTS "embedding";
8
+ ALTER TABLE "derwin"."rag_corpus" ADD COLUMN "embedding" vector(1536);
9
+
10
+ CREATE INDEX "rag_corpus_embedding_hnsw_idx"
11
+ ON "derwin"."rag_corpus"
12
+ USING hnsw ("embedding" vector_cosine_ops);
@@ -11,9 +11,11 @@
11
11
  // 2. A multi-project broken-fixture E2E that asserts isolation
12
12
  // (added in Sprint 13 / QAP-130).
13
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.
14
+ // pgvector convention: Sprint 5 Phase 1 enabled the pgvector extension and
15
+ // migrated `RAGCorpus.embedding` to `vector(1536)`. The column is typed as
16
+ // `Unsupported("vector(1536)")` because Prisma 5.22.0 has no native vector
17
+ // type — reads/writes use raw SQL via `$queryRaw` / `$executeRaw`. The HNSW
18
+ // index with `vector_cosine_ops` lives in the migration SQL.
17
19
  //
18
20
  // Multi-schema: every model + enum is in the `derwin` Postgres schema (not
19
21
  // `public`). This isolates Derwin's tables from each consumer's existing
@@ -494,16 +496,17 @@ model ClassificationTrust {
494
496
  }
495
497
 
496
498
  // Successful past fixes used as in-context examples for new dispatches.
497
- // Embedding column is Bytes for v1; QAP-076 (Sprint 7) switches to pgvector.
499
+ // Embedding column is `vector(1536)` (pgvector); Sprint 5 Phase 1 enabled the
500
+ // extension and migrated from `Bytes?`. See file header for query convention.
498
501
  model RAGCorpus {
499
502
  id String @id @default(cuid())
500
503
  projectId String
501
504
  classification String
502
505
  surface SurfaceCategory
503
506
 
504
- promptText String @db.Text // the prompt that produced the fix
505
- diff String @db.Text // the unified diff that worked
506
- embedding Bytes? // for semantic search
507
+ promptText String @db.Text // the prompt that produced the fix
508
+ diff String @db.Text // the unified diff that worked
509
+ embedding Unsupported("vector(1536)")? // pgvector Sprint 5 Phase 1 enabled the extension. Queries via raw SQL because Prisma 5.22.0 doesn't have native vector type support.
507
510
 
508
511
  fixAttemptId String // back-reference to the source attempt
509
512
  outcomeQuality Float // priority weight; declines if later usage shows problems
package/prisma/seed.ts CHANGED
@@ -6,15 +6,39 @@
6
6
  * minimal — just one Project (Lifeline) with its first ProjectProfile, a
7
7
  * default Policy, and a single SpendLedger row to verify FKs work.
8
8
  *
9
- * Real Profile content gets generated in QAP-047 (Sprint 4) by ingesting
10
- * Lifeline's actual docs. This seed is a placeholder that resembles what the
11
- * generated Profile will look like, so the dev experience isn't empty.
9
+ * Profile content lives in `seed-data/lifeline-profile.json` (Sprint 4
10
+ * Phase 5). The JSON is the canonical Lifeline Profile authored against the
11
+ * product brief §7.1 and is hot-swappable when ingestion (QAP-047) ships.
12
12
  *
13
- * Status: scaffolded in QAP-005. Ingestion-driven seeds replace this in QAP-047.
13
+ * Status: scaffolded in QAP-005; data externalized to JSON in Sprint 4 Phase 5.
14
14
  */
15
15
 
16
+ import { readFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
16
20
  import { PrismaClient, ProjectMode, ProjectType } from '../src/prisma.js';
17
21
 
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ interface LifelineProfileSeed {
26
+ type: string;
27
+ domainOntology: unknown;
28
+ riskProfile: unknown;
29
+ criticalFlows: unknown;
30
+ glossary: unknown;
31
+ dependencies: unknown;
32
+ complianceTags: string[];
33
+ ingestedDocsHash: string;
34
+ lastIngestedAt: string;
35
+ }
36
+
37
+ const lifelineProfilePath = join(__dirname, '..', 'seed-data', 'lifeline-profile.json');
38
+ const lifelineProfileData = JSON.parse(
39
+ readFileSync(lifelineProfilePath, 'utf-8'),
40
+ ) as LifelineProfileSeed;
41
+
18
42
  const prisma = new PrismaClient();
19
43
 
20
44
  async function main() {
@@ -36,75 +60,33 @@ async function main() {
36
60
  });
37
61
  console.log(` ✓ Project: ${lifeline.slug} (${lifeline.id})`);
38
62
 
39
- // ─── 2. Profile (placeholder; real one lands in QAP-047) ────────────
63
+ // ─── 2. Profile (data sourced from seed-data/lifeline-profile.json) ──
64
+ const lastIngestedAt = new Date(lifelineProfileData.lastIngestedAt);
40
65
  const profile = await prisma.projectProfile.upsert({
41
66
  where: { projectId: lifeline.id },
42
- update: {},
67
+ update: {
68
+ type: lifelineProfileData.type as ProjectType,
69
+ domainOntology: lifelineProfileData.domainOntology as object,
70
+ riskProfile: lifelineProfileData.riskProfile as object,
71
+ criticalFlows: lifelineProfileData.criticalFlows as object,
72
+ glossary: lifelineProfileData.glossary as object,
73
+ dependencies: lifelineProfileData.dependencies as object,
74
+ complianceTags: lifelineProfileData.complianceTags,
75
+ ingestedDocsHash: lifelineProfileData.ingestedDocsHash,
76
+ lastIngestedAt,
77
+ lastEvolvedAt: new Date(),
78
+ },
43
79
  create: {
44
80
  projectId: lifeline.id,
45
- type: ProjectType.ERP,
46
- domainOntology: {
47
- entities: [
48
- 'Patient',
49
- 'Booking',
50
- 'Provider',
51
- 'Encounter',
52
- 'ChartNote',
53
- 'Payment',
54
- 'Location',
55
- ],
56
- actions: ['book', 'arrive', 'sign', 'bill', 'refund', 'export'],
57
- roles: ['admin', 'provider', 'patient', 'front-desk'],
58
- },
59
- // 8-vector risk weights — see product brief §3.1 + §7.1
60
- riskProfile: {
61
- codeHealth: 0.8,
62
- functionalCorrectness: 1.0,
63
- uiUxIntegrity: 0.5,
64
- perfResilience: 0.7,
65
- security: 0.9,
66
- complianceAudit: 1.0, // HIPAA gates this to max
67
- multiTenantSafety: 1.0,
68
- operationalHealth: 0.7,
69
- },
70
- criticalFlows: [
71
- {
72
- name: 'patient-signup-to-first-booking',
73
- steps: ['signup', 'verify-email', 'browse-services', 'book', 'confirmation'],
74
- },
75
- {
76
- name: 'booking-to-arrived-to-encounter-to-signed-chart',
77
- steps: ['arrive', 'check-in', 'encounter-start', 'chart-note', 'sign'],
78
- },
79
- {
80
- name: 'chart-sign-to-bill-to-payment',
81
- steps: ['sign', 'bill-create', 'payment-process', 'reconcile'],
82
- },
83
- {
84
- name: 'admin-onboard-new-tenant',
85
- steps: ['create-location', 'invite-providers', 'configure-services'],
86
- },
87
- {
88
- name: 'patient-data-export',
89
- steps: ['request', 'verify-identity', 'package', 'deliver'],
90
- },
91
- ],
92
- glossary: {
93
- Tenant: 'medspa Location (NOT customer org)',
94
- Ticket: 'roadmap item (NOT support ticket)',
95
- Patient: 'end-user receiving services',
96
- Provider: 'licensed practitioner',
97
- Encounter: 'a recorded patient-provider interaction',
98
- },
99
- dependencies: [
100
- { name: 'Vagaro', kind: 'scheduling', integration: 'cron-pull' },
101
- { name: 'Stripe', kind: 'payments', integration: 'webhook' },
102
- { name: 'Twilio', kind: 'sms', integration: 'webhook' },
103
- { name: 'Anthropic API', kind: 'llm', integration: 'http' },
104
- ],
105
- complianceTags: ['HIPAA', 'SOC 2 (in progress)'],
106
- ingestedDocsHash: 'seed-placeholder',
107
- lastIngestedAt: new Date(),
81
+ type: lifelineProfileData.type as ProjectType,
82
+ domainOntology: lifelineProfileData.domainOntology as object,
83
+ riskProfile: lifelineProfileData.riskProfile as object,
84
+ criticalFlows: lifelineProfileData.criticalFlows as object,
85
+ glossary: lifelineProfileData.glossary as object,
86
+ dependencies: lifelineProfileData.dependencies as object,
87
+ complianceTags: lifelineProfileData.complianceTags,
88
+ ingestedDocsHash: lifelineProfileData.ingestedDocsHash,
89
+ lastIngestedAt,
108
90
  lastEvolvedAt: new Date(),
109
91
  },
110
92
  });
@@ -168,7 +150,7 @@ async function main() {
168
150
  });
169
151
  console.log(` ✓ SpendLedger: seed marker`);
170
152
 
171
- console.log('\nSeeded Lifeline. Real Profile generation lands in QAP-047 (Sprint 4).');
153
+ console.log('\nSeeded Lifeline. Profile data: packages/db/seed-data/lifeline-profile.json');
172
154
  }
173
155
 
174
156
  main()