service.py
python
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