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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/architecture.py +328 -0
- codegraph/audit.py +106 -0
- codegraph/cache.py +95 -0
- codegraph/cli.py +854 -0
- codegraph/config.py +43 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +1228 -0
- codegraph/epistemic.py +90 -0
- codegraph/errors.py +275 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +397 -0
- codegraph/frameworks.py +434 -0
- codegraph/freshness.py +295 -0
- codegraph/git.py +278 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +41 -0
- codegraph/graph/traversal.py +1291 -0
- codegraph/indexing/__init__.py +4 -0
- codegraph/indexing/classifier.py +274 -0
- codegraph/indexing/indexer.py +943 -0
- codegraph/indexing/models.py +338 -0
- codegraph/indexing/parser.py +1240 -0
- codegraph/indexing/scanner.py +200 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/interrogation.py +1582 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +736 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +289 -0
- codegraph/observability.py +151 -0
- codegraph/optimizer.py +372 -0
- codegraph/planner.py +417 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +363 -0
- codegraph/resolver.py +843 -0
- codegraph/resources/__init__.py +45 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +232 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +220 -0
- codegraph/search/__init__.py +23 -0
- codegraph/search/hybrid.py +301 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +3 -0
- codegraph/security/paths.py +35 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph_engine-2.1.1.dist-info/METADATA +334 -0
- codegraph_engine-2.1.1.dist-info/RECORD +62 -0
- codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
- codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
- codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
- 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")
|