gabriel / musehub public
Closed #19 Enhancement
filed by gabriel human · 48 days ago

feat: intel/api-surface GUI page — muse code api-surface web dashboard

0 Anchors
Blast radius
Churn 30d
0 Proposals

Summary

Port muse code api-surface into the Intel Hub as a fully interactive web page. No subprocess at request time — background worker on every muse push, data stored in musehub_intel_api_surface, SQL queries drive the UI.

The DB model (MusehubIntelApiSurface) and background provider (ApiSurfaceProvider) already exist. This ticket covers: fixing the provider's row-by-row upsert into a batch upsert, adding the route handler, template, SCSS, dashboard card, and full 7-tier test suite.


Current state

Layer Status
DB table musehub_intel_api_surface ✅ exists (migration 0004_phase1_intel_tables.py)
ORM model MusehubIntelApiSurface ✅ exists (musehub/db/musehub_models.py:1362)
Background provider ApiSurfaceProvider ✅ exists (musehub/services/musehub_intel_providers.py:1631) — row-by-row upsert, needs batch fix
Route handler intel_api_surface_page ❌ missing
Template intel_api_surface.html ❌ missing
SCSS pages/_api_surface.scss + components/_api_surface.scss ❌ missing
Dashboard card on intel_dashboard.html ❌ missing
Test suite tests/test_intel_api_surface.py ❌ missing

DB schema (existing)

class MusehubIntelApiSurface(Base):
    __tablename__ = "musehub_intel_api_surface"
    __table_args__ = (Index("ix_intel_api_surface_repo", "repo_id"),)

    repo_id:      Mapped[str]       # FK → musehub_repos.repo_id CASCADE
    address:      Mapped[str]       # composite PK with repo_id, e.g. "src/billing.py::compute_total"
    kind:         Mapped[str]       # function | async_function | class | method | async_method
    signature_id: Mapped[str|None]  # content-addressed hash of the signature object
    visibility:   Mapped[str]       # "public" (only public symbols stored)
    ref:          Mapped[str]       # branch/commit ref at harvest time

muse code api-surface --json output per symbol (musehub repo: 8,333 symbols):

{
  "address":       "src/billing.py::compute_total",
  "kind":          "function",
  "name":          "compute_total",
  "qualified_name":"musehub.billing.compute_total",
  "language":      "Python",
  "content_id":    "sha256:...",
  "signature_id":  "sha256:...",
  "body_hash":     "sha256:..."
}

Page design — ASCII art

┌─────────────────────────────────────────────────────────────────────────────┐
│  ← Intel Hub    ◈ API Surface                                               │
│  Public symbols whose signature is part of the contract.                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │   8,333      │  │   4,210      │  │   1,874      │  │    892       │   │
│  │   Total      │  │  Functions   │  │   Classes    │  │   Methods    │   │
│  └──────────────┘  └──────────────┘  └──────────────┘  └──────────────┘   │
│  ┌──────────────┐  ┌──────────────┐                                        │
│  │    763       │  │    594       │                                         │
│  │  Async Fns   │  │ Async Methods│                                         │
│  └──────────────┘  └──────────────┘                                        │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  Filter: [ All ▼ ] [ function ] [ async_function ] [ class ] ...    │   │
│  │           Search: [________________________]   Top: [20][50][100]   │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  ADDRESS                                   KIND              REF    │   │
│  ├─────────────────────────────────────────────────────────────────────┤   │
│  │  src/billing.py :: compute_total           function          dev    │   │
│  │  src/auth.py    :: validate_token          async_function    dev    │   │
│  │  src/models.py  :: UserRecord              class             dev    │   │
│  │  src/models.py  :: UserRecord.save         method            dev    │   │
│  │  ...                                                                │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

Color / icon: (diamond) icon · var(--color-accent) (cyan) — mirrors the intel_dashboard.html card. Use icon("diamond", 16) from the SVG sprite.


Implementation plan — load-bearing order

Phase 1 — Provider batch fix (prerequisite for correct data)

File: musehub/services/musehub_intel_providers.py

The current ApiSurfaceProvider.compute() executes one INSERT per symbol in a Python loop — 8,333 round-trips on musehub. Replace with a single chunked pg_insert(...).values(chunk).on_conflict_do_update(...) call matching the pattern used by TypeProvider, VelocityProvider, and ClonesProvider.

