@contractspec/lib.runtime-sandbox 2.7.6 → 2.7.9

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.
@@ -7,9 +7,9 @@ import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';
7
7
  import type { TemplateId } from '../services';
8
8
 
9
9
  export interface SeedTemplateParams {
10
- templateId: TemplateId;
11
- projectId: string;
12
- db: DatabasePort;
10
+ templateId: TemplateId;
11
+ projectId: string;
12
+ db: DatabasePort;
13
13
  }
14
14
 
15
15
  /**
@@ -18,35 +18,35 @@ export interface SeedTemplateParams {
18
18
  * Unknown templates are a no-op (safe default).
19
19
  */
20
20
  export async function seedTemplate(params: SeedTemplateParams): Promise<void> {
21
- const { templateId, projectId, db } = params;
22
-
23
- switch (templateId) {
24
- case 'todos-app':
25
- return seedTodos({ projectId, db });
26
- case 'messaging-app':
27
- return seedMessaging({ projectId, db });
28
- case 'recipe-app-i18n':
29
- return seedRecipes({ projectId, db });
30
- case 'crm-pipeline':
31
- return seedCrmPipeline({ projectId, db });
32
- case 'saas-boilerplate':
33
- return seedSaasBoilerplate({ projectId, db });
34
- case 'agent-console':
35
- return seedAgentConsole({ projectId, db });
36
- case 'workflow-system':
37
- return seedWorkflowSystem({ projectId, db });
38
- case 'marketplace':
39
- return seedMarketplace({ projectId, db });
40
- case 'integration-hub':
41
- return seedIntegrationHub({ projectId, db });
42
- case 'analytics-dashboard':
43
- return seedAnalyticsDashboard({ projectId, db });
44
- case 'policy-safe-knowledge-assistant':
45
- return seedPolicyKnowledgeAssistant({ projectId, db });
46
- default:
47
- // Unknown template - no-op
48
- return;
49
- }
21
+ const { templateId, projectId, db } = params;
22
+
23
+ switch (templateId) {
24
+ case 'todos-app':
25
+ return seedTodos({ projectId, db });
26
+ case 'messaging-app':
27
+ return seedMessaging({ projectId, db });
28
+ case 'recipe-app-i18n':
29
+ return seedRecipes({ projectId, db });
30
+ case 'crm-pipeline':
31
+ return seedCrmPipeline({ projectId, db });
32
+ case 'saas-boilerplate':
33
+ return seedSaasBoilerplate({ projectId, db });
34
+ case 'agent-console':
35
+ return seedAgentConsole({ projectId, db });
36
+ case 'workflow-system':
37
+ return seedWorkflowSystem({ projectId, db });
38
+ case 'marketplace':
39
+ return seedMarketplace({ projectId, db });
40
+ case 'integration-hub':
41
+ return seedIntegrationHub({ projectId, db });
42
+ case 'analytics-dashboard':
43
+ return seedAnalyticsDashboard({ projectId, db });
44
+ case 'policy-safe-knowledge-assistant':
45
+ return seedPolicyKnowledgeAssistant({ projectId, db });
46
+ default:
47
+ // Unknown template - no-op
48
+ return;
49
+ }
50
50
  }
51
51
 
52
52
  // --------------------------------------------------------------------------
