codeboarding 0.9.0__tar.gz → 0.9.3__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 (140) hide show
  1. {codeboarding-0.9.0/codeboarding.egg-info → codeboarding-0.9.3}/PKG-INFO +2 -1
  2. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/agent.py +9 -0
  3. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/llm_config.py +13 -0
  4. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/planner_agent.py +1 -1
  5. {codeboarding-0.9.0 → codeboarding-0.9.3/codeboarding.egg-info}/PKG-INFO +2 -1
  6. {codeboarding-0.9.0 → codeboarding-0.9.3}/codeboarding.egg-info/requires.txt +1 -0
  7. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/diagram_generator.py +53 -26
  8. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/reexpansion.py +1 -1
  9. {codeboarding-0.9.0 → codeboarding-0.9.3}/github_action.py +1 -1
  10. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/unused_code_diagnostics.py +9 -5
  11. {codeboarding-0.9.0 → codeboarding-0.9.3}/main.py +1 -1
  12. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/context.py +20 -5
  13. {codeboarding-0.9.0 → codeboarding-0.9.3}/pyproject.toml +6 -3
  14. {codeboarding-0.9.0 → codeboarding-0.9.3}/repo_utils/change_detector.py +1 -1
  15. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/__init__.py +192 -52
  16. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/lsp_client/client.py +117 -27
  17. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_github_action.py +1 -1
  18. {codeboarding-0.9.0 → codeboarding-0.9.3}/user_config.py +4 -0
  19. {codeboarding-0.9.0 → codeboarding-0.9.3}/utils.py +1 -0
  20. {codeboarding-0.9.0 → codeboarding-0.9.3}/LICENSE +0 -0
  21. {codeboarding-0.9.0 → codeboarding-0.9.3}/PYPI.md +0 -0
  22. {codeboarding-0.9.0 → codeboarding-0.9.3}/README.md +0 -0
  23. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/__init__.py +0 -0
  24. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/abstraction_agent.py +0 -0
  25. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/agent_responses.py +0 -0
  26. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/cluster_methods_mixin.py +0 -0
  27. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/constants.py +0 -0
  28. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/dependency_discovery.py +0 -0
  29. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/details_agent.py +0 -0
  30. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/meta_agent.py +0 -0
  31. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/__init__.py +0 -0
  32. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/abstract_prompt_factory.py +0 -0
  33. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/claude_prompts.py +0 -0
  34. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/deepseek_prompts.py +0 -0
  35. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/gemini_flash_prompts.py +0 -0
  36. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/glm_prompts.py +0 -0
  37. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/gpt_prompts.py +0 -0
  38. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/kimi_prompts.py +0 -0
  39. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/prompts/prompt_factory.py +0 -0
  40. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/__init__.py +0 -0
  41. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/base.py +0 -0
  42. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/get_external_deps.py +0 -0
  43. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/get_method_invocations.py +0 -0
  44. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_cfg.py +0 -0
  45. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_docs.py +0 -0
  46. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_file.py +0 -0
  47. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_file_structure.py +0 -0
  48. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_git_diff.py +0 -0
  49. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_packages.py +0 -0
  50. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_source.py +0 -0
  51. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/read_structure.py +0 -0
  52. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/tools/toolkit.py +0 -0
  53. {codeboarding-0.9.0 → codeboarding-0.9.3}/agents/validation.py +0 -0
  54. {codeboarding-0.9.0 → codeboarding-0.9.3}/caching/__init__.py +0 -0
  55. {codeboarding-0.9.0 → codeboarding-0.9.3}/caching/cache.py +0 -0
  56. {codeboarding-0.9.0 → codeboarding-0.9.3}/caching/meta_cache.py +0 -0
  57. {codeboarding-0.9.0 → codeboarding-0.9.3}/codeboarding.egg-info/SOURCES.txt +0 -0
  58. {codeboarding-0.9.0 → codeboarding-0.9.3}/codeboarding.egg-info/dependency_links.txt +0 -0
  59. {codeboarding-0.9.0 → codeboarding-0.9.3}/codeboarding.egg-info/entry_points.txt +0 -0
  60. {codeboarding-0.9.0 → codeboarding-0.9.3}/codeboarding.egg-info/top_level.txt +0 -0
  61. {codeboarding-0.9.0 → codeboarding-0.9.3}/core/__init__.py +0 -0
  62. {codeboarding-0.9.0 → codeboarding-0.9.3}/core/plugin_loader.py +0 -0
  63. {codeboarding-0.9.0 → codeboarding-0.9.3}/core/protocols.py +0 -0
  64. {codeboarding-0.9.0 → codeboarding-0.9.3}/core/registry.py +0 -0
  65. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/__init__.py +0 -0
  66. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/analysis_json.py +0 -0
  67. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/file_coverage.py +0 -0
  68. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/__init__.py +0 -0
  69. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/component_checker.py +0 -0
  70. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/file_manager.py +0 -0
  71. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/impact_analyzer.py +0 -0
  72. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/io_utils.py +0 -0
  73. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/models.py +0 -0
  74. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/path_patching.py +0 -0
  75. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/scoped_analysis.py +0 -0
  76. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/updater.py +0 -0
  77. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/incremental/validation.py +0 -0
  78. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/manifest.py +0 -0
  79. {codeboarding-0.9.0 → codeboarding-0.9.3}/diagram_analysis/version.py +0 -0
  80. {codeboarding-0.9.0 → codeboarding-0.9.3}/duckdb_crud.py +0 -0
  81. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/__init__.py +0 -0
  82. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/__init__.py +0 -0
  83. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/circular_deps.py +0 -0
  84. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/cohesion.py +0 -0
  85. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/coupling.py +0 -0
  86. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/function_size.py +0 -0
  87. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/god_class.py +0 -0
  88. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/inheritance.py +0 -0
  89. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/checks/instability.py +0 -0
  90. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/config.py +0 -0
  91. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/constants.py +0 -0
  92. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/models.py +0 -0
  93. {codeboarding-0.9.0 → codeboarding-0.9.3}/health/runner.py +0 -0
  94. {codeboarding-0.9.0 → codeboarding-0.9.3}/health_main.py +0 -0
  95. {codeboarding-0.9.0 → codeboarding-0.9.3}/install.py +0 -0
  96. {codeboarding-0.9.0 → codeboarding-0.9.3}/logging_config.py +0 -0
  97. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/__init__.py +0 -0
  98. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/callbacks.py +0 -0
  99. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/mixin.py +0 -0
  100. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/paths.py +0 -0
  101. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/stats.py +0 -0
  102. {codeboarding-0.9.0 → codeboarding-0.9.3}/monitoring/writers.py +0 -0
  103. {codeboarding-0.9.0 → codeboarding-0.9.3}/output_generators/__init__.py +0 -0
  104. {codeboarding-0.9.0 → codeboarding-0.9.3}/output_generators/html.py +0 -0
  105. {codeboarding-0.9.0 → codeboarding-0.9.3}/output_generators/html_template.py +0 -0
  106. {codeboarding-0.9.0 → codeboarding-0.9.3}/output_generators/markdown.py +0 -0
  107. {codeboarding-0.9.0 → codeboarding-0.9.3}/output_generators/mdx.py +0 -0
  108. {codeboarding-0.9.0 → codeboarding-0.9.3}/output_generators/sphinx.py +0 -0
  109. {codeboarding-0.9.0 → codeboarding-0.9.3}/repo_utils/__init__.py +0 -0
  110. {codeboarding-0.9.0 → codeboarding-0.9.3}/repo_utils/errors.py +0 -0
  111. {codeboarding-0.9.0 → codeboarding-0.9.3}/repo_utils/git_diff.py +0 -0
  112. {codeboarding-0.9.0 → codeboarding-0.9.3}/repo_utils/ignore.py +0 -0
  113. {codeboarding-0.9.0 → codeboarding-0.9.3}/setup.cfg +0 -0
  114. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/analysis_cache.py +0 -0
  115. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/analysis_result.py +0 -0
  116. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/cluster_change_analyzer.py +0 -0
  117. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/cluster_helpers.py +0 -0
  118. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/constants.py +0 -0
  119. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/git_diff_analyzer.py +0 -0
  120. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/graph.py +0 -0
  121. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/incremental_orchestrator.py +0 -0
  122. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/java_config_scanner.py +0 -0
  123. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/java_utils.py +0 -0
  124. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/lsp_client/__init__.py +0 -0
  125. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/lsp_client/diagnostics.py +0 -0
  126. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/lsp_client/java_client.py +0 -0
  127. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/lsp_client/language_settings.py +0 -0
  128. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/lsp_client/typescript_client.py +0 -0
  129. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/programming_language.py +0 -0
  130. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/reference_resolve_mixin.py +0 -0
  131. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/scanner.py +0 -0
  132. {codeboarding-0.9.0 → codeboarding-0.9.3}/static_analyzer/typescript_config_scanner.py +0 -0
  133. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_incremental_analyzer.py +0 -0
  134. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_install.py +0 -0
  135. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_logging_config.py +0 -0
  136. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_main.py +0 -0
  137. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_vscode_constants.py +0 -0
  138. {codeboarding-0.9.0 → codeboarding-0.9.3}/tests/test_windows_compatibility.py +0 -0
  139. {codeboarding-0.9.0 → codeboarding-0.9.3}/tool_registry.py +0 -0
  140. {codeboarding-0.9.0 → codeboarding-0.9.3}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.9.0
