codeboarding 0.10.2__tar.gz → 0.10.4__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 (175) hide show
  1. {codeboarding-0.10.2/codeboarding.egg-info → codeboarding-0.10.4}/PKG-INFO +2 -1
  2. {codeboarding-0.10.2 → codeboarding-0.10.4}/README.md +4 -2
  3. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/abstraction_agent.py +14 -2
  4. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/agent.py +5 -0
  5. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/agent_responses.py +8 -16
  6. codeboarding-0.10.4/agents/cluster_budget.py +21 -0
  7. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/cluster_methods_mixin.py +170 -17
  8. codeboarding-0.10.4/agents/constants.py +38 -0
  9. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/details_agent.py +12 -2
  10. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/llm_config.py +67 -40
  11. codeboarding-0.10.4/agents/model_capabilities.py +217 -0
  12. {codeboarding-0.10.2 → codeboarding-0.10.4/codeboarding.egg-info}/PKG-INFO +2 -1
  13. {codeboarding-0.10.2 → codeboarding-0.10.4}/codeboarding.egg-info/SOURCES.txt +13 -3
  14. {codeboarding-0.10.2 → codeboarding-0.10.4}/codeboarding.egg-info/requires.txt +1 -0
  15. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/__init__.py +0 -1
  16. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/analysis_json.py +146 -115
  17. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/diagram_generator.py +0 -86
  18. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/incremental_updater.py +126 -76
  19. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/run_context.py +3 -0
  20. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/unused_code_diagnostics.py +13 -0
  21. {codeboarding-0.10.2 → codeboarding-0.10.4}/install.py +206 -68
  22. {codeboarding-0.10.2 → codeboarding-0.10.4}/pyproject.toml +6 -3
  23. {codeboarding-0.10.2 → codeboarding-0.10.4}/repo_utils/ignore.py +9 -2
  24. {codeboarding-0.10.2 → codeboarding-0.10.4}/repo_utils/method_diff.py +53 -68
  25. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/__init__.py +134 -11
  26. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/analysis_cache.py +2 -0
  27. codeboarding-0.10.4/static_analyzer/cfg_skip_planner.py +207 -0
  28. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/constants.py +8 -10
  29. codeboarding-0.10.4/static_analyzer/csharp_config_scanner.py +96 -0
  30. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/adapters/__init__.py +4 -0
  31. codeboarding-0.10.4/static_analyzer/engine/adapters/csharp_adapter.py +222 -0
  32. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/adapters/go_adapter.py +15 -0
  33. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/adapters/java_adapter.py +14 -9
  34. codeboarding-0.10.4/static_analyzer/engine/adapters/rust_adapter.py +189 -0
  35. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/call_graph_builder.py +50 -40
  36. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/language_adapter.py +79 -0
  37. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/lsp_client.py +125 -18
  38. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/result_converter.py +2 -1
  39. codeboarding-0.10.4/static_analyzer/engine/utils.py +68 -0
  40. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/graph.py +153 -35
  41. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/incremental_orchestrator.py +1 -1
  42. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/java_utils.py +18 -1
  43. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/node.py +2 -0
  44. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_install.py +67 -0
  45. codeboarding-0.10.4/tests/test_registry_coverage.py +396 -0
  46. codeboarding-0.10.4/tests/test_tool_registry.py +1655 -0
  47. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_user_config.py +34 -1
  48. codeboarding-0.10.4/tests/test_windows_compatibility.py +61 -0
  49. codeboarding-0.10.4/tool_registry/__init__.py +72 -0
  50. codeboarding-0.10.4/tool_registry/installers.py +628 -0
  51. codeboarding-0.10.4/tool_registry/manifest.py +347 -0
  52. codeboarding-0.10.4/tool_registry/paths.py +264 -0
  53. codeboarding-0.10.4/tool_registry/registry.py +275 -0
  54. {codeboarding-0.10.2 → codeboarding-0.10.4}/user_config.py +23 -6
  55. {codeboarding-0.10.2 → codeboarding-0.10.4}/vscode_constants.py +19 -0
  56. codeboarding-0.10.2/agents/constants.py +0 -13
  57. codeboarding-0.10.2/diagram_analysis/manifest.py +0 -153
  58. codeboarding-0.10.2/static_analyzer/engine/utils.py +0 -22
  59. codeboarding-0.10.2/tests/test_tool_registry.py +0 -94
  60. codeboarding-0.10.2/tests/test_windows_compatibility.py +0 -53
  61. codeboarding-0.10.2/tool_registry.py +0 -635
  62. {codeboarding-0.10.2 → codeboarding-0.10.4}/LICENSE +0 -0
  63. {codeboarding-0.10.2 → codeboarding-0.10.4}/PYPI.md +0 -0
  64. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/__init__.py +0 -0
  65. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/change_status.py +0 -0
  66. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/dependency_discovery.py +0 -0
  67. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/meta_agent.py +0 -0
  68. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/planner_agent.py +0 -0
  69. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/__init__.py +0 -0
  70. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/abstract_prompt_factory.py +0 -0
  71. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/claude_prompts.py +0 -0
  72. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/deepseek_prompts.py +0 -0
  73. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/gemini_flash_prompts.py +0 -0
  74. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/glm_prompts.py +0 -0
  75. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/gpt_prompts.py +0 -0
  76. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/kimi_prompts.py +0 -0
  77. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/prompts/prompt_factory.py +0 -0
  78. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/__init__.py +0 -0
  79. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/base.py +0 -0
  80. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/get_external_deps.py +0 -0
  81. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/get_method_invocations.py +0 -0
  82. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_cfg.py +0 -0
  83. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_docs.py +0 -0
  84. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_file.py +0 -0
  85. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_file_structure.py +0 -0
  86. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_git_diff.py +0 -0
  87. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_packages.py +0 -0
  88. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_source.py +0 -0
  89. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/read_structure.py +0 -0
  90. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/tools/toolkit.py +0 -0
  91. {codeboarding-0.10.2 → codeboarding-0.10.4}/agents/validation.py +0 -0
  92. {codeboarding-0.10.2 → codeboarding-0.10.4}/caching/__init__.py +0 -0
  93. {codeboarding-0.10.2 → codeboarding-0.10.4}/caching/cache.py +0 -0
  94. {codeboarding-0.10.2 → codeboarding-0.10.4}/caching/details_cache.py +0 -0
  95. {codeboarding-0.10.2 → codeboarding-0.10.4}/caching/meta_cache.py +0 -0
  96. {codeboarding-0.10.2 → codeboarding-0.10.4}/codeboarding.egg-info/dependency_links.txt +0 -0
  97. {codeboarding-0.10.2 → codeboarding-0.10.4}/codeboarding.egg-info/entry_points.txt +0 -0
  98. {codeboarding-0.10.2 → codeboarding-0.10.4}/codeboarding.egg-info/top_level.txt +0 -0
  99. {codeboarding-0.10.2 → codeboarding-0.10.4}/constants.py +0 -0
  100. {codeboarding-0.10.2 → codeboarding-0.10.4}/core/__init__.py +0 -0
  101. {codeboarding-0.10.2 → codeboarding-0.10.4}/core/plugin_loader.py +0 -0
  102. {codeboarding-0.10.2 → codeboarding-0.10.4}/core/protocols.py +0 -0
  103. {codeboarding-0.10.2 → codeboarding-0.10.4}/core/registry.py +0 -0
  104. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/file_coverage.py +0 -0
  105. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/incremental_types.py +0 -0
  106. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/io_utils.py +0 -0
  107. {codeboarding-0.10.2 → codeboarding-0.10.4}/diagram_analysis/version.py +0 -0
  108. {codeboarding-0.10.2 → codeboarding-0.10.4}/duckdb_crud.py +0 -0
  109. {codeboarding-0.10.2 → codeboarding-0.10.4}/github_action.py +0 -0
  110. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/__init__.py +0 -0
  111. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/__init__.py +0 -0
  112. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/circular_deps.py +0 -0
  113. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/cohesion.py +0 -0
  114. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/coupling.py +0 -0
  115. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/function_size.py +0 -0
  116. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/god_class.py +0 -0
  117. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/inheritance.py +0 -0
  118. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/checks/instability.py +0 -0
  119. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/config.py +0 -0
  120. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/constants.py +0 -0
  121. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/models.py +0 -0
  122. {codeboarding-0.10.2 → codeboarding-0.10.4}/health/runner.py +0 -0
  123. {codeboarding-0.10.2 → codeboarding-0.10.4}/health_main.py +0 -0
  124. {codeboarding-0.10.2 → codeboarding-0.10.4}/logging_config.py +0 -0
  125. {codeboarding-0.10.2 → codeboarding-0.10.4}/main.py +0 -0
  126. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/__init__.py +0 -0
  127. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/callbacks.py +0 -0
  128. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/context.py +0 -0
  129. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/mixin.py +0 -0
  130. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/paths.py +0 -0
  131. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/stats.py +0 -0
  132. {codeboarding-0.10.2 → codeboarding-0.10.4}/monitoring/writers.py +0 -0
  133. {codeboarding-0.10.2 → codeboarding-0.10.4}/output_generators/__init__.py +0 -0
  134. {codeboarding-0.10.2 → codeboarding-0.10.4}/output_generators/html.py +0 -0
  135. {codeboarding-0.10.2 → codeboarding-0.10.4}/output_generators/html_template.py +0 -0
  136. {codeboarding-0.10.2 → codeboarding-0.10.4}/output_generators/markdown.py +0 -0
  137. {codeboarding-0.10.2 → codeboarding-0.10.4}/output_generators/mdx.py +0 -0
  138. {codeboarding-0.10.2 → codeboarding-0.10.4}/output_generators/sphinx.py +0 -0
  139. {codeboarding-0.10.2 → codeboarding-0.10.4}/repo_utils/__init__.py +0 -0
  140. {codeboarding-0.10.2 → codeboarding-0.10.4}/repo_utils/change_detector.py +0 -0
  141. {codeboarding-0.10.2 → codeboarding-0.10.4}/repo_utils/errors.py +0 -0
  142. {codeboarding-0.10.2 → codeboarding-0.10.4}/repo_utils/git_diff.py +0 -0
  143. {codeboarding-0.10.2 → codeboarding-0.10.4}/setup.cfg +0 -0
  144. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/analysis_result.py +0 -0
  145. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/cluster_change_analyzer.py +0 -0
  146. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/cluster_helpers.py +0 -0
  147. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/cluster_relations.py +0 -0
  148. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/__init__.py +0 -0
  149. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/adapters/php_adapter.py +0 -0
  150. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/adapters/python_adapter.py +0 -0
  151. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
  152. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/edge_build_context.py +0 -0
  153. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/edge_builder.py +0 -0
  154. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/hierarchy_builder.py +0 -0
  155. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/lsp_constants.py +0 -0
  156. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/models.py +0 -0
  157. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/progress.py +0 -0
  158. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/protocols.py +0 -0
  159. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/source_inspector.py +0 -0
  160. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/engine/symbol_table.py +0 -0
  161. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/git_diff_analyzer.py +0 -0
  162. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/java_config_scanner.py +0 -0
  163. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/lsp_client/__init__.py +0 -0
  164. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/lsp_client/diagnostics.py +0 -0
  165. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/programming_language.py +0 -0
  166. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/reference_resolve_mixin.py +0 -0
  167. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/scanner.py +0 -0
  168. {codeboarding-0.10.2 → codeboarding-0.10.4}/static_analyzer/typescript_config_scanner.py +0 -0
  169. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_github_action.py +0 -0
  170. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_logging_config.py +0 -0
  171. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_main.py +0 -0
  172. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_pyproject_packages.py +0 -0
  173. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_vscode_constants.py +0 -0
  174. {codeboarding-0.10.2 → codeboarding-0.10.4}/tests/test_windows_encoding.py +0 -0
  175. {codeboarding-0.10.2 → codeboarding-0.10.4}/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.10.2
