codeboarding 0.9.3__tar.gz → 0.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. {codeboarding-0.9.3/codeboarding.egg-info → codeboarding-0.9.4}/PKG-INFO +1 -1
  2. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/agent_responses.py +4 -4
  3. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/cluster_methods_mixin.py +5 -2
  4. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/dependency_discovery.py +5 -5
  5. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/planner_agent.py +9 -9
  6. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/gpt_prompts.py +1 -1
  7. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_file_structure.py +6 -6
  8. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_source.py +1 -1
  9. {codeboarding-0.9.3 → codeboarding-0.9.4/codeboarding.egg-info}/PKG-INFO +1 -1
  10. {codeboarding-0.9.3 → codeboarding-0.9.4}/codeboarding.egg-info/SOURCES.txt +2 -1
  11. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/diagram_generator.py +4 -4
  12. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/file_coverage.py +2 -2
  13. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/io_utils.py +2 -2
  14. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/models.py +3 -3
  15. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/manifest.py +2 -2
  16. {codeboarding-0.9.3 → codeboarding-0.9.4}/github_action.py +1 -1
  17. {codeboarding-0.9.3 → codeboarding-0.9.4}/logging_config.py +14 -0
  18. {codeboarding-0.9.3 → codeboarding-0.9.4}/main.py +1 -1
  19. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/context.py +4 -4
  20. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/writers.py +2 -2
  21. {codeboarding-0.9.3 → codeboarding-0.9.4}/output_generators/markdown.py +1 -1
  22. {codeboarding-0.9.3 → codeboarding-0.9.4}/output_generators/mdx.py +1 -1
  23. {codeboarding-0.9.3 → codeboarding-0.9.4}/output_generators/sphinx.py +1 -1
  24. {codeboarding-0.9.3 → codeboarding-0.9.4}/pyproject.toml +1 -1
  25. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/cluster_change_analyzer.py +1 -1
  26. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/graph.py +3 -3
  27. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/lsp_client/client.py +29 -11
  28. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_logging_config.py +73 -0
  29. codeboarding-0.9.4/tests/test_windows_encoding.py +49 -0
  30. {codeboarding-0.9.3 → codeboarding-0.9.4}/tool_registry.py +46 -5
  31. {codeboarding-0.9.3 → codeboarding-0.9.4}/user_config.py +1 -1
  32. {codeboarding-0.9.3 → codeboarding-0.9.4}/LICENSE +0 -0
  33. {codeboarding-0.9.3 → codeboarding-0.9.4}/PYPI.md +0 -0
  34. {codeboarding-0.9.3 → codeboarding-0.9.4}/README.md +0 -0
  35. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/__init__.py +0 -0
  36. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/abstraction_agent.py +0 -0
  37. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/agent.py +0 -0
  38. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/constants.py +0 -0
  39. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/details_agent.py +0 -0
  40. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/llm_config.py +0 -0
  41. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/meta_agent.py +0 -0
  42. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/__init__.py +0 -0
  43. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/abstract_prompt_factory.py +0 -0
  44. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/claude_prompts.py +0 -0
  45. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/deepseek_prompts.py +0 -0
  46. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/gemini_flash_prompts.py +0 -0
  47. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/glm_prompts.py +0 -0
  48. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/kimi_prompts.py +0 -0
  49. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/prompts/prompt_factory.py +0 -0
  50. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/__init__.py +0 -0
  51. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/base.py +0 -0
  52. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/get_external_deps.py +0 -0
  53. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/get_method_invocations.py +0 -0
  54. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_cfg.py +0 -0
  55. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_docs.py +0 -0
  56. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_file.py +0 -0
  57. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_git_diff.py +0 -0
  58. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_packages.py +0 -0
  59. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/read_structure.py +0 -0
  60. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/tools/toolkit.py +0 -0
  61. {codeboarding-0.9.3 → codeboarding-0.9.4}/agents/validation.py +0 -0
  62. {codeboarding-0.9.3 → codeboarding-0.9.4}/caching/__init__.py +0 -0
  63. {codeboarding-0.9.3 → codeboarding-0.9.4}/caching/cache.py +0 -0
  64. {codeboarding-0.9.3 → codeboarding-0.9.4}/caching/meta_cache.py +0 -0
  65. {codeboarding-0.9.3 → codeboarding-0.9.4}/codeboarding.egg-info/dependency_links.txt +0 -0
  66. {codeboarding-0.9.3 → codeboarding-0.9.4}/codeboarding.egg-info/entry_points.txt +0 -0
  67. {codeboarding-0.9.3 → codeboarding-0.9.4}/codeboarding.egg-info/requires.txt +0 -0
  68. {codeboarding-0.9.3 → codeboarding-0.9.4}/codeboarding.egg-info/top_level.txt +0 -0
  69. {codeboarding-0.9.3 → codeboarding-0.9.4}/core/__init__.py +0 -0
  70. {codeboarding-0.9.3 → codeboarding-0.9.4}/core/plugin_loader.py +0 -0
  71. {codeboarding-0.9.3 → codeboarding-0.9.4}/core/protocols.py +0 -0
  72. {codeboarding-0.9.3 → codeboarding-0.9.4}/core/registry.py +0 -0
  73. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/__init__.py +0 -0
  74. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/analysis_json.py +0 -0
  75. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/__init__.py +0 -0
  76. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/component_checker.py +0 -0
  77. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/file_manager.py +0 -0
  78. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/impact_analyzer.py +0 -0
  79. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/path_patching.py +0 -0
  80. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/reexpansion.py +0 -0
  81. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/scoped_analysis.py +0 -0
  82. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/updater.py +0 -0
  83. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/incremental/validation.py +0 -0
  84. {codeboarding-0.9.3 → codeboarding-0.9.4}/diagram_analysis/version.py +0 -0
  85. {codeboarding-0.9.3 → codeboarding-0.9.4}/duckdb_crud.py +0 -0
  86. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/__init__.py +0 -0
  87. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/__init__.py +0 -0
  88. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/circular_deps.py +0 -0
  89. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/cohesion.py +0 -0
  90. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/coupling.py +0 -0
  91. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/function_size.py +0 -0
  92. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/god_class.py +0 -0
  93. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/inheritance.py +0 -0
  94. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/instability.py +0 -0
  95. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/checks/unused_code_diagnostics.py +0 -0
  96. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/config.py +0 -0
  97. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/constants.py +0 -0
  98. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/models.py +0 -0
  99. {codeboarding-0.9.3 → codeboarding-0.9.4}/health/runner.py +0 -0
  100. {codeboarding-0.9.3 → codeboarding-0.9.4}/health_main.py +0 -0
  101. {codeboarding-0.9.3 → codeboarding-0.9.4}/install.py +0 -0
  102. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/__init__.py +0 -0
  103. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/callbacks.py +0 -0
  104. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/mixin.py +0 -0
  105. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/paths.py +0 -0
  106. {codeboarding-0.9.3 → codeboarding-0.9.4}/monitoring/stats.py +0 -0
  107. {codeboarding-0.9.3 → codeboarding-0.9.4}/output_generators/__init__.py +0 -0
  108. {codeboarding-0.9.3 → codeboarding-0.9.4}/output_generators/html.py +0 -0
  109. {codeboarding-0.9.3 → codeboarding-0.9.4}/output_generators/html_template.py +0 -0
  110. {codeboarding-0.9.3 → codeboarding-0.9.4}/repo_utils/__init__.py +0 -0
  111. {codeboarding-0.9.3 → codeboarding-0.9.4}/repo_utils/change_detector.py +0 -0
  112. {codeboarding-0.9.3 → codeboarding-0.9.4}/repo_utils/errors.py +0 -0
  113. {codeboarding-0.9.3 → codeboarding-0.9.4}/repo_utils/git_diff.py +0 -0
  114. {codeboarding-0.9.3 → codeboarding-0.9.4}/repo_utils/ignore.py +0 -0
  115. {codeboarding-0.9.3 → codeboarding-0.9.4}/setup.cfg +0 -0
  116. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/__init__.py +0 -0
  117. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/analysis_cache.py +0 -0
  118. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/analysis_result.py +0 -0
  119. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/cluster_helpers.py +0 -0
  120. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/constants.py +0 -0
  121. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/git_diff_analyzer.py +0 -0
  122. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/incremental_orchestrator.py +0 -0
  123. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/java_config_scanner.py +0 -0
  124. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/java_utils.py +0 -0
  125. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/lsp_client/__init__.py +0 -0
  126. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/lsp_client/diagnostics.py +0 -0
  127. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/lsp_client/java_client.py +0 -0
  128. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/lsp_client/language_settings.py +0 -0
  129. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/lsp_client/typescript_client.py +0 -0
  130. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/programming_language.py +0 -0
  131. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/reference_resolve_mixin.py +0 -0
  132. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/scanner.py +0 -0
  133. {codeboarding-0.9.3 → codeboarding-0.9.4}/static_analyzer/typescript_config_scanner.py +0 -0
  134. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_github_action.py +0 -0
  135. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_incremental_analyzer.py +0 -0
  136. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_install.py +0 -0
  137. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_main.py +0 -0
  138. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_vscode_constants.py +0 -0
  139. {codeboarding-0.9.3 → codeboarding-0.9.4}/tests/test_windows_compatibility.py +0 -0
  140. {codeboarding-0.9.3 → codeboarding-0.9.4}/utils.py +0 -0
  141. {codeboarding-0.9.3 → codeboarding-0.9.4}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License: MIT
