gabriel / musehub public
Closed #26
filed by gabriel human · 45 days ago

Enriched Repo Cards — pulse, autonomy, hottest symbol, blast leader, health signal

0 Anchors
Blast radius
Churn 30d
0 Proposals

Enriched Repo Cards — Live Intelligence at a Glance

Vision

╔══════════════════════════════════════════════════════════════════════╗
║  gabriel/musehub                                               →     ║
║  MuseHub remote server                                               ║
║                                                                      ║
║  PULSE (30d)                      AUTONOMY                           ║
║  ▁▂▁▃▅▄▇█▆▅▄▃▂▁▂▃▄▅▆▇▆▅▄▃▂▁▂▃   ████████████████░░░░  94%          ║
║                                   agent / human                      ║
║                                                                      ║
║  🔥 HOTTEST SYMBOL                💥 BLAST LEADER                   ║
║  wire_push_stream  ×12 / 30d      session_middleware  →847 syms      ║
║                                                                      ║
║  HEALTH  ██████████  ● CLEAN      0 dead  ·  0 errors  ·  0 warns   ║
╚══════════════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════════════╗
║  gabriel/muse                                                  →     ║
║  VCS engine and CLI                                                  ║
║                                                                      ║
║  PULSE (30d)                      AUTONOMY                           ║
║  ▁▁▂▄▃▅▄▆▇█▇▆▅▄▃▄▅▆▇▆▅▄▃▅▆▇▆▅   ███████████░░░░░░░░░  61%          ║
║                                                                      ║
║  🔥 HOTTEST SYMBOL                💥 BLAST LEADER                   ║
║  _genesis_hash     ×8  / 30d      CommitRecord        →312 syms      ║
║                                                                      ║
║  HEALTH  ███████░░░  ● WARN       3 dead  ·  0 errors  ·  2 warns   ║
╚══════════════════════════════════════════════════════════════════════╝

Data Sources (all postgres, no engine calls at render time)

Signal Table Key Columns
Pulse sparkline musehub_commits repo_id, timestamp
Autonomy ratio musehub_commits repo_id, agent_id
Hottest symbol musehub_symbol_intel repo_id, address, churn_30d, last_changed
Blast leader musehub_symbol_intel repo_id, address, blast
Dead code count musehub_intel_dead repo_id, address, confidence
Breakage musehub_intel_breakage_meta repo_id, total_issues, error_count, warning_count

One enrichment query per repo at page load. No per-card API calls. No engine subprocess.


Spectral Template

The card uses the --gradient-spectral palette for the pulse sparkline bars and the autonomy bar fill. Health signal maps to semantic colors.

// Spectral palette (from :root)
// --gradient-spectral: linear-gradient(90deg, #60a5fa, #818cf8, #a78bfa, #c084fc)
// Individual stops available as named vars:
//   blue   → var(--color-accent)   #60a5fa
//   indigo → var(--color-purple)   #818cf8  (alias)
//   violet → var(--color-purple)   #a78bfa
//   pink   → #c084fc               (use inline or add --color-pink token)

// Sparkline bars: each bar colored by position along spectral gradient
// Bar i of N → hue interpolated from blue (#60a5fa) → violet (#a78bfa)
// Rendered as inline SVG <rect> elements; color set via fill attribute

// Autonomy bar: filled portion uses --gradient-spectral (left-to-right)
// Empty portion uses var(--bg-elevated)

// Health signal:
//   CLEAN  → var(--color-success)   #3fb950
//   WARN   → var(--color-warning)   #d29922
//   RISK   → var(--color-danger)    #f87171
// Threshold: CLEAN = 0 dead + 0 errors; WARN = any dead OR warnings; RISK = any errors

Card HTML structure (target markup)

