diff --git a/SCOPE.md b/SCOPE.md index eca25e4..af41c03 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -17,7 +17,7 @@ Learnings update both SCOPE and INTENT where necessary. | Layer | State | |-------|-------| -| Code | Foundation slice implemented (SHARD-WP-0007): `provenance` + `policy` leaves, `model` (Identity/Placement/Span/Page/CapabilityProfile), `adapters` (contract + FolderAdapter + conformance suite), `coordination` (event-sourced DecisionLog), `union` (resolution + chorus, overlay-aware), `InformationSpace` orchestrator. Write path added (SHARD-WP-0008): writable adapter, overlay engine (draft→patch→apply-under-drift), edit() unifies write-through + overlay-before-mutation. attach→resolve→read + edit/overlay/apply work; 64 tests green | +| Code | Foundation slice implemented (SHARD-WP-0007): `provenance` + `policy` leaves, `model` (Identity/Placement/Span/Page/CapabilityProfile), `adapters` (contract + FolderAdapter + conformance suite), `coordination` (event-sourced DecisionLog), `union` (resolution + chorus, overlay-aware), `InformationSpace` orchestrator. Write path added (SHARD-WP-0008): writable adapter, overlay engine (draft→patch→apply-under-drift), edit() unifies write-through + overlay-before-mutation. Native engine implemented (SHARD-WP-0014): `engine` (kernel + typed-extension runtime + per-shard activation [ADR-0001] + capability-profile-from-extensions + EngineShardAdapter + the `ext.struct` built-in) — an engine shard attaches to an InformationSpace as a canonical-mode shard. 107 tests green, ~97% coverage | | Intent | `INTENT.md` established; authorization-in-core amendments drafted | | Research | yawex prior art; c2 origins; federation concepts; wikiengines overview (`research/260608-*/`); XWiki/TWiki/Foswiki deep dives (`research/260613-*/`); Xanadu + ZigZag + Roam + Obsidian + Notion + Joplin + Logseq + local-first workspaces (Anytype/AFFiNE/AppFlowy) + Trilium + Wiki.js + Federated Wiki + Wikibase + git-forge wikis + TiddlyWiki + ikiwiki + Quip + MojoMojo + Oddmuse + UseModWiki deep dives & shard-spectrum synthesis (`research/260614-*/`) | | Demand | NetKingdom integration asks captured, not yet negotiated | diff --git a/src/shard_wiki/engine/extensions/__init__.py b/src/shard_wiki/engine/extensions/__init__.py new file mode 100644 index 0000000..4afae81 --- /dev/null +++ b/src/shard_wiki/engine/extensions/__init__.py @@ -0,0 +1,10 @@ +"""engine/extensions/ — built-in typed extensions for the wiki engine. + +Each is a typed :class:`~shard_wiki.engine.extension.Extension` a shard activates only if needed. +``ext.struct`` (typed records) is the first; more (views, addressing, computational, authz) follow +the same pattern. +""" + +from shard_wiki.engine.extensions.struct import StructExt, parse_frontmatter + +__all__ = ["StructExt", "parse_frontmatter"] diff --git a/src/shard_wiki/engine/extensions/struct.py b/src/shard_wiki/engine/extensions/struct.py new file mode 100644 index 0000000..c8e98a2 --- /dev/null +++ b/src/shard_wiki/engine/extensions/struct.py @@ -0,0 +1,81 @@ +"""ext.struct — typed records, a first built-in extension (WikiEngineCoreArchitecture X-STRUCT). + +Demonstrates the typed-extension framework end-to-end. A page may carry a leading in-text +frontmatter block (`---` … `---`, `key: value` lines — git-diffable structure, blueprint T12). +With this extension **active**, the engine: + +- **ON_WRITE** validates the structured block (optionally against an allowed-field set) — a + malformed/disallowed structured page is rejected; the body is otherwise unchanged + (content-preserving, so write conformance holds); +- **ON_READ** tags such pages as `PageShape.TYPED_RECORD`; +- **ON_PROFILE** raises the shard's profile with the `structured-payload` verb (E-5). + +With the extension **inactive**, the kernel treats the same page as opaque prose — the feature +is genuinely absent (honest profile). This is "activate only what you need" in action. +""" + +from __future__ import annotations + +import dataclasses +from collections.abc import Iterable, Mapping +from typing import Any + +from shard_wiki.engine.extension import Extension, Hook +from shard_wiki.engine.profile import ProfileContribution +from shard_wiki.model import Page, PageShape, Verb + +__all__ = ["StructExt", "parse_frontmatter"] + + +def parse_frontmatter(body: str) -> tuple[dict[str, str], bool]: + """Parse a leading ``---`` … ``---`` block of ``key: value`` lines. + + Returns ``(fields, has_block)``. An unterminated opening ``---`` is *not* a valid block. + """ + lines = body.splitlines() + if not lines or lines[0].strip() != "---": + return {}, False + fields: dict[str, str] = {} + for line in lines[1:]: + if line.strip() == "---": + return fields, True + if ":" in line: + key, _, value = line.partition(":") + fields[key.strip()] = value.strip() + return {}, False # no closing fence → not a frontmatter block + + +class StructExt(Extension): + id = "ext.struct" + declares_types = ("record",) + provides = ("capability.wiki.page-model",) + + def __init__(self, allowed_fields: Iterable[str] | None = None) -> None: + self._allowed: set[str] | None = set(allowed_fields) if allowed_fields is not None else None + + def hooks(self) -> Mapping[Hook, Any]: + return { + Hook.ON_WRITE: self._on_write, + Hook.ON_READ: self._on_read, + Hook.ON_PROFILE: self._on_profile, + } + + def _on_write(self, body: str, ctx: Any) -> str: + fields, has_block = parse_frontmatter(body) + if has_block and self._allowed is not None: + disallowed = set(fields) - self._allowed + if disallowed: + raise ValueError(f"ext.struct: disallowed fields {sorted(disallowed)}") + return body # structure stays in-text (git-diffable); body unchanged + + def _on_read(self, page: Page, ctx: Any) -> Page: + _, has_block = parse_frontmatter(page.body) + return dataclasses.replace(page, shape=PageShape.TYPED_RECORD) if has_block else page + + def _on_profile(self, payload: Any, ctx: Any) -> ProfileContribution: + return ProfileContribution(verbs_add=frozenset({Verb.STRUCTURED_PAYLOAD})) + + @staticmethod + def fields(body: str) -> dict[str, str]: + """Parsed structured fields of a page body (empty if it has no frontmatter block).""" + return parse_frontmatter(body)[0] diff --git a/tests/test_ext_struct.py b/tests/test_ext_struct.py new file mode 100644 index 0000000..5e1ca89 --- /dev/null +++ b/tests/test_ext_struct.py @@ -0,0 +1,64 @@ +"""Tests for the ext.struct built-in extension (SHARD-WP-0014 T6).""" + +import pytest + +from shard_wiki import InformationSpace +from shard_wiki.adapters import assert_conformant +from shard_wiki.engine import ExtensionRuntime, build_engine_shard +from shard_wiki.engine.extensions import StructExt, parse_frontmatter +from shard_wiki.model import PageShape, Verb + +_STRUCT_PAGE = "---\ntitle: Spec\nstatus: draft\n---\nbody text" + + +def test_parse_frontmatter(): + fields, has = parse_frontmatter(_STRUCT_PAGE) + assert has and fields == {"title": "Spec", "status": "draft"} + assert parse_frontmatter("just prose") == ({}, False) + assert parse_frontmatter("---\nunterminated") == ({}, False) + + +def _runtime(allowed=None): + rt = ExtensionRuntime() + rt.register(StructExt(allowed_fields=allowed)) + return rt + + +def test_feature_absent_when_extension_off(): + shard = build_engine_shard("off", ExtensionRuntime(), activate=set()) + shard.write("Spec", _STRUCT_PAGE) + assert shard.read("Spec").shape is PageShape.PROSE # kernel: opaque prose + assert not shard.profile().supports(Verb.STRUCTURED_PAYLOAD) # honest absence + + +def test_feature_present_when_extension_on(): + shard = build_engine_shard("on", _runtime(), activate={"ext.struct"}) + shard.write("Spec", _STRUCT_PAGE) + assert shard.read("Spec").shape is PageShape.TYPED_RECORD # tagged by ext.struct + assert shard.read("Spec").body.endswith("body text") # content preserved (in-text) + assert shard.profile().supports(Verb.STRUCTURED_PAYLOAD) # profile reflects activation (E-5) + assert_conformant(shard) # still conformant + + +def test_plain_page_is_not_tagged_even_when_on(): + shard = build_engine_shard("on", _runtime(), activate={"ext.struct"}) + shard.write("Plain", "no frontmatter here") + assert shard.read("Plain").shape is PageShape.PROSE + + +def test_allowed_fields_validation_rejects_disallowed(): + shard = build_engine_shard("v", _runtime(allowed={"title"}), activate={"ext.struct"}) + with pytest.raises(ValueError, match="disallowed fields"): + shard.write("Bad", "---\ntitle: ok\nsecret: no\n---\nx") + shard.write("Good", "---\ntitle: ok\n---\nx") # allowed field passes + assert shard.read("Good").shape is PageShape.TYPED_RECORD + + +def test_through_information_space_edit(): + space = InformationSpace("team") + space.attach(build_engine_shard("wikiE", _runtime(), activate={"ext.struct"})) + space.union.shard("wikiE").write("Doc", "---\ntitle: T\n---\nv1") + res = space.edit("Doc", "---\ntitle: T2\n---\nv2") # overlay→apply→write-through + assert res.status.value == "applied" + page = space.read("Doc") + assert page.shape is PageShape.TYPED_RECORD and "v2" in page.body diff --git a/workplans/SHARD-WP-0014-engine-implementation.md b/workplans/SHARD-WP-0014-engine-implementation.md index dc1e73d..d354f96 100644 --- a/workplans/SHARD-WP-0014-engine-implementation.md +++ b/workplans/SHARD-WP-0014-engine-implementation.md @@ -4,7 +4,7 @@ type: workplan title: "wiki-engine implementation — kernel + typed-extension runtime + activation" domain: whynot repo: shard-wiki -status: active +status: done owner: tegwick topic_slug: whynot created: "2026-06-15" @@ -124,7 +124,7 @@ integration: engine shard passes `assert_conformant`; attach → resolve → edi ```task id: SHARD-WP-0014-T6 -status: todo +status: done priority: medium state_hub_task_id: "b88d1640-9afa-4957-aec3-a7264b09494c" ```