opencroc 0.1.7 → 0.3.1

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/index.js CHANGED
@@ -1,123 +1,2699 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // node_modules/tsup/assets/esm_shims.js
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var init_esm_shims = __esm({
15
+ "node_modules/tsup/assets/esm_shims.js"() {
16
+ "use strict";
17
+ }
18
+ });
19
+
20
+ // src/parsers/controller-parser.ts
21
+ var controller_parser_exports = {};
22
+ __export(controller_parser_exports, {
23
+ createControllerParser: () => createControllerParser,
24
+ inferRelatedTables: () => inferRelatedTables,
25
+ parseControllerDirectory: () => parseControllerDirectory,
26
+ parseControllerFile: () => parseControllerFile
27
+ });
28
+ import * as fs2 from "fs";
29
+ import * as path3 from "path";
30
+ import {
31
+ Project as Project2,
32
+ SyntaxKind as SyntaxKind2
33
+ } from "ts-morph";
34
+ function parseControllerFile(filePath) {
35
+ const absolutePath = path3.resolve(filePath);
36
+ if (!fs2.existsSync(absolutePath)) return [];
37
+ try {
38
+ const project = new Project2({ compilerOptions: { strict: false } });
39
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
40
+ const endpoints = [];
41
+ endpoints.push(...extractRouterCalls(sourceFile));
42
+ endpoints.push(...extractBaseCrudRoutes(sourceFile));
43
+ return deduplicateEndpoints(endpoints);
44
+ } catch {
45
+ return [];
46
+ }
47
+ }
48
+ function parseControllerDirectory(dirPath) {
49
+ const absoluteDir = path3.resolve(dirPath);
50
+ if (!fs2.existsSync(absoluteDir)) return [];
51
+ const files = fs2.readdirSync(absoluteDir).filter(
52
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
53
+ );
54
+ const endpoints = [];
55
+ for (const file of files) {
56
+ endpoints.push(...parseControllerFile(path3.join(absoluteDir, file)));
57
+ }
58
+ return deduplicateEndpoints(endpoints);
59
+ }
60
+ function extractRouterCalls(sourceFile) {
61
+ const endpoints = [];
62
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
63
+ for (const call of calls) {
64
+ const expr = call.getExpression();
65
+ if (expr.getKind() !== SyntaxKind2.PropertyAccessExpression) continue;
66
+ const propAccess = expr;
67
+ const methodName = propAccess.getName().toLowerCase();
68
+ if (!HTTP_METHODS.has(methodName)) continue;
69
+ const objectText = propAccess.getExpression().getText().trim();
70
+ if (!isRouterLike(objectText)) continue;
71
+ const args = call.getArguments();
72
+ if (args.length === 0) continue;
73
+ const routePath = resolveRoutePath(args[0], sourceFile);
74
+ if (!routePath) continue;
75
+ endpoints.push({
76
+ method: METHOD_MAP[methodName],
77
+ path: routePath,
78
+ pathParams: extractPathParams(routePath),
79
+ queryParams: [],
80
+ bodyFields: [],
81
+ responseFields: [],
82
+ relatedTables: [],
83
+ description: extractDescription(call)
84
+ });
85
+ }
86
+ return endpoints;
87
+ }
88
+ function isRouterLike(text) {
89
+ return text === "router" || text === "this.router";
90
+ }
91
+ function extractBaseCrudRoutes(sourceFile) {
92
+ const endpoints = [];
93
+ let isBaseCrud = false;
94
+ for (const cls of sourceFile.getClasses()) {
95
+ const heritage = cls.getExtends();
96
+ if (heritage?.getText().includes("BaseCrudController")) {
97
+ isBaseCrud = true;
98
+ break;
99
+ }
100
+ }
101
+ if (!isBaseCrud) return endpoints;
102
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
103
+ let resourcePath = null;
104
+ for (const call of calls) {
105
+ const exprText = call.getExpression().getText();
106
+ if ((exprText === "super.registerRoutes" || exprText.endsWith(".registerRoutes")) && !exprText.includes("Custom")) {
107
+ const args = call.getArguments();
108
+ if (args.length >= 2) resourcePath = extractStringLiteral(args[1]);
109
+ }
110
+ }
111
+ if (!resourcePath) return endpoints;
112
+ const basePath = `/v1/:tenantId/${resourcePath}`;
113
+ const crudRoutes = [
114
+ { method: "GET", path: basePath, desc: `List ${resourcePath}` },
115
+ { method: "GET", path: `${basePath}/:id`, desc: `Get ${resourcePath} by ID` },
116
+ { method: "POST", path: basePath, desc: `Create ${resourcePath}` },
117
+ { method: "PUT", path: `${basePath}/:id`, desc: `Update ${resourcePath}` },
118
+ { method: "DELETE", path: `${basePath}/:id`, desc: `Delete ${resourcePath}` },
119
+ { method: "POST", path: `${basePath}/batch-delete`, desc: `Batch delete ${resourcePath}` }
120
+ ];
121
+ for (const route of crudRoutes) {
122
+ endpoints.push({
123
+ method: route.method,
124
+ path: route.path,
125
+ pathParams: extractPathParams(route.path),
126
+ queryParams: [],
127
+ bodyFields: [],
128
+ responseFields: [],
129
+ relatedTables: [],
130
+ description: route.desc
131
+ });
132
+ }
133
+ return endpoints;
134
+ }
135
+ function inferRelatedTables(servicePaths) {
136
+ const tables = /* @__PURE__ */ new Set();
137
+ for (const sp of servicePaths) {
138
+ const absolutePath = path3.resolve(sp);
139
+ if (!fs2.existsSync(absolutePath)) continue;
140
+ try {
141
+ const content = fs2.readFileSync(absolutePath, "utf-8");
142
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"][^'"]*models[^'"]*['"]/g;
143
+ let match;
144
+ while ((match = importRegex.exec(content)) !== null) {
145
+ const names = match[1].split(",").map((s) => s.trim());
146
+ for (const name of names) {
147
+ const cleanName = name.replace(/\s+as\s+\w+/, "").trim();
148
+ if (cleanName) tables.add(pascalToSnake(cleanName));
149
+ }
150
+ }
151
+ } catch {
152
+ }
153
+ }
154
+ return Array.from(tables);
155
+ }
156
+ function resolveRoutePath(node, sourceFile) {
157
+ const kind = node.getKind();
158
+ if (kind === SyntaxKind2.StringLiteral) return node.getText().slice(1, -1);
159
+ if (kind === SyntaxKind2.TemplateExpression || kind === SyntaxKind2.NoSubstitutionTemplateLiteral) {
160
+ return resolveTemplateLiteral(node, sourceFile);
161
+ }
162
+ if (kind === SyntaxKind2.Identifier) {
163
+ return resolveVariableValue(sourceFile, node.getText().trim());
164
+ }
165
+ return null;
166
+ }
167
+ function resolveTemplateLiteral(node, sourceFile) {
168
+ let result = node.getText().slice(1, -1);
169
+ result = result.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
170
+ const resolved = resolveVariableValue(sourceFile, expr.trim());
171
+ return resolved || `{${expr.trim()}}`;
172
+ });
173
+ return result;
174
+ }
175
+ function resolveVariableValue(sourceFile, varName) {
176
+ for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind2.VariableDeclaration)) {
177
+ if (decl.getName() === varName) {
178
+ const init = decl.getInitializer();
179
+ if (!init) continue;
180
+ const t = init.getText().trim();
181
+ if (t.startsWith("'") && t.endsWith("'") || t.startsWith('"') && t.endsWith('"'))
182
+ return t.slice(1, -1);
183
+ if (t.startsWith("`") && t.endsWith("`"))
184
+ return resolveTemplateLiteral(init, sourceFile);
185
+ }
186
+ }
187
+ return null;
188
+ }
189
+ function extractPathParams(routePath) {
190
+ const params = [];
191
+ const regex = /:(\w+)/g;
192
+ let match;
193
+ while ((match = regex.exec(routePath)) !== null) params.push(match[1]);
194
+ return params;
195
+ }
196
+ function extractDescription(call) {
197
+ let current = call;
198
+ while (current.getParent() && current.getParent().getKind() !== SyntaxKind2.SourceFile && current.getParent().getKind() !== SyntaxKind2.Block) {
199
+ current = current.getParent();
200
+ }
201
+ const fullText = current.getFullText();
202
+ const leadingText = fullText.substring(0, fullText.indexOf(current.getText()));
203
+ const jsdocMatch = leadingText.match(/\/\*\*[\s\S]*?\*\s+(.+?)(?:\n|\*\/)/);
204
+ if (jsdocMatch) return jsdocMatch[1].replace(/^\*\s*/, "").trim();
205
+ const lineMatch = leadingText.match(/\/\/\s*(.+)/);
206
+ if (lineMatch) return lineMatch[1].trim();
207
+ return "";
208
+ }
209
+ function extractStringLiteral(node) {
210
+ const t = node.getText().trim();
211
+ if (t.startsWith("'") && t.endsWith("'") || t.startsWith('"') && t.endsWith('"'))
212
+ return t.slice(1, -1);
213
+ return null;
214
+ }
215
+ function pascalToSnake(name) {
216
+ return name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
217
+ }
218
+ function deduplicateEndpoints(endpoints) {
219
+ const seen = /* @__PURE__ */ new Map();
220
+ for (const ep of endpoints) {
221
+ const key = `${ep.method}:${ep.path}`;
222
+ if (!seen.has(key)) {
223
+ seen.set(key, ep);
224
+ } else {
225
+ const existing = seen.get(key);
226
+ const merged = /* @__PURE__ */ new Set([...existing.relatedTables, ...ep.relatedTables]);
227
+ existing.relatedTables = Array.from(merged);
228
+ if (!existing.description && ep.description) existing.description = ep.description;
229
+ }
230
+ }
231
+ return Array.from(seen.values());
232
+ }
233
+ function createControllerParser() {
234
+ return {
235
+ async parseFile(filePath) {
236
+ return parseControllerFile(filePath);
237
+ },
238
+ async parseDirectory(dirPath) {
239
+ return parseControllerDirectory(dirPath);
240
+ }
241
+ };
242
+ }
243
+ var HTTP_METHODS, METHOD_MAP;
244
+ var init_controller_parser = __esm({
245
+ "src/parsers/controller-parser.ts"() {
246
+ "use strict";
247
+ init_esm_shims();
248
+ HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch"]);
249
+ METHOD_MAP = {
250
+ get: "GET",
251
+ post: "POST",
252
+ put: "PUT",
253
+ delete: "DELETE",
254
+ patch: "PATCH"
255
+ };
256
+ }
257
+ });
258
+
259
+ // src/index.ts
260
+ init_esm_shims();
261
+
1
262
  // src/config.ts
263
+ init_esm_shims();
2
264
  function defineConfig(config) {
3
265
  return config;
4
266
  }
5
267
 
6
268
  // src/pipeline/index.ts
