@specverse/engines 6.66.0 → 6.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/inference/index.d.ts +1 -1
  2. package/dist/inference/index.d.ts.map +1 -1
  3. package/dist/inference/index.js +1 -1
  4. package/dist/inference/index.js.map +1 -1
  5. package/dist/inference/quint-transpiler.d.ts +18 -0
  6. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  7. package/dist/inference/quint-transpiler.js +32 -0
  8. package/dist/inference/quint-transpiler.js.map +1 -1
  9. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +14 -5
  10. package/dist/libs/instance-factories/services/mongodb-native-services.yaml +10 -0
  11. package/dist/libs/instance-factories/services/postgres-native-services.yaml +10 -0
  12. package/dist/libs/instance-factories/services/prisma-services.yaml +10 -0
  13. package/dist/libs/instance-factories/services/templates/_shared/guards-generator.js +209 -0
  14. package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +110 -23
  15. package/dist/libs/instance-factories/services/templates/postgres-native/controller-generator.js +104 -22
  16. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +133 -23
  17. package/dist/libs/instance-factories/services/templates/prisma/guards-generator.js +151 -0
  18. package/dist/parser/convention-processor.d.ts +44 -1
  19. package/dist/parser/convention-processor.d.ts.map +1 -1
  20. package/dist/parser/convention-processor.js +175 -1
  21. package/dist/parser/convention-processor.js.map +1 -1
  22. package/dist/parser/types/ast.d.ts +1 -1
  23. package/dist/parser/types/ast.d.ts.map +1 -1
  24. package/dist/parser/unified-parser.d.ts.map +1 -1
  25. package/dist/parser/unified-parser.js +25 -2
  26. package/dist/parser/unified-parser.js.map +1 -1
  27. package/dist/realize/index.d.ts.map +1 -1
  28. package/dist/realize/index.js +17 -0
  29. package/dist/realize/index.js.map +1 -1
  30. package/libs/instance-factories/controllers/templates/fastify/__tests__/actor-wiring.test.ts +80 -0
  31. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +14 -5
  32. package/libs/instance-factories/services/mongodb-native-services.yaml +10 -0
  33. package/libs/instance-factories/services/postgres-native-services.yaml +10 -0
  34. package/libs/instance-factories/services/prisma-services.yaml +10 -0
  35. package/libs/instance-factories/services/templates/_shared/guards-generator.ts +296 -0
  36. package/libs/instance-factories/services/templates/mongodb-native/__tests__/controller-with-constraints.test.ts +192 -0
  37. package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +144 -23
  38. package/libs/instance-factories/services/templates/postgres-native/__tests__/controller-with-constraints.test.ts +192 -0
  39. package/libs/instance-factories/services/templates/postgres-native/controller-generator.ts +130 -22
  40. package/libs/instance-factories/services/templates/prisma/__tests__/controller-with-constraints.test.ts +261 -0
  41. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +186 -22
  42. package/package.json +1 -1
@@ -15,12 +15,13 @@ function generateMongoNativeController(context) {
15
15
  Object.assign(modelRegistry, models);
16
16
  }
17
17
  const customActions = generateCustomActions(controller, modelRegistry);
18
- const validate = generateValidateMethod(model, modelName);
19
- const create = curedOps.create ? generateCreateMethod(model, modelName, modelVar, collection) : "";
18
+ const hasConstraints = Array.isArray(model.constraints) && model.constraints.length > 0;
19
+ const validate = generateValidateMethod(model, modelName, hasConstraints);
20
+ const create = curedOps.create ? generateCreateMethod(model, modelName, modelVar, collection, hasConstraints) : "";
20
21
  const retrieve = curedOps.retrieve ? generateRetrieveMethod(modelName, modelVar, collection) : "";
21
- const update = curedOps.update ? generateUpdateMethod(modelName, modelVar, collection) : "";
22
- const evolve = curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, collection) : "";
23
- const del = curedOps.delete ? generateDeleteMethod(modelName, modelVar, collection) : "";
22
+ const update = curedOps.update ? generateUpdateMethod(modelName, modelVar, collection, hasConstraints) : "";
23
+ const evolve = curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, collection, hasConstraints) : "";
24
+ const del = curedOps.delete ? generateDeleteMethod(modelName, modelVar, collection, hasConstraints) : "";
24
25
  const hasEventPublishing = curedOps.create || curedOps.update || curedOps.evolve || curedOps.delete;
