gabriel / musehub public
Closed #37
filed by gabriel human · 42 days ago

Proposal Reimagination: State Transition Manifests for a Multidimensional Universe

0 Anchors
Blast radius
Churn 30d
0 Proposals

Proposal Reimagination: State Transition Manifests for a Multidimensional Universe

The Paradigm Break

A pull request asks someone to pull a change to a text file. That framing belongs to the era when version control was a word processor with history.

A Muse proposal is something categorically different: it is a cryptographically-attested declaration that the world should transition from one multidimensional state to another.

The current implementation is a PR clone: from_branch + to_branch, three states, four review verdicts. That's Git vocabulary bolted onto a domain-agnostic state machine. This ticket is the complete reimagination.


What Muse Actually Tracks

Muse commits are snapshots of a state vector — not a file tree. Each commit captures the full multidimensional state across all active domains simultaneously:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MUSE COMMIT = STATE VECTOR                               │
├─────────────┬──────────────┬─────────────┬──────────────┬───────────────────┤
│  CODE       │  MIDI        │  STEMS       │  PROSE       │  PAYMENTS         │
│  ─────────  │  ─────────   │  ─────────   │  ─────────   │  ─────────────    │
│  AST diffs  │  Note events │  Waveform    │  Semantic    │  Ledger deltas    │
│  Symbol     │  Tracks      │  Fingerprint │  Readability │  Claim chains     │
│  blast      │  Tempo map   │  Frequency   │  Structure   │  Settlement state │
│  Breakage   │  Harmony     │  Amplitude   │  Citations   │  AVAX settlement  │
│  Test gaps  │  Rhythm      │  Stems tree  │  Sections    │  nonce linkage    │
└─────────────┴──────────────┴─────────────┴──────────────┴───────────────────┘

A proposal declares: "apply this state transition to the shared timeline". The reviewers are not just reading diffs — they are deciding whether the proposed future state is valid, safe, and desirable across every dimension the repo tracks.


What a Proposal IS (Redesigned)

╔══════════════════════════════════════════════════════════════════════════════╗
║  PROPOSAL — A SIGNED STATE TRANSITION DECLARATION                           ║
╠══════════════════════════════════════════════════════════════════════════════╣
║                                                                              ║
║   PROPOSER: @gabriel (Ed25519 sig over canonical PROPOSE message)           ║
║   TYPE: state_merge | stem_integration | midi_evolution | agent_delegation  ║
║          payment_settlement | identity_transition | canonical_release        ║
║                                                                              ║
║   FROM STATE:  sha256(from_branch HEAD snapshot)  ←  cryptographic anchor  ║
║   TO STATE:    sha256(to_branch HEAD snapshot)    ←  cryptographic anchor   ║
║                                                                              ║
║   DIMENSIONAL MANIFEST DELTA:                                                ║
║   ┌──────────────────────────────────────────────────────────────────────┐  ║
║   │  CODE    ▓▓▓▓▓▓▓░░░░  risk: HIGH   breakage: 2   test_gap: 3       │  ║
║   │  MIDI    ▓▓▓▓░░░░░░░  risk: MED    tracks: +1    harmony_Δ: 0.23   │  ║
║   │  STEMS   ▓░░░░░░░░░░  risk: LOW    added: 2      waveform_Δ: 0.05  │  ║
║   │  PROSE   ░░░░░░░░░░░  risk: NONE   no changes                      │  ║
║   │  PAYMENTS▓▓░░░░░░░░░  risk: LOW    Δ: +150K nanoMUSE               │  ║
║   └──────────────────────────────────────────────────────────────────────┘  ║
║                                                                              ║
║   OVERALL RISK VECTOR: [0.7, 0.4, 0.1, 0.0, 0.2]                          ║
║   AGGREGATE RISK SCORE: 0.48  (MEDIUM)                                      ║
║                                                                              ║
║   MERGE CONDITIONS:                                                          ║
║   [✓] 2 approvals required  [✓] code domain reviewed                       ║
║   [✗] midi domain not yet reviewed                                          ║
║   [✓] all commits signed    [✓] risk_score ≤ 60                            ║
║                                                                              ║
║   DEPENDENCIES: none  |  BLOCKS: proposal #7                                ║
╚══════════════════════════════════════════════════════════════════════════════╝

The Seven Proposal Types

Not all state transitions are the same. Type determines which domains are relevant, which reviewers are appropriate, and which merge strategies are valid.

╔══════════════════════════════════════════════════════════════════════════════╗
║  PROPOSAL TYPES                                                              ║
╠══════════════════════════════════════════════════════════════════════════════╣
║                                                                              ║
║  1. STATE_MERGE          — general cross-domain branch integration          ║
║     Domains: any  |  Merge: overlay | weave | rebase | domain_selective    ║
║                                                                              ║
║  2. STEM_INTEGRATION     — integrate new/updated audio stems               ║
║     Domains: stems (primary), payments (optional: licensing fees)          ║
║     Reviewer type: human musician or certified audio_review agent          ║
║                                                                              ║
║  3. MIDI_EVOLUTION       — propose a musical arrangement change            ║
║     Domains: midi (primary), stems (secondary if bounce attached)          ║
║     Diff shows: per-track note delta, harmonic tension, complexity         ║
║                                                                              ║
║  4. PAYMENT_SETTLEMENT   — settle a chain of MPay claims to AVAX L1       ║
║     Domains: payments (primary)                                             ║
║     Auto-merge when: all claims valid + on-chain confirmation received     ║
║                                                                              ║
║  5. AGENT_DELEGATION     — propose spawning/updating an agent identity     ║
║     Domains: identity  |  Requires: human + quorum if org                 ║
║     Scope changes are diff-visible: before/after capability sets          ║
║                                                                              ║
║  6. IDENTITY_TRANSITION  — key rotation / HD migration / org restructure  ║
║     Domains: identity  |  Highest security review requirement              ║
║                                                                              ║
║  7. CANONICAL_RELEASE    — tag a state as a versioned, immutable release  ║
║     Domains: all  |  Locks all dimensions at the tagged commit             ║
║     On merge: creates signed release manifest + fires release webhooks    ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝

The New State Machine

Current: open → merged | closed (three states, flat)

Redesigned: a typed, guarded lifecycle where each transition has preconditions.

                         ┌─────────────────────────────────────────────┐
                         │           PROPOSAL LIFECYCLE                 │
                         └─────────────────────────────────────────────┘

  create(draft=True)
        │
        ▼
   ┌─────────┐  publish()  ┌──────┐
   │ DRAFTING │───────────▶│ OPEN │
   └─────────┘             └──────┘
                              │  ╔════════════════════════════════════╗
                              │  ║  Open → In-Review                  ║
                              │  ║  Trigger: first reviewer assigned  ║
                              │  ╚════════════════════════════════════╝
                              │
                              ▼
                         ┌───────────┐
                         │ IN_REVIEW │◀────────────────────┐
                         └───────────┘                     │
                              │                            │ request_changes()
                              │ approve() [N approvals]    │
                              ▼                            │
                         ┌──────────┐  needs_changes()     │
                         │ APPROVED │──────────────────────┘
                         └──────────┘
                              │
                              │ merge_conditions_met()
                              ▼
                         ┌──────────┐  on-chain confirm   ┌─────────┐
                         │ SETTLING │────────────────────▶│ MERGED  │
                         └──────────┘                      └─────────┘
                              │
                         ┌────┴────┐  reject()   ┌──────────┐
                         │         │────────────▶│ REJECTED │
                         │  (any)  │             └──────────┘
                         │         │  abandon()  ┌───────────┐
                         └─────────┘────────────▶│ ABANDONED │
                                                  └───────────┘

State meanings:

  • DRAFTING: author still composing; not visible to others except invited reviewers
  • OPEN: published; accepting reviews
  • IN_REVIEW: at least one reviewer assigned; under active evaluation
  • APPROVED: all merge_conditions met; eligible to merge
  • SETTLING: merge initiated; for payment_settlement type, awaiting on-chain confirmation
  • MERGED: state transition applied; from_branch retired
  • REJECTED: explicitly closed by reviewer quorum or author after changes_requested
  • ABANDONED: closed by author; no activity; archivable

Dimensional Diff as the Core UX

The diff is not a unified text patch. It is a per-domain divergence manifest:

Code Domain

┌─────────────────────────────────────────────────────────────────────────────┐
│  CODE DOMAIN DIFF                                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│  SYMBOLS CHANGED (7)                          BLAST RADIUS                  │
│  ┌─────────────────────────────────────┐     ┌───────────────────────────┐ │
│  │  + auth.py::AuthService.login       │     │ auth.py::AuthService (★)  │ │
│  │  ~ auth.py::AuthService.verify      │     │  → api/routes/auth.py (2) │ │
│  │  - auth.py::LegacyAuth              │     │  → services/user.py   (1) │ │
│  │  + core/msign.py::verify_msign_hdr  │     │  → tests/test_auth.py (4) │ │
│  │  ~ core/msign.py::canonical_msg     │     └───────────────────────────┘ │
│  │  + tests/test_auth.py::TestMSign    │                                    │
│  │  ~ pyproject.toml                   │     BREAKAGE COUNT: 2             │
│  └─────────────────────────────────────┘     TEST GAP COUNT: 1             │
│                                               STRUCTURAL RISK: HIGH        │
└─────────────────────────────────────────────────────────────────────────────┘

MIDI Domain

┌─────────────────────────────────────────────────────────────────────────────┐
│  MIDI DOMAIN DIFF                                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│  TRACKS CHANGED (2 of 8)                      HARMONIC ANALYSIS            │
│  ┌─────────────────────────────────────┐     ┌───────────────────────────┐ │
│  │  ~ Piano  [bars 1–32]               │     │  Key: C minor → C major   │ │
│  │    notes: 128→156 (+28)             │     │  Tension delta: +0.23     │ │
│  │    velocity avg: 82→91 (+9)         │     │  Consonance: ↑ (brighter) │ │
│  │    density: 3.2→3.9 notes/beat      │     └───────────────────────────┘ │
│  │                                     │                                    │
│  │  + Strings [bars 17–32] (new track) │     RHYTHMIC ANALYSIS             │
│  │    notes: 64                        │     ┌───────────────────────────┐ │
│  │    velocity avg: 68                 │     │  Tempo: 120 BPM (no Δ)    │ │
│  │    range: C3–G5                     │     │  Groove delta: +0.08      │ │
│  └─────────────────────────────────────┘     │  Complexity: ↑ LOW        │ │
│                                               └───────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

Payments Domain

┌─────────────────────────────────────────────────────────────────────────────┐
│  PAYMENTS DOMAIN DIFF                                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│  LEDGER DELTA: +150,000 nanoMUSE                                            │
│  CLAIMS INTRODUCED: 3                                                       │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │  claim[0] nonce: 0xabc… from: gabriel to: aaronrene  50,000 nanoMUSE  │ │
│  │  claim[1] nonce: sha256(claim[0].sig)  from: gabriel to: studio9  …   │ │
│  │  claim[2] nonce: sha256(claim[1].sig)  from: gabriel to: mixeng7  …   │ │
│  └────────────────────────────────────────────────────────────────────────┘ │
│  AVAX SETTLEMENT: pending  |  payer_avax: 0x1a2b…  recipient: 0xcd3e…    │
│  CHAIN VERIFIED: all nonce links valid  |  Ed25519 sigs: ✓ all pass       │
└─────────────────────────────────────────────────────────────────────────────┘

