Skip to content

feat(health): attribute bug fixes to symbols and flag bug magnets#940

Merged
RaghavChamadiya merged 1 commit into
mainfrom
feat/fix-event-symbol-rollups
Jul 20, 2026
Merged

feat(health): attribute bug fixes to symbols and flag bug magnets#940
RaghavChamadiya merged 1 commit into
mainfrom
feat/fix-event-symbol-rollups

Conversation

@RaghavChamadiya

Copy link
Copy Markdown
Member

fix_events has carried the old-side line ranges of every bug fix since #939, and wiki_symbols has carried where every symbol starts and ends. Nothing joined them, so the defect history stopped at file granularity — and nothing read the table at all.

What this adds

Symbol attribution. Each fix event's ranges are intersected with the file's symbol spans, so "this file was fixed 5 times" becomes "persist_graph_edges was fixed 5 times". A range cutting across a class and two of its methods attributes to the methods: any selected symbol containing another selected symbol is dropped, which keeps one fix from being counted at every level of nesting and stays language-agnostic.

Each row also stores how much to trust that join. exact requires that nothing has touched the file since the fix, because the spans are current-tree and the ranges come from the fix's own parent commit. Everything else is approximate, which on an active file is most rows. That is the truth rather than a defect — the alternative is re-parsing every historical revision — and it is why the surfaces are expected to hedge.

Per-file rollups on git_metadata: decayed fix mass, the bug_magnet flag it crosses, last_fix_at, and the symbol breakdown. Decay uses the 90-day half-life measured against the 21-repo calibration corpus, applied at read time over undecayed rows, so changing it re-runs a rollup rather than a reindex. The mass is stored next to the flag because a bare boolean cannot be argued with.

The magnet triggers at the mass of three fixes landed today. Read that with the decay in mind: three real fixes spread over a fortnight land near 2.9 and do not flag, so in practice it takes four recent ones. Deliberately the conservative end, since the flag exists to interrupt someone mid-edit.

file_signals() carries bug_magnet, last_fix_at and the top few symbol counts, which fans out to get_health, the REST breakdown, the dashboard panel and the VS Code hover in one change. None of them render it yet.

Shape

analysis/health/fix_attribution.py is pure and unit-testable without a DB, mirroring signals.py and trends.py; pipeline/fix_rollups.py does the reads and writes at the end of persist_git and persist_incremental_fix_events, failure-isolated like every other post-persist refresh. It recomputes the whole window each time rather than patching the files an update touched, because decay ages a rollup with nothing in the file changing.

No migration code: the additive reconciler in persistence/database.py picks up all six columns. Verified against a pre-change wiki.db — rows preserved, prior_defect_count untouched, rollups at their defaults until the next update.

Numbers

Measured with the shipped tracer walked live on django, flask, zod and this repo, joined against each one's existing index.

repo events attribution rate bug magnets
django 2,179 97.4% 0
flask 125 96.3% 0
zod 337 81.8% 8
repowise 1,145 96.8% 32

The denominator counts events whose lines land somewhere between the file's first and last symbol. The looser "file has any symbol at all" version reads 67-85%, and the gap is two measured effects rather than a broken join: on django and flask it is line drift over years (flask by event age: 100% / 100% / 88.9% / 74.6% across 0-180d, 180d-1y, 1-3y, 3y+), and on zod it is *.test.ts files the parser indexes with one or two named symbols, where a fix at line 700 has provably nothing to land in.

Spot-check on the file most worth checking, django's db/models/query.py: bulk_create 6x, bulk_update 5x, aiterator 4x, get 4x, in_bulk 4x.

On this repo the top magnets are pipeline/persist.py (mass 12.0, 13 fixes), pipeline/incremental.py, dead_code/analyzer.py, pipeline/orchestrator.py, workspace/update.py and update_cmd/command.py — the same hotspot list the repo already knows about, arrived at from bug fixes rather than churn. Named symbols include run_update 4x, update_workspace 3x, run_pipeline 3x.

The rollup pass costs 0.248s over 1,172 events and 739 files.

Testing

