@specverse/engines 6.53.1 → 6.63.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 (84) hide show
  1. package/dist/ai/analyse-runner.d.ts.map +1 -1
  2. package/dist/ai/analyse-runner.js +22 -1
  3. package/dist/ai/analyse-runner.js.map +1 -1
  4. package/dist/analyse-prepass/adapters/module-functions.d.ts +25 -0
  5. package/dist/analyse-prepass/adapters/module-functions.d.ts.map +1 -1
  6. package/dist/analyse-prepass/adapters/module-functions.js +54 -0
  7. package/dist/analyse-prepass/adapters/module-functions.js.map +1 -1
  8. package/dist/analyse-prepass/backends/gitnexus.d.ts +28 -0
  9. package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -1
  10. package/dist/analyse-prepass/backends/gitnexus.js +36 -2
  11. package/dist/analyse-prepass/backends/gitnexus.js.map +1 -1
  12. package/dist/analyse-prepass/index.d.ts.map +1 -1
  13. package/dist/analyse-prepass/index.js +17 -1
  14. package/dist/analyse-prepass/index.js.map +1 -1
  15. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +29 -10
  16. package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +11 -0
  17. package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +39 -19
  18. package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +35 -18
  19. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +16 -11
  20. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +1 -1
  21. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +34 -12
  22. package/dist/libs/instance-factories/services/templates/shared-patterns.js +5 -5
  23. package/dist/realize/index.d.ts.map +1 -1
  24. package/dist/realize/index.js +91 -10
  25. package/dist/realize/index.js.map +1 -1
  26. package/dist/realize/per-action-recovery.d.ts +74 -0
  27. package/dist/realize/per-action-recovery.d.ts.map +1 -0
  28. package/dist/realize/per-action-recovery.js +255 -0
  29. package/dist/realize/per-action-recovery.js.map +1 -0
  30. package/dist/realize/per-owner-emit.d.ts +6 -0
  31. package/dist/realize/per-owner-emit.d.ts.map +1 -1
  32. package/dist/realize/per-owner-emit.js +22 -6
  33. package/dist/realize/per-owner-emit.js.map +1 -1
  34. package/dist/realize/per-owner-runner.d.ts +23 -2
  35. package/dist/realize/per-owner-runner.d.ts.map +1 -1
  36. package/dist/realize/per-owner-runner.js +91 -46
  37. package/dist/realize/per-owner-runner.js.map +1 -1
  38. package/dist/realize/post-emit-verify/diagnostics.d.ts +107 -0
  39. package/dist/realize/post-emit-verify/diagnostics.d.ts.map +1 -0
  40. package/dist/realize/post-emit-verify/diagnostics.js +148 -0
  41. package/dist/realize/post-emit-verify/diagnostics.js.map +1 -0
  42. package/dist/realize/post-emit-verify/feedback-runner.d.ts +41 -1
  43. package/dist/realize/post-emit-verify/feedback-runner.d.ts.map +1 -1
  44. package/dist/realize/post-emit-verify/feedback-runner.js +62 -6
  45. package/dist/realize/post-emit-verify/feedback-runner.js.map +1 -1
  46. package/dist/realize/post-emit-verify/index.d.ts +4 -2
  47. package/dist/realize/post-emit-verify/index.d.ts.map +1 -1
  48. package/dist/realize/post-emit-verify/index.js +3 -1
  49. package/dist/realize/post-emit-verify/index.js.map +1 -1
  50. package/dist/realize/post-emit-verify/reemit.d.ts +22 -1
  51. package/dist/realize/post-emit-verify/reemit.d.ts.map +1 -1
  52. package/dist/realize/post-emit-verify/reemit.js +20 -18
  53. package/dist/realize/post-emit-verify/reemit.js.map +1 -1
  54. package/dist/realize/post-emit-verify/types.d.ts +49 -0
  55. package/dist/realize/post-emit-verify/types.d.ts.map +1 -1
  56. package/dist/realize/post-emit-verify/verifier-manifest.d.ts.map +1 -1
  57. package/dist/realize/post-emit-verify/verifier-manifest.js +2 -0
  58. package/dist/realize/post-emit-verify/verifier-manifest.js.map +1 -1
  59. package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts +127 -0
  60. package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts.map +1 -0
  61. package/dist/realize/post-emit-verify/verifiers/stub-completeness.js +423 -0
  62. package/dist/realize/post-emit-verify/verifiers/stub-completeness.js.map +1 -0
  63. package/dist/realize/realize-context-snapshot.d.ts +70 -0
  64. package/dist/realize/realize-context-snapshot.d.ts.map +1 -0
  65. package/dist/realize/realize-context-snapshot.js +96 -0
  66. package/dist/realize/realize-context-snapshot.js.map +1 -0
  67. package/dist/realize/structural-validator.d.ts +36 -2
  68. package/dist/realize/structural-validator.d.ts.map +1 -1
  69. package/dist/realize/structural-validator.js +50 -7
  70. package/dist/realize/structural-validator.js.map +1 -1
  71. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +49 -15
  72. package/libs/instance-factories/services/templates/_shared/step-matching.ts +43 -0
  73. package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +39 -19
  74. package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +35 -18
  75. package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-create.test.ts +184 -0
  76. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +34 -5
  77. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +6 -1
  78. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +34 -12
  79. package/libs/instance-factories/services/templates/shared-patterns.ts +20 -10
  80. package/package.json +1 -1
  81. package/libs/instance-factories/services/templates/_shared/step-matching.d.ts +0 -39
  82. package/libs/instance-factories/services/templates/_shared/step-matching.d.ts.map +0 -1
  83. package/libs/instance-factories/services/templates/_shared/step-matching.js +0 -90
  84. package/libs/instance-factories/services/templates/_shared/step-matching.js.map +0 -1
