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.
Files changed (126) hide show
  1. agents/__init__.py +0 -0
  2. agents/abstraction_agent.py +150 -0
  3. agents/agent.py +467 -0
  4. agents/agent_responses.py +363 -0
  5. agents/cluster_methods_mixin.py +281 -0
  6. agents/constants.py +13 -0
  7. agents/dependency_discovery.py +159 -0
  8. agents/details_agent.py +174 -0
  9. agents/llm_config.py +309 -0
  10. agents/meta_agent.py +105 -0
  11. agents/planner_agent.py +105 -0
  12. agents/prompts/__init__.py +85 -0
  13. agents/prompts/abstract_prompt_factory.py +63 -0
  14. agents/prompts/claude_prompts.py +381 -0
  15. agents/prompts/deepseek_prompts.py +389 -0
  16. agents/prompts/gemini_flash_prompts.py +362 -0
  17. agents/prompts/glm_prompts.py +407 -0
  18. agents/prompts/gpt_prompts.py +470 -0
  19. agents/prompts/kimi_prompts.py +400 -0
  20. agents/prompts/prompt_factory.py +179 -0
  21. agents/tools/__init__.py +8 -0
  22. agents/tools/base.py +96 -0
  23. agents/tools/get_external_deps.py +47 -0
  24. agents/tools/get_method_invocations.py +47 -0
  25. agents/tools/read_cfg.py +60 -0
  26. agents/tools/read_docs.py +132 -0
  27. agents/tools/read_file.py +90 -0
  28. agents/tools/read_file_structure.py +156 -0
  29. agents/tools/read_git_diff.py +131 -0
  30. agents/tools/read_packages.py +60 -0
  31. agents/tools/read_source.py +105 -0
  32. agents/tools/read_structure.py +49 -0
  33. agents/tools/toolkit.py +119 -0
  34. agents/validation.py +383 -0
  35. caching/__init__.py +4 -0
  36. caching/cache.py +29 -0
  37. caching/meta_cache.py +227 -0
  38. codeboarding-0.9.0.dist-info/METADATA +223 -0
  39. codeboarding-0.9.0.dist-info/RECORD +126 -0
  40. codeboarding-0.9.0.dist-info/WHEEL +5 -0
  41. codeboarding-0.9.0.dist-info/entry_points.txt +3 -0
  42. codeboarding-0.9.0.dist-info/licenses/LICENSE +21 -0
  43. codeboarding-0.9.0.dist-info/top_level.txt +18 -0
  44. core/__init__.py +101 -0
  45. core/plugin_loader.py +46 -0
  46. core/protocols.py +27 -0
  47. core/registry.py +46 -0
  48. diagram_analysis/__init__.py +4 -0
  49. diagram_analysis/analysis_json.py +346 -0
  50. diagram_analysis/diagram_generator.py +486 -0
  51. diagram_analysis/file_coverage.py +212 -0
  52. diagram_analysis/incremental/__init__.py +63 -0
  53. diagram_analysis/incremental/component_checker.py +236 -0
  54. diagram_analysis/incremental/file_manager.py +217 -0
  55. diagram_analysis/incremental/impact_analyzer.py +238 -0
  56. diagram_analysis/incremental/io_utils.py +281 -0
  57. diagram_analysis/incremental/models.py +72 -0
  58. diagram_analysis/incremental/path_patching.py +164 -0
  59. diagram_analysis/incremental/reexpansion.py +166 -0
  60. diagram_analysis/incremental/scoped_analysis.py +227 -0
  61. diagram_analysis/incremental/updater.py +464 -0
  62. diagram_analysis/incremental/validation.py +48 -0
  63. diagram_analysis/manifest.py +152 -0
  64. diagram_analysis/version.py +6 -0
  65. duckdb_crud.py +125 -0
  66. github_action.py +172 -0
  67. health/__init__.py +3 -0
  68. health/checks/__init__.py +11 -0
  69. health/checks/circular_deps.py +48 -0
  70. health/checks/cohesion.py +93 -0
  71. health/checks/coupling.py +140 -0
  72. health/checks/function_size.py +85 -0
  73. health/checks/god_class.py +167 -0
  74. health/checks/inheritance.py +104 -0
  75. health/checks/instability.py +77 -0
  76. health/checks/unused_code_diagnostics.py +338 -0
  77. health/config.py +172 -0
  78. health/constants.py +19 -0
  79. health/models.py +186 -0
  80. health/runner.py +236 -0
  81. install.py +518 -0
  82. logging_config.py +105 -0
  83. main.py +529 -0
  84. monitoring/__init__.py +12 -0
  85. monitoring/callbacks.py +163 -0
  86. monitoring/context.py +158 -0
  87. monitoring/mixin.py +16 -0
  88. monitoring/paths.py +47 -0
  89. monitoring/stats.py +50 -0
  90. monitoring/writers.py +172 -0
  91. output_generators/__init__.py +0 -0
  92. output_generators/html.py +163 -0
  93. output_generators/html_template.py +382 -0
  94. output_generators/markdown.py +140 -0
  95. output_generators/mdx.py +171 -0
  96. output_generators/sphinx.py +175 -0
  97. repo_utils/__init__.py +277 -0
  98. repo_utils/change_detector.py +289 -0
  99. repo_utils/errors.py +6 -0
  100. repo_utils/git_diff.py +74 -0
  101. repo_utils/ignore.py +341 -0
  102. static_analyzer/__init__.py +335 -0
  103. static_analyzer/analysis_cache.py +699 -0
  104. static_analyzer/analysis_result.py +269 -0
  105. static_analyzer/cluster_change_analyzer.py +391 -0
  106. static_analyzer/cluster_helpers.py +79 -0
  107. static_analyzer/constants.py +166 -0
  108. static_analyzer/git_diff_analyzer.py +224 -0
  109. static_analyzer/graph.py +746 -0
  110. static_analyzer/incremental_orchestrator.py +671 -0
  111. static_analyzer/java_config_scanner.py +232 -0
  112. static_analyzer/java_utils.py +227 -0
  113. static_analyzer/lsp_client/__init__.py +12 -0
  114. static_analyzer/lsp_client/client.py +1642 -0
  115. static_analyzer/lsp_client/diagnostics.py +62 -0
  116. static_analyzer/lsp_client/java_client.py +517 -0
  117. static_analyzer/lsp_client/language_settings.py +97 -0
  118. static_analyzer/lsp_client/typescript_client.py +235 -0
  119. static_analyzer/programming_language.py +152 -0
  120. static_analyzer/reference_resolve_mixin.py +166 -0
  121. static_analyzer/scanner.py +95 -0
  122. static_analyzer/typescript_config_scanner.py +54 -0
  123. tool_registry.py +433 -0
  124. user_config.py +134 -0
  125. utils.py +56 -0
  126. vscode_constants.py +124 -0