Dimensional Comments

Comments are not line numbers. They are coordinates in state space:

class ProposalCommentTarget(TypedDict, total=False):
    """Domain-agnostic comment coordinate system.

    Exactly one of the domain-specific groups should be populated.
    'general' targets the proposal as a whole (no domain coordinate).
    """
    # Universal
    target_type: Literal["general", "code", "midi", "stem", "payment", "identity"]

    # Code domain
    symbol_address: str        # "auth.py::AuthService.login"
    line_start: int            # within the symbol's source range
    line_end: int

    # MIDI domain
    track_name: str            # "Piano", "Strings"
    beat_start: float          # 0.0-based within the track
    beat_end: float
    note_pitch: int            # 0–127 MIDI pitch (None = whole region)

    # Stem domain
    stem_id: str               # stem object_id
    timestamp_start: float     # seconds from stem start
    timestamp_end: float

    # Payment domain
    nonce_hex: str             # specific claim in the payment chain

    # Identity domain
    identity_handle: str       # which identity this comment targets

Proposal Chaining (Dependency DAG)

                 ┌─────────────────────────────────────────────┐
                 │              PROPOSAL DAG                    │
                 └─────────────────────────────────────────────┘

   #1 [MERGED]                #2 [MERGED]
   auth refactor              midi key change
        │                          │
        └──────────┬───────────────┘
                   │  depends_on: [#1, #2]
                   ▼
              #3 [OPEN]                 #5 [OPEN]
              release v2.0              agent scope expansion
                   │                         │
                   │  blocks: [#4]           │  depends_on: [#3]
                   ▼                         │
              #4 [DRAFTING]  ◀──────────────┘
              payment settlement for v2.0
class ProposalDependency(TypedDict):
    """Proposal dependency edge in the state transition DAG."""
    proposal_id: str        # this proposal
    depends_on: list[str]   # cannot merge until all of these are merged
    blocks: list[str]       # merging this proposal unblocks these
    soft_depends_on: list[str]  # informational only; does not gate merge

Merge Strategies (Expanded)

┌──────────────────────────────────────────────────────────────────────────────┐
│  MERGE STRATEGIES                                                            │
├─────────────────────┬────────────────────────────────────────────────────────┤
│  state_overlay      │  Current: from_branch wins all conflicts (theirs)      │
│  (current default)  │                                                        │
├─────────────────────┼────────────────────────────────────────────────────────┤
│  state_weave        │  Per-domain strategy selection:                        │
│                     │    code: overlay  (from_branch wins)                   │
│                     │    midi: interleave (merge tracks additively)          │
│                     │    payments: append (both chains preserved)            │
│                     │    stems: union (all stems kept)                       │
├─────────────────────┼────────────────────────────────────────────────────────┤
│  state_rebase       │  Replay from_branch commits on top of to_branch HEAD   │
│                     │  (one synthetic commit per from_branch commit)         │
├─────────────────────┼────────────────────────────────────────────────────────┤
│  domain_selective   │  Only merge specified domains:                         │
│                     │    --domains code,midi  (leave stems/payments alone)   │
│                     │  Enables: "take the MIDI changes, not the code"        │
├─────────────────────┼────────────────────────────────────────────────────────┤
│  phased             │  Merge domains in sequence with approval gates:        │
│                     │    phase 1: code (requires code reviewer approval)     │
│                     │    phase 2: midi (requires midi reviewer approval)     │
│                     │    phase 3: payments (requires on-chain confirm)       │
└─────────────────────┴────────────────────────────────────────────────────────┘

Merge Conditions — Declarative Gating

class MergeConditions(TypedDict, total=False):
    """Declarative preconditions that must all be satisfied before merge."""
    require_approvals: int               # minimum distinct approvals
    require_domains_approved: list[str]  # each domain must have ≥1 approval
    max_risk_score: float                # 0.0–1.0; proposal blocked if exceeded
    require_signed_commits: bool         # all commits in from_branch must be MSign-signed
    require_no_breakage: bool            # breakage_count must be 0
    require_test_coverage: bool          # no test_gap_count > 0
    require_payment_settled: bool        # for payment_settlement type: on-chain confirmation
    require_dependency_merged: bool      # all depends_on proposals must be merged first
    max_agent_commit_ratio: float        # 0.0–1.0; max fraction of commits from agents

These can be set in .muse/proposal_defaults.toml at the repo level, overridden per proposal.


Proposal as a Cryptographic Claim

A proposal is not just a database row. It is a signed state transition declaration:

PROPOSE\n{proposer_handle}\n{from_state_hash}\n{to_state_hash}\n{proposal_type}\n{ts}
  • from_state_hash = SHA-256 of the from_branch HEAD snapshot manifest
  • to_state_hash = SHA-256 of the to_branch HEAD snapshot manifest
  • proposal_type = one of the seven types above

This canonical message is signed with the proposer's Ed25519 key and stored as proposer_sig_b64. It means:

  1. The proposal is tamper-evident — any change to the from/to state hashes invalidates the signature
  2. The proposal is attribution-certain — we know exactly who declared this state transition, with cryptographic proof
  3. Review approvals are also signed: APPROVE_PROPOSAL\n{reviewer}\n{proposal_id}\n{ts} — reviewers attest to the proposed state

Reviewer Archetypes

Not all reviewers are equal. Reviewers have domain expertise:

┌──────────────────────────────────────────────────────────────────────────────┐
│  REVIEWER ARCHETYPES                                                         │
├───────────────────────┬──────────────────────────────────────────────────────┤
│  code_reviewer        │  human or agent; reviews code domain diff            │
│  midi_reviewer        │  human musician or certified MIDI analysis agent     │
│  audio_reviewer       │  human producer or certified stem analysis agent     │
│  payment_reviewer     │  human + AVAX wallet holder; approves settlements    │
│  security_reviewer    │  human; required for identity_transition proposals   │
│  release_manager      │  can approve canonical_release proposals             │
└───────────────────────┴──────────────────────────────────────────────────────┘

A proposal's merge_conditions.require_domains_approved gates specifically on domain-appropriate reviewers. A MIDI_EVOLUTION proposal doesn't need a code reviewer to approve.

Agent reviewers sign their reviews just like humans — ATTEST\n{reviewer}\n{proposal_id}\n{verdict}\n{ts} — making every approval cryptographically verifiable.


New Data Model

musehub_proposals — extended columns

-- New columns on top of existing schema:
ALTER TABLE musehub_proposals ADD COLUMN
    proposal_type       TEXT NOT NULL DEFAULT 'state_merge',
    state               TEXT NOT NULL DEFAULT 'open',  -- extended state machine
    proposer_sig_b64    TEXT,           -- Ed25519 sig over PROPOSE canonical message
    from_state_hash     TEXT,           -- SHA-256 of from_branch HEAD snapshot
    to_state_hash       TEXT,           -- SHA-256 of to_branch HEAD snapshot
    dimensional_risk    JSON,           -- per-domain risk vector: {"code": 0.7, ...}
    depends_on          JSON,           -- list[proposal_id]
    blocks              JSON,           -- list[proposal_id]
    soft_depends_on     JSON,           -- list[proposal_id]
    merge_conditions    JSON,           -- MergeConditions dict
    merge_strategy      TEXT NOT NULL DEFAULT 'state_overlay',
    selective_domains   JSON,           -- for domain_selective strategy
    is_draft            BOOLEAN NOT NULL DEFAULT FALSE,
    published_at        TIMESTAMPTZ,
    settling_at         TIMESTAMPTZ,
    abandoned_at        TIMESTAMPTZ,
    rejection_reason    TEXT;

musehub_proposal_dependencies — DAG edges

CREATE TABLE musehub_proposal_dependencies (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    proposal_id     TEXT NOT NULL REFERENCES musehub_proposals(proposal_id) ON DELETE CASCADE,
    depends_on_id   TEXT NOT NULL REFERENCES musehub_proposals(proposal_id),
    dependency_type TEXT NOT NULL DEFAULT 'hard',  -- 'hard' | 'soft'
    created_at      TIMESTAMPTZ DEFAULT now(),
    UNIQUE (proposal_id, depends_on_id)
);
CREATE INDEX ON musehub_proposal_dependencies (depends_on_id);

musehub_proposal_reviews — extended

-- New columns:
ALTER TABLE musehub_proposal_reviews ADD COLUMN
    review_domain       TEXT,           -- 'code' | 'midi' | 'stems' | etc.
    reviewer_sig_b64    TEXT,           -- Ed25519 sig over APPROVE_PROPOSAL canonical
    reviewer_archetype  TEXT;           -- 'code_reviewer' | 'midi_reviewer' | etc.

musehub_proposal_simulations — dry-merge results

CREATE TABLE musehub_proposal_simulations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    proposal_id     TEXT NOT NULL REFERENCES musehub_proposals(proposal_id) ON DELETE CASCADE,
    run_at          TIMESTAMPTZ DEFAULT now(),
    strategy        TEXT NOT NULL,
    result_state    TEXT NOT NULL,      -- 'success' | 'conflict' | 'error'
    conflicts       JSON,               -- per-domain conflict list
    merged_state_hash TEXT,             -- SHA-256 of what merged state WOULD be
    simulation_log  TEXT,
    agent_id        TEXT                -- which agent ran the simulation, if any
);

API Extensions

New/extended endpoints

POST   /api/repos/{repo_id}/proposals                     ← draft support, type field
PATCH  /api/repos/{repo_id}/proposals/{id}                ← update draft, merge_conditions
POST   /api/repos/{repo_id}/proposals/{id}/publish        ← draft → open
POST   /api/repos/{repo_id}/proposals/{id}/simulate       ← dry-merge + conflict detection
GET    /api/repos/{repo_id}/proposals/{id}/simulate       ← fetch latest simulation result
GET    /api/repos/{repo_id}/proposals/{id}/diff           ← extended: all domain diffs
POST   /api/repos/{repo_id}/proposals/{id}/abandon        ← open/approved → abandoned
GET    /api/repos/{repo_id}/proposals?depends_on=X        ← filter by DAG dependency
GET    /api/repos/{repo_id}/proposals?type=midi_evolution ← filter by proposal type

Extended ProposalCreate

class ProposalCreate(BaseModel):
    title: str = Field(min_length=1, max_length=500)
    from_branch: str = Field(min_length=1, max_length=255)
    to_branch: str = Field(min_length=1, max_length=255)
    body: str = Field(max_length=50_000, default="")
    proposal_type: ProposalType = ProposalType.STATE_MERGE
    is_draft: bool = False
    merge_conditions: MergeConditions | None = None
    merge_strategy: MergeStrategy = MergeStrategy.STATE_OVERLAY
    selective_domains: list[str] | None = None
    depends_on: list[str] = Field(default_factory=list)  # proposal IDs

CLI Extensions

# Draft workflow
muse hub proposal create --title "feat: auth v2" --draft
muse hub proposal publish <id>

# Type-aware creation
muse hub proposal create --title "Piano arrangement v3" --type midi_evolution
muse hub proposal create --title "Settle Q1 payments" --type payment_settlement

# Merge conditions
muse hub proposal create --title "Release v2.0" \
  --type canonical_release \
  --require-approvals 3 \
  --require-domains code,midi \
  --require-signed-commits \
  --require-no-breakage

# Domain-selective merge
muse hub proposal merge <id> --strategy domain_selective --domains code

# Dry-merge simulation (shows conflicts without applying)
muse hub proposal simulate <id>
muse hub proposal simulate <id> --strategy state_weave --json

# Dependency management
muse hub proposal depends-on <id> --add <other-id>
muse hub proposal depends-on <id> --remove <other-id>
muse hub proposal dag <id>           # show full dependency graph

# Enhanced diff
muse hub proposal diff <id>          # all domains
muse hub proposal diff <id> --domain midi   # MIDI domain only
muse hub proposal diff <id> --domain code   # code domain blast radius

# Abandon
muse hub proposal abandon <id> [--reason "..."]

# Domain-aware review
muse hub proposal review <id> --verdict approve --domain code
muse hub proposal review <id> --verdict approve --domain midi

Symbol Anchors & Blast Radius

New symbols to create

Symbol File Purpose
ProposalType musehub/models/musehub.py Enum: 7 proposal types
ProposalState musehub/models/musehub.py Extended state machine (7 states)
MergeStrategy musehub/models/musehub.py Enum: 5 strategies
MergeConditions musehub/models/musehub.py Typed merge preconditions
ProposalCommentTarget musehub/models/musehub.py Domain-agnostic coordinate
DimensionalRiskVector musehub/models/musehub.py Per-domain risk dict
simulate_merge musehub/services/musehub_proposals.py Dry-merge without committing
check_merge_conditions musehub/services/musehub_proposals.py Evaluate all conditions
compute_dimensional_risk musehub/services/musehub_proposal_risk.py Per-domain risk vector
verify_proposal_sig musehub/services/musehub_proposals.py Ed25519 sig over PROPOSE msg
build_proposal_claim muse/core/msign.py Sign PROPOSE canonical message
ProposalDependencyGraph musehub/services/proposal_dag.py DAG cycle detection + ordering

Blast radius of changes

musehub/models/musehub.py         ← ProposalType, MergeStrategy, MergeConditions
  ↓ imported by
musehub/api/routes/musehub/proposals.py    ← all 10+ endpoints
musehub/services/musehub_proposals.py      ← service layer
musehub/services/musehub_proposal_risk.py  ← risk engine
musehub/mcp/write_tools/proposals.py       ← MCP tools
  ↓
musehub/db/musehub_models.py       ← ORM + migrations
musehub/db/migrations/             ← 3 new migrations
  ↓
muse/cli/commands/hub.py           ← CLI: proposal create/merge/simulate/dag
muse/core/msign.py                 ← build_proposal_claim (new primitive)
  ↓
tests/test_proposals_*.py          ← all 8 tiers per phase

8-Tier Test Plan

Tier 1 — Shape / Schema

  • ProposalType enum has exactly 7 values
  • ProposalState enum has exactly 7 states
  • MergeStrategy enum has exactly 5 values
  • MergeConditions TypedDict has all expected keys
  • ProposalCreate.proposal_type defaults to STATE_MERGE
  • ProposalCreate.is_draft defaults to False
  • ProposalCreate.merge_strategy defaults to STATE_OVERLAY
  • ProposalCommentTarget allows all 6 target_type values
  • DB model has proposal_type, is_draft, dimensional_risk, depends_on, merge_conditions columns
  • musehub_proposal_dependencies table exists with correct FK constraints

Tier 2 — Round-Trip / Integration

  • Create draft proposal → state is drafting, invisible to other users
  • Publish draft → state becomes open
  • Assign reviewers → state becomes in_review
  • Submit N approvals → when conditions met, state becomes approved
  • Merge approved proposal → state becomes merged, from_branch deleted
  • Abandon open proposal → state becomes abandoned
  • Create with depends_on → merge blocked until dependency merged
  • simulate_merge → returns conflicts without advancing state
  • Domain-selective merge (code only) → MIDI/stems state unchanged
  • Payment_settlement proposal → settling state while awaiting on-chain confirm

Tier 3 — Edge Cases

  • Merge a proposal that depends on an already-merged proposal (should succeed)
  • Merge a proposal that depends on an open proposal (should be blocked)
  • Circular dependency detection: proposal A depends on B depends on A → 400
  • domain_selective merge with no valid domains → 400
  • Simulate merge on already-merged proposal → 409
  • Abandon a merged proposal → 409 (cannot abandon merged)
  • Submit review for a domain not in the proposal's diff → warning but allowed
  • merge_conditions.require_approvals: 0 → merge immediately on publish
  • Draft proposal → merge attempt → 400 (cannot merge a draft)
  • require_no_breakage: true but breakage_count=1 → merge blocked

Tier 4 — Stress

  • Create 1,000 proposals on one repo → list query under 200ms (keyset pagination)
  • Proposal with 50 depends_on edges → dependency resolution under 50ms
  • compute_dimensional_risk on a 500-symbol proposal under 100ms
  • simulate_merge on a repo with 10,000 tracked files under 2 seconds
  • 100 concurrent simulate_merge requests → no deadlock, all complete
  • list_proposal_comments with 5,000 threaded comments → under 500ms

Tier 5 — Data Integrity

  • from_state_hash matches SHA-256 of from_branch HEAD snapshot at creation time
  • proposer_sig_b64 verifies against proposer's registered public key
  • Review reviewer_sig_b64 verifies against reviewer's public key
  • depends_on JSON array entries are valid proposal UUIDs in the same repo
  • dimensional_risk values are all in [0.0, 1.0]
  • Merging proposal X automatically unblocks proposals that list X in depends_on
  • merged_at is set iff state = merged; null otherwise
  • settling_at is set iff state = settling; null otherwise

Tier 6 — Performance / Benchmarks

  • compute_dimensional_risk (code domain) under 50ms for 100-symbol proposal
  • simulate_merge (state_overlay strategy) under 500ms for 1,000-file repo
  • DAG cycle detection (Kahn's algorithm) under 10ms for 100-node graph
  • check_merge_conditions under 5ms (pure in-memory check after data loaded)
  • list_proposals with cursor pagination under 10ms per page
  • get_proposal_diff (all domains) under 300ms

Tier 7 — Security

  • Non-owner cannot merge a proposal (must be repo owner)
  • Forged proposer_sig_b64 (wrong key) → proposal creation rejected
  • Forged reviewer_sig_b64 (reviewer signs with wrong key) → review rejected
  • Proposal cannot self-reference in depends_on → 400
  • selective_domains list with injection payload (e.g., "../../etc") → 400
  • Agent with scope proposal:read cannot create (needs proposal:write)
  • Agent with proposal:write cannot merge (merge requires repo owner)
  • merge_conditions.max_agent_commit_ratio = 0.0 → proposal from agent-only branch blocked
  • Creating proposal with from_branch = to_branch → 400

Tier 8 — Docstrings / API Contract

  • simulate_merge has full docstring with Args, Returns, Raises
  • check_merge_conditions documents every condition field
  • compute_dimensional_risk documents the per-domain formula
  • ProposalType enum has per-member docstrings
  • MergeStrategy enum has per-member docstrings explaining domain behaviour
  • verify_proposal_sig documents the canonical message format
  • build_proposal_claim in muse.core.msign documents domain separator PROPOSE
  • ProposalDependencyGraph documents cycle detection algorithm

7-Phase Implementation Plan

Phase 1 — Extended Data Models & Migrations

Branch: task/proposal-models-v2

  1. Add ProposalType, ProposalState (extended), MergeStrategy enums to musehub/models/musehub.py
  2. Add MergeConditions, ProposalCommentTarget, DimensionalRiskVector TypedDicts
  3. Extend MusehubProposal ORM: proposal_type, is_draft, proposer_sig_b64, from_state_hash, to_state_hash, dimensional_risk, depends_on, blocks, merge_conditions, merge_strategy, selective_domains, published_at, settling_at, abandoned_at, rejection_reason
  4. Extend MusehubProposalReview ORM: review_domain, reviewer_sig_b64, reviewer_archetype
  5. Create musehub_proposal_dependencies table + ORM
  6. Create musehub_proposal_simulations table + ORM
  7. Three Alembic migrations (proposals extension, dependencies table, simulations table)
  8. Tier 1 + 5 tests

Symbols: ProposalType, MergeStrategy, MergeConditions, ProposalCommentTarget


Phase 2 — Cryptographic Primitives

Branch: task/proposal-crypto

  1. Add build_proposal_claim(signing, from_state_hash, to_state_hash, proposal_type, ts) -> ProposalClaim to muse/core/msign.py
    • Canonical: PROPOSE\n{proposer}\n{from_state_hash}\n{to_state_hash}\n{proposal_type}\n{ts}
  2. Add verify_proposal_sig(sig_b64, proposer_handle, from_state_hash, to_state_hash, proposal_type, ts, public_key_b64) -> tuple[bool, str]
  3. Add build_review_attestation(signing, proposal_id, verdict, domain, ts) -> ReviewAttestation
    • Canonical: APPROVE_PROPOSAL\n{reviewer}\n{proposal_id}\n{verdict}\n{domain}\n{ts}
  4. Tier 3 + 7 (crypto + security) tests

Phase 3 — DAG & Merge Condition Service

Branch: task/proposal-dag

  1. ProposalDependencyGraph in musehub/services/proposal_dag.py
    • add_dependency(proposal_id, depends_on_id) — Kahn's cycle detection
    • topological_order(root_id) -> list[str]
    • is_merge_unblocked(proposal_id, db) -> tuple[bool, list[str]] — checks all hard deps are merged
  2. check_merge_conditions(proposal, db) -> tuple[bool, list[str]] — evaluates every field of MergeConditions
  3. Tier 2 + 6 (round-trip + performance) tests

Phase 4 — Simulation Engine

Branch: task/proposal-simulate

  1. simulate_merge(proposal_id, strategy, db) -> SimulationResult — dry-merge returning conflicts without committing
  2. POST /api/repos/{repo_id}/proposals/{id}/simulate — runs simulation, stores in musehub_proposal_simulations
  3. GET /api/repos/{repo_id}/proposals/{id}/simulate — fetch latest simulation result
  4. Tier 4 (stress) tests — 100 concurrent simulations, 10K-file repos

Phase 5 — Extended State Machine & API Routes

Branch: task/proposal-state-machine

  1. Implement full 7-state lifecycle in service layer
  2. PATCH /api/repos/{repo_id}/proposals/{id} — update draft
  3. POST /api/repos/{repo_id}/proposals/{id}/publish
  4. POST /api/repos/{repo_id}/proposals/{id}/abandon
  5. POST /api/repos/{repo_id}/proposalsproposal_type, is_draft, merge_conditions in ProposalCreate
  6. GET /proposals?type=midi_evolution — type filtering
  7. GET /proposals?depends_on=X — DAG filtering
  8. Tier 2 + 7 (round-trip + security) tests

Phase 6 — CLI Extensions

Branch: task/proposal-cli-v2

muse hub proposal create --draft
muse hub proposal publish <id>
muse hub proposal simulate <id>
muse hub proposal dag <id>
muse hub proposal diff <id> --domain midi
muse hub proposal abandon <id>
muse hub proposal depends-on <id> --add <other-id>
muse hub proposal merge <id> --strategy domain_selective --domains code

All new flags carry --body-file support (already merged). --assignee added to proposal create for reviewer designation at creation time.


Phase 7 — MCP Tool Exposure

Branch: task/proposal-mcp-v2

New MCP tools:

  • muse_proposal_simulate(proposal_id, strategy) — agents can dry-run merges
  • muse_proposal_dag(proposal_id) — agents can visualize dependency graph
  • muse_proposal_diff(proposal_id, domain) — agents can read domain-specific diffs
  • muse_proposal_abandon(proposal_id, reason) — agents can clean up stale proposals
  • muse_proposal_publish(proposal_id) — agents can promote drafts to open
  • muse_check_merge_conditions(proposal_id) — agents can poll readiness

Why This Matters

GitHub pull requests evolved from email patches. They carry thirty years of assumptions about version control being about text files and line numbers.

Muse proposals are about state. When a musician changes a chord progression, there is no "line number" for the harmonic tension. When an agent proposes a payment settlement, there is no "diff hunk" for the ledger delta. When an org votes to rotate their signing key, the "change" is a cryptographic event, not a file.

The proposal system should be the most expressive part of MuseHub — the place where humans and agents negotiate, review, and collectively decide what the shared state of the world should be next. That deserves a model built from the ground up for this problem.


Assignee

@aaronrene — this is the biggest design surface on the platform. Coordinate with @gabriel on muse.core.msign primitives for build_proposal_claim and the review attestation format.

Priority

Critical. Every other feature converges here: stems integration, MIDI review, payment settlement, agent delegation. The proposal system is the governance layer for the entire multidimensional state machine.

Activity10
gabriel opened this issue 42 days ago
gabriel 42 days ago

Implementation Plan — Load-Bearing Execution Map

╔══════════════════════════════════════════════════════════════════════════════════╗
║                                                                                  ║
║   ░▒▓█  PROPOSAL REIMAGINATION — SPECTRAL EXECUTION MAP  █▓▒░                  ║
║                                                                                  ║
║   Seven phases. Seven layers of the dimensional stack.                          ║
║   Each phase is a complete, mergeable deliverable.                              ║
║   No phase depends on a future phase's code.                                    ║
║                                                                                  ║
║   ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐      ║
║   │  P1  │→ │  P2  │→ │  P3  │→ │  P4  │→ │  P5  │→ │  P6  │→ │  P7  │      ║
║   │Models│  │Crypto│  │ DAG  │  │ Sim  │  │ API  │  │ CLI  │  │ MCP  │      ║
║   │ + DB │  │      │  │+Cond │  │Engine│  │+State│  │      │  │Tools │      ║
║   └──────┘  └──────┘  └──────┘  └──────┘  └──────┘  └──────┘  └──────┘      ║
║                                                                                  ║
╚══════════════════════════════════════════════════════════════════════════════════╝

Phase 1 — Extended Data Models & Migrations

Branch: task/proposal-models-v2 Touches: musehub/models/musehub.py, musehub/db/musehub_models.py, alembic/versions/

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 1 — THE FOUNDATION
  Every other phase calls something defined here.
  Get this wrong and every other phase breaks.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

1a — New Pydantic Enums & TypedDicts (musehub/models/musehub.py)

Add these before the existing ProposalCreate class:

class ProposalType(str, Enum):
    STATE_MERGE          = "state_merge"
    STEM_INTEGRATION     = "stem_integration"
    MIDI_EVOLUTION       = "midi_evolution"
    PAYMENT_SETTLEMENT   = "payment_settlement"
    AGENT_DELEGATION     = "agent_delegation"
    IDENTITY_TRANSITION  = "identity_transition"
    CANONICAL_RELEASE    = "canonical_release"


class ProposalState(str, Enum):
    DRAFTING   = "drafting"
    OPEN       = "open"
    IN_REVIEW  = "in_review"
    APPROVED   = "approved"
    SETTLING   = "settling"
    MERGED     = "merged"
    ABANDONED  = "abandoned"


class MergeStrategy(str, Enum):
    STATE_OVERLAY    = "state_overlay"
    STATE_WEAVE      = "state_weave"
    STATE_REBASE     = "state_rebase"
    DOMAIN_SELECTIVE = "domain_selective"
    PHASED           = "phased"


class MergeConditions(CamelModel):
    require_approvals:          int        = 2
    require_domains_approved:   list[str]  = Field(default_factory=list)
    max_risk_score:             float      = 1.0
    require_signed_commits:     bool       = False
    require_no_breakage:        bool       = False
    require_test_coverage:      bool       = False
    require_payment_settled:    bool       = False
    require_dependency_merged:  bool       = True
    max_agent_commit_ratio:     float      = 1.0


class ProposalCommentTarget(CamelModel):
    target_type:      str             = "general"
    # code
    symbol_address:   str | None      = None
    line_start:       int | None      = None
    line_end:         int | None      = None
    # midi
    track_name:       str | None      = None
    beat_start:       float | None    = None
    beat_end:         float | None    = None
    note_pitch:       int | None      = None
    # stem
    stem_id:          str | None      = None
    timestamp_start:  float | None    = None
    timestamp_end:    float | None    = None
    # payment
    nonce_hex:        str | None      = None
    # identity
    identity_handle:  str | None      = None


# Type alias used throughout — per-domain risk float in [0.0, 1.0]
DimensionalRiskVector = dict[str, float]

Extend ProposalCreate to accept new fields:

class ProposalCreate(CamelModel):
    title:              str              = Field(min_length=1, max_length=500)
    from_branch:        str              = Field(min_length=1, max_length=255)
    to_branch:          str              = Field(min_length=1, max_length=255)
    body:               str              = Field(max_length=50_000, default="")
    proposal_type:      ProposalType     = ProposalType.STATE_MERGE
    is_draft:           bool             = False
    merge_conditions:   MergeConditions | None = None
    merge_strategy:     MergeStrategy    = MergeStrategy.STATE_OVERLAY
    selective_domains:  list[str] | None = None
    depends_on:         list[str]        = Field(default_factory=list)

1b — ORM Extension (musehub/db/musehub_models.py)

MusehubProposal — new columns to add:

# Proposal typing
proposal_type:       Mapped[str]        = mapped_column(String(32),  nullable=False, default="state_merge",    server_default="state_merge")
is_draft:            Mapped[bool]       = mapped_column(Boolean,     nullable=False, default=False,             server_default=sa.false())

# Cryptographic anchors
proposer_sig_b64:    Mapped[str | None] = mapped_column(Text,        nullable=True,  default=None)
from_state_hash:     Mapped[str | None] = mapped_column(String(128), nullable=True,  default=None)
to_state_hash:       Mapped[str | None] = mapped_column(String(128), nullable=True,  default=None)

# Dimensional risk
dimensional_risk:    Mapped[dict | None] = mapped_column(JSONB,      nullable=True,  default=None)

# Dependency DAG (denormalised fast-path; canonical edges in musehub_proposal_dependencies)
depends_on:          Mapped[list]       = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
blocks:              Mapped[list]       = mapped_column(ARRAY(Text), nullable=False, default_factory=list)
soft_depends_on:     Mapped[list]       = mapped_column(ARRAY(Text), nullable=False, default_factory=list)

# Merge configuration
merge_conditions:    Mapped[dict | None] = mapped_column(JSONB,      nullable=True,  default=None)
merge_strategy:      Mapped[str]        = mapped_column(String(32),  nullable=False, default="state_overlay",  server_default="state_overlay")
selective_domains:   Mapped[list | None] = mapped_column(ARRAY(Text),nullable=True,  default=None)

# Extended lifecycle timestamps
published_at:        Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, default=None)
settling_at:         Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, default=None)
abandoned_at:        Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, default=None)
rejection_reason:    Mapped[str | None]      = mapped_column(Text,   nullable=True,  default=None)

