codeboarding 0.15.0__tar.gz → 0.15.2__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 (196) hide show
  1. {codeboarding-0.15.0 → codeboarding-0.15.2}/PKG-INFO +1 -1
  2. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/llm_errors.py +66 -39
  3. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/tree_planner_agent.py +4 -2
  4. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding.egg-info/PKG-INFO +1 -1
  5. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding.egg-info/SOURCES.txt +3 -0
  6. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/commands/full_analysis.py +3 -3
  7. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/commands/incremental_analysis.py +3 -3
  8. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/main.py +10 -15
  9. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/diagram_generator.py +6 -4
  10. {codeboarding-0.15.0 → codeboarding-0.15.2}/pyproject.toml +5 -1
  11. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/__init__.py +11 -0
  12. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/analysis_cache.py +2 -1
  13. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/typescript_adapter.py +31 -1
  14. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/language_adapter.py +8 -0
  15. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/lsp_client.py +13 -3
  16. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/exceptions.py +4 -0
  17. codeboarding-0.15.2/static_analyzer/typescript_plugin/node_modules/codeboarding-workspace-sources/index.js +132 -0
  18. codeboarding-0.15.2/static_analyzer/typescript_plugin/node_modules/codeboarding-workspace-sources/package.json +6 -0
  19. codeboarding-0.15.2/static_analyzer/typescript_workspace.py +81 -0
  20. {codeboarding-0.15.0 → codeboarding-0.15.2}/LICENSE +0 -0
  21. {codeboarding-0.15.0 → codeboarding-0.15.2}/PYPI.md +0 -0
  22. {codeboarding-0.15.0 → codeboarding-0.15.2}/README.md +0 -0
  23. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/__init__.py +0 -0
  24. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/agent_responses.py +0 -0
  25. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/change_status.py +0 -0
  26. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/component_ownership.py +0 -0
  27. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/constants.py +0 -0
  28. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/content_hash.py +0 -0
  29. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/file_index_models.py +0 -0
  30. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/incremental_results.py +0 -0
  31. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/llm_config.py +0 -0
  32. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/llm_renderers/__init__.py +0 -0
  33. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/llm_renderers/call_graph.py +0 -0
  34. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/llm_renderers/scope.py +0 -0
  35. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/model_capabilities.py +0 -0
  36. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/planner_agent.py +0 -0
  37. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/prompts/__init__.py +0 -0
  38. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/prompts/profiles.py +0 -0
  39. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/prompts/shared.py +0 -0
  40. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/relation_edges.py +0 -0
  41. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/retry.py +0 -0
  42. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/scope_analysis_agent.py +0 -0
  43. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/scope_ids.py +0 -0
  44. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/tools/__init__.py +0 -0
  45. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/tools/base.py +0 -0
  46. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/tools/get_method_calls.py +0 -0
  47. {codeboarding-0.15.0 → codeboarding-0.15.2}/agents/tools/read_file.py +0 -0
  48. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding.egg-info/dependency_links.txt +0 -0
  49. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding.egg-info/entry_points.txt +0 -0
  50. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding.egg-info/requires.txt +0 -0
  51. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding.egg-info/top_level.txt +0 -0
  52. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/__init__.py +0 -0
  53. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/bootstrap.py +0 -0
  54. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/commands/__init__.py +0 -0
  55. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/commands/partial_analysis.py +0 -0
  56. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/github_action.py +0 -0
  57. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/render.py +0 -0
  58. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_cli/view_instructions.py +0 -0
  59. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/__init__.py +0 -0
  60. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/analysis.py +0 -0
  61. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/fingerprint_diff.py +0 -0
  62. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/orchestration.py +0 -0
  63. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/sources/__init__.py +0 -0
  64. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/sources/local.py +0 -0
  65. {codeboarding-0.15.0 → codeboarding-0.15.2}/codeboarding_workflows/sources/remote.py +0 -0
  66. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/__init__.py +0 -0
  67. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/analysis_json.py +0 -0
  68. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/cluster_relations.py +0 -0
  69. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/exceptions.py +0 -0
  70. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/file_coverage.py +0 -0
  71. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/file_index.py +0 -0
  72. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/incremental_changes.py +0 -0
  73. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/incremental_update.py +0 -0
  74. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/io_utils.py +0 -0
  75. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/reference_resolver.py +0 -0
  76. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/run_context.py +0 -0
  77. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/run_mode.py +0 -0
  78. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/scope_assembly.py +0 -0
  79. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/scope_plan.py +0 -0
  80. {codeboarding-0.15.0 → codeboarding-0.15.2}/diagram_analysis/tree_shape.py +0 -0
  81. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/__init__.py +0 -0
  82. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/checks/__init__.py +0 -0
  83. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/checks/circular_deps.py +0 -0
  84. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/checks/function_size.py +0 -0
  85. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/checks/unused_code_diagnostics.py +0 -0
  86. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/config.py +0 -0
  87. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/main.py +0 -0
  88. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/models.py +0 -0
  89. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/plugins/__init__.py +0 -0
  90. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/plugins/plugin_loader.py +0 -0
  91. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/plugins/protocols.py +0 -0
  92. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/plugins/registry.py +0 -0
  93. {codeboarding-0.15.0 → codeboarding-0.15.2}/health/runner.py +0 -0
  94. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/__init__.py +0 -0
  95. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/clustering_ids.py +0 -0
  96. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/constants.py +0 -0
  97. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/install.py +0 -0
  98. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/logging_config.py +0 -0
  99. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/__init__.py +0 -0
  100. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/callbacks.py +0 -0
  101. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/context.py +0 -0
  102. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/mixin.py +0 -0
  103. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/paths.py +0 -0
  104. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/stats.py +0 -0
  105. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/telemetry/__init__.py +0 -0
  106. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/telemetry/device_id.py +0 -0
  107. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/telemetry/events.py +0 -0
  108. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/telemetry/schemas.py +0 -0
  109. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/telemetry/service.py +0 -0
  110. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/monitoring/writers.py +0 -0
  111. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/repo_utils/__init__.py +0 -0
  112. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/repo_utils/change_detector.py +0 -0
  113. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/repo_utils/errors.py +0 -0
  114. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/repo_utils/git_ops.py +0 -0
  115. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/repo_utils/ignore.py +0 -0
  116. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/repo_utils/path_utils.py +0 -0
  117. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/__init__.py +0 -0
  118. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/grammars.py +0 -0
  119. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/installers.py +0 -0
  120. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/java.py +0 -0
  121. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/manifest.py +0 -0
  122. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/paths.py +0 -0
  123. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/tool_registry/registry.py +0 -0
  124. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/user_config.py +0 -0
  125. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/utils.py +0 -0
  126. {codeboarding-0.15.0 → codeboarding-0.15.2}/infra/vscode_constants.py +0 -0
  127. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/__init__.py +0 -0
  128. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/html.py +0 -0
  129. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/html_template.py +0 -0
  130. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/markdown.py +0 -0
  131. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/mdx.py +0 -0
  132. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/rendering.py +0 -0
  133. {codeboarding-0.15.0 → codeboarding-0.15.2}/output_generators/sphinx.py +0 -0
  134. {codeboarding-0.15.0 → codeboarding-0.15.2}/setup.cfg +0 -0
  135. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/analysis_result.py +0 -0
  136. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/cfg/__init__.py +0 -0
  137. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/cfg/call_graph.py +0 -0
  138. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/cfg/edge.py +0 -0
  139. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/cfg/location_key.py +0 -0
  140. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/__init__.py +0 -0
  141. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/exceptions.py +0 -0
  142. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/models.py +0 -0
  143. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/__init__.py +0 -0
  144. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/draft.py +0 -0
  145. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/frontier.py +0 -0
  146. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/inventory.py +0 -0
  147. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/replay.py +0 -0
  148. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/spec.py +0 -0
  149. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/names/tokens.py +0 -0
  150. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/clustering/service.py +0 -0
  151. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/config.py +0 -0
  152. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/csharp_config_scanner.py +0 -0
  153. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/dotnet_sdk.py +0 -0
  154. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/dotnet_solution.py +0 -0
  155. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/__init__.py +0 -0
  156. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/__init__.py +0 -0
  157. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/csharp_adapter.py +0 -0
  158. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/go_adapter.py +0 -0
  159. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/java_adapter.py +0 -0
  160. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/kotlin_adapter.py +0 -0
  161. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/php_adapter.py +0 -0
  162. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/python_adapter.py +0 -0
  163. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/adapters/rust_adapter.py +0 -0
  164. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/call_graph_builder.py +0 -0
  165. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/edge_build_context.py +0 -0
  166. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/edge_builder.py +0 -0
  167. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/hierarchy_builder.py +0 -0
  168. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/lsp_constants.py +0 -0
  169. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/lsp_recycler.py +0 -0
  170. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/models.py +0 -0
  171. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/process_memory.py +0 -0
  172. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/progress.py +0 -0
  173. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/protocols.py +0 -0
  174. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/result_converter.py +0 -0
  175. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/source_inspector.py +0 -0
  176. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/symbol_table.py +0 -0
  177. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/engine/utils.py +0 -0
  178. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/external_calls.py +0 -0
  179. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/graph_definitions.py +0 -0
  180. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/incremental_orchestrator.py +0 -0
  181. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/internal_references.py +0 -0
  182. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/java_config_scanner.py +0 -0
  183. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/java_utils.py +0 -0
  184. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/language_results.py +0 -0
  185. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/lsp_client/__init__.py +0 -0
  186. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/lsp_client/diagnostics.py +0 -0
  187. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/node.py +0 -0
  188. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/programming_language.py +0 -0
  189. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/scanner.py +0 -0
  190. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/typescript_config_scanner.py +0 -0
  191. {codeboarding-0.15.0 → codeboarding-0.15.2}/static_analyzer/workspace_dependencies.py +0 -0
  192. {codeboarding-0.15.0 → codeboarding-0.15.2}/tests/test_persisted_analysis_artifacts.py +0 -0
  193. {codeboarding-0.15.0 → codeboarding-0.15.2}/tests/test_pyproject_packages.py +0 -0
  194. {codeboarding-0.15.0 → codeboarding-0.15.2}/tests/test_view_instructions.py +0 -0
  195. {codeboarding-0.15.0 → codeboarding-0.15.2}/tests/test_windows_compatibility.py +0 -0
  196. {codeboarding-0.15.0 → codeboarding-0.15.2}/tests/test_windows_encoding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.15.0