3
+ Version: 0.9.3
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License: MIT
@@ -28,6 +28,7 @@ Requires-Dist: exceptiongroup>=1.2
28
28
  Requires-Dist: fastapi>=0.115
29
29
  Requires-Dist: filelock>=3.12
30
30
  Requires-Dist: gitpython>=3.1
31
+ Requires-Dist: google-api-core>=2.10
31
32
  Requires-Dist: google-genai>=1.10
32
33
  Requires-Dist: gql>=3.5
33
34
  Requires-Dist: injector>=0.21
@@ -32,6 +32,10 @@ from static_analyzer.reference_resolve_mixin import ReferenceResolverMixin
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
34
 
35
+ class EmptyExtractorMessageError(ValueError):
36
+ """Raised when extractor returns an empty message payload."""
37
+
38
+
35
39
  class CodeBoardingAgent(ReferenceResolverMixin, MonitoringMixin):
36
40
  def __init__(
37
41
  self,
@@ -281,9 +285,14 @@ class CodeBoardingAgent(ReferenceResolverMixin, MonitoringMixin):
281
285
  if "messages" in result and len(result["messages"]) != 0:
282
286
  message = result["messages"][0].content
283
287
  parser = PydanticOutputParser(pydantic_object=return_type)
288
+ if not message:
289
+ raise EmptyExtractorMessageError("Extractor returned empty message content")
284
290
  return self._try_parse(message, parser)
285
291
  parser = PydanticOutputParser(pydantic_object=return_type)
286
292
  return self._try_parse(response, parser)
293
+ except EmptyExtractorMessageError as e:
294
+ logger.warning(f"{e} (attempt {attempt + 1}/{max_retries})")
295
+ return self._parse_response(prompt, response, return_type, max_retries, attempt + 1)
287
296
  except AttributeError as e:
288
297
  # Workaround for trustcall bug: https://github.com/hinthornw/trustcall/issues/47
289
298
  # 'ExtractionState' object has no attribute 'tool_call_id' occurs during validation retry
@@ -235,6 +235,19 @@ LLM_PROVIDERS = {
235
235
  "max_retries": 0,
236
236
  },
237
237
  ),
