generated from coulomb/repo-seed
feat(model): Identity/Placement/Span, Page, CapabilityProfile (WP-0007 T2)
Identity = stable shard-scoped handle (not a fingerprint); Placement separate; Span carries a layered provenance delta. Page model (PageShape incl. the four computational shapes). CapabilityProfile with orthogonal-core axes + validate() applying §6.5 implication rules that reject impossible profiles. Imports only provenance/. 8 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
47
src/shard_wiki/model/__init__.py
Normal file
47
src/shard_wiki/model/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""model/ — the backend-neutral page model and capability types (the top waist).
|
||||||
|
|
||||||
|
Imports only the ``provenance`` leaf; nothing backend-specific lives here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from shard_wiki.model.capability import (
|
||||||
|
AccessGrant,
|
||||||
|
Addressing,
|
||||||
|
AttachmentMode,
|
||||||
|
CapabilityProfile,
|
||||||
|
ContentOpacity,
|
||||||
|
History,
|
||||||
|
MergeModel,
|
||||||
|
NativeQuery,
|
||||||
|
NotSupported,
|
||||||
|
OperationalEnvelope,
|
||||||
|
ProfileError,
|
||||||
|
Substrate,
|
||||||
|
Translation,
|
||||||
|
Verb,
|
||||||
|
WriteGranularity,
|
||||||
|
)
|
||||||
|
from shard_wiki.model.identity import Identity, Placement, Span
|
||||||
|
from shard_wiki.model.page import Page, PageShape
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Identity",
|
||||||
|
"Placement",
|
||||||
|
"Span",
|
||||||
|
"Page",
|
||||||
|
"PageShape",
|
||||||
|
"CapabilityProfile",
|
||||||
|
"Verb",
|
||||||
|
"Substrate",
|
||||||
|
"AttachmentMode",
|
||||||
|
"WriteGranularity",
|
||||||
|
"ContentOpacity",
|
||||||
|
"OperationalEnvelope",
|
||||||
|
"AccessGrant",
|
||||||
|
"History",
|
||||||
|
"MergeModel",
|
||||||
|
"Addressing",
|
||||||
|
"NativeQuery",
|
||||||
|
"Translation",
|
||||||
|
"ProfileError",
|
||||||
|
"NotSupported",
|
||||||
|
]
|
||||||
220
src/shard_wiki/model/capability.py
Normal file
220
src/shard_wiki/model/capability.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
"""Capability profile — capability-as-data (CoreArchitectureBlueprint §6, I-3).
|
||||||
|
|
||||||
|
A binding's abilities are a *position on each capability spectrum*, not a boolean checklist.
|
||||||
|
Descriptively there are fifteen spectra (§6.1); operationally they reduce to a small
|
||||||
|
**orthogonal core** plus **implied** axes, with **implication rules that forbid impossible
|
||||||
|
profiles** (§6.5). ``CapabilityProfile.validate()`` enforces those rules; a profile that
|
||||||
|
violates one is rejected (the structural half of "capability-as-data is sound" — the behavioural
|
||||||
|
half is the conformance suite, T4).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from shard_wiki.provenance import Liveness
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Verb",
|
||||||
|
"Substrate",
|
||||||
|
"AttachmentMode",
|
||||||
|
"WriteGranularity",
|
||||||
|
"ContentOpacity",
|
||||||
|
"OperationalEnvelope",
|
||||||
|
"AccessGrant",
|
||||||
|
"History",
|
||||||
|
"MergeModel",
|
||||||
|
"Addressing",
|
||||||
|
"NativeQuery",
|
||||||
|
"Translation",
|
||||||
|
"CapabilityProfile",
|
||||||
|
"ProfileError",
|
||||||
|
"NotSupported",
|
||||||
|
]
|
||||||
|
|
||||||
|
CONTRACT_VERSION = "0.1"
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileError(ValueError):
|
||||||
|
"""Raised when a capability profile is internally impossible (violates §6.5 rules)."""
|
||||||
|
|
||||||
|
|
||||||
|
class NotSupported(Exception):
|
||||||
|
"""Raised by an adapter when an operation verb is not in its capability profile."""
|
||||||
|
|
||||||
|
|
||||||
|
class Verb(Enum):
|
||||||
|
READ = "read"
|
||||||
|
WRITE = "write"
|
||||||
|
DIFF = "diff"
|
||||||
|
MERGE = "merge"
|
||||||
|
LOCK = "lock"
|
||||||
|
VERSION = "version"
|
||||||
|
PUBLISH = "publish"
|
||||||
|
NOTIFY = "notify"
|
||||||
|
TRANSCLUDE_SOURCE = "transclude-source"
|
||||||
|
TRANSLATE_SYNTAX = "translate-syntax"
|
||||||
|
STRUCTURED_PAYLOAD = "structured-payload"
|
||||||
|
DERIVE_PROJECTION = "derive-projection" # gated, off by default (T18)
|
||||||
|
EXECUTE = "execute" # gated, off by default (T18)
|
||||||
|
|
||||||
|
|
||||||
|
# --- orthogonal core axes (§6.5a) ---
|
||||||
|
class Substrate(Enum):
|
||||||
|
GIT = "git"
|
||||||
|
FILES = "files"
|
||||||
|
RELATIONAL_DB = "relational-db"
|
||||||
|
EXTERNAL_API = "external-api"
|
||||||
|
CRDT = "crdt"
|
||||||
|
|
||||||
|
|
||||||
|
class WriteGranularity(Enum):
|
||||||
|
NONE = "none" # read-only
|
||||||
|
WHOLE_FILE = "whole-file"
|
||||||
|
SECTION = "section"
|
||||||
|
PER_PAGE = "per-page"
|
||||||
|
PER_BLOCK = "per-block"
|
||||||
|
|
||||||
|
|
||||||
|
class ContentOpacity(Enum):
|
||||||
|
TRANSPARENT = "transparent"
|
||||||
|
STRUCTURED_REEVALUABLE = "structured-re-evaluable"
|
||||||
|
ENCRYPTED = "encrypted"
|
||||||
|
PER_ITEM = "per-item"
|
||||||
|
PROPRIETARY_LOSSY = "proprietary-lossy"
|
||||||
|
|
||||||
|
|
||||||
|
class OperationalEnvelope(Enum):
|
||||||
|
LOCAL_UNBOUNDED = "local-unbounded"
|
||||||
|
REALTIME = "realtime"
|
||||||
|
RATE_LIMITED = "rate-limited"
|
||||||
|
|
||||||
|
|
||||||
|
class AccessGrant(Enum):
|
||||||
|
OPEN = "open"
|
||||||
|
TOKEN = "token"
|
||||||
|
OAUTH_SCOPED = "oauth-scoped"
|
||||||
|
P2P_KEY = "p2p-key"
|
||||||
|
ENTERPRISE_ACL = "enterprise-acl"
|
||||||
|
|
||||||
|
|
||||||
|
# --- implied axes (§6.5b): may be derived from the core, validated for consistency ---
|
||||||
|
class History(Enum):
|
||||||
|
NONE = "none"
|
||||||
|
INTERNAL_ONLY = "internal-only"
|
||||||
|
CRDT_LOG = "crdt-log"
|
||||||
|
OPEN_FILE = "open-file"
|
||||||
|
GIT_NATIVE = "git-native"
|
||||||
|
|
||||||
|
|
||||||
|
class MergeModel(Enum):
|
||||||
|
NONE = "none"
|
||||||
|
GIT_TEXT = "git-text"
|
||||||
|
KEEP_BOTH = "keep-both"
|
||||||
|
NATIVE_CRDT = "native-crdt"
|
||||||
|
COEXIST_RANK = "coexist-rank"
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentMode(Enum):
|
||||||
|
# NB: there is deliberately no IMAGE mode — an image/live-memory blob is never an attach
|
||||||
|
# target (I-12). It participates only via export→files.
|
||||||
|
FILE_STORE = "file-store"
|
||||||
|
GIT_IS_STORE = "git-is-store"
|
||||||
|
IN_ENGINE_HOST = "in-engine-host"
|
||||||
|
LOCAL_REST = "local-rest"
|
||||||
|
EXTERNAL_API = "external-api"
|
||||||
|
DIRECT_DB = "direct-db"
|
||||||
|
CRDT_REPLICA = "crdt-replica"
|
||||||
|
P2P = "p2p"
|
||||||
|
|
||||||
|
|
||||||
|
class Addressing(Enum):
|
||||||
|
NONE = "none"
|
||||||
|
PATH = "path"
|
||||||
|
PAGE_ID = "page-id"
|
||||||
|
SPAN = "span"
|
||||||
|
BLOCK_ID = "block-id"
|
||||||
|
STORE_UUID = "store-uuid"
|
||||||
|
TUMBLER = "tumbler"
|
||||||
|
|
||||||
|
|
||||||
|
class NativeQuery(Enum):
|
||||||
|
NONE = "none"
|
||||||
|
TEXT = "text"
|
||||||
|
DERIVED_INDEX = "derived-index"
|
||||||
|
DATALOG_GRAPH = "datalog-graph"
|
||||||
|
DB_QUERY = "db-query"
|
||||||
|
SPARQL = "sparql"
|
||||||
|
|
||||||
|
|
||||||
|
class Translation(Enum):
|
||||||
|
NATIVE = "native"
|
||||||
|
LOSSLESS = "lossless"
|
||||||
|
LOSSY_WITH_REPORT = "lossy-with-report"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CapabilityProfile:
|
||||||
|
"""A verified-at-registration position on each capability axis, plus supported verbs."""
|
||||||
|
|
||||||
|
# orthogonal core
|
||||||
|
substrate: Substrate
|
||||||
|
attachment_mode: AttachmentMode
|
||||||
|
write_granularity: WriteGranularity
|
||||||
|
content_opacity: ContentOpacity
|
||||||
|
operational_envelope: OperationalEnvelope
|
||||||
|
access_grant: AccessGrant
|
||||||
|
liveness: Liveness # computational/liveness axis (15th)
|
||||||
|
# implied
|
||||||
|
history: History
|
||||||
|
merge_model: MergeModel
|
||||||
|
addressing: Addressing
|
||||||
|
native_query: NativeQuery
|
||||||
|
translation: Translation
|
||||||
|
supported_verbs: frozenset[Verb] = field(default_factory=frozenset)
|
||||||
|
contract_version: str = CONTRACT_VERSION
|
||||||
|
|
||||||
|
def supports(self, verb: Verb) -> bool:
|
||||||
|
return verb in self.supported_verbs
|
||||||
|
|
||||||
|
def validate(self) -> CapabilityProfile:
|
||||||
|
"""Apply the §6.5 implication rules; raise :class:`ProfileError` on an impossible
|
||||||
|
combination. Returns ``self`` so it can be used fluently at construction."""
|
||||||
|
rules: list[tuple[bool, str]] = [
|
||||||
|
(
|
||||||
|
Verb.READ in self.supported_verbs,
|
||||||
|
"every shard must support READ (the capability floor)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.attachment_mode is not AttachmentMode.GIT_IS_STORE
|
||||||
|
or (self.substrate is Substrate.GIT and self.history is History.GIT_NATIVE),
|
||||||
|
"git-is-store ⟹ substrate=git ∧ history=git-native",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.content_opacity is not ContentOpacity.ENCRYPTED
|
||||||
|
or self.native_query is NativeQuery.NONE,
|
||||||
|
"encrypted opacity ⟹ native-query=none",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.merge_model is not MergeModel.NATIVE_CRDT
|
||||||
|
or (
|
||||||
|
self.history is History.CRDT_LOG
|
||||||
|
and self.operational_envelope is OperationalEnvelope.REALTIME
|
||||||
|
),
|
||||||
|
"native-crdt merge ⟹ history=crdt-log ∧ envelope=realtime",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(self.write_granularity is WriteGranularity.NONE)
|
||||||
|
== (Verb.WRITE not in self.supported_verbs),
|
||||||
|
"write-granularity=none ⟺ WRITE not supported (read-only consistency)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Verb.MERGE not in self.supported_verbs or self.merge_model is not MergeModel.NONE,
|
||||||
|
"MERGE verb ⟹ merge-model ≠ none",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
broken = [msg for ok, msg in rules if not ok]
|
||||||
|
if broken:
|
||||||
|
raise ProfileError("; ".join(broken))
|
||||||
|
return self
|
||||||
60
src/shard_wiki/model/identity.py
Normal file
60
src/shard_wiki/model/identity.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Identity, Placement, Span — three distinct concepts (CoreArchitectureBlueprint §7.2).
|
||||||
|
|
||||||
|
- **Identity** is a *stable handle* assigned/minted by a shard; it survives edits. It is NEVER
|
||||||
|
derived from content (a content fingerprint identifies a *version*, not a *page*).
|
||||||
|
- **Placement** is *where* an identity sits (path in a shard); one identity may have many
|
||||||
|
placements (a DAG). Placement can change without changing identity.
|
||||||
|
- **Span** is a sub-page address within an identity, optionally carrying a provenance delta.
|
||||||
|
|
||||||
|
Equivalence (detecting that two *distinct* identities hold the same content) is a separate
|
||||||
|
mechanism and lives in ``union/`` — not here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from shard_wiki.provenance import SpanProvenanceDelta
|
||||||
|
|
||||||
|
__all__ = ["Identity", "Placement", "Span"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Identity:
|
||||||
|
"""A shard-scoped, stable page handle. Value-equal; stable across content edits.
|
||||||
|
|
||||||
|
``shard`` scopes the handle so native ids survive projection and never collide across
|
||||||
|
shards. ``key`` is the backend's stable handle (page name / native uid) — not a fingerprint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
shard: str
|
||||||
|
key: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.shard}:{self.key}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Placement:
|
||||||
|
"""Where an identity sits: a path within a shard. Mutable over an identity's life."""
|
||||||
|
|
||||||
|
shard: str
|
||||||
|
path: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.shard}/{self.path}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Span:
|
||||||
|
"""A sub-page address within a page identity, with an optional provenance delta.
|
||||||
|
|
||||||
|
``ref`` is a native span id where the backend mints one (Roam ``:block/uid``, Logseq
|
||||||
|
``id::``), else a positional/fingerprint address. ``delta`` overrides the page envelope
|
||||||
|
only where this span genuinely differs (effective-vs-own, blueprint §7.3).
|
||||||
|
"""
|
||||||
|
|
||||||
|
ref: str
|
||||||
|
start: int | None = None
|
||||||
|
end: int | None = None
|
||||||
|
delta: SpanProvenanceDelta | None = None
|
||||||
43
src/shard_wiki/model/page.py
Normal file
43
src/shard_wiki/model/page.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""The backend-neutral wiki page model — the top narrow waist (CoreArchitectureBlueprint §7).
|
||||||
|
|
||||||
|
Markdown-first but stretchable: every shape reduces to
|
||||||
|
``(content|source, structure, provenance envelope, [derivation rule])``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from shard_wiki.model.identity import Identity, Placement, Span
|
||||||
|
from shard_wiki.provenance import ProvenanceEnvelope
|
||||||
|
|
||||||
|
__all__ = ["PageShape", "Page"]
|
||||||
|
|
||||||
|
|
||||||
|
class PageShape(Enum):
|
||||||
|
"""The page-model shapes (blueprint §7.1). The slice handles PROSE; the rest are declared
|
||||||
|
so adapters can tag content faithfully (union without erasure)."""
|
||||||
|
|
||||||
|
PROSE = "prose"
|
||||||
|
TYPED_RECORD = "typed-record"
|
||||||
|
TYPED_GRAPH = "typed-graph"
|
||||||
|
INLINE_EMBEDDED = "inline-embedded"
|
||||||
|
NON_MARKDOWN_ASSET = "non-markdown-asset"
|
||||||
|
ONE_SOURCE_MANY_PROJECTIONS = "one-source-many-projections"
|
||||||
|
NOTEBOOK = "notebook"
|
||||||
|
PROGRAM_AS_PAGE = "program-as-page"
|
||||||
|
LIVE_TEMPORAL = "live-temporal"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Page:
|
||||||
|
"""A page in the union: a stable identity, content, the page-level provenance envelope,
|
||||||
|
its placements, and its spans (whose deltas layer over the page envelope, §7.3)."""
|
||||||
|
|
||||||
|
identity: Identity
|
||||||
|
body: str
|
||||||
|
envelope: ProvenanceEnvelope
|
||||||
|
shape: PageShape = PageShape.PROSE
|
||||||
|
placements: tuple[Placement, ...] = ()
|
||||||
|
spans: tuple[Span, ...] = field(default_factory=tuple)
|
||||||
106
tests/test_model.py
Normal file
106
tests/test_model.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""Tests for the page model + capability profile (SHARD-WP-0007 T2)."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from shard_wiki.model import (
|
||||||
|
AccessGrant,
|
||||||
|
Addressing,
|
||||||
|
AttachmentMode,
|
||||||
|
CapabilityProfile,
|
||||||
|
ContentOpacity,
|
||||||
|
History,
|
||||||
|
Identity,
|
||||||
|
MergeModel,
|
||||||
|
NativeQuery,
|
||||||
|
OperationalEnvelope,
|
||||||
|
Page,
|
||||||
|
PageShape,
|
||||||
|
ProfileError,
|
||||||
|
Substrate,
|
||||||
|
Translation,
|
||||||
|
Verb,
|
||||||
|
WriteGranularity,
|
||||||
|
)
|
||||||
|
from shard_wiki.provenance import Liveness, ProvenanceEnvelope
|
||||||
|
|
||||||
|
|
||||||
|
def _read_only_folder_profile(**over) -> CapabilityProfile:
|
||||||
|
base = dict(
|
||||||
|
substrate=Substrate.FILES,
|
||||||
|
attachment_mode=AttachmentMode.FILE_STORE,
|
||||||
|
write_granularity=WriteGranularity.NONE,
|
||||||
|
content_opacity=ContentOpacity.TRANSPARENT,
|
||||||
|
operational_envelope=OperationalEnvelope.LOCAL_UNBOUNDED,
|
||||||
|
access_grant=AccessGrant.OPEN,
|
||||||
|
liveness=Liveness.STATIC,
|
||||||
|
history=History.NONE,
|
||||||
|
merge_model=MergeModel.NONE,
|
||||||
|
addressing=Addressing.PATH,
|
||||||
|
native_query=NativeQuery.NONE,
|
||||||
|
translation=Translation.NATIVE,
|
||||||
|
supported_verbs=frozenset({Verb.READ}),
|
||||||
|
)
|
||||||
|
base.update(over)
|
||||||
|
return CapabilityProfile(**base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_identity_is_stable_across_content_and_value_equal():
|
||||||
|
a = Identity("shardA", "Home")
|
||||||
|
b = Identity("shardA", "Home")
|
||||||
|
assert a == b and hash(a) == hash(b)
|
||||||
|
assert str(a) == "shardA:Home"
|
||||||
|
# Identity does not depend on content: a Page edit reuses the same identity.
|
||||||
|
env = ProvenanceEnvelope(source_shard="shardA")
|
||||||
|
p1 = Page(a, "first body", env)
|
||||||
|
p2 = Page(a, "edited body", env)
|
||||||
|
assert p1.identity == p2.identity # stable handle, not a fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_only_folder_profile_validates():
|
||||||
|
assert _read_only_folder_profile().validate().supports(Verb.READ)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_read_is_rejected():
|
||||||
|
with pytest.raises(ProfileError, match="READ"):
|
||||||
|
_read_only_folder_profile(supported_verbs=frozenset()).validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_git_is_store_requires_git_substrate_and_history():
|
||||||
|
with pytest.raises(ProfileError, match="git-is-store"):
|
||||||
|
_read_only_folder_profile(attachment_mode=AttachmentMode.GIT_IS_STORE).validate()
|
||||||
|
# consistent git-is-store profile validates
|
||||||
|
_read_only_folder_profile(
|
||||||
|
attachment_mode=AttachmentMode.GIT_IS_STORE,
|
||||||
|
substrate=Substrate.GIT,
|
||||||
|
history=History.GIT_NATIVE,
|
||||||
|
).validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypted_forbids_native_query():
|
||||||
|
with pytest.raises(ProfileError, match="encrypted"):
|
||||||
|
_read_only_folder_profile(
|
||||||
|
content_opacity=ContentOpacity.ENCRYPTED, native_query=NativeQuery.DB_QUERY
|
||||||
|
).validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_verb_requires_write_granularity():
|
||||||
|
with pytest.raises(ProfileError, match="read-only consistency"):
|
||||||
|
_read_only_folder_profile(supported_verbs=frozenset({Verb.READ, Verb.WRITE})).validate()
|
||||||
|
# writable profile is consistent
|
||||||
|
_read_only_folder_profile(
|
||||||
|
write_granularity=WriteGranularity.PER_PAGE,
|
||||||
|
supported_verbs=frozenset({Verb.READ, Verb.WRITE}),
|
||||||
|
).validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_crdt_merge_requires_crdt_log_and_realtime():
|
||||||
|
with pytest.raises(ProfileError, match="native-crdt"):
|
||||||
|
_read_only_folder_profile(
|
||||||
|
merge_model=MergeModel.NATIVE_CRDT,
|
||||||
|
supported_verbs=frozenset({Verb.READ, Verb.MERGE}),
|
||||||
|
).validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_page_defaults_to_prose():
|
||||||
|
p = Page(Identity("s", "k"), "x", ProvenanceEnvelope(source_shard="s"))
|
||||||
|
assert p.shape is PageShape.PROSE
|
||||||
@@ -56,7 +56,7 @@ delta). Frozen dataclasses, no tree deps. Tests: ⊕ identity (no delta), overri
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: SHARD-WP-0007-T2
|
id: SHARD-WP-0007-T2
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "780ad01f-c3e1-4b49-9ae9-60e0324178a7"
|
state_hub_task_id: "780ad01f-c3e1-4b49-9ae9-60e0324178a7"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user