feat(intel): Clone Browser — visual GUI for muse code clones
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--spectralwith{{ icon("copy", 16) }}.cl-statsrow: 4 chips — exact count, near count, total clusters, total symbols.cl-hotspotsbar list (top 5 files, sorted byclone_symbols).intel-filter-pill/.intel-filter-pill--activefor tier + top filters.cl-list→.cl-rowper cluster (clickable<a>→ detail page).cl-badge.cl-badge--exact(color:var(--color-accent)) or.cl-badge--near(color:var(--color-warning)).cl-bar-fillproportional tomember_count / max_member_count- Cross-file badge
.cl-cross-fileshows{{ icon("split", 12) }} cross-filein 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 }}
·
<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_jsonis 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--spectral → var(--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 1 —
intel_clones_pageandintel_clones_detail_pageroutes added; all helper functions have full docstrings; registered with@router.get(...)decorators - Phase 2 —
intel_clones.html+intel_clones_detail.htmlrender with correctintel-subhd-title--spectral; dashboard card present; no template references undefined context variables - Phase 3 —
_clones.scss(pages + components) added and wired intoapp.scss; SCSS compiles without errors; CI build passes - Phase 4 —
intel_dashboard_pagesuppliesclones_count,clones_symbols,clones_preview; no 500 when no clone data exists - Phase 5 — All 7 test tiers pass;
muse code test --jsonreports green for all clones test files - Median page load for
/intel/clones(500 clusters) < 200ms - No
muse code clonessubprocess runs at request time — reads DB only members_jsonparse errors degrade gracefully (no 500)- All 484 clusters in the musehub repo render without visual regressions after deploy
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-subhdwithcopyicon + 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
clusterparam
intel_dashboard.html — 9th intel-card (Clones):
copyicon invar(--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 / 480pxsrc/scss/components/_clones.scss— all visual tokens:color-mixtier badges,--color-accent(exact) /--color-warning(near) bar fills,--color-teallanguage pills,--font-monohashesapp.scsswired:@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→ 200GET /gabriel/musehub/intel/clones/detail→ 200GET /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
All fidelity gaps closed and dashboard gate fixed in this push (71e4eb32).
Changes:
StableProvider:days_stablenow uses calendar days from commit timestamps, matching the CLI's time unitEntangleProvider:co_change_ratenow uses Jaccard (co / |union|) — was using min, which inflated ratesintel_hotspots_page: migrated off legacyload_intel_snapshotpath; now queriesMusehubSymbolIntel.churn_30ddirectly, consistent with all other intel routesintel_dashboard.html: removed theindex_metagate 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.pycovering all three provider fixes and the hotspots route migration
79/79 tests green.
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_stablecalendar days,co_change_rateJaccard, hotspots off legacy snapshot ✅ - Dashboard gate removed — all intel cards always visible ✅
- Clones data now populating on staging (confirmed after push
71e4eb32)
Phase 1 complete ✅
Commit:
sha256:6ed177822077ontask/intel-clones-uiWhat 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 frommembers_jsonat request time; repo-wide file hotspot aggregation; zeromuse codesubprocessesintel_clones_detail_page— single cluster lookup bycluster_hash; full member parse + file breakdown grouping; graceful empty state on blank/unknown hashintel_dashboard_pageextended withclones_count,clones_symbols,clones_preview(top 5 bymember_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_breakdownTier 1 unit tests —
tests/test_clones_unit.py: 31/31 passingUp 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)