gabriel / musehub public
service.py python
109 lines 4.5 KB
Raw
sha256:c73281cffffbc70487969720af074e54b34326d59ec6aff0827fa16512a4e512 docs(mwp-2): mark all acceptance criteria met; add closing … Sonnet 4.6 85 days ago
1 """IdentityGraphService — unified surface enforcing all three invariants atomically."""
2
3 from collections import defaultdict
4 from decimal import Decimal
5
6 from .cycle import CycleDetector, CycleError
7 from .dag import EdgeType, GraphEdge, IdentityDAG, NodeType
8 from .depth import RootDistanceIndex
9 from .quorum import OrgSpec, QuorumEngine, VoteRecord
10
11 type MemberWeights = dict[str, Decimal]
12
13 class IdentityGraphService:
14 def __init__(self) -> None:
15 self._node_types: dict[str, NodeType] = {}
16 self._quorums: dict[str, int] = {}
17 self._member_weights: dict[str, MemberWeights] = defaultdict(dict)
18 self._dag = IdentityDAG.empty()
19 self._depth_index: RootDistanceIndex | None = None
20
21 # ── mutations ──────────────────────────────────────────────────────────────
22
23 def add_identity(
24 self,
25 handle: str,
26 node_type: NodeType,
27 quorum: int | None = None,
28 ) -> None:
29 if handle in self._node_types:
30 raise ValueError(f"Identity {handle!r} already exists")
31 self._node_types[handle] = node_type
32 self._dag.add_node(handle, node_type)
33 if node_type == NodeType.ORG and quorum is not None:
34 self._quorums[handle] = quorum
35 self._depth_index = None
36
37 def add_spawn(self, from_handle: str, to_handle: str) -> None:
38 if from_handle not in self._node_types:
39 raise KeyError(from_handle)
40 if to_handle not in self._node_types:
41 raise KeyError(to_handle)
42 CycleDetector(self._dag).assert_no_cycle(from_handle, to_handle, EdgeType.SPAWNS)
43 self._dag.add_edge(from_handle, to_handle)
44 self._depth_index = None
45
46 def add_membership(
47 self,
48 member_handle: str,
49 org_handle: str,
50 weight: Decimal,
51 ) -> None:
52 if member_handle not in self._node_types:
53 raise KeyError(member_handle)
54 if org_handle not in self._node_types:
55 raise KeyError(org_handle)
56 if self._node_types[org_handle] != NodeType.ORG:
57 raise ValueError(f"{org_handle!r} is not an org")
58 CycleDetector(self._dag).assert_no_cycle(member_handle, org_handle, EdgeType.MEMBER_OF)
59 self._dag.add_edge(member_handle, org_handle)
60 self._member_weights[org_handle][member_handle] = weight
61 self._depth_index = None
62
63 # ── queries ────────────────────────────────────────────────────────────────
64
65 def node_type(self, handle: str) -> NodeType:
66 if handle not in self._node_types:
67 raise KeyError(handle)
68 return self._node_types[handle]
69
70 def root_distance(self, handle: str) -> int | None:
71 return self._index().distance(handle)
72
73 def human_ancestors(self, handle: str) -> set[str]:
74 return self._index().human_ancestors(handle)
75
76 def is_quorum_met(
77 self,
78 org_handle: str,
79 voters: set[str],
80 sub_votes: dict[str, set[str]] | None = None,
81 ) -> bool:
82 if sub_votes is None:
83 sub_votes = {}
84 vote_records: list[VoteRecord] = []
85 for voter in voters:
86 vote_records.append(VoteRecord(voter_handle=voter, org_handle=org_handle))
87 for sub_org, sub_voters in sub_votes.items():
88 for voter in sub_voters:
89 vote_records.append(VoteRecord(voter_handle=voter, org_handle=sub_org))
90 engine = self._build_quorum_engine()
91 return engine.is_quorum_met(org_handle, vote_records)
92
93 # ── internals ─────────────────────────────────────────────────────────────
94
95 def _index(self) -> RootDistanceIndex:
96 if self._depth_index is None:
97 self._depth_index = RootDistanceIndex.build(self._dag)
98 return self._depth_index
99
100 def _build_quorum_engine(self) -> QuorumEngine:
101 orgs: dict[str, OrgSpec] = {}
102 for handle, ntype in self._node_types.items():
103 if ntype == NodeType.ORG:
104 orgs[handle] = OrgSpec(
105 handle=handle,
106 quorum=self._quorums.get(handle, 1),
107 member_weights=dict(self._member_weights.get(handle, {})),
108 )
109 return QuorumEngine(orgs=orgs)
File History 2 commits
sha256:c73281cffffbc70487969720af074e54b34326d59ec6aff0827fa16512a4e512 docs(mwp-2): mark all acceptance criteria met; add closing … Sonnet 4.6 85 days ago
sha256:4dd2a937f66f8e36d9aa59bd1bf3cb880ca1d503fef67300a0cba3b482784081 test(mwp2): Phase 0 RED — reproduction tests for _walk_comm… Sonnet 4.6 85 days ago