Skip to content

fresh

last synced 2026-06-17T22:39:35.514075+00:00 · coverage 100% (debt-report)

Validation by Beadloom doc_sync — same source as sync-check.

Debt Report

Architecture debt aggregation, scoring, and trend tracking.

Source: src/beadloom/application/debt_report/ (package — models, config, collect, scoring, trend, render; __init__.py re-exports the stable public surface). Decomposed by cohesion in BDL-059 S4; import paths unchanged.

Specification

Purpose

The debt report module aggregates all architecture health signals (lint violations, documentation gaps, complexity smells, test coverage gaps) into a single quantified debt score (0-100) with category breakdown, per-node top offenders, and trend tracking against graph snapshots. It provides a unified answer to "how healthy is our architecture?" and supports CI gating via the --fail-if flag on the CLI.

Debt Score Formula

debt_score = min(100, sum(category_scores))

category_scores:
  rule_violations = (error_count * rule_error) + (warning_count * rule_warning)
  doc_gaps        = (undocumented * undocumented_node) + (stale * stale_doc)
                  + (untracked * untracked_file)
  complexity      = (oversized * oversized_domain) + (high_fan_out * high_fan_out)
                  + (dormant * dormant_domain)
  test_gaps       = (untested * untested_domain)

Default weights (configurable via config.yml debt_report section):

WeightDefaultDescription
rule_error3.0Per lint error
rule_warning1.0Per lint warning
undocumented_node2.0Per node without docs
stale_doc1.0Per stale doc-code pair
untracked_file0.5Per untracked source file
oversized_domain2.0Per oversized domain
high_fan_out1.0Per high fan-out node
dormant_domain0.5Per dormant domain
untested_domain1.0Per untested domain

Default thresholds:

ThresholdDefaultDescription
oversized_symbols200Symbol count above which a domain is oversized
high_fan_out10Edge count above which a node has high fan-out
dormant_months3Months without git activity for dormant classification

Severity Classification

Score RangeSeverityIndicator
0cleancheck mark (green)
1-10lowfilled circle (yellow)
11-25mediumtriangle (yellow)
26-50highdiamond (red)
51-100criticalX mark (red bold)

Data Structures

DebtWeights (frozen dataclass)

Per-item weights and thresholds for debt score computation.

FieldTypeDefaultDescription
rule_errorfloat3.0Weight for lint errors
rule_warningfloat1.0Weight for lint warnings
undocumented_nodefloat2.0Weight for undocumented nodes
stale_docfloat1.0Weight for stale docs
untracked_filefloat0.5Weight for untracked files
oversized_domainfloat2.0Weight for oversized domains
high_fan_outfloat1.0Weight for high fan-out nodes
dormant_domainfloat0.5Weight for dormant domains
untested_domainfloat1.0Weight for untested domains
oversized_symbolsint200Oversized threshold
high_fan_out_thresholdint10Fan-out threshold
dormant_monthsint3Dormant threshold (months)

DebtData (frozen dataclass)

Raw counts aggregated from all data sources.

FieldTypeDescription
error_countintLint rule errors
warning_countintLint rule warnings
undocumented_countintNodes without docs
stale_countintStale sync pairs
untracked_countintUntracked source files
oversized_countintOversized domains
high_fan_out_countintHigh fan-out nodes
dormant_countintDormant domains
untested_countintUntested domains
node_issuesdict[str, list[str]]Per-node issue tracking for top offenders

CategoryScore (frozen dataclass)

FieldTypeDescription
namestrCategory: rule_violations, doc_gaps, complexity, test_gaps
scorefloatWeighted score for this category
detailsdict[str, int | float]Per-item breakdown (e.g. {"errors": 2, "warnings": 1})

NodeDebt (frozen dataclass)

FieldTypeDescription
ref_idstrGraph node reference ID
scorefloatDebt contribution for this node
reasonslist[str]Issue reasons (e.g. ["undocumented", "stale_doc"])

DebtTrend (frozen dataclass)

FieldTypeDescription
previous_snapshotstrDisplay string for the previous snapshot (ISO date + optional label)
previous_scorefloatDebt score from previous snapshot
deltafloatChange in overall debt score
category_deltasdict[str, float]Per-category score changes