@@ -54,396 +54,655 @@ export async function seedTemplate(params: SeedTemplateParams): Promise<void> {
54
54
  // --------------------------------------------------------------------------
55
55
 
56
56
  async function seedTodos(params: { projectId: string; db: DatabasePort }) {
57
- const { projectId, db } = params;
58
-
59
- // Check if already seeded
60
- const existing = await db.query(
61
- `SELECT COUNT(*) as count FROM template_task WHERE "projectId" = $1`,
62
- [projectId]
63
- );
64
- if ((existing.rows[0]?.count as number) > 0) return;
65
-
66
- const workCategoryId = 'todo_category_ops';
67
- const homeCategoryId = 'todo_category_home';
68
-
69
- await db.execute(
70
- `INSERT INTO template_task_category (id, "projectId", name, color) VALUES ($1, $2, $3, $4)`,
71
- [workCategoryId, projectId, 'Operations', '#8b5cf6']
72
- );
73
- await db.execute(
74
- `INSERT INTO template_task_category (id, "projectId", name, color) VALUES ($1, $2, $3, $4)`,
75
- [homeCategoryId, projectId, 'Home', '#f472b6']
76
- );
77
-
78
- const tasks = [
79
- {
80
- id: 'todo_task_1',
81
- title: 'Review intent signals',
82
- description: "Scan yesterday's signals and flag the ones to promote.",
83
- categoryId: workCategoryId,
84
- priority: 'HIGH',
85
- },
86
- {
87
- id: 'todo_task_2',
88
- title: 'Schedule studio walkthrough',
89
- description: "Prep the sandbox before tomorrow's ceremony.",
90
- categoryId: workCategoryId,
91
- priority: 'MEDIUM',
92
- },
93
- {
94
- id: 'todo_task_3',
95
- title: 'Collect testimonials',
96
- description: "Ask last week's pilot crew for quotes.",
97
- categoryId: homeCategoryId,
98
- priority: 'LOW',
99
- },
100
- ];
101
-
102
- for (const task of tasks) {
103
- await db.execute(
104
- `INSERT INTO template_task (id, "projectId", "categoryId", title, description, completed, priority, tags)
57
+ const { projectId, db } = params;
58
+
59
+ // Check if already seeded
60
+ const existing = await db.query(
61
+ `SELECT COUNT(*) as count FROM template_task WHERE "projectId" = $1`,
62
+ [projectId]
63
+ );
64
+ if ((existing.rows[0]?.count as number) > 0) return;
65
+
66
+ const workCategoryId = 'todo_category_ops';
67
+ const homeCategoryId = 'todo_category_home';
68
+
69
+ await db.execute(
70
+ `INSERT INTO template_task_category (id, "projectId", name, color) VALUES ($1, $2, $3, $4)`,
71
+ [workCategoryId, projectId, 'Operations', '#8b5cf6']
72
+ );
73
+ await db.execute(
74
+ `INSERT INTO template_task_category (id, "projectId", name, color) VALUES ($1, $2, $3, $4)`,
75
+ [homeCategoryId, projectId, 'Home', '#f472b6']
76
+ );
77
+
78
+ const tasks = [
79
+ {
80
+ id: 'todo_task_1',
81
+ title: 'Review intent signals',
82
+ description: "Scan yesterday's signals and flag the ones to promote.",
83
+ categoryId: workCategoryId,
84
+ priority: 'HIGH',
85
+ },
86
+ {
87
+ id: 'todo_task_2',
88
+ title: 'Schedule studio walkthrough',
89
+ description: "Prep the sandbox before tomorrow's ceremony.",
90
+ categoryId: workCategoryId,
91
+ priority: 'MEDIUM',
92
+ },
93
+ {
94
+ id: 'todo_task_3',
95
+ title: 'Collect testimonials',
96
+ description: "Ask last week's pilot crew for quotes.",
97
+ categoryId: homeCategoryId,
98
+ priority: 'LOW',
99
+ },
100
+ ];
101
+
102
+ for (const task of tasks) {
103
+ await db.execute(
104
+ `INSERT INTO template_task (id, "projectId", "categoryId", title, description, completed, priority, tags)
105
105
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
106
- [
107
- task.id,
108
- projectId,
109
- task.categoryId,
110
- task.title,
111
- task.description,
112
- 0,
113
- task.priority,
114
- JSON.stringify(['demo']),
115
- ]
116
- );
117
- }
106
+ [
107
+ task.id,
108
+ projectId,
109
+ task.categoryId,
110
+ task.title,
111
+ task.description,
112
+ 0,
113
+ task.priority,
114
+ JSON.stringify(['demo']),
115
+ ]
116
+ );
117
+ }
118
118
  }
119
119
 
