roam-code 7.5.0__tar.gz → 8.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. {roam_code-7.5.0/src/roam_code.egg-info → roam_code-8.0.0}/PKG-INFO +11 -11
  2. {roam_code-7.5.0 → roam_code-8.0.0}/README.md +10 -10
  3. {roam_code-7.5.0 → roam_code-8.0.0}/pyproject.toml +1 -1
  4. roam_code-8.0.0/src/roam/bridges/__init__.py +2 -0
  5. roam_code-8.0.0/src/roam/bridges/base.py +49 -0
  6. roam_code-8.0.0/src/roam/bridges/bridge_protobuf.py +337 -0
  7. roam_code-8.0.0/src/roam/bridges/bridge_salesforce.py +195 -0
  8. roam_code-8.0.0/src/roam/bridges/registry.py +39 -0
  9. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/cli.py +2 -1
  10. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/changed_files.py +19 -2
  11. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_coverage_gaps.py +209 -10
  12. roam_code-8.0.0/src/roam/commands/cmd_dead.py +1070 -0
  13. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_grep.py +8 -0
  14. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_pr_risk.py +203 -5
  15. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_testmap.py +3 -15
  16. roam_code-8.0.0/src/roam/commands/cmd_trend.py +427 -0
  17. roam_code-8.0.0/src/roam/commands/cmd_xlang.py +154 -0
  18. roam_code-8.0.0/src/roam/commands/gate_presets.py +205 -0
  19. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/connection.py +2 -0
  20. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/schema.py +1 -0
  21. roam_code-8.0.0/src/roam/graph/anomaly.py +461 -0
  22. roam_code-8.0.0/src/roam/index/file_roles.py +507 -0
  23. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/indexer.py +7 -2
  24. roam_code-8.0.0/src/roam/index/test_conventions.py +338 -0
  25. {roam_code-7.5.0 → roam_code-8.0.0/src/roam_code.egg-info}/PKG-INFO +11 -11
  26. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/SOURCES.txt +26 -0
  27. roam_code-8.0.0/tests/test_anomaly.py +541 -0
  28. roam_code-8.0.0/tests/test_bridges.py +312 -0
  29. roam_code-8.0.0/tests/test_commands_architecture.py +763 -0
  30. roam_code-8.0.0/tests/test_commands_exploration.py +782 -0
  31. roam_code-8.0.0/tests/test_commands_health.py +633 -0
  32. roam_code-8.0.0/tests/test_commands_refactoring.py +342 -0
  33. roam_code-8.0.0/tests/test_commands_workflow.py +617 -0
  34. roam_code-8.0.0/tests/test_dead_aging.py +376 -0
  35. roam_code-8.0.0/tests/test_file_roles.py +443 -0
  36. roam_code-8.0.0/tests/test_formatters.py +296 -0
  37. roam_code-8.0.0/tests/test_gate_presets.py +141 -0
  38. roam_code-8.0.0/tests/test_json_contracts.py +431 -0
  39. roam_code-8.0.0/tests/test_languages.py +1862 -0
  40. roam_code-8.0.0/tests/test_pr_risk_author.py +427 -0
  41. roam_code-8.0.0/tests/test_smoke.py +287 -0
  42. roam_code-8.0.0/tests/test_test_conventions.py +264 -0
  43. roam_code-7.5.0/src/roam/commands/cmd_dead.py +0 -595
  44. roam_code-7.5.0/src/roam/commands/cmd_trend.py +0 -208
  45. {roam_code-7.5.0 → roam_code-8.0.0}/LICENSE +0 -0
  46. {roam_code-7.5.0 → roam_code-8.0.0}/setup.cfg +0 -0
  47. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/__init__.py +0 -0
  48. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/__main__.py +0 -0
  49. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/__init__.py +0 -0
  50. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_affected_tests.py +0 -0
  51. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_alerts.py +0 -0
  52. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_breaking.py +0 -0
  53. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_bus_factor.py +0 -0
  54. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_clusters.py +0 -0
  55. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_complexity.py +0 -0
  56. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_context.py +0 -0
  57. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_conventions.py +0 -0
  58. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_coupling.py +0 -0
  59. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_debt.py +0 -0
  60. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_deps.py +0 -0
  61. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_describe.py +0 -0
  62. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_diagnose.py +0 -0
  63. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_diff.py +0 -0
  64. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_digest.py +0 -0
  65. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_doc_staleness.py +0 -0
  66. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_entry_points.py +0 -0
  67. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_fan.py +0 -0
  68. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_file.py +0 -0
  69. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_fitness.py +0 -0
  70. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_fn_coupling.py +0 -0
  71. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_health.py +0 -0
  72. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_impact.py +0 -0
  73. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_index.py +0 -0
  74. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_init.py +0 -0
  75. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_layers.py +0 -0
  76. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_map.py +0 -0
  77. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_module.py +0 -0
  78. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_owner.py +0 -0
  79. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_patterns.py +0 -0
  80. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_preflight.py +0 -0
  81. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_report.py +0 -0
  82. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_risk.py +0 -0
  83. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_safe_delete.py +0 -0
  84. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_safe_zones.py +0 -0
  85. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_search.py +0 -0
  86. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_sketch.py +0 -0
  87. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_snapshot.py +0 -0
  88. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_split.py +0 -0
  89. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_symbol.py +0 -0
  90. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_tour.py +0 -0
  91. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_trace.py +0 -0
  92. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_understand.py +0 -0
  93. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_uses.py +0 -0
  94. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_visualize.py +0 -0
  95. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_weather.py +0 -0
  96. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_why.py +0 -0
  97. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_ws.py +0 -0
  98. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/metrics_history.py +0 -0
  99. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/resolve.py +0 -0
  100. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/__init__.py +0 -0
  101. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/queries.py +0 -0
  102. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/__init__.py +0 -0
  103. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/builder.py +0 -0
  104. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/clusters.py +0 -0
  105. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/cycles.py +0 -0
  106. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/layers.py +0 -0
  107. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/pagerank.py +0 -0
  108. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/pathfinding.py +0 -0
  109. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/__init__.py +0 -0
  110. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/complexity.py +0 -0
  111. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/discovery.py +0 -0
  112. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/git_stats.py +0 -0
  113. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/incremental.py +0 -0
  114. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/parser.py +0 -0
  115. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/relations.py +0 -0
  116. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/symbols.py +0 -0
  117. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/__init__.py +0 -0
  118. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/apex_lang.py +0 -0
  119. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/aura_lang.py +0 -0
  120. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/base.py +0 -0
  121. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/c_lang.py +0 -0
  122. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/foxpro_lang.py +0 -0
  123. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/generic_lang.py +0 -0
  124. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/go_lang.py +0 -0
  125. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/java_lang.py +0 -0
  126. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/javascript_lang.py +0 -0
  127. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/php_lang.py +0 -0
  128. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/python_lang.py +0 -0
  129. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/registry.py +0 -0
  130. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/rust_lang.py +0 -0
  131. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/sfxml_lang.py +0 -0
  132. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/typescript_lang.py +0 -0
  133. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/visualforce_lang.py +0 -0
  134. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/mcp_server.py +0 -0
  135. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/output/__init__.py +0 -0
  136. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/output/formatter.py +0 -0
  137. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/output/sarif.py +0 -0
  138. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/__init__.py +0 -0
  139. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/aggregator.py +0 -0
  140. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/api_scanner.py +0 -0
  141. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/config.py +0 -0
  142. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/db.py +0 -0
  143. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/dependency_links.txt +0 -0
  144. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/entry_points.txt +0 -0
  145. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/requires.txt +0 -0
  146. {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/top_level.txt +0 -0
  147. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_basic.py +0 -0
  148. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_comprehensive.py +0 -0
  149. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_fixes.py +0 -0
  150. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_foxpro.py +0 -0
  151. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_new_features.py +0 -0
  152. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_performance.py +0 -0
  153. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_resolve.py +0 -0
  154. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_salesforce.py +0 -0
  155. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_v71_features.py +0 -0
  156. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_v7_features.py +0 -0
  157. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_visualize.py +0 -0
  158. {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roam-code
3
- Version: 7.5.0
3
+ Version: 8.0.0
4
4
  Summary: Instant codebase comprehension for AI coding agents
5
5
  Author: CosmoHac
6
6
  License-Expression: MIT
@@ -60,7 +60,7 @@ A semantic graph means Roam understands what functions call what, how modules de
60
60
  Codebase ──> [Index] ──> Semantic Graph ──> CLI ──> AI Agent
61
61
  │ │ │
62
62
  tree-sitter symbols one call
63
- 17 languages + edges replaces
63
+ 22 languages + edges replaces
64
64
  git history + metrics 5-10 tool calls
65
65
  ```
66
66
 
@@ -208,7 +208,7 @@ roam health
208
208
 
209
209
  ## Commands
210
210
 
211
- The [5 core commands](#core-commands) shown above cover ~80% of agent workflows. 58 total commands are organized into 8 categories.
211
+ The [5 core commands](#core-commands) shown above cover ~80% of agent workflows. 55 total commands are organized into 7 categories.
212
212
 
213
213
  <details>
214
214
  <summary><strong>Full command reference</strong></summary>
@@ -881,7 +881,7 @@ Codebase
881
881
  |
882
882
  [1] Discovery ──── git ls-files (respects .gitignore)
883
883
  |
884
- [2] Parse ──────── tree-sitter AST per file (17 languages)
884
+ [2] Parse ──────── tree-sitter AST per file (22 languages)
885
885
  |
886
886
  [3] Extract ────── symbols + references (calls, imports, inheritance)
887
887
  |
@@ -1020,7 +1020,7 @@ Delete `.roam/` from your project root to clean up local data.
1020
1020
  git clone https://github.com/Cranot/roam-code.git
1021
1021
  cd roam-code
1022
1022
  pip install -e .
1023
- pytest tests/ # 669 tests, Python 3.9-3.13
1023
+ pytest tests/ # 1656 tests, Python 3.9-3.13
1024
1024
  ```
1025
1025
 
1026
1026
  <details>
@@ -1033,7 +1033,7 @@ roam-code/
1033
1033
  ├── CHANGELOG.md
1034
1034
  ├── src/roam/
1035
1035
  │ ├── __init__.py # Version (from pyproject.toml)
1036
- │ ├── cli.py # Click CLI (58 commands, 8 categories)
1036
+ │ ├── cli.py # Click CLI (55 commands, 7 categories)
1037
1037
  │ ├── mcp_server.py # MCP server (19 tools, 2 resources)
1038
1038
  │ ├── db/
1039
1039
  │ │ ├── connection.py # SQLite (WAL, pragmas, batched IN)
@@ -1045,8 +1045,8 @@ roam-code/
1045
1045
  │ │ ├── parser.py # Tree-sitter parsing
1046
1046
  │ │ ├── symbols.py # Symbol + reference extraction
1047
1047
  │ │ ├── relations.py # Reference resolution -> edges
1048
- │ │ ├── complexity.py # Cognitive complexity (SonarSource)
1049
- │ │ ├── git_stats.py # Churn, co-change, blame, entropy
1048
+ │ │ ├── complexity.py # Cognitive complexity (SonarSource) + Halstead metrics
1049
+ │ │ ├── git_stats.py # Churn, co-change, blame, Renyi entropy
1050
1050
  │ │ └── incremental.py # mtime + hash change detection
1051
1051
  │ ├── languages/
1052
1052
  │ │ ├── base.py # Abstract LanguageExtractor
@@ -1060,7 +1060,7 @@ roam-code/
1060
1060
  │ │ └── aggregator.py # Cross-repo aggregation
1061
1061
  │ ├── graph/
1062
1062
  │ │ ├── builder.py, pagerank.py # DB -> NetworkX, PageRank
1063
- │ │ ├── cycles.py, clusters.py # Tarjan SCC, Louvain
1063
+ │ │ ├── cycles.py, clusters.py # Tarjan SCC, propagation cost, Louvain, modularity Q
1064
1064
  │ │ ├── layers.py, pathfinding.py # Topo layers, k-shortest paths
1065
1065
  │ │ ├── split.py, why.py # Decomposition, role classification
1066
1066
  │ ├── commands/
@@ -1069,7 +1069,7 @@ roam-code/
1069
1069
  │ └── output/
1070
1070
  │ ├── formatter.py # Token-efficient formatting
1071
1071
  │ └── sarif.py # SARIF 2.1.0 output
1072
- └── tests/ # 669 tests across 11 test files
1072
+ └── tests/ # 1656 tests across 28 test files
1073
1073
  ```
1074
1074
 
1075
1075
  </details>
@@ -1110,7 +1110,7 @@ Optional: [fastmcp](https://github.com/jlowin/fastmcp) (MCP server)
1110
1110
  git clone https://github.com/Cranot/roam-code.git
1111
1111
  cd roam-code
1112
1112
  pip install -e .
1113
- pytest tests/ # All 669 tests must pass
1113
+ pytest tests/ # All 1656 tests must pass
1114
1114
  ```
1115
1115
 
1116
1116
  Good first contributions: add a [Tier 1 language](src/roam/languages/) (see `go_lang.py` or `php_lang.py` as templates), improve reference resolution, add benchmark repos, extend SARIF converters, add MCP tools.
@@ -28,7 +28,7 @@ A semantic graph means Roam understands what functions call what, how modules de
28
28
  Codebase ──> [Index] ──> Semantic Graph ──> CLI ──> AI Agent
29
29
  │ │ │
30
30
  tree-sitter symbols one call
31
- 17 languages + edges replaces
31
+ 22 languages + edges replaces
32
32
  git history + metrics 5-10 tool calls
33
33
  ```
34
34
 
@@ -176,7 +176,7 @@ roam health
176
176
 
177
177
  ## Commands
178
178
 
179
- The [5 core commands](#core-commands) shown above cover ~80% of agent workflows. 58 total commands are organized into 8 categories.
179
+ The [5 core commands](#core-commands) shown above cover ~80% of agent workflows. 55 total commands are organized into 7 categories.
180
180
 
181
181
  <details>
182
182
  <summary><strong>Full command reference</strong></summary>
@@ -849,7 +849,7 @@ Codebase
849
849
  |
850
850
  [1] Discovery ──── git ls-files (respects .gitignore)
851
851
  |
852
- [2] Parse ──────── tree-sitter AST per file (17 languages)
852
+ [2] Parse ──────── tree-sitter AST per file (22 languages)
853
853
  |
854
854
  [3] Extract ────── symbols + references (calls, imports, inheritance)
855
855
  |
@@ -988,7 +988,7 @@ Delete `.roam/` from your project root to clean up local data.
988
988
  git clone https://github.com/Cranot/roam-code.git
989
989
  cd roam-code
990
990
  pip install -e .
991
- pytest tests/ # 669 tests, Python 3.9-3.13
991
+ pytest tests/ # 1656 tests, Python 3.9-3.13
992
992
  ```
993
993
 
994
994
  <details>
@@ -1001,7 +1001,7 @@ roam-code/
1001
1001
  ├── CHANGELOG.md
1002
1002
  ├── src/roam/
1003
1003
  │ ├── __init__.py # Version (from pyproject.toml)
1004
- │ ├── cli.py # Click CLI (58 commands, 8 categories)
1004
+ │ ├── cli.py # Click CLI (55 commands, 7 categories)
1005
1005
  │ ├── mcp_server.py # MCP server (19 tools, 2 resources)
1006
1006
  │ ├── db/
1007
1007
  │ │ ├── connection.py # SQLite (WAL, pragmas, batched IN)
@@ -1013,8 +1013,8 @@ roam-code/
1013
1013
  │ │ ├── parser.py # Tree-sitter parsing
1014
1014
  │ │ ├── symbols.py # Symbol + reference extraction
1015
1015
  │ │ ├── relations.py # Reference resolution -> edges
1016
- │ │ ├── complexity.py # Cognitive complexity (SonarSource)
1017
- │ │ ├── git_stats.py # Churn, co-change, blame, entropy
1016
+ │ │ ├── complexity.py # Cognitive complexity (SonarSource) + Halstead metrics
1017
+ │ │ ├── git_stats.py # Churn, co-change, blame, Renyi entropy
1018
1018
  │ │ └── incremental.py # mtime + hash change detection
1019
1019
  │ ├── languages/
1020
1020
  │ │ ├── base.py # Abstract LanguageExtractor
@@ -1028,7 +1028,7 @@ roam-code/
1028
1028
  │ │ └── aggregator.py # Cross-repo aggregation
1029
1029
  │ ├── graph/
1030
1030
  │ │ ├── builder.py, pagerank.py # DB -> NetworkX, PageRank
1031
- │ │ ├── cycles.py, clusters.py # Tarjan SCC, Louvain
1031
+ │ │ ├── cycles.py, clusters.py # Tarjan SCC, propagation cost, Louvain, modularity Q
1032
1032
  │ │ ├── layers.py, pathfinding.py # Topo layers, k-shortest paths
1033
1033
  │ │ ├── split.py, why.py # Decomposition, role classification
1034
1034
  │ ├── commands/
@@ -1037,7 +1037,7 @@ roam-code/
1037
1037
  │ └── output/
1038
1038
  │ ├── formatter.py # Token-efficient formatting
1039
1039
  │ └── sarif.py # SARIF 2.1.0 output
1040
- └── tests/ # 669 tests across 11 test files
1040
+ └── tests/ # 1656 tests across 28 test files
1041
1041
  ```
1042
1042
 
1043
1043
  </details>
@@ -1078,7 +1078,7 @@ Optional: [fastmcp](https://github.com/jlowin/fastmcp) (MCP server)
1078
1078
  git clone https://github.com/Cranot/roam-code.git
1079
1079
  cd roam-code
1080
1080
  pip install -e .
1081
- pytest tests/ # All 669 tests must pass
1081
+ pytest tests/ # All 1656 tests must pass
1082
1082
  ```
1083
1083
 
1084
1084
  Good first contributions: add a [Tier 1 language](src/roam/languages/) (see `go_lang.py` or `php_lang.py` as templates), improve reference resolution, add benchmark repos, extend SARIF converters, add MCP tools.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "roam-code"
7
- version = "7.5.0"
7
+ version = "8.0.0"
8
8
  description = "Instant codebase comprehension for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,2 @@
1
+ """Cross-language bridge framework for symbol resolution."""
2
+ from __future__ import annotations
@@ -0,0 +1,49 @@
1
+ """Abstract base class for cross-language bridges."""
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class LanguageBridge(ABC):
8
+ """Base class for cross-language symbol resolution bridges.
9
+
10
+ A bridge resolves symbols that cross language boundaries:
11
+ - Protobuf .proto -> generated Go/Java/Python stubs
12
+ - Salesforce Apex -> Aura/LWC/Visualforce
13
+ - GraphQL schema -> TypeScript/Python codegen
14
+ - OpenAPI spec -> client SDKs
15
+ """
16
+
17
+ @property
18
+ @abstractmethod
19
+ def name(self) -> str:
20
+ """Short identifier for this bridge (e.g. 'protobuf', 'salesforce')."""
21
+
22
+ @property
23
+ @abstractmethod
24
+ def source_extensions(self) -> frozenset[str]:
25
+ """File extensions this bridge reads from (e.g. frozenset({'.proto'}))."""
26
+
27
+ @property
28
+ @abstractmethod
29
+ def target_extensions(self) -> frozenset[str]:
30
+ """File extensions this bridge generates/links to."""
31
+
32
+ @abstractmethod
33
+ def detect(self, file_paths: list[str]) -> bool:
34
+ """Return True if this bridge is relevant for the given file set."""
35
+
36
+ @abstractmethod
37
+ def resolve(self, source_path: str, source_symbols: list[dict],
38
+ target_files: dict[str, list[dict]]) -> list[dict]:
39
+ """Resolve cross-language symbol links.
40
+
41
+ Args:
42
+ source_path: Path of the source file (e.g. foo.proto)
43
+ source_symbols: Symbols extracted from source_path
44
+ target_files: {path: [symbols]} for candidate target files
45
+
46
+ Returns:
47
+ List of edge dicts: [{"source": qualified_name, "target": qualified_name,
48
+ "kind": "x-lang", "bridge": self.name}]
49
+ """
@@ -0,0 +1,337 @@
1
+ """Protobuf cross-language bridge: .proto -> generated stubs.
2
+
3
+ Resolves cross-references between Protocol Buffer definitions and their
4
+ generated code in various languages:
5
+ - Python: *_pb2.py modules with classes matching message/service names
6
+ - Go: *.pb.go files with CamelCase struct names
7
+ - Java: OuterClass.MessageName pattern in *OuterClass.java
8
+ - C++: *.pb.h / *.pb.cc with namespace::MessageName
9
+ - TypeScript/JavaScript: *_pb.ts / *_pb.js
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+
16
+ from roam.bridges.base import LanguageBridge
17
+ from roam.bridges.registry import register_bridge
18
+
19
+
20
+ # Proto source extension
21
+ _PROTO_EXT = frozenset({".proto"})
22
+
23
+ # Generated stub file patterns by language
24
+ _GENERATED_PATTERNS: dict[str, re.Pattern] = {
25
+ "python": re.compile(r'_pb2\.pyi?$'),
26
+ "go": re.compile(r'\.pb\.go$'),
27
+ "java": re.compile(r'(?:OuterClass|Grpc|Proto)\.java$'),
28
+ "cpp_header": re.compile(r'\.pb\.h$'),
29
+ "cpp_source": re.compile(r'\.pb\.cc$'),
30
+ "typescript": re.compile(r'_pb\.(ts|d\.ts)$'),
31
+ "javascript": re.compile(r'_pb\.js$'),
32
+ "csharp": re.compile(r'\.g\.cs$'),
33
+ "ruby": re.compile(r'_pb\.rb$'),
34
+ }
35
+
36
+ # All target extensions that could be generated from .proto
37
+ _TARGET_EXTS = frozenset({
38
+ ".py", ".pyi", ".go", ".java", ".h", ".cc", ".cpp",
39
+ ".ts", ".js", ".cs", ".rb",
40
+ })
41
+
42
+ # Pattern to extract package from proto file symbols
43
+ _PROTO_PACKAGE_RE = re.compile(r'package\s+([\w.]+)')
44
+
45
+
46
+ class ProtobufBridge(LanguageBridge):
47
+ """Bridge between .proto definitions and generated language stubs."""
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ return "protobuf"
52
+
53
+ @property
54
+ def source_extensions(self) -> frozenset[str]:
55
+ return _PROTO_EXT
56
+
57
+ @property
58
+ def target_extensions(self) -> frozenset[str]:
59
+ return _TARGET_EXTS
60
+
61
+ def detect(self, file_paths: list[str]) -> bool:
62
+ """Detect if project has .proto files and potential generated stubs."""
63
+ has_proto = False
64
+ has_generated = False
65
+ for fp in file_paths:
66
+ ext = os.path.splitext(fp)[1].lower()
67
+ if ext == ".proto":
68
+ has_proto = True
69
+ # Check for generated stub patterns
70
+ basename = os.path.basename(fp)
71
+ for pattern in _GENERATED_PATTERNS.values():
72
+ if pattern.search(basename):
73
+ has_generated = True
74
+ break
75
+ if has_proto and has_generated:
76
+ return True
77
+ return False
78
+
79
+ def resolve(self, source_path: str, source_symbols: list[dict],
80
+ target_files: dict[str, list[dict]]) -> list[dict]:
81
+ """Resolve .proto symbols to their generated stubs.
82
+
83
+ Resolution strategies:
84
+ 1. File naming: foo.proto -> foo_pb2.py, foo.pb.go, etc.
85
+ 2. Symbol naming: message MyMessage -> class MyMessage (Python),
86
+ struct MyMessage (Go), MyMessage (Java inner class)
87
+ 3. Service naming: service MyService -> MyServiceClient, MyServiceServer
88
+ """
89
+ edges: list[dict] = []
90
+ source_ext = os.path.splitext(source_path)[1].lower()
91
+
92
+ if source_ext != ".proto":
93
+ return edges
94
+
95
+ # Get the proto file stem (e.g., "foo" from "foo.proto")
96
+ proto_stem = os.path.basename(source_path).rsplit(".", 1)[0]
97
+
98
+ # Classify source symbols into messages, services, enums
99
+ messages = []
100
+ services = []
101
+ enums = []
102
+ for sym in source_symbols:
103
+ kind = sym.get("kind", "")
104
+ if kind in ("message", "class", "struct"):
105
+ messages.append(sym)
106
+ elif kind in ("service", "interface"):
107
+ services.append(sym)
108
+ elif kind == "enum":
109
+ enums.append(sym)
110
+
111
+ # Find generated target files that correspond to this proto
112
+ generated_targets = self._find_generated_files(proto_stem, target_files)
113
+
114
+ # For each generated file, try to match symbols
115
+ for tpath, tsymbols, lang in generated_targets:
116
+ target_symbol_names = {
117
+ sym.get("name", ""): sym.get("qualified_name", "")
118
+ for sym in tsymbols
119
+ }
120
+
121
+ # Match message symbols
122
+ for msg in messages:
123
+ msg_name = msg.get("name", "")
124
+ msg_qname = msg.get("qualified_name", msg_name)
125
+ matched = self._match_message(msg_name, target_symbol_names, lang)
126
+ for target_qname in matched:
127
+ edges.append({
128
+ "source": msg_qname,
129
+ "target": target_qname,
130
+ "kind": "x-lang",
131
+ "bridge": self.name,
132
+ "mechanism": "proto-message",
133
+ "target_lang": lang,
134
+ })
135
+
136
+ # Match service symbols
137
+ for svc in services:
138
+ svc_name = svc.get("name", "")
139
+ svc_qname = svc.get("qualified_name", svc_name)
140
+ matched = self._match_service(svc_name, target_symbol_names, lang)
141
+ for target_qname in matched:
142
+ edges.append({
143
+ "source": svc_qname,
144
+ "target": target_qname,
145
+ "kind": "x-lang",
146
+ "bridge": self.name,
147
+ "mechanism": "proto-service",
148
+ "target_lang": lang,
149
+ })
150
+
151
+ # Match enum symbols
152
+ for enum in enums:
153
+ enum_name = enum.get("name", "")
154
+ enum_qname = enum.get("qualified_name", enum_name)
155
+ matched = self._match_enum(enum_name, target_symbol_names, lang)
156
+ for target_qname in matched:
157
+ edges.append({
158
+ "source": enum_qname,
159
+ "target": target_qname,
160
+ "kind": "x-lang",
161
+ "bridge": self.name,
162
+ "mechanism": "proto-enum",
163
+ "target_lang": lang,
164
+ })
165
+
166
+ return edges
167
+
168
+ def _find_generated_files(self, proto_stem: str,
169
+ target_files: dict[str, list[dict]]
170
+ ) -> list[tuple[str, list[dict], str]]:
171
+ """Find target files that were likely generated from this proto.
172
+
173
+ Returns list of (path, symbols, language) tuples.
174
+ """
175
+ results = []
176
+ proto_lower = proto_stem.lower()
177
+
178
+ for tpath, tsymbols in target_files.items():
179
+ basename = os.path.basename(tpath).lower()
180
+
181
+ # Check each language pattern
182
+ for lang, pattern in _GENERATED_PATTERNS.items():
183
+ if pattern.search(basename):
184
+ # Verify the stem matches the proto file
185
+ # e.g., "foo_pb2.py" stem is "foo", "foo.pb.go" stem is "foo"
186
+ generated_stem = self._extract_stem(basename, lang)
187
+ if generated_stem and generated_stem == proto_lower:
188
+ results.append((tpath, tsymbols, lang))
189
+ break # Only match one language per file
190
+
191
+ return results
192
+
193
+ def _extract_stem(self, basename: str, lang: str) -> str | None:
194
+ """Extract the original proto stem from a generated filename.
195
+
196
+ E.g., "foo_pb2.py" -> "foo", "foo.pb.go" -> "foo"
197
+ """
198
+ lower = basename.lower()
199
+ if lang == "python":
200
+ # foo_pb2.py or foo_pb2.pyi
201
+ m = re.match(r'^(.+)_pb2\.pyi?$', lower)
202
+ return m.group(1) if m else None
203
+ elif lang == "go":
204
+ # foo.pb.go
205
+ m = re.match(r'^(.+)\.pb\.go$', lower)
206
+ return m.group(1) if m else None
207
+ elif lang == "java":
208
+ # FooOuterClass.java or FooGrpc.java or FooProto.java
209
+ m = re.match(r'^(.+?)(?:outerclass|grpc|proto)\.java$', lower)
210
+ return m.group(1) if m else None
211
+ elif lang in ("cpp_header", "cpp_source"):
212
+ # foo.pb.h or foo.pb.cc
213
+ m = re.match(r'^(.+)\.pb\.(?:h|cc)$', lower)
214
+ return m.group(1) if m else None
215
+ elif lang == "typescript":
216
+ # foo_pb.ts or foo_pb.d.ts
217
+ m = re.match(r'^(.+)_pb\.(?:d\.)?ts$', lower)
218
+ return m.group(1) if m else None
219
+ elif lang == "javascript":
220
+ # foo_pb.js
221
+ m = re.match(r'^(.+)_pb\.js$', lower)
222
+ return m.group(1) if m else None
223
+ elif lang == "csharp":
224
+ # Foo.g.cs
225
+ m = re.match(r'^(.+)\.g\.cs$', lower)
226
+ return m.group(1) if m else None
227
+ elif lang == "ruby":
228
+ # foo_pb.rb
229
+ m = re.match(r'^(.+)_pb\.rb$', lower)
230
+ return m.group(1) if m else None
231
+ return None
232
+
233
+ def _match_message(self, msg_name: str, target_names: dict[str, str],
234
+ lang: str) -> list[str]:
235
+ """Match a proto message name to generated symbols.
236
+
237
+ Naming conventions vary by language:
238
+ - Python: class MyMessage in *_pb2.py (exact name)
239
+ - Go: struct MyMessage in *.pb.go (CamelCase preserved)
240
+ - Java: inner class MyMessage inside OuterClass
241
+ - C++: class MyMessage in namespace
242
+ """
243
+ matched = []
244
+ msg_lower = msg_name.lower()
245
+
246
+ for sym_name, sym_qname in target_names.items():
247
+ sym_lower = sym_name.lower()
248
+
249
+ # Exact match (most common for Python, Go, C++)
250
+ if sym_lower == msg_lower:
251
+ matched.append(sym_qname)
252
+ continue
253
+
254
+ # Go: proto snake_case -> CamelCase
255
+ # e.g., my_message -> MyMessage
256
+ if lang == "go" and self._snake_to_camel(msg_name).lower() == sym_lower:
257
+ matched.append(sym_qname)
258
+ continue
259
+
260
+ # Java: OuterClass.MessageName pattern
261
+ if lang == "java" and sym_lower.endswith("." + msg_lower):
262
+ matched.append(sym_qname)
263
+ continue
264
+
265
+ return matched
266
+
267
+ def _match_service(self, svc_name: str, target_names: dict[str, str],
268
+ lang: str) -> list[str]:
269
+ """Match a proto service name to generated symbols.
270
+
271
+ Generated service stubs commonly use suffixes:
272
+ - Python: MyServiceStub, MyServiceServicer
273
+ - Go: MyServiceClient, MyServiceServer
274
+ - Java: MyServiceGrpc, MyServiceBlockingStub
275
+ """
276
+ matched = []
277
+ svc_lower = svc_name.lower()
278
+
279
+ # Common generated suffixes for service stubs
280
+ suffixes = [
281
+ "", # exact match
282
+ "client", "server", "stub", "servicer",
283
+ "grpc", "blockingstub", "futurestub",
284
+ "implbase",
285
+ ]
286
+
287
+ for sym_name, sym_qname in target_names.items():
288
+ sym_lower = sym_name.lower()
289
+ for suffix in suffixes:
290
+ if sym_lower == svc_lower + suffix:
291
+ matched.append(sym_qname)
292
+ break
293
+ # Also check with underscore separator (Python style)
294
+ if suffix and sym_lower == svc_lower + "_" + suffix:
295
+ matched.append(sym_qname)
296
+ break
297
+
298
+ return matched
299
+
300
+ def _match_enum(self, enum_name: str, target_names: dict[str, str],
301
+ lang: str) -> list[str]:
302
+ """Match a proto enum name to generated symbols.
303
+
304
+ Enums generally keep their name across languages.
305
+ """
306
+ matched = []
307
+ enum_lower = enum_name.lower()
308
+
309
+ for sym_name, sym_qname in target_names.items():
310
+ sym_lower = sym_name.lower()
311
+
312
+ # Exact match
313
+ if sym_lower == enum_lower:
314
+ matched.append(sym_qname)
315
+ continue
316
+
317
+ # Go CamelCase conversion
318
+ if lang == "go" and self._snake_to_camel(enum_name).lower() == sym_lower:
319
+ matched.append(sym_qname)
320
+ continue
321
+
322
+ return matched
323
+
324
+ def _snake_to_camel(self, name: str) -> str:
325
+ """Convert snake_case to CamelCase.
326
+
327
+ E.g., my_message -> MyMessage, already_camel -> AlreadyCamel
328
+ """
329
+ if "_" not in name:
330
+ # Already CamelCase or single word; just capitalize first letter
331
+ return name[0].upper() + name[1:] if name else name
332
+ parts = name.split("_")
333
+ return "".join(p.capitalize() for p in parts if p)
334
+
335
+
336
+ # Auto-register on import
337
+ register_bridge(ProtobufBridge())