gabriel / musehub public
Open #33
filed by gabriel human · 37 days ago

Release Page: Semantic Version Observatory

0 Anchors
Blast radius
Churn 30d
0 Proposals

Release Page: Semantic Version Observatory

What a Muse Release Actually Is

Before designing the page, we have to be precise about what we're displaying.

A GitHub release is a zip archive pinned to a git tag, with a text box for release notes. Semantics are optional; the human writes them by hand.

A Muse release is a cryptographically-bound semantic contract between two states of the public symbol graph. It carries:

  • semver_major / semver_minor / semver_patch / semver_pre / semver_build — parsed from the tag and stored as queryable integers, not post-hoc parsed strings
  • channelstable | beta | alpha | nightly (replaces the boolean is_prerelease)
  • snapshot_id — content-addressed object store ID; reproducible forever, not just "was on this commit at this date"
  • semantic_report_json — a SemanticReleaseReport blob computed by muse.plugins.code.release_analysis.compute_release_analysis at push time: language stats, API surface diff (added/removed/modified symbols), file hotspots, refactor events, agent provenance aggregated across the full commit window
  • changelog_json — typed ChangelogEntry array; every entry carries sem_ver_bump, breaking_changes, agent_id, model_id — derived from structural symbol diffs, not commit message conventions
  • agent_id + model_id — cryptographic AI provenance from the tip commit
  • gpg_signature — optional ASCII-armoured signature on the tag object
  • download_urls — JSON map of distribution package types to artifact URLs
  • assetsMusehubReleaseAsset attachments with per-asset download_count

The version number itself is inferred from the commit graph by run_suggest in muse/cli/commands/release.py. It walks every commit since the previous release, aggregates the sem_ver_bump field (derived from structural AST diffs, not keywords), and applies pre-1.0 adjustment rules. No human decides "this is a major version". The symbol contract decides.

The release page must express ALL of this. A page that shows a tag, a markdown box, and a download link is a GitHub clone. A page that shows the API surface diff, the symbol churn heatmap, the agent-to-human commit ratio, the breaking change blast radius, and the inferred bump justification is a Muse release.


The Release as Semantic Observatory

The individual release detail page is not a changelog. It is a semantic observatory — a snapshot of the codebase at a moment of intentional, structurally-validated stability.

╔══════════════════════════════════════════════════════════════════════╗
║  v2.4.0  ·  stable  ·  musehub/muse  ·  2026-04-14                  ║
║  ┌────────────────────────────────────────────────────────────────┐  ║
║  │  SEMANTIC CONTRACT DELTA (v2.3.0 → v2.4.0)                     │  ║
║  │                                                                 │  ║
║  │  +12 API symbols added   −2 removed   ~4 modified              │  ║
║  │  ████████████░░░░░░  72% public surface unchanged               │  ║
║  │                                                                 │  ║
║  │  Bump inferred: MINOR  (no breaking changes)                    │  ║
║  │  Pre-1.0 adjusted: NO                                           │  ║
║  └────────────────────────────────────────────────────────────────┘  ║
╠══════════════════════════════════════════════════════════════════════╣
║  COMMIT WINDOW  (47 commits  ·  12 unreleased resolved)             ║
║  ┌────────────────────────────────────────────────────────────────┐  ║
║  │  Human ██████░░░░░░░░  38%   Agent  ████████████  62%          │  ║
║  │  Models: claude-sonnet-4-6(31)  claude-opus-4-6(3)             │  ║
║  │  Agents: cccode-v3(18)  cccode-v4(13)  human(18)               │  ║
║  └────────────────────────────────────────────────────────────────┘  ║
╠══════════════════════════════════════════════════════════════════════╣
║  LANGUAGE COMPOSITION                                               ║
║  Python  ███████████████  68%  (142 files, 1,847 symbols)          ║
║  TypeScript  ████░░░░░░░  21%  ( 44 files,   312 symbols)          ║
║  HTML/Jinja2 ██░░░░░░░░░   9%  ( 19 files,    —  symbols)          ║
║  SCSS  █░░░░░░░░░░░░░░░░   2%  (  5 files,    —  symbols)          ║
╠══════════════════════════════════════════════════════════════════════╣
║  HOTSPOT FILES  (touches since previous release)                    ║
║  musehub/mcp/dispatcher.py          ████████████████  16 touches   ║
║  musehub/services/musehub_wire.py   ████████████░░░░  12 touches   ║
║  musehub/api/routes/musehub/…       ████████░░░░░░░░   8 touches   ║
╠══════════════════════════════════════════════════════════════════════╣
║  SYMBOL DELTA  (API surface diff)                                   ║
║  + SemanticReleaseReport.unique_models  [typing:TypedDict:class]   ║
║  + compute_release_analysis             [function]                 ║
║  ~ _sync_compute_report                 [modified]                 ║
║  − build_changelog_legacy              [function:removed]          ║
╚══════════════════════════════════════════════════════════════════════╝

