@specverse/engines 6.5.4 → 6.7.8

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 (19) hide show
  1. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +26 -10
  2. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +1 -1
  3. package/dist/libs/instance-factories/services/mongodb-native-services.yaml +84 -0
  4. package/dist/libs/instance-factories/services/templates/mongodb-native/client-generator.js +43 -0
  5. package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +252 -0
  6. package/dist/libs/instance-factories/services/templates/mongodb-native/service-generator.js +64 -0
  7. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +167 -26
  8. package/dist/realize/library/library.d.ts.map +1 -1
  9. package/dist/realize/library/library.js +11 -0
  10. package/dist/realize/library/library.js.map +1 -1
  11. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +48 -12
  12. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +1 -1
  13. package/libs/instance-factories/services/mongodb-native-services.yaml +84 -0
  14. package/libs/instance-factories/services/templates/mongodb-native/__tests__/controller-generator.test.ts +113 -0
  15. package/libs/instance-factories/services/templates/mongodb-native/client-generator.ts +51 -0
  16. package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +319 -0
  17. package/libs/instance-factories/services/templates/mongodb-native/service-generator.ts +83 -0
  18. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +231 -36
  19. package/package.json +4 -5
@@ -46,6 +46,85 @@ async function validateTypeScript(code: string): Promise<string | null> {
46
46
  }
47
47
  }
48
48
 
49
+ /**
50
+ * Type-check the generated body in isolation using the TypeScript compiler
51
+ * API. Returns null if clean, or a concise error string suitable for
52
+ * passing back to the LLM as a fix-up hint. Skipped silently if the `typescript`
53
+ * package isn't installed (the realize pipeline still runs full tsc against
54
+ * the realized output downstream — this is a faster, generator-side filter
55
+ * to catch the most common LLM mistakes before they hit the user).
56
+ *
57
+ * Why bother: the LLM regularly emits patterns that are valid syntax but
58
+ * fail strict tsc — RegExp index access (`m[1]` is `string | undefined`),
59
+ * unused locals, undefined references. Reprompting with the tsc error
60
+ * lets the LLM self-correct without burning a per-step retry.
61
+ */
62
+ async function validateTypeScriptTypes(code: string): Promise<string | null> {
63
+ let ts: any;
64
+ try {
65
+ ts = await import('typescript');
66
+ if (ts.default) ts = ts.default;
67
+ } catch {
68
+ return null; // typescript not available — skip
69
+ }
70
+ try {
71
+ const fileName = 'aiBehavior.ts';
72
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ES2022, true);
73
+ // Mirror the strict shape used by the realized backend's tsconfig so
74
+ // a body that passes here also passes downstream. Failing to match the
75
+ // realized strictness means we'd ship code that fails user-side tsc
76
+ // (defeating the whole point of generator-side validation).
77
+ const compilerOptions: any = {
78
+ target: ts.ScriptTarget.ES2022,
79
+ module: ts.ModuleKind.ESNext,
80
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
81
+ strict: true,
82
+ noImplicitAny: false, // body uses `any` extensively for inputs
83
+ noUnusedLocals: true,
84
+ noUnusedParameters: true,
85
+ noImplicitReturns: true,
86
+ noUncheckedIndexedAccess: true,
87
+ noFallthroughCasesInSwitch: true,
88
+ noEmit: true,
89
+ skipLibCheck: true,
90
+ types: [],
91
+ // Without `lib`, tsc can't resolve Promise / Array / RegExp etc., and
92
+ // every body fails the basic-types check before it even gets to the
93
+ // strict-null-check rules we actually want to validate.
94
+ lib: ['lib.es2022.d.ts'],
95
+ };
96
+ const defaultHost = ts.createCompilerHost(compilerOptions);
97
+ const host: any = {
98
+ ...defaultHost,
99
+ getSourceFile: (n: string, target: any) => {
100
+ if (n === fileName) return sourceFile;
101
+ return defaultHost.getSourceFile(n, target);
102
+ },
103
+ writeFile: () => {},
104
+ fileExists: (n: string) => n === fileName || defaultHost.fileExists(n),
105
+ readFile: (n: string) => (n === fileName ? code : defaultHost.readFile(n)),
106
+ };
107
+ const program = ts.createProgram([fileName], compilerOptions, host);
108
+ const diagnostics = [
109
+ ...program.getSyntacticDiagnostics(sourceFile),
110
+ ...program.getSemanticDiagnostics(sourceFile),
111
+ ];
112
+ if (diagnostics.length === 0) return null;
113
+ const formatted = diagnostics.slice(0, 5).map((d: any) => {
114
+ const msg = ts.flattenDiagnosticMessageText(d.messageText, '\n');
115
+ const pos = d.file && d.start !== undefined
116
+ ? d.file.getLineAndCharacterOfPosition(d.start)
117
+ : null;
118
+ return pos
119
+ ? `line ${pos.line + 1}, col ${pos.character + 1}: ${msg}`
120
+ : msg;
121
+ });
122
+ return formatted.join('; ');
123
+ } catch {
124
+ return null; // never let validation crash the generator
125
+ }
126
+ }
127
+
49
128
  /**
50
129
  * AI output cache — avoids re-calling Claude for unchanged steps.
51
130
  *
@@ -56,7 +135,7 @@ async function validateTypeScript(code: string): Promise<string | null> {
56
135
  * produces a new hash. The prompt version is part of the hash so
57
136
  * prompt upgrades also invalidate.
58
137
  */
