feat(engine): page-store kernel skeleton (WP-0014 T1)

engine/ package: EngineKernel (in-process page store with per-page version
history; create/edit-as-version, recoverable delete-tombstone, keys, current_rev)
+ wikilink extraction + in-shard link resolution / red-link detection (EC-1..EC-4).
Reuses model/provenance; git-IS-store backing slots in later. 6 tests green,
pyflakes clean, full suite green. Marks T1 done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:57:31 +02:00
parent e62560eb5a
commit 2100e956aa
5 changed files with 190 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
"""engine/ — shard-wiki's native, headless wiki engine (a canonical-mode shard backend).
A small page-store kernel + a typed-extension runtime (WikiEngineCoreArchitecture). The engine
is *one shard*: it is consumed by the orchestrator only via its `EngineShardAdapter`; it never
imports the derived tier (`union`/`projection`).
"""
from shard_wiki.engine.kernel import EngineKernel
from shard_wiki.engine.links import extract_wikilinks
__all__ = ["EngineKernel", "extract_wikilinks"]

View File

@@ -0,0 +1,87 @@
"""Engine kernel — the small page-store core (WikiEngineCoreArchitecture §3, EC-1…EC-4).
The irreducible engine: author/read/edit pages (edit = a new version; delete = a recoverable
tombstone — history is the floor, I-10), enumerate keys, and resolve `[[wikilinks]]` (red-link =
an unresolved target). No feature beyond this c2-minimum lives in the kernel; everything else is
a typed extension (E-3).
Storage is intentionally simple here (in-memory version history); the git-IS-store backing
(SHARD-WP-0009/0012) slots in behind the same API later. The kernel reuses the page model and
provenance leaf; it does not redefine them.
"""
from __future__ import annotations
from collections.abc import Iterable
from datetime import datetime, timezone
from shard_wiki.engine.links import extract_wikilinks
from shard_wiki.model import Identity, Page, Placement
from shard_wiki.provenance import Liveness, ProvenanceEnvelope, Staleness
__all__ = ["EngineKernel"]
class EngineKernel:
"""An in-process page store with per-page version history for one engine shard."""
def __init__(self, shard_id: str) -> None:
self.shard_id = shard_id
self._versions: dict[str, list[Page]] = {}
self._deleted: set[str] = set()
# --- write path (create/edit are one operation; both append a version) ---
def write(self, key: str, body: str) -> Page:
versions = self._versions.setdefault(key, [])
rev = str(len(versions) + 1)
page = Page(
identity=Identity(self.shard_id, key),
body=body,
envelope=ProvenanceEnvelope(
source_shard=self.shard_id,
liveness=Liveness.STATIC,
staleness=Staleness.FRESH,
source_rev=rev,
observed_at=datetime.now(tz=timezone.utc),
),
placements=(Placement(self.shard_id, key),),
)
versions.append(page)
self._deleted.discard(key)
return page
# --- read path ---
def exists(self, key: str) -> bool:
return key in self._versions and key not in self._deleted
def read(self, key: str) -> Page:
"""Latest version of a live page. Raises ``KeyError`` if absent or deleted."""
if not self.exists(key):
raise KeyError(key)
return self._versions[key][-1]
def keys(self) -> Iterable[str]:
return (k for k in sorted(self._versions) if k not in self._deleted)
def current_rev(self, key: str) -> str | None:
return self._versions[key][-1].envelope.source_rev if self.exists(key) else None
# --- history & recoverability (I-10): versions are retained across delete ---
def history(self, key: str) -> tuple[Page, ...]:
"""All versions ever written for ``key`` (oldest→newest), even after delete."""
return tuple(self._versions.get(key, ()))
def delete(self, key: str) -> None:
"""Tombstone a page (history retained; restore by writing again)."""
if key not in self._versions:
raise KeyError(key)
self._deleted.add(key)
# --- links (EC-4): resolution + red-link detection within this shard ---
def links(self, key: str) -> list[str]:
"""Wikilink targets in a page's current body."""
return extract_wikilinks(self.read(key).body)
def resolve_link(self, target: str) -> Identity | None:
"""Resolve a wikilink target to a live page identity, or ``None`` (a **red-link**)."""
return self.read(target).identity if self.exists(target) else None

View File

@@ -0,0 +1,25 @@
"""Wikilink extraction — the kernel's link primitive (WikiEngineCoreArchitecture EC-4).
`[[Target]]` and `[[Target|label]]`. CamelCase auto-linking is intentionally NOT here (it is an
opt-in concern per FederationRequirements ADR-06); the kernel only knows explicit wikilinks.
Link *resolution* (and red-link detection) is the kernel's job (it knows which keys exist);
*rendering* is a consumer concern (headless engine, no UI).
"""
from __future__ import annotations
import re
__all__ = ["extract_wikilinks"]
_WIKILINK = re.compile(r"\[\[([^\]|]+?)(?:\|[^\]]*)?\]\]")
def extract_wikilinks(body: str) -> list[str]:
"""Return the ordered, de-duplicated wikilink targets in ``body`` (label part dropped)."""
seen: dict[str, None] = {}
for m in _WIKILINK.finditer(body):
target = m.group(1).strip()
if target:
seen.setdefault(target, None)
return list(seen)

