Developer Docs Domain Protocol
PHASE 03

Domain Protocol

The MuseDomainPlugin protocol is the single seam between domain-specific knowledge and the Muse core engine. Implement six methods and any state space — MIDI, source code, genomics, 3D spatial, financial models, identity graphs — gets branching, merging, time-travel, typed diffs, conflict resolution, and semantic versioning for free. The engine handles the DAG; you handle the meaning.

The protocol

MuseDomainPlugin is a @runtime_checkable Protocol — duck-typed, not inherited. Your plugin is a plain class that satisfies the interface; no base class required, no registration decorator. The engine checks isinstance(plugin, MuseDomainPlugin) at load time, then calls the six methods throughout its lifecycle.

Two optional extension protocols layer additional capabilities on top. Implementing them unlocks engine features automatically:

ProtocolExtra methodUnlocks
MuseDomainPlugin Full VCS: branch, merge, log, diff, checkout, push/pull
AddressedMergePlugin merge_ops() Address-keyed Map merge — ops at different addresses commute automatically
HarmonyPlugin conflict_fingerprint() + similarity() Semantic conflict fingerprinting — Tier 3 fuzzy replay

Six methods

python muse.domain — MuseDomainPlugin
from muse.domain import (
    MuseDomainPlugin, LiveState, StateSnapshot,
    StateDelta, MergeResult, DriftReport, DomainSchema,
)
import pathlib

class MuseDomainPlugin(Protocol):

    def snapshot(self, live_state: LiveState) -> StateSnapshot:
        """Capture current state as a content-addressable SnapshotManifest.

        live_state is a Path (working tree) or an existing SnapshotManifest.
        Walk the domain's files, hash each one, return the manifest.
        Must honour .museignore — call ignore_filter(path) before adding.
        """

    def diff(
        self,
        base:   StateSnapshot,
        target: StateSnapshot,
        *,
        repo_root: pathlib.Path | None = None,
    ) -> StateDelta:
        """Compute the minimal StructuredDelta from base → target.

        StateDelta = {"domain": str, "ops": list[DomainOp], "summary": str,
                      "sem_ver_bump": "none"|"patch"|"minor"|"major",
                      "breaking_changes": list[str]}
        """

    def merge(
        self,
        base:  StateSnapshot,
        left:  StateSnapshot,
        right: StateSnapshot,
        *,
        repo_root: pathlib.Path | None = None,
    ) -> MergeResult:
        """Three-way merge. base is the common ancestor.

        MergeResult = {"merged": SnapshotManifest, "conflicts": list[str]}
        conflicts is a list of paths that could not be auto-merged.
        Consult .museattributes for per-path merge strategy overrides.
        """

    def drift(
        self,
        committed: StateSnapshot,
        live:      LiveState,
    ) -> DriftReport:
        """Detect uncommitted changes between HEAD snapshot and live working tree.

        DriftReport = dataclass(has_drift: bool, summary: str, delta: StateDelta | None)
        Called by `muse status` and `muse drift` commands.
        """

    def apply(self, delta: StateDelta, live_state: LiveState) -> LiveState:
        """Apply a delta to reconstruct historical state during checkout.

        For most file-based domains this is a no-op that returns live_state
        unchanged — the engine handles file restoration via the object store.
        Override when your domain needs post-processing (e.g. recompilation).
        """

    def schema(self) -> DomainSchema:
        """Declare the domain's data structure.

        Drives diff algorithm selection per dimension, merge mode,
        and the domain registry UI at /domains.
        """

Type aliases

AliasConcrete typeNotes
LiveStatePath | SnapshotManifestWorking tree path or in-memory snapshot
StateSnapshotSnapshotManifest{"files": dict[str,str], "domain": str, "directories": list[str]}
StateDeltaStructuredDeltaTyped ops list + summary + semver hint
MergeResultTypedDict{"merged": SnapshotManifest, "conflicts": list[str]}
DriftReportdataclasshas_drift: bool, summary: str, delta: StateDelta | None

What every plugin gets for free

