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