Skip to content

feat(calc-inventory): extract computed-numeric fields + formulas from source - #64

Merged
Cranot merged 3 commits into
mainfrom
feat/calc-inventory
Jul 15, 2026
Merged

feat(calc-inventory): extract computed-numeric fields + formulas from source#64
Cranot merged 3 commits into
mainfrom
feat/calc-inventory

Conversation

@Cranot

@Cranot Cranot commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Adds a deterministic AST extractor (index/calc_extract.py) + roam calc-inventory command 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).

… 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.
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

roam-code Analysis

Mode: incremental (changed-only) — base d9dba291cea3e4eab519416f5e6c2127e9fd77e0, 30 changed+dependent files

Health Score: 79/100 FAIR

health: Fair codebase (79/100) — 48 critical, 7 warnings, focus: god_components
pr-risk: High risk (60/100) — careful review needed (risk_level high) (driver: hotspot_score)

Health Metrics

Metric Value
Health Score 79/100
Tangle Ratio 0%
Propagation Cost 0.0015
Total Issues 65

PR Risk

Metric Value
Risk Score 60/100

Quality Gate: PASSED

Gate expression: health_score>=50

SARIF Upload

Metric Value
Category roam-code-self-analysis/self-analysis/py3.12
Results Uploaded 82
Full analysis output

health

{
  "_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: health pr-risk

@github-actions

Copy link
Copy Markdown

Roam Agent Review

Verdict: 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

  • No structural concerns at the configured thresholds. Standard review still recommended.

Powered by roam-code — Apache 2.0, 100% local. Customize thresholds in .roam/rules.yml. Docs.

Cranot added 2 commits July 16, 2026 01:00
… 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.
@Cranot
Cranot merged commit 766c9ca into main Jul 15, 2026
11 checks passed
@Cranot
Cranot deleted the feat/calc-inventory branch July 15, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant