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