Release List: Version Timeline with Semantic Context

╔══════════════════════════════════════════════════════════════════════╗
║  RELEASES  ·  gabriel/musehub  ·  14 total                          ║
║  [stable ▾]  [channel: all ▾]  [sort: newest ▾]  [⊕ new release]   ║
╠══════════════════════════════════════════════════════════════════════╣
║  ★ LATEST STABLE                                                    ║
║  ┌────────────────────────────────────────────────────────────────┐  ║
║  │  v2.4.0  ·  stable  ·  2026-04-14          47 commits         │  ║
║  │  "API surface stabilisation + agent provenance enrichment"     │  ║
║  │  MINOR bump  ·  +12 added  −2 removed  ~4 modified            │  ║
║  │  [Python 68%]  [TS 21%]  [Agent 62%]                          │  ║
║  │  [↓ 1.2k downloads]  [2 assets]  [12 issues resolved]         │  ║
║  └────────────────────────────────────────────────────────────────┘  ║
╠══════════════════════════════════════════════════════════════════════╣
║  v2.3.1  ·  stable  ·  2026-04-02                                   ║
║  PATCH  ·  fix: wire push object pack regression                    ║
║  +0 added  −0 removed  ~1 modified  ·  3 commits  ·  ↓ 840         ║
║ ─────────────────────────────────────────────────────────────────── ║
║  v2.3.0  ·  stable  ·  2026-03-28                                   ║
║  MINOR  ·  feat: MCP tool dispatcher + HTMX fragments               ║
║  +8 added  −1 removed  ~6 modified  ·  28 commits  ·  ↓ 2.1k       ║
║ ─────────────────────────────────────────────────────────────────── ║
║  v2.3.0-beta.2  ·  beta  · 2026-03-21                              ║
║  PRE-RELEASE  ·  [ beta chip ]                                      ║
║ ─────────────────────────────────────────────────────────────────── ║
║  v2.2.0  ·  stable  ·  2026-03-10                                   ║
║  MAJOR  ·  breaking: MusehubIssue schema rewrite                    ║
║  💥 BREAKING  ·  musehub/db/musehub_models.py::MusehubIssue        ║
║  +3 added  −7 removed  ~12 modified  ·  61 commits  ↓ 5.8k         ║
╚══════════════════════════════════════════════════════════════════════╝

Release Detail: Full Semantic Observatory Layout

┌─────────────────────────────────────────────────────────────────────┐
│  NAVIGATION BAR: code · issues · proposals · releases (active)      │
├──────────────────────────────────┬──────────────────────────────────┤
│  HEADER ZONE                     │  SIDEBAR                         │
│  v2.4.0  [stable]  [verified ✓] │  Quick Stats                     │
│  2026-04-14 · by gabriel         │  ─────────────────────────────── │
│  [↓ tar.gz]  [↓ zip]  [↓ whl]  │  47 commits in window            │
│                                  │  from v2.3.1 → HEAD             │
│  SEMANTIC DELTA                  │  2 assets  ·  1.2k downloads    │
│  ┌──────────────────────────┐   │                                   │
│  │  Bump: MINOR  (inferred) │   │  Agent Provenance                 │
│  │  API Added    ████  +12  │   │  ─────────────────────────────── │
│  │  API Removed  ▌     −2  │   │  Agent  ████████  62%             │
│  │  API Modified ██    ~4  │   │  Human  █████     38%             │
│  │  Unchanged    ███  214  │   │  claude-sonnet-4-6 (31)           │
│  └──────────────────────────┘   │  claude-opus-4-6   (3)           │
│                                  │  unique agents: 2                │
│  LANGUAGE COMPOSITION            │                                   │
│  ──────────────────────────────  │  GPG Signature                   │
│  Python    ███████████  68%      │  ─────────────────────────────── │
│  TypeScript ███░░░░░░░  21%      │  [✓ verified]                    │
│  HTML       █░░░░░░░░░   9%      │  fingerprint: abc123…            │
│  SCSS       ░░░░░░░░░░   2%      │                                  │
├──────────────────────────────────┴──────────────────────────────────┤
│  API SURFACE DIFF                        [expand all]  [filter ▾]   │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  + SemanticReleaseReport.unique_models  TypedDict:field      │   │
│  │  + analyse_release_background           async function       │   │
│  │  + _run_analysis                        async function       │   │
│  │  ~ _sync_compute_report                 signature changed    │   │
│  │  ✕ build_changelog_legacy               function: removed    │   │
│  └──────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│  CHANGELOG  (47 commits · grouped by bump)                          │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  ✨ MINOR (8 commits)                                         │   │
│  │    abc12345  feat: analyse_release_background async entry     │   │
│  │    def67890  feat: SemanticReleaseReport agent provenance     │   │
│  │  🔧 PATCH (31 commits)                                        │   │
│  │    …                                                          │   │
│  │  ○  NONE  (8 commits)                                         │   │
│  │    …                                                          │   │
│  └──────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│  HOTSPOT FILES  (most touched in this release window)               │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  musehub/mcp/dispatcher.py         ████████████████  16 chg  │   │
│  │  musehub/services/musehub_wire.py  ████████████░░░░  12 chg  │   │
│  │  musehub/api/routes/…/ui_issues.py ████████░░░░░░░░   8 chg  │   │
│  └──────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│  REFACTOR EVENTS                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  rename  ChangelogEntry.timestamp → committed_at  (abc123)   │   │
│  │  delete  build_changelog_legacy::…               (def456)   │   │
│  └──────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│  RESOLVED ISSUES & PROPOSALS                                        │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  issue #3   Proposals List / Mission Control [via commit]     │   │
│  │  issue #5   Issues List: Codebase Intelligence MC [via sym]  │   │
│  │  proposal #2  Proposal Reimagination [merged]                 │   │
│  └──────────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────────┤
│  ASSETS                                                              │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  musehub-2.4.0.tar.gz    [↓ 841]  [copy URL]                 │   │
│  │  musehub-2.4.0.whl       [↓ 382]  [copy URL]                 │   │
│  └──────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

