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

feat(intel): Clone Browser — visual GUI for muse code clones

0 Anchors
Blast radius
Churn 30d
0 Proposals

Context

muse code clones surfaces duplicate and near-duplicate symbols across the repo. The worker already indexes the data into musehub_intel_clones via ClonesProvider (registered as intel.code.clones). Only the UI layer is missing.

Current state — what already exists

Layer File Status
DB migration alembic/versions/0004_phase1_intel_tables.py ✅ shipped
ORM model musehub/db/musehub_models.py::MusehubIntelClones ✅ shipped
Worker provider musehub/services/musehub_intel_providers.py::ClonesProvider ✅ shipped
Job type key intel.code.clones in _PROVIDER_REGISTRY ✅ shipped
Provider unit tests tests/test_phase2_intel_providers.py (Layer 9) ✅ shipped
Route handler musehub/api/routes/musehub/ui_intel.py ❌ missing
List template musehub/templates/musehub/pages/intel_clones.html ❌ missing
Detail template musehub/templates/musehub/pages/intel_clones_detail.html ❌ missing
SCSS layout src/scss/pages/_clones.scss ❌ missing
SCSS visuals src/scss/components/_clones.scss ❌ missing
Dashboard card intel_dashboard.html — 9th intel-card ❌ missing

muse code clones --json output shape (confirmed against musehub repo)

{
  "exact_clone_clusters": 222,
  "near_clone_clusters":  262,
  "total_symbols_involved": 2370,
  "file_hotspots": [
    {"file": "docs/reference/type-contracts.md", "clone_symbols": 341},
    {"file": "docs/reference/mcp.md",            "clone_symbols": 81},
    ...
  ],
  "clusters": [
    {
      "tier":         "exact" | "near",
      "cluster_hash": "sha256:0dc8128bee43",
      "member_count": 106,
      "members": [
        {
          "address":      "musehub/api/routes/api/auth.py::logger",
          "kind":         "variable",
          "language":     "Python",
          "body_hash":    "sha256:0dc8128bee43",
          "signature_id": "sha256:2686af9f25e1",
          "content_id":   "sha256:0dc8128bee43"
        },
        ...
      ]
    }
  ]
}

DB schema (musehub_intel_clones)

repo_id      VARCHAR(128) PK  FK → musehub_repos
cluster_hash VARCHAR(128) PK
tier         VARCHAR(32)      -- "exact" | "near"
member_count INTEGER
members_json TEXT             -- JSON array of member objects
ref          VARCHAR(128)

Phase 1 — Route handlers (load-bearing first)

Add to musehub/api/routes/musehub/ui_intel.py.

Helper functions

_VALID_CLONE_TOPS = (20, 50, 100)
_VALID_CLONE_TIERS = ("", "exact", "near")   # "" = all

def _cl_tier_class(tier: str) -> str:
    """Map clone tier to CSS modifier class.

    Parameters
    ----------
    tier:
        ``"exact"`` or ``"near"``.

    Returns
    -------
    ``"cl-badge--exact"`` or ``"cl-badge--near"``.
    """
    return "cl-badge--exact" if tier == "exact" else "cl-badge--near"


def _cl_language_set(members_json: str) -> list[str]:
    """Return sorted distinct languages from a cluster's members JSON blob.

    Parameters
    ----------
    members_json:
        Raw JSON string stored in ``MusehubIntelClones.members_json``.

    Returns
    -------
    Alphabetically sorted list of distinct language names, e.g.
    ``["Markdown", "Python"]``.  Returns ``["—"]`` on parse error.
    """
    ...


def _cl_file_count(members_json: str) -> int:
    """Count distinct source files in a cluster's member list.

    Parameters
    ----------
    members_json:
        Raw JSON string stored in ``MusehubIntelClones.members_json``.

    Returns
    -------
    Number of distinct files.  Returns 0 on parse error.
    """
    ...


def _cl_is_cross_file(members_json: str) -> bool:
    """Return True when cluster members span more than one source file.

    Cross-file exact clones are higher-priority refactor candidates than
    same-file duplicates because they indicate copy-pasted logic that drifts
    independently.

    Parameters
    ----------
    members_json:
        Raw JSON string stored in ``MusehubIntelClones.members_json``.

    Returns
    -------
    ``True`` if ``_cl_file_count(members_json) > 1``, else ``False``.
    """
    ...

intel_clones_page