<a class="rc-card" href="/{{ repo.owner }}/{{ repo.slug }}">

  <!-- Row 1: name + arrow -->
  <div class="rc-card__header">
    <span class="rc-card__name">{{ repo.owner }}/{{ repo.slug }}</span>
    <span class="rc-card__arrow">→</span>
  </div>

  <!-- Row 2: description -->
  {% if repo.description %}
  <p class="rc-card__desc">{{ repo.description | truncate(80) }}</p>
  {% endif %}

  <!-- Row 3: pulse + autonomy side by side -->
  <div class="rc-card__metrics">
    <div class="rc-card__pulse">
      <span class="rc-card__metric-label">Pulse</span>
      <!-- inline SVG sparkline, 30 bars, spectral fill -->
      <svg class="rc-sparkline" viewBox="0 0 120 24" preserveAspectRatio="none"
           aria-label="Commit frequency last 30 days" role="img">
        {% for bucket in repo.pulse_buckets %}
        <rect
          x="{{ loop.index0 * 4 }}" y="{{ 24 - bucket.h }}"
          width="3" height="{{ bucket.h }}"
          fill="{{ bucket.color }}"
          rx="1"/>
        {% endfor %}
      </svg>
    </div>
    <div class="rc-card__autonomy">
      <span class="rc-card__metric-label">Autonomy</span>
      <div class="rc-autonomy-bar" role="meter"
           aria-valuenow="{{ repo.autonomy_pct }}" aria-valuemin="0" aria-valuemax="100"
           title="{{ repo.autonomy_pct }}% agent commits">
        <div class="rc-autonomy-bar__fill" style="width:{{ repo.autonomy_pct }}%"></div>
      </div>
      <span class="rc-card__metric-val">{{ repo.autonomy_pct }}%</span>
    </div>
  </div>

  <!-- Row 4: hottest symbol + blast leader -->
  <div class="rc-card__intel">
    {% if repo.hottest_symbol %}
    <div class="rc-intel-item rc-intel-item--hot">
      <span class="rc-intel-item__label">Hottest</span>
      <code class="rc-intel-item__addr">{{ repo.hottest_symbol.name }}</code>
      <span class="rc-intel-item__stat">×{{ repo.hottest_symbol.churn_30d }}</span>
    </div>
    {% endif %}
    {% if repo.blast_leader %}
    <div class="rc-intel-item rc-intel-item--blast">
      <span class="rc-intel-item__label">Blast</span>
      <code class="rc-intel-item__addr">{{ repo.blast_leader.name }}</code>
      <span class="rc-intel-item__stat">→{{ repo.blast_leader.blast }}</span>
    </div>
    {% endif %}
  </div>

  <!-- Row 5: health signal -->
  <div class="rc-card__health rc-card__health--{{ repo.health_status }}">
    <span class="rc-health-dot"></span>
    <span class="rc-health-label">{{ repo.health_status | upper }}</span>
    <span class="rc-health-detail">
      {{ repo.dead_count }} dead
      · {{ repo.error_count }} errors
      · {{ repo.warning_count }} warns
    </span>
  </div>

</a>

Implementation Phases

Phase 1 — Data layer: RepoCardEnrichment model + query

File: musehub/services/repo_card_enrichment.py

"""
repo_card_enrichment.py
=======================

Single-query enrichment service for the repo card component.

All signals are derived from existing postgres tables — no engine subprocess
calls, no per-card HTTP round trips.  The service issues one bulk query per
page load (not per card) by batching repo IDs.

Signal inventory
----------------
pulse_buckets   — list[PulseBucket]   30 daily commit-count buckets
autonomy_pct    — int                 0-100, % commits with agent_id non-null
hottest_symbol  — SymbolStat | None   highest churn_30d in symbol_intel
blast_leader    — SymbolStat | None   highest blast score in symbol_intel
dead_count      — int                 high-confidence dead symbols
error_count     — int                 breakage errors from breakage_meta
warning_count   — int                 breakage warnings from breakage_meta
health_status   — Literal['clean','warn','risk']
"""

Model classes:

@dataclass
class PulseBucket:
    """One day's commit count, pre-normalised to bar height (0–24px)."""
    date: str        # ISO date YYYY-MM-DD
    count: int       # raw commit count
    h: int           # bar height in SVG units (0–24), scaled to max in window
    color: str       # hex color interpolated along spectral gradient

