feat(provenance): layered ProvenanceEnvelope + effective() leaf (WP-0007 T1)

Dependency-free leaf rail: ProvenanceEnvelope + SpanProvenanceDelta + effective()
(page envelope ⊕ span delta; zero-cost inheritance when uniform). Liveness/
Staleness/OverlayState enums. 6 tests green. (blueprint §7.3, §11)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 08:55:35 +02:00
parent 3b4f10a349
commit aca9bf30f9
3 changed files with 198 additions and 1 deletions

View File

@@ -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, <empty delta>)`` 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)

73
tests/test_provenance.py Normal file
View File

@@ -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

View File

@@ -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