gabriel / musehub public
Closed #229 Bug enhancement
filed by gabriel human · 2 days ago

proposalType canonical cleanup — state_merge left behind by migration 0070

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

proposalType canonical cleanup — state_merge left behind by migration 0070

Context

muse#20 ("Signing identity resolution and proposalType canonical cleanup") bundled a muse-repo fix together with this musehub-side cleanup. The muse-repo phases (signing-identity resolution) have shipped; this issue carries forward the musehub-side work that ticket scoped out as a separate piece — splitting it out here since it has its own migration/service surface and shouldn't block on or be blocked by muse-repo releases.

Migration 0070 (alembic/versions/0070_backfill_merge_strategy_canonical.py) renamed merge_strategy values to canonical form (state_overlay→overlay, state_weave→weave, state_rebase→replay, domain_selective→selective) but never touched the separate proposal_type column. ProposalType still has one legacy-style member sitting alongside six already-canonical ones.

Current state (verified)

ProposalType enum — musehub/models/musehub.py:799-808:

class ProposalType(str, Enum):
    STATE_MERGE          = "state_merge"      # ← legacy-style, never renamed
    STEM_INTEGRATION     = "stem_integration"
    MIDI_EVOLUTION       = "midi_evolution"
    PAYMENT_SETTLEMENT   = "payment_settlement"
    AGENT_DELEGATION     = "agent_delegation"
    IDENTITY_TRANSITION  = "identity_transition"
    CANONICAL_RELEASE    = "canonical_release"

Only STATE_MERGE is non-canonical — the other six already follow the plain-noun convention MergeStrategy now uses post-0070 (recursive, overlay, snapshot, replay, weave, selective, phased, cherry_pick). No domain_*-prefixed proposalType values exist; state_merge is the only offender.

Every new proposal gets state_merge by default today — not just as an app-level default but at the DB column level: musehub/db/musehub_social_models.py:257 declares default="state_merge", server_default="state_merge", so even a raw SQL insert with no explicit value gets the legacy string.

Full list of "state_merge" occurrences (production code)

  • musehub/models/musehub.py:802 — enum definition
  • musehub/models/musehub.py:1397 — Field("state_merge", ...) default on a request/response model
  • musehub/models/musehub.py:1536 — docstring example
  • musehub/db/musehub_social_models.py:257 — ORM column default=/server_default=
  • musehub/services/musehub_proposals.py:166, 208, 1806 — default param + fallback (getattr(..., "state_merge"))
  • musehub/mcp/write_tools/proposals.py:100 — default param
  • musehub/mcp/dispatcher.py:870 — _str_or_none("proposal_type") or "state_merge"
  • musehub/mcp/tools/musehub.py:1658, 1662, 1663 — MCP tool JSON-schema enum list + default
  • musehub/templates/musehub/fragments/proposal_rows.html:21, 51
  • musehub/templates/musehub/fragments/proposal_row_detail.html:80, 85
  • musehub/templates/musehub/pages/proposal_detail.html:148
  • alembic/versions/0045_proposal_type_and_merge_strategy.py:21 — original column creation with server_default="state_merge" (historical migration — leave as-is, it reflects what was true at that point in time)
  • scripts/seed_local_proposals.py:56
  • Test fixtures across tests/test_proposal_*.py (many files) hardcode "state_merge" extensively

Proposed rename

Mirror migration 0070's pattern — strip the legacy prefix: state_merge → merge.

Phases

Phase 1 — Migration

New Alembic migration (0071 or next available number):

UPDATE musehub_proposals SET proposal_type = 'merge' WHERE proposal_type = 'state_merge';

Also alter the column's server_default from 'state_merge' to 'merge' so future raw inserts get the canonical value without relying on application-layer defaults.

Phase 2 — ProposalType enum and model defaults

  • musehub/models/musehub.py:802 — STATE_MERGE = "state_merge" → MERGE = "merge" (rename the member too, not just the value, so ProposalType.MERGE reads correctly at every call site)
  • musehub/models/musehub.py:1397 — update the Field(...) default
  • musehub/models/musehub.py:1536 — update the docstring example
  • musehub/db/musehub_social_models.py:257 — update default=/server_default=

Phase 3 — Service layer, MCP tools

  • musehub/services/musehub_proposals.py:166, 208, 1806
  • musehub/mcp/write_tools/proposals.py:100
  • musehub/mcp/dispatcher.py:870
  • musehub/mcp/tools/musehub.py:1658, 1662, 1663

