generated from coulomb/repo-seed
feat(engine): capability profile derived from active extensions (WP-0014 T4, E-5)
engine/profile.py: engine_base_profile() (kernel-only c2-minimum profile), ProfileContribution (an extension's ON_PROFILE contribution: axis raises + verbs), derive_profile() folds active extensions' contributions onto the base in deterministic order then validate() — so configuration->capability is one chain and composition can never yield an impossible profile (encrypted+native-query rejected). 5 tests green, 96 total, pyflakes clean. Marks T4 done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@ from shard_wiki.engine.extension import (
|
|||||||
)
|
)
|
||||||
from shard_wiki.engine.kernel import EngineKernel
|
from shard_wiki.engine.kernel import EngineKernel
|
||||||
from shard_wiki.engine.links import extract_wikilinks
|
from shard_wiki.engine.links import extract_wikilinks
|
||||||
|
from shard_wiki.engine.profile import (
|
||||||
|
ProfileContribution,
|
||||||
|
derive_profile,
|
||||||
|
engine_base_profile,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"EngineKernel",
|
"EngineKernel",
|
||||||
@@ -35,4 +40,7 @@ __all__ = [
|
|||||||
"StaticProvider",
|
"StaticProvider",
|
||||||
"ActivationResolver",
|
"ActivationResolver",
|
||||||
"feature_control_provider",
|
"feature_control_provider",
|
||||||
|
"engine_base_profile",
|
||||||
|
"ProfileContribution",
|
||||||
|
"derive_profile",
|
||||||
]
|
]
|
||||||
|
|||||||
112
src/shard_wiki/engine/profile.py
Normal file
112
src/shard_wiki/engine/profile.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""Capability profile derived from active extensions (WikiEngineCoreArchitecture E-5).
|
||||||
|
|
||||||
|
The engine's §A `CapabilityProfile` is **computed**, not hand-set: start from the kernel base
|
||||||
|
profile, then fold each active extension's `on_profile` contribution (in the runtime's
|
||||||
|
deterministic order), then `validate()`. This realizes the chain *configuration (which
|
||||||
|
extensions) → capability (the profile) → conformance* — activating an extension raises the
|
||||||
|
shard's advertised capabilities, and composition can never yield an impossible profile (validate
|
||||||
|
rejects it, §6.5).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from shard_wiki.engine.extension import ActiveExtensions, Hook
|
||||||
|
from shard_wiki.model import (
|
||||||
|
AccessGrant,
|
||||||
|
Addressing,
|
||||||
|
AttachmentMode,
|
||||||
|
CapabilityProfile,
|
||||||
|
ContentOpacity,
|
||||||
|
History,
|
||||||
|
MergeModel,
|
||||||
|
NativeQuery,
|
||||||
|
OperationalEnvelope,
|
||||||
|
Substrate,
|
||||||
|
Translation,
|
||||||
|
Verb,
|
||||||
|
WriteGranularity,
|
||||||
|
)
|
||||||
|
from shard_wiki.provenance import Liveness
|
||||||
|
|
||||||
|
__all__ = ["engine_base_profile", "ProfileContribution", "derive_profile"]
|
||||||
|
|
||||||
|
# Profile fields an extension may *raise* via on_profile (substrate/attachment are kernel-fixed).
|
||||||
|
_OVERRIDABLE = (
|
||||||
|
"write_granularity",
|
||||||
|
"content_opacity",
|
||||||
|
"liveness",
|
||||||
|
"history",
|
||||||
|
"merge_model",
|
||||||
|
"addressing",
|
||||||
|
"native_query",
|
||||||
|
"translation",
|
||||||
|
"access_grant",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def engine_base_profile() -> CapabilityProfile:
|
||||||
|
"""The kernel-only (no extensions) capability profile — the c2-minimum engine shard."""
|
||||||
|
return CapabilityProfile(
|
||||||
|
substrate=Substrate.FILES,
|
||||||
|
attachment_mode=AttachmentMode.IN_ENGINE_HOST,
|
||||||
|
write_granularity=WriteGranularity.PER_PAGE,
|
||||||
|
content_opacity=ContentOpacity.TRANSPARENT,
|
||||||
|
operational_envelope=OperationalEnvelope.LOCAL_UNBOUNDED,
|
||||||
|
access_grant=AccessGrant.OPEN,
|
||||||
|
liveness=Liveness.STATIC,
|
||||||
|
history=History.INTERNAL_ONLY,
|
||||||
|
merge_model=MergeModel.NONE,
|
||||||
|
addressing=Addressing.PATH,
|
||||||
|
native_query=NativeQuery.NONE,
|
||||||
|
translation=Translation.NATIVE,
|
||||||
|
supported_verbs=frozenset({Verb.READ, Verb.WRITE}),
|
||||||
|
).validate()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ProfileContribution:
|
||||||
|
"""An extension's contribution to the derived profile (returned from its ON_PROFILE hook).
|
||||||
|
|
||||||
|
A non-``None`` axis overrides that axis; ``verbs_add`` are unioned in. Order = the runtime's
|
||||||
|
deterministic dispatch order, so later extensions win on a contested axis."""
|
||||||
|
|
||||||
|
write_granularity: WriteGranularity | None = None
|
||||||
|
content_opacity: ContentOpacity | None = None
|
||||||
|
liveness: Liveness | None = None
|
||||||
|
history: History | None = None
|
||||||
|
merge_model: MergeModel | None = None
|
||||||
|
addressing: Addressing | None = None
|
||||||
|
native_query: NativeQuery | None = None
|
||||||
|
translation: Translation | None = None
|
||||||
|
access_grant: AccessGrant | None = None
|
||||||
|
verbs_add: frozenset[Verb] = frozenset()
|
||||||
|
|
||||||
|
|
||||||
|
def derive_profile(
|
||||||
|
active: ActiveExtensions, base: CapabilityProfile | None = None
|
||||||
|
) -> CapabilityProfile:
|
||||||
|
"""Fold active extensions' ON_PROFILE contributions onto ``base`` and validate the result.
|
||||||
|
|
||||||
|
Raises :class:`~shard_wiki.model.ProfileError` if the composed profile is impossible — so an
|
||||||
|
activation set can never advertise an invalid capability profile.
|
||||||
|
"""
|
||||||
|
profile = base or engine_base_profile()
|
||||||
|
contributions = active.dispatch_collect(Hook.ON_PROFILE)
|
||||||
|
|
||||||
|
overrides: dict[str, object] = {}
|
||||||
|
verbs: set[Verb] = set(profile.supported_verbs)
|
||||||
|
for contrib in contributions:
|
||||||
|
if not isinstance(contrib, ProfileContribution):
|
||||||
|
continue
|
||||||
|
for field_name in _OVERRIDABLE:
|
||||||
|
value = getattr(contrib, field_name)
|
||||||
|
if value is not None:
|
||||||
|
overrides[field_name] = value
|
||||||
|
verbs |= set(contrib.verbs_add)
|
||||||
|
|
||||||
|
return dataclasses.replace(
|
||||||
|
profile, supported_verbs=frozenset(verbs), **overrides
|
||||||
|
).validate()
|
||||||
98
tests/test_engine_profile.py
Normal file
98
tests/test_engine_profile.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""Tests for capability-profile-derived-from-extensions (SHARD-WP-0014 T4, E-5)."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from shard_wiki.engine import (
|
||||||
|
Extension,
|
||||||
|
ExtensionRuntime,
|
||||||
|
Hook,
|
||||||
|
ProfileContribution,
|
||||||
|
derive_profile,
|
||||||
|
engine_base_profile,
|
||||||
|
)
|
||||||
|
from shard_wiki.model import (
|
||||||
|
Addressing,
|
||||||
|
ContentOpacity,
|
||||||
|
NativeQuery,
|
||||||
|
ProfileError,
|
||||||
|
Verb,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StructExt(Extension):
|
||||||
|
id = "ext.struct"
|
||||||
|
|
||||||
|
def hooks(self):
|
||||||
|
return {
|
||||||
|
Hook.ON_PROFILE: lambda payload, ctx: ProfileContribution(
|
||||||
|
verbs_add=frozenset({Verb.STRUCTURED_PAYLOAD})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AddrExt(Extension):
|
||||||
|
id = "ext.addr"
|
||||||
|
|
||||||
|
def hooks(self):
|
||||||
|
return {
|
||||||
|
Hook.ON_PROFILE: lambda payload, ctx: ProfileContribution(
|
||||||
|
addressing=Addressing.SPAN, verbs_add=frozenset({Verb.TRANSCLUDE_SOURCE})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptExt(Extension):
|
||||||
|
id = "ext.encrypt"
|
||||||
|
|
||||||
|
def hooks(self):
|
||||||
|
return {Hook.ON_PROFILE: lambda p, c: ProfileContribution(content_opacity=ContentOpacity.ENCRYPTED)}
|
||||||
|
|
||||||
|
|
||||||
|
class QueryExt(Extension):
|
||||||
|
id = "ext.query"
|
||||||
|
|
||||||
|
def hooks(self):
|
||||||
|
return {Hook.ON_PROFILE: lambda p, c: ProfileContribution(native_query=NativeQuery.DB_QUERY)}
|
||||||
|
|
||||||
|
|
||||||
|
def _active(*exts, ids=None):
|
||||||
|
rt = ExtensionRuntime()
|
||||||
|
for e in exts:
|
||||||
|
rt.register(e)
|
||||||
|
return rt.activate(ids if ids is not None else {e.id for e in exts})
|
||||||
|
|
||||||
|
|
||||||
|
def test_base_profile_is_valid_kernel_minimum():
|
||||||
|
p = engine_base_profile()
|
||||||
|
assert p.supports(Verb.READ) and p.supports(Verb.WRITE)
|
||||||
|
assert not p.supports(Verb.STRUCTURED_PAYLOAD)
|
||||||
|
assert p.addressing is Addressing.PATH
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_extensions_yields_base():
|
||||||
|
rt = ExtensionRuntime()
|
||||||
|
active = rt.activate(set())
|
||||||
|
assert derive_profile(active) == engine_base_profile()
|
||||||
|
|
||||||
|
|
||||||
|
def test_activating_extension_raises_the_profile():
|
||||||
|
active = _active(StructExt(), AddrExt())
|
||||||
|
p = derive_profile(active)
|
||||||
|
assert p.supports(Verb.STRUCTURED_PAYLOAD) # from ext.struct
|
||||||
|
assert p.supports(Verb.TRANSCLUDE_SOURCE) # from ext.addr
|
||||||
|
assert p.addressing is Addressing.SPAN # raised by ext.addr
|
||||||
|
assert p.supports(Verb.READ) # base preserved
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_changes_with_active_set():
|
||||||
|
only_struct = derive_profile(_active(StructExt(), AddrExt(), ids={"ext.struct"}))
|
||||||
|
both = derive_profile(_active(StructExt(), AddrExt()))
|
||||||
|
assert not only_struct.supports(Verb.TRANSCLUDE_SOURCE)
|
||||||
|
assert both.supports(Verb.TRANSCLUDE_SOURCE) # E-5: profile reflects what's active
|
||||||
|
|
||||||
|
|
||||||
|
def test_composition_cannot_yield_an_impossible_profile():
|
||||||
|
# encrypted opacity + native query violates §6.5 implication rules -> derive must reject.
|
||||||
|
active = _active(EncryptExt(), QueryExt())
|
||||||
|
with pytest.raises(ProfileError):
|
||||||
|
derive_profile(active)
|
||||||
@@ -96,7 +96,7 @@ subset; absent-provider falls back to defaults; context scoping works.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: SHARD-WP-0014-T4
|
id: SHARD-WP-0014-T4
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "15fc8db7-cd80-4675-b387-81aa9bc7d308"
|
state_hub_task_id: "15fc8db7-cd80-4675-b387-81aa9bc7d308"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user