MusehubProposalReview — new columns:

review_domain:       Mapped[str | None] = mapped_column(String(32),  nullable=True,  default=None)
reviewer_sig_b64:    Mapped[str | None] = mapped_column(Text,        nullable=True,  default=None)
reviewer_archetype:  Mapped[str | None] = mapped_column(String(64),  nullable=True,  default=None)

Two new ORM classes:

class MusehubProposalDependency(Base):
    __tablename__ = "musehub_proposal_dependencies"
    __table_args__ = (
        UniqueConstraint("proposal_id", "depends_on_id", name="uq_proposal_dependency"),
        Index("ix_proposal_dep_depends_on", "depends_on_id"),
    )
    id:              Mapped[str] = mapped_column(String(128), primary_key=True)
    proposal_id:     Mapped[str] = mapped_column(String(128), ForeignKey("musehub_proposals.proposal_id", ondelete="CASCADE"), nullable=False, index=True)
    depends_on_id:   Mapped[str] = mapped_column(String(128), ForeignKey("musehub_proposals.proposal_id", ondelete="CASCADE"), nullable=False)
    dependency_type: Mapped[str] = mapped_column(String(16),  nullable=False, default="hard")
    created_at:      Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=_utc_now)


class MusehubProposalSimulation(Base):
    __tablename__ = "musehub_proposal_simulations"
    __table_args__ = (
        Index("ix_proposal_sim_proposal_run", "proposal_id", "run_at"),
    )
    id:                Mapped[str] = mapped_column(String(128), primary_key=True)
    proposal_id:       Mapped[str] = mapped_column(String(128), ForeignKey("musehub_proposals.proposal_id", ondelete="CASCADE"), nullable=False, index=True)
    run_at:            Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=_utc_now)
    strategy:          Mapped[str] = mapped_column(String(32),  nullable=False)
    result_state:      Mapped[str] = mapped_column(String(16),  nullable=False)   # success | conflict | error
    conflicts:         Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)
    merged_state_hash: Mapped[str | None]  = mapped_column(String(128), nullable=True, default=None)
    simulation_log:    Mapped[str | None]  = mapped_column(Text, nullable=True, default=None)
    agent_id:          Mapped[str | None]  = mapped_column(String(128), nullable=True, default=None)