class ApiSurfaceProvider:
    """Persist public API surface entries from ``muse code api-surface``.

    Harvests all public symbols on every push and upserts them into
    ``musehub_intel_api_surface`` in chunks of 1,000 rows.  No subprocess
    runs at request time — the route handler reads exclusively from Postgres.

    Parameters
    ----------
    session:
        Active async SQLAlchemy session (provided by the push worker).
    repo_id:
        Opaque repo identifier from ``musehub_repos.repo_id``.
    ref:
        Branch or commit ref string at push time (e.g. ``"dev"``).
    payload:
        Raw push event JSON — used only to resolve the on-disk repo root.

    Returns
    -------
    List of ``(event_name, metadata)`` tuples consumed by the telemetry layer.
    Returns ``[("intel.code.api_surface", {"count": N})]`` on success,
    ``[]`` when the repo root cannot be resolved or the command returns no data.
    """
    _CHUNK = 1_000

    async def compute(self, session, repo_id, ref, payload):
        root = await _resolve_repo_root(session, repo_id, payload)
        if root is None:
            return []
        data = await _run_muse(root, "api-surface")
        symbols = (data or {}).get("symbols", [])
        if not symbols:
            return []
        rows = [
            {
                "repo_id":      repo_id,
                "address":      s["address"],
                "kind":         s.get("kind", "unknown"),
                "signature_id": s.get("signature_id"),
                "visibility":   s.get("visibility", "public"),
                "ref":          ref,
            }
            for s in symbols
        ]
        for i in range(0, len(rows), self._CHUNK):
            chunk = rows[i : i + self._CHUNK]
            stmt = (
                pg_insert(db.MusehubIntelApiSurface)
                .values(chunk)
                .on_conflict_do_update(
                    index_elements=["repo_id", "address"],
                    set_={
                        "kind":         sa.literal_column("excluded.kind"),
                        "signature_id": sa.literal_column("excluded.signature_id"),
                        "visibility":   sa.literal_column("excluded.visibility"),
                        "ref":          sa.literal_column("excluded.ref"),
                    },
                )
            )
            await session.execute(stmt)
        return [("intel.code.api_surface", {"count": len(rows)})]

Why load-bearing first: downstream tests depend on correct data shape in the DB.


Phase 2 — TDD: write tests/test_intel_api_surface.py (all RED)

Write all 30 tests before touching the route or template. Pattern mirrors tests/test_intel_type.py exactly. Tests must fail (404 / import errors) before Phase 3.

Tier 1 — DB layer (T01–T03)
T01  MusehubIntelApiSurface row insert + read-back (repo_id + address composite PK)
T02  on_conflict_do_update: upsert updates kind/ref, leaves address unchanged
T03  cascade delete: dropping the repo purges all api_surface rows
Tier 2 — Provider (T04–T06)
T04  ApiSurfaceProvider.compute() calls _run_muse("api-surface") once per call
T05  8,333-symbol payload upserted in ≤ 9 SQL statements (ceil(8333/1000) = 9)
T06  Empty symbols list returns [] without touching the DB
Tier 3 — Route unit / integration (T07–T15)
T07  GET /owner/repo/intel/api-surface → 200 with repo that has data
T08  kind= filter: only rows with matching kind returned
T09  top= clamp: top=999 → coerced to 100
T10  top= invalid string → coerced to 20
T11  No data → empty-state template rendered (no 500)
T12  Repo not found → 404
T13  kind=function returns only function rows, not class rows
T14  kind=async_function filter works correctly
T15  top=50 returns exactly 50 rows when ≥ 50 exist
Tier 4 — E2E HTML assertions (T16–T19)
T16  Stat chip "Total" shows correct count from DB
T17  Kind breakdown chips: function / async_function / class / method / async_method counts correct
T18  Symbol address renders as "file :: name" split on "::"
T19  Active filter pill has CSS class "as-filter-btn--active"
Tier 5 — Data integrity (T20–T22)
T20  Two sequential upserts produce exactly N rows (no duplicates)
T21  Symbol with updated kind: after re-upsert, kind reflects new value
T22  Symbols from a deleted repo do not appear under a new repo with the same slug
Tier 6 — Performance (T23–T25)
T23  Provider processes 10,000-row payload in < 1.5s (mocked DB)
T24  Route handler responds in < 200ms with 100 rows in DB
T25  top=100 filter query hits the ix_intel_api_surface_repo index (EXPLAIN check)
Tier 7 — Security (T26–T30)
T26  Unauthenticated request to private repo → 403 or redirect
T27  address field rendered in template: "<script>" is escaped, not executed
T28  kind query param with SQL injection string → safely coerced, no error
T29  top=0 → coerced to 20, no empty-LIMIT SQL issued
T30  Cross-repo isolation: repo A's symbols never visible from repo B's page URL

