gabriel / musehub public
Open #24 Enhancement
filed by gabriel human · 42 days ago

Symbol Detail Page — God-Tier Redesign (issue #24)

0 Anchors
Blast radius
Churn 30d
0 Proposals

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.pysymbol_detail_page):

  1. Add targeted queries for all 7 missing intel tables, using address == address_value WHERE clauses:
    • MusehubIntelTypesd_type
    • MusehubIntelBlastRisksd_blast_risk
    • MusehubIntelStablesd_stable
    • MusehubIntelDeadsd_dead
    • MusehubIntelApiSurfacesd_api
    • MusehubIntelRefactorEvent (WHERE address == address_value, ORDER BY committed_at DESC, LIMIT 20) → sd_refactor_events
  2. Enrich MusehubSymbolIntel read to include gravity_pct, gravity_direct_dependents, gravity_transitive_dependents, gravity_max_depth, blast_top, author_count, last_author, churn_30d, churn_90d.
  3. Compute sd_narrative — a single auto-generated string: "Born {age} ago · {churn} lifetime changes · {versions} distinct versions · co-changed with {coupling} symbols".
  4. Compute sd_stability_pctmax(0, 100 - (churn_30d * 5)) capped 0–100. Simple proxy until a dedicated stability index exists.
  5. 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_30d annotation per partner (if available from their MusehubSymbolIntel row).

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-gap dividers that use --border-default.
  • Add refactoring event overlay: if a history entry's commit_id matches one in sd_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
Activity
gabriel opened this issue 42 days ago
No activity yet. Use the CLI to comment.