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.
- {codeboarding-0.10.4/codeboarding.egg-info → codeboarding-0.11.0}/PKG-INFO +21 -9
- {codeboarding-0.10.4 → codeboarding-0.11.0}/PYPI.md +10 -8
- {codeboarding-0.10.4 → codeboarding-0.11.0}/README.md +8 -8
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/agent.py +108 -118
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/agent_responses.py +10 -0
- codeboarding-0.11.0/agents/analysis_patcher.py +206 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/llm_config.py +16 -3
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/abstract_prompt_factory.py +8 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/claude_prompts.py +37 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/deepseek_prompts.py +37 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/gemini_flash_prompts.py +37 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/glm_prompts.py +37 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/gpt_prompts.py +37 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/kimi_prompts.py +37 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/prompt_factory.py +8 -0
- codeboarding-0.11.0/agents/retry.py +118 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/toolkit.py +0 -8
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/validation.py +12 -29
- {codeboarding-0.10.4 → codeboarding-0.11.0/codeboarding.egg-info}/PKG-INFO +21 -9
- {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/SOURCES.txt +29 -6
- {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/requires.txt +10 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/top_level.txt +2 -0
- codeboarding-0.11.0/codeboarding_cli/__init__.py +1 -0
- codeboarding-0.11.0/codeboarding_cli/bootstrap.py +53 -0
- codeboarding-0.11.0/codeboarding_cli/commands/__init__.py +1 -0
- codeboarding-0.11.0/codeboarding_cli/commands/full_analysis.py +197 -0
- codeboarding-0.11.0/codeboarding_cli/commands/incremental_analysis.py +137 -0
- codeboarding-0.11.0/codeboarding_cli/commands/partial_analysis.py +69 -0
- codeboarding-0.11.0/codeboarding_workflows/__init__.py +14 -0
- codeboarding-0.11.0/codeboarding_workflows/analysis.py +144 -0
- codeboarding-0.11.0/codeboarding_workflows/orchestration.py +48 -0
- codeboarding-0.11.0/codeboarding_workflows/rendering.py +92 -0
- codeboarding-0.11.0/codeboarding_workflows/sources/__init__.py +12 -0
- codeboarding-0.11.0/codeboarding_workflows/sources/local.py +23 -0
- codeboarding-0.11.0/codeboarding_workflows/sources/remote.py +71 -0
- codeboarding-0.11.0/diagram_analysis/__init__.py +22 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/diagram_generator.py +293 -10
- codeboarding-0.11.0/diagram_analysis/ease.py +68 -0
- codeboarding-0.10.4/diagram_analysis/incremental_types.py → codeboarding-0.11.0/diagram_analysis/incremental/delta.py +23 -9
- codeboarding-0.11.0/diagram_analysis/incremental/models.py +220 -0
- codeboarding-0.11.0/diagram_analysis/incremental/payload.py +129 -0
- codeboarding-0.11.0/diagram_analysis/incremental/pipeline.py +264 -0
- codeboarding-0.11.0/diagram_analysis/incremental/semantic_diff.py +557 -0
- codeboarding-0.11.0/diagram_analysis/incremental/trace_planner.py +435 -0
- codeboarding-0.11.0/diagram_analysis/incremental/tracer.py +458 -0
- codeboarding-0.10.4/diagram_analysis/incremental_updater.py → codeboarding-0.11.0/diagram_analysis/incremental/updater.py +149 -89
- {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/io_utils.py +3 -2
- codeboarding-0.11.0/diagram_analysis/run_metadata.py +146 -0
- codeboarding-0.11.0/github_action.py +129 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/install.py +1 -1
- {codeboarding-0.10.4 → codeboarding-0.11.0}/logging_config.py +1 -1
- codeboarding-0.11.0/main.py +99 -0
- codeboarding-0.11.0/output_generators/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/pyproject.toml +45 -3
- {codeboarding-0.10.4 → codeboarding-0.11.0}/repo_utils/__init__.py +2 -5
- codeboarding-0.11.0/repo_utils/change_detector.py +311 -0
- codeboarding-0.11.0/repo_utils/diff_parser.py +277 -0
- codeboarding-0.11.0/repo_utils/git_ops.py +283 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/__init__.py +2 -2
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/analysis_result.py +38 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/constants.py +31 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/csharp_adapter.py +3 -3
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/go_adapter.py +3 -2
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/java_adapter.py +3 -3
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/php_adapter.py +3 -3
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/python_adapter.py +3 -2
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/rust_adapter.py +5 -4
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/typescript_adapter.py +5 -4
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/language_adapter.py +15 -2
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/incremental_orchestrator.py +11 -11
- codeboarding-0.11.0/tests/test_cli_parser.py +70 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_github_action.py +11 -11
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_main.py +212 -218
- {codeboarding-0.10.4 → codeboarding-0.11.0}/utils.py +14 -1
- codeboarding-0.10.4/agents/tools/read_git_diff.py +0 -131
- codeboarding-0.10.4/diagram_analysis/__init__.py +0 -3
- codeboarding-0.10.4/github_action.py +0 -173
- codeboarding-0.10.4/main.py +0 -567
- codeboarding-0.10.4/repo_utils/change_detector.py +0 -294
- codeboarding-0.10.4/repo_utils/git_diff.py +0 -74
- codeboarding-0.10.4/repo_utils/method_diff.py +0 -162
- codeboarding-0.10.4/static_analyzer/git_diff_analyzer.py +0 -224
- {codeboarding-0.10.4 → codeboarding-0.11.0}/LICENSE +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/abstraction_agent.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/change_status.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/cluster_budget.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/cluster_methods_mixin.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/constants.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/dependency_discovery.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/details_agent.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/meta_agent.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/model_capabilities.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/planner_agent.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/prompts/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/base.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/get_external_deps.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/get_method_invocations.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_cfg.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_docs.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_file.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_file_structure.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_packages.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_source.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/agents/tools/read_structure.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/cache.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/details_cache.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/caching/meta_cache.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/dependency_links.txt +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/codeboarding.egg-info/entry_points.txt +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/constants.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/core/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/core/plugin_loader.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/core/protocols.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/core/registry.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/analysis_json.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/file_coverage.py +1 -1
- {codeboarding-0.10.4/output_generators → codeboarding-0.11.0/diagram_analysis/incremental}/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/run_context.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/diagram_analysis/version.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/duckdb_crud.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/circular_deps.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/cohesion.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/coupling.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/function_size.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/god_class.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/inheritance.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/instability.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/checks/unused_code_diagnostics.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/config.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/constants.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/models.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health/runner.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/health_main.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/callbacks.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/context.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/mixin.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/paths.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/stats.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/monitoring/writers.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/html.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/html_template.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/markdown.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/mdx.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/output_generators/sphinx.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/repo_utils/errors.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/repo_utils/ignore.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/setup.cfg +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/analysis_cache.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cfg_skip_planner.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cluster_change_analyzer.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cluster_helpers.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/cluster_relations.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/csharp_config_scanner.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/adapters/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/call_graph_builder.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/edge_build_context.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/edge_builder.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/hierarchy_builder.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/lsp_client.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/lsp_constants.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/models.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/progress.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/protocols.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/result_converter.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/source_inspector.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/symbol_table.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/engine/utils.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/graph.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/java_config_scanner.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/java_utils.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/lsp_client/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/lsp_client/diagnostics.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/node.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/programming_language.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/reference_resolve_mixin.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/scanner.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/static_analyzer/typescript_config_scanner.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_install.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_logging_config.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_pyproject_packages.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_registry_coverage.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_tool_registry.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_user_config.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_vscode_constants.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_windows_compatibility.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tests/test_windows_encoding.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/__init__.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/installers.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/manifest.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/paths.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/tool_registry/registry.py +0 -0
- {codeboarding-0.10.4 → codeboarding-0.11.0}/user_config.py +0 -0
- {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.
|
|
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
|
-
| `--
|
|
200
|
-
| `--
|
|
201
|
-
| `--
|
|
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
|
|
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
|
-
| `--
|
|
140
|
-
| `--
|
|
141
|
-
| `--
|
|
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
|
|
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
|
|
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 --
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
389
|
-
return
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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)
|