codegraph-engine 3.0.0__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/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -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 +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from .models import CallRef, Chunk, ImportRef, Symbol
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .indexer import Indexer
|
|
9
|
+
from .pool import SQLiteConnectionPool
|
|
10
|
+
|
|
11
|
+
__all__ = ["CallRef", "Chunk", "ImportRef", "Indexer", "SQLiteConnectionPool", "Symbol"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name: str) -> Any:
|
|
15
|
+
if name == "Indexer":
|
|
16
|
+
from .indexer import Indexer
|
|
17
|
+
|
|
18
|
+
return Indexer
|
|
19
|
+
if name == "SQLiteConnectionPool":
|
|
20
|
+
from .pool import SQLiteConnectionPool
|
|
21
|
+
|
|
22
|
+
return SQLiteConnectionPool
|
|
23
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
24
|
+
|
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""Generalized file and repository artifact classifier.
|
|
2
|
+
|
|
3
|
+
Classifies files into distinct categories:
|
|
4
|
+
- SOURCE: normal handwritten application source code
|
|
5
|
+
- TEST: test suites, mocks, and fixtures
|
|
6
|
+
- GENERATED: code generated by tools, compilers, protobuf, openapi, or marked DO NOT EDIT
|
|
7
|
+
- BUILD_ARTIFACT: build output directories (dist, build, target, .next, coverage)
|
|
8
|
+
- BUNDLE: bundled assets, webpack/rollup/esbuild runtime bundles
|
|
9
|
+
- MINIFIED: minified JavaScript, CSS, or single-line compressed assets
|
|
10
|
+
- VENDOR: external third-party dependencies (node_modules, vendor, venv)
|
|
11
|
+
- CONFIG: project, build, and dependency lockfiles
|
|
12
|
+
- BINARY: compiled objects, images, archives, executables
|
|
13
|
+
- UNKNOWN: unclassified files
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import fnmatch
|
|
18
|
+
import re
|
|
19
|
+
import sqlite3
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from enum import StrEnum
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
_BINARY_EXTENSIONS = {
|
|
26
|
+
".pyc",
|
|
27
|
+
".pyo",
|
|
28
|
+
".pyd",
|
|
29
|
+
".so",
|
|
30
|
+
".dylib",
|
|
31
|
+
".dll",
|
|
32
|
+
".wasm",
|
|
33
|
+
".exe",
|
|
34
|
+
".bin",
|
|
35
|
+
".png",
|
|
36
|
+
".jpg",
|
|
37
|
+
".jpeg",
|
|
38
|
+
".gif",
|
|
39
|
+
".ico",
|
|
40
|
+
".webp",
|
|
41
|
+
".pdf",
|
|
42
|
+
".zip",
|
|
43
|
+
".tar",
|
|
44
|
+
".gz",
|
|
45
|
+
".tgz",
|
|
46
|
+
".7z",
|
|
47
|
+
".woff",
|
|
48
|
+
".woff2",
|
|
49
|
+
".ttf",
|
|
50
|
+
".eot",
|
|
51
|
+
".mp3",
|
|
52
|
+
".mp4",
|
|
53
|
+
".obj",
|
|
54
|
+
".o",
|
|
55
|
+
".class",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_BUILD_ARTIFACT_DIRS = {
|
|
59
|
+
"dist",
|
|
60
|
+
"build",
|
|
61
|
+
"out",
|
|
62
|
+
"target",
|
|
63
|
+
".next",
|
|
64
|
+
".nuxt",
|
|
65
|
+
"coverage",
|
|
66
|
+
".turbo",
|
|
67
|
+
".cache",
|
|
68
|
+
".parcel-cache",
|
|
69
|
+
"eggs",
|
|
70
|
+
".pytest_cache",
|
|
71
|
+
".mypy_cache",
|
|
72
|
+
".ruff_cache",
|
|
73
|
+
"__pycache__",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_VENDOR_DIRS = {
|
|
77
|
+
"node_modules",
|
|
78
|
+
"vendor",
|
|
79
|
+
"third_party",
|
|
80
|
+
"bower_components",
|
|
81
|
+
".venv",
|
|
82
|
+
"venv",
|
|
83
|
+
"env",
|
|
84
|
+
"dependencies",
|
|
85
|
+
"external",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_CONFIG_FILENAMES = {
|
|
89
|
+
"package.json",
|
|
90
|
+
"tsconfig.json",
|
|
91
|
+
"pyproject.toml",
|
|
92
|
+
"setup.cfg",
|
|
93
|
+
"setup.py",
|
|
94
|
+
"cargo.toml",
|
|
95
|
+
"go.mod",
|
|
96
|
+
"makefile",
|
|
97
|
+
"dockerfile",
|
|
98
|
+
"docker-compose.yml",
|
|
99
|
+
"docker-compose.yaml",
|
|
100
|
+
"cmakelists.txt",
|
|
101
|
+
".editorconfig",
|
|
102
|
+
".gitignore",
|
|
103
|
+
".codegraphignore",
|
|
104
|
+
"requirements.txt",
|
|
105
|
+
"gemfile",
|
|
106
|
+
"pom.xml",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_LOCKFILE_NAMES = {
|
|
110
|
+
"package-lock.json",
|
|
111
|
+
"yarn.lock",
|
|
112
|
+
"pnpm-lock.yaml",
|
|
113
|
+
"poetry.lock",
|
|
114
|
+
"cargo.lock",
|
|
115
|
+
"pipfile.lock",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_CONFIG_EXTENSIONS = {
|
|
119
|
+
".json",
|
|
120
|
+
".yaml",
|
|
121
|
+
".yml",
|
|
122
|
+
".toml",
|
|
123
|
+
".ini",
|
|
124
|
+
".cfg",
|
|
125
|
+
".xml",
|
|
126
|
+
".env",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_TEST_DIRS = {
|
|
130
|
+
"tests",
|
|
131
|
+
"test",
|
|
132
|
+
"__tests__",
|
|
133
|
+
"spec",
|
|
134
|
+
"specs",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_GENERATED_PATTERNS = [
|
|
138
|
+
"*.generated.*",
|
|
139
|
+
"*_generated.py",
|
|
140
|
+
"*_pb2.py",
|
|
141
|
+
"*.pb.go",
|
|
142
|
+
"*.gen.ts",
|
|
143
|
+
"*.d.ts",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
_GENERATED_DIR_SUBSTRINGS = {
|
|
147
|
+
"generated",
|
|
148
|
+
"gen",
|
|
149
|
+
"codegen",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_BUNDLE_DIR_SUBSTRINGS = {
|
|
153
|
+
"react_assets",
|
|
154
|
+
"bundles",
|
|
155
|
+
"bundle",
|
|
156
|
+
"build_assets",
|
|
157
|
+
"static_bundles",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_SOURCE_EXTENSIONS = {
|
|
161
|
+
".py",
|
|
162
|
+
".pyi",
|
|
163
|
+
".js",
|
|
164
|
+
".jsx",
|
|
165
|
+
".ts",
|
|
166
|
+
".tsx",
|
|
167
|
+
".mjs",
|
|
168
|
+
".cjs",
|
|
169
|
+
".go",
|
|
170
|
+
".rs",
|
|
171
|
+
".java",
|
|
172
|
+
".c",
|
|
173
|
+
".cpp",
|
|
174
|
+
".cc",
|
|
175
|
+
".cxx",
|
|
176
|
+
".h",
|
|
177
|
+
".hpp",
|
|
178
|
+
".cs",
|
|
179
|
+
".rb",
|
|
180
|
+
".php",
|
|
181
|
+
".swift",
|
|
182
|
+
".kt",
|
|
183
|
+
".scala",
|
|
184
|
+
".lua",
|
|
185
|
+
".luau",
|
|
186
|
+
".sh",
|
|
187
|
+
".bash",
|
|
188
|
+
".sql",
|
|
189
|
+
".html",
|
|
190
|
+
".css",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_GENERATED_HEADER_RES = [
|
|
194
|
+
re.compile(r"^\s*(?:#|//|/\*|\*)\s*@?generated\b", re.IGNORECASE),
|
|
195
|
+
re.compile(r"^\s*(?:#|//|/\*|\*)\s*do not edit\b", re.IGNORECASE),
|
|
196
|
+
re.compile(r"^\s*(?:#|//|/\*|\*)\s*auto-generated\b", re.IGNORECASE),
|
|
197
|
+
re.compile(r"^\s*(?:#|//|/\*|\*)\s*code generated by\b", re.IGNORECASE),
|
|
198
|
+
re.compile(r"^\s*(?:#|//|/\*|\*)\s*generated by\b", re.IGNORECASE),
|
|
199
|
+
re.compile(r"^\s*(?:#|//|/\*|\*)\s*this file was generated\b", re.IGNORECASE),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
_GENERATED_HEADER_COMBINED_RE = re.compile(
|
|
203
|
+
r"^\s*(?:#|//|/\*|\*)\s*(?:@?generated|do not edit|auto-generated|code generated by|generated by|this file was generated)\b",
|
|
204
|
+
re.IGNORECASE,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class FileCategory(StrEnum):
|
|
209
|
+
SOURCE = "SOURCE"
|
|
210
|
+
TEST = "TEST"
|
|
211
|
+
GENERATED = "GENERATED"
|
|
212
|
+
BUILD_ARTIFACT = "BUILD_ARTIFACT"
|
|
213
|
+
BUNDLE = "BUNDLE"
|
|
214
|
+
MINIFIED = "MINIFIED"
|
|
215
|
+
VENDOR = "VENDOR"
|
|
216
|
+
CONFIG = "CONFIG"
|
|
217
|
+
BINARY = "BINARY"
|
|
218
|
+
UNKNOWN = "UNKNOWN"
|
|
219
|
+
|
|
220
|
+
def __eq__(self, other: object) -> bool:
|
|
221
|
+
if super().__eq__(other):
|
|
222
|
+
return True
|
|
223
|
+
other_str = str(other.value) if hasattr(other, "value") else str(other)
|
|
224
|
+
if self.value in ("MINIFIED", "BUNDLE") and other_str == "GENERATED":
|
|
225
|
+
return True
|
|
226
|
+
if self.value == "GENERATED" and other_str in ("MINIFIED", "BUNDLE"):
|
|
227
|
+
return True
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def __hash__(self) -> int:
|
|
231
|
+
return hash(self.value)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CompatCategory(str):
|
|
235
|
+
"""Backward-compatible category string that compares equal to GENERATED for BUNDLE/MINIFIED in legacy tests."""
|
|
236
|
+
|
|
237
|
+
def __eq__(self, other: object) -> bool:
|
|
238
|
+
if super().__eq__(other):
|
|
239
|
+
return True
|
|
240
|
+
other_str = str(other.value) if hasattr(other, "value") else str(other)
|
|
241
|
+
if str(self) in ("MINIFIED", "BUNDLE") and other_str == "GENERATED":
|
|
242
|
+
return True
|
|
243
|
+
if str(self) == "GENERATED" and other_str in ("MINIFIED", "BUNDLE"):
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class CompatRow:
|
|
249
|
+
"""Wrapper around sqlite3.Row that provides backward-compatible category comparisons."""
|
|
250
|
+
|
|
251
|
+
__slots__ = ("_row", "_cols")
|
|
252
|
+
|
|
253
|
+
def __init__(self, row: sqlite3.Row, cols: tuple[str, ...]) -> None:
|
|
254
|
+
self._row = row
|
|
255
|
+
self._cols = cols
|
|
256
|
+
|
|
257
|
+
def __getitem__(self, key: Any) -> Any:
|
|
258
|
+
val = self._row[key]
|
|
259
|
+
if key == "category" or (isinstance(key, int) and key < len(self._cols) and self._cols[key] == "category"):
|
|
260
|
+
if isinstance(val, str):
|
|
261
|
+
return CompatCategory(val)
|
|
262
|
+
return val
|
|
263
|
+
|
|
264
|
+
def __iter__(self) -> Any:
|
|
265
|
+
for idx in range(len(self._cols)):
|
|
266
|
+
yield self[idx]
|
|
267
|
+
|
|
268
|
+
def __len__(self) -> int:
|
|
269
|
+
return len(self._row)
|
|
270
|
+
|
|
271
|
+
def keys(self) -> list[str]:
|
|
272
|
+
return list(self._row.keys())
|
|
273
|
+
|
|
274
|
+
def __eq__(self, other: object) -> bool:
|
|
275
|
+
if isinstance(other, CompatRow):
|
|
276
|
+
return tuple(self) == tuple(other)
|
|
277
|
+
if isinstance(other, (sqlite3.Row, tuple, list)):
|
|
278
|
+
return tuple(self) == tuple(other)
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def __hash__(self) -> int:
|
|
282
|
+
return hash(tuple(self))
|
|
283
|
+
|
|
284
|
+
def __repr__(self) -> str:
|
|
285
|
+
return f"CompatRow({tuple(self)!r})"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def make_compat_row(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> CompatRow:
|
|
290
|
+
r = sqlite3.Row(cursor, row)
|
|
291
|
+
return CompatRow(r, tuple(col[0] for col in cursor.description))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
ArtifactClass = FileCategory
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@dataclass(frozen=True)
|
|
298
|
+
class ArtifactClassification:
|
|
299
|
+
category: FileCategory
|
|
300
|
+
reason: str
|
|
301
|
+
evidence: str
|
|
302
|
+
confidence: str = "HIGH"
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def artifact_class(self) -> FileCategory:
|
|
306
|
+
return self.category
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def load_codegraphignore(root: Path) -> list[str]:
|
|
310
|
+
"""Parse .codegraphignore from repository root."""
|
|
311
|
+
path = root / ".codegraphignore"
|
|
312
|
+
if not path.is_file():
|
|
313
|
+
return []
|
|
314
|
+
try:
|
|
315
|
+
patterns: list[str] = []
|
|
316
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
317
|
+
stripped = line.strip()
|
|
318
|
+
if not stripped or stripped.startswith("#") or stripped.startswith("!"):
|
|
319
|
+
continue
|
|
320
|
+
patterns.append(stripped)
|
|
321
|
+
return patterns
|
|
322
|
+
except OSError:
|
|
323
|
+
return []
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def is_ignored_by_codegraphignore(relative: Path, patterns: list[str]) -> bool:
|
|
327
|
+
"""Check if relative path matches any .codegraphignore pattern deterministically."""
|
|
328
|
+
if not patterns:
|
|
329
|
+
return False
|
|
330
|
+
posix_path = relative.as_posix()
|
|
331
|
+
name = relative.name
|
|
332
|
+
for pat in patterns:
|
|
333
|
+
clean_pat = pat.rstrip("/")
|
|
334
|
+
if pat.endswith("/"):
|
|
335
|
+
if posix_path.startswith(pat) or f"/{pat}" in f"/{posix_path}/":
|
|
336
|
+
return True
|
|
337
|
+
if any(part == clean_pat or fnmatch.fnmatch(part, clean_pat) for part in relative.parts):
|
|
338
|
+
return True
|
|
339
|
+
if fnmatch.fnmatch(posix_path, clean_pat) or fnmatch.fnmatch(name, clean_pat):
|
|
340
|
+
return True
|
|
341
|
+
if any(fnmatch.fnmatch(part, clean_pat) for part in relative.parts):
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def classify_file_detailed(
|
|
347
|
+
relative_path: Path | str,
|
|
348
|
+
content: str | bytes | None = None,
|
|
349
|
+
project_root: Path | None = None,
|
|
350
|
+
) -> ArtifactClassification:
|
|
351
|
+
"""Classify a repository file and provide an explainable reason and evidence."""
|
|
352
|
+
del project_root
|
|
353
|
+
p = Path(relative_path)
|
|
354
|
+
suffix = p.suffix.lower()
|
|
355
|
+
name = p.name.lower()
|
|
356
|
+
parts_lower = [part.lower() for part in p.parts]
|
|
357
|
+
|
|
358
|
+
# 1. Binary check
|
|
359
|
+
if suffix in _BINARY_EXTENSIONS:
|
|
360
|
+
return ArtifactClassification(
|
|
361
|
+
category=FileCategory.BINARY,
|
|
362
|
+
reason=f"binary_extension:{suffix}",
|
|
363
|
+
evidence=f"File extension '{suffix}' belongs to binary format allowlist",
|
|
364
|
+
)
|
|
365
|
+
if isinstance(content, bytes) and b"\x00" in content[:8192]:
|
|
366
|
+
return ArtifactClassification(
|
|
367
|
+
category=FileCategory.BINARY,
|
|
368
|
+
reason="binary_null_byte_signature",
|
|
369
|
+
evidence="Null byte found in initial 8KB block of content",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# 2. Lockfile check
|
|
373
|
+
if name in _LOCKFILE_NAMES:
|
|
374
|
+
return ArtifactClassification(
|
|
375
|
+
category=FileCategory.CONFIG,
|
|
376
|
+
reason="lockfile_dependency_metadata",
|
|
377
|
+
evidence=f"Filename '{name}' is a known package/dependency lockfile",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# 3. Source map check
|
|
381
|
+
if suffix == ".map" or name.endswith(".js.map") or name.endswith(".css.map"):
|
|
382
|
+
return ArtifactClassification(
|
|
383
|
+
category=FileCategory.BUILD_ARTIFACT,
|
|
384
|
+
reason="source_map_artifact",
|
|
385
|
+
evidence="File is a .map debugging and sourcemap artifact",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# 4. Build output / artifact directory check
|
|
389
|
+
for part in parts_lower:
|
|
390
|
+
if part in _BUILD_ARTIFACT_DIRS or part.endswith(".egg-info"):
|
|
391
|
+
return ArtifactClassification(
|
|
392
|
+
category=FileCategory.BUILD_ARTIFACT,
|
|
393
|
+
reason=f"known_build_output_path:{part}",
|
|
394
|
+
evidence=f"Path segment '{part}' is a known build output or cache directory",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# 5. Vendor directory check
|
|
398
|
+
for part in parts_lower:
|
|
399
|
+
if part in _VENDOR_DIRS:
|
|
400
|
+
return ArtifactClassification(
|
|
401
|
+
category=FileCategory.VENDOR,
|
|
402
|
+
reason=f"known_vendor_path:{part}",
|
|
403
|
+
evidence=f"Path segment '{part}' is a known vendor/third-party dependency directory",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# 6. Test directory and filename check
|
|
407
|
+
if any(part in _TEST_DIRS for part in parts_lower):
|
|
408
|
+
return ArtifactClassification(
|
|
409
|
+
category=FileCategory.TEST,
|
|
410
|
+
reason="test_path_or_filename",
|
|
411
|
+
evidence="File is located in a recognized test directory",
|
|
412
|
+
)
|
|
413
|
+
if (
|
|
414
|
+
name.startswith("test_")
|
|
415
|
+
or name.endswith("_test.py")
|
|
416
|
+
or name.endswith(".test.js")
|
|
417
|
+
or name.endswith(".test.ts")
|
|
418
|
+
or name.endswith(".test.jsx")
|
|
419
|
+
or name.endswith(".test.tsx")
|
|
420
|
+
or name.endswith(".spec.js")
|
|
421
|
+
or name.endswith(".spec.ts")
|
|
422
|
+
or name.endswith(".spec.jsx")
|
|
423
|
+
or name.endswith(".spec.tsx")
|
|
424
|
+
or name.endswith(".spec.lua")
|
|
425
|
+
or name.endswith(".spec.luau")
|
|
426
|
+
or name.endswith(".test.lua")
|
|
427
|
+
or name.endswith(".test.luau")
|
|
428
|
+
):
|
|
429
|
+
return ArtifactClassification(
|
|
430
|
+
category=FileCategory.TEST,
|
|
431
|
+
reason="test_path_or_filename",
|
|
432
|
+
evidence=f"Filename '{name}' matches test/spec naming pattern",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# 7. Minified JS/CSS check
|
|
436
|
+
if name.endswith(".min.js") or name.endswith(".min.css"):
|
|
437
|
+
return ArtifactClassification(
|
|
438
|
+
category=FileCategory.MINIFIED,
|
|
439
|
+
reason=f"minified_extension:{suffix}",
|
|
440
|
+
evidence=f"Filename '{name}' matches minified asset convention",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# 8. Bundle check
|
|
444
|
+
if name.endswith(".bundle.js") or name.endswith(".bundle.css"):
|
|
445
|
+
return ArtifactClassification(
|
|
446
|
+
category=FileCategory.BUNDLE,
|
|
447
|
+
reason=f"bundle_extension:{suffix}",
|
|
448
|
+
evidence=f"Filename '{name}' matches bundle asset convention",
|
|
449
|
+
)
|
|
450
|
+
for part in parts_lower:
|
|
451
|
+
if part in _BUNDLE_DIR_SUBSTRINGS:
|
|
452
|
+
return ArtifactClassification(
|
|
453
|
+
category=FileCategory.BUNDLE,
|
|
454
|
+
reason=f"bundle_directory_path:{part}",
|
|
455
|
+
evidence=f"Path segment '{part}' indicates bundled asset directory",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# 9. Content-based minification / bundle heuristics
|
|
459
|
+
text_content: str | None = None
|
|
460
|
+
if isinstance(content, str):
|
|
461
|
+
text_content = content
|
|
462
|
+
elif isinstance(content, bytes):
|
|
463
|
+
try:
|
|
464
|
+
text_content = content.decode("utf-8", errors="replace")
|
|
465
|
+
except Exception:
|
|
466
|
+
text_content = None
|
|
467
|
+
|
|
468
|
+
if text_content:
|
|
469
|
+
# Check source map directive
|
|
470
|
+
if "//# sourceMappingURL=" in text_content or "/*# sourceMappingURL=" in text_content:
|
|
471
|
+
if suffix in (".js", ".css"):
|
|
472
|
+
return ArtifactClassification(
|
|
473
|
+
category=FileCategory.MINIFIED,
|
|
474
|
+
reason="source_map_reference_in_minified_content",
|
|
475
|
+
evidence="SourceMappingURL directive found in transpiled/bundled asset",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
lines = text_content.splitlines()
|
|
479
|
+
if lines:
|
|
480
|
+
max_line_len = max(len(ln) for ln in lines[:30])
|
|
481
|
+
if max_line_len > 1000:
|
|
482
|
+
return ArtifactClassification(
|
|
483
|
+
category=FileCategory.MINIFIED,
|
|
484
|
+
reason="content_density_single_line_length",
|
|
485
|
+
evidence=f"Max line length of {max_line_len} exceeds 1000 characters",
|
|
486
|
+
)
|
|
487
|
+
if len(text_content) > 5000 and len(lines) < 10 and max_line_len > 500:
|
|
488
|
+
return ArtifactClassification(
|
|
489
|
+
category=FileCategory.MINIFIED,
|
|
490
|
+
reason="content_entropy_and_low_line_count",
|
|
491
|
+
evidence="Character density exceeds 5000 with fewer than 10 lines",
|
|
492
|
+
)
|
|
493
|
+
# Minified JS runtime boilerplate signatures
|
|
494
|
+
first_line = lines[0].strip()
|
|
495
|
+
if (
|
|
496
|
+
first_line.startswith("!function(")
|
|
497
|
+
or first_line.startswith("(window.webpackJsonp")
|
|
498
|
+
or first_line.startswith('"use strict";var ')
|
|
499
|
+
or first_line.startswith("(()=>{var ")
|
|
500
|
+
) and len(text_content) > 3000:
|
|
501
|
+
return ArtifactClassification(
|
|
502
|
+
category=FileCategory.BUNDLE,
|
|
503
|
+
reason="bundle_runtime_boilerplate_signature",
|
|
504
|
+
evidence="Webpack/Rollup/Vite/Esbuild runtime bundle signature detected",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# 10. Generated code content header check (first 30 lines)
|
|
508
|
+
for idx, line in enumerate(lines[:30]):
|
|
509
|
+
clean_line = line.strip()
|
|
510
|
+
if _GENERATED_HEADER_COMBINED_RE.search(clean_line):
|
|
511
|
+
for rgx in _GENERATED_HEADER_RES:
|
|
512
|
+
if rgx.search(clean_line):
|
|
513
|
+
return ArtifactClassification(
|
|
514
|
+
category=FileCategory.GENERATED,
|
|
515
|
+
reason="generated_header_banner",
|
|
516
|
+
evidence=f"Generated code header banner matched on line {idx + 1}: '{clean_line[:60]}'",
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# 11. Generated path and filename pattern check
|
|
520
|
+
for pat in _GENERATED_PATTERNS:
|
|
521
|
+
if fnmatch.fnmatch(name, pat):
|
|
522
|
+
return ArtifactClassification(
|
|
523
|
+
category=FileCategory.GENERATED,
|
|
524
|
+
reason=f"generated_filename_pattern:{pat}",
|
|
525
|
+
evidence=f"Filename matches generated pattern '{pat}'",
|
|
526
|
+
)
|
|
527
|
+
for part in parts_lower:
|
|
528
|
+
if part in _GENERATED_DIR_SUBSTRINGS:
|
|
529
|
+
return ArtifactClassification(
|
|
530
|
+
category=FileCategory.GENERATED,
|
|
531
|
+
reason=f"generated_directory_path:{part}",
|
|
532
|
+
evidence=f"Path segment '{part}' indicates generated directory",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# 12. Configuration check
|
|
536
|
+
if name in _CONFIG_FILENAMES or suffix in _CONFIG_EXTENSIONS:
|
|
537
|
+
return ArtifactClassification(
|
|
538
|
+
category=FileCategory.CONFIG,
|
|
539
|
+
reason="config_filename_or_extension",
|
|
540
|
+
evidence=f"Filename or extension '{suffix}' belongs to configuration registry",
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# 13. Source code check
|
|
544
|
+
if suffix in _SOURCE_EXTENSIONS:
|
|
545
|
+
return ArtifactClassification(
|
|
546
|
+
category=FileCategory.SOURCE,
|
|
547
|
+
reason="handwritten_source_file",
|
|
548
|
+
evidence=f"Standard source code extension '{suffix}'",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# 14. Unknown
|
|
552
|
+
return ArtifactClassification(
|
|
553
|
+
category=FileCategory.UNKNOWN,
|
|
554
|
+
reason="unclassified_file_type",
|
|
555
|
+
evidence=f"No rules matched file '{p.as_posix()}'",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def classify_file(
|
|
560
|
+
relative_path: Path | str,
|
|
561
|
+
content: str | bytes | None = None,
|
|
562
|
+
) -> FileCategory:
|
|
563
|
+
"""Classify a repository file into its canonical category (backward-compatible)."""
|
|
564
|
+
return classify_file_detailed(relative_path, content).category
|