DebtReport (frozen dataclass)

FieldTypeDescription
debt_scorefloatOverall debt score, 0-100
severitystrSeverity label: clean/low/medium/high/critical
categorieslist[CategoryScore]Four category scores
top_offenderslist[NodeDebt]Top 10 nodes ranked by debt contribution
trendDebtTrend | NoneTrend vs last snapshot, or None

Data Collection Sources

CategorySourceModule
Rule violationsevaluate_all(conn, rules)graph/rules/ (re-exported via the graph.rule_engine shim)
Doc gaps -- undocumentedNodes without docs (LEFT JOIN)application/debt_report/collect.py
Doc gaps -- stalesync_state entries with status='stale'application/debt_report/collect.py
Doc gaps -- untrackedNodes with source but no sync_stateapplication/debt_report/collect.py
Complexity -- oversizedSymbol count per node vs thresholdapplication/debt_report/collect.py
Complexity -- fan-outEdge count per node vs thresholdapplication/debt_report/collect.py
Complexity -- dormantanalyze_git_activity() with dormant levelinfrastructure/git_activity.py
Test gapsmap_tests() with coverage_estimate=nonecontext_oracle/test_mapper.py

Top Offenders

compute_top_offenders() ranks individual graph nodes by their weighted debt contribution. Each node's score is computed from its issue list in DebtData.node_issues:

  • "violation:error:<rule>" -- weighted by rule_error
  • "violation:warning:<rule>" -- weighted by rule_warning
  • Issue keywords (undocumented, stale_doc, oversized, high_fan_out, dormant, untested) -- weighted via _ISSUE_WEIGHT_MAP

Nodes are sorted by descending score (ties broken alphabetically by ref_id). Default limit: 10 nodes.

Trend Tracking

compute_debt_trend() compares the current debt report against the most recent graph snapshot. Snapshot data contains structural information (nodes, edges, symbol count) but does not include dynamic data (rules, docs, tests). Therefore:

  • The complexity category is recomputed from snapshot edges (high fan-out).
  • The rule_violations, doc_gaps, and test_gaps categories are set to 0 for the snapshot (not computable from snapshot data).

Trend output shows per-category directional arrows: improved (down arrow), regressed (up arrow), or unchanged (equals sign).

Config Loading

load_debt_weights() reads the debt_report section from config.yml at the project root. The section has two subsections: weights (per-item multipliers) and thresholds. Missing keys fall back to defaults. Missing file or invalid YAML also falls back to defaults.

Example config.yml:

yaml
debt_report:
  weights:
    rule_error: 3
    rule_warning: 1
    undocumented_node: 2
    stale_doc: 1
  thresholds:
    oversized_symbols: 200
    high_fan_out: 10
    dormant_months: 3

Category Short Names

The --category CLI flag and MCP category argument accept short names mapped to internal names:

Short NameInternal Name
rulesrule_violations
docsdoc_gaps
complexitycomplexity
teststest_gaps

CLI Interface

beadloom status --debt-report [--json] [--fail-if=EXPR] [--category=NAME] [--project DIR]
  • --debt-report: Show debt report instead of standard status.
  • --json: Output as structured JSON.
  • --fail-if=EXPR: CI gate. Expressions: score>N, errors>N. Exits with code 1 if condition is met.
  • --category=NAME: Filter to one category.

MCP Interface

Tool: get_debt_report

Arguments:

  • trend (bool, default false): Include trend vs last snapshot.
  • category (string, optional): Filter to a specific category.

Returns JSON with: debt_score, severity, categories, top_offenders, trend.

Output Formats

Rich (human-readable):

  • Header panel: "Architecture Debt Report"
  • Score line with severity indicator and label
  • Category breakdown with per-item detail lines (tree-style prefixes)
  • Top offenders table (rank, node, score, reasons)

JSON (machine-readable):

  • debt_score: float
  • severity: string
  • categories: list of {name, score, details}
  • top_offenders: list of {ref_id, score, reasons}
  • trend: null or {previous_snapshot, previous_score, delta, category_deltas}

API

Public Functions

python
def load_debt_weights(project_root: Path) -> DebtWeights

Load debt weights from config.yml debt_report section, falling back to defaults.