Phase 3 — Route handler intel_api_surface_page

File: musehub/api/routes/musehub/ui_intel.py

Add after intel_type_page. Registered at /{owner}/{repo}/intel/api-surface.

_VALID_AS_TOPS: frozenset[int] = frozenset({20, 50, 100})
_VALID_AS_KINDS: frozenset[str] = frozenset({
    "function", "async_function", "class", "method", "async_method",
})

@router.get("/{owner}/{repo}/intel/api-surface")
async def intel_api_surface_page(
    request: Request,
    owner: SlugParam,
    repo_slug: SlugParam,
    kind: str = "",
    top: int = 20,
    db: AsyncSession = Depends(get_db),
) -> Response:
    """API surface dashboard — stat chips, kind filter, symbol table.

    Reads exclusively from ``musehub_intel_api_surface``; no ``muse code``
    subprocess runs at request time.  Stat chips mirror the kind breakdown
    produced by ``muse code api-surface --json``: total, function,
    async_function, class, method, async_method counts.

    Parameters
    ----------
    kind:
        Filter by symbol kind (``"function"``, ``"async_function"``,
        ``"class"``, ``"method"``, ``"async_method"``).  Empty string returns
        all kinds.  Unknown values are silently coerced to ``""`` so the page
        never 400s on a bad filter.
    top:
        Number of symbols to display; clamped to ``_VALID_AS_TOPS``
        (20 / 50 / 100).  Defaults to 20.

    Returns
    -------
    Rendered ``intel_api_surface.html`` with context keys:
    ``kind_counts``, ``total_count``, ``symbols``,
    ``selected_kind``, ``selected_top``,
    ``valid_tops``, ``valid_kinds``.

    Raises
    ------
    404
        Repository not found or inaccessible.
    """

Phase 4 — Template intel_api_surface.html

File: musehub/templates/musehub/pages/intel_api_surface.html

Follows the exact structure of intel_type.html and intel_blast_risk.html.

Key elements:

  • Subheader: intel-subhd-title--spectral · <span style="color:var(--color-accent)">{{ icon("diamond", 16) }}</span> · "API Surface"
  • Stat chips row (6 chips): Total · Functions · Async Fns · Classes · Methods · Async Methods
  • Filter bar: kind pills (All, function, async_function, class, method, async_method) + top filter (20 | 50 | 100)
  • Symbol list (as-list): each row shows:
    • File part (before ::) in muted text
    • :: separator
    • Name part (after ::) in bold mono
    • Kind badge (color-coded: function→accent, class→purple, method→teal, async→orange tint)
    • ref chip (faint)
  • Empty state: diamond icon + "No API surface data yet. Push a commit to populate."

Kind badge colors (from theme tokens):

kind color
function --color-accent
async_function --color-orange
class --color-purple
method --color-teal
async_method --color-rose

Phase 5 — SCSS

Files:

  • src/scss/pages/_api_surface.scss — layout (flex rows, column widths, filter bar)
  • src/scss/components/_api_surface.scss — visual (badge colors, hover, stat chips)

Follow the _type.scss split exactly. Both files imported in _components.scss and app.scss under existing // ── Intel ── comments.

CSS namespace: .as- (API Surface).

// pages/_api_surface.scss
.as-wrap       { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem; }
.as-stats-row  { display: flex; gap: 1rem; flex-wrap: wrap; margin: 1.5rem 0; }
.as-stat-card  { /* mirrors .br-stat-card */ }
.as-filter-bar { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1.5rem; }
.as-list       { display: flex; flex-direction: column; gap: 2px; }
.as-row        { display: grid; grid-template-columns: 1fr auto auto; align-items: center; padding: 0.5rem 0.75rem; border-radius: 6px; }

