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.
- {roam_code-7.5.0/src/roam_code.egg-info → roam_code-8.0.0}/PKG-INFO +11 -11
- {roam_code-7.5.0 → roam_code-8.0.0}/README.md +10 -10
- {roam_code-7.5.0 → roam_code-8.0.0}/pyproject.toml +1 -1
- roam_code-8.0.0/src/roam/bridges/__init__.py +2 -0
- roam_code-8.0.0/src/roam/bridges/base.py +49 -0
- roam_code-8.0.0/src/roam/bridges/bridge_protobuf.py +337 -0
- roam_code-8.0.0/src/roam/bridges/bridge_salesforce.py +195 -0
- roam_code-8.0.0/src/roam/bridges/registry.py +39 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/cli.py +2 -1
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/changed_files.py +19 -2
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_coverage_gaps.py +209 -10
- roam_code-8.0.0/src/roam/commands/cmd_dead.py +1070 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_grep.py +8 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_pr_risk.py +203 -5
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_testmap.py +3 -15
- roam_code-8.0.0/src/roam/commands/cmd_trend.py +427 -0
- roam_code-8.0.0/src/roam/commands/cmd_xlang.py +154 -0
- roam_code-8.0.0/src/roam/commands/gate_presets.py +205 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/connection.py +2 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/schema.py +1 -0
- roam_code-8.0.0/src/roam/graph/anomaly.py +461 -0
- roam_code-8.0.0/src/roam/index/file_roles.py +507 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/indexer.py +7 -2
- roam_code-8.0.0/src/roam/index/test_conventions.py +338 -0
- {roam_code-7.5.0 → roam_code-8.0.0/src/roam_code.egg-info}/PKG-INFO +11 -11
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/SOURCES.txt +26 -0
- roam_code-8.0.0/tests/test_anomaly.py +541 -0
- roam_code-8.0.0/tests/test_bridges.py +312 -0
- roam_code-8.0.0/tests/test_commands_architecture.py +763 -0
- roam_code-8.0.0/tests/test_commands_exploration.py +782 -0
- roam_code-8.0.0/tests/test_commands_health.py +633 -0
- roam_code-8.0.0/tests/test_commands_refactoring.py +342 -0
- roam_code-8.0.0/tests/test_commands_workflow.py +617 -0
- roam_code-8.0.0/tests/test_dead_aging.py +376 -0
- roam_code-8.0.0/tests/test_file_roles.py +443 -0
- roam_code-8.0.0/tests/test_formatters.py +296 -0
- roam_code-8.0.0/tests/test_gate_presets.py +141 -0
- roam_code-8.0.0/tests/test_json_contracts.py +431 -0
- roam_code-8.0.0/tests/test_languages.py +1862 -0
- roam_code-8.0.0/tests/test_pr_risk_author.py +427 -0
- roam_code-8.0.0/tests/test_smoke.py +287 -0
- roam_code-8.0.0/tests/test_test_conventions.py +264 -0
- roam_code-7.5.0/src/roam/commands/cmd_dead.py +0 -595
- roam_code-7.5.0/src/roam/commands/cmd_trend.py +0 -208
- {roam_code-7.5.0 → roam_code-8.0.0}/LICENSE +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/setup.cfg +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/__main__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_affected_tests.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_alerts.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_breaking.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_bus_factor.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_clusters.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_complexity.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_context.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_conventions.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_coupling.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_debt.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_deps.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_describe.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_diagnose.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_diff.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_digest.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_doc_staleness.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_entry_points.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_fan.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_file.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_fitness.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_fn_coupling.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_health.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_impact.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_index.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_init.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_layers.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_map.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_module.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_owner.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_patterns.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_preflight.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_report.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_risk.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_safe_delete.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_safe_zones.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_search.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_sketch.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_snapshot.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_split.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_symbol.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_tour.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_trace.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_understand.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_uses.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_visualize.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_weather.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_why.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/cmd_ws.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/metrics_history.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/commands/resolve.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/db/queries.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/builder.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/clusters.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/cycles.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/layers.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/pagerank.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/graph/pathfinding.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/complexity.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/discovery.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/git_stats.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/incremental.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/parser.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/relations.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/index/symbols.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/apex_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/aura_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/base.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/c_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/foxpro_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/generic_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/go_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/java_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/javascript_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/php_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/python_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/registry.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/rust_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/sfxml_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/typescript_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/languages/visualforce_lang.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/mcp_server.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/output/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/output/formatter.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/output/sarif.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/__init__.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/aggregator.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/api_scanner.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/config.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam/workspace/db.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/dependency_links.txt +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/entry_points.txt +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/requires.txt +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/src/roam_code.egg-info/top_level.txt +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_basic.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_comprehensive.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_fixes.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_foxpro.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_new_features.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_performance.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_resolve.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_salesforce.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_v71_features.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_v7_features.py +0 -0
- {roam_code-7.5.0 → roam_code-8.0.0}/tests/test_visualize.py +0 -0
- {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:
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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/ #
|
|
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 (
|
|
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/ #
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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/ #
|
|
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 (
|
|
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/ #
|
|
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
|
|
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.
|
|
@@ -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())
|