Implement the six-method protocol and the core engine delivers four advanced capabilities automatically — no extra work required per domain.

Typed Delta Algebra StructuredDelta — every change is a typed operation

Unlike Git's blob diffs, Muse deltas are typed objects: InsertOp, ReplaceOp, DeleteOp — each carrying the address, before/after content IDs, and affected dimension. Machine-readable with muse read --json.

json muse read --json → structured_delta.ops
{
  "op":             "replace",
  "address":        "shared-state.mid",
  "old_content_id": "sha256:a1b2c3d4e5f67890…",
  "new_content_id": "sha256:e5f6a7b8c9d01234…"
}
Domain Schema Per-domain dimensions drive diff algorithm selection

Each plugin's schema() declares its dimensions and merge mode. The engine uses this to select the right diff algorithm per dimension and to surface only the dimensions that actually conflict.

code functions · classes · imports addressed
identity identities · relationships three_way
midi notes · tempo · structure addressed
Addressed Merge Address-keyed Map merge — independent ops commute automatically

Plugins implementing AddressedMergePlugin use address-keyed Map merge. Operations at different addresses commute automatically — only operations on the same address with incompatible intent surface a conflict.

Scenario A Different addresses — auto-merge
left InsertOp "notes-a.mid" tick=0 · C4 E4 G4
right InsertOp "notes-b.mid" tick=480 · D4 F4 A4
different addresses → ops commute automatically Clean merge · both applied
Scenario B Same address, conflicting intent
left ReplaceOp "shared-melody.mid" C4 E4 G4 · major triad
right ReplaceOp "shared-melody.mid" C4 E♭4 G4 · minor triad
same address · non-commuting content → conflict surfaced Conflict · Harmony resolves
Harmony Conflict Memory Semantic fingerprinting — recurring conflicts resolve without human input

Plugins implementing HarmonyPlugin provide conflict_fingerprint() and similarity(). Harmony's four-tier engine replays known resolutions automatically — Tier 3 matches conflicts by semantic shape, not blob identity. A human-verified resolution confidence of 1.0 gates the replay.

text Harmony tier stack
Tier 1 — Policy        declarative rule fires on path pattern
Tier 2 — Exact replay  blob fingerprint matches a saved resolution
Tier 3 — Semantic      similarity() score ≥ threshold → fuzzy replay
Tier 4 — Escalate      create hub issue; flag for human or agent

DomainSchema

The schema declaration tells the engine how your domain is structured: what dimensions exist, whether they merge independently, what algorithm to use, and what merge mode applies. It also drives the domain registry UI — the description, dimension list, and tags displayed on /domains.

python DomainSchema structure
{
    "domain":         "identity",
    "description":    "Cryptographic identity graph — humans, agents, orgs.",
    "schema_version": "1.0.0",
    "merge_mode":     "three_way",   # "three_way" | "crdt"
    "dimensions": [
        {
            "name":              "identities",
            "schema":            {"kind": "set"},   # "set" | "sequence" | "map" | "scalar"
            "independent_merge": True,        # this dim merges without blocking others
        },
        {
            "name":              "relationships",
            "schema":            {"kind": "set"},
            "independent_merge": True,
        },
    ],
}
FieldValuesEffect
merge_modethree_wayStandard common-ancestor merge; merge() called. If the plugin also implements AddressedMergePlugin, merge_ops() is called instead for address-keyed Map merge.
merge_modecrdtConvergent join; no common ancestor needed
schema.kindsetUnordered collection; insert/delete ops only
schema.kindsequenceOrdered list; insert/delete/move ops; OT safe
schema.kindmapKey-value; replace/mutate ops
schema.kindscalarSingle value; replace only; last-write-wins
independent_mergeTrueThis dimension merges without waiting for siblings

Five algebras. One typed result.

The engine selects the diff algorithm per dimension from your plugin's schema(). You declare the shape — the engine handles identity, diffing, and merge selection automatically.