120
120
  async function seedMessaging(params: { projectId: string; db: DatabasePort }) {
121
- const { projectId, db } = params;
121
+ const { projectId, db } = params;
122
122
 
123
- const existing = await db.query(
124
- `SELECT COUNT(*) as count FROM template_conversation WHERE "projectId" = $1`,
125
- [projectId]
126
- );
127
- if ((existing.rows[0]?.count as number) > 0) return;
123
+ const existing = await db.query(
124
+ `SELECT COUNT(*) as count FROM template_conversation WHERE "projectId" = $1`,
125
+ [projectId]
126
+ );
127
+ if ((existing.rows[0]?.count as number) > 0) return;
128
128
 
129
- const conversationId = 'conv_demo_1';
130
- const now = new Date().toISOString();
129
+ const conversationId = 'conv_demo_1';
130
+ const now = new Date().toISOString();
131
131
 
132
- await db.execute(
133
- `INSERT INTO template_conversation (id, "projectId", name, "isGroup", "avatarUrl", "updatedAt")
132
+ await db.execute(
133
+ `INSERT INTO template_conversation (id, "projectId", name, "isGroup", "avatarUrl", "updatedAt")
134
134
  VALUES ($1, $2, $3, $4, $5, $6)`,
135
- [conversationId, projectId, 'Team Chat', 1, null, now]
136
- );
135
+ [conversationId, projectId, 'Team Chat', 1, null, now]
136
+ );
137
137
 
138
- // Add participants
139
- await db.execute(
140
- `INSERT INTO template_conversation_participant (id, "conversationId", "projectId", "userId", "displayName", role, "joinedAt")
138
+ // Add participants
139
+ await db.execute(
140
+ `INSERT INTO template_conversation_participant (id, "conversationId", "projectId", "userId", "displayName", role, "joinedAt")
141
141
  VALUES ($1, $2, $3, $4, $5, $6, $7)`,
142
- ['part_1', conversationId, projectId, 'user_alice', 'Alice', 'member', now]
143
- );
144
- await db.execute(
145
- `INSERT INTO template_conversation_participant (id, "conversationId", "projectId", "userId", "displayName", role, "joinedAt")
142
+ ['part_1', conversationId, projectId, 'user_alice', 'Alice', 'member', now]
143
+ );
144
+ await db.execute(
145
+ `INSERT INTO template_conversation_participant (id, "conversationId", "projectId", "userId", "displayName", role, "joinedAt")
146
146
  VALUES ($1, $2, $3, $4, $5, $6, $7)`,
147
- ['part_2', conversationId, projectId, 'user_bob', 'Bob', 'member', now]
148
- );
147
+ ['part_2', conversationId, projectId, 'user_bob', 'Bob', 'member', now]
148
+ );
149
149
 
150
- // Add messages
151
- await db.execute(
152
- `INSERT INTO template_message (id, "conversationId", "projectId", "senderId", "senderName", content, attachments, status, "createdAt", "updatedAt")
150
+ // Add messages
151
+ await db.execute(
152
+ `INSERT INTO template_message (id, "conversationId", "projectId", "senderId", "senderName", content, attachments, status, "createdAt", "updatedAt")
153
153
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
154
- [
155
- 'msg_1',
156
- conversationId,
157
- projectId,
158
- 'user_alice',
159
- 'Alice',
160
- 'Hey team! Ready for the demo?',
161
- JSON.stringify([]),
162
- 'DELIVERED',
163
- now,
164
- now,
165
- ]
166
- );
154
+ [
155
+ 'msg_1',
156
+ conversationId,
157
+ projectId,
158
+ 'user_alice',
159
+ 'Alice',
160
+ 'Hey team! Ready for the demo?',
161
+ JSON.stringify([]),
162
+ 'DELIVERED',
163
+ now,
164
+ now,
165
+ ]
166
+ );
167
167
  }
168
168
 
169
169
  async function seedRecipes(params: { projectId: string; db: DatabasePort }) {
170
- const { projectId, db } = params;
171
-
172
- const existing = await db.query(
173
- `SELECT COUNT(*) as count FROM template_recipe WHERE "projectId" = $1`,
174
- [projectId]
175
- );
176
- if ((existing.rows[0]?.count as number) > 0) return;
177
-
178
- // Add category
179
- await db.execute(
180
- `INSERT INTO template_recipe_category (id, "nameEn", "nameFr", icon) VALUES ($1, $2, $3, $4)`,
181
- ['cat_main', 'Main Courses', 'Plats Principaux', '🍽️']
182
- );
183
-
184
- // Add recipe
185
- const recipeId = 'recipe_1';
186
- await db.execute(
187
- `INSERT INTO template_recipe (id, "projectId", "categoryId", "slugEn", "slugFr", "nameEn", "nameFr", "descriptionEn", "descriptionFr", "prepTimeMinutes", "cookTimeMinutes", servings, "isFavorite")
170
+ const { projectId, db } = params;
171
+
172
+ const existing = await db.query(
173
+ `SELECT COUNT(*) as count FROM template_recipe WHERE "projectId" = $1`,
174
+ [projectId]
175
+ );
176
+ if ((existing.rows[0]?.count as number) > 0) return;
177
+
178
+ // Add category
179
+ await db.execute(
180
+ `INSERT INTO template_recipe_category (id, "nameEn", "nameFr", icon) VALUES ($1, $2, $3, $4)`,
181
+ ['cat_main', 'Main Courses', 'Plats Principaux', '🍽️']
182
+ );
183
+
184
+ // Add recipe
185
+ const recipeId = 'recipe_1';
186
+ await db.execute(
187
+ `INSERT INTO template_recipe (id, "projectId", "categoryId", "slugEn", "slugFr", "nameEn", "nameFr", "descriptionEn", "descriptionFr", "prepTimeMinutes", "cookTimeMinutes", servings, "isFavorite")
188
188
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
189
- [
190
- recipeId,
191
- projectId,
192
- 'cat_main',
193
- 'grilled-salmon',
194
- 'saumon-grille',
195
- 'Grilled Salmon',
196
- 'Saumon Grillé',
197
- 'Delicious grilled salmon with lemon',
198
- 'Délicieux saumon grillé au citron',
199
- 10,
200
- 15,
201
- 4,
202
- 0,
203
- ]
204
- );
205
-
206
- // Add ingredients
207
- await db.execute(
208
- `INSERT INTO template_recipe_ingredient (id, "recipeId", "nameEn", "nameFr", quantity, ordering)
189
+ [
190
+ recipeId,
191
+ projectId,
192
+ 'cat_main',
193
+ 'grilled-salmon',
194
+ 'saumon-grille',
195
+ 'Grilled Salmon',
196
+ 'Saumon Grillé',
197
+ 'Delicious grilled salmon with lemon',
198
+ 'Délicieux saumon grillé au citron',
199
+ 10,
200
+ 15,
201
+ 4,
202
+ 0,
203
+ ]
204
+ );
205
+
206
+ // Add ingredients
207
+ await db.execute(
208
+ `INSERT INTO template_recipe_ingredient (id, "recipeId", "nameEn", "nameFr", quantity, ordering)
209
209
  VALUES ($1, $2, $3, $4, $5, $6)`,
210
- ['ing_1', recipeId, 'Salmon fillet', 'Filet de saumon', '4 pieces', 1]
211
- );
212
- await db.execute(
213
- `INSERT INTO template_recipe_ingredient (id, "recipeId", "nameEn", "nameFr", quantity, ordering)
210
+ ['ing_1', recipeId, 'Salmon fillet', 'Filet de saumon', '4 pieces', 1]
211
+ );
212
+ await db.execute(
213
+ `INSERT INTO template_recipe_ingredient (id, "recipeId", "nameEn", "nameFr", quantity, ordering)
214
214
  VALUES ($1, $2, $3, $4, $5, $6)`,
215
- ['ing_2', recipeId, 'Lemon', 'Citron', '1 whole', 2]
216
- );
215
+ ['ing_2', recipeId, 'Lemon', 'Citron', '1 whole', 2]
216
+ );
217
217
 
218
- // Add instructions
219
- await db.execute(
220
- `INSERT INTO template_recipe_instruction (id, "recipeId", "contentEn", "contentFr", ordering)
218
+ // Add instructions
219
+ await db.execute(
220
+ `INSERT INTO template_recipe_instruction (id, "recipeId", "contentEn", "contentFr", ordering)
221
221
  VALUES ($1, $2, $3, $4, $5)`,
222
- [
223
- 'inst_1',
224
- recipeId,
225
- 'Preheat grill to medium-high heat.',
226
- 'Préchauffer le grill à feu moyen-vif.',
227
- 1,
228
- ]
229
- );
230
- await db.execute(
231
- `INSERT INTO template_recipe_instruction (id, "recipeId", "contentEn", "contentFr", ordering)
222
+ [
223
+ 'inst_1',
224
+ recipeId,
225
+ 'Preheat grill to medium-high heat.',
226
+ 'Préchauffer le grill à feu moyen-vif.',
227
+ 1,
228
+ ]
229
+ );
230
+ await db.execute(
231
+ `INSERT INTO template_recipe_instruction (id, "recipeId", "contentEn", "contentFr", ordering)
232
232
  VALUES ($1, $2, $3, $4, $5)`,
233
- [
234
- 'inst_2',
235
- recipeId,
236
- 'Season salmon and grill for 4-5 minutes per side.',
237
- 'Assaisonner le saumon et griller 4-5 minutes de chaque côté.',
238
- 2,
239
- ]
240
- );
233
+ [
234
+ 'inst_2',
235
+ recipeId,
236
+ 'Season salmon and grill for 4-5 minutes per side.',
237
+ 'Assaisonner le saumon et griller 4-5 minutes de chaque côté.',
238
+ 2,
239
+ ]
240
+ );
241
241
  }
242
242
 
243
243
  async function seedCrmPipeline(params: {
244
- projectId: string;
245
- db: DatabasePort;
244
+ projectId: string;
245
+ db: DatabasePort;
246
246
  }) {
247
- const { projectId, db } = params;
248
-
249
- const existing = await db.query(
250
- `SELECT COUNT(*) as count FROM crm_pipeline WHERE "projectId" = $1`,
251
- [projectId]
252
- );
253
- if ((existing.rows[0]?.count as number) > 0) return;
254
-
255
- const pipelineId = 'pipeline_sales';
256
- await db.execute(
257
- `INSERT INTO crm_pipeline (id, "projectId", name) VALUES ($1, $2, $3)`,
258
- [pipelineId, projectId, 'Sales Pipeline']
259
- );
260
-
261
- const stages = [
262
- { id: 'stage_lead', name: 'Lead', position: 1 },
263
- { id: 'stage_contact', name: 'Contact Made', position: 2 },
264
- { id: 'stage_proposal', name: 'Proposal', position: 3 },
265
- { id: 'stage_negotiation', name: 'Negotiation', position: 4 },
266
- { id: 'stage_closed', name: 'Closed', position: 5 },
267
- ];
268
-
269
- for (const stage of stages) {
270
- await db.execute(
271
- `INSERT INTO crm_stage (id, "pipelineId", name, position) VALUES ($1, $2, $3, $4)`,
272
- [stage.id, pipelineId, stage.name, stage.position]
273
- );
274
- }
247
+ const { projectId, db } = params;
248
+
249
+ const existing = await db.query(
250
+ `SELECT COUNT(*) as count FROM crm_pipeline WHERE "projectId" = $1`,
251
+ [projectId]
252
+ );
253
+ if ((existing.rows[0]?.count as number) > 0) return;
254
+
255
+ const pipelineId = 'pipeline_sales';
256
+ await db.execute(
257
+ `INSERT INTO crm_pipeline (id, "projectId", name) VALUES ($1, $2, $3)`,
258
+ [pipelineId, projectId, 'Sales Pipeline']
259
+ );
260
+
261
+ const stages = [
262
+ { id: 'stage_lead', name: 'Lead', position: 1 },
263
+ { id: 'stage_contact', name: 'Contact Made', position: 2 },
264
+ { id: 'stage_proposal', name: 'Proposal', position: 3 },
265
+ { id: 'stage_negotiation', name: 'Negotiation', position: 4 },
266
+ { id: 'stage_closed', name: 'Closed', position: 5 },
267
+ ];
268
+
269
+ for (const stage of stages) {
270
+ await db.execute(
271
+ `INSERT INTO crm_stage (id, "pipelineId", name, position) VALUES ($1, $2, $3, $4)`,
272
+ [stage.id, pipelineId, stage.name, stage.position]
273
+ );
274
+ }
275
275
  }
276
276
 
277
277
  async function seedSaasBoilerplate(params: {
278
- projectId: string;
279
- db: DatabasePort;
278
+ projectId: string;
279
+ db: DatabasePort;
280
280
  }) {
281
- const { projectId, db } = params;
281
+ const { projectId, db } = params;
282
282
 
283
- const existing = await db.query(
284
- `SELECT COUNT(*) as count FROM saas_project WHERE "projectId" = $1`,
285
- [projectId]
286
- );
287
- if ((existing.rows[0]?.count as number) > 0) return;
283
+ const existing = await db.query(
284
+ `SELECT COUNT(*) as count FROM saas_project WHERE "projectId" = $1`,
285
+ [projectId]
286
+ );
287
+ if ((existing.rows[0]?.count as number) > 0) return;
288
288
 
289
- await db.execute(
290
- `INSERT INTO saas_project (id, "projectId", "organizationId", name, description, status, tier)
289
+ await db.execute(
290
+ `INSERT INTO saas_project (id, "projectId", "organizationId", name, description, status, tier)
291
291
  VALUES ($1, $2, $3, $4, $5, $6, $7)`,
292
- [
293
- 'saas_proj_1',
294
- projectId,
295
- 'org_demo',
296
- 'Demo Project',
297
- 'A demo SaaS project',
298
- 'ACTIVE',
299
- 'PRO',
300
- ]
301
- );
292
+ [
293
+ 'saas_proj_1',
294
+ projectId,
295
+ 'org_demo',
296
+ 'Demo Project',
297
+ 'A demo SaaS project',
298
+ 'ACTIVE',
299
+ 'PRO',
300
+ ]
301
+ );
302
302
  }
303
303
 
304
304
  async function seedAgentConsole(params: {
305
- projectId: string;
306
- db: DatabasePort;
305
+ projectId: string;
306
+ db: DatabasePort;
307
307
  }) {
308
- const { projectId, db } = params;
308
+ const { projectId, db } = params;
309
309
 
310
- const existing = await db.query(
311
- `SELECT COUNT(*) as count FROM agent_definition WHERE "projectId" = $1`,
312
- [projectId]
313
- );
314
- if ((existing.rows[0]?.count as number) > 0) return;
310
+ const existing = await db.query(
311
+ `SELECT COUNT(*) as count FROM agent_definition WHERE "projectId" = $1`,
312
+ [projectId]
313
+ );
314
+ if ((existing.rows[0]?.count as number) > 0) return;
315
315
 
316
- await db.execute(
317
- `INSERT INTO agent_definition (id, "projectId", "organizationId", name, description, "modelProvider", "modelName", status)
316
+ await db.execute(
317
+ `INSERT INTO agent_definition (id, "projectId", "organizationId", name, description, "modelProvider", "modelName", status)
318
318
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
319
- [
320
- 'agent_1',
321
- projectId,
322
- 'org_demo',
323
- 'Demo Agent',
324
- 'A demo AI agent',
325
- 'openai',
326
- 'gpt-4',
327
- 'ACTIVE',
328
- ]
329
- );
319
+ [
320
+ 'agent_1',
321
+ projectId,
322
+ 'org_demo',
323
+ 'Demo Agent',
324
+ 'A demo AI agent',
325
+ 'openai',
326
+ 'gpt-4',
327
+ 'ACTIVE',
328
+ ]
329
+ );
330
330
  }
331
331
 
332
332
  async function seedWorkflowSystem(params: {
333
- projectId: string;
334
- db: DatabasePort;
333
+ projectId: string;
334
+ db: DatabasePort;
335
335
  }) {
336
- const { projectId, db } = params;
337
-
338
- const existing = await db.query(
339
- `SELECT COUNT(*) as count FROM workflow_definition WHERE "projectId" = $1`,
340
- [projectId]
341
- );
342
- if ((existing.rows[0]?.count as number) > 0) return;
343
-
344
- await db.execute(
345
- `INSERT INTO workflow_definition (id, "projectId", "organizationId", name, description, type, status)
346
- VALUES ($1, $2, $3, $4, $5, $6, $7)`,
347
- [
348
- 'wf_1',
349
- projectId,
350
- 'org_demo',
351
- 'Approval Workflow',
352
- 'Demo approval workflow',
353
- 'APPROVAL',
354
- 'ACTIVE',
355
- ]
356
- );
336
+ const { projectId, db } = params;
337
+
338
+ const existing = await db.query(
339
+ `SELECT COUNT(*) as count FROM workflow_definition WHERE "projectId" = $1`,
340
+ [projectId]
341
+ );
342
+ if ((existing.rows[0]?.count as number) > 0) return;
343
+
344
+ const definitions = [
345
+ {
346
+ id: 'wf_expense',
347
+ name: 'Expense Approval',
348
+ description: 'Approve non-trivial spend before finance releases budget.',
349
+ type: 'APPROVAL',
350
+ status: 'ACTIVE',
351
+ createdAt: '2026-03-10T09:00:00.000Z',
352
+ updatedAt: '2026-03-20T08:15:00.000Z',
353
+ steps: [
354
+ {
355
+ id: 'wfstep_expense_manager',
356
+ name: 'Manager Review',
357
+ description: 'Validate business value and team budget.',
358
+ stepOrder: 1,
359
+ type: 'APPROVAL',
360
+ requiredRoles: ['manager'],
361
+ createdAt: '2026-03-10T09:00:00.000Z',
362
+ },
363
+ {
364
+ id: 'wfstep_expense_finance',
365
+ name: 'Finance Review',
366
+ description: 'Confirm ledger coding and spending threshold.',
367
+ stepOrder: 2,
368
+ type: 'APPROVAL',
369
+ requiredRoles: ['finance'],
370
+ createdAt: '2026-03-10T09:10:00.000Z',
371
+ },
372
+ ],
373
+ },
374
+ {
375
+ id: 'wf_vendor',
376
+ name: 'Vendor Onboarding',
377
+ description:
378
+ 'Sequence security, procurement, and legal before activation.',
379
+ type: 'SEQUENTIAL',
380
+ status: 'ACTIVE',
381
+ createdAt: '2026-03-08T11:00:00.000Z',
382
+ updatedAt: '2026-03-19T13:10:00.000Z',
383
+ steps: [
384
+ {
385
+ id: 'wfstep_vendor_security',
386
+ name: 'Security Check',
387
+ description: 'Review data access and integration scope.',
388
+ stepOrder: 1,
389
+ type: 'APPROVAL',
390
+ requiredRoles: ['security'],
391
+ createdAt: '2026-03-08T11:00:00.000Z',
392
+ },
393
+ {
394
+ id: 'wfstep_vendor_procurement',
395
+ name: 'Procurement Check',
396
+ description: 'Validate pricing, procurement policy, and owner.',
397
+ stepOrder: 2,
398
+ type: 'APPROVAL',
399
+ requiredRoles: ['procurement'],
400
+ createdAt: '2026-03-08T11:05:00.000Z',
401
+ },
402
+ {
403
+ id: 'wfstep_vendor_legal',
404
+ name: 'Legal Sign-off',
405
+ description: 'Approve terms before the vendor goes live.',
406
+ stepOrder: 3,
407
+ type: 'APPROVAL',
408
+ requiredRoles: ['legal'],
409
+ createdAt: '2026-03-08T11:10:00.000Z',
410
+ },
411
+ ],
412
+ },
413
+ {
414
+ id: 'wf_policy_exception',
415
+ name: 'Policy Exception',
416
+ description:
417
+ 'Escalate a temporary exception through team lead and compliance.',
418
+ type: 'APPROVAL',
419
+ status: 'DRAFT',
420
+ createdAt: '2026-03-15T07:30:00.000Z',
421
+ updatedAt: '2026-03-18T11:20:00.000Z',
422
+ steps: [
423
+ {
424
+ id: 'wfstep_policy_lead',
425
+ name: 'Team Lead Review',
426
+ description: 'Check urgency and expected blast radius.',
427
+ stepOrder: 1,
428
+ type: 'APPROVAL',
429
+ requiredRoles: ['team-lead'],
430
+ createdAt: '2026-03-15T07:30:00.000Z',
431
+ },
432
+ {
433
+ id: 'wfstep_policy_compliance',
434
+ name: 'Compliance Review',
435
+ description: 'Accept or reject the exception request.',
436
+ stepOrder: 2,
437
+ type: 'APPROVAL',
438
+ requiredRoles: ['compliance'],
439
+ createdAt: '2026-03-15T07:40:00.000Z',
440
+ },
441
+ ],
442
+ },
443
+ ] as const;
444
+
445
+ const instances = [
446
+ {
447
+ id: 'wfinst_expense_open',
448
+ definitionId: 'wf_expense',
449
+ status: 'IN_PROGRESS',
450
+ currentStepId: 'wfstep_expense_finance',
451
+ data: { amount: 4200, currency: 'EUR', vendor: 'Nimbus AI' },
452
+ requestedBy: 'sarah@contractspec.io',
453
+ startedAt: '2026-03-20T08:00:00.000Z',
454
+ completedAt: null,
455
+ approvals: [
456
+ {
457
+ id: 'wfappr_expense_manager',
458
+ stepId: 'wfstep_expense_manager',
459
+ status: 'APPROVED',
460
+ actorId: 'manager.demo',
461
+ comment: 'Approved for the Q2 automation budget.',
462
+ decidedAt: '2026-03-20T08:15:00.000Z',
463
+ createdAt: '2026-03-20T08:05:00.000Z',
464
+ },
465
+ {
466
+ id: 'wfappr_expense_finance',
467
+ stepId: 'wfstep_expense_finance',
468
+ status: 'PENDING',
469
+ actorId: null,
470
+ comment: null,
471
+ decidedAt: null,
472
+ createdAt: '2026-03-20T08:15:00.000Z',
473
+ },
474
+ ],
475
+ },
476
+ {
477
+ id: 'wfinst_vendor_done',
478
+ definitionId: 'wf_vendor',
479
+ status: 'COMPLETED',
480
+ currentStepId: null,
481
+ data: { vendor: 'Acme Cloud', riskTier: 'medium' },
482
+ requestedBy: 'leo@contractspec.io',
483
+ startedAt: '2026-03-19T09:30:00.000Z',
484
+ completedAt: '2026-03-19T13:10:00.000Z',
485
+ approvals: [
486
+ {
487
+ id: 'wfappr_vendor_security',
488
+ stepId: 'wfstep_vendor_security',
489
+ status: 'APPROVED',
490
+ actorId: 'security.demo',
491
+ comment: 'SOC2 scope is acceptable.',
492
+ decidedAt: '2026-03-19T10:10:00.000Z',
493
+ createdAt: '2026-03-19T09:35:00.000Z',
494
+ },
495
+ {
496
+ id: 'wfappr_vendor_procurement',
497
+ stepId: 'wfstep_vendor_procurement',
498
+ status: 'APPROVED',
499
+ actorId: 'procurement.demo',
500
+ comment: 'Commercial terms match the preferred vendor tier.',
501
+ decidedAt: '2026-03-19T11:25:00.000Z',
502
+ createdAt: '2026-03-19T10:15:00.000Z',
503
+ },
504
+ {
505
+ id: 'wfappr_vendor_legal',
506
+ stepId: 'wfstep_vendor_legal',
507
+ status: 'APPROVED',
508
+ actorId: 'legal.demo',
509
+ comment: 'MSA redlines are complete.',
510
+ decidedAt: '2026-03-19T13:05:00.000Z',
511
+ createdAt: '2026-03-19T11:30:00.000Z',
512
+ },
513
+ ],
514
+ },
515
+ {
516
+ id: 'wfinst_policy_rejected',
517
+ definitionId: 'wf_policy_exception',
518
+ status: 'REJECTED',
519
+ currentStepId: 'wfstep_policy_compliance',
520
+ data: { policy: 'Model rollout freeze', durationDays: 14 },
521
+ requestedBy: 'maya@contractspec.io',
522
+ startedAt: '2026-03-18T10:00:00.000Z',
523
+ completedAt: '2026-03-18T11:20:00.000Z',
524
+ approvals: [
525
+ {
526
+ id: 'wfappr_policy_lead',
527
+ stepId: 'wfstep_policy_lead',
528
+ status: 'APPROVED',
529
+ actorId: 'lead.demo',
530
+ comment: 'Escalation justified for the release train.',
531
+ decidedAt: '2026-03-18T10:30:00.000Z',
532
+ createdAt: '2026-03-18T10:05:00.000Z',
533
+ },
534
+ {
535
+ id: 'wfappr_policy_compliance',
536
+ stepId: 'wfstep_policy_compliance',
537
+ status: 'REJECTED',
538
+ actorId: 'compliance.demo',
539
+ comment: 'Exception exceeds the allowed blast radius.',
540
+ decidedAt: '2026-03-18T11:15:00.000Z',
541
+ createdAt: '2026-03-18T10:35:00.000Z',
542
+ },
543
+ ],
544
+ },
545
+ ] as const;
546
+
547
+ for (const definition of definitions) {
548
+ await db.execute(
549
+ `INSERT INTO workflow_definition (id, "projectId", "organizationId", name, description, type, status, "createdAt", "updatedAt")
550
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
551
+ [
552
+ definition.id,
553
+ projectId,
554
+ 'org_demo',
555
+ definition.name,
556
+ definition.description,
557
+ definition.type,
558
+ definition.status,
559
+ definition.createdAt,
560
+ definition.updatedAt,
561
+ ]
562
+ );
563
+
564
+ for (const step of definition.steps) {
565
+ await db.execute(
566
+ `INSERT INTO workflow_step (id, "definitionId", name, description, "stepOrder", type, "requiredRoles", "createdAt")
567
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
568
+ [
569
+ step.id,
570
+ definition.id,
571
+ step.name,
572
+ step.description,
573
+ step.stepOrder,
574
+ step.type,
575
+ JSON.stringify(step.requiredRoles),
576
+ step.createdAt,
577
+ ]
578
+ );
579
+ }
580
+ }
581
+
582
+ for (const instance of instances) {
583
+ await db.execute(
584
+ `INSERT INTO workflow_instance (id, "projectId", "definitionId", status, "currentStepId", data, "requestedBy", "startedAt", "completedAt")
585
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
586
+ [
587
+ instance.id,
588
+ projectId,
589
+ instance.definitionId,
590
+ instance.status,
591
+ instance.currentStepId,
592
+ JSON.stringify(instance.data),
593
+ instance.requestedBy,
594
+ instance.startedAt,
595
+ instance.completedAt,
596
+ ]
597
+ );
598
+
599
+ for (const approval of instance.approvals) {
600
+ await db.execute(
601
+ `INSERT INTO workflow_approval (id, "instanceId", "stepId", status, "actorId", comment, "decidedAt", "createdAt")
602
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
603
+ [
604
+ approval.id,
605
+ instance.id,
606
+ approval.stepId,
607
+ approval.status,
608
+ approval.actorId,
609
+ approval.comment,
610
+ approval.decidedAt,
611
+ approval.createdAt,
612
+ ]
613
+ );
614
+ }
615
+ }
357
616
  }
358
617
 
359
618
  async function seedMarketplace(params: {
360
- projectId: string;
361
- db: DatabasePort;
619
+ projectId: string;
620
+ db: DatabasePort;
362
621
  }) {
363
- const { projectId, db } = params;
622
+ const { projectId, db } = params;
364
623
 
365
- const existing = await db.query(
366
- `SELECT COUNT(*) as count FROM marketplace_store WHERE "projectId" = $1`,
367
- [projectId]
368
- );
369
- if ((existing.rows[0]?.count as number) > 0) return;
624
+ const existing = await db.query(
625
+ `SELECT COUNT(*) as count FROM marketplace_store WHERE "projectId" = $1`,
626
+ [projectId]
627
+ );
628
+ if ((existing.rows[0]?.count as number) > 0) return;
370
629
 
371
- await db.execute(
372
- `INSERT INTO marketplace_store (id, "projectId", "organizationId", name, description, status)
630
+ await db.execute(
631
+ `INSERT INTO marketplace_store (id, "projectId", "organizationId", name, description, status)
373
632
  VALUES ($1, $2, $3, $4, $5, $6)`,
374
- ['store_1', projectId, 'org_demo', 'Demo Store', 'A demo store', 'ACTIVE']
375
- );
633
+ ['store_1', projectId, 'org_demo', 'Demo Store', 'A demo store', 'ACTIVE']
634
+ );
376
635
  }
377
636
 
378
637
  async function seedIntegrationHub(params: {
379
- projectId: string;
380
- db: DatabasePort;
638
+ projectId: string;
639
+ db: DatabasePort;
381
640
  }) {
382
- const { projectId, db } = params;
641
+ const { projectId, db } = params;
383
642
 
384
- const existing = await db.query(
385
- `SELECT COUNT(*) as count FROM integration WHERE "projectId" = $1`,
386
- [projectId]
387
- );
388
- if ((existing.rows[0]?.count as number) > 0) return;
643
+ const existing = await db.query(
644
+ `SELECT COUNT(*) as count FROM integration WHERE "projectId" = $1`,
645
+ [projectId]
646
+ );
647
+ if ((existing.rows[0]?.count as number) > 0) return;
389
648
 
390
- await db.execute(
391
- `INSERT INTO integration (id, "projectId", "organizationId", name, description, type, status)
649
+ await db.execute(
650
+ `INSERT INTO integration (id, "projectId", "organizationId", name, description, type, status)
392
651
  VALUES ($1, $2, $3, $4, $5, $6, $7)`,
393
- [
394
- 'int_1',
395
- projectId,
396
- 'org_demo',
397
- 'Salesforce',
398
- 'Salesforce CRM integration',
399
- 'CRM',
400
- 'ACTIVE',
401
- ]
402
- );
652
+ [
653
+ 'int_1',
654
+ projectId,
655
+ 'org_demo',
656
+ 'Salesforce',
657
+ 'Salesforce CRM integration',
658
+ 'CRM',
659
+ 'ACTIVE',
660
+ ]
661
+ );
403
662
  }
404
663
 
405
664
  async function seedAnalyticsDashboard(params: {
406
- projectId: string;
407
- db: DatabasePort;
665
+ projectId: string;
666
+ db: DatabasePort;
408
667
  }) {
409
- const { projectId, db } = params;
668
+ const { projectId, db } = params;
410
669
 
411
- const existing = await db.query(
412
- `SELECT COUNT(*) as count FROM analytics_dashboard WHERE "projectId" = $1`,
413
- [projectId]
414
- );
415
- if ((existing.rows[0]?.count as number) > 0) return;
670
+ const existing = await db.query(
671
+ `SELECT COUNT(*) as count FROM analytics_dashboard WHERE "projectId" = $1`,
672
+ [projectId]
673
+ );
674
+ if ((existing.rows[0]?.count as number) > 0) return;
416
675
 
417
- await db.execute(
418
- `INSERT INTO analytics_dashboard (id, "projectId", "organizationId", name, slug, description, status)
676
+ await db.execute(
677
+ `INSERT INTO analytics_dashboard (id, "projectId", "organizationId", name, slug, description, status)
419
678
  VALUES ($1, $2, $3, $4, $5, $6, $7)`,
420
- [
421
- 'dash_1',
422
- projectId,
423
- 'org_demo',
424
- 'Sales Overview',
425
- 'sales-overview',
426
- 'Sales performance dashboard',
427
- 'PUBLISHED',
428
- ]
429
- );
679
+ [
680
+ 'dash_1',
681
+ projectId,
682
+ 'org_demo',
683
+ 'Sales Overview',
684
+ 'sales-overview',
685
+ 'Sales performance dashboard',
686
+ 'PUBLISHED',
687
+ ]
688
+ );
430
689
  }
431
690
 
432
691
  async function seedPolicyKnowledgeAssistant(params: {
433
- projectId: string;
434
- db: DatabasePort;
692
+ projectId: string;
693
+ db: DatabasePort;
435
694
  }) {
436
- const { projectId, db } = params;
695
+ const { projectId, db } = params;
437
696
 
438
- const existing = await db.query(
439
- `SELECT COUNT(*) as count FROM psa_user_context WHERE "projectId" = $1`,
440
- [projectId]
441
- );
442
- if ((existing.rows[0]?.count as number) > 0) return;
697
+ const existing = await db.query(
698
+ `SELECT COUNT(*) as count FROM psa_user_context WHERE "projectId" = $1`,
699
+ [projectId]
700
+ );
701
+ if ((existing.rows[0]?.count as number) > 0) return;
443
702
 
444
- await db.execute(
445
- `INSERT INTO psa_user_context ("projectId", locale, jurisdiction, "allowedScope")
703
+ await db.execute(
704
+ `INSERT INTO psa_user_context ("projectId", locale, jurisdiction, "allowedScope")
446
705
  VALUES ($1, $2, $3, $4)`,
447
- [projectId, 'en-GB', 'EU', 'education_only']
448
- );
706
+ [projectId, 'en-GB', 'EU', 'education_only']
707
+ );
449
708
  }