25
26
  return `/**
26
27
  * ${controllerName}
@@ -31,6 +32,7 @@ import { ObjectId, type Filter, type Document } from 'mongodb';
31
32
  import { getCollection } from '../db/mongoClient.js';
32
33
  ${hasEventPublishing || customActions.needsAiBehaviors ? `import { eventBus } from '../events/eventBus.js';` : ""}
33
34
  ${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${controllerName}.ai.js';` : ""}
35
+ ${hasConstraints ? `import { runGuards as runConstraintGuards } from './${modelName}.guards.js';` : ""}
34
36
 
35
37
  const COLLECTION_NAME = '${collection}';
36
38
 
@@ -67,17 +69,33 @@ function collectionName(model) {
67
69
  if (model?.storage?.collection) return String(model.storage.collection);
68
70
  return model.name.toLowerCase() + "s";
69
71
  }
70
- function generateValidateMethod(model, modelName) {
72
+ function generateValidateMethod(model, modelName, hasConstraints) {
73
+ const opTypeUnion = hasConstraints ? `'create' | 'update' | 'evolve' | 'delete' | \`evolve.\${string}\`` : `'create' | 'update' | 'evolve'`;
74
+ const constraintsCheck = hasConstraints ? `
75
+ // Phase 2 \u2014 run model.constraints[] guards matching this operation.
76
+ // Slice 15b \u2014 ctx carries a per-model query helper for subquery sugars.
77
+ const __guardCtx = {
78
+ query: (modelName: string) => ({
79
+ exists: async (predicate: (e: any) => boolean) => {
80
+ const __col = await getCollection(modelName.toLowerCase() + 's');
81
+ const all = await __col.find({}).toArray();
82
+ return all.some(predicate);
83
+ },
84
+ }),
85
+ };
86
+ const constraintViolations = await runConstraintGuards(_data, _context.operation, _actor, __guardCtx);
87
+ for (const v of constraintViolations) errors.push(v.message);` : "";
71
88
  return `
72
89
  /**
73
- * Validate ${modelName} data \u2014 runs before create / update / evolve.
90
+ * Validate ${modelName} data \u2014 runs before create / update / evolve / delete.
74
91
  */
75
- public validate(
92
+ public async validate(
76
93
  _data: any,
77
- _context: { operation: 'create' | 'update' | 'evolve' }
78
- ): { valid: boolean; errors: string[] } {
94
+ _context: { operation: ${opTypeUnion} },
95
+ _actor: any = null
96
+ ): Promise<{ valid: boolean; errors: string[] }> {
79
97
  const errors: string[] = [];
80
- ${generateValidationLogic(model)}
98
+ ${generateValidationLogic(model)}${constraintsCheck}
81
99
  return { valid: errors.length === 0, errors };
82
100
  }
83
101
  `;
@@ -104,13 +122,24 @@ function generateValidationLogic(model) {
104
122
  });
105
123
  return out.join("\n") || " // No validation rules defined";
106
124
  }
