diff --git a/src/shard_wiki/provenance/__init__.py b/src/shard_wiki/provenance/__init__.py new file mode 100644 index 0000000..af0354f --- /dev/null +++ b/src/shard_wiki/provenance/__init__.py @@ -0,0 +1,116 @@ +"""Provenance — the cross-cutting leaf rail (CoreArchitectureBlueprint §7.3, §11). + +Every artifact in the union carries a :class:`ProvenanceEnvelope`. To keep per-span cost near +zero when provenance is uniform, envelopes are **layered**: a page-level envelope plus optional +per-span deltas. The *effective* provenance of a span is ``page_envelope ⊕ span_delta`` — a span +with no delta inherits the page envelope at zero cost ("effective-vs-own", the same pattern the +page model uses for computed metadata). + +This module is a **dependency-free leaf**: it imports nothing else in the package and contains +only stable data types plus the pure ``effective`` composition. Mechanism never lives here. +""" + +from __future__ import annotations + +import dataclasses +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum + +__all__ = [ + "Liveness", + "Staleness", + "OverlayState", + "ProvenanceEnvelope", + "SpanProvenanceDelta", + "effective", +] + + +class Liveness(Enum): + """Where a view sits on the live↔snapshot axis (blueprint §8, T18).""" + + STATIC = "static" + CAPTURED_SNAPSHOT = "captured-snapshot" + LIVE_OVER_FILES = "live-over-files" + VIEW_TIME = "view-time" + IRREDUCIBLY_LIVE = "irreducibly-live" + + +class Staleness(Enum): + """Freshness state of cached/projected content (blueprint §8.8). ``UNAVAILABLE`` is the + dead-shard state (FederationRequirements ADR-04).""" + + LIVE = "live" + FRESH = "fresh" + STALE = "stale" + UNAVAILABLE = "unavailable" + + +class OverlayState(Enum): + """Overlay lifecycle state of an artifact (FederationRequirements ADR-05).""" + + NONE = "none" + DRAFT = "draft" + PATCH_PENDING = "patch-pending" + APPLIED = "applied" + + +@dataclass(frozen=True, slots=True) +class ProvenanceEnvelope: + """The metadata every page/span carries — union without erasure (INTENT I-4). + + This is the *page-level* (or fully-resolved *effective*) form. Per-span overrides are + expressed as a :class:`SpanProvenanceDelta` and composed via :func:`effective`. + """ + + source_shard: str + liveness: Liveness = Liveness.STATIC + staleness: Staleness = Staleness.FRESH + overlay_state: OverlayState = OverlayState.NONE + source_rev: str | None = None + observed_at: datetime | None = None + authz_context: str | None = None + divergence: tuple[str, ...] = () + lineage: str | None = None + + +@dataclass(frozen=True, slots=True) +class SpanProvenanceDelta: + """A per-span override of a page envelope. Every field is optional; ``None`` (or, for + ``divergence``, the sentinel default) means *inherit the page-level value*. A span with an + all-default delta — or no delta at all — costs nothing and resolves to the page envelope. + """ + + source_shard: str | None = None + liveness: Liveness | None = None + staleness: Staleness | None = None + overlay_state: OverlayState | None = None + source_rev: str | None = None + observed_at: datetime | None = None + authz_context: str | None = None + # Sentinel: ``None`` means inherit; an explicit (possibly empty) tuple overrides. + divergence: tuple[str, ...] | None = None + lineage: str | None = None + + def is_empty(self) -> bool: + """True when the delta overrides nothing (the common, zero-cost case).""" + return all(getattr(self, f.name) is None for f in dataclasses.fields(self)) + + +def effective( + base: ProvenanceEnvelope, delta: SpanProvenanceDelta | None = None +) -> ProvenanceEnvelope: + """Compose a page envelope with an optional span delta → the span's effective provenance. + + ``effective(base, None)`` and ``effective(base, )`` both return ``base`` + unchanged (zero-cost inheritance). Any non-``None`` delta field overrides ``base``. + """ + if delta is None or delta.is_empty(): + return base + overrides = { + f.name: value + for f in dataclasses.fields(delta) + if (value := getattr(delta, f.name)) is not None + } + return dataclasses.replace(base, **overrides) diff --git a/tests/test_provenance.py b/tests/test_provenance.py new file mode 100644 index 0000000..0898672 --- /dev/null +++ b/tests/test_provenance.py @@ -0,0 +1,73 @@ +"""Tests for the provenance leaf rail (SHARD-WP-0007 T1).""" + +from datetime import datetime, timezone + +from shard_wiki.provenance import ( + Liveness, + OverlayState, + ProvenanceEnvelope, + SpanProvenanceDelta, + Staleness, + effective, +) + + +def _base() -> ProvenanceEnvelope: + return ProvenanceEnvelope( + source_shard="shardA", + liveness=Liveness.STATIC, + staleness=Staleness.FRESH, + source_rev="r1", + observed_at=datetime(2026, 6, 15, tzinfo=timezone.utc), + ) + + +def test_no_delta_returns_base_identity(): + base = _base() + assert effective(base) is base + assert effective(base, SpanProvenanceDelta()) is base + + +def test_empty_delta_is_detected(): + assert SpanProvenanceDelta().is_empty() + assert not SpanProvenanceDelta(source_shard="other").is_empty() + + +def test_single_field_override(): + base = _base() + eff = effective(base, SpanProvenanceDelta(source_shard="shardB")) + assert eff.source_shard == "shardB" + # Untouched fields inherit from the page envelope. + assert eff.source_rev == "r1" + assert eff.liveness is Liveness.STATIC + # Base is unchanged (frozen, pure composition). + assert base.source_shard == "shardA" + + +def test_partial_delta_overrides_several_fields(): + base = _base() + eff = effective( + base, + SpanProvenanceDelta( + staleness=Staleness.STALE, + overlay_state=OverlayState.DRAFT, + divergence=("shardB:Home",), + ), + ) + assert eff.staleness is Staleness.STALE + assert eff.overlay_state is OverlayState.DRAFT + assert eff.divergence == ("shardB:Home",) + assert eff.source_shard == "shardA" # inherited + + +def test_divergence_empty_tuple_overrides_but_none_inherits(): + base = ProvenanceEnvelope(source_shard="s", divergence=("x",)) + # None → inherit the page's divergence. + assert effective(base, SpanProvenanceDelta(divergence=None)).divergence == ("x",) + # Explicit empty tuple → override (clear it). + assert effective(base, SpanProvenanceDelta(divergence=())).divergence == () + + +def test_envelope_is_hashable_and_value_equal(): + assert _base() == _base() + assert len({_base(), _base()}) == 1 diff --git a/workplans/SHARD-WP-0007-foundation-implementation.md b/workplans/SHARD-WP-0007-foundation-implementation.md index d97b742..c1e238d 100644 --- a/workplans/SHARD-WP-0007-foundation-implementation.md +++ b/workplans/SHARD-WP-0007-foundation-implementation.md @@ -11,6 +11,7 @@ created: "2026-06-15" updated: "2026-06-15" depends_on: - SHARD-WP-0002 +state_hub_workstream_id: "d551fba1-9c48-4841-a9a5-a9f190f73a60" --- # SHARD-WP-0007 — Foundation implementation @@ -41,8 +42,9 @@ projection, authz beyond the null provider, a network API. Those are later workp ```task id: SHARD-WP-0007-T1 -status: todo +status: done priority: high +state_hub_task_id: "be8f9efe-cfba-46d0-be91-f9b6e87bc0d2" ``` `src/shard_wiki/provenance/`: the `ProvenanceEnvelope` value type (source_shard, source_rev?, @@ -56,6 +58,7 @@ delta). Frozen dataclasses, no tree deps. Tests: ⊕ identity (no delta), overri id: SHARD-WP-0007-T2 status: todo priority: high +state_hub_task_id: "780ad01f-c3e1-4b49-9ae9-60e0324178a7" ``` `src/shard_wiki/model/`: `Identity` (shard-scoped stable handle, value-equal, survives edits), @@ -70,6 +73,7 @@ identity stability vs content change; profile validation accepts/rejects. id: SHARD-WP-0007-T3 status: todo priority: high +state_hub_task_id: "f6e35ddb-ab1e-406a-82f8-563244455f6b" ``` `src/shard_wiki/adapters/`: `AdapterContract` (a `Protocol`/ABC with the operation verbs; @@ -84,6 +88,7 @@ git-or-none history). Imports `model/`, `provenance/`. Tests: read a temp folder id: SHARD-WP-0007-T4 status: todo priority: medium +state_hub_task_id: "12d7fb8d-3842-4142-a462-9d1e6efe58bd" ``` `src/shard_wiki/adapters/conformance.py`: a battery that, given a binding, **verifies declared @@ -98,6 +103,7 @@ lying stub fails with a precise diff. id: SHARD-WP-0007-T5 status: todo priority: high +state_hub_task_id: "c87b1896-59a5-4cde-a292-1086caebd085" ``` `src/shard_wiki/coordination/`: `DecisionLog` — append-only, **totally ordered per space** @@ -112,6 +118,7 @@ append→ordered read; fold reproduces current state; read-your-writes. id: SHARD-WP-0007-T6 status: todo priority: high +state_hub_task_id: "fed38b60-dc0b-40cf-93e9-ab0260aa3ff9" ``` `src/shard_wiki/policy/` (leaf: presets + `resolve()` for canonical-source = chorus default); @@ -126,6 +133,7 @@ the same name → chorus; alias from the log redirects resolution. id: SHARD-WP-0007-T7 status: todo priority: medium +state_hub_task_id: "86c0b255-e373-460d-859d-a39bf6ea4ffb" ``` A thin `shard_wiki` entry (attach shard, resolve, read) tying the slice together; an