proposalType canonical cleanup — state_merge left behind by migration 0070
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 definitionmusehub/models/musehub.py:1397—Field("state_merge", ...)default on a request/response modelmusehub/models/musehub.py:1536— docstring examplemusehub/db/musehub_social_models.py:257— ORM columndefault=/server_default=musehub/services/musehub_proposals.py:166, 208, 1806— default param + fallback (getattr(..., "state_merge"))musehub/mcp/write_tools/proposals.py:100— default parammusehub/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 + defaultmusehub/templates/musehub/fragments/proposal_rows.html:21, 51musehub/templates/musehub/fragments/proposal_row_detail.html:80, 85musehub/templates/musehub/pages/proposal_detail.html:148alembic/versions/0045_proposal_type_and_merge_strategy.py:21— original column creation withserver_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, soProposalType.MERGEreads correctly at every call site)musehub/models/musehub.py:1397— update theField(...)defaultmusehub/models/musehub.py:1536— update the docstring examplemusehub/db/musehub_social_models.py:257— updatedefault=/server_default=
Phase 3 — Service layer, MCP tools
musehub/services/musehub_proposals.py:166, 208, 1806musehub/mcp/write_tools/proposals.py:100musehub/mcp/dispatcher.py:870musehub/mcp/tools/musehub.py:1658, 1662, 1663
Phase 4 — Templates
musehub/templates/musehub/fragments/proposal_rows.html:21, 51musehub/templates/musehub/fragments/proposal_row_detail.html:80, 85musehub/templates/musehub/pages/proposal_detail.html:148
Phase 5 — Fixtures and seed data
scripts/seed_local_proposals.py:56- All
tests/test_proposal_*.pyfixtures using the legacy string
Phase 6 — Verification
- Grep gate: no
state_mergestring literal survives anywhere inmusehub/orscripts/(the historical0045migration 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_mergerows tomergeand updates the column's server default ProposalType.MERGEis the only canonical member name/value for this case;STATE_MERGEno longer exists- No
state_mergeliteral 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
ProposalTypemember — all six others are already canonical, confirmed by audit. MergeStrategyenum /merge_strategycolumn — already fully canonicalized by migration 0070.
Implemented — commit
e2af87b3ondev/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): backfillsproposal_type = 'state_merge'→'merge', and updates the column'sserver_defaultso future raw inserts default to the canonical value too (mirroring migration 0072's drift-fix pattern formerge_strategy).Enum:
ProposalType.STATE_MERGErenamed toProposalType.MERGE(the member name, not just the value) inmusehub/models/musehub.py. Searching for theProposalType.STATE_MERGEsymbol (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_*.pyfiles with hardcoded legacy-value references updated (includingtest_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 noProposalTypemember may usestate_*/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 withproposal_type).Verification
state_merge/STATE_MERGEoccurrences anywhere in the repo except the two deliberate exceptions above.alembic upgrade headagainst the local dev DB; confirmed via directpsqlquery that all 3 existing proposal rows now readproposal_type = 'merge'.muse hub proposal list --jsonagainst the local dev hub confirms the live API now returns"proposalType": "merge".create_proposal's live (container-loaded) function signature directly inside the runningmusehubcontainer — confirmedproposal_type: str = 'merge', proving the fix is actually live, not just on disk.musehubandmusehub_workercontainers; confirmed both healthy post-migration (/healthzreportsdb: true, worker logs "MuseHub worker started").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
stagingandproductionmuse remotes (version control), but have not rundeploy/push.sh staging/prodor 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.