107
- function generateCreateMethod(model, modelName, modelVar, collection) {
125
+ function generateCreateMethod(model, modelName, modelVar, collection, hasConstraints = false) {
126
+ const belongsToLoad = hasConstraints ? generateMongoBelongsToLoad(model) : "";
127
+ const validateSelf = belongsToLoad ? "__mergedSelf" : "data";
108
128
  return `
109
129
  /**
110
130
  * Create a new ${modelName}.
111
131
  */
112
- public async create(data: any): Promise<any> {
113
- const validation = this.validate(data, { operation: 'create' });
132
+ public async create(data: any, _actor: any = null): Promise<any> {
133
+ ${belongsToLoad ? `
134
+ // Phase 2 Slice 15 \u2014 Create-time relation loading. Mirrors prisma's
135
+ // pattern: for each belongsTo FK in input, load the related doc and
136
+ // merge into self so constraint guards traversing self.<rel> see
137
+ // the full related entity, not just the FK id.
138
+ const __loadedRels: Record<string, any> = {};
139
+ ${belongsToLoad}
140
+ const __mergedSelf = { ...data, ...__loadedRels };
141
+ ` : ""}
142
+ const validation = await this.validate(${validateSelf}, { operation: 'create' }, _actor);
114
143
  if (!validation.valid) throw new Error(\`Validation failed: \${validation.errors.join(', ')}\`);
115
144
 
116
145
  const collection = await getCollection(COLLECTION_NAME);
@@ -122,6 +151,20 @@ function generateCreateMethod(model, modelName, modelVar, collection) {
122
151
  }
123
152
  `;
124
153
  }
154
+ function generateMongoBelongsToLoad(model) {
155
+ const rels = Array.isArray(model.relationships) ? model.relationships : Object.values(model.relationships || {});
156
+ const belongsToRels = rels.filter((r) => r.type === "belongsTo");
157
+ if (belongsToRels.length === 0) return "";
158
+ return belongsToRels.map((rel) => {
159
+ const relName = rel.name;
160
+ const targetName = rel.target;
161
+ const fkField = `${relName}Id`;
162
+ return `if (data.${fkField}) {
163
+ const __col_${relName} = await getCollection('${targetName.toLowerCase()}s');
164
+ __loadedRels.${relName} = await __col_${relName}.findOne(byId(data.${fkField}));
165
+ }`;
166
+ }).join("\n ");
167
+ }
125
168
  function generateRetrieveMethod(modelName, modelVar, collection) {
126
169
  return `
127
170
  /**
@@ -144,13 +187,23 @@ function generateRetrieveMethod(modelName, modelVar, collection) {
144
187
  }
145
188
  `;
146
189
  }
147
- function generateUpdateMethod(modelName, modelVar, collection) {
190
+ function generateUpdateMethod(modelName, modelVar, collection, hasConstraints = false) {
148
191
  return `
149
192
  /**
150
193
  * Update ${modelName}.
151
194
  */
152
- public async update(id: string, data: any): Promise<any> {
153
- const validation = this.validate(data, { operation: 'update' });
195
+ public async update(id: string, data: any, _actor: any = null): Promise<any> {
196
+ const collection = await getCollection(COLLECTION_NAME);
197
+ ${hasConstraints ? `
198
+ // Phase 2 \u2014 Update self-from-DB. Load + merge before validate so
199
+ // update-time constraints see the full entity, not just the partial input.
200
+ const __existing = await collection.findOne(byId(id));
201
+ if (!__existing) throw new Error('${modelName} not found');
202
+ const __merged = { ...__existing, ...data };
203
+ const validation = await this.validate(__merged, { operation: 'update' }, _actor);
204
+ ` : `
205
+ const validation = await this.validate(data, { operation: 'update' }, _actor);
206
+ `}
154
207
  if (!validation.valid) throw new Error(\`Validation failed: \${validation.errors.join(', ')}\`);
155
208
 
156
209
  // Strip nested objects + id \u2014 only scalar fields are written.
@@ -162,7 +215,6 @@ function generateUpdateMethod(modelName, modelVar, collection) {
162
215
  updateData[key] = value;
163
216
  }
164
217
 
165
- const collection = await getCollection(COLLECTION_NAME);
166
218
  await collection.updateOne(byId(id), { $set: updateData });
167
219
  const ${modelVar} = await collection.findOne(byId(id));
168
220
  if (!${modelVar}) throw new Error('${modelName} not found after update');
@@ -172,7 +224,7 @@ function generateUpdateMethod(modelName, modelVar, collection) {
172
224
  }
173
225
  `;
174
226
  }
175
- function generateEvolveMethod(model, modelName, modelVar, collection) {
227
+ function generateEvolveMethod(model, modelName, modelVar, collection, hasConstraints = false) {
176
228
  const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
177
229
  const lifecycle = lifecycles[0];
178
230
  const lifecycleName = lifecycle?.name || "status";
@@ -181,12 +233,23 @@ function generateEvolveMethod(model, modelName, modelVar, collection) {
181
233
  ...Object.keys(validTransitions),
182
234
  ...Object.values(validTransitions).flat()
183
235
  ]));
236
+ const evolveOpsMap = {};
237
+ if (lifecycle?.transitions) {
238
+ for (const [actionName, t] of Object.entries(lifecycle.transitions)) {
239
+ const from = t.from;
240
+ const to = t.to;
241
+ if (typeof from === "string" && typeof to === "string") {
242
+ evolveOpsMap[from] = evolveOpsMap[from] ?? {};
243
+ evolveOpsMap[from][to] = actionName;
244
+ }
245
+ }
246
+ }
184
247
  return `
185
248
  /**
186
249
  * Evolve ${modelName} through lifecycle "${lifecycleName}"
187
250
  * States: ${states.join(" \u2192 ") || "(none declared)"}
188
251
  */
189
- public async evolve(id: string, data: any): Promise<any> {
252
+ public async evolve(id: string, data: any, _actor: any = null): Promise<any> {
190
253
  const collection = await getCollection(COLLECTION_NAME);
191
254
  const current = await collection.findOne(byId(id));
192
255
  if (!current) throw new Error('${modelName} not found');
@@ -203,7 +266,22 @@ function generateEvolveMethod(model, modelName, modelVar, collection) {
203
266
  throw new Error(\`Invalid transition: \${currentState} \u2192 \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
204
267
  }
205
268
  ` : ""}
206
-
269
+ ${hasConstraints ? `
270
+ // Phase 2 \u2014 resolve the transition's action name so constraints
271
+ // scoped to \`on: 'evolve.<action>'\` match correctly. EVOLVE_OPS is
272
+ // baked at codegen from the lifecycle definition.
273
+ const EVOLVE_OPS: Record<string, Record<string, string>> = ${JSON.stringify(evolveOpsMap)};
274
+ const currentStateForOp = (current as any)[targetLifecycle];
275
+ const actionName = EVOLVE_OPS[currentStateForOp]?.[targetState] ?? targetState;
276
+ const evolveValidation = await this.validate(
277
+ { ...data, ...current, [targetLifecycle]: targetState },
278
+ { operation: \`evolve.\${actionName}\` as any },
279
+ _actor
280
+ );
281
+ if (!evolveValidation.valid) {
282
+ throw new Error(\`Validation failed: \${evolveValidation.errors.join(', ')}\`);
283
+ }
284
+ ` : ""}
207
285
  await collection.updateOne(byId(id), { $set: { [targetLifecycle]: targetState } });
208
286
  const ${modelVar} = await collection.findOne(byId(id));
209
287
  if (!${modelVar}) throw new Error('${modelName} not found after evolve');
@@ -213,14 +291,23 @@ function generateEvolveMethod(model, modelName, modelVar, collection) {
213
291
  }
214
292
  `;
215
293
  }
216
- function generateDeleteMethod(modelName, modelVar, collection) {
294
+ function generateDeleteMethod(modelName, modelVar, collection, hasConstraints = false) {
217
295
  return `
218
296
  /**
219
297
  * Delete ${modelName}.
220
298
  */
221
- public async delete(id: string): Promise<void> {
299
+ public async delete(id: string, _actor: any = null): Promise<void> {
222
300
  const collection = await getCollection(COLLECTION_NAME);
223
301
  const ${modelVar} = await collection.findOne(byId(id));
302
+ ${hasConstraints ? `
303
+ // Phase 2 \u2014 run delete-scoped constraint guards against the loaded record.
304
+ if (${modelVar}) {
305
+ const deleteValidation = await this.validate(${modelVar}, { operation: 'delete' }, _actor);
306
+ if (!deleteValidation.valid) {
307
+ throw new Error(\`Validation failed: \${deleteValidation.errors.join(', ')}\`);
308
+ }
309
+ }
310
+ ` : ""}
224
311
  await collection.deleteOne(byId(id));
225
312
  if (${modelVar}) {
226
313
  await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
@@ -15,12 +15,13 @@ function generatePgNativeController(context) {
15
15
  Object.assign(modelRegistry, models);
16
16
  }
17
17
  const customActions = generateCustomActions(controller, modelRegistry);
18
- const validate = generateValidateMethod(model, modelName);
19
- const create = curedOps.create ? generateCreateMethod(modelName, modelVar) : "";
18
+ const hasConstraints = Array.isArray(model.constraints) && model.constraints.length > 0;
19
+ const validate = generateValidateMethod(model, modelName, hasConstraints);
20
+ const create = curedOps.create ? generateCreateMethod(model, modelName, modelVar, hasConstraints) : "";
20
21
  const retrieve = curedOps.retrieve ? generateRetrieveMethod(modelName, modelVar) : "";
21
- const update = curedOps.update ? generateUpdateMethod(modelName, modelVar) : "";
22
- const evolve = curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar) : "";
23
- const del = curedOps.delete ? generateDeleteMethod(modelName, modelVar) : "";
22
+ const update = curedOps.update ? generateUpdateMethod(modelName, modelVar, hasConstraints) : "";
23
+ const evolve = curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, hasConstraints) : "";
24
+ const del = curedOps.delete ? generateDeleteMethod(modelName, modelVar, hasConstraints) : "";
24
25
  const hasEventPublishing = curedOps.create || curedOps.update || curedOps.evolve || curedOps.delete;
25
26
  const helperImports = [
26
27
  "insertOne",
@@ -40,6 +41,7 @@ function generatePgNativeController(context) {
40
41
  import { ${helperImports} } from '../db/pgClient.js';
41
42
  ${hasEventPublishing || customActions.needsAiBehaviors ? `import { eventBus } from '../events/eventBus.js';` : ""}
42
43
  ${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${controllerName}.ai.js';` : ""}
44
+ ${hasConstraints ? `import { runGuards as runConstraintGuards } from './${modelName}.guards.js';` : ""}
43
45
 
44
46
  const TABLE_NAME = '${table}';
45
47
 
@@ -64,17 +66,32 @@ function tableName(model) {
64
66
  if (model?.storage?.table) return String(model.storage.table);
65
67
  return model.name.toLowerCase() + "s";
66
68
  }
67
- function generateValidateMethod(model, modelName) {
69
+ function generateValidateMethod(model, modelName, hasConstraints) {
70
+ const opTypeUnion = hasConstraints ? `'create' | 'update' | 'evolve' | 'delete' | \`evolve.\${string}\`` : `'create' | 'update' | 'evolve'`;
71
+ const constraintsCheck = hasConstraints ? `
72
+ // Phase 2 \u2014 run model.constraints[] guards matching this operation.
73
+ // Slice 15b \u2014 ctx carries a per-model query helper for subquery sugars.
74
+ const __guardCtx = {
75
+ query: (modelName: string) => ({
76
+ exists: async (predicate: (e: any) => boolean) => {
77
+ const all = await findAll(modelName);
78
+ return all.some(predicate);
79
+ },
80
+ }),
81
+ };
82
+ const constraintViolations = await runConstraintGuards(_data, _context.operation, _actor, __guardCtx);
83
+ for (const v of constraintViolations) errors.push(v.message);` : "";
68
84
  return `
69
85
  /**
70
- * Validate ${modelName} data \u2014 runs before create / update / evolve.
86
+ * Validate ${modelName} data \u2014 runs before create / update / evolve / delete.
71
87
  */
72
- public validate(
88
+ public async validate(
73
89
  _data: any,
74
- _context: { operation: 'create' | 'update' | 'evolve' }
75
- ): { valid: boolean; errors: string[] } {
90
+ _context: { operation: ${opTypeUnion} },
91
+ _actor: any = null
92
+ ): Promise<{ valid: boolean; errors: string[] }> {
76
93
  const errors: string[] = [];
77
- ${generateValidationLogic(model)}
94
+ ${generateValidationLogic(model)}${constraintsCheck}
78
95
  return { valid: errors.length === 0, errors };
79
96
  }
80
97
  `;
@@ -101,13 +118,23 @@ function generateValidationLogic(model) {
101
118
  });
102
119
  return out.join("\n") || " // No validation rules defined";
103
120
  }
104
- function generateCreateMethod(modelName, modelVar) {
121
+ function generateCreateMethod(model, modelName, modelVar, hasConstraints = false) {
122
+ const belongsToLoad = hasConstraints ? generatePgBelongsToLoad(model) : "";
123
+ const validateSelf = belongsToLoad ? "__mergedSelf" : "data";
105
124
  return `
106
125
  /**
107
126
  * Create a new ${modelName}.
108
127
  */
109
- public async create(data: any): Promise<any> {
110
- const validation = this.validate(data, { operation: 'create' });
128
+ public async create(data: any, _actor: any = null): Promise<any> {
129
+ ${belongsToLoad ? `
130
+ // Phase 2 Slice 15 \u2014 Create-time relation loading. Mirrors prisma's
131
+ // pattern: for each belongsTo FK in input, load the related row and
132
+ // merge into self for constraint guards.
133
+ const __loadedRels: Record<string, any> = {};
134
+ ${belongsToLoad}
135
+ const __mergedSelf = { ...data, ...__loadedRels };
136
+ ` : ""}
137
+ const validation = await this.validate(${validateSelf}, { operation: 'create' }, _actor);
111
138
  if (!validation.valid) throw new Error(\`Validation failed: \${validation.errors.join(', ')}\`);
112
139
 
113
140
  const ${modelVar} = await insertOne(TABLE_NAME, { ...data });
@@ -117,6 +144,19 @@ function generateCreateMethod(modelName, modelVar) {
117
144
  }
118
145
  `;
119
146
  }
147
+ function generatePgBelongsToLoad(model) {
148
+ const rels = Array.isArray(model.relationships) ? model.relationships : Object.values(model.relationships || {});
149
+ const belongsToRels = rels.filter((r) => r.type === "belongsTo");
150
+ if (belongsToRels.length === 0) return "";
151
+ return belongsToRels.map((rel) => {
152
+ const relName = rel.name;
153
+ const targetName = rel.target;
154
+ const fkField = `${relName}Id`;
155
+ return `if (data.${fkField}) {
156
+ __loadedRels.${relName} = await findOneByField('${targetName}', 'id', data.${fkField});
157
+ }`;
158
+ }).join("\n ");
159
+ }
120
160
  function generateRetrieveMethod(modelName, _modelVar) {
121
161
  return `
122
162
  /**
@@ -140,13 +180,22 @@ function generateRetrieveMethod(modelName, _modelVar) {
140
180
  }
141
181
  `;
142
182
  }
143
- function generateUpdateMethod(modelName, modelVar) {
183
+ function generateUpdateMethod(modelName, modelVar, hasConstraints = false) {
144
184
  return `
145
185
  /**
146
186
  * Update ${modelName}.
147
187
  */
148
- public async update(id: string, data: any): Promise<any> {
149
- const validation = this.validate(data, { operation: 'update' });
188
+ public async update(id: string, data: any, _actor: any = null): Promise<any> {
189
+ ${hasConstraints ? `
190
+ // Phase 2 \u2014 Update self-from-DB. Load + merge before validate so
191
+ // update-time constraints see the full entity, not just partial input.
192
+ const __existing = await findOneByField(TABLE_NAME, 'id', id);
193
+ if (!__existing) throw new Error('${modelName} not found');
194
+ const __merged = { ...__existing, ...data };
195
+ const validation = await this.validate(__merged, { operation: 'update' }, _actor);
196
+ ` : `
197
+ const validation = await this.validate(data, { operation: 'update' }, _actor);
198
+ `}
150
199
  if (!validation.valid) throw new Error(\`Validation failed: \${validation.errors.join(', ')}\`);
151
200
 
152
201
  // Strip nested objects + id \u2014 only scalar fields are written.
@@ -167,7 +216,7 @@ function generateUpdateMethod(modelName, modelVar) {
167
216
  }
168
217
  `;
169
218
  }
170
- function generateEvolveMethod(model, modelName, modelVar) {
219
+ function generateEvolveMethod(model, modelName, modelVar, hasConstraints = false) {
171
220
  const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
172
221
  const lifecycle = lifecycles[0];
173
222
  const lifecycleName = lifecycle?.name || "status";
@@ -176,12 +225,23 @@ function generateEvolveMethod(model, modelName, modelVar) {
176
225
  ...Object.keys(validTransitions),
177
226
  ...Object.values(validTransitions).flat()
178
227
  ]));
228
+ const evolveOpsMap = {};
229
+ if (lifecycle?.transitions) {
230
+ for (const [actionName, t] of Object.entries(lifecycle.transitions)) {
231
+ const from = t.from;
232
+ const to = t.to;
233
+ if (typeof from === "string" && typeof to === "string") {
234
+ evolveOpsMap[from] = evolveOpsMap[from] ?? {};
235
+ evolveOpsMap[from][to] = actionName;
236
+ }
237
+ }
238
+ }
179
239
  return `
180
240
  /**
181
241
  * Evolve ${modelName} through lifecycle "${lifecycleName}"
182
242
  * States: ${states.join(" \u2192 ") || "(none declared)"}
183
243
  */
184
- public async evolve(id: string, data: any): Promise<any> {
244
+ public async evolve(id: string, data: any, _actor: any = null): Promise<any> {
185
245
  const current = await findOneByField(TABLE_NAME, 'id', id);
186
246
  if (!current) throw new Error('${modelName} not found');
187
247
 
@@ -197,7 +257,20 @@ function generateEvolveMethod(model, modelName, modelVar) {
197
257
  throw new Error(\`Invalid transition: \${currentState} \u2192 \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
198
258
  }
199
259
  ` : ""}
200
-
260
+ ${hasConstraints ? `
261
+ // Phase 2 \u2014 resolve transition action name + call validate with evolve.<action>.
262
+ const EVOLVE_OPS: Record<string, Record<string, string>> = ${JSON.stringify(evolveOpsMap)};
263
+ const currentStateForOp = (current as any)[targetLifecycle];
264
+ const actionName = EVOLVE_OPS[currentStateForOp]?.[targetState] ?? targetState;
265
+ const evolveValidation = await this.validate(
266
+ { ...data, ...current, [targetLifecycle]: targetState },
267
+ { operation: \`evolve.\${actionName}\` as any },
268
+ _actor
269
+ );
270
+ if (!evolveValidation.valid) {
271
+ throw new Error(\`Validation failed: \${evolveValidation.errors.join(', ')}\`);
272
+ }
273
+ ` : ""}
201
274
  await updateOneById(TABLE_NAME, id, { [targetLifecycle]: targetState });
202
275
  const ${modelVar} = await findOneByField(TABLE_NAME, 'id', id);
203
276
  if (!${modelVar}) throw new Error('${modelName} not found after evolve');
@@ -207,13 +280,22 @@ function generateEvolveMethod(model, modelName, modelVar) {
207
280
  }
208
281
  `;
209
282
  }
210
- function generateDeleteMethod(modelName, modelVar) {
283
+ function generateDeleteMethod(modelName, modelVar, hasConstraints = false) {
211
284
  return `
212
285
  /**
213
286
  * Delete ${modelName}.
214
287
  */
215
- public async delete(id: string): Promise<void> {
288
+ public async delete(id: string, _actor: any = null): Promise<void> {
216
289
  const ${modelVar} = await findOneByField(TABLE_NAME, 'id', id);
290
+ ${hasConstraints ? `
291
+ // Phase 2 \u2014 run delete-scoped constraint guards against the loaded record.
292
+ if (${modelVar}) {
293
+ const deleteValidation = await this.validate(${modelVar}, { operation: 'delete' }, _actor);
294
+ if (!deleteValidation.valid) {
295
+ throw new Error(\`Validation failed: \${deleteValidation.errors.join(', ')}\`);
296
+ }
297
+ }
298
+ ` : ""}
217
299
  await deleteOneById(TABLE_NAME, id);
218
300
  if (${modelVar}) {
219
301
  await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);