@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.
- package/dist/ai/analyse-runner.d.ts.map +1 -1
- package/dist/ai/analyse-runner.js +22 -1
- package/dist/ai/analyse-runner.js.map +1 -1
- package/dist/analyse-prepass/adapters/module-functions.d.ts +25 -0
- package/dist/analyse-prepass/adapters/module-functions.d.ts.map +1 -1
- package/dist/analyse-prepass/adapters/module-functions.js +54 -0
- package/dist/analyse-prepass/adapters/module-functions.js.map +1 -1
- package/dist/analyse-prepass/backends/gitnexus.d.ts +28 -0
- package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -1
- package/dist/analyse-prepass/backends/gitnexus.js +36 -2
- package/dist/analyse-prepass/backends/gitnexus.js.map +1 -1
- package/dist/analyse-prepass/index.d.ts.map +1 -1
- package/dist/analyse-prepass/index.js +17 -1
- package/dist/analyse-prepass/index.js.map +1 -1
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +29 -10
- package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +11 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +39 -19
- package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +35 -18
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +16 -11
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +1 -1
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +34 -12
- package/dist/libs/instance-factories/services/templates/shared-patterns.js +5 -5
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +91 -10
- package/dist/realize/index.js.map +1 -1
- package/dist/realize/per-action-recovery.d.ts +74 -0
- package/dist/realize/per-action-recovery.d.ts.map +1 -0
- package/dist/realize/per-action-recovery.js +255 -0
- package/dist/realize/per-action-recovery.js.map +1 -0
- package/dist/realize/per-owner-emit.d.ts +6 -0
- package/dist/realize/per-owner-emit.d.ts.map +1 -1
- package/dist/realize/per-owner-emit.js +22 -6
- package/dist/realize/per-owner-emit.js.map +1 -1
- package/dist/realize/per-owner-runner.d.ts +23 -2
- package/dist/realize/per-owner-runner.d.ts.map +1 -1
- package/dist/realize/per-owner-runner.js +91 -46
- package/dist/realize/per-owner-runner.js.map +1 -1
- package/dist/realize/post-emit-verify/diagnostics.d.ts +107 -0
- package/dist/realize/post-emit-verify/diagnostics.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/diagnostics.js +148 -0
- package/dist/realize/post-emit-verify/diagnostics.js.map +1 -0
- package/dist/realize/post-emit-verify/feedback-runner.d.ts +41 -1
- package/dist/realize/post-emit-verify/feedback-runner.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/feedback-runner.js +62 -6
- package/dist/realize/post-emit-verify/feedback-runner.js.map +1 -1
- package/dist/realize/post-emit-verify/index.d.ts +4 -2
- package/dist/realize/post-emit-verify/index.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/index.js +3 -1
- package/dist/realize/post-emit-verify/index.js.map +1 -1
- package/dist/realize/post-emit-verify/reemit.d.ts +22 -1
- package/dist/realize/post-emit-verify/reemit.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/reemit.js +20 -18
- package/dist/realize/post-emit-verify/reemit.js.map +1 -1
- package/dist/realize/post-emit-verify/types.d.ts +49 -0
- package/dist/realize/post-emit-verify/types.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/verifier-manifest.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/verifier-manifest.js +2 -0
- package/dist/realize/post-emit-verify/verifier-manifest.js.map +1 -1
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts +127 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.js +423 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.js.map +1 -0
- package/dist/realize/realize-context-snapshot.d.ts +70 -0
- package/dist/realize/realize-context-snapshot.d.ts.map +1 -0
- package/dist/realize/realize-context-snapshot.js +96 -0
- package/dist/realize/realize-context-snapshot.js.map +1 -0
- package/dist/realize/structural-validator.d.ts +36 -2
- package/dist/realize/structural-validator.d.ts.map +1 -1
- package/dist/realize/structural-validator.js +50 -7
- package/dist/realize/structural-validator.js.map +1 -1
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +49 -15
- package/libs/instance-factories/services/templates/_shared/step-matching.ts +43 -0
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +39 -19
- package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +35 -18
- package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-create.test.ts +184 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +34 -5
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +6 -1
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +34 -12
- package/libs/instance-factories/services/templates/shared-patterns.ts +20 -10
- package/package.json +1 -1
- package/libs/instance-factories/services/templates/_shared/step-matching.d.ts +0 -39
- package/libs/instance-factories/services/templates/_shared/step-matching.d.ts.map +0 -1
- package/libs/instance-factories/services/templates/_shared/step-matching.js +0 -90
- 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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (${
|
|
502
|
+
await (${serviceVar} as any).${method}({ ${args} });`;
|
|
486
503
|
},
|
|
487
504
|
},
|
|
488
505
|
|
package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-create.test.ts
ADDED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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
|
-
*
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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,
|
|
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,
|
|
26
|
-
{ name: 'update-field', pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
|
|
27
|
-
{ name: 'update', pattern: /^update\s+(\w+)$/i,
|
|
28
|
-
{ name: 'delete', pattern: /^delete\s+(\w+)/i,
|
|
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' },
|