@derwinjs/db 0.3.0 → 0.4.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,31 @@
1
+ /**
2
+ * ProjectProfileStore — Prisma implementation against Derwin's
3
+ * ProjectProfile table.
4
+ *
5
+ * QAP-Sprint4 Phase 1. Implements the ProjectProfileStore contract from
6
+ * @derwinjs/sdk (see packages/sdk/src/types/project-profile-store.ts) against
7
+ * the @derwinjs/db Prisma client. The Profile is Layer A — per-project
8
+ * domain ontology, 8-vector risk weights, critical flows, glossary, and
9
+ * dependency graph. Sprint 4 ships the read path; Sprint 6+ adds Profile
10
+ * evolution (writes from outcomes) on top of this same contract.
11
+ *
12
+ * Tenant isolation: the ProjectProfile.projectId column is `@unique`, so
13
+ * `findUnique({ where: { projectId } })` is the canonical scoping query —
14
+ * there is at most one row per project. As a defense-in-depth check we
15
+ * verify the returned row's projectId matches the requested one (a Prisma
16
+ * bug or schema drift could otherwise leak across tenants).
17
+ *
18
+ * JSON columns: domainOntology / riskProfile / criticalFlows / glossary /
19
+ * dependencies are typed as `Json` in the Prisma schema. We cast through
20
+ * `unknown` to the SDK's typed shapes on read; on write we cast in the
21
+ * other direction. The Profile authoring path (Sprint 6+) is the schema-
22
+ * narrowing chokepoint, not the storage layer.
23
+ */
24
+ import { type PrismaClient } from './prisma.js';
25
+ import { type ProjectProfileStore } from '@derwinjs/sdk';
26
+ export interface PrismaProjectProfileStoreConfig {
27
+ /** Generated Prisma client. Pass an instance per process. */
28
+ prisma: PrismaClient;
29
+ }
30
+ export declare function createPrismaProjectProfileStore(config: PrismaProjectProfileStoreConfig): ProjectProfileStore;
31
+ //# sourceMappingURL=project-profile-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-profile-store.d.ts","sourceRoot":"","sources":["../src/project-profile-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAQL,KAAK,mBAAmB,EAEzB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,+BAA+B;IAC9C,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAID,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,+BAA+B,GACtC,mBAAmB,CA+ErB"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * ProjectProfileStore — Prisma implementation against Derwin's
3
+ * ProjectProfile table.
4
+ *
5
+ * QAP-Sprint4 Phase 1. Implements the ProjectProfileStore contract from
6
+ * @derwinjs/sdk (see packages/sdk/src/types/project-profile-store.ts) against
7
+ * the @derwinjs/db Prisma client. The Profile is Layer A — per-project
8
+ * domain ontology, 8-vector risk weights, critical flows, glossary, and
9
+ * dependency graph. Sprint 4 ships the read path; Sprint 6+ adds Profile
10
+ * evolution (writes from outcomes) on top of this same contract.
11
+ *
12
+ * Tenant isolation: the ProjectProfile.projectId column is `@unique`, so
13
+ * `findUnique({ where: { projectId } })` is the canonical scoping query —
14
+ * there is at most one row per project. As a defense-in-depth check we
15
+ * verify the returned row's projectId matches the requested one (a Prisma
16
+ * bug or schema drift could otherwise leak across tenants).
17
+ *
18
+ * JSON columns: domainOntology / riskProfile / criticalFlows / glossary /
19
+ * dependencies are typed as `Json` in the Prisma schema. We cast through
20
+ * `unknown` to the SDK's typed shapes on read; on write we cast in the
21
+ * other direction. The Profile authoring path (Sprint 6+) is the schema-
22
+ * narrowing chokepoint, not the storage layer.
23
+ */
24
+ import { Prisma } from './prisma.js';
25
+ import { ProjectProfileStoreError, } from '@derwinjs/sdk';
26
+ // ─── Factory ─────────────────────────────────────────────────────────────
27
+ export function createPrismaProjectProfileStore(config) {
28
+ const { prisma } = config;
29
+ return {
30
+ async getProfile(projectId) {
31
+ validateProjectId(projectId);
32
+ const row = await prisma.projectProfile.findUnique({ where: { projectId } });
33
+ if (!row) {
34
+ return null;
35
+ }
36
+ // Defense-in-depth: the @unique constraint on projectId means this
37
+ // mismatch shouldn't be reachable, but we still verify before
38
+ // returning. Anything else collapses tenant isolation.
39
+ if (row.projectId !== projectId) {
40
+ return null;
41
+ }
42
+ return mapProfile(row);
43
+ },
44
+ async putProfile(input) {
45
+ validatePutInput(input);
46
+ const now = new Date();
47
+ const dependencies = (input.dependencies ?? []);
48
+ const complianceTags = input.complianceTags ?? [];
49
+ let row;
50
+ try {
51
+ row = await prisma.projectProfile.upsert({
52
+ where: { projectId: input.projectId },
53
+ create: {
54
+ projectId: input.projectId,
55
+ // ProjectType enum is validated at the Prisma layer when the
56
+ // row hits the database — invalid strings surface as a
57
+ // PrismaClientKnownRequestError on the round-trip.
58
+ type: input.type,
59
+ domainOntology: input.domainOntology,
60
+ riskProfile: input.riskProfile,
61
+ criticalFlows: input.criticalFlows,
62
+ glossary: input.glossary,
63
+ dependencies,
64
+ complianceTags,
65
+ ingestedDocsHash: input.ingestedDocsHash,
66
+ lastIngestedAt: input.lastIngestedAt,
67
+ lastEvolvedAt: now,
68
+ },
69
+ update: {
70
+ type: input.type,
71
+ domainOntology: input.domainOntology,
72
+ riskProfile: input.riskProfile,
73
+ criticalFlows: input.criticalFlows,
74
+ glossary: input.glossary,
75
+ dependencies,
76
+ complianceTags,
77
+ ingestedDocsHash: input.ingestedDocsHash,
78
+ lastIngestedAt: input.lastIngestedAt,
79
+ lastEvolvedAt: now,
80
+ },
81
+ });
82
+ }
83
+ catch (e) {
84
+ if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
85
+ throw new ProjectProfileStoreError('fk_violation', `Unknown projectId '${input.projectId}' — foreign key violation`, e);
86
+ }
87
+ throw e;
88
+ }
89
+ return {
90
+ profile: mapProfile(row),
91
+ ref: { id: row.id, projectId: row.projectId },
92
+ };
93
+ },
94
+ };
95
+ }
96
+ // ─── Helpers ─────────────────────────────────────────────────────────────
97
+ /**
98
+ * Convert a Prisma ProjectProfile row to the SDK value type. Json columns
99
+ * are narrowed via cast through `unknown`. Top-level shape is sanity-checked
100
+ * before returning so malformed rows surface as a typed error rather than
101
+ * runtime explosions deep in the consumer.
102
+ */
103
+ function mapProfile(row) {
104
+ const domainOntology = row.domainOntology;
105
+ const riskProfile = row.riskProfile;
106
+ const criticalFlows = row.criticalFlows;
107
+ const glossary = (row.glossary ?? {});
108
+ const dependencies = (row.dependencies ?? []);
109
+ // Defense-in-depth — top-level keys should at least be present. We don't
110
+ // deep-validate (that's the Profile authoring path's job), but we catch
111
+ // truly malformed rows instead of returning garbage.
112
+ if (!isObject(domainOntology) ||
113
+ !Array.isArray(domainOntology.entities) ||
114
+ !Array.isArray(domainOntology.actions) ||
115
+ !Array.isArray(domainOntology.roles)) {
116
+ throw new ProjectProfileStoreError('invalid_input', `Profile row ${row.id} has malformed domainOntology JSON`);
117
+ }
118
+ if (!isObject(riskProfile)) {
119
+ throw new ProjectProfileStoreError('invalid_input', `Profile row ${row.id} has malformed riskProfile JSON`);
120
+ }
121
+ return {
122
+ type: row.type,
123
+ domainOntology,
124
+ riskProfile,
125
+ criticalFlows: Array.isArray(criticalFlows) ? criticalFlows : [],
126
+ glossary,
127
+ dependencies: Array.isArray(dependencies) ? dependencies : [],
128
+ complianceTags: row.complianceTags,
129
+ ingestedDocsHash: row.ingestedDocsHash,
130
+ lastIngestedAt: row.lastIngestedAt,
131
+ lastEvolvedAt: row.lastEvolvedAt,
132
+ createdAt: row.createdAt,
133
+ updatedAt: row.updatedAt,
134
+ };
135
+ }
136
+ function isObject(value) {
137
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
138
+ }
139
+ function validateProjectId(projectId) {
140
+ if (typeof projectId !== 'string' || !projectId.trim()) {
141
+ throw new ProjectProfileStoreError('invalid_input', 'projectId is required');
142
+ }
143
+ }
144
+ function validatePutInput(input) {
145
+ validateProjectId(input.projectId);
146
+ if (typeof input.type !== 'string' || !input.type.trim()) {
147
+ throw new ProjectProfileStoreError('invalid_input', 'type is required');
148
+ }
149
+ if (typeof input.ingestedDocsHash !== 'string' || !input.ingestedDocsHash.trim()) {
150
+ throw new ProjectProfileStoreError('invalid_input', 'ingestedDocsHash is required');
151
+ }
152
+ if (!(input.lastIngestedAt instanceof Date) || Number.isNaN(input.lastIngestedAt.getTime())) {
153
+ throw new ProjectProfileStoreError('invalid_input', 'lastIngestedAt must be a valid Date');
154
+ }
155
+ }
156
+ //# sourceMappingURL=project-profile-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-profile-store.js","sourceRoot":"","sources":["../src/project-profile-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
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
+ export {};
25
+ //# sourceMappingURL=smoke-seed-lifeline-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke-seed-lifeline-profile.d.ts","sourceRoot":"","sources":["../../src/scripts/smoke-seed-lifeline-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
@@ -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.4.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.4.0",
37
+ "@derwinjs/sdk": "0.4.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
  }
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()