feat(calc-inventory): extract computed-numeric fields + formulas from source - #64
Merged
Conversation
… source roam records declarations and references but never the arithmetic between them, so a VAT/rounding assignment is invisible to the symbol layer. Add a deterministic AST extractor (index/calc_extract.py) that walks assignment nodes whose value is a calculation — an arithmetic operator, an accumulation, or a rounding/math call — recording target, formula, operands, literals, and any rounding function, in any tree-sitter language (PHP, JS/TS, Python, Go, Java, ...). The calc-inventory command surfaces them: inventory (where is money computed and how), --money (accounting-shaped fields), and --divergence (fields with the same name computed by different formulas — the drift that bites when a value is computed in a backend and a frontend and the copies fall out of sync; ranked cross-language /rounding-divergent first). --round-funcs adds project rounding wrappers. Zero model calls; fail-open on missing grammar. Validated on real code: 25 formulas in a live VAT service + the frontend Math.round VAT split. 14 tests; registry/parity/dead-surface green; surface counts synced.
roam-code AnalysisMode: incremental ( Health Score: 79/100 health: Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components Health Metrics
PR Risk
Quality Gate: PASSEDGate expression: SARIF Upload
Full analysis outputhealth{
"_meta": {
"cache_ttl_s": 300,
"cacheable": true,
"index_age_s": 937,
"index_status": {
"dirty_files": 0,
"fresh": false,
"head_commit": "f90bd5e07de1",
"hint": "index latest commit 97418ea4f29e != HEAD f90bd5e07de1 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
"indexed_commit": "97418ea4f29e"
},
"latency_ms": null,
"response_tokens": 4087,
"roam_version": "13.9.0",
"timestamp": "2026-07-15T22:08:29Z"
},
"actionable_count": 15,
"actionable_cycles": 0,
"agent_contract": {
"confidence": null,
"facts": [
"Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components",
"health score 79",
"tangle ratio 0.0",
"0.0015 propagation cost findings",
"issue count 65"
],
"next_commands": [
"roam debt",
"roam trends --days 30"
],
"risks": []
},
"algebraic_connectivity": null,
"algebraic_connectivity_available": false,
"bottleneck_thresholds": {
"p70": 408.5,
"p90": 4107.7,
"population": 2320,
"utility_multiplier": 1.5
},
"category_severity": {
"bottlenecks": {
"critical": 15,
"info": 0,
"warning": 0
},
"cycles": {
"critical": 0,
"info": 0,
"warning": 0
},
"god_components": {
"critical": 33,
"info": 10,
"warning": 7
},
"layer_violations": {
"critical": 0,
"info": 0,
"warning": 0
}
},
"command": "health",
"cycles_actionable": 0,
"cycles_total": 17,
"framework_filtered": 0,
"health_score": 79,
"ignored_cycles": 17,
"imported_coverable_lines": 0,
"imported_coverage_files": 0,
"imported_coverage_pct": null,
"imported_covered_lines": 0,
"index_status": {
"dirty_files": 0,
"fresh": false,
"head_commit": "f90bd5e07de1",
"hint": "index latest commit 97418ea4f29e != HEAD f90bd5e07de1 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
"indexed_commit": "97418ea4f29e"
},
"issue_count": 65,
"list_counts": {
"bottlenecks": 15,
"cycle_break_suggestions": 0,
"cycles": 17,
"god_components": 50,
"layer_violations": 0,
"next_steps": 2,
"score_breakdown": 5
},
"project": "roam-code",
"propagation_cost": 0.0015,
"schema": "roam-envelope-v1",
"schema_version": "1.1.0",
"severity": {
"critical": 48,
"info": 27,
"warning": 7
},
"summary": {
"actionable_cycles": 0,
"algebraic_connectivity": null,
"algebraic_connectivity_available": false,
"category_severity": {
"bottlenecks": {
"critical": 15,
"info": 0,
"warning": 0
},
"cycles": {
"critical": 0,
"info": 0,
"warning": 0
},
"god_components": {
"critical": 33,
"info": 10,
"warning": 7
},
"layer_violations": {
"critical": 0,
"info": 0,
"warning": 0
}
},
"cycles_actionable": 0,
"cycles_definition": "Cycle counts derived from `roam.graph.cycles.find_cycles(G, min_size=2)` on the symbol graph. `cycles_total` = all SCCs of size >= 2; `cycles_actionable` = SCCs spanning >=2 files AND no test files (same-file and test-only cycles are informational). Run `roam health` for the per-cycle breakdown.",
"cycles_total": 17,
"detail_available": true,
"god_components": 50,
"god_components_definition": "God components: symbols where `(in_degree + out_degree) > 20` from the `graph_metrics` table, with utility-aware severity bands (standard >50=CRITICAL >30=WARNING; utility >150=CRITICAL >90=WARNING). Run `roam health` for the per-symbol breakdown. Legacy aliases: `god_objects` (fingerprint), `god_classes` (rules).",
"health_score": 79,
"health_score_definition": "weighted geometric mean (0-100) of 5 sigmoid health factors: tangle_ratio, god_components, bottlenecks, layer_violations, file_health (+coverage if available).",
"ignored_cycles": 17,
"imported_coverage_files": 0,
"imported_coverage_pct": null,
"issue_count": 65,
"partial_success": true,
"preserved_list_truncations": {},
"propagation_cost": 0.0015,
"severity": {
"critical": 48,
"info": 27,
"warning": 7
},
"tangle_ratio": 0,
"tangle_ratio_definition": "fraction of symbols inside non-trivial SCCs; higher = more cyclic coupling.",
"total_cycles": 17,
"truncated": true,
"verdict": "Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components",
"warnings_out": [
"health_algebraic_connectivity_warning:RuntimeWarning:algebraic_connectivity compute failed (ModuleNotFoundError): No module named 'numpy'; returning 0.0 sentinel — value is NOT a legitimate disconnected-graph reading"
]
},
"tangle_ratio": 0,
"total_cycles": 17,
"utility_count": 35,
"version": "13.9.0",
"warnings_out": [
"health_algebraic_connectivity_warning:RuntimeWarning:algebraic_connectivity compute failed (ModuleNotFoundError): No module named 'numpy'; returning 0.0 sentinel — value is NOT a legitimate disconnected-graph reading"
]
}pr-risk{
"_meta": {
"cache_ttl_s": 60,
"cacheable": true,
"index_age_s": 940,
"index_status": {
"dirty_files": 0,
"fresh": false,
"head_commit": "f90bd5e07de1",
"hint": "index latest commit 97418ea4f29e != HEAD f90bd5e07de1 — git-derived metrics (commits, churn, co-change, weather) may be stale. Run `roam index --force`.",
"indexed_commit": "97418ea4f29e"
},
"latency_ms": null,
"response_tokens": 2224,
"roam_version": "13.9.0",
"timestamp": "2026-07-15T22:08:32Z"
},
"actor": null,
"agent_contract": {
"confidence": null,
"facts": [
"High risk (60/100) — careful review needed (risk_level high) (driver: hotspot_score)",
"risk score 60",
"3 risk rank findings",
"17 changed files",
"899 lines added"
],
"next_commands": [],
"risks": [
"pr-risk: high (60/100) on d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD — driver: hotspot_score",
"High blast radius: 9650 of 41711 symbols affected (23.1%) on d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD"
]
},
"author": null,
"blast_radius_pct": 23.1,
"bus_factor_risk": 0.79,
"change_shape": "mixed",
"changed_files": 17,
"closest_historical_pattern": [
"AGENTS.md",
"README.md",
"docs/COMMANDS.md",
"docs/ci-integration.md",
"llms-install.md",
"skills/roam/SKILL.md",
"src/roam/cli.py",
"src/roam/commands/cmd_calc_inventory.py",
"src/roam/competitor_site_data.py",
"src/roam/index/calc_extract.py",
"src/roam/modes/policy.py",
"templates/distribution/landing-page/docs/agent-contract.html",
"templates/distribution/landing-page/docs/integration-tutorials.html",
"templates/distribution/landing-page/llms.txt",
"templates/distribution/landing-page/pricing.html",
"templates/distribution/landing-page/setup.html",
"tests/test_calc_inventory.py"
],
"closest_similarity": 1,
"cluster_spread": 0.01,
"clusters_touched": 69,
"command": "pr-risk",
"coupling_score": 0.01,
"dead_code": [
{
"file": "src/roam/cli.py",
"kind": "class",
"name": "LazyGroup"
},
{
"file": "src/roam/cli.py",
"kind": "method",
"name": "parse_args"
},
{
"file": "src/roam/cli.py",
"kind": "method",
"name": "list_commands"
},
{
"file": "src/roam/cli.py",
"kind": "method",
"name": "get_command"
},
{
"file": "src/roam/cli.py",
"kind": "method",
"name": "resolve_command"
},
{
"file": "src/roam/cli.py",
"kind": "method",
"name": "invoke"
},
{
"file": "src/roam/cli.py",
"kind": "method",
"name": "format_help"
}
],
"dead_exports": 7,
"familiarity": {
"avg_familiarity": 1,
"files": [],
"files_assessed": 0
},
"findings": [
{
"claim": "pr-risk: high (60/100) on d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD — driver: hotspot_score",
"confidence": "heuristic",
"evidence": {
"actor": null,
"author": null,
"blast_radius_pct": 23.1,
"bus_factor_risk": 0.786,
"changed_files_count": 17,
"commit_range": "d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD",
"coupling_score": 0.011,
"created_at_epoch": 1784153312,
"diff_id": "b12e9554de96",
"familiarity_risk": 0,
"file_list": [
"AGENTS.md",
"README.md",
"docs/COMMANDS.md",
"docs/ci-integration.md",
"llms-install.md",
"skills/roam/SKILL.md",
"src/roam/cli.py",
"src/roam/commands/cmd_calc_inventory.py",
"src/roam/competitor_site_data.py",
"src/roam/index/calc_extract.py",
"src/roam/modes/policy.py",
"templates/distribution/landing-page/docs/agent-contract.html",
"templates/distribution/landing-page/docs/integration-tutorials.html",
"templates/distribution/landing-page/llms.txt",
"templates/distribution/landing-page/pricing.html",
"templates/distribution/landing-page/setup.html",
"tests/test_calc_inventory.py"
],
"hotspot_score": 1,
"label": "d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD",
"lines_added": 899,
"lines_removed": 20,
"minor_risk": 0,
"novelty_score": 0,
"reductive_change": false,
"risk_level": "high",
"risk_score": 60,
"staged": false,
"test_coverage_pct": 57.1,
"top_driver": "hotspot_score"
},
"finding_id_str": "pr-risk:composite-risk-score:b12e9554de96",
"kind": "pr-risk:composite-risk-score",
"severity": "high",
"source_detector": "pr-risk",
"source_version": "1.0.0",
"subject_id": null,
"subject_kind": "commit"
},
{
"claim": "High blast radius: 9650 of 41711 symbols affected (23.1%) on d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD",
"confidence": "structural",
"evidence": {
"affected_symbols": 9650,
"blast_radius_pct": 23.1,
"changed_files_count": 17,
"changed_symbol_ids_count": 182,
"commit_range": "d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD",
"created_at_epoch": 1784153312,
"diff_id": "b12e9554de96",
"file_list": [
"AGENTS.md",
"README.md",
"docs/COMMANDS.md",
"docs/ci-integration.md",
"llms-install.md",
"skills/roam/SKILL.md",
"src/roam/cli.py",
"src/roam/commands/cmd_calc_inventory.py",
"src/roam/competitor_site_data.py",
"src/roam/index/calc_extract.py",
"src/roam/modes/policy.py",
"templates/distribution/landing-page/docs/agent-contract.html",
"templates/distribution/landing-page/docs/integration-tutorials.html",
"templates/distribution/landing-page/llms.txt",
"templates/distribution/landing-page/pricing.html",
"templates/distribution/landing-page/setup.html",
"tests/test_calc_inventory.py"
],
"label": "d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD",
"staged": false,
"total_symbols": 41711
},
"finding_id_str": "pr-risk:high-blast-radius-symbol-touched:b12e9554de96",
"kind": "pr-risk:high-blast-radius-symbol-touched",
"severity": "high",
"source_detector": "pr-risk",
"source_version": "1.0.0",
"subject_id": null,
"subject_kind": "commit"
}
],
"hotspot_score": 1,
"label": "d9dba291cea3e4eab519416f5e6c2127e9fd77e0..HEAD",
"layer_spread": 0.61,
"layers_touched": 19,
"lines_added": 899,
"lines_removed": 20,
"minor_risk": {
"files": [],
"files_assessed": 0,
"minor_files": 0
},
"novelty_score": 0,
"per_file": [
{
"blast": 9545,
"churn": 3392,
"is_test": false,
"lines_added": 2,
"lines_removed": 0,
"path": "src/roam/cli.py",
"symbols": 86
},
{
"blast": 8253,
"churn": 659,
"is_test": false,
"lines_added": 2,
"lines_removed": 0,
"path": "src/roam/modes/policy.py",
"symbols": 18
},
{
"blast": 158,
"churn": 285,
"is_test": false,
"lines_added": 327,
"lines_removed": 0,
"path": "src/roam/index/calc_extract.py",
"symbols": 30
},
{
"blast": 68,
"churn": 3553,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "src/roam/competitor_site_data.py",
"symbols": 24
},
{
"blast": 44,
"churn": 286,
"is_test": false,
"lines_added": 316,
"lines_removed": 0,
"path": "src/roam/commands/cmd_calc_inventory.py",
"symbols": 7
},
{
"blast": 5,
"churn": 162,
"is_test": true,
"lines_added": 231,
"lines_removed": 0,
"path": "tests/test_calc_inventory.py",
"symbols": 17
},
{
"blast": 0,
"churn": 1059,
"is_test": false,
"lines_added": 4,
"lines_removed": 4,
"path": "AGENTS.md",
"symbols": 0
},
{
"blast": 0,
"churn": 8936,
"is_test": false,
"lines_added": 3,
"lines_removed": 3,
"path": "README.md",
"symbols": 0
},
{
"blast": 0,
"churn": 358,
"is_test": false,
"lines_added": 3,
"lines_removed": 2,
"path": "docs/COMMANDS.md",
"symbols": 0
},
{
"blast": 0,
"churn": 524,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "docs/ci-integration.md",
"symbols": 0
},
{
"blast": 0,
"churn": 646,
"is_test": false,
"lines_added": 2,
"lines_removed": 2,
"path": "llms-install.md",
"symbols": 0
},
{
"blast": 0,
"churn": 325,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "skills/roam/SKILL.md",
"symbols": 0
},
{
"blast": 0,
"churn": 670,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "templates/distribution/landing-page/docs/agent-contract.html",
"symbols": 0
},
{
"blast": 0,
"churn": 459,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "templates/distribution/landing-page/docs/integration-tutorials.html",
"symbols": 0
},
{
"blast": 0,
"churn": 221,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "templates/distribution/landing-page/llms.txt",
"symbols": 0
},
{
"blast": 0,
"churn": 1019,
"is_test": false,
"lines_added": 1,
"lines_removed": 1,
"path": "templates/distribution/landing-page/pricing.html",
"symbols": 0
},
{
"blast": 0,
"churn": 289,
"is_test": false,
"lines_added": 2,
"lines_removed": 2,
"path": "templates/distribution/landing-page/setup.html",
"symbols": 0
}
],
"project": "roam-code",
"reductive_change": false,
"reductive_discount_applied": false,
"risk_level": "high",
"risk_level_canonical": "high",
"risk_rank": 3,
"risk_score": 60,
"schema": "roam-envelope-v1",
"schema_version": "1.1.0",
"suggested_reviewers": [
{
"actor": "Cranot",
"author": "Cranot",
"lines": 16241
},
{
"actor": "holive",
"author": "holive",
"lines": 92
},
{
"actor": "prakteon-operator",
"author": "prakteon-operator",
"lines": 11
}
],
"summary": {
"change_shape": "mixed",
"changed_files": 17,
"findings_count": 2,
"lines_added": 899,
"lines_removed": 20,
"partial_success": false,
"risk_level": "high",
"risk_level_canonical": "high",
"risk_rank": 3,
"risk_score": 60,
"score_classification": "classified",
"verdict": "High risk (60/100) — careful review needed (risk_level high) (driver: hotspot_score)"
},
"test_coverage_pct": 57.1,
"total_clusters": 13150,
"total_layers": 31,
"version": "13.9.0",
"warnings_out": []
}roam-code analysis | Commands: |
Roam Agent ReviewVerdict: SAFE (risk_level low) blast-radius 0/100 · ai-likelihood 31/100 · rule violations 0 · critique high-severity 0 Verdict: SAFE. All structural signals clean at the configured thresholds. Next steps
Powered by roam-code — Apache 2.0, 100% local. Customize thresholds in |
… divergence Two production-grade improvements: 1. Correctness: --round-funcs previously mutated a module-global frozenset, which would LEAK across calls in the long-running MCP server (a per-request widening would corrupt every later request). Thread extra_round_funcs as a parameter through extract_calcs / extract_calcs_from_file / _analyze_rhs instead. Regression test asserts no leak. 2. Value: language-aware rounding SEMANTICS divergence. The same field computed with the same call name 'round' can silently disagree to-the-cent — PHP round() is half-away-from-zero, JS Math.round is half-up-toward-+inf, Python round() is banker's (half-to-even). _ROUNDING_SEMANTICS maps (lang, fn) -> tie/direction; --divergence now flags rounding_semantics_divergent and ranks it the highest signal. Catches the frontend<->backend rounding bug a formula diff alone misses. 4 new tests (18 total). Prepush gate 7/7 green.
Opt-in gate (exit 5) when any field is rounding-semantics-divergent — the same field computed with the same call name but different tie behavior across implementations (PHP round half-away vs JS Math.round half-up vs Python round banker's), the clearest to-the-cent money bug. Lets any project enforce calc-consistency in CI without wiring roam verify. Off by default. 2 tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a deterministic AST extractor (
index/calc_extract.py) +roam calc-inventorycommand that enumerates computed-numeric fields and their formulas in any tree-sitter language (PHP/JS/TS/Python/Go/Java/...).Why: roam records declarations and references but never the arithmetic between them, so a VAT/rounding assignment was invisible to the symbol layer.
What it does: inventory (where money is computed and how),
--money(accounting-shaped filter),--divergence(same-name-different-formula drift, ranked cross-language/rounding-divergent first — catches frontend↔backend rounding divergence),--round-funcs(project wrappers). Zero model calls; fail-open on missing grammar.Validation: 25 formulas from a live VAT service; 14 tests; registry/parity/dead-surface/mode/doc/budget gates green; surface counts synced (276). Tests skip gracefully when the tree-sitter language-pack is unavailable (transient-download robustness).