@router.get("/{owner}/{repo_slug}/intel/clones", summary="Clone cluster browser")
async def intel_clones_page(
    request: Request,
    owner: SlugParam,
    repo_slug: SlugParam,
    tier: str = "",
    top: int = 20,
    db: AsyncSession = Depends(get_db),
) -> Response:
    """Duplicate-code cluster browser — exact and near-clone index.

    Reads from ``musehub_intel_clones`` ordered by ``member_count DESC``.
    Parses each row's ``members_json`` blob to derive per-cluster language set,
    file count, and cross-file flag.  No subprocess calls at request time.

    Parameters
    ----------
    tier:
        Filter to ``"exact"`` or ``"near"`` clusters; ``""`` returns both.
        Silently coerces invalid values to ``""`` (all).
    top:
        Number of clusters to show; clamped to ``_VALID_CLONE_TOPS``
        (default 20, max 100).

    Returns
    -------
    Rendered ``intel_clones.html`` with context:
    ``clusters``, ``total_count``, ``exact_count``, ``near_count``,
    ``total_symbols``, ``file_hotspots``, ``selected_tier``, ``selected_top``,
    ``valid_tops``, ``valid_tiers``.

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

intel_clones_detail_page

@router.get("/{owner}/{repo_slug}/intel/clones/detail", summary="Clone cluster detail")
async def intel_clones_detail_page(
    request: Request,
    owner: SlugParam,
    repo_slug: SlugParam,
    cluster: str = "",
    db: AsyncSession = Depends(get_db),
) -> Response:
    """Per-cluster member browser — all duplicate symbols in one cluster.

    Fetches the single ``musehub_intel_clones`` row identified by
    ``cluster_hash == cluster``, parses ``members_json``, and groups members
    by source file for the file-breakdown section.

    Parameters
    ----------
    cluster:
        Full or prefix-matched ``cluster_hash`` (``sha256:<hex>``).
        Returns the empty-state template when ``cluster`` is blank.

    Returns
    -------
    Rendered ``intel_clones_detail.html`` with context:
    ``cluster_data`` (or ``None``), ``cluster_hash``.

    ``cluster_data`` dict keys:
        ``tier``, ``tier_class``, ``member_count``, ``members``,
        ``file_count``, ``is_cross_file``, ``languages``,
        ``files_breakdown`` (list of ``{file, count, members}``), ``ref``.

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

Phase 2 — Jinja2 templates

A. intel_clones.html — list page

┌─────────────────────────────────────────────────────────────────────────────┐
│ breadcrumb: owner / repo / intel / clones                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ← Intel Hub     [copy] CLONES           ← .intel-subhd-title--spectral    │
│  Duplicate symbols grouped into exact and near-clone clusters.              │
│                                                                             │
├──────────┬───────────┬──────────────┬────────────────────────────────────┤
│  222     │  262      │  484         │  2370                              │
│  exact   │  near     │  total       │  symbols                           │
│  .cl-stat                                                                  │
├──────────┴───────────┴──────────────┴────────────────────────────────────┤
│                                                                             │
│  FILE HOTSPOTS    (top 5 bars, .cl-hotspot-*)                               │
│  docs/reference/type-contracts.md  ████████████████████ 341               │
│  docs/reference/mcp.md             █████████            81                │
│  tests/test_mcp_write_tools.py     ████                 35                │
│  tests/test_mcp_mist_tools.py      ███                  29                │
│  tests/test_mist_routes.py         ███                  24                │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│  filter: [All] [Exact] [Near]                    top [20] [50] [100]        │
│  .intel-filter-pill / .intel-filter-pill--active                           │
├─────────────────────────────────────────────────────────────────────────────┤
│  CLUSTER LIST  (.cl-list)                                                   │
│                                                                             │
│  ┌──────────────────────────────────────────────────────────────────────┐  │
│  │ [EXACT] sha256:0dc8128b…  ██████████████████████████████  106  ×86f │  │
│  │          1 file · Python                                  .cl-row    │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────────────────────┐  │
│  │ [NEAR]  sha256:7a4bd12e…  ██████████████████████          51   ×51f │  │
│  │          12 files · Python  [⚡ cross-file]               .cl-row    │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
│  ...                                                                         │
│                                                                             │
│  (empty state — .cl-empty)                                                  │
│  [copy icon 32px]                                                           │
│  No clone clusters yet. Push a commit to trigger intel.code.clones.        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Key template elements:

  • .intel-subhd + .intel-subhd-title--spectral with {{ icon("copy", 16) }}
  • .cl-stats row: 4 chips — exact count, near count, total clusters, total symbols
  • .cl-hotspots bar list (top 5 files, sorted by clone_symbols)
  • .intel-filter-pill / .intel-filter-pill--active for tier + top filters
  • .cl-list.cl-row per cluster (clickable <a> → detail page)
  • .cl-badge.cl-badge--exact (color: var(--color-accent)) or .cl-badge--near (color: var(--color-warning))
  • .cl-bar-fill proportional to member_count / max_member_count
  • Cross-file badge .cl-cross-file shows {{ icon("split", 12) }} cross-file in warning color

