codegraph-engine 2.1.1__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.
Files changed (62) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/architecture.py +328 -0
  4. codegraph/audit.py +106 -0
  5. codegraph/cache.py +95 -0
  6. codegraph/cli.py +854 -0
  7. codegraph/config.py +43 -0
  8. codegraph/constraints.py +238 -0
  9. codegraph/context.py +1228 -0
  10. codegraph/epistemic.py +90 -0
  11. codegraph/errors.py +275 -0
  12. codegraph/evidence/__init__.py +15 -0
  13. codegraph/evidence/citations.py +397 -0
  14. codegraph/frameworks.py +434 -0
  15. codegraph/freshness.py +295 -0
  16. codegraph/git.py +278 -0
  17. codegraph/graph/__init__.py +46 -0
  18. codegraph/graph/models.py +41 -0
  19. codegraph/graph/traversal.py +1291 -0
  20. codegraph/indexing/__init__.py +4 -0
  21. codegraph/indexing/classifier.py +274 -0
  22. codegraph/indexing/indexer.py +943 -0
  23. codegraph/indexing/models.py +338 -0
  24. codegraph/indexing/parser.py +1240 -0
  25. codegraph/indexing/scanner.py +200 -0
  26. codegraph/indexing/test_framework.py +116 -0
  27. codegraph/interrogation.py +1582 -0
  28. codegraph/llm/__init__.py +3 -0
  29. codegraph/llm/base.py +15 -0
  30. codegraph/llm/context.py +20 -0
  31. codegraph/mcp/__init__.py +3 -0
  32. codegraph/mcp/server.py +736 -0
  33. codegraph/memory/__init__.py +3 -0
  34. codegraph/memory/store.py +46 -0
  35. codegraph/models.py +289 -0
  36. codegraph/observability.py +151 -0
  37. codegraph/optimizer.py +372 -0
  38. codegraph/planner.py +417 -0
  39. codegraph/py.typed +1 -0
  40. codegraph/query_expansion.py +199 -0
  41. codegraph/ranking.py +363 -0
  42. codegraph/resolver.py +843 -0
  43. codegraph/resources/__init__.py +45 -0
  44. codegraph/resources/cache.py +117 -0
  45. codegraph/resources/coalescer.py +83 -0
  46. codegraph/resources/debouncer.py +98 -0
  47. codegraph/resources/governor.py +232 -0
  48. codegraph/resources/policy.py +123 -0
  49. codegraph/retrieval_policy.py +220 -0
  50. codegraph/search/__init__.py +23 -0
  51. codegraph/search/hybrid.py +301 -0
  52. codegraph/search/semantic.py +28 -0
  53. codegraph/security/__init__.py +3 -0
  54. codegraph/security/paths.py +35 -0
  55. codegraph/target_resolver.py +348 -0
  56. codegraph/task.py +637 -0
  57. codegraph_engine-2.1.1.dist-info/METADATA +334 -0
  58. codegraph_engine-2.1.1.dist-info/RECORD +62 -0
  59. codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
  60. codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
  61. codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
  62. codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,200 @@