Sequence Myers / LCS on SHA-256 IDs
notes · nucleotides · animation frames · git objects
before
a1b2
C4
c3d4
E4
e5f6
G4
g7h8
B♭4
= match
× DeleteOp
= match
↓ MoveOp
a1b2
C4
k1l2
F4
e5f6
G4
n5o6
A4
g7h8
B♭4
+ InsertOp
+ InsertOp
↑ arrived
after
Identity is hash-based: two elements are equal iff their SHA-256 hashes match — content is never inspected by the core. Delete + insert pairs sharing the same hash are collapsed into MoveOps in a post-pass.
Tree Zhang-Shasha / GumTree
scene graphs · ASTs · track hierarchies
BEFORE session intro verse C4 E4 G4 MoveOp + InsertOp AFTER session intro verse C4 E4 G4 A4
Edit distance over node hierarchy — moves preserve subtree identity across parent changes.
Tensor Sparse / block numerical diff
sim state · voxel grids · weight matrices
t = 0
t = 1 (Δ)
|Δ| = 0 |Δ| ≤ ε |Δ| > ε → PatchOp
Configurable ε threshold. Sparse mode records only changed blocks — efficient for large tensors.
Set Set algebra · add / remove
annotations · tags · gene ontology terms
before
GO:0001234 GO:0005634 GO:0006915 GO:0016020
× del
+ ins
after
GO:0001234 GO:0005634 GO:0016020 GO:0042592
Unordered — no position tracking. Pure membership delta: {removed} and {added}.
Map Recursive key-by-key delegation
metadata · configs · nested structures
tempo 120 → 140 scalar → PatchOp
notes […] → […′] sequence → LCS
tags {…} → {…′} set → algebra
author "Bach" = "Bach"
Each key is diffed by whichever algorithm matches its declared type — recursively, to arbitrary depth.
diff() → StructuredDelta all five algorithms produce the same typed operation list
InsertOp DeleteOp MoveOp ReplaceOp PatchOp
merge_mode: “three_way” + AddressedMergePlugin
Address-keyed Map merge — ops on different addresses commute automatically; same-address conflicts surface to Harmony for resolution
or
merge_mode: “crdt”
CRDT join() — convergent, no coordination required; any two replicas always reach the same state

Typed delta algebra

Unlike Git's blob diffs, Muse deltas are typed objects. Every change is an op with a known shape: the address it targets, before/after content IDs, and the dimension it belongs to. This makes deltas machine-readable, replayable, and composable — the foundation for address-keyed Map merge and semantic conflict resolution.

python muse.domain — DomainOp variants
# Ordered-sequence domains (MIDI note-level): position required
InsertOp  = {"op": "insert", "address": str, "content_id": str, "position": int}
DeleteOp  = {"op": "delete", "address": str, "content_id": str, "position": int}

# Name-addressed (unordered) domains: no position field — address IS the identity
AddressedInsertOp = {"op": "insert", "address": str, "content_id": str,
                      "content_summary": str}
AddressedDeleteOp = {"op": "delete", "address": str, "content_id": str,
                      "content_summary": str}

# Reposition within an ordered sequence — commutes with non-overlapping moves
MoveOp    = {"op": "move",   "address": str, "from_position": int, "to_position": int}

# Atomic leaf-level change; old_content_id enables conflict detection
ReplaceOp = {"op": "replace", "address": str,
              "old_content_id": str, "new_content_id": str}

# Entity modification: id + named field mutations (for structured records)
MutateOp  = {"op": "mutate", "address": str,
              "entity_id": str, "mutations": dict[str, Any]}

# Container modification: path + nested child ops (subtree patch)
PatchOp   = {"op": "patch",   "address": str, "child_ops": list[DomainOp]}

# Rename any first-class entity — directory, file, or symbol (address changed, content unchanged)
RenameOp          = {"op": "rename",            "address": str, "from_address": str}
json muse read --json (structured_delta excerpt)
{
  "domain":       "identity",
  "summary":      "Added 1 identity, 2 relationships",
  "sem_ver_bump": "minor",
  "ops": [
    {
      "op":         "insert",
      "address":    "identities/claude-code.json",
      "content_id": "sha256:a1b2c3...",
      "position":   null
    },
    {
      "op":             "replace",
      "address":        "identities/gabriel.json",
      "old_content_id": "sha256:d4e5f6...",
      "new_content_id": "sha256:7890ab..."
    }
  ]
}

