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,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
|
+
)
|