Release Page: Semantic Version Observatory
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 stringschannel—stable | beta | alpha | nightly(replaces the booleanis_prerelease)snapshot_id— content-addressed object store ID; reproducible forever, not just "was on this commit at this date"semantic_report_json— aSemanticReleaseReportblob computed bymuse.plugins.code.release_analysis.compute_release_analysisat push time: language stats, API surface diff (added/removed/modified symbols), file hotspots, refactor events, agent provenance aggregated across the full commit windowchangelog_json— typedChangelogEntryarray; every entry carriessem_ver_bump,breaking_changes,agent_id,model_id— derived from structural symbol diffs, not commit message conventionsagent_id+model_id— cryptographic AI provenance from the tip commitgpg_signature— optional ASCII-armoured signature on the tag objectdownload_urls— JSON map of distribution package types to artifact URLsassets—MusehubReleaseAssetattachments with per-assetdownload_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 bumpGET /{owner}/{repo}/releases/{tag}/api-diff— API surface diff tableGET /{owner}/{repo}/releases/{tag}/provenance— agent/human breakdown
What Makes This Uniquely Muse
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.
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 renamedandsignature changedare distinct events.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_bumpfield on commits reflects structural change regardless of what the files contain.Agent provenance is first-class. The
agent_commits / human_commitsratio andunique_modelsbreakdown are stored on every release and displayed alongside the code metrics. In the agent-first era this is as important as "who authored this".Content-addressed reproducibility.
snapshot_idpins 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".Breaking change blast radius. The
breaking_changeslist 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 samemuse code impactindex 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)tomusehub/services/musehub_releases.py - Add
infer_bump_from_changelog(changelog)— mirrors_bump_ranklogic frommuse/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_pageto callprefetch_release_list_indexesthenenrich_release_list_entryper row - Add
ReleaseListEntryTypedDict to route module - Update
releases.htmltemplate 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_pageto build fullReleaseDetailContext:- parse
semantic_report_json - parse
changelog_json+ callinfer_bump_from_changelog - resolve
resolved_issuesandresolved_proposalsvia anchor matching
- parse
- Add
_build_release_detail_contextprivate helper - Update
release_detail.htmlwith 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.htmlto usehx-getlazy 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_driverslist — each with commit short-ID, message, bump level, and symbol change summary - Add
pre_1_0_adjustedcallout when applicable - Tier 3 edge-case tests for 0.x adjustment display
Phase 6: Security Hardening
- Audit all fields passed to templates for
sanitize_displaycoverage: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.scsswith bump badge colour tokens:--bump-major,--bump-minor,--bump-patch,--bump-none - Update
src/ts/pages/release-detail.tsfor HTMX fragment event hooks - Update
src/ts/pages/release-list.tsfor 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