feat(health): attribute bug fixes to symbols and flag bug magnets#940
Conversation
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.
|
✅ Health: 7.5 (unchanged) 📋 At a glance Files & modules (2)
📌 Before you merge
🗺️ 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
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)
🩹 Review priority (files here with the most recent bug-fix history — defects cluster, so review these first)
🔎 More signals (3)🔥 Hotspots touched (5)
2 more
🔗 Hidden coupling (2 files)
💀 Dead code (1 finding)
📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-19 19:00 UTC |
…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.
fix_eventshas carried the old-side line ranges of every bug fix since #939, andwiki_symbolshas 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_edgeswas 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.
exactrequires 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 isapproximate, 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, thebug_magnetflag 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()carriesbug_magnet,last_fix_atand the top few symbol counts, which fans out toget_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.pyis pure and unit-testable without a DB, mirroringsignals.pyandtrends.py;pipeline/fix_rollups.pydoes the reads and writes at the end ofpersist_gitandpersist_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.pypicks up all six columns. Verified against a pre-changewiki.db— rows preserved,prior_defect_countuntouched, 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.
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.tsfiles 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_create6x,bulk_update5x,aiterator4x,get4x,in_bulk4x.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.pyandupdate_cmd/command.py— the same hotspot list the repo already knows about, arrived at from bug fixes rather than churn. Named symbols includerun_update4x,update_workspace3x,run_pipeline3x.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 throughgetattr, so aGitMetadatarow 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-checkclean.