B. intel_clones_detail.html — cluster detail page

┌─────────────────────────────────────────────────────────────────────────────┐
│ breadcrumb: owner / repo / intel / clones / detail                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ← Clones     [copy] Cluster Detail      ← .intel-subhd-title--spectral    │
│                                                                             │
│  .cl-detail-hd                                                              │
│  sha256:0dc8128bee43                                                        │
│  exact · 106 members · 1 file · Python                                     │
│                                                                             │
├──────────┬───────────┬──────────────┬────────────────────────────────────┤
│ [EXACT]  │ 106       │ 1            │ Python                             │
│ tier     │ members   │ file         │ language                           │
│  .cl-detail-chip                                                            │
├──────────┴───────────┴──────────────┴────────────────────────────────────┤
│                                                                             │
│  MEMBERS  (.cl-member-list)                                                 │
│  musehub/api/routes/api/auth.py::logger           [variable]  Python      │
│  musehub/api/routes/api/identities.py::logger     [variable]  Python      │
│  musehub/api/routes/api/orgs.py::logger            [variable]  Python      │
│  ... (paginated / collapsed after 20)                                       │
│                                                                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  FILE BREAKDOWN  (.cl-file-breakdown)                                       │
│  musehub/api/routes/api/auth.py           ██  1 member                    │
│  musehub/api/routes/api/identities.py     ██  1 member                    │
│  musehub/api/routes/api/orgs.py           ██  1 member                    │
│  ... (top 10 files by member count)                                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Key elements:

  • .cl-detail-hd — hash + meta line (tier · N members · N files · language)
  • .cl-detail-chip ×4 — tier badge, member count (accent), file count, primary language
  • .cl-member-list — scrollable list, each row: monospace address + kind badge + language pill
  • .cl-file-breakdown — one bar per file, width proportional to member share
  • Symbol address links → {{ base_url }}/symbol/{{ m.address | urlencode }} (future-proof)

C. Dashboard card (9th card)

Add after the Velocity card in intel_dashboard.html:

{# Clones #}
<div class="intel-card">
  <div class="intel-card-hd">
    <span class="intel-card-title">
      <span style="color:var(--color-warning)">{{ icon("copy", 12) }}</span> CLONES
    </span>
    <a href="{{ base_url }}/intel/clones" class="intel-card-more">View all →</a>
  </div>
  {% if clones_count > 0 %}
  <div class="intel-dead-summary">
    <span class="intel-dead-count" style="color:var(--color-warning)">
      {{ clones_count | fmtnum }}
    </span> cluster{{ "s" if clones_count != 1 }}
    &nbsp;·&nbsp;
    <span style="color:var(--color-warning)">
      {{ clones_symbols | fmtnum }}
    </span> symbols
  </div>
  <ul class="intel-dead-list">
    {% for c in clones_preview %}
    <li class="intel-dead-row">
      <a href="{{ base_url }}/intel/clones/detail?cluster={{ c.cluster_hash | urlencode }}"
         class="intel-dead-addr font-mono"
         title="{{ c.cluster_hash }}">
        {{ c.cluster_hash[7:19] }}…
      </a>
      <span class="cl-badge cl-badge--{{ c.tier }}" style="font-size:0.6rem;">
        {{ c.tier }}
      </span>
      <span class="intel-dead-age">{{ c.member_count | fmtnum }}×</span>
    </li>
    {% endfor %}
  </ul>
  {% else %}
  <div class="intel-card-empty">No clone clusters yet.</div>
  {% endif %}
</div>

intel_dashboard_page must add these context variables (same pattern as velocity_count/velocity_preview):

# Top 5 clusters by member_count
clones_result = await db.execute(
    sa.select(dbm.MusehubIntelClones)
    .where(dbm.MusehubIntelClones.repo_id == repo_id)
    .order_by(sa.desc(dbm.MusehubIntelClones.member_count))
    .limit(5)
)
clones_preview = clones_result.scalars().all()
clones_count_result = await db.execute(
    sa.select(sa.func.count())
    .select_from(dbm.MusehubIntelClones)
    .where(dbm.MusehubIntelClones.repo_id == repo_id)
)
clones_count = clones_count_result.scalar_one_or_none() or 0
clones_symbols_result = await db.execute(
    sa.select(sa.func.sum(dbm.MusehubIntelClones.member_count))
    .where(dbm.MusehubIntelClones.repo_id == repo_id)
)
clones_symbols = clones_symbols_result.scalar_one_or_none() or 0

Phase 3 — SCSS

Two new SCSS files following the structural/visual split used by velocity:

src/scss/pages/_clones.scss (layout only — no colors)

// ─────────────────────────────────────────────────────────────────────────────
// Page: Clones  (.cl-* layout)
// STRUCTURAL LAYOUT ONLY — zero colors, zero typography.
// Visual rules live in components/_clones.scss.
// ─────────────────────────────────────────────────────────────────────────────

.cl-wrap { padding: 0; }

// ── Stat chips ────────────────────────────────────────────────────────────────
.cl-stats {
  display: flex;
  gap: 1rem;
  margin-bottom: 1.25rem;
  @media (max-width: 540px) { flex-wrap: wrap; }
}
.cl-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  padding: 0.75rem 1.25rem;
  flex: 1;
}
.cl-stat__val { display: block; }
.cl-stat__lbl { display: block; }