1c — Three Alembic Migrations

0045_proposal_type_and_state_columns.py
  ├─ ALTER TABLE musehub_proposals ADD COLUMN proposal_type, is_draft,
  │   proposer_sig_b64, from_state_hash, to_state_hash, dimensional_risk,
  │   depends_on, blocks, soft_depends_on, merge_conditions, merge_strategy,
  │   selective_domains, published_at, settling_at, abandoned_at, rejection_reason
  └─ ALTER TABLE musehub_proposal_reviews ADD COLUMN review_domain,
      reviewer_sig_b64, reviewer_archetype

0046_proposal_dependencies_table.py
  └─ CREATE TABLE musehub_proposal_dependencies (+ FK + unique + index)

0047_proposal_simulations_table.py
  └─ CREATE TABLE musehub_proposal_simulations (+ FK + index)

Note: migrations 0045 uses op.add_column (transactional, no CONCURRENTLY needed). Migrations 0046 and 0047 use CREATE TABLE (transactional). No autocommit block required.


1d — Phase 1 Tests

File: tests/test_proposal_reimagination_phase1.py

Tier 1 — Shape
  ProposalType has exactly 7 members
  ProposalState has exactly 7 members
  MergeStrategy has exactly 5 members
  MergeConditions.require_approvals defaults to 2
  MergeConditions.require_dependency_merged defaults to True
  ProposalCreate.proposal_type defaults to STATE_MERGE
  ProposalCreate.is_draft defaults to False
  ProposalCreate.merge_strategy defaults to STATE_OVERLAY
  ProposalCommentTarget.target_type defaults to "general"
  MusehubProposal ORM has proposal_type column
  MusehubProposal ORM has is_draft column
  MusehubProposal ORM has merge_conditions column
  musehub_proposal_dependencies table exists in pg_catalog
  musehub_proposal_simulations table exists in pg_catalog
  musehub_proposal_dependencies has unique constraint on (proposal_id, depends_on_id)

Tier 5 — Data Integrity
  ProposalType values are all lowercase snake_case strings
  ProposalState values match the 7-state machine exactly
  MergeStrategy.DOMAIN_SELECTIVE requires selective_domains to be non-null
  depends_on ARRAY(Text) column defaults to empty list (not NULL)
  blocks ARRAY(Text) column defaults to empty list (not NULL)
  dimensional_risk JSONB stores and retrieves float values correctly
  merge_conditions JSONB round-trips MergeConditions model fields

Phase 2 — Cryptographic Primitives

Branch: task/proposal-crypto Touches: muse/core/msign.py, musehub/services/musehub_proposals.py

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 2 — CRYPTOGRAPHIC SUBSTRATE
  Every signed object in the system traces back to one identity.
  Domain separators prevent cross-protocol forgery.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

2a — Canonical Message Formats

┌─────────────────────────────────────────────────────────────────────────────┐
│  CANONICAL MESSAGES — domain separator \n delimited                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  PROPOSE                                                                     │
│  {proposer_handle}                                                           │
│  {from_state_hash}   ← sha256: prefix, lowercase hex                       │
│  {to_state_hash}     ← sha256: prefix, lowercase hex                       │
│  {proposal_type}     ← one of 7 ProposalType values                        │
│  {iso_timestamp}     ← UTC, microsecond precision                           │
│                                                                              │
│  APPROVE_PROPOSAL                                                            │
│  {reviewer_handle}                                                           │
│  {proposal_id}       ← sha256: prefixed genesis ID                         │
│  {verdict}           ← approve | changes_requested | dismiss                │
│  {domain}            ← code | midi | stems | pay | identity | general       │
│  {iso_timestamp}                                                             │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

2b — New symbols in muse/core/msign.py

@dataclass(frozen=True)
class ProposalClaim:
    proposer:        str
    from_state_hash: str
    to_state_hash:   str
    proposal_type:   str
    timestamp:       str
    signature:       str   # ed25519:<base64url>
    public_key:      str   # ed25519:<base64url>

    def canonical_message(self) -> bytes:
        return "\n".join([
            "PROPOSE", self.proposer, self.from_state_hash,
            self.to_state_hash, self.proposal_type, self.timestamp,
        ]).encode()


@dataclass(frozen=True)
class ReviewAttestation:
    reviewer:    str
    proposal_id: str
    verdict:     str
    domain:      str
    timestamp:   str
    signature:   str
    public_key:  str

    def canonical_message(self) -> bytes:
        return "\n".join([
            "APPROVE_PROPOSAL", self.reviewer, self.proposal_id,
            self.verdict, self.domain, self.timestamp,
        ]).encode()


def build_proposal_claim(
    signing:         SigningIdentity,
    from_state_hash: str,
    to_state_hash:   str,
    proposal_type:   str,
    ts:              str,
) -> ProposalClaim:
    """Sign a PROPOSE canonical message. Returns ProposalClaim with ed25519 sig."""


def verify_proposal_sig(
    sig_b64:         str,
    proposer_handle: str,
    from_state_hash: str,
    to_state_hash:   str,
    proposal_type:   str,
    ts:              str,
    public_key_b64:  str,
) -> tuple[bool, str]:
    """Verify PROPOSE sig. Returns (valid, reason)."""


def build_review_attestation(
    signing:     SigningIdentity,
    proposal_id: str,
    verdict:     str,
    domain:      str,
    ts:          str,
) -> ReviewAttestation:
    """Sign an APPROVE_PROPOSAL canonical message."""

2c — Integration in proposal creation

In musehub/services/musehub_proposals.py, create_proposal():

1. resolve from_state_hash = sha256:<HEAD snapshot id of from_branch>
2. resolve to_state_hash   = sha256:<HEAD snapshot id of to_branch>
3. if proposer has registered public key in MusehubIdentity:
       claim = build_proposal_claim(signing, from_state_hash, to_state_hash,
                                    proposal_type, now_utc_iso())
       proposal.proposer_sig_b64 = claim.signature
       proposal.from_state_hash  = from_state_hash
       proposal.to_state_hash    = to_state_hash
4. if is_draft: state = ProposalState.DRAFTING
   else:        state = ProposalState.OPEN

2d — Phase 2 Tests

Tier 3 — Canonical message encoding
  build_proposal_claim() produces deterministic bytes for identical inputs
  canonical message starts with "PROPOSE\n"
  verify_proposal_sig() returns True for a self-signed claim
  verify_proposal_sig() returns False if from_state_hash is mutated
  verify_proposal_sig() returns False if timestamp is mutated
  domain separator "PROPOSE" cannot verify an "APPROVE_PROPOSAL" message
  build_review_attestation() canonical starts with "APPROVE_PROPOSAL\n"
  review attestation with wrong proposal_id fails verification

Tier 7 — Security
  Forged sig (wrong signing key) → verify_proposal_sig returns (False, reason)
  build_proposal_claim with empty from_state_hash → raises ValueError
  domain separator collision test: PROPOSE sig != APPROVE_PROPOSAL sig for same payload
  public_key from a different identity → verification fails
  proposal creation with unregistered proposer → proposer_sig_b64 is None (graceful)
  review submission with forged sig → 400 with "invalid_reviewer_signature"

Phase 3 — DAG & Merge Condition Service

Branch: task/proposal-dag New file: musehub/services/proposal_dag.py Touches: musehub/services/musehub_proposals.py

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 3 — DEPENDENCY TOPOLOGY
  A proposal that is blocked is not broken — it is waiting.
  The DAG resolves the order in which states may change.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

3a — DAG Engine (musehub/services/proposal_dag.py)

┌─────────────────────────────────────────────────────────────────────────────┐
│  KAHN'S ALGORITHM — cycle detection + topological sort                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Input: edges List[(proposal_id, depends_on_id)]                           │
│                                                                              │
│  1. Build in_degree map: {node: count of incoming hard edges}              │
│  2. Enqueue all nodes with in_degree == 0                                  │
│  3. While queue not empty:                                                  │
│       pop node → append to sorted order                                    │
│       for each node that depends_on this node:                             │
│           decrement its in_degree                                           │
│           if in_degree == 0: enqueue it                                    │
│  4. If len(sorted) < len(nodes): CYCLE DETECTED → raise CyclicDependency   │
│                                                                              │
│  O(V + E) where V = proposals in repo, E = dependency edges                │
└─────────────────────────────────────────────────────────────────────────────┘

Symbols to create:

class CyclicDependencyError(ValueError):
    """Raised when adding an edge would create a cycle."""

class ProposalDependencyGraph:
    """In-memory DAG over proposal IDs for a single repo."""

    def add_edge(self, proposal_id: str, depends_on_id: str) -> None:
        """Add a hard dependency edge. Raises CyclicDependencyError on cycle."""

    def topological_order(self) -> list[str]:
        """Kahn's algorithm. Raises CyclicDependencyError if cycle exists."""

    def is_unblocked(self, proposal_id: str) -> tuple[bool, list[str]]:
        """True iff all hard depends_on are in 'merged' state.
        Returns (unblocked, list_of_blocking_proposal_ids)."""

    @classmethod
    async def from_repo(cls, session: AsyncSession, repo_id: str) -> "ProposalDependencyGraph":
        """Load all dependency edges for a repo in one query."""


async def check_merge_conditions(
    proposal: MusehubProposal,
    session:  AsyncSession,
) -> tuple[bool, list[str]]:
    """Evaluate every field of proposal.merge_conditions.

    Returns (all_met, list_of_unmet_condition_descriptions).

    Evaluation order (fail-fast on first unmet hard condition):
      1. require_dependency_merged  → DAG.is_unblocked()
      2. require_approvals          → count approved reviews
      3. require_domains_approved   → per-domain approval check
      4. max_risk_score             → proposal.risk_score <= threshold
      5. require_signed_commits     → all commits have proposer_sig_b64
      6. require_no_breakage        → breakage_count == 0
      7. require_test_coverage      → test_gap_count == 0
      8. require_payment_settled    → settling state + on-chain flag
      9. max_agent_commit_ratio     → agent commits / total commits
    """

3b — Phase 3 Tests

Tier 2 — Round-trip
  add_edge(A, B): A.is_unblocked() is False while B is open
  add_edge(A, B): A.is_unblocked() is True after B is merged
  topological_order() returns B before A for A depends_on B
  from_repo() loads edges from musehub_proposal_dependencies table

Tier 3 — Edge cases
  A depends_on B depends_on A → CyclicDependencyError on second add_edge
  A depends_on itself → CyclicDependencyError
  check_merge_conditions: require_approvals=0 → passes immediately
  check_merge_conditions: max_risk_score=0.0 and risk_score=0.01 → fails
  check_merge_conditions: require_no_breakage=True + breakage_count=1 → fails
  check_merge_conditions: all conditions default → passes with 2 approvals

Tier 6 — Performance
  topological_order() on 100-node graph completes in < 10ms
  from_repo() for repo with 1000 proposals, 500 edges < 50ms
  check_merge_conditions() is purely in-memory after data loaded < 5ms

Phase 4 — Simulation Engine

Branch: task/proposal-simulate New symbol: simulate_merge in musehub/services/musehub_proposals.py New routes: POST + GET /api/repos/{id}/proposals/{id}/simulate

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 4 — THE DRY DIMENSION
  Simulate the state transition without applying it.
  See the conflict manifold before it collapses into reality.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

4a — SimulationResult model

class SimulationConflict(CamelModel):
    domain:        str
    path:          str          # file path or symbol address or track name
    conflict_type: str          # content | schema | dependency | payment_chain
    ours:          str | None   # current to_branch value (truncated)
    theirs:        str | None   # from_branch value (truncated)
    auto_resolvable: bool       # True if Harmony has a saved resolution


class SimulationResult(CamelModel):
    simulation_id:    str
    proposal_id:      str
    strategy:         str
    run_at:           datetime
    result_state:     Literal["success", "conflict", "error"]
    conflicts:        list[SimulationConflict]
    conflict_count:   int
    merged_state_hash: str | None   # what the merged snapshot ID WOULD be
    agent_id:         str | None
    duration_ms:      float

4b — simulate_merge implementation shape

simulate_merge(proposal, strategy, session) → SimulationResult

  1. Resolve from_branch HEAD and to_branch HEAD snapshot IDs
  2. Call muse merge --dry-run from_branch --json (in-process via muse library)
     → get conflict_paths, per-domain manifests
  3. For each conflict_path:
       domain = infer_domain(path)
       harmony_result = harmony.engine(path, ours_id, theirs_id)
       auto_resolvable = harmony_result.resolution is not None
       append SimulationConflict(domain, path, ...)
  4. If no conflicts:
       compute merged_state_hash = sha256(sorted(to_branch_manifest + from_branch_manifest))
       result_state = "success"
  5. Store MusehubProposalSimulation row
  6. Return SimulationResult

  NEVER: commits anything, advances any branch, or modifies working state.

4c — API routes

POST /api/repos/{repo_id}/proposals/{proposal_id}/simulate
  Body: { "strategy": "state_overlay" }  (optional, defaults to proposal.merge_strategy)
  Returns: SimulationResult
  Status: 202 Accepted (async) or 200 if < 500ms
  Error 409: proposal already merged
  Error 423: simulation already running for this proposal