3
+ Version: 0.15.2
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -1,17 +1,4 @@
1
- """Provider-agnostic detection and typing of LLM authentication failures.
2
-
3
- A rejected API key (HTTP 401, or a provider's equivalent auth/permission
4
- error) is *permanent* for the run: retrying it wastes minutes of backoff and
5
- floods telemetry with identical ``$exception`` events. The agent retry loop and
6
- the CLI both need to recognize one when they see it, regardless of which SDK
7
- raised it.
8
-
9
- ``detect_auth_error`` maps a raw provider exception to an :class:`LLMAuthError`
10
- (or ``None`` when it isn't an auth failure). The typed error carries the
11
- provider, a masked key tail, and the provider's own message, and forwards them
12
- as ``telemetry_properties`` so the PostHog ``$exception`` event — and the
13
- dashboard's structured ``error`` columns — populate instead of staying null.
14
- """
1
+ """Provider-agnostic LLM failure classification, exceptions, and retry helpers."""
15
2
 
16
3
  from __future__ import annotations
17
4
 
@@ -33,6 +20,11 @@ EXIT_AUTH_ERROR = 2
33
20
  # Process exit code for an exhausted token or credit quota: the key is fine, the account is not.
34
21
  EXIT_QUOTA_EXHAUSTED = 3
