# cstat code complexity score v0 ## Status Implemented by the `scorecard` CLI command and `src/scorecard.rs`. This score is intentionally heuristic. It is the first deterministic target for agents to optimize before the project has behavioral data from agent cleanup loops. Incompatible formula or JSON changes must bump `score_version`. ## Purpose `code_complexity_cost_v0` measures structural reasoning burden in a Rust codebase. Lower cost means the code is structurally cleaner according to this metric. The score is designed for agent optimization: an agent can compare before/after runs and try to reduce the scalar by simplifying functions, reducing oversized files, lowering coupling, shrinking excessive abstraction surface, and removing stale code. The score does not prove correctness, safety, maintainability, public API fitness, or behavior preservation. Those are harness responsibilities. ## Non-goals The score must not include: - test pass/fail status; - benchmark pass/fail status; - coverage pass/fail status; - reward shaping; - task-specific acceptance criteria; - model training logic; - patch generation; - semantic proof of dead-code safety; - rustc-level name resolution or macro expansion requirements for v0. The harness may reject an attempted optimization if tests, benchmarks, coverage, protected files, or behavior checks fail. That rejection is separate from the complexity score. ## Optimization contract The agent-facing objective is: ```text minimize code_complexity_cost_v0 ``` The environment-facing objective is: ```text minimize code_complexity_cost_v0 subject to harness guards passing ``` The score itself is pure structural cost. It does not contain verification signals or guardrail penalties. ## Scope The score covers all discovered Rust code: - production code; - test code; - benchmark code; - any future discovered Rust scope such as examples. All scopes contribute to the default total with equal weight. Tests and benches are code; if they are structurally complex, they increase the score. The harness is responsible for preventing agents from deleting, weakening, or bypassing those scopes. The output must still include a scope breakdown so users can understand where cost comes from: ```json "scope_breakdown": { "production": { "cost": 900.0 }, "tests": { "cost": 120.0 }, "benches": { "cost": 30.0 } } ``` Scope labels are explanatory. They do not change scoring weight in v0. ## Primary scalar ```text code_complexity_cost = function_complexity_cost + file_concentration_cost + module_coupling_cost + abstraction_surface_cost + api_surface_cost + stale_surface_cost ``` Also emit a normalized companion scalar: ```text code_complexity_cost_per_kloc = code_complexity_cost / max(1.0, code_lines / 1000.0) ``` Use `code_complexity_cost` for before/after comparisons inside the same repo. Use `code_complexity_cost_per_kloc` only for rough cross-repo comparisons. ## Component 1: function complexity Function complexity is the primary local reasoning-cost signal. For each discovered Rust function or method: ```text function_cost = 2.0 * max(0, cyclomatic - 5) + 3.0 * max(0, nesting_depth - 3) + 0.2 * max(0, line_count - 50) + 0.5 * max(0, body_stmt_count - 20) + 1.0 * max(0, signature_score - 6) ``` Where: ```text cyclomatic = branch_points + 1 signature_score = param_count + return_type_complexity + generic_param_count + trait_bound_count + where_predicate_count ``` Then: ```text function_complexity_cost = sum(function_cost) ``` Rationale: - Branching increases path reasoning. - Nesting increases local context stack depth. - Long functions increase scan burden, but line count has low weight to avoid rewarding code golf. - Body statement count adds a syntax-level size signal that is less sensitive to formatting than physical lines. - Signature complexity captures call-boundary and API reasoning cost. - Thresholds avoid penalizing ordinary small functions. ## Component 2: file concentration File concentration measures how much code and symbol surface is packed into one file. For each discovered Rust file: ```text file_cost = 0.04 * max(0, code_lines - 400) + 0.25 * max(0, total_symbols - 35) + 1.0 * max(0, function_count - 25) ``` Then: ```text file_concentration_cost = sum(file_cost) ``` Rationale: - Large files require more navigation and context loading. - Symbol piles make a file harder to summarize. - Function piles usually indicate several concepts sharing one module. - The thresholds keep normal files free and penalize concentration rather than raw existence of code. ## Component 3: module coupling Module coupling measures source-module navigation burden and architectural tangling. ```text module_coupling_cost = sum_per_module( 2.0 * max(0, out_degree - 5) + 1.0 * max(0, in_degree - 10) ) + sum_bidirectional_pairs(20.0 + 5.0 * pair_strength) + sum_low_cohesion_modules( if function_count >= 4: 2.0 * function_count * max(0.0, 0.55 - combined_cohesion) ) ``` Rationale: - Fan-out means a module must know about many neighbors, so it is weighted more heavily than fan-in. - Fan-in can indicate useful central code, so it receives a higher threshold and lower weight. - Bidirectional pairs are strong architecture smells and receive a large fixed penalty. - Low cohesion matters only when a module has enough functions for cohesion to be meaningful. For v0, use existing dependency, module-degree, coupling-pair, and cohesion signals. If full graph-cycle detection is added later, it should be a new field or a `score_version` bump. ## Component 4: abstraction surface Abstraction surface measures named conceptual inventory independent of line count. Per file or module, using symbol counts: ```text abstraction_surface_cost = 0.5 * max(0, structs - 12) + 0.5 * max(0, enums - 8) + 1.0 * max(0, traits - 4) + 0.5 * max(0, trait_impls - 8) + 0.2 * max(0, consts + statics - 10) ``` Then sum across files/modules. Rationale: - Named concepts are real reasoning surface even when individual functions are small. - Traits are weighted higher because they introduce abstraction and dispatch reasoning. - This component should not punish ordinary data modeling; it only penalizes excessive local surface. ## Component 5: API surface API surface measures interface area that callers outside the local module may depend on. ```text api_surface_cost = sum_visible_symbols(api_visibility_weight * api_kind_weight) + sum_visible_functions(api_visibility_weight * 0.5 * max(0, signature_score - 3)) ``` Where: ```text api_visibility_weight: pub = 1.0 pub(crate) = 0.4 api_kind_weight: function/method = 1.0 struct = 1.0 enum = 1.0 trait = 2.0 const/static = 0.5 ``` Then: ```text api_surface_cost = sum(api symbol costs) ``` Rationale: - `pub` creates repository-external compatibility surface. - `pub(crate)` creates crate-wide non-local reasoning surface, but is cheaper because it remains internally changeable. - Traits are weighted higher because they expose behavioral contracts and dispatch/implementation reasoning. - Visible functions receive a small signature surcharge so complex public call boundaries cost more than simple public call boundaries. - This component may encourage visibility reduction; public API compatibility checks and human review decide whether a reduction is allowed. For v1, only direct `pub` and `pub(crate)` item visibility are counted. Private items and narrower restricted visibilities such as `pub(super)` are not counted. ## Component 6: stale surface Stale surface measures code that appears unused by current static evidence. ```text stale_surface_cost = 0.5 * static_dead_code_candidate_count ``` Rationale: - Unused code still imposes reading, search, and maintenance cost. - The weight is low because static dead-code candidates can be false positives around macros, public API usage, trait-object dispatch, build scripts, and string-based dispatch. - The score may encourage deletion; the harness and human review decide whether deletion is allowed. ## JSON output contract The score command should emit compact JSON by default when `--json` is passed: ```json { "cstat_version": "0.1.0", "score_version": "code_complexity_cost_v1", "target": ".", "code_complexity_cost": 1234.5, "code_complexity_cost_per_kloc": 104.7, "component_costs": { "function_complexity": { "cost": 800.0, "functions_scored": 120 }, "file_concentration": { "cost": 120.0, "files_scored": 14 }, "module_coupling": { "cost": 180.0, "modules_scored": 14 }, "abstraction_surface": { "cost": 90.0, "symbols_scored": 300 }, "api_surface": { "cost": 20.0, "public_symbols": 12, "crate_symbols": 8 }, "stale_surface": { "cost": 44.5, "candidate_count": 89 } }, "scope_breakdown": { "production": { "cost": 900.0, "functions_scored": 100, "files_scored": 10 }, "tests": { "cost": 300.0, "functions_scored": 20, "files_scored": 4 }, "benches": { "cost": 34.5, "functions_scored": 2, "files_scored": 1 } }, "top_contributors": [ { "kind": "function", "scope": "production", "file": "src/main.rs", "function": "main", "cost": 115.4, "reasons": { "cyclomatic": 45, "nesting_depth": 4, "line_count": 212, "body_stmt_count": 11, "signature_score": 1 }, "component_costs": { "branching": 80.0, "nesting": 3.0, "span": 32.4, "signature": 0.0 } } ], "metadata": { "rust_files": 25, "code_lines": 11793, "parse_error_files": 0 } } ``` Required stable fields: - `cstat_version` - `score_version` - `target` - `code_complexity_cost` - `code_complexity_cost_per_kloc` - `component_costs` - `scope_breakdown` - `top_contributors` - `metadata` Field additions are allowed within the same score version if they do not change existing field meaning. Formula changes, field removals, or semantic changes must bump `score_version`. ## Human output contract Human output should be short and explanatory: ```text cstat code complexity score score version: code_complexity_cost_v1 lower is cleaner; harness guards behavior separately total cost: 1234.5 cost / KLOC: 104.7 components: function complexity: 800.0 file concentration: 120.0 module coupling: 180.0 abstraction surface: 90.0 api surface: 20.0 stale surface: 44.5 top contributors: 1. function src/main.rs::main 115.4 2. module src/flow 73.0 3. file src/coverage.rs 47.4 ``` Do not print every underlying probe. This command is a scorecard, not an `all` command. ## Implementation guide Recommended module: ```text src/scorecard.rs ``` Recommended CLI: ```text cstat scorecard --path [--json] [--top N] [-v] ``` Initial implementation should support project mode first. Selected-file mode can be added later if it naturally falls out of the data model. Implementation steps: 1. Parse/discover Rust files using existing project discovery. 2. Parse symbols with `ast_parser::parse_project`. 3. Build function rows using the same raw fields as `branching`, `signature`, and `span`. 4. Count line and symbol concentration per file using existing `loc` and `symbols` data where possible. 5. Compute dependency/coupling/cohesion data using existing `deps` logic. 6. Compute API surface from visible parsed symbols and function signatures. 7. Compute stale surface using existing `dead_code` analysis. 8. Classify each function/file into a scope label for reporting: - `production` - `tests` - `benches` - future labels as needed 9. Compute component costs. 10. Sum total cost. 11. Build top contributors from function, file, module, abstraction, API surface, and stale contributors. 12. Render compact human output or stable JSON. Avoid shelling out to existing CLI commands from the implementation. Reuse the same Rust collectors directly so the score is fast, deterministic, and testable. ## Scope classification guide Scope classification is only for explanation and breakdown in v0. It does not change scoring weight. Suggested rules: - `benches`: file under `benches/` or benchmark-recognized function where known; - `tests`: file under `tests/`, function with `#[test]`, or code inside obvious `#[cfg(test)]` modules where existing analysis can identify it; - `production`: default for discovered source that is not classified above. If classification is ambiguous, classify as `production` and avoid hiding cost. The score should remain deterministic. ## Top contributors Emit enough contributor detail for an agent to choose a bounded cleanup target. Contributor kinds: - `function` - `file` - `module` - `abstraction_surface` - `stale_surface` Each contributor should include: - `kind` - `scope` - `file` or `module` - `function` when applicable - `cost` - raw reason fields used to compute cost - per-reason component costs when applicable Default top count: 20. ## Known limitations - The weights are guessed, not learned. - Physical line count can be affected by formatting, so it has low weight and is paired with body statement count. - Static dead-code detection can be wrong. - Dependency extraction is source-level and can miss macro-generated edges. - Symbol counts come from syntax parsing, not full semantic analysis. - A lower score does not prove better architecture in every local case. - The harness must prevent destructive or behavior-changing optimizations. These limitations are acceptable for v0 because the score is deterministic, explainable, and calibrated enough to start collecting before/after cleanup data.