gabriel / muse public
path_cmd.py python
227 lines 7.6 KB
Raw
sha256:94f494a8f59e4b708ebb89e304209737ce34324a33aae83840a6f6b06d7b8d9d docs: add domain-extensibility.md — the two-axis breadth/de… Sonnet 5 2 days ago
1 """muse path — HD derivation path tooling.
2
3 Subcommands::
4
5 muse path annotate <path> Decode a raw HD path to human-readable form.
6
7 Example (six-level, pre-Phase-2, or seven-level hub-scoped — musehub#221)::
8
9 $ muse path annotate "m/1075233755'/1660078172'/0'/0'/0'/0'"
10 purpose: muse (1075233755)
11 domain: muse/identity (1660078172)
12 entity_type: human (0)
13 entity_id: 0
14 role: sign (0)
15 index: 0
16
17 $ muse path annotate "m/1075233755'/1660078172'/0'/0'/0'/1449979433'/0'"
18 purpose: muse (1075233755)
19 domain: muse/identity (1660078172)
20 entity_type: human (0)
21 entity_id: 0
22 role: sign (0)
23 hub: 1449979433
24 index: 0
25
26 JSON schema for ``muse path annotate --json``::
27
28 {
29 "raw_path": "m/1075233755'/1660078172'/0'/0'/0'/1449979433'/0'",
30 "purpose": "muse",
31 "purpose_idx": 1075233755,
32 "domain": "muse/identity",
33 "domain_idx": 1660078172,
34 "domain_registered": true,
35 "entity_type": "human",
36 "entity_type_idx": 0,
37 "entity_id": 0,
38 "role": "sign",
39 "role_idx": 0,
40 "hub_idx": 1449979433,
41 "hub_scoped": true,
42 "index": 0
43 }
44
45 ``hub_idx`` is ``null`` and ``hub_scoped`` is ``false`` for six-level paths
46 that predate hub scoping. Unrecognised integers are rendered as their raw
47 value with a ``?`` suffix.
48 """
49
50 import argparse
51 import json
52 import re
53 import sys
54 from typing import TypedDict
55
56 from muse.core.envelope import EnvelopeJson, make_envelope
57 from muse.core.errors import ExitCode
58 from muse.core.paths import user_domain_registry_path
59 from muse.core.slip010 import MUSE_PURPOSE
60 from muse.core.timing import start_timer
61 from muse.core.types import load_json_file
62
63 # Inline the lookup to avoid circular imports — domain_cmd is not a lib.
64 import hashlib
65 import os
66 import pathlib
67
68 class _DomainSeed(TypedDict):
69 name: str
70 index: int
71
72
73 # ---------------------------------------------------------------------------
74 # Known constant maps
75 # ---------------------------------------------------------------------------
76
77 _ENTITY_NAMES: dict[int, str] = {
78 0: "human",
79 1: "agent",
80 2: "org",
81 }
82
83 _ROLE_NAMES: dict[int, str] = {
84 0: "sign",
85 1: "receive",
86 2: "provision",
87 3: "attest",
88 4: "delegate",
89 }
90
91 _SEED_DOMAINS: list[_DomainSeed] = [
92 {"name": "muse/identity", "index": 1660078172},
93 {"name": "muse/payments", "index": 284229149},
94 {"name": "muse/code", "index": 678195575},
95 {"name": "muse/music", "index": 1755707987},
96 {"name": "muse/midi", "index": 1444628350},
97 {"name": "muse/blockchain", "index": 1556829714},
98 {"name": "muse/generic", "index": 2023564266},
99 ]
100
101 def _load_domain_map() -> dict[int, str]:
102 """Return index → name map from the registry, falling back to the seed."""
103 paths = [
104 os.environ.get("MUSE_DOMAIN_REGISTRY", ""),
105 str(user_domain_registry_path()),
106 ]
107 for p in paths:
108 if p:
109 data = load_json_file(pathlib.Path(p))
110 if isinstance(data, dict):
111 return {e["index"]: e["name"] for e in data.get("domains", [])}
112 return {e["index"]: e["name"] for e in _SEED_DOMAINS}
113
114 # ---------------------------------------------------------------------------
115 # Path parser
116 # ---------------------------------------------------------------------------
117
118 _PATH_RE = re.compile(r"^m((?:/\d+'?)+)$")
119 _SEGMENT_RE = re.compile(r"/(\d+)'?")
120
121 class _PathAnnotation(TypedDict):
122 raw_path: str
123 purpose: str
124 purpose_idx: int
125 domain: str
126 domain_idx: int
127 domain_registered: bool
128 entity_type: str
129 entity_type_idx: int
130 entity_id: int
131 role: str
132 role_idx: int
133 hub_idx: int | None
134 hub_scoped: bool
135 index: int
136
137
138 def _annotate(raw: str) -> _PathAnnotation:
139 m = _PATH_RE.match(raw.strip())
140 segments = [int(s) for s in _SEGMENT_RE.findall(m.group(1))] if m else []
141 # Six levels: purpose/domain/entity_type/entity_id/role/index (pre-Phase-2).
142 # Seven levels: same, with a hub segment inserted before index (musehub#221).
143 if len(segments) not in (6, 7):
144 raise ValueError(
145 f"Cannot parse path {raw!r}. "
146 "Expected format: m/<purpose>'/<domain>'/<entity_type>'/<entity_id>'/<role>'/[<hub>'/]<index>'"
147 )
148 purpose_idx, domain_idx, et_idx, entity_id, role_idx = segments[:5]
149 hub_idx: int | None = segments[5] if len(segments) == 7 else None
150 index = segments[-1]
151
152 domain_map = _load_domain_map()
153
154 purpose_name = "muse" if purpose_idx == MUSE_PURPOSE else f"{purpose_idx}?"
155 domain_name = domain_map.get(domain_idx, f"{domain_idx}?")
156 domain_registered = domain_idx in domain_map
157 entity_name = _ENTITY_NAMES.get(et_idx, f"{et_idx}?")
158 role_name = _ROLE_NAMES.get(role_idx, f"{role_idx}?")
159
160 return {
161 "raw_path": raw.strip(),
162 "purpose": purpose_name,
163 "purpose_idx": purpose_idx,
164 "domain": domain_name,
165 "domain_idx": domain_idx,
166 "domain_registered": domain_registered,
167 "entity_type": entity_name,
168 "entity_type_idx": et_idx,
169 "entity_id": entity_id,
170 "role": role_name,
171 "role_idx": role_idx,
172 "hub_idx": hub_idx,
173 "hub_scoped": hub_idx is not None,
174 "index": index,
175 }
176
177 # ---------------------------------------------------------------------------
178 # Subcommand: annotate
179 # ---------------------------------------------------------------------------
180
181 def _run_annotate(args: argparse.Namespace) -> None:
182 elapsed = start_timer()
183 try:
184 result = _annotate(args.path)
185 except ValueError as exc:
186 print(json.dumps({"error": str(exc)}), file=sys.stderr)
187 raise SystemExit(ExitCode.USER_ERROR)
188
189 if args.json_out:
190 print(json.dumps({**make_envelope(elapsed), **result}))
191 else:
192 print(f"purpose: {result['purpose']} ({result['purpose_idx']})")
193 reg = "" if result["domain_registered"] else " [unregistered]"
194 print(f"domain: {result['domain']} ({result['domain_idx']}){reg}")
195 print(f"entity_type: {result['entity_type']} ({result['entity_type_idx']})")
196 print(f"entity_id: {result['entity_id']}")
197 print(f"role: {result['role']} ({result['role_idx']})")
198 if result["hub_scoped"]:
199 print(f"hub: {result['hub_idx']}")
200 print(f"index: {result['index']}")
201
202 # ---------------------------------------------------------------------------
203 # Registration
204 # ---------------------------------------------------------------------------
205
206 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
207 """Register the ``muse path`` namespace."""
208 parser = subparsers.add_parser(
209 "path",
210 help="HD derivation path tooling — annotate raw paths to human-readable form.",
211 description=__doc__,
212 formatter_class=argparse.RawDescriptionHelpFormatter,
213 )
214 path_subs = parser.add_subparsers(dest="path_subcmd", metavar="SUBCMD")
215 path_subs.required = True
216
217 # ── annotate ───────────────────────────────────────────────────────
218 p_annotate = path_subs.add_parser(
219 "annotate",
220 help="Decode a raw HD path to human-readable form.",
221 )
222 p_annotate.add_argument(
223 "path",
224 help="Raw HD path, e.g. \"m/1075233755'/1660078172'/0'/0'/0'/0'\".",
225 )
226 p_annotate.add_argument("--json", "-j", action="store_true", dest="json_out")
227 p_annotate.set_defaults(func=_run_annotate)
File History 1 commit
sha256:94f494a8f59e4b708ebb89e304209737ce34324a33aae83840a6f6b06d7b8d9d docs: add domain-extensibility.md — the two-axis breadth/de… Sonnet 5 2 days ago