@dataclass
class SymbolStat:
    """Compact symbol reference for card display."""
    address: str     # full symbol address e.g. "musehub/services/foo.py::bar"
    name: str        # short name — last segment after "::"
    churn_30d: int   # times changed in last 30 days (hottest signal)
    blast: int       # downstream symbol count (blast leader signal)

@dataclass
class RepoCardEnrichment:
    """
    Complete enrichment payload for one repo card.

    Constructed by ``enrich_repo_cards()`` in a single batched query.
    All fields have safe defaults so a card always renders even if intel
    tables are empty for a given repo.
    """
    repo_id: str
    pulse_buckets: list[PulseBucket]
    autonomy_pct: int
    hottest_symbol: SymbolStat | None
    blast_leader: SymbolStat | None
    dead_count: int
    error_count: int
    warning_count: int

    @property
    def health_status(self) -> str:
        """
        Three-tier health classification.

        CLEAN — zero dead symbols AND zero breakage errors.
        WARN  — dead symbols present OR breakage warnings present.
        RISK  — any breakage errors present (errors outrank warnings).
        """
        if self.error_count > 0:
            return "risk"
        if self.dead_count > 0 or self.warning_count > 0:
            return "warn"
        return "clean"

Key query design:

-- Pulse: daily commit counts per repo for last 30 days
SELECT
    repo_id,
    DATE_TRUNC('day', timestamp)::date AS day,
    COUNT(*)                           AS cnt
FROM musehub_commits
WHERE repo_id = ANY(:repo_ids)
  AND timestamp >= NOW() - INTERVAL '30 days'
GROUP BY repo_id, day
ORDER BY repo_id, day;

-- Autonomy: agent vs human commit ratio
SELECT
    repo_id,
    COUNT(*) FILTER (WHERE agent_id IS NOT NULL AND agent_id != '') AS agent_commits,
    COUNT(*)                                                         AS total_commits
FROM musehub_commits
WHERE repo_id = ANY(:repo_ids)
GROUP BY repo_id;

-- Hottest + blast leader from symbol_intel (one query, ranked per repo)
SELECT DISTINCT ON (repo_id)
    repo_id, address, churn_30d, blast
FROM musehub_symbol_intel
WHERE repo_id = ANY(:repo_ids)
ORDER BY repo_id, churn_30d DESC NULLS LAST;

-- Dead code count
SELECT repo_id, COUNT(*) AS dead_count
FROM musehub_intel_dead
WHERE repo_id = ANY(:repo_ids)
  AND confidence = 'high'
GROUP BY repo_id;

-- Breakage summary
SELECT repo_id, error_count, warning_count
FROM musehub_intel_breakage_meta
WHERE repo_id = ANY(:repo_ids);

Phase 2 — Route integration

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

# In domain_detail_page():
enrichments = await enrich_repo_cards(db, [r["repo_id"] for r in repos_result.repos])
enrichment_map = {e.repo_id: e for e in enrichments}

# Merge into repos context
repos_ctx = []
for repo in repos_result.repos:
    enc = enrichment_map.get(repo["repo_id"])
    repos_ctx.append({**repo, "enrichment": enc})

Phase 3 — CSS (src/scss/components/_repo-cards.scss)

// ─────────────────────────────────────────────────────────────────────────────
// Component: rc-card  (Enriched Repo Card)
//
// Used on:
//   - domain detail page  (/domains/@author/slug)
//   - explore page        (/explore)
//   - profile page        (/profile)
//   - search results
//
// Spectral palette usage:
//   pulse bars → interpolated from --color-accent to --color-purple along bar index
//   autonomy fill → --gradient-spectral left-to-right
//   health dot → --color-success / --color-warning / --color-danger
// ─────────────────────────────────────────────────────────────────────────────

.rc-card { ... }
.rc-sparkline { ... }
.rc-autonomy-bar { ... }
.rc-autonomy-bar__fill { background: var(--gradient-spectral); ... }
.rc-intel-item { ... }
.rc-card__health--clean .rc-health-dot { background: var(--color-success); }
.rc-card__health--warn  .rc-health-dot { background: var(--color-warning); }
.rc-card__health--risk  .rc-health-dot { background: var(--color-danger);  }