The Inferred Bump Justification Panel

This is the most uniquely Muse feature on the page. No other VCS shows this.

┌─────────────────────────────────────────────────────────────────────┐
│  WHY v2.4.0 AND NOT v2.3.1 OR v3.0.0?                              │
│                                                                      │
│  muse release suggest walked 47 commits since v2.3.1.               │
│                                                                      │
│  Bump drivers (commits that raised the aggregate bump):              │
│  ✨  def67890  feat: SemanticReleaseReport.unique_models             │
│               new TypedDict field on a public class → MINOR          │
│  ✨  abc12345  feat: analyse_release_background entry point          │
│               new public async function exported → MINOR             │
│                                                                      │
│  No breaking changes detected.                                       │
│  Highest bump seen: MINOR → tag: v2.4.0                             │
│  Pre-1.0 adjustment: N/A (major = 2)                                │
└─────────────────────────────────────────────────────────────────────┘

New TypedDict: ReleaseDetailContext

class ReleaseDetailContext(TypedDict):
    """Enriched release detail context for the semantic observatory page.

    All fields are pre-resolved server-side. Templates must never call
    additional DB queries after this dict is constructed.

    Fields
    ------
    release : dict
        Serialised MusehubRelease (model_dump mode='json').
    semantic_report : SemanticReleaseReport | None
        Parsed from semantic_report_json; None when blank or malformed.
    changelog : list[ChangelogEntry]
        Parsed from changelog_json; grouped in template by sem_ver_bump.
    bump_drivers : list[ChangelogEntry]
        Subset of changelog where sem_ver_bump != 'none'.
        These are the commits that *justified* the version increment.
    inferred_bump : SemVerBump
        Highest bump seen across bump_drivers.
    pre_1_0_adjusted : bool
        True when 0.x semver rules shifted the bump downward.
    api_added : list[ApiChangeSummary]
        Public symbols added in this release window.
    api_removed : list[ApiChangeSummary]
        Public symbols removed (breaking if they were stable).
    api_modified : list[ApiChangeSummary]
        Public symbols with changed signatures.
    breaking_changes : list[str]
        Deduplicated symbol addresses that appear in breaking_changes
        across all changelog commits.
    hotspot_files : list[FileHotspot]
        Top N files by change_count in this release window.
    refactor_events : list[RefactorEventSummary]
        Structural rename/move/delete events detected by the analyser.
    resolved_issues : list[dict]
        Issues resolved in this release (commit or symbol anchor match).
    resolved_proposals : list[dict]
        Proposals merged in this release window.
    agent_ratio_pct : int
        Percentage of commits in window authored by AI agents (0–100).
    human_commits : int
        Commits by humans in this window.
    agent_commits : int
        Commits by AI agents in this window.
    unique_agents : list[str]
        Distinct agent_id values across changelog commits.
    unique_models : list[str]
        Distinct model_id values across changelog commits.
    assets : list[dict]
        Serialised MusehubReleaseAsset objects.
    total_downloads : int
        Sum of download_count across all assets.
    available_formats : list[str]
        Non-empty keys from release.download_urls.
    gpg_verified : bool
        True when gpg_signature is non-empty.
    jsonld_script : str
        Pre-rendered JSON-LD <script> block for SEO.
    """

    release: dict
    semantic_report: "SemanticReleaseReport | None"
    changelog: "list[ChangelogEntry]"
    bump_drivers: "list[ChangelogEntry]"
    inferred_bump: "SemVerBump"
    pre_1_0_adjusted: bool
    api_added: "list[ApiChangeSummary]"
    api_removed: "list[ApiChangeSummary]"
    api_modified: "list[ApiChangeSummary]"
    breaking_changes: list[str]
    hotspot_files: "list[FileHotspot]"
    refactor_events: "list[RefactorEventSummary]"
    resolved_issues: list[dict]
    resolved_proposals: list[dict]
    agent_ratio_pct: int
    human_commits: int
    agent_commits: int
    unique_agents: list[str]
    unique_models: list[str]
    assets: list[dict]
    total_downloads: int
    available_formats: list[str]
    gpg_verified: bool
    jsonld_script: str