59
- const PROMPT_VERSION = '9.1.0';
138
+ const PROMPT_VERSION = '9.2.0';
60
139
 
61
140
  function cacheKey(step: string, modelName: string, operationName: string, functionName: string, inputs: string[]): string {
62
141
  const payload = JSON.stringify({ step, modelName, operationName, functionName, inputs: [...inputs].sort(), v: PROMPT_VERSION });
@@ -229,15 +308,76 @@ export async function generateAiBehaviorsFile(opts: {
229
308
  let cacheHits = 0;
230
309
  let cacheMisses = 0;
231
310
  for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
232
- // Pure function signature: all inputs as a typed destructured object
233
- const signature = inputs.length > 0
234
- ? `input: { ${inputs.map(n => `${n}: any`).join('; ')} }`
235
- : 'input: Record<string, never>';
236
-
237
- // Inside the body, destructure for readability
238
- const destructure = inputs.length > 0
239
- ? ` const { ${inputs.join(', ')} } = input;`
240
- : '';
311
+ // Pure function signature + destructure are built AFTER the body so we
312
+ // can match what the LLM actually references — strict tsc's
313
+ // noUnusedLocals / noUnusedParameters fire on every input the body
314
+ // didn't touch. We tolerate underuse rather than re-prompting the LLM:
315
+ // the body is correct as-is, we just trim the wrapper to fit it.
316
+ //
317
+ // Three cases:
318
+ // - body uses some inputs: destructure only those
319
+ // - body uses none directly but references `input.X`: keep `input`
320
+ // - body doesn't touch input at all: prefix signature with `_` so
321
+ // noUnusedParameters is happy
322
+ /** Strip string/template/comment content so identifier-reference checks
323
+ * don't match `reward` inside a kebab-case string like `'reward-not-granted'`
324
+ * or `// reward stays as text`. We replace the literal contents with
325
+ * spaces so positions stay roughly aligned in case of debugging. */
326
+ const stripLiteralsAndComments = (src: string): string => {
327
+ return src
328
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
329
+ .replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length))
330
+ .replace(/(['"])(?:\\.|(?!\1).)*\1/g, (m) => m[0] + ' '.repeat(m.length - 2) + m[0])
331
+ .replace(/`(?:\\.|\$\{[^}]*\}|(?!`).)*`/g, (m) => '`' + ' '.repeat(m.length - 2) + '`');
332
+ };
333
+
334
+ /** Detect whether the body manages its own input access (so adding our
335
+ * own destructure would either duplicate-declare or leave us with
336
+ * unused-locals). Three shapes the LLM emits:
337
+ * 1. `const { X, Y } = input;` (destructure)
338
+ * 2. `const X = input.X;` (per-property)
339
+ * 3. `const _X = input.X;` (per-property with rename)
340
+ * If ANY of these appear, the body is its own input setup and we
341
+ * should leave it alone. */
342
+ const bodyHandlesInputItself = (body: string): boolean => {
343
+ const codeOnly = stripLiteralsAndComments(body);
344
+ // Destructure: `const { ... } = input;`
345
+ if (/(?:const|let|var)\s*\{[^}]+\}\s*=\s*input\b/.test(codeOnly)) return true;
346
+ // Per-property access: any `(const|let|var) <name> = input.<name>`
347
+ if (/(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*input\.[A-Za-z_$][\w$]*/.test(codeOnly)) return true;
348
+ return false;
349
+ };
350
+
351
+ /** Does the body actually USE the local variable `n` (vs only mention
352
+ * it as an object-literal property name like `step1Result:` in a
353
+ * return object)? A real usage is `n` followed by something other than
354
+ * `:` — i.e. accessed in an expression. We approximate by requiring
355
+ * the next non-whitespace char to be a code-meaningful operator/end. */
356
+ const isRealReference = (n: string, codeOnly: string): boolean => {
357
+ const escaped = n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
358
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$:])`, 'g');
359
+ return re.test(codeOnly);
360
+ };
361
+
362
+ const buildSignatureAndDestructure = (body: string): { signature: string; destructure: string } => {
363
+ if (inputs.length === 0) {
364
+ return { signature: 'input: Record<string, never>', destructure: '' };
365
+ }
366
+ const codeOnly = stripLiteralsAndComments(body);
367
+ const referenced = inputs.filter((n) => isRealReference(n, codeOnly));
368
+ const usesInputObject = /(?<![A-Za-z0-9_$])input(?![A-Za-z0-9_$])/.test(codeOnly);
369
+ const sigName = (referenced.length > 0 || usesInputObject) ? 'input' : '_input';
370
+ const sig = `${sigName}: { ${inputs.map(n => `${n}: any`).join('; ')} }`;
371
+ // If the body already manages its own input access (destructure or
372
+ // per-property) OR accesses input.X directly, don't add a wrapper
373
+ // destructure — would leave us with unused locals (TS6198) or
374
+ // duplicate declarations (TS2451).
375
+ const bodyAccessesInputProps = /(?<![A-Za-z0-9_$])input\.[A-Za-z_$]/.test(codeOnly);
376
+ const destructure = (referenced.length > 0 && !bodyHandlesInputItself(body) && !bodyAccessesInputProps)
377
+ ? ` const { ${referenced.join(', ')} } = input;`
378
+ : '';
379
+ return { signature: sig, destructure };
380
+ };
241
381
 
242
382
  // Build return type from spec declaration (if provided)
243
383
  // returns can be:
@@ -258,11 +398,14 @@ export async function generateAiBehaviorsFile(opts: {
258
398
  let source: 'AI-CACHED' | 'AI-GENERATED' | 'AI-INVALID' | 'STUB' = body ? 'AI-CACHED' : 'STUB';
259
399
  if (body) {
260
400
  // Validate cache entry — a previously valid entry may have been
261
- // corrupted on disk or the validator rules may have changed
401
+ // corrupted on disk OR the validation rules may have tightened
402
+ // (e.g. tsc type checks added). A cache hit must still be re-validated
403
+ // through both gates so old bodies don't leak past the new bar.
262
404
  const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
263
- const validationError = await validateTypeScript(testCode);
264
- if (validationError) {
265
- console.warn(` [ai-validate] cached ${functionName} failed validation: ${validationError}`);
405
+ const syntaxError = await validateTypeScript(testCode);
406
+ const typeError = syntaxError ? null : await validateTypeScriptTypes(testCode);
407
+ if (syntaxError || typeError) {
408
+ console.warn(` [ai-validate] cached ${functionName} failed validation: ${syntaxError || typeError}`);
266
409
  body = null; // Force regeneration
267
410
  source = 'STUB';
268
411
  } else {
@@ -285,17 +428,62 @@ export async function generateAiBehaviorsFile(opts: {
285
428
  });
286
429
 
287
430
  if (body) {
288
- // Validate the generated body as a standalone function
431
+ // Two-tier validation:
432
+ // 1. esbuild syntax check — fast, catches unbalanced braces, etc.
433
+ // 2. tsc type check — slower, catches `string | undefined` index
434
+ // access, unused locals, and undefined references the LLM
435
+ // regularly produces. On tsc failure, we re-prompt the LLM
436
+ // with the error message appended; only ONE retry to bound
437
+ // cost. If that still fails we keep the body but mark it
438
+ // AI-INVALID so the user sees it needs review.
289
439
  const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
290
- const validationError = await validateTypeScript(testCode);
291
- if (validationError) {
292
- console.warn(` [ai-validate] ${functionName} has syntax error: ${validationError}`);
293
- // Don't cache invalid output; treat as failed generation
294
- body = `// AI-generated code failed validation: ${validationError}\n // Step: ${step}\n throw new Error('AI behavior has invalid syntax — see comment above');`;
440
+ const syntaxError = await validateTypeScript(testCode);
441
+ if (syntaxError) {
442
+ console.warn(` [ai-validate] ${functionName} has syntax error: ${syntaxError}`);
443
+ body = `// AI-generated code failed validation: ${syntaxError}\n // Step: ${step}\n throw new Error('AI behavior has invalid syntax see comment above');`;
295
444
  source = 'AI-INVALID';
296
445
  } else {
297
- source = 'AI-GENERATED';
298
- cacheWrite(key, body);
446
+ const typeError = await validateTypeScriptTypes(testCode);
447
+ if (typeError) {
448
+ console.warn(` [ai-validate] ${functionName} type errors: ${typeError}`);
449
+ try {
450
+ const retryHint = `Your previous output produced TypeScript type errors:\n${typeError}\n\nFix these specifically — common causes:\n- RegExp match indices are 'string | undefined'; use non-null assertion or extract to a typed variable\n- Strict null checks: guard or assert before use\n- Don't declare locals you never reference\n\nIMPORTANT: The destructure line \`const { ... } = input;\` is added by the wrapper, NOT by you. Output ONLY the function body that goes AFTER that line — do not repeat the destructure or you will produce duplicate-declaration errors.`;
451
+ const retried = await aiService.generateBehavior({
452
+ step: `${step}\n\n${retryHint}`,
453
+ modelName,
454
+ operationName,
455
+ functionName,
456
+ parameterNames: inputs,
457
+ availableModels,
458
+ spec,
459
+ returnType,
460
+ });
461
+ if (retried) {
462
+ const retryCode = `export async function ${functionName}(input: any): Promise<any> {\n${retried}\n}`;
463
+ const retrySyntaxError = await validateTypeScript(retryCode);
464
+ const retryTypeError = retrySyntaxError ? null : await validateTypeScriptTypes(retryCode);
465
+ if (!retrySyntaxError && !retryTypeError) {
466
+ body = retried;
467
+ source = 'AI-GENERATED';
468
+ cacheWrite(key, body);
469
+ } else {
470
+ // Retry didn't help — keep the original body so the user can fix
471
+ // manually, but mark it as INVALID so it stands out.
472
+ source = 'AI-INVALID';
473
+ cacheWrite(key, body);
474
+ }
475
+ } else {
476
+ source = 'AI-INVALID';
477
+ cacheWrite(key, body);
478
+ }
479
+ } catch {
480
+ source = 'AI-INVALID';
481
+ cacheWrite(key, body);
482
+ }
483
+ } else {
484
+ source = 'AI-GENERATED';
485
+ cacheWrite(key, body);
486
+ }
299
487
  }
300
488
  }
301
489
  } catch {
@@ -335,8 +523,14 @@ ${inputsDoc}${returnsDoc} * Source: ${source}
335
523
  ? 'AI returned code with syntax errors — function throws at runtime. Fix or regenerate.'
336
524
  : 'STUB — Claude CLI unavailable. Install Claude Code or implement manually.'}
337
525
  */
338
- export async function ${functionName}(${signature}): Promise<${returnType}> {
339
- ${destructure ? destructure + '\n' : ''}${body}
526
+ export async function ${functionName}(${(() => {
527
+ const { signature: sig } = buildSignatureAndDestructure(body);
528
+ return sig;
529
+ })()}): Promise<${returnType}> {
530
+ ${(() => {
531
+ const { destructure } = buildSignatureAndDestructure(body);
532
+ return destructure ? destructure + '\n' : '';
533
+ })()}${body}
340
534
  }`);
341
535
  }
342
536
 
@@ -356,27 +550,28 @@ ${destructure ? destructure + '\n' : ''}${body}
356
550
  * These functions could not be generated from convention patterns.
357
551
  * They are called by ${ownerName} when executing operations.
358
552
  *
553
+ * PURE-FUNCTION CONTRACT — these bodies must NOT touch the database, the
554
+ * event bus, or any external service. Persistence and side effects happen
555
+ * in the calling controller; this file does pure transformations only.
556
+ *
359
557
  * Options for each function:
360
558
  * - Implement manually (recommended for business-critical logic)
361
559
  * - Use AI generation: specverse ai generate <function>
362
560
  * - Refactor the spec step to use a convention pattern
363
561
  *
364
- * Convention patterns that ARE auto-generated (no AI needed):
365
- * "Find {Model} by {field}" prisma.model.findUniqueOrThrow(...)
366
- * "Create {Model}" → prisma.model.create(...)
367
- * "Update {Model} {field} to {value}" → prisma.model.update(...)
368
- * "Delete {Model}" prisma.model.delete(...)
369
- * "Transition {Model} to {state}" → prisma.model.update({ status: ... })
370
- * "Count {Model}s per {Group}" → prisma.model.groupBy(...)
371
- * See step-conventions.ts for the full list.
562
+ * Convention patterns that ARE auto-generated by the realize engine (no
563
+ * AI needed) these are emitted inline in the controller, not here:
564
+ * "Find {Model} by {field}" → ORM-specific find call
565
+ * "Create {Model}" → ORM-specific create call
566
+ * "Update {Model} {field} to {value}"
567
+ * "Delete {Model}"
568
+ * "Transition {Model} to {state}"
569
+ * "Count {Model}s per {Group}"
570
+ * See the ORM's step-conventions module for the full list.
372
571
  *
373
572
  * Generated: ${new Date().toISOString().split('T')[0]}
374
573
  */
375
574
 
376
- import { PrismaClient } from '@prisma/client';
377
-
378
- const prisma = new PrismaClient();
379
-
380
575
  ${functions.join('\n\n')}
381
576
  `;
382
577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specverse/engines",
3
- "version": "6.5.4",
3
+ "version": "6.7.8",
4
4
  "description": "SpecVerse toolchain — parser, inference, realize, generators, AI, registry, bundles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -62,26 +62,25 @@
62
62
  "@ai-sdk/openai-compatible": "^2.0.41",
63
63
  "@ai-sdk/provider": "^3.0.8",
64
64
  "@specverse/assets": "^1.6.0",
65
- "@specverse/engines": "^6.4.0",
66
65
  "@specverse/entities": "^5.1.0",
67
66
  "@specverse/runtime": "^5.0.1",
68
67
  "@specverse/types": "^5.1.0",
69
68
  "ai": "^6.0.168",
70
69
  "ajv": "^8.17.0",
71
70
  "ajv-formats": "^2.1.0",
71
+ "esbuild": "^0.25.0",
72
72
  "glob": "^10.0.0",
73
73
  "graphology": "^0.26.0",
74
74
  "graphology-communities-louvain": "^2.0.2",
75
75
  "handlebars": "^4.7.9",
76
76
  "js-yaml": "^4.1.0",
77
77
  "semver": "^7.0.0",
78
+ "typescript": "^5.4.0",
78
79
  "yaml": "^2.8.1",
79
80
  "zod": "^4.3.6"
80
81
  },
81
82
  "devDependencies": {
82
- "@types/node": "^25.5.0",
83
- "esbuild": "^0.25.0",
84
- "typescript": "^5.4.0"
83
+ "@types/node": "^25.5.0"
85
84
  },
86
85
  "files": [
87
86
  "dist",