Seven Tiers of Tests

Tier 1 — Unit

File: tests/test_repo_card_enrichment_unit.py

"""
Unit tests for RepoCardEnrichment model and pure helper functions.

T100 — health_status returns 'clean' when all counts are zero
T101 — health_status returns 'warn' when dead_count > 0, error_count == 0
T102 — health_status returns 'risk' when error_count > 0 (dominates dead+warn)
T103 — SymbolStat.name extracts last segment after '::'
T104 — PulseBucket.h is clamped to [0, 24]
T105 — autonomy_pct of 0 when total_commits == 0 (no ZeroDivisionError)
T106 — pulse_buckets always has exactly 30 entries (zero-filled for missing days)
T107 — spectral_color(0, 30) returns --color-accent hex
T108 — spectral_color(29, 30) returns --color-purple hex
T109 — enrich_repo_cards returns empty-safe RepoCardEnrichment for unknown repo_id
"""

Tier 2 — Integration

File: tests/test_repo_card_enrichment_integration.py

"""
Integration tests against a real test database.

T200 — enrich_repo_cards with a repo that has 30 days of commits returns
       pulse_buckets of length 30 with correct daily counts
T201 — autonomy_pct is 100 for a repo where all commits have agent_id set
T202 — autonomy_pct is 0 for a repo where no commits have agent_id set
T203 — hottest_symbol matches the symbol with highest churn_30d in symbol_intel
T204 — blast_leader matches the symbol with highest blast score
T205 — dead_count counts only high-confidence dead symbols
T206 — health_status is 'risk' when breakage_meta has error_count > 0
T207 — health_status is 'warn' when dead symbols exist but no errors
T208 — enrich_repo_cards batches correctly — one call for N repos issues
       exactly 5 SQL queries regardless of N (verified via query counter)
T209 — repos with no intel data return safe zero-value enrichment (no crash)
"""

Tier 3 — End-to-End

File: tests/test_repo_card_e2e.py

"""
End-to-end tests against the live HTTP server (httpx AsyncClient).

T300 — GET /domains/@gabriel/code renders rc-card elements for each public repo
T301 — Each rc-card contains .rc-sparkline SVG with 30 rect children
T302 — Each rc-card contains .rc-autonomy-bar with aria-valuenow attribute
T303 — Each rc-card contains .rc-card__health with data-status in {clean,warn,risk}
T304 — rc-intel-item--hot is absent when symbol_intel table is empty for repo
T305 — rc-intel-item--blast is absent when symbol_intel table is empty for repo
T306 — Card links href="/owner/slug" for each repo
T307 — Page renders < 800ms with 12 enriched cards (perf gate in e2e)
"""

Tier 4 — Stress

File: tests/test_repo_card_stress.py

"""
Stress tests for the enrichment service under load.

T400 — enrich_repo_cards with 100 repo_ids completes in < 500ms
T401 — 50 concurrent GET /domains/@gabriel/code requests all return 200
       with no query failures or connection pool exhaustion
T402 — enrichment with repos having 10,000 commits each does not OOM
T403 — sparkline normalisation is stable when one bucket has 10x the
       commits of all others (no bar overflow)
"""

Tier 5 — State

File: tests/test_repo_card_state.py

"""
State consistency tests — enrichment output tracks DB mutations correctly.

T500 — After inserting a new commit with agent_id set, autonomy_pct increases
T501 — After inserting a new commit, pulse_buckets[today].count increments
T502 — After inserting a high-confidence dead symbol, health_status degrades
       from 'clean' to 'warn'
T503 — After inserting a breakage error row, health_status degrades to 'risk'
T504 — After deleting all dead symbols, health_status recovers to 'clean'
T505 — hottest_symbol updates when a new symbol_intel row has higher churn_30d
"""

Tier 6 — Integrity

File: tests/test_repo_card_integrity.py

