/intel/blast-risk — pre-release risk dashboard
Overview
GET /{owner}/{repo_slug}/intel/blast-risk surfaces every symbol whose combination of blast radius, churn velocity, test coverage gap, and structural coupling makes it the highest-risk thing to touch before a release. This is not a "things to delete" page — it is a "things to be careful about" page. Where the gravity page tells you what has the most downstream reach, blast-risk tells you which of those things is also poorly tested, frequently changed, and tightly coupled to other volatile symbols. The intersection is where accidents happen.
Each row renders a composite risk score alongside four normalized sub-scores (impact, churn, test-gap, coupling) as a mini radar so you can see at a glance why a symbol is risky. A critical filter bar lets you scope by risk tier (critical / high / medium / low) and symbol kind. A per-symbol detail view (Phase 4) breaks each sub-score down to its raw inputs so an engineer can make an informed decision before shipping.
The data source is musehub_intel_blast_risk, already populated by BlastRiskProvider on every code push. The route and template are currently stubs. This issue replaces them with the full implementation.
Full Page Layout
┌──────────────────────────────────────────────────────────────────────┐
│ .intel-wrap (bg-base, edge-to-edge) │
│ │
│ ┌── .intel-page-header (bg-surface, border-bottom: border-default) ─┐
│ │ ← Intel Hub [gradient-spectral text] BLAST RISK │
│ │ Symbols most likely to cause regressions before your next │
│ │ release — ranked by composite risk score. │
│ └──────────────────────────────────────────────────────────────────┘│
│ │
│ ┌── .br-stats-row (4 stat cards, bg-elevated, border-default) ─────┐
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ 247 │ │ 12 │ │ 38 │ │ 4.2x │ │
│ │ │ TOTAL │ │ CRITICAL │ │ HIGH │ │ AVG RISK │ │
│ │ │ text-mut │ │ col-dang │ │ col-warn │ │ text-mut │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ └────────────────────────────────────────────────────────────────── ┘
│ │
│ ┌── .br-filter-bar (bg-surface, border-default) ───────────────────┐
│ │ Risk: [ALL] [CRITICAL] [HIGH] [MEDIUM] [LOW] │
│ │ Kind: [ALL] [function] [class] [method] [async_function] │
│ └────────────────────────────────────────────────────────────────── ┘
│ │
│ ┌── .br-table (bg-surface, border-default, radius-md) ──────────────┐
│ │ .br-table-header (border-subtle, text-muted, text-xs uppercase) │
│ │ # │ SYMBOL │ KIND │ RISK │ SCORE │ ████ IMPACT│CHURN│GAP│COUP │
│ │ ─────────────────────────────── border-subtle ───────────────── │
│ │ .br-row (bg-surface → bg-hover on hover) │
│ │ 1 musehub/auth.py::validate_token [badge: method] │
│ │ [badge badge-danger: CRITICAL] score: 94 │
│ │ [████████░░] [████░░░░░░] [██████░░░░] [█████░░░░░] │
│ │ impact:87 churn:52 gap:71 coupling:63 │
│ │ [→ detail] │
│ │ .br-row │
│ │ 2 musehub/storage/backends.py::S3Backend.put [badge: method] │
│ │ [badge badge-danger: CRITICAL] score: 91 │
│ │ [█████████░] [███████░░░] [████████░░] [███░░░░░░░] │
│ └────────────────────────────────────────────────────────────────── ┘
│ │
│ [ empty state: icon(activity,48) + text-muted — 0 rows ] │
└──────────────────────────────────────────────────────────────────────┘
Per-symbol detail panel (Phase 4)
┌── .br-detail (bg-surface, border-default, radius-md) ────────────────┐
│ ← Back to Blast Risk │
│ │
│ musehub/auth.py::validate_token [badge: async_method] │
│ [badge badge-danger: CRITICAL] composite score: 94 / 100 │
│ │
│ ┌── Sub-scores ──────────────────────────────────────────────────── ┐│
│ │ IMPACT [████████████████████░░░░░░] 87 blast=34 deps ││
│ │ CHURN [████████░░░░░░░░░░░░░░░░░░] 52 14 commits/30d ││
│ │ TEST GAP [██████████████░░░░░░░░░░░░] 71 0 test coverage ││
│ │ COUPLING [████████████░░░░░░░░░░░░░░] 63 8 entangled files ││
│ └────────────────────────────────────────────────────────────────── ┘│
│ │
│ ┌── blast_top (direct dependents) ──────────────────────────────── ┐│
│ │ musehub/api/routes/musehub/ui_auth.py::login_page ││
│ │ musehub/api/routes/musehub/ui_auth.py::logout_page ││
│ │ musehub/services/musehub_session.py::create_session ││
│ └────────────────────────────────────────────────────────────────── ┘│
└───────────────────────────────────────────────────────────────────────┘
Spectral Token Map
| Element | Token | Class / Usage |
|---|---|---|
| Page background | --bg-base |
.intel-wrap |
| Page header | --bg-surface + --border-default |
.intel-page-header |
| Page title | --gradient-spectral text fill |
BLAST RISK heading |
| Stat cards | --bg-elevated + --border-default |
.br-stat-card |
| Critical count | --color-danger |
stat value + badge |
| High count | --color-warning |
stat value + badge |
| Medium count | --color-orange |
stat value + badge |
| Low count | --text-muted |
stat value + badge |
| Filter pills | pill pill--active / pill |
risk and kind filter buttons |
| Table header | --border-subtle + --text-muted + --text-xs |
.br-table-header |
| Row hover | --bg-hover |
.br-row:hover |
| Risk badge critical | badge badge-danger |
per-row risk tier |
| Risk badge high | badge badge-warning |
per-row risk tier |
| Risk badge medium | badge badge-accent |
per-row risk tier |
| Risk badge low | badge (default) |
per-row risk tier |
| Kind badge function | badge badge-accent |
symbol kind |
| Kind badge class | badge badge-agent (purple) |
symbol kind |
| Kind badge method | badge (default) |
symbol kind |
| Sub-score bar track | --bg-elevated |
.br-score-track |
| Sub-score bar fill critical | --color-danger |
.br-score-fill--critical |
| Sub-score bar fill high | --color-warning |
.br-score-fill--high |
| Sub-score bar fill medium | --color-orange |
.br-score-fill--medium |
| Sub-score bar fill low | --color-success |
.br-score-fill--low |
| Symbol address | --font-mono + --color-accent-link |
.br-address |
| File path meta | --text-sm + --text-muted |
.br-meta |
| Detail link | btn btn-ghost btn-sm + icon(arrow-right, 14) |
per-row → detail |
| Empty state icon | icon(activity, 48) |
zero-results panel |
| Back link | --color-accent-link |
← Intel Hub |
| Score number | --font-mono + --text-primary |
.br-score-value |
| Gradient surface cards | --gradient-surface + --border-strong |
detail sub-score cards |
SCSS Architecture
Two new files, two-layer split — no selector appears in both:
src/scss/
components/_blast_risk.scss ← color · border · font · badge · hover · transition
pages/_blast_risk.scss ← display · flex · grid · padding · gap · width · height
Both wired into app.scss under their respective layers.
Data Model
MusehubIntelBlastRisk (already exists — no migration needed):
repo_id: str — FK to musehub_repos (CASCADE)
address: str — symbol address (PK with repo_id)
kind: str — function | method | class | async_function | …
risk: str — "critical" | "high" | "medium" | "low"
risk_score: int — composite 0–100
impact_score: float — normalized blast radius contribution
churn_score: float — normalized churn velocity contribution
test_gap_score: float — normalized test coverage gap contribution
coupling_score: float — normalized structural coupling contribution
ref: str — commit ref that produced this row
musehub_symbol_intel is the companion table for raw inputs:
blast,blast_direct,blast_cross,blast_top→ feedsimpact_scorechurn,churn_30d→ feedschurn_scoregravity_pct→ contextual weight
No new columns. No migration needed for Phases 1–4.
Phases (load-bearing order)
Phase 1 — SQL-derived BlastRiskProvider + comprehensive tests
Why first: every other phase reads from musehub_intel_blast_risk. The current provider calls muse code blast-risk via subprocess — the same pattern we already replaced in DeadProvider. Replace it with a pure SQL derivation from musehub_symbol_intel so it works in every environment without a local muse repo.
Rewrite BlastRiskProvider in musehub/services/musehub_intel_providers.py:
SQL derivation formula:
impact_score = LEAST(blast / 50.0, 1.0) — normalized blast radius (cap at 50)
churn_score = LEAST(churn_30d / 20.0, 1.0) — normalized 30-day churn (cap at 20)
test_gap_score = 1.0 - COALESCE(test_coverage, 0) — inverted coverage (no coverage data yet → 1.0)
coupling_score = LEAST(blast_cross / 10.0, 1.0) — normalized cross-domain blast (cap at 10)
risk_score = round(
impact_score * 40 + — impact is the heaviest weight
churn_score * 25 +
test_gap_score * 20 +
coupling_score * 15
)
risk tier:
critical → risk_score >= 75
high → risk_score >= 50
medium → risk_score >= 25
low → risk_score < 25
tracked_kinds = {function, async_function, method, async_method, class}
Only symbols with blast > 0 are candidates (zero blast = no impact, no risk).
TDD layers (tests/test_phase1_blast_risk_provider.py):
Unit tests (no DB):
P1_01 "intel.code.blast_risk" in _PROVIDER_REGISTRY
P1_02 BlastRiskProvider satisfies IntelProvider protocol
P1_03 job_types_for_push("code") includes "intel.code.blast_risk"
P1_04 job_types_for_push("midi") excludes "intel.code.blast_risk"
P1_05 _risk_tier(75) == "critical"
P1_06 _risk_tier(50) == "high"
P1_07 _risk_tier(25) == "medium"
P1_08 _risk_tier(24) == "low"
P1_09 _compute_risk_score() with all-max inputs → 100
P1_10 _compute_risk_score() with all-zero inputs → 0
P1_11 _compute_risk_score() impact weight is 40, churn 25, gap 20, coupling 15
Integration tests (DB):
P1_12 high blast, high churn → critical tier
P1_13 blast=0 → symbol excluded (no zero-blast symbols)
P1_14 untracked kind "import" → excluded
P1_15 risk_score capped at 100 (overflow inputs)
P1_16 idempotent — run twice, one row per address
P1_17 returns [("intel.code.blast_risk", {"count": N})]
P1_18 empty repo → returns []
P1_19 no subprocess spawned (asyncio.create_subprocess_exec never called)
State integrity:
P1_20 re-run updates risk_score in-place (upsert, not duplicate)
P1_21 upsert does not touch symbol_intel blast/churn columns
P1_22 ref column updated to latest push ref on each run
Performance:
P1_23 1000 symbol rows processed in < 5 seconds
Docstrings required on:
BlastRiskProviderclass — what it does, data source, formula summary, output shapeBlastRiskProvider.compute— parameters, return value, side effects, SQL derivation notes- Module-level
_BLAST_RISK_TRACKED_KINDS,_BLAST_RISK_WEIGHTS,_risk_tier
Phase 2 — Route + empty state + comprehensive tests
Why second: route must exist before template work.
Rewrite intel_blast_risk_page in musehub/api/routes/musehub/ui_intel.py:
Query params:
risk(optional) — filter to tier:critical|high|medium|lowkind(optional) — filter to symbol kindtop(optional, default 100, valid: 50 | 100 | 250) — limit
Handler queries musehub_intel_blast_risk ordered by risk_score DESC then address ASC.
Context keys passed to template:
{
"rows": list[MusehubIntelBlastRisk], # filtered + sorted
"total_count": int, # all rows regardless of filter
"critical_count": int,
"high_count": int,
"avg_risk_score": float,
"selected_risk": str | None,
"selected_kind": str | None,
"selected_top": int,
"valid_risks": ["critical", "high", "medium", "low"],
"valid_kinds": sorted(_BLAST_RISK_TRACKED_KINDS),
"valid_tops": [50, 100, 250],
}
TDD layers (tests/test_phase2_blast_risk_route.py):
Route registration:
P2_01 route "intel/blast-risk" registered in ui_intel router
P2_02 GET /{owner}/{repo_slug}/intel/blast-risk → 200
P2_03 unknown repo → 404
P2_04 Content-Type is text/html
Empty state:
P2_05 zero rows → 200 with empty-state text ("no blast risk" | "clean" | "no candidates")
Filter:
P2_06 ?risk=critical filters to critical tier only
P2_07 ?risk=high filters to high tier only
P2_08 ?kind=function filters to function kind only
P2_09 invalid risk param is ignored (returns all)
P2_10 invalid kind param is ignored (returns all)
Ordering:
P2_11 rows ordered by risk_score descending
P2_12 rows with equal risk_score ordered by address ascending
Stats context:
P2_13 total_count correct (unfiltered)
P2_14 critical_count correct
P2_15 avg_risk_score within 0.1 of expected
Security:
P2_16 SQL injection attempt in risk param → 400 or ignored (no crash)
P2_17 SQL injection attempt in kind param → 400 or ignored (no crash)
P2_18 XSS attempt in kind param does not appear unescaped in HTML
Stress:
P2_19 10 concurrent requests to /intel/blast-risk → all 200, no 500
P2_20 response with 250 rows completes in < 500ms
Phase 3 — Risk-ranked table + score bars + SCSS (no tests)
Why third: visual structure depends on the route delivering data.
Template structure (musehub/templates/musehub/pages/intel_blast_risk.html):
.intel-page-header— title "BLAST RISK" with--gradient-spectraltext fill; subtitle; back link← Intel Hub.br-stats-row— four--bg-elevatedstat cards: total, critical count (--color-danger), high count (--color-warning), avg score.br-filter-bar— risk tier pills + kind pills (active state withpill--active).br-table—--bg-surface+--border-default+--radius-md.br-table-header—#,SYMBOL,KIND,RISK,SCORE, four sub-score columns, detail link column.br-rowper symbol — address--font-mono, kind badge, risk tier badge, composite score, four mini score bars,→ detaillink
- Empty state:
icon(activity, 48)+--text-muted
Sub-score bars: each sub-score (0.0–1.0 float) renders as a filled bar with color keyed to its absolute value:
- ≥ 0.75 →
--color-danger - ≥ 0.50 →
--color-warning - ≥ 0.25 →
--color-orange - < 0.25 →
--color-success
Pure helpers to expose from ui_intel.py:
def _risk_color_class(score: float) -> str:
"""Return CSS class for a normalized sub-score bar fill."""
...
def _score_bar_pct(score: float) -> int:
"""Clamp 0.0–1.0 float to 0–100 integer for bar width %."""
...
SCSS:
components/_blast_risk.scss— colors, badges, bar fills, hover transitionspages/_blast_risk.scss— table grid, flex layout, padding, responsive
Phase 4 — Per-symbol detail page + comprehensive tests
Why fourth: requires the list (Phase 3) to exist for navigation.
New route:
GET /{owner}/{repo_slug}/intel/blast-risk/detail?address=<symbol_address>
Shows:
- Symbol header: address, kind badge, risk badge, composite score
- Four sub-score bars with raw input labels (blast radius count, commits/30d, test coverage %, entangled files)
blast_toplist of direct dependents (frommusehub_symbol_intel.blast_top)- Back link
← Blast Risk
Join musehub_intel_blast_risk with musehub_symbol_intel to get blast_top, blast, churn_30d raw values.
TDD layers (tests/test_phase4_blast_risk_detail.py):
Route:
P4_01 route path contains "blast-risk/detail"
P4_02 known address → 200
P4_03 unknown address → 200 with empty-state (not 404)
P4_04 missing address param → 200 with empty-state
Content:
P4_05 symbol address rendered in HTML
P4_06 composite risk_score rendered
P4_07 all four sub-score values rendered
P4_08 risk tier badge rendered
P4_09 blast_top dependents listed (when non-empty)
P4_10 back link to /intel/blast-risk present
Pure helpers:
P4_11 _risk_color_class(0.8) == "br-score-fill--critical"
P4_12 _risk_color_class(0.6) == "br-score-fill--high"
P4_13 _risk_color_class(0.3) == "br-score-fill--medium"
P4_14 _risk_color_class(0.1) == "br-score-fill--low"
P4_15 _score_bar_pct(0.0) == 0
P4_16 _score_bar_pct(1.0) == 100
P4_17 _score_bar_pct(0.5) == 50
P4_18 _score_bar_pct(1.5) clamped to 100 (no overflow)
End-to-end:
P4_19 seed symbol_intel + blast_risk row → GET detail → HTML contains address, score, blast_top[0]
P4_20 symbol with empty blast_top → detail page renders without error
Security:
P4_21 XSS in address param does not appear unescaped in HTML
P4_22 path traversal in address param → safe (no 500, no file read)
Phase 5 — Navigation + Intel Hub integration + tests
Why last: nav links reference all prior pages; hub counts are cosmetic.
Back links: ← Intel Hub on list and detail; ← Blast Risk on detail.
Page title: <title>Blast Risk · {repo_slug}</title>
Intel Hub context: expose blast_risk_count, critical_count in the hub context dict for future tile integration (mirrors dead page pattern).
TDD layers (tests/test_phase5_blast_risk_nav.py):
P5_01 back link to /intel present in list HTML
P5_02 back link to /intel present in empty-state HTML
P5_03 page title contains "Blast Risk"
P5_04 detail page back link to /intel/blast-risk present
P5_05 detail page title contains "Blast Risk"
File Checklist
New files
tests/test_phase1_blast_risk_provider.py
tests/test_phase2_blast_risk_route.py
tests/test_phase4_blast_risk_detail.py
tests/test_phase5_blast_risk_nav.py
src/scss/components/_blast_risk.scss
src/scss/pages/_blast_risk.scss
Modified files
musehub/services/musehub_intel_providers.py ← rewrite BlastRiskProvider
musehub/api/routes/musehub/ui_intel.py ← rewrite intel_blast_risk_page + add detail route
musehub/templates/musehub/pages/intel_blast_risk.html ← full rewrite
src/scss/app.scss ← wire _blast_risk.scss into both layers
No new migrations — musehub_intel_blast_risk already has all required columns.
Testing Matrix
| Layer | Suite | What it covers |
|---|---|---|
| Unit | test_phase1_blast_risk_provider.py P1_01–P1_11 |
Formula correctness, tier thresholds, weight constants — no DB |
| Integration | test_phase1_blast_risk_provider.py P1_12–P1_22 |
Full upsert round-trip, exclusions, idempotency |
| Performance | test_phase1_blast_risk_provider.py P1_23 |
1000 rows < 5s |
| Route | test_phase2_blast_risk_route.py P2_01–P2_15 |
HTTP responses, filtering, ordering, stat counts |
| Security | test_phase2_blast_risk_route.py P2_16–P2_18 |
SQL injection, XSS in query params |
| Stress | test_phase2_blast_risk_route.py P2_19–P2_20 |
10 concurrent requests, 250-row response time |
| State integrity | test_phase1_blast_risk_provider.py P1_20–P1_22 |
Upsert semantics, no cross-table mutation |
| E2E | test_phase4_blast_risk_detail.py P4_19–P4_20 |
Seed → push → GET detail → assert HTML |
| Security | test_phase4_blast_risk_detail.py P4_21–P4_22 |
XSS, path traversal in address param |
| Navigation | test_phase5_blast_risk_nav.py P5_01–P5_05 |
Back links, titles, breadcrumbs |
Reference: Gravity + Dead Code Implementations
Follow the patterns established in issues #9 (gravity) and #10 (dead code):
- SQL derivation in the provider (no subprocess)
- Route queries the intel table directly
- COALESCE upserts for non-destructive re-runs
- Two-layer SCSS split
- TDD-first on every phase that touches logic
Key files: musehub/services/musehub_intel_providers.py::GravityProvider, musehub/services/musehub_intel_providers.py::DeadProvider, musehub/api/routes/musehub/ui_intel.py::intel_gravity_page, musehub/api/routes/musehub/ui_intel.py::intel_dead_page.
Phases 1-3 shipped to staging
Phase 1 - SQL-derived BlastRiskProvider 30/30 tests green
- Replaced muse code blast-risk subprocess with pure SQL derivation from musehub_symbol_intel
- Formula: impact x40 + churn x25 + test_gap x20 + coupling x15, capped at 100
- Risk tiers: critical>=75, high>=50, medium>=25, low<25
- Upserts into musehub_intel_blast_risk per (repo_id, address)
- Added populate_existing=True to SELECT so reruns always read fresh DB state
Phase 2 - Route + Template 20/20 tests green
- Rewrote intel_blast_risk_page to query musehub_intel_blast_risk directly (no subprocess)
- ?risk=critical|high|medium|low filter, ?top=25|50|100|250 limit
- Stat cards: critical / high / medium / low / total counts
- New template: tier-badged rows ordered by risk_score DESC, filter bar, responsive
Phase 3 - SCSS builds clean
- components/_blast_risk.scss: visual rules (colors, tier-tinted stat cards, filter bar, scored rows)
- pages/_blast_risk.scss: structural layout (flex stat row, responsive meta hide <700px)
- Wired into app.scss
Live on staging. Phases 4 (detail page) and 5 (nav integration) next.
All 5 phases shipped and live on staging.
- Phase 1: BlastRiskProvider — pure SQL derivation from musehub_symbol_intel (30 tests)
- Phase 2: /intel/blast-risk list page with stat cards, tier filter bar, top-N selector (20 tests)
- Phase 3: Two-layer SCSS split (components/_blast_risk.scss + pages/_blast_risk.scss)
- Phase 4: /intel/blast-risk/detail per-symbol page with sub-score bars and blast_top dependents (22 tests)
- Phase 5: Navigation integration with Intel Hub (5 tests)
Also added: explosion icon (orange, custom SVG), clickable list rows → detail page, fmtnum formatting on all integer outputs.
Phases 1–3 shipped to staging 🚀
Phase 1 — SQL-derived BlastRiskProvider ✅ 30/30 tests green
muse code blast-risksubprocess with pure SQL derivation frommusehub_symbol_intelimpact×40 + churn×25 + test_gap×20 + coupling×15, capped at 100musehub_intel_blast_riskper (repo_id, address)populate_existing=Trueto SELECT so reruns always read fresh DB statePhase 2 — Route + Template ✅ 20/20 tests green
intel_blast_risk_pageto querymusehub_intel_blast_riskdirectly (no subprocess)?risk=critical|high|medium|lowfilter,?top=25|50|100|250limitrisk_score DESC, filter bar, responsivePhase 3 — SCSS ✅ builds clean
components/_blast_risk.scss: visual rules (colors, tier-tinted stat cards, filter bar, scored rows)pages/_blast_risk.scss: structural layout (flex stat row, responsive meta hide <700px)app.scss; CSS compiled and deployedLive on staging. Phases 4 (detail page) and 5 (nav integration) next.