codeboarding 0.12.4__tar.gz → 0.12.5__tar.gz

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 (200) hide show
  1. {codeboarding-0.12.4/codeboarding.egg-info → codeboarding-0.12.5}/PKG-INFO +1 -1
  2. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/agent_responses.py +59 -2
  3. codeboarding-0.12.5/agents/cluster_ids.py +42 -0
  4. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/cluster_methods_mixin.py +103 -24
  5. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/details_agent.py +4 -2
  6. codeboarding-0.12.5/agents/incremental_agent.py +608 -0
  7. codeboarding-0.12.5/agents/incremental_planning_agent.py +291 -0
  8. codeboarding-0.12.5/agents/incremental_results.py +18 -0
  9. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/__init__.py +2 -0
  10. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/abstract_prompt_factory.py +4 -0
  11. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/claude_prompts.py +45 -1
  12. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/deepseek_prompts.py +41 -1
  13. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/gemini_flash_prompts.py +37 -1
  14. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/glm_prompts.py +42 -1
  15. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/gpt_prompts.py +37 -1
  16. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/kimi_prompts.py +36 -1
  17. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/prompts/prompt_factory.py +4 -0
  18. codeboarding-0.12.5/agents/scope_ids.py +1 -0
  19. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/base.py +2 -0
  20. codeboarding-0.12.5/agents/tools/read_git_diff.py +99 -0
  21. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/toolkit.py +15 -0
  22. {codeboarding-0.12.4 → codeboarding-0.12.5/codeboarding.egg-info}/PKG-INFO +1 -1
  23. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding.egg-info/SOURCES.txt +9 -0
  24. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_cli/commands/full_analysis.py +4 -1
  25. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_cli/commands/incremental_analysis.py +3 -0
  26. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_cli/commands/partial_analysis.py +4 -0
  27. codeboarding-0.12.5/codeboarding_cli/view_instructions.py +41 -0
  28. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/analysis_json.py +1 -1
  29. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/cluster_delta.py +334 -68
  30. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/diagram_generator.py +211 -28
  31. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/io_utils.py +1 -19
  32. {codeboarding-0.12.4 → codeboarding-0.12.5}/pyproject.toml +1 -1
  33. {codeboarding-0.12.4 → codeboarding-0.12.5}/repo_utils/__init__.py +2 -3
  34. codeboarding-0.12.5/repo_utils/path_utils.py +41 -0
  35. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/__init__.py +1 -0
  36. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/analysis_cache.py +9 -6
  37. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/analysis_result.py +2 -0
  38. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/cluster_helpers.py +3 -1
  39. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/graph.py +30 -0
  40. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/language_results.py +2 -2
  41. codeboarding-0.12.5/static_analyzer/method_cluster_paths.py +56 -0
  42. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/scanner.py +82 -9
  43. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_tool_registry.py +4 -4
  44. codeboarding-0.12.5/tests/test_view_instructions.py +32 -0
  45. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_windows_compatibility.py +30 -0
  46. {codeboarding-0.12.4 → codeboarding-0.12.5}/tool_registry/paths.py +17 -0
  47. {codeboarding-0.12.4 → codeboarding-0.12.5}/tool_registry/registry.py +3 -5
  48. {codeboarding-0.12.4 → codeboarding-0.12.5}/utils.py +2 -20
  49. codeboarding-0.12.4/agents/incremental_agent.py +0 -787
  50. {codeboarding-0.12.4 → codeboarding-0.12.5}/LICENSE +0 -0
  51. {codeboarding-0.12.4 → codeboarding-0.12.5}/PYPI.md +0 -0
  52. {codeboarding-0.12.4 → codeboarding-0.12.5}/README.md +0 -0
  53. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/__init__.py +0 -0
  54. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/abstraction_agent.py +0 -0
  55. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/agent.py +0 -0
  56. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/change_status.py +0 -0
  57. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/cluster_budget.py +0 -0
  58. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/constants.py +0 -0
  59. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/dependency_discovery.py +0 -0
  60. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/llm_config.py +0 -0
  61. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/meta_agent.py +0 -0
  62. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/model_capabilities.py +0 -0
  63. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/planner_agent.py +0 -0
  64. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/retry.py +0 -0
  65. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/__init__.py +0 -0
  66. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/get_external_deps.py +0 -0
  67. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/get_method_invocations.py +0 -0
  68. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_cfg.py +0 -0
  69. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_docs.py +0 -0
  70. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_file.py +0 -0
  71. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_file_structure.py +0 -0
  72. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_packages.py +0 -0
  73. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_source.py +0 -0
  74. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/tools/read_structure.py +0 -0
  75. {codeboarding-0.12.4 → codeboarding-0.12.5}/agents/validation.py +0 -0
  76. {codeboarding-0.12.4 → codeboarding-0.12.5}/caching/__init__.py +0 -0
  77. {codeboarding-0.12.4 → codeboarding-0.12.5}/caching/cache.py +0 -0
  78. {codeboarding-0.12.4 → codeboarding-0.12.5}/caching/details_cache.py +0 -0
  79. {codeboarding-0.12.4 → codeboarding-0.12.5}/caching/meta_cache.py +0 -0
  80. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding.egg-info/dependency_links.txt +0 -0
  81. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding.egg-info/entry_points.txt +0 -0
  82. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding.egg-info/requires.txt +0 -0
  83. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding.egg-info/top_level.txt +0 -0
  84. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_cli/__init__.py +0 -0
  85. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_cli/bootstrap.py +0 -0
  86. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_cli/commands/__init__.py +0 -0
  87. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/__init__.py +0 -0
  88. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/analysis.py +0 -0
  89. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/orchestration.py +0 -0
  90. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/rendering.py +0 -0
  91. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/sources/__init__.py +0 -0
  92. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/sources/local.py +0 -0
  93. {codeboarding-0.12.4 → codeboarding-0.12.5}/codeboarding_workflows/sources/remote.py +0 -0
  94. {codeboarding-0.12.4 → codeboarding-0.12.5}/constants.py +0 -0
  95. {codeboarding-0.12.4 → codeboarding-0.12.5}/core/__init__.py +0 -0
  96. {codeboarding-0.12.4 → codeboarding-0.12.5}/core/plugin_loader.py +0 -0
  97. {codeboarding-0.12.4 → codeboarding-0.12.5}/core/protocols.py +0 -0
  98. {codeboarding-0.12.4 → codeboarding-0.12.5}/core/registry.py +0 -0
  99. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/__init__.py +0 -0
  100. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/cluster_snapshot.py +0 -0
  101. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/exceptions.py +0 -0
  102. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/file_coverage.py +0 -0
  103. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/run_context.py +0 -0
  104. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/run_mode.py +0 -0
  105. {codeboarding-0.12.4 → codeboarding-0.12.5}/diagram_analysis/version.py +0 -0
  106. {codeboarding-0.12.4 → codeboarding-0.12.5}/github_action.py +0 -0
  107. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/__init__.py +0 -0
  108. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/__init__.py +0 -0
  109. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/circular_deps.py +0 -0
  110. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/cohesion.py +0 -0
  111. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/coupling.py +0 -0
  112. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/function_size.py +0 -0
  113. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/god_class.py +0 -0
  114. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/inheritance.py +0 -0
  115. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/instability.py +0 -0
  116. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/checks/unused_code_diagnostics.py +0 -0
  117. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/config.py +0 -0
  118. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/models.py +0 -0
  119. {codeboarding-0.12.4 → codeboarding-0.12.5}/health/runner.py +0 -0
  120. {codeboarding-0.12.4 → codeboarding-0.12.5}/install.py +0 -0
  121. {codeboarding-0.12.4 → codeboarding-0.12.5}/logging_config.py +0 -0
  122. {codeboarding-0.12.4 → codeboarding-0.12.5}/main.py +0 -0
  123. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/__init__.py +0 -0
  124. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/callbacks.py +0 -0
  125. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/context.py +0 -0
  126. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/mixin.py +0 -0
  127. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/paths.py +0 -0
  128. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/stats.py +0 -0
  129. {codeboarding-0.12.4 → codeboarding-0.12.5}/monitoring/writers.py +0 -0
  130. {codeboarding-0.12.4 → codeboarding-0.12.5}/output_generators/__init__.py +0 -0
  131. {codeboarding-0.12.4 → codeboarding-0.12.5}/output_generators/html.py +0 -0
  132. {codeboarding-0.12.4 → codeboarding-0.12.5}/output_generators/html_template.py +0 -0
  133. {codeboarding-0.12.4 → codeboarding-0.12.5}/output_generators/markdown.py +0 -0
  134. {codeboarding-0.12.4 → codeboarding-0.12.5}/output_generators/mdx.py +0 -0
  135. {codeboarding-0.12.4 → codeboarding-0.12.5}/output_generators/sphinx.py +0 -0
  136. {codeboarding-0.12.4 → codeboarding-0.12.5}/repo_utils/change_detector.py +0 -0
  137. {codeboarding-0.12.4 → codeboarding-0.12.5}/repo_utils/diff_parser.py +0 -0
  138. {codeboarding-0.12.4 → codeboarding-0.12.5}/repo_utils/errors.py +0 -0
  139. {codeboarding-0.12.4 → codeboarding-0.12.5}/repo_utils/git_ops.py +0 -0
  140. {codeboarding-0.12.4 → codeboarding-0.12.5}/repo_utils/ignore.py +0 -0
  141. {codeboarding-0.12.4 → codeboarding-0.12.5}/setup.cfg +0 -0
  142. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/cfg_skip_planner.py +0 -0
  143. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/cluster_relations.py +0 -0
  144. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/constants.py +0 -0
  145. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/csharp_config_scanner.py +0 -0
  146. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/dotnet_sdk.py +0 -0
  147. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/__init__.py +0 -0
  148. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/__init__.py +0 -0
  149. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/csharp_adapter.py +0 -0
  150. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/go_adapter.py +0 -0
  151. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/java_adapter.py +0 -0
  152. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/php_adapter.py +0 -0
  153. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/python_adapter.py +0 -0
  154. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/rust_adapter.py +0 -0
  155. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
  156. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/call_graph_builder.py +0 -0
  157. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/edge_build_context.py +0 -0
  158. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/edge_builder.py +0 -0
  159. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/hierarchy_builder.py +0 -0
  160. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/language_adapter.py +0 -0
  161. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/lsp_client.py +0 -0
  162. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/lsp_constants.py +0 -0
  163. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/models.py +0 -0
  164. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/progress.py +0 -0
  165. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/protocols.py +0 -0
  166. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/result_converter.py +0 -0
  167. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/source_inspector.py +0 -0
  168. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/symbol_table.py +0 -0
  169. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/engine/utils.py +0 -0
  170. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/incremental_orchestrator.py +0 -0
  171. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/java_config_scanner.py +0 -0
  172. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/java_utils.py +0 -0
  173. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/leiden_utils.py +0 -0
  174. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/lsp_client/__init__.py +0 -0
  175. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/lsp_client/diagnostics.py +0 -0
  176. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/node.py +0 -0
  177. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/programming_language.py +0 -0
  178. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/reference_resolve_mixin.py +0 -0
  179. {codeboarding-0.12.4 → codeboarding-0.12.5}/static_analyzer/typescript_config_scanner.py +0 -0
  180. {codeboarding-0.12.4 → codeboarding-0.12.5}/telemetry/__init__.py +0 -0
  181. {codeboarding-0.12.4 → codeboarding-0.12.5}/telemetry/device_id.py +0 -0
  182. {codeboarding-0.12.4 → codeboarding-0.12.5}/telemetry/events.py +0 -0
  183. {codeboarding-0.12.4 → codeboarding-0.12.5}/telemetry/schemas.py +0 -0
  184. {codeboarding-0.12.4 → codeboarding-0.12.5}/telemetry/service.py +0 -0
  185. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_cli_parser.py +0 -0
  186. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_github_action.py +0 -0
  187. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_install.py +0 -0
  188. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_logging_config.py +0 -0
  189. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_main.py +0 -0
  190. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_pyproject_packages.py +0 -0
  191. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_registry_coverage.py +0 -0
  192. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_telemetry_events.py +0 -0
  193. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_user_config.py +0 -0
  194. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_vscode_constants.py +0 -0
  195. {codeboarding-0.12.4 → codeboarding-0.12.5}/tests/test_windows_encoding.py +0 -0
  196. {codeboarding-0.12.4 → codeboarding-0.12.5}/tool_registry/__init__.py +0 -0
  197. {codeboarding-0.12.4 → codeboarding-0.12.5}/tool_registry/installers.py +0 -0
  198. {codeboarding-0.12.4 → codeboarding-0.12.5}/tool_registry/manifest.py +0 -0
  199. {codeboarding-0.12.4 → codeboarding-0.12.5}/user_config.py +0 -0
  200. {codeboarding-0.12.4 → codeboarding-0.12.5}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.12.4