Address-keyed commutativity

When AddressedMergePlugin.merge_ops() is called, the engine passes both branches' op lists. Operations at different addresses commute automatically — both are applied, no conflict. Only operations at the same address with incompatible intent surface as a conflict:

text commutativity rules
left: InsertOp("identities/alice.json")   ─┐
right: InsertOp("identities/bob.json")    ─┘  → auto-merge (different addresses)

left: ReplaceOp("identities/gabriel.json", v1→v2)   ─┐
right: ReplaceOp("identities/gabriel.json", v1→v3)  ─┘  → CONFLICT (same addr, different targets)

left: ReplaceOp("identities/gabriel.json", v1→v2)   ─┐
right: ReplaceOp("identities/gabriel.json", v1→v2)  ─┘  → auto-merge (consensus: identical op)

Addressed-merge example — conflict and resolution

Two agents check out the same commit and both edit config/app.json. They diverge on MAX_CONNECTIONS and converge on an unrelated key. Here is the full lifecycle: deltas, merge call, conflict surfacing, manual resolution, harmony learning.

Base state

json config/app.json — common ancestor (sha256:base00…)
{
  "MAX_CONNECTIONS": 10,
  "TIMEOUT_MS":      3000,
  "LOG_LEVEL":       "info"
}

Agent A's delta (feat/scale-up)

json structured_delta — agent A
{
  "domain": "json-docs",
  "ops": [
    {
      "op":             "replace",
      "address":        "config/app.json",
      "old_content_id": "sha256:base00…",
      "new_content_id": "sha256:left01…"    // MAX_CONNECTIONS: 10 → 20
    }
  ],
  "summary": "scale MAX_CONNECTIONS to 20"
}

Agent B's delta (feat/high-load)

json structured_delta — agent B
{
  "domain": "json-docs",
  "ops": [
    {
      "op":             "replace",
      "address":        "config/app.json",
      "old_content_id": "sha256:base00…",
      "new_content_id": "sha256:right02…"   // MAX_CONNECTIONS: 10 → 50, TIMEOUT_MS: 3000 → 5000
    }
  ],
  "summary": "high-load tuning: MAX_CONNECTIONS 50, TIMEOUT_MS 5000"
}

Merge call and conflict

When dev merges both branches, merge_ops() receives both op lists. Both ops target the same address (config/app.json) with the same old_content_id but different new_content_id values — an irreconcilable divergence. The engine surfaces a conflict:

bash
muse merge feat/high-load
Merging feat/high-load into dev…
   auto-merged:  0 paths
   conflicts:    1 path
      config/app.json  (both branches modified from same base)
Merge incomplete. Resolve conflicts then: muse commit -m "merge: …"

Manual resolution

Edit config/app.json to the desired final value, stage it, and commit. Harmony records the resolution automatically with confidence=1.0 (human-verified). The next time this exact conflict recurs — same old_content_id, same two branches — Harmony replays the resolution without human input.

bash
# Manually set MAX_CONNECTIONS: 50, TIMEOUT_MS: 5000 (take B's tuning, accept A's intent)
# ... edit config/app.json ...
muse code add config/app.json
muse commit -m "merge: resolve MAX_CONNECTIONS conflict — use 50 for high-load" \
  --agent-id claude-code --model-id claude-sonnet-4-6 --sign
committed  sha256:9e21b8…
✔ harmony: recorded resolution for 'config/app.json'  (confidence 1.0, human-verified)
The JsonDocPlugin above uses merge_mode: three_way and calls merge(). Plugins that also implement AddressedMergePlugin have merge_ops() called instead, which returns op-level conflict descriptions — enabling the engine to auto-merge ops at different addresses even within the same file. The example above is simplified for clarity.

