generated from coulomb/repo-seed
feat(coordination): event-sourced DecisionLog keystone (WP-0007 T5)
Append-only, totally-ordered-per-space decision log (in-process append authority; git+lease later) with event types overlay/binding/alias/merge/fork, and a derived fold to CoordinationState (aliases LWW, transitively-merged equivalence groups, open overlays). derived = f(log); read-your-writes. 6 tests green. (blueprint §8.1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
10
src/shard_wiki/coordination/__init__.py
Normal file
10
src/shard_wiki/coordination/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""coordination/ — the event-sourced decision log (L3, coordination-canonical state)."""
|
||||
|
||||
from shard_wiki.coordination.decision_log import (
|
||||
CoordinationState,
|
||||
DecisionEvent,
|
||||
DecisionLog,
|
||||
EventType,
|
||||
)
|
||||
|
||||
__all__ = ["DecisionLog", "DecisionEvent", "EventType", "CoordinationState"]
|
||||
124
src/shard_wiki/coordination/decision_log.py
Normal file
124
src/shard_wiki/coordination/decision_log.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""The event-sourced coordination decision log — the keystone (CoreArchitectureBlueprint §8.1).
|
||||
|
||||
Coordination-canonical state (overlays, equivalence bindings, aliases, merges, forks) is an
|
||||
**append-only decision log**, not a mutable file; the queryable *current* state is a **derived
|
||||
fold** of the log (tier-3 disposable). The log is **totally ordered per space** via a single
|
||||
**append authority** — here an in-process counter; a git-backed, lease-held authority is a later
|
||||
binding. That total order is what gives read-your-writes across readers (§8.6).
|
||||
|
||||
`derived = f(canonical)`: :class:`CoordinationState` is always reproducible by replaying the log.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["EventType", "DecisionEvent", "CoordinationState", "DecisionLog"]
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
OVERLAY_CREATED = "overlay-created"
|
||||
BINDING_MADE = "binding-made"
|
||||
ALIAS_SET = "alias-set"
|
||||
MERGE_DECIDED = "merge-decided"
|
||||
PAGE_FORKED = "page-forked"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DecisionEvent:
|
||||
"""One immutable, ordered decision. ``seq`` is the per-space total order."""
|
||||
|
||||
seq: int
|
||||
space: str
|
||||
type: EventType
|
||||
payload: Mapping[str, Any]
|
||||
actor: str | None = None
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CoordinationState:
|
||||
"""The derived fold of a space's log: current aliases + equivalence groups + open overlays.
|
||||
|
||||
Disposable (tier-3): always recomputable from the log via :meth:`DecisionLog.fold`.
|
||||
"""
|
||||
|
||||
aliases: Mapping[str, str]
|
||||
equivalence_groups: tuple[frozenset[str], ...]
|
||||
open_overlays: Mapping[str, Mapping[str, Any]]
|
||||
|
||||
def resolve_alias(self, name: str) -> str | None:
|
||||
return self.aliases.get(name)
|
||||
|
||||
def equivalent_to(self, identity: str) -> frozenset[str]:
|
||||
"""All identities equivalent to ``identity`` (including itself if bound), else just it."""
|
||||
for group in self.equivalence_groups:
|
||||
if identity in group:
|
||||
return group
|
||||
return frozenset({identity})
|
||||
|
||||
|
||||
class DecisionLog:
|
||||
"""In-memory append-only log, totally ordered per space (the append authority for a process).
|
||||
|
||||
A later binding swaps the storage for git + a per-space lease without changing this API.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._events: dict[str, list[DecisionEvent]] = {}
|
||||
|
||||
def append(
|
||||
self,
|
||||
space: str,
|
||||
type: EventType,
|
||||
payload: Mapping[str, Any],
|
||||
actor: str | None = None,
|
||||
) -> DecisionEvent:
|
||||
seq = len(self._events.get(space, ()))
|
||||
event = DecisionEvent(seq=seq, space=space, type=type, payload=dict(payload), actor=actor)
|
||||
self._events.setdefault(space, []).append(event)
|
||||
return event
|
||||
|
||||
def events(self, space: str) -> tuple[DecisionEvent, ...]:
|
||||
"""The space's events in append (total) order. Read-your-writes: a just-appended event
|
||||
is present immediately."""
|
||||
return tuple(self._events.get(space, ()))
|
||||
|
||||
def fold(self, space: str) -> CoordinationState:
|
||||
"""Replay the log into current coordination state (derived = f(log))."""
|
||||
aliases: dict[str, str] = {}
|
||||
overlays: dict[str, dict[str, Any]] = {}
|
||||
groups: list[set[str]] = []
|
||||
|
||||
for event in self.events(space):
|
||||
if event.type is EventType.ALIAS_SET:
|
||||
aliases[event.payload["alias"]] = event.payload["target"]
|
||||
elif event.type is EventType.BINDING_MADE:
|
||||
_merge_group(groups, {str(m) for m in event.payload["members"]})
|
||||
elif event.type is EventType.OVERLAY_CREATED:
|
||||
overlays[event.payload["overlay_id"]] = dict(event.payload)
|
||||
elif event.type is EventType.MERGE_DECIDED:
|
||||
# A merge resolution may collapse an overlay; minimal handling for the slice.
|
||||
overlays.pop(event.payload.get("overlay_id", ""), None)
|
||||
elif event.type is EventType.PAGE_FORKED:
|
||||
_merge_group(groups, {str(event.payload["source"]), str(event.payload["fork"])})
|
||||
|
||||
return CoordinationState(
|
||||
aliases=MappingProxyType(dict(aliases)),
|
||||
equivalence_groups=tuple(frozenset(g) for g in groups),
|
||||
open_overlays=MappingProxyType({k: MappingProxyType(v) for k, v in overlays.items()}),
|
||||
)
|
||||
|
||||
|
||||
def _merge_group(groups: list[set[str]], members: set[str]) -> None:
|
||||
"""Union-merge ``members`` into ``groups`` (any existing group sharing a member absorbs it)."""
|
||||
touching = [g for g in groups if g & members]
|
||||
for g in touching:
|
||||
groups.remove(g)
|
||||
members |= g
|
||||
groups.append(members)
|
||||
Reference in New Issue
Block a user