GET /api/repos/{repo_id}/proposals/{proposal_id}/simulate
  Returns: most recent SimulationResult for this proposal
  Error 404: no simulation run yet

4d — Phase 4 Tests

Tier 2 — Round-trip
  simulate on clean proposal → result_state = "success"
  simulate on conflicting branches → result_state = "conflict", conflicts non-empty
  simulate stores row in musehub_proposal_simulations
  GET /simulate after POST → returns same simulation_id
  simulate on merged proposal → 409

Tier 4 — Stress
  100 concurrent simulate requests on 10 different proposals → all complete, no deadlock
  simulate on repo with 1000 tracked files → completes under 2000ms
  SimulationConflict.auto_resolvable = True when Harmony has saved resolution

Tier 7 — Security
  simulate with non-owner token → 403 (simulate requires at least read access)
  simulate on proposal in different repo → 404

Phase 5 — Extended State Machine & API Routes

Branch: task/proposal-state-machine Touches: musehub/services/musehub_proposals.py, musehub/api/routes/musehub/proposals.py

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 5 — THE STATE MANIFOLD
  Every transition is guarded. Every guard is explicit.
  Illegal transitions are statically unreachable.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

5a — Transition table (guards are checked in service layer, not route layer)

┌────────────────┬────────────────┬──────────────────────────────────────────────┐
│  FROM          │  TO            │  GUARD                                       │
├────────────────┼────────────────┼──────────────────────────────────────────────┤
│  *             │  DRAFTING      │  is_draft=True at creation                   │
│  *             │  OPEN          │  is_draft=False at creation OR publish()      │
│  DRAFTING      │  OPEN          │  publish(): proposal.author == actor          │
│  OPEN          │  IN_REVIEW     │  first reviewer assigned                      │
│  IN_REVIEW     │  APPROVED      │  check_merge_conditions() → all met           │
│  APPROVED      │  IN_REVIEW     │  changes_requested review submitted           │
│  APPROVED      │  SETTLING      │  proposal_type == PAYMENT_SETTLEMENT          │
│  APPROVED      │  MERGED        │  proposal_type != PAYMENT_SETTLEMENT          │
│  SETTLING      │  MERGED        │  on-chain confirmation received               │
│  OPEN          │  ABANDONED     │  abandon(): author or repo owner              │
│  IN_REVIEW     │  ABANDONED     │  abandon(): author or repo owner              │
│  APPROVED      │  ABANDONED     │  abandon(): author or repo owner              │
│  OPEN          │  ABANDONED     │  reject(): reviewer quorum                    │
├────────────────┼────────────────┼──────────────────────────────────────────────┤
│  MERGED        │  (any)         │  ✗ TERMINAL — no transition out              │
│  ABANDONED     │  (any)         │  ✗ TERMINAL — no transition out              │
└────────────────┴────────────────┴──────────────────────────────────────────────┘

5b — New/extended service functions

async def publish_proposal(proposal_id: str, actor: str, session: AsyncSession) -> MusehubProposal:
    """DRAFTING → OPEN. Only the author may publish."""

async def abandon_proposal(proposal_id: str, actor: str, reason: str | None, session: AsyncSession) -> MusehubProposal:
    """Any non-terminal state → ABANDONED. Author or repo owner only."""

async def transition_to_in_review(proposal_id: str, session: AsyncSession) -> None:
    """Called automatically when first reviewer is assigned."""

async def evaluate_and_advance(proposal_id: str, session: AsyncSession) -> str:
    """Re-evaluate merge conditions after each review submission.
    Advances state if conditions newly met. Returns new state."""

5c — New API routes

POST /api/repos/{repo_id}/proposals/{id}/publish
  Body: {}
  Effect: DRAFTING → OPEN, sets published_at
  Error 409: already published

POST /api/repos/{repo_id}/proposals/{id}/abandon
  Body: { "reason": "..." }  (optional)
  Effect: any non-terminal → ABANDONED, sets abandoned_at
  Error 409: already merged or abandoned

PATCH /api/repos/{repo_id}/proposals/{id}
  Body: ProposalUpdate (title, body, merge_conditions, selective_domains)
  Guard: only in DRAFTING or OPEN; only author may update
  Error 403: not author
  Error 409: proposal is not in a mutable state

Extend GET /api/repos/{repo_id}/proposals:

New query params:
  ?type=midi_evolution          filter by ProposalType
  ?depends_on={proposal_id}     filter proposals that depend on this ID
  ?is_draft=true|false          show/hide drafts
  ?reviewer={handle}            filter by assigned reviewer

POST /api/repos/{repo_id}/proposals — extend ProposalCreate handler:

1. If is_draft=True: state = DRAFTING, published_at = None
2. Compute from_state_hash, to_state_hash from branch HEAD snapshots
3. If proposer has public key: build_proposal_claim → proposer_sig_b64
4. If depends_on is non-empty:
     load DAG for repo
     for each dep_id: graph.add_edge(new_proposal_id, dep_id)  ← cycle check
     if CyclicDependencyError: return 400
5. Insert musehub_proposal_dependencies rows for hard edges

5d — Phase 5 Tests

Tier 2 — Round-trip
  create draft → state=drafting, not visible in default list
  publish draft → state=open, published_at set
  assign reviewer → state=in_review
  submit 2 approvals (conditions met) → state=approved
  merge approved proposal → state=merged, merged_at set
  abandon open proposal → state=abandoned, abandoned_at set
  create with depends_on=[open proposal] → is_blocked=True
  create with depends_on=[merged proposal] → is_blocked=False

Tier 3 — Edge cases
  create A depends_on B depends_on A → 400 cyclic
  publish already-open proposal → 409
  merge a draft → 400 cannot merge draft
  abandon a merged proposal → 409

Tier 7 — Security
  non-author publish → 403
  non-author/non-owner abandon → 403
  PATCH proposal in MERGED state → 409
  depends_on proposal in different repo → 400

Phase 6 — CLI Extensions

Branch: task/proposal-cli-v2 Touches: muse/cli/commands/hub.py (or wherever muse hub proposal subcommands live)

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 6 — THE OPERATOR SURFACE
  The CLI is the terminal where the multidimensional meets the human hand.
  Every flag is a dimension knob.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

6a — Extended muse hub proposal create

muse hub proposal create
  --title "feat: auth v2"
  --from-branch feat/auth-v2
  --to-branch dev
  [--draft]                              new: create in DRAFTING state
  [--type state_merge|midi_evolution|…]  new: ProposalType
  [--strategy state_overlay|…]          new: MergeStrategy
  [--domains code,midi]                  new: for domain_selective strategy
  [--require-approvals N]               new: MergeConditions.require_approvals
  [--require-domains code,midi]         new: MergeConditions.require_domains_approved
  [--require-signed-commits]            new: MergeConditions.require_signed_commits
  [--require-no-breakage]               new: MergeConditions.require_no_breakage
  [--max-risk-score 0.7]                new: MergeConditions.max_risk_score
  [--depends-on sha256:abc,sha256:def]  new: hard dependency IDs
  [--body-file path/to/body.md]         existing, already merged
  [--assignee handle]                   new: assign reviewer at creation time

6b — New subcommands

muse hub proposal publish <id>
  → DRAFTING → OPEN; sets published_at

muse hub proposal abandon <id> [--reason "..."]
  → any non-terminal → ABANDONED

muse hub proposal simulate <id> [--strategy state_weave] [--json]
  → POST /simulate, poll until result, render conflict table

muse hub proposal dag <id> [--json]
  → render dependency graph as ASCII tree:

    #5 [OPEN] "Release v2.0"
    ├── depends on #1 [MERGED] "auth refactor"
    ├── depends on #2 [MERGED] "midi key change"
    └── blocks #6 [DRAFTING] "payment settlement for v2.0"
        └── depends on #5 [OPEN]

muse hub proposal diff <id> [--domain code|midi|stems|pay|all] [--json]
  → GET /diff, filter to requested domain, render

muse hub proposal review submit <id>
  --verdict approve|changes_requested|dismiss
  [--domain code|midi|stems|pay|identity]
  [--body "LGTM — harmonic tension within threshold"]
  → build_review_attestation, POST review with sig

muse hub proposal depends-on <id> --add <dep-id>
muse hub proposal depends-on <id> --remove <dep-id>
muse hub proposal depends-on <id> --list

6c — Phase 6 Tests

Tier 2 — CLI round-trip (httpx AsyncClient against test ASGI app)
  muse hub proposal create --draft → proposal in DRAFTING state
  muse hub proposal publish <id> → state = OPEN
  muse hub proposal simulate <id> → SimulationResult returned
  muse hub proposal dag <id> → DAG edges visible
  muse hub proposal abandon <id> --reason "stale" → state = ABANDONED
  muse hub proposal depends-on <id> --add <dep-id> → dependency created
  muse hub proposal review submit --verdict approve → approval count increments

Tier 7 — Security
  --require-no-breakage flag is persisted in merge_conditions JSON
  --domains flag with spaces in domain name → 422
  --max-risk-score 1.5 → validation error (> 1.0)
  depends-on --add <id from different repo> → 400

Phase 7 — MCP Tool Exposure

Branch: task/proposal-mcp-v2 Touches: musehub/mcp/tools/musehub.py, musehub/mcp/dispatcher.py, musehub/services/musehub_mcp_executor.py

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  SPECTRAL LAYER 7 — THE AGENT INTERFACE
  Agents perceive the proposal manifold through these tools.
  No agent touches a DB directly. All state flows through signed transitions.
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

7a — New MCP tools (6 tools → tool count 120 → 126)

musehub_simulate_proposal(proposal_id, strategy?)
  → POST /simulate; returns SimulationResult
  Use: before merge, to detect conflicts across all domains

musehub_proposal_dag(proposal_id)
  → GET DAG edges; returns {proposal_id, depends_on[], blocks[], soft_depends_on[]}
  Use: agents planning merge order check this before recommending a merge

musehub_proposal_diff(proposal_id, domain?)
  → GET /diff with optional domain filter
  Use: agents doing domain-specific review read this before submitting verdict

musehub_abandon_proposal(proposal_id, reason?)
  → POST /abandon; requires proposal:write scope
  Use: cleanup agents abandon stale proposals past a TTL

musehub_publish_proposal(proposal_id)
  → POST /publish; requires proposal:write scope
  Use: agents that draft proposals promote them after self-review

musehub_check_merge_conditions(proposal_id)
  → calls check_merge_conditions(); returns {all_met, unmet_conditions[]}
  Use: agents poll this before recommending or requesting a merge

7b — Tool description philosophy

Each tool description must answer:

  1. What does this return?
  2. When should an agent call it (not call it)?
  3. What's the correct follow-up tool?

Example for musehub_simulate_proposal:

"Run a dry-merge simulation for a proposal without applying any changes.
Returns a conflict manifest per domain and a predicted merged_state_hash if
no conflicts exist. Call this before musehub_merge_proposal when the proposal
touches multiple domains or has a non-trivial risk score. Do NOT call on
already-merged proposals (returns 409). Pair with musehub_check_merge_conditions
to confirm all gates pass before recommending a merge."

7c — Phase 7 Tests