Extension protocols

AddressedMergePlugin

python muse.domain — AddressedMergePlugin
class AddressedMergePlugin(MuseDomainPlugin, Protocol):
    def merge_ops(
        self,
        base:       StateSnapshot,
        ours_snap:  StateSnapshot,
        theirs_snap:StateSnapshot,
        ours_ops:   list[DomainOp],
        theirs_ops: list[DomainOp],
        *,
        repo_root: pathlib.Path | None = None,
    ) -> MergeResult:
        """Address-keyed Map merge. Ops at different addresses commute; ops at the
        same address with incompatible intent produce a conflict entry."""

HarmonyPlugin

python muse.domain — HarmonyPlugin
class HarmonyPlugin(MuseDomainPlugin, Protocol):
    def conflict_fingerprint(
        self,
        path:     str,
        ours_id:  str,   # "sha256:..." of ours blob
        theirs_id:str,   # "sha256:..." of theirs blob
        repo_root:pathlib.Path,
    ) -> str:            # 64-char hex semantic fingerprint
        """Produce a fingerprint that captures the conflict's structural shape
        independent of timestamps, signatures, or formatting noise.

        Two conflicts that represent the same logical change (e.g. the same
        edge being added with different timestamps) must return the same
        fingerprint — enabling Harmony Tier 3 to replay the resolution."""

    def similarity(self, fp_a: str, fp_b: str) -> float:
        """Score structural similarity between two conflict fingerprints.
        Return value in [0.0, 1.0]. Out-of-range values are clamped.
        DefaultPlugin returns 1.0 for identical fps, 0.0 otherwise."""

The identity domain's conflict_fingerprint() hashes the (from_handle, edge_type, to_handle) triple from a relationship file, ignoring signatures and timestamps entirely. Two pushes that add the same logical edge with different authorization timestamps produce the same fingerprint and replay the same resolution — without human intervention.

Shipped domains

Three domains ship with Muse. All three are fully active, with typed deltas, structured merge, and .museattributes strategy control.

code

Source-code versioning with symbol-level addressed merge. Tree-sitter parses 11 languages into ASTs; functions, classes, and imports merge independently. Two branches that each add a new function auto-merge. Two branches that both modify the same function body produce a conflict at the symbol level, not the line level — the conflict description names the function, not a line range. Dimensions: functions, classes, imports, variables, expressions.

identity

Identity graph versioning with DAG invariant enforcement. Two dimensions — identities (set) and relationships (set) — both merge independently. New identities and new relationships added on separate branches auto-merge. Same-file modifications conflict. During merge, I1 acyclicity is re-enforced on the merged relationship set: any new edge that would introduce a cycle becomes a conflict rather than being silently applied — the graph is never left in a cyclic state.

mist

Content-addressed artifact hosting with signed provenance. A mist repo stores one artifact per file, keyed by the first 12 characters of the base-58 SHA-256 of its bytes — the filename is the identity. Any artifact type is first-class: MIDI, Solidity ABIs, JSON schemas, code, images, or arbitrary binary. Every artifact carries an Ed25519 author signature; AI-produced artifacts also embed agent_id and model_id for full provenance. Because MistPlugin satisfies MuseDomainPlugin, all Muse CLI commands — status, diff, merge, log — work on mist repos without any engine changes. See Phase 12: Mist Domain for the full reference.

Minimal plugin

This is a complete, working domain plugin for a flat directory of JSON documents. It implements the full MuseDomainPlugin protocol — six methods, under 80 lines. No base class, no decorator.

python my_domain/plugin.py
from __future__ import annotations
import hashlib, json, pathlib
from muse.domain import (
    DomainSchema, DriftReport, LiveState, MergeResult,
    MuseDomainPlugin, SnapshotManifest, StateDelta, StateSnapshot,
)

def _sha256(data: bytes) -> str:
    return "sha256:" + hashlib.sha256(data).hexdigest()

