@specverse/engines 6.6.3 → 6.11.2
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/inference/core/specly-converter.d.ts.map +1 -1
- package/dist/inference/core/specly-converter.js +20 -0
- package/dist/inference/core/specly-converter.js.map +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +72 -22
- package/dist/inference/index.js.map +1 -1
- package/dist/inference/logical/generators/controller-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/controller-generator.js +26 -4
- package/dist/inference/logical/generators/controller-generator.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +26 -10
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +50 -15
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +27 -7
- package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +59 -23
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +319 -0
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +192 -28
- package/dist/parser/processors/ExecutableProcessor.d.ts.map +1 -1
- package/dist/parser/processors/ExecutableProcessor.js +14 -1
- package/dist/parser/processors/ExecutableProcessor.js.map +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +22 -3
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +48 -12
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +80 -21
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +49 -8
- package/libs/instance-factories/services/templates/mongodb-native/__tests__/controller-generator.test.ts +3 -1
- package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +82 -25
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +423 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +287 -38
- package/package.json +6 -6
|
@@ -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.
|
|
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 });
|
|
@@ -94,10 +173,33 @@ function cacheWrite(key: string, body: string): void {
|
|
|
94
173
|
*
|
|
95
174
|
* This is async — it calls the AI engine to generate function bodies.
|
|
96
175
|
*/
|
|
97
|
-
|
|
176
|
+
/** Optional matcher for orm-agnostic step matching. When present, this is
|
|
177
|
+
* used in place of the prisma library's `matchStep` so that the unmatched
|
|
178
|
+
* functions' names + inputs are computed by the same conventions library
|
|
179
|
+
* the consuming controller is using. Without this, mongo-native (or any
|
|
180
|
+
* non-prisma) controllers would diverge: they'd emit `aiBehaviors.X` calls
|
|
181
|
+
* with one inputs set while this generator would emit a function with a
|
|
182
|
+
* different signature. */
|
|
183
|
+
type StepMatcher = (step: string, ctx: any) => {
|
|
184
|
+
matched: boolean;
|
|
185
|
+
call?: string;
|
|
186
|
+
helperMethod?: string;
|
|
187
|
+
functionName?: string;
|
|
188
|
+
inputs?: string[];
|
|
189
|
+
resultVar?: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default async function generateAiBehaviors(
|
|
193
|
+
context: TemplateContext,
|
|
194
|
+
matcher?: StepMatcher,
|
|
195
|
+
): Promise<string> {
|
|
98
196
|
const { controller, model } = context;
|
|
99
197
|
if (!controller?.actions) return '';
|
|
100
198
|
|
|
199
|
+
// Default matcher = prisma's matchStep for back-compat. Mongo-native
|
|
200
|
+
// (and any future ORM) passes its own matcher.
|
|
201
|
+
const stepMatcher: StepMatcher = matcher || (matchStep as any);
|
|
202
|
+
|
|
101
203
|
const modelName = model?.name || controller.model || 'Model';
|
|
102
204
|
const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
103
205
|
// List of all entity-model names available in the realized output. Forwarded
|
|
@@ -155,7 +257,7 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
155
257
|
resultName: stepAs,
|
|
156
258
|
};
|
|
157
259
|
|
|
158
|
-
const result =
|
|
260
|
+
const result = stepMatcher(stepText, ctx);
|
|
159
261
|
if (!result.matched && result.functionName) {
|
|
160
262
|
// Avoid duplicate function definitions
|
|
161
263
|
const existing = unmatchedFunctions.find(f => f.functionName === result.functionName);
|
|
@@ -229,15 +331,107 @@ export async function generateAiBehaviorsFile(opts: {
|
|
|
229
331
|
let cacheHits = 0;
|
|
230
332
|
let cacheMisses = 0;
|
|
231
333
|
for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
|
|
232
|
-
// Pure function signature
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
334
|
+
// Pure function signature + destructure are built AFTER the body so we
|
|
335
|
+
// can match what the LLM actually references — strict tsc's
|
|
336
|
+
// noUnusedLocals / noUnusedParameters fire on every input the body
|
|
337
|
+
// didn't touch. We tolerate underuse rather than re-prompting the LLM:
|
|
338
|
+
// the body is correct as-is, we just trim the wrapper to fit it.
|
|
339
|
+
//
|
|
340
|
+
// Three cases:
|
|
341
|
+
// - body uses some inputs: destructure only those
|
|
342
|
+
// - body uses none directly but references `input.X`: keep `input`
|
|
343
|
+
// - body doesn't touch input at all: prefix signature with `_` so
|
|
344
|
+
// noUnusedParameters is happy
|
|
345
|
+
/** Strip string/template/comment content so identifier-reference checks
|
|
346
|
+
* don't match `reward` inside a kebab-case string like `'reward-not-granted'`
|
|
347
|
+
* or `// reward stays as text`. We replace the literal contents with
|
|
348
|
+
* spaces so positions stay roughly aligned in case of debugging.
|
|
349
|
+
*
|
|
350
|
+
* Critical: template-string `${...}` interpolations are EXPRESSIONS, not
|
|
351
|
+
* literal text — they really do reference `input.X` etc. Strip only the
|
|
352
|
+
* non-interpolation segments of the template so the expression content
|
|
353
|
+
* survives the regex check. */
|
|
354
|
+
const stripLiteralsAndComments = (src: string): string => {
|
|
355
|
+
let out = src
|
|
356
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
357
|
+
.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length))
|
|
358
|
+
.replace(/(['"])(?:\\.|(?!\1).)*\1/g, (m) => m[0] + ' '.repeat(m.length - 2) + m[0]);
|
|
359
|
+
// Strip text INSIDE backticks but preserve `${...}` expressions.
|
|
360
|
+
// We rebuild template contents by replacing literal-text spans
|
|
361
|
+
// (everything between `\`` and `${`, between `}` and `${`, or
|
|
362
|
+
// between `}` and the closing `\``) with spaces.
|
|
363
|
+
out = out.replace(/`((?:\\.|\$\{[^}]*\}|(?!`).)*)`/g, (full, content) => {
|
|
364
|
+
// Walk the content, copy `${...}` segments verbatim, replace other
|
|
365
|
+
// characters with spaces.
|
|
366
|
+
let result = '`';
|
|
367
|
+
let i = 0;
|
|
368
|
+
while (i < content.length) {
|
|
369
|
+
if (content[i] === '\\' && i + 1 < content.length) {
|
|
370
|
+
result += ' '; i += 2; continue;
|
|
371
|
+
}
|
|
372
|
+
if (content[i] === '$' && content[i + 1] === '{') {
|
|
373
|
+
// copy interpolation verbatim including the `${...}` markers
|
|
374
|
+
const end = content.indexOf('}', i);
|
|
375
|
+
const slice = end >= 0 ? content.slice(i, end + 1) : content.slice(i);
|
|
376
|
+
result += slice;
|
|
377
|
+
i += slice.length;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
result += ' ';
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
383
|
+
return result + '`';
|
|
384
|
+
});
|
|
385
|
+
return out;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/** Detect whether the body manages its own input access (so adding our
|
|
389
|
+
* own destructure would either duplicate-declare or leave us with
|
|
390
|
+
* unused-locals). Three shapes the LLM emits:
|
|
391
|
+
* 1. `const { X, Y } = input;` (destructure)
|
|
392
|
+
* 2. `const X = input.X;` (per-property)
|
|
393
|
+
* 3. `const _X = input.X;` (per-property with rename)
|
|
394
|
+
* If ANY of these appear, the body is its own input setup and we
|
|
395
|
+
* should leave it alone. */
|
|
396
|
+
const bodyHandlesInputItself = (body: string): boolean => {
|
|
397
|
+
const codeOnly = stripLiteralsAndComments(body);
|
|
398
|
+
// Destructure: `const { ... } = input;`
|
|
399
|
+
if (/(?:const|let|var)\s*\{[^}]+\}\s*=\s*input\b/.test(codeOnly)) return true;
|
|
400
|
+
// Per-property access: any `(const|let|var) <name> = input.<name>`
|
|
401
|
+
if (/(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*input\.[A-Za-z_$][\w$]*/.test(codeOnly)) return true;
|
|
402
|
+
return false;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/** Does the body actually USE the local variable `n` (vs only mention
|
|
406
|
+
* it as an object-literal property name like `step1Result:` in a
|
|
407
|
+
* return object)? A real usage is `n` followed by something other than
|
|
408
|
+
* `:` — i.e. accessed in an expression. We approximate by requiring
|
|
409
|
+
* the next non-whitespace char to be a code-meaningful operator/end. */
|
|
410
|
+
const isRealReference = (n: string, codeOnly: string): boolean => {
|
|
411
|
+
const escaped = n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
412
|
+
const re = new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$:])`, 'g');
|
|
413
|
+
return re.test(codeOnly);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const buildSignatureAndDestructure = (body: string): { signature: string; destructure: string } => {
|
|
417
|
+
if (inputs.length === 0) {
|
|
418
|
+
return { signature: 'input: Record<string, never>', destructure: '' };
|
|
419
|
+
}
|
|
420
|
+
const codeOnly = stripLiteralsAndComments(body);
|
|
421
|
+
const referenced = inputs.filter((n) => isRealReference(n, codeOnly));
|
|
422
|
+
const usesInputObject = /(?<![A-Za-z0-9_$])input(?![A-Za-z0-9_$])/.test(codeOnly);
|
|
423
|
+
const sigName = (referenced.length > 0 || usesInputObject) ? 'input' : '_input';
|
|
424
|
+
const sig = `${sigName}: { ${inputs.map(n => `${n}: any`).join('; ')} }`;
|
|
425
|
+
// If the body already manages its own input access (destructure or
|
|
426
|
+
// per-property) OR accesses input.X directly, don't add a wrapper
|
|
427
|
+
// destructure — would leave us with unused locals (TS6198) or
|
|
428
|
+
// duplicate declarations (TS2451).
|
|
429
|
+
const bodyAccessesInputProps = /(?<![A-Za-z0-9_$])input\.[A-Za-z_$]/.test(codeOnly);
|
|
430
|
+
const destructure = (referenced.length > 0 && !bodyHandlesInputItself(body) && !bodyAccessesInputProps)
|
|
431
|
+
? ` const { ${referenced.join(', ')} } = input;`
|
|
432
|
+
: '';
|
|
433
|
+
return { signature: sig, destructure };
|
|
434
|
+
};
|
|
241
435
|
|
|
242
436
|
// Build return type from spec declaration (if provided)
|
|
243
437
|
// returns can be:
|
|
@@ -258,11 +452,14 @@ export async function generateAiBehaviorsFile(opts: {
|
|
|
258
452
|
let source: 'AI-CACHED' | 'AI-GENERATED' | 'AI-INVALID' | 'STUB' = body ? 'AI-CACHED' : 'STUB';
|
|
259
453
|
if (body) {
|
|
260
454
|
// Validate cache entry — a previously valid entry may have been
|
|
261
|
-
// corrupted on disk
|
|
455
|
+
// corrupted on disk OR the validation rules may have tightened
|
|
456
|
+
// (e.g. tsc type checks added). A cache hit must still be re-validated
|
|
457
|
+
// through both gates so old bodies don't leak past the new bar.
|
|
262
458
|
const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
459
|
+
const syntaxError = await validateTypeScript(testCode);
|
|
460
|
+
const typeError = syntaxError ? null : await validateTypeScriptTypes(testCode);
|
|
461
|
+
if (syntaxError || typeError) {
|
|
462
|
+
console.warn(` [ai-validate] cached ${functionName} failed validation: ${syntaxError || typeError}`);
|
|
266
463
|
body = null; // Force regeneration
|
|
267
464
|
source = 'STUB';
|
|
268
465
|
} else {
|
|
@@ -285,17 +482,62 @@ export async function generateAiBehaviorsFile(opts: {
|
|
|
285
482
|
});
|
|
286
483
|
|
|
287
484
|
if (body) {
|
|
288
|
-
//
|
|
485
|
+
// Two-tier validation:
|
|
486
|
+
// 1. esbuild syntax check — fast, catches unbalanced braces, etc.
|
|
487
|
+
// 2. tsc type check — slower, catches `string | undefined` index
|
|
488
|
+
// access, unused locals, and undefined references the LLM
|
|
489
|
+
// regularly produces. On tsc failure, we re-prompt the LLM
|
|
490
|
+
// with the error message appended; only ONE retry to bound
|
|
491
|
+
// cost. If that still fails we keep the body but mark it
|
|
492
|
+
// AI-INVALID so the user sees it needs review.
|
|
289
493
|
const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
console.warn(` [ai-validate] ${functionName} has syntax error: ${
|
|
293
|
-
//
|
|
294
|
-
body = `// AI-generated code failed validation: ${validationError}\n // Step: ${step}\n throw new Error('AI behavior has invalid syntax — see comment above');`;
|
|
494
|
+
const syntaxError = await validateTypeScript(testCode);
|
|
495
|
+
if (syntaxError) {
|
|
496
|
+
console.warn(` [ai-validate] ${functionName} has syntax error: ${syntaxError}`);
|
|
497
|
+
body = `// AI-generated code failed validation: ${syntaxError}\n // Step: ${step}\n throw new Error('AI behavior has invalid syntax — see comment above');`;
|
|
295
498
|
source = 'AI-INVALID';
|
|
296
499
|
} else {
|
|
297
|
-
|
|
298
|
-
|
|
500
|
+
const typeError = await validateTypeScriptTypes(testCode);
|
|
501
|
+
if (typeError) {
|
|
502
|
+
console.warn(` [ai-validate] ${functionName} type errors: ${typeError}`);
|
|
503
|
+
try {
|
|
504
|
+
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.`;
|
|
505
|
+
const retried = await aiService.generateBehavior({
|
|
506
|
+
step: `${step}\n\n${retryHint}`,
|
|
507
|
+
modelName,
|
|
508
|
+
operationName,
|
|
509
|
+
functionName,
|
|
510
|
+
parameterNames: inputs,
|
|
511
|
+
availableModels,
|
|
512
|
+
spec,
|
|
513
|
+
returnType,
|
|
514
|
+
});
|
|
515
|
+
if (retried) {
|
|
516
|
+
const retryCode = `export async function ${functionName}(input: any): Promise<any> {\n${retried}\n}`;
|
|
517
|
+
const retrySyntaxError = await validateTypeScript(retryCode);
|
|
518
|
+
const retryTypeError = retrySyntaxError ? null : await validateTypeScriptTypes(retryCode);
|
|
519
|
+
if (!retrySyntaxError && !retryTypeError) {
|
|
520
|
+
body = retried;
|
|
521
|
+
source = 'AI-GENERATED';
|
|
522
|
+
cacheWrite(key, body);
|
|
523
|
+
} else {
|
|
524
|
+
// Retry didn't help — keep the original body so the user can fix
|
|
525
|
+
// manually, but mark it as INVALID so it stands out.
|
|
526
|
+
source = 'AI-INVALID';
|
|
527
|
+
cacheWrite(key, body);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
source = 'AI-INVALID';
|
|
531
|
+
cacheWrite(key, body);
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
source = 'AI-INVALID';
|
|
535
|
+
cacheWrite(key, body);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
source = 'AI-GENERATED';
|
|
539
|
+
cacheWrite(key, body);
|
|
540
|
+
}
|
|
299
541
|
}
|
|
300
542
|
}
|
|
301
543
|
} catch {
|
|
@@ -335,8 +577,14 @@ ${inputsDoc}${returnsDoc} * Source: ${source}
|
|
|
335
577
|
? 'AI returned code with syntax errors — function throws at runtime. Fix or regenerate.'
|
|
336
578
|
: 'STUB — Claude CLI unavailable. Install Claude Code or implement manually.'}
|
|
337
579
|
*/
|
|
338
|
-
export async function ${functionName}(${
|
|
339
|
-
|
|
580
|
+
export async function ${functionName}(${(() => {
|
|
581
|
+
const { signature: sig } = buildSignatureAndDestructure(body);
|
|
582
|
+
return sig;
|
|
583
|
+
})()}): Promise<${returnType}> {
|
|
584
|
+
${(() => {
|
|
585
|
+
const { destructure } = buildSignatureAndDestructure(body);
|
|
586
|
+
return destructure ? destructure + '\n' : '';
|
|
587
|
+
})()}${body}
|
|
340
588
|
}`);
|
|
341
589
|
}
|
|
342
590
|
|
|
@@ -356,27 +604,28 @@ ${destructure ? destructure + '\n' : ''}${body}
|
|
|
356
604
|
* These functions could not be generated from convention patterns.
|
|
357
605
|
* They are called by ${ownerName} when executing operations.
|
|
358
606
|
*
|
|
607
|
+
* PURE-FUNCTION CONTRACT — these bodies must NOT touch the database, the
|
|
608
|
+
* event bus, or any external service. Persistence and side effects happen
|
|
609
|
+
* in the calling controller; this file does pure transformations only.
|
|
610
|
+
*
|
|
359
611
|
* Options for each function:
|
|
360
612
|
* - Implement manually (recommended for business-critical logic)
|
|
361
613
|
* - Use AI generation: specverse ai generate <function>
|
|
362
614
|
* - Refactor the spec step to use a convention pattern
|
|
363
615
|
*
|
|
364
|
-
* Convention patterns that ARE auto-generated (no
|
|
365
|
-
*
|
|
366
|
-
* "
|
|
367
|
-
* "
|
|
368
|
-
* "
|
|
369
|
-
* "
|
|
370
|
-
* "
|
|
371
|
-
*
|
|
616
|
+
* Convention patterns that ARE auto-generated by the realize engine (no
|
|
617
|
+
* AI needed) — these are emitted inline in the controller, not here:
|
|
618
|
+
* "Find {Model} by {field}" → ORM-specific find call
|
|
619
|
+
* "Create {Model}" → ORM-specific create call
|
|
620
|
+
* "Update {Model} {field} to {value}"
|
|
621
|
+
* "Delete {Model}"
|
|
622
|
+
* "Transition {Model} to {state}"
|
|
623
|
+
* "Count {Model}s per {Group}"
|
|
624
|
+
* See the ORM's step-conventions module for the full list.
|
|
372
625
|
*
|
|
373
626
|
* Generated: ${new Date().toISOString().split('T')[0]}
|
|
374
627
|
*/
|
|
375
628
|
|
|
376
|
-
import { PrismaClient } from '@prisma/client';
|
|
377
|
-
|
|
378
|
-
const prisma = new PrismaClient();
|
|
379
|
-
|
|
380
629
|
${functions.join('\n\n')}
|
|
381
630
|
`;
|
|
382
631
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specverse/engines",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.2",
|
|
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,26 @@
|
|
|
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.
|
|
66
|
-
"@specverse/entities": "^5.
|
|
65
|
+
"@specverse/engines": "^6.8.7",
|
|
66
|
+
"@specverse/entities": "^5.2.2",
|
|
67
67
|
"@specverse/runtime": "^5.0.1",
|
|
68
68
|
"@specverse/types": "^5.1.0",
|
|
69
69
|
"ai": "^6.0.168",
|
|
70
70
|
"ajv": "^8.17.0",
|
|
71
71
|
"ajv-formats": "^2.1.0",
|
|
72
|
+
"esbuild": "^0.25.0",
|
|
72
73
|
"glob": "^10.0.0",
|
|
73
74
|
"graphology": "^0.26.0",
|
|
74
75
|
"graphology-communities-louvain": "^2.0.2",
|
|
75
76
|
"handlebars": "^4.7.9",
|
|
76
77
|
"js-yaml": "^4.1.0",
|
|
77
78
|
"semver": "^7.0.0",
|
|
79
|
+
"typescript": "^5.4.0",
|
|
78
80
|
"yaml": "^2.8.1",
|
|
79
81
|
"zod": "^4.3.6"
|
|
80
82
|
},
|
|
81
83
|
"devDependencies": {
|
|
82
|
-
"@types/node": "^25.5.0"
|
|
83
|
-
"esbuild": "^0.25.0",
|
|
84
|
-
"typescript": "^5.4.0"
|
|
84
|
+
"@types/node": "^25.5.0"
|
|
85
85
|
},
|
|
86
86
|
"files": [
|
|
87
87
|
"dist",
|