// ── File hotspots ─────────────────────────────────────────────────────────────
.cl-hotspots { margin-bottom: 1.5rem; }
.cl-hotspot-row {
  display: grid;
  grid-template-columns: minmax(180px, 1fr) 1fr 3.5em;
  align-items: center;
  gap: 0.75rem;
  padding: 0.35rem 1rem;
}
.cl-hotspot-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl-hotspot-track { height: 3px; border-radius: 2px; overflow: hidden; }
.cl-hotspot-fill  { height: 100%; }
.cl-hotspot-val   { text-align: right; }

// ── Cluster list ──────────────────────────────────────────────────────────────
.cl-list { margin-bottom: 1.5rem; }
.cl-row {
  display: grid;
  grid-template-columns: 4rem minmax(120px, 200px) 1fr auto auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 1rem;
  text-decoration: none;
  @media (max-width: 800px) {
    grid-template-columns: 4rem minmax(100px, 160px) 1fr auto;
    .cl-row__meta { display: none; }
  }
  @media (max-width: 480px) {
    grid-template-columns: 4rem 1fr auto;
    .cl-row__bar-wrap { display: none; }
  }
}
.cl-row__hash     { font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl-row__bar-wrap { flex: 1; display: flex; align-items: center; gap: 0.4rem; }
.cl-row__bar-track { flex: 1; height: 4px; border-radius: 2px; overflow: hidden; }
.cl-row__bar-fill  { height: 100%; border-radius: 2px; }
.cl-row__count    { width: 2.5em; text-align: right; }
.cl-row__meta     { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }

// ── Detail page ───────────────────────────────────────────────────────────────
.cl-detail-hd { margin-bottom: 1rem; }
.cl-detail-hash { font-family: var(--font-mono); }
.cl-detail-meta { }

.cl-detail-chips {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
}
.cl-detail-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  padding: 0.7rem 1.1rem;
}
.cl-detail-chip__val { display: block; }
.cl-detail-chip__lbl { display: block; }

.cl-member-list   { margin-bottom: 1.5rem; }
.cl-member-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 1rem;
  @media (max-width: 540px) { grid-template-columns: 1fr auto; }
}
.cl-member-addr { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cl-file-breakdown { margin-bottom: 1.5rem; }
.cl-file-row {
  display: grid;
  grid-template-columns: minmax(160px, 1fr) 120px 4em;
  align-items: center;
  gap: 0.75rem;
  padding: 0.35rem 1rem;
}
.cl-file-name  { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl-file-track { height: 3px; border-radius: 2px; overflow: hidden; }
.cl-file-fill  { height: 100%; }
.cl-file-count { text-align: right; }

.cl-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 3rem 1rem;
  text-align: center;
}

src/scss/components/_clones.scss (visuals only — no layout)

// ─────────────────────────────────────────────────────────────────────────────
// Component: Clones  (.cl-* visuals)
// VISUAL RULES ONLY — no layout or sizing here.
// ─────────────────────────────────────────────────────────────────────────────

// ── Stat chips ────────────────────────────────────────────────────────────────
.cl-stat {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
}
.cl-stat__val {
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--color-accent);
  font-variant-numeric: tabular-nums;
}
.cl-stat__lbl {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
}