@@ -180,7 +180,7 @@ class AnalysisInsights(LLMBaseModel):
180
180
  def llm_str(self):
181
181
  if not self.components:
182
182
  return "No abstract components found."
183
- title = "# 📦 Abstract Components Overview\n"
183
+ title = "# Abstract Components Overview\n"
184
184
  body = "\n".join(ac.llm_str() for ac in self.components)
185
185
  relations = "\n".join(cr.llm_str() for cr in self.components_relations)
186
186
  return title + body + relations
@@ -251,7 +251,7 @@ class CFGAnalysisInsights(LLMBaseModel):
251
251
  def llm_str(self):
252
252
  if not self.components:
253
253
  return "No abstract components found in the CFG."
254
- title = "# 📦 Abstract Components Overview from CFG\n"
254
+ title = "# Abstract Components Overview from CFG\n"
255
255
  body = "\n".join(ac.llm_str() for ac in self.components)
256
256
  relations = "\n".join(cr.llm_str() for cr in self.components_relations)
257
257
  return title + body + relations
@@ -309,7 +309,7 @@ class MetaAnalysisInsights(LLMBaseModel):
309
309
  )
310
310
 
311
311
  def llm_str(self):
312
- title = "# 🎯 Project Metadata Analysis\n"
312
+ title = "# Project Metadata Analysis\n"
313
313
  content = f"""
314
314
  **Project Type:** {self.project_type}
315
315
  **Domain:** {self.domain}
@@ -341,7 +341,7 @@ class ComponentFiles(LLMBaseModel):
341
341
  def llm_str(self):
342
342
  if not self.file_paths:
343
343
  return "No files classified."
344
- title = "# 📄 Component File Classifications\n"
344
+ title = "# Component File Classifications\n"
345
345
  body = "\n".join(f"- `{fc.file_path}` -> Component: `{fc.component_name}`" for fc in self.file_paths)
346
346
  return title + body
347
347
 
@@ -2,10 +2,13 @@ import logging
2
2
  import os
3
3
  from pathlib import Path
4
4
 
5
- from agents.agent_responses import Component, AnalysisInsights
5
+ from agents.agent_responses import AnalysisInsights, Component
6
6
  from static_analyzer.analysis_result import StaticAnalysisResults
7
+ from static_analyzer.cluster_helpers import (
8
+ get_all_cluster_ids,
9
+ get_files_for_cluster_ids,
10
+ )
7
11
  from static_analyzer.graph import ClusterResult
8
- from static_analyzer.cluster_helpers import get_files_for_cluster_ids, get_all_cluster_ids
9
12
 
10
13
  logger = logging.getLogger(__name__)
11
14
 
@@ -31,7 +31,7 @@ class DependencyFileSpec:
31
31
 
32
32
 
33
33
  DEPENDENCY_REGISTRY: tuple[DependencyFileSpec, ...] = (
34
- # ── Python ──
34
+ # -- Python --
35
35
  DependencyFileSpec("requirements.txt", Ecosystem.PYTHON, FileRole.MANIFEST),
36
36
  DependencyFileSpec("requirements-dev.txt", Ecosystem.PYTHON, FileRole.MANIFEST),
37
37
  DependencyFileSpec("requirements-test.txt", Ecosystem.PYTHON, FileRole.MANIFEST),
@@ -52,7 +52,7 @@ DEPENDENCY_REGISTRY: tuple[DependencyFileSpec, ...] = (
52
52
  DependencyFileSpec("pixi.toml", Ecosystem.PYTHON, FileRole.MANIFEST),
53
53
  DependencyFileSpec("requirements.in", Ecosystem.PYTHON, FileRole.MANIFEST),
54
54
  DependencyFileSpec("pixi.lock", Ecosystem.PYTHON, FileRole.LOCK),
55
- # ── Node / TypeScript / JavaScript ──
55
+ # -- Node / TypeScript / JavaScript --
56
56
  DependencyFileSpec("package.json", Ecosystem.NODE, FileRole.MANIFEST),
57
57
  DependencyFileSpec("package-lock.json", Ecosystem.NODE, FileRole.LOCK),
58
58
  DependencyFileSpec("yarn.lock", Ecosystem.NODE, FileRole.LOCK),
@@ -65,12 +65,12 @@ DEPENDENCY_REGISTRY: tuple[DependencyFileSpec, ...] = (
65
65
  DependencyFileSpec("deno.jsonc", Ecosystem.NODE, FileRole.MANIFEST),
66
66
  DependencyFileSpec("deno.lock", Ecosystem.NODE, FileRole.LOCK),
67
67
  DependencyFileSpec("lerna.json", Ecosystem.NODE, FileRole.CONFIG),
68
- # ── Go ──
68
+ # -- Go --
69
69
  DependencyFileSpec("go.mod", Ecosystem.GO, FileRole.MANIFEST),
70
70
  DependencyFileSpec("go.sum", Ecosystem.GO, FileRole.LOCK),
71
71
  DependencyFileSpec("go.work", Ecosystem.GO, FileRole.CONFIG),
72
72
  DependencyFileSpec("go.work.sum", Ecosystem.GO, FileRole.LOCK),
73
- # ── Java / JVM ──
73
+ # -- Java / JVM --
74
74
  DependencyFileSpec("pom.xml", Ecosystem.JAVA, FileRole.MANIFEST),
75
75
  DependencyFileSpec("pom.properties", Ecosystem.JAVA, FileRole.CONFIG),
76
76
  DependencyFileSpec("build.gradle", Ecosystem.JAVA, FileRole.MANIFEST),
@@ -81,7 +81,7 @@ DEPENDENCY_REGISTRY: tuple[DependencyFileSpec, ...] = (
81
81
  DependencyFileSpec("build.sbt", Ecosystem.JAVA, FileRole.MANIFEST),
82
82
  DependencyFileSpec("gradle.lockfile", Ecosystem.JAVA, FileRole.LOCK),
83
83
  DependencyFileSpec("verification-metadata.xml", Ecosystem.JAVA, FileRole.LOCK),
84
- # ── PHP ──
84
+ # -- PHP --
85
85
  DependencyFileSpec("composer.json", Ecosystem.PHP, FileRole.MANIFEST),
86
86
  DependencyFileSpec("composer.lock", Ecosystem.PHP, FileRole.LOCK),
87
87
  DependencyFileSpec("symfony.lock", Ecosystem.PHP, FileRole.LOCK),
@@ -6,14 +6,14 @@ should be expanded into sub-components. Unlike the previous LLM-based approach,
6
6
  this uses CFG clustering structure as the source of truth.
7
7
 
8
8
  Expansion Rules:
9
- 1. If component has source_cluster_ids expandable (CFG structure exists)
10
- 2. If component has no clusters but has files expandable ONE level (to explain files)
11
- 3. If neither component nor its parent has clusters leaf (stop expanding)
9
+ 1. If component has source_cluster_ids -> expandable (CFG structure exists)
10
+ 2. If component has no clusters but has files -> expandable ONE level (to explain files)
11
+ 3. If neither component nor its parent has clusters -> leaf (stop expanding)
12
12
 
13
13
  Example:
14
- - Component: "Agents" (clusters: [1,2,3]) expand
15
- - Sub-component: "DetailsAgent" (clusters: [], files: [details_agent.py]) expand (parent had clusters)
16
- - Sub-sub-component: "run_method" (clusters: [], files: []) DON'T expand (parent had no clusters)
14
+ - Component: "Agents" (clusters: [1,2,3]) -> expand (yes)
15
+ - Sub-component: "DetailsAgent" (clusters: [], files: [details_agent.py]) -> expand (yes, parent had clusters)
16
+ - Sub-sub-component: "run_method" (clusters: [], files: []) -> DON'T expand (no, parent had no clusters)
17
17
  """