New TypedDict: ReleaseListEntry

class ReleaseListEntry(TypedDict):
    """Enriched release list row — zero additional DB queries after prefetch.

    All fields derive from the MusehubRelease row and its parsed JSON blobs.
    The list page must never lazy-load per-row; prefetch once and project.

    Fields
    ------
    release_id : str
    tag : str
    title : str
    channel : ReleaseChannel
    semver_str : str       e.g. "2.4.0" without the leading "v"
    semver_bump : SemVerBump
        Highest bump inferred from changelog_json drivers.
    is_draft : bool
    is_breaking : bool
        True when breaking_changes is non-empty in changelog_json.
    breaking_count : int
        Number of distinct breaking symbol addresses.
    api_added_count : int
    api_removed_count : int
    api_modified_count : int
    commit_count : int
        Length of changelog_json.
    agent_ratio_pct : int  (0–100)
    hottest_file : str | None
        file_path of the top FileHotspot, or None if no report.
    resolved_issue_count : int
    resolved_proposal_count : int
    total_downloads : int
    asset_count : int
    created_at : str  (ISO-8601)
    author : str
    agent_id : str
    model_id : str
    """

    release_id: str
    tag: str
    title: str
    channel: "ReleaseChannel"
    semver_str: str
    semver_bump: "SemVerBump"
    is_draft: bool
    is_breaking: bool
    breaking_count: int
    api_added_count: int
    api_removed_count: int
    api_modified_count: int
    commit_count: int
    agent_ratio_pct: int
    hottest_file: "str | None"
    resolved_issue_count: int
    resolved_proposal_count: int
    total_downloads: int
    asset_count: int
    created_at: str
    author: str
    agent_id: str
    model_id: str

New Pure Functions

def parse_semantic_report(json_str: str) -> "SemanticReleaseReport | None":
    """Deserialise the semantic_report_json blob stored on MusehubRelease.

    Returns None when the string is empty, null, or malformed — never raises.
    Callers must treat None as "analysis not available" and render a placeholder.

    Parameters
    ----------
    json_str : str
        Raw value of MusehubRelease.semantic_report_json.

    Returns
    -------
    SemanticReleaseReport | None
        Parsed report dict, or None on any failure.

    Notes
    -----
    - Called once per release in release_detail_page; result is injected into
      ReleaseDetailContext and never re-parsed at render time.
    - Does not validate field types beyond what json.loads provides.
    """
    ...


def infer_bump_from_changelog(changelog: list[dict]) -> tuple["SemVerBump", list[dict]]:
    """Walk a parsed changelog_json list and return the highest bump + drivers.

    Parameters
    ----------
    changelog : list[dict]
        Parsed ChangelogEntry objects from changelog_json.

    Returns
    -------
    tuple[SemVerBump, list[dict]]
        (highest_bump, driver_commits) where driver_commits are all entries
        with sem_ver_bump != 'none', sorted by bump rank descending.

    Notes
    -----
    - Pure function; no DB access, no I/O.
    - Uses the same _BUMP_ORDER rank logic as run_suggest in release.py.
    - Called from both release_detail_page and enrich_release_list_entry.
    """
    ...


def enrich_release_list_entry(
    release: "MusehubRelease",
    asset_stats: dict[str, int],
    issue_resolution_index: dict[str, int],
    proposal_resolution_index: dict[str, int],
) -> "ReleaseListEntry":
    """Project a MusehubRelease ORM row into a ReleaseListEntry for list rendering.

    Zero additional DB queries — all lookup data arrives via index maps
    pre-fetched by prefetch_release_list_indexes().

    Parameters
    ----------
    release : MusehubRelease
        ORM model row, already loaded.
    asset_stats : dict[str, int]
        Maps release_id → total_download_count. Pre-fetched via one GROUP BY query.
    issue_resolution_index : dict[str, int]
        Maps release_id → count of issues resolved in that release window.
    proposal_resolution_index : dict[str, int]
        Maps release_id → count of proposals merged in that release window.

    Returns
    -------
    ReleaseListEntry
        Fully populated list entry; safe to pass directly to Jinja2 template.

    Notes
    -----
    - Parses changelog_json inline (no DB). If malformed, sets all counts to 0.
    - Parses semantic_report_json inline (no DB). If empty, hottest_file=None.
    - agent_ratio_pct = round(agent_commits / max(commit_count, 1) * 100).
    """
    ...


async def prefetch_release_list_indexes(
    db: "AsyncSession",
    repo_id: str,
    release_ids: list[str],
) -> tuple[dict[str, int], dict[str, int], dict[str, int]]:
    """Batch-prefetch all per-release aggregate stats in three queries.

    Returns
    -------
    tuple[asset_stats, issue_resolution_index, proposal_resolution_index]
        Three dicts keyed by release_id. All three queries run concurrently
        via asyncio.gather().

    Notes
    -----
    - Must be called once before building the release list. Never call inside
      a per-row loop.
    - asset_stats: SUM(download_count) GROUP BY release_id across
      musehub_release_assets.
    - issue_resolution_index: COUNT of issues where released_in == release_id.
    - proposal_resolution_index: COUNT of merged proposals in the commit window.
    """
    ...

Symbol Anchors

Existing Symbols (blast radius context)

Symbol File Kind Blast Radius
SemanticReleaseReport muse/core/semver.py TypedDict 14 callers
ChangelogEntry muse/core/semver.py TypedDict touches all changelog consumers
SemVerBump muse/domain/__init__.py Literal type touches run_suggest, _bump_rank, changelog parsing
_sync_compute_report musehub/services/release_analysis.py function 1 direct caller
analyse_release_background musehub/services/release_analysis.py async function 0 (entry point)
run_suggest muse/cli/commands/release.py function root of bump inference
_bump_rank muse/cli/commands/release.py function called only by run_suggest
_BUMP_ORDER muse/cli/commands/release.py variable inlined into _bump_rank
run_add muse/cli/commands/release.py function tag creation entry point
release_list_page musehub/api/routes/musehub/ui_releases.py async function primary target
release_detail_page musehub/api/routes/musehub/ui_releases.py async function primary target
MusehubRelease musehub/db/musehub_models.py ORM class all release services
MusehubReleaseAsset musehub/db/musehub_models.py ORM class asset download tracking
_format_release muse/cli/commands/release.py function CLI display formatter

New Symbols to Create

Symbol File Kind
ReleaseDetailContext musehub/api/routes/musehub/ui_releases.py TypedDict
ReleaseListEntry musehub/api/routes/musehub/ui_releases.py TypedDict
parse_semantic_report musehub/services/musehub_releases.py function
infer_bump_from_changelog musehub/services/musehub_releases.py function
enrich_release_list_entry musehub/services/musehub_releases.py function
prefetch_release_list_indexes musehub/services/musehub_releases.py async function
_build_release_detail_context musehub/api/routes/musehub/ui_releases.py async function
_enrich_release_list musehub/api/routes/musehub/ui_releases.py async function
release_bump_badge musehub/templates/musehub/… Jinja2 macro
release_api_diff_panel musehub/templates/musehub/… HTMX fragment
release_changelog_fragment musehub/templates/musehub/… HTMX fragment
release_provenance_panel musehub/templates/musehub/… HTMX fragment

HTMX Fragment Architecture (3-layer)

GET /{owner}/{repo}/releases
  └── full page: releases.html
        ├── hero card: latest_release (inline SSR)
        └── hx-get=…/releases/fragment?cursor=…
              └── fragment: release_rows.html
                    └── per-row: ReleaseListEntry rendered inline

GET /{owner}/{repo}/releases/{tag}
  └── full page: release_detail.html
        ├── semantic delta panel (inline SSR)
        ├── hx-get=…/releases/{tag}/changelog   → release_changelog_fragment.html
        ├── hx-get=…/releases/{tag}/api-diff    → release_api_diff_panel.html
        └── hx-get=…/releases/{tag}/provenance  → release_provenance_panel.html

New HTMX endpoints:

  • GET /{owner}/{repo}/releases/{tag}/changelog — changelog grouped by bump
  • GET /{owner}/{repo}/releases/{tag}/api-diff — API surface diff table
  • GET /{owner}/{repo}/releases/{tag}/provenance — agent/human breakdown