// ── Tier badge ────────────────────────────────────────────────────────────────
.cl-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.15em 0.55em;
  border-radius: 3px;
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
}
.cl-badge--exact {
  background: color-mix(in srgb, var(--color-accent) 18%, transparent);
  color: var(--color-accent);
  border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
}
.cl-badge--near {
  background: color-mix(in srgb, var(--color-warning) 18%, transparent);
  color: var(--color-warning);
  border: 1px solid color-mix(in srgb, var(--color-warning) 35%, transparent);
}

// ── Cross-file badge ──────────────────────────────────────────────────────────
.cl-cross-file {
  display: inline-flex;
  align-items: center;
  gap: 0.2em;
  font-size: 0.62rem;
  color: var(--color-warning);
  opacity: 0.85;
}

// ── File hotspots ─────────────────────────────────────────────────────────────
.cl-hotspot-section-title {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
  margin-bottom: 0.5rem;
  padding: 0 1rem;
}
.cl-hotspot-row:hover { background: var(--bg-overlay); }
.cl-hotspot-file {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--fg-default);
}
.cl-hotspot-track { background: var(--bg-overlay); }
.cl-hotspot-fill  { background: var(--color-warning); opacity: 0.7; }
.cl-hotspot-val {
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg-muted);
}

// ── Cluster list ──────────────────────────────────────────────────────────────
.cl-list {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.cl-row {
  border-bottom: 1px solid var(--border-subtle);
  color: var(--fg-default);
  transition: background 0.1s;
  &:last-child { border-bottom: none; }
  &:hover { background: var(--bg-overlay); }
}
.cl-row__hash { font-size: 0.78rem; color: var(--fg-muted); }
.cl-row__bar-track { background: var(--bg-overlay); }
.cl-row__bar-fill--exact { background: var(--color-accent); opacity: 0.7; }
.cl-row__bar-fill--near  { background: var(--color-warning); opacity: 0.7; }
.cl-row__count {
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg-default);
  font-weight: 500;
}

// ── Detail page ───────────────────────────────────────────────────────────────
.cl-detail-hash {
  font-size: 0.95rem;
  color: var(--fg-muted);
  margin-bottom: 0.25rem;
}
.cl-detail-meta {
  font-size: 0.8rem;
  color: var(--fg-muted);
}
.cl-detail-chip {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  min-width: 80px;
}
.cl-detail-chip__val {
  font-size: 1.35rem;
  font-weight: 600;
  color: var(--fg-default);
  font-variant-numeric: tabular-nums;
  &--accent { color: var(--color-accent); }
  &--warn   { color: var(--color-warning); }
}
.cl-detail-chip__lbl {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
}

.cl-section-title {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
  padding: 0 1rem;
  margin-bottom: 0.5rem;
}

.cl-member-list {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.cl-member-row {
  border-bottom: 1px solid var(--border-subtle);
  &:last-child { border-bottom: none; }
  &:hover { background: var(--bg-overlay); }
}
.cl-member-addr {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--fg-default);
}
.cl-kind-badge {
  font-size: 0.62rem;
  padding: 0.1em 0.4em;
  border-radius: 3px;
  background: var(--bg-overlay);
  color: var(--fg-muted);
  text-transform: lowercase;
}
.cl-lang-pill {
  font-size: 0.62rem;
  padding: 0.1em 0.45em;
  border-radius: 3px;
  background: color-mix(in srgb, var(--color-teal) 15%, transparent);
  color: var(--color-teal);
}

.cl-file-breakdown {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  overflow: hidden;
  padding: 0.5rem 0;
}
.cl-file-row:hover { background: var(--bg-overlay); }
.cl-file-name { font-family: var(--font-mono); font-size: 0.8rem; color: var(--fg-default); }
.cl-file-track { background: var(--bg-overlay); }
.cl-file-fill  { background: var(--color-accent); opacity: 0.5; }
.cl-file-count { font-size: 0.75rem; color: var(--fg-muted); font-variant-numeric: tabular-nums; }

