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