35
22
 
23
+ CODEBOARDING_QUOTA_CODE = "free_credits_exhausted"
24
+ # Response contract: licensing-aws/gha_proxy/handler.py.
25
+ PROXY_ERROR_TYPE = "gha_proxy"
26
+ PROXY_QUOTA_MESSAGE = "Resource exhausted: token limit reached"
27
+
36
28
  # Class names, across SDKs, that always mean "credentials were rejected".
37
29
  # openai/anthropic/cerebras raise ``AuthenticationError``; google raises
38
30
  # ``Unauthenticated``/``PermissionDenied``; Bedrock (botocore) surfaces
@@ -114,6 +106,65 @@ class LLMAuthError(RuntimeError):
114
106
  self.telemetry_properties = telemetry_properties
115
107
 
116
108
 
109
+ class LLMQuotaError(RuntimeError):
110
+ """An LLM quota is exhausted, with recovery guidance and failure telemetry."""
111
+
112
+ def __init__(self, error: BaseException):
113
+ self.telemetry_properties = llm_failure_properties(error)
114
+ self.is_codeboarding_quota = _is_codeboarding_quota(error)
115
+ _, key_tail = current_provider_key_context()
116
+ if self.is_codeboarding_quota:
117
+ message = (
118
+ "Your CodeBoarding quota is exhausted. "
119
+ "Buy a CodeBoarding license at https://codeboarding.org/pricing, "
120
+ "or wait for your weekly reset."
121
+ )
122
+ elif key_tail == CODEBOARDING_KEY_TAIL:
123
+ message = (
124
+ "The hosted LLM provider's quota is exhausted. "
125
+ "This is a CodeBoarding service issue. Please try again later or contact CodeBoarding support."
126
+ )
127
+ else:
128
+ message = (
129
+ "The LLM provider's token or credit quota is exhausted. "
130
+ "Add credits or raise the quota for this provider, then re-run."
131
+ )
132
+ super().__init__(message)
133
+
134
+
135
+ def llm_failure_properties(exc: BaseException) -> dict:
136
+ """Classify a failed LLM call for exception telemetry."""
137
+ quota = is_quota_exhausted(exc)
138
+ context = isinstance(exc, ScopeContextTooLargeError) or any(p.search(str(exc)) for p in _CONTEXT_OVERFLOW_PATTERNS)
139
+ provider, _key_tail = current_provider_key_context()
140
+ properties = {
141
+ "error_type": "quota" if quota else "context" if context else "llm",
142
+ "error_provider": provider,
143
+ "error_status_code": _status_code(exc),
144
+ "error_message": str(exc)[:500],
145
+ }
146
+ if _is_codeboarding_quota(exc):
147
+ properties["error_code"] = CODEBOARDING_QUOTA_CODE
148
+ return properties
149
+
150
+
151
+ def is_quota_exhausted(exc: BaseException) -> bool:
152
+ status = _status_code(exc)
153
+ quota_code = getattr(exc, "code", None) == "insufficient_quota" or "insufficient_quota" in str(exc)
154
+ return status == 402 or (status == 429 and quota_code)
155
+
156
+
157
+ def _is_codeboarding_quota(exc: BaseException) -> bool:
158
+ body = getattr(exc, "body", {})
159
+ error = body.get("error", body) if isinstance(body, dict) else {}
160
+ return (
161
+ _status_code(exc) == 402
162
+ and isinstance(error, dict)
163
+ and error.get("type") == PROXY_ERROR_TYPE
164
+ and error.get("message") == PROXY_QUOTA_MESSAGE
165
+ )
166
+
167
+
117
168
  def _status_code(exc: BaseException) -> int | None:
