@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
@@ -19,13 +19,14 @@ function generatePrismaController(context) {
19
19
  const idType = idAttr?.type || "UUID";
20
20
  const needsIntParse = idType === "Integer" || idType === "Int" || idType === "Number";
21
21
  const customActions = generateCustomActions(controller, modelName, modelVar);
22
+ const hasConstraints = Array.isArray(model.constraints) && model.constraints.length > 0;
22
23
  const classBody = [
23
- generateValidateMethod(model, modelName),
24
- curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : "",
24
+ generateValidateMethod(model, modelName, hasConstraints),
25
+ curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints) : "",
25
26
  curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) : "",
26
- curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : "",
27
- curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) : "",
28
- curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) : "",
27
+ curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints) : "",
28
+ curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints) : "",
29
+ curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints) : "",
29
30
  customActions.code
30
31
  ].filter(Boolean).join("\n ");
31
32
  const usesPrisma = /\bprisma\b/.test(classBody);
@@ -35,7 +36,8 @@ function generatePrismaController(context) {
35
36
  const imports = [
36
37
  usesPrisma ? `import { PrismaClient } from '@prisma/client';` : "",
37
38
  usesEventBus ? `import { eventBus } from '../events/eventBus.js';` : "",
38
- usesAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : ""
39
+ usesAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : "",
40
+ hasConstraints ? `import { runGuards as runConstraintGuards } from './${modelName}.guards.js';` : ""
39
41
  ].filter(Boolean).join("\n");
40
42
  const declarations = [
41
43
  usesPrisma ? `const prisma = new PrismaClient();` : "",
@@ -62,19 +64,39 @@ export const ${modelVar}Controller = new ${controllerName}();
62
64
  export default ${modelVar}Controller;
63
65
  `;
64
66
  }
65
- function generateValidateMethod(model, modelName) {
67
+ function generateValidateMethod(model, modelName, hasConstraints) {
68
+ const opTypeUnion = hasConstraints ? `'create' | 'update' | 'evolve' | 'delete' | \`evolve.\${string}\`` : `'create' | 'update' | 'evolve'`;
69
+ const constraintsCheck = hasConstraints ? `
70
+ // Phase 2 \u2014 run model.constraints[] guards matching this operation.
71
+ // ctx carries a per-model query helper so subquery sugars (rewritten
72
+ // to async guards by guards-generator) can run their findFirst calls.
73
+ const __guardCtx = {
74
+ query: (modelName: string) => {
75
+ const delegate = (prisma as any)[modelName.charAt(0).toLowerCase() + modelName.slice(1)];
76
+ if (!delegate) return undefined;
77
+ return {
78
+ exists: async (predicate: (e: any) => boolean) => {
79
+ const all = await delegate.findMany();
80
+ return all.some(predicate);
81
+ },
82
+ };
83
+ },
84
+ };
85
+ const constraintViolations = await runConstraintGuards(_data, _context.operation, _actor, __guardCtx);
86
+ for (const v of constraintViolations) errors.push(v.message);` : "";
66
87
  return `
67
88
  /**
68
89
  * Validate ${modelName} data
69
90
  * Unified validation method for all operations
70
91
  */
71
- public validate(
92
+ public async validate(
72
93
  _data: any,
73
- _context: { operation: 'create' | 'update' | 'evolve' }
74
- ): { valid: boolean; errors: string[] } {
94
+ _context: { operation: ${opTypeUnion} },
95
+ _actor: any = null
96
+ ): Promise<{ valid: boolean; errors: string[] }> {
75
97
  const errors: string[] = [];
76
98
 
77
- ${generateValidationLogic(model, "_data", "_context")}
99
+ ${generateValidationLogic(model, "_data", "_context")}${constraintsCheck}
78
100
 
79
101
  return {
80
102
  valid: errors.length === 0,
@@ -125,14 +147,27 @@ function generateValidationLogic(model, dataParam = "_data", contextParam = "_co
125
147
  });
126
148
  return validations.join("\n") || "// No validation rules defined";
127
149
  }
128
- function generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) {
150
+ function generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints = false) {
151
+ const belongsToLoad = hasConstraints ? generateBelongsToLoad(model, allModels) : "";
152
+ const validateSelf = belongsToLoad ? "__mergedSelf" : "data";
129
153
  return `
130
154
  /**
131
155
  * Create a new ${modelName}
132
156
  */
133
- public async create(data: any): Promise<any> {
157
+ public async create(data: any, _actor: any = null): Promise<any> {
158
+ ${belongsToLoad ? `
159
+ // Phase 2 Slice 15 \u2014 Create-time relation loading. For each belongsTo
160
+ // FK present in input, load the related entity and merge into self
161
+ // so constraints traversing self.<rel>.<field> see the full related
162
+ // record (not just the FK id). Mirrors Slice 8's Update pattern but
163
+ // for the create path. Only fires when the model has declared
164
+ // constraints; one extra round-trip per FK in input.
165
+ const __loadedRels: Record<string, any> = {};
166
+ ${belongsToLoad}
167
+ const __mergedSelf = { ...data, ...__loadedRels };
168
+ ` : ""}
134
169
  // Validate input
135
- const validationResult = this.validate(data, { operation: 'create' });
170
+ const validationResult = await this.validate(${validateSelf}, { operation: 'create' }, _actor);
136
171
  if (!validationResult.valid) {
137
172
  throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
138
173
  }
@@ -153,6 +188,33 @@ function generateCreateMethod(model, modelName, modelVar, prismaDelegate, contro
153
188
  }
154
189
  `;
155
190
  }
191
+ function generateBelongsToLoad(model, allModels) {
192
+ const rels = Array.isArray(model.relationships) ? model.relationships : Object.values(model.relationships || {});
193
+ const belongsToRels = rels.filter((r) => r.type === "belongsTo");
194
+ if (belongsToRels.length === 0) return "";
195
+ const RESERVED_WORDS = /* @__PURE__ */ new Set(["import", "export", "default", "class", "function", "return", "delete", "new", "this", "switch", "case", "break", "continue", "for", "while", "do", "if", "else", "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "let", "const", "var", "void", "with", "yield", "async", "await", "enum", "implements", "interface", "package", "private", "protected", "public", "static", "super", "extends"]);
196
+ return belongsToRels.map((rel) => {
197
+ const relName = rel.name;
198
+ const targetName = rel.target;
199
+ const fkField = `${relName}Id`;
200
+ const targetVar = targetName.charAt(0).toLowerCase() + targetName.slice(1);
201
+ const targetDelegate = RESERVED_WORDS.has(targetVar) ? `prisma['${targetVar}']` : `prisma.${targetVar}`;
202
+ let idExpr = `data.${fkField}`;
203
+ if (allModels) {
204
+ const targetModel = allModels.find((m) => m.name === targetName);
205
+ if (targetModel) {
206
+ const idAttr = (Array.isArray(targetModel.attributes) ? targetModel.attributes : Object.values(targetModel.attributes || {})).find((a) => a.name === "id");
207
+ const idType = idAttr?.type || "String";
208
+ if (idType === "Integer" || idType === "Int" || idType === "Number") {
209
+ idExpr = `parseInt(data.${fkField}, 10)`;
210
+ }
211
+ }
212
+ }
213
+ return `if (data.${fkField}) {
214
+ __loadedRels.${relName} = await ${targetDelegate}.findUnique({ where: { id: ${idExpr} } });
215
+ }`;
216
+ }).join("\n ");
217
+ }
156
218
  function generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) {
157
219
  return `
158
220
  /**
@@ -176,14 +238,26 @@ function generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) {
176
238
  }
177
239
  `;
178
240
  }
179
- function generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) {
241
+ function generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels, hasConstraints = false) {
180
242
  return `
181
243
  /**
182
244
  * Update ${modelName}
183
245
  */
184
- public async update(id: string, data: any): Promise<any> {
246
+ public async update(id: string, data: any, _actor: any = null): Promise<any> {
247
+ ${hasConstraints ? `
248
+ // Phase 2 \u2014 Update self-from-DB: load the entity first and validate
249
+ // against the MERGED \`loaded + input\` shape so update-time constraints
250
+ // like \`self.poll.votingStatus == "open"\` see the full record even
251
+ // when the caller only sent a partial payload. Costs one extra DB
252
+ // round-trip; only fires for models with declared constraints.
253
+ const __existing = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) }${generateIncludeRelationships(model)} });
254
+ if (!__existing) throw new Error('${modelName} not found');
255
+ const __merged = { ...__existing, ...data };
256
+ const validationResult = await this.validate(__merged, { operation: 'update' }, _actor);
257
+ ` : `
185
258
  // Validate input
186
- const validationResult = this.validate(data, { operation: 'update' });
259
+ const validationResult = await this.validate(data, { operation: 'update' }, _actor);
260
+ `}
187
261
  if (!validationResult.valid) {
188
262
  throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
189
263
  }
@@ -213,12 +287,23 @@ function generateUpdateMethod(model, modelName, modelVar, prismaDelegate, contro
213
287
  }
214
288
  `;
215
289
  }
216
- function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) {
290
+ function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints = false) {
217
291
  const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
218
292
  const lifecycle = lifecycles[0];
219
293
  const lifecycleName = lifecycle?.name || "status";
220
294
  const states = lifecycle?.states || [];
221
295
  const validTransitions = lifecycle ? buildTransitionMap(lifecycle) : {};
296
+ const evolveOpsMap = {};
297
+ if (lifecycle?.transitions) {
298
+ for (const [actionName, t] of Object.entries(lifecycle.transitions)) {
299
+ const from = t.from;
300
+ const to = t.to;
301
+ if (typeof from === "string" && typeof to === "string") {
302
+ evolveOpsMap[from] = evolveOpsMap[from] ?? {};
303
+ evolveOpsMap[from][to] = actionName;
304
+ }
305
+ }
306
+ }
222
307
  return `
223
308
  /**
224
309
  * Evolve ${modelName} through lifecycle "${lifecycleName}"
@@ -229,7 +314,7 @@ function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, contro
229
314
  * runtime's useTransitionStateMutation always sends the former; the
230
315
  * realized smoke-parity script can send either.
231
316
  */
232
- public async evolve(id: string, data: any): Promise<any> {
317
+ public async evolve(id: string, data: any, _actor: any = null): Promise<any> {
233
318
  // Get current record to check lifecycle state
234
319
  const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
235
320
  if (!current) {
@@ -254,7 +339,22 @@ function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, contro
254
339
  throw new Error(\`Invalid transition: \${currentState} \u2192 \${targetState}. Allowed: \${allowed.join(', ') || 'none'}\`);
255
340
  }
256
341
  ` : ""}
257
-
342
+ ${hasConstraints ? `
343
+ // Phase 2 \u2014 resolve the transition's action name so constraints scoped
344
+ // to \`on: 'evolve.<action>'\` match correctly. EVOLVE_OPS is baked at
345
+ // codegen from the lifecycle definition.
346
+ const EVOLVE_OPS: Record<string, Record<string, string>> = ${JSON.stringify(evolveOpsMap)};
347
+ const currentStateForOp = (current as any)[targetLifecycle];
348
+ const actionName = EVOLVE_OPS[currentStateForOp]?.[targetState] ?? targetState;
349
+ const evolveValidation = await this.validate(
350
+ { ...data, ...current, [targetLifecycle]: targetState },
351
+ { operation: \`evolve.\${actionName}\` as any },
352
+ _actor
353
+ );
354
+ if (!evolveValidation.valid) {
355
+ throw new Error(\`Validation failed: \${evolveValidation.errors.join(', ')}\`);
356
+ }
357
+ ` : ""}
258
358
  // Build the Prisma update payload \u2014 only the lifecycle column
259
359
  // changes. Strips toState/lifecycleName/state so Prisma doesn't
260
360
  // reject unknown fields.
@@ -273,15 +373,25 @@ function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, contro
273
373
  }
274
374
  `;
275
375
  }
276
- function generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) {
376
+ function generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller, hasConstraints = false) {
277
377
  return `
278
378
  /**
279
379
  * Delete ${modelName}
280
380
  */
281
- public async delete(id: string): Promise<void> {
381
+ public async delete(id: string, _actor: any = null): Promise<void> {
282
382
  // Get record before deletion for event
283
383
  const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
284
-
384
+ ${hasConstraints ? `
385
+ // Phase 2 \u2014 run delete-scoped constraint guards against the loaded
386
+ // record. \`self\` is the entity being deleted (loaded above), giving
387
+ // delete-time constraints access to the entity's current field values.
388
+ if (${modelVar}) {
389
+ const deleteValidation = await this.validate(${modelVar}, { operation: 'delete' }, _actor);
390
+ if (!deleteValidation.valid) {
391
+ throw new Error(\`Validation failed: \${deleteValidation.errors.join(', ')}\`);
392
+ }
393
+ }
394
+ ` : ""}
285
395
  await ${prismaDelegate}.delete({
286
396
  where: { id: parseId(id) }
287
397
  });
@@ -0,0 +1,151 @@
1
+ import { transpilePhase2Guard } from "@specverse/engines/inference";
2
+ function generatePrismaGuards(context) {
3
+ const { model } = context;
4
+ if (!model) {
5
+ throw new Error("Model is required for guards generation");
6
+ }
7
+ const constraints = model.constraints ?? [];
8
+ if (constraints.length === 0) {
9
+ return generateEmptyGuardsModule(model.name);
10
+ }
11
+ const guardFns = [];
12
+ const constraintsTable = [];
13
+ for (const c of constraints) {
14
+ const transpiled = transpilePhase2Guard(
15
+ c.requires.name,
16
+ c.requires.params ?? `self: any, actor: any`,
17
+ c.requires.body
18
+ );
19
+ guardFns.push(transpiled.typescript);
20
+ const onArrayLiteral = `[${c.on.map((o) => JSON.stringify(o)).join(", ")}]`;
21
+ const sourceLiteral = JSON.stringify(
22
+ c.requires.source.input ?? ""
23
+ );
24
+ constraintsTable.push(
25
+ ` {
26
+ on: ${onArrayLiteral},
27
+ guard: ${c.requires.name},
28
+ name: ${JSON.stringify(c.requires.name)},
29
+ source: ${sourceLiteral}
30
+ }`
31
+ );
32
+ }
33
+ return `/**
34
+ * Auto-generated by SpecVerse Phase 2 (Validate-Centric Constraints).
35
+ *
36
+ * Guard functions for ${model.name}'s declared constraints.
37
+ * DO NOT EDIT \u2014 regenerated on every \`spv realize all\`.
38
+ *
39
+ * Slice 1 limitations:
40
+ * - \`self\` is the input payload (not the loaded entity). For Update/Delete/
41
+ * Evolve, paths through \`self\` see only the fields the caller sent.
42
+ * - \`actor\` defaults to null. Constraints using \`actor.*\` paths fail at
43
+ * runtime when called without a user context.
44
+ */
45
+
46
+ export interface Violation {
47
+ constraint: string;
48
+ scope: string;
49
+ source: string;
50
+ message: string;
51
+ }
52
+
53
+ export interface ConstraintRecord {
54
+ on: string[];
55
+ guard: (self: any, actor: any) => boolean;
56
+ name: string;
57
+ source: string;
58
+ }
59
+
60
+ ${guardFns.join("\n\n")}
61
+
62
+ export const MODEL_CONSTRAINTS: ConstraintRecord[] = [
63
+ ${constraintsTable.join(",\n")}
64
+ ];
65
+
66
+ /**
67
+ * Match a runtime operation against a constraint's \`on:\` array.
68
+ *
69
+ * Op shapes:
70
+ * - exact string: 'create', 'update', 'delete', 'retrieve'
71
+ * - 'evolve.<transition>': specific lifecycle transition
72
+ *
73
+ * \`on:\` entry shapes:
74
+ * - '*': always matches
75
+ * - 'evolve.*': matches any 'evolve.X' op
76
+ * - else: exact-string equality
77
+ */
78
+ export function matchesOp(constraintOn: string[], op: string): boolean {
79
+ for (const o of constraintOn) {
80
+ if (o === '*') return true;
81
+ if (o === op) return true;
82
+ if (o === 'evolve.*' && op.startsWith('evolve.')) return true;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Run all constraints whose \`on:\` matches the requested op. Each guard
89
+ * is invoked with (input, actor). Returns the collected violations.
90
+ *
91
+ * A guard that returns false produces one Violation; a guard that throws
92
+ * (e.g. actor=null with an actor.* path) is caught and surfaced as a
93
+ * violation with the thrown message \u2014 fail closed.
94
+ */
95
+ export function runGuards(
96
+ input: any,
97
+ op: string,
98
+ actor: any = null,
99
+ ): Violation[] {
100
+ const violations: Violation[] = [];
101
+ for (const c of MODEL_CONSTRAINTS) {
102
+ if (!matchesOp(c.on, op)) continue;
103
+ let passed = false;
104
+ let thrown: string | null = null;
105
+ try {
106
+ passed = c.guard(input, actor);
107
+ } catch (e: any) {
108
+ thrown = e?.message ?? String(e);
109
+ }
110
+ if (!passed) {
111
+ violations.push({
112
+ constraint: c.name,
113
+ scope: op,
114
+ source: c.source,
115
+ message: thrown
116
+ ? \`Constraint "\${c.source}" could not be evaluated: \${thrown}\`
117
+ : \`Constraint "\${c.source}" failed\`,
118
+ });
119
+ }
120
+ }
121
+ return violations;
122
+ }
123
+ `;
124
+ }
125
+ function generateEmptyGuardsModule(modelName) {
126
+ return `/**
127
+ * Auto-generated by SpecVerse Phase 2 \u2014 no constraints declared on ${modelName}.
128
+ * Empty stub kept for symmetry; controllers gate their imports on this file.
129
+ */
130
+
131
+ export interface Violation {
132
+ constraint: string;
133
+ scope: string;
134
+ source: string;
135
+ message: string;
136
+ }
137
+
138
+ export const MODEL_CONSTRAINTS: any[] = [];
139
+
140
+ export function matchesOp(_constraintOn: string[], _op: string): boolean {
141
+ return false;
142
+ }
143
+
144
+ export function runGuards(_input: any, _op: string, _actor: any = null): Violation[] {
145
+ return [];
146
+ }
147
+ `;
148
+ }
149
+ export {
150
+ generatePrismaGuards as default
151
+ };
@@ -11,6 +11,10 @@ import { SpecVerseAST } from './types/ast.js';
11
11
  import { ProcessorContext } from '@specverse/types';
12
12
  export declare class ConventionProcessor implements ProcessorContext {
13
13
  warnings: string[];
14
+ /** Hard errors collected during processing. The parser rolls these up
15
+ * into the parseResult.errors so callers (e.g. app-demo's spec-loader)
16
+ * can refuse to load specs with broken constraints. */
17
+ errors: string[];
14
18
  private entityProcessors;
15
19
  private behaviouralProcessor;
16
20
  private componentEntityTypes;
@@ -20,7 +24,12 @@ export declare class ConventionProcessor implements ProcessorContext {
20
24
  */
21
25
  getWarnings(): string[];
22
26
  /**
23
- * Clear accumulated warnings
27
+ * Get accumulated hard errors. Spec consumers should treat a non-empty
28
+ * list as a load-blocker (refuse to start the spec server).
29
+ */
30
+ getErrors(): string[];
31
+ /**
32
+ * Clear accumulated warnings + errors.
24
33
  */
25
34
  clearWarnings(): void;
26
35
  /**
@@ -35,6 +44,40 @@ export declare class ConventionProcessor implements ProcessorContext {
35
44
  * Process a single component
36
45
  */
37
46
  private processComponent;
47
+ /**
48
+ * Phase 2 — expand a model's `constraints:` array (author form) plus any
49
+ * `transitions.condition` aliases into the typed `ModelConstraintSpec[]`
50
+ * the rest of the pipeline consumes.
51
+ *
52
+ * - `requires:` strings, arrays, and `{and|or|not}` trees are walked
53
+ * recursively; each leaf is expanded via BehaviouralConventionProcessor
54
+ * with the owning model as `currentModel`. Sub-expressions are joined
55
+ * with Quint `and`/`or`/`not` to produce ONE compound guard per record.
56
+ * - `transitions.condition` from structured lifecycles is auto-mapped to
57
+ * `{on: 'evolve.<transition>', requires: condition}` — author can write
58
+ * either form, never both for the same transition (current pipeline
59
+ * warns rather than errors; tightening to a parse-time conflict error
60
+ * is a follow-up).
61
+ * - Leaves that don't match any convention emit a parser warning and skip
62
+ * the affected record; remaining records still produce guards.
63
+ */
64
+ private expandModelConstraints;
65
+ /**
66
+ * Expand one `{on, requires}` record into a ModelConstraintSpec.
67
+ *
68
+ * `origin` distinguishes author-written records ('authored') from records
69
+ * synthesised from `transitions.condition` ('transition-alias'). The
70
+ * distinction is preserved in `requires.source.convention` so the
71
+ * AST → YAML round-trip only emits author records back as `model.constraints`
72
+ * (aliased ones still live in `lifecycles.*.transitions.<action>` as
73
+ * `when=` clauses — emitting them in both places would double-validate).
74
+ */
75
+ private expandConstraintRecord;
76
+ /**
77
+ * Recursively expand a `requires:` tree into a single Quint expression.
78
+ * Returns null if any leaf fails to match a convention.
79
+ */
80
+ private expandRequiresTree;
38
81
  /**
39
82
  * Process a single manifest
40
83
  */
@@ -1 +1 @@
1
- {"version":3,"file":"convention-processor.d.ts","sourceRoot":"","sources":["../../src/parser/convention-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAIL,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAQpD,qBAAa,mBAAoB,YAAW,gBAAgB;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAM;IAE/B,OAAO,CAAC,gBAAgB,CAAyC;IACjE,OAAO,CAAC,oBAAoB,CAAiC;IAE7D,OAAO,CAAC,oBAAoB,CAAW;;IAwCvC;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAIvB;;OAEG;IACH,aAAa,IAAI,IAAI;IAIrB;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIjC;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,YAAY;IAiCpC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAyExB;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAwDhC;;;OAGG;IACH,OAAO,CAAC,UAAU;IA+ClB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;OAEG;IACH,OAAO,CAAC,eAAe;CAYxB"}
1
+ {"version":3,"file":"convention-processor.d.ts","sourceRoot":"","sources":["../../src/parser/convention-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAIL,YAAY,EAKb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAQpD,qBAAa,mBAAoB,YAAW,gBAAgB;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAM;IAC/B;;2DAEuD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAM;IAE7B,OAAO,CAAC,gBAAgB,CAAyC;IACjE,OAAO,CAAC,oBAAoB,CAAiC;IAE7D,OAAO,CAAC,oBAAoB,CAAW;;IAwCvC;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAIvB;;;OAGG;IACH,SAAS,IAAI,MAAM,EAAE;IAIrB;;OAEG;IACH,aAAa,IAAI,IAAI;IAKrB;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIjC;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,YAAY;IAiCpC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAwGxB;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,sBAAsB;IA8C9B;;;;;;;;;OASG;IACH,OAAO,CAAC,sBAAsB;IAkD9B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAuC1B;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAwDhC;;;OAGG;IACH,OAAO,CAAC,UAAU;IA+ClB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;OAEG;IACH,OAAO,CAAC,eAAe;CAYxB"}