Tier 1 — Catalogue
  MCP_TOOLS has exactly 126 entries after Phase 7
  musehub_simulate_proposal present in catalogue
  musehub_proposal_dag present in catalogue
  musehub_proposal_diff present in catalogue
  musehub_abandon_proposal present in catalogue
  musehub_publish_proposal present in catalogue
  musehub_check_merge_conditions present in catalogue
  all 6 new tools have "annotations" field (MCP 2025-11-25 spec)

Tier 2 — Dispatcher round-trip
  musehub_simulate_proposal dispatches to execute_simulate_proposal
  musehub_proposal_dag returns edges for seeded proposal
  musehub_check_merge_conditions returns all_met=True when conditions satisfied
  musehub_abandon_proposal transitions to ABANDONED state

Tier 7 — Security
  musehub_abandon_proposal called by non-owner → ok=False, error_code=forbidden
  musehub_publish_proposal called on already-open proposal → ok=False, error_code=conflict
  musehub_simulate_proposal on merged proposal → ok=False, error_code=already_merged

Cross-Phase Dependency Map

╔══════════════════════════════════════════════════════════════════════════════╗
║                                                                              ║
║   P1 ──────────────────────────────────────────────────────────────────┐   ║
║   (models + DB)                                                        │   ║
║      │                                                                 │   ║
║      ├──▶ P2 (crypto primitives)                                       │   ║
║      │      │                                                          │   ║
║      │      └──▶ P5 (state machine) ◀── P3 (DAG + conditions) ◀──────┘   ║
║      │                │                                                    ║
║      └──▶ P3 (DAG)    │                                                    ║
║              │        │                                                    ║
║              │        ├──▶ P4 (simulation)                                 ║
║              │        │       │                                            ║
║              │        └──────▶│──▶ P6 (CLI)                               ║
║              │                │       │                                    ║
║              └────────────────┴──────▶└──▶ P7 (MCP)                       ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝

  Hard rule: no phase imports from a later phase.
  P2 does NOT import from P3. P3 does NOT import from P5.
  The dependency arrow is always backwards (later phases import earlier phases).

File Touch Map

┌─────────────────────────────────────────────────────────────────────────────┐
│  FILE                                              P1 P2 P3 P4 P5 P6 P7   │
├─────────────────────────────────────────────────────────────────────────────┤
│  musehub/models/musehub.py                          ██ ·  ·  ·  █  ·  ·   │
│  musehub/db/musehub_models.py                       ██ ·  ·  ·  ·  ·  ·   │
│  alembic/versions/0045–0047_*.py                    ██ ·  ·  ·  ·  ·  ·   │
│  muse/core/msign.py                                 ·  ██ ·  ·  ·  ·  ·   │
│  musehub/services/musehub_proposals.py              ·  █  ██ █  ██ ·  ·   │
│  musehub/services/proposal_dag.py           (new)   ·  ·  ██ ·  ·  ·  ·   │
│  musehub/api/routes/musehub/proposals.py            ·  ·  ·  █  ██ ·  ·   │
│  muse/cli/commands/hub.py                           ·  ·  ·  ·  ·  ██ ·   │
│  musehub/mcp/tools/musehub.py                       ·  ·  ·  ·  ·  ·  ██  │
│  musehub/mcp/dispatcher.py                          ·  ·  ·  ·  ·  ·  ██  │
│  musehub/services/musehub_mcp_executor.py           ·  ·  ·  ·  ·  ·  ██  │
│  tests/test_proposal_reimagination_phase*.py        ██ ██ ██ ██ ██ ██ ██  │
└─────────────────────────────────────────────────────────────────────────────┘
  ██ = primary owner of this file in this phase
  █  = touches this file but does not own it

Spectral Risk Visualization — Dimensional Heat at Merge Time

When this is fully implemented, the proposal detail page will render the dimensional heat spectrum — the full risk vector across all active domains:

┌─────────────────────────────────────────────────────────────────────────────┐
│  PROPOSAL #42 — "auth refactor + midi key change + Q1 payment settlement"  │
│                                                                             │
│  DIMENSIONAL RISK SPECTRUM                                                  │
│                                                                             │
│  CODE    ████████████████████████████████████████░░░░░░░░░░  0.80 CRITICAL │
│  MIDI    ████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░  0.48 HIGH     │
│  STEMS   ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  0.16 LOW      │
│  PROSE   ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  0.00 NONE     │
│  PAY     ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  0.24 MEDIUM   │
│                                                                             │
│  AGGREGATE: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░  0.55 HIGH     │
│                                                                             │
│  MERGE CONDITIONS                                                           │
│  ✅ 3/2 approvals     ✅ code reviewed    ⏳ midi not yet reviewed          │
│  ✅ commits signed    ✅ risk ≤ 0.80      ✅ no breakage                    │
│                                                                             │
│  DEPENDENCY DAG                                                             │
│  ✅ #38 merged        ✅ #39 merged       🔒 blocks #45                     │
└─────────────────────────────────────────────────────────────────────────────┘

This visualization is the end state of all seven phases working together. The risk vector is computed in Phase 1, the DAG state in Phase 3, the conditions evaluation in Phase 3, and the full UI render is downstream of this ticket in the proposal detail redesign.


Notes for Executor

  • Each phase branch merges to dev independently — no phase waits for a later phase's PR
  • Test files follow the naming pattern tests/test_proposal_reimagination_phase{N}.py
  • ORM changes in Phase 1 must not break existing MusehubProposal queries — all new columns have server-side defaults
  • The state column already exists; Phase 1 extends its value set but the column does not change type (TEXT, max 20 chars → extend to 32 to accommodate payment_settlement)
  • Phase 2 crypto work happens in the muse repo (muse/core/msign.py), not musehub — coordinate commits across both repos
  • Phase 6 CLI work similarly touches the muse repo — same cross-repo coordination required
  • Migration 0045 must be transactional (all ADD COLUMN ops) — no CONCURRENTLY needed
  • Simulations in Phase 4 are best-effort, never block the merge path — a failed simulation is not a merge gate unless explicitly configured in merge_conditions
gabriel 42 days ago

✅ Phase 1 Complete — Proposal Type System, Merge Strategy & Dimensional Models

Branch: task/proposal-models-v2 Commits: sha256:d70f04ec (models + ORM) · sha256:a6942164 (tests) Tests: 43 passed, 0 failed


What landed

musehub/models/musehub.py

  • ProposalType enum — 7 values: state_merge, stem_integration, midi_evolution, payment_settlement, agent_delegation, identity_transition, canonical_release
  • ProposalState enum — 7 states: drafting → open → in_review → approved → settling → merged | abandoned
  • MergeStrategy enum — 5 strategies: state_overlay, state_weave, state_rebase, domain_selective, phased
  • MergeConditions Pydantic model — 9 merge gate fields with validated defaults
  • ProposalCommentTarget — domain-agnostic coordinate system for code/midi/stem/payment/identity inline comments
  • DimensionalRiskVector type alias (dict[str, float])
  • ProposalCreate extended: proposal_type, is_draft, merge_conditions, merge_strategy, selective_domains, depends_on
  • ProposalResponse extended: same new fields + risk_score, dimensional_risk

musehub/db/musehub_models.py

  • MusehubProposal +15 columns: type/strategy/conditions, MIDI summary, payment summary, agent provenance
  • MusehubProposalReview +3 columns: reviewed_domains, domain_risk_acknowledged, suggested_merge_strategy
  • MusehubProposalDependency new table — DAG edges with unique constraint + cascade delete
  • MusehubProposalSimulation new table — phased-merge simulation cache with (proposal, type) unique constraint

Migrations (applied to local Docker Postgres ✅)

  • 0045 — proposal_type, is_draft, merge_conditions, merge_strategy, selective_domains, dimensional_risk
  • 0046 — MIDI/payment domain summaries, agent_model/agent_spawned_by, review dimensional fields
  • 0047 — musehub_proposal_dependencies + musehub_proposal_simulations tables

Test coverage (43 tests in tests/test_proposal_reimagination_phase1.py)

Tier Tests What
Unit 28 Enum values, MergeConditions bounds, ProposalCommentTarget domains, DimensionalRiskVector, ProposalCreate/Response defaults + camelCase JSON
Integration 15 ORM round-trips for all new columns, unique constraint fires, cascade delete verified

Up next — Phase 2: Dependency DAG Engine

Kahn's algorithm, cycle detection, blocked_by/blocks resolution, merge ordering — wired into the proposal service layer.

gabriel 42 days ago

Phase 1 Complete — Proposal Type System, Merge Strategy and Dimensional Models

Branch: task/proposal-models-v2 Commits: sha256:d70f04ec (models + ORM) and sha256:a6942164 (tests) Tests: 43 passed, 0 failed

What landed

musehub/models/musehub.py

  • ProposalType enum — 7 values: state_merge, stem_integration, midi_evolution, payment_settlement, agent_delegation, identity_transition, canonical_release
  • ProposalState enum — 7 states: drafting > open > in_review > approved > settling > merged | abandoned
  • MergeStrategy enum — 5 strategies: state_overlay, state_weave, state_rebase, domain_selective, phased
  • MergeConditions Pydantic model — 9 merge gate fields with validated defaults
  • ProposalCommentTarget — domain-agnostic coordinate system for code/midi/stem/payment/identity inline comments
  • DimensionalRiskVector type alias (dict[str, float])
  • ProposalCreate extended: proposal_type, is_draft, merge_conditions, merge_strategy, selective_domains, depends_on
  • ProposalResponse extended: same new fields + risk_score, dimensional_risk

musehub/db/musehub_models.py

  • MusehubProposal +15 columns: type/strategy/conditions, MIDI summary, payment summary, agent provenance
  • MusehubProposalReview +3 columns: reviewed_domains, domain_risk_acknowledged, suggested_merge_strategy
  • MusehubProposalDependency new table — DAG edges with unique constraint + cascade delete
  • MusehubProposalSimulation new table — phased-merge simulation cache with (proposal, type) unique constraint

Migrations applied to local Docker Postgres:

  • 0045: proposal_type, is_draft, merge_conditions, merge_strategy, selective_domains, dimensional_risk
  • 0046: MIDI/payment domain summaries, agent_model/agent_spawned_by, review dimensional fields
  • 0047: musehub_proposal_dependencies + musehub_proposal_simulations tables

Test coverage (43 tests)

  • Unit (28): Enum values, MergeConditions bounds, ProposalCommentTarget domains, DimensionalRiskVector, ProposalCreate/Response defaults + camelCase JSON round-trip
  • Integration (15): ORM round-trips for all new columns, unique constraint fires, cascade delete verified

Up next — Phase 2: Dependency DAG Engine

Kahn's algorithm, cycle detection, blocked_by/blocks resolution, merge ordering — wired into the proposal service layer.

gabriel 42 days ago

Phase 2 Complete — Dependency DAG Engine

Branch: task/proposal-models-v2 Commit: sha256:fca4d08c Tests: 29 passed (72 total across Phases 1+2)

What landed

musehub/services/proposal_dag.py — pure DAG engine (no DB, independently testable):

  • ProposalDag dataclass: depends_on/required_by adjacency + merged_ids + number_by_id
  • build_dag: construct from raw (dependent_id, dependency_id) edge pairs
  • topological_sort: Kahn's algorithm; raises CycleError(cycle_ids) on any cycle
  • detect_cycle: bool test without mutating the DAG
  • blocked_by_numbers: live deps only (merged proposals excluded)
  • blocks_numbers: reverse edge — who is waiting on this proposal
  • is_blocked: True iff any unmerged dependency exists
  • load_dag_for_proposals: partial DAG from DB, scoped to current page + transitive neighbours (2 queries)
  • create_dependency_edges: validates existence, detects cycles pre-write, persists MusehubProposalDependency rows

