@specverse/engines 4.1.19 → 4.1.21
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/behavior-regenerate.d.ts +24 -0
- package/dist/ai/behavior-regenerate.d.ts.map +1 -0
- package/dist/ai/behavior-regenerate.js +41 -0
- package/dist/ai/behavior-regenerate.js.map +1 -0
- package/dist/ai/index.d.ts +2 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +3 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +1 -1
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -1
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +81 -0
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +153 -3
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +7 -1
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +244 -0
- package/dist/realize/index.js.bak +758 -0
- package/libs/instance-factories/applications/templates/react/package-json-generator.ts +1 -1
- package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +1 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +81 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +213 -2
- package/libs/instance-factories/tools/templates/mcp/static/package.json +2 -2
- package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +7 -1
- package/package.json +1 -1
|
@@ -44,7 +44,7 @@ export default function generatePackageJson(context: TemplateContext): string {
|
|
|
44
44
|
"postcss": "^8.4.47",
|
|
45
45
|
"tailwindcss": "^3.4.13",
|
|
46
46
|
"typescript": "^5.2.0",
|
|
47
|
-
"vite": "^
|
|
47
|
+
"vite": "^6.0.0",
|
|
48
48
|
"eslint": "^8.53.0",
|
|
49
49
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
|
50
50
|
"@typescript-eslint/parser": "^6.10.0",
|
package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts
CHANGED
|
@@ -53,7 +53,7 @@ export default function generateRuntimePackageJson(context: TemplateContext): st
|
|
|
53
53
|
"postcss": "^8.4.47",
|
|
54
54
|
"tailwindcss": "^3.4.13",
|
|
55
55
|
"typescript": "^5.2.0",
|
|
56
|
-
"vite": "^
|
|
56
|
+
"vite": "^6.0.0",
|
|
57
57
|
"eslint": "^8.53.0",
|
|
58
58
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
|
59
59
|
"@typescript-eslint/parser": "^6.10.0",
|
|
@@ -843,6 +843,87 @@ import { EngineRegistry } from '@specverse/entities';`,
|
|
|
843
843
|
console.log(template);
|
|
844
844
|
}`
|
|
845
845
|
},
|
|
846
|
+
'ai.regenerate': {
|
|
847
|
+
imports: `import { readFileSync, existsSync } from 'fs';
|
|
848
|
+
import { resolve } from 'path';
|
|
849
|
+
import { EngineRegistry } from '@specverse/entities';
|
|
850
|
+
import type { ParserEngine } from '@specverse/types';`,
|
|
851
|
+
handler: `// Parse Owner.functionName — or just Owner with --all
|
|
852
|
+
const target = fn;
|
|
853
|
+
const dotIdx = target.indexOf('.');
|
|
854
|
+
const ownerName = dotIdx >= 0 ? target.slice(0, dotIdx) : target;
|
|
855
|
+
const functionName = dotIdx >= 0 ? target.slice(dotIdx + 1) : null;
|
|
856
|
+
if (!functionName && !options.all) {
|
|
857
|
+
console.error('Function must be specified as Owner.functionName, or use --all to regenerate every AI function in the owner.');
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Resolve spec file — default to specs/main-inferred.specly next to cwd
|
|
862
|
+
const userCwd = process.env.SPECVERSE_USER_CWD || process.cwd();
|
|
863
|
+
const specCandidates = options.spec
|
|
864
|
+
? [resolve(userCwd, options.spec)]
|
|
865
|
+
: [resolve(userCwd, 'specs/main-inferred.specly'), resolve(userCwd, 'specs/main.specly')];
|
|
866
|
+
const specPath = specCandidates.find(p => existsSync(p));
|
|
867
|
+
if (!specPath) {
|
|
868
|
+
console.error('Spec not found. Tried: ' + specCandidates.join(', '));
|
|
869
|
+
console.error('Pass --spec <file> or run from a project root with specs/main.specly.');
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
console.log('Using spec: ' + specPath);
|
|
873
|
+
|
|
874
|
+
const registry = new EngineRegistry();
|
|
875
|
+
await registry.discover();
|
|
876
|
+
const parser = registry.getEngineForCapability('parse') as ParserEngine;
|
|
877
|
+
if (!parser) { console.error('No parser engine found.'); process.exit(1); }
|
|
878
|
+
await parser.initialize();
|
|
879
|
+
|
|
880
|
+
const specContent = readFileSync(specPath, 'utf8');
|
|
881
|
+
const parseResult = parser.parseContent(specContent, specPath);
|
|
882
|
+
if (parseResult.errors.length > 0) {
|
|
883
|
+
console.error('Invalid spec:');
|
|
884
|
+
parseResult.errors.forEach((e: string) => console.error(' ' + e));
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Flatten components down to a realize-shaped spec
|
|
889
|
+
// (controllers, services, models at top level) — matches what
|
|
890
|
+
// regenerateBehavior expects. Collections may be arrays (parser
|
|
891
|
+
// output) or objects (inferred spec); preserve the incoming shape.
|
|
892
|
+
const ast: any = parseResult.ast;
|
|
893
|
+
const flat: any = { ...ast };
|
|
894
|
+
const components = ast?.components || {};
|
|
895
|
+
for (const comp of Object.values(components) as any[]) {
|
|
896
|
+
for (const key of ['models', 'controllers', 'services']) {
|
|
897
|
+
const collection = comp?.[key];
|
|
898
|
+
if (!collection) continue;
|
|
899
|
+
if (Array.isArray(collection)) {
|
|
900
|
+
flat[key] = [...(Array.isArray(flat[key]) ? flat[key] : []), ...collection];
|
|
901
|
+
} else {
|
|
902
|
+
flat[key] = { ...(flat[key] && !Array.isArray(flat[key]) ? flat[key] : {}), ...collection };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const { regenerateBehavior } = await import('@specverse/engines/ai');
|
|
908
|
+
const outputDir = resolve(userCwd, options.output || 'generated/code');
|
|
909
|
+
try {
|
|
910
|
+
const result = await regenerateBehavior({
|
|
911
|
+
spec: flat,
|
|
912
|
+
ownerName,
|
|
913
|
+
targetFunction: options.all ? null : functionName,
|
|
914
|
+
outputDir,
|
|
915
|
+
allFunctions: !!options.all,
|
|
916
|
+
});
|
|
917
|
+
console.log(\`✅ Regenerated \${result.filePath}\`);
|
|
918
|
+
console.log(\` Owner: \${ownerName}\`);
|
|
919
|
+
console.log(\` Unmatched functions: \${result.unmatchedCount}\`);
|
|
920
|
+
console.log(\` Target: \${options.all ? 'all' : functionName}\`);
|
|
921
|
+
console.log(\` Cache entries cleared: \${result.cacheCleared}\`);
|
|
922
|
+
} catch (err: any) {
|
|
923
|
+
console.error('Regeneration failed: ' + (err?.message || err));
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}`
|
|
926
|
+
},
|
|
846
927
|
// === session subcommands ===
|
|
847
928
|
'session.create': {
|
|
848
929
|
imports: `import { EngineRegistry } from '@specverse/entities';`,
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
import type { TemplateContext } from '@specverse/types';
|
|
21
21
|
import { matchStep, type StepContext } from './step-conventions.js';
|
|
22
22
|
import { createHash } from 'crypto';
|
|
23
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
24
|
-
import { join } from 'path';
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
24
|
+
import { dirname, join } from 'path';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Validate generated TypeScript using esbuild's transform.
|
|
@@ -374,3 +374,214 @@ const prisma = new PrismaClient();
|
|
|
374
374
|
${functions.join('\n\n')}
|
|
375
375
|
`;
|
|
376
376
|
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Regenerate a single owner's AI behaviors file, optionally targeting one
|
|
380
|
+
* function for cache bypass.
|
|
381
|
+
*
|
|
382
|
+
* Used by `specverse ai regenerate <Owner.fn>`. Works on both controllers
|
|
383
|
+
* (actions) and services (operations). When `targetFunction` is provided,
|
|
384
|
+
* its cache entry is deleted before regeneration so Claude is re-consulted
|
|
385
|
+
* for that one function; all other functions in the owner reuse their
|
|
386
|
+
* cache. When `targetFunction` is null, the whole file is rebuilt without
|
|
387
|
+
* touching the cache.
|
|
388
|
+
*
|
|
389
|
+
* `spec` should be the inferred spec (normalized shape: `controllers` and
|
|
390
|
+
* `services` are either objects keyed by name or arrays with `.name`).
|
|
391
|
+
*/
|
|
392
|
+
export async function regenerateAiBehaviors(opts: {
|
|
393
|
+
spec: any;
|
|
394
|
+
ownerName: string;
|
|
395
|
+
targetFunction: string | null;
|
|
396
|
+
outputDir: string;
|
|
397
|
+
allFunctions?: boolean;
|
|
398
|
+
}): Promise<{
|
|
399
|
+
filePath: string;
|
|
400
|
+
targetFound: boolean;
|
|
401
|
+
unmatchedCount: number;
|
|
402
|
+
cacheCleared: number;
|
|
403
|
+
}> {
|
|
404
|
+
const { spec, ownerName, targetFunction, outputDir, allFunctions } = opts;
|
|
405
|
+
|
|
406
|
+
// Resolve the owner — could be a controller (has .actions) or a service (has .operations).
|
|
407
|
+
// Accept both the "object keyed by name" and "array with .name" shapes.
|
|
408
|
+
const findOwner = (collection: any): any => {
|
|
409
|
+
if (!collection) return null;
|
|
410
|
+
if (Array.isArray(collection)) return collection.find((c: any) => c?.name === ownerName) || null;
|
|
411
|
+
return collection[ownerName] || null;
|
|
412
|
+
};
|
|
413
|
+
const controller = findOwner(spec.controllers);
|
|
414
|
+
const service = findOwner(spec.services);
|
|
415
|
+
if (!controller && !service) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Owner '${ownerName}' not found in spec. Looked in controllers and services — ` +
|
|
418
|
+
`check that the name matches exactly (case-sensitive) and that you're pointing ` +
|
|
419
|
+
`at an inferred spec (try 'specs/main-inferred.specly').`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Build the unmatched-function list the same way realize does.
|
|
424
|
+
type UnmatchedFn = {
|
|
425
|
+
functionName: string;
|
|
426
|
+
step: string;
|
|
427
|
+
operationName: string;
|
|
428
|
+
parameterNames: string[];
|
|
429
|
+
inputs: string[];
|
|
430
|
+
returns?: Record<string, string> | string;
|
|
431
|
+
modelName: string;
|
|
432
|
+
};
|
|
433
|
+
const unmatched: UnmatchedFn[] = [];
|
|
434
|
+
const allModels: string[] = Object.keys(spec.models || {});
|
|
435
|
+
|
|
436
|
+
if (controller) {
|
|
437
|
+
const modelName = controller.model || controller.modelReference || ownerName.replace(/Controller$/, '');
|
|
438
|
+
const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
439
|
+
const actions = controller.actions || {};
|
|
440
|
+
for (const [actionName, action] of Object.entries(actions) as [string, any][]) {
|
|
441
|
+
const steps = action.steps || [];
|
|
442
|
+
const parameterNames = Object.keys(action.parameters || {});
|
|
443
|
+
const declaredVars = new Set<string>();
|
|
444
|
+
const preconditions = action.requires || action.preconditions || [];
|
|
445
|
+
for (const pc of preconditions) {
|
|
446
|
+
const m = typeof pc === 'string' ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
|
|
447
|
+
if (m) declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
|
|
448
|
+
}
|
|
449
|
+
for (let i = 0; i < steps.length; i++) {
|
|
450
|
+
const stepInput = steps[i];
|
|
451
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput?.step;
|
|
452
|
+
const stepReturns = typeof stepInput === 'object' ? stepInput?.returns : undefined;
|
|
453
|
+
if (typeof stepText !== 'string') continue;
|
|
454
|
+
const ctx: StepContext = {
|
|
455
|
+
modelName,
|
|
456
|
+
prismaModel: modelVar,
|
|
457
|
+
serviceName: ownerName,
|
|
458
|
+
operationName: actionName,
|
|
459
|
+
stepNum: i + 1,
|
|
460
|
+
parameterNames,
|
|
461
|
+
declaredVars,
|
|
462
|
+
resultName: typeof stepInput === 'object' ? stepInput?.as : undefined,
|
|
463
|
+
};
|
|
464
|
+
const result = matchStep(stepText, ctx);
|
|
465
|
+
if (!result.matched && result.functionName) {
|
|
466
|
+
if (!unmatched.find(f => f.functionName === result.functionName)) {
|
|
467
|
+
unmatched.push({
|
|
468
|
+
functionName: result.functionName,
|
|
469
|
+
step: stepText,
|
|
470
|
+
operationName: actionName,
|
|
471
|
+
parameterNames,
|
|
472
|
+
inputs: result.inputs || [],
|
|
473
|
+
returns: stepReturns,
|
|
474
|
+
modelName,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} else if (service) {
|
|
481
|
+
// Service operations share the same matchStep logic but without a model prefix.
|
|
482
|
+
const modelName = service.name?.replace(/Service$/, '') || ownerName;
|
|
483
|
+
const operations = service.operations || {};
|
|
484
|
+
const opEntries: [string, any][] = Array.isArray(operations)
|
|
485
|
+
? operations.map((op: any) => [op.name, op])
|
|
486
|
+
: Object.entries(operations);
|
|
487
|
+
for (const [opName, operation] of opEntries) {
|
|
488
|
+
const steps = operation.steps || operation.implementation?.steps || [];
|
|
489
|
+
const parameterNames = Object.keys(operation.parameters || {});
|
|
490
|
+
const declaredVars = new Set<string>();
|
|
491
|
+
const preconditions = operation.requires || operation.preconditions || [];
|
|
492
|
+
for (const pc of preconditions) {
|
|
493
|
+
const m = typeof pc === 'string' ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
|
|
494
|
+
if (m) declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
|
|
495
|
+
}
|
|
496
|
+
for (let i = 0; i < steps.length; i++) {
|
|
497
|
+
const stepInput = steps[i];
|
|
498
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput?.step;
|
|
499
|
+
const stepReturns = typeof stepInput === 'object' ? stepInput?.returns : undefined;
|
|
500
|
+
if (typeof stepText !== 'string') continue;
|
|
501
|
+
const ctx: StepContext = {
|
|
502
|
+
modelName,
|
|
503
|
+
prismaModel: modelName.charAt(0).toLowerCase() + modelName.slice(1),
|
|
504
|
+
serviceName: ownerName,
|
|
505
|
+
operationName: opName,
|
|
506
|
+
stepNum: i + 1,
|
|
507
|
+
parameterNames,
|
|
508
|
+
declaredVars,
|
|
509
|
+
resultName: typeof stepInput === 'object' ? stepInput?.as : undefined,
|
|
510
|
+
};
|
|
511
|
+
const result = matchStep(stepText, ctx);
|
|
512
|
+
if (!result.matched && result.functionName) {
|
|
513
|
+
if (!unmatched.find(f => f.functionName === result.functionName)) {
|
|
514
|
+
unmatched.push({
|
|
515
|
+
functionName: result.functionName,
|
|
516
|
+
step: stepText,
|
|
517
|
+
operationName: opName,
|
|
518
|
+
parameterNames,
|
|
519
|
+
inputs: result.inputs || [],
|
|
520
|
+
returns: stepReturns,
|
|
521
|
+
modelName: ownerName,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (unmatched.length === 0) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`No AI behaviors to regenerate for '${ownerName}'. All of its steps match ` +
|
|
532
|
+
`built-in convention patterns (see step-conventions.ts). If you expected an ` +
|
|
533
|
+
`AI-generated function, double-check the spec step text.`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Find the target function (unless regenerating the whole owner)
|
|
538
|
+
const targetFound = !targetFunction || unmatched.some(f => f.functionName === targetFunction);
|
|
539
|
+
if (targetFunction && !targetFound) {
|
|
540
|
+
const known = unmatched.map(f => f.functionName).join(', ');
|
|
541
|
+
throw new Error(
|
|
542
|
+
`Function '${targetFunction}' not found among '${ownerName}' unmatched functions. ` +
|
|
543
|
+
`Known: ${known || '(none)'}.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Delete the cache entry for the target function (or all of them if --all)
|
|
548
|
+
// so Claude is re-consulted. Other cache entries are untouched so other
|
|
549
|
+
// functions reuse their previous output.
|
|
550
|
+
let cacheCleared = 0;
|
|
551
|
+
const functionsToInvalidate = allFunctions
|
|
552
|
+
? unmatched.map(f => f.functionName)
|
|
553
|
+
: targetFunction ? [targetFunction] : [];
|
|
554
|
+
for (const fnName of functionsToInvalidate) {
|
|
555
|
+
const fn = unmatched.find(f => f.functionName === fnName)!;
|
|
556
|
+
const key = cacheKey(fn.step, fn.modelName, fn.operationName, fn.functionName, fn.inputs);
|
|
557
|
+
const path = join(cacheDir(), `${key}.ts`);
|
|
558
|
+
if (existsSync(path)) {
|
|
559
|
+
try {
|
|
560
|
+
unlinkSync(path);
|
|
561
|
+
cacheCleared++;
|
|
562
|
+
} catch (err: any) {
|
|
563
|
+
console.warn(` [ai-cache] failed to delete ${path}: ${err?.message || err}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Regenerate the full file (cache hits reuse output for non-target functions)
|
|
569
|
+
const code = await generateAiBehaviorsFile({
|
|
570
|
+
ownerName,
|
|
571
|
+
unmatchedFunctions: unmatched,
|
|
572
|
+
availableModels: allModels,
|
|
573
|
+
spec,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${ownerName}.ai.ts`);
|
|
577
|
+
const dir = dirname(filePath);
|
|
578
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
579
|
+
writeFileSync(filePath, code, 'utf8');
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
filePath,
|
|
583
|
+
targetFound,
|
|
584
|
+
unmatchedCount: unmatched.length,
|
|
585
|
+
cacheCleared,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"@types/node": "^20.19.11",
|
|
59
59
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
60
60
|
"@typescript-eslint/parser": "^7.0.0",
|
|
61
|
-
"@vitest/coverage-v8": "^
|
|
61
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
62
62
|
"eslint": "^8.57.0",
|
|
63
63
|
"typescript": "^5.9.2",
|
|
64
|
-
"vitest": "^
|
|
64
|
+
"vitest": "^3.2.4"
|
|
65
65
|
},
|
|
66
66
|
"files": [
|
|
67
67
|
"dist/local/",
|
|
@@ -257,7 +257,13 @@ function generatePackageJson(
|
|
|
257
257
|
devDependencies: {
|
|
258
258
|
'@types/vscode': '^1.80.0',
|
|
259
259
|
'@vscode/vsce': '^3.0.0',
|
|
260
|
-
esbuild: '^0.
|
|
260
|
+
esbuild: '^0.25.0',
|
|
261
|
+
},
|
|
262
|
+
overrides: {
|
|
263
|
+
// Force the patched version of lodash through @vscode/vsce →
|
|
264
|
+
// @secretlint → @textlint transitive chain (4.17.23 has a
|
|
265
|
+
// code-injection advisory in `_.template`).
|
|
266
|
+
lodash: '^4.18.1',
|
|
261
267
|
},
|
|
262
268
|
};
|
|
263
269
|
}
|