@@ -0,0 +1,346 @@
1
+ import logging
2
+ from datetime import datetime, timezone
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from agents.agent_responses import Component, Relation, AnalysisInsights, assign_component_ids
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class RelationJson(Relation):
12
+ """Relation subclass that includes src_id/dst_id in JSON serialization."""
13
+
14
+ src_id: str = Field(default="", description="Component ID of the source.")
15
+ dst_id: str = Field(default="", description="Component ID of the destination.")
16
+
17
+
18
+ class ComponentJson(Component):
19
+ # Override to include in JSON serialization (parent has exclude=True)
20
+ component_id: str = Field(description="Deterministic unique identifier for this component.")
21
+ can_expand: bool = Field(
22
+ description="Whether the component can be expanded in detail or not.",
23
+ default=False,
24
+ )
25
+ assigned_files: list[str] = Field(
26
+ description="A list of source code names of files assigned to the component.",
27
+ default_factory=list,
28
+ )
29
+ # Nested sub-analysis for expanded components
30
+ components: list["ComponentJson"] | None = Field(
31
+ description="Sub-components if expanded, None otherwise.", default=None
32
+ )
33
+ components_relations: list[RelationJson] | None = Field(
34
+ description="Relations among sub-components if expanded, None otherwise.",
35
+ default=None,
36
+ )
37
+
38
+
39
+ class NotAnalyzedFile(BaseModel):
40
+ path: str = Field(description="Relative path of the file.")
41
+ reason: str = Field(description="Exclusion reason for the file.")
42
+
43
+
44
+ class FileCoverageSummary(BaseModel):
45
+ total_files: int = Field(description="Total number of text files in the repository.")
46
+ analyzed: int = Field(description="Number of files included in the analysis.")
47
+ not_analyzed: int = Field(description="Number of files excluded from the analysis.")
48
+ not_analyzed_by_reason: dict[str, int] = Field(
49
+ default_factory=dict, description="Count of excluded files grouped by reason."
50
+ )
51
+
52
+
53
+ class FileCoverageReport(BaseModel):
54
+ version: int = Field(default=1, description="Schema version of the file coverage report.")
55
+ generated_at: str = Field(description="ISO timestamp of when the report was generated.")
56
+ analyzed_files: list[str] = Field(description="List of analyzed file paths.")
57
+ not_analyzed_files: list[NotAnalyzedFile] = Field(description="List of excluded files with optional reasons.")
58
+ summary: FileCoverageSummary = Field(description="Aggregated coverage counts.")
59
+
60
+
61
+ class AnalysisMetadata(BaseModel):
62
+ generated_at: str = Field(description="ISO timestamp of when the analysis was generated.")
63
+ repo_name: str = Field(description="Name of the analyzed repository.")
64
+ depth_level: int = Field(description="Maximum depth level of the analysis.")
65
+ file_coverage_summary: FileCoverageSummary = Field(
66
+ default_factory=lambda: FileCoverageSummary(
67
+ total_files=0, analyzed=0, not_analyzed=0, not_analyzed_by_reason={}
68
+ ),
69
+ description="Lightweight file coverage counts.",
70
+ )
71
+
72
+
73
+ class UnifiedAnalysisJson(BaseModel):
74
+ metadata: AnalysisMetadata = Field(description="Metadata about the analysis run.")
75
+ description: str = Field(
76
+ description="One paragraph explaining the functionality which is represented by this graph."
77
+ )
78
+ components: list[ComponentJson] = Field(description="List of the components identified in the project.")
79
+ components_relations: list[RelationJson] = Field(description="List of relations among the components.")
80
+
81
+
82
+ def from_component_to_json_component(
83
+ component: Component,
84
+ expandable_components: list[Component],
85
+ sub_analyses: dict[str, tuple[AnalysisInsights, list[Component]]] | None = None,
86
+ processed_ids: set[str] | None = None,
87
+ ) -> ComponentJson:
88
+ """Convert a Component to a ComponentJson, optionally nesting sub-analysis data."""
89
+ if processed_ids is None:
90
+ processed_ids = set()
91
+
92
+ component_id_val: str = component.component_id
93
+ if component_id_val in processed_ids:
94
+ logger.warning(f"Component {component.name} (ID: {component_id_val}) already processed, skipping expansion")
95
+ can_expand = False
96
+ else:
97
+ processed_ids.add(component_id_val)
98
+ can_expand = any(c.component_id == component.component_id for c in expandable_components)
99
+
100
+ nested_components: list[ComponentJson] | None = None
101
+ nested_relations: list[RelationJson] | None = None
102
+
103
+ if can_expand and sub_analyses and component.component_id in sub_analyses:
104
+ sub_analysis, sub_expandable = sub_analyses[component.component_id]
105
+ nested_components = [
106
+ from_component_to_json_component(c, sub_expandable, sub_analyses, processed_ids)
107
+ for c in sub_analysis.components
108
+ ]
109
+ nested_relations = [
110
+ RelationJson(
111
+ relation=r.relation,
112
+ src_name=r.src_name,
113
+ dst_name=r.dst_name,
114
+ src_id=r.src_id,
115
+ dst_id=r.dst_id,
116
+ )
117
+ for r in sub_analysis.components_relations
118
+ ]
119
+
120
+ return ComponentJson(
121
+ name=component.name,
122
+ component_id=component.component_id,
123
+ description=component.description,
124
+ key_entities=component.key_entities,
125
+ source_cluster_ids=component.source_cluster_ids,
126
+ assigned_files=component.assigned_files,
127
+ can_expand=can_expand,
128
+ components=nested_components,
129
+ components_relations=nested_relations,
130
+ )
131
+
132
+
133
+ def from_analysis_to_json(
134
+ analysis: AnalysisInsights,
135
+ expandable_components: list[Component],
136
+ sub_analyses: dict[str, tuple[AnalysisInsights, list[Component]]] | None = None,
137
+ ) -> str:
138
+ """Convert an AnalysisInsights to a flat JSON string (legacy-compatible, no metadata wrapper)."""
139
+ components_json = [
140
+ from_component_to_json_component(c, expandable_components, sub_analyses, None) for c in analysis.components
141
+ ]
142
+ # Build a dict matching the old AnalysisInsightsJson shape but with nested components
143
+ relations_json = [
144
+ RelationJson(
145
+ relation=r.relation,
146
+ src_name=r.src_name,
147
+ dst_name=r.dst_name,
148
+ src_id=r.src_id,
149
+ dst_id=r.dst_id,
150
+ )
151
+ for r in analysis.components_relations
152
+ ]
153
+ data = {
154
+ "description": analysis.description,
155
+ "components": [c.model_dump(exclude_none=True) for c in components_json],
156
+ "components_relations": [r.model_dump() for r in relations_json],
157
+ }
158
+ import json
159
+
160
+ return json.dumps(data, indent=2)
161
+
162
+
163
+ def _compute_depth_level(
164
+ sub_analyses: dict[str, tuple[AnalysisInsights, list[Component]]] | None,
165
+ ) -> int:
166
+ """Compute the maximum depth level from the sub_analyses structure.
167
+
168
+ Returns 1 if there are no sub-analyses (root only), 2 if there is one level of
169
+ sub-analyses, etc. Recursively traverses nested sub-analyses to find true max depth.
170
+ """
171
+ if not sub_analyses:
172
+ return 1
173
+
174
+ def get_depth(analysis: AnalysisInsights, visited: set[str]) -> int:
175
+ """Recursively compute depth for a sub-analysis."""
176
+ max_depth = 1
177
+ for comp in analysis.components:
178
+ if comp.component_id in sub_analyses and comp.component_id not in visited:
179
+ visited.add(comp.component_id)
180
+ sub_analysis, _ = sub_analyses[comp.component_id]
181
+ child_depth = 1 + get_depth(sub_analysis, visited)
182
+ max_depth = max(max_depth, child_depth)
183
+ visited.remove(comp.component_id)
184
+ return max_depth
185
+
186
+ max_depth = 1
187
+ for cid, (sub_analysis, _) in sub_analyses.items():
188
+ # Only compute depth for root-level sub-analyses (not referenced by others)
189
+ is_root_level = True
190
+ for other_cid, (other_analysis, _) in sub_analyses.items():
191
+ if other_cid != cid:
192
+ for comp in other_analysis.components:
193
+ if comp.component_id == cid:
194
+ is_root_level = False
195
+ break
196
+ if not is_root_level:
197
+ break
198
+
199
+ if is_root_level:
200
+ visited = {cid}
201
+ depth = 1 + get_depth(sub_analysis, visited)
202
+ max_depth = max(max_depth, depth)
203
+
204
+ return max_depth
205
+
206
+
207
+ def build_unified_analysis_json(
208
+ analysis: AnalysisInsights,
209
+ expandable_components: list[Component],
210
+ repo_name: str,
211
+ sub_analyses: dict[str, tuple[AnalysisInsights, list[Component]]] | None = None,
212
+ file_coverage_summary: FileCoverageSummary | None = None,
213
+ ) -> str:
214
+ """Build the full unified analysis JSON with metadata and nested sub-analyses.
215
+
216
+ The depth_level metadata is computed automatically from the sub_analyses structure
217
+ if not provided explicitly.
218
+ """
219
+ components_json = [
220
+ from_component_to_json_component(c, expandable_components, sub_analyses, None) for c in analysis.components
221
+ ]
222
+
223
+ # Use default summary if none provided
224
+ if file_coverage_summary is None:
225
+ summary = FileCoverageSummary(total_files=0, analyzed=0, not_analyzed=0, not_analyzed_by_reason={})
226
+ else:
227
+ summary = file_coverage_summary
228
+
229
+ relations_json = [
230
+ RelationJson(
231
+ relation=r.relation,
232
+ src_name=r.src_name,
233
+ dst_name=r.dst_name,
234
+ src_id=r.src_id,
235
+ dst_id=r.dst_id,
236
+ )
237
+ for r in analysis.components_relations
238
+ ]
239
+ unified = UnifiedAnalysisJson(
240
+ metadata=AnalysisMetadata(
241
+ generated_at=datetime.now(timezone.utc).isoformat(),
242
+ repo_name=repo_name,
243
+ depth_level=_compute_depth_level(sub_analyses),
244
+ file_coverage_summary=summary,
245
+ ),
246
+ description=analysis.description,
247
+ components=components_json,
248
+ components_relations=relations_json,
249
+ )
250
+ return unified.model_dump_json(indent=2, exclude_none=True)
251
+
252
+
253
+ def parse_unified_analysis(
254
+ data: dict,
255
+ ) -> tuple[AnalysisInsights, dict[str, AnalysisInsights]]:
256
+ """Parse a unified analysis JSON dict into root AnalysisInsights and sub-analyses.
257
+
258
+ Returns:
259
+ (root_analysis, sub_analyses_dict) where sub_analyses_dict maps component_id
260
+ to its nested AnalysisInsights.
261
+ """
262
+ sub_analyses: dict[str, AnalysisInsights] = {}
263
+ root_analysis = _extract_analysis_recursive(data, sub_analyses)
264
+
265
+ # Backward compatibility: if components lack component_id, assign deterministically
266
+ if any(c.component_id == "" for c in root_analysis.components):
267
+ _assign_ids_and_rekey(root_analysis, sub_analyses)
268
+
269
+ return root_analysis, sub_analyses
270
+
271
+
272
+ def _assign_ids_and_rekey(
273
+ root_analysis: AnalysisInsights,
274
+ sub_analyses: dict[str, AnalysisInsights],
275
+ ) -> None:
276
+ """Assign component IDs to an analysis loaded from old JSON (without IDs) and re-key sub_analyses."""
277
+ from agents.agent_responses import ROOT_PARENT_ID
278
+
279
+ # Build old name -> sub_analysis mapping before clearing
280
+ old_subs = dict(sub_analyses)
281
+ sub_analyses.clear()
282
+
283
+ # Assign IDs to root and recursively to sub-analyses
284
+ _assign_ids_recursive(root_analysis, old_subs, sub_analyses, ROOT_PARENT_ID)
285
+
286
+
287
+ def _assign_ids_recursive(
288
+ analysis: AnalysisInsights,
289
+ old_subs: dict[str, AnalysisInsights],
290
+ new_subs: dict[str, AnalysisInsights],
291
+ parent_id: str,
292
+ ) -> None:
293
+ """Recursively assign IDs and re-key sub_analyses from name-keyed to id-keyed."""
294
+ assign_component_ids(analysis, parent_id=parent_id)
295
+ for comp in analysis.components:
296
+ # Check if this component had a sub-analysis keyed by name
297
+ if comp.name in old_subs:
298
+ sub = old_subs[comp.name]
299
+ new_subs[comp.component_id] = sub
300
+ _assign_ids_recursive(sub, old_subs, new_subs, comp.component_id)
301
+
302
+
303
+ def build_id_to_name_map(root_analysis: AnalysisInsights, sub_analyses: dict[str, AnalysisInsights]) -> dict[str, str]:
304
+ """Build a mapping from component_id to component name across all analysis levels."""
305
+ id_to_name: dict[str, str] = {c.component_id: c.name for c in root_analysis.components}
306
+ for sub_analysis in sub_analyses.values():
307
+ for comp in sub_analysis.components:
308
+ id_to_name[comp.component_id] = comp.name
309
+ return id_to_name
310
+
311
+
312
+ def _extract_analysis_recursive(data: dict, sub_analyses: dict[str, AnalysisInsights]) -> AnalysisInsights:
313
+ """Recursively extract AnalysisInsights from data dict, collecting all sub-analyses.
314
+
315
+ Args:
316
+ data: The analysis data dict containing components, description, etc.
317
+ sub_analyses: Dict to populate with component_id -> AnalysisInsights mappings.
318
+
319
+ Returns:
320
+ AnalysisInsights for this level (components are non-nested at this level).
321
+ """
322
+ components: list[Component] = []
323
+
324
+ for comp_data in data.get("components", []):
325
+ # Create the component for this level (non-nested)
326
+ component = Component(
327
+ name=comp_data["name"],
328
+ component_id=comp_data.get("component_id", ""),
329
+ description=comp_data["description"],
330
+ key_entities=comp_data.get("key_entities", []),
331
+ assigned_files=comp_data.get("assigned_files", []),
332
+ source_cluster_ids=comp_data.get("source_cluster_ids", []),
333
+ )
334
+ components.append(component)
335
+
336
+ # Recursively process nested components if they exist
337
+ nested_components = comp_data.get("components")
338
+ if nested_components is not None:
339
+ sub_analysis = _extract_analysis_recursive(comp_data, sub_analyses)
340
+ sub_analyses[component.component_id or comp_data["name"]] = sub_analysis
341
+
342
+ return AnalysisInsights(
343
+ description=data.get("description", ""),
344
+ components=components,
345
+ components_relations=[Relation(**r) for r in data.get("components_relations", [])],
346
+ )