roam-code 8.0.1__tar.gz → 8.1.1__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-8.0.1 → roam_code-8.1.1}/PKG-INFO +4 -4
- {roam_code-8.0.1 → roam_code-8.1.1}/README.md +3 -3
- {roam_code-8.0.1 → roam_code-8.1.1}/pyproject.toml +4 -1
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_complexity.py +20 -9
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_dead.py +28 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_health.py +10 -2
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/python_lang.py +208 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam_code.egg-info/PKG-INFO +4 -4
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam_code.egg-info/SOURCES.txt +1 -0
- roam_code-8.1.1/tests/test_python_extractor_v2.py +485 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/LICENSE +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/setup.cfg +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/__main__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/bridges/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/bridges/base.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/bridges/bridge_protobuf.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/bridges/bridge_salesforce.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/bridges/registry.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/cli.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/changed_files.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_affected_tests.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_alerts.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_breaking.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_bus_factor.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_clusters.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_context.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_conventions.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_coupling.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_coverage_gaps.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_debt.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_deps.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_describe.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_diagnose.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_diff.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_digest.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_doc_staleness.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_entry_points.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_fan.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_file.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_fitness.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_fn_coupling.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_grep.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_impact.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_index.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_init.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_layers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_map.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_module.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_owner.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_patterns.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_pr_risk.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_preflight.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_report.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_risk.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_safe_delete.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_safe_zones.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_search.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_sketch.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_snapshot.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_split.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_symbol.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_testmap.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_tour.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_trace.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_trend.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_understand.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_uses.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_visualize.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_weather.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_why.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_ws.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/cmd_xlang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/context_helpers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/gate_presets.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/graph_helpers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/metrics_history.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/commands/resolve.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/db/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/db/connection.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/db/queries.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/db/schema.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/anomaly.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/builder.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/clusters.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/cycles.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/layers.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/pagerank.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/graph/pathfinding.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/complexity.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/discovery.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/file_roles.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/git_stats.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/incremental.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/indexer.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/parser.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/relations.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/symbols.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/index/test_conventions.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/apex_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/aura_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/base.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/c_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/foxpro_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/generic_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/go_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/java_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/javascript_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/php_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/registry.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/rust_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/sfxml_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/typescript_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/languages/visualforce_lang.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/mcp_server.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/output/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/output/formatter.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/output/sarif.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/workspace/__init__.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/workspace/aggregator.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/workspace/api_scanner.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/workspace/config.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam/workspace/db.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam_code.egg-info/dependency_links.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam_code.egg-info/entry_points.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam_code.egg-info/requires.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/src/roam_code.egg-info/top_level.txt +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_anomaly.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_basic.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_bridges.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_commands_architecture.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_commands_exploration.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_commands_health.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_commands_refactoring.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_commands_workflow.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_comprehensive.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_dead_aging.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_file_roles.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_fixes.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_formatters.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_foxpro.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_gate_presets.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_json_contracts.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_languages.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_performance.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_pr_risk_author.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_resolve.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_salesforce.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_smoke.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_test_conventions.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_v6_features.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_v71_features.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_v7_features.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_visualize.py +0 -0
- {roam_code-8.0.1 → roam_code-8.1.1}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: roam-code
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.1.1
|
|
4
4
|
Summary: Instant codebase comprehension for AI coding agents
|
|
5
5
|
Author: CosmoHac
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1023,7 +1023,7 @@ Delete `.roam/` from your project root to clean up local data.
|
|
|
1023
1023
|
git clone https://github.com/Cranot/roam-code.git
|
|
1024
1024
|
cd roam-code
|
|
1025
1025
|
pip install -e ".[dev]" # includes pytest, ruff
|
|
1026
|
-
pytest tests/ #
|
|
1026
|
+
pytest tests/ # 1691 tests, Python 3.9-3.13
|
|
1027
1027
|
|
|
1028
1028
|
# Or use Make targets:
|
|
1029
1029
|
make dev # install with dev extras
|
|
@@ -1079,7 +1079,7 @@ roam-code/
|
|
|
1079
1079
|
│ └── output/
|
|
1080
1080
|
│ ├── formatter.py # Token-efficient formatting
|
|
1081
1081
|
│ └── sarif.py # SARIF 2.1.0 output
|
|
1082
|
-
└── tests/ #
|
|
1082
|
+
└── tests/ # 1691 tests across 29 test files
|
|
1083
1083
|
```
|
|
1084
1084
|
|
|
1085
1085
|
</details>
|
|
@@ -1120,7 +1120,7 @@ Optional: [fastmcp](https://github.com/jlowin/fastmcp) (MCP server)
|
|
|
1120
1120
|
git clone https://github.com/Cranot/roam-code.git
|
|
1121
1121
|
cd roam-code
|
|
1122
1122
|
pip install -e .
|
|
1123
|
-
pytest tests/ # All
|
|
1123
|
+
pytest tests/ # All 1691 tests must pass
|
|
1124
1124
|
```
|
|
1125
1125
|
|
|
1126
1126
|
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.
|
|
@@ -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 ".[dev]" # includes pytest, ruff
|
|
991
|
-
pytest tests/ #
|
|
991
|
+
pytest tests/ # 1691 tests, Python 3.9-3.13
|
|
992
992
|
|
|
993
993
|
# Or use Make targets:
|
|
994
994
|
make dev # install with dev extras
|
|
@@ -1044,7 +1044,7 @@ roam-code/
|
|
|
1044
1044
|
│ └── output/
|
|
1045
1045
|
│ ├── formatter.py # Token-efficient formatting
|
|
1046
1046
|
│ └── sarif.py # SARIF 2.1.0 output
|
|
1047
|
-
└── tests/ #
|
|
1047
|
+
└── tests/ # 1691 tests across 29 test files
|
|
1048
1048
|
```
|
|
1049
1049
|
|
|
1050
1050
|
</details>
|
|
@@ -1085,7 +1085,7 @@ Optional: [fastmcp](https://github.com/jlowin/fastmcp) (MCP server)
|
|
|
1085
1085
|
git clone https://github.com/Cranot/roam-code.git
|
|
1086
1086
|
cd roam-code
|
|
1087
1087
|
pip install -e .
|
|
1088
|
-
pytest tests/ # All
|
|
1088
|
+
pytest tests/ # All 1691 tests must pass
|
|
1089
1089
|
```
|
|
1090
1090
|
|
|
1091
1091
|
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 = "8.
|
|
7
|
+
version = "8.1.1"
|
|
8
8
|
description = "Instant codebase comprehension for AI coding agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -55,6 +55,9 @@ where = ["src"]
|
|
|
55
55
|
[tool.pytest.ini_options]
|
|
56
56
|
testpaths = ["tests"]
|
|
57
57
|
addopts = "-q --tb=short"
|
|
58
|
+
markers = [
|
|
59
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
60
|
+
]
|
|
58
61
|
|
|
59
62
|
[tool.ruff]
|
|
60
63
|
target-version = "py39"
|
|
@@ -5,6 +5,8 @@ functions/methods by cognitive complexity to identify the hardest-to-
|
|
|
5
5
|
understand code in the project.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
8
10
|
import click
|
|
9
11
|
|
|
10
12
|
from roam.db.connection import open_db
|
|
@@ -12,6 +14,15 @@ from roam.commands.resolve import ensure_index
|
|
|
12
14
|
from roam.output.formatter import loc, abbrev_kind, to_json, json_envelope
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def _safe_metric(row, key, default=0.0):
|
|
18
|
+
"""Safely access a metric column that may not exist in older DBs."""
|
|
19
|
+
try:
|
|
20
|
+
v = row[key]
|
|
21
|
+
return v if v is not None else default
|
|
22
|
+
except (KeyError, IndexError):
|
|
23
|
+
return default
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
def _severity(score: float) -> str:
|
|
16
27
|
"""Map cognitive complexity score to a severity label."""
|
|
17
28
|
if score >= 25:
|
|
@@ -144,11 +155,11 @@ def complexity(ctx, target, limit, threshold, by_file, bumpy_road):
|
|
|
144
155
|
"return_count": r["return_count"],
|
|
145
156
|
"bool_op_count": r["bool_op_count"],
|
|
146
157
|
"callback_depth": r["callback_depth"],
|
|
147
|
-
"cyclomatic_density": r
|
|
148
|
-
"halstead_volume": r
|
|
149
|
-
"halstead_difficulty": r
|
|
150
|
-
"halstead_effort": r
|
|
151
|
-
"halstead_bugs": r
|
|
158
|
+
"cyclomatic_density": _safe_metric(r, "cyclomatic_density"),
|
|
159
|
+
"halstead_volume": _safe_metric(r, "halstead_volume"),
|
|
160
|
+
"halstead_difficulty": _safe_metric(r, "halstead_difficulty"),
|
|
161
|
+
"halstead_effort": _safe_metric(r, "halstead_effort"),
|
|
162
|
+
"halstead_bugs": _safe_metric(r, "halstead_bugs"),
|
|
152
163
|
"severity": _severity(r["cognitive_complexity"]),
|
|
153
164
|
}
|
|
154
165
|
for r in rows
|
|
@@ -181,11 +192,11 @@ def complexity(ctx, target, limit, threshold, by_file, bumpy_road):
|
|
|
181
192
|
factors.append(f"params={r['param_count']}")
|
|
182
193
|
if r["return_count"] >= 4:
|
|
183
194
|
factors.append(f"ret={r['return_count']}")
|
|
184
|
-
cd = r
|
|
185
|
-
if cd
|
|
195
|
+
cd = _safe_metric(r, "cyclomatic_density")
|
|
196
|
+
if cd > 0.15:
|
|
186
197
|
factors.append(f"density={cd:.2f}")
|
|
187
|
-
hv = r
|
|
188
|
-
if hv
|
|
198
|
+
hv = _safe_metric(r, "halstead_volume")
|
|
199
|
+
if hv > 500:
|
|
189
200
|
factors.append(f"H.vol={hv:.0f}")
|
|
190
201
|
|
|
191
202
|
factor_str = f" ({', '.join(factors)})" if factors else ""
|
|
@@ -40,6 +40,20 @@ _API_PREFIXES = ("get", "use", "create", "validate", "fetch", "update",
|
|
|
40
40
|
"delete", "find", "check", "make", "build", "parse")
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
_ABC_METHOD_NAMES = frozenset({
|
|
44
|
+
"language_name", "file_extensions", "extract_symbols", "extract_references",
|
|
45
|
+
"get_docstring", "get_signature", "node_text",
|
|
46
|
+
"detect", "supported_bridges",
|
|
47
|
+
"resolve_cross_language", "get_bridge_edges",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_test_path(file_path):
|
|
52
|
+
"""Check if a file is a test file (discovered by pytest, not imported)."""
|
|
53
|
+
base = os.path.basename(file_path).lower()
|
|
54
|
+
return base.startswith("test_") or base.endswith("_test.py")
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
def _dead_action(r, file_imported):
|
|
44
58
|
"""Compute actionable verdict and confidence % for a dead symbol.
|
|
45
59
|
|
|
@@ -62,6 +76,18 @@ def _dead_action(r, file_imported):
|
|
|
62
76
|
except (KeyError, IndexError):
|
|
63
77
|
kind = ""
|
|
64
78
|
|
|
79
|
+
# Test file symbols — discovered by pytest, never imported directly
|
|
80
|
+
if _is_test_path(r["file_path"]):
|
|
81
|
+
return "INTENTIONAL", 10
|
|
82
|
+
|
|
83
|
+
# CLI command functions — loaded dynamically via LazyGroup/importlib
|
|
84
|
+
if base.startswith("cmd_") and kind == "function":
|
|
85
|
+
return "INTENTIONAL", 20
|
|
86
|
+
|
|
87
|
+
# ABC method overrides — called polymorphically, not by direct import
|
|
88
|
+
if kind == "method" and name in _ABC_METHOD_NAMES:
|
|
89
|
+
return "INTENTIONAL", 10
|
|
90
|
+
|
|
65
91
|
# Entry point / lifecycle hooks (check original case for camelCase hooks)
|
|
66
92
|
if name in _ENTRY_NAMES or name_lower in _ENTRY_NAMES:
|
|
67
93
|
return "INTENTIONAL", 60
|
|
@@ -251,6 +277,8 @@ def _analyze_dead(conn):
|
|
|
251
277
|
Returns (high, low, imported_files) where high/low are lists of Row objects.
|
|
252
278
|
"""
|
|
253
279
|
rows = conn.execute(UNREFERENCED_EXPORTS).fetchall()
|
|
280
|
+
# Exclude test files — their symbols are discovered by pytest, not imported
|
|
281
|
+
rows = [r for r in rows if not _is_test_path(r["file_path"])]
|
|
254
282
|
if not rows:
|
|
255
283
|
return [], [], set()
|
|
256
284
|
|
|
@@ -52,13 +52,21 @@ _FRAMEWORK_NAMES = frozenset({
|
|
|
52
52
|
_UTILITY_PATH_PATTERNS = (
|
|
53
53
|
"composables/", "utils/", "services/", "lib/", "helpers/",
|
|
54
54
|
"shared/", "config/", "core/", "hooks/", "stores/",
|
|
55
|
+
"output/", "db/", "common/", "internal/", "infra/",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_UTILITY_FILE_PATTERNS = (
|
|
59
|
+
"resolve.py", "helpers.py", "common.py", "base.py",
|
|
55
60
|
)
|
|
56
61
|
|
|
57
62
|
|
|
58
63
|
def _is_utility_path(file_path):
|
|
59
|
-
"""Check if a file is in a utility/infrastructure directory."""
|
|
64
|
+
"""Check if a file is in a utility/infrastructure directory or is a known utility file."""
|
|
60
65
|
p = file_path.replace("\\", "/").lower()
|
|
61
|
-
|
|
66
|
+
if any(pat in p for pat in _UTILITY_PATH_PATTERNS):
|
|
67
|
+
return True
|
|
68
|
+
basename = p.rsplit("/", 1)[-1] if "/" in p else p
|
|
69
|
+
return basename in _UTILITY_FILE_PATTERNS
|
|
62
70
|
|
|
63
71
|
|
|
64
72
|
def _percentile(sorted_values, pct):
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
from .base import LanguageExtractor
|
|
4
4
|
|
|
5
|
+
# Builtin type names that don't create real reference edges
|
|
6
|
+
_BUILTIN_TYPES = frozenset({
|
|
7
|
+
"int", "str", "float", "bool", "bytes", "None",
|
|
8
|
+
"list", "dict", "set", "tuple", "type", "object",
|
|
9
|
+
})
|
|
10
|
+
|
|
5
11
|
|
|
6
12
|
class PythonExtractor(LanguageExtractor):
|
|
7
13
|
"""Full Python symbol and reference extractor."""
|
|
@@ -170,6 +176,15 @@ class PythonExtractor(LanguageExtractor):
|
|
|
170
176
|
parent_name=parent_name,
|
|
171
177
|
))
|
|
172
178
|
|
|
179
|
+
# Extract instance attributes from __init__ body (self.x = ...)
|
|
180
|
+
if name == "__init__" and parent_name:
|
|
181
|
+
body = node.child_by_field_name("body")
|
|
182
|
+
if body:
|
|
183
|
+
self_name = self._detect_self_name(node)
|
|
184
|
+
self._extract_init_attributes(
|
|
185
|
+
body, source, symbols, parent_name, dunder_all, self_name,
|
|
186
|
+
)
|
|
187
|
+
|
|
173
188
|
def _extract_class(self, node, source, file_path, symbols, parent_name, dunder_all, decorator_node=None):
|
|
174
189
|
name_node = node.child_by_field_name("name")
|
|
175
190
|
if name_node is None:
|
|
@@ -320,6 +335,88 @@ class PythonExtractor(LanguageExtractor):
|
|
|
320
335
|
return text
|
|
321
336
|
return None
|
|
322
337
|
|
|
338
|
+
def _detect_self_name(self, func_node) -> bytes:
|
|
339
|
+
"""Detect the self/cls parameter name from a method's first argument (Pyan-inspired)."""
|
|
340
|
+
params = func_node.child_by_field_name("parameters")
|
|
341
|
+
if params:
|
|
342
|
+
for child in params.children:
|
|
343
|
+
if child.type == "identifier":
|
|
344
|
+
return child.text
|
|
345
|
+
# typed_parameter: (self: Self) or typed_default_parameter
|
|
346
|
+
if child.type in ("typed_parameter", "typed_default_parameter"):
|
|
347
|
+
name_node = child.child_by_field_name("name")
|
|
348
|
+
if name_node and name_node.type == "identifier":
|
|
349
|
+
return name_node.text
|
|
350
|
+
return b"self"
|
|
351
|
+
|
|
352
|
+
def _extract_init_attributes(self, body_node, source, symbols, parent_name,
|
|
353
|
+
dunder_all, self_name):
|
|
354
|
+
"""Extract instance attributes from __init__ body (self.x = value)."""
|
|
355
|
+
seen = set()
|
|
356
|
+
# Collect existing class-level property names to avoid duplicates
|
|
357
|
+
for sym in symbols:
|
|
358
|
+
if sym.get("parent_name") == parent_name and sym.get("kind") == "property":
|
|
359
|
+
seen.add(sym["name"])
|
|
360
|
+
|
|
361
|
+
self._collect_self_assignments(body_node, source, symbols, parent_name,
|
|
362
|
+
dunder_all, self_name, seen)
|
|
363
|
+
|
|
364
|
+
def _collect_self_assignments(self, node, source, symbols, parent_name,
|
|
365
|
+
dunder_all, self_name, seen):
|
|
366
|
+
"""Walk __init__ body collecting self.x = ... assignments."""
|
|
367
|
+
for child in node.children:
|
|
368
|
+
if child.type == "expression_statement":
|
|
369
|
+
for sub in child.children:
|
|
370
|
+
if sub.type == "assignment":
|
|
371
|
+
self._try_extract_self_attr(
|
|
372
|
+
sub, source, symbols, parent_name, dunder_all,
|
|
373
|
+
self_name, seen,
|
|
374
|
+
)
|
|
375
|
+
elif child.type == "assignment":
|
|
376
|
+
self._try_extract_self_attr(
|
|
377
|
+
child, source, symbols, parent_name, dunder_all,
|
|
378
|
+
self_name, seen,
|
|
379
|
+
)
|
|
380
|
+
# Recurse into conditional/try/with blocks (common in __init__)
|
|
381
|
+
elif child.type in ("if_statement", "try_statement", "with_statement",
|
|
382
|
+
"for_statement", "block"):
|
|
383
|
+
self._collect_self_assignments(
|
|
384
|
+
child, source, symbols, parent_name, dunder_all,
|
|
385
|
+
self_name, seen,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def _try_extract_self_attr(self, assign_node, source, symbols, parent_name,
|
|
389
|
+
dunder_all, self_name, seen):
|
|
390
|
+
"""If assign_node is `self.x = value`, extract x as a property symbol."""
|
|
391
|
+
left = assign_node.child_by_field_name("left")
|
|
392
|
+
if left is None or left.type != "attribute":
|
|
393
|
+
return
|
|
394
|
+
obj = left.child_by_field_name("object")
|
|
395
|
+
if obj is None or obj.type != "identifier" or obj.text != self_name:
|
|
396
|
+
return
|
|
397
|
+
attr_node = left.child_by_field_name("attribute")
|
|
398
|
+
if attr_node is None:
|
|
399
|
+
return
|
|
400
|
+
attr_name = self.node_text(attr_node, source)
|
|
401
|
+
if attr_name in seen:
|
|
402
|
+
return
|
|
403
|
+
seen.add(attr_name)
|
|
404
|
+
|
|
405
|
+
right = assign_node.child_by_field_name("right")
|
|
406
|
+
default_value = self._extract_literal_value(right, source) if right else None
|
|
407
|
+
|
|
408
|
+
qualified = f"{parent_name}.{attr_name}"
|
|
409
|
+
symbols.append(self._make_symbol(
|
|
410
|
+
name=attr_name,
|
|
411
|
+
kind="property",
|
|
412
|
+
line_start=assign_node.start_point[0] + 1,
|
|
413
|
+
line_end=assign_node.end_point[0] + 1,
|
|
414
|
+
qualified_name=qualified,
|
|
415
|
+
visibility=self._visibility(attr_name),
|
|
416
|
+
parent_name=parent_name,
|
|
417
|
+
default_value=default_value,
|
|
418
|
+
))
|
|
419
|
+
|
|
323
420
|
def _is_exported(self, name: str, dunder_all: set[str] | None) -> bool:
|
|
324
421
|
if dunder_all is not None:
|
|
325
422
|
return name in dunder_all
|
|
@@ -333,6 +430,15 @@ class PythonExtractor(LanguageExtractor):
|
|
|
333
430
|
self._extract_from_import(child, source, refs, scope_name)
|
|
334
431
|
elif child.type == "call":
|
|
335
432
|
self._extract_call(child, source, refs, scope_name)
|
|
433
|
+
elif child.type == "decorated_definition":
|
|
434
|
+
self._extract_decorator_refs(child, source, refs, scope_name)
|
|
435
|
+
self._walk_refs(child, source, file_path, refs, scope_name)
|
|
436
|
+
elif child.type in ("assignment", "expression_statement"):
|
|
437
|
+
# Walk type annotations on assignments: x: Path = ..., self.x: int = ...
|
|
438
|
+
# Note: type aliases (PathList = List[Path]) are NOT handled here —
|
|
439
|
+
# the RHS is a subscript expression, not a type annotation field.
|
|
440
|
+
self._extract_assignment_type_refs(child, source, refs, scope_name)
|
|
441
|
+
self._walk_refs(child, source, file_path, refs, scope_name)
|
|
336
442
|
else:
|
|
337
443
|
# Recurse, updating scope for classes/functions
|
|
338
444
|
new_scope = scope_name
|
|
@@ -341,8 +447,110 @@ class PythonExtractor(LanguageExtractor):
|
|
|
341
447
|
if n:
|
|
342
448
|
fname = self.node_text(n, source)
|
|
343
449
|
new_scope = f"{scope_name}.{fname}" if scope_name else fname
|
|
450
|
+
# Extract type annotation refs from function parameters and return
|
|
451
|
+
if child.type == "function_definition":
|
|
452
|
+
self._extract_type_refs(child, source, refs, new_scope)
|
|
344
453
|
self._walk_refs(child, source, file_path, refs, new_scope)
|
|
345
454
|
|
|
455
|
+
def _extract_decorator_refs(self, decorated_node, source, refs, scope_name):
|
|
456
|
+
"""Extract references from decorators (e.g., @cache, @app.route)."""
|
|
457
|
+
for child in decorated_node.children:
|
|
458
|
+
if child.type == "decorator":
|
|
459
|
+
# The decorator content is after the '@'
|
|
460
|
+
for sub in child.children:
|
|
461
|
+
if sub.type == "identifier":
|
|
462
|
+
name = self.node_text(sub, source)
|
|
463
|
+
refs.append(self._make_reference(
|
|
464
|
+
target_name=name,
|
|
465
|
+
kind="call",
|
|
466
|
+
line=sub.start_point[0] + 1,
|
|
467
|
+
source_name=scope_name,
|
|
468
|
+
))
|
|
469
|
+
elif sub.type == "attribute":
|
|
470
|
+
name = self.node_text(sub, source)
|
|
471
|
+
refs.append(self._make_reference(
|
|
472
|
+
target_name=name,
|
|
473
|
+
kind="call",
|
|
474
|
+
line=sub.start_point[0] + 1,
|
|
475
|
+
source_name=scope_name,
|
|
476
|
+
))
|
|
477
|
+
elif sub.type == "call":
|
|
478
|
+
self._extract_call(sub, source, refs, scope_name)
|
|
479
|
+
|
|
480
|
+
def _extract_type_refs(self, func_node, source, refs, scope_name):
|
|
481
|
+
"""Extract references from type annotations in function signatures."""
|
|
482
|
+
# Parameter type annotations
|
|
483
|
+
params = func_node.child_by_field_name("parameters")
|
|
484
|
+
if params:
|
|
485
|
+
for param in params.children:
|
|
486
|
+
type_node = param.child_by_field_name("type")
|
|
487
|
+
if type_node:
|
|
488
|
+
self._walk_type_node(type_node, source, refs, scope_name)
|
|
489
|
+
|
|
490
|
+
# Return type annotation
|
|
491
|
+
ret = func_node.child_by_field_name("return_type")
|
|
492
|
+
if ret:
|
|
493
|
+
self._walk_type_node(ret, source, refs, scope_name)
|
|
494
|
+
|
|
495
|
+
def _extract_assignment_type_refs(self, node, source, refs, scope_name):
|
|
496
|
+
"""Extract type_ref edges from annotated assignments (class fields, module vars)."""
|
|
497
|
+
targets = [node] if node.type == "assignment" else []
|
|
498
|
+
if node.type == "expression_statement":
|
|
499
|
+
for sub in node.children:
|
|
500
|
+
if sub.type == "assignment":
|
|
501
|
+
targets.append(sub)
|
|
502
|
+
for assign in targets:
|
|
503
|
+
type_node = assign.child_by_field_name("type")
|
|
504
|
+
if type_node:
|
|
505
|
+
self._walk_type_node(type_node, source, refs, scope_name)
|
|
506
|
+
|
|
507
|
+
def _walk_type_node(self, node, source, refs, scope_name):
|
|
508
|
+
"""Walk a type annotation node and extract type references."""
|
|
509
|
+
if node.type == "identifier":
|
|
510
|
+
name = self.node_text(node, source)
|
|
511
|
+
if name not in _BUILTIN_TYPES:
|
|
512
|
+
refs.append(self._make_reference(
|
|
513
|
+
target_name=name,
|
|
514
|
+
kind="type_ref",
|
|
515
|
+
line=node.start_point[0] + 1,
|
|
516
|
+
source_name=scope_name,
|
|
517
|
+
))
|
|
518
|
+
elif node.type == "attribute":
|
|
519
|
+
name = self.node_text(node, source)
|
|
520
|
+
refs.append(self._make_reference(
|
|
521
|
+
target_name=name,
|
|
522
|
+
kind="type_ref",
|
|
523
|
+
line=node.start_point[0] + 1,
|
|
524
|
+
source_name=scope_name,
|
|
525
|
+
))
|
|
526
|
+
elif node.type == "string":
|
|
527
|
+
# Forward reference: Optional["Config"] -> extract "Config"
|
|
528
|
+
for child in node.children:
|
|
529
|
+
if child.type == "string_content":
|
|
530
|
+
name = self.node_text(child, source).strip()
|
|
531
|
+
# Simple identifier forward ref: "Config"
|
|
532
|
+
if name.isidentifier() and name not in _BUILTIN_TYPES:
|
|
533
|
+
refs.append(self._make_reference(
|
|
534
|
+
target_name=name,
|
|
535
|
+
kind="type_ref",
|
|
536
|
+
line=child.start_point[0] + 1,
|
|
537
|
+
source_name=scope_name,
|
|
538
|
+
))
|
|
539
|
+
# Dotted forward ref: "module.ClassName"
|
|
540
|
+
elif "." in name and all(
|
|
541
|
+
p.isidentifier() for p in name.split(".")
|
|
542
|
+
):
|
|
543
|
+
refs.append(self._make_reference(
|
|
544
|
+
target_name=name,
|
|
545
|
+
kind="type_ref",
|
|
546
|
+
line=child.start_point[0] + 1,
|
|
547
|
+
source_name=scope_name,
|
|
548
|
+
))
|
|
549
|
+
else:
|
|
550
|
+
# Recurse into generic types like List[Item], Optional[str], etc.
|
|
551
|
+
for child in node.children:
|
|
552
|
+
self._walk_type_node(child, source, refs, scope_name)
|
|
553
|
+
|
|
346
554
|
def _extract_import(self, node, source, refs, scope_name):
|
|
347
555
|
# import x, import x.y, import x as y
|
|
348
556
|
for child in node.children:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: roam-code
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.1.1
|
|
4
4
|
Summary: Instant codebase comprehension for AI coding agents
|
|
5
5
|
Author: CosmoHac
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1023,7 +1023,7 @@ Delete `.roam/` from your project root to clean up local data.
|
|
|
1023
1023
|
git clone https://github.com/Cranot/roam-code.git
|
|
1024
1024
|
cd roam-code
|
|
1025
1025
|
pip install -e ".[dev]" # includes pytest, ruff
|
|
1026
|
-
pytest tests/ #
|
|
1026
|
+
pytest tests/ # 1691 tests, Python 3.9-3.13
|
|
1027
1027
|
|
|
1028
1028
|
# Or use Make targets:
|
|
1029
1029
|
make dev # install with dev extras
|
|
@@ -1079,7 +1079,7 @@ roam-code/
|
|
|
1079
1079
|
│ └── output/
|
|
1080
1080
|
│ ├── formatter.py # Token-efficient formatting
|
|
1081
1081
|
│ └── sarif.py # SARIF 2.1.0 output
|
|
1082
|
-
└── tests/ #
|
|
1082
|
+
└── tests/ # 1691 tests across 29 test files
|
|
1083
1083
|
```
|
|
1084
1084
|
|
|
1085
1085
|
</details>
|
|
@@ -1120,7 +1120,7 @@ Optional: [fastmcp](https://github.com/jlowin/fastmcp) (MCP server)
|
|
|
1120
1120
|
git clone https://github.com/Cranot/roam-code.git
|
|
1121
1121
|
cd roam-code
|
|
1122
1122
|
pip install -e .
|
|
1123
|
-
pytest tests/ # All
|
|
1123
|
+
pytest tests/ # All 1691 tests must pass
|
|
1124
1124
|
```
|
|
1125
1125
|
|
|
1126
1126
|
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.
|