// ── Empty state ───────────────────────────────────────────────────────────────
.cl-empty { color: var(--fg-muted); }

Wire both into src/scss/app.scss under the // ── Pages and // ── Components sections respectively.


Phase 4 — Dashboard context

Extend intel_dashboard_page in ui_intel.py to supply:

"clones_count":   clones_count,    # int — total clusters for this repo
"clones_symbols": clones_symbols,  # int — sum(member_count)
"clones_preview": [                 # top 5 by member_count
    {
        "cluster_hash": row.cluster_hash,
        "tier":         row.tier,
        "member_count": row.member_count,
    }
    for row in clones_preview
],

Rebuild the local Docker image after this change (same Python-only rebuild pattern used for the velocity card: docker compose build musehub && docker compose up -d musehub).


Phase 5 — TDD: all seven tiers

Tier 1 — Unit

File: tests/test_clones_unit.py

Test the pure helper functions in isolation (no DB, no HTTP).

ID Test Assertion
U01 _cl_tier_class("exact") returns "cl-badge--exact"
U02 _cl_tier_class("near") returns "cl-badge--near"
U03 _cl_tier_class("unknown") returns "cl-badge--near" (safe default)
U04 _cl_language_set('[{"language":"Python"},{"language":"Python"}]') ["Python"]
U05 _cl_language_set('[{"language":"Python"},{"language":"Markdown"}]') ["Markdown","Python"]
U06 _cl_language_set("invalid json") ["—"]
U07 _cl_language_set("[]") ["—"]
U08 _cl_file_count on 3 members across 2 files 2
U09 _cl_file_count on 3 members in 1 file 1
U10 _cl_file_count("") 0
U11 _cl_is_cross_file — 2 files True
U12 _cl_is_cross_file — 1 file False
U13 _cl_is_cross_file — malformed JSON False

Tier 2 — Integration

File: tests/test_clones_integration.py

Test routes with a real async DB session and fixture rows. Use AsyncClient + lifespan.

ID Test Assertion
I01 GET /gabriel/musehub/intel/clones — repo with 5 clusters 200, all 5 in body
I02 GET …/intel/clones — repo with no clusters 200, empty state rendered
I03 GET …/intel/clones?tier=exact only exact clusters in response
I04 GET …/intel/clones?tier=near only near clusters in response
I05 GET …/intel/clones?tier=invalid coerces to all, 200
I06 GET …/intel/clones?top=50 top pill 50 has active class
I07 GET …/intel/clones?top=9999 clamps to default (20), 200
I08 GET …/intel/clones/detail?cluster=<hash> — known cluster 200, member list present
I09 GET …/intel/clones/detail?cluster=unknown 200, empty state rendered
I10 GET …/intel/clones/detail — no cluster param 200, empty state rendered
I11 GET …/intel dashboard — with clones rows clones card shows correct count
I12 GET …/intel dashboard — no clones rows card shows empty state, no 500
I13 Detail page — members grouped by file correctly files_breakdown len = distinct files
I14 Detail page — cross-file cluster shows cross-file badge cl-cross-file in body
I15 Detail page — same-file cluster hides cross-file badge cl-cross-file absent

Tier 3 — End-to-end

File: tests/test_clones_e2e.py

Full navigation flow using the test client; no mocking.

ID Test Flow
E01 Intel Hub → Clones list Dashboard intel/clones link renders list
E02 Clones list → Cluster detail Click cl-row link → detail page with hash in URL
E03 Detail → back to Clones ← Clones breadcrumb link returns to list
E04 Tier filter round-trip ?tier=exact → only exact shown; switch to ?tier=near
E05 Top filter round-trip ?top=50 → pill active; cluster count ≤ 50
E06 Empty repo path No clusters → empty state at /intel/clones; no 500 at /intel

Tier 4 — Stress

File: tests/test_clones_stress.py

ID Test Fixture Assertion
S01 List page — 500 clusters Insert 500 rows via factory Response < 500ms
S02 Detail page — 300 members members_json with 300-element array Response < 200ms
S03 Dashboard card — 10 000 total clusters Count query still < 50ms 200, card renders
S04 Tier filter — 500 exact + 500 near ?tier=exact returns ≤ 20, query < 100ms correct subset
S05 File hotspots — 50 files in one cluster All 50 files in breakdown No truncation bug

Tier 5 — State integrity