class JsonDocPlugin:
    """Version a flat directory of *.json documents."""

    def schema(self) -> DomainSchema:
        return {
            "domain":         "json-docs",
            "description":    "Flat directory of JSON documents.",
            "schema_version": "1.0.0",
            "merge_mode":     "three_way",
            "dimensions": [{
                "name": "documents",
                "schema": {"kind": "set"},
                "independent_merge": True,
            }],
        }

    def snapshot(self, live_state: LiveState) -> StateSnapshot:
        root = pathlib.Path(live_state) if isinstance(live_state, (str, pathlib.Path)) else None
        if root is None:
            return live_state   # already a SnapshotManifest
        files = {}
        for p in sorted(root.glob("**/*.json")):
            rel = p.relative_to(root).as_posix()
            files[rel] = _sha256(p.read_bytes())
        return SnapshotManifest(files=files, domain="json-docs", directories=[])

    def diff(self, base: StateSnapshot, target: StateSnapshot, **_) -> StateDelta:
        b, t = base["files"], target["files"]
        ops = []
        for path in t.keys() - b.keys():
            ops.append({"op": "insert", "address": path, "content_id": t[path], "position": None})
        for path in b.keys() - t.keys():
            ops.append({"op": "delete", "address": path, "content_id": b[path], "position": None})
        for path in b.keys() & t.keys():
            if b[path] != t[path]:
                ops.append({"op": "replace", "address": path,
                             "old_content_id": b[path], "new_content_id": t[path]})
        adds = sum(1 for o in ops if o["op"] == "insert")
        return {"domain": "json-docs", "ops": ops,
                "summary": f"{adds} added, {len(ops)-adds} changed",
                "sem_ver_bump": "minor" if adds else "patch"}

    def merge(self, base, left, right, **_) -> MergeResult:
        b, l, r = base["files"], left["files"], right["files"]
        merged, conflicts = dict(b), []
        for path in l.keys() | r.keys() | b.keys():
            lv, rv, bv = l.get(path), r.get(path), b.get(path)
            if lv == rv:                merged[path] = lv   # consensus / unchanged
            elif lv is None:           merged.pop(path, None)  # deleted on left
            elif rv is None:           merged.pop(path, None)  # deleted on right
            elif lv == bv:              merged[path] = rv   # only right changed
            elif rv == bv:              merged[path] = lv   # only left changed
            else:                       conflicts.append(path)  # both changed differently
        merged = {k: v for k, v in merged.items() if v is not None}
        return {"merged": SnapshotManifest(files=merged, domain="json-docs", directories=[]),
                "conflicts": conflicts}

    def drift(self, committed, live) -> DriftReport:
        current = self.snapshot(live)
        delta = self.diff(committed, current)
        return DriftReport(has_drift=bool(delta["ops"]), summary=delta["summary"], delta=delta)

    def apply(self, delta, live_state) -> LiveState:
        return live_state  # engine handles file restoration via object store

.museattributes

.museattributes is a gitattributes-style file that lets you override the merge strategy for specific paths or patterns — without touching plugin code. The engine reads it before calling merge() and adjusts per-path behavior. Useful for lock files, generated code, or any file where "ours always wins" is the right policy.

