Symbol Detail Page — God-Tier Redesign (issue #24)
Overview
The symbol detail page is the most powerful page in MuseHub — it is the place where Muse's fundamental advantage over file-level VCS becomes visible. A symbol is the atomic unit of Muse's intelligence model: every churn metric, every blast-risk score, every type annotation, every refactoring event, every coupling relationship is anchored to a symbol address, not a file path.
But the current page does not convey this. A first-time visitor lands on a dense dump of data — a name, a wall of badges, a timeline, a coupling list — with no narrative, no hierarchy of importance, and no clear "so what?" This issue redesigns it from scratch to be singularity-mode: a mission control panel for a single symbol, where every dimension of Muse's intelligence is surfaced in the right place, at the right weight.
Current Page Inventory
What exists today (440-line template, 1146-line SCSS):
- Zone 0 — Identity hero: name, badges, vitals strip, 12-week spark, op-mix bar
- Zone 1 — Signal callouts: HOTSPOT / BLAST RISK / DEAD CODE banners
- Zone 2 — 2-col grid: Provenance timeline (left) + sidebar (right)
- Sidebar: Coupling Partners, Vitals card, Clones
Intel tables queried today: MusehubSymbolIntel, MusehubSymbolHistoryEntry,
MusehubHashOccurrenceEntry, MusehubIntelResult (snapshot blob).
Intel tables NOT yet queried on this page (but fully populated in DB):
| Table | Columns of value |
|---|---|
MusehubIntelType |
type_score, return_annotation, return_is_any, params_total, params_annotated, params_with_any |
MusehubIntelBlastRisk |
risk, risk_score, impact_score, churn_score, test_gap_score, coupling_score |
MusehubIntelStable |
days_stable, since_start, last_changed_commit |
MusehubIntelDead |
confidence, reason, dismissed |
MusehubIntelApiSurface |
visibility, signature_id |
MusehubIntelRefactorEvent |
kind, detail, commit_id, commit_message |
MusehubSymbolIntel |
gravity_pct, gravity_direct_dependents, gravity_transitive_dependents, gravity_max_depth, blast_top, author_count, last_author, churn_30d, churn_90d |
Target Layout — ASCII Architecture
┌──────────────────────────────────────────────────────────────────────────────┐
│ ZONE 0 — IDENTITY COMMAND STRIP (full-width, always above fold) │
│ │
│ ┌──────────────────────────────────────────────────┐ ┌──────────────────┐ │
│ │ file/path.py │ │ AGE CHURN │ │
│ │ symbol_name [fn] [modify] [🔥 HOTSPOT] │ │ 24d 40 │ │
│ │ │ │ BLAST GRAVITY │ │
│ │ "Born 24 days ago · 40 lifetime changes · │ │ 20 4.2% │ │
│ │ rewritten 3 times · co-changed with 20 symbols"│ └──────────────────┘ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ██ velocity spark ██░░░██░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12 weeks │
│ ▓░░ add ████ modify ░ delete op-mix bar │
├──────────────────────────────────────────────────────────────────────────────┤
│ ZONE 1 — HEALTH DIMENSIONS (5 score cards, horizontal strip) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐ │
│ │ STABILITY │ │ TYPE COVER │ │ BLAST RISK │ │ GRAVITY │ │ API PUBLIC│ │
│ │ ████░░░░ │ │ ██████░░ │ │ ████████ │ │ ██░░░░░░ │ │ ✓ │ │
│ │ 44% │ │ 75% │ │ CRITICAL │ │ 4.2% │ │ public │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ └───────────┘ │
├───────────────────────────────────┬──────────────────────────────────────────┤
│ ZONE 2 — PROVENANCE TIMELINE │ ZONE 3 — RELATIONSHIPS │
│ │ │
│ v3 ◆ sha256:d30 · 2h ago ← HEAD │ COUPLING PARTNERS (20) │
│ ├─[mod] refactor: eliminate … │ ┌──────────────────────────────────┐ │
│ │ │ │ other_symbol ████████ 12× 100%│ │
│ ◀── 12d gap ──▶ │ │ asyncio █████░░░ 2× 100%│ │
│ │ └──────────────────────────────────┘ │
│ v2 ◆ sha256:a62 · 12d ago │ │
│ ├─[mod] perf: parallelise … │ BLAST RADIUS │
│ │ │ direct: 3 · transitive: 8 · depth: 2 │
│ ◀── 12d gap ──▶ │ Top dependents: │
│ │ → some_caller.py::invoke_it │
│ v1 ◆ sha256:ab2 · 24d ago ← born│ → another.py::run_pipeline │
│ └─[add] initial commit │ │
│ │ CLONES (2) │
│ [↓ Show 37 older entries] │ → tests/test_foo.py::same_fn │
│ │ → utils/compat.py::legacy_fn │
├───────────────────────────────────┴──────────────────────────────────────────┤
│ ZONE 4 — REFACTORING HISTORY (from detect-refactor, address-filtered) │
│ │
│ [impl] refactor: domain-agnostic intel system sha256:d30 12d ago │
│ [sig] perf: rewrite signature for async batch sha256:a62 18d ago │
│ [impl] feat: initial implementation sha256:ab2 24d ago │
│ │
│ (hidden if no refactor events for this address) │
├──────────────────────────────────────────────────────────────────────────────┤
│ ZONE 5 — TYPE HEALTH CARD (hidden if no type intel for this address) │
│ │
│ return: AsyncGenerator[PushStreamChunk, None] ← fully annotated │
│ params: 4 / 4 annotated 0 Any params type_score: 100% │
└──────────────────────────────────────────────────────────────────────────────┘
Theme and Visual Language
Guiding principle: this page is a character sheet for a symbol. Every dimension of Muse's intelligence should be visible in one scroll.
.sd-* — Symbol Detail namespace (all new classes use this prefix)
| Theme element | Rule |
|---|---|
| Identity strip | Full-width, dark --bg-surface background, left border 3px gradient from --gradient-spectral. Name is 1.6rem, gradient text. |
| Vitals quad | 2×2 grid of stat chips (Age, Churn, Blast, Gravity). Compact, monospace numbers, --font-mono, muted labels. |
| Narrative line | Single auto-generated sentence in --text-secondary, 0.78rem. No heading. Just context. |
| Health dimensions | 5-card horizontal strip. Each card: icon, label, fill bar (CSS only, no JS), tier/score. Background --bg-page, border --border-default, hover lifts to --bg-surface. |
| Timeline | Left-border accent line (--border-default). Version epoch dividers use --gradient-spectral dot. Gaps use ASCII art ◀── Xd gap ──▶. Entry hover: --bg-surface. |
| Relationships | Right column, 3 stacked cards. Coupling heat bars match the co-change count. Blast radius card shows direct/transitive/depth/gravity as a 2×2 micro-grid. |
| Refactoring history | Dense table, 4 cols: kind badge · address · detail · commit+time. Kind badges reuse rf-kind-badge--* from detect-refactor. |
| Type health | Compact card: return type as <code>, params ratio, Any-warning badges. Reuse existing .any-badge from type health page. |
| Signal callouts | Keep existing sym2-callout-* styles — only the placement changes (they move below the health strip). |
| Colors | All intel signals stay on-brand: 🔥 orange, 💥 red/danger, 🧊 muted blue, ✓ green. |
| Mobile | Below 900px: health strip becomes 2-col, relationships collapse below timeline. |
Implementation Plan — Phase-by-Phase
Phase 1 — Data layer (load-bearing foundation)
Everything else is blocked on this.
Route changes (musehub/api/routes/musehub/ui_symbols.py — symbol_detail_page):
- Add targeted queries for all 7 missing intel tables, using
address == address_valueWHERE clauses:MusehubIntelType→sd_typeMusehubIntelBlastRisk→sd_blast_riskMusehubIntelStable→sd_stableMusehubIntelDead→sd_deadMusehubIntelApiSurface→sd_apiMusehubIntelRefactorEvent(WHERE address == address_value, ORDER BY committed_at DESC, LIMIT 20) →sd_refactor_events
- Enrich
MusehubSymbolIntelread to includegravity_pct,gravity_direct_dependents,gravity_transitive_dependents,gravity_max_depth,blast_top,author_count,last_author,churn_30d,churn_90d. - Compute
sd_narrative— a single auto-generated string:"Born {age} ago · {churn} lifetime changes · {versions} distinct versions · co-changed with {coupling} symbols". - Compute
sd_stability_pct—max(0, 100 - (churn_30d * 5))capped 0–100. Simple proxy until a dedicated stability index exists. - Run all 7 queries in one
asyncio.gather()— no serial waterfall.
New context keys added to template: sd_type, sd_blast_risk, sd_stable, sd_dead, sd_api, sd_refactor_events, sd_narrative, sd_stability_pct, sd_gravity_* fields from MusehubSymbolIntel.
Phase 2 — Health dimensions strip (Zone 1)
New HTML section between the identity hero and the callouts:
<div class="sd-health-strip">
<div class="sd-health-card sd-health-card--stability">...</div>
<div class="sd-health-card sd-health-card--type">...</div>
<div class="sd-health-card sd-health-card--blast">...</div>
<div class="sd-health-card sd-health-card--gravity">...</div>
<div class="sd-health-card sd-health-card--api">...</div>
</div>
Each card: icon · label · fill bar · score/tier text.
New SCSS file: src/scss/components/_symbol_detail.scss.
CSS-only fill bars — no JavaScript.
Phase 3 — Identity banner restructure (Zone 0)
Split the existing sym2-hero into a 2-column layout:
- Left (flex: 1): file eyebrow, symbol name, badges, narrative line, velocity spark, op-mix bar
- Right (fixed width, ~180px): 2×2 vitals quad — Age · Churn · Blast · Gravity
New class .sd-vitals-quad with 2-col CSS grid.
Narrative line uses .sd-narrative — one sentence, no heading.
Phase 4 — Refactoring history section (Zone 4)
New section below the main grid, visible only when sd_refactor_events is non-empty:
{% if sd_refactor_events %}
<section class="sd-refactor-section">
<h2 class="sd-section-title">REFACTORING HISTORY</h2>
<div class="sd-refactor-list">
{% for ev in sd_refactor_events %}
<div class="sd-refactor-row">
<span class="rf-kind-badge rf-kind-badge--{{ ev.kind }}">{{ ev.kind }}</span>
<span class="sd-refactor-detail">{{ ev.detail or ev.commit_message[:60] }}</span>
<a href="{{ base_url }}/commits/{{ ev.commit_id }}" class="sym2-commit-link">{{ ev.commit_id | shortsha }}</a>
<span class="sd-refactor-time">{{ ev.committed_at | fmtrelative }}</span>
</div>
{% endfor %}
</div>
</section>
{% endif %}
Reuses rf-kind-badge--* classes from detect-refactor component.
Phase 5 — Enhanced relationships panel (Zone 3 right column)
Replace the flat coupling list with a 3-card stack:
Card A — Coupling Partners (existing, enhanced):
- Add
churn_30dannotation per partner (if available from theirMusehubSymbolIntelrow).
Card B — Blast Radius (new):
Pull from MusehubSymbolIntel: gravity_pct, gravity_direct_dependents, gravity_transitive_dependents, gravity_max_depth, blast_top.
Show as a 2×2 micro-grid (direct · transitive · depth · gravity%) + top-5 dependent addresses linked.
Card C — Clones (existing, relocated here from sidebar).
Phase 6 — Type health card (Zone 5)
New section at page bottom, visible only when sd_type is populated:
{% if sd_type %}
<section class="sd-type-section">
<h2 class="sd-section-title">TYPE HEALTH</h2>
...type_score bar, return annotation, params ratio, Any badges...
</section>
{% endif %}
Reuse existing .th-* visual classes from type health page.
Phase 7 — Timeline polish
- Replace existing
◀── Xd gap ──▶text with styled.sd-tl-gapdividers that use--border-default. - Add refactoring event overlay: if a history entry's
commit_idmatches one insd_refactor_events, show a small[impl]/[sig]/[move]/[rename]tag next to the entry. - Move signal callout group (
sym2-callout-group) to below the health strip and above the timeline grid (better visual hierarchy — you see signals before the details).
Testing — All Seven Tiers
T1 — Unit tests
# test_symbol_detail_unit.py
# T101: _compute_narrative returns expected string for all field combinations
# T102: _compute_stability_pct clamps to 0–100 correctly
# T103: sd_vitals_quad contains all four metrics when intel present
# T104: narrative omits clauses for missing fields gracefully
# T105: _infer_sym_kind correct for CamelCase / ALL_CAPS / fn / import
T2 — Integration tests
# test_symbol_detail_integration.py
# T201: route returns 200 when MusehubSymbolHistoryEntry rows exist
# T202: route returns 404 when no history rows for address
# T203: all 7 intel tables queried without serial waterfall (use asyncio mock)
# T204: sd_type absent from ctx when MusehubIntelType has no row for address
# T205: sd_refactor_events ordered by committed_at DESC, LIMIT 20
# T206: sd_blast_risk None when symbol not in MusehubIntelBlastRisk
# T207: sd_api None when symbol not in MusehubIntelApiSurface
# T208: sd_stable None when symbol not in MusehubIntelStable
# T209: gravity fields present on ctx when MusehubSymbolIntel populated
# T210: co-change SQL query returns results without full-table scan
T3 — End-to-end HTML tests
# test_symbol_detail_e2e.py
# T301: symbol name renders in <h1> with .gradient-text
# T302: health strip renders 5 .sd-health-card elements
# T303: sd_refactor_events renders .sd-refactor-section when events present
# T304: sd-type-section renders when sd_type present, hidden when absent
# T305: blast radius card renders direct/transitive/depth/gravity values
# T306: coupling partners link to /{owner}/{repo}/symbol/{address}
# T307: refactoring history rows show rf-kind-badge--{kind}
# T308: vitals quad renders Age, Churn, Blast, Gravity chips
# T309: narrative line present in .sd-narrative element
# T310: API surface card shows 'public' badge when sd_api present
T4 — Stress tests
# test_symbol_detail_stress.py
# T401: symbol with 10,000 history entries — page renders in < 500ms
# T402: symbol co-changed with 500 partners — SQL GROUP BY handles without timeout
# T403: 50 concurrent requests to symbol_detail_page — no DB connection exhaustion
# T404: symbol with 1,000 refactor events — LIMIT 20 enforced in SQL
# T405: co-change query with 50,000 target_commits in IN clause stays < 200ms
T5 — Data integrity tests
# test_symbol_detail_integrity.py
# T501: type_score always 0.0–1.0 range from MusehubIntelType
# T502: blast_top addresses all resolvable as valid symbol addresses
# T503: sd_stability_pct never outside 0–100 range
# T504: refactor events for address match address field exactly (no substring match)
# T505: version_count never > len(history) — derived correctly
# T506: co_changed coupling_pct never > 100
T6 — Performance tests
# test_symbol_detail_perf.py
# T601: all 7 intel table queries complete in single asyncio.gather — verify no serial await
# T602: targeted MusehubSymbolHistoryEntry index scan (repo_id + address) used — verify EXPLAIN
# T603: co-change SQL query uses ix_symbol_history_repo_address index — verify EXPLAIN
# T604: total route handler time < 100ms for symbol with 500 history entries on warm DB
# T605: no N+1 queries — assert db.execute call count <= 12 for any symbol
T7 — Security tests
# test_symbol_detail_security.py
# T701: address path param with ../../../etc/passwd returns 404, not 500
# T702: address with SQL injection chars ('; DROP TABLE) returns 404 safely
# T703: symbol name containing <script> is escaped in every render zone
# T704: commit messages containing HTML tags escaped in timeline
# T705: refactor detail field containing <img src=x onerror=...> escaped
# T706: address param length > 512 chars returns 422 or 404, not 500
Docstrings Required
Every new function must carry a complete docstring following this pattern:
async def symbol_detail_page(
request: Request,
owner: SlugParam,
repo_slug: SlugParam,
address: str,
db: AsyncSession = Depends(get_db),
) -> Response:
"""Render the Symbol Detail page — mission control for a single symbol.
Aggregates data from eight normalized intel tables in parallel:
MusehubSymbolHistoryEntry (timeline), MusehubSymbolIntel (metrics),
MusehubIntelType (type health), MusehubIntelBlastRisk (risk scores),
MusehubIntelStable (stability), MusehubIntelDead (dead-code status),
MusehubIntelApiSurface (public contract membership), and
MusehubIntelRefactorEvent (detected refactoring history).
All seven intel lookups are issued concurrently via asyncio.gather to
avoid serial waterfall latency. The history query is a targeted index
scan on (repo_id, address) — never a full-table load.
The co-change coupling section is computed by a SQL COUNT + GROUP BY
query rather than iterating all history rows in Python.
Args:
request: FastAPI request context (passed to TemplateResponse).
owner: Repository owner handle (validated slug).
repo_slug: Repository slug (validated).
address: Symbol address in ``file.py::SymbolName`` format.
Accepted as a path parameter so ``::`` is preserved.
db: Async SQLAlchemy session from dependency injection.
Returns:
HTMLResponse rendering ``musehub/pages/symbol_detail.html`` with
a ``SymbolDetailContext`` dict containing all zones' data.
Raises:
HTTPException(404): when no history rows exist for ``address``
in this repository (symbol unknown to the index).
"""
Load-Bearing Order (dependency graph)
Phase 1 (data layer)
│
├─→ Phase 2 (health strip) ← needs sd_type, sd_blast_risk, sd_stable, sd_api
├─→ Phase 3 (identity restructure) ← needs sd_narrative, sd_gravity_*, sd_stability_pct
├─→ Phase 4 (refactor history) ← needs sd_refactor_events
├─→ Phase 5 (relationships) ← needs gravity fields from MusehubSymbolIntel
├─→ Phase 6 (type health card) ← needs sd_type
└─→ Phase 7 (timeline polish) ← needs sd_refactor_events for overlay tags
Phase 2, 3, 4, 5, 6 are independent of each other — can ship in any order
after Phase 1. Phase 7 depends on Phase 4.
Recommended ship order: 1 → 2 → 3 → 5 → 4 → 6 → 7
Files Touched
| File | Change |
|---|---|
musehub/api/routes/musehub/ui_symbols.py |
Major — add 7 parallel queries, narrative computation, gravity fields |
musehub/templates/musehub/pages/symbol_detail.html |
Major redesign — all 5 new zones |
src/scss/components/_symbol_detail.scss |
New file — all .sd-* rules |
src/scss/app.scss |
Add @use for _symbol_detail.scss |
tests/test_symbol_detail_unit.py |
New — T101–T105 |
tests/test_symbol_detail_integration.py |
New — T201–T210 |
tests/test_symbol_detail_e2e.py |
New — T301–T310 |
tests/test_symbol_detail_stress.py |
New — T401–T405 |
tests/test_symbol_detail_integrity.py |
New — T501–T506 |
tests/test_symbol_detail_perf.py |
New — T601–T605 |
tests/test_symbol_detail_security.py |
New — T701–T706 |