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.
Files changed (44) hide show
  1. treegetter/__init__.py +64 -0
  2. treegetter/__main__.py +9 -0
  3. treegetter/api.py +146 -0
  4. treegetter/architecture.py +93 -0
  5. treegetter/cache.py +61 -0
  6. treegetter/config.py +158 -0
  7. treegetter/context.py +123 -0
  8. treegetter/dependency.py +280 -0
  9. treegetter/detector.py +41 -0
  10. treegetter/entrypoints.py +105 -0
  11. treegetter/exceptions.py +44 -0
  12. treegetter/exporters.py +169 -0
  13. treegetter/filters.py +139 -0
  14. treegetter/formatter.py +151 -0
  15. treegetter/language.py +62 -0
  16. treegetter/models.py +306 -0
  17. treegetter/plugins/__init__.py +55 -0
  18. treegetter/plugins/base.py +58 -0
  19. treegetter/plugins/csharp.py +31 -0
  20. treegetter/plugins/django.py +35 -0
  21. treegetter/plugins/fastapi.py +37 -0
  22. treegetter/plugins/flask.py +37 -0
  23. treegetter/plugins/flutter.py +35 -0
  24. treegetter/plugins/go.py +28 -0
  25. treegetter/plugins/java.py +33 -0
  26. treegetter/plugins/javascript.py +38 -0
  27. treegetter/plugins/nextjs.py +48 -0
  28. treegetter/plugins/php.py +28 -0
  29. treegetter/plugins/python.py +42 -0
  30. treegetter/plugins/react.py +47 -0
  31. treegetter/plugins/rust.py +28 -0
  32. treegetter/repository.py +258 -0
  33. treegetter/scanner.py +238 -0
  34. treegetter/search.py +121 -0
  35. treegetter/statistics.py +125 -0
  36. treegetter/summary.py +73 -0
  37. treegetter/tokenizer.py +43 -0
  38. treegetter/utils.py +136 -0
  39. treegetter-2.0.0.dist-info/METADATA +64 -0
  40. treegetter-2.0.0.dist-info/RECORD +44 -0
  41. treegetter-2.0.0.dist-info/WHEEL +5 -0
  42. treegetter-2.0.0.dist-info/entry_points.txt +2 -0
  43. treegetter-2.0.0.dist-info/licenses/LICENSE +21 -0
  44. 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
@@ -0,0 +1,9 @@
1
+ """CLI entrypoint for treegetter package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from .api import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
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