codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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