opencroc 1.6.9 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -819,20 +819,20 @@ function detectCycles(dag) {
819
819
  const color = /* @__PURE__ */ new Map();
820
820
  for (const node of dag.nodes) color.set(node, 0 /* WHITE */);
821
821
  const warnings = [];
822
- const path14 = [];
822
+ const path17 = [];
823
823
  function dfs(node) {
824
824
  color.set(node, 1 /* GRAY */);
825
- path14.push(node);
825
+ path17.push(node);
826
826
  for (const neighbor of adjacency.get(node) || []) {
827
827
  const nc = color.get(neighbor);
828
828
  if (nc === 1 /* GRAY */) {
829
- const cycleStart = path14.indexOf(neighbor);
830
- warnings.push(`Cycle detected: ${path14.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
829
+ const cycleStart = path17.indexOf(neighbor);
830
+ warnings.push(`Cycle detected: ${path17.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
831
831
  } else if (nc === 0 /* WHITE */) {
832
832
  dfs(neighbor);
833
833
  }
834
834
  }
835
- path14.pop();
835
+ path17.pop();
836
836
  color.set(node, 2 /* BLACK */);
837
837
  }
838
838
  for (const node of dag.nodes) {
@@ -3896,50 +3896,50 @@ var defaultFs = {
3896
3896
  mkdirp: (d) => mkdirSync(d, { recursive: true })
3897
3897
  };
3898
3898
  async function applyControlledFix(opts) {
3899
- const fs12 = opts.fs ?? defaultFs;
3899
+ const fs15 = opts.fs ?? defaultFs;
3900
3900
  const scope = opts.options?.scope ?? "config-only";
3901
3901
  const dryRun = opts.options?.dryRun ?? true;
3902
3902
  const verify = opts.options?.verify ?? true;
3903
3903
  const configPath = opts.configPath;
3904
3904
  const backupPath = configPath + ".backup";
3905
- if (!fs12.exists(configPath)) {
3905
+ if (!fs15.exists(configPath)) {
3906
3906
  return { success: false, scope, fixedItems: [], rolledBack: false, error: `Config file not found: ${configPath}` };
3907
3907
  }
3908
- const originalContent = fs12.read(configPath);
3909
- fs12.write(backupPath, originalContent);
3908
+ const originalContent = fs15.read(configPath);
3909
+ fs15.write(backupPath, originalContent);
3910
3910
  const validation = opts.validator.validate(originalContent);
3911
3911
  if (validation.passed) {
3912
- cleanup(fs12, backupPath);
3912
+ cleanup(fs15, backupPath);
3913
3913
  return { success: true, scope, fixedItems: [], rolledBack: false };
3914
3914
  }
3915
3915
  let fixResult;
3916
3916
  try {
3917
3917
  fixResult = opts.fixer.fix(originalContent, validation.errors);
3918
3918
  } catch (err) {
3919
- rollback(fs12, backupPath, configPath);
3919
+ rollback(fs15, backupPath, configPath);
3920
3920
  return { success: false, scope, fixedItems: [], rolledBack: true, error: `Fix threw: ${err instanceof Error ? err.message : String(err)}` };
3921
3921
  }
3922
3922
  if (!fixResult.success) {
3923
- rollback(fs12, backupPath, configPath);
3923
+ rollback(fs15, backupPath, configPath);
3924
3924
  return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Remaining errors: ${fixResult.remainingErrors.join("; ")}` };
3925
3925
  }
3926
3926
  if (dryRun) {
3927
3927
  const dryValidation = opts.validator.validate(fixResult.fixedContent);
3928
3928
  if (!dryValidation.passed) {
3929
- rollback(fs12, backupPath, configPath);
3929
+ rollback(fs15, backupPath, configPath);
3930
3930
  return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Dry-run validation failed: ${dryValidation.errors.join("; ")}` };
3931
3931
  }
3932
3932
  }
3933
- fs12.write(configPath, fixResult.fixedContent);
3933
+ fs15.write(configPath, fixResult.fixedContent);
3934
3934
  if (verify) {
3935
- const reloaded = fs12.read(configPath);
3935
+ const reloaded = fs15.read(configPath);
3936
3936
  const postValidation = opts.validator.validate(reloaded);
3937
3937
  if (!postValidation.passed) {
3938
- rollback(fs12, backupPath, configPath);
3938
+ rollback(fs15, backupPath, configPath);
3939
3939
  return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Post-write verification failed: ${postValidation.errors.join("; ")}` };
3940
3940
  }
3941
3941
  }
3942
- cleanup(fs12, backupPath);
3942
+ cleanup(fs15, backupPath);
3943
3943
  let prUrl;
3944
3944
  if (scope === "config-and-source" && opts.attribution && opts.prGenerator) {
3945
3945
  try {
@@ -3949,16 +3949,16 @@ async function applyControlledFix(opts) {
3949
3949
  }
3950
3950
  return { success: true, scope, fixedItems: fixResult.fixedItems, rolledBack: false, prUrl };
3951
3951
  }
3952
- function rollback(fs12, backupPath, configPath) {
3953
- if (fs12.exists(backupPath)) {
3954
- const backup = fs12.read(backupPath);
3955
- fs12.write(configPath, backup);
3956
- fs12.remove(backupPath);
3952
+ function rollback(fs15, backupPath, configPath) {
3953
+ if (fs15.exists(backupPath)) {
3954
+ const backup = fs15.read(backupPath);
3955
+ fs15.write(configPath, backup);
3956
+ fs15.remove(backupPath);
3957
3957
  }
3958
3958
  }
3959
- function cleanup(fs12, backupPath) {
3960
- if (fs12.exists(backupPath)) {
3961
- fs12.remove(backupPath);
3959
+ function cleanup(fs15, backupPath) {
3960
+ if (fs15.exists(backupPath)) {
3961
+ fs15.remove(backupPath);
3962
3962
  }
3963
3963
  }
3964
3964
 
@@ -6648,21 +6648,21 @@ function extractPath(url) {
6648
6648
  return url;
6649
6649
  }
6650
6650
  }
6651
- function shouldIgnore(path14) {
6652
- const lower = path14.toLowerCase();
6651
+ function shouldIgnore(path17) {
6652
+ const lower = path17.toLowerCase();
6653
6653
  return IGNORE_KEYWORDS.some((kw) => lower.includes(kw));
6654
6654
  }
6655
6655
  function selectCandidates(responses, maxCount = 20) {
6656
6656
  const unique = /* @__PURE__ */ new Map();
6657
6657
  for (const item of responses) {
6658
6658
  if (!item.url.includes("/api/")) continue;
6659
- const path14 = extractPath(item.url);
6660
- if (shouldIgnore(path14)) continue;
6659
+ const path17 = extractPath(item.url);
6660
+ if (shouldIgnore(path17)) continue;
6661
6661
  const method = item.method.toUpperCase();
6662
6662
  if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) continue;
6663
- const key = item.requestId ? `rid:${item.requestId}` : `mp:${method}:${path14}`;
6663
+ const key = item.requestId ? `rid:${item.requestId}` : `mp:${method}:${path17}`;
6664
6664
  if (!unique.has(key)) {
6665
- unique.set(key, { requestId: item.requestId, method, path: path14, url: item.url });
6665
+ unique.set(key, { requestId: item.requestId, method, path: path17, url: item.url });
6666
6666
  }
6667
6667
  }
6668
6668
  return Array.from(unique.values()).slice(0, maxCount);
@@ -6675,12 +6675,12 @@ function selectCandidatesFromLogs(logs, maxCount = 20) {
6675
6675
  const method = getField(log, "method").toUpperCase();
6676
6676
  if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) continue;
6677
6677
  const rawPath = getField(log, "apiPath") || getField(log, "url");
6678
- const path14 = extractPath(rawPath);
6679
- if (!path14.includes("/api/") || shouldIgnore(path14)) continue;
6678
+ const path17 = extractPath(rawPath);
6679
+ if (!path17.includes("/api/") || shouldIgnore(path17)) continue;
6680
6680
  const requestId = getField(log, "requestId") || void 0;
6681
- const key = requestId ? `rid:${requestId}` : `mp:${method}:${path14}`;
6681
+ const key = requestId ? `rid:${requestId}` : `mp:${method}:${path17}`;
6682
6682
  if (!unique.has(key)) {
6683
- unique.set(key, { requestId, method, path: path14, url: rawPath });
6683
+ unique.set(key, { requestId, method, path: path17, url: rawPath });
6684
6684
  }
6685
6685
  }
6686
6686
  return Array.from(unique.values()).slice(0, maxCount);
@@ -7142,6 +7142,2072 @@ function printOrchestrationSummary(summary) {
7142
7142
  lines.push("");
7143
7143
  return lines;
7144
7144
  }
7145
+
7146
+ // src/scanner/language-detector.ts
7147
+ init_esm_shims();
7148
+ import * as fs12 from "fs";
7149
+ import * as path14 from "path";
7150
+ var EXTENSION_MAP = {
7151
+ ".ts": "typescript",
7152
+ ".tsx": "typescript",
7153
+ ".mts": "typescript",
7154
+ ".cts": "typescript",
7155
+ ".js": "javascript",
7156
+ ".jsx": "javascript",
7157
+ ".mjs": "javascript",
7158
+ ".cjs": "javascript",
7159
+ ".py": "python",
7160
+ ".pyw": "python",
7161
+ ".pyi": "python",
7162
+ ".go": "go",
7163
+ ".java": "java",
7164
+ ".kt": "kotlin",
7165
+ ".kts": "kotlin",
7166
+ ".rs": "rust",
7167
+ ".rb": "ruby",
7168
+ ".php": "php",
7169
+ ".cs": "csharp",
7170
+ ".cpp": "cpp",
7171
+ ".cc": "cpp",
7172
+ ".cxx": "cpp",
7173
+ ".c": "c",
7174
+ ".h": "c",
7175
+ ".swift": "swift",
7176
+ ".dart": "dart",
7177
+ ".vue": "vue",
7178
+ ".svelte": "svelte",
7179
+ ".astro": "astro",
7180
+ ".sql": "sql",
7181
+ ".graphql": "graphql",
7182
+ ".gql": "graphql",
7183
+ ".proto": "protobuf",
7184
+ ".yaml": "yaml",
7185
+ ".yml": "yaml",
7186
+ ".json": "json",
7187
+ ".toml": "toml",
7188
+ ".md": "markdown",
7189
+ ".html": "html",
7190
+ ".htm": "html",
7191
+ ".css": "css",
7192
+ ".scss": "scss",
7193
+ ".less": "less",
7194
+ ".sass": "sass",
7195
+ ".sh": "shell",
7196
+ ".bash": "shell",
7197
+ ".zsh": "shell",
7198
+ ".ps1": "powershell",
7199
+ ".dockerfile": "docker",
7200
+ ".tf": "terraform",
7201
+ ".lua": "lua",
7202
+ ".r": "r",
7203
+ ".R": "r",
7204
+ ".scala": "scala",
7205
+ ".ex": "elixir",
7206
+ ".exs": "elixir",
7207
+ ".erl": "erlang",
7208
+ ".zig": "zig"
7209
+ };
7210
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
7211
+ "node_modules",
7212
+ ".git",
7213
+ ".svn",
7214
+ ".hg",
7215
+ "dist",
7216
+ "build",
7217
+ "out",
7218
+ "target",
7219
+ "__pycache__",
7220
+ ".cache",
7221
+ ".next",
7222
+ ".nuxt",
7223
+ ".output",
7224
+ "vendor",
7225
+ "venv",
7226
+ ".venv",
7227
+ "env",
7228
+ ".env",
7229
+ "coverage",
7230
+ ".idea",
7231
+ ".vscode",
7232
+ ".vs",
7233
+ ".turbo",
7234
+ ".nx",
7235
+ "bower_components",
7236
+ "jspm_packages"
7237
+ ]);
7238
+ var MAX_DEPTH = 8;
7239
+ var MAX_FILES = 1e4;
7240
+ function detectProject(rootDir) {
7241
+ const absRoot = path14.resolve(rootDir);
7242
+ const languages = {};
7243
+ const linesByLanguage = {};
7244
+ const files = [];
7245
+ let totalFiles = 0;
7246
+ let totalLines = 0;
7247
+ function walk(dir, depth) {
7248
+ if (depth > MAX_DEPTH || totalFiles > MAX_FILES) return;
7249
+ let entries;
7250
+ try {
7251
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
7252
+ } catch {
7253
+ return;
7254
+ }
7255
+ for (const entry of entries) {
7256
+ if (totalFiles > MAX_FILES) break;
7257
+ const fullPath = path14.join(dir, entry.name);
7258
+ if (entry.isDirectory()) {
7259
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
7260
+ walk(fullPath, depth + 1);
7261
+ }
7262
+ continue;
7263
+ }
7264
+ if (!entry.isFile()) continue;
7265
+ const ext = path14.extname(entry.name).toLowerCase();
7266
+ const lang = detectLanguageByFile(entry.name, ext);
7267
+ if (!lang) continue;
7268
+ let lineCount = 0;
7269
+ let fileSize = 0;
7270
+ try {
7271
+ const stat = fs12.statSync(fullPath);
7272
+ fileSize = stat.size;
7273
+ if (fileSize < 1048576) {
7274
+ const content = fs12.readFileSync(fullPath, "utf-8");
7275
+ lineCount = content.split("\n").length;
7276
+ }
7277
+ } catch {
7278
+ continue;
7279
+ }
7280
+ languages[lang] = (languages[lang] || 0) + 1;
7281
+ linesByLanguage[lang] = (linesByLanguage[lang] || 0) + lineCount;
7282
+ totalFiles++;
7283
+ totalLines += lineCount;
7284
+ const relPath = path14.relative(absRoot, fullPath).replace(/\\/g, "/");
7285
+ files.push({ path: relPath, language: lang, lines: lineCount, size: fileSize });
7286
+ }
7287
+ }
7288
+ walk(absRoot, 0);
7289
+ const codeLangs = Object.entries(languages).filter(([k]) => !["json", "yaml", "toml", "markdown", "html", "css", "scss", "less", "sass"].includes(k)).sort((a, b) => b[1] - a[1]);
7290
+ const primaryLanguage = codeLangs[0]?.[0] || "unknown";
7291
+ const frameworks = detectFrameworks(absRoot, languages, files);
7292
+ const projectType = detectProjectType(absRoot, languages, frameworks);
7293
+ const packageManager = detectPackageManager(absRoot);
7294
+ return {
7295
+ languages,
7296
+ linesByLanguage,
7297
+ totalFiles,
7298
+ totalLines,
7299
+ primaryLanguage,
7300
+ frameworks,
7301
+ projectType,
7302
+ packageManager,
7303
+ files
7304
+ };
7305
+ }
7306
+ function detectLanguageByFile(fileName, ext) {
7307
+ if (fileName === "Dockerfile" || fileName.startsWith("Dockerfile.")) return "docker";
7308
+ if (fileName === "Makefile") return "makefile";
7309
+ if (fileName === "CMakeLists.txt") return "cmake";
7310
+ if (fileName === "Vagrantfile") return "ruby";
7311
+ if (fileName === "Gemfile") return "ruby";
7312
+ if (fileName === "Rakefile") return "ruby";
7313
+ if (fileName === "Cargo.toml") return "rust";
7314
+ if (fileName === "go.mod" || fileName === "go.sum") return "go";
7315
+ return EXTENSION_MAP[ext] || null;
7316
+ }
7317
+ var FRAMEWORK_RULES = [
7318
+ // --- Node.js / JavaScript ---
7319
+ {
7320
+ name: "express",
7321
+ detect: (root) => detectFromPackageJson(root, "express", "Express")
7322
+ },
7323
+ {
7324
+ name: "nestjs",
7325
+ detect: (root) => detectFromPackageJson(root, "@nestjs/core", "NestJS")
7326
+ },
7327
+ {
7328
+ name: "fastify",
7329
+ detect: (root) => detectFromPackageJson(root, "fastify", "Fastify")
7330
+ },
7331
+ {
7332
+ name: "koa",
7333
+ detect: (root) => detectFromPackageJson(root, "koa", "Koa")
7334
+ },
7335
+ {
7336
+ name: "hapi",
7337
+ detect: (root) => detectFromPackageJson(root, "@hapi/hapi", "Hapi")
7338
+ },
7339
+ {
7340
+ name: "nextjs",
7341
+ detect: (root) => detectFromPackageJson(root, "next", "Next.js")
7342
+ },
7343
+ {
7344
+ name: "nuxtjs",
7345
+ detect: (root) => detectFromPackageJson(root, "nuxt", "Nuxt.js")
7346
+ },
7347
+ {
7348
+ name: "react",
7349
+ detect: (root) => detectFromPackageJson(root, "react", "React")
7350
+ },
7351
+ {
7352
+ name: "vue",
7353
+ detect: (root) => detectFromPackageJson(root, "vue", "Vue")
7354
+ },
7355
+ {
7356
+ name: "angular",
7357
+ detect: (root) => detectFromPackageJson(root, "@angular/core", "Angular")
7358
+ },
7359
+ {
7360
+ name: "svelte",
7361
+ detect: (root) => detectFromPackageJson(root, "svelte", "Svelte")
7362
+ },
7363
+ {
7364
+ name: "electron",
7365
+ detect: (root) => detectFromPackageJson(root, "electron", "Electron")
7366
+ },
7367
+ {
7368
+ name: "sequelize",
7369
+ detect: (root) => detectFromPackageJson(root, "sequelize", "Sequelize")
7370
+ },
7371
+ {
7372
+ name: "typeorm",
7373
+ detect: (root) => detectFromPackageJson(root, "typeorm", "TypeORM")
7374
+ },
7375
+ {
7376
+ name: "prisma",
7377
+ detect: (root) => detectFromPackageJson(root, "prisma", "Prisma") || detectFromPackageJson(root, "@prisma/client", "Prisma")
7378
+ },
7379
+ {
7380
+ name: "mongoose",
7381
+ detect: (root) => detectFromPackageJson(root, "mongoose", "Mongoose")
7382
+ },
7383
+ {
7384
+ name: "playwright",
7385
+ detect: (root) => detectFromPackageJson(root, "@playwright/test", "Playwright")
7386
+ },
7387
+ // --- Python ---
7388
+ {
7389
+ name: "django",
7390
+ detect: (root) => detectFromRequirements(root, "django", "Django") || detectFromFile(root, "manage.py", "Django")
7391
+ },
7392
+ {
7393
+ name: "flask",
7394
+ detect: (root) => detectFromRequirements(root, "flask", "Flask")
7395
+ },
7396
+ {
7397
+ name: "fastapi",
7398
+ detect: (root) => detectFromRequirements(root, "fastapi", "FastAPI")
7399
+ },
7400
+ {
7401
+ name: "pytorch",
7402
+ detect: (root) => detectFromRequirements(root, "torch", "PyTorch")
7403
+ },
7404
+ {
7405
+ name: "tensorflow",
7406
+ detect: (root) => detectFromRequirements(root, "tensorflow", "TensorFlow")
7407
+ },
7408
+ // --- Go ---
7409
+ {
7410
+ name: "gin",
7411
+ detect: (root) => detectFromGoMod(root, "github.com/gin-gonic/gin", "Gin")
7412
+ },
7413
+ {
7414
+ name: "echo",
7415
+ detect: (root) => detectFromGoMod(root, "github.com/labstack/echo", "Echo")
7416
+ },
7417
+ {
7418
+ name: "fiber",
7419
+ detect: (root) => detectFromGoMod(root, "github.com/gofiber/fiber", "Fiber")
7420
+ },
7421
+ // --- Java ---
7422
+ {
7423
+ name: "spring-boot",
7424
+ detect: (root) => detectFromFile(root, "pom.xml", "Spring Boot", "spring-boot") || detectFromFile(root, "build.gradle", "Spring Boot", "spring-boot")
7425
+ },
7426
+ // --- Rust ---
7427
+ {
7428
+ name: "actix-web",
7429
+ detect: (root) => detectFromCargoToml(root, "actix-web", "Actix Web")
7430
+ },
7431
+ {
7432
+ name: "axum",
7433
+ detect: (root) => detectFromCargoToml(root, "axum", "Axum")
7434
+ },
7435
+ // --- Ruby ---
7436
+ {
7437
+ name: "rails",
7438
+ detect: (root) => detectFromFile(root, "Gemfile", "Ruby on Rails", "rails")
7439
+ },
7440
+ // --- PHP ---
7441
+ {
7442
+ name: "laravel",
7443
+ detect: (root) => detectFromFile(root, "artisan", "Laravel")
7444
+ }
7445
+ ];
7446
+ function detectFromPackageJson(root, dep, name) {
7447
+ const candidates = [
7448
+ path14.join(root, "package.json"),
7449
+ path14.join(root, "backend", "package.json"),
7450
+ path14.join(root, "server", "package.json"),
7451
+ path14.join(root, "api", "package.json"),
7452
+ path14.join(root, "frontend", "package.json"),
7453
+ path14.join(root, "web", "package.json"),
7454
+ path14.join(root, "client", "package.json")
7455
+ ];
7456
+ for (const pkgPath of candidates) {
7457
+ try {
7458
+ if (!fs12.existsSync(pkgPath)) continue;
7459
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
7460
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
7461
+ if (dep in allDeps) {
7462
+ return {
7463
+ name,
7464
+ version: allDeps[dep]?.replace(/[\^~>=<]*/g, ""),
7465
+ confidence: 0.95,
7466
+ evidence: `Found "${dep}" in ${path14.relative(root, pkgPath)}`
7467
+ };
7468
+ }
7469
+ } catch {
7470
+ continue;
7471
+ }
7472
+ }
7473
+ return null;
7474
+ }
7475
+ function detectFromRequirements(root, dep, name) {
7476
+ const candidates = [
7477
+ path14.join(root, "requirements.txt"),
7478
+ path14.join(root, "Pipfile"),
7479
+ path14.join(root, "pyproject.toml"),
7480
+ path14.join(root, "setup.py"),
7481
+ path14.join(root, "setup.cfg")
7482
+ ];
7483
+ for (const filePath of candidates) {
7484
+ try {
7485
+ if (!fs12.existsSync(filePath)) continue;
7486
+ const content = fs12.readFileSync(filePath, "utf-8");
7487
+ const pattern = new RegExp(`^${dep}([>=<~!\\s]|$)`, "im");
7488
+ if (pattern.test(content)) {
7489
+ return {
7490
+ name,
7491
+ confidence: 0.9,
7492
+ evidence: `Found "${dep}" in ${path14.basename(filePath)}`
7493
+ };
7494
+ }
7495
+ } catch {
7496
+ continue;
7497
+ }
7498
+ }
7499
+ return null;
7500
+ }
7501
+ function detectFromGoMod(root, module, name) {
7502
+ const goModPath = path14.join(root, "go.mod");
7503
+ try {
7504
+ if (!fs12.existsSync(goModPath)) return null;
7505
+ const content = fs12.readFileSync(goModPath, "utf-8");
7506
+ if (content.includes(module)) {
7507
+ return {
7508
+ name,
7509
+ confidence: 0.95,
7510
+ evidence: `Found "${module}" in go.mod`
7511
+ };
7512
+ }
7513
+ } catch {
7514
+ }
7515
+ return null;
7516
+ }
7517
+ function detectFromCargoToml(root, crate, name) {
7518
+ const cargoPath = path14.join(root, "Cargo.toml");
7519
+ try {
7520
+ if (!fs12.existsSync(cargoPath)) return null;
7521
+ const content = fs12.readFileSync(cargoPath, "utf-8");
7522
+ if (content.includes(crate)) {
7523
+ return {
7524
+ name,
7525
+ confidence: 0.9,
7526
+ evidence: `Found "${crate}" in Cargo.toml`
7527
+ };
7528
+ }
7529
+ } catch {
7530
+ }
7531
+ return null;
7532
+ }
7533
+ function detectFromFile(root, fileName, name, searchTerm) {
7534
+ const filePath = path14.join(root, fileName);
7535
+ try {
7536
+ if (!fs12.existsSync(filePath)) return null;
7537
+ if (searchTerm) {
7538
+ const content = fs12.readFileSync(filePath, "utf-8");
7539
+ if (!content.toLowerCase().includes(searchTerm.toLowerCase())) return null;
7540
+ }
7541
+ return {
7542
+ name,
7543
+ confidence: 0.8,
7544
+ evidence: `Found ${fileName}${searchTerm ? ` containing "${searchTerm}"` : ""}`
7545
+ };
7546
+ } catch {
7547
+ return null;
7548
+ }
7549
+ }
7550
+ function detectFrameworks(root, langs, files) {
7551
+ const detected = [];
7552
+ for (const rule of FRAMEWORK_RULES) {
7553
+ const result = rule.detect(root, langs, files);
7554
+ if (result) detected.push(result);
7555
+ }
7556
+ return detected;
7557
+ }
7558
+ function detectProjectType(root, langs, frameworks) {
7559
+ const frameworkNames = new Set(frameworks.map((f) => f.name.toLowerCase()));
7560
+ const hasLerna = fs12.existsSync(path14.join(root, "lerna.json"));
7561
+ const hasPnpmWorkspace = fs12.existsSync(path14.join(root, "pnpm-workspace.yaml"));
7562
+ const hasNxJson = fs12.existsSync(path14.join(root, "nx.json"));
7563
+ const hasTurboJson = fs12.existsSync(path14.join(root, "turbo.json"));
7564
+ let hasWorkspaces = false;
7565
+ try {
7566
+ const pkg = JSON.parse(fs12.readFileSync(path14.join(root, "package.json"), "utf-8"));
7567
+ hasWorkspaces = Array.isArray(pkg.workspaces) || typeof pkg.workspaces === "object";
7568
+ } catch {
7569
+ }
7570
+ if (hasLerna || hasPnpmWorkspace || hasNxJson || hasTurboJson || hasWorkspaces) {
7571
+ return "monorepo";
7572
+ }
7573
+ try {
7574
+ const pkg = JSON.parse(fs12.readFileSync(path14.join(root, "package.json"), "utf-8"));
7575
+ if (pkg.main || pkg.exports || pkg.module) {
7576
+ const hasNoServer = !frameworkNames.has("express") && !frameworkNames.has("fastify") && !frameworkNames.has("koa") && !frameworkNames.has("nestjs");
7577
+ const hasNoFrontend = !frameworkNames.has("react") && !frameworkNames.has("vue") && !frameworkNames.has("angular") && !frameworkNames.has("svelte");
7578
+ if (hasNoServer && hasNoFrontend && pkg.keywords) return "library";
7579
+ }
7580
+ if (pkg.bin) return "cli-tool";
7581
+ } catch {
7582
+ }
7583
+ if (frameworkNames.has("next.js") || frameworkNames.has("nuxt.js")) return "frontend-ssr";
7584
+ if (frameworkNames.has("electron")) return "fullstack";
7585
+ if (langs["dart"]) return "mobile";
7586
+ if (langs["swift"] && !langs["typescript"] && !langs["python"]) return "mobile";
7587
+ const hasBackend = frameworkNames.has("express") || frameworkNames.has("fastify") || frameworkNames.has("nestjs") || frameworkNames.has("koa") || frameworkNames.has("django") || frameworkNames.has("flask") || frameworkNames.has("fastapi") || frameworkNames.has("gin") || frameworkNames.has("spring boot") || frameworkNames.has("rails") || frameworkNames.has("laravel");
7588
+ const hasFrontend = frameworkNames.has("react") || frameworkNames.has("vue") || frameworkNames.has("angular") || frameworkNames.has("svelte");
7589
+ if (hasBackend && hasFrontend) return "fullstack";
7590
+ if (hasBackend) return "backend-api";
7591
+ if (hasFrontend) return "frontend-spa";
7592
+ if (frameworkNames.has("actix web") || frameworkNames.has("axum") || frameworkNames.has("gin") || frameworkNames.has("echo") || frameworkNames.has("fiber")) {
7593
+ return "backend-api";
7594
+ }
7595
+ return "unknown";
7596
+ }
7597
+ function detectPackageManager(root) {
7598
+ if (fs12.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
7599
+ if (fs12.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
7600
+ if (fs12.existsSync(path14.join(root, "bun.lockb"))) return "bun";
7601
+ if (fs12.existsSync(path14.join(root, "package-lock.json"))) return "npm";
7602
+ if (fs12.existsSync(path14.join(root, "Pipfile.lock"))) return "pipenv";
7603
+ if (fs12.existsSync(path14.join(root, "poetry.lock"))) return "poetry";
7604
+ if (fs12.existsSync(path14.join(root, "go.sum"))) return "go-modules";
7605
+ if (fs12.existsSync(path14.join(root, "Cargo.lock"))) return "cargo";
7606
+ if (fs12.existsSync(path14.join(root, "Gemfile.lock"))) return "bundler";
7607
+ if (fs12.existsSync(path14.join(root, "composer.lock"))) return "composer";
7608
+ return void 0;
7609
+ }
7610
+
7611
+ // src/scanner/project-scanner.ts
7612
+ init_esm_shims();
7613
+ import * as fs13 from "fs";
7614
+ import * as path15 from "path";
7615
+ async function scanProject(options) {
7616
+ const { rootDir, maxDeepScan = 500, onProgress } = options;
7617
+ const startTime = Date.now();
7618
+ onProgress?.("detecting", 0, "Detecting languages and frameworks...");
7619
+ const detection = detectProject(rootDir);
7620
+ onProgress?.("detecting", 100, `Found ${detection.totalFiles} files, primary: ${detection.primaryLanguage}`);
7621
+ const entities = [];
7622
+ const relationships = [];
7623
+ const sourceFiles = detection.files.filter((f) => {
7624
+ const lang = f.language;
7625
+ return !["json", "yaml", "toml", "markdown", "html", "css", "scss", "less", "sass", "docker", "shell", "powershell"].includes(lang);
7626
+ });
7627
+ const filesToAnalyze = sourceFiles.slice(0, maxDeepScan);
7628
+ for (let i = 0; i < filesToAnalyze.length; i++) {
7629
+ const file = filesToAnalyze[i];
7630
+ const percent = Math.round(i / filesToAnalyze.length * 100);
7631
+ if (i % 20 === 0) {
7632
+ onProgress?.("scanning", percent, `Scanning ${file.path}...`);
7633
+ }
7634
+ const fullPath = path15.join(rootDir, file.path);
7635
+ try {
7636
+ const extracted = extractEntitiesFromFile(fullPath, file.path, file.language);
7637
+ entities.push(...extracted.entities);
7638
+ relationships.push(...extracted.relationships);
7639
+ } catch {
7640
+ }
7641
+ }
7642
+ onProgress?.("scanning", 100, `Extracted ${entities.length} entities`);
7643
+ onProgress?.("configs", 0, "Parsing config files...");
7644
+ const configEntities = extractFromConfigs(rootDir, detection);
7645
+ entities.push(...configEntities.entities);
7646
+ relationships.push(...configEntities.relationships);
7647
+ onProgress?.("configs", 100, "Config parsing complete");
7648
+ onProgress?.("relations", 0, "Building relationships...");
7649
+ const inferredRelations = inferRelationships(entities, rootDir);
7650
+ relationships.push(...inferredRelations);
7651
+ onProgress?.("relations", 100, `${relationships.length} total relationships`);
7652
+ const discoveredFiles = detection.files.map((f) => ({
7653
+ path: f.path,
7654
+ language: f.language,
7655
+ category: categorizeFile(f.path, f.language),
7656
+ lines: f.lines,
7657
+ size: f.size
7658
+ }));
7659
+ return {
7660
+ languages: detection.languages,
7661
+ frameworks: detection.frameworks,
7662
+ files: discoveredFiles,
7663
+ entities,
7664
+ relationships,
7665
+ duration: Date.now() - startTime
7666
+ };
7667
+ }
7668
+ function extractEntitiesFromFile(fullPath, relPath, language) {
7669
+ switch (language) {
7670
+ case "typescript":
7671
+ case "javascript":
7672
+ return extractFromTsJs(fullPath, relPath, language);
7673
+ case "python":
7674
+ return extractFromPython(fullPath, relPath);
7675
+ case "go":
7676
+ return extractFromGo(fullPath, relPath);
7677
+ case "java":
7678
+ case "kotlin":
7679
+ return extractFromJavaKotlin(fullPath, relPath, language);
7680
+ case "rust":
7681
+ return extractFromRust(fullPath, relPath);
7682
+ case "ruby":
7683
+ return extractFromRuby(fullPath, relPath);
7684
+ case "php":
7685
+ return extractFromPHP(fullPath, relPath);
7686
+ case "vue":
7687
+ case "svelte":
7688
+ return extractFromTsJs(fullPath, relPath, "typescript");
7689
+ // Extract script section
7690
+ default:
7691
+ return { entities: [], relationships: [] };
7692
+ }
7693
+ }
7694
+ function extractFromTsJs(fullPath, relPath, language) {
7695
+ const content = fs13.readFileSync(fullPath, "utf-8");
7696
+ const entities = [];
7697
+ const relationships = [];
7698
+ const fileId = `file:${relPath}`;
7699
+ entities.push({
7700
+ id: fileId,
7701
+ name: path15.basename(relPath),
7702
+ type: "file",
7703
+ filePath: relPath,
7704
+ language,
7705
+ metadata: {}
7706
+ });
7707
+ const classRegex = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?\s*\{/g;
7708
+ let match;
7709
+ while ((match = classRegex.exec(content)) !== null) {
7710
+ const className = match[1];
7711
+ const extendsClass = match[2];
7712
+ const classId = `class:${relPath}:${className}`;
7713
+ entities.push({
7714
+ id: classId,
7715
+ name: className,
7716
+ type: detectClassType(className, content),
7717
+ filePath: relPath,
7718
+ line: getLineNumber(content, match.index),
7719
+ language,
7720
+ metadata: { extends: extendsClass }
7721
+ });
7722
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
7723
+ if (extendsClass) {
7724
+ relationships.push({
7725
+ sourceId: classId,
7726
+ targetId: `class:*:${extendsClass}`,
7727
+ relation: "extends"
7728
+ });
7729
+ }
7730
+ }
7731
+ const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
7732
+ while ((match = funcRegex.exec(content)) !== null) {
7733
+ const funcName = match[1];
7734
+ const funcId = `func:${relPath}:${funcName}`;
7735
+ entities.push({
7736
+ id: funcId,
7737
+ name: funcName,
7738
+ type: "function",
7739
+ filePath: relPath,
7740
+ line: getLineNumber(content, match.index),
7741
+ language,
7742
+ metadata: {}
7743
+ });
7744
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
7745
+ }
7746
+ const arrowFuncRegex = /export\s+(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/g;
7747
+ while ((match = arrowFuncRegex.exec(content)) !== null) {
7748
+ const funcName = match[1];
7749
+ const funcId = `func:${relPath}:${funcName}`;
7750
+ entities.push({
7751
+ id: funcId,
7752
+ name: funcName,
7753
+ type: "function",
7754
+ filePath: relPath,
7755
+ line: getLineNumber(content, match.index),
7756
+ language,
7757
+ metadata: { arrow: true }
7758
+ });
7759
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
7760
+ }
7761
+ const routeRegex = /(?:router|app)\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
7762
+ while ((match = routeRegex.exec(content)) !== null) {
7763
+ const method = match[1].toUpperCase();
7764
+ const routePath = match[2];
7765
+ const apiId = `api:${method}:${routePath}`;
7766
+ entities.push({
7767
+ id: apiId,
7768
+ name: `${method} ${routePath}`,
7769
+ type: "api",
7770
+ filePath: relPath,
7771
+ line: getLineNumber(content, match.index),
7772
+ language,
7773
+ metadata: { method, path: routePath }
7774
+ });
7775
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
7776
+ }
7777
+ const importRegex = /(?:import\s+.*from\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\))/g;
7778
+ while ((match = importRegex.exec(content)) !== null) {
7779
+ const importPath = match[1] || match[2];
7780
+ if (importPath.startsWith(".")) {
7781
+ const resolved = resolveRelativeImport(relPath, importPath);
7782
+ relationships.push({
7783
+ sourceId: fileId,
7784
+ targetId: `file:${resolved}`,
7785
+ relation: "imports"
7786
+ });
7787
+ } else {
7788
+ const depName = importPath.startsWith("@") ? importPath.split("/").slice(0, 2).join("/") : importPath.split("/")[0];
7789
+ const depId = `dep:${depName}`;
7790
+ entities.push({
7791
+ id: depId,
7792
+ name: depName,
7793
+ type: "dependency",
7794
+ filePath: "",
7795
+ language: "external",
7796
+ metadata: { external: true }
7797
+ });
7798
+ relationships.push({ sourceId: fileId, targetId: depId, relation: "depends-on" });
7799
+ }
7800
+ }
7801
+ if (content.includes(".init(") || content.includes("Model.init") || content.includes("@Entity") || content.includes("defineModel")) {
7802
+ const tableMatch = content.match(/tableName:\s*['"`](\w+)['"`]/);
7803
+ if (tableMatch) {
7804
+ const tableName = tableMatch[1];
7805
+ const modelId = `model:${tableName}`;
7806
+ entities.push({
7807
+ id: modelId,
7808
+ name: tableName,
7809
+ type: "model",
7810
+ filePath: relPath,
7811
+ language,
7812
+ metadata: { orm: "sequelize" }
7813
+ });
7814
+ relationships.push({ sourceId: modelId, targetId: fileId, relation: "belongs-to" });
7815
+ }
7816
+ }
7817
+ return { entities, relationships };
7818
+ }
7819
+ function extractFromPython(fullPath, relPath) {
7820
+ const content = fs13.readFileSync(fullPath, "utf-8");
7821
+ const entities = [];
7822
+ const relationships = [];
7823
+ const fileId = `file:${relPath}`;
7824
+ entities.push({ id: fileId, name: path15.basename(relPath), type: "file", filePath: relPath, language: "python", metadata: {} });
7825
+ const classRegex = /^class\s+(\w+)(?:\(([^)]*)\))?\s*:/gm;
7826
+ let match;
7827
+ while ((match = classRegex.exec(content)) !== null) {
7828
+ const className = match[1];
7829
+ const bases = match[2];
7830
+ const classId = `class:${relPath}:${className}`;
7831
+ entities.push({
7832
+ id: classId,
7833
+ name: className,
7834
+ type: detectPythonClassType(className, bases || "", content),
7835
+ filePath: relPath,
7836
+ line: getLineNumber(content, match.index),
7837
+ language: "python",
7838
+ metadata: { bases }
7839
+ });
7840
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
7841
+ }
7842
+ const funcRegex = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
7843
+ while ((match = funcRegex.exec(content)) !== null) {
7844
+ const funcName = match[1];
7845
+ if (funcName.startsWith("_") && funcName !== "__init__") continue;
7846
+ const funcId = `func:${relPath}:${funcName}`;
7847
+ entities.push({
7848
+ id: funcId,
7849
+ name: funcName,
7850
+ type: "function",
7851
+ filePath: relPath,
7852
+ line: getLineNumber(content, match.index),
7853
+ language: "python",
7854
+ metadata: {}
7855
+ });
7856
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
7857
+ }
7858
+ const routeRegex = /@(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
7859
+ while ((match = routeRegex.exec(content)) !== null) {
7860
+ const method = match[1].toUpperCase();
7861
+ const routePath = match[2];
7862
+ const apiId = `api:${method}:${routePath}`;
7863
+ entities.push({
7864
+ id: apiId,
7865
+ name: `${method} ${routePath}`,
7866
+ type: "api",
7867
+ filePath: relPath,
7868
+ line: getLineNumber(content, match.index),
7869
+ language: "python",
7870
+ metadata: { method, path: routePath }
7871
+ });
7872
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
7873
+ }
7874
+ const djangoUrlRegex = /path\s*\(\s*['"]([^'"]+)['"],\s*(\w+)/g;
7875
+ while ((match = djangoUrlRegex.exec(content)) !== null) {
7876
+ const routePath = match[1];
7877
+ const apiId = `api:ANY:${routePath}`;
7878
+ entities.push({
7879
+ id: apiId,
7880
+ name: routePath,
7881
+ type: "route",
7882
+ filePath: relPath,
7883
+ line: getLineNumber(content, match.index),
7884
+ language: "python",
7885
+ metadata: { handler: match[2] }
7886
+ });
7887
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
7888
+ }
7889
+ const djangoModelRegex = /class\s+(\w+)\((?:models\.)?Model\)/g;
7890
+ while ((match = djangoModelRegex.exec(content)) !== null) {
7891
+ const modelName = match[1];
7892
+ const modelId = `model:${modelName}`;
7893
+ entities.push({
7894
+ id: modelId,
7895
+ name: modelName,
7896
+ type: "model",
7897
+ filePath: relPath,
7898
+ language: "python",
7899
+ metadata: { orm: "django" }
7900
+ });
7901
+ relationships.push({ sourceId: modelId, targetId: fileId, relation: "belongs-to" });
7902
+ }
7903
+ const sqlalchemyRegex = /class\s+(\w+)\(.*(?:Base|DeclarativeBase|db\.Model)\)/g;
7904
+ while ((match = sqlalchemyRegex.exec(content)) !== null) {
7905
+ const modelName = match[1];
7906
+ const modelId = `model:${modelName}`;
7907
+ entities.push({
7908
+ id: modelId,
7909
+ name: modelName,
7910
+ type: "model",
7911
+ filePath: relPath,
7912
+ language: "python",
7913
+ metadata: { orm: "sqlalchemy" }
7914
+ });
7915
+ relationships.push({ sourceId: modelId, targetId: fileId, relation: "belongs-to" });
7916
+ }
7917
+ const importRegex = /^(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm;
7918
+ while ((match = importRegex.exec(content)) !== null) {
7919
+ const mod = match[1] || match[2];
7920
+ if (mod.startsWith(".")) {
7921
+ relationships.push({ sourceId: fileId, targetId: `file:${mod}`, relation: "imports" });
7922
+ } else {
7923
+ const depName = mod.split(".")[0];
7924
+ entities.push({ id: `dep:${depName}`, name: depName, type: "dependency", filePath: "", language: "external", metadata: { external: true } });
7925
+ relationships.push({ sourceId: fileId, targetId: `dep:${depName}`, relation: "depends-on" });
7926
+ }
7927
+ }
7928
+ return { entities, relationships };
7929
+ }
7930
+ function extractFromGo(fullPath, relPath) {
7931
+ const content = fs13.readFileSync(fullPath, "utf-8");
7932
+ const entities = [];
7933
+ const relationships = [];
7934
+ const fileId = `file:${relPath}`;
7935
+ entities.push({ id: fileId, name: path15.basename(relPath), type: "file", filePath: relPath, language: "go", metadata: {} });
7936
+ const structRegex = /type\s+(\w+)\s+struct\s*\{/g;
7937
+ let match;
7938
+ while ((match = structRegex.exec(content)) !== null) {
7939
+ const structName = match[1];
7940
+ const structId = `class:${relPath}:${structName}`;
7941
+ entities.push({
7942
+ id: structId,
7943
+ name: structName,
7944
+ type: structName.endsWith("Model") || structName.endsWith("Entity") ? "model" : "class",
7945
+ filePath: relPath,
7946
+ line: getLineNumber(content, match.index),
7947
+ language: "go",
7948
+ metadata: {}
7949
+ });
7950
+ relationships.push({ sourceId: structId, targetId: fileId, relation: "belongs-to" });
7951
+ }
7952
+ const funcRegex = /func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/g;
7953
+ while ((match = funcRegex.exec(content)) !== null) {
7954
+ const funcName = match[1];
7955
+ if (funcName[0] !== funcName[0].toUpperCase()) continue;
7956
+ const funcId = `func:${relPath}:${funcName}`;
7957
+ entities.push({
7958
+ id: funcId,
7959
+ name: funcName,
7960
+ type: "function",
7961
+ filePath: relPath,
7962
+ line: getLineNumber(content, match.index),
7963
+ language: "go",
7964
+ metadata: {}
7965
+ });
7966
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
7967
+ }
7968
+ const ginRouteRegex = /\.(GET|POST|PUT|PATCH|DELETE)\s*\(\s*"([^"]+)"/gi;
7969
+ while ((match = ginRouteRegex.exec(content)) !== null) {
7970
+ const method = match[1].toUpperCase();
7971
+ const routePath = match[2];
7972
+ const apiId = `api:${method}:${routePath}`;
7973
+ entities.push({
7974
+ id: apiId,
7975
+ name: `${method} ${routePath}`,
7976
+ type: "api",
7977
+ filePath: relPath,
7978
+ line: getLineNumber(content, match.index),
7979
+ language: "go",
7980
+ metadata: { method, path: routePath }
7981
+ });
7982
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
7983
+ }
7984
+ return { entities, relationships };
7985
+ }
7986
+ function extractFromJavaKotlin(fullPath, relPath, language) {
7987
+ const content = fs13.readFileSync(fullPath, "utf-8");
7988
+ const entities = [];
7989
+ const relationships = [];
7990
+ const fileId = `file:${relPath}`;
7991
+ entities.push({ id: fileId, name: path15.basename(relPath), type: "file", filePath: relPath, language, metadata: {} });
7992
+ const classRegex = /(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+(\w+)(?:\s+extends\s+(\w+))?/g;
7993
+ let match;
7994
+ while ((match = classRegex.exec(content)) !== null) {
7995
+ const className = match[1];
7996
+ const classId = `class:${relPath}:${className}`;
7997
+ entities.push({
7998
+ id: classId,
7999
+ name: className,
8000
+ type: content.includes("@Entity") || content.includes("@Table") ? "model" : "class",
8001
+ filePath: relPath,
8002
+ line: getLineNumber(content, match.index),
8003
+ language,
8004
+ metadata: {}
8005
+ });
8006
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
8007
+ }
8008
+ const springRegex = /@(?:Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/g;
8009
+ while ((match = springRegex.exec(content)) !== null) {
8010
+ const routePath = match[1];
8011
+ const apiId = `api:ANY:${routePath}`;
8012
+ entities.push({
8013
+ id: apiId,
8014
+ name: routePath,
8015
+ type: "api",
8016
+ filePath: relPath,
8017
+ line: getLineNumber(content, match.index),
8018
+ language,
8019
+ metadata: { path: routePath }
8020
+ });
8021
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
8022
+ }
8023
+ return { entities, relationships };
8024
+ }
8025
+ function extractFromRust(fullPath, relPath) {
8026
+ const content = fs13.readFileSync(fullPath, "utf-8");
8027
+ const entities = [];
8028
+ const relationships = [];
8029
+ const fileId = `file:${relPath}`;
8030
+ entities.push({ id: fileId, name: path15.basename(relPath), type: "file", filePath: relPath, language: "rust", metadata: {} });
8031
+ const structRegex = /pub\s+struct\s+(\w+)/g;
8032
+ let match;
8033
+ while ((match = structRegex.exec(content)) !== null) {
8034
+ const structName = match[1];
8035
+ const structId = `class:${relPath}:${structName}`;
8036
+ entities.push({ id: structId, name: structName, type: "class", filePath: relPath, line: getLineNumber(content, match.index), language: "rust", metadata: {} });
8037
+ relationships.push({ sourceId: structId, targetId: fileId, relation: "belongs-to" });
8038
+ }
8039
+ const funcRegex = /pub\s+(?:async\s+)?fn\s+(\w+)/g;
8040
+ while ((match = funcRegex.exec(content)) !== null) {
8041
+ const funcName = match[1];
8042
+ const funcId = `func:${relPath}:${funcName}`;
8043
+ entities.push({ id: funcId, name: funcName, type: "function", filePath: relPath, line: getLineNumber(content, match.index), language: "rust", metadata: {} });
8044
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
8045
+ }
8046
+ return { entities, relationships };
8047
+ }
8048
+ function extractFromRuby(fullPath, relPath) {
8049
+ const content = fs13.readFileSync(fullPath, "utf-8");
8050
+ const entities = [];
8051
+ const relationships = [];
8052
+ const fileId = `file:${relPath}`;
8053
+ entities.push({ id: fileId, name: path15.basename(relPath), type: "file", filePath: relPath, language: "ruby", metadata: {} });
8054
+ const classRegex = /class\s+(\w+)(?:\s*<\s*(\w+))?/g;
8055
+ let match;
8056
+ while ((match = classRegex.exec(content)) !== null) {
8057
+ const className = match[1];
8058
+ const base = match[2];
8059
+ const classId = `class:${relPath}:${className}`;
8060
+ const type = base === "ApplicationRecord" || base === "ActiveRecord::Base" ? "model" : "class";
8061
+ entities.push({ id: classId, name: className, type, filePath: relPath, line: getLineNumber(content, match.index), language: "ruby", metadata: { extends: base } });
8062
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
8063
+ }
8064
+ const defRegex = /def\s+(?:self\.)?(\w+)/g;
8065
+ while ((match = defRegex.exec(content)) !== null) {
8066
+ const funcName = match[1];
8067
+ if (funcName.startsWith("_")) continue;
8068
+ const funcId = `func:${relPath}:${funcName}`;
8069
+ entities.push({ id: funcId, name: funcName, type: "function", filePath: relPath, line: getLineNumber(content, match.index), language: "ruby", metadata: {} });
8070
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
8071
+ }
8072
+ return { entities, relationships };
8073
+ }
8074
+ function extractFromPHP(fullPath, relPath) {
8075
+ const content = fs13.readFileSync(fullPath, "utf-8");
8076
+ const entities = [];
8077
+ const relationships = [];
8078
+ const fileId = `file:${relPath}`;
8079
+ entities.push({ id: fileId, name: path15.basename(relPath), type: "file", filePath: relPath, language: "php", metadata: {} });
8080
+ const classRegex = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/g;
8081
+ let match;
8082
+ while ((match = classRegex.exec(content)) !== null) {
8083
+ const className = match[1];
8084
+ const base = match[2];
8085
+ const classId = `class:${relPath}:${className}`;
8086
+ const type = base === "Model" || base === "Eloquent" ? "model" : "class";
8087
+ entities.push({ id: classId, name: className, type, filePath: relPath, line: getLineNumber(content, match.index), language: "php", metadata: {} });
8088
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
8089
+ }
8090
+ const laravelRouteRegex = /Route::(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
8091
+ while ((match = laravelRouteRegex.exec(content)) !== null) {
8092
+ const method = match[1].toUpperCase();
8093
+ const routePath = match[2];
8094
+ const apiId = `api:${method}:${routePath}`;
8095
+ entities.push({ id: apiId, name: `${method} ${routePath}`, type: "api", filePath: relPath, line: getLineNumber(content, match.index), language: "php", metadata: { method, path: routePath } });
8096
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
8097
+ }
8098
+ return { entities, relationships };
8099
+ }
8100
+ function extractFromConfigs(rootDir, _detection) {
8101
+ const entities = [];
8102
+ const relationships = [];
8103
+ const pkgPath = path15.join(rootDir, "package.json");
8104
+ if (fs13.existsSync(pkgPath)) {
8105
+ try {
8106
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
8107
+ const allDeps = { ...pkg.dependencies };
8108
+ for (const [name, version] of Object.entries(allDeps)) {
8109
+ const depId = `dep:${name}`;
8110
+ entities.push({
8111
+ id: depId,
8112
+ name,
8113
+ type: "dependency",
8114
+ filePath: "package.json",
8115
+ language: "external",
8116
+ metadata: { version, source: "npm", external: true }
8117
+ });
8118
+ }
8119
+ } catch {
8120
+ }
8121
+ }
8122
+ const openAPIFiles = ["openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "swagger.yaml"];
8123
+ for (const apiFile of openAPIFiles) {
8124
+ const apiPath = path15.join(rootDir, apiFile);
8125
+ if (fs13.existsSync(apiPath)) {
8126
+ try {
8127
+ const content = fs13.readFileSync(apiPath, "utf-8");
8128
+ const pathRegex = /"(\/[^"]+)":\s*\{/g;
8129
+ let match;
8130
+ while ((match = pathRegex.exec(content)) !== null) {
8131
+ const routePath = match[1];
8132
+ const apiId = `api:ANY:${routePath}`;
8133
+ entities.push({
8134
+ id: apiId,
8135
+ name: routePath,
8136
+ type: "api",
8137
+ filePath: apiFile,
8138
+ language: "openapi",
8139
+ metadata: { source: "openapi" }
8140
+ });
8141
+ }
8142
+ } catch {
8143
+ }
8144
+ }
8145
+ }
8146
+ const composeFiles = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
8147
+ for (const composeFile of composeFiles) {
8148
+ const composePath = path15.join(rootDir, composeFile);
8149
+ if (fs13.existsSync(composePath)) {
8150
+ try {
8151
+ const content = fs13.readFileSync(composePath, "utf-8");
8152
+ const serviceRegex = /^\s{2}(\w[\w-]*):\s*$/gm;
8153
+ let match;
8154
+ while ((match = serviceRegex.exec(content)) !== null) {
8155
+ const serviceName = match[1];
8156
+ if (serviceName === "services" || serviceName === "volumes" || serviceName === "networks") continue;
8157
+ entities.push({
8158
+ id: `service:${serviceName}`,
8159
+ name: serviceName,
8160
+ type: detectServiceType(serviceName),
8161
+ filePath: composeFile,
8162
+ language: "docker",
8163
+ metadata: { source: "docker-compose" }
8164
+ });
8165
+ }
8166
+ } catch {
8167
+ }
8168
+ }
8169
+ }
8170
+ return { entities, relationships };
8171
+ }
8172
+ function inferRelationships(entities, _rootDir) {
8173
+ const relationships = [];
8174
+ const models = entities.filter((e) => e.type === "model");
8175
+ const apis = entities.filter((e) => e.type === "api");
8176
+ for (const api of apis) {
8177
+ const apiPath = api.metadata.path || api.name;
8178
+ for (const model of models) {
8179
+ const modelName = model.name.toLowerCase().replace(/_/g, "");
8180
+ const pathLower = apiPath.toLowerCase().replace(/[/-]/g, "");
8181
+ if (pathLower.includes(modelName) || modelName.includes(pathLower.split("/").pop() || "")) {
8182
+ const method = api.metadata.method || "ANY";
8183
+ const relation = ["POST", "PUT", "PATCH", "DELETE"].includes(method) ? "writes" : "reads";
8184
+ relationships.push({ sourceId: api.id, targetId: model.id, relation });
8185
+ }
8186
+ }
8187
+ }
8188
+ const fileEntities = entities.filter((e) => e.type === "file");
8189
+ for (const file of fileEntities) {
8190
+ const dir = path15.dirname(file.filePath).split("/")[0];
8191
+ if (dir && dir !== ".") {
8192
+ const moduleId = `module:${dir}`;
8193
+ if (!entities.some((e) => e.id === moduleId)) {
8194
+ entities.push({
8195
+ id: moduleId,
8196
+ name: dir,
8197
+ type: "module",
8198
+ filePath: dir,
8199
+ language: "directory",
8200
+ metadata: {}
8201
+ });
8202
+ }
8203
+ relationships.push({ sourceId: file.id, targetId: moduleId, relation: "belongs-to" });
8204
+ }
8205
+ }
8206
+ return relationships;
8207
+ }
8208
+ function getLineNumber(content, index) {
8209
+ return content.slice(0, index).split("\n").length;
8210
+ }
8211
+ function resolveRelativeImport(currentFile, importPath) {
8212
+ const dir = path15.dirname(currentFile);
8213
+ let resolved = path15.posix.join(dir, importPath);
8214
+ if (!path15.extname(resolved)) {
8215
+ resolved += ".ts";
8216
+ }
8217
+ return resolved;
8218
+ }
8219
+ function categorizeFile(filePath, language) {
8220
+ const lower = filePath.toLowerCase();
8221
+ if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("__tests__") || lower.includes("/test/") || lower.includes("/tests/")) return "test";
8222
+ if (["json", "yaml", "toml"].includes(language) || lower.includes("config") || lower.includes(".env")) return "config";
8223
+ if (language === "markdown" || lower.includes("/docs/") || lower.includes("/doc/")) return "docs";
8224
+ if (language === "docker" || lower.includes("makefile") || lower.includes("webpack") || lower.includes("rollup") || lower.includes("vite")) return "build";
8225
+ if (["html", "css", "scss", "less"].includes(language)) return "asset";
8226
+ return "source";
8227
+ }
8228
+ function detectClassType(name, content) {
8229
+ if (content.includes(".init(") || content.includes("@Entity") || content.includes("tableName")) return "model";
8230
+ if (name.includes("Controller") || name.includes("Handler")) return "service";
8231
+ if (name.includes("Service") || name.includes("Provider")) return "service";
8232
+ if (name.includes("Middleware")) return "middleware";
8233
+ if (name.includes("Component") || name.includes("Widget")) return "component";
8234
+ return "class";
8235
+ }
8236
+ function detectPythonClassType(name, bases, _content) {
8237
+ if (bases.includes("Model") || bases.includes("Base") || bases.includes("db.Model")) return "model";
8238
+ if (name.includes("View") || name.includes("ViewSet") || bases.includes("APIView")) return "service";
8239
+ if (name.includes("Serializer")) return "class";
8240
+ return "class";
8241
+ }
8242
+ function detectServiceType(name) {
8243
+ const lower = name.toLowerCase();
8244
+ if (lower.includes("redis") || lower.includes("memcache")) return "cache";
8245
+ if (lower.includes("rabbit") || lower.includes("kafka") || lower.includes("nats")) return "queue";
8246
+ if (lower.includes("postgres") || lower.includes("mysql") || lower.includes("mongo") || lower.includes("db")) return "database";
8247
+ return "external-api";
8248
+ }
8249
+
8250
+ // src/scanner/github-cloner.ts
8251
+ init_esm_shims();
8252
+ import * as fs14 from "fs";
8253
+ import * as os from "os";
8254
+ import * as path16 from "path";
8255
+ import { execSync } from "child_process";
8256
+ async function cloneAndScan(options) {
8257
+ const { target, cloneDir, branch, depth = 1, keepClone, onProgress, ...scanOpts } = options;
8258
+ const resolved = resolveTarget(target);
8259
+ let projectDir;
8260
+ if (resolved.type === "local") {
8261
+ projectDir = resolved.path;
8262
+ onProgress?.("clone", 100, `Using local directory: ${projectDir}`);
8263
+ } else {
8264
+ const tempBase = cloneDir || path16.join(os.tmpdir(), "opencroc-scan");
8265
+ fs14.mkdirSync(tempBase, { recursive: true });
8266
+ projectDir = path16.join(tempBase, resolved.repoName);
8267
+ if (fs14.existsSync(projectDir)) {
8268
+ fs14.rmSync(projectDir, { recursive: true, force: true });
8269
+ }
8270
+ onProgress?.("clone", 10, `Cloning ${resolved.url}...`);
8271
+ const branchArg = branch ? `--branch ${branch}` : "";
8272
+ const depthArg = depth > 0 ? `--depth ${depth}` : "";
8273
+ const cmd = `git clone ${branchArg} ${depthArg} --single-branch ${resolved.url} "${projectDir}"`;
8274
+ try {
8275
+ execSync(cmd, {
8276
+ stdio: "pipe",
8277
+ timeout: 12e4
8278
+ // 2 minutes max
8279
+ });
8280
+ } catch (err) {
8281
+ throw new Error(`Failed to clone repository: ${err.message}`);
8282
+ }
8283
+ onProgress?.("clone", 100, `Cloned to ${projectDir}`);
8284
+ }
8285
+ const scanResult = await scanProject({
8286
+ rootDir: projectDir,
8287
+ ...scanOpts,
8288
+ onProgress
8289
+ });
8290
+ if (resolved.type === "git" && !keepClone) {
8291
+ try {
8292
+ fs14.rmSync(projectDir, { recursive: true, force: true });
8293
+ } catch {
8294
+ }
8295
+ }
8296
+ return {
8297
+ ...scanResult,
8298
+ clonedPath: resolved.type === "git" ? projectDir : void 0
8299
+ };
8300
+ }
8301
+ function resolveTarget(target) {
8302
+ const resolved = path16.resolve(target);
8303
+ if (fs14.existsSync(resolved)) {
8304
+ return {
8305
+ type: "local",
8306
+ path: resolved,
8307
+ repoName: path16.basename(resolved)
8308
+ };
8309
+ }
8310
+ if (target.startsWith("https://") || target.startsWith("http://") || target.startsWith("git@")) {
8311
+ let url = target;
8312
+ if (!url.endsWith(".git")) url += ".git";
8313
+ const repoName = path16.basename(url, ".git");
8314
+ return { type: "git", path: "", url, repoName };
8315
+ }
8316
+ if (/^[\w.-]+\/[\w.-]+$/.test(target)) {
8317
+ const url = `https://github.com/${target}.git`;
8318
+ const repoName = target.split("/")[1];
8319
+ return { type: "git", path: "", url, repoName };
8320
+ }
8321
+ throw new Error(
8322
+ `Cannot resolve target "${target}". Expected: local path, GitHub URL, or shorthand (user/repo).`
8323
+ );
8324
+ }
8325
+
8326
+ // src/graph/index.ts
8327
+ init_esm_shims();
8328
+ function buildKnowledgeGraph(scanResult, options) {
8329
+ const startTime = Date.now();
8330
+ const entityMap = /* @__PURE__ */ new Map();
8331
+ for (const entity of scanResult.entities) {
8332
+ if (!entityMap.has(entity.id)) {
8333
+ entityMap.set(entity.id, entity);
8334
+ }
8335
+ }
8336
+ const nodes = [];
8337
+ for (const entity of entityMap.values()) {
8338
+ nodes.push({
8339
+ id: entity.id,
8340
+ label: entity.name,
8341
+ type: entity.type,
8342
+ filePath: entity.filePath || void 0,
8343
+ line: entity.line,
8344
+ module: inferModule(entity),
8345
+ language: entity.language,
8346
+ metadata: entity.metadata,
8347
+ status: "idle"
8348
+ });
8349
+ }
8350
+ const edges = [];
8351
+ const edgeSet = /* @__PURE__ */ new Set();
8352
+ for (const rel of scanResult.relationships) {
8353
+ let targetId = rel.targetId;
8354
+ if (targetId.includes(":*:")) {
8355
+ const suffix = targetId.split(":*:")[1];
8356
+ const resolved = findMatchingEntity(entityMap, targetId.split(":")[0], suffix);
8357
+ if (resolved) {
8358
+ targetId = resolved;
8359
+ } else {
8360
+ continue;
8361
+ }
8362
+ }
8363
+ if (rel.sourceId === targetId) continue;
8364
+ if (!entityMap.has(rel.sourceId) && !entityMap.has(targetId)) continue;
8365
+ const edgeKey = `${rel.sourceId}->${targetId}:${rel.relation}`;
8366
+ if (edgeSet.has(edgeKey)) continue;
8367
+ edgeSet.add(edgeKey);
8368
+ edges.push({
8369
+ id: `edge-${edges.length}`,
8370
+ source: rel.sourceId,
8371
+ target: targetId,
8372
+ relation: rel.relation,
8373
+ metadata: rel.metadata
8374
+ });
8375
+ }
8376
+ const projectInfo = buildProjectMetadata(scanResult, options, nodes);
8377
+ return {
8378
+ nodes,
8379
+ edges,
8380
+ projectInfo,
8381
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
8382
+ buildDuration: Date.now() - startTime
8383
+ };
8384
+ }
8385
+ function queryNodes(graph, filter) {
8386
+ return graph.nodes.filter((n) => {
8387
+ if (filter.type && n.type !== filter.type) return false;
8388
+ if (filter.language && n.language !== filter.language) return false;
8389
+ if (filter.module && n.module !== filter.module) return false;
8390
+ return true;
8391
+ });
8392
+ }
8393
+ function getNeighbors(graph, nodeId) {
8394
+ return {
8395
+ incoming: graph.edges.filter((e) => e.target === nodeId),
8396
+ outgoing: graph.edges.filter((e) => e.source === nodeId)
8397
+ };
8398
+ }
8399
+ function bfsTraversal2(graph, startNodeId, maxDepth = 3) {
8400
+ const visited = /* @__PURE__ */ new Set();
8401
+ const queue = [{ id: startNodeId, depth: 0 }];
8402
+ visited.add(startNodeId);
8403
+ const adjacency = /* @__PURE__ */ new Map();
8404
+ for (const edge of graph.edges) {
8405
+ if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
8406
+ adjacency.get(edge.source).push(edge.target);
8407
+ if (!adjacency.has(edge.target)) adjacency.set(edge.target, []);
8408
+ adjacency.get(edge.target).push(edge.source);
8409
+ }
8410
+ while (queue.length > 0) {
8411
+ const current = queue.shift();
8412
+ if (current.depth >= maxDepth) continue;
8413
+ const neighbors = adjacency.get(current.id) || [];
8414
+ for (const neighbor of neighbors) {
8415
+ if (!visited.has(neighbor)) {
8416
+ visited.add(neighbor);
8417
+ queue.push({ id: neighbor, depth: current.depth + 1 });
8418
+ }
8419
+ }
8420
+ }
8421
+ visited.delete(startNodeId);
8422
+ return [...visited];
8423
+ }
8424
+ function findPaths(graph, fromId, toId, maxPaths = 5, maxDepth = 6) {
8425
+ const paths = [];
8426
+ const adjacency = /* @__PURE__ */ new Map();
8427
+ for (const edge of graph.edges) {
8428
+ if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
8429
+ adjacency.get(edge.source).push(edge.target);
8430
+ }
8431
+ function dfs(current, target, path17, visited2) {
8432
+ if (paths.length >= maxPaths) return;
8433
+ if (path17.length > maxDepth) return;
8434
+ if (current === target) {
8435
+ paths.push([...path17]);
8436
+ return;
8437
+ }
8438
+ const neighbors = adjacency.get(current) || [];
8439
+ for (const neighbor of neighbors) {
8440
+ if (!visited2.has(neighbor)) {
8441
+ visited2.add(neighbor);
8442
+ path17.push(neighbor);
8443
+ dfs(neighbor, target, path17, visited2);
8444
+ path17.pop();
8445
+ visited2.delete(neighbor);
8446
+ }
8447
+ }
8448
+ }
8449
+ const visited = /* @__PURE__ */ new Set([fromId]);
8450
+ dfs(fromId, toId, [fromId], visited);
8451
+ return paths;
8452
+ }
8453
+ function toMermaid(graph, options) {
8454
+ const maxNodes = options?.maxNodes || 50;
8455
+ const nodeTypes = options?.nodeTypes;
8456
+ let filteredNodes = graph.nodes;
8457
+ if (nodeTypes) {
8458
+ filteredNodes = filteredNodes.filter((n) => nodeTypes.includes(n.type));
8459
+ }
8460
+ filteredNodes = filteredNodes.slice(0, maxNodes);
8461
+ const nodeIds = new Set(filteredNodes.map((n) => n.id));
8462
+ const filteredEdges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
8463
+ const lines = ["graph TD"];
8464
+ lines.push(" classDef model fill:#4ecca3,color:#000,stroke:#2d9970");
8465
+ lines.push(" classDef api fill:#e94560,color:#fff,stroke:#c23049");
8466
+ lines.push(" classDef service fill:#3498db,color:#fff,stroke:#2378b8");
8467
+ lines.push(" classDef module fill:#f39c12,color:#000,stroke:#c27d0e");
8468
+ lines.push(" classDef component fill:#9b59b6,color:#fff,stroke:#7d3c98");
8469
+ lines.push(" classDef file fill:#555,color:#fff,stroke:#333");
8470
+ for (const node of filteredNodes) {
8471
+ const safeId = sanitizeMermaidId(node.id);
8472
+ const safeLabel = node.label.replace(/"/g, "'");
8473
+ lines.push(` ${safeId}["${safeLabel}"]:::${node.type}`);
8474
+ }
8475
+ for (const edge of filteredEdges) {
8476
+ const safeSource = sanitizeMermaidId(edge.source);
8477
+ const safeTarget = sanitizeMermaidId(edge.target);
8478
+ const label = edge.relation;
8479
+ lines.push(` ${safeSource} -->|${label}| ${safeTarget}`);
8480
+ }
8481
+ return lines.join("\n");
8482
+ }
8483
+ function getGraphStats(graph) {
8484
+ const stats = {
8485
+ totalNodes: graph.nodes.length,
8486
+ totalEdges: graph.edges.length
8487
+ };
8488
+ for (const node of graph.nodes) {
8489
+ const key = `${node.type}Count`;
8490
+ stats[key] = (stats[key] || 0) + 1;
8491
+ }
8492
+ for (const edge of graph.edges) {
8493
+ const key = `${edge.relation}Count`;
8494
+ stats[key] = (stats[key] || 0) + 1;
8495
+ }
8496
+ return stats;
8497
+ }
8498
+ function inferModule(entity) {
8499
+ if (!entity.filePath) return void 0;
8500
+ const parts = entity.filePath.split("/");
8501
+ if (parts.length > 1) {
8502
+ const dir = parts[0];
8503
+ if (dir === "src" && parts.length > 2) return parts[1];
8504
+ return dir;
8505
+ }
8506
+ return void 0;
8507
+ }
8508
+ function findMatchingEntity(entityMap, typePrefix, nameSuffix) {
8509
+ for (const [id, entity] of entityMap) {
8510
+ if (id.startsWith(`${typePrefix}:`) && entity.name === nameSuffix) {
8511
+ return id;
8512
+ }
8513
+ }
8514
+ return null;
8515
+ }
8516
+ function buildProjectMetadata(scanResult, options, nodes) {
8517
+ const stats = {
8518
+ totalFiles: scanResult.files.length,
8519
+ totalLines: scanResult.files.reduce((sum, f) => sum + f.lines, 0),
8520
+ modules: nodes.filter((n) => n.type === "module").length,
8521
+ classes: nodes.filter((n) => n.type === "class").length,
8522
+ functions: nodes.filter((n) => n.type === "function").length,
8523
+ apiEndpoints: nodes.filter((n) => n.type === "api").length,
8524
+ dataModels: nodes.filter((n) => n.type === "model").length,
8525
+ dependencies: nodes.filter((n) => n.type === "dependency").length,
8526
+ linesByLanguage: scanResult.languages
8527
+ };
8528
+ return {
8529
+ name: options.projectName,
8530
+ source: options.source,
8531
+ sourceUrl: options.sourceUrl,
8532
+ rootPath: options.rootPath,
8533
+ languages: scanResult.languages,
8534
+ frameworks: scanResult.frameworks.map((f) => f.name),
8535
+ packageManager: void 0,
8536
+ projectType: "unknown",
8537
+ stats
8538
+ };
8539
+ }
8540
+ function sanitizeMermaidId(id) {
8541
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
8542
+ }
8543
+
8544
+ // src/insight/index.ts
8545
+ init_esm_shims();
8546
+ async function analyzeRisks(graph, options) {
8547
+ const risks = [];
8548
+ let riskCounter = 0;
8549
+ options?.onProgress?.("risk-analysis", 0, "Starting risk analysis...");
8550
+ const apis = graph.nodes.filter((n) => n.type === "api");
8551
+ const middlewares = graph.nodes.filter((n) => n.type === "middleware");
8552
+ const hasAuthMiddleware = middlewares.some(
8553
+ (m) => m.label.toLowerCase().includes("auth") || m.label.toLowerCase().includes("jwt") || m.label.toLowerCase().includes("session")
8554
+ );
8555
+ for (const api of apis) {
8556
+ const incoming = graph.edges.filter((e) => e.target === api.id);
8557
+ const hasAuth = incoming.some((e) => {
8558
+ const sourceNode = graph.nodes.find((n) => n.id === e.source);
8559
+ return sourceNode?.type === "middleware" && (sourceNode.label.toLowerCase().includes("auth") || e.relation === "middleware-of");
8560
+ });
8561
+ if (!hasAuth && !hasAuthMiddleware) {
8562
+ const apiPath = api.metadata.path || api.label;
8563
+ const isSensitive = /user|admin|password|token|secret|key|delete|payment/i.test(apiPath);
8564
+ if (isSensitive) {
8565
+ risks.push({
8566
+ id: `risk-${++riskCounter}`,
8567
+ category: "security",
8568
+ severity: "high",
8569
+ title: `Potentially unprotected sensitive endpoint: ${api.label}`,
8570
+ description: `The endpoint ${api.label} appears to handle sensitive data but no authentication middleware was detected in the graph.`,
8571
+ affectedNodes: [api.id],
8572
+ suggestion: "Add authentication middleware to protect this endpoint.",
8573
+ confidence: 0.6
8574
+ });
8575
+ }
8576
+ }
8577
+ }
8578
+ options?.onProgress?.("risk-analysis", 20, "Checking data integrity...");
8579
+ const models = graph.nodes.filter((n) => n.type === "model");
8580
+ for (const model of models) {
8581
+ const writeEdges = graph.edges.filter((e) => e.target === model.id && e.relation === "writes");
8582
+ if (writeEdges.length > 3) {
8583
+ risks.push({
8584
+ id: `risk-${++riskCounter}`,
8585
+ category: "data-integrity",
8586
+ severity: "medium",
8587
+ title: `High write fan-in on model: ${model.label}`,
8588
+ description: `Model "${model.label}" is written to by ${writeEdges.length} different endpoints. This increases risk of data conflicts and race conditions.`,
8589
+ affectedNodes: [model.id, ...writeEdges.map((e) => e.source)],
8590
+ suggestion: "Consider adding transaction boundaries or an optimistic locking strategy.",
8591
+ confidence: 0.7
8592
+ });
8593
+ }
8594
+ }
8595
+ const foreignKeyEdges = graph.edges.filter((e) => e.relation === "foreign-key" || e.relation === "cascade-delete");
8596
+ for (const fk of foreignKeyEdges) {
8597
+ const sourceNode = graph.nodes.find((n) => n.id === fk.source);
8598
+ const targetNode = graph.nodes.find((n) => n.id === fk.target);
8599
+ if (sourceNode && targetNode) {
8600
+ const dependents = graph.edges.filter(
8601
+ (e) => (e.relation === "foreign-key" || e.relation === "cascade-delete") && e.target === fk.target
8602
+ );
8603
+ if (dependents.length >= 3) {
8604
+ risks.push({
8605
+ id: `risk-${++riskCounter}`,
8606
+ category: "data-integrity",
8607
+ severity: "high",
8608
+ title: `Cascade risk: ${targetNode.label} has ${dependents.length} dependent tables`,
8609
+ description: `Deleting records from "${targetNode.label}" could cascade to ${dependents.length} other tables.`,
8610
+ affectedNodes: [fk.target, ...dependents.map((d) => d.source)],
8611
+ suggestion: "Implement soft deletes or add cascade protection.",
8612
+ confidence: 0.85
8613
+ });
8614
+ }
8615
+ }
8616
+ }
8617
+ options?.onProgress?.("risk-analysis", 40, "Checking performance...");
8618
+ const moduleNodes = graph.nodes.filter((n) => n.type === "module");
8619
+ for (const mod of moduleNodes) {
8620
+ const children = graph.edges.filter((e) => e.target === mod.id && e.relation === "belongs-to");
8621
+ if (children.length > 50) {
8622
+ risks.push({
8623
+ id: `risk-${++riskCounter}`,
8624
+ category: "maintainability",
8625
+ severity: "medium",
8626
+ title: `Large module: ${mod.label} (${children.length} entities)`,
8627
+ description: `Module "${mod.label}" contains ${children.length} entities. Consider splitting for better maintainability.`,
8628
+ affectedNodes: [mod.id],
8629
+ suggestion: "Split into smaller, focused sub-modules.",
8630
+ confidence: 0.75
8631
+ });
8632
+ }
8633
+ }
8634
+ const cycles = detectCycles2(graph);
8635
+ for (const cycle of cycles) {
8636
+ risks.push({
8637
+ id: `risk-${++riskCounter}`,
8638
+ category: "logic",
8639
+ severity: "high",
8640
+ title: `Circular dependency detected: ${cycle.map((id) => graph.nodes.find((n) => n.id === id)?.label || id).join(" \u2192 ")}`,
8641
+ description: `A circular dependency was found involving ${cycle.length} entities. This can cause initialization issues and makes testing harder.`,
8642
+ affectedNodes: cycle,
8643
+ suggestion: "Break the cycle by introducing an interface or Event-based decoupling.",
8644
+ confidence: 0.9
8645
+ });
8646
+ }
8647
+ options?.onProgress?.("risk-analysis", 60, "Checking maintainability...");
8648
+ for (const node of graph.nodes) {
8649
+ if (node.type === "file" || node.type === "dependency") continue;
8650
+ const outgoing = graph.edges.filter((e) => e.source === node.id);
8651
+ const incoming = graph.edges.filter((e) => e.target === node.id);
8652
+ const coupling = outgoing.length + incoming.length;
8653
+ if (coupling > 15) {
8654
+ risks.push({
8655
+ id: `risk-${++riskCounter}`,
8656
+ category: "maintainability",
8657
+ severity: "medium",
8658
+ title: `High coupling: ${node.label} (${coupling} connections)`,
8659
+ description: `"${node.label}" has ${coupling} connections (${outgoing.length} outgoing, ${incoming.length} incoming). Changes here will have wide impact.`,
8660
+ affectedNodes: [node.id],
8661
+ suggestion: "Consider extracting shared logic or adding an abstraction layer.",
8662
+ confidence: 0.7
8663
+ });
8664
+ }
8665
+ }
8666
+ for (const api of apis) {
8667
+ const apiPath = api.metadata.path || api.label;
8668
+ if (apiPath.includes(":") || apiPath.includes("{")) {
8669
+ const method = api.metadata.method || "";
8670
+ if (["DELETE", "PUT", "PATCH"].includes(method)) {
8671
+ risks.push({
8672
+ id: `risk-${++riskCounter}`,
8673
+ category: "security",
8674
+ severity: "low",
8675
+ title: `Verify input validation: ${api.label}`,
8676
+ description: `Endpoint ${api.label} accepts path parameters. Ensure proper input validation and authorization checks.`,
8677
+ affectedNodes: [api.id],
8678
+ suggestion: "Add input validation middleware and verify the user has permission to modify the specified resource.",
8679
+ confidence: 0.5
8680
+ });
8681
+ }
8682
+ }
8683
+ }
8684
+ options?.onProgress?.("risk-analysis", 80, `Found ${risks.length} risks`);
8685
+ if (options?.useLlm && options.llm) {
8686
+ try {
8687
+ const llmRisks = await getLlmRisks(graph, risks, options.llm);
8688
+ risks.push(...llmRisks);
8689
+ } catch {
8690
+ }
8691
+ }
8692
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
8693
+ risks.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
8694
+ options?.onProgress?.("risk-analysis", 100, `Analysis complete: ${risks.length} risks found`);
8695
+ return risks;
8696
+ }
8697
+ function analyzeImpact(graph, nodeId) {
8698
+ const node = graph.nodes.find((n) => n.id === nodeId);
8699
+ if (!node) {
8700
+ return {
8701
+ sourceNode: nodeId,
8702
+ directImpact: [],
8703
+ transitiveImpact: [],
8704
+ riskLevel: "low",
8705
+ summary: `Node "${nodeId}" not found in the graph.`,
8706
+ mermaidText: ""
8707
+ };
8708
+ }
8709
+ const { incoming, outgoing } = getNeighbors(graph, nodeId);
8710
+ const directNodes = /* @__PURE__ */ new Set([
8711
+ ...incoming.map((e) => e.source),
8712
+ ...outgoing.map((e) => e.target)
8713
+ ]);
8714
+ directNodes.delete(nodeId);
8715
+ const transitiveNodes = bfsTraversal2(graph, nodeId, 3);
8716
+ const totalImpact = transitiveNodes.length;
8717
+ let riskLevel;
8718
+ if (totalImpact > 20) riskLevel = "critical";
8719
+ else if (totalImpact > 10) riskLevel = "high";
8720
+ else if (totalImpact > 5) riskLevel = "medium";
8721
+ else riskLevel = "low";
8722
+ const summary = `Changing "${node.label}" directly affects ${directNodes.size} entities and transitively impacts ${transitiveNodes.length} entities (risk: ${riskLevel}).`;
8723
+ const impactNodeIds = /* @__PURE__ */ new Set([nodeId, ...directNodes, ...transitiveNodes.slice(0, 20)]);
8724
+ const impactNodes = graph.nodes.filter((n) => impactNodeIds.has(n.id));
8725
+ const impactEdges = graph.edges.filter((e) => impactNodeIds.has(e.source) && impactNodeIds.has(e.target));
8726
+ let mermaidText = "graph TD\n";
8727
+ mermaidText += ` style ${sanitizeId2(nodeId)} fill:#e94560,color:#fff
8728
+ `;
8729
+ for (const dn of directNodes) {
8730
+ mermaidText += ` style ${sanitizeId2(dn)} fill:#f39c12,color:#000
8731
+ `;
8732
+ }
8733
+ for (const n of impactNodes) {
8734
+ mermaidText += ` ${sanitizeId2(n.id)}["${n.label.replace(/"/g, "'")}"]
8735
+ `;
8736
+ }
8737
+ for (const e of impactEdges) {
8738
+ mermaidText += ` ${sanitizeId2(e.source)} -->|${e.relation}| ${sanitizeId2(e.target)}
8739
+ `;
8740
+ }
8741
+ return {
8742
+ sourceNode: nodeId,
8743
+ directImpact: [...directNodes],
8744
+ transitiveImpact: transitiveNodes,
8745
+ riskLevel,
8746
+ summary,
8747
+ mermaidText
8748
+ };
8749
+ }
8750
+ async function generateReport(graph, perspective, risks, options) {
8751
+ if (options?.useLlm && options.llm) {
8752
+ return generateLlmReport(graph, perspective, risks, options.llm);
8753
+ }
8754
+ switch (perspective) {
8755
+ case "developer":
8756
+ return buildDeveloperReport(graph, risks);
8757
+ case "architect":
8758
+ return buildArchitectReport(graph, risks);
8759
+ case "tester":
8760
+ return buildTesterReport(graph, risks);
8761
+ case "product":
8762
+ return buildProductReport(graph, risks);
8763
+ case "student":
8764
+ return buildStudentReport(graph, risks);
8765
+ case "executive":
8766
+ return buildExecutiveReport(graph, risks);
8767
+ default:
8768
+ return buildDeveloperReport(graph, risks);
8769
+ }
8770
+ }
8771
+ function buildDeveloperReport(graph, risks) {
8772
+ const { projectInfo } = graph;
8773
+ const stats = projectInfo.stats;
8774
+ const sections = [
8775
+ {
8776
+ heading: "Project Overview",
8777
+ content: `**${projectInfo.name}** is a ${projectInfo.projectType} project using ${projectInfo.frameworks.join(", ") || "unknown frameworks"}.
8778
+
8779
+ - **Files**: ${stats.totalFiles} | **Lines**: ${stats.totalLines.toLocaleString()}
8780
+ - **Languages**: ${Object.entries(projectInfo.languages).map(([k, v]) => `${k}(${v})`).join(", ")}
8781
+ - **APIs**: ${stats.apiEndpoints} | **Models**: ${stats.dataModels} | **Functions**: ${stats.functions}`
8782
+ },
8783
+ {
8784
+ heading: "Architecture Map",
8785
+ content: "Module-level dependency graph:",
8786
+ visualization: {
8787
+ type: "mermaid",
8788
+ data: toMermaid(graph, { nodeTypes: ["module", "model", "api"], maxNodes: 30 })
8789
+ }
8790
+ },
8791
+ {
8792
+ heading: "API Endpoints",
8793
+ content: graph.nodes.filter((n) => n.type === "api").map((a) => `- \`${a.label}\` (${a.filePath || "unknown"})`).join("\n") || "No API endpoints detected."
8794
+ },
8795
+ {
8796
+ heading: "Data Models",
8797
+ content: graph.nodes.filter((n) => n.type === "model").map((m) => `- **${m.label}** (${m.filePath || "unknown"})`).join("\n") || "No data models detected."
8798
+ },
8799
+ {
8800
+ heading: "Risk Report",
8801
+ content: risks.length === 0 ? "No significant risks detected." : risks.slice(0, 10).map(
8802
+ (r) => `- **[${r.severity.toUpperCase()}]** ${r.title}
8803
+ ${r.description}`
8804
+ ).join("\n\n")
8805
+ }
8806
+ ];
8807
+ return {
8808
+ perspective: "developer",
8809
+ title: `Developer Report: ${projectInfo.name}`,
8810
+ summary: `${projectInfo.name} \u2014 ${stats.apiEndpoints} APIs, ${stats.dataModels} models, ${risks.length} risks detected.`,
8811
+ sections,
8812
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8813
+ };
8814
+ }
8815
+ function buildArchitectReport(graph, risks) {
8816
+ const { projectInfo } = graph;
8817
+ const modules = graph.nodes.filter((n) => n.type === "module");
8818
+ const criticalRisks = risks.filter((r) => r.severity === "critical" || r.severity === "high");
8819
+ const sections = [
8820
+ {
8821
+ heading: "System Architecture",
8822
+ content: `**Type**: ${projectInfo.projectType}
8823
+ **Frameworks**: ${projectInfo.frameworks.join(", ")}
8824
+ **Modules**: ${modules.length}
8825
+
8826
+ The system is organized into ${modules.length} modules with ${graph.edges.length} relationships.`,
8827
+ visualization: {
8828
+ type: "mermaid",
8829
+ data: toMermaid(graph, { nodeTypes: ["module"], maxNodes: 20 })
8830
+ }
8831
+ },
8832
+ {
8833
+ heading: "Module Coupling Analysis",
8834
+ content: modules.map((m) => {
8835
+ const edges = graph.edges.filter((e) => e.source === m.id || e.target === m.id);
8836
+ return `- **${m.label}**: ${edges.length} connections`;
8837
+ }).join("\n")
8838
+ },
8839
+ {
8840
+ heading: "Technical Debt & Risk",
8841
+ content: criticalRisks.length === 0 ? "No critical or high-severity risks." : criticalRisks.map((r) => `- **[${r.severity}]** ${r.title}
8842
+ _Suggestion_: ${r.suggestion || "N/A"}`).join("\n\n")
8843
+ },
8844
+ {
8845
+ heading: "Recommendations",
8846
+ content: generateArchitectRecommendations(graph, risks)
8847
+ }
8848
+ ];
8849
+ return {
8850
+ perspective: "architect",
8851
+ title: `Architecture Report: ${projectInfo.name}`,
8852
+ summary: `${modules.length} modules, ${graph.edges.length} relationships, ${criticalRisks.length} critical/high risks.`,
8853
+ sections,
8854
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8855
+ };
8856
+ }
8857
+ function buildTesterReport(graph, risks) {
8858
+ const apis = graph.nodes.filter((n) => n.type === "api");
8859
+ const tests = graph.nodes.filter((n) => n.type === "test");
8860
+ const riskyApis = risks.filter((r) => r.category === "security" || r.category === "data-integrity");
8861
+ const sections = [
8862
+ {
8863
+ heading: "Test Coverage Overview",
8864
+ content: `- **API Endpoints**: ${apis.length}
8865
+ - **Test Files Found**: ${tests.length}
8866
+ - **Estimated Coverage**: ${tests.length > 0 ? Math.min(Math.round(tests.length / Math.max(apis.length, 1) * 100), 100) : 0}%`
8867
+ },
8868
+ {
8869
+ heading: "Priority Test Targets",
8870
+ content: "Endpoints with highest risk that need testing first:\n\n" + riskyApis.slice(0, 10).map((r, i) => `${i + 1}. **${r.title}** (${r.severity})
8871
+ ${r.description}`).join("\n\n")
8872
+ },
8873
+ {
8874
+ heading: "Edge Cases to Consider",
8875
+ content: apis.slice(0, 10).map((api) => {
8876
+ const method = api.metadata.method || "ANY";
8877
+ const suggestions = [];
8878
+ if (method === "POST" || method === "PUT") suggestions.push("Empty body", "Invalid types", "Missing required fields", "Extremely long strings");
8879
+ if (method === "DELETE") suggestions.push("Non-existent ID", "Already deleted", "ID with dependencies");
8880
+ if (method === "GET") suggestions.push("Invalid query params", "Large pagination", "Non-existent ID");
8881
+ return `- **${api.label}**: ${suggestions.join(", ")}`;
8882
+ }).join("\n")
8883
+ }
8884
+ ];
8885
+ return {
8886
+ perspective: "tester",
8887
+ title: `Testing Report: ${graph.projectInfo.name}`,
8888
+ summary: `${apis.length} endpoints to test, ${riskyApis.length} high-risk areas identified.`,
8889
+ sections,
8890
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8891
+ };
8892
+ }
8893
+ function buildProductReport(graph, risks) {
8894
+ const { projectInfo } = graph;
8895
+ const modules = graph.nodes.filter((n) => n.type === "module");
8896
+ const sections = [
8897
+ {
8898
+ heading: "What Does This System Do?",
8899
+ content: `This is a **${projectInfo.projectType}** system built with ${projectInfo.frameworks.join(", ")}. It contains ${modules.length} functional modules and ${projectInfo.stats.apiEndpoints} service interfaces.`
8900
+ },
8901
+ {
8902
+ heading: "Feature Map",
8903
+ content: modules.map((m) => {
8904
+ const children = graph.edges.filter((e) => e.target === m.id).length;
8905
+ return `- **${m.label}** \u2014 ${children} components`;
8906
+ }).join("\n")
8907
+ },
8908
+ {
8909
+ heading: "Health Status",
8910
+ content: (() => {
8911
+ const critical = risks.filter((r) => r.severity === "critical").length;
8912
+ const high = risks.filter((r) => r.severity === "high").length;
8913
+ if (critical > 0) return `\u26A0\uFE0F **Needs Attention**: ${critical} critical issues found that could affect users.`;
8914
+ if (high > 3) return `\u26A1 **Minor Concerns**: ${high} areas that should be improved.`;
8915
+ return "\u2705 **Healthy**: No critical issues detected. System is in good shape.";
8916
+ })()
8917
+ }
8918
+ ];
8919
+ return {
8920
+ perspective: "product",
8921
+ title: `Product Overview: ${projectInfo.name}`,
8922
+ summary: `${modules.length} feature modules, ${risks.filter((r) => r.severity === "critical").length} critical issues.`,
8923
+ sections,
8924
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8925
+ };
8926
+ }
8927
+ function buildStudentReport(graph, risks) {
8928
+ const { projectInfo } = graph;
8929
+ const sections = [
8930
+ {
8931
+ heading: "What is this project?",
8932
+ content: `This is a **${projectInfo.projectType}** project. Let's break it down step by step!
8933
+
8934
+ **Languages used**: ${Object.keys(projectInfo.languages).join(", ")}
8935
+ **Frameworks**: ${projectInfo.frameworks.join(", ") || "None detected"}
8936
+
8937
+ Think of this project like a building:
8938
+ - The **frameworks** are the building's foundation
8939
+ - The **modules** are different rooms
8940
+ - The **APIs** are the doors and windows (interfaces to the outside world)
8941
+ - The **models** are the furniture and storage (data structures)`
8942
+ },
8943
+ {
8944
+ heading: "How is it organized?",
8945
+ content: `The project has **${projectInfo.stats.modules}** modules (think: folders of related code).
8946
+
8947
+ Each module typically contains:
8948
+ 1. **Controllers/Routes** \u2014 Handle incoming requests (like a receptionist)
8949
+ 2. **Services** \u2014 Business logic (like the workers)
8950
+ 3. **Models** \u2014 Data structures (like forms and documents)
8951
+
8952
+ Here's a simplified view:`,
8953
+ visualization: {
8954
+ type: "mermaid",
8955
+ data: toMermaid(graph, { nodeTypes: ["module", "model"], maxNodes: 15 })
8956
+ }
8957
+ },
8958
+ {
8959
+ heading: "Key Concepts to Learn",
8960
+ content: `Based on this project, you should study:
8961
+
8962
+ ` + (projectInfo.frameworks.includes("Express") ? "- **Express.js** \u2014 Node.js web framework for building APIs\n" : "") + (projectInfo.frameworks.includes("React") ? "- **React** \u2014 Frontend UI library for building user interfaces\n" : "") + (projectInfo.frameworks.includes("Sequelize") ? "- **Sequelize** \u2014 ORM for database operations\n" : "") + `- **REST APIs** \u2014 How the frontend talks to the backend
8963
+ - **MVC Pattern** \u2014 Model-View-Controller architecture
8964
+ - **Authentication** \u2014 How users log in and stay logged in`
8965
+ },
8966
+ {
8967
+ heading: "Things to Watch Out For",
8968
+ content: risks.length > 0 ? `Here are ${Math.min(risks.length, 5)} interesting issues found:
8969
+
8970
+ ` + risks.slice(0, 5).map((r, i) => `${i + 1}. **${r.title}**
8971
+ _Why it matters_: ${r.description}
8972
+ _How to fix_: ${r.suggestion || "Research this topic!"}`).join("\n\n") : "This project looks clean! No major issues found."
8973
+ }
8974
+ ];
8975
+ return {
8976
+ perspective: "student",
8977
+ title: `Learning Guide: ${projectInfo.name}`,
8978
+ summary: `A ${projectInfo.projectType} project \u2014 great for learning ${Object.keys(projectInfo.languages).join(", ")}!`,
8979
+ sections,
8980
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8981
+ };
8982
+ }
8983
+ function buildExecutiveReport(graph, risks) {
8984
+ const { projectInfo } = graph;
8985
+ const critical = risks.filter((r) => r.severity === "critical").length;
8986
+ const high = risks.filter((r) => r.severity === "high").length;
8987
+ const medium = risks.filter((r) => r.severity === "medium").length;
8988
+ const healthScore = Math.max(0, 100 - (critical * 20 + high * 10 + medium * 3));
8989
+ const sections = [
8990
+ {
8991
+ heading: "Health Score",
8992
+ content: `# ${healthScore}/100
8993
+
8994
+ ` + (healthScore >= 80 ? "\u2705 System is healthy and well-maintained." : healthScore >= 60 ? "\u26A1 System needs some attention. Address high-priority items." : "\u26A0\uFE0F System has significant issues that need immediate attention.")
8995
+ },
8996
+ {
8997
+ heading: "Key Metrics",
8998
+ content: `| Metric | Value |
8999
+ |--------|-------|
9000
+ | Codebase Size | ${projectInfo.stats.totalLines.toLocaleString()} lines |
9001
+ | Technologies | ${projectInfo.frameworks.length} frameworks |
9002
+ | API Surface | ${projectInfo.stats.apiEndpoints} endpoints |
9003
+ | Data Models | ${projectInfo.stats.dataModels} tables |
9004
+ | Critical Issues | ${critical} |
9005
+ | High Issues | ${high} |`
9006
+ },
9007
+ {
9008
+ heading: "Top 3 Risks Needing Action",
9009
+ content: risks.slice(0, 3).map(
9010
+ (r, i) => `${i + 1}. **${r.title}** (${r.severity})`
9011
+ ).join("\n") || "No significant risks."
9012
+ }
9013
+ ];
9014
+ return {
9015
+ perspective: "executive",
9016
+ title: `Executive Summary: ${projectInfo.name}`,
9017
+ summary: `Health: ${healthScore}/100 | ${critical} critical, ${high} high risks | ${projectInfo.stats.apiEndpoints} APIs`,
9018
+ sections,
9019
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
9020
+ };
9021
+ }
9022
+ async function simulateScenario(graph, scenario, llm) {
9023
+ const relatedNodes = scenario.steps.map((s) => s.endpoint).filter(Boolean).flatMap((endpoint) => graph.nodes.filter((n) => n.type === "api" && n.label.includes(endpoint))).map((n) => n.id);
9024
+ const context = relatedNodes.flatMap((nodeId) => {
9025
+ const neighbors = getNeighbors(graph, nodeId);
9026
+ return [
9027
+ ...neighbors.outgoing.map((e) => {
9028
+ const target = graph.nodes.find((n) => n.id === e.target);
9029
+ return `${e.relation}: ${target?.label || e.target}`;
9030
+ })
9031
+ ];
9032
+ });
9033
+ const prompt = `You are an expert software engineer analyzing a system.
9034
+
9035
+ Project: ${graph.projectInfo.name}
9036
+ Frameworks: ${graph.projectInfo.frameworks.join(", ")}
9037
+
9038
+ Scenario: ${scenario.name}
9039
+ Description: ${scenario.description}
9040
+
9041
+ Steps:
9042
+ ${scenario.steps.map((s, i) => `${i + 1}. ${s.action} \u2014 ${s.endpoint || "N/A"}`).join("\n")}
9043
+
9044
+ Related context from knowledge graph:
9045
+ ${context.join("\n")}
9046
+
9047
+ Predict:
9048
+ 1. What would happen when executing this scenario?
9049
+ 2. What anomalies or edge cases could occur?
9050
+ 3. Rate the risk (0-100) of this scenario failing in production.
9051
+
9052
+ Respond in JSON: { "prediction": "...", "anomalies": ["..."], "riskScore": N, "confidence": 0.X }`;
9053
+ const response = await llm.chat([{ role: "user", content: prompt }]);
9054
+ try {
9055
+ const parsed = JSON.parse(cleanJsonResponse(response));
9056
+ return {
9057
+ scenario,
9058
+ prediction: parsed.prediction || "Unable to predict.",
9059
+ anomalies: parsed.anomalies || [],
9060
+ riskScore: parsed.riskScore || 50,
9061
+ confidence: parsed.confidence || 0.5
9062
+ };
9063
+ } catch {
9064
+ return {
9065
+ scenario,
9066
+ prediction: response,
9067
+ anomalies: [],
9068
+ riskScore: 50,
9069
+ confidence: 0.3
9070
+ };
9071
+ }
9072
+ }
9073
+ function detectCycles2(graph) {
9074
+ const cycles = [];
9075
+ const adjacency = /* @__PURE__ */ new Map();
9076
+ for (const edge of graph.edges) {
9077
+ if (edge.relation !== "imports" && edge.relation !== "depends-on" && edge.relation !== "calls") continue;
9078
+ if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
9079
+ adjacency.get(edge.source).push(edge.target);
9080
+ }
9081
+ const visited = /* @__PURE__ */ new Set();
9082
+ const inStack = /* @__PURE__ */ new Set();
9083
+ const path17 = [];
9084
+ function dfs(node) {
9085
+ if (cycles.length >= 5) return;
9086
+ visited.add(node);
9087
+ inStack.add(node);
9088
+ path17.push(node);
9089
+ for (const neighbor of adjacency.get(node) || []) {
9090
+ if (!visited.has(neighbor)) {
9091
+ dfs(neighbor);
9092
+ } else if (inStack.has(neighbor)) {
9093
+ const cycleStart = path17.indexOf(neighbor);
9094
+ if (cycleStart >= 0) {
9095
+ cycles.push([...path17.slice(cycleStart), neighbor]);
9096
+ }
9097
+ }
9098
+ }
9099
+ path17.pop();
9100
+ inStack.delete(node);
9101
+ }
9102
+ for (const node of adjacency.keys()) {
9103
+ if (!visited.has(node)) {
9104
+ dfs(node);
9105
+ }
9106
+ }
9107
+ return cycles;
9108
+ }
9109
+ function generateArchitectRecommendations(_graph, risks) {
9110
+ const items = [];
9111
+ const securityRisks = risks.filter((r) => r.category === "security");
9112
+ if (securityRisks.length > 0) {
9113
+ items.push(`1. **Security Hardening**: Address ${securityRisks.length} security findings before next release.`);
9114
+ }
9115
+ const couplingRisks = risks.filter((r) => r.category === "maintainability");
9116
+ if (couplingRisks.length > 2) {
9117
+ items.push(`2. **Reduce Coupling**: ${couplingRisks.length} modules show high coupling. Consider introducing service boundaries.`);
9118
+ }
9119
+ const dataRisks = risks.filter((r) => r.category === "data-integrity");
9120
+ if (dataRisks.length > 0) {
9121
+ items.push(`3. **Data Protection**: ${dataRisks.length} data integrity concerns. Add transaction boundaries and cascade protections.`);
9122
+ }
9123
+ if (items.length === 0) {
9124
+ items.push("Architecture looks solid. Continue monitoring coupling metrics as the system grows.");
9125
+ }
9126
+ return items.join("\n\n");
9127
+ }
9128
+ async function getLlmRisks(graph, existingRisks, llm) {
9129
+ const prompt = `Analyze this project knowledge graph for additional risks not already identified.
9130
+
9131
+ Project: ${graph.projectInfo.name}
9132
+ Type: ${graph.projectInfo.projectType}
9133
+ Frameworks: ${graph.projectInfo.frameworks.join(", ")}
9134
+ Stats: ${graph.projectInfo.stats.apiEndpoints} APIs, ${graph.projectInfo.stats.dataModels} models
9135
+
9136
+ Already identified risks (${existingRisks.length}):
9137
+ ${existingRisks.slice(0, 5).map((r) => `- [${r.severity}] ${r.title}`).join("\n")}
9138
+
9139
+ API Endpoints: ${graph.nodes.filter((n) => n.type === "api").slice(0, 20).map((n) => n.label).join(", ")}
9140
+ Models: ${graph.nodes.filter((n) => n.type === "model").slice(0, 20).map((n) => n.label).join(", ")}
9141
+
9142
+ Return up to 3 additional risks in JSON array format:
9143
+ [{ "category": "security|performance|data-integrity|logic|maintainability|reliability", "severity": "critical|high|medium|low", "title": "...", "description": "...", "suggestion": "..." }]`;
9144
+ const response = await llm.chat([{ role: "user", content: prompt }]);
9145
+ try {
9146
+ const parsed = JSON.parse(cleanJsonResponse(response));
9147
+ if (!Array.isArray(parsed)) return [];
9148
+ return parsed.slice(0, 3).map((r, i) => ({
9149
+ id: `risk-llm-${i}`,
9150
+ category: r.category || "logic",
9151
+ severity: r.severity || "medium",
9152
+ title: r.title || "LLM-detected risk",
9153
+ description: r.description || "",
9154
+ affectedNodes: [],
9155
+ suggestion: r.suggestion,
9156
+ confidence: 0.6
9157
+ }));
9158
+ } catch {
9159
+ return [];
9160
+ }
9161
+ }
9162
+ async function generateLlmReport(graph, perspective, risks, llm) {
9163
+ const perspectiveDescriptions = {
9164
+ developer: "a software developer who wants technical details, code patterns, and API documentation",
9165
+ architect: "a software architect who cares about modularity, coupling, tech debt, and system design",
9166
+ tester: "a QA engineer who wants to know what to test, edge cases, and risk areas",
9167
+ product: "a product manager who wants to understand features in business terms, not code",
9168
+ student: "a computer science student learning from this codebase, explain concepts step by step",
9169
+ executive: "a CTO/VP who wants a one-page health summary with actionable insights"
9170
+ };
9171
+ const prompt = `Generate a project analysis report for ${graph.projectInfo.name} from the perspective of ${perspectiveDescriptions[perspective]}.
9172
+
9173
+ Project Info:
9174
+ - Type: ${graph.projectInfo.projectType}
9175
+ - Frameworks: ${graph.projectInfo.frameworks.join(", ")}
9176
+ - Stats: ${graph.projectInfo.stats.totalFiles} files, ${graph.projectInfo.stats.apiEndpoints} APIs, ${graph.projectInfo.stats.dataModels} models
9177
+ - Languages: ${Object.entries(graph.projectInfo.languages).map(([k, v]) => `${k}(${v} files)`).join(", ")}
9178
+
9179
+ Top risks:
9180
+ ${risks.slice(0, 5).map((r) => `- [${r.severity}] ${r.title}`).join("\n")}
9181
+
9182
+ Generate 3-5 report sections with clear headings and content. Use markdown formatting.
9183
+ Respond in JSON: { "title": "...", "summary": "...", "sections": [{ "heading": "...", "content": "..." }] }`;
9184
+ const response = await llm.chat([{ role: "user", content: prompt }]);
9185
+ try {
9186
+ const parsed = JSON.parse(cleanJsonResponse(response));
9187
+ return {
9188
+ perspective,
9189
+ title: parsed.title || `${perspective} Report`,
9190
+ summary: parsed.summary || "",
9191
+ sections: (parsed.sections || []).map((s) => ({
9192
+ heading: s.heading || "Section",
9193
+ content: s.content || ""
9194
+ })),
9195
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
9196
+ };
9197
+ } catch {
9198
+ return buildDeveloperReport(graph, risks);
9199
+ }
9200
+ }
9201
+ function cleanJsonResponse(response) {
9202
+ let cleaned = response.trim();
9203
+ if (cleaned.startsWith("```json")) cleaned = cleaned.slice(7);
9204
+ else if (cleaned.startsWith("```")) cleaned = cleaned.slice(3);
9205
+ if (cleaned.endsWith("```")) cleaned = cleaned.slice(0, -3);
9206
+ return cleaned.trim();
9207
+ }
9208
+ function sanitizeId2(id) {
9209
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
9210
+ }
7145
9211
  export {
7146
9212
  NetworkMonitor,
7147
9213
  SYSTEM_PROMPTS,
@@ -7149,14 +9215,18 @@ export {
7149
9215
  COMMANDS as VSCODE_COMMANDS,
7150
9216
  aggregateLogCompletion,
7151
9217
  analyzeFailureWithLLM,
9218
+ analyzeImpact,
9219
+ analyzeRisks,
7152
9220
  applyControlledFix,
7153
9221
  autoFix,
9222
+ bfsTraversal2 as bfsTraversal,
7154
9223
  buildBackendChecklist,
7155
9224
  buildClassToTableMap,
7156
9225
  buildDashboardDataFromPipeline,
7157
9226
  buildDashboardDataFromReportJson,
7158
9227
  buildFailureSummary,
7159
9228
  buildGraph,
9229
+ buildKnowledgeGraph,
7160
9230
  buildModuleTree,
7161
9231
  buildPath,
7162
9232
  buildStatusTree,
@@ -7165,6 +9235,7 @@ export {
7165
9235
  categorizeFailure,
7166
9236
  classNameToTableName,
7167
9237
  classifyFailure,
9238
+ cloneAndScan,
7168
9239
  compareTestRuns,
7169
9240
  createAdapter,
7170
9241
  createApiChainAnalyzer,
@@ -7195,9 +9266,11 @@ export {
7195
9266
  definePlugin,
7196
9267
  detectAdapter,
7197
9268
  detectCycles,
9269
+ detectProject,
7198
9270
  extractIdFromText,
7199
9271
  extractParamNames,
7200
9272
  extractParamsFromHref,
9273
+ findPaths,
7201
9274
  formatComparisonReport,
7202
9275
  formatValidationResult,
7203
9276
  generateAllModuleConfigs,
@@ -7212,6 +9285,7 @@ export {
7212
9285
  generateGlobalSetup,
7213
9286
  generateGlobalTeardown,
7214
9287
  generateHtmlReport,
9288
+ generateReport as generateInsightReport,
7215
9289
  generateJsonReport,
7216
9290
  generateMarkdownReport,
7217
9291
  generateModuleConfig,
@@ -7219,7 +9293,9 @@ export {
7219
9293
  generateReports,
7220
9294
  generateVisualDashboard,
7221
9295
  generateVisualDashboardHtml,
9296
+ getGraphStats,
7222
9297
  getModulePreset,
9298
+ getNeighbors,
7223
9299
  inferDependencies,
7224
9300
  inferRelatedTables,
7225
9301
  listCiPlatforms,
@@ -7238,6 +9314,7 @@ export {
7238
9314
  parsePlaywrightReport,
7239
9315
  parseValidatorRules,
7240
9316
  printOrchestrationSummary,
9317
+ queryNodes,
7241
9318
  recoverJSON,
7242
9319
  renderChecklistMarkdown,
7243
9320
  renderTokenReportMarkdown,
@@ -7247,8 +9324,11 @@ export {
7247
9324
  resolveFromSeedData,
7248
9325
  runDialogLoop,
7249
9326
  scanModuleMetadata,
9327
+ scanProject,
7250
9328
  selectCandidates,
7251
9329
  selectCandidatesFromLogs,
9330
+ simulateScenario,
9331
+ toMermaid,
7252
9332
  topologicalSort,
7253
9333
  validateConfig,
7254
9334
  validateDryrun,