From ff96ee0c48789eee9773c923b84a53e75979944f Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 16 Jun 2026 00:36:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(engine):=20EngineShardAdapter=20=E2=80=94?= =?UTF-8?q?=20engine=20as=20a=20canonical-mode=20shard=20(WP-0014=20T5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engine/adapter.py: EngineShardAdapter implements adapters.ShardAdapter (read/write run extension transform hooks; profile derived from active extensions, E-5; current_rev for apply-under-drift) + build_engine_shard() helper (explicit ids or activation provider). runtime.available() added. Engine shard passes assert_conformant and attaches to an InformationSpace — resolve + edit (overlay->apply->write-through) work, and the declared profile reflects the active extensions. 5 tests green, pyflakes clean. Co-Authored-By: Claude Opus 4.8 --- src/shard_wiki/engine/__init__.py | 3 + src/shard_wiki/engine/adapter.py | 82 +++++++++++++++++++ src/shard_wiki/engine/extension.py | 4 + tests/test_engine_adapter.py | 80 ++++++++++++++++++ .../SHARD-WP-0014-engine-implementation.md | 2 +- 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/shard_wiki/engine/adapter.py create mode 100644 tests/test_engine_adapter.py diff --git a/src/shard_wiki/engine/__init__.py b/src/shard_wiki/engine/__init__.py index 41f8861..6a7eb17 100644 --- a/src/shard_wiki/engine/__init__.py +++ b/src/shard_wiki/engine/__init__.py @@ -19,6 +19,7 @@ from shard_wiki.engine.extension import ( ExtensionRuntime, Hook, ) +from shard_wiki.engine.adapter import EngineShardAdapter, build_engine_shard from shard_wiki.engine.kernel import EngineKernel from shard_wiki.engine.links import extract_wikilinks from shard_wiki.engine.profile import ( @@ -43,4 +44,6 @@ __all__ = [ "engine_base_profile", "ProfileContribution", "derive_profile", + "EngineShardAdapter", + "build_engine_shard", ] diff --git a/src/shard_wiki/engine/adapter.py b/src/shard_wiki/engine/adapter.py new file mode 100644 index 0000000..2f35a77 --- /dev/null +++ b/src/shard_wiki/engine/adapter.py @@ -0,0 +1,82 @@ +"""EngineShardAdapter — the engine exposed as a canonical-mode shard (WikiEngineCoreArchitecture +§6, E-1/E-5). + +The engine is *one shard*: the orchestrator consumes it only through this `ShardAdapter`. The +adapter is backed by the kernel (T1) + a composed extension set (T2/T3); its §A capability +profile is **derived from the active extensions** (T4), so the orchestrator sees an honest, +conformance-verifiable profile that reflects exactly what is activated. Read/write run the +extensions' transform hooks; everything above this stays in the orchestrator (no union/projection +import). +""" + +from __future__ import annotations + +from collections.abc import Iterable + +from shard_wiki.adapters import ShardAdapter +from shard_wiki.engine.activation import ActivationContext, ActivationProvider, ActivationResolver +from shard_wiki.engine.extension import ActiveExtensions, ExtensionRuntime, Hook +from shard_wiki.engine.kernel import EngineKernel +from shard_wiki.engine.profile import derive_profile +from shard_wiki.model import CapabilityProfile, NotSupported, Page, Verb + +__all__ = ["EngineShardAdapter", "build_engine_shard"] + + +class EngineShardAdapter(ShardAdapter): + def __init__( + self, + kernel: EngineKernel, + active: ActiveExtensions, + base_profile: CapabilityProfile | None = None, + ) -> None: + self._kernel = kernel + self._active = active + self._profile = derive_profile(active, base_profile) # validated (E-5) + + @property + def shard_id(self) -> str: + return self._kernel.shard_id + + def profile(self) -> CapabilityProfile: + return self._profile + + def keys(self) -> Iterable[str]: + return self._kernel.keys() + + def read(self, key: str) -> Page: + page = self._kernel.read(key) + return self._active.dispatch_transform(Hook.ON_READ, page, {"shard_id": self.shard_id}) + + def current_rev(self, key: str) -> str | None: + return self._kernel.current_rev(key) + + def write(self, key: str, body: str) -> Page: + if not self._profile.supports(Verb.WRITE): + raise NotSupported(f"{type(self).__name__} ({self.shard_id}) is read-only") + body = self._active.dispatch_transform( + Hook.ON_WRITE, body, {"shard_id": self.shard_id, "key": key} + ) + return self._kernel.write(key, body) + + +def build_engine_shard( + shard_id: str, + runtime: ExtensionRuntime, + *, + activate: Iterable[str] | None = None, + provider: ActivationProvider | None = None, + context: ActivationContext | None = None, + base_profile: CapabilityProfile | None = None, +) -> EngineShardAdapter: + """Stand up an engine shard: resolve which extensions are active (explicit ``activate`` ids, + or via an activation ``provider`` over the runtime's available set), compose them, and wrap a + fresh kernel as a `ShardAdapter`. + """ + if provider is not None: + ctx = context or ActivationContext(shard_id) + ids = ActivationResolver(provider).active_extensions(runtime.available(), ctx) + else: + ids = set(activate or ()) + active = runtime.activate(ids) + return EngineShardAdapter(EngineKernel(shard_id), active, base_profile) diff --git a/src/shard_wiki/engine/extension.py b/src/shard_wiki/engine/extension.py index ed719b8..f8b1590 100644 --- a/src/shard_wiki/engine/extension.py +++ b/src/shard_wiki/engine/extension.py @@ -86,6 +86,10 @@ class ExtensionRuntime: def __init__(self) -> None: self._registered: dict[str, Extension] = {} + def available(self) -> frozenset[str]: + """Ids of all registered extensions (the candidate set for activation).""" + return frozenset(self._registered) + def register(self, ext: Extension) -> Extension: """Register an extension after structural verification (mirrors §6.6).""" if not ext.id or not ext.id.startswith("ext."): diff --git a/tests/test_engine_adapter.py b/tests/test_engine_adapter.py new file mode 100644 index 0000000..048e612 --- /dev/null +++ b/tests/test_engine_adapter.py @@ -0,0 +1,80 @@ +"""Tests for EngineShardAdapter (SHARD-WP-0014 T5): engine as a canonical-mode shard.""" + +from shard_wiki import InformationSpace +from shard_wiki.adapters import assert_conformant +from shard_wiki.engine import ( + Extension, + ExtensionRuntime, + Hook, + ProfileContribution, + StaticProvider, + build_engine_shard, +) +from shard_wiki.engine.activation import ActivationContext +from shard_wiki.model import Verb + + +class StructProfileExt(Extension): + """Profile-only extension (no body transform → write stays content-preserving).""" + + id = "ext.struct" + + def hooks(self): + return { + Hook.ON_PROFILE: lambda p, c: ProfileContribution( + verbs_add=frozenset({Verb.STRUCTURED_PAYLOAD}) + ) + } + + +def _runtime(): + rt = ExtensionRuntime() + rt.register(StructProfileExt()) + return rt + + +def test_kernel_only_engine_shard_is_conformant(): + shard = build_engine_shard("eng", ExtensionRuntime(), activate=set()) + shard.write("Home", "hi") + report = assert_conformant(shard) # read + positive write probe + assert report.ok + assert shard.profile().supports(Verb.WRITE) + assert not shard.profile().supports(Verb.STRUCTURED_PAYLOAD) + + +def test_profile_reflects_activated_extensions(): + off = build_engine_shard("a", _runtime(), activate=set()) + on = build_engine_shard("b", _runtime(), activate={"ext.struct"}) + assert not off.profile().supports(Verb.STRUCTURED_PAYLOAD) + assert on.profile().supports(Verb.STRUCTURED_PAYLOAD) # E-5 + assert_conformant(on) + + +def test_activation_via_provider(): + provider = StaticProvider(flags={"ext.struct": True}) + shard = build_engine_shard("c", _runtime(), provider=provider, context=ActivationContext("c")) + assert shard.profile().supports(Verb.STRUCTURED_PAYLOAD) + + +def test_attach_resolve_edit_through_engine_shard(tmp_path): + space = InformationSpace("team") + space.attach(build_engine_shard("wikiE", ExtensionRuntime(), activate=set())) + # seed a page directly via the shard, then read + edit through the orchestrator + space.union.shard("wikiE").write("Home", "v1") + assert space.read("Home").body == "v1" + result = space.edit("Home", "v2") # overlay -> apply-under-drift -> write-through + assert result.status.value == "applied" + assert space.read("Home").body == "v2" + + +def test_on_write_transform_runs(): + class Upper(Extension): + id = "ext.upper" + def hooks(self): + return {Hook.ON_WRITE: lambda body, ctx: body.upper()} + + rt = ExtensionRuntime() + rt.register(Upper()) + shard = build_engine_shard("u", rt, activate={"ext.upper"}) + shard.write("P", "quiet") + assert shard.read("P").body == "QUIET" # extension transformed the write diff --git a/workplans/SHARD-WP-0014-engine-implementation.md b/workplans/SHARD-WP-0014-engine-implementation.md index a3211e1..dc1e73d 100644 --- a/workplans/SHARD-WP-0014-engine-implementation.md +++ b/workplans/SHARD-WP-0014-engine-implementation.md @@ -110,7 +110,7 @@ profile; the derived profile is valid and conformance-passes. ```task id: SHARD-WP-0014-T5 -status: todo +status: done priority: high state_hub_task_id: "2fbf498c-efe9-400a-8a13-7f1b521b3534" ```