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:
2026-06-16 00:27:11 +02:00
parent c40fa3c934
commit b9bb1f7d10
4 changed files with 219 additions and 1 deletions

View 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()