feat: intel/api-surface GUI page — muse code api-surface web dashboard
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)
- File part (before
- 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 cardmusehub/api/routes/musehub/ui_intel.py— register route + addapi_surface_countto 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:
ApiSurfaceProviderbatch 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;
diamondicon 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 |
All acceptance criteria met and verified on staging.
Delivered:
ApiSurfaceProviderrewrote from_run_musesubprocess → pure-Pythonparse_symbols()on stored snapshot objects (mirrorsTypeProvider). Fixes staging where no on-disk repo checkout exists.intel_api_surface_pageat/{owner}/{repo}/intel/api-surface— kind filter, top clamp, 404 on unknown repo.pages/_api_surface.scss+components/_api_surface.scss,intel-wraplayout consistent with all other intel subpages.icon-diamondadded to SVG sprite.