@specverse/engines 6.11.2 → 6.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/behavior-ai-service.js +2 -2
- package/dist/ai/behavior-ai-service.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +22 -5
- package/dist/libs/instance-factories/services/postgres-native-services.yaml +90 -0
- package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +44 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +18 -6
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +230 -34
- package/dist/libs/instance-factories/services/templates/postgres-native/client-generator.js +165 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/controller-generator.js +300 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/ddl-generator.js +169 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/service-generator.js +65 -0
- package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +433 -0
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +1 -1
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +7 -34
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +8 -0
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +46 -24
- package/libs/instance-factories/services/postgres-native-services.yaml +90 -0
- package/libs/instance-factories/services/templates/_shared/step-matching.ts +103 -0
- package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +25 -5
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +336 -68
- package/libs/instance-factories/services/templates/postgres-native/__tests__/controller-generator.test.ts +193 -0
- package/libs/instance-factories/services/templates/postgres-native/client-generator.ts +178 -0
- package/libs/instance-factories/services/templates/postgres-native/controller-generator.ts +372 -0
- package/libs/instance-factories/services/templates/postgres-native/ddl-generator.ts +236 -0
- package/libs/instance-factories/services/templates/postgres-native/service-generator.ts +84 -0
- package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +539 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +1 -1
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +21 -68
- package/package.json +3 -3
|
@@ -39,7 +39,7 @@ function loadBehaviorPrompt() {
|
|
|
39
39
|
// 1. Try @specverse/assets first.
|
|
40
40
|
try {
|
|
41
41
|
const pkg = require.resolve('@specverse/assets/package.json');
|
|
42
|
-
candidates.push(join(dirname(pkg), 'prompts', 'core', 'standard', '
|
|
42
|
+
candidates.push(join(dirname(pkg), 'prompts', 'core', 'standard', 'default', 'behavior.prompt.yaml'));
|
|
43
43
|
}
|
|
44
44
|
catch {
|
|
45
45
|
/* not installed — fall through */
|
|
@@ -47,7 +47,7 @@ function loadBehaviorPrompt() {
|
|
|
47
47
|
// 2. Legacy engines-bundled fallback.
|
|
48
48
|
const __filename = fileURLToPath(import.meta.url);
|
|
49
49
|
const __dirname = dirname(__filename);
|
|
50
|
-
candidates.push(join(__dirname, '..', '..', 'assets', 'prompts', 'core', 'standard', '
|
|
50
|
+
candidates.push(join(__dirname, '..', '..', 'assets', 'prompts', 'core', 'standard', 'default', 'behavior.prompt.yaml'), join(__dirname, '..', '..', '..', 'assets', 'prompts', 'core', 'standard', 'default', 'behavior.prompt.yaml'));
|
|
51
51
|
for (const path of candidates) {
|
|
52
52
|
if (existsSync(path)) {
|
|
53
53
|
const doc = yaml.load(readFileSync(path, 'utf8'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"behavior-ai-service.js","sourceRoot":"","sources":["../../src/ai/behavior-ai-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AA4B/C;;;;GAIG;AACH,SAAS,kBAAkB;IACzB,IAAI,CAAC;QACH,MAAM,UAAU,GAAa,EAAE,CAAC;QAEhC,kCAAkC;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;YAC9D,UAAU,CAAC,IAAI,CACb,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"behavior-ai-service.js","sourceRoot":"","sources":["../../src/ai/behavior-ai-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,7 +12,10 @@ function generateBackendPackageJson(context) {
|
|
|
12
12
|
const appName = (spec.metadata?.component || "app").toLowerCase().replace(/\s+/g, "-");
|
|
13
13
|
const orm = resolveOrmName(manifest);
|
|
14
14
|
const isMongoNative = orm === "MongoDBNativeDriver";
|
|
15
|
-
const
|
|
15
|
+
const isPgNative = orm === "PostgresNativeDriver";
|
|
16
|
+
const dbScripts = isMongoNative ? {} : isPgNative ? {
|
|
17
|
+
"db:setup": 'psql "$POSTGRES_URL" -f src/db/schema.sql'
|
|
18
|
+
} : {
|
|
16
19
|
"db:setup": "prisma generate && prisma db push",
|
|
17
20
|
"db:generate": "prisma generate",
|
|
18
21
|
"db:push": "prisma db push",
|
|
@@ -20,8 +23,8 @@ function generateBackendPackageJson(context) {
|
|
|
20
23
|
"db:studio": "prisma studio",
|
|
21
24
|
"db:seed": "tsx prisma/seed.ts"
|
|
22
25
|
};
|
|
23
|
-
const ormDeps = isMongoNative ? { "mongodb": "^6.3.0" } : { "@prisma/client": "^5.7.0" };
|
|
24
|
-
const ormDevDeps = isMongoNative ? {} : { "prisma": "^5.7.0" };
|
|
26
|
+
const ormDeps = isMongoNative ? { "mongodb": "^6.3.0" } : isPgNative ? { "pg": "^8.11.0" } : { "@prisma/client": "^5.7.0" };
|
|
27
|
+
const ormDevDeps = isMongoNative ? {} : isPgNative ? { "@types/pg": "^8.11.0" } : { "prisma": "^5.7.0" };
|
|
25
28
|
const pkg = {
|
|
26
29
|
name: `${appName}-backend`,
|
|
27
30
|
version: spec.metadata?.version || "1.0.0",
|
|
@@ -61,7 +64,17 @@ function generateBackendPackageJson(context) {
|
|
|
61
64
|
"eventemitter3": "^5.0.0",
|
|
62
65
|
"zod": "^3.22.0",
|
|
63
66
|
"dotenv": "^16.3.0",
|
|
64
|
-
"commander": "^13.0.0"
|
|
67
|
+
"commander": "^13.0.0",
|
|
68
|
+
// AI-behavior whitelist — these libs are allowed to be dynamic-
|
|
69
|
+
// imported from generated `*.ai.ts` pure functions. They cover the
|
|
70
|
+
// common cases (JWT, hashing, formula eval) without giving the LLM
|
|
71
|
+
// unbounded library access. Listed unconditionally for now; revisit
|
|
72
|
+
// (TODO #43K-B-review) whether to gate per-spec when the spec
|
|
73
|
+
// doesn't actually use them.
|
|
74
|
+
"jsonwebtoken": "^9.0.0",
|
|
75
|
+
"bcryptjs": "^2.4.3",
|
|
76
|
+
"uuid": "^9.0.0",
|
|
77
|
+
"expr-eval": "^2.0.2"
|
|
65
78
|
},
|
|
66
79
|
devDependencies: {
|
|
67
80
|
"typescript": "^5.3.0",
|
|
@@ -72,7 +85,11 @@ function generateBackendPackageJson(context) {
|
|
|
72
85
|
"@vitest/coverage-v8": "^3.0.0",
|
|
73
86
|
"eslint": "^9.0.0",
|
|
74
87
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
75
|
-
"@typescript-eslint/parser": "^8.0.0"
|
|
88
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
89
|
+
// Type definitions for the AI-behavior whitelist libs (#43K-B).
|
|
90
|
+
"@types/jsonwebtoken": "^9.0.0",
|
|
91
|
+
"@types/bcryptjs": "^2.4.0",
|
|
92
|
+
"@types/uuid": "^9.0.0"
|
|
76
93
|
},
|
|
77
94
|
engines: {
|
|
78
95
|
node: ">=20.0.0"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: PostgresNativeDriver
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
category: service
|
|
4
|
+
description: "Business logic services using the native node-postgres (pg) driver — raw SQL, no ORM layer"
|
|
5
|
+
|
|
6
|
+
metadata:
|
|
7
|
+
author: "SpecVerse Team"
|
|
8
|
+
license: "MIT"
|
|
9
|
+
tags: [services, business-logic, postgres, pg, native-driver, controllers]
|
|
10
|
+
|
|
11
|
+
compatibility:
|
|
12
|
+
specverse: ">=5.0.0"
|
|
13
|
+
node: ">=18.0.0"
|
|
14
|
+
|
|
15
|
+
# Same shape as MongoDBNativeDriver — the pg pool IS the data layer; no ORM
|
|
16
|
+
# layer to swap in independently. This single factory therefore provides
|
|
17
|
+
# both the orm.* capabilities and the service-layer capabilities.
|
|
18
|
+
capabilities:
|
|
19
|
+
provides:
|
|
20
|
+
- "orm.schema" # Emits the pg pool client + DDL (the connection IS the schema layer)
|
|
21
|
+
- "orm.client"
|
|
22
|
+
- "orm.postgres.native"
|
|
23
|
+
- "service.controller"
|
|
24
|
+
- "service.business"
|
|
25
|
+
- "service.crud"
|
|
26
|
+
requires:
|
|
27
|
+
- "storage.database.relational"
|
|
28
|
+
|
|
29
|
+
technology:
|
|
30
|
+
runtime: "node"
|
|
31
|
+
language: "typescript"
|
|
32
|
+
orm: "postgres-native"
|
|
33
|
+
version: "^8.11.0"
|
|
34
|
+
|
|
35
|
+
dependencies:
|
|
36
|
+
runtime:
|
|
37
|
+
- name: "pg"
|
|
38
|
+
version: "^8.11.0"
|
|
39
|
+
|
|
40
|
+
dev:
|
|
41
|
+
- name: "@types/pg"
|
|
42
|
+
version: "^8.11.0"
|
|
43
|
+
- name: "@types/node"
|
|
44
|
+
version: "^20.8.0"
|
|
45
|
+
- name: "typescript"
|
|
46
|
+
version: "^5.2.0"
|
|
47
|
+
|
|
48
|
+
codeTemplates:
|
|
49
|
+
# `schema` slot emits the pg pool singleton + a co-located CREATE TABLE
|
|
50
|
+
# script (schema.sql) so users can bootstrap a database without pulling
|
|
51
|
+
# in an external migrator. The pool generator owns the runtime
|
|
52
|
+
# connection; the DDL generator owns the static SQL.
|
|
53
|
+
schema:
|
|
54
|
+
engine: typescript
|
|
55
|
+
generator: "libs/instance-factories/services/templates/postgres-native/client-generator.ts"
|
|
56
|
+
outputPattern: "{backendDir}/src/db/pgClient.ts"
|
|
57
|
+
|
|
58
|
+
ddl:
|
|
59
|
+
engine: typescript
|
|
60
|
+
generator: "libs/instance-factories/services/templates/postgres-native/ddl-generator.ts"
|
|
61
|
+
outputPattern: "{backendDir}/src/db/schema.sql"
|
|
62
|
+
|
|
63
|
+
controllers:
|
|
64
|
+
engine: typescript
|
|
65
|
+
generator: "libs/instance-factories/services/templates/postgres-native/controller-generator.ts"
|
|
66
|
+
outputPattern: "{backendDir}/src/controllers/{model}Controller.ts"
|
|
67
|
+
|
|
68
|
+
services:
|
|
69
|
+
engine: typescript
|
|
70
|
+
generator: "libs/instance-factories/services/templates/postgres-native/service-generator.ts"
|
|
71
|
+
outputPattern: "{backendDir}/src/services/{service}.ts"
|
|
72
|
+
|
|
73
|
+
configuration:
|
|
74
|
+
outputStructure: "monorepo"
|
|
75
|
+
backendDir: "backend"
|
|
76
|
+
tableNaming: "lowercase-pluralized" # User → users, OrderItem → orderitems
|
|
77
|
+
validation: true
|
|
78
|
+
eventPublishing: true
|
|
79
|
+
errorHandling: "throw"
|
|
80
|
+
|
|
81
|
+
requirements:
|
|
82
|
+
dependencies:
|
|
83
|
+
npm:
|
|
84
|
+
dependencies:
|
|
85
|
+
"pg": "^8.11.0"
|
|
86
|
+
environment:
|
|
87
|
+
- name: "POSTGRES_URL"
|
|
88
|
+
description: "PostgreSQL connection string (e.g. postgres://user:pass@localhost:5432/myapp)"
|
|
89
|
+
required: true
|
|
90
|
+
configuration: {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function toMethod(words) {
|
|
2
|
+
const cleaned = words.trim().replace(/[^A-Za-z0-9\s]+/g, " ");
|
|
3
|
+
const camel = cleaned.replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase());
|
|
4
|
+
const safe = camel.replace(/[^A-Za-z0-9_$]/g, "");
|
|
5
|
+
return safe || "unnamedStep";
|
|
6
|
+
}
|
|
7
|
+
function toVar(name) {
|
|
8
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
9
|
+
}
|
|
10
|
+
function matchAgainstConventions(step, ctx, conventions, aiArgsExpr) {
|
|
11
|
+
for (const convention of conventions) {
|
|
12
|
+
const m = step.match(convention.pattern);
|
|
13
|
+
if (m) {
|
|
14
|
+
const call = convention.generateCall(m, ctx);
|
|
15
|
+
if (call) {
|
|
16
|
+
return {
|
|
17
|
+
matched: true,
|
|
18
|
+
call,
|
|
19
|
+
helperMethod: convention.generateMethod?.(m, ctx)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const functionName = toMethod(step);
|
|
25
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
26
|
+
const paramNames = ctx.parameterNames || [];
|
|
27
|
+
const inputs = [...paramNames, ...declared];
|
|
28
|
+
const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
|
|
29
|
+
if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
|
|
30
|
+
const inputObj = aiArgsExpr(inputs, paramNames);
|
|
31
|
+
return {
|
|
32
|
+
matched: false,
|
|
33
|
+
call: ` // Step ${ctx.stepNum}: ${step} [AI-generated \u2014 pure transform]
|
|
34
|
+
const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
|
|
35
|
+
functionName,
|
|
36
|
+
inputs,
|
|
37
|
+
resultVar
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
matchAgainstConventions,
|
|
42
|
+
toMethod,
|
|
43
|
+
toVar
|
|
44
|
+
};
|
package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildTransitionMap, isAutoField } from "@specverse/types/spec-rules";
|
|
2
2
|
function generateMongoNativeController(context) {
|
|
3
|
-
const { controller, model } = context;
|
|
3
|
+
const { controller, model, models } = context;
|
|
4
4
|
if (!controller) throw new Error("Controller is required in template context");
|
|
5
5
|
if (!model) throw new Error("Model is required for controller generation");
|
|
6
6
|
const controllerName = controller.name;
|
|
@@ -8,7 +8,13 @@ function generateMongoNativeController(context) {
|
|
|
8
8
|
const modelVar = lowerFirst(modelName);
|
|
9
9
|
const collection = collectionName(model);
|
|
10
10
|
const curedOps = controller.cured || {};
|
|
11
|
-
const
|
|
11
|
+
const modelRegistry = {};
|
|
12
|
+
if (Array.isArray(models)) {
|
|
13
|
+
for (const m of models) if (m?.name) modelRegistry[m.name] = m;
|
|
14
|
+
} else if (models && typeof models === "object") {
|
|
15
|
+
Object.assign(modelRegistry, models);
|
|
16
|
+
}
|
|
17
|
+
const customActions = generateCustomActions(controller, modelRegistry);
|
|
12
18
|
const validate = generateValidateMethod(model, modelName);
|
|
13
19
|
const create = curedOps.create ? generateCreateMethod(model, modelName, modelVar, collection) : "";
|
|
14
20
|
const retrieve = curedOps.retrieve ? generateRetrieveMethod(modelName, modelVar, collection) : "";
|
|
@@ -23,7 +29,7 @@ function generateMongoNativeController(context) {
|
|
|
23
29
|
*/
|
|
24
30
|
import { ObjectId, type Filter, type Document } from 'mongodb';
|
|
25
31
|
import { getCollection } from '../db/mongoClient.js';
|
|
26
|
-
${hasEventPublishing ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
32
|
+
${hasEventPublishing || customActions.needsAiBehaviors ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
27
33
|
${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${controllerName}.ai.js';` : ""}
|
|
28
34
|
|
|
29
35
|
const COLLECTION_NAME = '${collection}';
|
|
@@ -223,7 +229,7 @@ function generateDeleteMethod(modelName, modelVar, collection) {
|
|
|
223
229
|
`;
|
|
224
230
|
}
|
|
225
231
|
import { matchMongoStep } from "./step-conventions.js";
|
|
226
|
-
function generateCustomActions(controller) {
|
|
232
|
+
function generateCustomActions(controller, modelRegistry = {}) {
|
|
227
233
|
if (!controller.actions || Object.keys(controller.actions).length === 0) {
|
|
228
234
|
return { code: "", needsAiBehaviors: false };
|
|
229
235
|
}
|
|
@@ -233,7 +239,12 @@ function generateCustomActions(controller) {
|
|
|
233
239
|
const out = [];
|
|
234
240
|
let needsAiBehaviors = false;
|
|
235
241
|
for (const [actionName, action] of Object.entries(controller.actions)) {
|
|
236
|
-
if (CRUD_NAMES.has(actionName))
|
|
242
|
+
if (CRUD_NAMES.has(actionName)) {
|
|
243
|
+
console.warn(
|
|
244
|
+
`\u26A0\uFE0F ${controller.name || "Controller"}.${actionName} \u2014 behaviour-derived action collides with the auto-generated CURVED \`${actionName}\` op. Dropped to avoid TS2393 duplicate-implementation. Rename the behaviour (e.g. \`${actionName}Soft\` / \`hardDelete\`) if you need the custom logic.`
|
|
245
|
+
);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
237
248
|
const steps = Array.isArray(action.steps) ? action.steps : [];
|
|
238
249
|
const stepsHeader = steps.length > 0 ? steps.map((s) => ` * - ${typeof s === "string" ? s : s.action || JSON.stringify(s)}`).join("\n") : " * (no spec steps declared)";
|
|
239
250
|
const declaredVars = /* @__PURE__ */ new Set();
|
|
@@ -253,7 +264,8 @@ function generateCustomActions(controller) {
|
|
|
253
264
|
operationName: actionName,
|
|
254
265
|
stepNum: i + 1,
|
|
255
266
|
parameterNames: Object.keys(action.parameters || {}),
|
|
256
|
-
declaredVars
|
|
267
|
+
declaredVars,
|
|
268
|
+
models: modelRegistry
|
|
257
269
|
};
|
|
258
270
|
const result = matchMongoStep(stepText, ctx);
|
|
259
271
|
stepBodies.push(result.call);
|
|
@@ -1,15 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
toVar,
|
|
3
|
+
matchAgainstConventions
|
|
4
|
+
} from "../_shared/step-matching.js";
|
|
5
|
+
function deriveModelDefaults(modelName, ctx) {
|
|
6
|
+
if (!ctx.models) return [];
|
|
7
|
+
const model = ctx.models[modelName] || ctx.models[modelName.charAt(0).toUpperCase() + modelName.slice(1)];
|
|
8
|
+
if (!model) return [];
|
|
9
|
+
const attrs = model.attributes;
|
|
10
|
+
if (!attrs) return [];
|
|
11
|
+
const list = Array.isArray(attrs) ? attrs.map((a) => [a.name, a]) : Object.entries(attrs);
|
|
12
|
+
const declaredVars = ctx.declaredVars || /* @__PURE__ */ new Set();
|
|
13
|
+
const out = [];
|
|
14
|
+
const conventionManaged = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "id"]);
|
|
15
|
+
for (const [name, attr] of list) {
|
|
16
|
+
if (!name) continue;
|
|
17
|
+
if (conventionManaged.has(name)) continue;
|
|
18
|
+
const required = !!attr.required;
|
|
19
|
+
const hasDefault = attr.default !== void 0;
|
|
20
|
+
if (!required && !hasDefault) continue;
|
|
21
|
+
if (hasDefault) {
|
|
22
|
+
out.push(`${name}: ${formatDefault(attr.default, attr.type)}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const type = (attr.type || "String").toLowerCase();
|
|
26
|
+
if (type === "integer" || type === "int" || type === "number" || type === "float") {
|
|
27
|
+
const min = attr.min ?? 0;
|
|
28
|
+
out.push(`${name}: ${min}`);
|
|
29
|
+
} else if (type === "boolean") {
|
|
30
|
+
out.push(`${name}: false`);
|
|
31
|
+
} else if (type === "datetime" || type === "date") {
|
|
32
|
+
if (attr.auto === "now") {
|
|
33
|
+
out.push(`${name}: new Date().toISOString()`);
|
|
34
|
+
} else {
|
|
35
|
+
out.push(`${name}: new Date().toISOString()`);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const selfVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
39
|
+
const fkMatch = name.match(/^(.+)Id$/);
|
|
40
|
+
if (fkMatch && fkMatch[1] !== selfVar && declaredVars.has(fkMatch[1])) {
|
|
41
|
+
out.push(`${name}: (${fkMatch[1]} as any)?._id ?? (${fkMatch[1]} as any)?.id`);
|
|
42
|
+
} else {
|
|
43
|
+
out.push(`${name}: ''`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function formatDefault(value, type) {
|
|
50
|
+
if (value === null || value === void 0) return "null";
|
|
51
|
+
if (typeof value === "boolean") return String(value);
|
|
52
|
+
if (typeof value === "number") return String(value);
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
55
|
+
if (value === "true" || value === "false") return value;
|
|
56
|
+
if (value === "now" && (type === "DateTime" || type === "Date")) {
|
|
57
|
+
return "new Date().toISOString()";
|
|
58
|
+
}
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify(value);
|
|
3
62
|
}
|
|
4
63
|
function toCollection(modelName) {
|
|
5
64
|
return modelName.toLowerCase() + "s";
|
|
6
65
|
}
|
|
7
|
-
function toMethod(words) {
|
|
8
|
-
const cleaned = words.trim().replace(/[^A-Za-z0-9\s]+/g, " ");
|
|
9
|
-
const camel = cleaned.replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase());
|
|
10
|
-
const safe = camel.replace(/[^A-Za-z0-9_$]/g, "");
|
|
11
|
-
return safe || "unnamedStep";
|
|
12
|
-
}
|
|
13
66
|
function resolveValue(rawValue, ctx) {
|
|
14
67
|
const value = rawValue.trim().replace(/^['"]|['"]$/g, "");
|
|
15
68
|
if (/^(current\s*time|now|timestamp)$/i.test(value)) return "new Date().toISOString()";
|
|
@@ -26,6 +79,14 @@ function resolveValue(rawValue, ctx) {
|
|
|
26
79
|
if (/\s/.test(value)) return `/* TODO: resolve "${value}" */ null`;
|
|
27
80
|
return `'${value.replace(/'/g, "\\'")}'`;
|
|
28
81
|
}
|
|
82
|
+
function mostRecentStepResult(ctx) {
|
|
83
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
84
|
+
const stepResults = declared.filter((v) => /^step\d+Result$/.test(v)).sort((a, b) => parseInt(b.slice(4), 10) - parseInt(a.slice(4), 10));
|
|
85
|
+
return stepResults[0] ?? null;
|
|
86
|
+
}
|
|
87
|
+
function pascal(model) {
|
|
88
|
+
return model.charAt(0).toUpperCase() + model.slice(1);
|
|
89
|
+
}
|
|
29
90
|
const MONGO_STEP_CONVENTIONS = [
|
|
30
91
|
// --- Find / Lookup by single field ---
|
|
31
92
|
// Matches: "Look up X by Y", "Find X by Y", "Find X by Y or fail with 404"
|
|
@@ -47,7 +108,8 @@ const MONGO_STEP_CONVENTIONS = [
|
|
|
47
108
|
const failOnMissing = /or\s+fail/i.test(m[0]);
|
|
48
109
|
return ` // Step ${ctx.stepNum}: Find ${model} by ${field}
|
|
49
110
|
const ${modelVar}_collection = await getCollection('${collection}');
|
|
50
|
-
|
|
111
|
+
let ${modelVar} = await ${modelVar}_collection.findOne({ ${field}: ${idVal} });
|
|
112
|
+
if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) (${modelVar} as any).id = String((${modelVar} as any)._id);${failOnMissing ? `
|
|
51
113
|
if (!${modelVar}) throw new Error('${model} not found');` : ""}`;
|
|
52
114
|
}
|
|
53
115
|
},
|
|
@@ -67,9 +129,22 @@ const MONGO_STEP_CONVENTIONS = [
|
|
|
67
129
|
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2} (already loaded)`;
|
|
68
130
|
}
|
|
69
131
|
declared.add(modelVar);
|
|
132
|
+
const resolveFieldSource = (f) => {
|
|
133
|
+
const stripIdMatch = f.match(/^(.+)Id$/);
|
|
134
|
+
if (stripIdMatch) {
|
|
135
|
+
const candidate = stripIdMatch[1];
|
|
136
|
+
if (candidate !== modelVar && declared.has(candidate)) {
|
|
137
|
+
return `(${candidate} as any)?._id ?? (${candidate} as any)?.id`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return `args.${f}`;
|
|
141
|
+
};
|
|
142
|
+
const f1Src = resolveFieldSource(f1);
|
|
143
|
+
const f2Src = resolveFieldSource(f2);
|
|
70
144
|
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2}
|
|
71
145
|
const ${modelVar}_collection = await getCollection('${collection}');
|
|
72
|
-
|
|
146
|
+
let ${modelVar} = await ${modelVar}_collection.findOne({ ${f1}: ${f1Src}, ${f2}: ${f2Src} });
|
|
147
|
+
if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) (${modelVar} as any).id = String((${modelVar} as any)._id);`;
|
|
73
148
|
}
|
|
74
149
|
},
|
|
75
150
|
// --- Create model record ---
|
|
@@ -227,6 +302,148 @@ const MONGO_STEP_CONVENTIONS = [
|
|
|
227
302
|
}`;
|
|
228
303
|
}
|
|
229
304
|
},
|
|
305
|
+
// ──────────────────────────────────────────────────────────────────
|
|
306
|
+
// SIDE-EFFECT CONVENTIONS — patterns that the generator handles
|
|
307
|
+
// mechanically. The LLM never sees these; persistence happens
|
|
308
|
+
// deterministically from the spec text.
|
|
309
|
+
// ──────────────────────────────────────────────────────────────────
|
|
310
|
+
// --- Persist / Save / Store {Model} record ---
|
|
311
|
+
// Matches: "Persist refresh token for revocation tracking",
|
|
312
|
+
// "Save user record", "Store session"
|
|
313
|
+
// Source of the record: most-recent step{N}Result if any (typical case
|
|
314
|
+
// — the prior AI step computed what to persist), else `args`.
|
|
315
|
+
{
|
|
316
|
+
name: "persist",
|
|
317
|
+
pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
|
|
318
|
+
generateCall: (m, ctx) => {
|
|
319
|
+
const target = toVar(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
|
|
320
|
+
const collection = toCollection(target);
|
|
321
|
+
const recordSrc = mostRecentStepResult(ctx) ?? "args";
|
|
322
|
+
return ` // Step ${ctx.stepNum}: Persist ${m[1]}
|
|
323
|
+
{
|
|
324
|
+
const _coll = await getCollection('${collection}');
|
|
325
|
+
await _coll.insertOne(${recordSrc} && typeof ${recordSrc} === 'object' && !Array.isArray(${recordSrc}) ? ${recordSrc} as any : { value: ${recordSrc} });
|
|
326
|
+
}`;
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
// --- Conditional create: "If X does not exist, create new X with ..." ---
|
|
330
|
+
// Reuses the model variable already declared by a prior find; mutates
|
|
331
|
+
// it to point at the newly-created record so subsequent steps see it.
|
|
332
|
+
{
|
|
333
|
+
name: "conditional-create",
|
|
334
|
+
pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
|
|
335
|
+
generateCall: (m, ctx) => {
|
|
336
|
+
const modelVar = toVar(m[1]);
|
|
337
|
+
const Model = pascal(m[2]);
|
|
338
|
+
const collection = toCollection(Model);
|
|
339
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
340
|
+
const defaults = deriveModelDefaults(Model, ctx);
|
|
341
|
+
const defaultsBlock = defaults.length > 0 ? defaults.join(", ") + "," : "";
|
|
342
|
+
return ` // Step ${ctx.stepNum}: If ${modelVar} does not exist, create new ${Model}
|
|
343
|
+
if (!${modelVar}) {
|
|
344
|
+
const _newRecord = { ${defaultsBlock} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
345
|
+
const _ins = await (await getCollection('${collection}')).insertOne(_newRecord);
|
|
346
|
+
${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord } as any;
|
|
347
|
+
} else if (${modelVar} && !(${modelVar} as any).id && (${modelVar} as any)._id) {
|
|
348
|
+
(${modelVar} as any).id = String((${modelVar} as any)._id);
|
|
349
|
+
}`;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
// --- Conditional update: "If X exists, update Y" ---
|
|
353
|
+
// Updates a single field on the previously-loaded record. Field defaults
|
|
354
|
+
// to lastLoginAt-style timestamp when the step says "update Y" without
|
|
355
|
+
// a "to <value>" clause.
|
|
356
|
+
{
|
|
357
|
+
name: "conditional-update",
|
|
358
|
+
pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
|
|
359
|
+
generateCall: (m, ctx) => {
|
|
360
|
+
const modelVar = toVar(m[1]);
|
|
361
|
+
const field = m[2];
|
|
362
|
+
const collection = toCollection(m[1]);
|
|
363
|
+
if (!ctx.declaredVars?.has(modelVar)) return "";
|
|
364
|
+
return ` // Step ${ctx.stepNum}: If ${modelVar} exists, update ${field}
|
|
365
|
+
if (${modelVar}) {
|
|
366
|
+
await (await getCollection('${collection}')).updateOne(
|
|
367
|
+
{ _id: ${modelVar}._id },
|
|
368
|
+
{ $set: { ${field}: new Date().toISOString() } }
|
|
369
|
+
);
|
|
370
|
+
}`;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
// --- Auto-create / Bulk fan-out create ---
|
|
374
|
+
// Matches: "Auto-create player profiles for all available games"
|
|
375
|
+
// "Create X records for all Y"
|
|
376
|
+
// Pure-mechanical: enumerate the source collection, build a record per
|
|
377
|
+
// item linking it to the most-recently-loaded model var (typically
|
|
378
|
+
// `user`), insertMany. No AI hop — domain-specific field defaults are
|
|
379
|
+
// limited to the link fields (userId, gameId) plus createdAt/updatedAt.
|
|
380
|
+
// Anything richer should be done in a follow-up step (e.g. "Set displayName...").
|
|
381
|
+
{
|
|
382
|
+
name: "auto-create-loop",
|
|
383
|
+
pattern: /^(?:auto-create|bulk\s+create)\s+(\w+)\s+(?:profile|record|entry|entries)?s?\s+for\s+(?:all\s+)?(?:available\s+)?(\w+)$/i,
|
|
384
|
+
generateCall: (m, ctx) => {
|
|
385
|
+
const Model = pascal(m[1]);
|
|
386
|
+
const targetCollection = toCollection(Model);
|
|
387
|
+
const sourceCollection = m[2].toLowerCase();
|
|
388
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
389
|
+
const ownerVar = declared.find((v) => !/^step\d+Result$/.test(v) && v !== "args");
|
|
390
|
+
const sourceSingular = sourceCollection.replace(/s$/, "");
|
|
391
|
+
const linkField = sourceSingular + "Id";
|
|
392
|
+
const ownerLinkField = ownerVar ? ownerVar + "Id" : "ownerId";
|
|
393
|
+
const modelDefaults = deriveModelDefaults(Model, ctx);
|
|
394
|
+
const defaultsBlock = modelDefaults.length > 0 ? modelDefaults.filter((d) => !d.startsWith(`${ownerLinkField}:`) && !d.startsWith(`${linkField}:`)).join(", ") : "";
|
|
395
|
+
return ` // Step ${ctx.stepNum}: Auto-create ${m[1]} ${m[2]} for all ${m[2]}
|
|
396
|
+
{
|
|
397
|
+
const _allItems = await (await getCollection('${sourceCollection}')).find({}).toArray();
|
|
398
|
+
const _ownerId = ${ownerVar ? `(${ownerVar} as any)?.id ?? (${ownerVar} as any)?._id` : "null"};
|
|
399
|
+
const _records = _allItems.map((_item: any) => ({
|
|
400
|
+
${defaultsBlock ? defaultsBlock + "," : ""}
|
|
401
|
+
${ownerLinkField}: _ownerId,
|
|
402
|
+
${linkField}: (_item as any).${sourceSingular}Id ?? (_item as any).id ?? String((_item as any)._id),
|
|
403
|
+
createdAt: new Date().toISOString(),
|
|
404
|
+
updatedAt: new Date().toISOString(),
|
|
405
|
+
}));
|
|
406
|
+
if (_records.length > 0) {
|
|
407
|
+
await (await getCollection('${targetCollection}')).insertMany(_records as any);
|
|
408
|
+
}
|
|
409
|
+
}`;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
// --- "Otherwise create new X record" — pairs with prior conditional ---
|
|
413
|
+
// Emitted as an `else` branch attached to the conditional that came
|
|
414
|
+
// before. We don't enforce ordering at the convention level; if the
|
|
415
|
+
// author writes "Otherwise" without a prior "If ... does not exist,
|
|
416
|
+
// create" the emitted else lands without an if and tsc catches it.
|
|
417
|
+
{
|
|
418
|
+
name: "otherwise-create",
|
|
419
|
+
pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
|
|
420
|
+
generateCall: (m, ctx) => {
|
|
421
|
+
const Model = pascal(m[1]);
|
|
422
|
+
const modelVar = toVar(Model);
|
|
423
|
+
const collection = toCollection(Model);
|
|
424
|
+
const wasDeclared = ctx.declaredVars?.has(modelVar);
|
|
425
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
426
|
+
const defaults = deriveModelDefaults(Model, ctx);
|
|
427
|
+
const supplied = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const entry of defaults) {
|
|
429
|
+
const colonIdx = entry.indexOf(":");
|
|
430
|
+
if (colonIdx > 0) supplied.add(entry.slice(0, colonIdx).trim());
|
|
431
|
+
}
|
|
432
|
+
const fkAssignments = declared.filter((v) => v !== modelVar && !/^step\d+Result$/.test(v) && v !== "args").filter((v) => !supplied.has(v + "Id")).map((v) => {
|
|
433
|
+
supplied.add(v + "Id");
|
|
434
|
+
return `${v}Id: (${v} as any)?._id ?? (${v} as any)?.id`;
|
|
435
|
+
}).join(", ");
|
|
436
|
+
const defaultsBlock = defaults.length > 0 ? defaults.join(", ") + "," : "";
|
|
437
|
+
ctx.declaredVars?.add(modelVar);
|
|
438
|
+
return ` // Step ${ctx.stepNum}: Otherwise create new ${Model} record
|
|
439
|
+
else {
|
|
440
|
+
const _newRecord = { ${defaultsBlock} ${fkAssignments ? fkAssignments + "," : ""} ...args, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
441
|
+
const _ins = await (await getCollection('${collection}')).insertOne(_newRecord);
|
|
442
|
+
${wasDeclared ? `${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord } as any;` : `const ${modelVar} = { _id: _ins.insertedId, id: String(_ins.insertedId), ..._newRecord };
|
|
443
|
+
void ${modelVar};`}
|
|
444
|
+
}`;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
230
447
|
// --- Send/Emit/Publish event ---
|
|
231
448
|
// Emits an eventBus.publish call. The payload references the controller's
|
|
232
449
|
// primary model variable IF it was declared by a prior matched step;
|
|
@@ -288,32 +505,11 @@ const MONGO_STEP_CONVENTIONS = [
|
|
|
288
505
|
}
|
|
289
506
|
];
|
|
290
507
|
function matchMongoStep(step, ctx) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (m) {
|
|
294
|
-
const call = convention.generateCall(m, ctx);
|
|
295
|
-
if (call) {
|
|
296
|
-
return { matched: true, call };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
const functionName = toMethod(step);
|
|
301
|
-
const declared = Array.from(ctx.declaredVars || []);
|
|
302
|
-
const paramNames = ctx.parameterNames || [];
|
|
303
|
-
const inputs = [...paramNames, ...declared];
|
|
304
|
-
const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
|
|
305
|
-
if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
|
|
306
|
-
const inputObj = inputs.length > 0 ? `{ ${inputs.map((n) => paramNames.includes(n) ? `${n}: args.${n}` : n).join(", ")} }` : "{}";
|
|
307
|
-
return {
|
|
308
|
-
matched: false,
|
|
309
|
-
call: ` // Step ${ctx.stepNum}: ${step} [AI-generated \u2014 pure function]
|
|
310
|
-
const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
|
|
311
|
-
functionName,
|
|
312
|
-
inputs,
|
|
313
|
-
resultVar
|
|
314
|
-
};
|
|
508
|
+
const aiArgsExpr = (inputs, paramNames) => inputs.length > 0 ? `{ ${inputs.map((n) => paramNames.includes(n) ? `${n}: args.${n}` : n).join(", ")} }` : "{}";
|
|
509
|
+
return matchAgainstConventions(step, ctx, MONGO_STEP_CONVENTIONS, aiArgsExpr);
|
|
315
510
|
}
|
|
316
511
|
export {
|
|
317
512
|
MONGO_STEP_CONVENTIONS,
|
|
513
|
+
deriveModelDefaults,
|
|
318
514
|
matchMongoStep
|
|
319
515
|
};
|