118
169
  code = getattr(exc, "status_code", None)
119
170
  if code is None:
@@ -183,30 +234,6 @@ def detect_auth_error(exc: BaseException, *, provider: str, key_tail: str) -> LL
183
234
  )
184
235
 
185
236
 
186
- def _is_quota_exhausted(exc: BaseException) -> bool:
187
- status = _status_code(exc)
188
- quota_code = getattr(exc, "code", None) == "insufficient_quota" or "insufficient_quota" in str(exc)
189
- return status == 402 or (status == 429 and quota_code)
190
-
191
-
192
- def llm_failure_properties(exc: BaseException) -> dict:
193
- """``$exception`` properties for a failed LLM call, telling an exhausted quota or an oversized prompt
194
- apart from other failures.
195
-
196
- Why: the hosted proxy's paywall is a 402, and OpenAI reports an empty balance as a 429 ``insufficient_quota``.
197
- An oversized prompt is ours to fix, not the provider's, so it gets its own type on the dashboard.
198
- """
199
- quota = _is_quota_exhausted(exc)
200
- context = isinstance(exc, ScopeContextTooLargeError) or any(p.search(str(exc)) for p in _CONTEXT_OVERFLOW_PATTERNS)
201
- provider, _key_tail = current_provider_key_context()
202
- return {
203
- "error_type": "quota" if quota else "context" if context else "llm",
204
- "error_provider": provider,
205
- "error_status_code": _status_code(exc),
206
- "error_message": str(exc)[:500],
207
- }
208
-
209
-
210
237
  def is_transient_llm_error(exc: BaseException) -> bool:
211
238
  """Whether the same request may succeed if sent again: throttled, a server error, or no answer at all.
212
239
 
@@ -216,7 +243,7 @@ def is_transient_llm_error(exc: BaseException) -> bool:
216
243
  """
217
244
  status = _status_code(exc)
218
245
  if status is not None:
219
- return status in (408, 429) and not _is_quota_exhausted(exc) or status >= 500
246
+ return status in (408, 429) and not is_quota_exhausted(exc) or status >= 500
220
247
  return any(cls.__name__ in _TRANSIENT_TYPE_NAMES for cls in type(exc).__mro__)
221
248
 
222
249
 
@@ -16,7 +16,7 @@ from langchain_core.prompts import PromptTemplate
16
16
  from agents.agent_responses import PlannedGroup, TreePlanInsights
17
17
  from agents.llm_config import get_current_prompt_profile, supports_json_mode
18
18
  from infra.monitoring.callbacks import MONITORING_CALLBACK
19
- from agents.llm_errors import LLMAuthError, raise_if_auth_error
19
+ from agents.llm_errors import LLMAuthError, LLMQuotaError, is_quota_exhausted, raise_if_auth_error
20
20
  from agents.prompts import get_tree_plan_prompts
21
21
  from agents.retry import LLM_MAX_ATTEMPTS, RetryAction, RetryDecision, classify_llm_error, with_retries
22
22
  from infra.monitoring import trace
@@ -103,7 +103,7 @@ class TreePlannerAgent(MonitoringMixin):
103
103
  return folded
104
104
 
105
105
  def _draw(self, prompt: str, labelled: dict[str, CandidateGroup]) -> list[TreePlanInsights]:
106
- """``draws`` answers to one prompt, drawn concurrently; a draw that fails is dropped."""
106
+ """Draw concurrently, dropping failed draws unless credentials or quota prevent further work."""
107
107
  # Not ``with``: its ``__exit__`` waits for a stalled draw; a copied context carries the
108
108
  # monitoring step into each worker.
109
109
  pool = ThreadPoolExecutor(max_workers=self.draws)
@@ -117,6 +117,8 @@ class TreePlannerAgent(MonitoringMixin):
117
117
  except LLMAuthError:
118
118
  raise
119
119
  except Exception as exc:
120
+ if is_quota_exhausted(exc):
121
+ raise LLMQuotaError(exc) from exc
120
122
  failures.append(exc)
121
123
  logger.warning("[Planner] draw failed: %s", exc)
122
124
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.15.0
3
+ Version: 0.15.2
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -140,6 +140,7 @@ static_analyzer/node.py
140
140
  static_analyzer/programming_language.py
141
141
  static_analyzer/scanner.py
142
142
  static_analyzer/typescript_config_scanner.py
143
+ static_analyzer/typescript_workspace.py
143
144
  static_analyzer/workspace_dependencies.py
144
145
  static_analyzer/cfg/__init__.py
145
146
  static_analyzer/cfg/call_graph.py
@@ -184,6 +185,8 @@ static_analyzer/engine/adapters/rust_adapter.py
184
185
  static_analyzer/engine/adapters/typescript_adapter.py