@@ -20,6 +20,7 @@ import {
20
20
  type SharedConvention,
21
21
  type SharedStepContext,
22
22
  } from '../_shared/step-matching.js';
23
+ import { safeEntityName } from '@specverse/types';
23
24
 
24
25
  export type PgStepConvention = SharedConvention<PgStepContext>;
25
26
 
@@ -146,7 +147,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
146
147
  generateCall: (m, ctx) => {
147
148
  const model = m[1]!;
148
149
  const field = m[2]!;
149
- const modelVar = toVar(model);
150
+ const modelVar = safeEntityName(model);
151
+ if (!modelVar) return '';
150
152
  const table = toTable(model);
151
153
  const params = ctx.parameterNames || [];
152
154
  const declared = ctx.declaredVars || new Set();
@@ -174,7 +176,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
174
176
  const model = m[1]!;
175
177
  const f1 = m[2]!;
176
178
  const f2 = m[3]!;
177
- const modelVar = toVar(model);
179
+ const modelVar = safeEntityName(model);
180
+ if (!modelVar) return '';
178
181
  const table = toTable(model);
179
182
  const declared = ctx.declaredVars || new Set();
180
183
  if (declared.has(modelVar)) {
@@ -201,10 +204,13 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
201
204
  // --- Create model record ---
202
205
  {
203
206
  name: 'create',
204
- pattern: /^create\s+(?:new\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
207
+ // Leading-adjective skip (NL_LEADING_ADJECTIVES) + isUnsafeEntityName
208
+ // safety net — see comment in prisma/step-conventions.ts "find" block.
209
+ pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
205
210
  generateCall: (m, ctx) => {
206
211
  const model = m[1]!;
207
- const modelVar = toVar(model);
212
+ const modelVar = safeEntityName(model);
213
+ if (!modelVar) return '';
208
214
  const table = toTable(model);
209
215
  const params = ctx.parameterNames || [];
210
216
  const declared = ctx.declaredVars || new Set();
@@ -218,12 +224,13 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
218
224
  // --- Update specific field on previously-loaded model ---
219
225
  {
220
226
  name: 'update-field',
221
- pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
227
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i,
222
228
  generateCall: (m, ctx) => {
223
229
  const model = m[1]!;
230
+ const modelVar = safeEntityName(model);
231
+ if (!modelVar) return '';
224
232
  const field = m[2]!;
225
233
  const rawValue = m[3]!;
226
- const modelVar = toVar(model);
227
234
  if (!ctx.declaredVars?.has(modelVar)) return '';
228
235
  const table = toTable(model);
229
236
  const val = resolveValue(rawValue, ctx);
@@ -235,11 +242,12 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
235
242
  // --- Update field timestamp ---
236
243
  {
237
244
  name: 'update-field-timestamp',
238
- pattern: /^update\s+(\w+)\s+(\w+)\s+timestamp$/i,
245
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+timestamp$/i,
239
246
  generateCall: (m, ctx) => {
240
247
  const model = m[1]!;
248
+ const modelVar = safeEntityName(model);
249
+ if (!modelVar) return '';
241
250
  const field = m[2]!;
242
- const modelVar = toVar(model);
243
251
  if (!ctx.declaredVars?.has(modelVar)) return '';
244
252
  const table = toTable(model);
245
253
  return ` // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
@@ -250,10 +258,11 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
250
258
  // --- Generic "Update X" (writes args back) ---
251
259
  {
252
260
  name: 'update',
253
- pattern: /^update\s+(\w+)(?:\s+(.+))?$/i,
261
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(.+))?$/i,
254
262
  generateCall: (m, ctx) => {
255
263
  const model = m[1]!;
256
- const modelVar = toVar(model);
264
+ const modelVar = safeEntityName(model);
265
+ if (!modelVar) return '';
257
266
  if (!ctx.declaredVars?.has(modelVar)) return '';
258
267
  const table = toTable(model);
259
268
  return ` // Step ${ctx.stepNum}: Update ${model}
@@ -264,10 +273,11 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
264
273
  // --- Delete model record ---
265
274
  {
266
275
  name: 'delete',
267
- pattern: /^delete\s+(\w+)/i,
276
+ pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i,
268
277
  generateCall: (m, ctx) => {
269
278
  const model = m[1]!;
270
- const modelVar = toVar(model);
279
+ const modelVar = safeEntityName(model);
280
+ if (!modelVar) return '';
271
281
  if (!ctx.declaredVars?.has(modelVar)) return '';
272
282
  const table = toTable(model);
273
283
  return ` // Step ${ctx.stepNum}: Delete ${model}
@@ -282,7 +292,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
282
292
  generateCall: (m, ctx) => {
283
293
  const model = m[1]!;
284
294
  const state = m[2]!;
285
- const modelVar = toVar(model);
295
+ const modelVar = safeEntityName(model);
296
+ if (!modelVar) return '';
286
297
  if (!ctx.declaredVars?.has(modelVar)) return '';
287
298
  const table = toTable(model);
288
299
  return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
@@ -337,7 +348,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
337
348
  name: 'persist',
338
349
  pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
339
350
  generateCall: (m, ctx) => {
340
- const target = toVar(m[1]!.replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
351
+ const target = safeEntityName(m[1]!.replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
352
+ if (!target) return '';
341
353
  const table = toTable(target);
342
354
  const recordSrc = mostRecentStepResult(ctx) ?? 'args';
343
355
  // Ensure `record` is an object before insertOne — wrap primitives.
@@ -355,7 +367,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
355
367
  name: 'conditional-create',
356
368
  pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
357
369
  generateCall: (m, ctx) => {
358
- const modelVar = toVar(m[1]!);
370
+ const modelVar = safeEntityName(m[1]!);
371
+ if (!modelVar) return '';
359
372
  const Model = pascal(m[2]!);
360
373
  const table = toTable(Model);
361
374
  if (!ctx.declaredVars?.has(modelVar)) return '';
@@ -374,7 +387,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
374
387
  name: 'conditional-update',
375
388
  pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
376
389
  generateCall: (m, ctx) => {
377
- const modelVar = toVar(m[1]!);
390
+ const modelVar = safeEntityName(m[1]!);
391
+ if (!modelVar) return '';
378
392
  const field = m[2]!;
379
393
  const table = toTable(m[1]!);
380
394
  if (!ctx.declaredVars?.has(modelVar)) return '';
@@ -428,7 +442,8 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
428
442
  pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
429
443
  generateCall: (m, ctx) => {
430
444
  const Model = pascal(m[1]!);
431
- const modelVar = toVar(Model);
445
+ const modelVar = safeEntityName(Model);
446
+ if (!modelVar) return '';
432
447
  const table = toTable(Model);
433
448
  const wasDeclared = ctx.declaredVars?.has(modelVar);
434
449
  const declared = Array.from(ctx.declaredVars || []);
@@ -480,9 +495,11 @@ export const PG_STEP_CONVENTIONS: PgStepConvention[] = [
480
495
  generateCall: (m, ctx) => {
481
496
  const service = m[1]!;
482
497
  const method = m[2]!;
498
+ const serviceVar = safeEntityName(service);
499
+ if (!serviceVar) return '';
483
500
  const args = (ctx.parameterNames || []).join(', ');
484
501
  return ` // Step ${ctx.stepNum}: Call ${service}.${method}
485
- await (${toVar(service)} as any).${method}({ ${args} });`;
502
+ await (${serviceVar} as any).${method}({ ${args} });`;
486
503
  },
487
504
  },
488
505
 
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Step-conventions — `create` pattern reserved-word regression.
3
+ *
4
+ * Surfaced 2026-05-14 by the 9-cell comparison matrix: sonnet's
5
+ * idle-meta realize γ-stubbed `PlayersController.post0` with the
6
+ * precise diagnostic "pre-baked step 7 uses reserved keyword 'new' as
7
+ * a variable identifier — cannot emit coherently". The pre-baked
8
+ * snippet was:
9
+ *
10
+ * // Step 7: Create new
11
+ * const new = await prisma.new.create({ data: data });
12
+ *
13
+ * Root cause: prisma's `create` convention pattern was
14
+ * `/^create\s+(\w+).../i` — for the step "Create new Player with level 1",
15
+ * m[1] captured the adjective "new" rather than the noun "Player".
16
+ * Then toVar("new") = "new" (a JS reserved word) became both the
17
+ * variable name AND the prisma model name in the generated snippet,
18
+ * neither of which is legal/correct.
19
+ *
20
+ * mongo and pg conventions already had `(?:new\s+)?` in their create
21
+ * patterns — this was a parity gap. Fix: add the same optional group
22
+ * to prisma's create pattern. After the fix, m[1] = "Player" → snippet
23
+ * uses `prisma.player.create`.
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { STEP_CONVENTIONS } from '../step-conventions.js';
28
+ import type { StepContext } from '../step-conventions.js';
29
+
30
+ function findCreateConvention() {
31
+ const c = STEP_CONVENTIONS.find((x) => x.name === 'create');
32
+ expect(c).toBeDefined();
33
+ return c!;
34
+ }
35
+
36
+ function makeCtx(overrides: Partial<StepContext> = {}): StepContext {
37
+ return {
38
+ modelName: 'Player',
39
+ serviceName: 'PlayersController',
40
+ operationName: 'post0',
41
+ stepNum: 7,
42
+ parameterNames: [],
43
+ declaredVars: new Set<string>(),
44
+ ...overrides,
45
+ } as any;
46
+ }
47
+
48
+ describe('prisma step-convention `create` — adjective-skip + entity-name correctness', () => {
49
+ it('extracts the entity noun, not the adjective, from "Create new Player with …"', () => {
50
+ const convention = findCreateConvention();
51
+ const m = 'Create new Player with level 1, experience 0'.match(convention.pattern)!;
52
+ expect(m[1]).toBe('Player');
53
+ });
54
+
55
+ it('emits a snippet referencing `prisma.player` (not `prisma.new`) for the adjective case', () => {
56
+ const convention = findCreateConvention();
57
+ const m = 'Create new Player with level 1, experience 0'.match(convention.pattern)!;
58
+ const call = convention.generateCall(m, makeCtx())!;
59
+ expect(call).toContain('prisma.player.create');
60
+ expect(call).not.toContain('prisma.new');
61
+ expect(call).not.toContain('const new ');
62
+ });
63
+
64
+ it('still works for the no-adjective case "Create Player with …" (no regression)', () => {
65
+ const convention = findCreateConvention();
66
+ const m = 'Create Player with level 1'.match(convention.pattern)!;
67
+ expect(m[1]).toBe('Player');
68
+ const call = convention.generateCall(m, makeCtx())!;
69
+ expect(call).toContain('prisma.player.create');
70
+ });
71
+
72
+ it('uses parameter names for the data shape when provided', () => {
73
+ const convention = findCreateConvention();
74
+ const m = 'Create new Player with level 1'.match(convention.pattern)!;
75
+ const call = convention.generateCall(m, makeCtx({ parameterNames: ['userId', 'gameId'] }))!;
76
+ expect(call).toContain('{ userId, gameId }');
77
+ });
78
+
79
+ it('emits a syntactically valid `const <var> = await prisma.<var>.create(...)` shape', () => {
80
+ const convention = findCreateConvention();
81
+ const m = 'Create new Player with level 1'.match(convention.pattern)!;
82
+ const call = convention.generateCall(m, makeCtx())!;
83
+ // const <var> = await prisma.<var>.create(...);
84
+ // Variable name must NOT be a JS reserved word; both the variable
85
+ // and the prisma model reference should be the lowercased entity.
86
+ expect(call).toMatch(/const\s+player\s+=\s+await\s+prisma\.player\.create/);
87
+ });
88
+
89
+ // Wider adjective list (engines 6.60.4) — kept in sync with
90
+ // NL_LEADING_ADJECTIVES in _shared/step-matching.ts.
91
+ it.each([
92
+ ['existing', 'Create existing Player with level 1'],
93
+ ['current', 'Create current Player'],
94
+ ['the', 'Create the Player with level 1'],
95
+ ['a', 'Create a Player with level 1'],
96
+ ['an', 'Create an Order with total 100'],
97
+ ])('skips the leading adjective %s (no regression for any in the list)', (_adj, step) => {
98
+ const convention = findCreateConvention();
99
+ const m = step.match(convention.pattern)!;
100
+ // m[1] is the noun (Player/Order), never the adjective.
101
+ expect(['Player', 'Order']).toContain(m[1]);
102
+ const call = convention.generateCall(m, makeCtx())!;
103
+ expect(call).not.toMatch(/const\s+(existing|current|the|a|an)\s+=/);
104
+ expect(call).not.toMatch(/prisma\.(existing|current|the|a|an)\./);
105
+ });
106
+ });
107
+
108
+ describe('prisma step-convention `create` — reserved-word safety net', () => {
109
+ // The pattern's leading-adjective skip catches the common cases. The
110
+ // safety net catches anything that still leaks a JS reserved word as
111
+ // the entity name (an adjective not in the skip list, or a mis-parse).
112
+ // generateCall returns '' → the matcher falls through to the AI
113
+ // [WRITE] fallback instead of emitting `const class = await prisma.class...`.
114
+ it.each([
115
+ ['class', 'Create class Foo'],
116
+ ['function', 'Create function Bar'],
117
+ ['delete', 'Create delete Marker'],
118
+ ['return', 'Create return Receipt'],
119
+ ])('returns empty string when m[1] would be reserved word "%s"', (reserved, step) => {
120
+ const convention = findCreateConvention();
121
+ const m = step.match(convention.pattern);
122
+ // The pattern still matches structurally; the generateCall guard refuses.
123
+ if (m) {
124
+ expect(m[1]!.toLowerCase()).toBe(reserved);
125
+ const call = convention.generateCall(m, makeCtx());
126
+ expect(call).toBe('');
127
+ }
128
+ });
129
+
130
+ it('matches but refuses "Create new class" (post-adjective-skip the captured noun is reserved)', () => {
131
+ const convention = findCreateConvention();
132
+ const m = 'Create new class'.match(convention.pattern)!;
133
+ expect(m[1]).toBe('class');
134
+ expect(convention.generateCall(m, makeCtx())).toBe('');
135
+ });
136
+ });
137
+
138
+ describe('prisma step-conventions — extracted-input reserved-word safety (transition + call-service)', () => {
139
+ // 2026-05-15 audit of all toVar(extracted) sites. Same shape as the
140
+ // create-convention safety net above: when the captured noun lowercases
141
+ // to a TS reserved word, generateCall returns '' so the matcher falls
142
+ // through to the AI [WRITE] fallback instead of emitting illegal TS.
143
+
144
+ function findConvention(name: string) {
145
+ const c = STEP_CONVENTIONS.find((x) => x.name === name);
146
+ expect(c).toBeDefined();
147
+ return c!;
148
+ }
149
+
150
+ it.each([
151
+ ['delete', 'transition delete to active'],
152
+ ['return', 'transition return to closed'],
153
+ ['class', 'transition class to graduated'],
154
+ ])('transition: refuses reserved-word model "%s"', (_reserved, step) => {
155
+ const convention = findConvention('transition');
156
+ const m = step.match(convention.pattern)!;
157
+ expect(convention.generateCall(m, makeCtx())).toBe('');
158
+ });
159
+
160
+ it('transition: still works for a real model name (no regression)', () => {
161
+ const convention = findConvention('transition');
162
+ const m = 'transition Player to active'.match(convention.pattern)!;
163
+ const ctx = makeCtx({ declaredVars: new Set(['player']) });
164
+ const call = convention.generateCall(m, ctx)!;
165
+ expect(call).toContain('prisma.player.update');
166
+ expect(call).toContain("'active'");
167
+ });
168
+
169
+ it.each([
170
+ ['delete', 'call delete.purge'],
171
+ ['return', 'call return.process'],
172
+ ])('call-service: refuses reserved-word service name "%s"', (_reserved, step) => {
173
+ const convention = findConvention('call-service');
174
+ const m = step.match(convention.pattern)!;
175
+ expect(convention.generateCall(m, makeCtx())).toBe('');
176
+ });
177
+
178
+ it('call-service: still works for a real service name (no regression)', () => {
179
+ const convention = findConvention('call-service');
180
+ const m = 'call notificationService.send'.match(convention.pattern)!;
181
+ const call = convention.generateCall(m, makeCtx())!;
182
+ expect(call).toContain('notificationService.send');
183
+ });
184
+ });
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import type { TemplateContext } from '@specverse/types';
21
+ import { safeFunctionName } from '@specverse/types';
21
22
  import { matchStep, type StepContext } from './step-conventions.js';
22
23
  import { validateImportWhitelist } from '@specverse/engines/ai';
23
24
  import { createHash } from 'crypto';
@@ -352,6 +353,15 @@ export async function generateAiBehaviorsFile(opts: {
352
353
  let cacheHits = 0;
353
354
  let cacheMisses = 0;
354
355
  for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
356
+ // Reserved-word safety for the function declaration. `functionName`
357
+ // came from `toMethod(step)` in step-matching; a step text like
358
+ // "Delete" camelCases to `delete` — a JS keyword that's illegal as
359
+ // a function-declaration identifier. Suffix-rename the decl and
360
+ // append a re-export so callers (`aiBehaviors.delete(...)`) still
361
+ // resolve via the re-export's binding. Same shape as the γ-stub
362
+ // renderers in per-owner-runner + per-action-recovery.
363
+ const { name: safeName, reserved: nameReserved } = safeFunctionName(functionName);
364
+
355
365
  // Pure function signature + destructure are built AFTER the body so we
356
366
  // can match what the LLM actually references — strict tsc's
357
367
  // noUnusedLocals / noUnusedParameters fire on every input the body
@@ -471,6 +481,22 @@ export async function generateAiBehaviorsFile(opts: {
471
481
  returnType = `{ ${fields} }`;
472
482
  }
473
483
 
484
+ // Build the validation harness EXACTLY as the function is finally
485
+ // emitted: real signature + the wrapper-added destructure + real
486
+ // return type. Pre-fix the harness was a bare
487
+ // `export async function X(input: any)` with NO destructure — but
488
+ // the LLM is explicitly told (see the retry-hint prose below) to
489
+ // emit ONLY the body that goes AFTER `const { ... } = input;`. So a
490
+ // valid body using bare input references (`return userId + 1`)
491
+ // would spuriously fail re-validation with "Cannot find name
492
+ // 'userId'", forcing regeneration / STUB. The harness must match
493
+ // emission or the gate rejects its own correct output.
494
+ const buildTestCode = (rawBody: string): string => {
495
+ const { signature, destructure } = buildSignatureAndDestructure(rawBody);
496
+ const destructureLine = destructure ? destructure + '\n' : '';
497
+ return `export async function ${safeName}(${signature}): Promise<${returnType}> {\n${destructureLine}${rawBody}\n}`;
498
+ };
499
+
474
500
  // Check cache first — skip Claude if we've generated this exact step before
475
501
  const key = cacheKey(step, modelName, operationName, functionName, inputs);
476
502
  let body: string | null = cacheRead(key);
@@ -480,7 +506,7 @@ export async function generateAiBehaviorsFile(opts: {
480
506
  // corrupted on disk OR the validation rules may have tightened
481
507
  // (e.g. tsc type checks added). A cache hit must still be re-validated
482
508
  // through both gates so old bodies don't leak past the new bar.
483
- const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
509
+ const testCode = buildTestCode(body);
484
510
  const syntaxError = await validateTypeScript(testCode);
485
511
  const typeError = syntaxError ? null : await validateTypeScriptTypes(testCode);
486
512
  const importError = (syntaxError || typeError) ? null : validateImports(testCode);
@@ -516,7 +542,7 @@ export async function generateAiBehaviorsFile(opts: {
516
542
  // with the error message appended; only ONE retry to bound
517
543
  // cost. If that still fails we keep the body but mark it
518
544
  // AI-INVALID so the user sees it needs review.
519
- const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
545
+ const testCode = buildTestCode(body);
520
546
  const syntaxError = await validateTypeScript(testCode);
521
547
  if (syntaxError) {
522
548
  console.warn(` [ai-validate] ${functionName} has syntax error: ${syntaxError}`);
@@ -547,7 +573,7 @@ export async function generateAiBehaviorsFile(opts: {
547
573
  returnType,
548
574
  });
549
575
  if (retried) {
550
- const retryCode = `export async function ${functionName}(input: any): Promise<any> {\n${retried}\n}`;
576
+ const retryCode = buildTestCode(retried);
551
577
  const retrySyntaxError = await validateTypeScript(retryCode);
552
578
  const retryTypeError = retrySyntaxError ? null : await validateTypeScriptTypes(retryCode);
553
579
  const retryImportError = (retrySyntaxError || retryTypeError) ? null : validateImports(retryCode);
@@ -594,6 +620,9 @@ export async function generateAiBehaviorsFile(opts: {
594
620
  ? ` * Returns: ${returnType}\n`
595
621
  : '';
596
622
 
623
+ const reExportLine = nameReserved
624
+ ? `\nexport { ${safeName} as ${functionName} };`
625
+ : '';
597
626
  functions.push(`/**
598
627
  * ${functionName}
599
628
  *
@@ -612,7 +641,7 @@ ${inputsDoc}${returnsDoc} * Source: ${source}
612
641
  ? 'AI returned code with syntax errors — function throws at runtime. Fix or regenerate.'
613
642
  : 'STUB — Claude CLI unavailable. Install Claude Code or implement manually.'}
614
643
  */
615
- export async function ${functionName}(${(() => {
644
+ ${nameReserved ? 'async' : 'export async'} function ${safeName}(${(() => {
616
645
  const { signature: sig } = buildSignatureAndDestructure(body);
617
646
  return sig;
618
647
  })()}): Promise<${returnType}> {
@@ -620,7 +649,7 @@ ${(() => {
620
649
  const { destructure } = buildSignatureAndDestructure(body);
621
650
  return destructure ? destructure + '\n' : '';
622
651
  })()}${body}
623
- }`);
652
+ }${reExportLine}`);
624
653
  }
625
654
 
626
655
  // End the session
@@ -65,7 +65,12 @@ export default function generatePrismaController(context: TemplateContext): stri
65
65
 
66
66
  const usesPrisma = /\bprisma\b/.test(classBody);
67
67
  const usesParseId = /\bparseId\(/.test(classBody);
68
- const usesEventBus = hasEventPublishing(curedOps, controller);
68
+ // Symbol-presence check on the actual class body — matches the
69
+ // `usesPrisma` pattern. Pre-fix this was a stub `hasEventPublishing`
70
+ // that returned true unconditionally, producing an unused-import
71
+ // TS6133 on every backend-only ProductController (validate-only,
72
+ // no CRUD, no eventBus.publish in the body).
73
+ const usesEventBus = /\beventBus\./.test(classBody);
69
74
  const usesAiBehaviors = customActions.needsAiBehaviors;
70
75
 
71
76
  // Build top-of-file declarations conditionally.
@@ -17,6 +17,7 @@ import {
17
17
  type SharedConvention,
18
18
  type SharedStepContext,
19
19
  } from '../_shared/step-matching.js';
20
+ import { safeEntityName } from '@specverse/types';
20
21
 
21
22
  export type StepConvention = SharedConvention<StepContext>;
22
23
 
@@ -125,13 +126,27 @@ export const STEP_CONVENTIONS: StepConvention[] = [
125
126
  },
126
127
 
127
128
  // --- Find / Lookup ---
129
+ //
130
+ // All entity-targeting patterns (find / create / update / delete) share
131
+ // two safeguards:
132
+ // 1. A pattern-level leading-adjective skip
133
+ // `(?:(?:new|existing|current|the|a|an)\s+)?` so NL phrasings like
134
+ // "Find existing Player by id" or "Create new Player with ..."
135
+ // extract the noun, not the adjective. Kept in sync with
136
+ // `NL_LEADING_ADJECTIVES` in `_shared/step-matching.ts`.
137
+ // 2. `safeEntityName(m[1])` — the reserved-word-safe primitive from
138
+ // `@specverse/types`. Returns the lowerCamel identifier, or `null`
139
+ // when the extraction would emit a TS reserved word
140
+ // (e.g. `prisma.new.create`). On null, generateCall returns `''` so
141
+ // the matcher falls through to the AI [WRITE] fallback.
128
142
  {
129
143
  name: 'find',
130
- pattern: /^find\s+(\w+)\s+by\s+(\w+)/i,
144
+ pattern: /^find\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+by\s+(\w+)/i,
131
145
  generateCall: (m, ctx) => {
132
146
  const model = m[1];
147
+ const modelVar = safeEntityName(model);
148
+ if (!modelVar) return '';
133
149
  const field = m[2];
134
- const modelVar = toVar(model);
135
150
  const params = ctx.parameterNames || [];
136
151
  const declared = ctx.declaredVars || new Set();
137
152
 
@@ -154,10 +169,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
154
169
  // --- Create ---
155
170
  {
156
171
  name: 'create',
157
- pattern: /^create\s+(\w+)(?:\s+(?:with\s+)?(.+))?/i,
172
+ pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
158
173
  generateCall: (m, ctx) => {
159
174
  const model = m[1];
160
- const modelVar = toVar(model);
175
+ const modelVar = safeEntityName(model);
176
+ if (!modelVar) return '';
161
177
  // Build data from parameter names if available
162
178
  const paramNames = ctx.parameterNames || [];
163
179
  const dataFields = paramNames.length > 0
@@ -171,12 +187,13 @@ export const STEP_CONVENTIONS: StepConvention[] = [
171
187
  // --- Update field ---
172
188
  {
173
189
  name: 'update-field',
174
- pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
190
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i,
175
191
  generateCall: (m, ctx) => {
176
192
  const model = m[1];
193
+ const modelVar = safeEntityName(model);
194
+ if (!modelVar) return '';
177
195
  const field = m[2];
178
196
  const rawValue = m[3];
179
- const modelVar = toVar(model);
180
197
  const val = resolveValue(rawValue, ctx);
181
198
  return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${rawValue.trim()}
182
199
  await prisma.${modelVar}.update({
@@ -189,10 +206,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
189
206
  // --- Generic update ---
190
207
  {
191
208
  name: 'update',
192
- pattern: /^update\s+(\w+)(?:\s+(.+))?/i,
209
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(.+))?/i,
193
210
  generateCall: (m, ctx) => {
194
211
  const model = m[1];
195
- const modelVar = toVar(model);
212
+ const modelVar = safeEntityName(model);
213
+ if (!modelVar) return '';
196
214
  return ` // Step ${ctx.stepNum}: Update ${model}
197
215
  await prisma.${modelVar}.update({
198
216
  where: { id: ${modelVar}.id },
@@ -204,10 +222,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
204
222
  // --- Delete ---
205
223
  {
206
224
  name: 'delete',
207
- pattern: /^delete\s+(\w+)/i,
225
+ pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i,
208
226
  generateCall: (m, ctx) => {
209
227
  const model = m[1];
210
- const modelVar = toVar(model);
228
+ const modelVar = safeEntityName(model);
229
+ if (!modelVar) return '';
211
230
  return ` // Step ${ctx.stepNum}: Delete ${model}
212
231
  await prisma.${modelVar}.delete({ where: { id: ${modelVar}.id } });`;
213
232
  },
@@ -220,7 +239,8 @@ export const STEP_CONVENTIONS: StepConvention[] = [
220
239
  generateCall: (m, ctx) => {
221
240
  const model = m[1];
222
241
  const state = m[2];
223
- const modelVar = toVar(model);
242
+ const modelVar = safeEntityName(model);
243
+ if (!modelVar) return '';
224
244
  return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
225
245
  if (${modelVar}.status === '${state}') throw new Error('${model} is already ${state}');
226
246
  await prisma.${modelVar}.update({
@@ -314,8 +334,10 @@ export const STEP_CONVENTIONS: StepConvention[] = [
314
334
  generateCall: (m, ctx) => {
315
335
  const service = m[1];
316
336
  const method = m[2];
337
+ const serviceVar = safeEntityName(service);
338
+ if (!serviceVar) return '';
317
339
  return ` // Step ${ctx.stepNum}: Call ${service}.${method}
318
- await ${toVar(service)}.${method}({ ${(ctx.parameterNames || []).join(', ')} });`;
340
+ await ${serviceVar}.${method}({ ${(ctx.parameterNames || []).join(', ')} });`;
319
341
  },
320
342
  },
321
343
 
@@ -1,12 +1,22 @@
1
1
  /**
2
2
  * Shared Convention Pattern Definitions
3
3
  *
4
- * The canonical list of step convention patterns. Both the Prisma factory
5
- * (code generation) and the Memory factory (runtime interpretation) use
6
- * these same patterns. Add new patterns here both targets pick them up.
4
+ * Canonical pattern list consumed by the Memory factory's runtime
5
+ * interpreter (`generateInterpreterModule` reads them to emit
6
+ * dispatch code). Prisma / Mongo / Postgres step-conventions split
7
+ * off into their own inline-regex files historically and no longer
8
+ * read from here — changes in this file primarily affect Memory.
7
9
  *
8
- * This is the "interface" of the convention system. Each factory provides
9
- * the "implementation" (generateCall or execute).
10
+ * Entity-targeting patterns (find / create / update-field / update /
11
+ * delete) carry a leading-adjective skip
12
+ * `(?:(?:new|existing|current|the|a|an)\s+)?` so an NL step like
13
+ * "Create new Player with …" extracts `Player`, not `new`. Mirrors
14
+ * `NL_LEADING_ADJECTIVES` in `_shared/step-matching.ts`. Without this,
15
+ * Memory's interpreter would call `ctx.store.create("new", data)` and
16
+ * fail at lookup; the structural cause is identical to the TS-side bug
17
+ * that `safeEntityName` (from `@specverse/types`) addresses in the
18
+ * other ORMs — only the *consequence* differs (runtime lookup failure
19
+ * here vs TS syntax error there).
10
20
  */
11
21
 
12
22
  export interface PatternDefinition {
@@ -19,13 +29,13 @@ export const CONVENTION_PATTERNS: PatternDefinition[] = [
19
29
  { name: 'validate', pattern: /^validate\s+(.+)/i, description: 'Validate input data' },
20
30
  { name: 'check-no-existing', pattern: /^check\s+(?:no\s+existing|.*has\s+no\s+existing)\s+(\w+)\s+for\s+(\w+)\s+in\s+(\w+)/i, description: 'Check no duplicate entity exists for a pair' },
21
31
  { name: 'check', pattern: /^check\s+(.+)/i, description: 'Check a condition' },
22
- { name: 'find', pattern: /^find\s+(\w+)\s+by\s+(\w+)/i, description: 'Find entity by field' },
32
+ { name: 'find', pattern: /^find\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+by\s+(\w+)/i, description: 'Find entity by field' },
23
33
  { name: 'find-all-for', pattern: /^find\s+(?:all\s+)?(\w+?)s?\s+(?:for|of|belonging\s+to)\s+(\w+)/i, description: 'Cross-model query via foreign key' },
24
34
  { name: 'count-per', pattern: /^count\s+(\w+?)s?\s+(?:per|by|for\s+each)\s+(\w+)/i, description: 'Group-by aggregation count' },
25
- { name: 'create', pattern: /^create\s+(\w+)(?:\s+record)?/i, description: 'Create a new entity' },
26
- { name: 'update-field', pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i, description: 'Update specific field to value' },
27
- { name: 'update', pattern: /^update\s+(\w+)$/i, description: 'Update entity with params' },
28
- { name: 'delete', pattern: /^delete\s+(\w+)/i, description: 'Delete an entity' },
35
+ { name: 'create', pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+record)?/i, description: 'Create a new entity' },
36
+ { name: 'update-field', pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i, description: 'Update specific field to value' },
37
+ { name: 'update', pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)$/i, description: 'Update entity with params' },
38
+ { name: 'delete', pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i, description: 'Delete an entity' },
29
39
  { name: 'transition', pattern: /^transition\s+(\w+)\s+to\s+(\w+)/i, description: 'Lifecycle state transition' },
30
40
  { name: 'set', pattern: /^set\s+(\w+)\s+to\s+(.+)/i, description: 'Set a field value' },
31
41
  { name: 'increment', pattern: /^increment\s+(\w+)\s+by\s+(\w+)/i, description: 'Increment numeric field' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specverse/engines",
3
- "version": "6.53.1",
3
+ "version": "6.63.0",
4
4
  "description": "SpecVerse toolchain — parser, inference, realize, generators, AI, registry, bundles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",