File: tests/test_clones_state_integrity.py

ID Test Assertion
SI01 members_json is always parseable All rows in DB parse without json.JSONDecodeError
SI02 tier values are only "exact" or "near" No rows outside this set
SI03 member_count matches len(json.loads(members_json)) No count/content mismatch
SI04 ClonesProvider upsert — idempotent Running twice with same payload → same row count
SI05 ClonesProvider upsert — update Re-run with new member_count overwrites existing
SI06 CASCADE delete — repo deleted All musehub_intel_clones rows for repo are gone

Tier 6 — Performance

File: tests/test_clones_performance.py

Use time.perf_counter() assertions; not pytest-benchmark (no extra dep).

ID Test Threshold
P01 List query — 500 rows < 50ms
P02 Detail query — 1 row < 10ms
P03 Dashboard count + top-5 query < 20ms total
P04 _cl_language_set on 300-member JSON < 1ms
P05 _cl_file_count on 300-member JSON < 1ms
P06 Full render — 20 clusters via template < 100ms

Tier 7 — Security

File: tests/test_clones_security.py

ID Test Vector Assertion
SEC01 Path traversal in cluster param cluster=../../etc/passwd 200 or 400, not 500; no file read
SEC02 SQL injection in tier param tier=exact' OR '1'='1 Coerces to "", no DB error
SEC03 SQL injection in top param top=1;DROP TABLE musehub_intel_clones-- 422 (FastAPI int validation)
SEC04 XSS in member address field address = <script>alert(1)</script> HTML-escaped in response
SEC05 XSS in members_json cluster hash crafted hash with <img onerror=...> HTML-escaped
SEC06 Oversized cluster param 4 096-char string 400 or graceful 200 empty state
SEC07 members_json with embedded HTML "address":"<b>bold</b>::fn" Tags escaped in template output

Strong docstring requirements

Every new function, class, and method must carry a Google-style docstring that documents:

  • Purpose — what problem it solves (not what the code does)
  • Parameters — with types, allowed values, and what happens on invalid input
  • Returns — exact structure, with examples where the shape is non-obvious
  • Raises — only genuine exceptions (not catch-all)
  • Why — one sentence explaining any non-obvious design choice (e.g. why members_json is parsed in Python rather than stored as denormalized columns)

Avoid restating the function name or its obvious mechanics. Focus on invariants, edge cases, and the data contract.


Spectral theme checklist

Every visual element must use design tokens from variables.scss:

Element Token
Page title gradient .intel-subhd-title--spectralvar(--gradient-spectral)
Exact tier badge bg color-mix(in srgb, var(--color-accent) 18%, transparent)
Near tier badge bg color-mix(in srgb, var(--color-warning) 18%, transparent)
Exact bar fill var(--color-accent)
Near bar fill var(--color-warning)
Cross-file badge var(--color-warning)
Chip __val color var(--color-accent)
Section title text var(--fg-muted)
Mono hash text var(--font-mono)
Card border var(--border-default)
Hover row bg var(--bg-overlay)

Acceptance criteria

  • Phase 1intel_clones_page and intel_clones_detail_page routes added; all helper functions have full docstrings; registered with @router.get(...) decorators
  • Phase 2intel_clones.html + intel_clones_detail.html render with correct intel-subhd-title--spectral; dashboard card present; no template references undefined context variables
  • Phase 3_clones.scss (pages + components) added and wired into app.scss; SCSS compiles without errors; CI build passes
  • Phase 4intel_dashboard_page supplies clones_count, clones_symbols, clones_preview; no 500 when no clone data exists
  • Phase 5 — All 7 test tiers pass; muse code test --json reports green for all clones test files
  • Median page load for /intel/clones (500 clusters) < 200ms
  • No muse code clones subprocess runs at request time — reads DB only
  • members_json parse errors degrade gracefully (no 500)
  • All 484 clusters in the musehub repo render without visual regressions after deploy
Activity4
gabriel opened this issue 47 days ago
gabriel 47 days ago

Phase 1 complete ✅

Commit: sha256:6ed177822077 on task/intel-clones-ui

What shipped

Route handlers added to musehub/api/routes/musehub/ui_intel.py:

  • intel_clones_page — tier (exact/near/all) + top (20/50/100) filters; per-cluster language set, file count, and cross-file flag derived from members_json at request time; repo-wide file hotspot aggregation; zero muse code subprocesses
  • intel_clones_detail_page — single cluster lookup by cluster_hash; full member parse + file breakdown grouping; graceful empty state on blank/unknown hash
  • intel_dashboard_page extended with clones_count, clones_symbols, clones_preview (top 5 by member_count)