1
+ """Secure, deterministic repository scanner.
2
+
3
+ Architectural Invariants:
4
+ 1. Low-level primitive `safe_path()` is strict and fail-closed (raises SecurityError on escape).
5
+ 2. Repository scanner catches SecurityError on individual unsafe symlink/traversal entries,
6
+ records the security skip, and continues scanning without aborting the entire repository.
7
+ 3. Unsafe/escaped paths are NEVER indexed or exposed.
8
+ 4. Scan statistics are tracked explicitly.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import fnmatch
13
+ import os
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+
17
+ from codegraph.errors import SecurityError
18
+ from codegraph.indexing.classifier import (
19
+ FileCategory,
20
+ classify_file,
21
+ is_ignored_by_codegraphignore,
22
+ load_codegraphignore,
23
+ )
24
+ from codegraph.security import is_sensitive, safe_path
25
+
26
+ IGNORED_DIRS = {
27
+ ".git",
28
+ ".venv",
29
+ "venv",
30
+ "node_modules",
31
+ "__pycache__",
32
+ "dist",
33
+ "build",
34
+ ".mypy_cache",
35
+ ".pytest_cache",
36
+ }
37
+
38
+ LANGUAGES = {
39
+ ".py": "python",
40
+ ".js": "javascript",
41
+ ".jsx": "javascript",
42
+ ".ts": "typescript",
43
+ ".tsx": "typescript",
44
+ }
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ScanResult:
49
+ path: Path
50
+ relative_path: Path
51
+ language: str
52
+ category: FileCategory = FileCategory.SOURCE
53
+
54
+
55
+ @dataclass
56
+ class ScanStats:
57
+ scanned: int = 0
58
+ accepted: int = 0
59
+ skipped: int = 0
60
+ skipped_security: int = 0
61
+ skipped_binary: int = 0
62
+ skipped_secret: int = 0
63
+ skipped_large: int = 0
64
+ skipped_ignored: int = 0
65
+ skipped_details: list[str] = field(default_factory=list)
66
+
67
+ def as_dict(self) -> dict[str, object]:
68
+ return {
69
+ "scanned": self.scanned,
70
+ "accepted": self.accepted,
71
+ "skipped": self.skipped,
72
+ "skipped_security": self.skipped_security,
73
+ "skipped_binary": self.skipped_binary,
74
+ "skipped_secret": self.skipped_secret,
75
+ "skipped_large": self.skipped_large,
76
+ "skipped_ignored": self.skipped_ignored,
77
+ }
78
+
79
+
80
+ def is_binary(path: Path) -> bool:
81
+ try:
82
+ return b"\x00" in path.read_bytes()[:8192]
83
+ except OSError:
84
+ return True
85
+
86
+
87
+ def scan_with_stats(
88
+ repository: Path,
89
+ max_file_size: int,
90
+ exclusions: tuple[str, ...] = (),
91
+ ) -> tuple[list[ScanResult], ScanStats]:
92
+ """Scan repository for supported source files, catching unsafe symlinks and collecting stats."""
93
+ root = repository.resolve(strict=True)
94
+ gitignore = _gitignore_patterns(root)
95
+ codegraphignore = load_codegraphignore(root)
96
+ results: list[ScanResult] = []
97
+ stats = ScanStats()
98
+
99
+ for directory, dirs, files in os.walk(root, followlinks=False):
100
+ dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
101
+ for filename in files:
102
+ stats.scanned += 1
103
+ absolute = Path(directory) / filename
104
+ try:
105
+ rel_unresolved = absolute.relative_to(root)
106
+ except ValueError:
107
+ stats.skipped += 1
108
+ stats.skipped_security += 1
109
+ continue
110
+
111
+ try:
112
+ # safe_path raises SecurityError if symlink resolves outside root or uses traversal escapes
113
+ resolved = safe_path(root, rel_unresolved)
114
+ except SecurityError:
115
+ stats.skipped += 1
116
+ stats.skipped_security += 1
117
+ stats.skipped_details.append(f"Security blocked: {rel_unresolved}")
118
+ continue
119
+
120
+ relative = resolved.relative_to(root)
121
+
122
+ try:
123
+ if not resolved.is_file():
124
+ stats.skipped += 1
125
+ continue
126
+ if resolved.stat().st_size > max_file_size:
127
+ stats.skipped += 1
128
+ stats.skipped_large += 1
129
+ continue
130
+ if is_sensitive(relative):
131
+ stats.skipped += 1
132
+ stats.skipped_secret += 1
133
+ continue
134
+ if (
135
+ _ignored_by_gitignore(relative, gitignore)
136
+ or is_ignored_by_codegraphignore(relative, codegraphignore)
137
+ or any(relative.match(p) for p in exclusions)
138
+ ):
139
+ stats.skipped += 1
140
+ stats.skipped_ignored += 1
141
+ continue
142
+ if is_binary(resolved):
143
+ stats.skipped += 1
144
+ stats.skipped_binary += 1
145
+ continue
146
+
147
+ category = classify_file(relative)
148
+ if category in (FileCategory.BUILD_ARTIFACT, FileCategory.VENDOR, FileCategory.BINARY):
149
+ stats.skipped += 1
150
+ if category == FileCategory.BINARY:
151
+ stats.skipped_binary += 1
152
+ else:
153
+ stats.skipped_ignored += 1
154
+ continue
155
+ except OSError:
156
+ stats.skipped += 1
157
+ continue
158
+
159
+ language = LANGUAGES.get(relative.suffix.lower())
160
+ if language:
161
+ stats.accepted += 1
162
+ results.append(ScanResult(resolved, relative, language, category=category))
163
+ else:
164
+ stats.skipped += 1
165
+
166
+ results.sort(key=lambda item: item.relative_path.as_posix())
167
+ return results, stats
168
+
169
+
170
+ def scan(
171
+ repository: Path,
172
+ max_file_size: int,
173
+ exclusions: tuple[str, ...] = (),
174
+ ) -> list[ScanResult]:
175
+ """Securely scan repository for supported source files."""
176
+ results, _ = scan_with_stats(repository, max_file_size, exclusions)
177
+ return results
178
+
179
+
180
+ def _gitignore_patterns(root: Path) -> list[str]:
181
+ path = root / ".gitignore"
182
+ try:
183
+ return [
184
+ line.strip()
185
+ for line in path.read_text().splitlines()
186
+ if line.strip() and not line.lstrip().startswith("#") and not line.startswith("!")
187
+ ]
188
+ except OSError:
189
+ return []
190
+
191
+
192
+ def _ignored_by_gitignore(relative: Path, patterns: list[str]) -> bool:
193
+ """Small, deterministic subset: ordinary glob and directory patterns; negation is unsupported."""
194
+ value = relative.as_posix()
195
+ return any(
196
+ fnmatch.fnmatch(value, pattern.rstrip("/"))
197
+ or (pattern.endswith("/") and value.startswith(pattern))
198
+ or fnmatch.fnmatch(relative.name, pattern)
199
+ for pattern in patterns
200
+ )
@@ -0,0 +1,116 @@
1
+ """Source-backed test framework detection.
2
+
3
+ Detects test frameworks (PYTEST, UNITTEST, JEST, VITEST, MOCHA, OTHER, UNKNOWN)
4
+ using concrete repository facts (imports, class inheritance, configuration files).
5
+ Does not guess or assume tests exist merely because 'test' appears in a path.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sqlite3
11
+ from enum import StrEnum
12
+ from pathlib import Path
13
+
14
+
15
+ class TestFramework(StrEnum):
16
+ PYTEST = "PYTEST"
17
+ UNITTEST = "UNITTEST"
18
+ JEST = "JEST"
19
+ VITEST = "VITEST"
20
+ MOCHA = "MOCHA"
21
+ OTHER = "OTHER"
22
+ UNKNOWN = "UNKNOWN"
23
+
24
+
25
+ def detect_test_framework(
26
+ con: sqlite3.Connection,
27
+ repository: Path,
28
+ ) -> tuple[TestFramework, bool, str]:
29
+ """Detect test framework and whether tests are present in the repository.
30
+
31
+ Returns:
32
+ (framework, tests_present, evidence)
33
+ """
34
+ root = repository.resolve()
35
+
36
+ # 1. Query files for test files
37
+ test_rows = []
38
+ try:
39
+ test_rows = con.execute(
40
+ "SELECT path FROM files WHERE (category='TEST' OR path LIKE 'tests/%' "
41
+ "OR path LIKE 'test/%' OR path LIKE '%/test_%.py' OR path LIKE '%_test.py' "
42
+ "OR path LIKE '%.test.%' OR path LIKE '%.spec.%') AND status='ok'"
43
+ ).fetchall()
44
+ except sqlite3.OperationalError:
45
+ pass
46
+
47
+ test_paths = [r[0] for r in test_rows]
48
+
49
+ # If no test files in index, check filesystem as fallback
50
+ if not test_paths:
51
+ for pat in ("tests", "test"):
52
+ d = root / pat
53
+ if d.is_dir() and any(d.iterdir()):
54
+ test_paths.append(pat)
55
+ break
56
+
57
+ if not test_paths:
58
+ return (TestFramework.UNKNOWN, False, "No test files or directories detected")
59
+
60
+ # 2. Check Python imports from test files
61
+ try:
62
+ imp_rows = con.execute(
63
+ "SELECT DISTINCT module FROM imports WHERE module IN ('pytest', 'unittest') "
64
+ "OR module LIKE 'pytest.%' OR module LIKE 'unittest.%'"
65
+ ).fetchall()
66
+ imp_modules = {r[0] for r in imp_rows}
67
+ if any(m.startswith("pytest") for m in imp_modules):
68
+ return (TestFramework.PYTEST, True, "Source evidence: imports pytest")
69
+ if any(m.startswith("unittest") for m in imp_modules):
70
+ return (TestFramework.UNITTEST, True, "Source evidence: imports unittest")
71
+ except sqlite3.OperationalError:
72
+ pass
73
+
74
+ # 3. Check Python configuration files
75
+ if (root / "pytest.ini").is_file() or (root / "conftest.py").is_file():
76
+ return (TestFramework.PYTEST, True, "Configuration evidence: pytest.ini or conftest.py present")
77
+
78
+ pyproject = root / "pyproject.toml"
79
+ if pyproject.is_file():
80
+ try:
81
+ content = pyproject.read_text(encoding="utf-8", errors="replace")
82
+ if "tool.pytest" in content or "pytest" in content:
83
+ return (TestFramework.PYTEST, True, "Configuration evidence: pyproject.toml defines pytest")
84
+ except OSError:
85
+ pass
86
+
87
+ # 4. Check JS/TS package.json
88
+ pkg_json = root / "package.json"
89
+ if pkg_json.is_file():
90
+ try:
91
+ data = json.loads(pkg_json.read_text(encoding="utf-8", errors="replace"))
92
+ dev_deps = data.get("devDependencies", {})
93
+ deps = data.get("dependencies", {})
94
+ scripts = data.get("scripts", {})
95
+ all_deps = {**deps, **dev_deps}
96
+ scripts_str = " ".join(str(v) for v in scripts.values())
97
+
98
+ if "vitest" in all_deps or "vitest" in scripts_str:
99
+ return (TestFramework.VITEST, True, "Package evidence: vitest declared in package.json")
100
+ if "jest" in all_deps or "jest" in scripts_str:
101
+ return (TestFramework.JEST, True, "Package evidence: jest declared in package.json")
102
+ if "mocha" in all_deps or "mocha" in scripts_str:
103
+ return (TestFramework.MOCHA, True, "Package evidence: mocha declared in package.json")
104
+ except (OSError, json.JSONDecodeError):
105
+ pass
106
+
107
+ # 5. Check config files for JS test runners
108
+ for f in root.glob("jest.config.*"):
109
+ if f.is_file():
110
+ return (TestFramework.JEST, True, f"Configuration evidence: {f.name} present")
111
+ for f in root.glob("vitest.config.*"):
112
+ if f.is_file():
113
+ return (TestFramework.VITEST, True, f"Configuration evidence: {f.name} present")
114
+
115
+ # 6. Fallback if test files exist but specific runner not identified
116
+ return (TestFramework.OTHER, True, f"Test files present ({len(test_paths)} test files) with unspecified runner")