What Makes This Uniquely Muse

  1. Bump is inferred, not declared. The page shows not just "v2.4.0" but why — which commits drove the MINOR bump and which specific symbols changed. No other VCS UI exposes this.

  2. API surface diff is structural, not textual. The delta is derived from AST-level symbol extraction (SemanticReleaseReport.api_added/removed/modified), not a diff of text. function renamed and signature changed are distinct events.

  3. Semver is domain-agnostic. The same bump inference works for Python packages, TypeScript libs, MIDI bundles (Stori/Maestro), and smart contract ABIs. The sem_ver_bump field on commits reflects structural change regardless of what the files contain.

  4. Agent provenance is first-class. The agent_commits / human_commits ratio and unique_models breakdown are stored on every release and displayed alongside the code metrics. In the agent-first era this is as important as "who authored this".

  5. Content-addressed reproducibility. snapshot_id pins the release to the Muse object store permanently. Every file at every version is retrievable forever, not just "was at this commit hash when the CI ran".

  6. Breaking change blast radius. The breaking_changes list isn't prose — it's symbol addresses (file.py::Symbol). Each one can be clicked to see its blast radius (how many callers are affected) using the same muse code impact index that powers the issues page.


8-Tier Test Plan

Tier 1: Shape

class TestReleasePageShape:
    """Structural contracts for new TypedDicts and pure functions."""

    def test_release_list_entry_has_all_required_keys(self) -> None:
        """ReleaseListEntry TypedDict defines all 22 documented keys."""

    def test_release_detail_context_has_all_required_keys(self) -> None:
        """ReleaseDetailContext TypedDict defines all 24 documented keys."""

    def test_parse_semantic_report_has_docstring(self) -> None:
        """parse_semantic_report has a docstring with Parameters and Returns."""

    def test_infer_bump_from_changelog_has_docstring(self) -> None:
        """infer_bump_from_changelog has a docstring with Notes."""

    def test_enrich_release_list_entry_has_docstring(self) -> None:
        """enrich_release_list_entry has a docstring with Notes."""

    def test_prefetch_release_list_indexes_has_docstring(self) -> None:
        """prefetch_release_list_indexes has a docstring with Returns."""

    def test_bump_order_constant_exists(self) -> None:
        """_BUMP_ORDER exists and contains none < patch < minor < major."""

    def test_release_channel_literals(self) -> None:
        """ReleaseChannel covers stable, beta, alpha, nightly."""

Tier 2: Round-Trip

class TestReleasePageRoundTrip:
    """Integration contracts for the UI routes."""

    async def test_release_list_page_renders_latest_hero(
        self, client: AsyncClient, seeded_releases: list[MusehubRelease]
    ) -> None:
        """GET /owner/repo/releases returns 200 with latest stable in hero zone."""

    async def test_release_detail_page_renders_semantic_report(
        self, client: AsyncClient, release_with_report: MusehubRelease
    ) -> None:
        """GET /owner/repo/releases/{tag} renders API diff panel when report present."""

    async def test_release_detail_page_renders_placeholder_when_no_report(
        self, client: AsyncClient, release_no_report: MusehubRelease
    ) -> None:
        """Release detail renders graceful placeholder when semantic_report_json empty."""

    async def test_changelog_fragment_endpoint_returns_html(
        self, client: AsyncClient, release_with_changelog: MusehubRelease
    ) -> None:
        """GET /owner/repo/releases/{tag}/changelog returns 200 and grouped bump rows."""

    async def test_api_diff_fragment_returns_added_removed_modified(
        self, client: AsyncClient, release_with_report: MusehubRelease
    ) -> None:
        """GET /owner/repo/releases/{tag}/api-diff returns correct delta counts."""

    async def test_provenance_fragment_returns_agent_ratio(
        self, client: AsyncClient, release_with_agents: MusehubRelease
    ) -> None:
        """GET /owner/repo/releases/{tag}/provenance shows agent_ratio_pct."""

Tier 3: Edge Cases

class TestReleasePageEdgeCases:
    """Boundary conditions for release page enrichment."""

    def test_parse_semantic_report_returns_none_on_empty_string(self) -> None:
        """parse_semantic_report('') returns None without raising."""

    def test_parse_semantic_report_returns_none_on_null_json(self) -> None:
        """parse_semantic_report('null') returns None."""

    def test_parse_semantic_report_returns_none_on_malformed_json(self) -> None:
        """parse_semantic_report('{bad') returns None without raising."""

    def test_infer_bump_none_when_all_changelog_entries_are_none(self) -> None:
        """infer_bump_from_changelog returns ('none', []) when no drivers."""

    def test_infer_bump_major_beats_minor_beats_patch(self) -> None:
        """infer_bump_from_changelog returns max bump across mixed entries."""

    def test_enrich_release_list_entry_with_malformed_changelog(self) -> None:
        """enrich_release_list_entry sets commit_count=0 on JSON parse failure."""

    def test_release_list_page_with_no_releases(
        self, client: AsyncClient
    ) -> None:
        """GET /owner/repo/releases returns 200 with empty state (no hero card)."""

    def test_release_detail_page_404_on_unknown_tag(
        self, client: AsyncClient
    ) -> None:
        """GET /owner/repo/releases/v99.99.99 returns 404."""

    def test_pre_1_0_bump_adjustment_reflected_in_list_entry(self) -> None:
        """0.x repo: MAJOR bump becomes MINOR; ReleaseListEntry.semver_bump='minor'."""

    def test_is_breaking_true_when_breaking_changes_present(self) -> None:
        """enrich_release_list_entry sets is_breaking=True when changelog has breakage."""

    def test_agent_ratio_zero_when_no_agent_commits(self) -> None:
        """agent_ratio_pct is 0 when all commits have empty agent_id."""

    def test_agent_ratio_100_when_all_agent_commits(self) -> None:
        """agent_ratio_pct is 100 when every changelog entry has non-empty agent_id."""