7
- function createPipeline(_config) {
269
+ init_esm_shims();
270
+ import * as fs4 from "fs";
271
+ import * as path5 from "path";
272
+
273
+ // src/parsers/model-parser.ts
274
+ init_esm_shims();
275
+ import * as fs from "fs";
276
+ import * as path2 from "path";
277
+ import {
278
+ Project,
279
+ SyntaxKind
280
+ } from "ts-morph";
281
+ function parseModelFile(filePath) {
282
+ const absolutePath = path2.resolve(filePath);
283
+ if (!fs.existsSync(absolutePath)) return null;
284
+ const project = new Project({ compilerOptions: { strict: false } });
285
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
286
+ const initCall = findInitCall(sourceFile);
287
+ if (!initCall) return null;
288
+ const args = initCall.getArguments();
289
+ if (args.length < 2) return null;
290
+ const fields = parseFieldDefinitions(args[0]);
291
+ const { tableName, indexes } = parseOptions(args[1]);
292
+ if (!tableName) return null;
293
+ return { tableName, fields, indexes };
294
+ }
295
+ function parseModuleModels(modelDir) {
296
+ const absoluteDir = path2.resolve(modelDir);
297
+ if (!fs.existsSync(absoluteDir)) return [];
298
+ const files = fs.readdirSync(absoluteDir).filter(
299
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts" && f !== "associations.ts"
300
+ );
301
+ const schemas = [];
302
+ for (const file of files) {
303
+ try {
304
+ const schema = parseModelFile(path2.join(absoluteDir, file));
305
+ if (schema) schemas.push(schema);
306
+ } catch {
307
+ }
308
+ }
309
+ return schemas;
310
+ }
311
+ function findInitCall(sourceFile) {
312
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
313
+ for (const call of calls) {
314
+ const expr = call.getExpression();
315
+ if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
316
+ const propAccess = expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
317
+ if (propAccess.getName() === "init") return call;
318
+ }
319
+ }
320
+ return null;
321
+ }
322
+ function parseFieldDefinitions(fieldsNode) {
323
+ const fields = [];
324
+ if (fieldsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) return fields;
325
+ const objLiteral = fieldsNode;
326
+ for (const prop of objLiteral.getProperties()) {
327
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
328
+ const propAssign = prop;
329
+ const initializer = propAssign.getInitializer();
330
+ if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
331
+ fields.push(parseFieldObject(propAssign.getName(), initializer));
332
+ }
333
+ return fields;
334
+ }
335
+ function parseFieldObject(fieldName, fieldObj) {
336
+ const field = { name: fieldName, type: "STRING", allowNull: true, primaryKey: false };
337
+ for (const prop of fieldObj.getProperties()) {
338
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
339
+ const propAssign = prop;
340
+ const key = propAssign.getName();
341
+ const init = propAssign.getInitializer();
342
+ if (!init) continue;
343
+ switch (key) {
344
+ case "type":
345
+ field.type = extractDataType(init);
346
+ break;
347
+ case "allowNull":
348
+ field.allowNull = init.getText().trim() === "true";
349
+ break;
350
+ case "primaryKey":
351
+ field.primaryKey = init.getText().trim() === "true";
352
+ break;
353
+ case "defaultValue":
354
+ field.defaultValue = extractDefaultValue(init);
355
+ break;
356
+ }
357
+ }
358
+ return field;
359
+ }
360
+ function extractDataType(node) {
361
+ const text = node.getText().trim();
362
+ const callMatch = text.match(/^DataTypes\.(\w+)\((.+)\)$/);
363
+ if (callMatch) return `${callMatch[1]}(${callMatch[2]})`;
364
+ const propMatch = text.match(/^DataTypes\.(\w+)$/);
365
+ if (propMatch) return propMatch[1];
366
+ return text;
367
+ }
368
+ function extractDefaultValue(node) {
369
+ const text = node.getText().trim();
370
+ if (text === "DataTypes.NOW") return "DataTypes.NOW";
371
+ if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
372
+ return text.slice(1, -1);
373
+ if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
374
+ if (text === "true") return true;
375
+ if (text === "false") return false;
376
+ if (text === "null") return null;
377
+ return text;
378
+ }
379
+ function parseOptions(optionsNode) {
380
+ let tableName = null;
381
+ let indexes = [];
382
+ if (optionsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) return { tableName, indexes };
383
+ const objLiteral = optionsNode;
384
+ for (const prop of objLiteral.getProperties()) {
385
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
386
+ const propAssign = prop;
387
+ const key = propAssign.getName();
388
+ const init = propAssign.getInitializer();
389
+ if (!init) continue;
390
+ if (key === "tableName") tableName = extractStringValue(init);
391
+ if (key === "indexes") indexes = parseIndexes(init);
392
+ }
393
+ return { tableName, indexes };
394
+ }
395
+ function extractStringValue(node) {
396
+ const text = node.getText().trim();
397
+ if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
398
+ return text.slice(1, -1);
399
+ return null;
400
+ }
401
+ function parseIndexes(node) {
402
+ if (node.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
403
+ const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
404
+ const indexes = [];
405
+ for (const el of arr.getElements()) {
406
+ if (el.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
407
+ const idx = parseIndexObject(el);
408
+ if (idx) indexes.push(idx);
409
+ }
410
+ return indexes;
411
+ }
412
+ function parseIndexObject(obj) {
413
+ let name = "";
414
+ let fields = [];
415
+ let unique = false;
416
+ for (const prop of obj.getProperties()) {
417
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
418
+ const pa = prop;
419
+ const init = pa.getInitializer();
420
+ if (!init) continue;
421
+ switch (pa.getName()) {
422
+ case "name":
423
+ name = extractStringValue(init) || "";
424
+ break;
425
+ case "fields":
426
+ fields = extractStringArray(init);
427
+ break;
428
+ case "unique":
429
+ unique = init.getText().trim() === "true";
430
+ break;
431
+ }
432
+ }
433
+ if (!name || fields.length === 0) return null;
434
+ return { name, fields, unique };
435
+ }
436
+ function extractStringArray(node) {
437
+ if (node.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
438
+ const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
439
+ return arr.getElements().map((el) => el.getText().trim()).filter((t) => t.startsWith("'") || t.startsWith('"')).map((t) => t.slice(1, -1));
440
+ }
441
+ function createModelParser() {
8
442
  return {
9
- async run(_steps) {
10
- throw new Error("Pipeline not yet implemented");
443
+ async parseFile(filePath) {
444
+ return parseModelFile(filePath);
445
+ },
446
+ async parseDirectory(dirPath) {
447
+ return parseModuleModels(dirPath);
11
448
  }
12
449
  };
13
450
  }
14
451
 
15
- // src/parsers/model-parser.ts
16
- function createModelParser() {
452
+ // src/pipeline/index.ts
453
+ init_controller_parser();
454
+
455
+ // src/parsers/association-parser.ts
456
+ init_esm_shims();
457
+ import * as fs3 from "fs";
458
+ import * as path4 from "path";
459
+ import {
460
+ Project as Project3,
461
+ SyntaxKind as SyntaxKind3
462
+ } from "ts-morph";
463
+ function parseAssociationFile(filePath, classToTableMap, moduleTablePrefix) {
464
+ const absolutePath = path4.resolve(filePath);
465
+ if (!fs3.existsSync(absolutePath)) return [];
466
+ const project = new Project3({ compilerOptions: { strict: false } });
467
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
468
+ const importPathMap = collectImportPaths(sourceFile);
469
+ const rawAssociations = extractAssociationCalls(sourceFile, importPathMap);
470
+ if (rawAssociations.length === 0) return [];
471
+ return deduplicateRelations(rawAssociations, classToTableMap, moduleTablePrefix);
472
+ }
473
+ function buildClassToTableMap(modelDir) {
474
+ const map = /* @__PURE__ */ new Map();
475
+ const absoluteDir = path4.resolve(modelDir);
476
+ if (!fs3.existsSync(absoluteDir)) return map;
477
+ const files = fs3.readdirSync(absoluteDir).filter(
478
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts" && f !== "associations.ts"
479
+ );
480
+ for (const file of files) {
481
+ try {
482
+ const schema = parseModelFile(path4.join(absoluteDir, file));
483
+ if (schema) {
484
+ const className = file.replace(".ts", "");
485
+ map.set(className, schema.tableName);
486
+ }
487
+ } catch {
488
+ }
489
+ }
490
+ return map;
491
+ }
492
+ function collectImportPaths(sourceFile) {
493
+ const map = /* @__PURE__ */ new Map();
494
+ for (const decl of sourceFile.getImportDeclarations()) {
495
+ const moduleSpecifier = decl.getModuleSpecifierValue();
496
+ for (const named of decl.getNamedImports()) {
497
+ map.set(named.getName(), moduleSpecifier);
498
+ }
499
+ }
500
+ return map;
501
+ }
502
+ function extractAssociationCalls(sourceFile, importPathMap) {
503
+ const associations = [];
504
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression);
505
+ for (const call of calls) {
506
+ const expr = call.getExpression();
507
+ if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
508
+ const propAccess = expr.asKindOrThrow(SyntaxKind3.PropertyAccessExpression);
509
+ const methodName = propAccess.getName();
510
+ if (methodName !== "hasMany" && methodName !== "belongsTo" && methodName !== "hasOne") continue;
511
+ const sourceClass = propAccess.getExpression().getText().trim();
512
+ const args = call.getArguments();
513
+ if (args.length < 1) continue;
514
+ const targetClass = args[0].getText().trim();
515
+ let foreignKey = "";
516
+ if (args.length >= 2 && args[1].getKind() === SyntaxKind3.ObjectLiteralExpression) {
517
+ foreignKey = extractStringProperty(args[1], "foreignKey");
518
+ }
519
+ associations.push({
520
+ sourceClass,
521
+ targetClass,
522
+ foreignKey,
523
+ type: methodName,
524
+ importPath: importPathMap.get(targetClass)
525
+ });
526
+ }
527
+ return associations;
528
+ }
529
+ function extractStringProperty(obj, propertyName) {
530
+ for (const prop of obj.getProperties()) {
531
+ if (prop.getKind() !== SyntaxKind3.PropertyAssignment) continue;
532
+ const pa = prop;
533
+ if (pa.getName() !== propertyName) continue;
534
+ const init = pa.getInitializer();
535
+ if (!init) continue;
536
+ const text = init.getText().trim();
537
+ if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
538
+ return text.slice(1, -1);
539
+ return text;
540
+ }
541
+ return "";
542
+ }
543
+ function classNameToTableName(className) {
544
+ return className.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
545
+ }
546
+ function resolveTableName(className, classToTableMap) {
547
+ if (classToTableMap?.has(className)) return classToTableMap.get(className);
548
+ return classNameToTableName(className);
549
+ }
550
+ function isCrossModuleRef(targetTableName, importPath, moduleTablePrefix) {
551
+ if (moduleTablePrefix) return !targetTableName.startsWith(moduleTablePrefix);
552
+ if (importPath) {
553
+ const upLevels = (importPath.match(/\.\.\//g) || []).length;
554
+ return upLevels >= 2;
555
+ }
556
+ return false;
557
+ }
558
+ function deduplicateRelations(rawAssociations, classToTableMap, moduleTablePrefix) {
559
+ const seen = /* @__PURE__ */ new Map();
560
+ for (const raw of rawAssociations) {
561
+ const sourceTable = resolveTableName(raw.sourceClass, classToTableMap);
562
+ const targetTable = resolveTableName(raw.targetClass, classToTableMap);
563
+ const crossModule = isCrossModuleRef(targetTable, raw.importPath, moduleTablePrefix);
564
+ let parentTable;
565
+ let childTable;
566
+ let cardinality;
567
+ switch (raw.type) {
568
+ case "hasMany":
569
+ parentTable = sourceTable;
570
+ childTable = targetTable;
571
+ cardinality = "1:N";
572
+ break;
573
+ case "belongsTo":
574
+ parentTable = targetTable;
575
+ childTable = sourceTable;
576
+ cardinality = "N:1";
577
+ break;
578
+ case "hasOne":
579
+ parentTable = sourceTable;
580
+ childTable = targetTable;
581
+ cardinality = "1:1";
582
+ break;
583
+ }
584
+ const dedupeKey = `${parentTable}|${childTable}|${raw.foreignKey}`;
585
+ if (seen.has(dedupeKey)) {
586
+ const existing = seen.get(dedupeKey);
587
+ if (existing.cardinality === "N:1" && (cardinality === "1:N" || cardinality === "1:1")) {
588
+ seen.set(dedupeKey, {
589
+ sourceTable: parentTable,
590
+ sourceField: "id",
591
+ targetTable: childTable,
592
+ targetField: raw.foreignKey,
593
+ cardinality,
594
+ isCrossModule: crossModule || existing.isCrossModule
595
+ });
596
+ }
597
+ } else {
598
+ seen.set(dedupeKey, {
599
+ sourceTable: parentTable,
600
+ sourceField: "id",
601
+ targetTable: childTable,
602
+ targetField: raw.foreignKey,
603
+ cardinality,
604
+ isCrossModule: crossModule
605
+ });
606
+ }
607
+ }
608
+ return Array.from(seen.values());
609
+ }
610
+ function createAssociationParser() {
17
611
  return {
18
- async parseFile(_filePath) {
19
- throw new Error("Model parser not yet implemented");
20
- },
21
- async parseDirectory(_dirPath) {
22
- throw new Error("Model parser not yet implemented");
612
+ async parseFile(filePath) {
613
+ return parseAssociationFile(filePath);
23
614
  }
24
615
  };
25
616
  }
26
617
 
27
- // src/parsers/controller-parser.ts
28
- function createControllerParser() {
618
+ // src/analyzers/api-chain-analyzer.ts
619
+ init_esm_shims();
620
+ var EXCLUDED_PARAMS = /* @__PURE__ */ new Set(["tenantId"]);
621
+ function toNodeKey(endpoint) {
622
+ return `${endpoint.method} ${endpoint.path}`;
623
+ }
624
+ function paramToResourceHint(param) {
625
+ const stripped = param.endsWith("Id") ? param.slice(0, -2) : param;
626
+ return stripped.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
627
+ }
628
+ function postProducesResource(postEndpoint, resourceHint) {
629
+ const segments = postEndpoint.path.split("/").filter((s) => s && !s.startsWith(":"));
630
+ if (segments.length === 0) return false;
631
+ const lastSegment = segments[segments.length - 1].toLowerCase();
632
+ if (lastSegment.includes(resourceHint)) return true;
633
+ const parts = lastSegment.split("-");
634
+ if (parts.some((p) => p === resourceHint || p.startsWith(resourceHint))) return true;
635
+ if (resourceHint.length <= 4) {
636
+ const abbreviation = parts.map((p) => p[0]).join("");
637
+ if (abbreviation.startsWith(resourceHint)) return true;
638
+ }
639
+ return false;
640
+ }
641
+ function inferDependencies(endpoints) {
642
+ const dependencies = [];
643
+ const postEndpoints = endpoints.filter((ep) => ep.method === "POST");
644
+ for (const consumer of endpoints) {
645
+ const consumedParams = consumer.pathParams.filter((p) => !EXCLUDED_PARAMS.has(p));
646
+ if (consumedParams.length === 0) continue;
647
+ for (const param of consumedParams) {
648
+ if (param === "id") {
649
+ const basePath = consumer.path.replace(/\/:id(\/.*)?$/, "");
650
+ const producer2 = postEndpoints.find((ep) => ep.path === basePath);
651
+ if (producer2 && toNodeKey(producer2) !== toNodeKey(consumer)) {
652
+ dependencies.push({ from: consumer, to: producer2, paramMapping: { [`:${param}`]: "response.data.id" } });
653
+ }
654
+ continue;
655
+ }
656
+ const resourceHint = paramToResourceHint(param);
657
+ if (!resourceHint) continue;
658
+ const producer = postEndpoints.find((ep) => postProducesResource(ep, resourceHint));
659
+ if (producer && toNodeKey(producer) !== toNodeKey(consumer)) {
660
+ dependencies.push({ from: consumer, to: producer, paramMapping: { [`:${param}`]: "response.data.id" } });
661
+ }
662
+ }
663
+ }
664
+ return deduplicateDependencies(dependencies);
665
+ }
666
+ function deduplicateDependencies(deps) {
667
+ const map = /* @__PURE__ */ new Map();
668
+ for (const dep of deps) {
669
+ const key = `${toNodeKey(dep.from)}\u2192${toNodeKey(dep.to)}`;
670
+ if (map.has(key)) {
671
+ Object.assign(map.get(key).paramMapping, dep.paramMapping);
672
+ } else {
673
+ map.set(key, { ...dep, paramMapping: { ...dep.paramMapping } });
674
+ }
675
+ }
676
+ return Array.from(map.values());
677
+ }
678
+ function buildGraph(endpoints, dependencies) {
679
+ const nodeSet = /* @__PURE__ */ new Set();
680
+ for (const ep of endpoints) nodeSet.add(toNodeKey(ep));
681
+ const edges = [];
682
+ for (const dep of dependencies) {
683
+ edges.push({
684
+ from: toNodeKey(dep.from),
685
+ to: toNodeKey(dep.to),
686
+ label: Object.keys(dep.paramMapping).join(", ") || void 0
687
+ });
688
+ }
689
+ return { nodes: Array.from(nodeSet), edges };
690
+ }
691
+ function detectCycles(dag) {
692
+ const adjacency = /* @__PURE__ */ new Map();
693
+ for (const node of dag.nodes) adjacency.set(node, []);
694
+ for (const edge of dag.edges) adjacency.get(edge.from)?.push(edge.to);
695
+ const color = /* @__PURE__ */ new Map();
696
+ for (const node of dag.nodes) color.set(node, 0 /* WHITE */);
697
+ const warnings = [];
698
+ const path9 = [];
699
+ function dfs(node) {
700
+ color.set(node, 1 /* GRAY */);
701
+ path9.push(node);
702
+ for (const neighbor of adjacency.get(node) || []) {
703
+ const nc = color.get(neighbor);
704
+ if (nc === 1 /* GRAY */) {
705
+ const cycleStart = path9.indexOf(neighbor);
706
+ warnings.push(`Cycle detected: ${path9.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
707
+ } else if (nc === 0 /* WHITE */) {
708
+ dfs(neighbor);
709
+ }
710
+ }
711
+ path9.pop();
712
+ color.set(node, 2 /* BLACK */);
713
+ }
714
+ for (const node of dag.nodes) {
715
+ if (color.get(node) === 0 /* WHITE */) dfs(node);
716
+ }
717
+ return warnings;
718
+ }
719
+ function topologicalSort(dag) {
720
+ const inDegree = /* @__PURE__ */ new Map();
721
+ const adjacency = /* @__PURE__ */ new Map();
722
+ for (const node of dag.nodes) {
723
+ inDegree.set(node, 0);
724
+ adjacency.set(node, []);
725
+ }
726
+ for (const edge of dag.edges) {
727
+ adjacency.get(edge.from)?.push(edge.to);
728
+ inDegree.set(edge.to, (inDegree.get(edge.to) || 0) + 1);
729
+ }
730
+ const queue = [];
731
+ for (const [node, degree] of inDegree) {
732
+ if (degree === 0) queue.push(node);
733
+ }
734
+ const sorted = [];
735
+ while (queue.length > 0) {
736
+ const node = queue.shift();
737
+ sorted.push(node);
738
+ for (const neighbor of adjacency.get(node) || []) {
739
+ const nd = (inDegree.get(neighbor) || 1) - 1;
740
+ inDegree.set(neighbor, nd);
741
+ if (nd === 0) queue.push(neighbor);
742
+ }
743
+ }
744
+ return sorted;
745
+ }
746
+ function createApiChainAnalyzer() {
29
747
  return {
30
- async parseFile(_filePath) {
31
- throw new Error("Controller parser not yet implemented");
32
- },
33
- async parseDirectory(_dirPath) {
34
- throw new Error("Controller parser not yet implemented");
748
+ analyze(endpoints) {
749
+ const dependencies = inferDependencies(endpoints);
750
+ const dag = buildGraph(endpoints, dependencies);
751
+ const cycleWarnings = detectCycles(dag);
752
+ return {
753
+ moduleName: "",
754
+ endpoints,
755
+ dependencies,
756
+ dag,
757
+ hasCycles: cycleWarnings.length > 0,
758
+ cycleWarnings
759
+ };
35
760
  }
36
761
  };
37
762
  }
38
763
 
39
- // src/parsers/association-parser.ts
40
- function createAssociationParser() {
764
+ // src/generators/er-diagram-generator.ts
765
+ init_esm_shims();
766
+ function toMermaidType(fieldType) {
767
+ const upper = fieldType.toUpperCase();
768
+ if (upper.startsWith("STRING")) return "string";
769
+ if (upper === "BIGINT" || upper === "INTEGER") return "bigint";
770
+ if (upper === "BOOLEAN") return "boolean";
771
+ if (upper.startsWith("DATE") || upper === "NOW") return "datetime";
772
+ if (upper === "JSON" || upper === "JSONB") return "json";
773
+ if (upper === "TEXT") return "text";
774
+ if (upper === "FLOAT" || upper === "DOUBLE" || upper === "DECIMAL") return "float";
775
+ if (upper === "UUID") return "uuid";
776
+ if (upper.startsWith("ENUM")) return "enum";
777
+ return "string";
778
+ }
779
+ function sanitizeEntityName(name) {
780
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
781
+ }
782
+ function generateMermaidER(tables, relations) {
783
+ const lines = ["erDiagram"];
784
+ for (const table of tables) {
785
+ const entityName = sanitizeEntityName(table.tableName);
786
+ lines.push(` ${entityName} {`);
787
+ for (const field of table.fields) {
788
+ const mType = toMermaidType(field.type);
789
+ const pk = field.primaryKey ? "PK" : "";
790
+ const comment = field.comment ? ` "${field.comment}"` : "";
791
+ lines.push(` ${mType} ${field.name}${pk ? " " + pk : ""}${comment}`);
792
+ }
793
+ lines.push(" }");
794
+ }
795
+ const tableNames = new Set(tables.map((t) => t.tableName));
796
+ for (const rel of relations) {
797
+ if (!tableNames.has(rel.sourceTable) || !tableNames.has(rel.targetTable)) continue;
798
+ const src = sanitizeEntityName(rel.sourceTable);
799
+ const tgt = sanitizeEntityName(rel.targetTable);
800
+ const linkStyle = rel.isCrossModule ? ".." : "--";
801
+ let cardinality;
802
+ switch (rel.cardinality) {
803
+ case "1:N":
804
+ cardinality = `||${linkStyle}o{`;
805
+ break;
806
+ case "N:1":
807
+ cardinality = `}o${linkStyle}||`;
808
+ break;
809
+ case "1:1":
810
+ cardinality = `||${linkStyle}||`;
811
+ break;
812
+ default:
813
+ cardinality = `||${linkStyle}o{`;
814
+ }
815
+ lines.push(` ${src} ${cardinality} ${tgt} : "${rel.targetField}"`);
816
+ }
817
+ return lines.join("\n");
818
+ }
819
+ function createERDiagramGenerator() {
41
820
  return {
42
- async parseFile(_filePath) {
43
- throw new Error("Association parser not yet implemented");
821
+ generate(tables, relations) {
822
+ const mermaidText = generateMermaidER(tables, relations);
823
+ return { tables, relations, mermaidText };
44
824
  }
45
825
  };
46
826
  }
47
827
 
48
828
  // src/generators/test-code-generator.ts
829
+ init_esm_shims();
830
+ function resolvePathParam(param, ids) {
831
+ if (ids.includes(param)) return `createdIds['${param}']`;
832
+ const stripped = param.endsWith("Id") ? param.slice(0, -2) : param;
833
+ if (ids.includes(stripped)) return `createdIds['${stripped}']`;
834
+ if (param === "id") return `createdIds['id']`;
835
+ return `createdIds['${param}'] || '1'`;
836
+ }
837
+ function buildUrlCode(step) {
838
+ const pathParams = step.endpoint.pathParams;
839
+ if (pathParams.length === 0) return `const url = '${step.endpoint.path}';`;
840
+ let urlTemplate = step.endpoint.path;
841
+ const replacements = [];
842
+ for (const param of pathParams) {
843
+ urlTemplate = urlTemplate.replace(`:${param}`, `\${${resolvePathParam(param, pathParams)}}`);
844
+ replacements.push(param);
845
+ }
846
+ return `const url = \`${urlTemplate}\`;`;
847
+ }
848
+ function generateAssertions(step) {
849
+ const lines = [];
850
+ if (step.assertions.length > 0) {
851
+ for (const assertion of step.assertions) {
852
+ lines.push(` expect(${assertion}).toBeTruthy();`);
853
+ }
854
+ } else {
855
+ if (step.endpoint.method === "POST") {
856
+ lines.push(" expect(response.status()).toBeLessThan(400);");
857
+ lines.push(" const body = await response.json();");
858
+ lines.push(" if (body.data?.id) createdIds['id'] = body.data.id;");
859
+ } else if (step.endpoint.method === "GET") {
860
+ lines.push(" expect(response.ok()).toBeTruthy();");
861
+ } else if (step.endpoint.method === "DELETE") {
862
+ lines.push(" expect(response.status()).toBeLessThan(400);");
863
+ } else {
864
+ lines.push(" expect(response.status()).toBeLessThan(400);");
865
+ }
866
+ }
867
+ return lines;
868
+ }
869
+ function generateTestFile(chain) {
870
+ const lines = [];
871
+ lines.push(`import { test, expect } from '@playwright/test';`);
872
+ lines.push("");
873
+ lines.push(`test.describe('${chain.name}', () => {`);
874
+ lines.push(" const createdIds: Record<string, string> = {};");
875
+ lines.push("");
876
+ for (const step of chain.steps) {
877
+ lines.push(` test('Step ${step.order}: ${step.description}', async ({ request }) => {`);
878
+ lines.push(` // ${step.action}: ${step.endpoint.method} ${step.endpoint.path}`);
879
+ lines.push(` ${buildUrlCode(step)}`);
880
+ lines.push("");
881
+ if (step.endpoint.method === "GET") {
882
+ lines.push(" const response = await request.get(url);");
883
+ } else if (step.endpoint.method === "POST") {
884
+ lines.push(" const response = await request.post(url, { data: {} });");
885
+ } else if (step.endpoint.method === "PUT") {
886
+ lines.push(" const response = await request.put(url, { data: {} });");
887
+ } else if (step.endpoint.method === "DELETE") {
888
+ lines.push(" const response = await request.delete(url);");
889
+ } else if (step.endpoint.method === "PATCH") {
890
+ lines.push(" const response = await request.patch(url, { data: {} });");
891
+ }
892
+ lines.push("");
893
+ lines.push(...generateAssertions(step));
894
+ lines.push(" });");
895
+ lines.push("");
896
+ }
897
+ lines.push("});");
898
+ return lines.join("\n");
899
+ }
49
900
  function createTestCodeGenerator() {
50
901
  return {
51
- generate(_chains) {
52
- throw new Error("Test code generator not yet implemented");
902
+ generate(chains) {
903
+ return chains.map((chain) => ({
904
+ filePath: `${chain.module}/${chain.name.replace(/\s+/g, "-").toLowerCase()}.spec.ts`,
905
+ content: generateTestFile(chain),
906
+ module: chain.module,
907
+ chain: chain.name
908
+ }));
53
909
  }
54
910
  };
55
911
  }
56
912
 
913
+ // src/validators/config-validator.ts
914
+ init_esm_shims();
915
+ var REQUIRED_FIELDS = ["backendRoot"];
916
+ var VALID_ADAPTERS = ["sequelize", "typeorm", "prisma"];
917
+ var VALID_STEPS = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
918
+ var VALID_LLM_PROVIDERS = ["openai", "zhipu", "ollama", "custom"];
919
+ var VALID_REPORT_FORMATS = ["html", "json", "markdown"];
920
+ var VALID_HEAL_MODES = ["config-only", "config-and-source"];
921
+ function validateConfig(config) {
922
+ const errors = [];
923
+ for (const field of REQUIRED_FIELDS) {
924
+ if (!config[field]) {
925
+ errors.push({
926
+ module: "config",
927
+ field,
928
+ message: `Missing required field: ${field}`,
929
+ severity: "error"
930
+ });
931
+ }
932
+ }
933
+ if (config.backendRoot && typeof config.backendRoot !== "string") {
934
+ errors.push({
935
+ module: "config",
936
+ field: "backendRoot",
937
+ message: "backendRoot must be a string path",
938
+ severity: "error"
939
+ });
940
+ }
941
+ if (config.adapter && typeof config.adapter === "string") {
942
+ if (!VALID_ADAPTERS.includes(config.adapter)) {
943
+ errors.push({
944
+ module: "config",
945
+ field: "adapter",
946
+ message: `Invalid adapter: ${config.adapter}. Must be one of: ${VALID_ADAPTERS.join(", ")}`,
947
+ severity: "error"
948
+ });
949
+ }
950
+ }
951
+ if (config.steps && Array.isArray(config.steps)) {
952
+ for (const step of config.steps) {
953
+ if (!VALID_STEPS.includes(step)) {
954
+ errors.push({
955
+ module: "config",
956
+ field: "steps",
957
+ message: `Invalid pipeline step: ${step}. Must be one of: ${VALID_STEPS.join(", ")}`,
958
+ severity: "error"
959
+ });
960
+ }
961
+ }
962
+ }
963
+ if (config.llm && typeof config.llm === "object") {
964
+ const llm = config.llm;
965
+ if (llm.provider && !VALID_LLM_PROVIDERS.includes(llm.provider)) {
966
+ errors.push({
967
+ module: "config",
968
+ field: "llm.provider",
969
+ message: `Invalid LLM provider: ${llm.provider}. Must be one of: ${VALID_LLM_PROVIDERS.join(", ")}`,
970
+ severity: "error"
971
+ });
972
+ }
973
+ if (llm.provider && llm.provider !== "ollama" && !llm.apiKey) {
974
+ errors.push({
975
+ module: "config",
976
+ field: "llm.apiKey",
977
+ message: "LLM apiKey is required for cloud providers",
978
+ severity: "warning"
979
+ });
980
+ }
981
+ }
982
+ if (config.report && typeof config.report === "object") {
983
+ const report = config.report;
984
+ if (report.format && Array.isArray(report.format)) {
985
+ for (const fmt of report.format) {
986
+ if (!VALID_REPORT_FORMATS.includes(fmt)) {
987
+ errors.push({
988
+ module: "config",
989
+ field: "report.format",
990
+ message: `Invalid report format: ${fmt}. Must be one of: ${VALID_REPORT_FORMATS.join(", ")}`,
991
+ severity: "error"
992
+ });
993
+ }
994
+ }
995
+ }
996
+ }
997
+ if (config.selfHealing && typeof config.selfHealing === "object") {
998
+ const sh = config.selfHealing;
999
+ if (sh.mode && !VALID_HEAL_MODES.includes(sh.mode)) {
1000
+ errors.push({
1001
+ module: "config",
1002
+ field: "selfHealing.mode",
1003
+ message: `Invalid self-healing mode: ${sh.mode}. Must be one of: ${VALID_HEAL_MODES.join(", ")}`,
1004
+ severity: "error"
1005
+ });
1006
+ }
1007
+ if (sh.maxIterations && (typeof sh.maxIterations !== "number" || sh.maxIterations < 1)) {
1008
+ errors.push({
1009
+ module: "config",
1010
+ field: "selfHealing.maxIterations",
1011
+ message: "maxIterations must be a positive number",
1012
+ severity: "error"
1013
+ });
1014
+ }
1015
+ }
1016
+ return errors;
1017
+ }
1018
+
1019
+ // src/pipeline/index.ts
1020
+ var ALL_STEPS = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
1021
+ function createPipeline(config) {
1022
+ return {
1023
+ async run(steps) {
1024
+ const startTime = Date.now();
1025
+ const activeSteps = steps || config.steps || ALL_STEPS;
1026
+ const result = {
1027
+ modules: [],
1028
+ erDiagrams: /* @__PURE__ */ new Map(),
1029
+ chainPlans: /* @__PURE__ */ new Map(),
1030
+ generatedFiles: [],
1031
+ validationErrors: [],
1032
+ duration: 0
1033
+ };
1034
+ if (activeSteps.includes("scan")) {
1035
+ const backendRoot = path5.resolve(config.backendRoot);
1036
+ const modelsDir = path5.join(backendRoot, "models");
1037
+ if (fs4.existsSync(modelsDir)) {
1038
+ const dirs = fs4.readdirSync(modelsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1039
+ const moduleFilter = config.modules;
1040
+ for (const dir of dirs) {
1041
+ if (moduleFilter && !moduleFilter.includes(dir)) continue;
1042
+ result.modules.push(dir);
1043
+ }
1044
+ if (result.modules.length === 0) {
1045
+ result.modules.push("default");
1046
+ } else {
1047
+ const rootFiles = fs4.readdirSync(modelsDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts");
1048
+ if (rootFiles.length > 0) {
1049
+ result.modules.unshift("default");
1050
+ }
1051
+ }
1052
+ }
1053
+ }
1054
+ const resolveModelDir = (backendRoot, mod) => mod === "default" ? path5.join(backendRoot, "models") : path5.join(backendRoot, "models", mod);
1055
+ const resolveControllerDir = (backendRoot, mod) => mod === "default" ? path5.join(backendRoot, "controllers") : path5.join(backendRoot, "controllers", mod);
1056
+ if (activeSteps.includes("er-diagram")) {
1057
+ const erGen = createERDiagramGenerator();
1058
+ const backendRoot = path5.resolve(config.backendRoot);
1059
+ for (const mod of result.modules) {
1060
+ const modelDir = resolveModelDir(backendRoot, mod);
1061
+ const tables = fs4.existsSync(modelDir) ? parseModuleModels(modelDir) : [];
1062
+ const relations = [];
1063
+ const assocFile = path5.join(modelDir, "associations.ts");
1064
+ if (fs4.existsSync(assocFile)) {
1065
+ relations.push(...parseAssociationFile(assocFile));
1066
+ }
1067
+ if (fs4.existsSync(modelDir)) {
1068
+ const modelFiles = fs4.readdirSync(modelDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts" && f !== "associations.ts");
1069
+ for (const file of modelFiles) {
1070
+ try {
1071
+ const embedded = parseAssociationFile(path5.join(modelDir, file));
1072
+ relations.push(...embedded);
1073
+ } catch {
1074
+ }
1075
+ }
1076
+ }
1077
+ const erResult = erGen.generate(tables, relations);
1078
+ result.erDiagrams.set(mod, erResult);
1079
+ }
1080
+ }
1081
+ if (activeSteps.includes("api-chain")) {
1082
+ const chainAnalyzer = createApiChainAnalyzer();
1083
+ const backendRoot = path5.resolve(config.backendRoot);
1084
+ for (const mod of result.modules) {
1085
+ const controllerDir = resolveControllerDir(backendRoot, mod);
1086
+ const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
1087
+ const analysis = chainAnalyzer.analyze(endpoints);
1088
+ analysis.moduleName = mod;
1089
+ if (analysis.hasCycles) {
1090
+ for (const warning of analysis.cycleWarnings) {
1091
+ result.validationErrors.push({
1092
+ module: mod,
1093
+ field: "api-chain",
1094
+ message: warning,
1095
+ severity: "warning"
1096
+ });
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+ if (activeSteps.includes("plan")) {
1102
+ const backendRoot = path5.resolve(config.backendRoot);
1103
+ const chainAnalyzer = createApiChainAnalyzer();
1104
+ for (const mod of result.modules) {
1105
+ const controllerDir = resolveControllerDir(backendRoot, mod);
1106
+ const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
1107
+ const analysis = chainAnalyzer.analyze(endpoints);
1108
+ const topoOrder = topologicalSort(analysis.dag);
1109
+ const chains = generateChainPlan(mod, endpoints, topoOrder);
1110
+ result.chainPlans.set(mod, chains);
1111
+ }
1112
+ }
1113
+ if (activeSteps.includes("codegen")) {
1114
+ const testGen = createTestCodeGenerator();
1115
+ const outDir = config.outDir || "./opencroc-output";
1116
+ for (const [_mod, plan] of result.chainPlans) {
1117
+ const files = testGen.generate(plan.chains);
1118
+ for (const file of files) {
1119
+ file.filePath = path5.join(outDir, file.filePath);
1120
+ }
1121
+ result.generatedFiles.push(...files);
1122
+ }
1123
+ }
1124
+ if (activeSteps.includes("validate")) {
1125
+ const configErrors = validateConfig(config);
1126
+ result.validationErrors.push(...configErrors);
1127
+ }
1128
+ result.duration = Date.now() - startTime;
1129
+ return result;
1130
+ }
1131
+ };
1132
+ }
1133
+ function generateChainPlan(moduleName, endpoints, _topoOrder) {
1134
+ const groups = /* @__PURE__ */ new Map();
1135
+ for (const ep of endpoints) {
1136
+ const segments = ep.path.split("/").filter((s) => s && !s.startsWith(":"));
1137
+ const resource = segments[segments.length - 1] || "default";
1138
+ if (!groups.has(resource)) groups.set(resource, []);
1139
+ groups.get(resource).push(ep);
1140
+ }
1141
+ const chains = [];
1142
+ let totalSteps = 0;
1143
+ for (const [resource, eps] of groups) {
1144
+ const steps = eps.map((ep, i) => ({
1145
+ order: i + 1,
1146
+ action: ep.method,
1147
+ endpoint: ep,
1148
+ description: ep.description || `${ep.method} ${ep.path}`,
1149
+ assertions: []
1150
+ }));
1151
+ chains.push({ name: `${resource} CRUD chain`, module: moduleName, steps });
1152
+ totalSteps += steps.length;
1153
+ }
1154
+ return { chains, totalSteps };
1155
+ }
1156
+
1157
+ // src/index.ts
1158
+ init_controller_parser();
1159
+
57
1160
  // src/generators/mock-data-generator.ts
1161
+ init_esm_shims();
1162
+ function randomInt(min, max) {
1163
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1164
+ }
1165
+ function randomString(prefix, fieldName) {
1166
+ const ts = Date.now().toString(36);
1167
+ const rand = Math.random().toString(36).slice(2, 6);
1168
+ return `${prefix}${fieldName}_${ts}_${rand}`;
1169
+ }
1170
+ function generateUUID() {
1171
+ const hex = () => Math.random().toString(16).slice(2, 6);
1172
+ return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${(8 + randomInt(0, 3)).toString(16)}${hex().slice(1)}-${hex()}${hex()}${hex()}`;
1173
+ }
1174
+ function generateFieldValue(fieldName, fieldType, isForeignKey, parentTable) {
1175
+ const upper = fieldType.toUpperCase();
1176
+ if (isForeignKey && parentTable) {
1177
+ return `{{parentRecordIds.${parentTable}}}`;
1178
+ }
1179
+ if (upper.startsWith("STRING") || upper === "TEXT") return randomString("test_", fieldName);
1180
+ if (upper === "BIGINT" || upper === "INTEGER") return randomInt(1, 999999);
1181
+ if (upper === "BOOLEAN") return true;
1182
+ if (upper.startsWith("DATE") || upper === "NOW") return (/* @__PURE__ */ new Date()).toISOString();
1183
+ if (upper === "UUID") return generateUUID();
1184
+ if (upper.startsWith("ENUM")) return "ACTIVE";
1185
+ if (upper === "JSON" || upper === "JSONB") return {};
1186
+ if (upper === "FLOAT" || upper === "DOUBLE" || upper === "DECIMAL") return Math.round(Math.random() * 1e4) / 100;
1187
+ return randomString("val_", fieldName);
1188
+ }
58
1189
  function createMockDataGenerator() {
59
1190
  return {
60
- generateForTable(_schema) {
61
- throw new Error("Mock data generator not yet implemented");
1191
+ generateForTable(schema) {
1192
+ const record = {};
1193
+ const ts = Date.now().toString(36);
1194
+ const rand = Math.random().toString(36).slice(2, 6);
1195
+ for (const field of schema.fields) {
1196
+ if (field.primaryKey) continue;
1197
+ if (field.defaultValue !== void 0) continue;
1198
+ const isForeignKey = field.name.endsWith("_id") && !field.primaryKey;
1199
+ const parentTable = isForeignKey ? field.name.replace(/_id$/, "") : void 0;
1200
+ let value = generateFieldValue(field.name, field.type, isForeignKey, parentTable);
1201
+ if (field.unique && typeof value === "string") {
1202
+ value = `${value}__e2e_test_${ts}_${rand}`;
1203
+ }
1204
+ record[field.name] = value;
1205
+ }
1206
+ return record;
62
1207
  },
63
- generateForTables(_schemas) {
64
- throw new Error("Mock data generator not yet implemented");
1208
+ generateForTables(schemas) {
1209
+ const result = /* @__PURE__ */ new Map();
1210
+ for (const schema of schemas) {
1211
+ const record = this.generateForTable(schema);
1212
+ result.set(schema.tableName, [record]);
1213
+ }
1214
+ return result;
65
1215
  }
66
1216
  };
67
1217
  }
68
1218
 
69
- // src/generators/er-diagram-generator.ts
70
- function createERDiagramGenerator() {
1219
+ // src/analyzers/impact-reporter.ts
1220
+ init_esm_shims();
1221
+ var MAX_BFS_DEPTH = 5;
1222
+ function extractTablesFromErrorChain(errorChainPath) {
1223
+ const segments = errorChainPath.split("\u2192").map((s) => s.trim());
1224
+ return segments.filter((s) => !s.includes("/") && !s.includes(" ") && s.includes("_"));
1225
+ }
1226
+ function buildTableAdjacency(relations) {
1227
+ const adj = /* @__PURE__ */ new Map();
1228
+ for (const rel of relations) {
1229
+ if (!adj.has(rel.sourceTable)) adj.set(rel.sourceTable, /* @__PURE__ */ new Set());
1230
+ if (!adj.has(rel.targetTable)) adj.set(rel.targetTable, /* @__PURE__ */ new Set());
1231
+ adj.get(rel.sourceTable).add(rel.targetTable);
1232
+ adj.get(rel.targetTable).add(rel.sourceTable);
1233
+ }
1234
+ return adj;
1235
+ }
1236
+ function bfsTraversal(seedTables, adjacency, maxDepth = MAX_BFS_DEPTH) {
1237
+ const visited = /* @__PURE__ */ new Set();
1238
+ const queue = [];
1239
+ for (const t of seedTables) {
1240
+ if (adjacency.has(t)) {
1241
+ queue.push({ table: t, depth: 0 });
1242
+ visited.add(t);
1243
+ }
1244
+ }
1245
+ while (queue.length > 0) {
1246
+ const { table, depth } = queue.shift();
1247
+ if (depth >= maxDepth) continue;
1248
+ for (const neighbor of adjacency.get(table) || []) {
1249
+ if (!visited.has(neighbor)) {
1250
+ visited.add(neighbor);
1251
+ queue.push({ table: neighbor, depth: depth + 1 });
1252
+ }
1253
+ }
1254
+ }
1255
+ return Array.from(visited);
1256
+ }
1257
+ function findAffectedEndpoints(tables, analysisResults) {
1258
+ const tableSet = new Set(tables);
1259
+ const affected = [];
1260
+ for (const result of analysisResults) {
1261
+ for (const ep of result.endpoints) {
1262
+ if (ep.relatedTables.some((t) => tableSet.has(t))) {
1263
+ affected.push(ep);
1264
+ }
1265
+ }
1266
+ }
1267
+ return affected;
1268
+ }
1269
+ function generateMermaidDiagram(seedTables, affectedTables, relations) {
1270
+ const relevantTables = /* @__PURE__ */ new Set([...seedTables, ...affectedTables]);
1271
+ const lines = ["flowchart TD"];
1272
+ const seedSet = new Set(seedTables);
1273
+ for (const t of relevantTables) {
1274
+ const label = seedSet.has(t) ? `${t}:::error` : t;
1275
+ lines.push(` ${sanitizeId(t)}["${label}"]`);
1276
+ }
1277
+ for (const rel of relations) {
1278
+ if (relevantTables.has(rel.sourceTable) && relevantTables.has(rel.targetTable)) {
1279
+ const arrow = rel.isCrossModule ? "-.->" : "-->";
1280
+ lines.push(` ${sanitizeId(rel.sourceTable)} ${arrow}|${rel.targetField}| ${sanitizeId(rel.targetTable)}`);
1281
+ }
1282
+ }
1283
+ lines.push(" classDef error fill:#f96,stroke:#333,stroke-width:2px");
1284
+ return lines.join("\n");
1285
+ }
1286
+ function sanitizeId(name) {
1287
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
1288
+ }
1289
+ function createImpactReporter() {
71
1290
  return {
72
- generate(_tables, _relations) {
73
- throw new Error("ER diagram generator not yet implemented");
1291
+ analyze(failures, erDiagrams, analysisResults) {
1292
+ const allRelations = [];
1293
+ for (const er of erDiagrams.values()) {
1294
+ allRelations.push(...er.relations);
1295
+ }
1296
+ const seedTables = [];
1297
+ for (const failure of failures) {
1298
+ if (failure.errorChainPath) {
1299
+ seedTables.push(...extractTablesFromErrorChain(failure.errorChainPath));
1300
+ }
1301
+ }
1302
+ const adjacency = buildTableAdjacency(allRelations);
1303
+ const affectedTables = bfsTraversal(seedTables, adjacency);
1304
+ const affectedEndpoints = findAffectedEndpoints(affectedTables, analysisResults);
1305
+ const affectedModules = [...new Set(analysisResults.filter((r) => r.endpoints.some((ep) => affectedEndpoints.includes(ep))).map((r) => r.moduleName))];
1306
+ const affectedChains = failures.map((f) => f.chain);
1307
+ const mermaidText = generateMermaidDiagram(seedTables, affectedTables, allRelations);
1308
+ const count = affectedEndpoints.length;
1309
+ const severity = count > 10 ? "critical" : count > 5 ? "high" : count > 2 ? "medium" : "low";
1310
+ return {
1311
+ affectedModules,
1312
+ affectedChains,
1313
+ affectedEndpoints,
1314
+ affectedTables,
1315
+ severity,
1316
+ mermaidText
1317
+ };
74
1318
  }
75
1319
  };
76
1320
  }
77
1321
 
78
- // src/analyzers/api-chain-analyzer.ts
79
- function createApiChainAnalyzer() {
1322
+ // src/self-healing/index.ts
1323
+ init_esm_shims();
1324
+
1325
+ // src/llm/index.ts
1326
+ init_esm_shims();
1327
+
1328
+ // src/llm/openai.ts
1329
+ init_esm_shims();
1330
+ var DEFAULT_MODELS = {
1331
+ openai: "gpt-4o-mini",
1332
+ zhipu: "glm-4"
1333
+ };
1334
+ var DEFAULT_BASE_URLS = {
1335
+ openai: "https://api.openai.com/v1",
1336
+ zhipu: "https://open.bigmodel.cn/api/paas/v4"
1337
+ };
1338
+ function createOpenAIProvider(config) {
1339
+ const provider = config.provider === "zhipu" ? "zhipu" : "openai";
1340
+ const baseUrl = config.baseUrl || DEFAULT_BASE_URLS[provider];
1341
+ const model = config.model || DEFAULT_MODELS[provider];
1342
+ const maxTokens = config.maxTokens || 2048;
1343
+ const temperature = config.temperature ?? 0.3;
1344
+ if (!config.apiKey) {
1345
+ throw new Error(
1346
+ `API key is required for ${provider}. Set it in config or via OPENCROC_LLM_API_KEY env variable.`
1347
+ );
1348
+ }
80
1349
  return {
81
- analyze(_endpoints) {
82
- throw new Error("API chain analyzer not yet implemented");
1350
+ name: provider,
1351
+ async chat(messages) {
1352
+ const url = `${baseUrl}/chat/completions`;
1353
+ const response = await fetch(url, {
1354
+ method: "POST",
1355
+ headers: {
1356
+ "Content-Type": "application/json",
1357
+ "Authorization": `Bearer ${config.apiKey}`
1358
+ },
1359
+ body: JSON.stringify({
1360
+ model,
1361
+ messages,
1362
+ max_tokens: maxTokens,
1363
+ temperature
1364
+ })
1365
+ });
1366
+ if (!response.ok) {
1367
+ const errorText = await response.text().catch(() => "unknown error");
1368
+ throw new Error(`LLM API error (${response.status}): ${errorText}`);
1369
+ }
1370
+ const data = await response.json();
1371
+ const content = data.choices?.[0]?.message?.content;
1372
+ if (!content) {
1373
+ throw new Error("LLM returned empty response");
1374
+ }
1375
+ return content;
1376
+ },
1377
+ estimateTokens(text) {
1378
+ const cjkChars = (text.match(/[\u4e00-\u9fff\u3000-\u303f]/g) || []).length;
1379
+ const otherChars = text.length - cjkChars;
1380
+ return Math.ceil(otherChars / 4 + cjkChars / 2);
83
1381
  }
84
1382
  };
85
1383
  }
86
1384
 
87
- // src/analyzers/impact-reporter.ts
88
- function createImpactReporter() {
1385
+ // src/llm/ollama.ts
1386
+ init_esm_shims();
1387
+ function createOllamaProvider(config) {
1388
+ const baseUrl = config.baseUrl || "http://localhost:11434";
1389
+ const model = config.model || "llama3";
89
1390
  return {
90
- async analyze(_failedEndpoints) {
91
- throw new Error("Impact reporter not yet implemented");
1391
+ name: "ollama",
1392
+ async chat(messages) {
1393
+ const url = `${baseUrl}/api/chat`;
1394
+ const response = await fetch(url, {
1395
+ method: "POST",
1396
+ headers: { "Content-Type": "application/json" },
1397
+ body: JSON.stringify({
1398
+ model,
1399
+ messages,
1400
+ stream: false
1401
+ })
1402
+ });
1403
+ if (!response.ok) {
1404
+ const errorText = await response.text().catch(() => "unknown error");
1405
+ throw new Error(`Ollama API error (${response.status}): ${errorText}`);
1406
+ }
1407
+ const data = await response.json();
1408
+ const content = data.message?.content;
1409
+ if (!content) {
1410
+ throw new Error("Ollama returned empty response");
1411
+ }
1412
+ return content;
1413
+ },
1414
+ estimateTokens(text) {
1415
+ const cjkChars = (text.match(/[\u4e00-\u9fff\u3000-\u303f]/g) || []).length;
1416
+ const otherChars = text.length - cjkChars;
1417
+ return Math.ceil(otherChars / 4 + cjkChars / 2);
92
1418
  }
93
1419
  };
94
1420
  }
95
1421
 
96
- // src/validators/config-validator.ts
97
- function validateConfig(_config) {
98
- return [];
1422
+ // src/llm/index.ts
1423
+ function createLlmProvider(config) {
1424
+ const resolved = {
1425
+ ...config,
1426
+ apiKey: config.apiKey || process.env.OPENCROC_LLM_API_KEY
1427
+ };
1428
+ switch (config.provider) {
1429
+ case "openai":
1430
+ case "zhipu":
1431
+ return createOpenAIProvider(resolved);
1432
+ case "ollama":
1433
+ return createOllamaProvider(resolved);
1434
+ default:
1435
+ throw new Error(
1436
+ `Unknown LLM provider: "${config.provider}". Available: openai, zhipu, ollama`
1437
+ );
1438
+ }
1439
+ }
1440
+ function createTokenTracker(provider) {
1441
+ let total = 0;
1442
+ return {
1443
+ track(text) {
1444
+ total += provider.estimateTokens(text);
1445
+ },
1446
+ trackChat(messages, response) {
1447
+ for (const msg of messages) {
1448
+ total += provider.estimateTokens(msg.content);
1449
+ }
1450
+ total += provider.estimateTokens(response);
1451
+ },
1452
+ get total() {
1453
+ return total;
1454
+ },
1455
+ reset() {
1456
+ total = 0;
1457
+ }
1458
+ };
99
1459
  }
1460
+ var SYSTEM_PROMPTS = {
1461
+ failureAnalysis: `You are an expert test failure analyst for an E2E testing framework.
1462
+ Given a test failure error message and its context, analyze the root cause and suggest a fix.
1463
+ Respond in JSON format: { "rootCause": string, "category": string, "suggestedFix": string, "confidence": number }
1464
+ Categories: backend-5xx, timeout, endpoint-not-found, data-constraint, network, frontend-render, test-script, unknown.`,
1465
+ chainPlanning: `You are an API test chain planner.
1466
+ Given a list of API endpoints and their dependencies, generate an optimal test execution order.
1467
+ Consider data dependencies, authentication requirements, and cleanup steps.
1468
+ Respond in JSON format: { "chains": [{ "name": string, "steps": [{ "endpoint": string, "method": string, "description": string }] }] }`
1469
+ };
100
1470
 
101
1471
  // src/self-healing/index.ts
102
- function createSelfHealingLoop(_config) {
1472
+ function categorizeFailure(errorMessage) {
1473
+ const msg = errorMessage.toLowerCase();
1474
+ if (/5\d{2}|internal server error/.test(msg))
1475
+ return { category: "backend-5xx", confidence: 0.9 };
1476
+ if (/timeout|timed?\s*out/.test(msg))
1477
+ return { category: "timeout", confidence: 0.8 };
1478
+ if (/404|not found/.test(msg))
1479
+ return { category: "endpoint-not-found", confidence: 0.85 };
1480
+ if (/4[0-2]\d|validation|constraint/.test(msg))
1481
+ return { category: "data-constraint", confidence: 0.75 };
1482
+ if (/econnrefused|enotfound|network/.test(msg))
1483
+ return { category: "network", confidence: 0.9 };
1484
+ if (/selector|locator|element/.test(msg))
1485
+ return { category: "frontend-render", confidence: 0.7 };
1486
+ if (/storage\s*state|auth|login/.test(msg))
1487
+ return { category: "test-script", confidence: 0.8 };
1488
+ return { category: "unknown", confidence: 0.5 };
1489
+ }
1490
+ async function analyzeFailureWithLLM(errorMessage, llm) {
1491
+ const heuristic = categorizeFailure(errorMessage);
1492
+ if (!llm) {
1493
+ return {
1494
+ rootCause: errorMessage,
1495
+ category: heuristic.category,
1496
+ suggestedFix: "",
1497
+ confidence: heuristic.confidence
1498
+ };
1499
+ }
1500
+ try {
1501
+ const response = await llm.chat([
1502
+ { role: "system", content: SYSTEM_PROMPTS.failureAnalysis },
1503
+ { role: "user", content: `Analyze this test failure:
1504
+
1505
+ ${errorMessage}` }
1506
+ ]);
1507
+ const parsed = JSON.parse(response);
1508
+ return {
1509
+ rootCause: parsed.rootCause || errorMessage,
1510
+ category: parsed.category || heuristic.category,
1511
+ suggestedFix: parsed.suggestedFix || "",
1512
+ confidence: parsed.confidence || heuristic.confidence
1513
+ };
1514
+ } catch {
1515
+ return {
1516
+ rootCause: errorMessage,
1517
+ category: heuristic.category,
1518
+ suggestedFix: "",
1519
+ confidence: heuristic.confidence
1520
+ };
1521
+ }
1522
+ }
1523
+ async function attemptConfigFix(_testResultsDir, _mode, _llm) {
1524
+ return {
1525
+ success: false,
1526
+ scope: "config-only",
1527
+ fixedItems: [],
1528
+ rolledBack: false
1529
+ };
1530
+ }
1531
+ function createSelfHealingLoop(config, llm) {
1532
+ return {
1533
+ async run(testResultsDir) {
1534
+ const maxIterations = config.maxIterations || 3;
1535
+ const mode = config.mode || "config-only";
1536
+ const fixed = [];
1537
+ const remaining = [];
1538
+ let iterations = 0;
1539
+ let totalTokensUsed = 0;
1540
+ for (let i = 0; i < maxIterations; i++) {
1541
+ iterations = i + 1;
1542
+ const outcome = await attemptConfigFix(testResultsDir, mode, llm);
1543
+ if (outcome.success) {
1544
+ fixed.push(...outcome.fixedItems);
1545
+ } else {
1546
+ remaining.push(`iteration-${i + 1}: no fix applied`);
1547
+ }
1548
+ if (llm) {
1549
+ totalTokensUsed += llm.estimateTokens(`iteration-${i + 1}`);
1550
+ }
1551
+ if (outcome.success && outcome.fixedItems.length > 0) break;
1552
+ }
1553
+ return {
1554
+ iterations,
1555
+ fixed,
1556
+ remaining,
1557
+ totalTokensUsed
1558
+ };
1559
+ }
1560
+ };
1561
+ }
1562
+
1563
+ // src/adapters/sequelize.ts
1564
+ init_esm_shims();
1565
+ init_controller_parser();
1566
+ function createSequelizeAdapter() {
1567
+ return {
1568
+ name: "sequelize",
1569
+ async parseModels(dir) {
1570
+ return parseModuleModels(dir);
1571
+ },
1572
+ async parseAssociations(file) {
1573
+ return parseAssociationFile(file);
1574
+ },
1575
+ async parseControllers(dir) {
1576
+ const endpoints = parseControllerDirectory(dir);
1577
+ return endpoints.map((ep) => ({
1578
+ method: ep.method,
1579
+ path: ep.path,
1580
+ handler: "",
1581
+ controllerClass: ""
1582
+ }));
1583
+ }
1584
+ };
1585
+ }
1586
+
1587
+ // src/adapters/typeorm.ts
1588
+ init_esm_shims();
1589
+ import * as fs5 from "fs";
1590
+ import * as path6 from "path";
1591
+ import {
1592
+ Project as Project4
1593
+ } from "ts-morph";
1594
+ var TYPEORM_TYPE_MAP = {
1595
+ "PrimaryGeneratedColumn": "BIGINT",
1596
+ "PrimaryColumn": "BIGINT",
1597
+ "CreateDateColumn": "DATE",
1598
+ "UpdateDateColumn": "DATE",
1599
+ "DeleteDateColumn": "DATE",
1600
+ "VersionColumn": "INTEGER"
1601
+ };
1602
+ var TYPEORM_COLUMN_TYPE_MAP = {
1603
+ "varchar": "STRING",
1604
+ "text": "TEXT",
1605
+ "int": "INTEGER",
1606
+ "integer": "INTEGER",
1607
+ "bigint": "BIGINT",
1608
+ "float": "FLOAT",
1609
+ "double": "DOUBLE",
1610
+ "decimal": "DECIMAL",
1611
+ "boolean": "BOOLEAN",
1612
+ "bool": "BOOLEAN",
1613
+ "date": "DATEONLY",
1614
+ "datetime": "DATE",
1615
+ "timestamp": "DATE",
1616
+ "json": "JSON",
1617
+ "jsonb": "JSONB",
1618
+ "enum": "ENUM",
1619
+ "uuid": "UUID"
1620
+ };
1621
+ function tsTypeToFieldType(tsType) {
1622
+ const t = tsType.toLowerCase().trim();
1623
+ if (t === "string") return "STRING";
1624
+ if (t === "number") return "INTEGER";
1625
+ if (t === "boolean") return "BOOLEAN";
1626
+ if (t === "date") return "DATE";
1627
+ return "STRING";
1628
+ }
1629
+ function classNameToTableName2(name) {
1630
+ return name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
1631
+ }
1632
+ function extractDecoratorStringArg(decoratorText) {
1633
+ const match = decoratorText.match(/\(\s*['"]([^'"]+)['"]\s*\)/);
1634
+ return match?.[1];
1635
+ }
1636
+ function extractDecoratorObjectArg(decoratorText) {
1637
+ const result = {};
1638
+ const objMatch = decoratorText.match(/\(\s*\{([^}]*)\}\s*\)/);
1639
+ if (!objMatch) return result;
1640
+ const body = objMatch[1];
1641
+ const pairs = body.matchAll(/(\w+)\s*:\s*['"]([^'"]*)['"]/g);
1642
+ for (const pair of pairs) {
1643
+ result[pair[1]] = pair[2];
1644
+ }
1645
+ const boolPairs = body.matchAll(/(\w+)\s*:\s*(true|false)/g);
1646
+ for (const pair of boolPairs) {
1647
+ result[pair[1]] = pair[2];
1648
+ }
1649
+ return result;
1650
+ }
1651
+ function parseTypeORMFile(filePath) {
1652
+ const absolutePath = path6.resolve(filePath);
1653
+ if (!fs5.existsSync(absolutePath)) return null;
1654
+ const project = new Project4({ compilerOptions: { strict: false } });
1655
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
1656
+ const classes = sourceFile.getClasses();
1657
+ for (const cls of classes) {
1658
+ const entityDecorator = cls.getDecorator("Entity");
1659
+ if (!entityDecorator) continue;
1660
+ const tableName = extractDecoratorStringArg(entityDecorator.getText()) || extractDecoratorObjectArg(entityDecorator.getText()).name || classNameToTableName2(cls.getName() || "unknown");
1661
+ const fields = extractTypeORMFields(cls);
1662
+ return { tableName, className: cls.getName(), fields };
1663
+ }
1664
+ return null;
1665
+ }
1666
+ function extractTypeORMFields(cls) {
1667
+ const fields = [];
1668
+ for (const prop of cls.getProperties()) {
1669
+ const field = parseTypeORMProperty(prop);
1670
+ if (field) fields.push(field);
1671
+ }
1672
+ return fields;
1673
+ }
1674
+ function parseTypeORMProperty(prop) {
1675
+ const decorators = prop.getDecorators();
1676
+ if (decorators.length === 0) return null;
1677
+ const name = prop.getName();
1678
+ let type = "STRING";
1679
+ let primaryKey = false;
1680
+ let allowNull = true;
1681
+ let unique = false;
1682
+ for (const dec of decorators) {
1683
+ const decName = dec.getName();
1684
+ const decText = dec.getText();
1685
+ if (decName === "PrimaryGeneratedColumn" || decName === "PrimaryColumn") {
1686
+ primaryKey = true;
1687
+ type = TYPEORM_TYPE_MAP[decName] || "BIGINT";
1688
+ allowNull = false;
1689
+ const argType = extractDecoratorStringArg(decText);
1690
+ if (argType === "uuid") type = "UUID";
1691
+ if (argType === "increment") type = "BIGINT";
1692
+ }
1693
+ if (decName === "Column") {
1694
+ const objArgs = extractDecoratorObjectArg(decText);
1695
+ if (objArgs.type && TYPEORM_COLUMN_TYPE_MAP[objArgs.type]) {
1696
+ type = TYPEORM_COLUMN_TYPE_MAP[objArgs.type];
1697
+ } else {
1698
+ const simpleType = extractDecoratorStringArg(decText);
1699
+ if (simpleType && TYPEORM_COLUMN_TYPE_MAP[simpleType]) {
1700
+ type = TYPEORM_COLUMN_TYPE_MAP[simpleType];
1701
+ }
1702
+ }
1703
+ if (objArgs.nullable === "false") allowNull = false;
1704
+ if (objArgs.unique === "true") unique = true;
1705
+ if (type === "STRING") {
1706
+ const tsType = prop.getType().getText();
1707
+ type = tsTypeToFieldType(tsType);
1708
+ }
1709
+ }
1710
+ if (decName in TYPEORM_TYPE_MAP) {
1711
+ type = TYPEORM_TYPE_MAP[decName];
1712
+ }
1713
+ if (decName === "CreateDateColumn" || decName === "UpdateDateColumn" || decName === "DeleteDateColumn") {
1714
+ allowNull = true;
1715
+ }
1716
+ }
1717
+ const recognizedDecorators = [
1718
+ "Column",
1719
+ "PrimaryGeneratedColumn",
1720
+ "PrimaryColumn",
1721
+ "CreateDateColumn",
1722
+ "UpdateDateColumn",
1723
+ "DeleteDateColumn",
1724
+ "VersionColumn",
1725
+ "ManyToOne",
1726
+ "OneToMany",
1727
+ "OneToOne",
1728
+ "ManyToMany",
1729
+ "JoinColumn",
1730
+ "JoinTable"
1731
+ ];
1732
+ const hasRecognized = decorators.some((d) => recognizedDecorators.includes(d.getName()));
1733
+ if (!hasRecognized) return null;
1734
+ const isRelationOnly = decorators.every(
1735
+ (d) => ["ManyToOne", "OneToMany", "OneToOne", "ManyToMany", "JoinColumn", "JoinTable"].includes(d.getName())
1736
+ );
1737
+ if (isRelationOnly) return null;
1738
+ return { name, type, allowNull, primaryKey, unique };
1739
+ }
1740
+ function parseTypeORMAssociations(filePath) {
1741
+ const absolutePath = path6.resolve(filePath);
1742
+ if (!fs5.existsSync(absolutePath)) return [];
1743
+ const project = new Project4({ compilerOptions: { strict: false } });
1744
+ const sourceFile = project.addSourceFileAtPath(absolutePath);
1745
+ const relations = [];
1746
+ for (const cls of sourceFile.getClasses()) {
1747
+ const entityDecorator = cls.getDecorator("Entity");
1748
+ if (!entityDecorator) continue;
1749
+ const sourceTable = extractDecoratorStringArg(entityDecorator.getText()) || classNameToTableName2(cls.getName() || "unknown");
1750
+ for (const prop of cls.getProperties()) {
1751
+ const rel = extractRelationFromProperty(prop, sourceTable);
1752
+ if (rel) relations.push(rel);
1753
+ }
1754
+ }
1755
+ return relations;
1756
+ }
1757
+ function extractRelationFromProperty(prop, sourceTable) {
1758
+ const decorators = prop.getDecorators();
1759
+ for (const dec of decorators) {
1760
+ const decName = dec.getName();
1761
+ const decText = dec.getText();
1762
+ if (decName === "ManyToOne") {
1763
+ const targetClass = extractRelationTarget(decText);
1764
+ if (!targetClass) continue;
1765
+ const targetTable = classNameToTableName2(targetClass);
1766
+ const fkField = findJoinColumnField(decorators) || `${prop.getName()}_id`;
1767
+ return {
1768
+ sourceTable,
1769
+ sourceField: fkField,
1770
+ targetTable,
1771
+ targetField: "id",
1772
+ cardinality: "N:1"
1773
+ };
1774
+ }
1775
+ if (decName === "OneToMany") {
1776
+ const targetClass = extractRelationTarget(decText);
1777
+ if (!targetClass) continue;
1778
+ const targetTable = classNameToTableName2(targetClass);
1779
+ return {
1780
+ sourceTable,
1781
+ sourceField: "id",
1782
+ targetTable,
1783
+ targetField: `${classNameToTableName2(sourceTable)}_id`,
1784
+ cardinality: "1:N"
1785
+ };
1786
+ }
1787
+ if (decName === "OneToOne") {
1788
+ const targetClass = extractRelationTarget(decText);
1789
+ if (!targetClass) continue;
1790
+ const targetTable = classNameToTableName2(targetClass);
1791
+ return {
1792
+ sourceTable,
1793
+ sourceField: "id",
1794
+ targetTable,
1795
+ targetField: `${classNameToTableName2(sourceTable)}_id`,
1796
+ cardinality: "1:1"
1797
+ };
1798
+ }
1799
+ }
1800
+ return null;
1801
+ }
1802
+ function extractRelationTarget(decoratorText) {
1803
+ const match = decoratorText.match(/\(\s*(?:\(\)\s*=>|type\s*=>|\w+\s*=>)\s*(\w+)/);
1804
+ return match?.[1] || null;
1805
+ }
1806
+ function findJoinColumnField(decorators) {
1807
+ for (const dec of decorators) {
1808
+ if (dec.getName() === "JoinColumn") {
1809
+ const args = extractDecoratorObjectArg(dec.getText());
1810
+ if (args.name) return args.name;
1811
+ }
1812
+ }
1813
+ return null;
1814
+ }
1815
+ function parseTypeORMDirectory(dir) {
1816
+ const absoluteDir = path6.resolve(dir);
1817
+ if (!fs5.existsSync(absoluteDir)) return [];
1818
+ const files = fs5.readdirSync(absoluteDir).filter(
1819
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
1820
+ );
1821
+ const schemas = [];
1822
+ for (const file of files) {
1823
+ try {
1824
+ const schema = parseTypeORMFile(path6.join(absoluteDir, file));
1825
+ if (schema) schemas.push(schema);
1826
+ } catch {
1827
+ }
1828
+ }
1829
+ return schemas;
1830
+ }
1831
+ function parseTypeORMAssociationsFromDir(dir) {
1832
+ const absoluteDir = path6.resolve(dir);
1833
+ if (!fs5.existsSync(absoluteDir)) return [];
1834
+ const files = fs5.readdirSync(absoluteDir).filter(
1835
+ (f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
1836
+ );
1837
+ const relations = [];
1838
+ for (const file of files) {
1839
+ try {
1840
+ relations.push(...parseTypeORMAssociations(path6.join(absoluteDir, file)));
1841
+ } catch {
1842
+ }
1843
+ }
1844
+ return relations;
1845
+ }
1846
+ function createTypeORMAdapter() {
103
1847
  return {
104
- async run(_testResultsDir) {
105
- throw new Error("Self-healing loop not yet implemented");
1848
+ name: "typeorm",
1849
+ async parseModels(dir) {
1850
+ return parseTypeORMDirectory(dir);
1851
+ },
1852
+ async parseAssociations(file) {
1853
+ const dir = path6.dirname(file);
1854
+ return parseTypeORMAssociationsFromDir(dir);
1855
+ },
1856
+ async parseControllers(dir) {
1857
+ const { parseControllerDirectory: parseControllerDirectory2 } = await Promise.resolve().then(() => (init_controller_parser(), controller_parser_exports));
1858
+ const endpoints = parseControllerDirectory2(dir);
1859
+ return endpoints.map((ep) => ({
1860
+ method: ep.method,
1861
+ path: ep.path,
1862
+ handler: "",
1863
+ controllerClass: ""
1864
+ }));
106
1865
  }
107
1866
  };
108
1867
  }
1868
+
1869
+ // src/adapters/prisma.ts
1870
+ init_esm_shims();
1871
+ import * as fs6 from "fs";
1872
+ import * as path7 from "path";
1873
+ var PRISMA_TYPE_MAP = {
1874
+ "String": "STRING",
1875
+ "Int": "INTEGER",
1876
+ "BigInt": "BIGINT",
1877
+ "Float": "FLOAT",
1878
+ "Decimal": "DECIMAL",
1879
+ "Boolean": "BOOLEAN",
1880
+ "DateTime": "DATE",
1881
+ "Json": "JSON",
1882
+ "Bytes": "BLOB"
1883
+ };
1884
+ function parsePrismaSchema(content) {
1885
+ const models = [];
1886
+ const modelRegex = /model\s+(\w+)\s*\{([^}]*)\}/g;
1887
+ let match;
1888
+ while ((match = modelRegex.exec(content)) !== null) {
1889
+ const modelName = match[1];
1890
+ const body = match[2];
1891
+ const fields = parsePrismaFields(body);
1892
+ const mapDirective = body.match(/@@map\(["']([^"']+)["']\)/);
1893
+ models.push({
1894
+ name: modelName,
1895
+ fields,
1896
+ tableName: mapDirective?.[1]
1897
+ });
1898
+ }
1899
+ return models;
1900
+ }
1901
+ function parsePrismaFields(body) {
1902
+ const fields = [];
1903
+ const lines = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"));
1904
+ for (const line of lines) {
1905
+ const field = parsePrismaFieldLine(line);
1906
+ if (field) fields.push(field);
1907
+ }
1908
+ return fields;
1909
+ }
1910
+ function parsePrismaFieldLine(line) {
1911
+ const match = line.match(/^(\w+)\s+(\w+)(\[\])?\??/);
1912
+ if (!match) return null;
1913
+ const name = match[1];
1914
+ const rawType = match[2];
1915
+ const isList = !!match[3];
1916
+ const isOptional = line.includes("?");
1917
+ const field = {
1918
+ name,
1919
+ type: rawType,
1920
+ isOptional,
1921
+ isList,
1922
+ isId: /@id\b/.test(line),
1923
+ isUnique: /@unique\b/.test(line),
1924
+ isUpdatedAt: /@updatedAt\b/.test(line)
1925
+ };
1926
+ const defaultMatch = line.match(/@default\(([^)]+)\)/);
1927
+ if (defaultMatch) field.defaultValue = defaultMatch[1];
1928
+ const mapMatch = line.match(/@map\(["']([^"']+)["']\)/);
1929
+ if (mapMatch) field.mapName = mapMatch[1];
1930
+ const nativeMatch = line.match(/@db\.(\w+(?:\([^)]*\))?)/);
1931
+ if (nativeMatch) field.nativeType = nativeMatch[1];
1932
+ const relMatch = line.match(/@relation\(([^)]*)\)/);
1933
+ if (relMatch) {
1934
+ field.relation = parseRelationDirective(relMatch[1]);
1935
+ }
1936
+ return field;
1937
+ }
1938
+ function parseRelationDirective(content) {
1939
+ const rel = {};
1940
+ const nameMatch = content.match(/(?:name:\s*)?["']([^"']+)["']/);
1941
+ if (nameMatch) rel.name = nameMatch[1];
1942
+ const fieldsMatch = content.match(/fields:\s*\[([^\]]+)\]/);
1943
+ if (fieldsMatch) {
1944
+ rel.fields = fieldsMatch[1].split(",").map((s) => s.trim());
1945
+ }
1946
+ const refsMatch = content.match(/references:\s*\[([^\]]+)\]/);
1947
+ if (refsMatch) {
1948
+ rel.references = refsMatch[1].split(",").map((s) => s.trim());
1949
+ }
1950
+ return rel;
1951
+ }
1952
+ function modelNameToTableName(name) {
1953
+ return name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
1954
+ }
1955
+ function prismaModelsToSchemas(models) {
1956
+ return models.map((model) => {
1957
+ const tableName = model.tableName || modelNameToTableName(model.name);
1958
+ const fields = [];
1959
+ for (const f of model.fields) {
1960
+ if (f.isList) continue;
1961
+ if (!PRISMA_TYPE_MAP[f.type] && !f.relation) continue;
1962
+ if (!PRISMA_TYPE_MAP[f.type] && f.relation && !f.relation.fields) continue;
1963
+ const fieldType = PRISMA_TYPE_MAP[f.type] || "STRING";
1964
+ fields.push({
1965
+ name: f.mapName || f.name,
1966
+ type: fieldType,
1967
+ allowNull: f.isOptional,
1968
+ primaryKey: f.isId,
1969
+ unique: f.isUnique,
1970
+ defaultValue: f.defaultValue
1971
+ });
1972
+ }
1973
+ return { tableName, className: model.name, fields };
1974
+ });
1975
+ }
1976
+ function prismaModelsToRelations(models) {
1977
+ const relations = [];
1978
+ const seen = /* @__PURE__ */ new Set();
1979
+ for (const model of models) {
1980
+ const sourceTable = model.tableName || modelNameToTableName(model.name);
1981
+ for (const field of model.fields) {
1982
+ if (!field.relation?.fields || !field.relation?.references) continue;
1983
+ const targetModel = models.find((m) => m.name === field.type);
1984
+ const targetTable = targetModel ? targetModel.tableName || modelNameToTableName(targetModel.name) : modelNameToTableName(field.type);
1985
+ const sourceField = field.relation.fields[0];
1986
+ const targetField = field.relation.references[0];
1987
+ const key = `${sourceTable}|${sourceField}|${targetTable}|${targetField}`;
1988
+ if (seen.has(key)) continue;
1989
+ seen.add(key);
1990
+ const isList = field.isList;
1991
+ relations.push({
1992
+ sourceTable,
1993
+ sourceField,
1994
+ targetTable,
1995
+ targetField,
1996
+ cardinality: isList ? "1:N" : "N:1"
1997
+ });
1998
+ }
1999
+ }
2000
+ return relations;
2001
+ }
2002
+ function parsePrismaFile(filePath) {
2003
+ const absolutePath = path7.resolve(filePath);
2004
+ if (!fs6.existsSync(absolutePath)) return { schemas: [], relations: [] };
2005
+ const content = fs6.readFileSync(absolutePath, "utf-8");
2006
+ const models = parsePrismaSchema(content);
2007
+ return {
2008
+ schemas: prismaModelsToSchemas(models),
2009
+ relations: prismaModelsToRelations(models)
2010
+ };
2011
+ }
2012
+ function findPrismaSchemaFile(dir) {
2013
+ const candidates = [
2014
+ path7.join(dir, "schema.prisma"),
2015
+ path7.join(dir, "prisma", "schema.prisma"),
2016
+ path7.join(dir, "..", "prisma", "schema.prisma")
2017
+ ];
2018
+ for (const c of candidates) {
2019
+ if (fs6.existsSync(c)) return c;
2020
+ }
2021
+ return null;
2022
+ }
2023
+ function createPrismaAdapter() {
2024
+ return {
2025
+ name: "prisma",
2026
+ async parseModels(dir) {
2027
+ const schemaFile = findPrismaSchemaFile(dir);
2028
+ if (!schemaFile) return [];
2029
+ const { schemas } = parsePrismaFile(schemaFile);
2030
+ return schemas;
2031
+ },
2032
+ async parseAssociations(file) {
2033
+ const schemaFile = findPrismaSchemaFile(path7.dirname(file)) || file;
2034
+ const { relations } = parsePrismaFile(schemaFile);
2035
+ return relations;
2036
+ },
2037
+ async parseControllers(dir) {
2038
+ const { parseControllerDirectory: parseControllerDirectory2 } = await Promise.resolve().then(() => (init_controller_parser(), controller_parser_exports));
2039
+ const endpoints = parseControllerDirectory2(dir);
2040
+ return endpoints.map((ep) => ({
2041
+ method: ep.method,
2042
+ path: ep.path,
2043
+ handler: "",
2044
+ controllerClass: ""
2045
+ }));
2046
+ }
2047
+ };
2048
+ }
2049
+
2050
+ // src/adapters/registry.ts
2051
+ init_esm_shims();
2052
+ import * as fs7 from "fs";
2053
+ import * as path8 from "path";
2054
+ var ADAPTER_FACTORIES = {
2055
+ sequelize: createSequelizeAdapter,
2056
+ typeorm: createTypeORMAdapter,
2057
+ prisma: createPrismaAdapter
2058
+ };
2059
+ function createAdapter(name) {
2060
+ const factory = ADAPTER_FACTORIES[name];
2061
+ if (!factory) {
2062
+ throw new Error(`Unknown adapter: "${name}". Available: ${Object.keys(ADAPTER_FACTORIES).join(", ")}`);
2063
+ }
2064
+ return factory();
2065
+ }
2066
+ function detectAdapter(backendRoot) {
2067
+ const root = path8.resolve(backendRoot);
2068
+ const prismaLocations = [
2069
+ path8.join(root, "prisma", "schema.prisma"),
2070
+ path8.join(root, "schema.prisma"),
2071
+ path8.join(root, "..", "prisma", "schema.prisma")
2072
+ ];
2073
+ for (const loc of prismaLocations) {
2074
+ if (fs7.existsSync(loc)) return "prisma";
2075
+ }
2076
+ const modelsDir = path8.join(root, "models");
2077
+ if (fs7.existsSync(modelsDir)) {
2078
+ try {
2079
+ const files = fs7.readdirSync(modelsDir, { recursive: true });
2080
+ for (const file of files) {
2081
+ const filePath = path8.join(modelsDir, String(file));
2082
+ if (!filePath.endsWith(".ts")) continue;
2083
+ try {
2084
+ const content = fs7.readFileSync(filePath, "utf-8");
2085
+ if (/@Entity\s*\(/.test(content)) return "typeorm";
2086
+ } catch {
2087
+ }
2088
+ }
2089
+ } catch {
2090
+ }
2091
+ }
2092
+ return "sequelize";
2093
+ }
2094
+ function resolveAdapter(adapterOrName, backendRoot) {
2095
+ if (typeof adapterOrName === "object" && adapterOrName !== null) {
2096
+ return adapterOrName;
2097
+ }
2098
+ const name = adapterOrName === "auto" || !adapterOrName ? detectAdapter(backendRoot) : adapterOrName;
2099
+ return createAdapter(name);
2100
+ }
2101
+
2102
+ // src/plugins/index.ts
2103
+ init_esm_shims();
2104
+ function createPluginRegistry() {
2105
+ const plugins = [];
2106
+ function register(plugin) {
2107
+ if (!plugin.name) {
2108
+ throw new Error("Plugin must have a name");
2109
+ }
2110
+ if (plugins.some((p) => p.name === plugin.name)) {
2111
+ throw new Error(`Plugin "${plugin.name}" is already registered`);
2112
+ }
2113
+ plugins.push(plugin);
2114
+ }
2115
+ async function unregister(name) {
2116
+ const idx = plugins.findIndex((p) => p.name === name);
2117
+ if (idx === -1) return;
2118
+ const plugin = plugins[idx];
2119
+ if (plugin.teardown) {
2120
+ await plugin.teardown();
2121
+ }
2122
+ plugins.splice(idx, 1);
2123
+ }
2124
+ function get(name) {
2125
+ return plugins.find((p) => p.name === name);
2126
+ }
2127
+ function list() {
2128
+ return plugins.map((p) => p.name);
2129
+ }
2130
+ async function invoke(hook, ...args) {
2131
+ for (const plugin of plugins) {
2132
+ const fn = plugin[hook];
2133
+ if (typeof fn === "function") {
2134
+ await fn.apply(plugin, args);
2135
+ }
2136
+ }
2137
+ }
2138
+ async function applyConfigTransforms(config) {
2139
+ let result = config;
2140
+ for (const plugin of plugins) {
2141
+ if (plugin.transformConfig) {
2142
+ result = await plugin.transformConfig(result);
2143
+ }
2144
+ }
2145
+ return result;
2146
+ }
2147
+ return { register, unregister, get, list, invoke, applyConfigTransforms };
2148
+ }
2149
+ function definePlugin(plugin) {
2150
+ return plugin;
2151
+ }
2152
+
2153
+ // src/ci/index.ts
2154
+ init_esm_shims();
2155
+ function generateGitHubActionsTemplate(opts = {}) {
2156
+ const nodeVersions = opts.nodeVersions ?? ["20.x"];
2157
+ const install = opts.installCommand ?? "npm ci";
2158
+ const genArgs = opts.generateArgs ?? "--all";
2159
+ const testArgs = opts.testArgs ?? "";
2160
+ const healStep = opts.selfHeal ? `
2161
+ - name: Self-heal failures
2162
+ if: failure()
2163
+ run: npx opencroc heal --max-iterations 3` : "";
2164
+ const matrix = nodeVersions.length > 1 ? `
2165
+ strategy:
2166
+ matrix:
2167
+ node-version: [${nodeVersions.join(", ")}]` : "";
2168
+ const nodeSetup = nodeVersions.length > 1 ? "${{ matrix.node-version }}" : nodeVersions[0];
2169
+ return `# Generated by OpenCroc \u2014 AI-native E2E testing
2170
+ # https://github.com/opencroc/opencroc
2171
+
2172
+ name: OpenCroc E2E
2173
+
2174
+ on:
2175
+ push:
2176
+ branches: [main]
2177
+ pull_request:
2178
+ branches: [main]
2179
+
2180
+ jobs:
2181
+ e2e:
2182
+ runs-on: ubuntu-latest${matrix}
2183
+ steps:
2184
+ - uses: actions/checkout@v4
2185
+
2186
+ - uses: actions/setup-node@v4
2187
+ with:
2188
+ node-version: '${nodeSetup}'
2189
+
2190
+ - name: Install dependencies
2191
+ run: ${install}
2192
+
2193
+ - name: Install Playwright browsers
2194
+ run: npx playwright install --with-deps chromium
2195
+
2196
+ - name: Generate E2E tests
2197
+ run: npx opencroc generate ${genArgs}
2198
+
2199
+ - name: Run E2E tests
2200
+ run: npx opencroc test ${testArgs}
2201
+ ${healStep}
2202
+ - name: Upload test report
2203
+ if: always()
2204
+ uses: actions/upload-artifact@v4
2205
+ with:
2206
+ name: opencroc-report
2207
+ path: opencroc-output/
2208
+ retention-days: 14
2209
+ `;
2210
+ }
2211
+ function generateGitLabCITemplate(opts = {}) {
2212
+ const install = opts.installCommand ?? "npm ci";
2213
+ const genArgs = opts.generateArgs ?? "--all";
2214
+ const testArgs = opts.testArgs ?? "";
2215
+ const nodeVersion = opts.nodeVersions?.[0] ?? "20";
2216
+ return `# Generated by OpenCroc \u2014 AI-native E2E testing
2217
+ # https://github.com/opencroc/opencroc
2218
+
2219
+ image: node:${nodeVersion}
2220
+
2221
+ stages:
2222
+ - generate
2223
+ - test
2224
+
2225
+ variables:
2226
+ PLAYWRIGHT_BROWSERS_PATH: \${CI_PROJECT_DIR}/.cache/ms-playwright
2227
+
2228
+ cache:
2229
+ key: \${CI_COMMIT_REF_SLUG}
2230
+ paths:
2231
+ - node_modules/
2232
+ - .cache/ms-playwright/
2233
+
2234
+ generate:
2235
+ stage: generate
2236
+ script:
2237
+ - ${install}
2238
+ - npx opencroc generate ${genArgs}
2239
+ artifacts:
2240
+ paths:
2241
+ - opencroc-output/
2242
+ expire_in: 1 day
2243
+
2244
+ e2e:
2245
+ stage: test
2246
+ needs: [generate]
2247
+ before_script:
2248
+ - ${install}
2249
+ - npx playwright install --with-deps chromium
2250
+ script:
2251
+ - npx opencroc test ${testArgs}
2252
+ artifacts:
2253
+ when: always
2254
+ paths:
2255
+ - opencroc-output/
2256
+ expire_in: 14 days
2257
+ `;
2258
+ }
2259
+ var TEMPLATES = {
2260
+ github: generateGitHubActionsTemplate,
2261
+ gitlab: generateGitLabCITemplate
2262
+ };
2263
+ function listCiPlatforms() {
2264
+ return Object.keys(TEMPLATES);
2265
+ }
2266
+ function generateCiTemplate(platform, opts = {}) {
2267
+ const generator = TEMPLATES[platform];
2268
+ if (!generator) {
2269
+ throw new Error(
2270
+ `Unknown CI platform: "${platform}". Available: ${Object.keys(TEMPLATES).join(", ")}`
2271
+ );
2272
+ }
2273
+ return generator(opts);
2274
+ }
2275
+
2276
+ // src/reporters/index.ts
2277
+ init_esm_shims();
2278
+ function generateJsonReport(result) {
2279
+ const serializable = {
2280
+ modules: result.modules,
2281
+ erDiagrams: Object.fromEntries(
2282
+ Array.from(result.erDiagrams.entries()).map(([k, v]) => [
2283
+ k,
2284
+ { tables: v.tables.length, relations: v.relations.length, mermaidText: v.mermaidText }
2285
+ ])
2286
+ ),
2287
+ chainPlans: Object.fromEntries(
2288
+ Array.from(result.chainPlans.entries()).map(([k, v]) => [
2289
+ k,
2290
+ { chains: v.chains.length, totalSteps: v.totalSteps }
2291
+ ])
2292
+ ),
2293
+ generatedFiles: result.generatedFiles.map((f) => ({
2294
+ filePath: f.filePath,
2295
+ module: f.module,
2296
+ chain: f.chain
2297
+ })),
2298
+ validationErrors: result.validationErrors,
2299
+ duration: result.duration
2300
+ };
2301
+ return {
2302
+ format: "json",
2303
+ content: JSON.stringify(serializable, null, 2),
2304
+ filename: "opencroc-report.json"
2305
+ };
2306
+ }
2307
+ function generateMarkdownReport(result) {
2308
+ const lines = [
2309
+ "# OpenCroc Report",
2310
+ "",
2311
+ `**Duration**: ${result.duration}ms`,
2312
+ `**Modules**: ${result.modules.length} (${result.modules.join(", ")})`,
2313
+ "",
2314
+ "## ER Diagrams",
2315
+ ""
2316
+ ];
2317
+ for (const [mod, er] of result.erDiagrams) {
2318
+ lines.push(`### ${mod}`);
2319
+ lines.push(`- Tables: ${er.tables.length}`);
2320
+ lines.push(`- Relations: ${er.relations.length}`);
2321
+ lines.push("");
2322
+ }
2323
+ lines.push("## Chain Plans", "");
2324
+ for (const [mod, plan] of result.chainPlans) {
2325
+ lines.push(`### ${mod}`);
2326
+ lines.push(`- Chains: ${plan.chains.length}`);
2327
+ lines.push(`- Total Steps: ${plan.totalSteps}`);
2328
+ lines.push("");
2329
+ }
2330
+ lines.push(`## Generated Files (${result.generatedFiles.length})`, "");
2331
+ for (const f of result.generatedFiles) {
2332
+ lines.push(`- \`${f.filePath}\` (${f.module} / ${f.chain})`);
2333
+ }
2334
+ if (result.validationErrors.length > 0) {
2335
+ lines.push("", "## Validation Issues", "");
2336
+ const errors = result.validationErrors.filter((e) => e.severity === "error");
2337
+ const warnings = result.validationErrors.filter((e) => e.severity === "warning");
2338
+ if (errors.length > 0) {
2339
+ lines.push(`### Errors (${errors.length})`, "");
2340
+ for (const e of errors) {
2341
+ lines.push(`- **[${e.module}]** ${e.field}: ${e.message}`);
2342
+ }
2343
+ }
2344
+ if (warnings.length > 0) {
2345
+ lines.push(`### Warnings (${warnings.length})`, "");
2346
+ for (const w of warnings) {
2347
+ lines.push(`- **[${w.module}]** ${w.field}: ${w.message}`);
2348
+ }
2349
+ }
2350
+ }
2351
+ lines.push("", "---", "*Generated by [OpenCroc](https://github.com/opencroc/opencroc)*");
2352
+ return {
2353
+ format: "markdown",
2354
+ content: lines.join("\n"),
2355
+ filename: "opencroc-report.md"
2356
+ };
2357
+ }
2358
+ function escapeHtml(s) {
2359
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2360
+ }
2361
+ function erSummaryRows(erDiagrams) {
2362
+ const rows = [];
2363
+ for (const [mod, er] of erDiagrams) {
2364
+ rows.push(`<tr><td>${escapeHtml(mod)}</td><td>${er.tables.length}</td><td>${er.relations.length}</td></tr>`);
2365
+ }
2366
+ return rows.join("\n");
2367
+ }
2368
+ function chainSummaryRows(chainPlans) {
2369
+ const rows = [];
2370
+ for (const [mod, plan] of chainPlans) {
2371
+ rows.push(`<tr><td>${escapeHtml(mod)}</td><td>${plan.chains.length}</td><td>${plan.totalSteps}</td></tr>`);
2372
+ }
2373
+ return rows.join("\n");
2374
+ }
2375
+ function fileListRows(files) {
2376
+ return files.map((f) => `<tr><td><code>${escapeHtml(f.filePath)}</code></td><td>${escapeHtml(f.module)}</td><td>${escapeHtml(f.chain)}</td></tr>`).join("\n");
2377
+ }
2378
+ function validationRows(errors) {
2379
+ return errors.map(
2380
+ (e) => `<tr class="${e.severity}"><td><span class="badge ${e.severity}">${e.severity}</span></td><td>${escapeHtml(e.module)}</td><td>${escapeHtml(e.field)}</td><td>${escapeHtml(e.message)}</td></tr>`
2381
+ ).join("\n");
2382
+ }
2383
+ function generateHtmlReport(result) {
2384
+ const totalTables = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.tables.length, 0);
2385
+ const totalRelations = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.relations.length, 0);
2386
+ const totalChains = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.chains.length, 0);
2387
+ const totalSteps = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.totalSteps, 0);
2388
+ const errorCount = result.validationErrors.filter((e) => e.severity === "error").length;
2389
+ const warnCount = result.validationErrors.filter((e) => e.severity === "warning").length;
2390
+ const html = `<!DOCTYPE html>
2391
+ <html lang="en">
2392
+ <head>
2393
+ <meta charset="utf-8" />
2394
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2395
+ <title>OpenCroc Report</title>
2396
+ <style>
2397
+ :root { --bg: #0d1117; --fg: #c9d1d9; --card: #161b22; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --yellow: #d29922; --red: #f85149; }
2398
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2399
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); padding: 2rem; }
2400
+ h1 { color: var(--accent); margin-bottom: 0.25rem; }
2401
+ .subtitle { color: #8b949e; margin-bottom: 2rem; }
2402
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
2403
+ .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
2404
+ .card .label { font-size: 0.85rem; color: #8b949e; }
2405
+ .card .value { font-size: 2rem; font-weight: 700; color: var(--accent); }
2406
+ .card .value.green { color: var(--green); }
2407
+ .card .value.yellow { color: var(--yellow); }
2408
+ .card .value.red { color: var(--red); }
2409
+ section { margin-bottom: 2rem; }
2410
+ h2 { color: var(--fg); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 1rem; }
2411
+ table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 8px; overflow: hidden; }
2412
+ th, td { text-align: left; padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); }
2413
+ th { background: #21262d; color: #8b949e; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; }
2414
+ tr:last-child td { border-bottom: none; }
2415
+ code { background: #21262d; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
2416
+ .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
2417
+ .badge.error { background: rgba(248,81,73,0.15); color: var(--red); }
2418
+ .badge.warning { background: rgba(210,153,34,0.15); color: var(--yellow); }
2419
+ footer { margin-top: 3rem; text-align: center; color: #484f58; font-size: 0.85rem; }
2420
+ footer a { color: var(--accent); text-decoration: none; }
2421
+ </style>
2422
+ </head>
2423
+ <body>
2424
+ <h1>OpenCroc Report</h1>
2425
+ <p class="subtitle">Generated in ${result.duration}ms &middot; ${result.modules.length} module(s)</p>
2426
+
2427
+ <div class="grid">
2428
+ <div class="card"><div class="label">Modules</div><div class="value">${result.modules.length}</div></div>
2429
+ <div class="card"><div class="label">Tables</div><div class="value">${totalTables}</div></div>
2430
+ <div class="card"><div class="label">Relations</div><div class="value">${totalRelations}</div></div>
2431
+ <div class="card"><div class="label">Chains</div><div class="value">${totalChains}</div></div>
2432
+ <div class="card"><div class="label">Steps</div><div class="value">${totalSteps}</div></div>
2433
+ <div class="card"><div class="label">Files</div><div class="value green">${result.generatedFiles.length}</div></div>
2434
+ <div class="card"><div class="label">Errors</div><div class="value${errorCount > 0 ? " red" : ""}">${errorCount}</div></div>
2435
+ <div class="card"><div class="label">Warnings</div><div class="value${warnCount > 0 ? " yellow" : ""}">${warnCount}</div></div>
2436
+ </div>
2437
+
2438
+ <section>
2439
+ <h2>ER Diagrams</h2>
2440
+ <table>
2441
+ <thead><tr><th>Module</th><th>Tables</th><th>Relations</th></tr></thead>
2442
+ <tbody>${erSummaryRows(result.erDiagrams)}</tbody>
2443
+ </table>
2444
+ </section>
2445
+
2446
+ <section>
2447
+ <h2>Chain Plans</h2>
2448
+ <table>
2449
+ <thead><tr><th>Module</th><th>Chains</th><th>Steps</th></tr></thead>
2450
+ <tbody>${chainSummaryRows(result.chainPlans)}</tbody>
2451
+ </table>
2452
+ </section>
2453
+
2454
+ <section>
2455
+ <h2>Generated Files (${result.generatedFiles.length})</h2>
2456
+ <table>
2457
+ <thead><tr><th>File</th><th>Module</th><th>Chain</th></tr></thead>
2458
+ <tbody>${fileListRows(result.generatedFiles)}</tbody>
2459
+ </table>
2460
+ </section>
2461
+
2462
+ ${result.validationErrors.length > 0 ? `<section>
2463
+ <h2>Validation Issues (${result.validationErrors.length})</h2>
2464
+ <table>
2465
+ <thead><tr><th>Severity</th><th>Module</th><th>Field</th><th>Message</th></tr></thead>
2466
+ <tbody>${validationRows(result.validationErrors)}</tbody>
2467
+ </table>
2468
+ </section>` : ""}
2469
+
2470
+ <footer>
2471
+ Generated by <a href="https://github.com/opencroc/opencroc">OpenCroc</a>
2472
+ </footer>
2473
+ </body>
2474
+ </html>`;
2475
+ return {
2476
+ format: "html",
2477
+ content: html,
2478
+ filename: "opencroc-report.html"
2479
+ };
2480
+ }
2481
+ var REPORTERS = {
2482
+ html: generateHtmlReport,
2483
+ json: generateJsonReport,
2484
+ markdown: generateMarkdownReport
2485
+ };
2486
+ function generateReports(result, formats = ["html"]) {
2487
+ return formats.map((fmt) => {
2488
+ const gen = REPORTERS[fmt];
2489
+ if (!gen) throw new Error(`Unknown report format: "${fmt}". Available: ${Object.keys(REPORTERS).join(", ")}`);
2490
+ return gen(result);
2491
+ });
2492
+ }
2493
+
2494
+ // src/vscode/index.ts
2495
+ init_esm_shims();
2496
+ var COMMANDS = [
2497
+ { command: "opencroc.init", title: "Initialize Project", category: "OpenCroc" },
2498
+ { command: "opencroc.generate", title: "Generate Tests", category: "OpenCroc" },
2499
+ { command: "opencroc.generateModule", title: "Generate Tests for Module...", category: "OpenCroc" },
2500
+ { command: "opencroc.test", title: "Run Tests", category: "OpenCroc" },
2501
+ { command: "opencroc.testModule", title: "Run Tests for Module...", category: "OpenCroc" },
2502
+ { command: "opencroc.validate", title: "Validate Configuration", category: "OpenCroc" },
2503
+ { command: "opencroc.heal", title: "Self-Heal Failures", category: "OpenCroc" },
2504
+ { command: "opencroc.openReport", title: "Open Report", category: "OpenCroc" },
2505
+ { command: "opencroc.ci", title: "Generate CI Template", category: "OpenCroc" }
2506
+ ];
2507
+ function buildModuleTree(modules) {
2508
+ return modules.map((mod) => ({
2509
+ label: mod,
2510
+ description: "module",
2511
+ iconId: "symbol-module",
2512
+ children: [
2513
+ { label: "Generate Tests", command: "opencroc.generateModule", iconId: "play" },
2514
+ { label: "Run Tests", command: "opencroc.testModule", iconId: "testing-run-icon" },
2515
+ { label: "View ER Diagram", command: "opencroc.openReport", iconId: "graph" }
2516
+ ]
2517
+ }));
2518
+ }
2519
+ function buildStatusTree(stats) {
2520
+ return [
2521
+ { label: `Modules: ${stats.modules}`, iconId: "symbol-module" },
2522
+ { label: `Tables: ${stats.tables}`, iconId: "database" },
2523
+ { label: `Relations: ${stats.relations}`, iconId: "git-merge" },
2524
+ { label: `Generated: ${stats.generatedFiles} files`, iconId: "file-code" },
2525
+ {
2526
+ label: stats.errors > 0 ? `Errors: ${stats.errors}` : "No errors",
2527
+ iconId: stats.errors > 0 ? "error" : "pass"
2528
+ }
2529
+ ];
2530
+ }
2531
+ function generateExtensionManifest() {
2532
+ return {
2533
+ name: "opencroc",
2534
+ displayName: "OpenCroc",
2535
+ description: "AI-native E2E testing \u2014 generate, run, and self-heal tests from VS Code",
2536
+ version: "0.1.0",
2537
+ publisher: "opencroc",
2538
+ license: "MIT",
2539
+ repository: { type: "git", url: "https://github.com/opencroc/opencroc" },
2540
+ engines: { vscode: "^1.85.0" },
2541
+ categories: ["Testing"],
2542
+ keywords: ["e2e", "testing", "playwright", "ai", "self-healing"],
2543
+ activationEvents: ["workspaceContains:opencroc.config.ts", "workspaceContains:opencroc.config.js"],
2544
+ main: "./out/extension.js",
2545
+ contributes: {
2546
+ commands: COMMANDS.map((c) => ({
2547
+ command: c.command,
2548
+ title: c.title,
2549
+ category: c.category
2550
+ })),
2551
+ viewsContainers: {
2552
+ activitybar: [
2553
+ {
2554
+ id: "opencroc",
2555
+ title: "OpenCroc",
2556
+ icon: "resources/opencroc.svg"
2557
+ }
2558
+ ]
2559
+ },
2560
+ views: {
2561
+ opencroc: [
2562
+ { id: "opencroc.status", name: "Status" },
2563
+ { id: "opencroc.modules", name: "Modules" }
2564
+ ]
2565
+ },
2566
+ configuration: {
2567
+ title: "OpenCroc",
2568
+ properties: {
2569
+ "opencroc.autoGenerate": {
2570
+ type: "boolean",
2571
+ default: false,
2572
+ description: "Automatically regenerate tests on file save"
2573
+ },
2574
+ "opencroc.reportFormat": {
2575
+ type: "string",
2576
+ default: "html",
2577
+ enum: ["html", "json", "markdown"],
2578
+ description: "Default report format"
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2583
+ };
2584
+ }
2585
+ function generateExtensionEntrypoint() {
2586
+ return `import * as vscode from 'vscode';
2587
+ import { exec } from 'child_process';
2588
+ import { promisify } from 'util';
2589
+
2590
+ const run = promisify(exec);
2591
+
2592
+ export function activate(context: vscode.ExtensionContext) {
2593
+ const outputChannel = vscode.window.createOutputChannel('OpenCroc');
2594
+
2595
+ async function runCommand(cmd: string) {
2596
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
2597
+ if (!workspaceFolder) {
2598
+ vscode.window.showErrorMessage('No workspace folder open');
2599
+ return;
2600
+ }
2601
+ outputChannel.show();
2602
+ outputChannel.appendLine(\`> \${cmd}\`);
2603
+ try {
2604
+ const { stdout, stderr } = await run(cmd, { cwd: workspaceFolder.uri.fsPath });
2605
+ if (stdout) outputChannel.appendLine(stdout);
2606
+ if (stderr) outputChannel.appendLine(stderr);
2607
+ vscode.window.showInformationMessage(\`OpenCroc: \${cmd} completed\`);
2608
+ } catch (err: unknown) {
2609
+ const message = err instanceof Error ? err.message : String(err);
2610
+ outputChannel.appendLine(\`Error: \${message}\`);
2611
+ vscode.window.showErrorMessage(\`OpenCroc: \${message}\`);
2612
+ }
2613
+ }
2614
+
2615
+ context.subscriptions.push(
2616
+ vscode.commands.registerCommand('opencroc.init', () => runCommand('npx opencroc init --yes')),
2617
+ vscode.commands.registerCommand('opencroc.generate', () => runCommand('npx opencroc generate --all')),
2618
+ vscode.commands.registerCommand('opencroc.test', () => runCommand('npx opencroc test')),
2619
+ vscode.commands.registerCommand('opencroc.validate', () => runCommand('npx opencroc validate')),
2620
+ vscode.commands.registerCommand('opencroc.heal', () => runCommand('npx opencroc heal')),
2621
+ vscode.commands.registerCommand('opencroc.ci', async () => {
2622
+ const platform = await vscode.window.showQuickPick(['github', 'gitlab'], {
2623
+ placeHolder: 'Select CI platform',
2624
+ });
2625
+ if (platform) {
2626
+ await runCommand(\`npx opencroc ci --platform=\${platform}\`);
2627
+ }
2628
+ }),
2629
+ vscode.commands.registerCommand('opencroc.generateModule', async () => {
2630
+ const mod = await vscode.window.showInputBox({ prompt: 'Module name' });
2631
+ if (mod) await runCommand(\`npx opencroc generate --module=\${mod}\`);
2632
+ }),
2633
+ vscode.commands.registerCommand('opencroc.testModule', async () => {
2634
+ const mod = await vscode.window.showInputBox({ prompt: 'Module name' });
2635
+ if (mod) await runCommand(\`npx opencroc test --module=\${mod}\`);
2636
+ }),
2637
+ );
2638
+
2639
+ outputChannel.appendLine('OpenCroc extension activated');
2640
+ }
2641
+
2642
+ export function deactivate() {}
2643
+ `;
2644
+ }
109
2645
  export {
2646
+ SYSTEM_PROMPTS,
2647
+ COMMANDS as VSCODE_COMMANDS,
2648
+ analyzeFailureWithLLM,
2649
+ buildClassToTableMap,
2650
+ buildGraph,
2651
+ buildModuleTree,
2652
+ buildStatusTree,
2653
+ categorizeFailure,
2654
+ classNameToTableName,
2655
+ createAdapter,
110
2656
  createApiChainAnalyzer,
111
2657
  createAssociationParser,
112
2658
  createControllerParser,
113
2659
  createERDiagramGenerator,
114
2660
  createImpactReporter,
2661
+ createLlmProvider,
115
2662
  createMockDataGenerator,
116
2663
  createModelParser,
2664
+ createOllamaProvider,
2665
+ createOpenAIProvider,
117
2666
  createPipeline,
2667
+ createPluginRegistry,
2668
+ createPrismaAdapter,
118
2669
  createSelfHealingLoop,
2670
+ createSequelizeAdapter,
119
2671
  createTestCodeGenerator,
2672
+ createTokenTracker,
2673
+ createTypeORMAdapter,
120
2674
  defineConfig,
2675
+ definePlugin,
2676
+ detectAdapter,
2677
+ detectCycles,
2678
+ generateCiTemplate,
2679
+ generateExtensionEntrypoint,
2680
+ generateExtensionManifest,
2681
+ generateGitHubActionsTemplate,
2682
+ generateGitLabCITemplate,
2683
+ generateHtmlReport,
2684
+ generateJsonReport,
2685
+ generateMarkdownReport,
2686
+ generateReports,
2687
+ inferDependencies,
2688
+ inferRelatedTables,
2689
+ listCiPlatforms,
2690
+ parseAssociationFile,
2691
+ parseControllerDirectory,
2692
+ parseControllerFile,
2693
+ parseModelFile,
2694
+ parseModuleModels,
2695
+ resolveAdapter,
2696
+ topologicalSort,
121
2697
  validateConfig
122
2698
  };
123
2699
  //# sourceMappingURL=index.js.map