3
+ Version: 0.12.5
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -3,12 +3,16 @@ from __future__ import annotations
3
3
  import abc
4
4
  import logging
5
5
  from abc import abstractmethod
6
+ from enum import StrEnum
6
7
  from pathlib import PurePosixPath
7
8
  from typing import get_origin, Optional
8
9
 
9
10
  from pydantic import BaseModel, Field
10
11
  from pydantic.fields import FieldInfo
11
12
 
13
+ from agents.cluster_ids import CodeBoardingClusterId, GraphClusterId
14
+ from agents.scope_ids import ROOT_SCOPE_ID
15
+
12
16
  logger = logging.getLogger(__name__)
13
17
 
14
18
 
@@ -183,7 +187,7 @@ class ClustersComponent(LLMBaseModel):
183
187
  name: str = Field(
184
188
  description="Short, descriptive name for this cluster group (e.g., 'Authentication', 'Data Pipeline', 'Request Handling')"
185
189
  )
186
- cluster_ids: list[int] = Field(
190
+ cluster_ids: list[GraphClusterId] = Field(
187
191
  description="List of cluster IDs from the CFG analysis that are grouped together (e.g., [1, 3, 5])"
188
192
  )
189
193
  description: str = Field(
@@ -305,7 +309,7 @@ class Component(LLMBaseModel):
305
309
  default_factory=list,
306
310
  )
307
311
 
308
- source_cluster_ids: list[int] = Field(
312
+ source_cluster_ids: list[CodeBoardingClusterId] = Field(
309
313
  description="List of cluster IDs from CFG analysis that this component encompasses (populated deterministically from source_group_names).",
310
314
  default_factory=list,
311
315
  exclude=True,
@@ -570,6 +574,59 @@ class ScopeRelations(LLMBaseModel):
570
574
  return "\n".join(r.llm_str() for r in self.components_relations)
571
575
 
572
576
 
577
+ class ScopeOperationAction(StrEnum):
578
+ CREATE_COMPONENT = "create_component"
579
+ UPDATE_COMPONENT = "update_component"
580
+ DELETE_COMPONENT = "delete_component"
581
+ NOOP = "noop"
582
+ REGENERATE_SCOPE = "regenerate_scope"
583
+
584
+
585
+ class ScopedClusterRef(LLMBaseModel):
586
+ """A cluster reference scoped by component depth and language."""
587
+
588
+ scope_id: str = Field(description="Component scope id; use 'root' for the top-level scope.")
589
+ language: str = Field(description="Programming language for this cluster.")
590
+ cluster_id: int = Field(description="Cluster id within the scope/language cluster result.")
591
+
592
+ def llm_str(self):
593
+ scope_id = self.scope_id or ROOT_SCOPE_ID
594
+ return f"{scope_id}:{self.language}:{self.cluster_id}"
595
+
596
+
597
+ class ScopeOperation(LLMBaseModel):
598
+ """One diagram update operation for a single scope."""
599
+
600
+ action: ScopeOperationAction = Field(description="Operation to apply in this scope.")
601
+ cluster_refs: list[ScopedClusterRef] = Field(description="New-side clusters this operation accounts for.")
602
+ component_id: str | None = Field(
603
+ default=None,
604
+ description="Existing component id for update/delete/noop; null when creating a component.",
605
+ )
606
+ name: str | None = Field(default=None, description="Component name for create/update operations.")
607
+ description: str | None = Field(default=None, description="Component description for create/update operations.")
608
+ recurse: bool = Field(
609
+ default=False, description="Whether this component should be considered for child-scope update."
610
+ )
611
+ rationale: str = Field(description="Short reason for the operation, especially for ambiguous reshapes.")
612
+
613
+ def llm_str(self):
614
+ refs = ", ".join(ref.llm_str() for ref in self.cluster_refs) or "no clusters"
615
+ target = self.component_id or self.name or "new component"
616
+ return f"{self.action}: {refs} -> {target}; recurse={self.recurse}; {self.rationale}"
617
+
618
+
619
+ class ScopeUpdateDecision(LLMBaseModel):
620
+ """LLM-selected operations for one incremental scope update."""
621
+
622
+ operations: list[ScopeOperation] = Field(description="Operations to apply to the current scope.")
623
+
624
+ def llm_str(self):
625
+ if not self.operations:
626
+ return "No scope operations."
627
+ return "\n".join(operation.llm_str() for operation in self.operations)
628
+
629
+
573
630
  class FilePath(LLMBaseModel):
574
631
  """File path with optional line range reference."""
575
632
 
@@ -0,0 +1,42 @@
1
+ type GraphClusterId = int
2
+ type CodeBoardingClusterId = str
3
+
4
+
5
+ class GraphClusterIds:
6
+ @classmethod
7
+ def sort(cls, cluster_ids: set[GraphClusterId]) -> list[GraphClusterId]:
8
+ return sorted(cluster_ids)
9
+
10
+
11
+ class CodeBoardingClusterIds:
12
+ @classmethod
13
+ def sort(cls, cluster_ids: set[CodeBoardingClusterId]) -> list[CodeBoardingClusterId]:
14
+ # Sort by depth first, then naturally within a level: ["1", "2", "10", "2.1", "3.4"].
15
+ return sorted(cluster_ids, key=_cluster_id_sort_key)
16
+
17
+ @classmethod
18
+ def from_graph_id(cls, cluster_id: GraphClusterId) -> CodeBoardingClusterId:
19
+ return str(cluster_id)
20
+
21
+ @classmethod
22
+ def from_graph_ids(cls, cluster_ids: set[GraphClusterId]) -> list[CodeBoardingClusterId]:
23
+ return [cls.from_graph_id(cluster_id) for cluster_id in GraphClusterIds.sort(cluster_ids)]
24
+
25
+ @classmethod
26
+ def qualify_local_id(
27
+ cls, local_cluster_id: CodeBoardingClusterId, source_cluster_id_prefix: str = ""
28
+ ) -> CodeBoardingClusterId:
29
+ if not source_cluster_id_prefix:
30
+ return local_cluster_id
31
+ return f"{source_cluster_id_prefix}.{local_cluster_id}"
32
+
33
+ @classmethod
34
+ def qualify_local_ids(
35
+ cls, local_cluster_ids: list[CodeBoardingClusterId], source_cluster_id_prefix: str = ""
36
+ ) -> list[CodeBoardingClusterId]:
37
+ return [cls.qualify_local_id(cluster_id, source_cluster_id_prefix) for cluster_id in local_cluster_ids]
38
+
39
+
40
+ def _cluster_id_sort_key(cluster_id: CodeBoardingClusterId) -> tuple[tuple[int, int | str], ...]:
41
+ parts = tuple((0, int(part)) if part.isdigit() else (1, part) for part in cluster_id.split("."))
42
+ return ((0, len(parts)), *parts)
@@ -16,9 +16,12 @@ from agents.agent_responses import (
16
16
  MethodEntry,
17
17
  )
18
18
  from agents.cluster_budget import ClusterPromptBudget
19
+ from agents.cluster_ids import CodeBoardingClusterId, CodeBoardingClusterIds, GraphClusterId
19
20
  from agents.llm_config import get_current_agent_context_window, get_current_agent_model_ref
20
21
  from agents.model_capabilities import ContextWindow
21
22
  from constants import MIN_CLUSTERS_THRESHOLD
23
+ from diagram_analysis.cluster_delta import _delta_for_language
24
+ from diagram_analysis.cluster_snapshot import ClusterSnapshotEntry
22
25
  from static_analyzer.analysis_result import StaticAnalysisResults
23
26
  from static_analyzer.cfg_skip_planner import ContextBudgetExceededError, plan_skip_set
24
27
  from static_analyzer.cluster_helpers import (
@@ -44,7 +47,31 @@ logger = logging.getLogger(__name__)
44
47
  class _RenderedClusterString:
45
48
  text: str
46
49
  by_language: dict[str, str]
47
- cluster_ids: set[int]
50
+ cluster_ids: set[GraphClusterId]
51
+
52
+
53
+ def scoped_snapshot_from_lineage(cfg: CallGraph, scope_id: str) -> dict[int, ClusterSnapshotEntry]:
54
+ """Build a scoped snapshot from each method's recorded cluster ancestry/path."""
55
+ if not scope_id:
56
+ return {}
57
+ prefix = f"{scope_id}."
58
+ entries: dict[int, ClusterSnapshotEntry] = {}
59
+ for qname, cluster_ids in cfg.method_cluster_paths_snapshot():
60
+ if qname not in cfg.nodes:
61
+ continue
62
+ for cluster_id in cluster_ids:
63
+ if not cluster_id.startswith(prefix):
64
+ continue
65
+ local_id = cluster_id.removeprefix(prefix)
66
+ if not local_id.isdigit():
67
+ continue
68
+ entry = entries.setdefault(int(local_id), ClusterSnapshotEntry())
69
+ entry.members.add(qname)
70
+ file_path = cfg.nodes[qname].file_path
71
+ if file_path:
72
+ entry.files.add(file_path)
73
+ entry.member_files[qname] = file_path
74
+ return entries
48
75
 
49
76
 
50
77
  def _describe_window(ctx: ContextWindow) -> str:
@@ -327,7 +354,7 @@ class ClusterMethodsMixin:
327
354
 
328
355
  def _resolve_cluster_ids_from_groups(self, analysis: AnalysisInsights, cluster_analysis: ClusterAnalysis) -> None:
329
356
  """Resolve source_cluster_ids deterministically from source_group_names via case-insensitive lookup."""
330
- group_name_to_ids: dict[str, list[int]] = {
357
+ group_name_to_ids: dict[str, list[GraphClusterId]] = {
331
358
  cc.name.lower(): cc.cluster_ids for cc in cluster_analysis.cluster_components
332
359
  }
333
360
 
@@ -340,7 +367,7 @@ class ClusterMethodsMixin:
340
367
  logger.warning(
341
368
  f"[{self.__class__.__name__}] Unresolved group name '{gname}' for component '{component.name}'"
342
369
  )
343
- component.source_cluster_ids = sorted(set(resolved_ids))
370
+ component.source_cluster_ids = CodeBoardingClusterIds.from_graph_ids(set(resolved_ids))
344
371
 
345
372
  def _expand_to_method_level_clusters(self, cfg: CallGraph, cluster_result: ClusterResult) -> ClusterResult:
346
373
  """
@@ -399,7 +426,9 @@ class ClusterMethodsMixin:
399
426
  )
400
427
 
401
428
  def _create_strict_component_subgraph(
402
- self, component: Component
429
+ self,
430
+ component: Component,
431
+ source_cluster_id_prefix: str = "",
403
432
  ) -> tuple[str, dict[str, ClusterResult], dict[str, CallGraph]]:
404
433
  """
405
434
  Create a strict subgraph containing ONLY nodes from the component's file_methods.
@@ -440,8 +469,17 @@ class ClusterMethodsMixin:
440
469
  if sub_cfg.nodes:
441
470
  subgraph_cfgs[lang] = sub_cfg
442
471
 
443
- # Calculate clusters for the subgraph
444
- sub_cluster_result = sub_cfg.cluster()
472
+ seeded_snapshot = scoped_snapshot_from_lineage(sub_cfg, source_cluster_id_prefix)
473
+ if seeded_snapshot:
474
+ scoped_delta = _delta_for_language(
475
+ str(lang),
476
+ sub_cfg.to_networkx(),
477
+ seeded_snapshot,
478
+ )
479
+ sub_cluster_result = scoped_delta.cluster_results
480
+ else:
481
+ # Calculate clusters for the subgraph
482
+ sub_cluster_result = sub_cfg.cluster()
445
483
 
446
484
  # Merge into super-clusters if too many (same limit as AbstractionAgent)
447
485
  if len(sub_cluster_result.clusters) > MAX_LLM_CLUSTERS:
@@ -461,6 +499,12 @@ class ClusterMethodsMixin:
461
499
  cfg_nx = {lang: subgraph_cfgs[lang].to_networkx() for lang in cluster_results}
462
500
  enforce_cross_language_budget(cluster_results, cfg_nx)
463
501
 
502
+ if source_cluster_id_prefix:
503
+ for lang, cluster_result in cluster_results.items():
504
+ self.static_analysis.get_cfg(Language(lang)).record_cluster_paths(
505
+ cluster_result, source_cluster_id_prefix
506
+ )
507
+
464
508
  result_parts = []
465
509
  for lang in self.static_analysis.get_languages():
466
510
  if lang not in cluster_results:
@@ -609,26 +653,33 @@ class ClusterMethodsMixin:
609
653
  groups.append(FileMethodGroup(file_path=file_path, methods=methods))
610
654
  return groups
611
655
 
612
- def _build_cluster_to_component_map(self, analysis: AnalysisInsights) -> dict[int, Component]:
656
+ def _build_cluster_to_component_map(self, analysis: AnalysisInsights) -> dict[CodeBoardingClusterId, Component]:
613
657
  """Build cluster_id -> Component mapping from source_cluster_ids."""
614
- cluster_to_component: dict[int, Component] = {}
658
+ cluster_to_component: dict[CodeBoardingClusterId, Component] = {}
615
659
  for comp in analysis.components:
616
660
  for cid in comp.source_cluster_ids:
617
661
  cluster_to_component[cid] = comp
618
662
  return cluster_to_component
619
663
 
620
- def _build_node_to_cluster_map(self, cluster_results: dict[str, ClusterResult]) -> tuple[dict[str, int], set[int]]:
664
+ def _build_node_to_cluster_map(
665
+ self, cluster_results: dict[str, ClusterResult], source_cluster_id_prefix: str = ""
666
+ ) -> tuple[dict[str, CodeBoardingClusterId], set[CodeBoardingClusterId]]:
621
667
  """Build node_name (qualified name) -> cluster_id mapping and collect all cluster IDs."""
622
- all_cluster_ids: set[int] = set()
623
- node_to_cluster: dict[str, int] = {}
668
+ all_cluster_ids: set[CodeBoardingClusterId] = set()
669
+ node_to_cluster: dict[str, CodeBoardingClusterId] = {}
624
670
  for cr in cluster_results.values():
625
671
  for cid, members in cr.clusters.items():
626
- all_cluster_ids.add(cid)
672
+ cluster_id = CodeBoardingClusterIds.qualify_local_id(
673
+ CodeBoardingClusterIds.from_graph_id(cid), source_cluster_id_prefix
674
+ )
675
+ all_cluster_ids.add(cluster_id)
627
676
  for name in members:
628
- node_to_cluster[name] = cid
677
+ node_to_cluster[name] = cluster_id
629
678
  return node_to_cluster, all_cluster_ids
630
679
 
631
- def _validate_cluster_coverage(self, cluster_to_component: dict[int, Component], all_cluster_ids: set[int]) -> None:
680
+ def _validate_cluster_coverage(
681
+ self, cluster_to_component: dict[CodeBoardingClusterId, Component], all_cluster_ids: set[CodeBoardingClusterId]
682
+ ) -> None:
632
683
  """Log an error if any cluster IDs are not mapped to a component."""
633
684
  unmapped_cluster_ids = sorted(all_cluster_ids - set(cluster_to_component.keys()))
634
685
  if unmapped_cluster_ids:
@@ -642,7 +693,8 @@ class ClusterMethodsMixin:
642
693
  self,
643
694
  node: Node,
644
695
  cluster_results: dict[str, ClusterResult],
645
- cluster_to_component: dict[int, Component],
696
+ cluster_to_component: dict[str, Component],
697
+ source_cluster_id_prefix: str = "",
646
698
  ) -> Component | None:
647
699
  """Try to assign a node to a component based on its file already belonging to a cluster."""
648
700
  file_path = node.file_path
@@ -651,7 +703,10 @@ class ClusterMethodsMixin:
651
703
  for cr in cluster_results.values():
652
704
  cluster_ids = cr.get_clusters_for_file(file_path)
653
705
  for cid in cluster_ids:
654
- comp = cluster_to_component.get(cid)
706
+ cluster_id = CodeBoardingClusterIds.qualify_local_id(
707
+ CodeBoardingClusterIds.from_graph_id(cid), source_cluster_id_prefix
708
+ )
709
+ comp = cluster_to_component.get(cluster_id)
655
710
  if comp is not None:
656
711
  return comp
657
712
  return None
@@ -659,11 +714,12 @@ class ClusterMethodsMixin:
659
714
  def _assign_nodes_to_components(
660
715
  self,
661
716
  all_nodes: dict[str, Node],
662
- node_to_cluster: dict[str, int],
663
- cluster_to_component: dict[int, Component],
717
+ node_to_cluster: dict[str, str],
718
+ cluster_to_component: dict[str, Component],
664
719
  cluster_results: dict[str, ClusterResult],
665
720
  fallback_component: Component,
666
721
  cfg_graphs: dict[str, CallGraph] | None = None,
722
+ source_cluster_id_prefix: str = "",
667
723
  ) -> dict[str, list[Node]]:
668
724
  """Assign every node to a component via its cluster, file co-location, graph distance, or fallback."""
669
725
  component_nodes: dict[str, list[Node]] = defaultdict(list)
@@ -691,7 +747,7 @@ class ClusterMethodsMixin:
691
747
  node = all_nodes[qname]
692
748
 
693
749
  # 1. Try file co-location: if the node's file already belongs to a cluster/component
694
- comp = self._find_component_by_file(node, cluster_results, cluster_to_component)
750
+ comp = self._find_component_by_file(node, cluster_results, cluster_to_component, source_cluster_id_prefix)
695
751
  if comp is not None:
696
752
  assigned_by_file += 1
697
753
  component_nodes[comp.component_id].append(node)
@@ -699,8 +755,15 @@ class ClusterMethodsMixin:
699
755
 
700
756
  # 2. Try graph distance: find the nearest cluster in the call graph
701
757
  nearest_cid = self._find_nearest_cluster(qname, cluster_results, undirected_graphs)
702
- if nearest_cid is not None and nearest_cid in cluster_to_component:
703
- comp = cluster_to_component[nearest_cid]
758
+ nearest_cluster_id = (
759
+ CodeBoardingClusterIds.qualify_local_id(
760
+ CodeBoardingClusterIds.from_graph_id(nearest_cid), source_cluster_id_prefix
761
+ )
762
+ if nearest_cid is not None
763
+ else ""
764
+ )
765
+ if nearest_cluster_id in cluster_to_component:
766
+ comp = cluster_to_component[nearest_cluster_id]
704
767
  assigned_by_graph += 1
705
768
  component_nodes[comp.component_id].append(node)
706
769
  continue
@@ -716,7 +779,7 @@ class ClusterMethodsMixin:
716
779
  f"{assigned_by_graph} by graph distance, {assigned_by_fallback} to fallback"
717
780
  )
718
781
  if assigned_by_fallback:
719
- logger.warning(
782
+ logger.error(
720
783
  f"{assigned_by_fallback} node(s) fell back to '{fallback_component.name}' "
721
784
  f"— files: {sorted(fallback_files)}"
722
785
  )
@@ -754,6 +817,7 @@ class ClusterMethodsMixin:
754
817
  analysis: AnalysisInsights,
755
818
  cluster_results: dict[str, ClusterResult],
756
819
  cfg_graphs: dict[str, CallGraph] | None = None,
820
+ source_cluster_id_prefix: str = "",
757
821
  ) -> None:
758
822
  """Deterministically populate ``file_methods`` on every component.
759
823
 
@@ -777,11 +841,17 @@ class ClusterMethodsMixin:
777
841
  # per-component subgraph in DetailsAgent, which runs in parallel).
778
842
  all_nodes = self._collect_all_cfg_nodes(cluster_results, cfg_graphs)
779
843
  cluster_to_component = self._build_cluster_to_component_map(analysis)
780
- node_to_cluster, all_cluster_ids = self._build_node_to_cluster_map(cluster_results)
844
+ node_to_cluster, all_cluster_ids = self._build_node_to_cluster_map(cluster_results, source_cluster_id_prefix)
781
845
  self._validate_cluster_coverage(cluster_to_component, all_cluster_ids)
782
846
 
783
847
  component_nodes = self._assign_nodes_to_components(
784
- all_nodes, node_to_cluster, cluster_to_component, cluster_results, analysis.components[0], cfg_graphs
848
+ all_nodes,
849
+ node_to_cluster,
850
+ cluster_to_component,
851
+ cluster_results,
852
+ analysis.components[0],
853
+ cfg_graphs,
854
+ source_cluster_id_prefix,
785
855
  )
786
856
 
787
857
  for comp in analysis.components:
@@ -795,6 +865,7 @@ class ClusterMethodsMixin:
795
865
  self,
796
866
  analysis: AnalysisInsights,
797
867
  cfg_graphs: dict[str, CallGraph] | None = None,
868
+ source_cluster_id_prefix: str = "",
798
869
  ) -> None:
799
870
  """Build inter-component relations from CFG edges and merge with LLM relations.
800
871
 
@@ -810,6 +881,14 @@ class ClusterMethodsMixin:
810
881
  node_to_component = build_node_to_component_map(analysis)
811
882
  static_relations = build_component_relations(node_to_component, cfg_graphs)
812
883
  analysis.components_relations = merge_relations(analysis.components_relations, static_relations, analysis)
884
+ self._prefix_local_cluster_ids(analysis, source_cluster_id_prefix)
885
+
886
+ def _prefix_local_cluster_ids(self, analysis: AnalysisInsights, prefix: str) -> None:
887
+ """Prefix detail-subgraph cluster ids with their owning component scope."""
888
+ for component in analysis.components:
889
+ component.source_cluster_ids = CodeBoardingClusterIds.qualify_local_ids(
890
+ component.source_cluster_ids, prefix
891
+ )
813
892
 
814
893
  def build_scope_cfg_string(self, analysis: AnalysisInsights) -> str:
815
894
  """Render cross-component communication edges as a human-readable string for the LLM.
@@ -217,7 +217,9 @@ class DetailsAgent(ClusterMethodsMixin, CodeBoardingAgent):
217
217
 
218
218
  # Step 1: Create subgraph from component's assigned files using strict filtering
219
219
  # If subgraph has < MIN_CLUSTERS_THRESHOLD clusters, auto-expands to method-level
220
- _subgraph_str, subgraph_cluster_results, subgraph_cfgs = self._create_strict_component_subgraph(component)
220
+ _subgraph_str, subgraph_cluster_results, subgraph_cfgs = self._create_strict_component_subgraph(
221
+ component, source_cluster_id_prefix=component.component_id
222
+ )
221
223
 
222
224
  # Step 2: Group clusters within the subgraph
223
225
  cluster_analysis = self.step_clusters_grouping(component, subgraph_cluster_results)
@@ -238,7 +240,7 @@ class DetailsAgent(ClusterMethodsMixin, CodeBoardingAgent):
238
240
  self.populate_file_methods(analysis, subgraph_cluster_results, subgraph_cfgs)
239
241
 
240
242
  # Step 7: Build static inter-component relations from subgraph CFG edges
241
- self.build_static_relations(analysis, subgraph_cfgs)
243
+ self.build_static_relations(analysis, subgraph_cfgs, source_cluster_id_prefix=component.component_id)
242
244
 
243
245
  # Step 8: Fix source code reference lines (resolves reference_file paths)
244
246
  analysis = self.fix_source_code_reference_lines(analysis)