238
+ "openrouter": LLMConfig(
239
+ chat_class=ChatOpenAI,
240
+ api_key_env="OPENROUTER_API_KEY",
241
+ agent_model="google/gemini-2.5-flash",
242
+ parsing_model="google/gemini-2.5-flash",
243
+ llm_type=LLMType.GEMINI_FLASH,
244
+ extra_args={
245
+ "base_url": lambda: os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1"),
246
+ "max_tokens": None,
247
+ "timeout": None,
248
+ "max_retries": 0,
249
+ },
250
+ ),
238
251
  }
239
252
 
240
253
 
@@ -79,7 +79,7 @@ def should_expand_component(
79
79
  return False
80
80
 
81
81
 
82
- def plan_analysis(
82
+ def get_expandable_components(
83
83
  analysis: AnalysisInsights,
84
84
  parent_had_clusters: bool = True,
85
85
  min_files: int = DEFAULT_MIN_FILES,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.9.0
3
+ Version: 0.9.3
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License: MIT
@@ -28,6 +28,7 @@ Requires-Dist: exceptiongroup>=1.2
28
28
  Requires-Dist: fastapi>=0.115
29
29
  Requires-Dist: filelock>=3.12
30
30
  Requires-Dist: gitpython>=3.1
31
+ Requires-Dist: google-api-core>=2.10
31
32
  Requires-Dist: google-genai>=1.10
32
33
  Requires-Dist: gql>=3.5
33
34
  Requires-Dist: injector>=0.21
@@ -9,6 +9,7 @@ exceptiongroup>=1.2
9
9
  fastapi>=0.115
10
10
  filelock>=3.12
11
11
  gitpython>=3.1
12
+ google-api-core>=2.10
12
13
  google-genai>=1.10
13
14
  gql>=3.5
14
15
  injector>=0.21
@@ -13,7 +13,7 @@ from agents.agent_responses import AnalysisInsights, Component
13
13
  from agents.details_agent import DetailsAgent
14
14
  from agents.llm_config import initialize_llms
15
15
  from agents.meta_agent import MetaAgent
16
- from agents.planner_agent import plan_analysis
16
+ from agents.planner_agent import get_expandable_components
17
17
  from diagram_analysis.analysis_json import (
18
18
  FileCoverageReport,
19
19
  FileCoverageSummary,
@@ -35,9 +35,10 @@ from monitoring.mixin import MonitoringMixin
35
35
  from monitoring.paths import generate_run_id, get_monitoring_run_dir
36
36
  from repo_utils import get_git_commit_hash, get_repo_state_hash
37
37
  from repo_utils.ignore import RepoIgnoreManager
38
- from static_analyzer import get_static_analysis
38
+ from static_analyzer import StaticAnalyzer, get_static_analysis
39
39
  from static_analyzer.analysis_result import StaticAnalysisResults
40
40
  from static_analyzer.scanner import ProjectScanner
41
+ from utils import get_cache_dir
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
@@ -53,6 +54,7 @@ class DiagramGenerator:
53
54
  project_name: str | None = None,
54
55
  run_id: str | None = None,
55
56
  monitoring_enabled: bool = False,
57
+ static_analyzer: StaticAnalyzer | None = None,
56
58
  ):
57
59
  self.repo_location = repo_location
58
60
  self.temp_folder = temp_folder
@@ -63,6 +65,10 @@ class DiagramGenerator:
63
65
  self.run_id = run_id
64
66
  self.monitoring_enabled = monitoring_enabled
65
67
  self.force_full_analysis = False # Set to True to skip incremental updates
68
+ # Optional pre-started StaticAnalyzer injected by long-lived callers (e.g. the
69
+ # wrapper). When set, pre_analysis() uses it directly instead of creating a new
70
+ # one-shot analyzer via get_static_analysis().
71
+ self._static_analyzer = static_analyzer
66
72
 
67
73
  self.details_agent: DetailsAgent | None = None
68
74
  self.static_analysis: StaticAnalysisResults | None = None # Cache static analysis for reuse
@@ -87,7 +93,7 @@ class DiagramGenerator:
87
93
  parent_had_clusters = bool(component.source_cluster_ids)
88
94
 
89
95
  # Get new components to analyze (deterministic, no LLM)
90
- new_components = plan_analysis(analysis, parent_had_clusters=parent_had_clusters)
96
+ new_components = get_expandable_components(analysis, parent_had_clusters=parent_had_clusters)
91
97
 
92
98
  return component.component_id, analysis, new_components
93
99
  except Exception as e:
@@ -107,7 +113,7 @@ class DiagramGenerator:
107
113
  repo_path=self.repo_location,
108
114
  )
109
115
  if health_report is not None:
110
- health_path = os.path.join(self.output_dir, "health", "health_report.json")
116
+ health_path = Path(self.output_dir) / "health" / "health_report.json"
111
117
  with open(health_path, "w") as f:
112
118
  f.write(health_report.model_dump_json(indent=2, exclude_none=True))
113
119
  logger.info(f"Health report written to {health_path} (score: {health_report.overall_score:.3f})")
@@ -138,11 +144,18 @@ class DiagramGenerator:
138
144
  summary=FileCoverageSummary(**self.file_coverage_data["summary"]),
139
145
  )
140
146
 
141
- coverage_path = os.path.join(self.output_dir, "file_coverage.json")
147
+ coverage_path = Path(self.output_dir) / "file_coverage.json"
142
148
  with open(coverage_path, "w") as f:
143
149
  f.write(report.model_dump_json(indent=2, exclude_none=True))
144
150
  logger.info(f"File coverage report written to {coverage_path}")
145
151
 
152
+ def _get_static_from_injected_analyzer(self, cache_dir: Path | None) -> StaticAnalysisResults:
153
+ result = self._static_analyzer.analyze( # type: ignore[union-attr]
154
+ cache_dir=cache_dir,
155
+ )
156
+ result.diagnostics = self._static_analyzer.collected_diagnostics # type: ignore[union-attr]
157
+ return result
158
+
146
159
  def pre_analysis(self):
147
160
  analysis_start_time = time.time()
148
161
 
@@ -157,14 +170,27 @@ class DiagramGenerator:
157
170
  )