"""
Data integrity tests — enrichment never surfaces data from the wrong repo.

T600 — pulse_buckets for repo A contains no commits from repo B
T601 — hottest_symbol.address is always prefixed with a path belonging to the
       queried repo (no cross-repo symbol leakage)
T602 — autonomy_pct for a repo with 0 commits is exactly 0 (not inherited)
T603 — dead_count for repo A is unaffected by dead symbols in repo B
T604 — enrich_repo_cards([]) returns [] without querying any table
T605 — blast_leader.blast is always a non-negative integer (no sentinel -1)
"""

Tier 7 — Performance

File: tests/test_repo_card_performance.py

"""
Performance regression tests — query plan and timing gates.

T700 — EXPLAIN ANALYZE on the pulse query shows index scan on
       (repo_id, timestamp) — no sequential scan on musehub_commits
T701 — EXPLAIN ANALYZE on the symbol_intel query shows DISTINCT ON
       uses the (repo_id, churn_30d) index
T702 — Total enrichment wall time for 12 repos is < 100ms at p95
       (measured over 100 iterations in a warm-cache scenario)
T703 — Memory footprint of enrich_repo_cards(12 repos × 30 buckets)
       does not exceed 2MB (tracemalloc gate)
T704 — Adding a covering index on musehub_commits(repo_id, timestamp, agent_id)
       reduces autonomy query cost by > 50% vs table scan (EXPLAIN diff)
"""

Security

File: tests/test_repo_card_security.py

"""
Security tests — enrichment cannot be used to leak private repo data.

T800 — enrich_repo_cards only returns data for repo_ids explicitly passed in;
       passing a private repo_id for another user returns zero-value enrichment
       (service layer must respect visibility filtering done by the caller)
T801 — symbol addresses in hottest_symbol and blast_leader are HTML-escaped
       when rendered in the template (no XSS via crafted symbol names)
T802 — pulse_buckets.color values are hex strings matching /^#[0-9a-f]{6}$/i;
       no user-controlled content flows into SVG fill attributes
T803 — repo_id inputs to enrich_repo_cards are validated as sha256: prefixed
       strings before being interpolated into SQL (parameterised query audit)
T804 — A repo with visibility='private' does not appear in domain card listing
       even if its repo_id is known (enforced at list_repos_for_domain layer)
"""

Indexes Required

-- Pulse query — already has (repo_id, timestamp) but add agent_id for autonomy
CREATE INDEX CONCURRENTLY IF NOT EXISTS
  ix_musehub_commits_repo_ts_agent
  ON musehub_commits (repo_id, timestamp DESC, agent_id);

-- Symbol intel — covering index for card queries
CREATE INDEX CONCURRENTLY IF NOT EXISTS
  ix_musehub_symbol_intel_repo_churn
  ON musehub_symbol_intel (repo_id, churn_30d DESC NULLS LAST);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
  ix_musehub_symbol_intel_repo_blast
  ON musehub_symbol_intel (repo_id, blast DESC NULLS LAST);

Reuse Contract

The rc-card component and RepoCardEnrichment service are designed for reuse across all repo listing surfaces:

  • /domains/@author/slug — domain detail (this ticket)
  • /explore — global repo discovery
  • /@author — profile page
  • /search — search results

Any page that renders a list of repos should call enrich_repo_cards() and pass the result into the template as enrichment_map. The card partial will be extracted to musehub/templates/musehub/partials/repo_card.html.

Activity1
gabriel opened this issue 45 days ago
gabriel 45 days ago

All deliverables complete and landed to main.

Phase 1enrich_repo_cards() service with all 5 signals (pulse, autonomy, hottest, blast, dead/breakage) in 5 batch queries regardless of repo count.

Phase 2 — Wired into all four repo listing surfaces: domain detail, explore, profile, and search.

Phase 3_repo_cards.scss with rc-grid, rc-card, sparkline, autonomy bar, donut health gauge, and intel counts.

Tests — T100–T805 across 8 tiers (unit, integration, e2e, stress, state, integrity, performance, security), all passing.

Indexesix_symbol_intel_repo_churn (existing) + ix_symbol_intel_repo_blast (migration 0033) covering both DISTINCT ON queries.