codegraph-engine 2.1.1__py3-none-any.whl → 2.1.3__py3-none-any.whl

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.
codegraph/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """CodeGraph MCP: local, evidence-backed codebase intelligence."""
2
2
 
3
- __version__ = "2.1.1"
3
+ __version__ = "2.1.3"
4
4
  from codegraph.errors import (
5
5
  CodeGraphError,
6
6
  ErrorCode,
codegraph/frameworks.py CHANGED
@@ -79,6 +79,7 @@ class RouteDetection:
79
79
  resolution_status: str = "RESOLVED" # RESOLVED | UNRESOLVED | AMBIGUOUS
80
80
  evidence: str = ""
81
81
  module: str = ""
82
+ endpoint_id_override: str | None = None
82
83
 
83
84
  @property
84
85
  def route_signature(self) -> str:
@@ -87,13 +88,18 @@ class RouteDetection:
87
88
 
88
89
  @property
89
90
  def endpoint_id(self) -> str:
90
- """Stable, globally unique endpoint identifier scoped by module to prevent collisions."""
91
- if self.module:
92
- mod = self.module
93
- else:
94
- from codegraph.indexing.models import normalize_module
95
- mod = normalize_module(self.file_path)
96
- return f"API_ENDPOINT:{mod}:{self.http_method} {self.normalized_route}"
91
+ """Stable, deterministic, globally unique endpoint identifier for a route declaration."""
92
+ if self.endpoint_id_override:
93
+ return self.endpoint_id_override
94
+
95
+ clean_path = self.file_path.replace("\\", "/").strip()
96
+ while clean_path.startswith("./"):
97
+ clean_path = clean_path[2:]
98
+ clean_path = clean_path.lstrip("/") or "root"
99
+
100
+ handler_part = f"#{self.handler_name}" if self.handler_name else ""
101
+ fw_part = f":{self.framework}" if self.framework else ""
102
+ return f"API_ENDPOINT{fw_part}:{clean_path}:{self.line}:{self.http_method} {self.normalized_route}{handler_part}"
97
103
 
98
104
  def as_dict(self) -> dict[str, object]:
99
105
  return {
@@ -655,7 +655,18 @@ class Indexer:
655
655
  con.executemany(
656
656
  "INSERT INTO framework_routes(endpoint_id, framework, http_method, route_path, "
657
657
  "normalized_route, handler_name, handler_canonical_id, file_path, line, evidence, confidence) "
658
- "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
658
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?) "
659
+ "ON CONFLICT(endpoint_id) DO UPDATE SET "
660
+ "framework=excluded.framework, "
661
+ "http_method=excluded.http_method, "
662
+ "route_path=excluded.route_path, "
663
+ "normalized_route=excluded.normalized_route, "
664
+ "handler_name=excluded.handler_name, "
665
+ "handler_canonical_id=excluded.handler_canonical_id, "
666
+ "file_path=excluded.file_path, "
667
+ "line=excluded.line, "
668
+ "evidence=excluded.evidence, "
669
+ "confidence=excluded.confidence",
659
670
  [
660
671
  (
661
672
  r.endpoint_id,
@@ -794,6 +805,7 @@ class Indexer:
794
805
  line=int(r["line"]),
795
806
  evidence=str(r["evidence"]),
796
807
  confidence=str(r["confidence"]),
808
+ endpoint_id_override=str(r["endpoint_id"]),
797
809
  )
798
810
  for r in rt_rows
799
811
  ]
@@ -930,6 +942,20 @@ def check_database_health(con: sqlite3.Connection, repository: Path | None = Non
930
942
  if parser_ver and parser_ver != PARSER_VERSION:
931
943
  issues.append(f"Parser version mismatch: indexed with {parser_ver}, active parser is {PARSER_VERSION}.")
932
944
 
945
+ # 8. Orphan framework routes
946
+ orphan_routes = con.execute(
947
+ "SELECT count(*) FROM framework_routes r LEFT JOIN files f ON r.file_path = f.path WHERE f.path IS NULL"
948
+ ).fetchone()[0]
949
+ if orphan_routes > 0:
950
+ issues.append(f"Found {orphan_routes} orphan route(s) referencing non-existent files.")
951
+
952
+ # 9. Duplicate endpoint IDs
953
+ dup_routes = con.execute(
954
+ "SELECT endpoint_id, count(*) FROM framework_routes GROUP BY endpoint_id HAVING count(*) > 1"
955
+ ).fetchall()
956
+ if dup_routes:
957
+ issues.append(f"Found {len(dup_routes)} duplicate route endpoint ID(s).")
958
+
933
959
  status = "OK" if not issues else "ISSUES_FOUND"
934
960
  return {
935
961
  "status": status,
@@ -16,7 +16,8 @@ from .cache import (
16
16
  )
17
17
  from .coalescer import RequestCoalescer, get_global_coalescer
18
18
  from .debouncer import FileChangeDebouncer
19
- from .governor import ResourceGovernor, get_global_governor, get_process_memory_mb
19
+ from .governor import ResourceGovernor, get_global_governor
20
+ from .monitor import get_process_memory_mb
20
21
  from .policy import (
21
22
  ActivityMode,
22
23
  PressureLevel,
@@ -5,13 +5,12 @@ throttling background indexing, managing process memory, and cooperatively yield
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
- import resource
9
- import sys
10
8
  import threading
11
9
  import time
12
10
  from collections.abc import Iterator
13
11
  from contextlib import contextmanager
14
12
 
13
+ from .monitor import get_process_memory_mb
15
14
  from .policy import (
16
15
  ActivityMode,
17
16
  PressureLevel,
@@ -22,18 +21,6 @@ from .policy import (
22
21
  )
23
22
 
24
23
 
25
- def get_process_memory_mb() -> float:
26
- """Return estimated resident process memory in megabytes portably."""
27
- try:
28
- usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
29
- if sys.platform == "darwin":
30
- return round(usage / (1024 * 1024), 2)
31
- else:
32
- return round(usage / 1024, 2)
33
- except Exception:
34
- return 0.0
35
-
36
-
37
24
  class ResourceGovernor:
38
25
  """Thread-safe central resource governor and priority scheduler."""
39
26
 
@@ -97,17 +84,17 @@ class ResourceGovernor:
97
84
  with self._lock:
98
85
  active_inter = self._active_interactive
99
86
 
100
- if limit_mb and mem_mb >= limit_mb * 0.90:
87
+ if mem_mb is not None and limit_mb and mem_mb >= limit_mb * 0.90:
101
88
  return PressureLevel.CRITICAL
102
89
  if active_inter >= self.policy.max_interactive_workers * 2:
103
90
  return PressureLevel.CRITICAL
104
91
 
105
- if limit_mb and mem_mb >= limit_mb * 0.75:
92
+ if mem_mb is not None and limit_mb and mem_mb >= limit_mb * 0.75:
106
93
  return PressureLevel.HIGH
107
94
  if active_inter > self.policy.max_interactive_workers:
108
95
  return PressureLevel.HIGH
109
96
 
110
- if limit_mb and mem_mb >= limit_mb * 0.60:
97
+ if mem_mb is not None and limit_mb and mem_mb >= limit_mb * 0.60:
111
98
  return PressureLevel.ELEVATED
112
99
  if active_inter > 0:
113
100
  return PressureLevel.ELEVATED
@@ -0,0 +1,102 @@
1
+ """Cross-platform process resource monitoring for CodeGraph.
2
+
3
+ Provides portable resident memory (RSS) measurements across Windows, macOS,
4
+ and Linux without requiring Unix-only APIs at import or runtime.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from typing import Any
10
+
11
+
12
+ def _get_unix_memory_mb() -> float | None:
13
+ """Read process RSS using standard library resource module on Unix."""
14
+ try:
15
+ import resource # Unix-only standard library module
16
+
17
+ usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
18
+ if sys.platform == "darwin":
19
+ # macOS ru_maxrss is returned in bytes
20
+ return round(usage / (1024 * 1024), 2)
21
+ else:
22
+ # Linux and BSD ru_maxrss is returned in kilobytes
23
+ return round(usage / 1024, 2)
24
+ except Exception:
25
+ return None
26
+
27
+
28
+ def _get_psutil_memory_mb() -> float | None:
29
+ """Read process RSS using psutil if installed."""
30
+ try:
31
+ import psutil # type: ignore[import-untyped]
32
+
33
+ rss_bytes = float(psutil.Process().memory_info().rss)
34
+ return round(rss_bytes / (1024 * 1024), 2)
35
+ except Exception:
36
+ return None
37
+
38
+
39
+ def _get_windows_memory_mb_ctypes() -> float | None:
40
+ """Read process RSS on Windows using standard library ctypes (zero dependencies)."""
41
+ try:
42
+ import ctypes
43
+ from ctypes import wintypes
44
+
45
+ class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
46
+ _fields_ = [
47
+ ("cb", wintypes.DWORD),
48
+ ("PageFaultCount", wintypes.DWORD),
49
+ ("PeakWorkingSetSize", ctypes.c_size_t),
50
+ ("WorkingSetSize", ctypes.c_size_t),
51
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
52
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
53
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
54
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
55
+ ("PagefileUsage", ctypes.c_size_t),
56
+ ("PeakPagefileUsage", ctypes.c_size_t),
57
+ ]
58
+
59
+ windll: Any = getattr(ctypes, "windll", None)
60
+ if windll is None:
61
+ return None
62
+
63
+ handle = windll.kernel32.GetCurrentProcess()
64
+ counters = PROCESS_MEMORY_COUNTERS()
65
+ counters.cb = ctypes.sizeof(PROCESS_MEMORY_COUNTERS)
66
+
67
+ # On Windows 7+, K32GetProcessMemoryInfo is in kernel32; otherwise in psapi
68
+ get_info = getattr(windll.kernel32, "K32GetProcessMemoryInfo", None)
69
+ if get_info is None:
70
+ psapi = getattr(windll, "psapi", None)
71
+ if psapi is not None:
72
+ get_info = getattr(psapi, "GetProcessMemoryInfo", None)
73
+
74
+ if get_info is not None and bool(get_info(handle, ctypes.byref(counters), counters.cb)):
75
+ # WorkingSetSize is the resident set size in bytes
76
+ return round(float(counters.WorkingSetSize) / (1024 * 1024), 2)
77
+ except Exception:
78
+ return None
79
+ return None
80
+
81
+
82
+ def get_process_memory_mb() -> float | None:
83
+ """Return estimated resident process memory in megabytes cross-platform.
84
+
85
+ Returns:
86
+ float: Estimated memory in MB if measurement is supported and succeeds.
87
+ None: If measurement is unsupported or fails (degrades gracefully to UNKNOWN).
88
+ """
89
+ if sys.platform == "win32":
90
+ # 1. On Windows: try psutil first, then ctypes Windows API
91
+ mem = _get_psutil_memory_mb()
92
+ if mem is not None:
93
+ return mem
94
+ return _get_windows_memory_mb_ctypes()
95
+
96
+ # 2. On Unix-like platforms (macOS, Linux): try standard resource module
97
+ mem = _get_unix_memory_mb()
98
+ if mem is not None:
99
+ return mem
100
+
101
+ # 3. Fallback to psutil on any platform if resource module failed or is unavailable
102
+ return _get_psutil_memory_mb()
@@ -113,7 +113,7 @@ class ResourceState:
113
113
  queued_tasks: int = 0
114
114
  recent_cpu_time_ms: float = 0.0
115
115
  recent_wall_time_ms: float = 0.0
116
- estimated_memory_mb: float = 0.0
116
+ estimated_memory_mb: float | None = None
117
117
  pressure_level: str = PressureLevel.NORMAL.value
118
118
  activity_mode: str = ActivityMode.IDLE.value
119
119
  indexing_active: bool = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraph-engine
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: Evidence-backed local codebase intelligence for MCP clients
5
5
  Author: CodeGraph contributors
6
6
  License-Expression: MIT
@@ -11,6 +11,8 @@ Requires-Dist: pydantic>=2.7
11
11
  Requires-Dist: typer>=0.12
12
12
  Provides-Extra: mcp
13
13
  Requires-Dist: mcp<2,>=1.0; extra == "mcp"
14
+ Provides-Extra: system
15
+ Requires-Dist: psutil>=5.9; extra == "system"
14
16
  Provides-Extra: dev
15
17
  Requires-Dist: pytest>=8; extra == "dev"
16
18
  Requires-Dist: pytest-cov>=5; extra == "dev"
@@ -1,4 +1,4 @@
1
- codegraph/__init__.py,sha256=_fvuBpqIOyFtG6zV5ouDbAzcHh6kaZOODpnBo5zKE8A,845
1
+ codegraph/__init__.py,sha256=thPpcGX8Jit9lAYlhWDftcMUr5nLgXns6eHeIGUD81Q,845
2
2
  codegraph/agent.py,sha256=We0TWEXSMBBz6t2NQEGPB6gv-2P1IwyCmxoFGPN_CBA,911
3
3
  codegraph/architecture.py,sha256=m6RZgXDMoDkSx8KO0xuQHtz5rT0h5B7Jjh1yVE2H2LI,12069
4
4
  codegraph/audit.py,sha256=j6fFL6b4vKfnAdUYU7goqQBv4ysrxy-ecBJvT8rYoSI,3301
@@ -9,7 +9,7 @@ codegraph/constraints.py,sha256=AR_d07ENmbcDhWmcbruy4DvBYXsEdvSwkatyQ6JO-dE,8554
9
9
  codegraph/context.py,sha256=S_NJ6obPrdmlpA0RO6qD-L06d8sU-UaujzllZ1I2pSI,52219
10
10
  codegraph/epistemic.py,sha256=ShvLKjRBE6FKCBc1oARDji-NIyw3Molyrj3Eu8TzY3g,2465
11
11
  codegraph/errors.py,sha256=uzuXQe1NsOXV94xr0nmjvINPidGiTc9705tKqT928jQ,9967
12
- codegraph/frameworks.py,sha256=ZdBnA0WExyJ0ShgUHxSlB82xHF24wO32b_PReVyqWnk,16165
12
+ codegraph/frameworks.py,sha256=Cdv8NBr0Hb920rZStiZUMZFY-3Whcj2YUHVP2VU-2mE,16491
13
13
  codegraph/freshness.py,sha256=PVV7pLFz2xfDIj2DhLWB85ppn-0GZyxjoglxEdr3GBY,10041
14
14
  codegraph/git.py,sha256=DLFsvuOKiCLiBXMuxRCoMK06y1FaxT_D13pwpIA1Jek,8528
15
15
  codegraph/interrogation.py,sha256=jQ7AQOLJBeWgJe3P1qduYb4toRaDRcM8reB1xtNoT9c,54755
@@ -31,7 +31,7 @@ codegraph/graph/models.py,sha256=qq7RY4CkoDof8GS4cJIznfIB239Wo83yq2cOaIvUrXY,930
31
31
  codegraph/graph/traversal.py,sha256=3gLp1drVLCSRpRVQYtfAmi_6nZOES-pd9C2zE3vfvuA,49848
32
32
  codegraph/indexing/__init__.py,sha256=Svr4c_L0j3neb5WiTcWDYPyqAdvpsuOIBhOAXFvBv1E,149
33
33
  codegraph/indexing/classifier.py,sha256=PKY54vjj2RFk665xXYctIZuzR8OshIfFdzGuOWfuAfQ,7101
34
- codegraph/indexing/indexer.py,sha256=2HEi2DOW3JGm9xnuVXIm6n8sngkvjeCNIu7p3gSxbqY,37908
34
+ codegraph/indexing/indexer.py,sha256=XEOz0C522QJdJypqWnGBXbSlgjfvp-DIDAbM_I_P6bE,39158
35
35
  codegraph/indexing/models.py,sha256=h0ssLYF2Hbw5Ma05Lz4hXnxsoQXCrhZEdvgQHV2YzNU,11612
36
36
  codegraph/indexing/parser.py,sha256=YhR8_xs9gDGO95sw2oq3SfQwwZIw5fla5TjXVc6d4UM,46643
37
37
  codegraph/indexing/scanner.py,sha256=jEbqnDeAtxrNqMKuMHvW63C3U08SxpSzfYEJ3l7B6gU,6375
@@ -43,20 +43,21 @@ codegraph/mcp/__init__.py,sha256=BmjrnksGkbG7TPqwbyQvgYj9uei8pFSFpfkRpaGVdJU,63
43
43
  codegraph/mcp/server.py,sha256=CnBdY5Qa9T5LkAFpA3TwwpTkGetkauY7JmiGO2U9ocg,31395
44
44
  codegraph/memory/__init__.py,sha256=yfQgSZ9Tii6PPZiLW0ImwC2k-Bg6m7mnUm_K1laBems,58
45
45
  codegraph/memory/store.py,sha256=K3iD9VWF5if4OmYCTb5KxtZ3zj45kfEKmz8yivWzP-M,1490
46
- codegraph/resources/__init__.py,sha256=14FNWGRA0ZXr_m-smhFUxY8dvhgH2f1EqmyXaV6TxHA,1276
46
+ codegraph/resources/__init__.py,sha256=KPpsePEf6cNVvpMI4tnIbYVSw6Q77kaOP6wcmEko7tE,1296
47
47
  codegraph/resources/cache.py,sha256=mdhlrXjhbBGChiF_rR1SnMN27oVxjpx9f4zu5jV-hbI,4054
48
48
  codegraph/resources/coalescer.py,sha256=bKbTmX8q-R0FjNJjZQrh3Us2ZcuYsIKTrH0WimWcxQs,2510
49
49
  codegraph/resources/debouncer.py,sha256=LjDv7uGxixlH-N7vVNkphvlhvtGh6t4jGKFAGg_nX7A,3084
50
- codegraph/resources/governor.py,sha256=mk9pgxlflvrvm87aZQqexqUt5H6aiJlOlY-AjwiaWWg,8971
51
- codegraph/resources/policy.py,sha256=pJScYxmhJiSk8c9FlfORTpxf68ZpM2UwNgd4U_LhUWQ,4026
50
+ codegraph/resources/governor.py,sha256=JFZrjwXb19YtPUGoVg2I9yiKBg8Dbw3q1H8Q2J9RW68,8681
51
+ codegraph/resources/monitor.py,sha256=TZMpxvtNS4nwG1w511LAkpef4XThQjNCYZP8qu__Iy0,3753
52
+ codegraph/resources/policy.py,sha256=Xv0Bd1ZzvCcXhAObrbABxTAxdKxFoUUgCrVNSLY5u5w,4034
52
53
  codegraph/search/__init__.py,sha256=LFdgDMGE_cGWFFBk--uoRappyC_LnKvfKedogv-p5kQ,504
53
54
  codegraph/search/hybrid.py,sha256=pqdRZSwe7wEEETxmUjV5Ns7_oxus60jggnOPNRfFpg0,9617
54
55
  codegraph/search/semantic.py,sha256=QZaJVEoOZIZrp4O6SmUAzMiqMPfopgmPSpqzL3T7GVg,836
55
56
  codegraph/security/__init__.py,sha256=QsucVn1YOuC1IFrd9-EHeZd9kdHv8XUQgRZ9bTz_6_4,142
56
57
  codegraph/security/paths.py,sha256=BtiVvDgMzfL7_Dj3COt6B2Aui4A7hH13QwPUgl5-7No,1241
57
- codegraph_engine-2.1.1.dist-info/licenses/LICENSE,sha256=I6wm9APPQhaH4Cj9H9z8I2jahgYh7ik-9x7wFrmsGOo,1079
58
- codegraph_engine-2.1.1.dist-info/METADATA,sha256=A5yelC6QUOe9FKbvQfbQnMHra-hAPoxfQZY19e8B4bI,13240
59
- codegraph_engine-2.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
60
- codegraph_engine-2.1.1.dist-info/entry_points.txt,sha256=U2xQ3yhqOaaDyFtV9p7N0FDref1VEk56RE_Bnvrnycc,48
61
- codegraph_engine-2.1.1.dist-info/top_level.txt,sha256=RqBj9sPbifZTb9aeHtnbxTgKJvfHIQdmYE1Brv8Wdkg,10
62
- codegraph_engine-2.1.1.dist-info/RECORD,,
58
+ codegraph_engine-2.1.3.dist-info/licenses/LICENSE,sha256=I6wm9APPQhaH4Cj9H9z8I2jahgYh7ik-9x7wFrmsGOo,1079
59
+ codegraph_engine-2.1.3.dist-info/METADATA,sha256=5ZWgJ24vTGtBD7TQ1eOhRr2FlizC9vGNDBplfBCN49s,13309
60
+ codegraph_engine-2.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
61
+ codegraph_engine-2.1.3.dist-info/entry_points.txt,sha256=U2xQ3yhqOaaDyFtV9p7N0FDref1VEk56RE_Bnvrnycc,48
62
+ codegraph_engine-2.1.3.dist-info/top_level.txt,sha256=RqBj9sPbifZTb9aeHtnbxTgKJvfHIQdmYE1Brv8Wdkg,10
63
+ codegraph_engine-2.1.3.dist-info/RECORD,,