158
171
  self._monitoring_agents["MetaAgent"] = self.meta_agent
159
172
 
160
- # Run static analysis and meta analysis in parallel
161
- with ThreadPoolExecutor(max_workers=2) as executor:
173
+ def get_static_with_injected_analyzer() -> StaticAnalysisResults:
174
+ cache_dir = None if self.force_full_analysis else get_cache_dir(self.repo_location)
175
+ return self._get_static_from_injected_analyzer(cache_dir)
176
+
177
+ def get_static_with_new_analyzer() -> StaticAnalysisResults:
162
178
  skip_cache = self.force_full_analysis
163
179
  if skip_cache:
164
180
  logger.info("Force full analysis: skipping static analysis cache")
165
- static_future = executor.submit(get_static_analysis, self.repo_location, skip_cache=skip_cache)
166
- meta_future = executor.submit(self.meta_agent.get_meta_context, refresh=self.force_full_analysis)
181
+ return get_static_analysis(self.repo_location, skip_cache=skip_cache)
182
+
183
+ # Decide how to obtain static analysis results, then run it in parallel
184
+ # with the meta-context computation so neither blocks the other.
185
+ if self._static_analyzer is not None:
186
+ logger.info("Using injected StaticAnalyzer (clients already running)")
187
+ static_callable = get_static_with_injected_analyzer
188
+ else:
189
+ static_callable = get_static_with_new_analyzer
167
190
 