Phase 4 — Templates

  • musehub/templates/musehub/fragments/proposal_rows.html:21, 51
  • musehub/templates/musehub/fragments/proposal_row_detail.html:80, 85
  • musehub/templates/musehub/pages/proposal_detail.html:148

Phase 5 — Fixtures and seed data

  • scripts/seed_local_proposals.py:56
  • All tests/test_proposal_*.py fixtures using the legacy string

Phase 6 — Verification

  • Grep gate: no state_merge string literal survives anywhere in musehub/ or scripts/ (the historical 0045 migration file is the one deliberate exception — it must stay as a faithful record of what that migration actually did)
  • Confirm existing proposals in staging/production DBs read back as proposal_type: "merge" after the migration runs
  • Confirm creating a new proposal via both the HTTP API and MCP tools, with no explicit proposal_type, produces "merge"

Acceptance criteria

  • Migration backfills all existing state_merge rows to merge and updates the column's server default
  • ProposalType.MERGE is the only canonical member name/value for this case; STATE_MERGE no longer exists
  • No state_merge literal remains in service layer, MCP tools, or templates
  • Seed script and test fixtures updated
  • New proposals (API and MCP) default to proposal_type: "merge"

Out of scope

  • Any other ProposalType member — all six others are already canonical, confirmed by audit.
  • MergeStrategy enum / merge_strategy column — already fully canonicalized by migration 0070.
◎ Activity1
●
gabriel opened this issue 2 days ago
○
gabriel 1 day ago

Implemented — commit e2af87b3 on dev/main, pushed to local/staging/production muse remotes (version control only — see note on live deployment below).

What shipped

Migration 0082 (alembic/versions/0082_rename_proposal_type_state_merge.py): backfills proposal_type = 'state_merge' → 'merge', and updates the column's server_default so future raw inserts default to the canonical value too (mirroring migration 0072's drift-fix pattern for merge_strategy).

Enum: ProposalType.STATE_MERGE renamed to ProposalType.MERGE (the member name, not just the value) in musehub/models/musehub.py. Searching for the ProposalType.STATE_MERGE symbol (not just the "state_merge" string literal) turned up two more usages beyond the original audit — lines 915 and 1007 — both Field defaults referencing the enum member directly.

Everywhere else: DB model default, service layer (3 sites), MCP write tool default, MCP dispatcher fallback, MCP tool JSON-schema enum/default, three Jinja templates (comparison/fallback literals), and the seed script — all updated to "merge".

Tests: six tests/test_proposal_*.py files with hardcoded legacy-value references updated (including test_proposal_type_badge.py, which is currently skipped pending the unrelated musehub#96 design decision — updated anyway so it won't carry a stale literal once re-enabled). Added a new invariant test asserting no ProposalType member may use state_*/domain_* legacy naming, to catch this exact class of drift immediately if it recurs.

Deliberately left untouched: alembic/versions/0045_proposal_type_and_merge_strategy.py (the historical migration that originally created the column — a faithful record of what was true at that point in time) and two unrelated test function names containing "state_merge" as a substring of "state_merged" (proposal lifecycle state, nothing to do with proposal_type).

Verification

  • Grep gate: zero state_merge/STATE_MERGE occurrences anywhere in the repo except the two deliberate exceptions above.
  • Ran alembic upgrade head against the local dev DB; confirmed via direct psql query that all 3 existing proposal rows now read proposal_type = 'merge'.
  • muse hub proposal list --json against the local dev hub confirms the live API now returns "proposalType": "merge".
  • Inspected create_proposal's live (container-loaded) function signature directly inside the running musehub container — confirmed proposal_type: str = 'merge', proving the fix is actually live, not just on disk.
  • Restarted both musehub and musehub_worker containers; confirmed both healthy post-migration (/healthz reports db: true, worker logs "MuseHub worker started").
  • 841 tests passed, 10 skipped (pre-existing, unrelated musehub#96 skip) across the full proposal, MCP dispatcher, and MCP write-tools suites.

Not yet done — actual staging/production deployment

Everything above was verified against the local dev environment (local Docker containers + local dev DB). I pushed the code to the staging and production muse remotes (version control), but have not run deploy/push.sh staging/prod or applied migration 0082 against the real staging/production databases — that's a live-infrastructure change affecting shared state, so I'm flagging it rather than doing it unprompted. Let me know if you'd like me to proceed with the staging deploy next.

✓
closed this issue 1 day ago