generated from coulomb/repo-seed
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:
11
src/shard_wiki/engine/__init__.py
Normal file
11
src/shard_wiki/engine/__init__.py
Normal 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"]
|
||||
87
src/shard_wiki/engine/kernel.py
Normal file
87
src/shard_wiki/engine/kernel.py
Normal 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
|
||||
25
src/shard_wiki/engine/links.py
Normal file
25
src/shard_wiki/engine/links.py
Normal 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)
|
||||
59
tests/test_engine_kernel.py
Normal file
59
tests/test_engine_kernel.py
Normal 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"]
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user