185
186
  static_analyzer/lsp_client/__init__.py
186
187
  static_analyzer/lsp_client/diagnostics.py
188
+ static_analyzer/typescript_plugin/node_modules/codeboarding-workspace-sources/index.js
189
+ static_analyzer/typescript_plugin/node_modules/codeboarding-workspace-sources/package.json
187
190
  tests/test_persisted_analysis_artifacts.py
188
191
  tests/test_pyproject_packages.py
189
192
  tests/test_view_instructions.py
@@ -5,7 +5,8 @@ from pathlib import Path
5
5
  from tqdm import tqdm
6
6
 
7
7
  from agents.llm_config import LLMConfigError
8
- from agents.llm_errors import LLMAuthError
8
+ from agents.llm_errors import LLMAuthError, LLMQuotaError
9
+ from diagram_analysis.exceptions import ScopeSemanticsError
9
10
  from codeboarding_cli.bootstrap import bootstrap_environment, resolve_local_run_paths
10
11
  from codeboarding_cli.view_instructions import print_view_instructions
11
12
  from codeboarding_workflows.analysis import run_full
@@ -13,7 +14,6 @@ from codeboarding_workflows.orchestration import run_analysis_pipeline
13
14
  from codeboarding_workflows.sources import SourceContext, local_source, remote_source
14
15
  from infra.constants import CLI_ROOT_DOCUMENT_NAME
15
16
  from diagram_analysis import DEFAULT_DEPTH_CAP, RunContext, RunPaths
16
- from diagram_analysis.exceptions import ScopeSemanticsError
17
17
  from infra.monitoring import monitor_execution
18
18
  from infra.monitoring.paths import get_monitoring_run_dir
19
19
  from output_generators.rendering import render_docs
@@ -161,7 +161,7 @@ def _run_remote(args: argparse.Namespace) -> None:
161
161
  upload=args.upload,
162
162
  should_monitor=should_monitor,
163
163
  )
164
- except (LLMAuthError, ScopeSemanticsError):
164
+ except (LLMAuthError, LLMQuotaError, ScopeSemanticsError):
165
165
  # The next repository would fail the same way, so stop the run.
166
166
  raise
167
167
  except Exception:
@@ -5,12 +5,12 @@ import sys
5
5
  from typing import Any
6
6
 
7
7
  from agents.llm_config import LLMConfigError
8
- from agents.llm_errors import LLMAuthError
8
+ from agents.llm_errors import LLMAuthError, LLMQuotaError
9
+ from diagram_analysis.exceptions import ScopeSemanticsError
9
10
  from codeboarding_cli.bootstrap import bootstrap_environment, resolve_local_run_paths
10
11
  from codeboarding_cli.view_instructions import print_view_instructions
11
12
  from codeboarding_workflows.analysis import BaselineUnavailableError, run_incremental
12
13
  from diagram_analysis import RunContext
13
- from diagram_analysis.exceptions import ScopeSemanticsError
14
14
  from diagram_analysis.run_mode import RunMode
15
15
  from infra.utils import monitoring_enabled
16
16
 
@@ -61,7 +61,7 @@ def run_from_args(args: argparse.Namespace, parser: argparse.ArgumentParser) ->
61
61
  # to prompt for a full run; no stack trace needed.
62
62
  logger.info("Incremental unavailable: %s", exc)
63
63
  _emit_error(str(exc))
64
- except (LLMAuthError, ScopeSemanticsError):
64
+ except (LLMAuthError, LLMQuotaError, ScopeSemanticsError):
65
65
  # A full run would fail the same way, so don't ask for one.
66
66
  raise
67
67
  except Exception as exc:
@@ -1,18 +1,20 @@
1
1
  import argparse
2
+ import logging
2
3
  import os
3
4
  import sys
4
- import traceback
5
5
  from pathlib import Path
6
6
 
7
7
  from agents.llm_config import CODEBOARDING_KEY_TAIL
8
- from agents.llm_errors import EXIT_AUTH_ERROR, EXIT_QUOTA_EXHAUSTED, LLMAuthError
8
+ from agents.llm_errors import EXIT_AUTH_ERROR, EXIT_QUOTA_EXHAUSTED, LLMAuthError, LLMQuotaError
9
9
  from codeboarding_cli.bootstrap import resolve_local_run_paths
10
10
  from codeboarding_cli.commands import full_analysis, incremental_analysis, partial_analysis
11
- from diagram_analysis.exceptions import ScopeSemanticsError
12
11
  from output_generators import SUPPORTED_FORMATS, render as render_output
13
12
  from infra.utils import ANALYSIS_FILENAME
14
13
 
15
14
  _SUBCOMMANDS = {"full", "incremental", "partial"}
15
+ # CodeBoarding-action/scripts/analyze_repository.py parses this prefix; keep it stable.
16
+ _ERROR_PREFIX = "CodeBoarding: "
17
+ logger = logging.getLogger(__name__)
16
18
 
17
19
 
18
20
  def _build_shared_parser() -> argparse.ArgumentParser:
@@ -108,22 +110,15 @@ def _dispatch(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None
108
110
  output_dir=run_paths.output_dir,
109
111
  )
110
112
  except LLMAuthError as exc:
111
- # A rejected API key is the user's to fix, not a crash: print one
112
- # actionable line (no traceback) and exit with a distinct code.
113
- print(f"\nCodeBoarding: {exc}", file=sys.stderr)
113
+ logger.error("%s%s", _ERROR_PREFIX, exc)
114
114
  if exc.key_tail != CODEBOARDING_KEY_TAIL:
115
- print(
115
+ logger.error(
116
116
  "Check your LLM provider API key (shell env or ~/.codeboarding/config.toml) and re-run.",
117
- file=sys.stderr,
118
117
  )
119
118
  raise SystemExit(EXIT_AUTH_ERROR) from exc
120
- except ScopeSemanticsError as exc:
121
- if exc.telemetry_properties.get("error_type") != "quota":
122
- raise
123
- # Same exit as any failure, but with a code callers (the GitHub Action) can name.
124
- traceback.print_exception(exc)
125
- print("\nCodeBoarding: the LLM provider's token or credit quota is exhausted.", file=sys.stderr)
126
- print("Add credits or raise the quota for this provider, then re-run.", file=sys.stderr)
119
+ except LLMQuotaError as exc:
120
+ logger.exception("LLM quota failure")
121
+ logger.error("%s%s", _ERROR_PREFIX, exc)
127
122
  raise SystemExit(EXIT_QUOTA_EXHAUSTED) from exc
128
123
 
129
124
 
@@ -30,7 +30,7 @@ from diagram_analysis.incremental_update import (
30
30
  from agents.incremental_results import RecursiveScopeUpdateResult
31
31
  from agents.file_index_models import FileEntry, FileMethodGroup, MethodEntry
32
32
  from agents.llm_config import initialize_agent_llm
33
- from agents.llm_errors import LLMAuthError, llm_failure_properties
33
+ from agents.llm_errors import LLMAuthError, LLMQuotaError, is_quota_exhausted, llm_failure_properties
34
34
  from agents.relation_edges import (
35
35
  drop_misattributed_edges,
36
36
  index_relation_endpoints,
@@ -818,9 +818,11 @@ class DiagramGenerator:
818
818
  incremental,
819
819
  enclosing_names=enclosing_names,
820
820
  )
821
- except LLMAuthError:
821
+ except (LLMAuthError, LLMQuotaError, ScopeSemanticsError):
822
822
  raise
823
823
  except Exception as error:
824
+ if is_quota_exhausted(error):
825
+ raise LLMQuotaError(error) from error
824
826
  raise ScopeSemanticsError(scope.scope_id, telemetry_properties=llm_failure_properties(error)) from error
825
827
  if semantics is None:
826
828
  with self._naming_counts_lock:
@@ -1120,7 +1122,7 @@ class DiagramGenerator:
1120
1122
  new_components = [child for child in analysis.components if child.component_id in preclustered_scopes]
1121
1123
 
1122
1124
  return component.component_id, analysis, new_components
1123
- except (LLMAuthError, ScopeSemanticsError):
1125
+ except (LLMAuthError, LLMQuotaError, ScopeSemanticsError):
1124
1126
  # An LLM failure aborts the whole run rather than shipping this component with deterministic names.
1125
1127
  raise
1126
1128
  except Exception:
@@ -1335,7 +1337,7 @@ class DiagramGenerator:
1335
1337
  stats["completed"] += 1
1336
1338
  try:
1337
1339
  outcomes.append((component, level, future.result()))
1338
- except (LLMAuthError, ScopeSemanticsError):
1340
+ except (LLMAuthError, LLMQuotaError, ScopeSemanticsError):
1339
1341
  for pending in future_to_task:
1340
1342
  pending.cancel()
1341
1343
  raise
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboarding"
7
- version = "0.15.0"
7
+ version = "0.15.2"
8
8
  description = "Interactive Diagrams for Code"
9
9
  readme = "PYPI.md"
10
10
  license = "MIT"
@@ -106,6 +106,10 @@ packages = [
106
106
  ]
107
107
  include-package-data = true
108
108
 
109
+ [tool.setuptools.package-data]
110
+ # tsserver loads a plugin only from <location>/node_modules/<name>.
111
+ static_analyzer = ["typescript_plugin/node_modules/codeboarding-workspace-sources/*"]
112
+
109
113
  [project.scripts]
110
114
  codeboarding = "codeboarding_cli.main:main"
111
115
  codeboarding-setup = "infra.install:main"
@@ -363,6 +363,10 @@ class StaticAnalyzer:
363
363
  continue
364
364
  try:
365
365
  started.append((engine_config, self._spawn_engine_client(engine_config)))
366
+ except StaticAnalysisFatalError:
367
+ for _, client in started: # never handed to ``stop_clients``
368
+ client.shutdown()
369
+ raise
366
370
  except Exception as exc:
367
371
  logger.exception(
368
372
  f"Failed to start engine LSP client for {adapter.language}; "
@@ -500,6 +504,7 @@ class StaticAnalyzer:
500
504
  extra_client_capabilities=getattr(adapter, "extra_client_capabilities", {}) or {},
501
505
  error_verdict=adapter.error_verdict,
502
506
  workspace_folders=workspace_folders,
507
+ commands=adapter.get_lsp_commands(project_path),
503
508
  )
504
509
  try:
505
510
  engine_client.start()
@@ -918,6 +923,12 @@ class StaticAnalyzer:
918
923
  adapter, project_path = engine_config.adapter, engine_config.project_path
919
924
  language = adapter.results_language
920
925
  changed_files = self._changed_files_for_language(project_path, cached_sha, adapter.language)
926
+ if changed_files is not None and adapter.rebuilds_for(changed_files, project_path):
927
+ logger.info(f"warmstart {adapter.language}: a change alters how imports resolve; rebuilding")
928
+ changed_files = None
929
+ if resident_client is not None:
930
+ # Why: a server resolves imports as the project was when it started; a stopped one is replaced.
931
+ resident_client.shutdown()
921
932
  # A full re-analysis is the full pass's, so a split config runs as its parts. Changed files stay on the
922
933
  # config's own server: tsserver loads only the projects of the files it opens.
923
934
  if changed_files is None:
@@ -74,8 +74,9 @@ _LEGACY_CACHE_SUBDIR = "cache"
74
74
  # v11: every language builds its call edges from definitions at the call site.
75
75
  # v12: a large TypeScript monorepo runs one server per tsconfig project, so a graph one out-of-memory
76
76
  # server left without edges is rebuilt; bases, implementations and edge-only declarations link across engines.
77
+ # v13: a TypeScript call into a sibling workspace package resolves to its sources without an install or build.
77
78
  # Older pickles are treated as cache misses and re-run.
78
- _TAG_VERSION = "v12"
79
+ _TAG_VERSION = "v13"
79
80
 
80
81
 
81
82
  class StaticAnalysisCache:
@@ -2,11 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
6
+ import re
7
+ from collections.abc import Collection
5
8
  from pathlib import Path
6
9
 
7
10
  from infra.repo_utils.ignore import RepoIgnoreManager
8
11
  from static_analyzer.config import Language
9
12
  from static_analyzer.engine.language_adapter import LanguageAdapter
13
+ from static_analyzer.exceptions import WorkspaceSetupError
14
+ from static_analyzer.typescript_workspace import PLUGIN_LOCATION, PLUGIN_NAME, workspace_members
15
+
16
+ logger = logging.getLogger(__name__)
10
17
 
11
18
 
12
19
  class TypeScriptAdapter(LanguageAdapter):
@@ -34,7 +41,30 @@ class TypeScriptAdapter(LanguageAdapter):
34
41
  def get_lsp_init_options(self, ignore_manager: RepoIgnoreManager | None = None) -> dict:
35
42
  # Why: by default a syntax-only server answers while the project still loads, and a definition
36
43
  # that needs an inferred type -- ``.then(t => t.serialize())`` -- comes back empty on a cold start.
37
- return {"tsserver": {"useSyntaxServer": "never"}}
44
+ # Why the plugin: a call into a sibling workspace package of an uninstalled, unbuilt checkout resolves nowhere;
45
+ # the plugin resolves it to the member's sources (see ``typescript_workspace``).
46
+ return {
47
+ "tsserver": {"useSyntaxServer": "never"},
48
+ "plugins": [{"name": PLUGIN_NAME, "location": str(PLUGIN_LOCATION)}],
49
+ }
50
+
51
+ def get_lsp_commands(self, project_root: Path) -> list[dict]:
52
+ """Hands the workspace members to the plugin; tsserver gives the configuration to each project it loads."""
53
+ members = workspace_members(project_root)
54
+ if not members:
55
+ return []
56
+ if not (PLUGIN_LOCATION / "node_modules" / PLUGIN_NAME / "index.js").is_file():
57
+ raise WorkspaceSetupError(f"{PLUGIN_NAME} is missing from this install; reinstall CodeBoarding")
58
+ logger.info(f"tsserver resolves {len(members)} workspace members of {project_root} to their sources")
59
+ configuration = {"root": str(project_root), "members": {name: str(path) for name, path in members.items()}}
60
+ return [{"command": "_typescript.configurePlugin", "arguments": [PLUGIN_NAME, configuration]}]
61
+
62
+ def rebuilds_for(self, changed_files: Collection[Path], project_root: Path) -> bool:
63
+ """Whether a change alters how imports resolve: a tsconfig, a workspace declaration, or a workspace's manifest."""
64
+ names = {path.name for path in changed_files}
65
+ if "pnpm-workspace.yaml" in names or any(re.fullmatch(r"[tj]sconfig.*\.json", name) for name in names):
66
+ return True
67
+ return "package.json" in names and bool(workspace_members(project_root))
38
68
 
39
69
  def get_lsp_heap_options(self, heap_bytes: int) -> dict:
40
70
  # Why: at Node's default heap tsserver runs out of memory on a large tsconfig project and answers nothing.
@@ -271,6 +271,14 @@ class LanguageAdapter(ABC):
271
271
  """
272
272
  return {}
273
273
 
274
+ def get_lsp_commands(self, project_root: Path) -> list[dict]:
275
+ """``workspace/executeCommand`` params the server is sent once initialized, and again on a restart."""
276
+ return []
277
+
278
+ def rebuilds_for(self, changed_files: Collection[Path], project_root: Path) -> bool:
279
+ """Whether a warm start over *changed_files* must re-analyse the whole project on a fresh server."""
280
+ return False
281
+
274
282
  def workspace_folders(self, project_root: Path, source_files: Sequence[Path]) -> list[Path]:
275
283
  """Workspace folders to announce to the server in place of the project, or none."""
276
284
  return []
@@ -21,7 +21,7 @@ from pathlib import Path
21
21
  from static_analyzer.config import LANGUAGE_ID_BY_SUFFIX
22
22
  from static_analyzer.engine.lsp_constants import REQUEST_BATCH_SIZE
23
23
  from static_analyzer.engine.utils import uri_to_path
24
- from static_analyzer.exceptions import LanguageServerStalledError, StaticAnalysisFatalError
24
+ from static_analyzer.exceptions import LanguageServerStalledError, StaticAnalysisFatalError, WorkspaceSetupError
25
25
  from static_analyzer.lsp_client.diagnostics import FileDiagnosticsMap, LSPDiagnostic
26
26
 
27
27
  logger = logging.getLogger(__name__)
@@ -113,6 +113,7 @@ class LSPClient:
113
113
  extra_client_capabilities: dict | None = None,
114
114
  error_verdict: Callable[[dict], ErrorVerdict] | None = None,
115
115
  workspace_folders: list[Path] | None = None,
116
+ commands: list[dict] | None = None,
116
117
  ) -> None:
117
118
  self._command = command
118
119
  self._project_root = project_root
@@ -129,6 +130,7 @@ class LSPClient:
129
130
  self._error_verdict = error_verdict
130
131
  # Announced in place of the project folder, for a server handed a workspace described apart from it.
131
132
  self._workspace_folders = workspace_folders or []
133
+ self._commands = commands or []
132
134
  self._process: subprocess.Popen | None = None # type: ignore[type-arg]
133
135
  self._stdout_fd: int | None = None
134
136
  self._request_id = 0
@@ -257,6 +259,8 @@ class LSPClient:
257
259
  if isinstance(init_result, dict):
258
260
  self._server_capabilities = init_result.get("capabilities") or {}
259
261
  self._send_notification("initialized", {})
262
+ for command in self._commands:
263
+ self._send_request("workspace/executeCommand", command, timeout=self._default_timeout, required=True)
260
264
 
261
265
  # LSP servers receive configuration through three mechanisms:
262
266
  # 1. initializationOptions — sent in the initialize request above.
@@ -706,8 +710,10 @@ class LSPClient:
706
710
  answers.append(BatchAnswer(parsed, req_id not in unserved, req_id in retryable))
707
711
  return answers
708
712
 
709
- def _send_request(self, method: str, params: dict | list | None, timeout: int) -> dict | list | None:
710
- """Send a JSON-RPC request and wait for the response."""
713
+ def _send_request(
714
+ self, method: str, params: dict | list | None, timeout: int, required: bool = False
715
+ ) -> dict | list | None:
716
+ """Send a JSON-RPC request and wait for the response; a *required* one that does not succeed is fatal."""
711
717
  if self.server_failure:
712
718
  self.check_alive()
713
719
  self._request_id += 1
@@ -731,6 +737,8 @@ class LSPClient:
731
737
 
732
738
  if "error" in msg:
733
739
  error = msg["error"]
740
+ if required:
741
+ raise WorkspaceSetupError(f"The language server rejected {method} {params}: {error}")
734
742
  if isinstance(error, dict) and error.get("code") == LSP_METHOD_NOT_FOUND:
735
743
  raise MethodNotFoundError(error.get("message", "Method not found"))
736
744
  logger.warning("LSP error for request %d (%s): %s", req_id, method, error)
@@ -742,6 +750,8 @@ class LSPClient:
742
750
  return None
743
751
  return msg.get("result")
744
752
 
753
+ if required:
754
+ raise WorkspaceSetupError(f"The language server did not answer {method} {params} within {timeout}s")
745
755
  raise TimeoutError(f"Timeout waiting for LSP response to request {req_id}")
746
756
 
747
757
  def _send_notification(self, method: str, params: dict | list | None) -> None:
@@ -5,5 +5,9 @@ class StaticAnalysisFatalError(RuntimeError):
5
5
  """Raised when continuing would produce misleading cached analysis."""
6
6
 
7
7
 
8
+ class WorkspaceSetupError(StaticAnalysisFatalError):
9
+ """Raised when the workspace a language server must be set up with cannot be, so its answers would miss edges."""
10
+
11
+
8
12
  class LanguageServerStalledError(StaticAnalysisFatalError):
9
13
  """Raised when a language server stays up but the backend that answers for it has died."""