text .museattributes
# Pattern            Strategy
*.lock               merge=ours
generated/**         merge=ours
tracks/master.mid    merge=theirs          # always take incoming master
config/schema.json   merge=union           # merge JSON keys additively
*.mid                merge=domain          # delegate to MidiPlugin (default)
StrategyBehaviour
domainDelegate to the active domain plugin (default)
oursAlways take our version; no conflict raised
theirsAlways take their version; no conflict raised
unionAdditive merge where possible; conflict if destructive
binaryTreat as opaque blob; conflict on any difference
bash
# Query the resolved strategy for a path
muse check-attr tracks/drums.mid --json

# List all attribute rules
muse attributes list --json

# Validate the .museattributes file
muse attributes validate --json

Should I write a domain plugin?

Most projects don't need a custom plugin. Work through this tree before writing any code:

Do you need Muse to understand the structure of your files?
├── No  → Use the code or json-docs domain. Add .museattributes
│         rules for files that need custom merge strategies. Done.
│
└── Yes → Do your files have multiple independent dimensions
          (e.g. notes vs. tempo in a MIDI file)?
          │
          ├── No  → Implement MuseDomainPlugin with merge_mode: three_way.
          │         Override merge() for your custom merge semantics.
          │         ~50 lines. See the JsonDocPlugin example.
          │
          └── Yes → Can edits to different dimensions always
                    merge without conflicting each other?
                    │
                    ├── Yes → Use independent_merge: True per dimension
                    │         in DomainSchema. The engine parallelises
                    │         dimension merges automatically.
                    │
                    └── No  → Implement AddressedMergePlugin.
                              Override merge_ops()
                              for address-keyed Map merge. Adds ~30 lines.
                              │
                              └── Also want Harmony Tier 3
                                  (semantic conflict replay)?
                                  └── Implement HarmonyPlugin too.
                                      Add conflict_fingerprint()
                                      + similarity(). ~20 more lines.
Your situationWhat to implementApprox. lines
File types Muse doesn't know about, custom merge policy MuseDomainPlugin (three_way) ~50
Structured records with independent sub-dimensions MuseDomainPlugin + independent_merge: True ~50
Op-level merging — auto-resolve edits to different addresses in same file AddressedMergePlugin ~80
Semantic conflict memory — teach Harmony to recognize conflict shapes HarmonyPlugin ~20 additional
Just need "ours always wins" for a specific file type .museattributes rule, no plugin 1 line

Registry

Domains are registered in muse/plugins/registry.py. The registry is a plain dict keyed by domain name. The active domain for a repo is stored in .muse/repo.json and resolved at startup.

Hash-derived domain integers

Every domain name maps to a 31-bit integer used as the second level of the SLIP-0010 HD path (m/1075233755'/domain'/…). The mapping is deterministic and collision-resistant:

python domain_index — the formula
import hashlib

def domain_index(name: str) -> int:
    """Return the 31-bit HD path integer for a domain name string."""
    digest = hashlib.sha256(name.encode()).digest()
    return int.from_bytes(digest[:4], "big") & 0x7FFFFFFF
Domain nameHD path integer
muse/identity1660078172
muse/payments284229149
muse/code678195575
muse/mist915186137
muse/music1755707987
muse/midi1444628350
muse/prose1658731548
muse/blockchain1556829714
muse/generic2023564266
bash muse domain — CLI reference
# Compute the HD path integer for any domain name
muse domain index muse/identity --json
# → {"name": "muse/identity", "domain_int": 1660078172, ...}

# Reverse-lookup: name from integer
muse domain lookup 1660078172 --json
# → {"domain_int": 1660078172, "name": "muse/identity", ...}

# List all known first-party domains
muse domain list --json

# Verify the integer matches the name (useful in CI)
muse domain check muse/identity 1660078172 --json

Plugin registry

python muse/plugins/registry.py
from muse.plugins.code.plugin     import CodePlugin
from muse.plugins.identity.plugin import IdentityPlugin
from muse.plugins.mist.plugin     import MistPlugin
from my_domain.plugin             import JsonDocPlugin   # your plugin

_REGISTRY: dict[str, MuseDomainPlugin] = {
    "code":      CodePlugin(),
    "identity":  IdentityPlugin(),
    "mist":      MistPlugin(),
    "json-docs": JsonDocPlugin(),   # registered here
}
bash
# Init a repo with your domain
muse init --domain json-docs

# Inspect the active plugin
muse domain-info --json

# List all registered domains
muse domains --json

# See API surface of the active domain
muse api-surface --json

# Decode an HD path to human-readable labels
muse path annotate "m/1075233755'/1660078172'/0'/0'/0'/0'" --json
The hub also has a domain registry exposed at GET /api/musehub/domains and browsable at /domains. Domains registered on the hub can declare a viewer_type that drives how the hub renders files in that domain — enabling custom tree views, diff renderers, and conflict UIs per domain without touching hub code.