depgraph-core 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/depgraph-output.json +1787 -163
- package/README.md +6 -6
- package/depgraph-mcp.js +480 -73
- package/depgraph.js +323 -4
- package/package.json +2 -1
- package/scripts/benchmark.js +138 -0
package/depgraph-mcp.js
CHANGED
|
@@ -3263,8 +3263,8 @@ var require_utils = __commonJS({
|
|
|
3263
3263
|
}
|
|
3264
3264
|
return ind;
|
|
3265
3265
|
}
|
|
3266
|
-
function removeDotSegments(
|
|
3267
|
-
let input =
|
|
3266
|
+
function removeDotSegments(path8) {
|
|
3267
|
+
let input = path8;
|
|
3268
3268
|
const output = [];
|
|
3269
3269
|
let nextSlash = -1;
|
|
3270
3270
|
let len = 0;
|
|
@@ -3673,8 +3673,8 @@ var require_schemes = __commonJS({
|
|
|
3673
3673
|
}
|
|
3674
3674
|
if (wsComponent.resourceName) {
|
|
3675
3675
|
const queryIndex = wsComponent.resourceName.indexOf("?");
|
|
3676
|
-
const
|
|
3677
|
-
wsComponent.path =
|
|
3676
|
+
const path8 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
|
|
3677
|
+
wsComponent.path = path8 && path8 !== "/" ? path8 : void 0;
|
|
3678
3678
|
wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
|
|
3679
3679
|
wsComponent.resourceName = void 0;
|
|
3680
3680
|
}
|
|
@@ -7677,8 +7677,8 @@ function getErrorMap() {
|
|
|
7677
7677
|
|
|
7678
7678
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
7679
7679
|
var makeIssue = (params) => {
|
|
7680
|
-
const { data, path:
|
|
7681
|
-
const fullPath = [...
|
|
7680
|
+
const { data, path: path8, errorMaps, issueData } = params;
|
|
7681
|
+
const fullPath = [...path8, ...issueData.path || []];
|
|
7682
7682
|
const fullIssue = {
|
|
7683
7683
|
...issueData,
|
|
7684
7684
|
path: fullPath
|
|
@@ -7794,11 +7794,11 @@ var errorUtil;
|
|
|
7794
7794
|
|
|
7795
7795
|
// node_modules/zod/v3/types.js
|
|
7796
7796
|
var ParseInputLazyPath = class {
|
|
7797
|
-
constructor(parent, value,
|
|
7797
|
+
constructor(parent, value, path8, key) {
|
|
7798
7798
|
this._cachedPath = [];
|
|
7799
7799
|
this.parent = parent;
|
|
7800
7800
|
this.data = value;
|
|
7801
|
-
this._path =
|
|
7801
|
+
this._path = path8;
|
|
7802
7802
|
this._key = key;
|
|
7803
7803
|
}
|
|
7804
7804
|
get path() {
|
|
@@ -11435,10 +11435,10 @@ function assignProp(target, prop, value) {
|
|
|
11435
11435
|
configurable: true
|
|
11436
11436
|
});
|
|
11437
11437
|
}
|
|
11438
|
-
function getElementAtPath(obj,
|
|
11439
|
-
if (!
|
|
11438
|
+
function getElementAtPath(obj, path8) {
|
|
11439
|
+
if (!path8)
|
|
11440
11440
|
return obj;
|
|
11441
|
-
return
|
|
11441
|
+
return path8.reduce((acc, key) => acc?.[key], obj);
|
|
11442
11442
|
}
|
|
11443
11443
|
function promiseAllObject(promisesObj) {
|
|
11444
11444
|
const keys = Object.keys(promisesObj);
|
|
@@ -11758,11 +11758,11 @@ function aborted(x, startIndex = 0) {
|
|
|
11758
11758
|
}
|
|
11759
11759
|
return false;
|
|
11760
11760
|
}
|
|
11761
|
-
function prefixIssues(
|
|
11761
|
+
function prefixIssues(path8, issues) {
|
|
11762
11762
|
return issues.map((iss) => {
|
|
11763
11763
|
var _a;
|
|
11764
11764
|
(_a = iss).path ?? (_a.path = []);
|
|
11765
|
-
iss.path.unshift(
|
|
11765
|
+
iss.path.unshift(path8);
|
|
11766
11766
|
return iss;
|
|
11767
11767
|
});
|
|
11768
11768
|
}
|
|
@@ -15173,11 +15173,11 @@ function normalizeObjectSchema(schema) {
|
|
|
15173
15173
|
}
|
|
15174
15174
|
return void 0;
|
|
15175
15175
|
}
|
|
15176
|
-
function getDotPath(
|
|
15177
|
-
if (
|
|
15176
|
+
function getDotPath(path8) {
|
|
15177
|
+
if (path8.length === 0) {
|
|
15178
15178
|
return "object root";
|
|
15179
15179
|
}
|
|
15180
|
-
return
|
|
15180
|
+
return path8.reduce((acc, seg, index) => {
|
|
15181
15181
|
if (index === 0) {
|
|
15182
15182
|
return String(seg);
|
|
15183
15183
|
}
|
|
@@ -21545,7 +21545,8 @@ var SUPPORTED_EXTS = /* @__PURE__ */ new Set([
|
|
|
21545
21545
|
".vue",
|
|
21546
21546
|
".svelte",
|
|
21547
21547
|
".dart",
|
|
21548
|
-
".rs"
|
|
21548
|
+
".rs",
|
|
21549
|
+
".sql"
|
|
21549
21550
|
]);
|
|
21550
21551
|
var MAX_FILE_SIZE = 3e5;
|
|
21551
21552
|
var MAX_BFS_DEPTH = 10;
|
|
@@ -23592,9 +23593,274 @@ var RustParser = {
|
|
|
23592
23593
|
};
|
|
23593
23594
|
registerParser(RustParser);
|
|
23594
23595
|
|
|
23596
|
+
// src/languages/sql/helpers.ts
|
|
23597
|
+
var NAME_PART = '(?:"(?:[^"\\n]|"")*"|`(?:[^`\\n]|``)*`|\\[(?:[^\\]\\n]|\\]\\])*\\]|[\\w$]+)';
|
|
23598
|
+
var QUAL_NAME = `${NAME_PART}(?:\\s*\\.\\s*${NAME_PART})*`;
|
|
23599
|
+
var ROUTINE_RECOVERY_RX = new RegExp(
|
|
23600
|
+
`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?(?:FUNCTION|PROC(?:EDURE)?)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})`,
|
|
23601
|
+
"gi"
|
|
23602
|
+
);
|
|
23603
|
+
function lineOf(text, offset) {
|
|
23604
|
+
return text.slice(0, offset).split("\n").length;
|
|
23605
|
+
}
|
|
23606
|
+
function normIdent(name) {
|
|
23607
|
+
return name.split(".").map((p) => {
|
|
23608
|
+
const s = p.trim();
|
|
23609
|
+
if (s.length >= 2 && (s[0] === s[s.length - 1] && (s[0] === '"' || s[0] === "`") || s[0] === "[" && s[s.length - 1] === "]")) {
|
|
23610
|
+
return s.slice(1, -1).toLowerCase();
|
|
23611
|
+
}
|
|
23612
|
+
return s.toLowerCase();
|
|
23613
|
+
}).join(".");
|
|
23614
|
+
}
|
|
23615
|
+
function maskSqlComments(text) {
|
|
23616
|
+
const out = [];
|
|
23617
|
+
let i = 0;
|
|
23618
|
+
const n = text.length;
|
|
23619
|
+
function blank(upto) {
|
|
23620
|
+
for (const ch of text.slice(i, upto)) out.push(ch === "\n" ? "\n" : " ");
|
|
23621
|
+
return upto;
|
|
23622
|
+
}
|
|
23623
|
+
while (i < n) {
|
|
23624
|
+
const c = text[i];
|
|
23625
|
+
if (c === "'") {
|
|
23626
|
+
let j = i + 1;
|
|
23627
|
+
while (j < n && text[j] !== "\n") {
|
|
23628
|
+
if (text[j] === "'") {
|
|
23629
|
+
if (j + 1 < n && text[j + 1] === "'") {
|
|
23630
|
+
j += 2;
|
|
23631
|
+
continue;
|
|
23632
|
+
}
|
|
23633
|
+
j++;
|
|
23634
|
+
break;
|
|
23635
|
+
}
|
|
23636
|
+
j++;
|
|
23637
|
+
}
|
|
23638
|
+
i = blank(j);
|
|
23639
|
+
} else if (c === '"' || c === "`" || c === "[") {
|
|
23640
|
+
const closer = c === "[" ? "]" : c;
|
|
23641
|
+
let j = i + 1;
|
|
23642
|
+
let closed = false;
|
|
23643
|
+
while (j < n && text[j] !== "\n") {
|
|
23644
|
+
if (text[j] === closer) {
|
|
23645
|
+
if (j + 1 < n && text[j + 1] === closer) {
|
|
23646
|
+
j += 2;
|
|
23647
|
+
continue;
|
|
23648
|
+
}
|
|
23649
|
+
j++;
|
|
23650
|
+
closed = true;
|
|
23651
|
+
break;
|
|
23652
|
+
}
|
|
23653
|
+
j++;
|
|
23654
|
+
}
|
|
23655
|
+
const span = text.slice(i, j);
|
|
23656
|
+
if (closed && !span.includes("--") && !span.includes("/*")) {
|
|
23657
|
+
out.push(span);
|
|
23658
|
+
i = j;
|
|
23659
|
+
} else {
|
|
23660
|
+
let eol = text.indexOf("\n", i);
|
|
23661
|
+
if (eol === -1) eol = n;
|
|
23662
|
+
i = blank(eol);
|
|
23663
|
+
}
|
|
23664
|
+
} else if (c === "-" && i + 1 < n && text[i + 1] === "-") {
|
|
23665
|
+
let j = i;
|
|
23666
|
+
while (j < n && text[j] !== "\n") j++;
|
|
23667
|
+
i = blank(j);
|
|
23668
|
+
} else if (c === "/" && i + 1 < n && text[i + 1] === "*") {
|
|
23669
|
+
let depth = 1;
|
|
23670
|
+
let j = i + 2;
|
|
23671
|
+
while (j < n && depth > 0) {
|
|
23672
|
+
if (text[j] === "/" && j + 1 < n && text[j + 1] === "*") {
|
|
23673
|
+
depth++;
|
|
23674
|
+
j += 2;
|
|
23675
|
+
} else if (text[j] === "*" && j + 1 < n && text[j + 1] === "/") {
|
|
23676
|
+
depth--;
|
|
23677
|
+
j += 2;
|
|
23678
|
+
} else j++;
|
|
23679
|
+
}
|
|
23680
|
+
i = blank(j);
|
|
23681
|
+
} else {
|
|
23682
|
+
out.push(c);
|
|
23683
|
+
i++;
|
|
23684
|
+
}
|
|
23685
|
+
}
|
|
23686
|
+
return out.join("");
|
|
23687
|
+
}
|
|
23688
|
+
var NON_TABLES = /* @__PURE__ */ new Set([
|
|
23689
|
+
"select",
|
|
23690
|
+
"where",
|
|
23691
|
+
"set",
|
|
23692
|
+
"dual",
|
|
23693
|
+
"null",
|
|
23694
|
+
"true",
|
|
23695
|
+
"false",
|
|
23696
|
+
"first",
|
|
23697
|
+
"skip",
|
|
23698
|
+
"rows",
|
|
23699
|
+
"next",
|
|
23700
|
+
"only",
|
|
23701
|
+
"lateral",
|
|
23702
|
+
"values",
|
|
23703
|
+
"inserted",
|
|
23704
|
+
"deleted",
|
|
23705
|
+
"new",
|
|
23706
|
+
"old"
|
|
23707
|
+
]);
|
|
23708
|
+
function collectCteNames(text) {
|
|
23709
|
+
const ctes = /* @__PURE__ */ new Set();
|
|
23710
|
+
const rx = /\bWITH\s+(?:RECURSIVE\s+)?([\w$]+)\s*(?:\([^()]*\))?\s+AS\s*\(/gi;
|
|
23711
|
+
for (const m of text.matchAll(rx)) ctes.add(normIdent(m[1]));
|
|
23712
|
+
return ctes;
|
|
23713
|
+
}
|
|
23714
|
+
function collectTableRefs(masked, extraNonTables = /* @__PURE__ */ new Set()) {
|
|
23715
|
+
const skip = /* @__PURE__ */ new Set([...NON_TABLES, ...extraNonTables]);
|
|
23716
|
+
const refs = [];
|
|
23717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23718
|
+
const rx = new RegExp(
|
|
23719
|
+
`\\b(?:FROM|JOIN|INTO|UPDATE)\\s+(${QUAL_NAME})`,
|
|
23720
|
+
"gi"
|
|
23721
|
+
);
|
|
23722
|
+
for (const m of masked.matchAll(rx)) {
|
|
23723
|
+
const raw = m[1];
|
|
23724
|
+
const key = normIdent(raw);
|
|
23725
|
+
if (skip.has(key) || seen.has(key)) continue;
|
|
23726
|
+
seen.add(key);
|
|
23727
|
+
refs.push({ name: raw, line: lineOf(masked, m.index ?? 0) });
|
|
23728
|
+
}
|
|
23729
|
+
return refs;
|
|
23730
|
+
}
|
|
23731
|
+
function collectFkRefs(masked) {
|
|
23732
|
+
const refs = [];
|
|
23733
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23734
|
+
const rx = new RegExp(`\\bREFERENCES\\s+(${QUAL_NAME})`, "gi");
|
|
23735
|
+
for (const m of masked.matchAll(rx)) {
|
|
23736
|
+
const raw = m[1];
|
|
23737
|
+
const key = normIdent(raw);
|
|
23738
|
+
if (seen.has(key)) continue;
|
|
23739
|
+
seen.add(key);
|
|
23740
|
+
refs.push({ name: raw, line: lineOf(masked, m.index ?? 0) });
|
|
23741
|
+
}
|
|
23742
|
+
return refs;
|
|
23743
|
+
}
|
|
23744
|
+
|
|
23745
|
+
// src/languages/sql/patterns.ts
|
|
23746
|
+
var sqlEntityPatterns = [
|
|
23747
|
+
{
|
|
23748
|
+
regex: new RegExp(
|
|
23749
|
+
`\\bCREATE\\s+(?:TEMP(?:ORARY)?\\s+)?TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})`,
|
|
23750
|
+
"gi"
|
|
23751
|
+
),
|
|
23752
|
+
type: "table"
|
|
23753
|
+
},
|
|
23754
|
+
{
|
|
23755
|
+
regex: new RegExp(
|
|
23756
|
+
`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?(?:MATERIALIZED\\s+)?VIEW\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})`,
|
|
23757
|
+
"gi"
|
|
23758
|
+
),
|
|
23759
|
+
type: "view"
|
|
23760
|
+
},
|
|
23761
|
+
{
|
|
23762
|
+
regex: new RegExp(
|
|
23763
|
+
`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?FUNCTION\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})`,
|
|
23764
|
+
"gi"
|
|
23765
|
+
),
|
|
23766
|
+
type: "function"
|
|
23767
|
+
},
|
|
23768
|
+
{
|
|
23769
|
+
regex: new RegExp(
|
|
23770
|
+
`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?PROC(?:EDURE)?\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})`,
|
|
23771
|
+
"gi"
|
|
23772
|
+
),
|
|
23773
|
+
type: "procedure"
|
|
23774
|
+
},
|
|
23775
|
+
{
|
|
23776
|
+
regex: new RegExp(
|
|
23777
|
+
`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?TRIGGER\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})`,
|
|
23778
|
+
"gi"
|
|
23779
|
+
),
|
|
23780
|
+
type: "trigger"
|
|
23781
|
+
},
|
|
23782
|
+
{
|
|
23783
|
+
regex: new RegExp(
|
|
23784
|
+
`\\bCREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QUAL_NAME})\\s+ON\\b`,
|
|
23785
|
+
"gi"
|
|
23786
|
+
),
|
|
23787
|
+
type: "index"
|
|
23788
|
+
}
|
|
23789
|
+
];
|
|
23790
|
+
|
|
23791
|
+
// src/languages/sql/extractor.ts
|
|
23792
|
+
var import_path3 = __toESM(require("path"));
|
|
23793
|
+
var _currentFile = "";
|
|
23794
|
+
function extractEntities12(code, filePath) {
|
|
23795
|
+
_currentFile = filePath;
|
|
23796
|
+
const masked = maskSqlComments(code);
|
|
23797
|
+
const entities = [];
|
|
23798
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
23799
|
+
for (const { regex, type } of sqlEntityPatterns) {
|
|
23800
|
+
regex.lastIndex = 0;
|
|
23801
|
+
for (const m of masked.matchAll(regex)) {
|
|
23802
|
+
const raw = m[1].trim();
|
|
23803
|
+
const key = normIdent(raw);
|
|
23804
|
+
if (seenNames.has(key)) continue;
|
|
23805
|
+
seenNames.add(key);
|
|
23806
|
+
entities.push({ name: raw, type, line: lineOf(code, m.index ?? 0), complexity: "low" });
|
|
23807
|
+
}
|
|
23808
|
+
}
|
|
23809
|
+
ROUTINE_RECOVERY_RX.lastIndex = 0;
|
|
23810
|
+
for (const m of masked.matchAll(ROUTINE_RECOVERY_RX)) {
|
|
23811
|
+
const raw = m[1].trim();
|
|
23812
|
+
const key = normIdent(raw);
|
|
23813
|
+
if (seenNames.has(key)) continue;
|
|
23814
|
+
seenNames.add(key);
|
|
23815
|
+
entities.push({ name: `${raw}()`, type: "procedure", line: lineOf(code, m.index ?? 0), complexity: "low" });
|
|
23816
|
+
}
|
|
23817
|
+
return entities;
|
|
23818
|
+
}
|
|
23819
|
+
function extractImports12(code) {
|
|
23820
|
+
const masked = maskSqlComments(code);
|
|
23821
|
+
const imports = [];
|
|
23822
|
+
const fileBase = import_path3.default.basename(_currentFile, import_path3.default.extname(_currentFile));
|
|
23823
|
+
if (!fileBase) return imports;
|
|
23824
|
+
const ctes = collectCteNames(masked);
|
|
23825
|
+
for (const ref of collectFkRefs(masked)) {
|
|
23826
|
+
imports.push({ source: fileBase, names: [ref.name], isLocal: true });
|
|
23827
|
+
}
|
|
23828
|
+
for (const ref of collectTableRefs(masked, ctes)) {
|
|
23829
|
+
imports.push({ source: fileBase, names: [ref.name], isLocal: true });
|
|
23830
|
+
}
|
|
23831
|
+
return imports;
|
|
23832
|
+
}
|
|
23833
|
+
function extractExports12(code) {
|
|
23834
|
+
const masked = maskSqlComments(code);
|
|
23835
|
+
const names = [];
|
|
23836
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23837
|
+
for (const { regex } of sqlEntityPatterns) {
|
|
23838
|
+
regex.lastIndex = 0;
|
|
23839
|
+
for (const m of masked.matchAll(regex)) {
|
|
23840
|
+
const key = normIdent(m[1].trim());
|
|
23841
|
+
if (!seen.has(key)) {
|
|
23842
|
+
seen.add(key);
|
|
23843
|
+
names.push(m[1].trim());
|
|
23844
|
+
}
|
|
23845
|
+
}
|
|
23846
|
+
}
|
|
23847
|
+
return names;
|
|
23848
|
+
}
|
|
23849
|
+
|
|
23850
|
+
// src/languages/sql/index.ts
|
|
23851
|
+
var SqlParser = {
|
|
23852
|
+
lang: "sql",
|
|
23853
|
+
extensions: [".sql"],
|
|
23854
|
+
extractEntities: extractEntities12,
|
|
23855
|
+
extractImports: extractImports12,
|
|
23856
|
+
extractExports: extractExports12,
|
|
23857
|
+
entityPatterns: sqlEntityPatterns
|
|
23858
|
+
};
|
|
23859
|
+
registerParser(SqlParser);
|
|
23860
|
+
|
|
23595
23861
|
// src/stages/collector.ts
|
|
23596
23862
|
var import_fs = __toESM(require("fs"));
|
|
23597
|
-
var
|
|
23863
|
+
var import_path4 = __toESM(require("path"));
|
|
23598
23864
|
function collectFiles(dir) {
|
|
23599
23865
|
const results = [];
|
|
23600
23866
|
function walk(currentDir) {
|
|
@@ -23606,7 +23872,7 @@ function collectFiles(dir) {
|
|
|
23606
23872
|
return;
|
|
23607
23873
|
}
|
|
23608
23874
|
for (const entry of entries) {
|
|
23609
|
-
const fullPath =
|
|
23875
|
+
const fullPath = import_path4.default.join(currentDir, entry);
|
|
23610
23876
|
let stat;
|
|
23611
23877
|
try {
|
|
23612
23878
|
stat = import_fs.default.statSync(fullPath);
|
|
@@ -23620,7 +23886,7 @@ function collectFiles(dir) {
|
|
|
23620
23886
|
}
|
|
23621
23887
|
continue;
|
|
23622
23888
|
}
|
|
23623
|
-
const ext =
|
|
23889
|
+
const ext = import_path4.default.extname(entry);
|
|
23624
23890
|
if (SUPPORTED_EXTS.has(ext) && stat.size < MAX_FILE_SIZE) {
|
|
23625
23891
|
results.push(fullPath);
|
|
23626
23892
|
}
|
|
@@ -23632,7 +23898,7 @@ function collectFiles(dir) {
|
|
|
23632
23898
|
|
|
23633
23899
|
// src/stages/parser.ts
|
|
23634
23900
|
var import_fs2 = __toESM(require("fs"));
|
|
23635
|
-
var
|
|
23901
|
+
var import_path5 = __toESM(require("path"));
|
|
23636
23902
|
function parseFile(filePath) {
|
|
23637
23903
|
let code;
|
|
23638
23904
|
try {
|
|
@@ -23641,7 +23907,7 @@ function parseFile(filePath) {
|
|
|
23641
23907
|
console.warn(`\u26A0 Cannot read file: ${filePath}`);
|
|
23642
23908
|
return null;
|
|
23643
23909
|
}
|
|
23644
|
-
const ext =
|
|
23910
|
+
const ext = import_path5.default.extname(filePath).toLowerCase();
|
|
23645
23911
|
const parser = getLanguageParser(ext);
|
|
23646
23912
|
if (!parser) return null;
|
|
23647
23913
|
const isHashCommentLang = [".py", ".rb", ".sh", ".bash", ".ps1"].includes(ext);
|
|
@@ -23673,12 +23939,12 @@ function parseFiles(filePaths) {
|
|
|
23673
23939
|
}
|
|
23674
23940
|
|
|
23675
23941
|
// src/stages/graph.ts
|
|
23676
|
-
var
|
|
23942
|
+
var import_path6 = __toESM(require("path"));
|
|
23677
23943
|
function buildGraph(parsedFiles) {
|
|
23678
23944
|
const nodes = /* @__PURE__ */ new Map();
|
|
23679
23945
|
const edges = [];
|
|
23680
23946
|
for (const file of parsedFiles) {
|
|
23681
|
-
const fileBase =
|
|
23947
|
+
const fileBase = import_path6.default.basename(file.filePath, import_path6.default.extname(file.filePath));
|
|
23682
23948
|
for (const entity of file.entities) {
|
|
23683
23949
|
const id = makeId(entity.name, fileBase);
|
|
23684
23950
|
if (nodes.has(id)) continue;
|
|
@@ -23702,7 +23968,7 @@ function buildGraph(parsedFiles) {
|
|
|
23702
23968
|
fileMap.set(file.filePath, file);
|
|
23703
23969
|
}
|
|
23704
23970
|
for (const file of parsedFiles) {
|
|
23705
|
-
const fileBase =
|
|
23971
|
+
const fileBase = import_path6.default.basename(file.filePath, import_path6.default.extname(file.filePath));
|
|
23706
23972
|
for (const imp of file.imports) {
|
|
23707
23973
|
if (!imp.isLocal) continue;
|
|
23708
23974
|
const resolvedPath = resolvePath(file.filePath, imp.source, parsedFiles);
|
|
@@ -23712,7 +23978,7 @@ function buildGraph(parsedFiles) {
|
|
|
23712
23978
|
for (const importedName of imp.names) {
|
|
23713
23979
|
const targetEntity = targetFile.entities.find((e) => e.name === importedName);
|
|
23714
23980
|
if (!targetEntity) continue;
|
|
23715
|
-
const targetBase =
|
|
23981
|
+
const targetBase = import_path6.default.basename(resolvedPath, import_path6.default.extname(resolvedPath));
|
|
23716
23982
|
const toId = makeId(importedName, targetBase);
|
|
23717
23983
|
if (!nodes.has(toId)) continue;
|
|
23718
23984
|
const fromEntities = file.entities.length > 0 ? file.entities : [{ name: fileBase, type: "file", line: 0, complexity: "low" }];
|
|
@@ -23728,7 +23994,7 @@ function buildGraph(parsedFiles) {
|
|
|
23728
23994
|
from: fromId,
|
|
23729
23995
|
to: toId,
|
|
23730
23996
|
type: "imports",
|
|
23731
|
-
description: `${fromEntity.name} imports ${importedName} from ${
|
|
23997
|
+
description: `${fromEntity.name} imports ${importedName} from ${import_path6.default.basename(resolvedPath)}`
|
|
23732
23998
|
});
|
|
23733
23999
|
const fromNode = nodes.get(fromId);
|
|
23734
24000
|
const toNode = nodes.get(toId);
|
|
@@ -23750,7 +24016,7 @@ function makeId(name, fileBase) {
|
|
|
23750
24016
|
return `${cleanName}__${cleanBase}`;
|
|
23751
24017
|
}
|
|
23752
24018
|
function resolvePath(fromFile, importSource, allFiles) {
|
|
23753
|
-
const fromDir =
|
|
24019
|
+
const fromDir = import_path6.default.dirname(fromFile);
|
|
23754
24020
|
let normalizedSource = importSource;
|
|
23755
24021
|
if (normalizedSource.startsWith("crate::")) {
|
|
23756
24022
|
normalizedSource = normalizedSource.slice(7).replace(/::/g, "/");
|
|
@@ -23759,7 +24025,7 @@ function resolvePath(fromFile, importSource, allFiles) {
|
|
|
23759
24025
|
} else if (normalizedSource.startsWith("self::")) {
|
|
23760
24026
|
normalizedSource = "./" + normalizedSource.slice(6).replace(/::/g, "/");
|
|
23761
24027
|
}
|
|
23762
|
-
const base =
|
|
24028
|
+
const base = import_path6.default.join(fromDir, normalizedSource);
|
|
23763
24029
|
const candidates = [
|
|
23764
24030
|
base,
|
|
23765
24031
|
`${base}.ts`,
|
|
@@ -23769,14 +24035,15 @@ function resolvePath(fromFile, importSource, allFiles) {
|
|
|
23769
24035
|
`${base}/index.ts`,
|
|
23770
24036
|
`${base}/index.js`,
|
|
23771
24037
|
`${base}.dart`,
|
|
23772
|
-
`${base}.rs
|
|
24038
|
+
`${base}.rs`,
|
|
24039
|
+
`${base}.sql`
|
|
23773
24040
|
];
|
|
23774
24041
|
for (const candidate of candidates) {
|
|
23775
24042
|
const normalized = candidate.replace(/\\/g, "/");
|
|
23776
24043
|
const found = allFiles.find((f) => f.filePath.replace(/\\/g, "/") === normalized);
|
|
23777
24044
|
if (found) return found.filePath;
|
|
23778
24045
|
}
|
|
23779
|
-
const parentBase =
|
|
24046
|
+
const parentBase = import_path6.default.dirname(base);
|
|
23780
24047
|
if (parentBase && parentBase !== base) {
|
|
23781
24048
|
const parentCandidates = [
|
|
23782
24049
|
`${parentBase}.rs`,
|
|
@@ -23966,7 +24233,7 @@ function emptyReport(targetName, changeDescription, reason) {
|
|
|
23966
24233
|
|
|
23967
24234
|
// src/stages/gitdiff.ts
|
|
23968
24235
|
var import_child_process = require("child_process");
|
|
23969
|
-
var
|
|
24236
|
+
var import_path7 = __toESM(require("path"));
|
|
23970
24237
|
function getChangedEntities(options) {
|
|
23971
24238
|
const diff = runGitDiff(options);
|
|
23972
24239
|
if (!diff) return [];
|
|
@@ -24058,7 +24325,7 @@ function parseDiff(diff, projectDir) {
|
|
|
24058
24325
|
return entities;
|
|
24059
24326
|
}
|
|
24060
24327
|
function extractEntityFromContext(context, file) {
|
|
24061
|
-
const ext =
|
|
24328
|
+
const ext = import_path7.default.extname(file).toLowerCase();
|
|
24062
24329
|
const parser = getLanguageParser(ext);
|
|
24063
24330
|
if (parser?.entityPatterns) {
|
|
24064
24331
|
for (const { regex, type } of parser.entityPatterns) {
|
|
@@ -24110,28 +24377,79 @@ function buildPipeline(projectDir) {
|
|
|
24110
24377
|
const metrics = computeMetrics(graph);
|
|
24111
24378
|
return { files, parsed, graph, metrics };
|
|
24112
24379
|
}
|
|
24113
|
-
function
|
|
24380
|
+
function buildSubgraph(graph, focusName, maxDepth) {
|
|
24381
|
+
const focusNode = [...graph.nodes.values()].find((n) => n.name === focusName);
|
|
24382
|
+
if (!focusNode) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
|
|
24383
|
+
const included = /* @__PURE__ */ new Set();
|
|
24384
|
+
const queue = [{ id: focusNode.id, depth: 0 }];
|
|
24385
|
+
while (queue.length > 0) {
|
|
24386
|
+
const { id, depth } = queue.shift();
|
|
24387
|
+
if (included.has(id)) continue;
|
|
24388
|
+
included.add(id);
|
|
24389
|
+
if (depth >= maxDepth) continue;
|
|
24390
|
+
for (const edge of graph.edges) {
|
|
24391
|
+
if (edge.from === id && !included.has(edge.to)) queue.push({ id: edge.to, depth: depth + 1 });
|
|
24392
|
+
if (edge.to === id && !included.has(edge.from)) queue.push({ id: edge.from, depth: depth + 1 });
|
|
24393
|
+
}
|
|
24394
|
+
}
|
|
24395
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
24396
|
+
for (const id of included) {
|
|
24397
|
+
const node = graph.nodes.get(id);
|
|
24398
|
+
if (node) nodes.set(id, node);
|
|
24399
|
+
}
|
|
24400
|
+
return {
|
|
24401
|
+
nodes,
|
|
24402
|
+
edges: graph.edges.filter((e) => included.has(e.from) && included.has(e.to))
|
|
24403
|
+
};
|
|
24404
|
+
}
|
|
24405
|
+
function buildProseSummary(summary) {
|
|
24406
|
+
const s = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
24407
|
+
const shortIds = (ids) => ids.slice(0, 3).map((id) => id.split("__")[0]).join(", ") + (ids.length > 3 ? ` (+${ids.length - 3} more)` : "");
|
|
24408
|
+
const parts = [
|
|
24409
|
+
`${s(summary.totalFiles, "file")}, ${s(summary.totalNodes, "node")}, ${s(summary.totalEdges, "edge")}.`
|
|
24410
|
+
];
|
|
24411
|
+
if (summary.criticalNodes.length > 0) {
|
|
24412
|
+
parts.push(
|
|
24413
|
+
`${s(summary.criticalNodes.length, "critical node")} \u2014 change carefully: ${shortIds(summary.criticalNodes)}.`
|
|
24414
|
+
);
|
|
24415
|
+
}
|
|
24416
|
+
if (summary.entryPoints.length > 0) {
|
|
24417
|
+
parts.push(`${s(summary.entryPoints.length, "entry point")}: ${shortIds(summary.entryPoints)}.`);
|
|
24418
|
+
}
|
|
24419
|
+
if (summary.leafNodes.length > 0) {
|
|
24420
|
+
parts.push(`${s(summary.leafNodes.length, "leaf node")} (pure utilities with no outgoing deps).`);
|
|
24421
|
+
}
|
|
24422
|
+
if (summary.isolatedNodes.length > 0) {
|
|
24423
|
+
parts.push(`${s(summary.isolatedNodes.length, "isolated node")} \u2014 potential dead code: ${shortIds(summary.isolatedNodes)}.`);
|
|
24424
|
+
}
|
|
24425
|
+
return parts.join(" ");
|
|
24426
|
+
}
|
|
24427
|
+
function analyzeProject(projectDir, opts = {}) {
|
|
24428
|
+
const { verbosity = "full", focus, depth = 3 } = opts;
|
|
24114
24429
|
const { files, parsed, metrics } = buildPipeline(projectDir);
|
|
24115
24430
|
const totalLines = parsed.reduce((sum, f) => sum + f.lines, 0);
|
|
24116
|
-
|
|
24117
|
-
|
|
24118
|
-
|
|
24119
|
-
|
|
24120
|
-
|
|
24121
|
-
|
|
24122
|
-
|
|
24123
|
-
|
|
24124
|
-
|
|
24125
|
-
|
|
24126
|
-
|
|
24127
|
-
|
|
24128
|
-
|
|
24129
|
-
|
|
24130
|
-
},
|
|
24131
|
-
nodes: [...metrics.nodes.values()],
|
|
24132
|
-
edges: metrics.edges,
|
|
24133
|
-
files: parsed
|
|
24431
|
+
const graph = focus ? buildSubgraph(metrics, focus, depth) : metrics;
|
|
24432
|
+
const meta = {
|
|
24433
|
+
version: VERSION,
|
|
24434
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24435
|
+
totalFiles: parsed.length,
|
|
24436
|
+
totalLines
|
|
24437
|
+
};
|
|
24438
|
+
const summary = {
|
|
24439
|
+
totalNodes: graph.nodes.size,
|
|
24440
|
+
totalEdges: graph.edges.length,
|
|
24441
|
+
entryPoints: getEntryPoints(graph),
|
|
24442
|
+
leafNodes: getLeafNodes(graph),
|
|
24443
|
+
isolatedNodes: getIsolatedNodes(graph),
|
|
24444
|
+
criticalNodes: getCriticalNodes(graph)
|
|
24134
24445
|
};
|
|
24446
|
+
if (verbosity === "sketch") {
|
|
24447
|
+
return { meta, summary, nodes: [], edges: [], files: [] };
|
|
24448
|
+
}
|
|
24449
|
+
if (verbosity === "overview") {
|
|
24450
|
+
return { meta, summary, nodes: [...graph.nodes.values()], edges: [], files: [] };
|
|
24451
|
+
}
|
|
24452
|
+
return { meta, summary, nodes: [...graph.nodes.values()], edges: graph.edges, files: parsed };
|
|
24135
24453
|
}
|
|
24136
24454
|
function analyzeImpact(projectDir, targetNode, changeDescription) {
|
|
24137
24455
|
const { metrics } = buildPipeline(projectDir);
|
|
@@ -24149,16 +24467,68 @@ function analyzeGitImpact(options) {
|
|
|
24149
24467
|
}));
|
|
24150
24468
|
return { changedEntities, impacts };
|
|
24151
24469
|
}
|
|
24152
|
-
function getGraphSummary(projectDir) {
|
|
24470
|
+
function getGraphSummary(projectDir, verbosity = "overview") {
|
|
24153
24471
|
const { files, parsed, metrics } = buildPipeline(projectDir);
|
|
24154
|
-
|
|
24472
|
+
const base = {
|
|
24155
24473
|
totalFiles: files.length,
|
|
24156
24474
|
totalNodes: metrics.nodes.size,
|
|
24157
24475
|
totalEdges: metrics.edges.length,
|
|
24476
|
+
criticalNodes: getCriticalNodes(metrics)
|
|
24477
|
+
};
|
|
24478
|
+
if (verbosity === "sketch") {
|
|
24479
|
+
return { ...base, entryPoints: [], leafNodes: [], isolatedNodes: [] };
|
|
24480
|
+
}
|
|
24481
|
+
return {
|
|
24482
|
+
...base,
|
|
24158
24483
|
entryPoints: getEntryPoints(metrics),
|
|
24159
24484
|
leafNodes: getLeafNodes(metrics),
|
|
24160
|
-
isolatedNodes: getIsolatedNodes(metrics)
|
|
24161
|
-
|
|
24485
|
+
isolatedNodes: getIsolatedNodes(metrics)
|
|
24486
|
+
};
|
|
24487
|
+
}
|
|
24488
|
+
function describeNode(projectDir, nodeName) {
|
|
24489
|
+
const { metrics } = buildPipeline(projectDir);
|
|
24490
|
+
const node = [...metrics.nodes.values()].find((n) => n.name === nodeName);
|
|
24491
|
+
if (!node) {
|
|
24492
|
+
return {
|
|
24493
|
+
found: false,
|
|
24494
|
+
name: nodeName,
|
|
24495
|
+
file: "",
|
|
24496
|
+
line: 0,
|
|
24497
|
+
type: "",
|
|
24498
|
+
lang: "",
|
|
24499
|
+
centralityScore: 0,
|
|
24500
|
+
inDegree: 0,
|
|
24501
|
+
outDegree: 0,
|
|
24502
|
+
importedBy: [],
|
|
24503
|
+
imports: [],
|
|
24504
|
+
role: "not found"
|
|
24505
|
+
};
|
|
24506
|
+
}
|
|
24507
|
+
const importedBy = [...new Set(
|
|
24508
|
+
metrics.edges.filter((e) => e.to === node.id).map((e) => metrics.nodes.get(e.from)?.name).filter((n) => Boolean(n))
|
|
24509
|
+
)];
|
|
24510
|
+
const imports = [...new Set(
|
|
24511
|
+
metrics.edges.filter((e) => e.from === node.id).map((e) => metrics.nodes.get(e.to)?.name).filter((n) => Boolean(n))
|
|
24512
|
+
)];
|
|
24513
|
+
let role;
|
|
24514
|
+
if (node.centralityScore > 20) role = "critical shared dependency";
|
|
24515
|
+
else if (node.inDegree === 0 && node.outDegree > 0) role = "entry point";
|
|
24516
|
+
else if (node.outDegree === 0 && node.inDegree > 0) role = "leaf";
|
|
24517
|
+
else if (node.inDegree === 0 && node.outDegree === 0) role = "isolated \u2014 potential dead code";
|
|
24518
|
+
else role = "connector";
|
|
24519
|
+
return {
|
|
24520
|
+
found: true,
|
|
24521
|
+
name: node.name,
|
|
24522
|
+
file: node.file,
|
|
24523
|
+
line: node.line,
|
|
24524
|
+
type: node.type,
|
|
24525
|
+
lang: node.lang,
|
|
24526
|
+
centralityScore: node.centralityScore,
|
|
24527
|
+
inDegree: node.inDegree,
|
|
24528
|
+
outDegree: node.outDegree,
|
|
24529
|
+
importedBy,
|
|
24530
|
+
imports,
|
|
24531
|
+
role
|
|
24162
24532
|
};
|
|
24163
24533
|
}
|
|
24164
24534
|
|
|
@@ -24168,16 +24538,17 @@ var server = new McpServer({
|
|
|
24168
24538
|
version: VERSION
|
|
24169
24539
|
});
|
|
24170
24540
|
server.tool(
|
|
24171
|
-
"
|
|
24172
|
-
|
|
24541
|
+
"describe_node",
|
|
24542
|
+
'PREFERRED first tool for questions about a specific function, class, or entity. Returns what depends on it, what it depends on, its file/line, type, and role \u2014 all in ~50 tokens. Use this INSTEAD of analyze_project when the question is "tell me about X" or "what uses X" or "what does X import". Much cheaper than loading the full graph.',
|
|
24173
24543
|
{
|
|
24174
|
-
projectDir: external_exports.string().describe(
|
|
24175
|
-
|
|
24544
|
+
projectDir: external_exports.string().describe("Absolute path to the project directory"),
|
|
24545
|
+
nodeName: external_exports.string().describe(
|
|
24546
|
+
'Exact name of the entity to look up (e.g. "getUserById", "UserService", "AuthMiddleware")'
|
|
24176
24547
|
)
|
|
24177
24548
|
},
|
|
24178
|
-
async ({ projectDir }) => {
|
|
24549
|
+
async ({ projectDir, nodeName }) => {
|
|
24179
24550
|
try {
|
|
24180
|
-
const result =
|
|
24551
|
+
const result = describeNode(projectDir, nodeName);
|
|
24181
24552
|
return {
|
|
24182
24553
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
24183
24554
|
};
|
|
@@ -24191,15 +24562,22 @@ server.tool(
|
|
|
24191
24562
|
);
|
|
24192
24563
|
server.tool(
|
|
24193
24564
|
"get_graph_summary",
|
|
24194
|
-
|
|
24565
|
+
'Get a project-wide overview \u2014 file/node/edge counts, critical nodes, entry points, leaf nodes, and isolated nodes. Use verbosity "sketch" (~50 tokens) for a quick orientation, "overview" for the full summary. Use format "prose" to get a single readable sentence (~100 tokens) instead of JSON. Call this before analyze_project or when you only need counts and structure, not full node data.',
|
|
24195
24566
|
{
|
|
24196
|
-
projectDir: external_exports.string().describe("Absolute path to the project directory")
|
|
24567
|
+
projectDir: external_exports.string().describe("Absolute path to the project directory"),
|
|
24568
|
+
verbosity: external_exports.enum(["sketch", "overview", "full"]).optional().describe(
|
|
24569
|
+
'"sketch" = counts + critical nodes only (~50 tokens). "overview" = full summary with entry/leaf/isolated lists (default). "full" = same as overview.'
|
|
24570
|
+
),
|
|
24571
|
+
format: external_exports.enum(["json", "prose"]).optional().describe(
|
|
24572
|
+
'"json" = structured JSON object (default). "prose" = single readable paragraph (~100 tokens) \u2014 use when you want compact context without JSON overhead.'
|
|
24573
|
+
)
|
|
24197
24574
|
},
|
|
24198
|
-
async ({ projectDir }) => {
|
|
24575
|
+
async ({ projectDir, verbosity, format }) => {
|
|
24199
24576
|
try {
|
|
24200
|
-
const result = getGraphSummary(projectDir);
|
|
24577
|
+
const result = getGraphSummary(projectDir, verbosity ?? "overview");
|
|
24578
|
+
const text = format === "prose" ? buildProseSummary(result) : JSON.stringify(result, null, 2);
|
|
24201
24579
|
return {
|
|
24202
|
-
content: [{ type: "text", text
|
|
24580
|
+
content: [{ type: "text", text }]
|
|
24203
24581
|
};
|
|
24204
24582
|
} catch (err) {
|
|
24205
24583
|
return {
|
|
@@ -24211,16 +24589,14 @@ server.tool(
|
|
|
24211
24589
|
);
|
|
24212
24590
|
server.tool(
|
|
24213
24591
|
"simulate_impact",
|
|
24214
|
-
|
|
24592
|
+
'PREFERRED tool when the question is impact-oriented: "what breaks if I change X?". Use this INSTEAD of analyze_project \u2014 it requires no graph preload and costs ~200 tokens vs ~80k for the full graph. Returns risk score (0-100), risk level (LOW/MEDIUM/HIGH/CRITICAL), all affected nodes, a testing plan, and recommendations.',
|
|
24215
24593
|
{
|
|
24216
|
-
projectDir: external_exports.string().describe(
|
|
24217
|
-
"Absolute path to the project directory"
|
|
24218
|
-
),
|
|
24594
|
+
projectDir: external_exports.string().describe("Absolute path to the project directory"),
|
|
24219
24595
|
targetNode: external_exports.string().describe(
|
|
24220
24596
|
'Name of the function, class, or entity to simulate changing (e.g. "getUserById")'
|
|
24221
24597
|
),
|
|
24222
24598
|
changeDescription: external_exports.string().describe(
|
|
24223
|
-
'
|
|
24599
|
+
'Description of the proposed change (e.g. "removing the userId parameter")'
|
|
24224
24600
|
)
|
|
24225
24601
|
},
|
|
24226
24602
|
async ({ projectDir, targetNode, changeDescription }) => {
|
|
@@ -24237,9 +24613,40 @@ server.tool(
|
|
|
24237
24613
|
}
|
|
24238
24614
|
}
|
|
24239
24615
|
);
|
|
24616
|
+
server.tool(
|
|
24617
|
+
"analyze_project",
|
|
24618
|
+
'Return the dependency graph. EXPENSIVE at full verbosity \u2014 use verbosity "sketch" or "overview", or narrow scope with "focus" + "depth" to avoid token overload. Prefer describe_node for single-entity questions, simulate_impact for risk questions, and get_graph_summary for project-wide orientation.',
|
|
24619
|
+
{
|
|
24620
|
+
projectDir: external_exports.string().describe(
|
|
24621
|
+
"Absolute path to the project directory to analyze (e.g. /Users/me/my-app)"
|
|
24622
|
+
),
|
|
24623
|
+
verbosity: external_exports.enum(["sketch", "overview", "full"]).optional().describe(
|
|
24624
|
+
'"sketch" = meta + summary only, no nodes/edges (~100 tokens). "overview" = meta + summary + nodes, no edges. "full" = complete graph incl. edges and file detail (default, can be very large).'
|
|
24625
|
+
),
|
|
24626
|
+
focus: external_exports.string().optional().describe(
|
|
24627
|
+
"Entity name to centre the graph on \u2014 returns only the subgraph within `depth` hops of this node (bidirectional). Use to scope the result to one module instead of the whole project."
|
|
24628
|
+
),
|
|
24629
|
+
depth: external_exports.number().optional().describe(
|
|
24630
|
+
"Max BFS hops from the focus node (default 3). Only used when focus is set."
|
|
24631
|
+
)
|
|
24632
|
+
},
|
|
24633
|
+
async ({ projectDir, verbosity, focus, depth }) => {
|
|
24634
|
+
try {
|
|
24635
|
+
const result = analyzeProject(projectDir, { verbosity, focus, depth });
|
|
24636
|
+
return {
|
|
24637
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
24638
|
+
};
|
|
24639
|
+
} catch (err) {
|
|
24640
|
+
return {
|
|
24641
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
24642
|
+
isError: true
|
|
24643
|
+
};
|
|
24644
|
+
}
|
|
24645
|
+
}
|
|
24646
|
+
);
|
|
24240
24647
|
server.tool(
|
|
24241
24648
|
"git_impact",
|
|
24242
|
-
"
|
|
24649
|
+
'PREFERRED tool for "what did this commit/branch change and what does it break?". Detects changed functions/classes from a git diff and simulates their downstream impact \u2014 no manual entity name needed. Supports uncommitted changes, a specific commit SHA, or a branch comparison.',
|
|
24243
24650
|
{
|
|
24244
24651
|
projectDir: external_exports.string().describe(
|
|
24245
24652
|
"Absolute path to the project directory (must be a git repository)"
|