6 pure helper functions with full Google-style docstrings (purpose, params, returns, edge cases, design rationale): _cl_tier_class, _cl_language_set, _cl_file_count, _cl_is_cross_file, _cl_parse_members, _cl_files_breakdown

Tier 1 unit teststests/test_clones_unit.py: 31/31 passing

Up next

Phase 2 — Jinja2 templates:

  • intel_clones.html (list page: stat chips, file hotspot bars, tier filter, cluster list with cross-file badge)
  • intel_clones_detail.html (cluster detail: member table, file breakdown)
  • intel_dashboard.html — 9th intel-card (clones card)
gabriel 47 days ago

Phases 2 & 3 complete ✅ — live on staging

Commits: sha256:9b614e349d56 (templates) · sha256:d71bb3045dc7 (SCSS) Staging deploy: d71bb304-20260503093726 (slot: blue)

Phase 2 — Templates

intel_clones.html (list page):

  • intel-subhd with copy icon + spectral gradient title
  • 4 stat chips: exact clusters, near clusters, total, symbols involved
  • File hotspot bar list (top 5 by clone density, proportional widths)
  • Tier filter (all/exact/near) + top filter (20/50/100) — URL state preserved on navigation
  • Cluster list rows: tier badge, truncated hash, proportional bar fill, member count, language pills, cross-file badge when is_cross_file
  • Empty states for zero data and filtered-to-zero

intel_clones_detail.html (cluster detail):

  • Cluster header: full hash + meta line (tier · N members · N files · language)
  • 4 detail chips: tier badge, member count (accent), file count (warn if cross-file), language(s)
  • Full member table: address (mono), kind badge, language pill
  • File breakdown bar section (only rendered when file_count > 1)
  • Empty state for blank/unknown cluster param

intel_dashboard.html — 9th intel-card (Clones):

  • copy icon in var(--color-warning), cluster count + symbols count summary
  • Top-5 list with truncated hash, tier badge, member count

Phase 3 — SCSS

  • src/scss/pages/_clones.scss — structural layout, zero colors; responsive at 800px / 480px
  • src/scss/components/_clones.scss — all visual tokens: color-mix tier badges, --color-accent (exact) / --color-warning (near) bar fills, --color-teal language pills, --font-mono hashes
  • app.scss wired: @use "components/clones" + @use "pages/clones" as page-clones
  • SCSS build clean (sass --style=compressed, no errors or warnings)

Verified locally

  • GET /gabriel/musehub/intel/clones → 200
  • GET /gabriel/musehub/intel/clones/detail → 200
  • GET /gabriel/musehub/intel → 200 (dashboard card renders)

Up next

Phases 4–5 (TDD tiers 2–7): integration, E2E, stress, state integrity, performance, security tests — tests/test_clones_integration.py through tests/test_clones_security.py

gabriel 47 days ago

All fidelity gaps closed and dashboard gate fixed in this push (71e4eb32).

Changes:

  • StableProvider: days_stable now uses calendar days from commit timestamps, matching the CLI's time unit
  • EntangleProvider: co_change_rate now uses Jaccard (co / |union|) — was using min, which inflated rates
  • intel_hotspots_page: migrated off legacy load_intel_snapshot path; now queries MusehubSymbolIntel.churn_30d directly, consistent with all other intel routes
  • intel_dashboard.html: removed the index_meta gate that was hiding all cards when no intel summary existed; no-index notice is now a banner alongside the cards so all subpage links are always present and navigable
  • 8 stale phase-2 tests deleted (mocked subprocess against providers that were already rewritten to query the DB)
  • 7 new fidelity tests (F01–F07) in test_intel_fidelity.py covering all three provider fixes and the hotspots route migration

79/79 tests green.

gabriel 47 days ago

Issue complete ✅ — closing

All layers are live on staging:

  • Clone browser UI (list + detail pages, dashboard card, SCSS) ✅
  • E2E, integration, performance, security, stress tests ✅
  • Provider fidelity: days_stable calendar days, co_change_rate Jaccard, hotspots off legacy snapshot ✅
  • Dashboard gate removed — all intel cards always visible ✅
  • Clones data now populating on staging (confirmed after push 71e4eb32)