opencroc 1.7.0 → 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/cli/index.js CHANGED
@@ -853,20 +853,20 @@ function detectCycles(dag) {
853
853
  const color = /* @__PURE__ */ new Map();
854
854
  for (const node of dag.nodes) color.set(node, 0 /* WHITE */);
855
855
  const warnings = [];
856
- const path9 = [];
856
+ const path14 = [];
857
857
  function dfs(node) {
858
858
  color.set(node, 1 /* GRAY */);
859
- path9.push(node);
859
+ path14.push(node);
860
860
  for (const neighbor of adjacency.get(node) || []) {
861
861
  const nc = color.get(neighbor);
862
862
  if (nc === 1 /* GRAY */) {
863
- const cycleStart = path9.indexOf(neighbor);
864
- warnings.push(`Cycle detected: ${path9.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
863
+ const cycleStart = path14.indexOf(neighbor);
864
+ warnings.push(`Cycle detected: ${path14.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
865
865
  } else if (nc === 0 /* WHITE */) {
866
866
  dfs(neighbor);
867
867
  }
868
868
  }
869
- path9.pop();
869
+ path14.pop();
870
870
  color.set(node, 2 /* BLACK */);
871
871
  }
872
872
  for (const node of dag.nodes) {
@@ -1863,50 +1863,50 @@ var init_dialog_loop_runner = __esm({
1863
1863
  import { existsSync as existsSync8, copyFileSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync } from "fs";
1864
1864
  import { dirname as dirname2 } from "path";
1865
1865
  async function applyControlledFix(opts) {
1866
- const fs8 = opts.fs ?? defaultFs;
1866
+ const fs13 = opts.fs ?? defaultFs;
1867
1867
  const scope = opts.options?.scope ?? "config-only";
1868
1868
  const dryRun = opts.options?.dryRun ?? true;
1869
1869
  const verify = opts.options?.verify ?? true;
1870
1870
  const configPath = opts.configPath;
1871
1871
  const backupPath = configPath + ".backup";
1872
- if (!fs8.exists(configPath)) {
1872
+ if (!fs13.exists(configPath)) {
1873
1873
  return { success: false, scope, fixedItems: [], rolledBack: false, error: `Config file not found: ${configPath}` };
1874
1874
  }
1875
- const originalContent = fs8.read(configPath);
1876
- fs8.write(backupPath, originalContent);
1875
+ const originalContent = fs13.read(configPath);
1876
+ fs13.write(backupPath, originalContent);
1877
1877
  const validation = opts.validator.validate(originalContent);
1878
1878
  if (validation.passed) {
1879
- cleanup(fs8, backupPath);
1879
+ cleanup(fs13, backupPath);
1880
1880
  return { success: true, scope, fixedItems: [], rolledBack: false };
1881
1881
  }
1882
1882
  let fixResult;
1883
1883
  try {
1884
1884
  fixResult = opts.fixer.fix(originalContent, validation.errors);
1885
1885
  } catch (err) {
1886
- rollback(fs8, backupPath, configPath);
1886
+ rollback(fs13, backupPath, configPath);
1887
1887
  return { success: false, scope, fixedItems: [], rolledBack: true, error: `Fix threw: ${err instanceof Error ? err.message : String(err)}` };
1888
1888
  }
1889
1889
  if (!fixResult.success) {
1890
- rollback(fs8, backupPath, configPath);
1890
+ rollback(fs13, backupPath, configPath);
1891
1891
  return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Remaining errors: ${fixResult.remainingErrors.join("; ")}` };
1892
1892
  }
1893
1893
  if (dryRun) {
1894
1894
  const dryValidation = opts.validator.validate(fixResult.fixedContent);
1895
1895
  if (!dryValidation.passed) {
1896
- rollback(fs8, backupPath, configPath);
1896
+ rollback(fs13, backupPath, configPath);
1897
1897
  return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Dry-run validation failed: ${dryValidation.errors.join("; ")}` };
1898
1898
  }
1899
1899
  }
1900
- fs8.write(configPath, fixResult.fixedContent);
1900
+ fs13.write(configPath, fixResult.fixedContent);
1901
1901
  if (verify) {
1902
- const reloaded = fs8.read(configPath);
1902
+ const reloaded = fs13.read(configPath);
1903
1903
  const postValidation = opts.validator.validate(reloaded);
1904
1904
  if (!postValidation.passed) {
1905
- rollback(fs8, backupPath, configPath);
1905
+ rollback(fs13, backupPath, configPath);
1906
1906
  return { success: false, scope, fixedItems: fixResult.fixedItems, rolledBack: true, error: `Post-write verification failed: ${postValidation.errors.join("; ")}` };
1907
1907
  }
1908
1908
  }
1909
- cleanup(fs8, backupPath);
1909
+ cleanup(fs13, backupPath);
1910
1910
  let prUrl;
1911
1911
  if (scope === "config-and-source" && opts.attribution && opts.prGenerator) {
1912
1912
  try {
@@ -1916,16 +1916,16 @@ async function applyControlledFix(opts) {
1916
1916
  }
1917
1917
  return { success: true, scope, fixedItems: fixResult.fixedItems, rolledBack: false, prUrl };
1918
1918
  }
1919
- function rollback(fs8, backupPath, configPath) {
1920
- if (fs8.exists(backupPath)) {
1921
- const backup = fs8.read(backupPath);
1922
- fs8.write(configPath, backup);
1923
- fs8.remove(backupPath);
1919
+ function rollback(fs13, backupPath, configPath) {
1920
+ if (fs13.exists(backupPath)) {
1921
+ const backup = fs13.read(backupPath);
1922
+ fs13.write(configPath, backup);
1923
+ fs13.remove(backupPath);
1924
1924
  }
1925
1925
  }
1926
- function cleanup(fs8, backupPath) {
1927
- if (fs8.exists(backupPath)) {
1928
- fs8.remove(backupPath);
1926
+ function cleanup(fs13, backupPath) {
1927
+ if (fs13.exists(backupPath)) {
1928
+ fs13.remove(backupPath);
1929
1929
  }
1930
1930
  }
1931
1931
  var defaultFs;
@@ -4317,6 +4317,2218 @@ var init_agents = __esm({
4317
4317
  }
4318
4318
  });
4319
4319
 
4320
+ // src/scanner/language-detector.ts
4321
+ import * as fs8 from "fs";
4322
+ import * as path9 from "path";
4323
+ function detectProject(rootDir) {
4324
+ const absRoot = path9.resolve(rootDir);
4325
+ const languages = {};
4326
+ const linesByLanguage = {};
4327
+ const files = [];
4328
+ let totalFiles = 0;
4329
+ let totalLines = 0;
4330
+ function walk(dir, depth) {
4331
+ if (depth > MAX_DEPTH || totalFiles > MAX_FILES) return;
4332
+ let entries;
4333
+ try {
4334
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
4335
+ } catch {
4336
+ return;
4337
+ }
4338
+ for (const entry of entries) {
4339
+ if (totalFiles > MAX_FILES) break;
4340
+ const fullPath = path9.join(dir, entry.name);
4341
+ if (entry.isDirectory()) {
4342
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
4343
+ walk(fullPath, depth + 1);
4344
+ }
4345
+ continue;
4346
+ }
4347
+ if (!entry.isFile()) continue;
4348
+ const ext = path9.extname(entry.name).toLowerCase();
4349
+ const lang = detectLanguageByFile(entry.name, ext);
4350
+ if (!lang) continue;
4351
+ let lineCount = 0;
4352
+ let fileSize = 0;
4353
+ try {
4354
+ const stat = fs8.statSync(fullPath);
4355
+ fileSize = stat.size;
4356
+ if (fileSize < 1048576) {
4357
+ const content = fs8.readFileSync(fullPath, "utf-8");
4358
+ lineCount = content.split("\n").length;
4359
+ }
4360
+ } catch {
4361
+ continue;
4362
+ }
4363
+ languages[lang] = (languages[lang] || 0) + 1;
4364
+ linesByLanguage[lang] = (linesByLanguage[lang] || 0) + lineCount;
4365
+ totalFiles++;
4366
+ totalLines += lineCount;
4367
+ const relPath = path9.relative(absRoot, fullPath).replace(/\\/g, "/");
4368
+ files.push({ path: relPath, language: lang, lines: lineCount, size: fileSize });
4369
+ }
4370
+ }
4371
+ walk(absRoot, 0);
4372
+ 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]);
4373
+ const primaryLanguage = codeLangs[0]?.[0] || "unknown";
4374
+ const frameworks = detectFrameworks(absRoot, languages, files);
4375
+ const projectType = detectProjectType(absRoot, languages, frameworks);
4376
+ const packageManager = detectPackageManager(absRoot);
4377
+ return {
4378
+ languages,
4379
+ linesByLanguage,
4380
+ totalFiles,
4381
+ totalLines,
4382
+ primaryLanguage,
4383
+ frameworks,
4384
+ projectType,
4385
+ packageManager,
4386
+ files
4387
+ };
4388
+ }
4389
+ function detectLanguageByFile(fileName, ext) {
4390
+ if (fileName === "Dockerfile" || fileName.startsWith("Dockerfile.")) return "docker";
4391
+ if (fileName === "Makefile") return "makefile";
4392
+ if (fileName === "CMakeLists.txt") return "cmake";
4393
+ if (fileName === "Vagrantfile") return "ruby";
4394
+ if (fileName === "Gemfile") return "ruby";
4395
+ if (fileName === "Rakefile") return "ruby";
4396
+ if (fileName === "Cargo.toml") return "rust";
4397
+ if (fileName === "go.mod" || fileName === "go.sum") return "go";
4398
+ return EXTENSION_MAP[ext] || null;
4399
+ }
4400
+ function detectFromPackageJson(root, dep, name) {
4401
+ const candidates = [
4402
+ path9.join(root, "package.json"),
4403
+ path9.join(root, "backend", "package.json"),
4404
+ path9.join(root, "server", "package.json"),
4405
+ path9.join(root, "api", "package.json"),
4406
+ path9.join(root, "frontend", "package.json"),
4407
+ path9.join(root, "web", "package.json"),
4408
+ path9.join(root, "client", "package.json")
4409
+ ];
4410
+ for (const pkgPath of candidates) {
4411
+ try {
4412
+ if (!fs8.existsSync(pkgPath)) continue;
4413
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
4414
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
4415
+ if (dep in allDeps) {
4416
+ return {
4417
+ name,
4418
+ version: allDeps[dep]?.replace(/[\^~>=<]*/g, ""),
4419
+ confidence: 0.95,
4420
+ evidence: `Found "${dep}" in ${path9.relative(root, pkgPath)}`
4421
+ };
4422
+ }
4423
+ } catch {
4424
+ continue;
4425
+ }
4426
+ }
4427
+ return null;
4428
+ }
4429
+ function detectFromRequirements(root, dep, name) {
4430
+ const candidates = [
4431
+ path9.join(root, "requirements.txt"),
4432
+ path9.join(root, "Pipfile"),
4433
+ path9.join(root, "pyproject.toml"),
4434
+ path9.join(root, "setup.py"),
4435
+ path9.join(root, "setup.cfg")
4436
+ ];
4437
+ for (const filePath of candidates) {
4438
+ try {
4439
+ if (!fs8.existsSync(filePath)) continue;
4440
+ const content = fs8.readFileSync(filePath, "utf-8");
4441
+ const pattern = new RegExp(`^${dep}([>=<~!\\s]|$)`, "im");
4442
+ if (pattern.test(content)) {
4443
+ return {
4444
+ name,
4445
+ confidence: 0.9,
4446
+ evidence: `Found "${dep}" in ${path9.basename(filePath)}`
4447
+ };
4448
+ }
4449
+ } catch {
4450
+ continue;
4451
+ }
4452
+ }
4453
+ return null;
4454
+ }
4455
+ function detectFromGoMod(root, module, name) {
4456
+ const goModPath = path9.join(root, "go.mod");
4457
+ try {
4458
+ if (!fs8.existsSync(goModPath)) return null;
4459
+ const content = fs8.readFileSync(goModPath, "utf-8");
4460
+ if (content.includes(module)) {
4461
+ return {
4462
+ name,
4463
+ confidence: 0.95,
4464
+ evidence: `Found "${module}" in go.mod`
4465
+ };
4466
+ }
4467
+ } catch {
4468
+ }
4469
+ return null;
4470
+ }
4471
+ function detectFromCargoToml(root, crate, name) {
4472
+ const cargoPath = path9.join(root, "Cargo.toml");
4473
+ try {
4474
+ if (!fs8.existsSync(cargoPath)) return null;
4475
+ const content = fs8.readFileSync(cargoPath, "utf-8");
4476
+ if (content.includes(crate)) {
4477
+ return {
4478
+ name,
4479
+ confidence: 0.9,
4480
+ evidence: `Found "${crate}" in Cargo.toml`
4481
+ };
4482
+ }
4483
+ } catch {
4484
+ }
4485
+ return null;
4486
+ }
4487
+ function detectFromFile(root, fileName, name, searchTerm) {
4488
+ const filePath = path9.join(root, fileName);
4489
+ try {
4490
+ if (!fs8.existsSync(filePath)) return null;
4491
+ if (searchTerm) {
4492
+ const content = fs8.readFileSync(filePath, "utf-8");
4493
+ if (!content.toLowerCase().includes(searchTerm.toLowerCase())) return null;
4494
+ }
4495
+ return {
4496
+ name,
4497
+ confidence: 0.8,
4498
+ evidence: `Found ${fileName}${searchTerm ? ` containing "${searchTerm}"` : ""}`
4499
+ };
4500
+ } catch {
4501
+ return null;
4502
+ }
4503
+ }
4504
+ function detectFrameworks(root, langs, files) {
4505
+ const detected = [];
4506
+ for (const rule of FRAMEWORK_RULES) {
4507
+ const result = rule.detect(root, langs, files);
4508
+ if (result) detected.push(result);
4509
+ }
4510
+ return detected;
4511
+ }
4512
+ function detectProjectType(root, langs, frameworks) {
4513
+ const frameworkNames = new Set(frameworks.map((f) => f.name.toLowerCase()));
4514
+ const hasLerna = fs8.existsSync(path9.join(root, "lerna.json"));
4515
+ const hasPnpmWorkspace = fs8.existsSync(path9.join(root, "pnpm-workspace.yaml"));
4516
+ const hasNxJson = fs8.existsSync(path9.join(root, "nx.json"));
4517
+ const hasTurboJson = fs8.existsSync(path9.join(root, "turbo.json"));
4518
+ let hasWorkspaces = false;
4519
+ try {
4520
+ const pkg = JSON.parse(fs8.readFileSync(path9.join(root, "package.json"), "utf-8"));
4521
+ hasWorkspaces = Array.isArray(pkg.workspaces) || typeof pkg.workspaces === "object";
4522
+ } catch {
4523
+ }
4524
+ if (hasLerna || hasPnpmWorkspace || hasNxJson || hasTurboJson || hasWorkspaces) {
4525
+ return "monorepo";
4526
+ }
4527
+ try {
4528
+ const pkg = JSON.parse(fs8.readFileSync(path9.join(root, "package.json"), "utf-8"));
4529
+ if (pkg.main || pkg.exports || pkg.module) {
4530
+ const hasNoServer = !frameworkNames.has("express") && !frameworkNames.has("fastify") && !frameworkNames.has("koa") && !frameworkNames.has("nestjs");
4531
+ const hasNoFrontend = !frameworkNames.has("react") && !frameworkNames.has("vue") && !frameworkNames.has("angular") && !frameworkNames.has("svelte");
4532
+ if (hasNoServer && hasNoFrontend && pkg.keywords) return "library";
4533
+ }
4534
+ if (pkg.bin) return "cli-tool";
4535
+ } catch {
4536
+ }
4537
+ if (frameworkNames.has("next.js") || frameworkNames.has("nuxt.js")) return "frontend-ssr";
4538
+ if (frameworkNames.has("electron")) return "fullstack";
4539
+ if (langs["dart"]) return "mobile";
4540
+ if (langs["swift"] && !langs["typescript"] && !langs["python"]) return "mobile";
4541
+ 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");
4542
+ const hasFrontend = frameworkNames.has("react") || frameworkNames.has("vue") || frameworkNames.has("angular") || frameworkNames.has("svelte");
4543
+ if (hasBackend && hasFrontend) return "fullstack";
4544
+ if (hasBackend) return "backend-api";
4545
+ if (hasFrontend) return "frontend-spa";
4546
+ if (frameworkNames.has("actix web") || frameworkNames.has("axum") || frameworkNames.has("gin") || frameworkNames.has("echo") || frameworkNames.has("fiber")) {
4547
+ return "backend-api";
4548
+ }
4549
+ return "unknown";
4550
+ }
4551
+ function detectPackageManager(root) {
4552
+ if (fs8.existsSync(path9.join(root, "pnpm-lock.yaml"))) return "pnpm";
4553
+ if (fs8.existsSync(path9.join(root, "yarn.lock"))) return "yarn";
4554
+ if (fs8.existsSync(path9.join(root, "bun.lockb"))) return "bun";
4555
+ if (fs8.existsSync(path9.join(root, "package-lock.json"))) return "npm";
4556
+ if (fs8.existsSync(path9.join(root, "Pipfile.lock"))) return "pipenv";
4557
+ if (fs8.existsSync(path9.join(root, "poetry.lock"))) return "poetry";
4558
+ if (fs8.existsSync(path9.join(root, "go.sum"))) return "go-modules";
4559
+ if (fs8.existsSync(path9.join(root, "Cargo.lock"))) return "cargo";
4560
+ if (fs8.existsSync(path9.join(root, "Gemfile.lock"))) return "bundler";
4561
+ if (fs8.existsSync(path9.join(root, "composer.lock"))) return "composer";
4562
+ return void 0;
4563
+ }
4564
+ var EXTENSION_MAP, SKIP_DIRS, MAX_DEPTH, MAX_FILES, FRAMEWORK_RULES;
4565
+ var init_language_detector = __esm({
4566
+ "src/scanner/language-detector.ts"() {
4567
+ "use strict";
4568
+ init_esm_shims();
4569
+ EXTENSION_MAP = {
4570
+ ".ts": "typescript",
4571
+ ".tsx": "typescript",
4572
+ ".mts": "typescript",
4573
+ ".cts": "typescript",
4574
+ ".js": "javascript",
4575
+ ".jsx": "javascript",
4576
+ ".mjs": "javascript",
4577
+ ".cjs": "javascript",
4578
+ ".py": "python",
4579
+ ".pyw": "python",
4580
+ ".pyi": "python",
4581
+ ".go": "go",
4582
+ ".java": "java",
4583
+ ".kt": "kotlin",
4584
+ ".kts": "kotlin",
4585
+ ".rs": "rust",
4586
+ ".rb": "ruby",
4587
+ ".php": "php",
4588
+ ".cs": "csharp",
4589
+ ".cpp": "cpp",
4590
+ ".cc": "cpp",
4591
+ ".cxx": "cpp",
4592
+ ".c": "c",
4593
+ ".h": "c",
4594
+ ".swift": "swift",
4595
+ ".dart": "dart",
4596
+ ".vue": "vue",
4597
+ ".svelte": "svelte",
4598
+ ".astro": "astro",
4599
+ ".sql": "sql",
4600
+ ".graphql": "graphql",
4601
+ ".gql": "graphql",
4602
+ ".proto": "protobuf",
4603
+ ".yaml": "yaml",
4604
+ ".yml": "yaml",
4605
+ ".json": "json",
4606
+ ".toml": "toml",
4607
+ ".md": "markdown",
4608
+ ".html": "html",
4609
+ ".htm": "html",
4610
+ ".css": "css",
4611
+ ".scss": "scss",
4612
+ ".less": "less",
4613
+ ".sass": "sass",
4614
+ ".sh": "shell",
4615
+ ".bash": "shell",
4616
+ ".zsh": "shell",
4617
+ ".ps1": "powershell",
4618
+ ".dockerfile": "docker",
4619
+ ".tf": "terraform",
4620
+ ".lua": "lua",
4621
+ ".r": "r",
4622
+ ".R": "r",
4623
+ ".scala": "scala",
4624
+ ".ex": "elixir",
4625
+ ".exs": "elixir",
4626
+ ".erl": "erlang",
4627
+ ".zig": "zig"
4628
+ };
4629
+ SKIP_DIRS = /* @__PURE__ */ new Set([
4630
+ "node_modules",
4631
+ ".git",
4632
+ ".svn",
4633
+ ".hg",
4634
+ "dist",
4635
+ "build",
4636
+ "out",
4637
+ "target",
4638
+ "__pycache__",
4639
+ ".cache",
4640
+ ".next",
4641
+ ".nuxt",
4642
+ ".output",
4643
+ "vendor",
4644
+ "venv",
4645
+ ".venv",
4646
+ "env",
4647
+ ".env",
4648
+ "coverage",
4649
+ ".idea",
4650
+ ".vscode",
4651
+ ".vs",
4652
+ ".turbo",
4653
+ ".nx",
4654
+ "bower_components",
4655
+ "jspm_packages"
4656
+ ]);
4657
+ MAX_DEPTH = 8;
4658
+ MAX_FILES = 1e4;
4659
+ FRAMEWORK_RULES = [
4660
+ // --- Node.js / JavaScript ---
4661
+ {
4662
+ name: "express",
4663
+ detect: (root) => detectFromPackageJson(root, "express", "Express")
4664
+ },
4665
+ {
4666
+ name: "nestjs",
4667
+ detect: (root) => detectFromPackageJson(root, "@nestjs/core", "NestJS")
4668
+ },
4669
+ {
4670
+ name: "fastify",
4671
+ detect: (root) => detectFromPackageJson(root, "fastify", "Fastify")
4672
+ },
4673
+ {
4674
+ name: "koa",
4675
+ detect: (root) => detectFromPackageJson(root, "koa", "Koa")
4676
+ },
4677
+ {
4678
+ name: "hapi",
4679
+ detect: (root) => detectFromPackageJson(root, "@hapi/hapi", "Hapi")
4680
+ },
4681
+ {
4682
+ name: "nextjs",
4683
+ detect: (root) => detectFromPackageJson(root, "next", "Next.js")
4684
+ },
4685
+ {
4686
+ name: "nuxtjs",
4687
+ detect: (root) => detectFromPackageJson(root, "nuxt", "Nuxt.js")
4688
+ },
4689
+ {
4690
+ name: "react",
4691
+ detect: (root) => detectFromPackageJson(root, "react", "React")
4692
+ },
4693
+ {
4694
+ name: "vue",
4695
+ detect: (root) => detectFromPackageJson(root, "vue", "Vue")
4696
+ },
4697
+ {
4698
+ name: "angular",
4699
+ detect: (root) => detectFromPackageJson(root, "@angular/core", "Angular")
4700
+ },
4701
+ {
4702
+ name: "svelte",
4703
+ detect: (root) => detectFromPackageJson(root, "svelte", "Svelte")
4704
+ },
4705
+ {
4706
+ name: "electron",
4707
+ detect: (root) => detectFromPackageJson(root, "electron", "Electron")
4708
+ },
4709
+ {
4710
+ name: "sequelize",
4711
+ detect: (root) => detectFromPackageJson(root, "sequelize", "Sequelize")
4712
+ },
4713
+ {
4714
+ name: "typeorm",
4715
+ detect: (root) => detectFromPackageJson(root, "typeorm", "TypeORM")
4716
+ },
4717
+ {
4718
+ name: "prisma",
4719
+ detect: (root) => detectFromPackageJson(root, "prisma", "Prisma") || detectFromPackageJson(root, "@prisma/client", "Prisma")
4720
+ },
4721
+ {
4722
+ name: "mongoose",
4723
+ detect: (root) => detectFromPackageJson(root, "mongoose", "Mongoose")
4724
+ },
4725
+ {
4726
+ name: "playwright",
4727
+ detect: (root) => detectFromPackageJson(root, "@playwright/test", "Playwright")
4728
+ },
4729
+ // --- Python ---
4730
+ {
4731
+ name: "django",
4732
+ detect: (root) => detectFromRequirements(root, "django", "Django") || detectFromFile(root, "manage.py", "Django")
4733
+ },
4734
+ {
4735
+ name: "flask",
4736
+ detect: (root) => detectFromRequirements(root, "flask", "Flask")
4737
+ },
4738
+ {
4739
+ name: "fastapi",
4740
+ detect: (root) => detectFromRequirements(root, "fastapi", "FastAPI")
4741
+ },
4742
+ {
4743
+ name: "pytorch",
4744
+ detect: (root) => detectFromRequirements(root, "torch", "PyTorch")
4745
+ },
4746
+ {
4747
+ name: "tensorflow",
4748
+ detect: (root) => detectFromRequirements(root, "tensorflow", "TensorFlow")
4749
+ },
4750
+ // --- Go ---
4751
+ {
4752
+ name: "gin",
4753
+ detect: (root) => detectFromGoMod(root, "github.com/gin-gonic/gin", "Gin")
4754
+ },
4755
+ {
4756
+ name: "echo",
4757
+ detect: (root) => detectFromGoMod(root, "github.com/labstack/echo", "Echo")
4758
+ },
4759
+ {
4760
+ name: "fiber",
4761
+ detect: (root) => detectFromGoMod(root, "github.com/gofiber/fiber", "Fiber")
4762
+ },
4763
+ // --- Java ---
4764
+ {
4765
+ name: "spring-boot",
4766
+ detect: (root) => detectFromFile(root, "pom.xml", "Spring Boot", "spring-boot") || detectFromFile(root, "build.gradle", "Spring Boot", "spring-boot")
4767
+ },
4768
+ // --- Rust ---
4769
+ {
4770
+ name: "actix-web",
4771
+ detect: (root) => detectFromCargoToml(root, "actix-web", "Actix Web")
4772
+ },
4773
+ {
4774
+ name: "axum",
4775
+ detect: (root) => detectFromCargoToml(root, "axum", "Axum")
4776
+ },
4777
+ // --- Ruby ---
4778
+ {
4779
+ name: "rails",
4780
+ detect: (root) => detectFromFile(root, "Gemfile", "Ruby on Rails", "rails")
4781
+ },
4782
+ // --- PHP ---
4783
+ {
4784
+ name: "laravel",
4785
+ detect: (root) => detectFromFile(root, "artisan", "Laravel")
4786
+ }
4787
+ ];
4788
+ }
4789
+ });
4790
+
4791
+ // src/scanner/project-scanner.ts
4792
+ import * as fs9 from "fs";
4793
+ import * as path10 from "path";
4794
+ async function scanProject(options) {
4795
+ const { rootDir, maxDeepScan = 500, onProgress } = options;
4796
+ const startTime = Date.now();
4797
+ onProgress?.("detecting", 0, "Detecting languages and frameworks...");
4798
+ const detection = detectProject(rootDir);
4799
+ onProgress?.("detecting", 100, `Found ${detection.totalFiles} files, primary: ${detection.primaryLanguage}`);
4800
+ const entities = [];
4801
+ const relationships = [];
4802
+ const sourceFiles = detection.files.filter((f) => {
4803
+ const lang = f.language;
4804
+ return !["json", "yaml", "toml", "markdown", "html", "css", "scss", "less", "sass", "docker", "shell", "powershell"].includes(lang);
4805
+ });
4806
+ const filesToAnalyze = sourceFiles.slice(0, maxDeepScan);
4807
+ for (let i = 0; i < filesToAnalyze.length; i++) {
4808
+ const file = filesToAnalyze[i];
4809
+ const percent = Math.round(i / filesToAnalyze.length * 100);
4810
+ if (i % 20 === 0) {
4811
+ onProgress?.("scanning", percent, `Scanning ${file.path}...`);
4812
+ }
4813
+ const fullPath = path10.join(rootDir, file.path);
4814
+ try {
4815
+ const extracted = extractEntitiesFromFile(fullPath, file.path, file.language);
4816
+ entities.push(...extracted.entities);
4817
+ relationships.push(...extracted.relationships);
4818
+ } catch {
4819
+ }
4820
+ }
4821
+ onProgress?.("scanning", 100, `Extracted ${entities.length} entities`);
4822
+ onProgress?.("configs", 0, "Parsing config files...");
4823
+ const configEntities = extractFromConfigs(rootDir, detection);
4824
+ entities.push(...configEntities.entities);
4825
+ relationships.push(...configEntities.relationships);
4826
+ onProgress?.("configs", 100, "Config parsing complete");
4827
+ onProgress?.("relations", 0, "Building relationships...");
4828
+ const inferredRelations = inferRelationships(entities, rootDir);
4829
+ relationships.push(...inferredRelations);
4830
+ onProgress?.("relations", 100, `${relationships.length} total relationships`);
4831
+ const discoveredFiles = detection.files.map((f) => ({
4832
+ path: f.path,
4833
+ language: f.language,
4834
+ category: categorizeFile(f.path, f.language),
4835
+ lines: f.lines,
4836
+ size: f.size
4837
+ }));
4838
+ return {
4839
+ languages: detection.languages,
4840
+ frameworks: detection.frameworks,
4841
+ files: discoveredFiles,
4842
+ entities,
4843
+ relationships,
4844
+ duration: Date.now() - startTime
4845
+ };
4846
+ }
4847
+ function extractEntitiesFromFile(fullPath, relPath, language) {
4848
+ switch (language) {
4849
+ case "typescript":
4850
+ case "javascript":
4851
+ return extractFromTsJs(fullPath, relPath, language);
4852
+ case "python":
4853
+ return extractFromPython(fullPath, relPath);
4854
+ case "go":
4855
+ return extractFromGo(fullPath, relPath);
4856
+ case "java":
4857
+ case "kotlin":
4858
+ return extractFromJavaKotlin(fullPath, relPath, language);
4859
+ case "rust":
4860
+ return extractFromRust(fullPath, relPath);
4861
+ case "ruby":
4862
+ return extractFromRuby(fullPath, relPath);
4863
+ case "php":
4864
+ return extractFromPHP(fullPath, relPath);
4865
+ case "vue":
4866
+ case "svelte":
4867
+ return extractFromTsJs(fullPath, relPath, "typescript");
4868
+ // Extract script section
4869
+ default:
4870
+ return { entities: [], relationships: [] };
4871
+ }
4872
+ }
4873
+ function extractFromTsJs(fullPath, relPath, language) {
4874
+ const content = fs9.readFileSync(fullPath, "utf-8");
4875
+ const entities = [];
4876
+ const relationships = [];
4877
+ const fileId = `file:${relPath}`;
4878
+ entities.push({
4879
+ id: fileId,
4880
+ name: path10.basename(relPath),
4881
+ type: "file",
4882
+ filePath: relPath,
4883
+ language,
4884
+ metadata: {}
4885
+ });
4886
+ const classRegex = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?\s*\{/g;
4887
+ let match;
4888
+ while ((match = classRegex.exec(content)) !== null) {
4889
+ const className = match[1];
4890
+ const extendsClass = match[2];
4891
+ const classId = `class:${relPath}:${className}`;
4892
+ entities.push({
4893
+ id: classId,
4894
+ name: className,
4895
+ type: detectClassType(className, content),
4896
+ filePath: relPath,
4897
+ line: getLineNumber(content, match.index),
4898
+ language,
4899
+ metadata: { extends: extendsClass }
4900
+ });
4901
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
4902
+ if (extendsClass) {
4903
+ relationships.push({
4904
+ sourceId: classId,
4905
+ targetId: `class:*:${extendsClass}`,
4906
+ relation: "extends"
4907
+ });
4908
+ }
4909
+ }
4910
+ const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
4911
+ while ((match = funcRegex.exec(content)) !== null) {
4912
+ const funcName = match[1];
4913
+ const funcId = `func:${relPath}:${funcName}`;
4914
+ entities.push({
4915
+ id: funcId,
4916
+ name: funcName,
4917
+ type: "function",
4918
+ filePath: relPath,
4919
+ line: getLineNumber(content, match.index),
4920
+ language,
4921
+ metadata: {}
4922
+ });
4923
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
4924
+ }
4925
+ const arrowFuncRegex = /export\s+(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/g;
4926
+ while ((match = arrowFuncRegex.exec(content)) !== null) {
4927
+ const funcName = match[1];
4928
+ const funcId = `func:${relPath}:${funcName}`;
4929
+ entities.push({
4930
+ id: funcId,
4931
+ name: funcName,
4932
+ type: "function",
4933
+ filePath: relPath,
4934
+ line: getLineNumber(content, match.index),
4935
+ language,
4936
+ metadata: { arrow: true }
4937
+ });
4938
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
4939
+ }
4940
+ const routeRegex = /(?:router|app)\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
4941
+ while ((match = routeRegex.exec(content)) !== null) {
4942
+ const method = match[1].toUpperCase();
4943
+ const routePath = match[2];
4944
+ const apiId = `api:${method}:${routePath}`;
4945
+ entities.push({
4946
+ id: apiId,
4947
+ name: `${method} ${routePath}`,
4948
+ type: "api",
4949
+ filePath: relPath,
4950
+ line: getLineNumber(content, match.index),
4951
+ language,
4952
+ metadata: { method, path: routePath }
4953
+ });
4954
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
4955
+ }
4956
+ const importRegex = /(?:import\s+.*from\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\))/g;
4957
+ while ((match = importRegex.exec(content)) !== null) {
4958
+ const importPath = match[1] || match[2];
4959
+ if (importPath.startsWith(".")) {
4960
+ const resolved = resolveRelativeImport(relPath, importPath);
4961
+ relationships.push({
4962
+ sourceId: fileId,
4963
+ targetId: `file:${resolved}`,
4964
+ relation: "imports"
4965
+ });
4966
+ } else {
4967
+ const depName = importPath.startsWith("@") ? importPath.split("/").slice(0, 2).join("/") : importPath.split("/")[0];
4968
+ const depId = `dep:${depName}`;
4969
+ entities.push({
4970
+ id: depId,
4971
+ name: depName,
4972
+ type: "dependency",
4973
+ filePath: "",
4974
+ language: "external",
4975
+ metadata: { external: true }
4976
+ });
4977
+ relationships.push({ sourceId: fileId, targetId: depId, relation: "depends-on" });
4978
+ }
4979
+ }
4980
+ if (content.includes(".init(") || content.includes("Model.init") || content.includes("@Entity") || content.includes("defineModel")) {
4981
+ const tableMatch = content.match(/tableName:\s*['"`](\w+)['"`]/);
4982
+ if (tableMatch) {
4983
+ const tableName = tableMatch[1];
4984
+ const modelId = `model:${tableName}`;
4985
+ entities.push({
4986
+ id: modelId,
4987
+ name: tableName,
4988
+ type: "model",
4989
+ filePath: relPath,
4990
+ language,
4991
+ metadata: { orm: "sequelize" }
4992
+ });
4993
+ relationships.push({ sourceId: modelId, targetId: fileId, relation: "belongs-to" });
4994
+ }
4995
+ }
4996
+ return { entities, relationships };
4997
+ }
4998
+ function extractFromPython(fullPath, relPath) {
4999
+ const content = fs9.readFileSync(fullPath, "utf-8");
5000
+ const entities = [];
5001
+ const relationships = [];
5002
+ const fileId = `file:${relPath}`;
5003
+ entities.push({ id: fileId, name: path10.basename(relPath), type: "file", filePath: relPath, language: "python", metadata: {} });
5004
+ const classRegex = /^class\s+(\w+)(?:\(([^)]*)\))?\s*:/gm;
5005
+ let match;
5006
+ while ((match = classRegex.exec(content)) !== null) {
5007
+ const className = match[1];
5008
+ const bases = match[2];
5009
+ const classId = `class:${relPath}:${className}`;
5010
+ entities.push({
5011
+ id: classId,
5012
+ name: className,
5013
+ type: detectPythonClassType(className, bases || "", content),
5014
+ filePath: relPath,
5015
+ line: getLineNumber(content, match.index),
5016
+ language: "python",
5017
+ metadata: { bases }
5018
+ });
5019
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
5020
+ }
5021
+ const funcRegex = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
5022
+ while ((match = funcRegex.exec(content)) !== null) {
5023
+ const funcName = match[1];
5024
+ if (funcName.startsWith("_") && funcName !== "__init__") continue;
5025
+ const funcId = `func:${relPath}:${funcName}`;
5026
+ entities.push({
5027
+ id: funcId,
5028
+ name: funcName,
5029
+ type: "function",
5030
+ filePath: relPath,
5031
+ line: getLineNumber(content, match.index),
5032
+ language: "python",
5033
+ metadata: {}
5034
+ });
5035
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
5036
+ }
5037
+ const routeRegex = /@(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
5038
+ while ((match = routeRegex.exec(content)) !== null) {
5039
+ const method = match[1].toUpperCase();
5040
+ const routePath = match[2];
5041
+ const apiId = `api:${method}:${routePath}`;
5042
+ entities.push({
5043
+ id: apiId,
5044
+ name: `${method} ${routePath}`,
5045
+ type: "api",
5046
+ filePath: relPath,
5047
+ line: getLineNumber(content, match.index),
5048
+ language: "python",
5049
+ metadata: { method, path: routePath }
5050
+ });
5051
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
5052
+ }
5053
+ const djangoUrlRegex = /path\s*\(\s*['"]([^'"]+)['"],\s*(\w+)/g;
5054
+ while ((match = djangoUrlRegex.exec(content)) !== null) {
5055
+ const routePath = match[1];
5056
+ const apiId = `api:ANY:${routePath}`;
5057
+ entities.push({
5058
+ id: apiId,
5059
+ name: routePath,
5060
+ type: "route",
5061
+ filePath: relPath,
5062
+ line: getLineNumber(content, match.index),
5063
+ language: "python",
5064
+ metadata: { handler: match[2] }
5065
+ });
5066
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
5067
+ }
5068
+ const djangoModelRegex = /class\s+(\w+)\((?:models\.)?Model\)/g;
5069
+ while ((match = djangoModelRegex.exec(content)) !== null) {
5070
+ const modelName = match[1];
5071
+ const modelId = `model:${modelName}`;
5072
+ entities.push({
5073
+ id: modelId,
5074
+ name: modelName,
5075
+ type: "model",
5076
+ filePath: relPath,
5077
+ language: "python",
5078
+ metadata: { orm: "django" }
5079
+ });
5080
+ relationships.push({ sourceId: modelId, targetId: fileId, relation: "belongs-to" });
5081
+ }
5082
+ const sqlalchemyRegex = /class\s+(\w+)\(.*(?:Base|DeclarativeBase|db\.Model)\)/g;
5083
+ while ((match = sqlalchemyRegex.exec(content)) !== null) {
5084
+ const modelName = match[1];
5085
+ const modelId = `model:${modelName}`;
5086
+ entities.push({
5087
+ id: modelId,
5088
+ name: modelName,
5089
+ type: "model",
5090
+ filePath: relPath,
5091
+ language: "python",
5092
+ metadata: { orm: "sqlalchemy" }
5093
+ });
5094
+ relationships.push({ sourceId: modelId, targetId: fileId, relation: "belongs-to" });
5095
+ }
5096
+ const importRegex = /^(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm;
5097
+ while ((match = importRegex.exec(content)) !== null) {
5098
+ const mod = match[1] || match[2];
5099
+ if (mod.startsWith(".")) {
5100
+ relationships.push({ sourceId: fileId, targetId: `file:${mod}`, relation: "imports" });
5101
+ } else {
5102
+ const depName = mod.split(".")[0];
5103
+ entities.push({ id: `dep:${depName}`, name: depName, type: "dependency", filePath: "", language: "external", metadata: { external: true } });
5104
+ relationships.push({ sourceId: fileId, targetId: `dep:${depName}`, relation: "depends-on" });
5105
+ }
5106
+ }
5107
+ return { entities, relationships };
5108
+ }
5109
+ function extractFromGo(fullPath, relPath) {
5110
+ const content = fs9.readFileSync(fullPath, "utf-8");
5111
+ const entities = [];
5112
+ const relationships = [];
5113
+ const fileId = `file:${relPath}`;
5114
+ entities.push({ id: fileId, name: path10.basename(relPath), type: "file", filePath: relPath, language: "go", metadata: {} });
5115
+ const structRegex = /type\s+(\w+)\s+struct\s*\{/g;
5116
+ let match;
5117
+ while ((match = structRegex.exec(content)) !== null) {
5118
+ const structName = match[1];
5119
+ const structId = `class:${relPath}:${structName}`;
5120
+ entities.push({
5121
+ id: structId,
5122
+ name: structName,
5123
+ type: structName.endsWith("Model") || structName.endsWith("Entity") ? "model" : "class",
5124
+ filePath: relPath,
5125
+ line: getLineNumber(content, match.index),
5126
+ language: "go",
5127
+ metadata: {}
5128
+ });
5129
+ relationships.push({ sourceId: structId, targetId: fileId, relation: "belongs-to" });
5130
+ }
5131
+ const funcRegex = /func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(/g;
5132
+ while ((match = funcRegex.exec(content)) !== null) {
5133
+ const funcName = match[1];
5134
+ if (funcName[0] !== funcName[0].toUpperCase()) continue;
5135
+ const funcId = `func:${relPath}:${funcName}`;
5136
+ entities.push({
5137
+ id: funcId,
5138
+ name: funcName,
5139
+ type: "function",
5140
+ filePath: relPath,
5141
+ line: getLineNumber(content, match.index),
5142
+ language: "go",
5143
+ metadata: {}
5144
+ });
5145
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
5146
+ }
5147
+ const ginRouteRegex = /\.(GET|POST|PUT|PATCH|DELETE)\s*\(\s*"([^"]+)"/gi;
5148
+ while ((match = ginRouteRegex.exec(content)) !== null) {
5149
+ const method = match[1].toUpperCase();
5150
+ const routePath = match[2];
5151
+ const apiId = `api:${method}:${routePath}`;
5152
+ entities.push({
5153
+ id: apiId,
5154
+ name: `${method} ${routePath}`,
5155
+ type: "api",
5156
+ filePath: relPath,
5157
+ line: getLineNumber(content, match.index),
5158
+ language: "go",
5159
+ metadata: { method, path: routePath }
5160
+ });
5161
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
5162
+ }
5163
+ return { entities, relationships };
5164
+ }
5165
+ function extractFromJavaKotlin(fullPath, relPath, language) {
5166
+ const content = fs9.readFileSync(fullPath, "utf-8");
5167
+ const entities = [];
5168
+ const relationships = [];
5169
+ const fileId = `file:${relPath}`;
5170
+ entities.push({ id: fileId, name: path10.basename(relPath), type: "file", filePath: relPath, language, metadata: {} });
5171
+ const classRegex = /(?:public\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+(\w+)(?:\s+extends\s+(\w+))?/g;
5172
+ let match;
5173
+ while ((match = classRegex.exec(content)) !== null) {
5174
+ const className = match[1];
5175
+ const classId = `class:${relPath}:${className}`;
5176
+ entities.push({
5177
+ id: classId,
5178
+ name: className,
5179
+ type: content.includes("@Entity") || content.includes("@Table") ? "model" : "class",
5180
+ filePath: relPath,
5181
+ line: getLineNumber(content, match.index),
5182
+ language,
5183
+ metadata: {}
5184
+ });
5185
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
5186
+ }
5187
+ const springRegex = /@(?:Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/g;
5188
+ while ((match = springRegex.exec(content)) !== null) {
5189
+ const routePath = match[1];
5190
+ const apiId = `api:ANY:${routePath}`;
5191
+ entities.push({
5192
+ id: apiId,
5193
+ name: routePath,
5194
+ type: "api",
5195
+ filePath: relPath,
5196
+ line: getLineNumber(content, match.index),
5197
+ language,
5198
+ metadata: { path: routePath }
5199
+ });
5200
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
5201
+ }
5202
+ return { entities, relationships };
5203
+ }
5204
+ function extractFromRust(fullPath, relPath) {
5205
+ const content = fs9.readFileSync(fullPath, "utf-8");
5206
+ const entities = [];
5207
+ const relationships = [];
5208
+ const fileId = `file:${relPath}`;
5209
+ entities.push({ id: fileId, name: path10.basename(relPath), type: "file", filePath: relPath, language: "rust", metadata: {} });
5210
+ const structRegex = /pub\s+struct\s+(\w+)/g;
5211
+ let match;
5212
+ while ((match = structRegex.exec(content)) !== null) {
5213
+ const structName = match[1];
5214
+ const structId = `class:${relPath}:${structName}`;
5215
+ entities.push({ id: structId, name: structName, type: "class", filePath: relPath, line: getLineNumber(content, match.index), language: "rust", metadata: {} });
5216
+ relationships.push({ sourceId: structId, targetId: fileId, relation: "belongs-to" });
5217
+ }
5218
+ const funcRegex = /pub\s+(?:async\s+)?fn\s+(\w+)/g;
5219
+ while ((match = funcRegex.exec(content)) !== null) {
5220
+ const funcName = match[1];
5221
+ const funcId = `func:${relPath}:${funcName}`;
5222
+ entities.push({ id: funcId, name: funcName, type: "function", filePath: relPath, line: getLineNumber(content, match.index), language: "rust", metadata: {} });
5223
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
5224
+ }
5225
+ return { entities, relationships };
5226
+ }
5227
+ function extractFromRuby(fullPath, relPath) {
5228
+ const content = fs9.readFileSync(fullPath, "utf-8");
5229
+ const entities = [];
5230
+ const relationships = [];
5231
+ const fileId = `file:${relPath}`;
5232
+ entities.push({ id: fileId, name: path10.basename(relPath), type: "file", filePath: relPath, language: "ruby", metadata: {} });
5233
+ const classRegex = /class\s+(\w+)(?:\s*<\s*(\w+))?/g;
5234
+ let match;
5235
+ while ((match = classRegex.exec(content)) !== null) {
5236
+ const className = match[1];
5237
+ const base = match[2];
5238
+ const classId = `class:${relPath}:${className}`;
5239
+ const type = base === "ApplicationRecord" || base === "ActiveRecord::Base" ? "model" : "class";
5240
+ entities.push({ id: classId, name: className, type, filePath: relPath, line: getLineNumber(content, match.index), language: "ruby", metadata: { extends: base } });
5241
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
5242
+ }
5243
+ const defRegex = /def\s+(?:self\.)?(\w+)/g;
5244
+ while ((match = defRegex.exec(content)) !== null) {
5245
+ const funcName = match[1];
5246
+ if (funcName.startsWith("_")) continue;
5247
+ const funcId = `func:${relPath}:${funcName}`;
5248
+ entities.push({ id: funcId, name: funcName, type: "function", filePath: relPath, line: getLineNumber(content, match.index), language: "ruby", metadata: {} });
5249
+ relationships.push({ sourceId: funcId, targetId: fileId, relation: "belongs-to" });
5250
+ }
5251
+ return { entities, relationships };
5252
+ }
5253
+ function extractFromPHP(fullPath, relPath) {
5254
+ const content = fs9.readFileSync(fullPath, "utf-8");
5255
+ const entities = [];
5256
+ const relationships = [];
5257
+ const fileId = `file:${relPath}`;
5258
+ entities.push({ id: fileId, name: path10.basename(relPath), type: "file", filePath: relPath, language: "php", metadata: {} });
5259
+ const classRegex = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/g;
5260
+ let match;
5261
+ while ((match = classRegex.exec(content)) !== null) {
5262
+ const className = match[1];
5263
+ const base = match[2];
5264
+ const classId = `class:${relPath}:${className}`;
5265
+ const type = base === "Model" || base === "Eloquent" ? "model" : "class";
5266
+ entities.push({ id: classId, name: className, type, filePath: relPath, line: getLineNumber(content, match.index), language: "php", metadata: {} });
5267
+ relationships.push({ sourceId: classId, targetId: fileId, relation: "belongs-to" });
5268
+ }
5269
+ const laravelRouteRegex = /Route::(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
5270
+ while ((match = laravelRouteRegex.exec(content)) !== null) {
5271
+ const method = match[1].toUpperCase();
5272
+ const routePath = match[2];
5273
+ const apiId = `api:${method}:${routePath}`;
5274
+ entities.push({ id: apiId, name: `${method} ${routePath}`, type: "api", filePath: relPath, line: getLineNumber(content, match.index), language: "php", metadata: { method, path: routePath } });
5275
+ relationships.push({ sourceId: apiId, targetId: fileId, relation: "belongs-to" });
5276
+ }
5277
+ return { entities, relationships };
5278
+ }
5279
+ function extractFromConfigs(rootDir, _detection) {
5280
+ const entities = [];
5281
+ const relationships = [];
5282
+ const pkgPath = path10.join(rootDir, "package.json");
5283
+ if (fs9.existsSync(pkgPath)) {
5284
+ try {
5285
+ const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
5286
+ const allDeps = { ...pkg.dependencies };
5287
+ for (const [name, version] of Object.entries(allDeps)) {
5288
+ const depId = `dep:${name}`;
5289
+ entities.push({
5290
+ id: depId,
5291
+ name,
5292
+ type: "dependency",
5293
+ filePath: "package.json",
5294
+ language: "external",
5295
+ metadata: { version, source: "npm", external: true }
5296
+ });
5297
+ }
5298
+ } catch {
5299
+ }
5300
+ }
5301
+ const openAPIFiles = ["openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "swagger.yaml"];
5302
+ for (const apiFile of openAPIFiles) {
5303
+ const apiPath = path10.join(rootDir, apiFile);
5304
+ if (fs9.existsSync(apiPath)) {
5305
+ try {
5306
+ const content = fs9.readFileSync(apiPath, "utf-8");
5307
+ const pathRegex = /"(\/[^"]+)":\s*\{/g;
5308
+ let match;
5309
+ while ((match = pathRegex.exec(content)) !== null) {
5310
+ const routePath = match[1];
5311
+ const apiId = `api:ANY:${routePath}`;
5312
+ entities.push({
5313
+ id: apiId,
5314
+ name: routePath,
5315
+ type: "api",
5316
+ filePath: apiFile,
5317
+ language: "openapi",
5318
+ metadata: { source: "openapi" }
5319
+ });
5320
+ }
5321
+ } catch {
5322
+ }
5323
+ }
5324
+ }
5325
+ const composeFiles = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
5326
+ for (const composeFile of composeFiles) {
5327
+ const composePath = path10.join(rootDir, composeFile);
5328
+ if (fs9.existsSync(composePath)) {
5329
+ try {
5330
+ const content = fs9.readFileSync(composePath, "utf-8");
5331
+ const serviceRegex = /^\s{2}(\w[\w-]*):\s*$/gm;
5332
+ let match;
5333
+ while ((match = serviceRegex.exec(content)) !== null) {
5334
+ const serviceName = match[1];
5335
+ if (serviceName === "services" || serviceName === "volumes" || serviceName === "networks") continue;
5336
+ entities.push({
5337
+ id: `service:${serviceName}`,
5338
+ name: serviceName,
5339
+ type: detectServiceType(serviceName),
5340
+ filePath: composeFile,
5341
+ language: "docker",
5342
+ metadata: { source: "docker-compose" }
5343
+ });
5344
+ }
5345
+ } catch {
5346
+ }
5347
+ }
5348
+ }
5349
+ return { entities, relationships };
5350
+ }
5351
+ function inferRelationships(entities, _rootDir) {
5352
+ const relationships = [];
5353
+ const models = entities.filter((e) => e.type === "model");
5354
+ const apis = entities.filter((e) => e.type === "api");
5355
+ for (const api of apis) {
5356
+ const apiPath = api.metadata.path || api.name;
5357
+ for (const model of models) {
5358
+ const modelName = model.name.toLowerCase().replace(/_/g, "");
5359
+ const pathLower = apiPath.toLowerCase().replace(/[/-]/g, "");
5360
+ if (pathLower.includes(modelName) || modelName.includes(pathLower.split("/").pop() || "")) {
5361
+ const method = api.metadata.method || "ANY";
5362
+ const relation = ["POST", "PUT", "PATCH", "DELETE"].includes(method) ? "writes" : "reads";
5363
+ relationships.push({ sourceId: api.id, targetId: model.id, relation });
5364
+ }
5365
+ }
5366
+ }
5367
+ const fileEntities = entities.filter((e) => e.type === "file");
5368
+ for (const file of fileEntities) {
5369
+ const dir = path10.dirname(file.filePath).split("/")[0];
5370
+ if (dir && dir !== ".") {
5371
+ const moduleId = `module:${dir}`;
5372
+ if (!entities.some((e) => e.id === moduleId)) {
5373
+ entities.push({
5374
+ id: moduleId,
5375
+ name: dir,
5376
+ type: "module",
5377
+ filePath: dir,
5378
+ language: "directory",
5379
+ metadata: {}
5380
+ });
5381
+ }
5382
+ relationships.push({ sourceId: file.id, targetId: moduleId, relation: "belongs-to" });
5383
+ }
5384
+ }
5385
+ return relationships;
5386
+ }
5387
+ function getLineNumber(content, index) {
5388
+ return content.slice(0, index).split("\n").length;
5389
+ }
5390
+ function resolveRelativeImport(currentFile, importPath) {
5391
+ const dir = path10.dirname(currentFile);
5392
+ let resolved = path10.posix.join(dir, importPath);
5393
+ if (!path10.extname(resolved)) {
5394
+ resolved += ".ts";
5395
+ }
5396
+ return resolved;
5397
+ }
5398
+ function categorizeFile(filePath, language) {
5399
+ const lower = filePath.toLowerCase();
5400
+ if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("__tests__") || lower.includes("/test/") || lower.includes("/tests/")) return "test";
5401
+ if (["json", "yaml", "toml"].includes(language) || lower.includes("config") || lower.includes(".env")) return "config";
5402
+ if (language === "markdown" || lower.includes("/docs/") || lower.includes("/doc/")) return "docs";
5403
+ if (language === "docker" || lower.includes("makefile") || lower.includes("webpack") || lower.includes("rollup") || lower.includes("vite")) return "build";
5404
+ if (["html", "css", "scss", "less"].includes(language)) return "asset";
5405
+ return "source";
5406
+ }
5407
+ function detectClassType(name, content) {
5408
+ if (content.includes(".init(") || content.includes("@Entity") || content.includes("tableName")) return "model";
5409
+ if (name.includes("Controller") || name.includes("Handler")) return "service";
5410
+ if (name.includes("Service") || name.includes("Provider")) return "service";
5411
+ if (name.includes("Middleware")) return "middleware";
5412
+ if (name.includes("Component") || name.includes("Widget")) return "component";
5413
+ return "class";
5414
+ }
5415
+ function detectPythonClassType(name, bases, _content) {
5416
+ if (bases.includes("Model") || bases.includes("Base") || bases.includes("db.Model")) return "model";
5417
+ if (name.includes("View") || name.includes("ViewSet") || bases.includes("APIView")) return "service";
5418
+ if (name.includes("Serializer")) return "class";
5419
+ return "class";
5420
+ }
5421
+ function detectServiceType(name) {
5422
+ const lower = name.toLowerCase();
5423
+ if (lower.includes("redis") || lower.includes("memcache")) return "cache";
5424
+ if (lower.includes("rabbit") || lower.includes("kafka") || lower.includes("nats")) return "queue";
5425
+ if (lower.includes("postgres") || lower.includes("mysql") || lower.includes("mongo") || lower.includes("db")) return "database";
5426
+ return "external-api";
5427
+ }
5428
+ var init_project_scanner = __esm({
5429
+ "src/scanner/project-scanner.ts"() {
5430
+ "use strict";
5431
+ init_esm_shims();
5432
+ init_language_detector();
5433
+ }
5434
+ });
5435
+
5436
+ // src/scanner/github-cloner.ts
5437
+ import * as fs10 from "fs";
5438
+ import * as os from "os";
5439
+ import * as path11 from "path";
5440
+ import { execSync } from "child_process";
5441
+ async function cloneAndScan(options) {
5442
+ const { target, cloneDir, branch, depth = 1, keepClone, onProgress, ...scanOpts } = options;
5443
+ const resolved = resolveTarget(target);
5444
+ let projectDir;
5445
+ if (resolved.type === "local") {
5446
+ projectDir = resolved.path;
5447
+ onProgress?.("clone", 100, `Using local directory: ${projectDir}`);
5448
+ } else {
5449
+ const tempBase = cloneDir || path11.join(os.tmpdir(), "opencroc-scan");
5450
+ fs10.mkdirSync(tempBase, { recursive: true });
5451
+ projectDir = path11.join(tempBase, resolved.repoName);
5452
+ if (fs10.existsSync(projectDir)) {
5453
+ fs10.rmSync(projectDir, { recursive: true, force: true });
5454
+ }
5455
+ onProgress?.("clone", 10, `Cloning ${resolved.url}...`);
5456
+ const branchArg = branch ? `--branch ${branch}` : "";
5457
+ const depthArg = depth > 0 ? `--depth ${depth}` : "";
5458
+ const cmd = `git clone ${branchArg} ${depthArg} --single-branch ${resolved.url} "${projectDir}"`;
5459
+ try {
5460
+ execSync(cmd, {
5461
+ stdio: "pipe",
5462
+ timeout: 12e4
5463
+ // 2 minutes max
5464
+ });
5465
+ } catch (err) {
5466
+ throw new Error(`Failed to clone repository: ${err.message}`);
5467
+ }
5468
+ onProgress?.("clone", 100, `Cloned to ${projectDir}`);
5469
+ }
5470
+ const scanResult = await scanProject({
5471
+ rootDir: projectDir,
5472
+ ...scanOpts,
5473
+ onProgress
5474
+ });
5475
+ if (resolved.type === "git" && !keepClone) {
5476
+ try {
5477
+ fs10.rmSync(projectDir, { recursive: true, force: true });
5478
+ } catch {
5479
+ }
5480
+ }
5481
+ return {
5482
+ ...scanResult,
5483
+ clonedPath: resolved.type === "git" ? projectDir : void 0
5484
+ };
5485
+ }
5486
+ function resolveTarget(target) {
5487
+ const resolved = path11.resolve(target);
5488
+ if (fs10.existsSync(resolved)) {
5489
+ return {
5490
+ type: "local",
5491
+ path: resolved,
5492
+ repoName: path11.basename(resolved)
5493
+ };
5494
+ }
5495
+ if (target.startsWith("https://") || target.startsWith("http://") || target.startsWith("git@")) {
5496
+ let url = target;
5497
+ if (!url.endsWith(".git")) url += ".git";
5498
+ const repoName = path11.basename(url, ".git");
5499
+ return { type: "git", path: "", url, repoName };
5500
+ }
5501
+ if (/^[\w.-]+\/[\w.-]+$/.test(target)) {
5502
+ const url = `https://github.com/${target}.git`;
5503
+ const repoName = target.split("/")[1];
5504
+ return { type: "git", path: "", url, repoName };
5505
+ }
5506
+ throw new Error(
5507
+ `Cannot resolve target "${target}". Expected: local path, GitHub URL, or shorthand (user/repo).`
5508
+ );
5509
+ }
5510
+ var init_github_cloner = __esm({
5511
+ "src/scanner/github-cloner.ts"() {
5512
+ "use strict";
5513
+ init_esm_shims();
5514
+ init_project_scanner();
5515
+ }
5516
+ });
5517
+
5518
+ // src/graph/index.ts
5519
+ function buildKnowledgeGraph(scanResult, options) {
5520
+ const startTime = Date.now();
5521
+ const entityMap = /* @__PURE__ */ new Map();
5522
+ for (const entity of scanResult.entities) {
5523
+ if (!entityMap.has(entity.id)) {
5524
+ entityMap.set(entity.id, entity);
5525
+ }
5526
+ }
5527
+ const nodes = [];
5528
+ for (const entity of entityMap.values()) {
5529
+ nodes.push({
5530
+ id: entity.id,
5531
+ label: entity.name,
5532
+ type: entity.type,
5533
+ filePath: entity.filePath || void 0,
5534
+ line: entity.line,
5535
+ module: inferModule(entity),
5536
+ language: entity.language,
5537
+ metadata: entity.metadata,
5538
+ status: "idle"
5539
+ });
5540
+ }
5541
+ const edges = [];
5542
+ const edgeSet = /* @__PURE__ */ new Set();
5543
+ for (const rel of scanResult.relationships) {
5544
+ let targetId = rel.targetId;
5545
+ if (targetId.includes(":*:")) {
5546
+ const suffix = targetId.split(":*:")[1];
5547
+ const resolved = findMatchingEntity(entityMap, targetId.split(":")[0], suffix);
5548
+ if (resolved) {
5549
+ targetId = resolved;
5550
+ } else {
5551
+ continue;
5552
+ }
5553
+ }
5554
+ if (rel.sourceId === targetId) continue;
5555
+ if (!entityMap.has(rel.sourceId) && !entityMap.has(targetId)) continue;
5556
+ const edgeKey = `${rel.sourceId}->${targetId}:${rel.relation}`;
5557
+ if (edgeSet.has(edgeKey)) continue;
5558
+ edgeSet.add(edgeKey);
5559
+ edges.push({
5560
+ id: `edge-${edges.length}`,
5561
+ source: rel.sourceId,
5562
+ target: targetId,
5563
+ relation: rel.relation,
5564
+ metadata: rel.metadata
5565
+ });
5566
+ }
5567
+ const projectInfo = buildProjectMetadata(scanResult, options, nodes);
5568
+ return {
5569
+ nodes,
5570
+ edges,
5571
+ projectInfo,
5572
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
5573
+ buildDuration: Date.now() - startTime
5574
+ };
5575
+ }
5576
+ function getNeighbors(graph, nodeId) {
5577
+ return {
5578
+ incoming: graph.edges.filter((e) => e.target === nodeId),
5579
+ outgoing: graph.edges.filter((e) => e.source === nodeId)
5580
+ };
5581
+ }
5582
+ function bfsTraversal(graph, startNodeId, maxDepth = 3) {
5583
+ const visited = /* @__PURE__ */ new Set();
5584
+ const queue = [{ id: startNodeId, depth: 0 }];
5585
+ visited.add(startNodeId);
5586
+ const adjacency = /* @__PURE__ */ new Map();
5587
+ for (const edge of graph.edges) {
5588
+ if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
5589
+ adjacency.get(edge.source).push(edge.target);
5590
+ if (!adjacency.has(edge.target)) adjacency.set(edge.target, []);
5591
+ adjacency.get(edge.target).push(edge.source);
5592
+ }
5593
+ while (queue.length > 0) {
5594
+ const current = queue.shift();
5595
+ if (current.depth >= maxDepth) continue;
5596
+ const neighbors = adjacency.get(current.id) || [];
5597
+ for (const neighbor of neighbors) {
5598
+ if (!visited.has(neighbor)) {
5599
+ visited.add(neighbor);
5600
+ queue.push({ id: neighbor, depth: current.depth + 1 });
5601
+ }
5602
+ }
5603
+ }
5604
+ visited.delete(startNodeId);
5605
+ return [...visited];
5606
+ }
5607
+ function toMermaid(graph, options) {
5608
+ const maxNodes = options?.maxNodes || 50;
5609
+ const nodeTypes = options?.nodeTypes;
5610
+ let filteredNodes = graph.nodes;
5611
+ if (nodeTypes) {
5612
+ filteredNodes = filteredNodes.filter((n) => nodeTypes.includes(n.type));
5613
+ }
5614
+ filteredNodes = filteredNodes.slice(0, maxNodes);
5615
+ const nodeIds = new Set(filteredNodes.map((n) => n.id));
5616
+ const filteredEdges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
5617
+ const lines = ["graph TD"];
5618
+ lines.push(" classDef model fill:#4ecca3,color:#000,stroke:#2d9970");
5619
+ lines.push(" classDef api fill:#e94560,color:#fff,stroke:#c23049");
5620
+ lines.push(" classDef service fill:#3498db,color:#fff,stroke:#2378b8");
5621
+ lines.push(" classDef module fill:#f39c12,color:#000,stroke:#c27d0e");
5622
+ lines.push(" classDef component fill:#9b59b6,color:#fff,stroke:#7d3c98");
5623
+ lines.push(" classDef file fill:#555,color:#fff,stroke:#333");
5624
+ for (const node of filteredNodes) {
5625
+ const safeId = sanitizeMermaidId(node.id);
5626
+ const safeLabel = node.label.replace(/"/g, "'");
5627
+ lines.push(` ${safeId}["${safeLabel}"]:::${node.type}`);
5628
+ }
5629
+ for (const edge of filteredEdges) {
5630
+ const safeSource = sanitizeMermaidId(edge.source);
5631
+ const safeTarget = sanitizeMermaidId(edge.target);
5632
+ const label = edge.relation;
5633
+ lines.push(` ${safeSource} -->|${label}| ${safeTarget}`);
5634
+ }
5635
+ return lines.join("\n");
5636
+ }
5637
+ function getGraphStats(graph) {
5638
+ const stats = {
5639
+ totalNodes: graph.nodes.length,
5640
+ totalEdges: graph.edges.length
5641
+ };
5642
+ for (const node of graph.nodes) {
5643
+ const key = `${node.type}Count`;
5644
+ stats[key] = (stats[key] || 0) + 1;
5645
+ }
5646
+ for (const edge of graph.edges) {
5647
+ const key = `${edge.relation}Count`;
5648
+ stats[key] = (stats[key] || 0) + 1;
5649
+ }
5650
+ return stats;
5651
+ }
5652
+ function inferModule(entity) {
5653
+ if (!entity.filePath) return void 0;
5654
+ const parts = entity.filePath.split("/");
5655
+ if (parts.length > 1) {
5656
+ const dir = parts[0];
5657
+ if (dir === "src" && parts.length > 2) return parts[1];
5658
+ return dir;
5659
+ }
5660
+ return void 0;
5661
+ }
5662
+ function findMatchingEntity(entityMap, typePrefix, nameSuffix) {
5663
+ for (const [id, entity] of entityMap) {
5664
+ if (id.startsWith(`${typePrefix}:`) && entity.name === nameSuffix) {
5665
+ return id;
5666
+ }
5667
+ }
5668
+ return null;
5669
+ }
5670
+ function buildProjectMetadata(scanResult, options, nodes) {
5671
+ const stats = {
5672
+ totalFiles: scanResult.files.length,
5673
+ totalLines: scanResult.files.reduce((sum, f) => sum + f.lines, 0),
5674
+ modules: nodes.filter((n) => n.type === "module").length,
5675
+ classes: nodes.filter((n) => n.type === "class").length,
5676
+ functions: nodes.filter((n) => n.type === "function").length,
5677
+ apiEndpoints: nodes.filter((n) => n.type === "api").length,
5678
+ dataModels: nodes.filter((n) => n.type === "model").length,
5679
+ dependencies: nodes.filter((n) => n.type === "dependency").length,
5680
+ linesByLanguage: scanResult.languages
5681
+ };
5682
+ return {
5683
+ name: options.projectName,
5684
+ source: options.source,
5685
+ sourceUrl: options.sourceUrl,
5686
+ rootPath: options.rootPath,
5687
+ languages: scanResult.languages,
5688
+ frameworks: scanResult.frameworks.map((f) => f.name),
5689
+ packageManager: void 0,
5690
+ projectType: "unknown",
5691
+ stats
5692
+ };
5693
+ }
5694
+ function sanitizeMermaidId(id) {
5695
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
5696
+ }
5697
+ var init_graph = __esm({
5698
+ "src/graph/index.ts"() {
5699
+ "use strict";
5700
+ init_esm_shims();
5701
+ }
5702
+ });
5703
+
5704
+ // src/insight/index.ts
5705
+ async function analyzeRisks(graph, options) {
5706
+ const risks = [];
5707
+ let riskCounter = 0;
5708
+ options?.onProgress?.("risk-analysis", 0, "Starting risk analysis...");
5709
+ const apis = graph.nodes.filter((n) => n.type === "api");
5710
+ const middlewares = graph.nodes.filter((n) => n.type === "middleware");
5711
+ const hasAuthMiddleware = middlewares.some(
5712
+ (m) => m.label.toLowerCase().includes("auth") || m.label.toLowerCase().includes("jwt") || m.label.toLowerCase().includes("session")
5713
+ );
5714
+ for (const api of apis) {
5715
+ const incoming = graph.edges.filter((e) => e.target === api.id);
5716
+ const hasAuth = incoming.some((e) => {
5717
+ const sourceNode = graph.nodes.find((n) => n.id === e.source);
5718
+ return sourceNode?.type === "middleware" && (sourceNode.label.toLowerCase().includes("auth") || e.relation === "middleware-of");
5719
+ });
5720
+ if (!hasAuth && !hasAuthMiddleware) {
5721
+ const apiPath = api.metadata.path || api.label;
5722
+ const isSensitive = /user|admin|password|token|secret|key|delete|payment/i.test(apiPath);
5723
+ if (isSensitive) {
5724
+ risks.push({
5725
+ id: `risk-${++riskCounter}`,
5726
+ category: "security",
5727
+ severity: "high",
5728
+ title: `Potentially unprotected sensitive endpoint: ${api.label}`,
5729
+ description: `The endpoint ${api.label} appears to handle sensitive data but no authentication middleware was detected in the graph.`,
5730
+ affectedNodes: [api.id],
5731
+ suggestion: "Add authentication middleware to protect this endpoint.",
5732
+ confidence: 0.6
5733
+ });
5734
+ }
5735
+ }
5736
+ }
5737
+ options?.onProgress?.("risk-analysis", 20, "Checking data integrity...");
5738
+ const models = graph.nodes.filter((n) => n.type === "model");
5739
+ for (const model of models) {
5740
+ const writeEdges = graph.edges.filter((e) => e.target === model.id && e.relation === "writes");
5741
+ if (writeEdges.length > 3) {
5742
+ risks.push({
5743
+ id: `risk-${++riskCounter}`,
5744
+ category: "data-integrity",
5745
+ severity: "medium",
5746
+ title: `High write fan-in on model: ${model.label}`,
5747
+ description: `Model "${model.label}" is written to by ${writeEdges.length} different endpoints. This increases risk of data conflicts and race conditions.`,
5748
+ affectedNodes: [model.id, ...writeEdges.map((e) => e.source)],
5749
+ suggestion: "Consider adding transaction boundaries or an optimistic locking strategy.",
5750
+ confidence: 0.7
5751
+ });
5752
+ }
5753
+ }
5754
+ const foreignKeyEdges = graph.edges.filter((e) => e.relation === "foreign-key" || e.relation === "cascade-delete");
5755
+ for (const fk of foreignKeyEdges) {
5756
+ const sourceNode = graph.nodes.find((n) => n.id === fk.source);
5757
+ const targetNode = graph.nodes.find((n) => n.id === fk.target);
5758
+ if (sourceNode && targetNode) {
5759
+ const dependents = graph.edges.filter(
5760
+ (e) => (e.relation === "foreign-key" || e.relation === "cascade-delete") && e.target === fk.target
5761
+ );
5762
+ if (dependents.length >= 3) {
5763
+ risks.push({
5764
+ id: `risk-${++riskCounter}`,
5765
+ category: "data-integrity",
5766
+ severity: "high",
5767
+ title: `Cascade risk: ${targetNode.label} has ${dependents.length} dependent tables`,
5768
+ description: `Deleting records from "${targetNode.label}" could cascade to ${dependents.length} other tables.`,
5769
+ affectedNodes: [fk.target, ...dependents.map((d) => d.source)],
5770
+ suggestion: "Implement soft deletes or add cascade protection.",
5771
+ confidence: 0.85
5772
+ });
5773
+ }
5774
+ }
5775
+ }
5776
+ options?.onProgress?.("risk-analysis", 40, "Checking performance...");
5777
+ const moduleNodes = graph.nodes.filter((n) => n.type === "module");
5778
+ for (const mod of moduleNodes) {
5779
+ const children = graph.edges.filter((e) => e.target === mod.id && e.relation === "belongs-to");
5780
+ if (children.length > 50) {
5781
+ risks.push({
5782
+ id: `risk-${++riskCounter}`,
5783
+ category: "maintainability",
5784
+ severity: "medium",
5785
+ title: `Large module: ${mod.label} (${children.length} entities)`,
5786
+ description: `Module "${mod.label}" contains ${children.length} entities. Consider splitting for better maintainability.`,
5787
+ affectedNodes: [mod.id],
5788
+ suggestion: "Split into smaller, focused sub-modules.",
5789
+ confidence: 0.75
5790
+ });
5791
+ }
5792
+ }
5793
+ const cycles = detectCycles2(graph);
5794
+ for (const cycle of cycles) {
5795
+ risks.push({
5796
+ id: `risk-${++riskCounter}`,
5797
+ category: "logic",
5798
+ severity: "high",
5799
+ title: `Circular dependency detected: ${cycle.map((id) => graph.nodes.find((n) => n.id === id)?.label || id).join(" \u2192 ")}`,
5800
+ description: `A circular dependency was found involving ${cycle.length} entities. This can cause initialization issues and makes testing harder.`,
5801
+ affectedNodes: cycle,
5802
+ suggestion: "Break the cycle by introducing an interface or Event-based decoupling.",
5803
+ confidence: 0.9
5804
+ });
5805
+ }
5806
+ options?.onProgress?.("risk-analysis", 60, "Checking maintainability...");
5807
+ for (const node of graph.nodes) {
5808
+ if (node.type === "file" || node.type === "dependency") continue;
5809
+ const outgoing = graph.edges.filter((e) => e.source === node.id);
5810
+ const incoming = graph.edges.filter((e) => e.target === node.id);
5811
+ const coupling = outgoing.length + incoming.length;
5812
+ if (coupling > 15) {
5813
+ risks.push({
5814
+ id: `risk-${++riskCounter}`,
5815
+ category: "maintainability",
5816
+ severity: "medium",
5817
+ title: `High coupling: ${node.label} (${coupling} connections)`,
5818
+ description: `"${node.label}" has ${coupling} connections (${outgoing.length} outgoing, ${incoming.length} incoming). Changes here will have wide impact.`,
5819
+ affectedNodes: [node.id],
5820
+ suggestion: "Consider extracting shared logic or adding an abstraction layer.",
5821
+ confidence: 0.7
5822
+ });
5823
+ }
5824
+ }
5825
+ for (const api of apis) {
5826
+ const apiPath = api.metadata.path || api.label;
5827
+ if (apiPath.includes(":") || apiPath.includes("{")) {
5828
+ const method = api.metadata.method || "";
5829
+ if (["DELETE", "PUT", "PATCH"].includes(method)) {
5830
+ risks.push({
5831
+ id: `risk-${++riskCounter}`,
5832
+ category: "security",
5833
+ severity: "low",
5834
+ title: `Verify input validation: ${api.label}`,
5835
+ description: `Endpoint ${api.label} accepts path parameters. Ensure proper input validation and authorization checks.`,
5836
+ affectedNodes: [api.id],
5837
+ suggestion: "Add input validation middleware and verify the user has permission to modify the specified resource.",
5838
+ confidence: 0.5
5839
+ });
5840
+ }
5841
+ }
5842
+ }
5843
+ options?.onProgress?.("risk-analysis", 80, `Found ${risks.length} risks`);
5844
+ if (options?.useLlm && options.llm) {
5845
+ try {
5846
+ const llmRisks = await getLlmRisks(graph, risks, options.llm);
5847
+ risks.push(...llmRisks);
5848
+ } catch {
5849
+ }
5850
+ }
5851
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
5852
+ risks.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
5853
+ options?.onProgress?.("risk-analysis", 100, `Analysis complete: ${risks.length} risks found`);
5854
+ return risks;
5855
+ }
5856
+ function analyzeImpact(graph, nodeId) {
5857
+ const node = graph.nodes.find((n) => n.id === nodeId);
5858
+ if (!node) {
5859
+ return {
5860
+ sourceNode: nodeId,
5861
+ directImpact: [],
5862
+ transitiveImpact: [],
5863
+ riskLevel: "low",
5864
+ summary: `Node "${nodeId}" not found in the graph.`,
5865
+ mermaidText: ""
5866
+ };
5867
+ }
5868
+ const { incoming, outgoing } = getNeighbors(graph, nodeId);
5869
+ const directNodes = /* @__PURE__ */ new Set([
5870
+ ...incoming.map((e) => e.source),
5871
+ ...outgoing.map((e) => e.target)
5872
+ ]);
5873
+ directNodes.delete(nodeId);
5874
+ const transitiveNodes = bfsTraversal(graph, nodeId, 3);
5875
+ const totalImpact = transitiveNodes.length;
5876
+ let riskLevel;
5877
+ if (totalImpact > 20) riskLevel = "critical";
5878
+ else if (totalImpact > 10) riskLevel = "high";
5879
+ else if (totalImpact > 5) riskLevel = "medium";
5880
+ else riskLevel = "low";
5881
+ const summary = `Changing "${node.label}" directly affects ${directNodes.size} entities and transitively impacts ${transitiveNodes.length} entities (risk: ${riskLevel}).`;
5882
+ const impactNodeIds = /* @__PURE__ */ new Set([nodeId, ...directNodes, ...transitiveNodes.slice(0, 20)]);
5883
+ const impactNodes = graph.nodes.filter((n) => impactNodeIds.has(n.id));
5884
+ const impactEdges = graph.edges.filter((e) => impactNodeIds.has(e.source) && impactNodeIds.has(e.target));
5885
+ let mermaidText = "graph TD\n";
5886
+ mermaidText += ` style ${sanitizeId(nodeId)} fill:#e94560,color:#fff
5887
+ `;
5888
+ for (const dn of directNodes) {
5889
+ mermaidText += ` style ${sanitizeId(dn)} fill:#f39c12,color:#000
5890
+ `;
5891
+ }
5892
+ for (const n of impactNodes) {
5893
+ mermaidText += ` ${sanitizeId(n.id)}["${n.label.replace(/"/g, "'")}"]
5894
+ `;
5895
+ }
5896
+ for (const e of impactEdges) {
5897
+ mermaidText += ` ${sanitizeId(e.source)} -->|${e.relation}| ${sanitizeId(e.target)}
5898
+ `;
5899
+ }
5900
+ return {
5901
+ sourceNode: nodeId,
5902
+ directImpact: [...directNodes],
5903
+ transitiveImpact: transitiveNodes,
5904
+ riskLevel,
5905
+ summary,
5906
+ mermaidText
5907
+ };
5908
+ }
5909
+ async function generateReport(graph, perspective, risks, options) {
5910
+ if (options?.useLlm && options.llm) {
5911
+ return generateLlmReport(graph, perspective, risks, options.llm);
5912
+ }
5913
+ switch (perspective) {
5914
+ case "developer":
5915
+ return buildDeveloperReport(graph, risks);
5916
+ case "architect":
5917
+ return buildArchitectReport(graph, risks);
5918
+ case "tester":
5919
+ return buildTesterReport(graph, risks);
5920
+ case "product":
5921
+ return buildProductReport(graph, risks);
5922
+ case "student":
5923
+ return buildStudentReport(graph, risks);
5924
+ case "executive":
5925
+ return buildExecutiveReport(graph, risks);
5926
+ default:
5927
+ return buildDeveloperReport(graph, risks);
5928
+ }
5929
+ }
5930
+ function buildDeveloperReport(graph, risks) {
5931
+ const { projectInfo } = graph;
5932
+ const stats = projectInfo.stats;
5933
+ const sections = [
5934
+ {
5935
+ heading: "Project Overview",
5936
+ content: `**${projectInfo.name}** is a ${projectInfo.projectType} project using ${projectInfo.frameworks.join(", ") || "unknown frameworks"}.
5937
+
5938
+ - **Files**: ${stats.totalFiles} | **Lines**: ${stats.totalLines.toLocaleString()}
5939
+ - **Languages**: ${Object.entries(projectInfo.languages).map(([k, v]) => `${k}(${v})`).join(", ")}
5940
+ - **APIs**: ${stats.apiEndpoints} | **Models**: ${stats.dataModels} | **Functions**: ${stats.functions}`
5941
+ },
5942
+ {
5943
+ heading: "Architecture Map",
5944
+ content: "Module-level dependency graph:",
5945
+ visualization: {
5946
+ type: "mermaid",
5947
+ data: toMermaid(graph, { nodeTypes: ["module", "model", "api"], maxNodes: 30 })
5948
+ }
5949
+ },
5950
+ {
5951
+ heading: "API Endpoints",
5952
+ content: graph.nodes.filter((n) => n.type === "api").map((a) => `- \`${a.label}\` (${a.filePath || "unknown"})`).join("\n") || "No API endpoints detected."
5953
+ },
5954
+ {
5955
+ heading: "Data Models",
5956
+ content: graph.nodes.filter((n) => n.type === "model").map((m) => `- **${m.label}** (${m.filePath || "unknown"})`).join("\n") || "No data models detected."
5957
+ },
5958
+ {
5959
+ heading: "Risk Report",
5960
+ content: risks.length === 0 ? "No significant risks detected." : risks.slice(0, 10).map(
5961
+ (r) => `- **[${r.severity.toUpperCase()}]** ${r.title}
5962
+ ${r.description}`
5963
+ ).join("\n\n")
5964
+ }
5965
+ ];
5966
+ return {
5967
+ perspective: "developer",
5968
+ title: `Developer Report: ${projectInfo.name}`,
5969
+ summary: `${projectInfo.name} \u2014 ${stats.apiEndpoints} APIs, ${stats.dataModels} models, ${risks.length} risks detected.`,
5970
+ sections,
5971
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
5972
+ };
5973
+ }
5974
+ function buildArchitectReport(graph, risks) {
5975
+ const { projectInfo } = graph;
5976
+ const modules = graph.nodes.filter((n) => n.type === "module");
5977
+ const criticalRisks = risks.filter((r) => r.severity === "critical" || r.severity === "high");
5978
+ const sections = [
5979
+ {
5980
+ heading: "System Architecture",
5981
+ content: `**Type**: ${projectInfo.projectType}
5982
+ **Frameworks**: ${projectInfo.frameworks.join(", ")}
5983
+ **Modules**: ${modules.length}
5984
+
5985
+ The system is organized into ${modules.length} modules with ${graph.edges.length} relationships.`,
5986
+ visualization: {
5987
+ type: "mermaid",
5988
+ data: toMermaid(graph, { nodeTypes: ["module"], maxNodes: 20 })
5989
+ }
5990
+ },
5991
+ {
5992
+ heading: "Module Coupling Analysis",
5993
+ content: modules.map((m) => {
5994
+ const edges = graph.edges.filter((e) => e.source === m.id || e.target === m.id);
5995
+ return `- **${m.label}**: ${edges.length} connections`;
5996
+ }).join("\n")
5997
+ },
5998
+ {
5999
+ heading: "Technical Debt & Risk",
6000
+ content: criticalRisks.length === 0 ? "No critical or high-severity risks." : criticalRisks.map((r) => `- **[${r.severity}]** ${r.title}
6001
+ _Suggestion_: ${r.suggestion || "N/A"}`).join("\n\n")
6002
+ },
6003
+ {
6004
+ heading: "Recommendations",
6005
+ content: generateArchitectRecommendations(graph, risks)
6006
+ }
6007
+ ];
6008
+ return {
6009
+ perspective: "architect",
6010
+ title: `Architecture Report: ${projectInfo.name}`,
6011
+ summary: `${modules.length} modules, ${graph.edges.length} relationships, ${criticalRisks.length} critical/high risks.`,
6012
+ sections,
6013
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
6014
+ };
6015
+ }
6016
+ function buildTesterReport(graph, risks) {
6017
+ const apis = graph.nodes.filter((n) => n.type === "api");
6018
+ const tests = graph.nodes.filter((n) => n.type === "test");
6019
+ const riskyApis = risks.filter((r) => r.category === "security" || r.category === "data-integrity");
6020
+ const sections = [
6021
+ {
6022
+ heading: "Test Coverage Overview",
6023
+ content: `- **API Endpoints**: ${apis.length}
6024
+ - **Test Files Found**: ${tests.length}
6025
+ - **Estimated Coverage**: ${tests.length > 0 ? Math.min(Math.round(tests.length / Math.max(apis.length, 1) * 100), 100) : 0}%`
6026
+ },
6027
+ {
6028
+ heading: "Priority Test Targets",
6029
+ content: "Endpoints with highest risk that need testing first:\n\n" + riskyApis.slice(0, 10).map((r, i) => `${i + 1}. **${r.title}** (${r.severity})
6030
+ ${r.description}`).join("\n\n")
6031
+ },
6032
+ {
6033
+ heading: "Edge Cases to Consider",
6034
+ content: apis.slice(0, 10).map((api) => {
6035
+ const method = api.metadata.method || "ANY";
6036
+ const suggestions = [];
6037
+ if (method === "POST" || method === "PUT") suggestions.push("Empty body", "Invalid types", "Missing required fields", "Extremely long strings");
6038
+ if (method === "DELETE") suggestions.push("Non-existent ID", "Already deleted", "ID with dependencies");
6039
+ if (method === "GET") suggestions.push("Invalid query params", "Large pagination", "Non-existent ID");
6040
+ return `- **${api.label}**: ${suggestions.join(", ")}`;
6041
+ }).join("\n")
6042
+ }
6043
+ ];
6044
+ return {
6045
+ perspective: "tester",
6046
+ title: `Testing Report: ${graph.projectInfo.name}`,
6047
+ summary: `${apis.length} endpoints to test, ${riskyApis.length} high-risk areas identified.`,
6048
+ sections,
6049
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
6050
+ };
6051
+ }
6052
+ function buildProductReport(graph, risks) {
6053
+ const { projectInfo } = graph;
6054
+ const modules = graph.nodes.filter((n) => n.type === "module");
6055
+ const sections = [
6056
+ {
6057
+ heading: "What Does This System Do?",
6058
+ content: `This is a **${projectInfo.projectType}** system built with ${projectInfo.frameworks.join(", ")}. It contains ${modules.length} functional modules and ${projectInfo.stats.apiEndpoints} service interfaces.`
6059
+ },
6060
+ {
6061
+ heading: "Feature Map",
6062
+ content: modules.map((m) => {
6063
+ const children = graph.edges.filter((e) => e.target === m.id).length;
6064
+ return `- **${m.label}** \u2014 ${children} components`;
6065
+ }).join("\n")
6066
+ },
6067
+ {
6068
+ heading: "Health Status",
6069
+ content: (() => {
6070
+ const critical = risks.filter((r) => r.severity === "critical").length;
6071
+ const high = risks.filter((r) => r.severity === "high").length;
6072
+ if (critical > 0) return `\u26A0\uFE0F **Needs Attention**: ${critical} critical issues found that could affect users.`;
6073
+ if (high > 3) return `\u26A1 **Minor Concerns**: ${high} areas that should be improved.`;
6074
+ return "\u2705 **Healthy**: No critical issues detected. System is in good shape.";
6075
+ })()
6076
+ }
6077
+ ];
6078
+ return {
6079
+ perspective: "product",
6080
+ title: `Product Overview: ${projectInfo.name}`,
6081
+ summary: `${modules.length} feature modules, ${risks.filter((r) => r.severity === "critical").length} critical issues.`,
6082
+ sections,
6083
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
6084
+ };
6085
+ }
6086
+ function buildStudentReport(graph, risks) {
6087
+ const { projectInfo } = graph;
6088
+ const sections = [
6089
+ {
6090
+ heading: "What is this project?",
6091
+ content: `This is a **${projectInfo.projectType}** project. Let's break it down step by step!
6092
+
6093
+ **Languages used**: ${Object.keys(projectInfo.languages).join(", ")}
6094
+ **Frameworks**: ${projectInfo.frameworks.join(", ") || "None detected"}
6095
+
6096
+ Think of this project like a building:
6097
+ - The **frameworks** are the building's foundation
6098
+ - The **modules** are different rooms
6099
+ - The **APIs** are the doors and windows (interfaces to the outside world)
6100
+ - The **models** are the furniture and storage (data structures)`
6101
+ },
6102
+ {
6103
+ heading: "How is it organized?",
6104
+ content: `The project has **${projectInfo.stats.modules}** modules (think: folders of related code).
6105
+
6106
+ Each module typically contains:
6107
+ 1. **Controllers/Routes** \u2014 Handle incoming requests (like a receptionist)
6108
+ 2. **Services** \u2014 Business logic (like the workers)
6109
+ 3. **Models** \u2014 Data structures (like forms and documents)
6110
+
6111
+ Here's a simplified view:`,
6112
+ visualization: {
6113
+ type: "mermaid",
6114
+ data: toMermaid(graph, { nodeTypes: ["module", "model"], maxNodes: 15 })
6115
+ }
6116
+ },
6117
+ {
6118
+ heading: "Key Concepts to Learn",
6119
+ content: `Based on this project, you should study:
6120
+
6121
+ ` + (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
6122
+ - **MVC Pattern** \u2014 Model-View-Controller architecture
6123
+ - **Authentication** \u2014 How users log in and stay logged in`
6124
+ },
6125
+ {
6126
+ heading: "Things to Watch Out For",
6127
+ content: risks.length > 0 ? `Here are ${Math.min(risks.length, 5)} interesting issues found:
6128
+
6129
+ ` + risks.slice(0, 5).map((r, i) => `${i + 1}. **${r.title}**
6130
+ _Why it matters_: ${r.description}
6131
+ _How to fix_: ${r.suggestion || "Research this topic!"}`).join("\n\n") : "This project looks clean! No major issues found."
6132
+ }
6133
+ ];
6134
+ return {
6135
+ perspective: "student",
6136
+ title: `Learning Guide: ${projectInfo.name}`,
6137
+ summary: `A ${projectInfo.projectType} project \u2014 great for learning ${Object.keys(projectInfo.languages).join(", ")}!`,
6138
+ sections,
6139
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
6140
+ };
6141
+ }
6142
+ function buildExecutiveReport(graph, risks) {
6143
+ const { projectInfo } = graph;
6144
+ const critical = risks.filter((r) => r.severity === "critical").length;
6145
+ const high = risks.filter((r) => r.severity === "high").length;
6146
+ const medium = risks.filter((r) => r.severity === "medium").length;
6147
+ const healthScore = Math.max(0, 100 - (critical * 20 + high * 10 + medium * 3));
6148
+ const sections = [
6149
+ {
6150
+ heading: "Health Score",
6151
+ content: `# ${healthScore}/100
6152
+
6153
+ ` + (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.")
6154
+ },
6155
+ {
6156
+ heading: "Key Metrics",
6157
+ content: `| Metric | Value |
6158
+ |--------|-------|
6159
+ | Codebase Size | ${projectInfo.stats.totalLines.toLocaleString()} lines |
6160
+ | Technologies | ${projectInfo.frameworks.length} frameworks |
6161
+ | API Surface | ${projectInfo.stats.apiEndpoints} endpoints |
6162
+ | Data Models | ${projectInfo.stats.dataModels} tables |
6163
+ | Critical Issues | ${critical} |
6164
+ | High Issues | ${high} |`
6165
+ },
6166
+ {
6167
+ heading: "Top 3 Risks Needing Action",
6168
+ content: risks.slice(0, 3).map(
6169
+ (r, i) => `${i + 1}. **${r.title}** (${r.severity})`
6170
+ ).join("\n") || "No significant risks."
6171
+ }
6172
+ ];
6173
+ return {
6174
+ perspective: "executive",
6175
+ title: `Executive Summary: ${projectInfo.name}`,
6176
+ summary: `Health: ${healthScore}/100 | ${critical} critical, ${high} high risks | ${projectInfo.stats.apiEndpoints} APIs`,
6177
+ sections,
6178
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
6179
+ };
6180
+ }
6181
+ function detectCycles2(graph) {
6182
+ const cycles = [];
6183
+ const adjacency = /* @__PURE__ */ new Map();
6184
+ for (const edge of graph.edges) {
6185
+ if (edge.relation !== "imports" && edge.relation !== "depends-on" && edge.relation !== "calls") continue;
6186
+ if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
6187
+ adjacency.get(edge.source).push(edge.target);
6188
+ }
6189
+ const visited = /* @__PURE__ */ new Set();
6190
+ const inStack = /* @__PURE__ */ new Set();
6191
+ const path14 = [];
6192
+ function dfs(node) {
6193
+ if (cycles.length >= 5) return;
6194
+ visited.add(node);
6195
+ inStack.add(node);
6196
+ path14.push(node);
6197
+ for (const neighbor of adjacency.get(node) || []) {
6198
+ if (!visited.has(neighbor)) {
6199
+ dfs(neighbor);
6200
+ } else if (inStack.has(neighbor)) {
6201
+ const cycleStart = path14.indexOf(neighbor);
6202
+ if (cycleStart >= 0) {
6203
+ cycles.push([...path14.slice(cycleStart), neighbor]);
6204
+ }
6205
+ }
6206
+ }
6207
+ path14.pop();
6208
+ inStack.delete(node);
6209
+ }
6210
+ for (const node of adjacency.keys()) {
6211
+ if (!visited.has(node)) {
6212
+ dfs(node);
6213
+ }
6214
+ }
6215
+ return cycles;
6216
+ }
6217
+ function generateArchitectRecommendations(_graph, risks) {
6218
+ const items = [];
6219
+ const securityRisks = risks.filter((r) => r.category === "security");
6220
+ if (securityRisks.length > 0) {
6221
+ items.push(`1. **Security Hardening**: Address ${securityRisks.length} security findings before next release.`);
6222
+ }
6223
+ const couplingRisks = risks.filter((r) => r.category === "maintainability");
6224
+ if (couplingRisks.length > 2) {
6225
+ items.push(`2. **Reduce Coupling**: ${couplingRisks.length} modules show high coupling. Consider introducing service boundaries.`);
6226
+ }
6227
+ const dataRisks = risks.filter((r) => r.category === "data-integrity");
6228
+ if (dataRisks.length > 0) {
6229
+ items.push(`3. **Data Protection**: ${dataRisks.length} data integrity concerns. Add transaction boundaries and cascade protections.`);
6230
+ }
6231
+ if (items.length === 0) {
6232
+ items.push("Architecture looks solid. Continue monitoring coupling metrics as the system grows.");
6233
+ }
6234
+ return items.join("\n\n");
6235
+ }
6236
+ async function getLlmRisks(graph, existingRisks, llm) {
6237
+ const prompt2 = `Analyze this project knowledge graph for additional risks not already identified.
6238
+
6239
+ Project: ${graph.projectInfo.name}
6240
+ Type: ${graph.projectInfo.projectType}
6241
+ Frameworks: ${graph.projectInfo.frameworks.join(", ")}
6242
+ Stats: ${graph.projectInfo.stats.apiEndpoints} APIs, ${graph.projectInfo.stats.dataModels} models
6243
+
6244
+ Already identified risks (${existingRisks.length}):
6245
+ ${existingRisks.slice(0, 5).map((r) => `- [${r.severity}] ${r.title}`).join("\n")}
6246
+
6247
+ API Endpoints: ${graph.nodes.filter((n) => n.type === "api").slice(0, 20).map((n) => n.label).join(", ")}
6248
+ Models: ${graph.nodes.filter((n) => n.type === "model").slice(0, 20).map((n) => n.label).join(", ")}
6249
+
6250
+ Return up to 3 additional risks in JSON array format:
6251
+ [{ "category": "security|performance|data-integrity|logic|maintainability|reliability", "severity": "critical|high|medium|low", "title": "...", "description": "...", "suggestion": "..." }]`;
6252
+ const response = await llm.chat([{ role: "user", content: prompt2 }]);
6253
+ try {
6254
+ const parsed = JSON.parse(cleanJsonResponse(response));
6255
+ if (!Array.isArray(parsed)) return [];
6256
+ return parsed.slice(0, 3).map((r, i) => ({
6257
+ id: `risk-llm-${i}`,
6258
+ category: r.category || "logic",
6259
+ severity: r.severity || "medium",
6260
+ title: r.title || "LLM-detected risk",
6261
+ description: r.description || "",
6262
+ affectedNodes: [],
6263
+ suggestion: r.suggestion,
6264
+ confidence: 0.6
6265
+ }));
6266
+ } catch {
6267
+ return [];
6268
+ }
6269
+ }
6270
+ async function generateLlmReport(graph, perspective, risks, llm) {
6271
+ const perspectiveDescriptions = {
6272
+ developer: "a software developer who wants technical details, code patterns, and API documentation",
6273
+ architect: "a software architect who cares about modularity, coupling, tech debt, and system design",
6274
+ tester: "a QA engineer who wants to know what to test, edge cases, and risk areas",
6275
+ product: "a product manager who wants to understand features in business terms, not code",
6276
+ student: "a computer science student learning from this codebase, explain concepts step by step",
6277
+ executive: "a CTO/VP who wants a one-page health summary with actionable insights"
6278
+ };
6279
+ const prompt2 = `Generate a project analysis report for ${graph.projectInfo.name} from the perspective of ${perspectiveDescriptions[perspective]}.
6280
+
6281
+ Project Info:
6282
+ - Type: ${graph.projectInfo.projectType}
6283
+ - Frameworks: ${graph.projectInfo.frameworks.join(", ")}
6284
+ - Stats: ${graph.projectInfo.stats.totalFiles} files, ${graph.projectInfo.stats.apiEndpoints} APIs, ${graph.projectInfo.stats.dataModels} models
6285
+ - Languages: ${Object.entries(graph.projectInfo.languages).map(([k, v]) => `${k}(${v} files)`).join(", ")}
6286
+
6287
+ Top risks:
6288
+ ${risks.slice(0, 5).map((r) => `- [${r.severity}] ${r.title}`).join("\n")}
6289
+
6290
+ Generate 3-5 report sections with clear headings and content. Use markdown formatting.
6291
+ Respond in JSON: { "title": "...", "summary": "...", "sections": [{ "heading": "...", "content": "..." }] }`;
6292
+ const response = await llm.chat([{ role: "user", content: prompt2 }]);
6293
+ try {
6294
+ const parsed = JSON.parse(cleanJsonResponse(response));
6295
+ return {
6296
+ perspective,
6297
+ title: parsed.title || `${perspective} Report`,
6298
+ summary: parsed.summary || "",
6299
+ sections: (parsed.sections || []).map((s) => ({
6300
+ heading: s.heading || "Section",
6301
+ content: s.content || ""
6302
+ })),
6303
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
6304
+ };
6305
+ } catch {
6306
+ return buildDeveloperReport(graph, risks);
6307
+ }
6308
+ }
6309
+ function cleanJsonResponse(response) {
6310
+ let cleaned = response.trim();
6311
+ if (cleaned.startsWith("```json")) cleaned = cleaned.slice(7);
6312
+ else if (cleaned.startsWith("```")) cleaned = cleaned.slice(3);
6313
+ if (cleaned.endsWith("```")) cleaned = cleaned.slice(0, -3);
6314
+ return cleaned.trim();
6315
+ }
6316
+ function sanitizeId(id) {
6317
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
6318
+ }
6319
+ var init_insight = __esm({
6320
+ "src/insight/index.ts"() {
6321
+ "use strict";
6322
+ init_esm_shims();
6323
+ init_graph();
6324
+ }
6325
+ });
6326
+
6327
+ // src/server/routes/studio.ts
6328
+ function registerStudioRoutes(app, office) {
6329
+ app.post("/api/studio/scan", async (req, reply) => {
6330
+ const { target, branch, useLlm } = req.body || {};
6331
+ if (!target || typeof target !== "string") {
6332
+ reply.code(400).send({ error: 'Missing "target" field. Provide a local path or GitHub URL.' });
6333
+ return;
6334
+ }
6335
+ office.log(`\u{1F50D} Starting scan: ${target}`, "info");
6336
+ office.updateAgent("parser-croc", { status: "working", currentTask: `Scanning ${target}...`, progress: 0 });
6337
+ try {
6338
+ const scanResult = await cloneAndScan({
6339
+ target,
6340
+ branch,
6341
+ useLlm,
6342
+ keepClone: true,
6343
+ onProgress: (phase, percent, detail) => {
6344
+ office.updateAgent("parser-croc", { currentTask: detail || phase, progress: percent });
6345
+ office.broadcast("scan:progress", { phase, percent, detail });
6346
+ }
6347
+ });
6348
+ office.updateAgent("parser-croc", { status: "done", currentTask: "Scan complete", progress: 100 });
6349
+ office.log(`\u2705 Scan complete: ${scanResult.entities.length} entities, ${scanResult.relationships.length} relationships`);
6350
+ office.updateAgent("analyzer-croc", { status: "working", currentTask: "Building knowledge graph...", progress: 0 });
6351
+ const projectName = target.includes("/") ? target.split("/").pop().replace(".git", "") : target.split(/[\\/]/).pop();
6352
+ const graph = buildKnowledgeGraph(scanResult, {
6353
+ projectName,
6354
+ source: target.startsWith("http") || /^[\w.-]+\/[\w.-]+$/.test(target) ? "github" : "local",
6355
+ sourceUrl: target,
6356
+ rootPath: target
6357
+ });
6358
+ office.updateAgent("analyzer-croc", { status: "done", currentTask: "Graph built", progress: 100 });
6359
+ office.log(`\u{1F4CA} Knowledge graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
6360
+ office.updateAgent("planner-croc", { status: "working", currentTask: "Analyzing risks...", progress: 0 });
6361
+ const risks = await analyzeRisks(graph);
6362
+ office.updateAgent("planner-croc", { status: "done", currentTask: `${risks.length} risks found`, progress: 100 });
6363
+ store.graph = graph;
6364
+ store.risks = risks;
6365
+ store.scanTime = Date.now();
6366
+ store.source = target;
6367
+ office.broadcast("graph:update", {
6368
+ nodes: graph.nodes.map((n) => ({
6369
+ id: n.id,
6370
+ label: n.label,
6371
+ type: n.type,
6372
+ module: n.module,
6373
+ status: n.status
6374
+ })),
6375
+ edges: graph.edges.map((e) => ({
6376
+ source: e.source,
6377
+ target: e.target,
6378
+ relation: e.relation
6379
+ }))
6380
+ });
6381
+ return {
6382
+ ok: true,
6383
+ project: graph.projectInfo,
6384
+ stats: getGraphStats(graph),
6385
+ risks: risks.length,
6386
+ duration: graph.buildDuration + scanResult.duration
6387
+ };
6388
+ } catch (err) {
6389
+ office.updateAgent("parser-croc", { status: "error", currentTask: String(err) });
6390
+ office.log(`\u274C Scan failed: ${err}`, "error");
6391
+ reply.code(500).send({ error: `Scan failed: ${err.message}` });
6392
+ return;
6393
+ }
6394
+ });
6395
+ app.get("/api/studio/graph", async (_req, reply) => {
6396
+ if (!store.graph) {
6397
+ reply.code(404).send({ error: "No project scanned yet. POST /api/studio/scan first." });
6398
+ return;
6399
+ }
6400
+ return {
6401
+ nodes: store.graph.nodes,
6402
+ edges: store.graph.edges,
6403
+ projectInfo: store.graph.projectInfo,
6404
+ builtAt: store.graph.builtAt,
6405
+ stats: getGraphStats(store.graph)
6406
+ };
6407
+ });
6408
+ app.get("/api/studio/graph/mermaid", async (req, reply) => {
6409
+ if (!store.graph) {
6410
+ reply.code(404).send({ error: "No project scanned yet." });
6411
+ return;
6412
+ }
6413
+ const nodeTypes = req.query.types?.split(",");
6414
+ const maxNodes = req.query.maxNodes ? parseInt(req.query.maxNodes, 10) : 50;
6415
+ return {
6416
+ mermaid: toMermaid(store.graph, { nodeTypes, maxNodes })
6417
+ };
6418
+ });
6419
+ app.get("/api/studio/risks", async (req, reply) => {
6420
+ if (!store.graph) {
6421
+ reply.code(404).send({ error: "No project scanned yet." });
6422
+ return;
6423
+ }
6424
+ let risks = store.risks;
6425
+ if (req.query.severity) {
6426
+ risks = risks.filter((r) => r.severity === req.query.severity);
6427
+ }
6428
+ if (req.query.category) {
6429
+ risks = risks.filter((r) => r.category === req.query.category);
6430
+ }
6431
+ return { total: risks.length, risks };
6432
+ });
6433
+ app.get("/api/studio/impact/:nodeId", async (req, reply) => {
6434
+ if (!store.graph) {
6435
+ reply.code(404).send({ error: "No project scanned yet." });
6436
+ return;
6437
+ }
6438
+ const nodeId = decodeURIComponent(req.params.nodeId);
6439
+ const impact = analyzeImpact(store.graph, nodeId);
6440
+ return impact;
6441
+ });
6442
+ app.get("/api/studio/report/:perspective", async (req, reply) => {
6443
+ if (!store.graph) {
6444
+ reply.code(404).send({ error: "No project scanned yet." });
6445
+ return;
6446
+ }
6447
+ const validPerspectives = ["developer", "architect", "tester", "product", "student", "executive"];
6448
+ const perspective = req.params.perspective;
6449
+ if (!validPerspectives.includes(perspective)) {
6450
+ reply.code(400).send({ error: `Invalid perspective. Valid: ${validPerspectives.join(", ")}` });
6451
+ return;
6452
+ }
6453
+ office.updateAgent("reporter-croc", { status: "working", currentTask: `Generating ${perspective} report...` });
6454
+ const report2 = await generateReport(
6455
+ store.graph,
6456
+ perspective,
6457
+ store.risks
6458
+ );
6459
+ office.updateAgent("reporter-croc", { status: "done", currentTask: "Report ready" });
6460
+ return report2;
6461
+ });
6462
+ app.get("/api/studio/nodes", async (req, reply) => {
6463
+ if (!store.graph) {
6464
+ reply.code(404).send({ error: "No project scanned yet." });
6465
+ return;
6466
+ }
6467
+ let nodes = store.graph.nodes;
6468
+ if (req.query.type) nodes = nodes.filter((n) => n.type === req.query.type);
6469
+ if (req.query.language) nodes = nodes.filter((n) => n.language === req.query.language);
6470
+ if (req.query.module) nodes = nodes.filter((n) => n.module === req.query.module);
6471
+ if (req.query.search) {
6472
+ const q = req.query.search.toLowerCase();
6473
+ nodes = nodes.filter((n) => n.label.toLowerCase().includes(q) || n.id.toLowerCase().includes(q));
6474
+ }
6475
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
6476
+ return { total: nodes.length, nodes: nodes.slice(0, limit) };
6477
+ });
6478
+ app.get("/api/studio/node/:nodeId", async (req, reply) => {
6479
+ if (!store.graph) {
6480
+ reply.code(404).send({ error: "No project scanned yet." });
6481
+ return;
6482
+ }
6483
+ const nodeId = decodeURIComponent(req.params.nodeId);
6484
+ const node = store.graph.nodes.find((n) => n.id === nodeId);
6485
+ if (!node) {
6486
+ reply.code(404).send({ error: "Node not found." });
6487
+ return;
6488
+ }
6489
+ const incoming = store.graph.edges.filter((e) => e.target === nodeId);
6490
+ const outgoing = store.graph.edges.filter((e) => e.source === nodeId);
6491
+ const neighborIds = /* @__PURE__ */ new Set([...incoming.map((e) => e.source), ...outgoing.map((e) => e.target)]);
6492
+ const neighbors = store.graph.nodes.filter((n) => neighborIds.has(n.id));
6493
+ return { node, incoming, outgoing, neighbors };
6494
+ });
6495
+ app.get("/api/studio/summary", async (_req, reply) => {
6496
+ if (!store.graph) {
6497
+ reply.code(404).send({ error: "No project scanned yet." });
6498
+ return;
6499
+ }
6500
+ const { projectInfo } = store.graph;
6501
+ const stats = getGraphStats(store.graph);
6502
+ const critical = store.risks.filter((r) => r.severity === "critical").length;
6503
+ const high = store.risks.filter((r) => r.severity === "high").length;
6504
+ const healthScore = Math.max(0, 100 - (critical * 20 + high * 10));
6505
+ return {
6506
+ name: projectInfo.name,
6507
+ oneLiner: `A ${projectInfo.projectType} project using ${projectInfo.frameworks.join(", ") || "unknown"}, with ${stats.apiCount || 0} APIs and ${stats.modelCount || 0} data models.`,
6508
+ healthScore,
6509
+ stats,
6510
+ topRisks: store.risks.slice(0, 5).map((r) => ({ severity: r.severity, title: r.title })),
6511
+ source: store.source
6512
+ };
6513
+ });
6514
+ }
6515
+ var store;
6516
+ var init_studio = __esm({
6517
+ "src/server/routes/studio.ts"() {
6518
+ "use strict";
6519
+ init_esm_shims();
6520
+ init_github_cloner();
6521
+ init_graph();
6522
+ init_insight();
6523
+ store = {
6524
+ graph: null,
6525
+ risks: [],
6526
+ scanTime: 0,
6527
+ source: ""
6528
+ };
6529
+ }
6530
+ });
6531
+
4320
6532
  // src/execution/coordinator.ts
4321
6533
  var coordinator_exports = {};
4322
6534
  __export(coordinator_exports, {
@@ -4339,7 +6551,7 @@ function getFailureLines(output) {
4339
6551
  return output.split(/\r?\n/).filter((line) => /fail|error|timeout/i.test(line)).slice(0, 5);
4340
6552
  }
4341
6553
  function createExecutionCoordinator(deps = {}) {
4342
- const execSync = deps.execSync ?? nodeExecSync;
6554
+ const execSync2 = deps.execSync ?? nodeExecSync;
4343
6555
  const categorizeFailure2 = deps.categorizeFailure;
4344
6556
  return {
4345
6557
  async run(request) {
@@ -4348,7 +6560,7 @@ function createExecutionCoordinator(deps = {}) {
4348
6560
  const command = `npx playwright test ${request.testFiles.map((file) => `"${file}"`).join(" ")} --reporter=line 2>&1`;
4349
6561
  let output;
4350
6562
  try {
4351
- output = String(execSync(command, {
6563
+ output = String(execSync2(command, {
4352
6564
  cwd: request.cwd,
4353
6565
  encoding: "utf-8",
4354
6566
  timeout: timeoutMs,
@@ -4419,7 +6631,7 @@ var backend_manager_exports = {};
4419
6631
  __export(backend_manager_exports, {
4420
6632
  createBackendManager: () => createBackendManager
4421
6633
  });
4422
- import { resolve as resolve8 } from "path";
6634
+ import { resolve as resolve10 } from "path";
4423
6635
  import { spawn as nodeSpawn } from "child_process";
4424
6636
  function normalizeHealthUrl(server, baseURL) {
4425
6637
  if (server?.healthUrl) return server.healthUrl;
@@ -4481,7 +6693,7 @@ function createBackendManager(deps = {}) {
4481
6693
  throw new Error("BOOT_CONFIG_MISSING: runtime.server.command is required for managed mode");
4482
6694
  }
4483
6695
  const child = spawn(server.command, server.args ?? [], {
4484
- cwd: resolve8(request.cwd, server.cwd ?? "."),
6696
+ cwd: resolve10(request.cwd, server.cwd ?? "."),
4485
6697
  shell: true,
4486
6698
  stdio: "pipe",
4487
6699
  env: process.env
@@ -4516,13 +6728,13 @@ var runtime_bootstrap_exports = {};
4516
6728
  __export(runtime_bootstrap_exports, {
4517
6729
  createRuntimeBootstrap: () => createRuntimeBootstrap
4518
6730
  });
4519
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
4520
- import { dirname as dirname5, join as join13 } from "path";
6731
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
6732
+ import { dirname as dirname6, join as join16 } from "path";
4521
6733
  function ensureFile(filePath, content, force) {
4522
- if (existsSync15(filePath) && !force) {
6734
+ if (existsSync18(filePath) && !force) {
4523
6735
  return false;
4524
6736
  }
4525
- mkdirSync10(dirname5(filePath), { recursive: true });
6737
+ mkdirSync11(dirname6(filePath), { recursive: true });
4526
6738
  writeFileSync10(filePath, content, "utf-8");
4527
6739
  return true;
4528
6740
  }
@@ -4553,7 +6765,7 @@ function createRuntimeBootstrap(config) {
4553
6765
  const writtenFiles = [];
4554
6766
  const skippedFiles = [];
4555
6767
  for (const file of files) {
4556
- const filePath = join13(request.cwd, file.name);
6768
+ const filePath = join16(request.cwd, file.name);
4557
6769
  const written = ensureFile(filePath, file.content, force);
4558
6770
  if (written) writtenFiles.push(file.name);
4559
6771
  else skippedFiles.push(file.name);
@@ -4822,13 +7034,13 @@ var init_croc_office = __esm({
4822
7034
  const fullResult = await pipeline.run(["scan", "er-diagram", "api-chain", "plan", "codegen"]);
4823
7035
  this.lastPipelineResult = fullResult;
4824
7036
  this.lastGeneratedFiles = fullResult.generatedFiles;
4825
- const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
4826
- const { dirname: dirname7 } = await import("path");
7037
+ const { writeFileSync: writeFileSync13, mkdirSync: mkdirSync14 } = await import("fs");
7038
+ const { dirname: dirname8 } = await import("path");
4827
7039
  let filesWritten = 0;
4828
7040
  for (const file of fullResult.generatedFiles) {
4829
7041
  const fullPath = resolvePath(this.cwd, file.filePath);
4830
- mkdirSync11(dirname7(fullPath), { recursive: true });
4831
- writeFileSync11(fullPath, file.content, "utf-8");
7042
+ mkdirSync14(dirname8(fullPath), { recursive: true });
7043
+ writeFileSync13(fullPath, file.content, "utf-8");
4832
7044
  filesWritten++;
4833
7045
  }
4834
7046
  this.updateNodeStatus("controller", "passed");
@@ -4912,14 +7124,14 @@ var init_croc_office = __esm({
4912
7124
  let backendStatus;
4913
7125
  try {
4914
7126
  const { resolve: resolvePath } = await import("path");
4915
- const { existsSync: existsSync17 } = await import("fs");
7127
+ const { existsSync: existsSync21 } = await import("fs");
4916
7128
  const { createExecutionCoordinator: createExecutionCoordinator2 } = await Promise.resolve().then(() => (init_coordinator(), coordinator_exports));
4917
7129
  const { createBackendManager: createBackendManager2 } = await Promise.resolve().then(() => (init_backend_manager(), backend_manager_exports));
4918
7130
  const { createRuntimeBootstrap: createRuntimeBootstrap2 } = await Promise.resolve().then(() => (init_runtime_bootstrap(), runtime_bootstrap_exports));
4919
7131
  const { createAuthProvisioner: createAuthProvisioner2 } = await Promise.resolve().then(() => (init_auth_provisioner(), auth_provisioner_exports));
4920
7132
  const { buildExecutionQualityGate: buildExecutionQualityGate2 } = await Promise.resolve().then(() => (init_quality_gate(), quality_gate_exports));
4921
7133
  const { categorizeFailure: categorizeFailure2 } = await Promise.resolve().then(() => (init_self_healing(), self_healing_exports));
4922
- const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync17(f));
7134
+ const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync21(f));
4923
7135
  if (testFiles.length === 0) {
4924
7136
  this.log("\u26A0\uFE0F No test files found on disk", "warn");
4925
7137
  return { ok: false, task: "execute", duration: Date.now() - start, error: "No test files found on disk" };
@@ -5046,12 +7258,12 @@ var init_croc_office = __esm({
5046
7258
  });
5047
7259
  this.lastReports = reports;
5048
7260
  const { resolve: resolvePath } = await import("path");
5049
- const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
7261
+ const { writeFileSync: writeFileSync13, mkdirSync: mkdirSync14 } = await import("fs");
5050
7262
  const outDir = resolvePath(this.cwd, this.config.outDir || "./opencroc-output");
5051
- mkdirSync11(outDir, { recursive: true });
7263
+ mkdirSync14(outDir, { recursive: true });
5052
7264
  for (const report2 of reports) {
5053
7265
  const fullPath = resolvePath(outDir, report2.filename);
5054
- writeFileSync11(fullPath, report2.content, "utf-8");
7266
+ writeFileSync13(fullPath, report2.content, "utf-8");
5055
7267
  this.log(`\u{1F4C4} Generated ${report2.format} report: ${report2.filename}`);
5056
7268
  }
5057
7269
  this.updateAgent("reporter-croc", { status: "done", currentTask: `${reports.length} reports generated`, progress: 100 });
@@ -5175,7 +7387,7 @@ var init_croc_office = __esm({
5175
7387
  page: "app",
5176
7388
  application: "app"
5177
7389
  };
5178
- const inferModule = (filePath, type) => {
7390
+ const inferModule2 = (filePath, type) => {
5179
7391
  const parts = filePath.replace(/\\/g, "/").split("/");
5180
7392
  const typeDir = type === "model" ? "models" : "controllers";
5181
7393
  const typeDirIdx = parts.indexOf(typeDir);
@@ -5203,7 +7415,7 @@ var init_croc_office = __esm({
5203
7415
  });
5204
7416
  for (const file of modelFiles) {
5205
7417
  const parts = file.replace(/\\/g, "/").split("/");
5206
- const moduleName = inferModule(file, "model");
7418
+ const moduleName = inferModule2(file, "model");
5207
7419
  const fileName = parts[parts.length - 1].replace(/\.(ts|js)$/, "");
5208
7420
  const nodeId = `model:${fileName}`;
5209
7421
  moduleSet.add(moduleName);
@@ -5222,7 +7434,7 @@ var init_croc_office = __esm({
5222
7434
  });
5223
7435
  for (const file of controllerFiles) {
5224
7436
  const parts = file.replace(/\\/g, "/").split("/");
5225
- const moduleName = inferModule(file, "controller");
7437
+ const moduleName = inferModule2(file, "controller");
5226
7438
  const fileName = parts[parts.length - 1].replace(/\.(ts|js)$/, "").replace(/Controller$/, "");
5227
7439
  const nodeId = `controller:${fileName}`;
5228
7440
  moduleSet.add(moduleName);
@@ -5296,13 +7508,13 @@ import Fastify from "fastify";
5296
7508
  import fastifyStatic from "@fastify/static";
5297
7509
  import fastifyWebsocket from "@fastify/websocket";
5298
7510
  import { fileURLToPath as fileURLToPath2 } from "url";
5299
- import { dirname as dirname6, join as join14, resolve as resolve9 } from "path";
5300
- import { existsSync as existsSync16 } from "fs";
7511
+ import { dirname as dirname7, join as join17, resolve as resolve11 } from "path";
7512
+ import { existsSync as existsSync19 } from "fs";
5301
7513
  async function startServer(opts) {
5302
7514
  const app = Fastify({ logger: false });
5303
7515
  await app.register(fastifyWebsocket);
5304
- const webDir = resolve9(__dirname2, "../web");
5305
- if (existsSync16(webDir)) {
7516
+ const webDir = resolve11(__dirname2, "../web");
7517
+ if (existsSync19(webDir)) {
5306
7518
  await app.register(fastifyStatic, {
5307
7519
  root: webDir,
5308
7520
  prefix: "/",
@@ -5312,6 +7524,7 @@ async function startServer(opts) {
5312
7524
  const office = new CrocOffice(opts.config, opts.cwd);
5313
7525
  registerProjectRoutes(app, office);
5314
7526
  registerAgentRoutes(app, office);
7527
+ registerStudioRoutes(app, office);
5315
7528
  app.register(async (fastify) => {
5316
7529
  fastify.get("/ws", { websocket: true }, (socket) => {
5317
7530
  office.addClient(socket);
@@ -5323,8 +7536,11 @@ async function startServer(opts) {
5323
7536
  reply.code(404).send({ error: "Not found" });
5324
7537
  return;
5325
7538
  }
5326
- const indexPath = join14(webDir, "index.html");
5327
- if (existsSync16(indexPath)) {
7539
+ const studioPath = join17(webDir, "index-studio.html");
7540
+ const indexPath = join17(webDir, "index.html");
7541
+ if (existsSync19(studioPath)) {
7542
+ reply.sendFile("index-studio.html");
7543
+ } else if (existsSync19(indexPath)) {
5328
7544
  reply.sendFile("index.html");
5329
7545
  } else {
5330
7546
  reply.code(200).header("content-type", "text/html").send(getEmbeddedHtml());
@@ -5473,9 +7689,10 @@ var init_server = __esm({
5473
7689
  init_esm_shims();
5474
7690
  init_project();
5475
7691
  init_agents();
7692
+ init_studio();
5476
7693
  init_croc_office();
5477
7694
  __filename2 = fileURLToPath2(import.meta.url);
5478
- __dirname2 = dirname6(__filename2);
7695
+ __dirname2 = dirname7(__filename2);
5479
7696
  }
5480
7697
  });
5481
7698
 
@@ -5520,6 +7737,248 @@ var init_serve = __esm({
5520
7737
  }
5521
7738
  });
5522
7739
 
7740
+ // src/cli/commands/scan.ts
7741
+ var scan_exports = {};
7742
+ __export(scan_exports, {
7743
+ scan: () => scan
7744
+ });
7745
+ import * as fs11 from "fs";
7746
+ import * as path12 from "path";
7747
+ import chalk12 from "chalk";
7748
+ async function scan(target, opts) {
7749
+ console.log("");
7750
+ console.log(chalk12.green("\u{1F40A} OpenCroc Studio \u2014 Universal Project Scanner"));
7751
+ console.log(chalk12.gray(` Target: ${target}`));
7752
+ console.log("");
7753
+ const startTime = Date.now();
7754
+ console.log(chalk12.cyan("\u{1F4E1} Phase 1: Scanning project..."));
7755
+ const scanResult = await cloneAndScan({
7756
+ target,
7757
+ branch: opts.branch,
7758
+ keepClone: true,
7759
+ onProgress: (phase, percent, detail) => {
7760
+ if (percent % 25 === 0 || phase === "clone") {
7761
+ console.log(chalk12.gray(` [${phase}] ${percent}% ${detail || ""}`));
7762
+ }
7763
+ }
7764
+ });
7765
+ console.log(chalk12.green(` \u2705 Found ${scanResult.entities.length} entities, ${scanResult.relationships.length} relationships`));
7766
+ console.log(chalk12.gray(` Languages: ${Object.entries(scanResult.languages).filter(([k]) => !["json", "yaml", "markdown"].includes(k)).map(([k, v]) => `${k}(${v})`).join(", ")}`));
7767
+ console.log(chalk12.gray(` Frameworks: ${scanResult.frameworks.map((f) => f.name).join(", ") || "none detected"}`));
7768
+ console.log("");
7769
+ console.log(chalk12.cyan("\u{1F9E0} Phase 2: Building knowledge graph..."));
7770
+ const projectName = target.includes("/") ? target.split("/").pop().replace(".git", "") : path12.basename(path12.resolve(target));
7771
+ const isRemote = target.startsWith("http") || /^[\w.-]+\/[\w.-]+$/.test(target);
7772
+ const graph = buildKnowledgeGraph(scanResult, {
7773
+ projectName,
7774
+ source: isRemote ? "github" : "local",
7775
+ sourceUrl: target,
7776
+ rootPath: target
7777
+ });
7778
+ const stats = getGraphStats(graph);
7779
+ console.log(chalk12.green(` \u2705 ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
7780
+ printStats(stats);
7781
+ console.log("");
7782
+ let risks = [];
7783
+ if (opts.risks) {
7784
+ console.log(chalk12.cyan("\u26A0\uFE0F Phase 3: Analyzing risks..."));
7785
+ risks = await analyzeRisks(graph);
7786
+ printRisks(risks);
7787
+ console.log("");
7788
+ }
7789
+ if (opts.report) {
7790
+ console.log(chalk12.cyan(`\u{1F4CB} Phase 4: Generating ${opts.report} report...`));
7791
+ const report2 = await generateReport(graph, opts.report, risks);
7792
+ printReport(report2);
7793
+ console.log("");
7794
+ }
7795
+ fs11.mkdirSync(opts.output, { recursive: true });
7796
+ const graphPath = path12.join(opts.output, "knowledge-graph.json");
7797
+ fs11.writeFileSync(graphPath, JSON.stringify({
7798
+ projectInfo: graph.projectInfo,
7799
+ stats: getGraphStats(graph),
7800
+ nodes: graph.nodes.map((n) => ({ id: n.id, label: n.label, type: n.type, module: n.module, filePath: n.filePath })),
7801
+ edges: graph.edges.map((e) => ({ source: e.source, target: e.target, relation: e.relation })),
7802
+ risks: risks.length > 0 ? risks : void 0,
7803
+ builtAt: graph.builtAt
7804
+ }, null, 2));
7805
+ console.log(chalk12.gray(` \u{1F4C1} Graph saved: ${graphPath}`));
7806
+ if (opts.json) {
7807
+ const jsonPath = path12.join(opts.output, "scan-result.json");
7808
+ fs11.writeFileSync(jsonPath, JSON.stringify({
7809
+ project: graph.projectInfo,
7810
+ stats,
7811
+ risks: risks.length > 0 ? risks : void 0
7812
+ }, null, 2));
7813
+ console.log(chalk12.gray(` \u{1F4C1} JSON saved: ${jsonPath}`));
7814
+ }
7815
+ if (opts.mermaid) {
7816
+ const mermaidPath = path12.join(opts.output, "graph.mmd");
7817
+ const mermaidText = toMermaid(graph, { nodeTypes: ["module", "model", "api", "service"], maxNodes: 40 });
7818
+ fs11.writeFileSync(mermaidPath, mermaidText);
7819
+ console.log(chalk12.gray(` \u{1F4C1} Mermaid saved: ${mermaidPath}`));
7820
+ }
7821
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
7822
+ console.log("");
7823
+ console.log(chalk12.green(`\u{1F40A} Done in ${duration}s`));
7824
+ console.log(chalk12.gray(` Run ${chalk12.white("opencroc serve")} to explore the knowledge graph in Studio UI`));
7825
+ console.log("");
7826
+ }
7827
+ function printStats(stats) {
7828
+ const items = [];
7829
+ if (stats.moduleCount) items.push(`${stats.moduleCount} modules`);
7830
+ if (stats.apiCount) items.push(`${stats.apiCount} APIs`);
7831
+ if (stats.modelCount) items.push(`${stats.modelCount} models`);
7832
+ if (stats.classCount) items.push(`${stats.classCount} classes`);
7833
+ if (stats.functionCount) items.push(`${stats.functionCount} functions`);
7834
+ if (stats.dependencyCount) items.push(`${stats.dependencyCount} dependencies`);
7835
+ if (items.length > 0) {
7836
+ console.log(chalk12.gray(` ${items.join(" | ")}`));
7837
+ }
7838
+ }
7839
+ function printRisks(risks) {
7840
+ if (risks.length === 0) {
7841
+ console.log(chalk12.green(" \u2705 No significant risks detected."));
7842
+ return;
7843
+ }
7844
+ const critical = risks.filter((r) => r.severity === "critical").length;
7845
+ const high = risks.filter((r) => r.severity === "high").length;
7846
+ const medium = risks.filter((r) => r.severity === "medium").length;
7847
+ const low = risks.filter((r) => r.severity === "low").length;
7848
+ console.log(chalk12.yellow(` Found ${risks.length} risks:`));
7849
+ if (critical) console.log(chalk12.red(` \u{1F534} Critical: ${critical}`));
7850
+ if (high) console.log(chalk12.hex("#e67e22")(` \u{1F7E0} High: ${high}`));
7851
+ if (medium) console.log(chalk12.yellow(` \u{1F7E1} Medium: ${medium}`));
7852
+ if (low) console.log(chalk12.blue(` \u{1F535} Low: ${low}`));
7853
+ console.log("");
7854
+ console.log(chalk12.white(" Top risks:"));
7855
+ for (const r of risks.slice(0, 5)) {
7856
+ const color = r.severity === "critical" ? chalk12.red : r.severity === "high" ? chalk12.hex("#e67e22") : r.severity === "medium" ? chalk12.yellow : chalk12.blue;
7857
+ console.log(` ${color(`[${r.severity.toUpperCase()}]`)} ${r.title}`);
7858
+ }
7859
+ }
7860
+ function printReport(report2) {
7861
+ console.log(chalk12.green(` \u2705 ${report2.title}`));
7862
+ console.log(chalk12.gray(` ${report2.summary}`));
7863
+ for (const section of report2.sections) {
7864
+ console.log(chalk12.cyan(`
7865
+ \u2500\u2500 ${section.heading} \u2500\u2500`));
7866
+ const lines = section.content.split("\n").slice(0, 3);
7867
+ for (const line of lines) {
7868
+ console.log(chalk12.gray(` ${line}`));
7869
+ }
7870
+ if (section.content.split("\n").length > 3) {
7871
+ console.log(chalk12.gray(" ..."));
7872
+ }
7873
+ }
7874
+ }
7875
+ var init_scan = __esm({
7876
+ "src/cli/commands/scan.ts"() {
7877
+ "use strict";
7878
+ init_esm_shims();
7879
+ init_github_cloner();
7880
+ init_graph();
7881
+ init_insight();
7882
+ }
7883
+ });
7884
+
7885
+ // src/cli/commands/analyze.ts
7886
+ var analyze_exports = {};
7887
+ __export(analyze_exports, {
7888
+ analyze: () => analyze
7889
+ });
7890
+ import * as fs12 from "fs";
7891
+ import * as path13 from "path";
7892
+ import chalk13 from "chalk";
7893
+ async function analyze(target, opts) {
7894
+ console.log("");
7895
+ console.log(chalk13.green("\u{1F40A} OpenCroc Studio \u2014 Project Analysis"));
7896
+ console.log("");
7897
+ const absTarget = path13.resolve(target);
7898
+ const graphPath = path13.join(opts.output, "knowledge-graph.json");
7899
+ let graph;
7900
+ if (fs12.existsSync(graphPath)) {
7901
+ console.log(chalk13.gray(` Loading cached graph from ${graphPath}...`));
7902
+ }
7903
+ console.log(chalk13.cyan("\u{1F4E1} Scanning project..."));
7904
+ const scanResult = await scanProject({
7905
+ rootDir: absTarget,
7906
+ onProgress: (phase, percent, detail) => {
7907
+ if (percent === 100) console.log(chalk13.gray(` [${phase}] ${detail || "done"}`));
7908
+ }
7909
+ });
7910
+ const projectName = path13.basename(absTarget);
7911
+ graph = buildKnowledgeGraph(scanResult, {
7912
+ projectName,
7913
+ source: "local",
7914
+ rootPath: absTarget
7915
+ });
7916
+ console.log(chalk13.green(` \u2705 ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
7917
+ console.log("");
7918
+ if (opts.risks) {
7919
+ console.log(chalk13.cyan("\u26A0\uFE0F Risk Analysis"));
7920
+ const risks2 = await analyzeRisks(graph);
7921
+ if (risks2.length === 0) {
7922
+ console.log(chalk13.green(" \u2705 No significant risks detected."));
7923
+ } else {
7924
+ for (const r of risks2) {
7925
+ const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "high" ? "\u{1F7E0}" : r.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
7926
+ console.log(` ${icon} [${r.severity.toUpperCase()}] ${r.title}`);
7927
+ console.log(chalk13.gray(` ${r.description}`));
7928
+ if (r.suggestion) {
7929
+ console.log(chalk13.green(` \u{1F4A1} ${r.suggestion}`));
7930
+ }
7931
+ console.log("");
7932
+ }
7933
+ }
7934
+ }
7935
+ if (opts.impact) {
7936
+ console.log(chalk13.cyan(`\u{1F3AF} Impact Analysis: ${opts.impact}`));
7937
+ const impact = analyzeImpact(graph, opts.impact);
7938
+ console.log(` ${impact.summary}`);
7939
+ console.log(` Direct impact: ${impact.directImpact.length} entities`);
7940
+ console.log(` Transitive impact: ${impact.transitiveImpact.length} entities`);
7941
+ console.log(` Risk level: ${impact.riskLevel}`);
7942
+ console.log("");
7943
+ if (impact.mermaidText) {
7944
+ const mermaidPath = path13.join(opts.output, "impact.mmd");
7945
+ fs12.mkdirSync(opts.output, { recursive: true });
7946
+ fs12.writeFileSync(mermaidPath, impact.mermaidText);
7947
+ console.log(chalk13.gray(` \u{1F4C1} Impact diagram saved: ${mermaidPath}`));
7948
+ }
7949
+ }
7950
+ const perspective = opts.perspective;
7951
+ console.log(chalk13.cyan(`\u{1F4CB} ${perspective.charAt(0).toUpperCase() + perspective.slice(1)} Report`));
7952
+ console.log("");
7953
+ const risks = await analyzeRisks(graph);
7954
+ const report2 = await generateReport(graph, perspective, risks);
7955
+ console.log(chalk13.bold.green(` ${report2.title}`));
7956
+ console.log(chalk13.gray(` ${report2.summary}`));
7957
+ console.log("");
7958
+ for (const section of report2.sections) {
7959
+ console.log(chalk13.cyan(` \u2501\u2501 ${section.heading} \u2501\u2501`));
7960
+ const lines = section.content.split("\n");
7961
+ for (const line of lines) {
7962
+ console.log(` ${line}`);
7963
+ }
7964
+ console.log("");
7965
+ }
7966
+ fs12.mkdirSync(opts.output, { recursive: true });
7967
+ const reportPath = path13.join(opts.output, `report-${perspective}.json`);
7968
+ fs12.writeFileSync(reportPath, JSON.stringify(report2, null, 2));
7969
+ console.log(chalk13.gray(` \u{1F4C1} Report saved: ${reportPath}`));
7970
+ console.log("");
7971
+ }
7972
+ var init_analyze = __esm({
7973
+ "src/cli/commands/analyze.ts"() {
7974
+ "use strict";
7975
+ init_esm_shims();
7976
+ init_project_scanner();
7977
+ init_graph();
7978
+ init_insight();
7979
+ }
7980
+ });
7981
+
5523
7982
  // src/cli/index.ts
5524
7983
  init_esm_shims();
5525
7984
  import { Command } from "commander";
@@ -5569,5 +8028,13 @@ program.command("serve").description("Start OpenCroc Studio \u2014 pixel croc of
5569
8028
  const { serve: serve2 } = await Promise.resolve().then(() => (init_serve(), serve_exports));
5570
8029
  await serve2(opts);
5571
8030
  });
8031
+ program.command("scan").description("Scan any project and build knowledge graph (local path, GitHub URL, or user/repo)").argument("<target>", "Local path, GitHub URL (https://github.com/user/repo), or shorthand (user/repo)").option("-b, --branch <branch>", "Git branch to clone").option("-o, --output <dir>", "Output directory for results", "./opencroc-output").option("--json", "Output as JSON").option("--mermaid", "Output Mermaid diagram").option("--risks", "Include risk analysis").option("--report <perspective>", "Generate perspective report (developer/architect/tester/product/student/executive)").action(async (target, opts) => {
8032
+ const { scan: scan2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
8033
+ await scan2(target, opts);
8034
+ });
8035
+ program.command("analyze").description("Analyze a scanned project for risks and generate reports").argument("[target]", "Project path (default: current directory)", ".").option("--perspective <role>", "Report perspective (developer/architect/tester/product/student/executive)", "developer").option("--risks", "Show risk analysis", true).option("--impact <nodeId>", "Analyze impact of a specific node").option("-o, --output <dir>", "Output directory", "./opencroc-output").action(async (target, opts) => {
8036
+ const { analyze: analyze2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
8037
+ await analyze2(target, opts);
8038
+ });
5572
8039
  program.parse();
5573
8040
  //# sourceMappingURL=index.js.map