opencroc 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,2527 @@ 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/studio-store.ts
6328
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync6, writeFileSync as writeFileSync10 } from "fs";
6329
+ import { dirname as dirname6 } from "path";
6330
+ function isSerializedSnapshotFile(value) {
6331
+ return "snapshots" in value && Array.isArray(value.snapshots);
6332
+ }
6333
+ var EMPTY_STUDIO_STORE, FileStudioSnapshotStore;
6334
+ var init_studio_store = __esm({
6335
+ "src/server/studio-store.ts"() {
6336
+ "use strict";
6337
+ init_esm_shims();
6338
+ EMPTY_STUDIO_STORE = {
6339
+ graph: null,
6340
+ risks: [],
6341
+ scanTime: 0,
6342
+ source: ""
6343
+ };
6344
+ FileStudioSnapshotStore = class {
6345
+ filePath;
6346
+ maxSnapshots;
6347
+ constructor(filePath, maxSnapshots = 12) {
6348
+ this.filePath = filePath;
6349
+ this.maxSnapshots = maxSnapshots;
6350
+ }
6351
+ load() {
6352
+ const data = this.readFile();
6353
+ if (!data || !data.currentSnapshotId) return null;
6354
+ const current = data.snapshots.find((snapshot) => snapshot.id === data.currentSnapshotId);
6355
+ return current ? this.toProjectStore(current) : null;
6356
+ }
6357
+ save(snapshot) {
6358
+ const data = this.readFile() ?? { version: 2, currentSnapshotId: null, snapshots: [] };
6359
+ const record = this.toSnapshotRecord(snapshot);
6360
+ data.currentSnapshotId = record.id;
6361
+ data.snapshots = [record, ...data.snapshots].slice(0, this.maxSnapshots);
6362
+ mkdirSync11(dirname6(this.filePath), { recursive: true });
6363
+ writeFileSync10(this.filePath, JSON.stringify(data, null, 2), "utf-8");
6364
+ }
6365
+ list() {
6366
+ const data = this.readFile();
6367
+ if (!data) return [];
6368
+ return data.snapshots.map((snapshot) => ({
6369
+ id: snapshot.id,
6370
+ name: snapshot.name,
6371
+ source: snapshot.source,
6372
+ scanTime: snapshot.scanTime,
6373
+ nodeCount: snapshot.graph?.nodes.length ?? 0,
6374
+ riskCount: snapshot.risks.length,
6375
+ current: snapshot.id === data.currentSnapshotId,
6376
+ pinned: Boolean(snapshot.pinned),
6377
+ tags: Array.isArray(snapshot.tags) ? snapshot.tags : []
6378
+ })).sort((left, right) => {
6379
+ if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
6380
+ return right.scanTime - left.scanTime;
6381
+ });
6382
+ }
6383
+ loadById(id) {
6384
+ const data = this.readFile();
6385
+ if (!data) return null;
6386
+ const record = data.snapshots.find((snapshot) => snapshot.id === id);
6387
+ if (!record) return null;
6388
+ data.currentSnapshotId = record.id;
6389
+ mkdirSync11(dirname6(this.filePath), { recursive: true });
6390
+ writeFileSync10(this.filePath, JSON.stringify(data, null, 2), "utf-8");
6391
+ return this.toProjectStore(record);
6392
+ }
6393
+ rename(id, name) {
6394
+ const nextName = name.trim();
6395
+ if (!nextName) return false;
6396
+ const data = this.readFile();
6397
+ if (!data) return false;
6398
+ const record = data.snapshots.find((snapshot) => snapshot.id === id);
6399
+ if (!record) return false;
6400
+ record.name = nextName;
6401
+ mkdirSync11(dirname6(this.filePath), { recursive: true });
6402
+ writeFileSync10(this.filePath, JSON.stringify(data, null, 2), "utf-8");
6403
+ return true;
6404
+ }
6405
+ delete(id) {
6406
+ const data = this.readFile();
6407
+ if (!data) return false;
6408
+ const nextSnapshots = data.snapshots.filter((snapshot) => snapshot.id !== id);
6409
+ if (nextSnapshots.length === data.snapshots.length) return false;
6410
+ data.snapshots = nextSnapshots;
6411
+ if (data.currentSnapshotId === id) {
6412
+ data.currentSnapshotId = nextSnapshots[0]?.id ?? null;
6413
+ }
6414
+ mkdirSync11(dirname6(this.filePath), { recursive: true });
6415
+ writeFileSync10(this.filePath, JSON.stringify(data, null, 2), "utf-8");
6416
+ return true;
6417
+ }
6418
+ pin(id, pinned) {
6419
+ const data = this.readFile();
6420
+ if (!data) return false;
6421
+ const record = data.snapshots.find((snapshot) => snapshot.id === id);
6422
+ if (!record) return false;
6423
+ record.pinned = pinned;
6424
+ mkdirSync11(dirname6(this.filePath), { recursive: true });
6425
+ writeFileSync10(this.filePath, JSON.stringify(data, null, 2), "utf-8");
6426
+ return true;
6427
+ }
6428
+ updateTags(id, tags) {
6429
+ const data = this.readFile();
6430
+ if (!data) return false;
6431
+ const record = data.snapshots.find((snapshot) => snapshot.id === id);
6432
+ if (!record) return false;
6433
+ record.tags = this.normalizeTags(tags);
6434
+ mkdirSync11(dirname6(this.filePath), { recursive: true });
6435
+ writeFileSync10(this.filePath, JSON.stringify(data, null, 2), "utf-8");
6436
+ return true;
6437
+ }
6438
+ readFile() {
6439
+ if (!existsSync18(this.filePath)) return null;
6440
+ try {
6441
+ const raw = readFileSync6(this.filePath, "utf-8");
6442
+ const parsed = JSON.parse(raw);
6443
+ return this.normalize(parsed);
6444
+ } catch {
6445
+ return null;
6446
+ }
6447
+ }
6448
+ normalize(parsed) {
6449
+ if (isSerializedSnapshotFile(parsed)) {
6450
+ return {
6451
+ version: 2,
6452
+ currentSnapshotId: typeof parsed.currentSnapshotId === "string" ? parsed.currentSnapshotId : null,
6453
+ snapshots: parsed.snapshots.map((snapshot) => ({
6454
+ id: snapshot.id,
6455
+ name: snapshot.name,
6456
+ pinned: Boolean(snapshot.pinned),
6457
+ tags: this.normalizeTags(snapshot.tags),
6458
+ graph: snapshot.graph ?? null,
6459
+ risks: Array.isArray(snapshot.risks) ? snapshot.risks : [],
6460
+ scanTime: typeof snapshot.scanTime === "number" ? snapshot.scanTime : 0,
6461
+ source: typeof snapshot.source === "string" ? snapshot.source : ""
6462
+ }))
6463
+ };
6464
+ }
6465
+ const legacyStore = parsed;
6466
+ const legacy = this.toSnapshotRecord({
6467
+ graph: legacyStore.graph ?? null,
6468
+ risks: Array.isArray(legacyStore.risks) ? legacyStore.risks : [],
6469
+ scanTime: typeof legacyStore.scanTime === "number" ? legacyStore.scanTime : 0,
6470
+ source: typeof legacyStore.source === "string" ? legacyStore.source : ""
6471
+ });
6472
+ return {
6473
+ version: 2,
6474
+ currentSnapshotId: legacy.id,
6475
+ snapshots: [legacy]
6476
+ };
6477
+ }
6478
+ toSnapshotRecord(snapshot) {
6479
+ const name = snapshot.graph?.projectInfo?.name || this.deriveName(snapshot.source);
6480
+ return {
6481
+ id: `${snapshot.scanTime || Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
6482
+ name,
6483
+ pinned: false,
6484
+ tags: [],
6485
+ graph: snapshot.graph ?? null,
6486
+ risks: Array.isArray(snapshot.risks) ? snapshot.risks : [],
6487
+ scanTime: typeof snapshot.scanTime === "number" ? snapshot.scanTime : Date.now(),
6488
+ source: typeof snapshot.source === "string" ? snapshot.source : ""
6489
+ };
6490
+ }
6491
+ toProjectStore(snapshot) {
6492
+ return {
6493
+ graph: snapshot.graph ?? null,
6494
+ risks: Array.isArray(snapshot.risks) ? snapshot.risks : [],
6495
+ scanTime: typeof snapshot.scanTime === "number" ? snapshot.scanTime : 0,
6496
+ source: typeof snapshot.source === "string" ? snapshot.source : ""
6497
+ };
6498
+ }
6499
+ deriveName(source) {
6500
+ if (!source) return "unknown-project";
6501
+ const parts = source.split(/[\\/]/).filter(Boolean);
6502
+ return parts[parts.length - 1] || source;
6503
+ }
6504
+ normalizeTags(tags) {
6505
+ if (!Array.isArray(tags)) return [];
6506
+ return [...new Set(
6507
+ tags.map((tag) => typeof tag === "string" ? tag.trim() : "").filter(Boolean)
6508
+ )];
6509
+ }
6510
+ };
6511
+ }
6512
+ });
6513
+
6514
+ // src/server/routes/studio.ts
6515
+ function restoreStore(snapshotStore) {
6516
+ return snapshotStore?.load() ?? { ...EMPTY_STUDIO_STORE };
6517
+ }
6518
+ function registerStudioRoutes(app, office, snapshotStore) {
6519
+ const store = restoreStore(snapshotStore);
6520
+ if (store.graph) {
6521
+ office.log(`\u267B\uFE0F Restored Studio snapshot: ${store.graph.nodes.length} nodes, ${store.risks.length} risks`, "info");
6522
+ }
6523
+ const persistStore = () => {
6524
+ snapshotStore?.save(store);
6525
+ };
6526
+ const broadcastGraph = () => {
6527
+ if (!store.graph) return;
6528
+ office.broadcast("graph:update", {
6529
+ nodes: store.graph.nodes.map((n) => ({
6530
+ id: n.id,
6531
+ label: n.label,
6532
+ type: n.type,
6533
+ module: n.module,
6534
+ status: n.status
6535
+ })),
6536
+ edges: store.graph.edges.map((e) => ({
6537
+ source: e.source,
6538
+ target: e.target,
6539
+ relation: e.relation
6540
+ }))
6541
+ });
6542
+ };
6543
+ app.post("/api/studio/scan", async (req, reply) => {
6544
+ const { target, branch, useLlm } = req.body || {};
6545
+ if (!target || typeof target !== "string") {
6546
+ reply.code(400).send({ error: 'Missing "target" field. Provide a local path or GitHub URL.' });
6547
+ return;
6548
+ }
6549
+ office.log(`\u{1F50D} Starting scan: ${target}`, "info");
6550
+ office.updateAgent("parser-croc", { status: "working", currentTask: `Scanning ${target}...`, progress: 0 });
6551
+ try {
6552
+ const scanResult = await cloneAndScan({
6553
+ target,
6554
+ branch,
6555
+ useLlm,
6556
+ keepClone: true,
6557
+ onProgress: (phase, percent, detail) => {
6558
+ office.updateAgent("parser-croc", { currentTask: detail || phase, progress: percent });
6559
+ office.broadcast("scan:progress", { phase, percent, detail });
6560
+ }
6561
+ });
6562
+ office.updateAgent("parser-croc", { status: "done", currentTask: "Scan complete", progress: 100 });
6563
+ office.log(`\u2705 Scan complete: ${scanResult.entities.length} entities, ${scanResult.relationships.length} relationships`);
6564
+ office.updateAgent("analyzer-croc", { status: "working", currentTask: "Building knowledge graph...", progress: 0 });
6565
+ const projectName = target.includes("/") ? target.split("/").pop().replace(".git", "") : target.split(/[\\/]/).pop();
6566
+ const graph = buildKnowledgeGraph(scanResult, {
6567
+ projectName,
6568
+ source: target.startsWith("http") || /^[\w.-]+\/[\w.-]+$/.test(target) ? "github" : "local",
6569
+ sourceUrl: target,
6570
+ rootPath: target
6571
+ });
6572
+ office.updateAgent("analyzer-croc", { status: "done", currentTask: "Graph built", progress: 100 });
6573
+ office.log(`\u{1F4CA} Knowledge graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
6574
+ office.updateAgent("planner-croc", { status: "working", currentTask: "Analyzing risks...", progress: 0 });
6575
+ const risks = await analyzeRisks(graph);
6576
+ office.updateAgent("planner-croc", { status: "done", currentTask: `${risks.length} risks found`, progress: 100 });
6577
+ store.graph = graph;
6578
+ store.risks = risks;
6579
+ store.scanTime = Date.now();
6580
+ store.source = target;
6581
+ persistStore();
6582
+ broadcastGraph();
6583
+ return {
6584
+ ok: true,
6585
+ project: graph.projectInfo,
6586
+ stats: getGraphStats(graph),
6587
+ risks: risks.length,
6588
+ duration: graph.buildDuration + scanResult.duration
6589
+ };
6590
+ } catch (err) {
6591
+ office.updateAgent("parser-croc", { status: "error", currentTask: String(err) });
6592
+ office.log(`\u274C Scan failed: ${err}`, "error");
6593
+ reply.code(500).send({ error: `Scan failed: ${err.message}` });
6594
+ return;
6595
+ }
6596
+ });
6597
+ app.get("/api/studio/graph", async (_req, reply) => {
6598
+ if (!store.graph) {
6599
+ reply.code(404).send({ error: "No project scanned yet. POST /api/studio/scan first." });
6600
+ return;
6601
+ }
6602
+ return {
6603
+ nodes: store.graph.nodes,
6604
+ edges: store.graph.edges,
6605
+ projectInfo: store.graph.projectInfo,
6606
+ builtAt: store.graph.builtAt,
6607
+ stats: getGraphStats(store.graph)
6608
+ };
6609
+ });
6610
+ app.get("/api/studio/graph/mermaid", async (req, reply) => {
6611
+ if (!store.graph) {
6612
+ reply.code(404).send({ error: "No project scanned yet." });
6613
+ return;
6614
+ }
6615
+ const nodeTypes = req.query.types?.split(",");
6616
+ const maxNodes = req.query.maxNodes ? parseInt(req.query.maxNodes, 10) : 50;
6617
+ return {
6618
+ mermaid: toMermaid(store.graph, { nodeTypes, maxNodes })
6619
+ };
6620
+ });
6621
+ app.get("/api/studio/risks", async (req, reply) => {
6622
+ if (!store.graph) {
6623
+ reply.code(404).send({ error: "No project scanned yet." });
6624
+ return;
6625
+ }
6626
+ let risks = store.risks;
6627
+ if (req.query.severity) {
6628
+ risks = risks.filter((r) => r.severity === req.query.severity);
6629
+ }
6630
+ if (req.query.category) {
6631
+ risks = risks.filter((r) => r.category === req.query.category);
6632
+ }
6633
+ return { total: risks.length, risks };
6634
+ });
6635
+ app.get("/api/studio/impact/:nodeId", async (req, reply) => {
6636
+ if (!store.graph) {
6637
+ reply.code(404).send({ error: "No project scanned yet." });
6638
+ return;
6639
+ }
6640
+ const nodeId = decodeURIComponent(req.params.nodeId);
6641
+ const impact = analyzeImpact(store.graph, nodeId);
6642
+ return impact;
6643
+ });
6644
+ app.get("/api/studio/report/:perspective", async (req, reply) => {
6645
+ if (!store.graph) {
6646
+ reply.code(404).send({ error: "No project scanned yet." });
6647
+ return;
6648
+ }
6649
+ const validPerspectives = ["developer", "architect", "tester", "product", "student", "executive"];
6650
+ const perspective = req.params.perspective;
6651
+ if (!validPerspectives.includes(perspective)) {
6652
+ reply.code(400).send({ error: `Invalid perspective. Valid: ${validPerspectives.join(", ")}` });
6653
+ return;
6654
+ }
6655
+ office.updateAgent("reporter-croc", { status: "working", currentTask: `Generating ${perspective} report...` });
6656
+ const report2 = await generateReport(
6657
+ store.graph,
6658
+ perspective,
6659
+ store.risks
6660
+ );
6661
+ office.updateAgent("reporter-croc", { status: "done", currentTask: "Report ready" });
6662
+ return report2;
6663
+ });
6664
+ app.get("/api/studio/nodes", async (req, reply) => {
6665
+ if (!store.graph) {
6666
+ reply.code(404).send({ error: "No project scanned yet." });
6667
+ return;
6668
+ }
6669
+ let nodes = store.graph.nodes;
6670
+ if (req.query.type) nodes = nodes.filter((n) => n.type === req.query.type);
6671
+ if (req.query.language) nodes = nodes.filter((n) => n.language === req.query.language);
6672
+ if (req.query.module) nodes = nodes.filter((n) => n.module === req.query.module);
6673
+ if (req.query.search) {
6674
+ const q = req.query.search.toLowerCase();
6675
+ nodes = nodes.filter((n) => n.label.toLowerCase().includes(q) || n.id.toLowerCase().includes(q));
6676
+ }
6677
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
6678
+ return { total: nodes.length, nodes: nodes.slice(0, limit) };
6679
+ });
6680
+ app.get("/api/studio/node/:nodeId", async (req, reply) => {
6681
+ if (!store.graph) {
6682
+ reply.code(404).send({ error: "No project scanned yet." });
6683
+ return;
6684
+ }
6685
+ const nodeId = decodeURIComponent(req.params.nodeId);
6686
+ const node = store.graph.nodes.find((n) => n.id === nodeId);
6687
+ if (!node) {
6688
+ reply.code(404).send({ error: "Node not found." });
6689
+ return;
6690
+ }
6691
+ const incoming = store.graph.edges.filter((e) => e.target === nodeId);
6692
+ const outgoing = store.graph.edges.filter((e) => e.source === nodeId);
6693
+ const neighborIds = /* @__PURE__ */ new Set([...incoming.map((e) => e.source), ...outgoing.map((e) => e.target)]);
6694
+ const neighbors = store.graph.nodes.filter((n) => neighborIds.has(n.id));
6695
+ return { node, incoming, outgoing, neighbors };
6696
+ });
6697
+ app.get("/api/studio/summary", async (_req, reply) => {
6698
+ if (!store.graph) {
6699
+ reply.code(404).send({ error: "No project scanned yet." });
6700
+ return;
6701
+ }
6702
+ const { projectInfo } = store.graph;
6703
+ const stats = getGraphStats(store.graph);
6704
+ const critical = store.risks.filter((r) => r.severity === "critical").length;
6705
+ const high = store.risks.filter((r) => r.severity === "high").length;
6706
+ const healthScore = Math.max(0, 100 - (critical * 20 + high * 10));
6707
+ return {
6708
+ name: projectInfo.name,
6709
+ oneLiner: `A ${projectInfo.projectType} project using ${projectInfo.frameworks.join(", ") || "unknown"}, with ${stats.apiCount || 0} APIs and ${stats.modelCount || 0} data models.`,
6710
+ healthScore,
6711
+ stats,
6712
+ topRisks: store.risks.slice(0, 5).map((r) => ({ severity: r.severity, title: r.title })),
6713
+ source: store.source
6714
+ };
6715
+ });
6716
+ app.get("/api/studio/snapshots", async () => {
6717
+ const snapshots = snapshotStore?.list() ?? [];
6718
+ return {
6719
+ total: snapshots.length,
6720
+ snapshots
6721
+ };
6722
+ });
6723
+ app.post("/api/studio/snapshots/:id/load", async (req, reply) => {
6724
+ if (!snapshotStore) {
6725
+ reply.code(501).send({ error: "Snapshot persistence is not configured." });
6726
+ return;
6727
+ }
6728
+ const restored = snapshotStore.loadById(req.params.id);
6729
+ if (!restored) {
6730
+ reply.code(404).send({ error: "Snapshot not found." });
6731
+ return;
6732
+ }
6733
+ store.graph = restored.graph;
6734
+ store.risks = restored.risks;
6735
+ store.scanTime = restored.scanTime;
6736
+ store.source = restored.source;
6737
+ office.log(`\u267B\uFE0F Restored snapshot: ${store.source || "unknown source"}`, "info");
6738
+ broadcastGraph();
6739
+ return {
6740
+ ok: true,
6741
+ source: store.source,
6742
+ scanTime: store.scanTime,
6743
+ graph: store.graph ? {
6744
+ nodeCount: store.graph.nodes.length,
6745
+ edgeCount: store.graph.edges.length
6746
+ } : null,
6747
+ risks: store.risks.length
6748
+ };
6749
+ });
6750
+ app.post("/api/studio/snapshots/:id/rename", async (req, reply) => {
6751
+ if (!snapshotStore) {
6752
+ reply.code(501).send({ error: "Snapshot persistence is not configured." });
6753
+ return;
6754
+ }
6755
+ const name = req.body?.name?.trim();
6756
+ if (!name) {
6757
+ reply.code(400).send({ error: "Snapshot name is required." });
6758
+ return;
6759
+ }
6760
+ const renamed = snapshotStore.rename(req.params.id, name);
6761
+ if (!renamed) {
6762
+ reply.code(404).send({ error: "Snapshot not found." });
6763
+ return;
6764
+ }
6765
+ return {
6766
+ ok: true,
6767
+ snapshots: snapshotStore.list()
6768
+ };
6769
+ });
6770
+ app.post("/api/studio/snapshots/:id/pin", async (req, reply) => {
6771
+ if (!snapshotStore) {
6772
+ reply.code(501).send({ error: "Snapshot persistence is not configured." });
6773
+ return;
6774
+ }
6775
+ const pinned = Boolean(req.body?.pinned);
6776
+ const updated = snapshotStore.pin(req.params.id, pinned);
6777
+ if (!updated) {
6778
+ reply.code(404).send({ error: "Snapshot not found." });
6779
+ return;
6780
+ }
6781
+ return {
6782
+ ok: true,
6783
+ snapshots: snapshotStore.list()
6784
+ };
6785
+ });
6786
+ app.post("/api/studio/snapshots/:id/tags", async (req, reply) => {
6787
+ if (!snapshotStore) {
6788
+ reply.code(501).send({ error: "Snapshot persistence is not configured." });
6789
+ return;
6790
+ }
6791
+ const rawTags = Array.isArray(req.body?.tags) ? req.body.tags : [];
6792
+ const tags = [...new Set(
6793
+ rawTags.map((tag) => typeof tag === "string" ? tag.trim() : "").filter(Boolean)
6794
+ )];
6795
+ const updated = snapshotStore.updateTags(req.params.id, tags);
6796
+ if (!updated) {
6797
+ reply.code(404).send({ error: "Snapshot not found." });
6798
+ return;
6799
+ }
6800
+ return {
6801
+ ok: true,
6802
+ snapshots: snapshotStore.list()
6803
+ };
6804
+ });
6805
+ app.post("/api/studio/snapshots/:id/delete", async (req, reply) => {
6806
+ if (!snapshotStore) {
6807
+ reply.code(501).send({ error: "Snapshot persistence is not configured." });
6808
+ return;
6809
+ }
6810
+ const deleted = snapshotStore.delete(req.params.id);
6811
+ if (!deleted) {
6812
+ reply.code(404).send({ error: "Snapshot not found." });
6813
+ return;
6814
+ }
6815
+ const current = snapshotStore.load();
6816
+ store.graph = current?.graph ?? null;
6817
+ store.risks = current?.risks ?? [];
6818
+ store.scanTime = current?.scanTime ?? 0;
6819
+ store.source = current?.source ?? "";
6820
+ if (store.graph) {
6821
+ broadcastGraph();
6822
+ }
6823
+ return {
6824
+ ok: true,
6825
+ hasCurrent: Boolean(current?.graph),
6826
+ snapshots: snapshotStore.list()
6827
+ };
6828
+ });
6829
+ }
6830
+ var init_studio = __esm({
6831
+ "src/server/routes/studio.ts"() {
6832
+ "use strict";
6833
+ init_esm_shims();
6834
+ init_github_cloner();
6835
+ init_graph();
6836
+ init_insight();
6837
+ init_studio_store();
6838
+ }
6839
+ });
6840
+
4320
6841
  // src/execution/coordinator.ts
4321
6842
  var coordinator_exports = {};
4322
6843
  __export(coordinator_exports, {
@@ -4339,7 +6860,7 @@ function getFailureLines(output) {
4339
6860
  return output.split(/\r?\n/).filter((line) => /fail|error|timeout/i.test(line)).slice(0, 5);
4340
6861
  }
4341
6862
  function createExecutionCoordinator(deps = {}) {
4342
- const execSync = deps.execSync ?? nodeExecSync;
6863
+ const execSync2 = deps.execSync ?? nodeExecSync;
4343
6864
  const categorizeFailure2 = deps.categorizeFailure;
4344
6865
  return {
4345
6866
  async run(request) {
@@ -4348,7 +6869,7 @@ function createExecutionCoordinator(deps = {}) {
4348
6869
  const command = `npx playwright test ${request.testFiles.map((file) => `"${file}"`).join(" ")} --reporter=line 2>&1`;
4349
6870
  let output;
4350
6871
  try {
4351
- output = String(execSync(command, {
6872
+ output = String(execSync2(command, {
4352
6873
  cwd: request.cwd,
4353
6874
  encoding: "utf-8",
4354
6875
  timeout: timeoutMs,
@@ -4419,7 +6940,7 @@ var backend_manager_exports = {};
4419
6940
  __export(backend_manager_exports, {
4420
6941
  createBackendManager: () => createBackendManager
4421
6942
  });
4422
- import { resolve as resolve8 } from "path";
6943
+ import { resolve as resolve10 } from "path";
4423
6944
  import { spawn as nodeSpawn } from "child_process";
4424
6945
  function normalizeHealthUrl(server, baseURL) {
4425
6946
  if (server?.healthUrl) return server.healthUrl;
@@ -4481,7 +7002,7 @@ function createBackendManager(deps = {}) {
4481
7002
  throw new Error("BOOT_CONFIG_MISSING: runtime.server.command is required for managed mode");
4482
7003
  }
4483
7004
  const child = spawn(server.command, server.args ?? [], {
4484
- cwd: resolve8(request.cwd, server.cwd ?? "."),
7005
+ cwd: resolve10(request.cwd, server.cwd ?? "."),
4485
7006
  shell: true,
4486
7007
  stdio: "pipe",
4487
7008
  env: process.env
@@ -4516,14 +7037,14 @@ var runtime_bootstrap_exports = {};
4516
7037
  __export(runtime_bootstrap_exports, {
4517
7038
  createRuntimeBootstrap: () => createRuntimeBootstrap
4518
7039
  });
4519
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
4520
- import { dirname as dirname5, join as join13 } from "path";
7040
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "fs";
7041
+ import { dirname as dirname7, join as join16 } from "path";
4521
7042
  function ensureFile(filePath, content, force) {
4522
- if (existsSync15(filePath) && !force) {
7043
+ if (existsSync19(filePath) && !force) {
4523
7044
  return false;
4524
7045
  }
4525
- mkdirSync10(dirname5(filePath), { recursive: true });
4526
- writeFileSync10(filePath, content, "utf-8");
7046
+ mkdirSync12(dirname7(filePath), { recursive: true });
7047
+ writeFileSync11(filePath, content, "utf-8");
4527
7048
  return true;
4528
7049
  }
4529
7050
  function createRuntimeBootstrap(config) {
@@ -4553,7 +7074,7 @@ function createRuntimeBootstrap(config) {
4553
7074
  const writtenFiles = [];
4554
7075
  const skippedFiles = [];
4555
7076
  for (const file of files) {
4556
- const filePath = join13(request.cwd, file.name);
7077
+ const filePath = join16(request.cwd, file.name);
4557
7078
  const written = ensureFile(filePath, file.content, force);
4558
7079
  if (written) writtenFiles.push(file.name);
4559
7080
  else skippedFiles.push(file.name);
@@ -4822,13 +7343,13 @@ var init_croc_office = __esm({
4822
7343
  const fullResult = await pipeline.run(["scan", "er-diagram", "api-chain", "plan", "codegen"]);
4823
7344
  this.lastPipelineResult = fullResult;
4824
7345
  this.lastGeneratedFiles = fullResult.generatedFiles;
4825
- const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
4826
- const { dirname: dirname7 } = await import("path");
7346
+ const { writeFileSync: writeFileSync14, mkdirSync: mkdirSync15 } = await import("fs");
7347
+ const { dirname: dirname9 } = await import("path");
4827
7348
  let filesWritten = 0;
4828
7349
  for (const file of fullResult.generatedFiles) {
4829
7350
  const fullPath = resolvePath(this.cwd, file.filePath);
4830
- mkdirSync11(dirname7(fullPath), { recursive: true });
4831
- writeFileSync11(fullPath, file.content, "utf-8");
7351
+ mkdirSync15(dirname9(fullPath), { recursive: true });
7352
+ writeFileSync14(fullPath, file.content, "utf-8");
4832
7353
  filesWritten++;
4833
7354
  }
4834
7355
  this.updateNodeStatus("controller", "passed");
@@ -4912,14 +7433,14 @@ var init_croc_office = __esm({
4912
7433
  let backendStatus;
4913
7434
  try {
4914
7435
  const { resolve: resolvePath } = await import("path");
4915
- const { existsSync: existsSync17 } = await import("fs");
7436
+ const { existsSync: existsSync22 } = await import("fs");
4916
7437
  const { createExecutionCoordinator: createExecutionCoordinator2 } = await Promise.resolve().then(() => (init_coordinator(), coordinator_exports));
4917
7438
  const { createBackendManager: createBackendManager2 } = await Promise.resolve().then(() => (init_backend_manager(), backend_manager_exports));
4918
7439
  const { createRuntimeBootstrap: createRuntimeBootstrap2 } = await Promise.resolve().then(() => (init_runtime_bootstrap(), runtime_bootstrap_exports));
4919
7440
  const { createAuthProvisioner: createAuthProvisioner2 } = await Promise.resolve().then(() => (init_auth_provisioner(), auth_provisioner_exports));
4920
7441
  const { buildExecutionQualityGate: buildExecutionQualityGate2 } = await Promise.resolve().then(() => (init_quality_gate(), quality_gate_exports));
4921
7442
  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));
7443
+ const testFiles = this.lastGeneratedFiles.map((f) => resolvePath(this.cwd, f.filePath)).filter((f) => existsSync22(f));
4923
7444
  if (testFiles.length === 0) {
4924
7445
  this.log("\u26A0\uFE0F No test files found on disk", "warn");
4925
7446
  return { ok: false, task: "execute", duration: Date.now() - start, error: "No test files found on disk" };
@@ -5046,12 +7567,12 @@ var init_croc_office = __esm({
5046
7567
  });
5047
7568
  this.lastReports = reports;
5048
7569
  const { resolve: resolvePath } = await import("path");
5049
- const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync11 } = await import("fs");
7570
+ const { writeFileSync: writeFileSync14, mkdirSync: mkdirSync15 } = await import("fs");
5050
7571
  const outDir = resolvePath(this.cwd, this.config.outDir || "./opencroc-output");
5051
- mkdirSync11(outDir, { recursive: true });
7572
+ mkdirSync15(outDir, { recursive: true });
5052
7573
  for (const report2 of reports) {
5053
7574
  const fullPath = resolvePath(outDir, report2.filename);
5054
- writeFileSync11(fullPath, report2.content, "utf-8");
7575
+ writeFileSync14(fullPath, report2.content, "utf-8");
5055
7576
  this.log(`\u{1F4C4} Generated ${report2.format} report: ${report2.filename}`);
5056
7577
  }
5057
7578
  this.updateAgent("reporter-croc", { status: "done", currentTask: `${reports.length} reports generated`, progress: 100 });
@@ -5175,7 +7696,7 @@ var init_croc_office = __esm({
5175
7696
  page: "app",
5176
7697
  application: "app"
5177
7698
  };
5178
- const inferModule = (filePath, type) => {
7699
+ const inferModule2 = (filePath, type) => {
5179
7700
  const parts = filePath.replace(/\\/g, "/").split("/");
5180
7701
  const typeDir = type === "model" ? "models" : "controllers";
5181
7702
  const typeDirIdx = parts.indexOf(typeDir);
@@ -5203,7 +7724,7 @@ var init_croc_office = __esm({
5203
7724
  });
5204
7725
  for (const file of modelFiles) {
5205
7726
  const parts = file.replace(/\\/g, "/").split("/");
5206
- const moduleName = inferModule(file, "model");
7727
+ const moduleName = inferModule2(file, "model");
5207
7728
  const fileName = parts[parts.length - 1].replace(/\.(ts|js)$/, "");
5208
7729
  const nodeId = `model:${fileName}`;
5209
7730
  moduleSet.add(moduleName);
@@ -5222,7 +7743,7 @@ var init_croc_office = __esm({
5222
7743
  });
5223
7744
  for (const file of controllerFiles) {
5224
7745
  const parts = file.replace(/\\/g, "/").split("/");
5225
- const moduleName = inferModule(file, "controller");
7746
+ const moduleName = inferModule2(file, "controller");
5226
7747
  const fileName = parts[parts.length - 1].replace(/\.(ts|js)$/, "").replace(/Controller$/, "");
5227
7748
  const nodeId = `controller:${fileName}`;
5228
7749
  moduleSet.add(moduleName);
@@ -5296,13 +7817,13 @@ import Fastify from "fastify";
5296
7817
  import fastifyStatic from "@fastify/static";
5297
7818
  import fastifyWebsocket from "@fastify/websocket";
5298
7819
  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";
7820
+ import { dirname as dirname8, join as join17, resolve as resolve11 } from "path";
7821
+ import { existsSync as existsSync20 } from "fs";
5301
7822
  async function startServer(opts) {
5302
7823
  const app = Fastify({ logger: false });
5303
7824
  await app.register(fastifyWebsocket);
5304
- const webDir = resolve9(__dirname2, "../web");
5305
- if (existsSync16(webDir)) {
7825
+ const webDir = resolve11(__dirname2, "../web");
7826
+ if (existsSync20(webDir)) {
5306
7827
  await app.register(fastifyStatic, {
5307
7828
  root: webDir,
5308
7829
  prefix: "/",
@@ -5310,8 +7831,10 @@ async function startServer(opts) {
5310
7831
  });
5311
7832
  }
5312
7833
  const office = new CrocOffice(opts.config, opts.cwd);
7834
+ const snapshotStore = new FileStudioSnapshotStore(resolve11(opts.cwd, ".opencroc/studio-snapshot.json"));
5313
7835
  registerProjectRoutes(app, office);
5314
7836
  registerAgentRoutes(app, office);
7837
+ registerStudioRoutes(app, office, snapshotStore);
5315
7838
  app.register(async (fastify) => {
5316
7839
  fastify.get("/ws", { websocket: true }, (socket) => {
5317
7840
  office.addClient(socket);
@@ -5323,8 +7846,11 @@ async function startServer(opts) {
5323
7846
  reply.code(404).send({ error: "Not found" });
5324
7847
  return;
5325
7848
  }
5326
- const indexPath = join14(webDir, "index.html");
5327
- if (existsSync16(indexPath)) {
7849
+ const studioPath = join17(webDir, "index-studio.html");
7850
+ const indexPath = join17(webDir, "index.html");
7851
+ if (existsSync20(studioPath)) {
7852
+ reply.sendFile("index-studio.html");
7853
+ } else if (existsSync20(indexPath)) {
5328
7854
  reply.sendFile("index.html");
5329
7855
  } else {
5330
7856
  reply.code(200).header("content-type", "text/html").send(getEmbeddedHtml());
@@ -5473,9 +7999,11 @@ var init_server = __esm({
5473
7999
  init_esm_shims();
5474
8000
  init_project();
5475
8001
  init_agents();
8002
+ init_studio();
5476
8003
  init_croc_office();
8004
+ init_studio_store();
5477
8005
  __filename2 = fileURLToPath2(import.meta.url);
5478
- __dirname2 = dirname6(__filename2);
8006
+ __dirname2 = dirname8(__filename2);
5479
8007
  }
5480
8008
  });
5481
8009
 
@@ -5520,6 +8048,248 @@ var init_serve = __esm({
5520
8048
  }
5521
8049
  });
5522
8050
 
8051
+ // src/cli/commands/scan.ts
8052
+ var scan_exports = {};
8053
+ __export(scan_exports, {
8054
+ scan: () => scan
8055
+ });
8056
+ import * as fs11 from "fs";
8057
+ import * as path12 from "path";
8058
+ import chalk12 from "chalk";
8059
+ async function scan(target, opts) {
8060
+ console.log("");
8061
+ console.log(chalk12.green("\u{1F40A} OpenCroc Studio \u2014 Universal Project Scanner"));
8062
+ console.log(chalk12.gray(` Target: ${target}`));
8063
+ console.log("");
8064
+ const startTime = Date.now();
8065
+ console.log(chalk12.cyan("\u{1F4E1} Phase 1: Scanning project..."));
8066
+ const scanResult = await cloneAndScan({
8067
+ target,
8068
+ branch: opts.branch,
8069
+ keepClone: true,
8070
+ onProgress: (phase, percent, detail) => {
8071
+ if (percent % 25 === 0 || phase === "clone") {
8072
+ console.log(chalk12.gray(` [${phase}] ${percent}% ${detail || ""}`));
8073
+ }
8074
+ }
8075
+ });
8076
+ console.log(chalk12.green(` \u2705 Found ${scanResult.entities.length} entities, ${scanResult.relationships.length} relationships`));
8077
+ console.log(chalk12.gray(` Languages: ${Object.entries(scanResult.languages).filter(([k]) => !["json", "yaml", "markdown"].includes(k)).map(([k, v]) => `${k}(${v})`).join(", ")}`));
8078
+ console.log(chalk12.gray(` Frameworks: ${scanResult.frameworks.map((f) => f.name).join(", ") || "none detected"}`));
8079
+ console.log("");
8080
+ console.log(chalk12.cyan("\u{1F9E0} Phase 2: Building knowledge graph..."));
8081
+ const projectName = target.includes("/") ? target.split("/").pop().replace(".git", "") : path12.basename(path12.resolve(target));
8082
+ const isRemote = target.startsWith("http") || /^[\w.-]+\/[\w.-]+$/.test(target);
8083
+ const graph = buildKnowledgeGraph(scanResult, {
8084
+ projectName,
8085
+ source: isRemote ? "github" : "local",
8086
+ sourceUrl: target,
8087
+ rootPath: target
8088
+ });
8089
+ const stats = getGraphStats(graph);
8090
+ console.log(chalk12.green(` \u2705 ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
8091
+ printStats(stats);
8092
+ console.log("");
8093
+ let risks = [];
8094
+ if (opts.risks) {
8095
+ console.log(chalk12.cyan("\u26A0\uFE0F Phase 3: Analyzing risks..."));
8096
+ risks = await analyzeRisks(graph);
8097
+ printRisks(risks);
8098
+ console.log("");
8099
+ }
8100
+ if (opts.report) {
8101
+ console.log(chalk12.cyan(`\u{1F4CB} Phase 4: Generating ${opts.report} report...`));
8102
+ const report2 = await generateReport(graph, opts.report, risks);
8103
+ printReport(report2);
8104
+ console.log("");
8105
+ }
8106
+ fs11.mkdirSync(opts.output, { recursive: true });
8107
+ const graphPath = path12.join(opts.output, "knowledge-graph.json");
8108
+ fs11.writeFileSync(graphPath, JSON.stringify({
8109
+ projectInfo: graph.projectInfo,
8110
+ stats: getGraphStats(graph),
8111
+ nodes: graph.nodes.map((n) => ({ id: n.id, label: n.label, type: n.type, module: n.module, filePath: n.filePath })),
8112
+ edges: graph.edges.map((e) => ({ source: e.source, target: e.target, relation: e.relation })),
8113
+ risks: risks.length > 0 ? risks : void 0,
8114
+ builtAt: graph.builtAt
8115
+ }, null, 2));
8116
+ console.log(chalk12.gray(` \u{1F4C1} Graph saved: ${graphPath}`));
8117
+ if (opts.json) {
8118
+ const jsonPath = path12.join(opts.output, "scan-result.json");
8119
+ fs11.writeFileSync(jsonPath, JSON.stringify({
8120
+ project: graph.projectInfo,
8121
+ stats,
8122
+ risks: risks.length > 0 ? risks : void 0
8123
+ }, null, 2));
8124
+ console.log(chalk12.gray(` \u{1F4C1} JSON saved: ${jsonPath}`));
8125
+ }
8126
+ if (opts.mermaid) {
8127
+ const mermaidPath = path12.join(opts.output, "graph.mmd");
8128
+ const mermaidText = toMermaid(graph, { nodeTypes: ["module", "model", "api", "service"], maxNodes: 40 });
8129
+ fs11.writeFileSync(mermaidPath, mermaidText);
8130
+ console.log(chalk12.gray(` \u{1F4C1} Mermaid saved: ${mermaidPath}`));
8131
+ }
8132
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
8133
+ console.log("");
8134
+ console.log(chalk12.green(`\u{1F40A} Done in ${duration}s`));
8135
+ console.log(chalk12.gray(` Run ${chalk12.white("opencroc serve")} to explore the knowledge graph in Studio UI`));
8136
+ console.log("");
8137
+ }
8138
+ function printStats(stats) {
8139
+ const items = [];
8140
+ if (stats.moduleCount) items.push(`${stats.moduleCount} modules`);
8141
+ if (stats.apiCount) items.push(`${stats.apiCount} APIs`);
8142
+ if (stats.modelCount) items.push(`${stats.modelCount} models`);
8143
+ if (stats.classCount) items.push(`${stats.classCount} classes`);
8144
+ if (stats.functionCount) items.push(`${stats.functionCount} functions`);
8145
+ if (stats.dependencyCount) items.push(`${stats.dependencyCount} dependencies`);
8146
+ if (items.length > 0) {
8147
+ console.log(chalk12.gray(` ${items.join(" | ")}`));
8148
+ }
8149
+ }
8150
+ function printRisks(risks) {
8151
+ if (risks.length === 0) {
8152
+ console.log(chalk12.green(" \u2705 No significant risks detected."));
8153
+ return;
8154
+ }
8155
+ const critical = risks.filter((r) => r.severity === "critical").length;
8156
+ const high = risks.filter((r) => r.severity === "high").length;
8157
+ const medium = risks.filter((r) => r.severity === "medium").length;
8158
+ const low = risks.filter((r) => r.severity === "low").length;
8159
+ console.log(chalk12.yellow(` Found ${risks.length} risks:`));
8160
+ if (critical) console.log(chalk12.red(` \u{1F534} Critical: ${critical}`));
8161
+ if (high) console.log(chalk12.hex("#e67e22")(` \u{1F7E0} High: ${high}`));
8162
+ if (medium) console.log(chalk12.yellow(` \u{1F7E1} Medium: ${medium}`));
8163
+ if (low) console.log(chalk12.blue(` \u{1F535} Low: ${low}`));
8164
+ console.log("");
8165
+ console.log(chalk12.white(" Top risks:"));
8166
+ for (const r of risks.slice(0, 5)) {
8167
+ const color = r.severity === "critical" ? chalk12.red : r.severity === "high" ? chalk12.hex("#e67e22") : r.severity === "medium" ? chalk12.yellow : chalk12.blue;
8168
+ console.log(` ${color(`[${r.severity.toUpperCase()}]`)} ${r.title}`);
8169
+ }
8170
+ }
8171
+ function printReport(report2) {
8172
+ console.log(chalk12.green(` \u2705 ${report2.title}`));
8173
+ console.log(chalk12.gray(` ${report2.summary}`));
8174
+ for (const section of report2.sections) {
8175
+ console.log(chalk12.cyan(`
8176
+ \u2500\u2500 ${section.heading} \u2500\u2500`));
8177
+ const lines = section.content.split("\n").slice(0, 3);
8178
+ for (const line of lines) {
8179
+ console.log(chalk12.gray(` ${line}`));
8180
+ }
8181
+ if (section.content.split("\n").length > 3) {
8182
+ console.log(chalk12.gray(" ..."));
8183
+ }
8184
+ }
8185
+ }
8186
+ var init_scan = __esm({
8187
+ "src/cli/commands/scan.ts"() {
8188
+ "use strict";
8189
+ init_esm_shims();
8190
+ init_github_cloner();
8191
+ init_graph();
8192
+ init_insight();
8193
+ }
8194
+ });
8195
+
8196
+ // src/cli/commands/analyze.ts
8197
+ var analyze_exports = {};
8198
+ __export(analyze_exports, {
8199
+ analyze: () => analyze
8200
+ });
8201
+ import * as fs12 from "fs";
8202
+ import * as path13 from "path";
8203
+ import chalk13 from "chalk";
8204
+ async function analyze(target, opts) {
8205
+ console.log("");
8206
+ console.log(chalk13.green("\u{1F40A} OpenCroc Studio \u2014 Project Analysis"));
8207
+ console.log("");
8208
+ const absTarget = path13.resolve(target);
8209
+ const graphPath = path13.join(opts.output, "knowledge-graph.json");
8210
+ let graph;
8211
+ if (fs12.existsSync(graphPath)) {
8212
+ console.log(chalk13.gray(` Loading cached graph from ${graphPath}...`));
8213
+ }
8214
+ console.log(chalk13.cyan("\u{1F4E1} Scanning project..."));
8215
+ const scanResult = await scanProject({
8216
+ rootDir: absTarget,
8217
+ onProgress: (phase, percent, detail) => {
8218
+ if (percent === 100) console.log(chalk13.gray(` [${phase}] ${detail || "done"}`));
8219
+ }
8220
+ });
8221
+ const projectName = path13.basename(absTarget);
8222
+ graph = buildKnowledgeGraph(scanResult, {
8223
+ projectName,
8224
+ source: "local",
8225
+ rootPath: absTarget
8226
+ });
8227
+ console.log(chalk13.green(` \u2705 ${graph.nodes.length} nodes, ${graph.edges.length} edges`));
8228
+ console.log("");
8229
+ if (opts.risks) {
8230
+ console.log(chalk13.cyan("\u26A0\uFE0F Risk Analysis"));
8231
+ const risks2 = await analyzeRisks(graph);
8232
+ if (risks2.length === 0) {
8233
+ console.log(chalk13.green(" \u2705 No significant risks detected."));
8234
+ } else {
8235
+ for (const r of risks2) {
8236
+ const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "high" ? "\u{1F7E0}" : r.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
8237
+ console.log(` ${icon} [${r.severity.toUpperCase()}] ${r.title}`);
8238
+ console.log(chalk13.gray(` ${r.description}`));
8239
+ if (r.suggestion) {
8240
+ console.log(chalk13.green(` \u{1F4A1} ${r.suggestion}`));
8241
+ }
8242
+ console.log("");
8243
+ }
8244
+ }
8245
+ }
8246
+ if (opts.impact) {
8247
+ console.log(chalk13.cyan(`\u{1F3AF} Impact Analysis: ${opts.impact}`));
8248
+ const impact = analyzeImpact(graph, opts.impact);
8249
+ console.log(` ${impact.summary}`);
8250
+ console.log(` Direct impact: ${impact.directImpact.length} entities`);
8251
+ console.log(` Transitive impact: ${impact.transitiveImpact.length} entities`);
8252
+ console.log(` Risk level: ${impact.riskLevel}`);
8253
+ console.log("");
8254
+ if (impact.mermaidText) {
8255
+ const mermaidPath = path13.join(opts.output, "impact.mmd");
8256
+ fs12.mkdirSync(opts.output, { recursive: true });
8257
+ fs12.writeFileSync(mermaidPath, impact.mermaidText);
8258
+ console.log(chalk13.gray(` \u{1F4C1} Impact diagram saved: ${mermaidPath}`));
8259
+ }
8260
+ }
8261
+ const perspective = opts.perspective;
8262
+ console.log(chalk13.cyan(`\u{1F4CB} ${perspective.charAt(0).toUpperCase() + perspective.slice(1)} Report`));
8263
+ console.log("");
8264
+ const risks = await analyzeRisks(graph);
8265
+ const report2 = await generateReport(graph, perspective, risks);
8266
+ console.log(chalk13.bold.green(` ${report2.title}`));
8267
+ console.log(chalk13.gray(` ${report2.summary}`));
8268
+ console.log("");
8269
+ for (const section of report2.sections) {
8270
+ console.log(chalk13.cyan(` \u2501\u2501 ${section.heading} \u2501\u2501`));
8271
+ const lines = section.content.split("\n");
8272
+ for (const line of lines) {
8273
+ console.log(` ${line}`);
8274
+ }
8275
+ console.log("");
8276
+ }
8277
+ fs12.mkdirSync(opts.output, { recursive: true });
8278
+ const reportPath = path13.join(opts.output, `report-${perspective}.json`);
8279
+ fs12.writeFileSync(reportPath, JSON.stringify(report2, null, 2));
8280
+ console.log(chalk13.gray(` \u{1F4C1} Report saved: ${reportPath}`));
8281
+ console.log("");
8282
+ }
8283
+ var init_analyze = __esm({
8284
+ "src/cli/commands/analyze.ts"() {
8285
+ "use strict";
8286
+ init_esm_shims();
8287
+ init_project_scanner();
8288
+ init_graph();
8289
+ init_insight();
8290
+ }
8291
+ });
8292
+
5523
8293
  // src/cli/index.ts
5524
8294
  init_esm_shims();
5525
8295
  import { Command } from "commander";
@@ -5569,5 +8339,13 @@ program.command("serve").description("Start OpenCroc Studio \u2014 pixel croc of
5569
8339
  const { serve: serve2 } = await Promise.resolve().then(() => (init_serve(), serve_exports));
5570
8340
  await serve2(opts);
5571
8341
  });
8342
+ 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) => {
8343
+ const { scan: scan2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
8344
+ await scan2(target, opts);
8345
+ });
8346
+ 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) => {
8347
+ const { analyze: analyze2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
8348
+ await analyze2(target, opts);
8349
+ });
5572
8350
  program.parse();
5573
8351
  //# sourceMappingURL=index.js.map