codeboarding 0.9.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.
- agents/__init__.py +0 -0
- agents/abstraction_agent.py +150 -0
- agents/agent.py +467 -0
- agents/agent_responses.py +363 -0
- agents/cluster_methods_mixin.py +281 -0
- agents/constants.py +13 -0
- agents/dependency_discovery.py +159 -0
- agents/details_agent.py +174 -0
- agents/llm_config.py +309 -0
- agents/meta_agent.py +105 -0
- agents/planner_agent.py +105 -0
- agents/prompts/__init__.py +85 -0
- agents/prompts/abstract_prompt_factory.py +63 -0
- agents/prompts/claude_prompts.py +381 -0
- agents/prompts/deepseek_prompts.py +389 -0
- agents/prompts/gemini_flash_prompts.py +362 -0
- agents/prompts/glm_prompts.py +407 -0
- agents/prompts/gpt_prompts.py +470 -0
- agents/prompts/kimi_prompts.py +400 -0
- agents/prompts/prompt_factory.py +179 -0
- agents/tools/__init__.py +8 -0
- agents/tools/base.py +96 -0
- agents/tools/get_external_deps.py +47 -0
- agents/tools/get_method_invocations.py +47 -0
- agents/tools/read_cfg.py +60 -0
- agents/tools/read_docs.py +132 -0
- agents/tools/read_file.py +90 -0
- agents/tools/read_file_structure.py +156 -0
- agents/tools/read_git_diff.py +131 -0
- agents/tools/read_packages.py +60 -0
- agents/tools/read_source.py +105 -0
- agents/tools/read_structure.py +49 -0
- agents/tools/toolkit.py +119 -0
- agents/validation.py +383 -0
- caching/__init__.py +4 -0
- caching/cache.py +29 -0
- caching/meta_cache.py +227 -0
- codeboarding-0.9.0.dist-info/METADATA +223 -0
- codeboarding-0.9.0.dist-info/RECORD +126 -0
- codeboarding-0.9.0.dist-info/WHEEL +5 -0
- codeboarding-0.9.0.dist-info/entry_points.txt +3 -0
- codeboarding-0.9.0.dist-info/licenses/LICENSE +21 -0
- codeboarding-0.9.0.dist-info/top_level.txt +18 -0
- core/__init__.py +101 -0
- core/plugin_loader.py +46 -0
- core/protocols.py +27 -0
- core/registry.py +46 -0
- diagram_analysis/__init__.py +4 -0
- diagram_analysis/analysis_json.py +346 -0
- diagram_analysis/diagram_generator.py +486 -0
- diagram_analysis/file_coverage.py +212 -0
- diagram_analysis/incremental/__init__.py +63 -0
- diagram_analysis/incremental/component_checker.py +236 -0
- diagram_analysis/incremental/file_manager.py +217 -0
- diagram_analysis/incremental/impact_analyzer.py +238 -0
- diagram_analysis/incremental/io_utils.py +281 -0
- diagram_analysis/incremental/models.py +72 -0
- diagram_analysis/incremental/path_patching.py +164 -0
- diagram_analysis/incremental/reexpansion.py +166 -0
- diagram_analysis/incremental/scoped_analysis.py +227 -0
- diagram_analysis/incremental/updater.py +464 -0
- diagram_analysis/incremental/validation.py +48 -0
- diagram_analysis/manifest.py +152 -0
- diagram_analysis/version.py +6 -0
- duckdb_crud.py +125 -0
- github_action.py +172 -0
- health/__init__.py +3 -0
- health/checks/__init__.py +11 -0
- health/checks/circular_deps.py +48 -0
- health/checks/cohesion.py +93 -0
- health/checks/coupling.py +140 -0
- health/checks/function_size.py +85 -0
- health/checks/god_class.py +167 -0
- health/checks/inheritance.py +104 -0
- health/checks/instability.py +77 -0
- health/checks/unused_code_diagnostics.py +338 -0
- health/config.py +172 -0
- health/constants.py +19 -0
- health/models.py +186 -0
- health/runner.py +236 -0
- install.py +518 -0
- logging_config.py +105 -0
- main.py +529 -0
- monitoring/__init__.py +12 -0
- monitoring/callbacks.py +163 -0
- monitoring/context.py +158 -0
- monitoring/mixin.py +16 -0
- monitoring/paths.py +47 -0
- monitoring/stats.py +50 -0
- monitoring/writers.py +172 -0
- output_generators/__init__.py +0 -0
- output_generators/html.py +163 -0
- output_generators/html_template.py +382 -0
- output_generators/markdown.py +140 -0
- output_generators/mdx.py +171 -0
- output_generators/sphinx.py +175 -0
- repo_utils/__init__.py +277 -0
- repo_utils/change_detector.py +289 -0
- repo_utils/errors.py +6 -0
- repo_utils/git_diff.py +74 -0
- repo_utils/ignore.py +341 -0
- static_analyzer/__init__.py +335 -0
- static_analyzer/analysis_cache.py +699 -0
- static_analyzer/analysis_result.py +269 -0
- static_analyzer/cluster_change_analyzer.py +391 -0
- static_analyzer/cluster_helpers.py +79 -0
- static_analyzer/constants.py +166 -0
- static_analyzer/git_diff_analyzer.py +224 -0
- static_analyzer/graph.py +746 -0
- static_analyzer/incremental_orchestrator.py +671 -0
- static_analyzer/java_config_scanner.py +232 -0
- static_analyzer/java_utils.py +227 -0
- static_analyzer/lsp_client/__init__.py +12 -0
- static_analyzer/lsp_client/client.py +1642 -0
- static_analyzer/lsp_client/diagnostics.py +62 -0
- static_analyzer/lsp_client/java_client.py +517 -0
- static_analyzer/lsp_client/language_settings.py +97 -0
- static_analyzer/lsp_client/typescript_client.py +235 -0
- static_analyzer/programming_language.py +152 -0
- static_analyzer/reference_resolve_mixin.py +166 -0
- static_analyzer/scanner.py +95 -0
- static_analyzer/typescript_config_scanner.py +54 -0
- tool_registry.py +433 -0
- user_config.py +134 -0
- utils.py +56 -0
- vscode_constants.py +124 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rename-aware change detection using git.
|
|
3
|
+
|
|
4
|
+
This module provides enhanced change detection that properly tracks:
|
|
5
|
+
- File renames (with similarity percentage)
|
|
6
|
+
- File modifications
|
|
7
|
+
- Added/deleted files
|
|
8
|
+
- Copy detection
|
|
9
|
+
|
|
10
|
+
Uses `git diff --name-status -M -C` for accurate rename tracking.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import subprocess
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ChangeType(Enum):
|
|
23
|
+
"""Git change status types."""
|
|
24
|
+
|
|
25
|
+
ADDED = "A"
|
|
26
|
+
COPIED = "C"
|
|
27
|
+
DELETED = "D"
|
|
28
|
+
MODIFIED = "M"
|
|
29
|
+
RENAMED = "R"
|
|
30
|
+
TYPE_CHANGED = "T"
|
|
31
|
+
UNMERGED = "U"
|
|
32
|
+
UNKNOWN = "X"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DetectedChange:
|
|
37
|
+
"""A detected file change with rename tracking."""
|
|
38
|
+
|
|
39
|
+
change_type: ChangeType
|
|
40
|
+
file_path: str # Current/new path
|
|
41
|
+
old_path: str | None = None # For renames/copies: the original path
|
|
42
|
+
similarity: int | None = None # For renames/copies: 0-100%
|
|
43
|
+
|
|
44
|
+
def is_rename(self) -> bool:
|
|
45
|
+
return self.change_type == ChangeType.RENAMED
|
|
46
|
+
|
|
47
|
+
def is_content_change(self) -> bool:
|
|
48
|
+
"""Returns True if file content was modified (not just metadata/rename)."""
|
|
49
|
+
return self.change_type in (ChangeType.MODIFIED, ChangeType.ADDED)
|
|
50
|
+
|
|
51
|
+
def is_structural(self) -> bool:
|
|
52
|
+
"""Returns True if this affects file existence (add/delete)."""
|
|
53
|
+
return self.change_type in (ChangeType.ADDED, ChangeType.DELETED)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ChangeSet:
|
|
58
|
+
"""Collection of detected changes with helper methods."""
|
|
59
|
+
|
|
60
|
+
changes: list[DetectedChange] = field(default_factory=list)
|
|
61
|
+
base_ref: str = ""
|
|
62
|
+
target_ref: str = "HEAD"
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def renames(self) -> dict[str, str]:
|
|
66
|
+
"""Get rename mapping: old_path -> new_path."""
|
|
67
|
+
return {c.old_path: c.file_path for c in self.changes if c.is_rename() and c.old_path}
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def modified_files(self) -> list[str]:
|
|
71
|
+
"""Get list of modified file paths."""
|
|
72
|
+
return [c.file_path for c in self.changes if c.change_type == ChangeType.MODIFIED]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def added_files(self) -> list[str]:
|
|
76
|
+
"""Get list of added file paths."""
|
|
77
|
+
return [c.file_path for c in self.changes if c.change_type == ChangeType.ADDED]
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def deleted_files(self) -> list[str]:
|
|
81
|
+
"""Get list of deleted file paths."""
|
|
82
|
+
return [c.file_path for c in self.changes if c.change_type == ChangeType.DELETED]
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def all_affected_files(self) -> set[str]:
|
|
86
|
+
"""Get all files that were affected (current paths)."""
|
|
87
|
+
return {c.file_path for c in self.changes}
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def all_old_paths(self) -> set[str]:
|
|
91
|
+
"""Get all old paths (for renames)."""
|
|
92
|
+
return {c.old_path for c in self.changes if c.old_path}
|
|
93
|
+
|
|
94
|
+
def is_empty(self) -> bool:
|
|
95
|
+
return len(self.changes) == 0
|
|
96
|
+
|
|
97
|
+
def has_structural_changes(self) -> bool:
|
|
98
|
+
"""Returns True if any files were added or deleted."""
|
|
99
|
+
return any(c.is_structural() for c in self.changes)
|
|
100
|
+
|
|
101
|
+
def has_only_renames(self) -> bool:
|
|
102
|
+
"""Returns True if all changes are pure renames."""
|
|
103
|
+
return all(c.is_rename() for c in self.changes) and len(self.changes) > 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def detect_changes(
|
|
107
|
+
repo_dir: Path,
|
|
108
|
+
base_ref: str,
|
|
109
|
+
target_ref: str = "HEAD",
|
|
110
|
+
exclude_patterns: list[str] | None = None,
|
|
111
|
+
fetch_missing_refs: bool = True,
|
|
112
|
+
) -> ChangeSet:
|
|
113
|
+
"""
|
|
114
|
+
Detect file changes between two refs using rename-aware git diff.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
repo_dir: Path to the git repository
|
|
118
|
+
base_ref: Base reference (commit hash, tag, or branch)
|
|
119
|
+
target_ref: Target reference (default: HEAD, includes working tree)
|
|
120
|
+
exclude_patterns: List of path patterns to exclude (e.g., [".codeboarding/"])
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
ChangeSet with all detected changes
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
changes = detect_changes(repo_path, "abc1234")
|
|
127
|
+
for rename_old, rename_new in changes.renames.items():
|
|
128
|
+
print(f"Renamed: {rename_old} -> {rename_new}")
|
|
129
|
+
"""
|
|
130
|
+
changes: list[DetectedChange] = []
|
|
131
|
+
|
|
132
|
+
# Default exclusions for analysis output directories
|
|
133
|
+
if exclude_patterns is None:
|
|
134
|
+
exclude_patterns = [".codeboarding/", ".codeboarding\\"]
|
|
135
|
+
|
|
136
|
+
cmd = [
|
|
137
|
+
"git",
|
|
138
|
+
"diff",
|
|
139
|
+
"--name-status",
|
|
140
|
+
"-M",
|
|
141
|
+
"-C",
|
|
142
|
+
"--find-renames=50%",
|
|
143
|
+
base_ref,
|
|
144
|
+
target_ref,
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
def _run_diff() -> subprocess.CompletedProcess:
|
|
148
|
+
return subprocess.run(
|
|
149
|
+
cmd,
|
|
150
|
+
cwd=repo_dir,
|
|
151
|
+
capture_output=True,
|
|
152
|
+
text=True,
|
|
153
|
+
check=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = _run_diff()
|
|
158
|
+
except subprocess.CalledProcessError as e:
|
|
159
|
+
stderr_lower = (e.stderr or "").lower()
|
|
160
|
+
if fetch_missing_refs and "bad object" in stderr_lower:
|
|
161
|
+
logger.warning("Git diff failed due to missing ref (%s); fetching refs and retrying once", e.stderr.strip())
|
|
162
|
+
try:
|
|
163
|
+
subprocess.run(
|
|
164
|
+
["git", "fetch", "--all", "--prune", "--tags"],
|
|
165
|
+
cwd=repo_dir,
|
|
166
|
+
capture_output=True,
|
|
167
|
+
text=True,
|
|
168
|
+
check=True,
|
|
169
|
+
)
|
|
170
|
+
result = _run_diff()
|
|
171
|
+
except subprocess.CalledProcessError as fetch_err:
|
|
172
|
+
logger.error(f"Git fetch/diff retry failed: {fetch_err.stderr}")
|
|
173
|
+
return ChangeSet(changes=changes, base_ref=base_ref, target_ref=target_ref)
|
|
174
|
+
else:
|
|
175
|
+
logger.error(f"Git diff failed: {e.stderr}")
|
|
176
|
+
return ChangeSet(changes=changes, base_ref=base_ref, target_ref=target_ref)
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
logger.error("Git not found in PATH")
|
|
179
|
+
return ChangeSet(changes=changes, base_ref=base_ref, target_ref=target_ref)
|
|
180
|
+
|
|
181
|
+
for line in result.stdout.strip().split("\n"):
|
|
182
|
+
if not line:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
change = _parse_status_line(line)
|
|
186
|
+
if change:
|
|
187
|
+
# Skip excluded patterns
|
|
188
|
+
should_skip = False
|
|
189
|
+
for pattern in exclude_patterns:
|
|
190
|
+
if change.file_path.startswith(pattern):
|
|
191
|
+
should_skip = True
|
|
192
|
+
break
|
|
193
|
+
if change.old_path and change.old_path.startswith(pattern):
|
|
194
|
+
should_skip = True
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if should_skip:
|
|
198
|
+
logger.debug(f"Skipping excluded path: {change.file_path}")
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
changes.append(change)
|
|
202
|
+
logger.debug(f"Detected change: {change.change_type.name} {change.file_path}")
|
|
203
|
+
|
|
204
|
+
return ChangeSet(changes=changes, base_ref=base_ref, target_ref=target_ref)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def detect_changes_from_commit(repo_dir: Path, base_commit: str) -> ChangeSet:
|
|
208
|
+
"""
|
|
209
|
+
Detect changes from a specific commit to current working tree.
|
|
210
|
+
|
|
211
|
+
This includes both committed and uncommitted changes.
|
|
212
|
+
"""
|
|
213
|
+
return detect_changes(repo_dir, base_commit, "HEAD")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def detect_uncommitted_changes(repo_dir: Path) -> ChangeSet:
|
|
217
|
+
"""
|
|
218
|
+
Detect only uncommitted changes (staged + unstaged).
|
|
219
|
+
"""
|
|
220
|
+
return detect_changes(repo_dir, "HEAD", "")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _parse_status_line(line: str) -> DetectedChange | None:
|
|
224
|
+
"""
|
|
225
|
+
Parse a git diff --name-status line.
|
|
226
|
+
|
|
227
|
+
Format examples:
|
|
228
|
+
M file.py # Modified
|
|
229
|
+
A newfile.py # Added
|
|
230
|
+
D oldfile.py # Deleted
|
|
231
|
+
R100 old.py new.py # Renamed (100% similar)
|
|
232
|
+
R075 old.py new.py # Renamed (75% similar)
|
|
233
|
+
C100 src.py copy.py # Copied
|
|
234
|
+
|
|
235
|
+
Returns DetectedChange or None if parsing fails.
|
|
236
|
+
"""
|
|
237
|
+
parts = line.split("\t")
|
|
238
|
+
if len(parts) < 2:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
status = parts[0]
|
|
242
|
+
status_char = status[0].upper()
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
change_type = ChangeType(status_char)
|
|
246
|
+
except ValueError:
|
|
247
|
+
logger.warning(f"Unknown git status: {status_char}")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
# Handle renames and copies (have similarity percentage and two paths)
|
|
251
|
+
if change_type in (ChangeType.RENAMED, ChangeType.COPIED):
|
|
252
|
+
if len(parts) < 3:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
# Extract similarity percentage (e.g., "R100" -> 100)
|
|
256
|
+
similarity = None
|
|
257
|
+
if len(status) > 1:
|
|
258
|
+
try:
|
|
259
|
+
similarity = int(status[1:])
|
|
260
|
+
except ValueError:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
return DetectedChange(
|
|
264
|
+
change_type=change_type,
|
|
265
|
+
file_path=parts[2], # New path
|
|
266
|
+
old_path=parts[1], # Old path
|
|
267
|
+
similarity=similarity,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Simple change (A, M, D, T)
|
|
271
|
+
return DetectedChange(
|
|
272
|
+
change_type=change_type,
|
|
273
|
+
file_path=parts[1],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_current_commit(repo_dir: Path) -> str | None:
|
|
278
|
+
"""Get the current HEAD commit hash."""
|
|
279
|
+
try:
|
|
280
|
+
result = subprocess.run(
|
|
281
|
+
["git", "rev-parse", "HEAD"],
|
|
282
|
+
cwd=repo_dir,
|
|
283
|
+
capture_output=True,
|
|
284
|
+
text=True,
|
|
285
|
+
check=True,
|
|
286
|
+
)
|
|
287
|
+
return result.stdout.strip()
|
|
288
|
+
except subprocess.CalledProcessError:
|
|
289
|
+
return None
|
repo_utils/errors.py
ADDED
repo_utils/git_diff.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class FileChange:
|
|
8
|
+
"""
|
|
9
|
+
Container for the changes made to a single file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
filename: str
|
|
13
|
+
additions: int
|
|
14
|
+
deletions: int
|
|
15
|
+
added_lines: list[str] = field(default_factory=list)
|
|
16
|
+
removed_lines: list[str] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
def llm_str(self):
|
|
19
|
+
"""
|
|
20
|
+
Returns a string representation of the file change suitable for LLM processing.
|
|
21
|
+
"""
|
|
22
|
+
return f"File: {self.filename}, Added lines: +{self.additions}, Removed lines: -{self.deletions}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_git_diff(repo_dir: Path, version: str) -> list[FileChange]:
|
|
26
|
+
"""
|
|
27
|
+
Get the git diff between a specific version and the current working tree (uncommitted changes included).
|
|
28
|
+
|
|
29
|
+
:param repo_dir: Path to the repository directory.
|
|
30
|
+
:param version: The commit hash or tag to compare against.
|
|
31
|
+
:return: A list of FileChange objects describing the differences.
|
|
32
|
+
"""
|
|
33
|
+
changes: list[FileChange] = []
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from git import Repo
|
|
37
|
+
|
|
38
|
+
repo = Repo(repo_dir)
|
|
39
|
+
|
|
40
|
+
# Compare the specified version to the working tree (including staged + unstaged changes)
|
|
41
|
+
diff_index = repo.git.diff(version, "--patch")
|
|
42
|
+
|
|
43
|
+
# Group diff by file using parsing logic
|
|
44
|
+
current_file = None
|
|
45
|
+
added: list[str] = []
|
|
46
|
+
removed: list[str] = []
|
|
47
|
+
for line in diff_index.splitlines():
|
|
48
|
+
if line.startswith("diff --git"):
|
|
49
|
+
# Save previous file change, if any
|
|
50
|
+
if current_file:
|
|
51
|
+
changes.append(current_file)
|
|
52
|
+
# Start a new file
|
|
53
|
+
added, removed = [], []
|
|
54
|
+
filename = line.split(" b/")[-1]
|
|
55
|
+
current_file = FileChange(filename=filename, additions=0, deletions=0)
|
|
56
|
+
elif line.startswith("+++ ") or line.startswith("--- ") or line.startswith("@@"):
|
|
57
|
+
continue
|
|
58
|
+
elif line.startswith("+"):
|
|
59
|
+
added.append(line[1:])
|
|
60
|
+
elif line.startswith("-"):
|
|
61
|
+
removed.append(line[1:])
|
|
62
|
+
if current_file:
|
|
63
|
+
current_file.additions = len(added)
|
|
64
|
+
current_file.deletions = len(removed)
|
|
65
|
+
current_file.added_lines = added
|
|
66
|
+
current_file.removed_lines = removed
|
|
67
|
+
|
|
68
|
+
# Append the last file if it exists
|
|
69
|
+
if current_file:
|
|
70
|
+
changes.append(current_file)
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logging.error(f"Error obtaining git diff: {e}")
|
|
74
|
+
return changes
|