Tier 4: Stress

class TestReleasePageStress:
    """Performance and scale contracts."""

    async def test_release_list_page_100_releases_under_500ms(
        self, client: AsyncClient
    ) -> None:
        """Release list with 100 rows renders under 500 ms end-to-end."""

    def test_enrich_release_list_entry_1000_calls_under_1s(self) -> None:
        """enrich_release_list_entry called 1000 times completes under 1 second."""

    def test_parse_semantic_report_500kb_blob_under_100ms(self) -> None:
        """parse_semantic_report handles a 500 KB JSON blob in under 100 ms."""

    def test_infer_bump_from_changelog_1000_entries_under_10ms(self) -> None:
        """infer_bump_from_changelog with 1000 changelog entries completes under 10 ms."""

    async def test_prefetch_release_list_indexes_100_ids_single_batch(
        self, db: AsyncSession
    ) -> None:
        """prefetch_release_list_indexes issues exactly 3 DB queries for 100 release IDs."""

Tier 5: Data Integrity

class TestReleasePageDataIntegrity:
    """Correctness of projected fields."""

    def test_api_added_count_matches_report(self) -> None:
        """ReleaseListEntry.api_added_count equals len(semantic_report.api_added)."""

    def test_breaking_count_equals_unique_addresses_in_changelog(self) -> None:
        """is_breaking and breaking_count derive from deduplicated breaking_changes list."""

    def test_total_downloads_is_sum_of_asset_download_counts(self) -> None:
        """total_downloads == sum(asset.download_count for asset in release.assets)."""

    def test_agent_ratio_rounds_correctly(self) -> None:
        """3 agent + 7 human commits → agent_ratio_pct = 30."""

    def test_hottest_file_is_first_file_hotspot_by_change_count(self) -> None:
        """hottest_file == file_hotspots[0].file_path when report present."""

    def test_available_formats_excludes_empty_download_url_values(self) -> None:
        """available_formats only includes download_urls keys with non-empty values."""

    def test_gpg_verified_true_only_when_signature_non_empty(self) -> None:
        """gpg_verified == bool(release.gpg_signature)."""

    def test_semver_str_matches_major_minor_patch(self) -> None:
        """semver_str == f'{major}.{minor}.{patch}' when pre is empty."""

Tier 6: Performance

class TestReleasePagePerformance:
    """Timing constraints for critical code paths."""

    def test_parse_semantic_report_valid_json_under_5ms(self) -> None:
        """parse_semantic_report with well-formed 10 KB report completes under 5 ms."""

    def test_infer_bump_pure_function_under_1ms_for_50_entries(self) -> None:
        """infer_bump_from_changelog with 50 entries completes under 1 ms."""

    def test_enrich_release_list_entry_single_call_under_1ms(self) -> None:
        """Single enrich_release_list_entry call completes under 1 ms."""

Tier 7: Security

class TestReleasePageSecurity:
    """Input sanitisation for display fields."""

    @pytest.mark.parametrize("tag", [
        "<script>alert(1)</script>",
        "v1.0.0\x00hidden",
        "v1.0.0\r\nX-Injected: true",
        "v1.0.0" + "\u202e" + "evil",  # RTL override
        "../../../etc/passwd",
        "v1.0.0; rm -rf /",
    ])
    def test_tag_sanitised_before_display(self, tag: str) -> None:
        """release_detail_page passes tag through sanitize_display before rendering."""

    def test_release_title_xss_not_rendered_raw(
        self, client: AsyncClient
    ) -> None:
        """Release title containing HTML tags is escaped in the rendered page."""

    def test_asset_filename_not_rendered_unsanitised(self) -> None:
        """Asset filename containing path traversal is sanitized in display."""

    def test_changelog_message_xss_escaped(self) -> None:
        """ChangelogEntry.message containing HTML is escaped in changelog fragment."""

    def test_agent_id_with_null_byte_not_displayed(self) -> None:
        """agent_id containing null byte is sanitized before rendering in provenance panel."""

    def test_model_id_with_ansi_escape_not_displayed(self) -> None:
        """model_id containing ANSI escape is sanitized before display."""

    def test_semantic_report_json_injection_via_blob(self) -> None:
        """Malicious semantic_report_json blob that embeds HTML is not rendered raw."""