python
def collect_debt_data(
    conn: sqlite3.Connection,
    project_root: Path,
    weights: DebtWeights | None = None,
) -> DebtData

Aggregate raw counts from all data sources (lint, sync, doctor, git activity, test mapper).

python
def compute_debt_score(
    data: DebtData,
    weights: DebtWeights | None = None,
) -> DebtReport

Apply the weighted formula to produce a complete debt report. Caps score at 100.

python
def compute_top_offenders(
    data: DebtData,
    weights: DebtWeights,
    limit: int = 10,
) -> list[NodeDebt]

Rank nodes by their debt contribution and return the top N.

python
def compute_debt_trend(
    conn: sqlite3.Connection,
    current_report: DebtReport,
    project_root: Path,
    weights: DebtWeights | None = None,
) -> DebtTrend | None

Compare current debt against the last snapshot. Returns None if no snapshot exists.

python
def format_debt_report(report: DebtReport) -> str

Render a debt report as Rich-formatted terminal output.

python
def format_trend_section(trend: DebtTrend | None) -> str

Render trend data as plain text with directional arrows.

python
def format_debt_json(
    report: DebtReport,
    category: str | None = None,
) -> dict[str, Any]

Serialize a debt report to a JSON-safe dict with optional category filter.

python
def format_top_offenders_json(
    offenders: list[NodeDebt],
) -> list[dict[str, object]]

Serialize a list of NodeDebt to JSON-safe dicts.

Public Classes

python
@dataclass(frozen=True)
class DebtWeights: ...

@dataclass(frozen=True)
class DebtData: ...

@dataclass(frozen=True)
class CategoryScore: ...

@dataclass(frozen=True)
class NodeDebt: ...

@dataclass(frozen=True)
class DebtTrend: ...

@dataclass(frozen=True)
class DebtReport: ...

Invariants

  • The debt score is always clamped to the range [0, 100].
  • All four categories (rule_violations, doc_gaps, complexity, test_gaps) are always present in DebtReport.categories, even when their score is 0.
  • compute_debt_score never modifies the database. All data collection is read-only.
  • load_debt_weights always returns a valid DebtWeights instance, even with missing or malformed config files.
  • Top offenders are sorted by descending score with alphabetical ref_id tiebreaking for deterministic output.
  • Trend computation is based on structural snapshot data only; categories not stored in snapshots (rules, docs, tests) have 0 as the previous value.
  • Each private data collection helper (_count_undocumented, _count_stale, etc.) gracefully handles import failures or missing tables, returning zero counts.

Constraints

  • Requires a populated SQLite database. Running the debt report before beadloom reindex will produce a zero score (no data to aggregate).
  • Trend tracking depends on at least one graph snapshot existing (created by beadloom snapshot save or automatically during reindex).
  • Trend comparisons for non-structural categories (rules, docs, tests) always show zero for the previous snapshot since these are not captured in snapshot data.
  • The --fail-if CI gate only supports two expressions: score>N and errors>N. Other expressions produce an error.
  • Weight configuration lives in config.yml under the debt_report key. The module reads config.yml from the project root, not from .beadloom/.

Testing

Test file: tests/test_debt_report.py

Tests should cover the following scenarios:

  • Zero debt: Verify that an empty/healthy graph produces a debt score of 0 with severity clean.
  • Severity boundaries: Verify correct severity labels at boundaries (0, 1, 10, 11, 25, 26, 50, 51).
  • Category scoring: Verify each category independently contributes the correct weighted score.
  • Score capping: Verify that extreme values are capped at 100.
  • Config loading: Verify loading weights from config.yml, including missing file, missing section, and partial overrides.
  • Top offenders: Verify ranking by score, tiebreaking by ref_id, and limit enforcement.
  • Per-node issue tracking: Verify that violation:error:*, violation:warning:*, and issue keywords are correctly weighted.
  • Trend computation: Verify delta calculation when a snapshot exists, and None return when no snapshot exists.
  • Format JSON: Verify JSON serialization with and without category filter.
  • Format Rich: Verify that Rich output contains expected sections (header, score, categories, offenders).
  • Category short names: Verify that short names (rules, docs, tests) map correctly to internal names.