gabriel / musehub public
Closed #11 Enhancement
filed by gabriel human · 47 days ago

/intel/blast-risk — pre-release risk dashboard

0 Anchors
Blast radius
Churn 30d
0 Proposals

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 → feeds impact_score
  • churn, churn_30d → feeds churn_score
  • gravity_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:

  • BlastRiskProvider class — what it does, data source, formula summary, output shape
  • BlastRiskProvider.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 | low
  • kind (optional) — filter to symbol kind
  • top (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-spectral text fill; subtitle; back link ← Intel Hub
  • .br-stats-row — four --bg-elevated stat cards: total, critical count (--color-danger), high count (--color-warning), avg score
  • .br-filter-bar — risk tier pills + kind pills (active state with pill--active)
  • .br-table--bg-surface + --border-default + --radius-md
    • .br-table-header#, SYMBOL, KIND, RISK, SCORE, four sub-score columns, detail link column
    • .br-row per symbol — address --font-mono, kind badge, risk tier badge, composite score, four mini score bars, → detail link
  • 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 transitions
  • pages/_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_top list of direct dependents (from musehub_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.

Activity3
gabriel opened this issue 47 days ago
gabriel 47 days ago

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×40 + churn×25 + test_gap×20 + coupling×15, 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; CSS compiled and deployed

Live on staging. Phases 4 (detail page) and 5 (nav integration) next.

gabriel 47 days ago

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.

gabriel 47 days ago

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.