New unit coverage for the attribution rules, the decay curve and the magnet threshold, plus DB round-trips asserting that only production-code fixes reach the count (the same filter #931 put on prior_defect_count), that decay is anchored to the repo's newest commit rather than wall-clock time so two indexes of one checkout agree, and that re-running the pass is a no-op.

file_signals() reads the new columns through getattr, so a GitMetadata row or stub predating them returns every field callers already relied on — covered by its own test. Full python suite and all four JS suites green, npm run type-check clean.

fix_events has been carrying the old-side line ranges of every bug fix
since #939, and wiki_symbols has been carrying where every symbol starts
and ends. Nothing joined them, so the defect history stopped at file
granularity and nothing read the table at all.

Intersecting the two turns "this file was fixed 5 times" into
"persist_graph_edges was fixed 5 times", which is the granularity someone
about to edit that function can act on. Each event stores the symbols its
ranges landed in, plus an attribution field saying how much to trust the
join: exact only when nothing has touched the file since the fix, because
symbol spans are current-tree and the ranges are from the fix's own parent.

The per-file rollup lands on git_metadata: decayed fix mass, the bug_magnet
flag it crosses, last_fix_at, and the symbol breakdown. Decay uses the 90-day
half-life G1.4 measured, applied at read time from undecayed rows, so
changing it re-runs a rollup rather than a reindex. The mass is stored beside
the flag because a bare boolean cannot be argued with.

file_signals() carries all three fields, so get_health, the REST breakdown,
the dashboard panel and the VS Code hover all have the data. None of them
render it yet.

Measured on django, flask, zod and this repo: 82 to 97% of fix events whose
lines land where the file has symbols attribute to at least one, and the
symbols named in django's db/models/query.py are bulk_create, bulk_update,
get and in_bulk.
@repowise-bot

repowise-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

✅ Health: 7.5 (unchanged)

📋 At a glance
2 files changed health · 5 hotspots touched · 6 new findings introduced · 3 co-change pairs left out · 4 files with recent fix history · 1 dead-code finding.

Files & modules (2)
  • packages (6 files)
    • .../health/signals.py
    • .../health/file-signals-panel.test.tsx
    • .../pipeline/incremental.py
    • .../persistence/models.py
    • .../pipeline/persist.py
    • .../src/health.ts
  • tests (2 files)
    • .../health/test_signals.py
    • .../server/test_health_badge.py

📌 Before you merge

  • Run .../persistence/test_embed_batch_pg.py, tests/integration/test_mcp.py, .../analysis/test_security_scan.py, .../persistence/test_incremental_external_systems.py (+7 more): they depend on the changed files
  • .../persistence/test_models.py changed together with .../persistence/models.py in 14 past commits and isn't in this PR
  • .../pipeline/orchestrator.py changed together with .../pipeline/persist.py in 11 past commits and isn't in this PR

🗺️ Change map

flowchart LR
  subgraph PR ["Changed in this PR (7 with dependents)"]
    f_packages_core_src_repowise_core_analysis_health_signals_py[".../health/signals.py 🔥"]:::changed
    f_packages_core_src_repowise_core_pipeline_incremental_py[".../pipeline/incremental.py 🔥"]:::changed
    f_packages_core_src_repowise_core_persistence_models_py[".../persistence/models.py"]:::changed
    f_packages_core_src_repowise_core_pipeline_persist_py[".../pipeline/persist.py"]:::changed
    f_packages_server_src_repowise_server_routers_code_health_serializers_py[".../code_health/serializers.py"]:::changed
    f_packages_types_src_health_ts[".../src/health.ts"]:::changed
    f_tests_unit_server_test_health_badge_py[".../server/test_health_badge.py"]:::changed
  end
  f_packages_server_src_repowise_server_mcp_server_tool_context_enrichment_py[".../tool_context/enrichment.py"]
  f_packages_core_src_repowise_core_analysis_health_signals_py --> f_packages_server_src_repowise_server_mcp_server_tool_context_enrichment_py
  f_packages_server_src_repowise_server_mcp_server_tool_health_py[".../mcp_server/tool_health.py"]
  f_packages_core_src_repowise_core_analysis_health_signals_py --> f_packages_server_src_repowise_server_mcp_server_tool_health_py
  f_packages_server_src_repowise_server_routers_code_health_loaders_py[".../code_health/loaders.py"]
  f_packages_core_src_repowise_core_analysis_health_signals_py --> f_packages_server_src_repowise_server_routers_code_health_loaders_py
  f_packages_server_src_repowise_server_routers_files_py[".../routers/files.py"]
  f_packages_core_src_repowise_core_analysis_health_signals_py --> f_packages_server_src_repowise_server_routers_files_py
  f_packages_cli_src_repowise_cli_commands_update_cmd_incremental_py[".../update_cmd/incremental.py"]
  f_packages_core_src_repowise_core_pipeline_incremental_py --> f_packages_cli_src_repowise_cli_commands_update_cmd_incremental_py
  f_packages_cli_src_repowise_cli_commands_update_cmd_persistence_py[".../update_cmd/persistence.py"]
  f_packages_core_src_repowise_core_pipeline_incremental_py --> f_packages_cli_src_repowise_cli_commands_update_cmd_persistence_py
  f_packages_core_src_repowise_core_ingestion_git_indexer_indexer_py[".../git_indexer/indexer.py"]
  f_packages_core_src_repowise_core_pipeline_incremental_py --> f_packages_core_src_repowise_core_ingestion_git_indexer_indexer_py
  f_packages_core_src_repowise_core_workspace_update_py[".../workspace/update.py"]
  f_packages_core_src_repowise_core_pipeline_incremental_py --> f_packages_core_src_repowise_core_workspace_update_py
  f_packages_cli_src_repowise_cli_commands_decision_cmd_py[".../commands/decision_cmd.py"]
  f_packages_core_src_repowise_core_persistence_models_py --> f_packages_cli_src_repowise_cli_commands_decision_cmd_py
  f_packages_core_src_repowise_core_pipeline_persist_py --> f_packages_cli_src_repowise_cli_commands_update_cmd_persistence_py
  f_packages_types_src_health_ts --> f_packages_server_src_repowise_server_mcp_server_tool_health_py
  more(["+153 more dependents"])
  PR --> more
  w_tests_unit_persistence_test_models_py(["⚠️ .../persistence/test_models.py changed together 14×, not in PR"]):::warn
  f_packages_core_src_repowise_core_persistence_models_py -.- w_tests_unit_persistence_test_models_py
  w_packages_core_src_repowise_core_pipeline_orchestrator_py(["⚠️ .../pipeline/orchestrator.py changed together 11×, not in PR"]):::warn
  f_packages_core_src_repowise_core_pipeline_persist_py -.- w_packages_core_src_repowise_core_pipeline_orchestrator_py
  t_tests_unit_persistence_test_incremental_external_systems_py(["✅ .../persistence/test_incremental_external_systems.py"]):::guard
  t_tests_unit_persistence_test_incremental_external_systems_py -.-> f_packages_core_src_repowise_core_pipeline_incremental_py
  t_tests_integration_persistence_test_embed_batch_pg_py(["✅ .../persistence/test_embed_batch_pg.py"]):::guard
  t_tests_integration_persistence_test_embed_batch_pg_py -.-> f_packages_core_src_repowise_core_persistence_models_py
  t_tests_unit_analysis_test_security_scan_py(["✅ .../analysis/test_security_scan.py"]):::guard
  t_tests_unit_analysis_test_security_scan_py -.-> f_packages_core_src_repowise_core_pipeline_persist_py
  classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
  classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
  classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Loading

Solid arrows: code that imports the changed files (162 direct dependents, from the last indexed snapshot). Dashed: history/tests.

🚨 Change risk: high (riskier than 73% of this repo's commits)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline

🩹 Review priority (files here with the most recent bug-fix history — defects cluster, so review these first)

File Score Δ Why
.../health/signals.py 8.8 → 8.2 ▼ -0.6 score drifted on re-parse — no PR-attributable biomarkers
.../health/file-signals-panel.test.tsx 9.7 → 9.5 ▼ -0.2 score drifted on re-parse — no PR-attributable biomarkers
🔎 More signals (3)

🔥 Hotspots touched (5)

  • .../health/test_signals.py — 1 commits/90d, 0 dependents
  • .../server/test_health_badge.py — 4 commits/90d, 2 dependents
  • .../pipeline/incremental.py — 20 commits/90d, 8 dependents
2 more
  • .../health/file-signals-panel.test.tsx — 1 commits/90d, 0 dependents
  • .../health/signals.py — 1 commits/90d, 7 dependents

🔗 Hidden coupling (2 files)

  • .../persistence/models.py co-changes with .../persistence/test_models.py (14× — 🟡 notable) — not in this PR.
  • .../pipeline/persist.py co-changes with these files (not in this PR):
    • .../pipeline/orchestrator.py (11× — 🟡 notable)
    • .../persistence/test_models.py (10× — 🟡 notable)

💀 Dead code (1 finding)

  • 💀 .../pipeline/incremental.py _noop_log (confidence 0.65)

📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-19 19:00 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@RaghavChamadiya
RaghavChamadiya merged commit ad0a32f into main Jul 20, 2026
7 checks passed
@RaghavChamadiya
RaghavChamadiya deleted the feat/fix-event-symbol-rollups branch July 20, 2026 03:39
RaghavChamadiya added a commit that referenced this pull request Jul 20, 2026
…hover (#948)

The fix-event rollup has been computed and stored since #940 but nothing
rendered it. Three surfaces now do:

- FileSignalsPanel's bug-fix tile gains a "Bug magnet" badge and folds the
  last-fix age into its caption.
- The health file drawer gains a collapsed "Bug history" section listing which
  symbols the fixes landed in, with the last-fix age on the toggle so the
  "is this still happening?" question is answered without expanding.
- The VS Code file hover gains one line off the same contract, no new API.

The magnet badge never renders without the last-fix age beside it. It is a
claim about recent fix pressure, so unanchored it would describe a file fixed
four times last month and one fixed four times two years ago identically. All
three surfaces drop the flag rather than show it bare, and the per-symbol
counts are labelled approximate because fixes are matched to symbols by line
range and lines move.

Also fixes a real serialization bug behind all of this. SQLite's DATETIME bind
processor drops tzinfo, so last_fix_at read back naive and isoformat() produced
a string with no offset. JS parses that as local time, so west of UTC a fix
committed two hours ago landed in the future, tripped formatRelativeTimeOrNull's
future-guard, and the age silently vanished from copy required to carry it,
worst for the freshest fixes. Fixed once in signals.iso_utc rather than in each
consumer.
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.

2 participants