// components/_api_surface.scss
.as-kind-badge { /* pill badge, color per kind */ }
.as-address    { font-family: var(--font-mono); font-size: 0.85rem; }
.as-file       { color: var(--text-muted); }
.as-sep        { color: var(--text-muted); margin: 0 0.2rem; }
.as-name       { color: var(--text-primary); font-weight: 500; }

Phase 6 — Dashboard card + wire-up

Files:

  • musehub/templates/musehub/pages/intel_dashboard.html — add card after the Type card
  • musehub/api/routes/musehub/ui_intel.py — register route + add api_surface_count to dashboard context

Dashboard card:

<a href="{{ base_url }}/intel/api-surface" class="intel-card">
  <div class="intel-card__icon" style="color:var(--color-accent)">
    {{ icon("diamond", 24) }}
  </div>
  <div class="intel-card__body">
    <div class="intel-card__title">API Surface</div>
    <div class="intel-card__desc">Public symbols forming the repo's contract.</div>
  </div>
  <div class="intel-card__stat">{{ api_surface_count | fmtnum }}</div>
</a>

Icon: diamond — must be added to _icon_sprite.html if not present. Sprite entry:

<symbol id="icon-diamond" viewBox="0 0 24 24" fill="none" stroke="currentColor"
        stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0Z"/>
</symbol>

Docstring standard

Every function, class, and method added in this ticket must carry a NumPy-style docstring with at minimum: one-line summary, Parameters section, Returns section, Raises section (if applicable). No multi-paragraph inline comments. See intel_type_page docstring (ui_intel.py:1966) as the canonical template.


Theme references

Element Token
Page accent color var(--color-accent)
Subheader gradient intel-subhd-title--spectral
function badge var(--color-accent)
async_function badge var(--color-orange)
class badge var(--color-purple)
method badge var(--color-teal)
async_method badge var(--color-rose)
Mono font var(--font-mono)
Muted text var(--text-muted)

Acceptance criteria

  • Phase 1: ApiSurfaceProvider batch upserts ≤ ceil(N/1000) SQL statements
  • Phase 2: All 30 tests written and RED before any route code exists
  • Phase 3: Route returns 200 with data, 404 for unknown repo, coerces bad params
  • Phase 4: Template renders all 6 stat chips, filter bar, symbol list, empty state
  • Phase 5: SCSS compiles clean (npm run build), page matches design above
  • Phase 6: Dashboard card shows live count; diamond icon in sprite
  • All 30 tests GREEN after implementation
  • No subprocess called at request time
  • Page live on staging at /{owner}/{repo}/intel/api-surface

Files to create / modify

File Action
musehub/services/musehub_intel_providers.py modify ApiSurfaceProvider (batch upsert)
tests/test_intel_api_surface.py create (30 tests, TDD-first)
musehub/api/routes/musehub/ui_intel.py add intel_api_surface_page + dashboard ctx
musehub/templates/musehub/pages/intel_api_surface.html create
musehub/templates/musehub/pages/intel_dashboard.html add API surface card
musehub/templates/musehub/partials/_icon_sprite.html add icon-diamond if missing
src/scss/pages/_api_surface.scss create
src/scss/components/_api_surface.scss create
src/scss/app.scss + _components.scss add @use imports
Activity1
gabriel opened this issue 48 days ago
gabriel 48 days ago

All acceptance criteria met and verified on staging.

Delivered:

  • ApiSurfaceProvider rewrote from _run_muse subprocess → pure-Python parse_symbols() on stored snapshot objects (mirrors TypeProvider). Fixes staging where no on-disk repo checkout exists.
  • Batch upsert in chunks of 1,000 rows (≤ ceil(N/1000) SQL statements).
  • Route handler intel_api_surface_page at /{owner}/{repo}/intel/api-surface — kind filter, top clamp, 404 on unknown repo.
  • Template with 6 stat chips, kind filter bar, symbol list (file::name split), kind badges, empty state.
  • SCSS: pages/_api_surface.scss + components/_api_surface.scss, intel-wrap layout consistent with all other intel subpages.
  • Dashboard card with kind breakdown rows, proportional bars, count + %.
  • icon-diamond added to SVG sprite.
  • 30-test TDD suite — all GREEN.
  • Live on staging: https://staging.musehub.ai/gabriel/musehub/intel/api-surface (8,372 public symbols indexed).