191
+ with ThreadPoolExecutor(max_workers=2) as executor:
192
+ static_future = executor.submit(static_callable)
193
+ meta_future = executor.submit(self.meta_agent.get_meta_context, refresh=self.force_full_analysis)
168
194
  static_analysis = static_future.result()
169
195
  meta_context = meta_future.result()
170
196
 
@@ -205,7 +231,7 @@ class DiagramGenerator:
205
231
  )
206
232
  self._monitoring_agents["AbstractionAgent"] = self.abstraction_agent
207
233
 
208
- version_file = os.path.join(self.output_dir, "codeboarding_version.json")
234
+ version_file = Path(self.output_dir) / "codeboarding_version.json"
209
235
  with open(version_file, "w") as f:
210
236
  f.write(
211
237
  Version(
@@ -317,7 +343,7 @@ class DiagramGenerator:
317
343
 
318
344
  return expanded_components, sub_analyses
319
345
 
320
- def generate_analysis(self):
346
+ def generate_analysis(self) -> list[Path]:
321
347
  """
322
348
  Generate the graph analysis for the given repository.
323
349
  The output is stored in a single analysis.json file in output_dir.
@@ -337,7 +363,7 @@ class DiagramGenerator:
337
363
  analysis, cluster_results = self.abstraction_agent.run()
338
364
 
339
365
  # Get the initial components to analyze (deterministic, no LLM)
340
- root_components = plan_analysis(analysis)
366
+ root_components = get_expandable_components(analysis)
341
367
  logger.info(f"Found {len(root_components)} components to analyze at level 1")
342
368
 
343
369
  # Process components using a frontier queue: submit children as soon as parent finishes.
@@ -355,18 +381,15 @@ class DiagramGenerator:
355
381
  )
356
382
 
357
383
  # Final write of unified analysis.json
358
- analysis_path = str(
359
- save_analysis(
360
- analysis=analysis,
361
- output_dir=Path(self.output_dir),
362
- sub_analyses=sub_analyses,
363
- repo_name=self.repo_name,
364
- file_coverage_summary=file_coverage_summary,
365
- )
384
+ analysis_path = save_analysis(
385
+ analysis=analysis,
386
+ output_dir=Path(self.output_dir),
387
+ sub_analyses=sub_analyses,
388
+ repo_name=self.repo_name,
389
+ file_coverage_summary=file_coverage_summary,
366
390
  )
367
391
 
368
392
  logger.info(f"Analysis complete. Written unified analysis to {analysis_path}")
369
- print("Generated analysis file: %s", os.path.abspath(analysis_path))
370
393
 
371
394
  # Write file_coverage.json
372
395
  self._write_file_coverage()
@@ -396,7 +419,7 @@ class DiagramGenerator:
396
419
  except Exception as e:
397
420
  logger.warning(f"Failed to save manifest: {e}")
398
421
 
399
- def try_incremental_update(self) -> list[str] | None:
422
+ def try_incremental_update(self) -> list[Path] | None:
400
423
  """
401
424
  Attempt an incremental update if possible.
402
425
 
@@ -416,7 +439,11 @@ class DiagramGenerator:
416
439
  # recompute file assignments with cluster matching. Load it first.
417
440
  static_analysis = None
418
441
  try:
419
- static_analysis = get_static_analysis(self.repo_location)
442
+ if self._static_analyzer is not None:
443
+ static_analysis = self._static_analyzer.analyze(cache_dir=get_cache_dir(self.repo_location))
444
+ static_analysis.diagnostics = self._static_analyzer.collected_diagnostics
445
+ else:
446
+ static_analysis = get_static_analysis(self.repo_location)
420
447
  logger.info("Loaded static analysis for incremental update")
421
448
  except Exception as e:
422
449
  logger.warning(f"Could not load static analysis: {e}")
@@ -442,7 +469,7 @@ class DiagramGenerator:
442
469
 
443
470
  if impact.action == UpdateAction.NONE:
444
471
  logger.info("No changes detected, analysis is up to date")
445
- return [str(self.output_dir / "analysis.json")]
472
+ return [self.output_dir / "analysis.json"]
446
473
 
447
474
  # For structural changes, recompute which components are actually affected
448
475
  # after static analysis has been updated with cluster matching
@@ -465,13 +492,13 @@ class DiagramGenerator:
465
492
  )
466
493
  self._write_file_coverage()
467
494
 
468
- return [str(self.output_dir / "analysis.json")]
495
+ return [self.output_dir / "analysis.json"]
469
496
 
470
497
  # Incremental update failed or not possible
471
498
  logger.info("Incremental update not possible, falling back to full analysis")
472
499
  return None
473
500
 
474
- def generate_analysis_smart(self) -> list[str]:
501
+ def generate_analysis_smart(self) -> list[Path]:
475
502
  """
476
503
  Smart analysis that tries incremental first, falls back to full.
477
504
 
@@ -12,7 +12,7 @@ from agents.llm_config import initialize_llms
12
12
  from agents.agent_responses import AnalysisInsights
13
13
  from agents.details_agent import DetailsAgent
14
14
  from agents.meta_agent import MetaAgent
15
- from agents.planner_agent import plan_analysis
15
+ from agents.planner_agent import get_expandable_components
16
16
  from diagram_analysis.incremental.io_utils import load_sub_analysis, save_sub_analysis
17
17
  from diagram_analysis.incremental.models import ChangeImpact
18
18
  from diagram_analysis.incremental.path_patching import patch_sub_analysis
@@ -154,7 +154,7 @@ def generate_analysis(
154
154
  analysis_files = generator.generate_analysis_smart()
155
155
 
156
156
  # The generator now returns a single analysis.json path
157
- analysis_path = Path(analysis_files[0])
157
+ analysis_path = analysis_files[0]
158
158
 
159
159
  # Now generate the output docs:
160
160
  match extension:
@@ -136,13 +136,17 @@ class LSPDiagnosticsCollector:
136
136
  """Process all collected diagnostics and convert to issues."""
137
137
  self.issues = []
138
138
  skipped = 0
139
- seen_issues: set[tuple[str, str, int]] = set() # (file_path, code, line_start)
139
+ seen_issues: set[tuple[str, str, int]] = set() # (file_path, category, line_start)
140
140
 
141
141
  for item in self.diagnostics:
142
142
  issue = self._convert_to_issue(item.file_path, item.diagnostic)
143
143
  if issue:
144
- # Deduplicate: same file, code, and line
145
- key = (issue.file_path, issue.code or "", issue.line_start)
144
+ # Deduplicate: same file, category, and line.
145
+ # Using category instead of code prevents duplicates from the same
146
+ # LSP server reporting the same issue under different diagnostic codes
147
+ # (e.g. Pyright reports unused variables both via reportUnusedVariable
148
+ # and via the "Unnecessary" tag with different messages).
149
+ key = (issue.file_path, issue.category.value, issue.line_start)
146
150
  if key not in seen_issues:
147
151
  seen_issues.add(key)
148
152
  self.issues.append(issue)
@@ -187,8 +191,8 @@ class LSPDiagnosticsCollector:
187
191
 
188
192
  return DiagnosticIssue(
189
193
  file_path=file_path,
190
- line_start=diagnostic.range.start.line,
191
- line_end=diagnostic.range.end.line,
194
+ line_start=diagnostic.range.start.line + 1, # LSP lines are 0-based, convert to 1-based
195
+ line_end=diagnostic.range.end.line + 1, # LSP lines are 0-based, convert to 1-based
192
196
  message=message,
193
197
  code=code if code else None,
194
198
  category=category,
@@ -66,7 +66,7 @@ def generate_analysis(
66
66
  )
67
67
  generator.force_full_analysis = force_full
68
68
  generated_files = generator.generate_analysis()
69
- return [Path(path) for path in generated_files]
69
+ return generated_files
70
70
 
71
71
 
72
72
  def generate_markdown_docs(
@@ -3,7 +3,7 @@ import logging
3
3
  import functools
4
4
  import time
5
5
  import contextlib
6
- from contextvars import ContextVar
6
+ from contextvars import ContextVar, Token
7
7
  from pathlib import Path
8
8
  from typing import Callable, Any
9
9
 
@@ -71,25 +71,40 @@ def monitor_execution(
71
71
 
72
72
  # Allow the user to manually log steps via the yielded context
73
73
  class MonitorContext:
74
+ def __init__(self):
75
+ self._step_tokens: list[Token[str]] = []
76
+
74
77
  def step(self, name):
75
78
  trace_logger.info(json.dumps({"event": "phase_change", "step": name, "timestamp": time.time()}))
76
79
  # Also update the ContextVar for other components
77
- self._token = current_step.set(name)
80
+ token = current_step.set(name)
81
+ self._step_tokens.append(token)
78
82
 
79
- def end_step(self):
80
- pass
83
+ def end_step(self):
84
+ if not self._step_tokens:
85
+ return
86
+ token = self._step_tokens.pop()
87
+ current_step.reset(token)
88
+
89
+ def close(self):
90
+ while self._step_tokens:
91
+ self.end_step()
81
92
 
82
93
  # Initialize stats for this run
83
94
  run_stats = RunStats()
84
95
  stats_token = current_stats.set(run_stats)
85
96
 
97
+ monitor_context = MonitorContext()
98
+
86
99
  try:
87
100
  # Log start of run
88
101
  trace_logger.info(json.dumps({"event": "run_start", "run_id": run_id, "timestamp": time.time()}))
89
102
 
90
- yield MonitorContext()
103
+ yield monitor_context
91
104
 
92
105
  finally:
106
+ monitor_context.close()
107
+
93
108
  # Log end of run
94
109
  trace_logger.info(json.dumps({"event": "run_end", "run_id": run_id, "timestamp": time.time()}))
95
110
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboarding"
7
- version = "0.9.0"
7
+ version = "0.9.3"
8
8
  description = "Interactive Diagrams for Code"
9
9
  readme = "PYPI.md"
10
10
  license = {text = "MIT"}
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "fastapi>=0.115",
35
35
  "filelock>=3.12",
36
36
  "gitpython>=3.1",
37
+ "google-api-core>=2.10",
37
38
  "google-genai>=1.10",
38
39
  "gql>=3.5",
39
40
  "injector>=0.21",
@@ -106,7 +107,7 @@ warn_return_any = false
106
107
  warn_unused_configs = true
107
108
  disallow_untyped_defs = false
108
109
  check_untyped_defs = true
109
- exclude = "(?x)(^repos/|^build/|^dist/|^temp/|^static_analyzer/servers/)"
110
+ exclude = "(?x)(^repos/|^build/|^dist/|^temp/|^static_analyzer/servers/|^tests/integration/projects/)"
110
111
  explicit_package_bases = true
111
112
  disable_error_code = ["import-untyped", "no-any-return", "assignment"]
112
113
 
@@ -116,6 +117,8 @@ dev-dependencies = [
116
117
  "pre-commit==3.8.0",
117
118
  "mypy==1.19.0",
118
119
  "pytest-cov==7.0.0",
120
+ "build>=1.4.0",
121
+ "twine>=6.2.0",
119
122
  ]
120
123
 
121
124
  [tool.pytest.ini_options]
@@ -133,4 +136,4 @@ markers = [
133
136
  "typescript_lang: marks tests for TypeScript language",
134
137
  "php_lang: marks tests for PHP language",
135
138
  "javascript_lang: marks tests for JavaScript language",
136
- ]
139
+ ]
@@ -125,7 +125,7 @@ def detect_changes(
125
125
  Example:
126
126
  changes = detect_changes(repo_path, "abc1234")
127
127
  for rename_old, rename_new in changes.renames.items():
128
- print(f"Renamed: {rename_old} -> {rename_new}")
128
+ logger.info(f"Renamed: {rename_old} -> {rename_new}")
129
129
  """
130
130
  changes: list[DetectedChange] = []
131
131