codeboarding 0.10.4__tar.gz → 0.11.0__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.10.4/codeboarding.egg-info → codeboarding-0.11.0}/PKG-INFO +21 -9
  2. {codeboarding-0.10.4 → codeboarding-0.11.0}/PYPI.md +10 -8
  3. {codeboarding-0.10.4 → codeboarding-0.11.0}/README.md +8 -8
  4. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/agent.py +108 -118
  5. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/agent_responses.py +10 -0
  6. codeboarding-0.11.0/agents/analysis_patcher.py +206 -0
  7. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/llm_config.py +16 -3
  8. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/abstract_prompt_factory.py +8 -0
  9. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/claude_prompts.py +37 -0
  10. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/deepseek_prompts.py +37 -0
  11. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/gemini_flash_prompts.py +37 -0
  12. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/glm_prompts.py +37 -0
  13. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/gpt_prompts.py +37 -0
  14. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/kimi_prompts.py +37 -0
  15. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/prompt_factory.py +8 -0
  16. codeboarding-0.11.0/agents/retry.py +118 -0
  17. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/toolkit.py +0 -8
  18. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/validation.py +12 -29
  19. {codeboarding-0.10.4 → codeboarding-0.11.0/codeboarding.egg-info}/PKG-INFO +21 -9
  20. {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/SOURCES.txt +29 -6
  21. {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/requires.txt +10 -0
  22. {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/top_level.txt +2 -0
  23. codeboarding-0.11.0/codeboarding_cli/__init__.py +1 -0
  24. codeboarding-0.11.0/codeboarding_cli/bootstrap.py +53 -0
  25. codeboarding-0.11.0/codeboarding_cli/commands/__init__.py +1 -0
  26. codeboarding-0.11.0/codeboarding_cli/commands/full_analysis.py +197 -0
  27. codeboarding-0.11.0/codeboarding_cli/commands/incremental_analysis.py +137 -0
  28. codeboarding-0.11.0/codeboarding_cli/commands/partial_analysis.py +69 -0
  29. codeboarding-0.11.0/codeboarding_workflows/__init__.py +14 -0
  30. codeboarding-0.11.0/codeboarding_workflows/analysis.py +144 -0
  31. codeboarding-0.11.0/codeboarding_workflows/orchestration.py +48 -0
  32. codeboarding-0.11.0/codeboarding_workflows/rendering.py +92 -0
  33. codeboarding-0.11.0/codeboarding_workflows/sources/__init__.py +12 -0
  34. codeboarding-0.11.0/codeboarding_workflows/sources/local.py +23 -0
  35. codeboarding-0.11.0/codeboarding_workflows/sources/remote.py +71 -0
  36. codeboarding-0.11.0/diagram_analysis/__init__.py +22 -0
  37. {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/diagram_generator.py +293 -10
  38. codeboarding-0.11.0/diagram_analysis/ease.py +68 -0
  39. codeboarding-0.10.4/diagram_analysis/incremental_types.py → codeboarding-0.11.0/diagram_analysis/incremental/delta.py +23 -9
  40. codeboarding-0.11.0/diagram_analysis/incremental/models.py +220 -0
  41. codeboarding-0.11.0/diagram_analysis/incremental/payload.py +129 -0
  42. codeboarding-0.11.0/diagram_analysis/incremental/pipeline.py +264 -0
  43. codeboarding-0.11.0/diagram_analysis/incremental/semantic_diff.py +557 -0
  44. codeboarding-0.11.0/diagram_analysis/incremental/trace_planner.py +435 -0
  45. codeboarding-0.11.0/diagram_analysis/incremental/tracer.py +458 -0
  46. codeboarding-0.10.4/diagram_analysis/incremental_updater.py → codeboarding-0.11.0/diagram_analysis/incremental/updater.py +149 -89
  47. {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/io_utils.py +3 -2
  48. codeboarding-0.11.0/diagram_analysis/run_metadata.py +146 -0
  49. codeboarding-0.11.0/github_action.py +129 -0
  50. {codeboarding-0.10.4 → codeboarding-0.11.0}/install.py +1 -1
  51. {codeboarding-0.10.4 → codeboarding-0.11.0}/logging_config.py +1 -1
  52. codeboarding-0.11.0/main.py +99 -0
  53. codeboarding-0.11.0/output_generators/__init__.py +0 -0
  54. {codeboarding-0.10.4 → codeboarding-0.11.0}/pyproject.toml +45 -3
  55. {codeboarding-0.10.4 → codeboarding-0.11.0}/repo_utils/__init__.py +2 -5
  56. codeboarding-0.11.0/repo_utils/change_detector.py +311 -0
  57. codeboarding-0.11.0/repo_utils/diff_parser.py +277 -0
  58. codeboarding-0.11.0/repo_utils/git_ops.py +283 -0
  59. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/__init__.py +2 -2
  60. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/analysis_result.py +38 -0
  61. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/constants.py +31 -0
  62. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/csharp_adapter.py +3 -3
  63. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/go_adapter.py +3 -2
  64. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/java_adapter.py +3 -3
  65. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/php_adapter.py +3 -3
  66. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/python_adapter.py +3 -2
  67. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/rust_adapter.py +5 -4
  68. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/typescript_adapter.py +5 -4
  69. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/language_adapter.py +15 -2
  70. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/incremental_orchestrator.py +11 -11
  71. codeboarding-0.11.0/tests/test_cli_parser.py +70 -0
  72. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_github_action.py +11 -11
  73. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_main.py +212 -218
  74. {codeboarding-0.10.4 → codeboarding-0.11.0}/utils.py +14 -1
  75. codeboarding-0.10.4/agents/tools/read_git_diff.py +0 -131
  76. codeboarding-0.10.4/diagram_analysis/__init__.py +0 -3
  77. codeboarding-0.10.4/github_action.py +0 -173
  78. codeboarding-0.10.4/main.py +0 -567
  79. codeboarding-0.10.4/repo_utils/change_detector.py +0 -294
  80. codeboarding-0.10.4/repo_utils/git_diff.py +0 -74
  81. codeboarding-0.10.4/repo_utils/method_diff.py +0 -162
  82. codeboarding-0.10.4/static_analyzer/git_diff_analyzer.py +0 -224
  83. {codeboarding-0.10.4 → codeboarding-0.11.0}/LICENSE +0 -0
  84. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/__init__.py +0 -0
  85. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/abstraction_agent.py +0 -0
  86. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/change_status.py +0 -0
  87. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/cluster_budget.py +0 -0
  88. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/cluster_methods_mixin.py +0 -0
  89. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/constants.py +0 -0
  90. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/dependency_discovery.py +0 -0
  91. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/details_agent.py +0 -0
  92. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/meta_agent.py +0 -0
  93. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/model_capabilities.py +0 -0
  94. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/planner_agent.py +0 -0
  95. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/__init__.py +0 -0
  96. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/__init__.py +0 -0
  97. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/base.py +0 -0
  98. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/get_external_deps.py +0 -0
  99. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/get_method_invocations.py +0 -0
  100. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_cfg.py +0 -0
  101. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_docs.py +0 -0
  102. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_file.py +0 -0
  103. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_file_structure.py +0 -0
  104. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_packages.py +0 -0
  105. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_source.py +0 -0
  106. {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_structure.py +0 -0
  107. {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/__init__.py +0 -0
  108. {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/cache.py +0 -0
  109. {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/details_cache.py +0 -0
  110. {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/meta_cache.py +0 -0
  111. {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/dependency_links.txt +0 -0
  112. {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/entry_points.txt +0 -0
  113. {codeboarding-0.10.4 → codeboarding-0.11.0}/constants.py +0 -0
  114. {codeboarding-0.10.4 → codeboarding-0.11.0}/core/__init__.py +0 -0
  115. {codeboarding-0.10.4 → codeboarding-0.11.0}/core/plugin_loader.py +0 -0
  116. {codeboarding-0.10.4 → codeboarding-0.11.0}/core/protocols.py +0 -0
  117. {codeboarding-0.10.4 → codeboarding-0.11.0}/core/registry.py +0 -0
  118. {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/analysis_json.py +0 -0
  119. {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/file_coverage.py +1 -1
  120. {codeboarding-0.10.4/output_generators → codeboarding-0.11.0/diagram_analysis/incremental}/__init__.py +0 -0
  121. {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/run_context.py +0 -0
  122. {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/version.py +0 -0
  123. {codeboarding-0.10.4 → codeboarding-0.11.0}/duckdb_crud.py +0 -0
  124. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/__init__.py +0 -0
  125. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/__init__.py +0 -0
  126. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/circular_deps.py +0 -0
  127. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/cohesion.py +0 -0
  128. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/coupling.py +0 -0
  129. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/function_size.py +0 -0
  130. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/god_class.py +0 -0
  131. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/inheritance.py +0 -0
  132. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/instability.py +0 -0
  133. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/unused_code_diagnostics.py +0 -0
  134. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/config.py +0 -0
  135. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/constants.py +0 -0
  136. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/models.py +0 -0
  137. {codeboarding-0.10.4 → codeboarding-0.11.0}/health/runner.py +0 -0
  138. {codeboarding-0.10.4 → codeboarding-0.11.0}/health_main.py +0 -0
  139. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/__init__.py +0 -0
  140. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/callbacks.py +0 -0
  141. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/context.py +0 -0
  142. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/mixin.py +0 -0
  143. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/paths.py +0 -0
  144. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/stats.py +0 -0
  145. {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/writers.py +0 -0
  146. {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/html.py +0 -0
  147. {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/html_template.py +0 -0
  148. {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/markdown.py +0 -0
  149. {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/mdx.py +0 -0
  150. {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/sphinx.py +0 -0
  151. {codeboarding-0.10.4 → codeboarding-0.11.0}/repo_utils/errors.py +0 -0
  152. {codeboarding-0.10.4 → codeboarding-0.11.0}/repo_utils/ignore.py +0 -0
  153. {codeboarding-0.10.4 → codeboarding-0.11.0}/setup.cfg +0 -0
  154. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/analysis_cache.py +0 -0
  155. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cfg_skip_planner.py +0 -0
  156. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cluster_change_analyzer.py +0 -0
  157. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cluster_helpers.py +0 -0
  158. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cluster_relations.py +0 -0
  159. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/csharp_config_scanner.py +0 -0
  160. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/__init__.py +0 -0
  161. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/__init__.py +0 -0
  162. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/call_graph_builder.py +0 -0
  163. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/edge_build_context.py +0 -0
  164. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/edge_builder.py +0 -0
  165. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/hierarchy_builder.py +0 -0
  166. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/lsp_client.py +0 -0
  167. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/lsp_constants.py +0 -0
  168. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/models.py +0 -0
  169. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/progress.py +0 -0
  170. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/protocols.py +0 -0
  171. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/result_converter.py +0 -0
  172. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/source_inspector.py +0 -0
  173. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/symbol_table.py +0 -0
  174. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/utils.py +0 -0
  175. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/graph.py +0 -0
  176. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/java_config_scanner.py +0 -0
  177. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/java_utils.py +0 -0
  178. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/lsp_client/__init__.py +0 -0
  179. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/lsp_client/diagnostics.py +0 -0
  180. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/node.py +0 -0
  181. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/programming_language.py +0 -0
  182. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/reference_resolve_mixin.py +0 -0
  183. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/scanner.py +0 -0
  184. {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/typescript_config_scanner.py +0 -0
  185. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_install.py +0 -0
  186. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_logging_config.py +0 -0
  187. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_pyproject_packages.py +0 -0
  188. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_registry_coverage.py +0 -0
  189. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_tool_registry.py +0 -0
  190. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_user_config.py +0 -0
  191. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_vscode_constants.py +0 -0
  192. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_windows_compatibility.py +0 -0
  193. {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_windows_encoding.py +0 -0
  194. {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/__init__.py +0 -0
  195. {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/installers.py +0 -0
  196. {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/manifest.py +0 -0
  197. {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/paths.py +0 -0
  198. {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/registry.py +0 -0
  199. {codeboarding-0.10.4 → codeboarding-0.11.0}/user_config.py +0 -0
  200. {codeboarding-0.10.4 → codeboarding-0.11.0}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.10.4
3
+ Version: 0.11.0
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -24,6 +24,7 @@ Requires-Dist: fastapi>=0.115
24
24
  Requires-Dist: filelock>=3.12
25
25
  Requires-Dist: gitpython>=3.1
26
26
  Requires-Dist: google-api-core>=2.10
27
+ Requires-Dist: jsonpatch>=1.33
27
28
  Requires-Dist: jsonschema>=4.25
28
29
  Requires-Dist: langchain>=1.2
29
30
  Requires-Dist: langchain-anthropic>=1.3
@@ -42,6 +43,15 @@ Requires-Dist: pathspec>=0.12
42
43
  Requires-Dist: pyyaml>=6.0
43
44
  Requires-Dist: regex>=2024.11
44
45
  Requires-Dist: rich>=12.6
46
+ Requires-Dist: tree-sitter>=0.23
47
+ Requires-Dist: tree-sitter-c-sharp>=0.23
48
+ Requires-Dist: tree-sitter-go>=0.23
49
+ Requires-Dist: tree-sitter-java>=0.23
50
+ Requires-Dist: tree-sitter-javascript>=0.23
51
+ Requires-Dist: tree-sitter-php>=0.23
52
+ Requires-Dist: tree-sitter-python>=0.23
53
+ Requires-Dist: tree-sitter-rust>=0.23
54
+ Requires-Dist: tree-sitter-typescript>=0.23
45
55
  Requires-Dist: trustcall>=0.0.39
46
56
  Requires-Dist: uvicorn>=0.23
47
57
  Provides-Extra: dev
@@ -110,10 +120,10 @@ codeboarding-setup
110
120
 
111
121
  ```bash
112
122
  # Analyze a local repository (output goes to /path/to/repo/.codeboarding/)
113
- codeboarding --local /path/to/repo
123
+ codeboarding full --local /path/to/repo
114
124
 
115
125
  # Analyze a remote GitHub repository (cloned to cwd/repo_name/, output to cwd/repo_name/.codeboarding/)
116
- codeboarding https://github.com/user/repo
126
+ codeboarding full https://github.com/user/repo
117
127
  ```
118
128
 
119
129
  ### Python API
@@ -188,19 +198,21 @@ Shell environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) always
188
198
  ## CLI Reference
189
199
 
190
200
  ```
191
- codeboarding [REPO_URL ...] # remote: clone + analyze
192
- codeboarding --local PATH # local: analyze in-place
201
+ codeboarding full [REPO_URL ...] # remote: clone + analyze
202
+ codeboarding full --local PATH # local: analyze in-place
203
+ codeboarding incremental --local PATH # re-analyze only changed parts
204
+ codeboarding partial --local PATH --component-id ID # update one component
193
205
  ```
194
206
 
195
207
  | Option | Description |
196
208
  |---|---|
197
209
  | `--local PATH` | Analyze a local repository (output: `PATH/.codeboarding/`) |
198
210
  | `--depth-level INT` | Diagram depth (default: 1) |
199
- | `--incremental` | Smart incremental update (only re-analyze changed files) |
200
- | `--full` | Force full reanalysis, skip incremental detection |
201
- | `--partial-component-id ID` | Update a single component by its ID |
211
+ | `--force` | (full only) Force full reanalysis, skip cached static analysis |
212
+ | `--base-ref REF` / `--target-ref REF` | (incremental only) Git refs to diff |
213
+ | `--component-id ID` | (partial only) ID of the component to update |
202
214
  | `--binary-location PATH` | Custom path to language server binaries (overrides `~/.codeboarding/servers/`) |
203
- | `--upload` | Upload results to GeneratedOnBoardings repo (remote only) |
215
+ | `--upload` | (full, remote only) Upload results to GeneratedOnBoardings repo |
204
216
  | `--enable-monitoring` | Enable run monitoring |
205
217
 
206
218
  ---
@@ -50,10 +50,10 @@ codeboarding-setup
50
50
 
51
51
  ```bash
52
52
  # Analyze a local repository (output goes to /path/to/repo/.codeboarding/)
53
- codeboarding --local /path/to/repo
53
+ codeboarding full --local /path/to/repo
54
54
 
55
55
  # Analyze a remote GitHub repository (cloned to cwd/repo_name/, output to cwd/repo_name/.codeboarding/)
56
- codeboarding https://github.com/user/repo
56
+ codeboarding full https://github.com/user/repo
57
57
  ```
58
58
 
59
59
  ### Python API
@@ -128,19 +128,21 @@ Shell environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) always
128
128
  ## CLI Reference
129
129
 
130
130
  ```
131
- codeboarding [REPO_URL ...] # remote: clone + analyze
132
- codeboarding --local PATH # local: analyze in-place
131
+ codeboarding full [REPO_URL ...] # remote: clone + analyze
132
+ codeboarding full --local PATH # local: analyze in-place
133
+ codeboarding incremental --local PATH # re-analyze only changed parts
134
+ codeboarding partial --local PATH --component-id ID # update one component
133
135
  ```
134
136
 
135
137
  | Option | Description |
136
138
  |---|---|
137
139
  | `--local PATH` | Analyze a local repository (output: `PATH/.codeboarding/`) |
138
140
  | `--depth-level INT` | Diagram depth (default: 1) |
139
- | `--incremental` | Smart incremental update (only re-analyze changed files) |
140
- | `--full` | Force full reanalysis, skip incremental detection |
141
- | `--partial-component-id ID` | Update a single component by its ID |
141
+ | `--force` | (full only) Force full reanalysis, skip cached static analysis |
142
+ | `--base-ref REF` / `--target-ref REF` | (incremental only) Git refs to diff |
143
+ | `--component-id ID` | (partial only) ID of the component to update |
142
144
  | `--binary-location PATH` | Custom path to language server binaries (overrides `~/.codeboarding/servers/`) |
143
- | `--upload` | Upload results to GeneratedOnBoardings repo (remote only) |
145
+ | `--upload` | (full, remote only) Upload results to GeneratedOnBoardings repo |
144
146
  | `--enable-monitoring` | Enable run monitoring |
145
147
 
146
148
  ---
@@ -70,7 +70,7 @@ For a deeper architecture walkthrough, see [`.codeboarding/overview.md`](.codebo
70
70
  uv sync --frozen
71
71
  source .venv/bin/activate # On Windows: .venv\Scripts\activate
72
72
  python install.py
73
- python main.py --local /path/to/repo
73
+ python main.py full --local /path/to/repo
74
74
  ```
75
75
 
76
76
  ### Use the packaged CLI
@@ -80,7 +80,7 @@ Requires **Python 3.12 or 3.13**. The recommended install method is [pipx](https
80
80
  ```bash
81
81
  pipx install codeboarding --python python3.12
82
82
  codeboarding-setup
83
- codeboarding --local /path/to/repo
83
+ codeboarding full --local /path/to/repo
84
84
  ```
85
85
 
86
86
  Or, if you prefer pip, install into a virtual environment (not the global Python):
@@ -88,7 +88,7 @@ Or, if you prefer pip, install into a virtual environment (not the global Python
88
88
  ```bash
89
89
  pip install codeboarding
90
90
  codeboarding-setup
91
- codeboarding --local /path/to/repo
91
+ codeboarding full --local /path/to/repo
92
92
  ```
93
93
 
94
94
  Output is written to `/path/to/repo/.codeboarding/`.
@@ -120,19 +120,19 @@ Shell environment variables such as `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOG
120
120
 
121
121
  ```bash
122
122
  # Analyze a local repository
123
- python main.py --local ./my-project
123
+ python main.py full --local ./my-project
124
124
 
125
125
  # Increase diagram depth
126
- python main.py --local ./my-project --depth-level 2
126
+ python main.py full --local ./my-project --depth-level 2
127
127
 
128
128
  # Re-analyze only changed parts when possible
129
- python main.py --local ./my-project --incremental
129
+ python main.py incremental --local ./my-project
130
130
 
131
131
  # Update a single component by ID
132
- python main.py --local ./my-project --partial-component-id "1.2"
132
+ python main.py partial --local ./my-project --component-id "1.2"
133
133
 
134
134
  # Analyze a remote GitHub repository
135
- python main.py https://github.com/pytorch/pytorch
135
+ python main.py full https://github.com/pytorch/pytorch
136
136
  ```
137
137
 
138
138
  ## Where to use it
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import logging
3
- import time
4
3
  from pathlib import Path
5
4
 
6
5
  from google.api_core.exceptions import ResourceExhausted
@@ -15,6 +14,7 @@ from pydantic import ValidationError
15
14
  from trustcall import create_extractor
16
15
 
17
16
  from agents.prompts import get_validation_feedback_message
17
+ from agents.retry import RetryAction, RetryDecision, default_backoff, with_retries
18
18
  from agents.tools.base import RepoContext
19
19
  from agents.tools.toolkit import CodeBoardingToolkit
20
20
  from agents.validation import ValidationResult, score_validation_results, VALIDATOR_WEIGHTS, DEFAULT_VALIDATOR_WEIGHT
@@ -96,86 +96,69 @@ class CodeBoardingAgent(ReferenceResolverMixin, MonitoringMixin):
96
96
  def _invoke(self, prompt, callbacks: list | None = None) -> str:
97
97
  """Unified agent invocation method with timeout and exponential backoff.
98
98
 
99
- Uses exponential backoff based on total attempts, with different multipliers
100
- for different error types. This ensures backoff increases appropriately even
101
- when errors alternate between types.
99
+ Classification applied per exception:
100
+ - ``TimeoutError``: backoff ``min(10·2^n, 120)``, raise on exhaustion.
101
+ - ``ResourceExhausted``: backoff ``min(30·2^n, 300)``, raise on exhaustion.
102
+ - ``status_code == 404``: raise immediately (retired model ID, etc.).
103
+ - Other exceptions: backoff ``min(10·2^n, 120)``, return fallback string
104
+ on exhaustion (non-raising — callers treat the fallback as a failed run).
102
105
  """
103
- max_retries = 5
104
-
105
- for attempt in range(max_retries):
106
+ max_attempts = 5
107
+ # Counter captured by the closure so we can vary the per-attempt timeout
108
+ # without reaching into the retry helper.
109
+ attempt_counter = [0]
110
+
111
+ def call_once() -> str:
112
+ attempt = attempt_counter[0]
113
+ attempt_counter[0] += 1
106
114
  timeout_seconds = 300 if attempt == 0 else 600
107
- try:
108
- callback_list = callbacks or []
109
- # Always append monitoring callback - logging config controls output
110
- callback_list.append(MONITORING_CALLBACK)
111
- callback_list.append(self.agent_monitoring_callback)
112
-
113
- logger.info(
114
- f"Starting agent.invoke() [attempt {attempt + 1}/{max_retries}] with prompt length: {len(prompt)}, timeout: {timeout_seconds}s"
115
- )
116
-
117
- response = self._invoke_with_timeout(
118
- timeout_seconds=timeout_seconds, callback_list=callback_list, prompt=prompt
119
- )
120
-
121
- logger.info(
122
- f"Completed agent.invoke() - message count: {len(response['messages'])}, last message type: {type(response['messages'][-1])}"
115
+ callback_list = (callbacks or []) + [MONITORING_CALLBACK, self.agent_monitoring_callback]
116
+ logger.info(
117
+ f"Starting agent.invoke() [attempt {attempt + 1}/{max_attempts}] with prompt length: {len(prompt)}, timeout: {timeout_seconds}s"
118
+ )
119
+ response = self._invoke_with_timeout(
120
+ timeout_seconds=timeout_seconds, callback_list=callback_list, prompt=prompt
121
+ )
122
+ logger.info(
123
+ f"Completed agent.invoke() - message count: {len(response['messages'])}, last message type: {type(response['messages'][-1])}"
124
+ )
125
+ agent_response = response["messages"][-1]
126
+ assert isinstance(agent_response, AIMessage), f"Expected AIMessage, but got {type(agent_response)}"
127
+ if isinstance(agent_response.content, str):
128
+ return agent_response.content
129
+ if isinstance(agent_response.content, list):
130
+ return "".join(str(m) if not isinstance(m, str) else m for m in agent_response.content)
131
+ return "" # unreachable for AIMessage but satisfies typing
132
+
133
+ def classify(exc: Exception, attempt: int) -> RetryDecision:
134
+ if getattr(exc, "status_code", None) == 404:
135
+ logger.error(f"Permanent HTTP 404 — not retrying: {type(exc).__name__}: {exc}")
136
+ return RetryDecision(action=RetryAction.GIVE_UP)
137
+ if isinstance(exc, ResourceExhausted):
138
+ return RetryDecision(
139
+ action=RetryAction.RETRY,
140
+ backoff_s=default_backoff(attempt, initial_s=30.0, multiplier=2.0, max_s=300.0),
123
141
  )
142
+ # TimeoutError + generic Exception share the same backoff.
143
+ return RetryDecision(
144
+ action=RetryAction.RETRY,
145
+ backoff_s=default_backoff(attempt, initial_s=10.0, multiplier=2.0, max_s=120.0),
146
+ )
124
147
 
125
- agent_response = response["messages"][-1]
126
- assert isinstance(agent_response, AIMessage), f"Expected AIMessage, but got {type(agent_response)}"
127
- if isinstance(agent_response.content, str):
128
- return agent_response.content
129
- if isinstance(agent_response.content, list):
130
- return "".join(
131
- [
132
- str(message) if not isinstance(message, str) else message
133
- for message in agent_response.content
134
- ]
135
- )
136
-
137
- except TimeoutError as e:
138
- if attempt < max_retries - 1:
139
- # Exponential backoff: 10s * 2^attempt (10s, 20s, 40s, 80s)
140
- delay = min(10 * (2**attempt), 120)
141
- logger.warning(
142
- f"Agent invocation timed out after {timeout_seconds}s, retrying in {delay}s... (attempt {attempt + 1}/{max_retries})"
143
- )
144
- time.sleep(delay)
145
- else:
146
- logger.error(f"Agent invocation timed out after {timeout_seconds}s on final attempt")
147
- raise
148
-
149
- except ResourceExhausted as e:
150
- if attempt < max_retries - 1:
151
- # Longer backoff for rate limits: 30s * 2^attempt (30s, 60s, 120s, 240s)
152
- delay = min(30 * (2**attempt), 300)
153
- logger.warning(
154
- f"ResourceExhausted (rate limit): {e}\n"
155
- f"Retrying in {delay}s... (attempt {attempt + 1}/{max_retries})"
156
- )
157
- time.sleep(delay)
158
- else:
159
- logger.error(f"Max retries ({max_retries}) reached. ResourceExhausted: {e}")
160
- raise
161
-
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
-
168
- # Other errors (network, parsing, etc.) get standard exponential backoff
169
- if attempt < max_retries - 1:
170
- delay = min(10 * (2**attempt), 120)
171
- logger.warning(
172
- f"Agent error: {type(e).__name__}: {e}, retrying in {delay}s... (attempt {attempt + 1}/{max_retries})"
173
- )
174
- time.sleep(delay)
175
- # On final attempt, fall through to return error message below
176
-
177
- logger.error("Max retries reached. Failed to get response from the agent.")
178
- return "Could not get response from the agent."
148
+ def on_exhausted(exc: Exception) -> str:
149
+ # Typed exceptions surface the original error; only generic falls through
150
+ # to the historic fallback string that callers have long relied on.
151
+ if isinstance(exc, (TimeoutError, ResourceExhausted)):
152
+ raise exc
153
+ return "Could not get response from the agent."
154
+
155
+ return with_retries(
156
+ call_once,
157
+ max_attempts=max_attempts,
158
+ classify=classify,
159
+ on_exhausted=on_exhausted,
160
+ log_prefix="Agent invocation",
161
+ )
179
162
 
180
163
  def _invoke_with_timeout(self, timeout_seconds: int, callback_list: list, prompt: str):
181
164
  """Invoke agent with a timeout using threading."""
@@ -336,18 +319,27 @@ class CodeBoardingAgent(ReferenceResolverMixin, MonitoringMixin):
336
319
  return best_result
337
320
 
338
321
  def _parse_response(self, prompt, response, return_type, max_retries=5, attempt=0):
339
- if attempt >= max_retries:
340
- logger.error(f"Max retries ({max_retries}) reached for parsing response: {response}")
341
- raise Exception(f"Max retries reached for parsing response: {response}")
342
-
343
- extractor = create_extractor(self.parsing_llm, tools=[return_type], tool_choice=return_type.__name__)
344
322
  if response is None or response.strip() == "":
345
323
  logger.error(f"Empty response for prompt: {prompt}")
346
- try:
347
- result = extractor.invoke(
348
- return_type.extractor_str() + response,
349
- config={"callbacks": [MONITORING_CALLBACK, self.agent_monitoring_callback]},
350
- )
324
+
325
+ def call_once():
326
+ # Extractor is rebuilt on every attempt — previous trustcall state
327
+ # may have corrupted attributes (see the tool_call_id bug below).
328
+ extractor = create_extractor(self.parsing_llm, tools=[return_type], tool_choice=return_type.__name__)
329
+ try:
330
+ result = extractor.invoke(
331
+ return_type.extractor_str() + response,
332
+ config={"callbacks": [MONITORING_CALLBACK, self.agent_monitoring_callback]},
333
+ )
334
+ except AttributeError as e:
335
+ # Trustcall bug: https://github.com/hinthornw/trustcall/issues/47
336
+ # 'ExtractionState' object has no attribute 'tool_call_id' during validation retry.
337
+ # Treat as a non-retriable fallback to the Pydantic parser.
338
+ if "tool_call_id" in str(e):
339
+ logger.warning(f"Trustcall bug encountered, falling back to Pydantic parser: {e}")
340
+ parser = PydanticOutputParser(pydantic_object=return_type)
341
+ return self._try_parse(response, parser)
342
+ raise
351
343
  if "responses" in result and len(result["responses"]) != 0:
352
344
  return return_type.model_validate(result["responses"][0])
353
345
  if "messages" in result and len(result["messages"]) != 0:
@@ -358,38 +350,36 @@ class CodeBoardingAgent(ReferenceResolverMixin, MonitoringMixin):
358
350
  return self._try_parse(message, parser)
359
351
  parser = PydanticOutputParser(pydantic_object=return_type)
360
352
  return self._try_parse(response, parser)
361
- except EmptyExtractorMessageError as e:
362
- logger.warning(f"{e} (attempt {attempt + 1}/{max_retries})")
363
- return self._parse_response(prompt, response, return_type, max_retries, attempt + 1)
364
- except AttributeError as e:
365
- # Workaround for trustcall bug: https://github.com/hinthornw/trustcall/issues/47
366
- # 'ExtractionState' object has no attribute 'tool_call_id' occurs during validation retry
367
- if "tool_call_id" in str(e):
368
- logger.warning(f"Trustcall bug encountered, falling back to Pydantic parser: {e}")
369
- parser = PydanticOutputParser(pydantic_object=return_type)
370
- return self._try_parse(response, parser)
371
- raise
372
- except IndexError as e:
373
- # try to parse with the json parser if possible
374
- logger.warning(f"IndexError while parsing response (attempt {attempt + 1}/{max_retries}): {e}")
375
- return self._parse_response(prompt, response, return_type, max_retries, attempt + 1)
376
- except (json.JSONDecodeError, ValueError) as e:
377
- logger.warning(f"Parse error (attempt {attempt + 1}/{max_retries}): {e}")
378
- return self._parse_response(prompt, response, return_type, max_retries, attempt + 1)
379
- except ResourceExhausted as e:
380
- # Parsing uses exponential backoff for rate limits
381
- if attempt < max_retries - 1:
382
- # Exponential backoff: 30s * 2^attempt, capped at 300s
383
- delay = min(30 * (2**attempt), 300)
384
- logger.warning(
385
- f"ResourceExhausted during parsing (rate limit): {e}\n"
386
- f"Retrying in {delay}s... (attempt {attempt + 1}/{max_retries})"
353
+
354
+ def classify(exc: Exception, attempt: int) -> RetryDecision:
355
+ if isinstance(exc, ResourceExhausted):
356
+ return RetryDecision(
357
+ action=RetryAction.RETRY,
358
+ backoff_s=default_backoff(attempt, initial_s=30.0, multiplier=2.0, max_s=300.0),
387
359
  )
388
- time.sleep(delay)
389
- return self._parse_response(prompt, response, return_type, max_retries, attempt + 1)
390
- else:
391
- logger.error(f"Resource exhausted on final parsing attempt: {e}")
392
- raise
360
+ if isinstance(exc, (EmptyExtractorMessageError, IndexError, json.JSONDecodeError, ValueError)):
361
+ return RetryDecision(action=RetryAction.RETRY_NOW)
362
+ # AttributeError (non-tool_call_id) and any other exception: give up.
363
+ return RetryDecision(action=RetryAction.GIVE_UP)
364
+
365
+ def on_exhausted(exc: Exception):
366
+ # Preserve historic shape: ResourceExhausted surfaces the original exception;
367
+ # parse-error exhaustion wraps with a descriptive message naming the response.
368
+ if isinstance(exc, ResourceExhausted):
369
+ logger.error(f"Resource exhausted on final parsing attempt: {exc}")
370
+ raise exc
371
+ logger.error(f"Max retries ({max_retries}) reached for parsing response: {response}")
372
+ raise Exception(f"Max retries reached for parsing response: {response}")
373
+
374
+ # ``attempt`` kwarg kept for backwards-compat with callers that passed it;
375
+ # the effective attempt count is ``max_retries - attempt``.
376
+ return with_retries(
377
+ call_once,
378
+ max_attempts=max(1, max_retries - attempt),
379
+ classify=classify,
380
+ on_exhausted=on_exhausted,
381
+ log_prefix="Parse response",
382
+ )
393
383
 
394
384
  def _try_parse(self, message_content, parser):
395
385
  try:
@@ -160,6 +160,16 @@ class MethodEntry(BaseModel):
160
160
  node_type=method_change.node_type,
161
161
  )
162
162
 
163
+ @classmethod
164
+ def from_node(cls, node) -> MethodEntry:
165
+ """Build from a ``static_analyzer.Node``. Accepts ``Any`` to avoid a hard dep."""
166
+ return cls(
167
+ qualified_name=node.fully_qualified_name,
168
+ start_line=node.line_start,
169
+ end_line=node.line_end,
170
+ node_type=node.type.name,
171
+ )
172
+
163
173
 
164
174
  class FileMethodGroup(BaseModel):
165
175
  """All methods/functions belonging to a component within a single file."""
@@ -0,0 +1,206 @@
1
+ """EASE-encoded JSON Patch flow for incremental sub-analysis updates.
2
+
3
+ Given impacted components from the tracer, extracts the parent sub-analysis,
4
+ EASE-encodes it, asks the LLM for RFC 6902 patches, applies them, validates
5
+ the result, and merges back into the full analysis.
6
+
7
+ Structured output uses trustcall's ``create_extractor`` — the same pattern
8
+ ``agents/agent.py`` uses for parsing LLM responses into Pydantic models. The
9
+ extractor binds ``AnalysisPatch`` as the tool schema and forces the LLM to emit
10
+ a tool call matching it, so ``result["responses"][0]`` is already schema-valid.
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ from typing import Any
16
+
17
+ import jsonpatch
18
+ from langchain_core.language_models import BaseChatModel
19
+ from pydantic import ValidationError
20
+ from trustcall import create_extractor
21
+
22
+ from agents.agent_responses import AnalysisInsights
23
+ from agents.prompts.prompt_factory import get_patch_system_message
24
+ from diagram_analysis.ease import ease_decode, ease_encode
25
+ from diagram_analysis.incremental.models import AnalysisPatch, ImpactedComponent
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ MAX_PATCH_RETRIES = 3
30
+
31
+ # Fields excluded from the patching surface. `files` is the file-level index
32
+ # managed separately by the analysis pipeline; patching it here would race with
33
+ # the static-analysis update path.
34
+ _PATCH_EXCLUDE_TOP_LEVEL: set[str] = {"files"}
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Pydantic-native serialization
39
+ # ---------------------------------------------------------------------------
40
+ def _sub_analysis_to_dict(sub: AnalysisInsights) -> dict[str, Any]:
41
+ """Serialize a sub-analysis to a plain dict via Pydantic's model_dump."""
42
+ return sub.model_dump(mode="json", exclude=_PATCH_EXCLUDE_TOP_LEVEL, exclude_none=False)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # EASE encoding / decoding (walks nested arrays)
47
+ # ---------------------------------------------------------------------------
48
+ def _encode_sub_analysis(sub_dict: dict[str, Any]) -> dict[str, Any]:
49
+ """EASE-encode a sub-analysis dict."""
50
+ encoded = ease_encode(sub_dict, ["components", "components_relations"])
51
+ if isinstance(encoded.get("components"), dict):
52
+ for key, comp in encoded["components"].items():
53
+ if key == "display_order" or not isinstance(comp, dict):
54
+ continue
55
+ encoded["components"][key] = ease_encode(comp, ["key_entities", "file_methods"])
56
+ if isinstance(encoded["components"][key].get("file_methods"), dict):
57
+ for fm_key, fm in encoded["components"][key]["file_methods"].items():
58
+ if fm_key == "display_order" or not isinstance(fm, dict):
59
+ continue
60
+ encoded["components"][key]["file_methods"][fm_key] = ease_encode(fm, ["methods"])
61
+ return encoded
62
+
63
+
64
+ def _decode_sub_analysis(encoded: dict[str, Any]) -> dict[str, Any]:
65
+ """Decode EASE-encoded sub-analysis back to plain arrays."""
66
+ if isinstance(encoded.get("components"), dict):
67
+ for key, comp in encoded["components"].items():
68
+ if key == "display_order" or not isinstance(comp, dict):
69
+ continue
70
+ if isinstance(comp.get("file_methods"), dict):
71
+ for fm_key, fm in comp["file_methods"].items():
72
+ if fm_key == "display_order" or not isinstance(fm, dict):
73
+ continue
74
+ comp["file_methods"][fm_key] = ease_decode(fm, ["methods"])
75
+ encoded["components"][key] = ease_decode(comp, ["key_entities", "file_methods"])
76
+ return ease_decode(encoded, ["components", "components_relations"])
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Prompt construction
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ def _build_patch_prompt(
85
+ encoded_sub: dict[str, Any],
86
+ impact: ImpactedComponent,
87
+ sub_analysis_id: str,
88
+ ) -> str:
89
+ parts = [
90
+ "# Current Sub-Analysis (EASE-encoded)\n",
91
+ f"```json\n{json.dumps(encoded_sub, indent=2)}\n```\n",
92
+ "# Impact Dossier\n",
93
+ f"Sub-analysis ID: {sub_analysis_id}\n",
94
+ f"Impacted methods: {', '.join(impact.impacted_methods)}\n",
95
+ "\nGenerate a patch to update component descriptions, key_entities, and relations",
96
+ "to reflect the semantic changes indicated by the impacted methods.",
97
+ "Respond with sub_analysis_id, reasoning, and patches (list of op/path/value).",
98
+ ]
99
+ return "\n".join(parts)
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Apply patches
104
+ # ---------------------------------------------------------------------------
105
+ def _apply_patches(encoded: dict[str, Any], patch_ops: list[dict]) -> dict[str, Any]:
106
+ """Apply RFC 6902 patches to an EASE-encoded dict."""
107
+ patch = jsonpatch.JsonPatch(patch_ops)
108
+ return patch.apply(encoded)
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Validation via Pydantic native round-trip
113
+ # ---------------------------------------------------------------------------
114
+ def _validate_patched(decoded: dict[str, Any], original: AnalysisInsights) -> AnalysisInsights | None:
115
+ """Validate that a decoded sub-analysis round-trips to AnalysisInsights.
116
+
117
+ The decoded dict is missing excluded fields (see ``_PATCH_EXCLUDE_TOP_LEVEL``).
118
+ Those fields are re-grafted directly from the original model so the result
119
+ is complete. We cannot re-dump them because ``exclude=True`` Field settings
120
+ drop them entirely — we copy the live model attributes instead.
121
+ """
122
+ try:
123
+ validated = AnalysisInsights.model_validate(decoded)
124
+ for field_name in _PATCH_EXCLUDE_TOP_LEVEL:
125
+ original_value = getattr(original, field_name, None)
126
+ if original_value is not None:
127
+ setattr(validated, field_name, original_value)
128
+ return validated
129
+ except (ValidationError, KeyError, TypeError) as exc:
130
+ logger.warning("Patched sub-analysis failed validation: %s", exc)
131
+ return None
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Main patch flow
136
+ # ---------------------------------------------------------------------------
137
+ def patch_sub_analysis(
138
+ sub_analysis: AnalysisInsights,
139
+ sub_analysis_id: str,
140
+ impact: ImpactedComponent,
141
+ parsing_llm: BaseChatModel,
142
+ ) -> AnalysisInsights | None:
143
+ """Patch a single sub-analysis using EASE-encoded JSON patches.
144
+
145
+ Returns the patched AnalysisInsights, or None if patching fails after retries.
146
+ """
147
+ sub_dict = _sub_analysis_to_dict(sub_analysis)
148
+ encoded = _encode_sub_analysis(sub_dict)
149
+
150
+ prompt = _build_patch_prompt(encoded, impact, sub_analysis_id)
151
+ extractor = create_extractor(parsing_llm, tools=[AnalysisPatch], tool_choice=AnalysisPatch.__name__)
152
+
153
+ last_error = ""
154
+ for attempt in range(MAX_PATCH_RETRIES):
155
+ try:
156
+ full_prompt = get_patch_system_message() + "\n\n" + prompt
157
+ if last_error:
158
+ full_prompt += f"\n\nPrevious attempt failed validation: {last_error}\nPlease fix the patch."
159
+
160
+ result = extractor.invoke(full_prompt)
161
+ if "responses" not in result or not result["responses"]:
162
+ logger.warning("Patch extractor returned no responses (attempt %d)", attempt + 1)
163
+ continue
164
+
165
+ patch_response = AnalysisPatch.model_validate(result["responses"][0])
166
+ patch_ops = [op.model_dump(exclude_none=True) for op in patch_response.patches]
167
+
168
+ if not patch_ops:
169
+ logger.info("LLM returned empty patch for %s — no changes needed", sub_analysis_id)
170
+ return sub_analysis
171
+
172
+ patched = _apply_patches(encoded, patch_ops)
173
+ decoded = _decode_sub_analysis(patched)
174
+ validated = _validate_patched(decoded, sub_analysis)
175
+
176
+ if validated is not None:
177
+ logger.info("Successfully patched sub-analysis %s on attempt %d", sub_analysis_id, attempt + 1)
178
+ return validated
179
+
180
+ last_error = "Decoded result failed Pydantic validation"
181
+
182
+ except jsonpatch.JsonPatchException as exc:
183
+ last_error = f"JSON Patch application error: {exc}"
184
+ logger.warning("Patch apply failed for %s (attempt %d): %s", sub_analysis_id, attempt + 1, exc)
185
+ except Exception as exc:
186
+ last_error = str(exc)
187
+ logger.warning("Patch flow error for %s (attempt %d): %s", sub_analysis_id, attempt + 1, exc)
188
+
189
+ logger.error("Patching failed for sub-analysis %s after %d attempts", sub_analysis_id, MAX_PATCH_RETRIES)
190
+ return None
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Merge patched sub-analyses back
195
+ # ---------------------------------------------------------------------------
196
+ def merge_patched_sub_analyses(
197
+ sub_analyses: dict[str, AnalysisInsights],
198
+ patched: dict[str, AnalysisInsights],
199
+ ) -> None:
200
+ """Merge patched sub-analyses back into the analysis structures. Mutates in place."""
201
+ for sub_id, patched_sub in patched.items():
202
+ if sub_id in sub_analyses:
203
+ sub_analyses[sub_id] = patched_sub
204
+ logger.info("Merged patched sub-analysis %s", sub_id)
205
+ else:
206
+ logger.warning("Patched sub-analysis %s not found in existing sub_analyses", sub_id)