Tier 8: Docstrings

class TestReleasePageDocstrings:
    """Verify all new symbols are documented."""

    def test_release_detail_context_has_docstring_with_all_fields(self) -> None:
        """ReleaseDetailContext docstring lists all 24 fields."""

    def test_release_list_entry_has_docstring_with_all_fields(self) -> None:
        """ReleaseListEntry docstring lists all 22 fields."""

    def test_parse_semantic_report_docstring_covers_none_return(self) -> None:
        """parse_semantic_report docstring documents the None return case."""

    def test_infer_bump_from_changelog_docstring_covers_pure_note(self) -> None:
        """infer_bump_from_changelog docstring includes 'Pure function' Note."""

    def test_enrich_release_list_entry_docstring_covers_zero_queries(self) -> None:
        """enrich_release_list_entry docstring includes 'Zero additional DB queries'."""

    def test_prefetch_release_list_indexes_docstring_covers_gather(self) -> None:
        """prefetch_release_list_indexes docstring mentions asyncio.gather()."""

    def test_all_new_symbols_exist_as_module_attributes(self) -> None:
        """parse_semantic_report, infer_bump_from_changelog, enrich_release_list_entry,
        prefetch_release_list_indexes are importable from musehub.services.musehub_releases."""

7-Phase Implementation Plan

Phase 1: Data Layer — Parse & Enrich

  • Add parse_semantic_report(json_str) to musehub/services/musehub_releases.py
  • Add infer_bump_from_changelog(changelog) — mirrors _bump_rank logic from muse/cli/commands/release.py::_bump_rank
  • Add enrich_release_list_entry(release, asset_stats, issue_idx, proposal_idx)
  • Add prefetch_release_list_indexes(db, repo_id, release_ids)
  • Unit-test all four with Tier 1, 3, 5, 6, 7 tests

Phase 2: Release List Page Enrichment

  • Refactor release_list_page to call prefetch_release_list_indexes then enrich_release_list_entry per row
  • Add ReleaseListEntry TypedDict to route module
  • Update releases.html template to render bump badge, API delta counts, agent ratio, breaking change callout on each row
  • Tier 2 + Tier 4 list performance tests

Phase 3: Release Detail — Semantic Observatory

  • Refactor release_detail_page to build full ReleaseDetailContext:
    • parse semantic_report_json
    • parse changelog_json + call infer_bump_from_changelog
    • resolve resolved_issues and resolved_proposals via anchor matching
  • Add _build_release_detail_context private helper
  • Update release_detail.html with the four-panel layout (delta, languages, changelog, hotspots, API diff, provenance, resolved issues/proposals)
  • Tier 2 integration tests for full detail page

Phase 4: HTMX Fragments for Lazy Panels

  • Add three new HTMX endpoints: /releases/{tag}/changelog, /releases/{tag}/api-diff, /releases/{tag}/provenance
  • Create three new Jinja2 fragment templates
  • Update release_detail.html to use hx-get lazy loading for heavy panels
  • Tier 2 fragment endpoint tests

Phase 5: Inferred Bump Justification Panel

  • Add "Why this version?" collapsible panel to release_detail.html
  • Render bump_drivers list — each with commit short-ID, message, bump level, and symbol change summary
  • Add pre_1_0_adjusted callout when applicable
  • Tier 3 edge-case tests for 0.x adjustment display

Phase 6: Security Hardening

  • Audit all fields passed to templates for sanitize_display coverage: tag, title, body, author, agent_id, model_id, changelog messages, asset filenames, breaking change symbol addresses
  • Tier 7 security parametrised tests

Phase 7: Stylesheet & TypeScript

  • Update src/scss/_releases.scss with bump badge colour tokens: --bump-major, --bump-minor, --bump-patch, --bump-none
  • Update src/ts/pages/release-detail.ts for HTMX fragment event hooks
  • Update src/ts/pages/release-list.ts for cursor pagination + infinite scroll
  • Tier 4 render-performance smoke tests

What Makes This Uniquely Muse (Summary)

  • Version numbers are proved, not declared — the symbol contract graph determines the bump; the page shows the proof
  • API surface diff is structural (AST), not textual (line diff)
  • Semver inference works for any domain: Python, TypeScript, MIDI bundles, smart contract ABIs
  • Agent provenance is a first-class metric alongside human authorship
  • Content-addressed reproducibility via snapshot_id — not "was at commit X" but "is object Y in the immutable store"
  • Breaking change addresses are clickable symbol anchors, not prose notes
Activity
gabriel opened this issue 37 days ago
No activity yet. Use the CLI to comment.