View File

@@ -0,0 +1,59 @@
"""Tests for the engine kernel (SHARD-WP-0014 T1)."""
import pytest
from shard_wiki.engine import EngineKernel, extract_wikilinks
from shard_wiki.model import Identity
def test_write_creates_then_edits_as_history():
k = EngineKernel("eng")
p1 = k.write("Home", "first")
assert p1.identity == Identity("eng", "Home")
assert p1.envelope.source_rev == "1"
p2 = k.write("Home", "second")
assert p2.envelope.source_rev == "2"
assert k.read("Home").body == "second" # latest
assert [v.body for v in k.history("Home")] == ["first", "second"] # recoverable history
def test_read_missing_raises():
k = EngineKernel("eng")
with pytest.raises(KeyError):
k.read("Nope")
def test_delete_is_recoverable():
k = EngineKernel("eng")
k.write("Doc", "v1")
k.delete("Doc")
assert not k.exists("Doc")
with pytest.raises(KeyError):
k.read("Doc")
assert [v.body for v in k.history("Doc")] == ["v1"] # history retained
k.write("Doc", "v2") # restore by writing
assert k.exists("Doc") and k.read("Doc").body == "v2"
def test_keys_and_current_rev():
k = EngineKernel("eng")
k.write("A", "a")
k.write("B", "b")
k.write("A", "a2")
assert set(k.keys()) == {"A", "B"}
assert k.current_rev("A") == "2"
assert k.current_rev("Missing") is None
def test_links_and_red_link_resolution():
k = EngineKernel("eng")
k.write("Home", "see [[Target]] and [[Other|labelled]] and [[Target]] again")
k.write("Target", "exists")
assert k.links("Home") == ["Target", "Other"] # ordered, de-duped, label dropped
assert k.resolve_link("Target") == Identity("eng", "Target")
assert k.resolve_link("Other") is None # red-link (not yet created)
def test_extract_wikilinks_helper():
assert extract_wikilinks("none here") == []
assert extract_wikilinks("[[A]] [[B|x]] [[A]]") == ["A", "B"]

View File

@@ -12,6 +12,7 @@ updated: "2026-06-15"
depends_on:
- SHARD-WP-0007
- SHARD-WP-0013
state_hub_workstream_id: "bfce1644-d93d-44c7-af2c-6b0cb50cedd4"
---
# SHARD-WP-0014 — Wiki-engine implementation
@@ -47,8 +48,9 @@ computational extensions; the full feature-control control plane. Build the fram
```task
id: SHARD-WP-0014-T1
status: todo
status: done
priority: high
state_hub_task_id: "e81ba881-7e92-4581-99ff-b12ad2bcabb3"
```
`src/shard_wiki/engine/kernel.py`: the minimal kernel — a page store + lifecycle over existing
@@ -63,6 +65,7 @@ helper). No extensions yet. Tests: page CRUD-as-history; kernel-only shard works
id: SHARD-WP-0014-T2
status: todo
priority: high
state_hub_task_id: "8ae8e58a-f081-432b-b2c5-b6435fbf3843"
```
`src/shard_wiki/engine/extension.py`: the `Extension` contract (id, provides, types, hooks,
@@ -79,6 +82,7 @@ conformance catches a lying extension.
id: SHARD-WP-0014-T3
status: todo
priority: high
state_hub_task_id: "c4fe9df4-e6a8-4b7d-891b-59ceec6aebac"
```
`src/shard_wiki/engine/activation.py` (ADR-0001): resolve a shard's **activation profile**
@@ -94,6 +98,7 @@ subset; absent-provider falls back to defaults; context scoping works.
id: SHARD-WP-0014-T4
status: todo
priority: high
state_hub_task_id: "15fc8db7-cd80-4675-b387-81aa9bc7d308"
```
`src/shard_wiki/engine/profile.py`: fold the active extensions' `on_profile` contributions into a
@@ -107,6 +112,7 @@ profile; the derived profile is valid and conformance-passes.
id: SHARD-WP-0014-T5
status: todo
priority: high
state_hub_task_id: "2fbf498c-efe9-400a-8a13-7f1b521b3534"
```
`src/shard_wiki/engine/adapter.py`: `EngineShardAdapter` implements `adapters.ShardAdapter`,
@@ -120,6 +126,7 @@ integration: engine shard passes `assert_conformant`; attach → resolve → edi
id: SHARD-WP-0014-T6
status: todo
priority: medium
state_hub_task_id: "b88d1640-9afa-4957-aec3-a7264b09494c"
```
Implement one real extension end-to-end — **`ext.views` (BackLinks)** or **`ext.struct`