3
+ Version: 0.10.4
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -37,6 +37,7 @@ Requires-Dist: markdown>=3.8
37
37
  Requires-Dist: markdown-it-py>=3.0
38
38
  Requires-Dist: markitdown>=0.1
39
39
  Requires-Dist: networkx>=3.4
40
+ Requires-Dist: nodeenv>=1.10.0
40
41
  Requires-Dist: pathspec>=0.12
41
42
  Requires-Dist: pyyaml>=6.0
42
43
  Requires-Dist: regex>=2024.11
@@ -16,6 +16,8 @@ Install the extension from Open VSX.
16
16
  [![Python](https://img.shields.io/badge/Python-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
17
17
  [![Go](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev/)
18
18
  [![PHP](https://img.shields.io/badge/PHP-777BB4?style=flat-square&logo=php&logoColor=white)](https://www.php.net/)
19
+ [![Rust](https://img.shields.io/badge/Rust-000000?style=flat-square&logo=rust&logoColor=white)](https://www.rust-lang.org/)
20
+ [![C#](https://custom-icon-badges.demolab.com/badge/C%23-512BD4.svg?style=flat-square&logo=cshrp&logoColor=white)](https://learn.microsoft.com/en-us/dotnet/csharp/)
19
21
 
20
22
  ## Few use cases:
21
23
 
@@ -91,7 +93,7 @@ codeboarding --local /path/to/repo
91
93
 
92
94
  Output is written to `/path/to/repo/.codeboarding/`.
93
95
 
94
- `python install.py` and `codeboarding-setup` download language server binaries to `~/.codeboarding/servers/`, shared across projects. `npm` is required for Python, TypeScript, JavaScript, and PHP language servers; if it is missing, setup can install it via `nodeenv`.
96
+ `python install.py` and `codeboarding-setup` download language server binaries to `~/.codeboarding/servers/`, shared across projects. Node.js (and its bundled `npm`) is required for the Python, TypeScript, JavaScript, and PHP language servers; if neither `node` nor `CODEBOARDING_NODE_PATH` is set, setup downloads a pinned Node.js runtime into `~/.codeboarding/servers/nodeenv/` automatically.
95
97
 
96
98
  ## Configuration
97
99
 
@@ -141,7 +143,7 @@ python main.py https://github.com/pytorch/pytorch
141
143
 
142
144
  ## Supported stack
143
145
 
144
- - Languages: Python, TypeScript, JavaScript, Java, Go, PHP.
146
+ - Languages: Python, TypeScript, JavaScript, Java, Go, PHP, Rust.
145
147
  - LLM providers: OpenAI, Anthropic, Google, Vercel AI Gateway, AWS Bedrock, Ollama, OpenRouter, and more.
146
148
 
147
149
  ## Examples
@@ -70,8 +70,20 @@ class AbstractionAgent(ClusterMethodsMixin, CodeBoardingAgent):
70
70
 
71
71
  programming_langs = self.static_analysis.get_languages()
72
72
 
73
- # Build cluster string using the pre-computed cluster results
74
- cluster_str = self._build_cluster_string(programming_langs, cluster_results)
73
+ # Measure everything that wraps cfg_clusters (system message + rendered
74
+ # template with an empty slot) so the skip planner can back it out of
75
+ # the input window before budgeting the cluster string.
76
+ overhead_chars = len(str(self.system_message.content)) + len(
77
+ self.prompts["group_clusters"].format(
78
+ project_name=self.project_name,
79
+ cfg_clusters="",
80
+ meta_context=meta_context_str,
81
+ project_type=project_type,
82
+ )
83
+ )
84
+ cluster_str = self._build_cluster_string(
85
+ programming_langs, cluster_results, prompt_overhead_chars=overhead_chars
86
+ )
75
87
 
76
88
  prompt = self.prompts["group_clusters"].format(
77
89
  project_name=self.project_name,
@@ -160,6 +160,11 @@ class CodeBoardingAgent(ReferenceResolverMixin, MonitoringMixin):
160
160
  raise
161
161
 
162
162
  except Exception as e:
163
+ # HTTP 404 (e.g. retired model ID) is permanent — retrying won't help.
164
+ if getattr(e, "status_code", None) == 404:
165
+ logger.error(f"Permanent HTTP 404 — not retrying: {type(e).__name__}: {e}")
166
+ raise
167
+
163
168
  # Other errors (network, parsing, etc.) get standard exponential backoff
164
169
  if attempt < max_retries - 1:
165
170
  delay = min(10 * (2**attempt), 120)
@@ -1,12 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import abc
2
4
  import logging
3
5
  from abc import abstractmethod
6
+ from pathlib import PurePosixPath
4
7
  from typing import get_origin, Optional
5
8
 
6
9
  from pydantic import BaseModel, Field
7
10
 
8
- from agents.change_status import ChangeStatus
9
-
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
13
 
@@ -141,10 +142,6 @@ class MethodEntry(BaseModel):
141
142
  start_line: int = Field(description="Starting line number in the file.")
142
143
  end_line: int = Field(description="Ending line number in the file.")
143
144
  node_type: str = Field(description="Node type name matching NodeType enum (e.g. METHOD, FUNCTION, CLASS).")
144
- status: ChangeStatus = Field(
145
- default=ChangeStatus.UNCHANGED,
146
- description="Diff status of this method: added, modified, deleted, or unchanged.",
147
- )
148
145
 
149
146
  def __hash__(self) -> int:
150
147
  return hash(self.qualified_name)
@@ -155,13 +152,12 @@ class MethodEntry(BaseModel):
155
152
  return self.qualified_name == other.qualified_name
156
153
 
157
154
  @classmethod
158
- def from_method_change(cls, method_change, *, status_override: ChangeStatus | None = None) -> "MethodEntry":
155
+ def from_method_change(cls, method_change) -> MethodEntry:
159
156
  return cls(
160
157
  qualified_name=method_change.qualified_name,
161
158
  start_line=method_change.start_line,
162
159
  end_line=method_change.end_line,
163
160
  node_type=method_change.node_type,
164
- status=status_override or method_change.change_type,
165
161
  )
166
162
 
167
163
 
@@ -169,10 +165,6 @@ class FileMethodGroup(BaseModel):
169
165
  """All methods/functions belonging to a component within a single file."""
170
166
 
171
167
  file_path: str = Field(description="Relative path to the source file.")
172
- file_status: ChangeStatus = Field(
173
- default=ChangeStatus.UNCHANGED,
174
- description="Diff status of this file: added, modified, deleted, renamed, or unchanged.",
175
- )
176
168
  methods: list[MethodEntry] = Field(
177
169
  default_factory=list,
178
170
  description="Methods and functions in this file that belong to the component, sorted by start_line.",
@@ -182,10 +174,6 @@ class FileMethodGroup(BaseModel):
182
174
  class FileEntry(BaseModel):
183
175
  """Single source of truth for methods in one file."""
184
176
 
185
- file_status: ChangeStatus = Field(
186
- default=ChangeStatus.UNCHANGED,
187
- description="Diff status of this file: added, modified, deleted, renamed, or unchanged.",
188
- )
189
177
  methods: list[MethodEntry] = Field(
190
178
  default_factory=list,
191
179
  description="Methods and functions in this file, sorted by start line.",
@@ -261,6 +249,10 @@ class AnalysisInsights(LLMBaseModel):
261
249
  relations = "\n".join(cr.llm_str() for cr in self.components_relations)
262
250
  return title + body + relations
263
251
 
252
+ def file_to_component(self) -> dict[str, str]:
253
+ """Build file path -> component_id mapping from root components."""
254
+ return {str(PurePosixPath(fg.file_path)): c.component_id for c in self.components for fg in c.file_methods}
255
+
264
256
 
265
257
  def assign_component_ids(analysis: AnalysisInsights, parent_id: str = "") -> None:
266
258
  """Assign hierarchical component IDs based on sibling index.
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+
3
+ from agents.constants import ModelCapabilities
4
+
5
+ OUTPUT_HEADROOM_TOKENS = 8_000
6
+ CONTEXT_MARGIN = 0.9
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ClusterPromptBudget:
11
+ """Character budget for the full rendered ``cfg_clusters`` prompt slot."""
12
+
13
+ input_tokens: int
14
+ output_headroom_tokens: int = OUTPUT_HEADROOM_TOKENS
15
+ chars_per_token: float = ModelCapabilities.CHARS_PER_TOKEN
16
+ margin: float = CONTEXT_MARGIN
17
+
18
+ def available_chars(self, prompt_overhead_chars: int) -> int:
19
+ prompt_overhead_tokens = prompt_overhead_chars / self.chars_per_token
20
+ available_tokens = (self.input_tokens - self.output_headroom_tokens - prompt_overhead_tokens) * self.margin
21
+ return int(available_tokens * self.chars_per_token)
@@ -1,7 +1,9 @@
1
1
  import logging
2
2
  import os
3
3
  from collections import defaultdict
4
+ from dataclasses import dataclass
4
5
  from pathlib import Path
6
+ from typing import NoReturn
5
7
 
6
8
  import networkx as nx
7
9
 
@@ -13,8 +15,11 @@ from agents.agent_responses import (
13
15
  FileMethodGroup,
14
16
  MethodEntry,
15
17
  )
18
+ from agents.cluster_budget import ClusterPromptBudget
19
+ from agents.llm_config import get_current_agent_context_window
16
20
  from constants import MIN_CLUSTERS_THRESHOLD
17
21
  from static_analyzer.analysis_result import StaticAnalysisResults
22
+ from static_analyzer.cfg_skip_planner import ContextBudgetExceededError, plan_skip_set
18
23
  from static_analyzer.cluster_helpers import (
19
24
  MAX_LLM_CLUSTERS,
20
25
  enforce_cross_language_budget,
@@ -34,6 +39,13 @@ from static_analyzer.node import Node
34
39
  logger = logging.getLogger(__name__)
35
40
 
36
41
 
42
+ @dataclass(frozen=True)
43
+ class _RenderedClusterString:
44
+ text: str
45
+ by_language: dict[str, str]
46
+ cluster_ids: set[int]
47
+
48
+
37
49
  class ClusterMethodsMixin:
38
50
  """
39
51
  Mixin providing shared cluster-related functionality for agents.
@@ -61,6 +73,7 @@ class ClusterMethodsMixin:
61
73
  programming_langs: list[str],
62
74
  cluster_results: dict[str, ClusterResult],
63
75
  cluster_ids: set[int] | None = None,
76
+ prompt_overhead_chars: int = 0,
64
77
  ) -> str:
65
78
  """
66
79
  Build a cluster string for LLM consumption using pre-computed cluster results.
@@ -69,29 +82,59 @@ class ClusterMethodsMixin:
69
82
  programming_langs: List of languages to include
70
83
  cluster_results: Pre-computed cluster results mapping language -> ClusterResult
71
84
  cluster_ids: Optional set of cluster IDs to filter by
85
+ prompt_overhead_chars: Characters used by everything else in the
86
+ prompt (system message + rendered template with an empty
87
+ ``cfg_clusters`` slot). The skip planner subtracts this from
88
+ the model's input window before computing the char budget for
89
+ the cluster string.
72
90
 
73
91
  Returns:
74
92
  Formatted cluster string with headers per language
75
93
  """
76
- cluster_lines = []
94
+ rendered = self._render_cluster_string(programming_langs, cluster_results, cluster_ids, {})
95
+ if cluster_ids:
96
+ return rendered.text
97
+
98
+ char_budget = self._cluster_prompt_budget(prompt_overhead_chars)
99
+ if len(rendered.text) <= char_budget:
100
+ return rendered.text
101
+
102
+ per_lang_skip = self._plan_skip_sets(programming_langs, cluster_results, prompt_overhead_chars)
103
+ rendered_with_skips = self._render_cluster_string(
104
+ programming_langs, cluster_results, cluster_ids, per_lang_skip
105
+ )
106
+ if len(rendered_with_skips.text) > char_budget:
107
+ self._raise_cluster_budget_error(char_budget, rendered_with_skips, per_lang_skip)
108
+
109
+ return rendered_with_skips.text
110
+
111
+ def _render_cluster_string(
112
+ self,
113
+ programming_langs: list[str],
114
+ cluster_results: dict[str, ClusterResult],
115
+ cluster_ids: set[int] | None,
116
+ skip_sets: dict[str, set[str]],
117
+ ) -> _RenderedClusterString:
118
+ cluster_lines: list[str] = []
119
+ by_language: dict[str, str] = {}
77
120
  all_cluster_ids: set[int] = set()
78
121
 
79
122
  for lang in programming_langs:
80
123
  cfg = self.static_analysis.get_cfg(lang)
81
- # Get cluster result for this language
82
124
  cluster_result = cluster_results.get(lang)
83
- cluster_str = cfg.to_cluster_string(cluster_ids, cluster_result)
125
+ cluster_str = cfg.to_cluster_string(
126
+ cluster_ids or set(), cluster_result, skip_nodes=skip_sets.get(lang, set())
127
+ )
84
128
 
85
129
  if cluster_str.strip() and cluster_str not in ("empty", "none", "No clusters found."):
86
130
  header = "Component CFG" if cluster_ids else "Clusters"
87
- cluster_lines.append(f"\n## {lang.capitalize()} - {header}\n")
88
- cluster_lines.append(cluster_str)
89
- cluster_lines.append("\n")
131
+ lang_text = f"\n## {lang.capitalize()} - {header}\n{cluster_str}\n"
132
+ cluster_lines.append(lang_text)
133
+ by_language[lang] = lang_text
90
134
  if cluster_result:
91
135
  lang_ids = cluster_ids if cluster_ids else cluster_result.get_cluster_ids()
92
136
  all_cluster_ids.update(lang_ids)
93
137
 
94
- # Add explicit ID checklist so the LLM knows exactly which IDs to assign
95
138
  if all_cluster_ids and not cluster_ids:
96
139
  sorted_cluster_ids = sorted(all_cluster_ids)
97
140
  cluster_lines.append(
@@ -99,7 +142,117 @@ class ClusterMethodsMixin:
99
142
  f"Every one of these IDs: {sorted_cluster_ids} must appear in exactly one group."
100
143
  )
101
144
 
102
- return "".join(cluster_lines)
145
+ return _RenderedClusterString(text="".join(cluster_lines), by_language=by_language, cluster_ids=all_cluster_ids)
146
+
147
+ def _plan_skip_sets(
148
+ self,
149
+ programming_langs: list[str],
150
+ cluster_results: dict[str, ClusterResult],
151
+ prompt_overhead_chars: int,
152
+ ) -> dict[str, set[str]]:
153
+ """Compute per-language skip sets so the final combined cluster string fits."""
154
+ char_budget = self._cluster_prompt_budget(prompt_overhead_chars)
155
+ if char_budget <= 0:
156
+ ctx = get_current_agent_context_window()
157
+ msg = (
158
+ f"Prompt overhead ({prompt_overhead_chars} chars) consumes the entire agent input "
159
+ f"window ({ctx.input_tokens} tokens); no room for cluster renderings."
160
+ )
161
+ logger.error("[CFG skip planner] %s", msg)
162
+ raise ContextBudgetExceededError(msg)
163
+
164
+ langs_with_clusters = [l for l in programming_langs if cluster_results.get(l)]
165
+ if not langs_with_clusters:
166
+ return {}
167
+
168
+ skip_sets: dict[str, set[str]] = {}
169
+ rendered = self._render_cluster_string(programming_langs, cluster_results, None, skip_sets)
170
+ if len(rendered.text) <= char_budget:
171
+ return skip_sets
172
+
173
+ max_iterations = max(1, len(langs_with_clusters) * 5)
174
+ for _ in range(max_iterations):
175
+ deficit = len(rendered.text) - char_budget
176
+ ordered_langs = sorted(
177
+ langs_with_clusters,
178
+ key=lambda lang: len(rendered.by_language.get(lang, "")),
179
+ reverse=True,
180
+ )
181
+ progressed = False
182
+
183
+ for lang in ordered_langs:
184
+ lang_text = rendered.by_language.get(lang, "")
185
+ current_len = len(lang_text)
186
+ if current_len == 0:
187
+ continue
188
+
189
+ for target in self._language_budget_targets(current_len, deficit):
190
+ try:
191
+ skip = plan_skip_set(self.static_analysis.get_cfg(lang), cluster_results[lang], target)
192
+ except ContextBudgetExceededError:
193
+ continue
194
+
195
+ if skip == skip_sets.get(lang, set()):
196
+ continue
197
+
198
+ trial_skip_sets = dict(skip_sets)
199
+ if skip:
200
+ trial_skip_sets[lang] = skip
201
+ else:
202
+ trial_skip_sets.pop(lang, None)
203
+
204
+ trial_rendered = self._render_cluster_string(
205
+ programming_langs, cluster_results, None, trial_skip_sets
206
+ )
207
+ if len(trial_rendered.text) >= len(rendered.text):
208
+ continue
209
+
210
+ skip_sets = trial_skip_sets
211
+ rendered = trial_rendered
212
+ progressed = True
213
+ break
214
+
215
+ if progressed:
216
+ break
217
+
218
+ if len(rendered.text) <= char_budget:
219
+ return skip_sets
220
+ if not progressed:
221
+ break
222
+
223
+ self._raise_cluster_budget_error(char_budget, rendered, skip_sets)
224
+
225
+ @staticmethod
226
+ def _language_budget_targets(current_len: int, deficit: int) -> list[int]:
227
+ exact_target = max(0, current_len - deficit)
228
+ targets = {
229
+ exact_target,
230
+ int(current_len * 0.9),
231
+ int(current_len * 0.75),
232
+ int(current_len * 0.5),
233
+ 0,
234
+ }
235
+ return sorted((target for target in targets if target < current_len), reverse=True)
236
+
237
+ @staticmethod
238
+ def _raise_cluster_budget_error(
239
+ char_budget: int,
240
+ rendered: _RenderedClusterString,
241
+ skip_sets: dict[str, set[str]],
242
+ ) -> NoReturn:
243
+ per_lang_sizes = {lang: len(text) for lang, text in rendered.by_language.items()}
244
+ skipped_counts = {lang: len(skip) for lang, skip in skip_sets.items() if skip}
245
+ msg = (
246
+ f"Cluster render {len(rendered.text)} chars exceeds budget {char_budget}. "
247
+ f"Per-language sizes: {per_lang_sizes}; skipped nodes: {skipped_counts}."
248
+ )
249
+ logger.error("[CFG skip planner] %s", msg)
250
+ raise ContextBudgetExceededError(msg)
251
+
252
+ @staticmethod
253
+ def _cluster_prompt_budget(prompt_overhead_chars: int) -> int:
254
+ ctx = get_current_agent_context_window()
255
+ return ClusterPromptBudget(input_tokens=ctx.input_tokens).available_chars(prompt_overhead_chars)
103
256
 
104
257
  def _ensure_unique_key_entities(self, analysis: AnalysisInsights):
105
258
  """
@@ -247,11 +400,11 @@ class ClusterMethodsMixin:
247
400
  logger.warning(f"Component {component.name} has no assigned files")
248
401
  return "No assigned files found for this component.", {}, {}
249
402
 
250
- # Convert files to absolute paths for comparison
251
- assigned_file_set = set()
252
- for f in component_files:
253
- abs_path = os.path.join(self.repo_dir, f) if not os.path.isabs(f) else f
254
- assigned_file_set.add(abs_path)
403
+ # Collect qualified names for method-level filtering
404
+ assigned_qnames: set[str] = set()
405
+ for group in component.file_methods:
406
+ for method in group.methods:
407
+ assigned_qnames.add(method.qualified_name)
255
408
 
256
409
  cluster_results: dict[str, ClusterResult] = {}
257
410
  subgraph_cfgs: dict[str, CallGraph] = {}
@@ -259,8 +412,8 @@ class ClusterMethodsMixin:
259
412
  for lang in self.static_analysis.get_languages():
260
413
  cfg = self.static_analysis.get_cfg(lang)
261
414
 
262
- # Use strict filtering logic
263
- sub_cfg = cfg.filter_by_files(assigned_file_set)
415
+ # Filter by exact method set to prevent scope leakage
416
+ sub_cfg = cfg.filter_by_nodes(assigned_qnames)
264
417
 
265
418
  if sub_cfg.nodes:
266
419
  subgraph_cfgs[lang] = sub_cfg
@@ -299,7 +452,7 @@ class ClusterMethodsMixin:
299
452
  result = "".join(result_parts)
300
453
 
301
454
  if not result.strip():
302
- logger.warning(f"No CFG found for component {component.name} with {len(component_files)} files")
455
+ logger.warning(f"No CFG found for component {component.name} with {len(assigned_qnames)} methods")
303
456
  return "No relevant CFG clusters found for this component.", cluster_results, subgraph_cfgs
304
457
 
305
458
  return result, cluster_results, subgraph_cfgs
@@ -556,7 +709,7 @@ class ClusterMethodsMixin:
556
709
  for fmg in component.file_methods:
557
710
  entry = files.get(fmg.file_path)
558
711
  if entry is None:
559
- entry = FileEntry(file_status=fmg.file_status, methods=[])
712
+ entry = FileEntry(methods=[])
560
713
  files[fmg.file_path] = entry
561
714
 
562
715
  methods_by_qname = {m.qualified_name: m for m in entry.methods}
@@ -0,0 +1,38 @@
1
+ """Constants for the agents module."""
2
+
3
+
4
+ class LLMDefaults:
5
+ DEFAULT_AGENT_TEMPERATURE = 0
6
+ DEFAULT_PARSING_TEMPERATURE = 0
7
+ AWS_MAX_TOKENS = 4096
8
+
9
+
10
+ class FileStructureConfig:
11
+ MAX_LINES = 500
12
+ DEFAULT_MAX_DEPTH = 10
13
+ FALLBACK_MAX_LINES = 50000
14
+
15
+
16
+ class ModelCapabilities:
17
+ FALLBACK_INPUT = 256_000
18
+ FALLBACK_OUTPUT = 64_000
19
+ CACHE_TTL_SECONDS = 24 * 3600
20
+ CHARS_PER_TOKEN = 3.5 # community consensus conversion is around 3 or 4 chars/token.
21
+
22
+ SOURCES = {
23
+ "litellm": "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
24
+ "modelsdev": "https://models.dev/api.json",
25
+ "openrouter": "https://openrouter.ai/api/v1/models",
26
+ }
27
+
28
+ # models.dev uses slugs that diverge from our internal provider names.
29
+ MODELSDEV_SLUG = {
30
+ "aws": "amazon-bedrock",
31
+ "kimi": "moonshotai",
32
+ "glm": "zai",
33
+ }
34
+
35
+ OPENROUTER_PREFIX = {
36
+ "kimi": "moonshotai",
37
+ "glm": "z-ai",
38
+ }
@@ -84,8 +84,18 @@ class DetailsAgent(ClusterMethodsMixin, CodeBoardingAgent):
84
84
 
85
85
  programming_langs = self.static_analysis.get_languages()
86
86
 
87
- # Build cluster string using the pre-computed cluster results (same as AbstractionAgent)
88
- cluster_str = self._build_cluster_string(programming_langs, subgraph_cluster_results)
87
+ overhead_chars = len(str(self.system_message.content)) + len(
88
+ self.prompts["group_clusters"].format(
89
+ project_name=self.project_name,
90
+ cfg_clusters="",
91
+ component=component.llm_str(),
92
+ meta_context=meta_context_str,
93
+ project_type=project_type,
94
+ )
95
+ )
96
+ cluster_str = self._build_cluster_string(
97
+ programming_langs, subgraph_cluster_results, prompt_overhead_chars=overhead_chars
98
+ )
89
99
 
90
100
  prompt = self.prompts["group_clusters"].format(
91
101
  project_name=self.project_name,