treegetter 2.0.0__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.
- treegetter/__init__.py +64 -0
- treegetter/__main__.py +9 -0
- treegetter/api.py +146 -0
- treegetter/architecture.py +93 -0
- treegetter/cache.py +61 -0
- treegetter/config.py +158 -0
- treegetter/context.py +123 -0
- treegetter/dependency.py +280 -0
- treegetter/detector.py +41 -0
- treegetter/entrypoints.py +105 -0
- treegetter/exceptions.py +44 -0
- treegetter/exporters.py +169 -0
- treegetter/filters.py +139 -0
- treegetter/formatter.py +151 -0
- treegetter/language.py +62 -0
- treegetter/models.py +306 -0
- treegetter/plugins/__init__.py +55 -0
- treegetter/plugins/base.py +58 -0
- treegetter/plugins/csharp.py +31 -0
- treegetter/plugins/django.py +35 -0
- treegetter/plugins/fastapi.py +37 -0
- treegetter/plugins/flask.py +37 -0
- treegetter/plugins/flutter.py +35 -0
- treegetter/plugins/go.py +28 -0
- treegetter/plugins/java.py +33 -0
- treegetter/plugins/javascript.py +38 -0
- treegetter/plugins/nextjs.py +48 -0
- treegetter/plugins/php.py +28 -0
- treegetter/plugins/python.py +42 -0
- treegetter/plugins/react.py +47 -0
- treegetter/plugins/rust.py +28 -0
- treegetter/repository.py +258 -0
- treegetter/scanner.py +238 -0
- treegetter/search.py +121 -0
- treegetter/statistics.py +125 -0
- treegetter/summary.py +73 -0
- treegetter/tokenizer.py +43 -0
- treegetter/utils.py +136 -0
- treegetter-2.0.0.dist-info/METADATA +64 -0
- treegetter-2.0.0.dist-info/RECORD +44 -0
- treegetter-2.0.0.dist-info/WHEEL +5 -0
- treegetter-2.0.0.dist-info/entry_points.txt +2 -0
- treegetter-2.0.0.dist-info/licenses/LICENSE +21 -0
- treegetter-2.0.0.dist-info/top_level.txt +1 -0
treegetter/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""TreeGetter – AI-Ready Repository Intelligence Engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .api import generate_tree, get_tree, main
|
|
6
|
+
from .cache import CacheManager
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
AnalysisError,
|
|
9
|
+
CacheError,
|
|
10
|
+
CircularSymlinkError,
|
|
11
|
+
DirectoryTreeError,
|
|
12
|
+
InvalidPathError,
|
|
13
|
+
PermissionDeniedError,
|
|
14
|
+
PluginError,
|
|
15
|
+
ScannerError,
|
|
16
|
+
SearchError,
|
|
17
|
+
)
|
|
18
|
+
from .models import (
|
|
19
|
+
AIContext,
|
|
20
|
+
ArchitectureInfo,
|
|
21
|
+
DependencyInfo,
|
|
22
|
+
EntryPoint,
|
|
23
|
+
FileMetadata,
|
|
24
|
+
FrameworkInfo,
|
|
25
|
+
LanguageStat,
|
|
26
|
+
Node,
|
|
27
|
+
RepositorySummary,
|
|
28
|
+
SearchResult,
|
|
29
|
+
TreeConfig,
|
|
30
|
+
TreeStatistics,
|
|
31
|
+
)
|
|
32
|
+
from .plugins import BasePlugin, PluginRegistry
|
|
33
|
+
from .repository import Repository
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"get_tree",
|
|
37
|
+
"generate_tree",
|
|
38
|
+
"main",
|
|
39
|
+
"Repository",
|
|
40
|
+
"Node",
|
|
41
|
+
"TreeConfig",
|
|
42
|
+
"TreeStatistics",
|
|
43
|
+
"FileMetadata",
|
|
44
|
+
"FrameworkInfo",
|
|
45
|
+
"DependencyInfo",
|
|
46
|
+
"EntryPoint",
|
|
47
|
+
"ArchitectureInfo",
|
|
48
|
+
"LanguageStat",
|
|
49
|
+
"RepositorySummary",
|
|
50
|
+
"SearchResult",
|
|
51
|
+
"AIContext",
|
|
52
|
+
"BasePlugin",
|
|
53
|
+
"PluginRegistry",
|
|
54
|
+
"CacheManager",
|
|
55
|
+
"DirectoryTreeError",
|
|
56
|
+
"InvalidPathError",
|
|
57
|
+
"PermissionDeniedError",
|
|
58
|
+
"CircularSymlinkError",
|
|
59
|
+
"PluginError",
|
|
60
|
+
"CacheError",
|
|
61
|
+
"ScannerError",
|
|
62
|
+
"SearchError",
|
|
63
|
+
"AnalysisError",
|
|
64
|
+
]
|
treegetter/__main__.py
ADDED
treegetter/api.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Public high-level API functions for treegetter backward compatibility and CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .config import DEFAULT_IGNORE_DIRS, DEFAULT_IGNORE_PATTERNS, SUPPORTED_FORMATS
|
|
11
|
+
from .exceptions import DirectoryTreeError
|
|
12
|
+
from .filters import PathFilter
|
|
13
|
+
from .formatter import FormatterFactory
|
|
14
|
+
from .models import TreeConfig, TreeStatistics
|
|
15
|
+
from .scanner import DirectoryScanner
|
|
16
|
+
from .statistics import StatisticsEngine
|
|
17
|
+
from .utils import validate_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_tree(
|
|
21
|
+
path: str | Path,
|
|
22
|
+
*,
|
|
23
|
+
max_depth: int | None = None,
|
|
24
|
+
include_hidden: bool = False,
|
|
25
|
+
follow_symlinks: bool = False,
|
|
26
|
+
show_size: bool = False,
|
|
27
|
+
dirs_first: bool = True,
|
|
28
|
+
output_format: str = "unicode",
|
|
29
|
+
ignore_dirs: frozenset[str] | None = None,
|
|
30
|
+
ignore_patterns: tuple[str, ...] | None = None,
|
|
31
|
+
use_gitignore: bool = False,
|
|
32
|
+
show_stats: bool = False,
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Generate a formatted directory tree string."""
|
|
35
|
+
root = validate_path(path)
|
|
36
|
+
|
|
37
|
+
config = TreeConfig(
|
|
38
|
+
root=root,
|
|
39
|
+
max_depth=max_depth,
|
|
40
|
+
include_hidden=include_hidden,
|
|
41
|
+
follow_symlinks=follow_symlinks,
|
|
42
|
+
show_size=show_size,
|
|
43
|
+
dirs_first=dirs_first,
|
|
44
|
+
output_format=output_format,
|
|
45
|
+
ignore_dirs=frozenset(ignore_dirs) if ignore_dirs is not None else DEFAULT_IGNORE_DIRS,
|
|
46
|
+
ignore_patterns=(
|
|
47
|
+
tuple(ignore_patterns) if ignore_patterns is not None else DEFAULT_IGNORE_PATTERNS
|
|
48
|
+
),
|
|
49
|
+
use_gitignore=use_gitignore,
|
|
50
|
+
show_stats=show_stats,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
path_filter = PathFilter(config)
|
|
54
|
+
scanner = DirectoryScanner(config, path_filter)
|
|
55
|
+
tree = scanner.scan(root)
|
|
56
|
+
|
|
57
|
+
statistics: TreeStatistics | None = None
|
|
58
|
+
if config.show_stats:
|
|
59
|
+
statistics = StatisticsEngine().compute(tree)
|
|
60
|
+
|
|
61
|
+
formatter = FormatterFactory.create(config.output_format, show_size=config.show_size)
|
|
62
|
+
return formatter.format(tree, stats=statistics)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_tree(path: str | Path, **kwargs: Any) -> str:
|
|
66
|
+
"""Backward-compatible alias for generate_tree."""
|
|
67
|
+
return generate_tree(path, **kwargs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
prog="treegetter",
|
|
73
|
+
description="Generate a directory tree in unicode, ascii, markdown or json format.",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"path", nargs="?", default=".", help="Directory to scan (default: current directory)"
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument("-d", "--max-depth", type=int, default=None, help="Maximum depth to traverse")
|
|
79
|
+
parser.add_argument("-a", "--all", action="store_true", help="Include hidden files and directories")
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"-L", "--follow-symlinks", action="store_true", help="Descend into symlinked directories"
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument("-s", "--size", action="store_true", help="Show file sizes")
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--no-dirs-first", action="store_true", help="Do not list directories before files"
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"-f", "--format", choices=SUPPORTED_FORMATS, default="unicode", help="Output format"
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-i", "--ignore", action="append", default=[], metavar="GLOB",
|
|
92
|
+
help="Additional glob pattern to ignore (repeatable)",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--ignore-dir", action="append", default=[], metavar="NAME",
|
|
96
|
+
help="Additional directory name to ignore (repeatable)",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--gitignore", action="store_true", help="Respect a .gitignore file in the root directory"
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--stats", action="store_true", help="Show summary statistics")
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"-o", "--output", type=Path, default=None, help="Write output to a file instead of stdout"
|
|
104
|
+
)
|
|
105
|
+
return parser
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main(argv: list[str] | None = None) -> int:
|
|
109
|
+
parser = _build_arg_parser()
|
|
110
|
+
args = parser.parse_args(argv)
|
|
111
|
+
|
|
112
|
+
ignore_dirs = DEFAULT_IGNORE_DIRS | frozenset(args.ignore_dir)
|
|
113
|
+
ignore_patterns = DEFAULT_IGNORE_PATTERNS + tuple(args.ignore)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
rendered = generate_tree(
|
|
117
|
+
args.path,
|
|
118
|
+
max_depth=args.max_depth,
|
|
119
|
+
include_hidden=args.all,
|
|
120
|
+
follow_symlinks=args.follow_symlinks,
|
|
121
|
+
show_size=args.size,
|
|
122
|
+
dirs_first=not args.no_dirs_first,
|
|
123
|
+
output_format=args.format,
|
|
124
|
+
ignore_dirs=ignore_dirs,
|
|
125
|
+
ignore_patterns=ignore_patterns,
|
|
126
|
+
use_gitignore=args.gitignore,
|
|
127
|
+
show_stats=args.stats,
|
|
128
|
+
)
|
|
129
|
+
except DirectoryTreeError as exc:
|
|
130
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
131
|
+
return 1
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
134
|
+
return 2
|
|
135
|
+
|
|
136
|
+
if args.output is not None:
|
|
137
|
+
args.output.write_text(rendered, encoding="utf-8")
|
|
138
|
+
print(f"Written to {args.output}")
|
|
139
|
+
else:
|
|
140
|
+
print(rendered)
|
|
141
|
+
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Architecture pattern analyzer using folder structure heuristics and configuration files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from .models import ArchitectureInfo, FileMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ArchitectureAnalyzer:
|
|
12
|
+
"""Analyzes directory structure and file paths to identify architecture style."""
|
|
13
|
+
|
|
14
|
+
def analyze(self, root: Path, files: Sequence[FileMetadata] | None = None) -> ArchitectureInfo:
|
|
15
|
+
"""Detect architecture pattern and calculate confidence score."""
|
|
16
|
+
dir_names = set()
|
|
17
|
+
for p in root.glob("*"):
|
|
18
|
+
if p.is_dir():
|
|
19
|
+
dir_names.add(p.name.lower())
|
|
20
|
+
|
|
21
|
+
if files:
|
|
22
|
+
for f in files:
|
|
23
|
+
for part in f.path.parts:
|
|
24
|
+
dir_names.add(part.lower())
|
|
25
|
+
|
|
26
|
+
if (root / "packages").is_dir() or (root / "apps").is_dir() or (root / "lerna.json").is_file() or (root / "pnpm-workspace.yaml").is_file():
|
|
27
|
+
return ArchitectureInfo(
|
|
28
|
+
pattern="Monorepo",
|
|
29
|
+
confidence=0.95,
|
|
30
|
+
details={"indicator": "packages/apps workspace structure or configuration"},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
clean_keywords = {"domain", "usecases", "adapters", "entities", "interfaces", "infrastructure"}
|
|
34
|
+
if len(clean_keywords.intersection(dir_names)) >= 3:
|
|
35
|
+
return ArchitectureInfo(
|
|
36
|
+
pattern="Clean Architecture",
|
|
37
|
+
confidence=0.90,
|
|
38
|
+
details={"matching_layers": list(clean_keywords.intersection(dir_names))},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
hex_keywords = {"ports", "adapters", "domain", "application"}
|
|
42
|
+
if len(hex_keywords.intersection(dir_names)) >= 3:
|
|
43
|
+
return ArchitectureInfo(
|
|
44
|
+
pattern="Hexagonal Architecture",
|
|
45
|
+
confidence=0.90,
|
|
46
|
+
details={"matching_layers": list(hex_keywords.intersection(dir_names))},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
ddd_keywords = {"domain", "aggregates", "bounded_contexts", "value_objects", "repositories"}
|
|
50
|
+
if len(ddd_keywords.intersection(dir_names)) >= 3:
|
|
51
|
+
return ArchitectureInfo(
|
|
52
|
+
pattern="Domain Driven Design",
|
|
53
|
+
confidence=0.85,
|
|
54
|
+
details={"matching_elements": list(ddd_keywords.intersection(dir_names))},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
mvc_keywords = {"models", "views", "controllers"}
|
|
58
|
+
if mvc_keywords.issubset(dir_names) or {"model", "view", "controller"}.issubset(dir_names):
|
|
59
|
+
return ArchitectureInfo(
|
|
60
|
+
pattern="MVC",
|
|
61
|
+
confidence=0.95,
|
|
62
|
+
details={"matching_folders": ["models", "views", "controllers"]},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
layered_keywords = {"controllers", "services", "repositories", "routes"}
|
|
66
|
+
matches = layered_keywords.intersection(dir_names)
|
|
67
|
+
if len(matches) >= 2:
|
|
68
|
+
return ArchitectureInfo(
|
|
69
|
+
pattern="Layered Architecture",
|
|
70
|
+
confidence=0.85,
|
|
71
|
+
details={"layers": list(matches)},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
feature_keywords = {"features", "modules", "components"}
|
|
75
|
+
if len(feature_keywords.intersection(dir_names)) >= 1:
|
|
76
|
+
return ArchitectureInfo(
|
|
77
|
+
pattern="Feature-based",
|
|
78
|
+
confidence=0.75,
|
|
79
|
+
details={"matching_folders": list(feature_keywords.intersection(dir_names))},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if len([d for d in dir_names if "service" in d]) >= 2:
|
|
83
|
+
return ArchitectureInfo(
|
|
84
|
+
pattern="Microservices",
|
|
85
|
+
confidence=0.80,
|
|
86
|
+
details={"indicator": "multiple service directories"},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return ArchitectureInfo(
|
|
90
|
+
pattern="Modular / Standard Layout",
|
|
91
|
+
confidence=0.60,
|
|
92
|
+
details={"indicator": "standard package directory layout"},
|
|
93
|
+
)
|
treegetter/cache.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Thread-safe cache engine for repository scanning and intelligence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CacheManager:
|
|
12
|
+
"""Thread-safe cache manager supporting key-value operations and lazy get-or-set."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._cache: dict[str, Any] = {}
|
|
16
|
+
self._lock = threading.Lock()
|
|
17
|
+
|
|
18
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
19
|
+
"""Retrieve a cached value by key."""
|
|
20
|
+
with self._lock:
|
|
21
|
+
return self._cache.get(key, default)
|
|
22
|
+
|
|
23
|
+
def set(self, key: str, value: Any) -> None:
|
|
24
|
+
"""Store a key-value pair in cache."""
|
|
25
|
+
with self._lock:
|
|
26
|
+
self._cache[key] = value
|
|
27
|
+
|
|
28
|
+
def has(self, key: str) -> bool:
|
|
29
|
+
"""Check if a key exists in cache."""
|
|
30
|
+
with self._lock:
|
|
31
|
+
return key in self._cache
|
|
32
|
+
|
|
33
|
+
def get_or_compute(self, key: str, compute_func: Callable[[], T]) -> T:
|
|
34
|
+
"""Get cached value or compute and store it thread-safely."""
|
|
35
|
+
with self._lock:
|
|
36
|
+
if key in self._cache:
|
|
37
|
+
return self._cache[key]
|
|
38
|
+
|
|
39
|
+
value = compute_func()
|
|
40
|
+
|
|
41
|
+
with self._lock:
|
|
42
|
+
self._cache[key] = value
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
def invalidate(self, key: str) -> bool:
|
|
46
|
+
"""Remove a specific key from cache."""
|
|
47
|
+
with self._lock:
|
|
48
|
+
if key in self._cache:
|
|
49
|
+
del self._cache[key]
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def clear(self) -> None:
|
|
54
|
+
"""Clear all cached entries."""
|
|
55
|
+
with self._lock:
|
|
56
|
+
self._cache.clear()
|
|
57
|
+
|
|
58
|
+
def size(self) -> int:
|
|
59
|
+
"""Return total number of cached entries."""
|
|
60
|
+
with self._lock:
|
|
61
|
+
return len(self._cache)
|
treegetter/config.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Default configuration values and constants for the treegetter package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .models import TreeConfig
|
|
9
|
+
|
|
10
|
+
DEFAULT_IGNORE_DIRS: frozenset[str] = frozenset(
|
|
11
|
+
{
|
|
12
|
+
".git",
|
|
13
|
+
".svn",
|
|
14
|
+
".hg",
|
|
15
|
+
"__pycache__",
|
|
16
|
+
"node_modules",
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
"env",
|
|
20
|
+
".mypy_cache",
|
|
21
|
+
".pytest_cache",
|
|
22
|
+
".ruff_cache",
|
|
23
|
+
".idea",
|
|
24
|
+
".vscode",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
".tox",
|
|
28
|
+
"target",
|
|
29
|
+
"vendor",
|
|
30
|
+
".next",
|
|
31
|
+
".nuxt",
|
|
32
|
+
".turbo",
|
|
33
|
+
".cache",
|
|
34
|
+
"bin",
|
|
35
|
+
"obj",
|
|
36
|
+
".gradle",
|
|
37
|
+
".mvn",
|
|
38
|
+
".dart_tool",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
DEFAULT_IGNORE_PATTERNS: tuple[str, ...] = (
|
|
43
|
+
"*.pyc",
|
|
44
|
+
"*.pyo",
|
|
45
|
+
"*.egg-info",
|
|
46
|
+
"*.log",
|
|
47
|
+
"*.tmp",
|
|
48
|
+
"*.temp",
|
|
49
|
+
"*.swp",
|
|
50
|
+
"*.lock",
|
|
51
|
+
"package-lock.json",
|
|
52
|
+
"yarn.lock",
|
|
53
|
+
"pnpm-lock.yaml",
|
|
54
|
+
"Cargo.lock",
|
|
55
|
+
"poetry.lock",
|
|
56
|
+
"Pipfile.lock",
|
|
57
|
+
"composer.lock",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
SUPPORTED_FORMATS: tuple[str, ...] = ("unicode", "ascii", "markdown", "json")
|
|
61
|
+
|
|
62
|
+
EXTENSION_LANGUAGE_MAP: dict[str, str] = {
|
|
63
|
+
".py": "Python",
|
|
64
|
+
".pyw": "Python",
|
|
65
|
+
".js": "JavaScript",
|
|
66
|
+
".jsx": "JavaScript",
|
|
67
|
+
".mjs": "JavaScript",
|
|
68
|
+
".cjs": "JavaScript",
|
|
69
|
+
".ts": "TypeScript",
|
|
70
|
+
".tsx": "TypeScript",
|
|
71
|
+
".java": "Java",
|
|
72
|
+
".go": "Go",
|
|
73
|
+
".rs": "Rust",
|
|
74
|
+
".cpp": "C++",
|
|
75
|
+
".cxx": "C++",
|
|
76
|
+
".cc": "C++",
|
|
77
|
+
".hpp": "C++",
|
|
78
|
+
".h": "C++",
|
|
79
|
+
".c": "C",
|
|
80
|
+
".cs": "C#",
|
|
81
|
+
".php": "PHP",
|
|
82
|
+
".rb": "Ruby",
|
|
83
|
+
".html": "HTML",
|
|
84
|
+
".htm": "HTML",
|
|
85
|
+
".css": "CSS",
|
|
86
|
+
".scss": "CSS",
|
|
87
|
+
".sass": "CSS",
|
|
88
|
+
".less": "CSS",
|
|
89
|
+
".md": "Markdown",
|
|
90
|
+
".markdown": "Markdown",
|
|
91
|
+
".json": "JSON",
|
|
92
|
+
".yaml": "YAML",
|
|
93
|
+
".yml": "YAML",
|
|
94
|
+
".xml": "XML",
|
|
95
|
+
".dart": "Dart",
|
|
96
|
+
".sh": "Shell",
|
|
97
|
+
".bash": "Shell",
|
|
98
|
+
".zsh": "Shell",
|
|
99
|
+
".ps1": "PowerShell",
|
|
100
|
+
".sql": "SQL",
|
|
101
|
+
".dockerfile": "Dockerfile",
|
|
102
|
+
".toml": "TOML",
|
|
103
|
+
".env": "Config",
|
|
104
|
+
".ini": "Config",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
MIME_TYPE_MAP: dict[str, str] = {
|
|
108
|
+
".py": "text/x-python",
|
|
109
|
+
".js": "application/javascript",
|
|
110
|
+
".ts": "application/typescript",
|
|
111
|
+
".json": "application/json",
|
|
112
|
+
".html": "text/html",
|
|
113
|
+
".css": "text/css",
|
|
114
|
+
".md": "text/markdown",
|
|
115
|
+
".xml": "text/xml",
|
|
116
|
+
".yaml": "text/yaml",
|
|
117
|
+
".yml": "text/yaml",
|
|
118
|
+
".txt": "text/plain",
|
|
119
|
+
".png": "image/png",
|
|
120
|
+
".jpg": "image/jpeg",
|
|
121
|
+
".jpeg": "image/jpeg",
|
|
122
|
+
".svg": "image/svg+xml",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
PROJECT_MAP_PATTERNS: dict[str, list[str]] = {
|
|
126
|
+
"Controllers": ["*controller*", "*handler*", "*router*", "*endpoint*"],
|
|
127
|
+
"Services": ["*service*", "*usecase*", "*business*", "*logic*"],
|
|
128
|
+
"Repositories": ["*repository*", "*repo*", "*dao*", "*store*", "*db*"],
|
|
129
|
+
"Models": ["*model*", "*entity*", "*schema*", "*dto*", "*type*"],
|
|
130
|
+
"Routes": ["*route*", "*api*", "*url*", "*endpoint*"],
|
|
131
|
+
"Hooks": ["*hook*", "use*"],
|
|
132
|
+
"Components": ["*component*", "*view*", "*page*", "*screen*", "*widget*"],
|
|
133
|
+
"Tests": ["*test*", "*spec*", "test_*", "*_test.py", "*_test.go"],
|
|
134
|
+
"Utilities": ["*util*", "*helper*", "*tool*", "*common*", "*shared*"],
|
|
135
|
+
"Configuration": ["*config*", "*setting*", ".env*", "*.toml", "*.yaml", "*.json"],
|
|
136
|
+
"Documentation": ["*.md", "*doc*", "*readme*"],
|
|
137
|
+
"Assets": ["*asset*", "*image*", "*static*", "*public*", "*font*", "*icon*"],
|
|
138
|
+
"Scripts": ["*script*", "*build*", "*deploy*", "*.sh", "*.ps1"],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_default_config(root: Path, **overrides: Any) -> TreeConfig:
|
|
143
|
+
"""Build a :class:`TreeConfig` seeded with package defaults."""
|
|
144
|
+
defaults: dict[str, Any] = dict(
|
|
145
|
+
root=root,
|
|
146
|
+
max_depth=None,
|
|
147
|
+
include_hidden=False,
|
|
148
|
+
follow_symlinks=False,
|
|
149
|
+
show_size=False,
|
|
150
|
+
dirs_first=True,
|
|
151
|
+
output_format="unicode",
|
|
152
|
+
ignore_dirs=DEFAULT_IGNORE_DIRS,
|
|
153
|
+
ignore_patterns=DEFAULT_IGNORE_PATTERNS,
|
|
154
|
+
use_gitignore=False,
|
|
155
|
+
show_stats=False,
|
|
156
|
+
)
|
|
157
|
+
defaults.update(overrides)
|
|
158
|
+
return TreeConfig(**defaults)
|
treegetter/context.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""AI Context Builder generating compact, LLM-ready repository intelligence payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from .models import (
|
|
9
|
+
AIContext,
|
|
10
|
+
ArchitectureInfo,
|
|
11
|
+
DependencyInfo,
|
|
12
|
+
EntryPoint,
|
|
13
|
+
FileMetadata,
|
|
14
|
+
FrameworkInfo,
|
|
15
|
+
LanguageStat,
|
|
16
|
+
RepositorySummary,
|
|
17
|
+
)
|
|
18
|
+
from .tokenizer import TokenOptimizer
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AIContextBuilder:
|
|
22
|
+
"""Builds compact structured context for LLMs."""
|
|
23
|
+
|
|
24
|
+
def build_context(
|
|
25
|
+
self,
|
|
26
|
+
root: Path,
|
|
27
|
+
summary: RepositorySummary,
|
|
28
|
+
tree_str: str,
|
|
29
|
+
arch: ArchitectureInfo,
|
|
30
|
+
entrypoints: list[EntryPoint],
|
|
31
|
+
deps: DependencyInfo,
|
|
32
|
+
files: Sequence[FileMetadata],
|
|
33
|
+
stats: dict,
|
|
34
|
+
frameworks: list[FrameworkInfo],
|
|
35
|
+
languages: dict[str, LanguageStat],
|
|
36
|
+
max_tokens: int | None = None,
|
|
37
|
+
) -> AIContext:
|
|
38
|
+
"""Construct AIContext data model."""
|
|
39
|
+
project_map: dict[str, list[str]] = {
|
|
40
|
+
"Controllers": [],
|
|
41
|
+
"Services": [],
|
|
42
|
+
"Repositories": [],
|
|
43
|
+
"Models": [],
|
|
44
|
+
"Routes": [],
|
|
45
|
+
"Components": [],
|
|
46
|
+
"Tests": [],
|
|
47
|
+
"Utilities": [],
|
|
48
|
+
"Configuration": [],
|
|
49
|
+
"Documentation": [],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
important_files: list[dict[str, str]] = []
|
|
53
|
+
ignored_files: list[str] = [".git", "node_modules", "__pycache__", "dist", "build", ".venv"]
|
|
54
|
+
|
|
55
|
+
for f in files:
|
|
56
|
+
rel = self._to_rel_posix(f.path, root)
|
|
57
|
+
rel_lower = rel.lower()
|
|
58
|
+
|
|
59
|
+
if "controller" in rel_lower or "handler" in rel_lower:
|
|
60
|
+
project_map["Controllers"].append(rel)
|
|
61
|
+
elif "service" in rel_lower or "usecase" in rel_lower:
|
|
62
|
+
project_map["Services"].append(rel)
|
|
63
|
+
elif "repository" in rel_lower or "repo" in rel_lower or "store" in rel_lower:
|
|
64
|
+
project_map["Repositories"].append(rel)
|
|
65
|
+
elif "model" in rel_lower or "schema" in rel_lower or "entity" in rel_lower:
|
|
66
|
+
project_map["Models"].append(rel)
|
|
67
|
+
elif "route" in rel_lower or "api" in rel_lower:
|
|
68
|
+
project_map["Routes"].append(rel)
|
|
69
|
+
elif "component" in rel_lower or "view" in rel_lower:
|
|
70
|
+
project_map["Components"].append(rel)
|
|
71
|
+
elif "test" in rel_lower or "spec" in rel_lower:
|
|
72
|
+
project_map["Tests"].append(rel)
|
|
73
|
+
elif "util" in rel_lower or "helper" in rel_lower:
|
|
74
|
+
project_map["Utilities"].append(rel)
|
|
75
|
+
elif f.extension in (".toml", ".yaml", ".yml", ".json", ".env") or "config" in rel_lower:
|
|
76
|
+
project_map["Configuration"].append(rel)
|
|
77
|
+
elif f.extension in (".md", ".txt"):
|
|
78
|
+
project_map["Documentation"].append(rel)
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
f.name.lower().startswith("readme")
|
|
82
|
+
or f.name in ("package.json", "pyproject.toml", "Cargo.toml", "go.mod", "Dockerfile")
|
|
83
|
+
or any(ep.path == rel for ep in entrypoints[:3])
|
|
84
|
+
):
|
|
85
|
+
content_snippet = ""
|
|
86
|
+
if f.size < 50_000:
|
|
87
|
+
try:
|
|
88
|
+
content_snippet = f.path.read_text(encoding="utf-8", errors="ignore")[:500]
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
important_files.append({
|
|
93
|
+
"path": rel,
|
|
94
|
+
"language": f.language,
|
|
95
|
+
"snippet": content_snippet,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
context = AIContext(
|
|
99
|
+
summary=summary.to_dict(),
|
|
100
|
+
folder_tree=tree_str,
|
|
101
|
+
architecture=arch.to_dict(),
|
|
102
|
+
entrypoints=[ep.to_dict() for ep in entrypoints],
|
|
103
|
+
dependencies=deps.to_dict(),
|
|
104
|
+
project_map={k: v[:20] for k, v in project_map.items() if v},
|
|
105
|
+
important_files=important_files[:10],
|
|
106
|
+
statistics=stats,
|
|
107
|
+
framework=[fw.to_dict() for fw in frameworks],
|
|
108
|
+
languages={k: v.to_dict() for k, v in languages.items()},
|
|
109
|
+
ignored_files=ignored_files,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if max_tokens is not None:
|
|
113
|
+
tree_str_trimmed = TokenOptimizer.trim_to_token_limit(tree_str, int(max_tokens * 0.4))
|
|
114
|
+
context.folder_tree = tree_str_trimmed
|
|
115
|
+
|
|
116
|
+
return context
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _to_rel_posix(path: Path, root: Path) -> str:
|
|
120
|
+
try:
|
|
121
|
+
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
122
|
+
except (OSError, ValueError):
|
|
123
|
+
return path.name
|