@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
@@ -2,6 +2,7 @@ import {
2
2
  toVar,
3
3
  matchAgainstConventions
4
4
  } from "../_shared/step-matching.js";
5
+ import { safeEntityName } from "@specverse/types";
5
6
  function deriveModelDefaults(modelName, ctx) {
6
7
  if (!ctx.models) return [];
7
8
  const model = ctx.models[modelName] || ctx.models[modelName.charAt(0).toUpperCase() + modelName.slice(1)];
@@ -88,7 +89,8 @@ const PG_STEP_CONVENTIONS = [
88
89
  generateCall: (m, ctx) => {
89
90
  const model = m[1];
90
91
  const field = m[2];
91
- const modelVar = toVar(model);
92
+ const modelVar = safeEntityName(model);
93
+ if (!modelVar) return "";
92
94
  const table = toTable(model);
93
95
  const params = ctx.parameterNames || [];
94
96
  const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
@@ -111,7 +113,8 @@ const PG_STEP_CONVENTIONS = [
111
113
  const model = m[1];
112
114
  const f1 = m[2];
113
115
  const f2 = m[3];
114
- const modelVar = toVar(model);
116
+ const modelVar = safeEntityName(model);
117
+ if (!modelVar) return "";
115
118
  const table = toTable(model);
116
119
  const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
117
120
  if (declared.has(modelVar)) {
@@ -137,10 +140,13 @@ const PG_STEP_CONVENTIONS = [
137
140
  // --- Create model record ---
138
141
  {
139
142
  name: "create",
140
- pattern: /^create\s+(?:new\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
143
+ // Leading-adjective skip (NL_LEADING_ADJECTIVES) + isUnsafeEntityName
144
+ // safety net — see comment in prisma/step-conventions.ts "find" block.
145
+ pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
141
146
  generateCall: (m, ctx) => {
142
147
  const model = m[1];
143
- const modelVar = toVar(model);
148
+ const modelVar = safeEntityName(model);
149
+ if (!modelVar) return "";
144
150
  const table = toTable(model);
145
151
  const params = ctx.parameterNames || [];
146
152
  const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
@@ -153,12 +159,13 @@ const PG_STEP_CONVENTIONS = [
153
159
  // --- Update specific field on previously-loaded model ---
154
160
  {
155
161
  name: "update-field",
156
- pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
162
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i,
157
163
  generateCall: (m, ctx) => {
158
164
  const model = m[1];
165
+ const modelVar = safeEntityName(model);
166
+ if (!modelVar) return "";
159
167
  const field = m[2];
160
168
  const rawValue = m[3];
161
- const modelVar = toVar(model);
162
169
  if (!ctx.declaredVars?.has(modelVar)) return "";
163
170
  const table = toTable(model);
164
171
  const val = resolveValue(rawValue, ctx);
@@ -169,11 +176,12 @@ const PG_STEP_CONVENTIONS = [
169
176
  // --- Update field timestamp ---
170
177
  {
171
178
  name: "update-field-timestamp",
172
- pattern: /^update\s+(\w+)\s+(\w+)\s+timestamp$/i,
179
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+timestamp$/i,
173
180
  generateCall: (m, ctx) => {
174
181
  const model = m[1];
182
+ const modelVar = safeEntityName(model);
183
+ if (!modelVar) return "";
175
184
  const field = m[2];
176
- const modelVar = toVar(model);
177
185
  if (!ctx.declaredVars?.has(modelVar)) return "";
178
186
  const table = toTable(model);
179
187
  return ` // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
@@ -183,10 +191,11 @@ const PG_STEP_CONVENTIONS = [
183
191
  // --- Generic "Update X" (writes args back) ---
184
192
  {
185
193
  name: "update",
186
- pattern: /^update\s+(\w+)(?:\s+(.+))?$/i,
194
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(.+))?$/i,
187
195
  generateCall: (m, ctx) => {
188
196
  const model = m[1];
189
- const modelVar = toVar(model);
197
+ const modelVar = safeEntityName(model);
198
+ if (!modelVar) return "";
190
199
  if (!ctx.declaredVars?.has(modelVar)) return "";
191
200
  const table = toTable(model);
192
201
  return ` // Step ${ctx.stepNum}: Update ${model}
@@ -196,10 +205,11 @@ const PG_STEP_CONVENTIONS = [
196
205
  // --- Delete model record ---
197
206
  {
198
207
  name: "delete",
199
- pattern: /^delete\s+(\w+)/i,
208
+ pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i,
200
209
  generateCall: (m, ctx) => {
201
210
  const model = m[1];
202
- const modelVar = toVar(model);
211
+ const modelVar = safeEntityName(model);
212
+ if (!modelVar) return "";
203
213
  if (!ctx.declaredVars?.has(modelVar)) return "";
204
214
  const table = toTable(model);
205
215
  return ` // Step ${ctx.stepNum}: Delete ${model}
@@ -213,7 +223,8 @@ const PG_STEP_CONVENTIONS = [
213
223
  generateCall: (m, ctx) => {
214
224
  const model = m[1];
215
225
  const state = m[2];
216
- const modelVar = toVar(model);
226
+ const modelVar = safeEntityName(model);
227
+ if (!modelVar) return "";
217
228
  if (!ctx.declaredVars?.has(modelVar)) return "";
218
229
  const table = toTable(model);
219
230
  return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
@@ -265,7 +276,8 @@ const PG_STEP_CONVENTIONS = [
265
276
  name: "persist",
266
277
  pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
267
278
  generateCall: (m, ctx) => {
268
- const target = toVar(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
279
+ const target = safeEntityName(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
280
+ if (!target) return "";
269
281
  const table = toTable(target);
270
282
  const recordSrc = mostRecentStepResult(ctx) ?? "args";
271
283
  return ` // Step ${ctx.stepNum}: Persist ${m[1]}
@@ -281,7 +293,8 @@ const PG_STEP_CONVENTIONS = [
281
293
  name: "conditional-create",
282
294
  pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
283
295
  generateCall: (m, ctx) => {
284
- const modelVar = toVar(m[1]);
296
+ const modelVar = safeEntityName(m[1]);
297
+ if (!modelVar) return "";
285
298
  const Model = pascal(m[2]);
286
299
  const table = toTable(Model);
287
300
  if (!ctx.declaredVars?.has(modelVar)) return "";
@@ -299,7 +312,8 @@ const PG_STEP_CONVENTIONS = [
299
312
  name: "conditional-update",
300
313
  pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
301
314
  generateCall: (m, ctx) => {
302
- const modelVar = toVar(m[1]);
315
+ const modelVar = safeEntityName(m[1]);
316
+ if (!modelVar) return "";
303
317
  const field = m[2];
304
318
  const table = toTable(m[1]);
305
319
  if (!ctx.declaredVars?.has(modelVar)) return "";
@@ -347,7 +361,8 @@ const PG_STEP_CONVENTIONS = [
347
361
  pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
348
362
  generateCall: (m, ctx) => {
349
363
  const Model = pascal(m[1]);
350
- const modelVar = toVar(Model);
364
+ const modelVar = safeEntityName(Model);
365
+ if (!modelVar) return "";
351
366
  const table = toTable(Model);
352
367
  const wasDeclared = ctx.declaredVars?.has(modelVar);
353
368
  const declared = Array.from(ctx.declaredVars || []);
@@ -391,9 +406,11 @@ const PG_STEP_CONVENTIONS = [
391
406
  generateCall: (m, ctx) => {
392
407
  const service = m[1];
393
408
  const method = m[2];
409
+ const serviceVar = safeEntityName(service);
410
+ if (!serviceVar) return "";
394
411
  const args = (ctx.parameterNames || []).join(", ");
395
412
  return ` // Step ${ctx.stepNum}: Call ${service}.${method}
396
- await (${toVar(service)} as any).${method}({ ${args} });`;
413
+ await (${serviceVar} as any).${method}({ ${args} });`;
397
414
  }
398
415
  },
399
416
  // --- Return ---
@@ -1,3 +1,4 @@
1
+ import { safeFunctionName } from "@specverse/types";
1
2
  import { matchStep } from "./step-conventions.js";
2
3
  import { validateImportWhitelist } from "@specverse/engines/ai";
3
4
  import { createHash } from "crypto";
@@ -190,6 +191,7 @@ async function generateAiBehaviorsFile(opts) {
190
191
  let cacheHits = 0;
191
192
  let cacheMisses = 0;
192
193
  for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
194
+ const { name: safeName, reserved: nameReserved } = safeFunctionName(functionName);
193
195
  const stripLiteralsAndComments = (src) => {
194
196
  let out = src.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length)).replace(/(['"])(?:\\.|(?!\1).)*\1/g, (m) => m[0] + " ".repeat(m.length - 2) + m[0]);
195
197
  out = out.replace(/`((?:\\.|\$\{[^}]*\}|(?!`).)*)`/g, (full, content) => {
@@ -246,13 +248,18 @@ async function generateAiBehaviorsFile(opts) {
246
248
  const fields = Object.entries(returns).map(([k, v]) => `${k}: ${v}`).join("; ");
247
249
  returnType = `{ ${fields} }`;
248
250
  }
251
+ const buildTestCode = (rawBody) => {
252
+ const { signature, destructure } = buildSignatureAndDestructure(rawBody);
253
+ const destructureLine = destructure ? destructure + "\n" : "";
254
+ return `export async function ${safeName}(${signature}): Promise<${returnType}> {
255
+ ${destructureLine}${rawBody}
256
+ }`;
257
+ };
249
258
  const key = cacheKey(step, modelName, operationName, functionName, inputs);
250
259
  let body = cacheRead(key);
251
260
  let source = body ? "AI-CACHED" : "STUB";
252
261
  if (body) {
253
- const testCode = `export async function ${functionName}(input: any): Promise<any> {
254
- ${body}
255
- }`;
262
+ const testCode = buildTestCode(body);
256
263
  const syntaxError = await validateTypeScript(testCode);
257
264
  const typeError = syntaxError ? null : await validateTypeScriptTypes(testCode);
258
265
  const importError = syntaxError || typeError ? null : validateImports(testCode);
@@ -280,9 +287,7 @@ ${body}
280
287
  // Pass declared return type to Claude
281
288
  });
282
289
  if (body) {
283
- const testCode = `export async function ${functionName}(input: any): Promise<any> {
284
- ${body}
285
- }`;
290
+ const testCode = buildTestCode(body);
286
291
  const syntaxError = await validateTypeScript(testCode);
287
292
  if (syntaxError) {
288
293
  console.warn(` [ai-validate] ${functionName} has syntax error: ${syntaxError}`);
@@ -324,9 +329,7 @@ ${retryHint}`,
324
329
  returnType
325
330
  });
326
331
  if (retried) {
327
- const retryCode = `export async function ${functionName}(input: any): Promise<any> {
328
- ${retried}
329
- }`;
332
+ const retryCode = buildTestCode(retried);
330
333
  const retrySyntaxError = await validateTypeScript(retryCode);
331
334
  const retryTypeError = retrySyntaxError ? null : await validateTypeScriptTypes(retryCode);
332
335
  const retryImportError = retrySyntaxError || retryTypeError ? null : validateImports(retryCode);
@@ -364,6 +367,8 @@ ${retried}
364
367
  ` : "";
365
368
  const returnsDoc = returnType !== "any" ? ` * Returns: ${returnType}
366
369
  ` : "";
370
+ const reExportLine = nameReserved ? `
371
+ export { ${safeName} as ${functionName} };` : "";
367
372
  functions.push(`/**
368
373
  * ${functionName}
369
374
  *
@@ -376,7 +381,7 @@ ${inputsDoc}${returnsDoc} * Source: ${source}
376
381
  * All data comes in via \`input\`; all effects happen in the calling controller.
377
382
  * ${source === "AI-GENERATED" ? "AI-generated implementation. Review and test before deploying." : source === "AI-CACHED" ? "Restored from AI cache (.specverse/ai-cache/). Delete cache entry to regenerate." : source === "AI-INVALID" ? "AI returned code with syntax errors \u2014 function throws at runtime. Fix or regenerate." : "STUB \u2014 Claude CLI unavailable. Install Claude Code or implement manually."}
378
383
  */
379
- export async function ${functionName}(${(() => {
384
+ ${nameReserved ? "async" : "export async"} function ${safeName}(${(() => {
380
385
  const { signature: sig } = buildSignatureAndDestructure(body);
381
386
  return sig;
382
387
  })()}): Promise<${returnType}> {
@@ -384,7 +389,7 @@ ${(() => {
384
389
  const { destructure } = buildSignatureAndDestructure(body);
385
390
  return destructure ? destructure + "\n" : "";
386
391
  })()}${body}
387
- }`);
392
+ }${reExportLine}`);
388
393
  }
389
394
  if (aiService?.endSession) aiService.endSession();
390
395
  if (cacheHits > 0 || cacheMisses > 0) {
@@ -30,7 +30,7 @@ function generatePrismaController(context) {
30
30
  ].filter(Boolean).join("\n ");
31
31
  const usesPrisma = /\bprisma\b/.test(classBody);
32
32
  const usesParseId = /\bparseId\(/.test(classBody);
33
- const usesEventBus = hasEventPublishing(curedOps, controller);
33
+ const usesEventBus = /\beventBus\./.test(classBody);
34
34
  const usesAiBehaviors = customActions.needsAiBehaviors;
35
35
  const imports = [
36
36
  usesPrisma ? `import { PrismaClient } from '@prisma/client';` : "",
@@ -3,6 +3,7 @@ import {
3
3
  toVar,
4
4
  matchAgainstConventions
5
5
  } from "../_shared/step-matching.js";
6
+ import { safeEntityName } from "@specverse/types";
6
7
  function resolveValue(rawValue, ctx) {
7
8
  const value = rawValue.trim().replace(/^['"]|['"]$/g, "");
8
9
  if (/^(current\s*time|now|timestamp)$/i.test(value)) {
@@ -60,13 +61,27 @@ const STEP_CONVENTIONS = [
60
61
  }
61
62
  },
62
63
  // --- Find / Lookup ---
64
+ //
65
+ // All entity-targeting patterns (find / create / update / delete) share
66
+ // two safeguards:
67
+ // 1. A pattern-level leading-adjective skip
68
+ // `(?:(?:new|existing|current|the|a|an)\s+)?` so NL phrasings like
69
+ // "Find existing Player by id" or "Create new Player with ..."
70
+ // extract the noun, not the adjective. Kept in sync with
71
+ // `NL_LEADING_ADJECTIVES` in `_shared/step-matching.ts`.
72
+ // 2. `safeEntityName(m[1])` — the reserved-word-safe primitive from
73
+ // `@specverse/types`. Returns the lowerCamel identifier, or `null`
74
+ // when the extraction would emit a TS reserved word
75
+ // (e.g. `prisma.new.create`). On null, generateCall returns `''` so
76
+ // the matcher falls through to the AI [WRITE] fallback.
63
77
  {
64
78
  name: "find",
65
- pattern: /^find\s+(\w+)\s+by\s+(\w+)/i,
79
+ pattern: /^find\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+by\s+(\w+)/i,
66
80
  generateCall: (m, ctx) => {
67
81
  const model = m[1];
82
+ const modelVar = safeEntityName(model);
83
+ if (!modelVar) return "";
68
84
  const field = m[2];
69
- const modelVar = toVar(model);
70
85
  const params = ctx.parameterNames || [];
71
86
  const declared = ctx.declaredVars || /* @__PURE__ */ new Set();
72
87
  const idParam = field === "id" ? params.find((p) => p === `${modelVar}Id`) || params.find((p) => p === "id") || `${modelVar}Id` : params.find((p) => p === field) || `params.${field}`;
@@ -81,10 +96,11 @@ const STEP_CONVENTIONS = [
81
96
  // --- Create ---
82
97
  {
83
98
  name: "create",
84
- pattern: /^create\s+(\w+)(?:\s+(?:with\s+)?(.+))?/i,
99
+ pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
85
100
  generateCall: (m, ctx) => {
86
101
  const model = m[1];
87
- const modelVar = toVar(model);
102
+ const modelVar = safeEntityName(model);
103
+ if (!modelVar) return "";
88
104
  const paramNames = ctx.parameterNames || [];
89
105
  const dataFields = paramNames.length > 0 ? `{ ${paramNames.join(", ")} }` : "data";
90
106
  return ` // Step ${ctx.stepNum}: Create ${model}
@@ -94,12 +110,13 @@ const STEP_CONVENTIONS = [
94
110
  // --- Update field ---
95
111
  {
96
112
  name: "update-field",
97
- pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
113
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i,
98
114
  generateCall: (m, ctx) => {
99
115
  const model = m[1];
116
+ const modelVar = safeEntityName(model);
117
+ if (!modelVar) return "";
100
118
  const field = m[2];
101
119
  const rawValue = m[3];
102
- const modelVar = toVar(model);
103
120
  const val = resolveValue(rawValue, ctx);
104
121
  return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${rawValue.trim()}
105
122
  await prisma.${modelVar}.update({
@@ -111,10 +128,11 @@ const STEP_CONVENTIONS = [
111
128
  // --- Generic update ---
112
129
  {
113
130
  name: "update",
114
- pattern: /^update\s+(\w+)(?:\s+(.+))?/i,
131
+ pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(.+))?/i,
115
132
  generateCall: (m, ctx) => {
116
133
  const model = m[1];
117
- const modelVar = toVar(model);
134
+ const modelVar = safeEntityName(model);
135
+ if (!modelVar) return "";
118
136
  return ` // Step ${ctx.stepNum}: Update ${model}
119
137
  await prisma.${modelVar}.update({
120
138
  where: { id: ${modelVar}.id },
@@ -125,10 +143,11 @@ const STEP_CONVENTIONS = [
125
143
  // --- Delete ---
126
144
  {
127
145
  name: "delete",
128
- pattern: /^delete\s+(\w+)/i,
146
+ pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i,
129
147
  generateCall: (m, ctx) => {
130
148
  const model = m[1];
131
- const modelVar = toVar(model);
149
+ const modelVar = safeEntityName(model);
150
+ if (!modelVar) return "";
132
151
  return ` // Step ${ctx.stepNum}: Delete ${model}
133
152
  await prisma.${modelVar}.delete({ where: { id: ${modelVar}.id } });`;
134
153
  }
@@ -140,7 +159,8 @@ const STEP_CONVENTIONS = [
140
159
  generateCall: (m, ctx) => {
141
160
  const model = m[1];
142
161
  const state = m[2];
143
- const modelVar = toVar(model);
162
+ const modelVar = safeEntityName(model);
163
+ if (!modelVar) return "";
144
164
  return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
145
165
  if (${modelVar}.status === '${state}') throw new Error('${model} is already ${state}');
146
166
  await prisma.${modelVar}.update({
@@ -227,8 +247,10 @@ const STEP_CONVENTIONS = [
227
247
  generateCall: (m, ctx) => {
228
248
  const service = m[1];
229
249
  const method = m[2];
250
+ const serviceVar = safeEntityName(service);
251
+ if (!serviceVar) return "";
230
252
  return ` // Step ${ctx.stepNum}: Call ${service}.${method}
231
- await ${toVar(service)}.${method}({ ${(ctx.parameterNames || []).join(", ")} });`;
253
+ await ${serviceVar}.${method}({ ${(ctx.parameterNames || []).join(", ")} });`;
232
254
  }
233
255
  },
234
256
  // --- Return ---
@@ -2,13 +2,13 @@ const CONVENTION_PATTERNS = [
2
2
  { name: "validate", pattern: /^validate\s+(.+)/i, description: "Validate input data" },
3
3
  { 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" },
4
4
  { name: "check", pattern: /^check\s+(.+)/i, description: "Check a condition" },
5
- { name: "find", pattern: /^find\s+(\w+)\s+by\s+(\w+)/i, description: "Find entity by field" },
5
+ { name: "find", pattern: /^find\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+by\s+(\w+)/i, description: "Find entity by field" },
6
6
  { 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" },
7
7
  { name: "count-per", pattern: /^count\s+(\w+?)s?\s+(?:per|by|for\s+each)\s+(\w+)/i, description: "Group-by aggregation count" },
8
- { name: "create", pattern: /^create\s+(\w+)(?:\s+record)?/i, description: "Create a new entity" },
9
- { name: "update-field", pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i, description: "Update specific field to value" },
10
- { name: "update", pattern: /^update\s+(\w+)$/i, description: "Update entity with params" },
11
- { name: "delete", pattern: /^delete\s+(\w+)/i, description: "Delete an entity" },
8
+ { name: "create", pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+record)?/i, description: "Create a new entity" },
9
+ { 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" },
10
+ { name: "update", pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)$/i, description: "Update entity with params" },
11
+ { name: "delete", pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i, description: "Delete an entity" },
12
12
  { name: "transition", pattern: /^transition\s+(\w+)\s+to\s+(\w+)/i, description: "Lifecycle state transition" },
13
13
  { name: "set", pattern: /^set\s+(\w+)\s+to\s+(.+)/i, description: "Set a field value" },
14
14
  { name: "increment", pattern: /^increment\s+(\w+)\s+by\s+(\w+)/i, description: "Increment numeric field" },
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EACL,wCAAwC,EACxC,eAAe,EACf,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA2DnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAyxC9F;;;OAGG;YACW,UAAU;IA8DxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAItC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EACL,wCAAwC,EACxC,eAAe,EACf,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA2DnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAu2C9F;;;OAGG;YACW,UAAU;IA8DxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -423,26 +423,67 @@ class SpecVerseRealizeEngine {
423
423
  // savings, warm cache, and skill state persists across calls).
424
424
  const { runPerOwnerEmission, buildAvailableCapabilitiesString } = await import('./per-owner-runner.js');
425
425
  const { createRealizeOwnerLlmEmit } = await import('./per-owner-emit.js');
426
+ const { createPerActionRecovery } = await import('./per-action-recovery.js');
427
+ const { resolveProviderId: resolveProviderIdForRecovery } = await import('../ai/model-resolver.js');
426
428
  const perOwnerEmit = createRealizeOwnerLlmEmit({ model: sharedModel });
429
+ // Tier 2 — per-action recovery (Phase 5a of
430
+ // 2026-05-14-PER-ACTION-RECOVERY-TIER.md). When the whole-owner
431
+ // emit fails, the runner retries the owner action-by-action with
432
+ // full owner context before γ-stubbing. Default-on; the runner
433
+ // honours SPECVERSE_REALIZE_NO_RECOVERY_TIER=1 + the action-count
434
+ // cap. Stub provider has no LLM — skip wiring it then.
435
+ const perActionRecovery = resolveProviderIdForRecovery() === 'stub'
436
+ ? undefined
437
+ : createPerActionRecovery({ model: sharedModel });
427
438
  const availableCapabilities = buildAvailableCapabilitiesString(this.manifest);
428
439
  const ownerResult = await runPerOwnerEmission({
429
440
  spec, outputDir,
430
441
  matcher: sharedMatcher,
431
442
  conventions,
432
443
  perOwnerEmit,
444
+ perActionRecovery,
433
445
  availableClientsByOwner,
434
446
  availableCapabilities,
435
447
  silent: false,
436
448
  });
437
449
  if (ownerResult.totals.actionCount > 0) {
438
450
  const llmCount = ownerResult.totals.ownersEmittedByLLM;
451
+ const tier2Count = ownerResult.totals.ownersRecoveredByTier2;
439
452
  const fallbackCount = ownerResult.totals.ownersFellBack;
440
- const tail = fallbackCount > 0
441
- ? ` (${llmCount} LLM-emitted, ${fallbackCount} fell back to placeholder)`
442
- : '';
453
+ const tailParts = [];
454
+ if (llmCount > 0)
455
+ tailParts.push(`${llmCount} LLM-emitted`);
456
+ if (tier2Count > 0)
457
+ tailParts.push(`${tier2Count} via per-action recovery`);
458
+ if (fallbackCount > 0)
459
+ tailParts.push(`${fallbackCount} fell back to placeholder`);
460
+ const tail = tailParts.length > 0 ? ` (${tailParts.join(', ')})` : '';
443
461
  console.log(` ✅ Per-OWNER AI behaviors: ${ownerResult.totals.actionCount} action(s) across ${ownerResult.totals.ownerCount} owner(s)${tail}`);
444
462
  }
445
- // 2.5b Post-emit feedback (engines 6.53.0 — DEFAULT-ON, env hatch retired).
463
+ // 2.5a Diagnostic sidecar (default-on; opt-out via SPECVERSE_REALIZE_NO_DIAGNOSTICS=1).
464
+ //
465
+ // Phase 1 of 2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT.
466
+ // Runs the stub-completeness verifier in diagnostic-only mode
467
+ // (separate from the feedback runner), writes a sidecar with
468
+ // per-owner stub breakdown, prints a one-line CLI summary if
469
+ // there's anything to flag. Zero LLM calls, milliseconds runtime.
470
+ if (process.env.SPECVERSE_REALIZE_NO_DIAGNOSTICS !== '1') {
471
+ try {
472
+ const { runDiagnostics, formatSummaryLine } = await import('./post-emit-verify/index.js');
473
+ const summary = await runDiagnostics({
474
+ outputDir,
475
+ targetLanguage: 'typescript',
476
+ });
477
+ const line = formatSummaryLine(summary);
478
+ if (line)
479
+ console.log(line);
480
+ }
481
+ catch (e) {
482
+ // Diagnostic failure must never crash the realize pipeline.
483
+ errors.push(`Realize diagnostics: ${e?.message ?? String(e)}`);
484
+ }
485
+ }
486
+ // 2.5b Post-emit feedback (default-on; escape hatch retained).
446
487
  //
447
488
  // After all per-owner emits complete, run language-specific
448
489
  // verifiers (tsc, mypy, etc.) on the realized output and feed
@@ -453,7 +494,12 @@ class SpecVerseRealizeEngine {
453
494
  // Flip history:
454
495
  // 6.51.0 — opt-in via SPECVERSE_REALIZE_POST_VERIFY=1
455
496
  // 6.52.0 — default-on; opt-out via SPECVERSE_REALIZE_NO_POST_VERIFY=1
456
- // 6.53.0 — env hatch retired (this version)
497
+ // 6.53.0 — env hatch retired
498
+ // 6.53.2 — escape hatch restored (this version) after
499
+ // real-npm publish surfaced the need for a safety
500
+ // valve. Default-on stays; users with a genuine
501
+ // regression can set SPECVERSE_REALIZE_NO_POST_VERIFY=1
502
+ // until upstream ships a fix.
457
503
  //
458
504
  // Empirical evidence at flip time (Phase A corpus harness):
459
505
  // - scoremyclays-ollama: 9 → 0 tsc errors
@@ -462,12 +508,31 @@ class SpecVerseRealizeEngine {
462
508
  // generator errors as unmappable; no spurious re-emits)
463
509
  // - zero regressions across the corpus
464
510
  //
465
- // Env tuning still available:
511
+ // Env tuning:
512
+ // SPECVERSE_REALIZE_NO_POST_VERIFY=1 — opt out (escape hatch)
466
513
  // SPECVERSE_REALIZE_POST_VERIFY_PASSES=N — tune retry count (default 1)
467
514
  // SPECVERSE_REALIZE_POST_VERIFY_MAX_FILES=N — cap LLM spend per pass (default 20)
515
+ // SPECVERSE_REALIZE_POST_VERIFY_ENABLE=id,… — opt in to verifiers
516
+ // with enabledByDefault: false
517
+ // (e.g. stub-completeness)
518
+ //
519
+ // Gate: run feedback whenever there are owners AT ALL — not just
520
+ // LLM-emitted ones. Pre-6.58.0 the gate was
521
+ // `ownersEmittedByLLM > 0`, which skipped feedback entirely when
522
+ // EVERY owner γ-fell-back (structural-preservation rejection,
523
+ // parse failure, etc.). That defeated the stub-completeness
524
+ // verifier (engines 6.54.0+) in its single highest-value case:
525
+ // an all-fallback realize is ALL STUB001 γ-stubs, exactly what
526
+ // auto-reemit-with-context (Phase 3) is built to recover. The
527
+ // Phase 3 trial (2026-05-14, nestjs-billing × MarrBox/Ollama)
528
+ // surfaced this: both providers had all owners rejected by
529
+ // structural-preservation → 0 LLM-emitted → feedback skipped →
530
+ // 7 STUB001 stubs shipped unfixed. Now: `ownerCount > 0`. The
531
+ // only true skip is "no behaviors at all" (validate-only
532
+ // backends) — and the stub provider is still skipped below.
468
533
  //
469
534
  // Stub provider is auto-skipped (no LLM → can't fix).
470
- if (ownerResult.totals.ownersEmittedByLLM > 0) {
535
+ if (process.env.SPECVERSE_REALIZE_NO_POST_VERIFY !== '1' && ownerResult.totals.ownerCount > 0) {
471
536
  // Detect stub provider early — there's no LLM, so feedback
472
537
  // can't fire. Stub returns the prompt verbatim which would
473
538
  // appear as a "fix" but is garbage. Log + skip cleanly.
@@ -478,11 +543,27 @@ class SpecVerseRealizeEngine {
478
543
  }
479
544
  else {
480
545
  try {
481
- const { runPostEmitFeedback, buildLlmReemit } = await import('./post-emit-verify/index.js');
546
+ const { runPostEmitFeedback, buildLlmReemit, appendSpecGaps, formatSpecGapLine } = await import('./post-emit-verify/index.js');
482
547
  const maxPasses = parseInt(process.env.SPECVERSE_REALIZE_POST_VERIFY_PASSES ?? '1', 10) || 1;
483
548
  const maxFilesPerPass = parseInt(process.env.SPECVERSE_REALIZE_POST_VERIFY_MAX_FILES ?? '20', 10) || 20;
484
- const reemit = buildLlmReemit({ model: sharedModel });
485
- const feedbackResult = await runPostEmitFeedback({ outputDir, targetLanguage: 'typescript' }, reemit, { maxPasses, maxFilesPerPass });
549
+ const enableCsv = process.env.SPECVERSE_REALIZE_POST_VERIFY_ENABLE ?? '';
550
+ const enabledOverrides = enableCsv
551
+ ? new Set(enableCsv.split(',').map((s) => s.trim()).filter(Boolean))
552
+ : undefined;
553
+ const { loadContextSnapshot } = await import('./realize-context-snapshot.js');
554
+ const reemit = buildLlmReemit({
555
+ model: sharedModel,
556
+ contextLoader: (ownerName) => loadContextSnapshot(outputDir, ownerName),
557
+ });
558
+ const feedbackResult = await runPostEmitFeedback({ outputDir, targetLanguage: 'typescript' }, reemit, { maxPasses, maxFilesPerPass, enabledOverrides });
559
+ // Phase 2: surface spec gaps (STUB002s the treatment selector
560
+ // routed away from auto-reemit) into the diagnostic sidecar.
561
+ if (feedbackResult.specGaps.length > 0) {
562
+ appendSpecGaps(outputDir, feedbackResult.specGaps);
563
+ const line = formatSpecGapLine(feedbackResult.specGaps.length);
564
+ if (line)
565
+ console.log(line);
566
+ }
486
567
  const before = feedbackResult.initialErrors.length;
487
568
  const after = feedbackResult.finalErrors.length;
488
569
  if (before > 0) {