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/depgraph-mcp.js CHANGED
@@ -3263,8 +3263,8 @@ var require_utils = __commonJS({
3263
3263
  }
3264
3264
  return ind;
3265
3265
  }
3266
- function removeDotSegments(path7) {
3267
- let input = path7;
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 path7 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3677
- wsComponent.path = path7 && path7 !== "/" ? path7 : void 0;
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: path7, errorMaps, issueData } = params;
7681
- const fullPath = [...path7, ...issueData.path || []];
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, path7, key) {
7797
+ constructor(parent, value, path8, key) {
7798
7798
  this._cachedPath = [];
7799
7799
  this.parent = parent;
7800
7800
  this.data = value;
7801
- this._path = path7;
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, path7) {
11439
- if (!path7)
11438
+ function getElementAtPath(obj, path8) {
11439
+ if (!path8)
11440
11440
  return obj;
11441
- return path7.reduce((acc, key) => acc?.[key], obj);
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(path7, issues) {
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(path7);
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(path7) {
15177
- if (path7.length === 0) {
15176
+ function getDotPath(path8) {
15177
+ if (path8.length === 0) {
15178
15178
  return "object root";
15179
15179
  }
15180
- return path7.reduce((acc, seg, index) => {
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 import_path3 = __toESM(require("path"));
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 = import_path3.default.join(currentDir, entry);
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 = import_path3.default.extname(entry);
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 import_path4 = __toESM(require("path"));
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 = import_path4.default.extname(filePath).toLowerCase();
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 import_path5 = __toESM(require("path"));
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 = import_path5.default.basename(file.filePath, import_path5.default.extname(file.filePath));
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 = import_path5.default.basename(file.filePath, import_path5.default.extname(file.filePath));
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 = import_path5.default.basename(resolvedPath, import_path5.default.extname(resolvedPath));
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 ${import_path5.default.basename(resolvedPath)}`
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 = import_path5.default.dirname(fromFile);
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 = import_path5.default.join(fromDir, normalizedSource);
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 = import_path5.default.dirname(base);
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 import_path6 = __toESM(require("path"));
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 = import_path6.default.extname(file).toLowerCase();
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 analyzeProject(projectDir) {
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
- return {
24117
- meta: {
24118
- version: VERSION,
24119
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
24120
- totalFiles: parsed.length,
24121
- totalLines
24122
- },
24123
- summary: {
24124
- totalNodes: metrics.nodes.size,
24125
- totalEdges: metrics.edges.length,
24126
- entryPoints: getEntryPoints(metrics),
24127
- leafNodes: getLeafNodes(metrics),
24128
- isolatedNodes: getIsolatedNodes(metrics),
24129
- criticalNodes: getCriticalNodes(metrics)
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
- return {
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
- criticalNodes: getCriticalNodes(metrics)
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
- "analyze_project",
24172
- "Scan a project directory and return its full dependency graph \u2014 nodes, edges, entry points, leaf nodes, isolated nodes, critical nodes, and per-file metadata. Use this to understand the full structure of a codebase before making changes.",
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
- "Absolute path to the project directory to analyze (e.g. /Users/me/my-app)"
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 = analyzeProject(projectDir);
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
- "Get a lightweight summary of the dependency graph \u2014 file/node/edge counts, entry points, leaf nodes, isolated nodes, and critical nodes \u2014 without returning the full graph data. Faster than analyze_project when you only need the overview.",
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: JSON.stringify(result, null, 2) }]
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
- "Simulate the cascading downstream impact of changing a specific function, class, or entity. Returns a risk score (0-100), risk level (LOW/MEDIUM/HIGH/CRITICAL), a list of all affected nodes with their change requirements, a testing plan, and actionable recommendations.",
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
- 'Human-readable description of the proposed change (e.g. "removing the userId parameter")'
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
- "Detect which functions/classes changed in a git diff and simulate their downstream impact on the whole project. Supports three modes: uncommitted changes, a specific commit, or a branch comparison.",
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)"