Service integration (musehub_proposals.py):

  • create_proposal: accepts proposal_type, is_draft, merge_strategy, merge_conditions, selective_domains, depends_on
  • _prefetch_for_batch: adds Query 3 — DAG load for the page
  • _enrich_one: populates blocked_by/blocks/is_blocked; all_merge_conditions_met respects require_dependency_merged; reads MIDI/payment/agent fields from ORM
  • merge_proposal: gates on unmerged hard dependencies; raises RuntimeError listing blocked proposal numbers

API route: forwards all ProposalCreate fields; CycleError surfaced as HTTP 422

Test coverage (29 tests)

  • Unit pure DAG (24): build_dag, linear/diamond/multi-root topo sort, two/three-node cycles, CycleError message, merged-breaks-cycle, blocked_by/blocks sorted + merged-excluded
  • Integration DB (5): dependency edges persisted, unknown dep rejected, cycle rejected via create_dependency_edges, enrich_batch populates blocked_by/blocks/is_blocked, merge gated by unmerged dep

Up next — Phase 3: Merge Strategy Engine

STATE_OVERLAY, STATE_WEAVE, STATE_REBASE, DOMAIN_SELECTIVE, PHASED — each strategy gets its own execution path in merge_proposal.

gabriel 42 days ago

Phase 3 complete — Merge Strategy Engine ✅

Commit: sha256:a6ab45613263 on task/proposal-models-v2

What landed

musehub/services/proposal_merge_strategies.py (new — pure, no DB)

  • classify_domain(path) — single authority for domain routing; path-prefix rules beat extension rules
  • _manifest_delta(base, head) — (added, modified, removed) path sets
  • _apply_delta(target, delta_head, delta_base) — applies a branch's delta onto a target manifest
  • MergeResult / ConflictEntry dataclasses — structured merge output with conflict audit trail
  • Five strategy functions:
    • merge_state_overlay — from_branch wins all files; conflicts populated for audit when ancestor provided
    • merge_state_weave — true three-way file-level merge; clean changes applied conflict-free; both-sides changes surfaced
    • merge_state_rebase — applies only from_branch delta (vs ancestor) onto to_branch; to-only changes preserved
    • merge_domain_selective — applies only from_branch files whose domain is in selective_domains; files_skipped counted
    • merge_phased — applies dependency deltas in Kahn topological order then self; falls back to overlay when no phase_manifests
  • execute_merge_strategy(strategy, ...)) — router for all 5; state_weave/rebase fall back to overlay with warning when no ancestor

musehub/services/musehub_proposals.py (updated)

  • _resolve_ancestor_manifest(session, repo_id, from_branch, to_branch) — bounded walk (depth 200) to find merge-base commit
  • merge_proposal — dependency gate → load ancestor manifest → execute_merge_strategy → apply merge_result.manifest

tests/test_proposal_reimagination_phase3.py (new — 56 tests, all passing)

  • Uses muse.core.types.fake_id and blob_id(os.urandom()) throughout — no UUIDs
  • Unit coverage: domain classifier, all 5 strategy functions, router fallbacks
  • Integration coverage: default overlay merges correctly, domain_selective only applies selected domain

Status

  • Phase 1 (type system + ORM): ✅ committed
  • Phase 2 (DAG engine): ✅ committed
  • Phase 3 (merge strategy engine): ✅ committed
  • Phase 4 (simulation engine): 🔜
  • Phase 5 (API surface): 🔜
  • Phase 6 (MCP tools): 🔜
gabriel 42 days ago

Phase 4 complete — Simulation Engine ✅

Commit: sha256:2c9d8d66ec34 on task/proposal-models-v2

What landed

musehub/core/genesis.py

  • compute_simulation_id(proposal_id, simulation_type, from_branch_commit_id) — content-addressed ID; different commit tip → different ID (re-runs are distinguishable)

musehub/models/musehub.py

  • SimulationType enum: conflict_scan | risk_projection | dependency_order
  • SimulationResponse — simulation_id, proposal_id, type, result (JSONB payload), is_stale, from_branch_commit_id, duration_ms, created_at, expires_at
  • SimulationListResponse

musehub/services/proposal_simulation.py (new — pure, no DB)

  • simulate_conflict_scan — dry-runs the merge strategy; returns conflict_count, conflicting_files, conflicts_by_domain, files_added/modified/removed, domains_affected, strategy_used
  • simulate_risk_projection — weighted blend: 40% change_ratio + 40% conflict_ratio + 20% existing dimensional_risk; returns projected_domain_risk, overall_projected_risk, risk_band (low/medium/high), risk_delta
  • simulate_dependency_order — topological sort of live DAG; groups into parallel phases; cycle → cycle_detected=True + cycle_ids

musehub/services/musehub_proposals.py (updated)

  • run_simulation(session, repo_id, proposal_id, simulation_type) — always recomputes; upserts into musehub_proposal_simulations (one row per proposal×type)
  • get_simulation(session, repo_id, proposal_id, simulation_type) — reads cache; sets is_stale=True when from_branch has advanced
  • list_simulations(session, repo_id, proposal_id) — all cached simulations for a proposal

musehub/api/routes/musehub/proposals.py (updated)

  • POST /repos/{repo_id}/proposals/{proposal_id}/simulations/{simulation_type} — run/refresh
  • GET /repos/{repo_id}/proposals/{proposal_id}/simulations/{simulation_type} — read cache
  • GET /repos/{repo_id}/proposals/{proposal_id}/simulations — list all

tests/test_proposal_reimagination_phase4.py (new — 38 tests, all passing)

Status

  • Phase 1 (type system + ORM): ✅
  • Phase 2 (DAG engine): ✅
  • Phase 3 (merge strategy engine): ✅
  • Phase 4 (simulation engine): ✅
  • Phase 5 (API surface / enrichment): 🔜
  • Phase 6 (MCP tools): 🔜
gabriel 42 days ago

Phase 5 complete — API Surface Enrichment ✅

Commit: sha256:dd31f05becd9 on task/proposal-models-v2

What landed

ProposalResponse (single-proposal read) — 3 new fields:

  • blocked_by: list[int] — proposal_numbers of unmerged dependencies blocking this one
  • blocks: list[int] — proposal_numbers that depend on this proposal
  • is_blocked: bool — convenience flag
  • latest_simulations: dict[str, dict] — inline summary of all cached simulation results (keyed by type)

get_proposal — now runs two extra queries (one DAG load, one simulation load) and feeds both into _to_proposal_response so every single-proposal read is fully enriched.

list_proposals — three previously-silently-ignored filters now wired:

  • proposal_type list filter → DB predicate on musehub_proposals.proposal_type
  • is_draft bool filter → DB predicate on musehub_proposals.is_draft
  • merge_strategy list filter → DB predicate on musehub_proposals.merge_strategy

ProposalListFilters — two new fields: is_draft: bool | None, merge_strategy: list[str] | None

ProposalListEntry — two new fields:

  • merge_strategy: str — strategy value per row
  • simulation_conflict_count: int | None — conflict count from prefetched conflict_scan; None if never run

_enrich_one — uses dimensional_risk dict (Phase 1 columns) for multi-domain risk breakdown instead of the single scalar risk_score → code-domain path; falls back to scalar for backwards compatibility.

_prefetch_for_batch — Query 4 loads the latest conflict_scan result for every proposal in the current page; zero extra I/O per row in _enrich_one.

tests/test_proposal_reimagination_phase5.py — 21 tests, all passing. 187 total across all 5 phases.


Status

  • Phase 1 (type system + ORM): ✅
  • Phase 2 (DAG engine): ✅
  • Phase 3 (merge strategy engine): ✅
  • Phase 4 (simulation engine): ✅
  • Phase 5 (API surface enrichment): ✅
  • Phase 6 (MCP tools): 🔜
gabriel 42 days ago

Phase 6 complete — MCP tools ✅

Commit: sha256:f0572b29 on task/proposal-models-v2

Changes

musehub/mcp/write_tools/proposals.py

  • _simulation_data() — new serialiser for SimulationResponse
  • _proposal_data() — now includes all Phase 1-5 fields: proposal_type, is_draft, merge_strategy, merge_conditions, selective_domains, blocked_by, blocks, is_blocked, latest_simulations
  • execute_create_proposal — accepts and forwards proposal_type, is_draft, merge_strategy, merge_conditions, selective_domains, depends_on
  • execute_get_proposal — new; returns enriched proposal with DAG + simulation data
  • execute_run_simulation — new; runs and persists conflict_scan / risk_projection / dependency_order
  • execute_get_simulation — new; reads cached result, returns not_found with hint when absent
  • execute_list_simulations — new; lists all cached simulations (up to 3) for a proposal

musehub/mcp/tools/musehub.py

  • musehub_create_proposal schema updated: adds proposal_type, is_draft, merge_strategy, merge_conditions, selective_domains, depends_on
  • musehub_get_proposal — new tool
  • musehub_run_proposal_simulation — new tool
  • musehub_get_proposal_simulation — new tool
  • musehub_list_proposal_simulations — new tool

musehub/mcp/dispatcher.py

  • Routes all 5 new/updated tool calls to executors with correct arg forwarding

tests/test_proposal_reimagination_phase6.py — 38 tests, all passing

  • _proposal_data serialiser covers all new fields
  • _simulation_data shape
  • execute_create_proposal kwargs forwarding
  • execute_get_proposal enrichment + not_found
  • execute_run_simulation + validation
  • execute_get_simulation + not_found hint
  • execute_list_simulations + empty list
  • Tool schema presence + enum values
  • Dispatcher routing for all new tools

Tally

Phase Area Status
Phase 1 Models (ProposalType, MergeStrategy, SimulationType)
Phase 2 DAG engine (dependency ordering, cycle detection)
Phase 3 Merge strategy engine
Phase 4 Simulation engine (pure + persistence)
Phase 5 API surface enrichment (blocked_by, latest_simulations, filters)
Phase 6 MCP tools

Next: Phase 7 — UI/template updates, then open the merge proposal.

gabriel 42 days ago

Phase 7 complete — UI/templates ✅

Commit: sha256:8f6f64b7 on task/proposal-models-v2

Changes

proposal_rows.html (list view rows)

  • strategy_label + proposal_type_label macros — both skip defaults (state_overlay / state_merge) to avoid badge noise
  • merge_strategy badge in title row: shows weave / rebase / selective / phased
  • proposal_type badge in title row: shows stem / midi / payment / agent / identity / release
  • simulation_conflict_count pill in signals row: red conflict count with icon, or clean green check when 0

proposal_row_detail.html (expand panel)

  • Merge strategy + proposal type section (hidden when both are defaults)
  • Simulation results: ready/blocked badge from conflict count

proposal_detail.html (full page)

  • Hero eyebrow: draft, merge_strategy, proposal_type, and 🔒 Blocked badges
  • New Simulations section: 3-card grid — conflict_scan (count), risk_projection (band + % + Δ), dependency_order (phase count or cycle warning)
  • New sidebar Merge Approach scard: strategy, type, blocked_by links, blocks links (only renders when non-default or DAG dependencies exist)

Opening merge proposal now.

gabriel 42 days ago

All 7 phases complete. Merge proposal opened: sha256:89cde827 (task/proposal-models-v2dev). Closing.