18
18
 
19
19
  import logging
@@ -35,10 +35,10 @@ def should_expand_component(
35
35
  Determine if a component should be expanded into sub-components.
36
36
 
37
37
  Expansion logic:
38
- - If component has clusters expand (there's CFG structure to decompose)
39
- - If component has no clusters but has files expand if parent had clusters
38
+ - If component has clusters -> expand (there's CFG structure to decompose)
39
+ - If component has no clusters but has files -> expand if parent had clusters
40
40
  (allows one more level to explain file internals)
41
- - If neither component nor parent has clusters stop (leaf node)
41
+ - If neither component nor parent has clusters -> stop (leaf node)
42
42
 
43
43
  Args:
44
44
  component: The component to evaluate
@@ -213,7 +213,7 @@ RELATIONSHIPS_VALIDATION = """Validate component relationships for accuracy and
213
213
 
214
214
  1. **Accuracy:**
215
215
  - [ ] Relationship type is correct (dependency, composition, inheritance, etc.)
216
- - [ ] Direction is accurate (source target)
216
+ - [ ] Direction is accurate (source -> target)
217
217
  - [ ] Both components exist in the analysis
218
218
 
219
219
  2. **Completeness:**
@@ -124,19 +124,19 @@ def get_tree_string(
124
124
  entries = sorted([p for p in startpath.iterdir() if not (ignore_manager and ignore_manager.should_ignore(p))])
125
125
  except (PermissionError, FileNotFoundError):
126
126
  # Handle permission errors or non-existent directories
127
- return [indent + "└── [Error reading directory]"]
127
+ return [indent + "`--[Error reading directory]"]
128
128
 
129
129
  for i, entry_path in enumerate(entries):
130
130
  # Check if we've exceeded the maximum number of lines
131
131
  if len(tree_lines) >= max_lines:
132
- tree_lines.append(indent + "└── [Output truncated due to size limits]")
132
+ tree_lines.append(indent + "`-- [Output truncated due to size limits]")
133
133
  return tree_lines
134
134
 
135
- connector = "└── " if i == len(entries) - 1 else "├── "
135
+ connector = "`-- " if i == len(entries) - 1 else "|-- "
136
136
  tree_lines.append(indent + connector + entry_path.name)
137
137
 
138
138
  if entry_path.is_dir():
139
- extension = " " if i == len(entries) - 1 else " "
139
+ extension = " " if i == len(entries) - 1 else "| "
140
140
  subtree = get_tree_string(
141
141
  entry_path,
142
142
  indent + extension,
@@ -149,8 +149,8 @@ def get_tree_string(
149
149
 
150
150
  # Check again after adding subtree
151
151
  if len(tree_lines) >= max_lines:
152
- if tree_lines[-1] != indent + "└── [Output truncated due to size limits]":
153
- tree_lines.append(indent + "└── [Output truncated due to size limits]")
152
+ if tree_lines[-1] != indent + "`-- [Output truncated due to size limits]":
153
+ tree_lines.append(indent + "`-- [Output truncated due to size limits]")
154
154
  return tree_lines
155
155
 
156
156
  return tree_lines
@@ -95,7 +95,7 @@ class CodeReferenceReader(BaseRepoTool):
95
95
  logger.error(f"[Source Reference Tool] File {file_path} does not exist.")
96
96
  return f"Error: File {file_path} does not exist."
97
97
 
98
- with open(file_path, "r") as f:
98
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
99
99
  lines = f.readlines()
100
100
 
101
101
  if start_line < 0 or end_line > len(lines):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License: MIT
@@ -135,4 +135,5 @@ tests/test_install.py
135
135
  tests/test_logging_config.py
136
136
  tests/test_main.py
137
137
  tests/test_vscode_constants.py
138
- tests/test_windows_compatibility.py
138
+ tests/test_windows_compatibility.py
139
+ tests/test_windows_encoding.py
@@ -114,7 +114,7 @@ class DiagramGenerator:
114
114
  )
115
115
  if health_report is not None:
116
116
  health_path = Path(self.output_dir) / "health" / "health_report.json"
117
- with open(health_path, "w") as f:
117
+ with open(health_path, "w", encoding="utf-8") as f:
118
118
  f.write(health_report.model_dump_json(indent=2, exclude_none=True))
119
119
  logger.info(f"Health report written to {health_path} (score: {health_report.overall_score:.3f})")
120
120
  else:
@@ -145,7 +145,7 @@ class DiagramGenerator:
145
145
  )
146
146
 
147
147
  coverage_path = Path(self.output_dir) / "file_coverage.json"
148
- with open(coverage_path, "w") as f:
148
+ with open(coverage_path, "w", encoding="utf-8") as f:
149
149
  f.write(report.model_dump_json(indent=2, exclude_none=True))
150
150
  logger.info(f"File coverage report written to {coverage_path}")
151
151
 
@@ -232,7 +232,7 @@ class DiagramGenerator:
232
232
  self._monitoring_agents["AbstractionAgent"] = self.abstraction_agent
233
233
 
234
234
  version_file = Path(self.output_dir) / "codeboarding_version.json"
235
- with open(version_file, "w") as f:
235
+ with open(version_file, "w", encoding="utf-8") as f:
236
236
  f.write(
237
237
  Version(
238
238
  commit_hash=get_git_commit_hash(self.repo_location),
@@ -253,7 +253,7 @@ class DiagramGenerator:
253
253
 
254
254
  # Save code_stats.json
255
255
  code_stats_file = monitoring_dir / "code_stats.json"
256
- with open(code_stats_file, "w") as f:
256
+ with open(code_stats_file, "w", encoding="utf-8") as f:
257
257
  json.dump(static_stats, f, indent=2)
258
258
  logger.debug(f"Written code_stats.json to {code_stats_file}")
259
259
 
@@ -187,7 +187,7 @@ class FileCoverage:
187
187
  return None
188
188
 
189
189
  try:
190
- with open(coverage_path, "r") as f:
190
+ with open(coverage_path, "r", encoding="utf-8") as f:
191
191
  data = json.load(f)
192
192
  # Validate basic structure
193
193
  if "analyzed_files" in data and "not_analyzed_files" in data:
@@ -207,6 +207,6 @@ class FileCoverage:
207
207
  coverage: File coverage dictionary to save
208
208
  """
209
209
  coverage_path = output_dir / "file_coverage.json"
210
- with open(coverage_path, "w") as f:
210
+ with open(coverage_path, "w", encoding="utf-8") as f:
211
211
  json.dump(coverage, f, indent=2)
212
212
  logger.info(f"File coverage saved to {coverage_path}")
@@ -79,7 +79,7 @@ class _AnalysisFileStore:
79
79
  return None
80
80
 
81
81
  try:
82
- with open(self._analysis_path, "r") as f:
82
+ with open(self._analysis_path, "r", encoding="utf-8") as f:
83
83
  data = json.load(f)
84
84
 
85
85
  root_analysis, sub_analyses = parse_unified_analysis(data)
@@ -200,7 +200,7 @@ class _AnalysisFileStore:
200
200
  sub_expandable = self._compute_expandable_components(sub, parent_had_clusters=parent_had_clusters)
201
201
  sub_analyses_tuples[cid] = (sub, sub_expandable)
202
202
 
203
- with open(self._analysis_path, "w") as f:
203
+ with open(self._analysis_path, "w", encoding="utf-8") as f:
204
204
  f.write(
205
205
  build_unified_analysis_json(
206
206
  analysis=analysis,
@@ -58,11 +58,11 @@ class ChangeImpact:
58
58
  f"Dirty components: {self.dirty_components}",
59
59
  ]
60
60
  if self.components_needing_reexpansion:
61
- lines.append(f"🔄 Components needing re-expansion: {self.components_needing_reexpansion}")
61
+ lines.append(f"Components needing re-expansion: {self.components_needing_reexpansion}")
62
62
  if self.architecture_dirty:
63
- lines.append("⚠️ Architecture refresh needed")
63
+ lines.append("WARNING: Architecture refresh needed")
64
64
  if self.unassigned_files:
65
- lines.append(f"⚠️ Unassigned files: {self.unassigned_files}")
65
+ lines.append(f"WARNING: Unassigned files: {self.unassigned_files}")
66
66
  return "\n".join(lines)
67
67
 
68
68
 
@@ -109,7 +109,7 @@ def save_manifest(manifest: AnalysisManifest, output_dir: Path) -> Path:
109
109
  """
110
110
  manifest_path = output_dir / MANIFEST_FILENAME
111
111
 
112
- with open(manifest_path, "w") as f:
112
+ with open(manifest_path, "w", encoding="utf-8") as f:
113
113
  f.write(manifest.model_dump_json(indent=2))
114
114
 
115
115
  logger.info(f"Saved analysis manifest to {manifest_path}")
@@ -129,7 +129,7 @@ def load_manifest(output_dir: Path) -> AnalysisManifest | None:
129
129
  return None
130
130
 
131
131
  try:
132
- with open(manifest_path, "r") as f:
132
+ with open(manifest_path, "r", encoding="utf-8") as f:
133
133
  data = json.load(f)
134
134
 
135
135
  manifest = AnalysisManifest.model_validate(data)
@@ -22,7 +22,7 @@ def _load_all_analyses(analysis_path: Path) -> list[tuple[str, AnalysisInsights,
22
22
 
23
23
  Returns the root analysis as 'overview' plus one entry per expanded component.
24
24
  """
25
- with open(analysis_path, "r") as f:
25
+ with open(analysis_path, "r", encoding="utf-8") as f:
26
26
  data = json.load(f)
27
27
 
28
28
  root_analysis, sub_analyses = parse_unified_analysis(data)
@@ -1,3 +1,4 @@
1
+ import io
1
2
  import logging
2
3
  import logging.config
3
4
  import os
@@ -87,6 +88,19 @@ def setup_logging(
87
88
 
88
89
  logging.config.dictConfig(config)
89
90
 
91
+ # Reconfigure the console handler's stream to use 'replace' error handling
92
+ # so that non-encodable Unicode characters (e.g. \u2011 on Windows cp1251)
93
+ # don't crash the logging system.
94
+ for handler in logging.root.handlers:
95
+ if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler):
96
+ stream = handler.stream
97
+ if hasattr(stream, "reconfigure"):
98
+ stream.reconfigure(errors="replace")
99
+ elif hasattr(stream, "encoding") and stream.encoding and stream.encoding.lower() != "utf-8":
100
+ handler.stream = io.TextIOWrapper(
101
+ stream.buffer, encoding=stream.encoding, errors="replace", line_buffering=stream.line_buffering
102
+ )
103
+
90
104
  # Handle _latest.log symlink
91
105
  latest_log_path = logs_dir / "_latest.log"
92
106
  try:
@@ -82,7 +82,7 @@ def generate_markdown_docs(
82
82
 
83
83
  # Load the single unified analysis.json
84
84
  analysis_path = analysis_files[0]
85
- with open(analysis_path, "r") as f:
85
+ with open(analysis_path, "r", encoding="utf-8") as f:
86
86
  data = json.load(f)
87
87
 
88
88
  root_analysis, sub_analyses = parse_unified_analysis(data)
@@ -111,18 +111,18 @@ def monitor_execution(
111
111
  # Cleanup & Save Summary (Happens automatically on exit/crash)
112
112
  summary_file = out_path / f"summary.json"
113
113
  try:
114
- with open(summary_file, "w") as f:
114
+ with open(summary_file, "w", encoding="utf-8") as f:
115
115
  json.dump(run_stats.to_dict(), f, indent=2)
116
- logger.debug(f"Monitoring summary saved to {summary_file}")
116
+ logger.debug(f"Monitoring summary saved to {summary_file}")
117
117
  except Exception as e:
118
118
  logger.error(f"Failed to save monitoring summary: {e}")
119
119
 
120
120
  # Cleanup handler
121
121
  trace_logger.removeHandler(trace_handler)
122
122
  trace_handler.close()
123
- logger.debug(f"Execution traces saved to {trace_file}")
123
+ logger.debug(f"Execution traces saved to {trace_file}")
124
124
 
125
- logger.info(f"Run results saved to {out_path}")
125
+ logger.info(f"Run results saved to {out_path}")
126
126
 
127
127
  # Reset context var
128
128
  current_stats.reset(stats_token)
@@ -129,7 +129,7 @@ class StreamingStatsWriter:
129
129
 
130
130
  # Atomic write
131
131
  temp_file = self.llm_usage_file.with_suffix(".tmp")
132
- with open(temp_file, "w") as f:
132
+ with open(temp_file, "w", encoding="utf-8") as f:
133
133
  json.dump(data, f, indent=2)
134
134
  os.replace(temp_file, self.llm_usage_file)
135
135
 
@@ -165,7 +165,7 @@ class StreamingStatsWriter:
165
165
  }
166
166
 
167
167
  metadata_file = self.monitoring_dir / "run_metadata.json"
168
- with open(metadata_file, "w") as f:
168
+ with open(metadata_file, "w", encoding="utf-8") as f:
169
169
  json.dump(metadata, f, indent=2)
170
170
 
171
171
  except Exception as e:
@@ -124,7 +124,7 @@ def generate_markdown_file(
124
124
  repo_path=repo_path,
125
125
  )
126
126
  markdown_file = temp_dir / f"{file_name}.md"
127
- with open(markdown_file, "w") as f:
127
+ with open(markdown_file, "w", encoding="utf-8") as f:
128
128
  f.write(content)
129
129
  return markdown_file
130
130
 
@@ -155,7 +155,7 @@ def generate_mdx_file(
155
155
  repo_path=repo_path,
156
156
  )
157
157
  mdx_file = temp_dir / f"{file_name}.mdx"
158
- with open(mdx_file, "w") as f:
158
+ with open(mdx_file, "w", encoding="utf-8") as f:
159
159
  f.write(content)
160
160
  return mdx_file
161
161
 
@@ -156,7 +156,7 @@ def generate_rst_file(
156
156
  repo_path=repo_path,
157
157
  )
158
158
  rst_file = temp_dir / f"{file_name}.rst"
159
- with open(rst_file, "w") as f:
159
+ with open(rst_file, "w", encoding="utf-8") as f:
160
160
  f.write(content)
161
161
  return rst_file
162
162
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboarding"
7
- version = "0.9.3"
7
+ version = "0.9.4"
8
8
  description = "Interactive Diagrams for Code"
9
9
  readme = "PYPI.md"
10
10
  license = {text = "MIT"}
@@ -175,7 +175,7 @@ class ClusterChangeAnalyzer:
175
175
  for new_id in new_clusters.get_cluster_ids():
176
176
  new_nodes = new_clusters.get_nodes_for_cluster(new_id)
177
177
 
178
- # Calculate Jaccard similarity: |A B| / |A B|
178
+ # Calculate Jaccard similarity: |A & B| / |A | B|
179
179
  intersection = old_nodes & new_nodes
180
180
  union = old_nodes | new_nodes
181
181
 
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
5
5
  import networkx as nx
6
6
  import networkx.algorithms.community as nx_comm
7
7
 
8
- from static_analyzer.constants import ClusteringConfig, NodeType, Language, Node
8
+ from static_analyzer.constants import ClusteringConfig, Language, Node, NodeType
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
@@ -639,7 +639,7 @@ class CallGraph:
639
639
  if src_cluster is None or dst_cluster is None:
640
640
  continue
641
641
  if src_cluster != dst_cluster:
642
- cluster_to_cluster_calls[src_cluster][dst_cluster].append(f"{src} {dst}")
642
+ cluster_to_cluster_calls[src_cluster][dst_cluster].append(f"{src} -> {dst}")
643
643
 
644
644
  inter_cluster_str = "Inter-Cluster Connections:\n\n"
645
645
  if cluster_to_cluster_calls:
@@ -649,7 +649,7 @@ class CallGraph:
649
649
  src_display = src_cluster_id + 1
650
650
  dst_display = dst_cluster_id + 1
651
651
 
652
- inter_cluster_str += f"Cluster {src_display} Cluster {dst_display} via method calls:\n"
652
+ inter_cluster_str += f"Cluster {src_display} -> Cluster {dst_display} via method calls:\n"
653
653
  for call in calls:
654
654
  inter_cluster_str += f" - {call}\n"
655
655
  inter_cluster_str += "\n"
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
+ import platform
4
5
  import subprocess
5
6
  import threading
6
7
  import time
@@ -105,6 +106,9 @@ class LSPClient(ABC):
105
106
  self._responses: dict[int, dict] = {}
106
107
  self._notifications: list[dict] = []
107
108
  self._lock = threading.RLock()
109
+ # Separate lock for stdin writes to avoid holding _lock during
110
+ # potentially blocking I/O (pipe buffer full on Windows).
111
+ self._stdin_lock = threading.Lock()
108
112
  self._response_condition = threading.Condition(self._lock)
109
113
 
110
114
  # Initialize CallGraph
@@ -152,16 +156,17 @@ class LSPClient(ABC):
152
156
  message_id = self._message_id
153
157
  self._message_id += 1
154
158
 
155
- request = {
156
- "jsonrpc": "2.0",
157
- "id": message_id,
158
- "method": method,
159
- "params": params,
160
- }
159
+ request = {
160
+ "jsonrpc": "2.0",
161
+ "id": message_id,
162
+ "method": method,
163
+ "params": params,
164
+ }
161
165
 
162
- body = json.dumps(request)
163
- message = f"Content-Length: {len(body)}\r\n\r\n{body}"
166
+ body = json.dumps(request)
167
+ message = f"Content-Length: {len(body)}\r\n\r\n{body}"
164
168
 
169
+ with self._stdin_lock:
165
170
  if self._process and self._process.stdin:
166
171
  self._process.stdin.write(message.encode("utf-8"))
167
172
  self._process.stdin.flush()
@@ -178,7 +183,7 @@ class LSPClient(ABC):
178
183
  body = json.dumps(notification)
179
184
  message = f"Content-Length: {len(body)}\r\n\r\n{body}"
180
185
 
181
- with self._lock:
186
+ with self._stdin_lock:
182
187
  if self._process and self._process.stdin:
183
188
  self._process.stdin.write(message.encode("utf-8"))
184
189
  self._process.stdin.flush()
@@ -396,7 +401,7 @@ class LSPClient(ABC):
396
401
  body_bytes = body.encode("utf-8")
397
402
  header = f"Content-Length: {len(body_bytes)}\r\n\r\n"
398
403
 
399
- with self._lock:
404
+ with self._stdin_lock:
400
405
  if self._process and self._process.stdin:
401
406
  self._process.stdin.write(header.encode("utf-8") + body_bytes)
402
407
  self._process.stdin.flush()
@@ -754,7 +759,16 @@ class LSPClient(ABC):
754
759
  logger.info(f"Found {len(all_classes)} classes in workspace")
755
760
 
756
761
  cpu_count = os.cpu_count()
757
- max_workers = max(1, cpu_count - 1) if cpu_count else 1 # Use the number of cores but reserve one
762
+ if platform.system() == "Windows":
763
+ # On Windows, limit to 1 worker to prevent pipe buffer deadlock.
764
+ # Multiple concurrent stdin writes can fill the 4KB pipe buffer;
765
+ # if the LSP server then sends a request (e.g. workspace/configuration)
766
+ # and blocks waiting for our response, but we can't write it because
767
+ # the pipe is full, we deadlock. Sequential writes avoid this.
768
+ max_workers = 1
769
+ else:
770
+ max_workers = max(1, cpu_count - 1) if cpu_count else 1
771
+ logger.info(f"Starting file analysis with {max_workers} workers for {total_files} files...")
758
772
  successful_results = []
759
773
 
760
774
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
@@ -764,6 +778,7 @@ class LSPClient(ABC):
764
778
  }
765
779
 
766
780
  # Collect results as they complete
781
+ files_done = 0
767
782
  with tqdm(total=total_files, desc="[Unified Analysis] Processing files") as pbar:
768
783
  for future in as_completed(future_to_file):
769
784
  file_path = future_to_file[future]
@@ -782,7 +797,10 @@ class LSPClient(ABC):
782
797
  except Exception as e:
783
798
  logger.error(f"Exception processing {file_path}: {e}")
784
799
  finally:
800
+ files_done += 1
785
801
  pbar.update(1)
802
+ if files_done % 10 == 0 or files_done == total_files:
803
+ logger.info(f"Static analysis progress: {files_done}/{total_files} files done")
786
804
 
787
805
  logger.info(f"Successfully processed {len(successful_results)} files")
788
806