generated from coulomb/repo-seed
Add profile-driven runtime adapter resolution
This commit is contained in:
@@ -36,6 +36,29 @@ web framework binding.
|
|||||||
|
|
||||||
The default config is local and dependency-light.
|
The default config is local and dependency-light.
|
||||||
|
|
||||||
|
`RuntimeConfig.from_profile(...)` can derive this config from a
|
||||||
|
Markitect-compatible memory profile. It reads explicit adapter declarations
|
||||||
|
when present and also understands the first local store aliases used by the
|
||||||
|
fixtures:
|
||||||
|
|
||||||
|
- `local-graph-store` -> file-backed graph store
|
||||||
|
- `local-event-log` -> JSONL event log
|
||||||
|
- `markitect-context-package` -> local no-op package compiler boundary
|
||||||
|
|
||||||
|
`resolve_runtime_adapters(config)` materializes the local adapter bundle. It
|
||||||
|
supports:
|
||||||
|
|
||||||
|
- in-memory or file-backed graph stores
|
||||||
|
- in-memory or JSONL event logs
|
||||||
|
- recording or JSONL audit sinks
|
||||||
|
- no-op package compiler
|
||||||
|
- disabled or in-memory semantic index
|
||||||
|
- in-memory runtime registry
|
||||||
|
|
||||||
|
External adapter modes are valid configuration values, but they must be
|
||||||
|
supplied explicitly by the caller. The local resolver reports
|
||||||
|
`missing_external_adapter` instead of silently replacing them.
|
||||||
|
|
||||||
## Health
|
## Health
|
||||||
|
|
||||||
`health_report` emits:
|
`health_report` emits:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from .retrieval import (
|
|||||||
retrieve_graph_neighborhood,
|
retrieve_graph_neighborhood,
|
||||||
select_event_path,
|
select_event_path,
|
||||||
)
|
)
|
||||||
from .service import RuntimeConfig, health_report, service_contracts
|
from .service import RuntimeAdapterBundle, RuntimeConfig, health_report, resolve_runtime_adapters, runtime_from_config, service_contracts
|
||||||
from .planner import plan_profile_execution
|
from .planner import plan_profile_execution
|
||||||
from .runtime import PhaseMemoryRuntime
|
from .runtime import PhaseMemoryRuntime
|
||||||
|
|
||||||
@@ -98,7 +98,10 @@ __all__ = [
|
|||||||
"retrieve_graph_neighborhood",
|
"retrieve_graph_neighborhood",
|
||||||
"select_event_path",
|
"select_event_path",
|
||||||
"RuntimeConfig",
|
"RuntimeConfig",
|
||||||
|
"RuntimeAdapterBundle",
|
||||||
"health_report",
|
"health_report",
|
||||||
|
"resolve_runtime_adapters",
|
||||||
|
"runtime_from_config",
|
||||||
"service_contracts",
|
"service_contracts",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .adapters import (
|
from .adapters import (
|
||||||
AllowAllPolicyGateway,
|
AllowAllPolicyGateway,
|
||||||
|
FileBackedMemoryGraphStore,
|
||||||
InMemoryMemoryEventLog,
|
InMemoryMemoryEventLog,
|
||||||
InMemoryMemoryGraphStore,
|
InMemoryMemoryGraphStore,
|
||||||
InMemoryRuntimeRegistry,
|
InMemoryRuntimeRegistry,
|
||||||
InMemorySemanticIndex,
|
InMemorySemanticIndex,
|
||||||
|
JsonlAuditSink,
|
||||||
|
JsonlMemoryEventLog,
|
||||||
NoopContextPackageCompiler,
|
NoopContextPackageCompiler,
|
||||||
RecordingAuditSink,
|
RecordingAuditSink,
|
||||||
)
|
)
|
||||||
@@ -37,11 +41,22 @@ SERVICE_OPERATIONS = {
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RuntimeConfig:
|
class RuntimeConfig:
|
||||||
local_store_path: str = ".phase-memory-local"
|
local_store_path: str = ".phase-memory-local"
|
||||||
adapter_registry: dict[str, str] = field(default_factory=lambda: {"graph_store": "memory", "event_log": "memory"})
|
adapter_registry: dict[str, str] = field(
|
||||||
|
default_factory=lambda: {
|
||||||
|
"graph_store": "memory",
|
||||||
|
"event_log": "memory",
|
||||||
|
"policy_gateway": "allow-all",
|
||||||
|
"audit_sink": "recording",
|
||||||
|
"package_compiler": "noop",
|
||||||
|
"semantic_index": "disabled",
|
||||||
|
"runtime_registry": "memory",
|
||||||
|
}
|
||||||
|
)
|
||||||
policy_mode: str = "allow-all"
|
policy_mode: str = "allow-all"
|
||||||
audit_sink_mode: str = "recording"
|
audit_sink_mode: str = "recording"
|
||||||
package_compiler_mode: str = "noop"
|
package_compiler_mode: str = "noop"
|
||||||
semantic_index_mode: str = "disabled"
|
semantic_index_mode: str = "disabled"
|
||||||
|
runtime_registry_mode: str = "memory"
|
||||||
dry_run_default: bool = True
|
dry_run_default: bool = True
|
||||||
trust_zone_labels: tuple[str, ...] = ("local",)
|
trust_zone_labels: tuple[str, ...] = ("local",)
|
||||||
|
|
||||||
@@ -49,14 +64,108 @@ class RuntimeConfig:
|
|||||||
def local_default(cls) -> "RuntimeConfig":
|
def local_default(cls) -> "RuntimeConfig":
|
||||||
return cls()
|
return cls()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_profile(cls, profile: ProfileIntent | dict[str, Any], *, local_store_path: str | None = None) -> "RuntimeConfig":
|
||||||
|
profile_intent = profile if isinstance(profile, ProfileIntent) else ProfileIntent.from_mapping(profile)
|
||||||
|
base = cls.local_default()
|
||||||
|
runtime_metadata = _runtime_metadata(profile_intent)
|
||||||
|
stores = dict(profile_intent.stores)
|
||||||
|
policy = dict(profile_intent.policy)
|
||||||
|
activation = dict(profile_intent.activation)
|
||||||
|
observability = dict(profile_intent.observability)
|
||||||
|
|
||||||
|
store_path = str(
|
||||||
|
local_store_path
|
||||||
|
or runtime_metadata.get("local_store_path")
|
||||||
|
or profile_intent.metadata.get("local_store_path")
|
||||||
|
or stores.get("local_store_path")
|
||||||
|
or base.local_store_path
|
||||||
|
)
|
||||||
|
policy_mode = _mode_from(policy, ("mode", "policy_mode", "gateway"), default=base.policy_mode)
|
||||||
|
audit_sink_mode = _mode_from(observability, ("audit_sink", "audit_sink_mode"), default=_store_mode(stores, ("audit_sink", "audit"), base.audit_sink_mode))
|
||||||
|
package_compiler_mode = _mode_from(
|
||||||
|
runtime_metadata,
|
||||||
|
("package_compiler", "package_compiler_mode"),
|
||||||
|
default=_store_mode(stores, ("package_compiler", "context_package", "package"), base.package_compiler_mode),
|
||||||
|
)
|
||||||
|
semantic_index_mode = _mode_from(
|
||||||
|
activation,
|
||||||
|
("semantic_index", "semantic_index_mode"),
|
||||||
|
default=_store_mode(stores, ("semantic_index", "semantic"), base.semantic_index_mode),
|
||||||
|
)
|
||||||
|
runtime_registry_mode = _mode_from(
|
||||||
|
observability,
|
||||||
|
("runtime_registry", "runtime_registry_mode"),
|
||||||
|
default=_mode_from(runtime_metadata, ("runtime_registry", "runtime_registry_mode"), default=base.runtime_registry_mode),
|
||||||
|
)
|
||||||
|
graph_store_mode = _infer_graph_store_mode(stores, default=base.adapter_registry["graph_store"])
|
||||||
|
event_log_mode = _infer_event_log_mode(stores, default=base.adapter_registry["event_log"])
|
||||||
|
dry_run_default = _bool_from(policy, "dry_run_default", default=base.dry_run_default)
|
||||||
|
trust_zone_labels = _string_tuple(
|
||||||
|
policy.get("trust_zone_labels")
|
||||||
|
or policy.get("trust_zones")
|
||||||
|
or policy.get("required_labels")
|
||||||
|
or base.trust_zone_labels
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
local_store_path=store_path,
|
||||||
|
adapter_registry={
|
||||||
|
"graph_store": graph_store_mode,
|
||||||
|
"event_log": event_log_mode,
|
||||||
|
"policy_gateway": policy_mode,
|
||||||
|
"audit_sink": audit_sink_mode,
|
||||||
|
"package_compiler": package_compiler_mode,
|
||||||
|
"semantic_index": semantic_index_mode,
|
||||||
|
"runtime_registry": runtime_registry_mode,
|
||||||
|
},
|
||||||
|
policy_mode=policy_mode,
|
||||||
|
audit_sink_mode=audit_sink_mode,
|
||||||
|
package_compiler_mode=package_compiler_mode,
|
||||||
|
semantic_index_mode=semantic_index_mode,
|
||||||
|
runtime_registry_mode=runtime_registry_mode,
|
||||||
|
dry_run_default=dry_run_default,
|
||||||
|
trust_zone_labels=trust_zone_labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
def adapter_mode(self, adapter: str) -> str:
|
||||||
|
legacy = {
|
||||||
|
"graph_store": "memory",
|
||||||
|
"event_log": "memory",
|
||||||
|
"policy_gateway": self.policy_mode,
|
||||||
|
"audit_sink": self.audit_sink_mode,
|
||||||
|
"package_compiler": self.package_compiler_mode,
|
||||||
|
"semantic_index": self.semantic_index_mode,
|
||||||
|
"runtime_registry": self.runtime_registry_mode,
|
||||||
|
}
|
||||||
|
return _normalize_mode(self.adapter_registry.get(adapter) or legacy.get(adapter) or "")
|
||||||
|
|
||||||
def diagnostics(self) -> tuple[Diagnostic, ...]:
|
def diagnostics(self) -> tuple[Diagnostic, ...]:
|
||||||
diagnostics: list[Diagnostic] = []
|
diagnostics: list[Diagnostic] = []
|
||||||
if not self.local_store_path:
|
if not self.local_store_path:
|
||||||
diagnostics.append(Diagnostic("error", "missing_store_path", "Runtime config requires a local store path.", "local_store_path"))
|
diagnostics.append(Diagnostic("error", "missing_store_path", "Runtime config requires a local store path.", "local_store_path"))
|
||||||
if self.policy_mode not in {"allow-all", "external"}:
|
for adapter, allowed_modes in _SUPPORTED_ADAPTER_MODES.items():
|
||||||
diagnostics.append(Diagnostic("error", "unsupported_policy_mode", "Unsupported policy mode.", "policy_mode", {"policy_mode": self.policy_mode}))
|
mode = self.adapter_mode(adapter)
|
||||||
if self.semantic_index_mode not in {"disabled", "external"}:
|
if mode not in allowed_modes:
|
||||||
diagnostics.append(Diagnostic("error", "unsupported_semantic_index_mode", "Unsupported semantic index mode.", "semantic_index_mode", {"semantic_index_mode": self.semantic_index_mode}))
|
diagnostics.append(
|
||||||
|
Diagnostic(
|
||||||
|
"error",
|
||||||
|
"unsupported_adapter_mode",
|
||||||
|
"Unsupported adapter mode.",
|
||||||
|
f"adapter_registry.{adapter}",
|
||||||
|
{"adapter": adapter, "mode": mode, "allowed_modes": sorted(allowed_modes)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif mode == "external":
|
||||||
|
diagnostics.append(
|
||||||
|
Diagnostic(
|
||||||
|
"warn",
|
||||||
|
"external_adapter_declared",
|
||||||
|
"External adapter mode is declared and must be supplied by the caller.",
|
||||||
|
f"adapter_registry.{adapter}",
|
||||||
|
{"adapter": adapter},
|
||||||
|
)
|
||||||
|
)
|
||||||
return tuple(diagnostics)
|
return tuple(diagnostics)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
@@ -67,20 +176,134 @@ class RuntimeConfig:
|
|||||||
"audit_sink_mode": self.audit_sink_mode,
|
"audit_sink_mode": self.audit_sink_mode,
|
||||||
"package_compiler_mode": self.package_compiler_mode,
|
"package_compiler_mode": self.package_compiler_mode,
|
||||||
"semantic_index_mode": self.semantic_index_mode,
|
"semantic_index_mode": self.semantic_index_mode,
|
||||||
|
"runtime_registry_mode": self.runtime_registry_mode,
|
||||||
"dry_run_default": self.dry_run_default,
|
"dry_run_default": self.dry_run_default,
|
||||||
"trust_zone_labels": list(self.trust_zone_labels),
|
"trust_zone_labels": list(self.trust_zone_labels),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RuntimeAdapterBundle:
|
||||||
|
graph_store: Any
|
||||||
|
event_log: Any
|
||||||
|
package_compiler: Any
|
||||||
|
policy_gateway: Any
|
||||||
|
audit_sink: Any
|
||||||
|
semantic_index: Any | None = None
|
||||||
|
runtime_registry: Any | None = None
|
||||||
|
diagnostics: tuple[Diagnostic, ...] = ()
|
||||||
|
|
||||||
|
def to_runtime(self) -> PhaseMemoryRuntime:
|
||||||
|
errors = [diagnostic for diagnostic in self.diagnostics if diagnostic.severity == "error"]
|
||||||
|
if errors:
|
||||||
|
codes = ", ".join(diagnostic.code for diagnostic in errors)
|
||||||
|
raise ValueError(f"Runtime adapters are not ready: {codes}")
|
||||||
|
return PhaseMemoryRuntime(
|
||||||
|
graph_store=self.graph_store,
|
||||||
|
event_log=self.event_log,
|
||||||
|
package_compiler=self.package_compiler,
|
||||||
|
policy_gateway=self.policy_gateway,
|
||||||
|
audit_sink=self.audit_sink,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"graph_store": self.graph_store.__class__.__name__,
|
||||||
|
"event_log": self.event_log.__class__.__name__,
|
||||||
|
"package_compiler": self.package_compiler.__class__.__name__,
|
||||||
|
"policy_gateway": self.policy_gateway.__class__.__name__,
|
||||||
|
"audit_sink": self.audit_sink.__class__.__name__,
|
||||||
|
"semantic_index": self.semantic_index.__class__.__name__ if self.semantic_index is not None else "disabled",
|
||||||
|
"runtime_registry": self.runtime_registry.__class__.__name__ if self.runtime_registry is not None else "disabled",
|
||||||
|
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def service_contracts() -> dict[str, Any]:
|
def service_contracts() -> dict[str, Any]:
|
||||||
return {"schema_version": SERVICE_CONTRACT_SCHEMA, "operations": SERVICE_OPERATIONS}
|
return {"schema_version": SERVICE_CONTRACT_SCHEMA, "operations": SERVICE_OPERATIONS}
|
||||||
|
|
||||||
|
|
||||||
def runtime_from_config(config: RuntimeConfig | None = None) -> PhaseMemoryRuntime:
|
def resolve_runtime_adapters(config: RuntimeConfig | None = None, *, external_adapters: dict[str, Any] | None = None) -> RuntimeAdapterBundle:
|
||||||
config = config or RuntimeConfig.local_default()
|
config = config or RuntimeConfig.local_default()
|
||||||
# First service-ready slice keeps adapters dependency-light. External
|
external_adapters = dict(external_adapters or {})
|
||||||
# adapter resolution belongs behind the registry in later deployments.
|
diagnostics = list(config.diagnostics())
|
||||||
return PhaseMemoryRuntime()
|
root = Path(config.local_store_path)
|
||||||
|
|
||||||
|
graph_store = _resolve_adapter(
|
||||||
|
"graph_store",
|
||||||
|
config.adapter_mode("graph_store"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={
|
||||||
|
"memory": InMemoryMemoryGraphStore,
|
||||||
|
"file": lambda: FileBackedMemoryGraphStore(root),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
event_log = _resolve_adapter(
|
||||||
|
"event_log",
|
||||||
|
config.adapter_mode("event_log"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={
|
||||||
|
"memory": InMemoryMemoryEventLog,
|
||||||
|
"jsonl": lambda: JsonlMemoryEventLog(root / "events.jsonl"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
package_compiler = _resolve_adapter(
|
||||||
|
"package_compiler",
|
||||||
|
config.adapter_mode("package_compiler"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={"noop": NoopContextPackageCompiler},
|
||||||
|
)
|
||||||
|
policy_gateway = _resolve_adapter(
|
||||||
|
"policy_gateway",
|
||||||
|
config.adapter_mode("policy_gateway"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={"allow-all": AllowAllPolicyGateway},
|
||||||
|
)
|
||||||
|
audit_sink = _resolve_adapter(
|
||||||
|
"audit_sink",
|
||||||
|
config.adapter_mode("audit_sink"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={
|
||||||
|
"recording": RecordingAuditSink,
|
||||||
|
"jsonl": lambda: JsonlAuditSink(root / "audit.jsonl"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
semantic_index = _resolve_adapter(
|
||||||
|
"semantic_index",
|
||||||
|
config.adapter_mode("semantic_index"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={
|
||||||
|
"disabled": lambda: None,
|
||||||
|
"memory": InMemorySemanticIndex,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
runtime_registry = _resolve_adapter(
|
||||||
|
"runtime_registry",
|
||||||
|
config.adapter_mode("runtime_registry"),
|
||||||
|
external_adapters,
|
||||||
|
diagnostics,
|
||||||
|
local_factories={"memory": InMemoryRuntimeRegistry},
|
||||||
|
)
|
||||||
|
return RuntimeAdapterBundle(
|
||||||
|
graph_store=graph_store,
|
||||||
|
event_log=event_log,
|
||||||
|
package_compiler=package_compiler,
|
||||||
|
policy_gateway=policy_gateway,
|
||||||
|
audit_sink=audit_sink,
|
||||||
|
semantic_index=semantic_index,
|
||||||
|
runtime_registry=runtime_registry,
|
||||||
|
diagnostics=tuple(diagnostics),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_from_config(config: RuntimeConfig | None = None, *, external_adapters: dict[str, Any] | None = None) -> PhaseMemoryRuntime:
|
||||||
|
return resolve_runtime_adapters(config, external_adapters=external_adapters).to_runtime()
|
||||||
|
|
||||||
|
|
||||||
def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None = None) -> dict[str, Any]:
|
def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None = None) -> dict[str, Any]:
|
||||||
@@ -112,9 +335,15 @@ def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None =
|
|||||||
class LocalServiceRunner:
|
class LocalServiceRunner:
|
||||||
"""Minimal optional service runner shape without web framework dependency."""
|
"""Minimal optional service runner shape without web framework dependency."""
|
||||||
|
|
||||||
def __init__(self, runtime: PhaseMemoryRuntime | None = None, config: RuntimeConfig | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
runtime: PhaseMemoryRuntime | None = None,
|
||||||
|
config: RuntimeConfig | None = None,
|
||||||
|
*,
|
||||||
|
external_adapters: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
self.config = config or RuntimeConfig.local_default()
|
self.config = config or RuntimeConfig.local_default()
|
||||||
self.runtime = runtime or runtime_from_config(self.config)
|
self.runtime = runtime or runtime_from_config(self.config, external_adapters=external_adapters)
|
||||||
|
|
||||||
def handle(self, operation: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
def handle(self, operation: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
payload = payload or {}
|
payload = payload or {}
|
||||||
@@ -213,3 +442,135 @@ def default_conformance_adapters() -> dict[str, Any]:
|
|||||||
"semantic_index": InMemorySemanticIndex(),
|
"semantic_index": InMemorySemanticIndex(),
|
||||||
"runtime_registry": InMemoryRuntimeRegistry(),
|
"runtime_registry": InMemoryRuntimeRegistry(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_SUPPORTED_ADAPTER_MODES = {
|
||||||
|
"graph_store": {"memory", "file", "external"},
|
||||||
|
"event_log": {"memory", "jsonl", "external"},
|
||||||
|
"policy_gateway": {"allow-all", "external"},
|
||||||
|
"audit_sink": {"recording", "jsonl", "external"},
|
||||||
|
"package_compiler": {"noop", "external"},
|
||||||
|
"semantic_index": {"disabled", "memory", "external"},
|
||||||
|
"runtime_registry": {"memory", "external"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_MODE_ALIASES = {
|
||||||
|
"allow_all": "allow-all",
|
||||||
|
"allowall": "allow-all",
|
||||||
|
"local-allow-all": "allow-all",
|
||||||
|
"file-backed": "file",
|
||||||
|
"file-backed-graph-store": "file",
|
||||||
|
"local-graph-store": "file",
|
||||||
|
"local-json": "file",
|
||||||
|
"json-lines": "jsonl",
|
||||||
|
"local-event-log": "jsonl",
|
||||||
|
"jsonl-event-log": "jsonl",
|
||||||
|
"recording-audit": "recording",
|
||||||
|
"jsonl-audit": "jsonl",
|
||||||
|
"noop-compiler": "noop",
|
||||||
|
"markitect-context-package": "noop",
|
||||||
|
"in-memory": "memory",
|
||||||
|
"local-memory": "memory",
|
||||||
|
"off": "disabled",
|
||||||
|
"none": "disabled",
|
||||||
|
"false": "disabled",
|
||||||
|
"true": "memory",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_adapter(
|
||||||
|
adapter: str,
|
||||||
|
mode: str,
|
||||||
|
external_adapters: dict[str, Any],
|
||||||
|
diagnostics: list[Diagnostic],
|
||||||
|
*,
|
||||||
|
local_factories: dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
|
if mode == "external":
|
||||||
|
if adapter in external_adapters:
|
||||||
|
return external_adapters[adapter]
|
||||||
|
diagnostics.append(
|
||||||
|
Diagnostic(
|
||||||
|
"error",
|
||||||
|
"missing_external_adapter",
|
||||||
|
"External adapter mode was requested but no adapter instance was supplied.",
|
||||||
|
f"adapter_registry.{adapter}",
|
||||||
|
{"adapter": adapter},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
factory = local_factories.get(mode)
|
||||||
|
if factory is None:
|
||||||
|
diagnostics.append(
|
||||||
|
Diagnostic(
|
||||||
|
"error",
|
||||||
|
"unresolved_adapter_mode",
|
||||||
|
"Adapter mode cannot be resolved by the local runtime.",
|
||||||
|
f"adapter_registry.{adapter}",
|
||||||
|
{"adapter": adapter, "mode": mode},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return factory()
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_metadata(profile: ProfileIntent) -> dict[str, Any]:
|
||||||
|
runtime = profile.metadata.get("runtime")
|
||||||
|
return dict(runtime) if isinstance(runtime, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _mode_from(mapping: dict[str, Any], keys: tuple[str, ...], *, default: str) -> str:
|
||||||
|
for key in keys:
|
||||||
|
if key in mapping:
|
||||||
|
return _normalize_mode(mapping[key])
|
||||||
|
return _normalize_mode(default)
|
||||||
|
|
||||||
|
|
||||||
|
def _store_mode(stores: dict[str, str], keys: tuple[str, ...], default: str) -> str:
|
||||||
|
for key in keys:
|
||||||
|
if key in stores:
|
||||||
|
return _normalize_mode(stores[key])
|
||||||
|
return _normalize_mode(default)
|
||||||
|
|
||||||
|
|
||||||
|
def _infer_graph_store_mode(stores: dict[str, str], *, default: str) -> str:
|
||||||
|
specific = _store_mode(stores, ("graph_store", "memory_graph", "graph"), "")
|
||||||
|
if specific:
|
||||||
|
return specific
|
||||||
|
values = {_normalize_mode(value) for value in stores.values()}
|
||||||
|
if "file" in values:
|
||||||
|
return "file"
|
||||||
|
return _normalize_mode(default)
|
||||||
|
|
||||||
|
|
||||||
|
def _infer_event_log_mode(stores: dict[str, str], *, default: str) -> str:
|
||||||
|
specific = _store_mode(stores, ("event_log", "events", "conversation"), "")
|
||||||
|
if specific:
|
||||||
|
return specific
|
||||||
|
values = {_normalize_mode(value) for value in stores.values()}
|
||||||
|
if "jsonl" in values:
|
||||||
|
return "jsonl"
|
||||||
|
return _normalize_mode(default)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_mode(value: Any) -> str:
|
||||||
|
text = str(value).strip().lower().replace("_", "-")
|
||||||
|
return _MODE_ALIASES.get(text, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_from(mapping: dict[str, Any], key: str, *, default: bool) -> bool:
|
||||||
|
if key not in mapping:
|
||||||
|
return default
|
||||||
|
value = mapping[key]
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _string_tuple(value: Any) -> tuple[str, ...]:
|
||||||
|
if value is None:
|
||||||
|
return ()
|
||||||
|
if isinstance(value, str):
|
||||||
|
return (value,)
|
||||||
|
return tuple(str(item) for item in value)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from phase_memory.models import LifecycleState, MemoryNode
|
from phase_memory.models import LifecycleState, MemoryNode
|
||||||
from phase_memory.service import (
|
from phase_memory.service import (
|
||||||
HEALTH_REPORT_SCHEMA,
|
HEALTH_REPORT_SCHEMA,
|
||||||
@@ -15,9 +18,17 @@ from phase_memory.service import (
|
|||||||
default_conformance_adapters,
|
default_conformance_adapters,
|
||||||
health_report,
|
health_report,
|
||||||
kontextual_delegation_envelope,
|
kontextual_delegation_envelope,
|
||||||
|
resolve_runtime_adapters,
|
||||||
|
runtime_from_config,
|
||||||
service_contracts,
|
service_contracts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FIXTURES = Path(__file__).parent / "fixtures"
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name: str):
|
||||||
|
return json.loads((FIXTURES / name).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def test_service_contracts_list_runtime_operations() -> None:
|
def test_service_contracts_list_runtime_operations() -> None:
|
||||||
contracts = service_contracts()
|
contracts = service_contracts()
|
||||||
@@ -47,6 +58,61 @@ def test_service_runner_handles_health() -> None:
|
|||||||
assert response["ok"] is True
|
assert response["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_driven_runtime_config_resolves_file_backed_adapters(tmp_path) -> None:
|
||||||
|
config = RuntimeConfig.from_profile(
|
||||||
|
{
|
||||||
|
"schema_version": "markitect.memory.profile.v1",
|
||||||
|
"id": "profile.config",
|
||||||
|
"stores": {
|
||||||
|
"graph_store": "file",
|
||||||
|
"event_log": "jsonl",
|
||||||
|
},
|
||||||
|
"activation": {"semantic_index": "memory"},
|
||||||
|
"policy": {"mode": "allow-all", "trust_zones": ["local", "team"]},
|
||||||
|
"observability": {"audit_sink": "jsonl", "runtime_registry": "memory"},
|
||||||
|
"metadata": {"runtime": {"local_store_path": str(tmp_path / "memory-store")}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = resolve_runtime_adapters(config)
|
||||||
|
runtime = runtime_from_config(config)
|
||||||
|
runtime.graph_store.save_node(MemoryNode("node.config", "decision", "Config-driven node"))
|
||||||
|
|
||||||
|
assert config.adapter_mode("graph_store") == "file"
|
||||||
|
assert config.adapter_mode("event_log") == "jsonl"
|
||||||
|
assert config.trust_zone_labels == ("local", "team")
|
||||||
|
assert bundle.to_dict()["graph_store"] == "FileBackedMemoryGraphStore"
|
||||||
|
assert bundle.to_dict()["event_log"] == "JsonlMemoryEventLog"
|
||||||
|
assert bundle.to_dict()["semantic_index"] == "InMemorySemanticIndex"
|
||||||
|
assert (tmp_path / "memory-store" / "nodes" / "node.config.json").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_config_from_fixture_profile_understands_local_aliases() -> None:
|
||||||
|
config = RuntimeConfig.from_profile(_load("memory-profile.json"), local_store_path=".phase-memory-test")
|
||||||
|
|
||||||
|
assert config.adapter_mode("graph_store") == "file"
|
||||||
|
assert config.adapter_mode("event_log") == "jsonl"
|
||||||
|
assert config.adapter_mode("package_compiler") == "noop"
|
||||||
|
assert config.trust_zone_labels == ("project-local",)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_external_adapter_blocks_runtime_resolution() -> None:
|
||||||
|
registry = RuntimeConfig.local_default().adapter_registry.copy()
|
||||||
|
registry["policy_gateway"] = "external"
|
||||||
|
config = RuntimeConfig(adapter_registry=registry, policy_mode="external")
|
||||||
|
|
||||||
|
bundle = resolve_runtime_adapters(config)
|
||||||
|
|
||||||
|
assert "external_adapter_declared" in [diagnostic.code for diagnostic in bundle.diagnostics]
|
||||||
|
assert "missing_external_adapter" in [diagnostic.code for diagnostic in bundle.diagnostics]
|
||||||
|
try:
|
||||||
|
runtime_from_config(config)
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "missing_external_adapter" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("runtime_from_config should require supplied external adapters")
|
||||||
|
|
||||||
|
|
||||||
def test_default_adapter_conformance_helpers() -> None:
|
def test_default_adapter_conformance_helpers() -> None:
|
||||||
adapters = default_conformance_adapters()
|
adapters = default_conformance_adapters()
|
||||||
|
|
||||||
|
|||||||
@@ -44,22 +44,22 @@ not what adjacent repositories may already provide.
|
|||||||
|
|
||||||
## Current Baseline - 2026-05-18
|
## Current Baseline - 2026-05-18
|
||||||
|
|
||||||
Overall maturity: **4.2 / 5**
|
Overall maturity: **4.3 / 5**
|
||||||
|
|
||||||
The repo has crossed from intent-only into a working deterministic library
|
The repo has crossed from intent-only into a working deterministic library
|
||||||
foundation, a usable local runtime facade, a CLI, a file-backed local
|
foundation, a usable local runtime facade, a CLI, a file-backed local
|
||||||
workspace, first-slice policy/review/audit gates, a concrete Markitect package
|
workspace, first-slice policy/review/audit gates, a concrete Markitect package
|
||||||
bridge, deterministic activation quality helpers, and first-slice service
|
bridge, deterministic activation quality helpers, and first-slice service
|
||||||
readiness contracts.
|
readiness contracts with profile-driven runtime adapter resolution.
|
||||||
|
|
||||||
| Dimension | Current | Target | Evidence | Needed Next |
|
| Dimension | Current | Target | Evidence | Needed Next |
|
||||||
| --- | ---: | ---: | --- | --- |
|
| --- | ---: | ---: | --- | --- |
|
||||||
| Intent and boundaries | 4.0 | 5.0 | `INTENT.md`, `SCOPE.md`, `README.md`, architecture doc, PMEM-WP-0001 closure | Keep boundaries current as runtime behavior expands. |
|
| Intent and boundaries | 4.0 | 5.0 | `INTENT.md`, `SCOPE.md`, `README.md`, architecture doc, PMEM-WP-0001 closure | Keep boundaries current as runtime behavior expands. |
|
||||||
| Package foundation | 4.0 | 4.0 | Python package, exports, runtime facade, CLI entrypoint, config, service contracts, dependency-light tests | Maintain public API compatibility as adapters expand. |
|
| Package foundation | 4.0 | 4.0 | Python package, exports, runtime facade, CLI entrypoint, config, service contracts, dependency-light tests | Maintain public API compatibility as adapters expand. |
|
||||||
| Profile contract ingress | 2.5 | 4.0 | Markitect-compatible profile loading, diagnostics, runtime envelopes | Add profile-driven runtime configuration and richer compatibility coverage. |
|
| Profile contract ingress | 3.2 | 4.0 | Markitect-compatible profile loading, diagnostics, runtime envelopes, profile-derived runtime config, local adapter alias normalization | Add richer compatibility coverage. |
|
||||||
| Graph/event contract ingress | 3.5 | 4.0 | Graph loading, edge endpoint diagnostics, event model, JSONL event log, export, repair diagnostics, service import/export contracts | Add broader external adapter fixtures. |
|
| Graph/event contract ingress | 3.5 | 4.0 | Graph loading, edge endpoint diagnostics, event model, JSONL event log, export, repair diagnostics, service import/export contracts | Add broader external adapter fixtures. |
|
||||||
| Phase domain model | 3.0 | 4.0 | Phases, memory kinds, lifecycle states, actions, explicit path records | Add profile-driven transition rule evaluation and migration semantics. |
|
| Phase domain model | 3.0 | 4.0 | Phases, memory kinds, lifecycle states, actions, explicit path records | Add profile-driven transition rule evaluation and migration semantics. |
|
||||||
| Profile execution planning | 3.5 | 4.0 | Adapter plan, capabilities, policy gates, fallback behavior, CLI output, snapshot fixture, service contract | Add external config-driven adapter resolution. |
|
| Profile execution planning | 3.8 | 4.0 | Adapter plan, capabilities, policy gates, fallback behavior, CLI output, snapshot fixture, service contract, config-driven local adapter resolution | Add external adapter injection coverage. |
|
||||||
| Lifecycle planning | 3.0 | 4.0 | Transition, retention, refresh, compaction dry-run plans, review-gated local apply | Add profile-driven rule evaluation and service apply contracts. |
|
| Lifecycle planning | 3.0 | 4.0 | Transition, retention, refresh, compaction dry-run plans, review-gated local apply | Add profile-driven rule evaluation and service apply contracts. |
|
||||||
| Activation planning | 3.8 | 5.0 | Budgeted selection, Markitect-compatible selection output, package request envelope, graph neighborhoods, event paths, ranking, metadata preservation, metrics | Add semantic-index adapters and broader evaluation corpora. |
|
| Activation planning | 3.8 | 5.0 | Budgeted selection, Markitect-compatible selection output, package request envelope, graph neighborhoods, event paths, ranking, metadata preservation, metrics | Add semantic-index adapters and broader evaluation corpora. |
|
||||||
| Local persistence | 3.0 | 4.0 | Versioned local workspace, file-backed graph store, JSONL event log, JSONL audit sink | Add migration/repair utilities and stronger durability semantics. |
|
| Local persistence | 3.0 | 4.0 | Versioned local workspace, file-backed graph store, JSONL event log, JSONL audit sink | Add migration/repair utilities and stronger durability semantics. |
|
||||||
@@ -67,9 +67,9 @@ readiness contracts.
|
|||||||
| Observability and diagnostics | 3.5 | 4.0 | Planner diagnostics, runtime diagnostics, event log corruption checks, repair diagnostics, policy denial diagnostics, health envelopes, adapter status | Add production telemetry adapters. |
|
| Observability and diagnostics | 3.5 | 4.0 | Planner diagnostics, runtime diagnostics, event log corruption checks, repair diagnostics, policy denial diagnostics, health envelopes, adapter status | Add production telemetry adapters. |
|
||||||
| Markitect interop | 3.5 | 4.0 | Compatible contract ingress, optional validation boundary, enriched selection metadata, package request/response envelopes | Add live optional Markitect compiler adapter when available. |
|
| Markitect interop | 3.5 | 4.0 | Compatible contract ingress, optional validation boundary, enriched selection metadata, package request/response envelopes | Add live optional Markitect compiler adapter when available. |
|
||||||
| Kontextual/Infospace interop | 2.5 | 4.0 | Boundaries documented, small derived fixtures, activation quality report fixture, Kontextual delegation envelope | Add live fake/real delegation adapters and broader Infospace reports. |
|
| Kontextual/Infospace interop | 2.5 | 4.0 | Boundaries documented, small derived fixtures, activation quality report fixture, Kontextual delegation envelope | Add live fake/real delegation adapters and broader Infospace reports. |
|
||||||
| Testing and evaluation | 4.0 | 4.0 | 51 deterministic tests over planners, adapters, runtime envelopes, CLI, snapshots, file-store round trips, apply denial, review records, audit schema, policy redaction, Markitect bridge fixtures, retrieval, activation metrics, service contracts, config, health, and conformance | Add broader evaluation corpora. |
|
| Testing and evaluation | 4.0 | 4.0 | 54 deterministic tests over planners, adapters, runtime envelopes, CLI, snapshots, file-store round trips, apply denial, review records, audit schema, policy redaction, Markitect bridge fixtures, retrieval, activation metrics, service contracts, config, health, conformance, and adapter resolution | Add broader evaluation corpora. |
|
||||||
| Service readiness | 3.5 | 4.0 | Runtime ports, service contracts, config model, health checks, local service runner, adapter conformance helpers | Add framework-specific bindings and production adapter packs. |
|
| Service readiness | 3.8 | 4.0 | Runtime ports, service contracts, config model, profile-derived adapter resolution, health checks, local service runner, adapter conformance helpers | Add framework-specific bindings and production adapter packs. |
|
||||||
| Developer experience | 3.3 | 4.0 | README quick start, package map, runtime facade docs, CLI examples, local persistence guide | Add troubleshooting and richer examples. |
|
| Developer experience | 3.5 | 4.0 | README quick start, package map, runtime facade docs, CLI examples, local persistence guide, service adapter resolution docs | Add troubleshooting and richer examples. |
|
||||||
|
|
||||||
## Progress Update - PMEM-WP-0002
|
## Progress Update - PMEM-WP-0002
|
||||||
|
|
||||||
@@ -152,6 +152,27 @@ Remaining maturity blockers:
|
|||||||
- Optional framework-specific service bindings.
|
- Optional framework-specific service bindings.
|
||||||
- Production telemetry and audit retention integrations.
|
- Production telemetry and audit retention integrations.
|
||||||
|
|
||||||
|
## Progress Update - PMEM-WP-0008
|
||||||
|
|
||||||
|
Closed on 2026-05-18:
|
||||||
|
|
||||||
|
- Added `RuntimeConfig.from_profile(...)` for Markitect-compatible profile
|
||||||
|
mappings and `ProfileIntent` inputs.
|
||||||
|
- Added adapter alias normalization for local graph stores, event logs, audit
|
||||||
|
sinks, package compilers, semantic indexes, and runtime registries.
|
||||||
|
- Added `RuntimeAdapterBundle` and `resolve_runtime_adapters(...)`.
|
||||||
|
- Updated `runtime_from_config(...)` and `LocalServiceRunner` to use resolved
|
||||||
|
adapters.
|
||||||
|
- Added diagnostics that block unresolved external adapters.
|
||||||
|
- Added tests and service readiness documentation for the resolver path.
|
||||||
|
|
||||||
|
Remaining maturity blockers:
|
||||||
|
|
||||||
|
- Live external adapter implementations.
|
||||||
|
- Broader evaluation corpora.
|
||||||
|
- Optional framework-specific service bindings.
|
||||||
|
- Production telemetry and audit retention integrations.
|
||||||
|
|
||||||
## Progress Update - PMEM-WP-0004
|
## Progress Update - PMEM-WP-0004
|
||||||
|
|
||||||
Closed on 2026-05-18:
|
Closed on 2026-05-18:
|
||||||
@@ -215,6 +236,7 @@ flowchart TD
|
|||||||
WP5["PMEM-WP-0005\nMarkitect package bridge"]
|
WP5["PMEM-WP-0005\nMarkitect package bridge"]
|
||||||
WP6["PMEM-WP-0006\nRetrieval and activation quality"]
|
WP6["PMEM-WP-0006\nRetrieval and activation quality"]
|
||||||
WP7["PMEM-WP-0007\nService readiness and adapters"]
|
WP7["PMEM-WP-0007\nService readiness and adapters"]
|
||||||
|
WP8["PMEM-WP-0008\nProfile-driven runtime config"]
|
||||||
|
|
||||||
WP1 --> WP2
|
WP1 --> WP2
|
||||||
WP2 --> WP3
|
WP2 --> WP3
|
||||||
@@ -227,6 +249,9 @@ flowchart TD
|
|||||||
WP4 --> WP7
|
WP4 --> WP7
|
||||||
WP5 --> WP7
|
WP5 --> WP7
|
||||||
WP6 --> WP7
|
WP6 --> WP7
|
||||||
|
WP3 --> WP8
|
||||||
|
WP5 --> WP8
|
||||||
|
WP7 --> WP8
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next Tracking Cadence
|
## Next Tracking Cadence
|
||||||
|
|||||||
121
workplans/PMEM-WP-0008-profile-driven-runtime-configuration.md
Normal file
121
workplans/PMEM-WP-0008-profile-driven-runtime-configuration.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
id: PMEM-WP-0008
|
||||||
|
type: workplan
|
||||||
|
title: "Profile-Driven Runtime Configuration"
|
||||||
|
domain: markitect
|
||||||
|
repo: phase-memory
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: phase-memory
|
||||||
|
created: "2026-05-18"
|
||||||
|
updated: "2026-05-18"
|
||||||
|
---
|
||||||
|
|
||||||
|
# PMEM-WP-0008: Profile-Driven Runtime Configuration
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Turn Markitect-compatible memory profile metadata into concrete local runtime
|
||||||
|
configuration, then resolve that configuration into adapter instances without
|
||||||
|
silently pretending external adapters exist.
|
||||||
|
|
||||||
|
This advances the scorecard rows for profile contract ingress, profile
|
||||||
|
execution planning, service readiness, and developer experience.
|
||||||
|
|
||||||
|
## Current Evidence
|
||||||
|
|
||||||
|
PMEM-WP-0007 added `RuntimeConfig`, local service contracts, health reports,
|
||||||
|
and conformance helpers. The remaining gap was that `runtime_from_config`
|
||||||
|
returned a default in-memory runtime regardless of config content, so profiles
|
||||||
|
could not drive local file-backed runtime construction.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Build live Markitect, Kontextual, telemetry, or external policy adapters.
|
||||||
|
- Add framework-specific HTTP service bindings.
|
||||||
|
- Change runtime envelope schemas.
|
||||||
|
|
||||||
|
## Implementation Update - 2026-05-18
|
||||||
|
|
||||||
|
Implemented the first profile-driven runtime configuration slice:
|
||||||
|
|
||||||
|
- Added `RuntimeConfig.from_profile(...)` for Markitect-compatible profile
|
||||||
|
mapping and `ProfileIntent` inputs.
|
||||||
|
- Added adapter mode normalization for the local aliases already used in
|
||||||
|
fixtures, including `local-graph-store`, `local-event-log`, and
|
||||||
|
`markitect-context-package`.
|
||||||
|
- Added `RuntimeAdapterBundle` and `resolve_runtime_adapters(...)`.
|
||||||
|
- Updated `runtime_from_config(...)` and `LocalServiceRunner` to use adapter
|
||||||
|
resolution.
|
||||||
|
- Wired local file-backed graph stores, JSONL event logs, JSONL audit sinks,
|
||||||
|
in-memory semantic indexes, and in-memory runtime registries.
|
||||||
|
- Added diagnostics that block unresolved external adapters instead of falling
|
||||||
|
back silently.
|
||||||
|
- Added regression tests and service readiness docs.
|
||||||
|
|
||||||
|
## T01 - Derive runtime config from profiles
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: PMEM-WP-0008-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Map profile stores, policy, activation, observability, and runtime metadata
|
||||||
|
into `RuntimeConfig`.
|
||||||
|
|
||||||
|
## T02 - Normalize local adapter aliases
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: PMEM-WP-0008-T02
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Support fixture-compatible aliases such as `local-graph-store`,
|
||||||
|
`local-event-log`, and `markitect-context-package`.
|
||||||
|
|
||||||
|
## T03 - Resolve config to local adapters
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: PMEM-WP-0008-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Materialize supported local adapter modes into concrete adapter instances and
|
||||||
|
return a reusable adapter bundle.
|
||||||
|
|
||||||
|
## T04 - Block unresolved external adapters
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: PMEM-WP-0008-T04
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Emit diagnostics and prevent runtime construction when a profile or config
|
||||||
|
declares an external adapter that the caller did not provide.
|
||||||
|
|
||||||
|
## T05 - Add regression tests and docs
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: PMEM-WP-0008-T05
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Cover file-backed profile-driven resolution, missing external adapter
|
||||||
|
diagnostics, and document the supported first-slice adapter modes.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- A profile can configure a file-backed local runtime.
|
||||||
|
- Adapter resolution is deterministic and inspectable.
|
||||||
|
- External adapter modes cannot silently degrade to local allow-all behavior.
|
||||||
|
- Tests cover success and failure paths.
|
||||||
|
|
||||||
|
## Closure Review - 2026-05-18
|
||||||
|
|
||||||
|
Closed after adding profile-derived runtime config, adapter bundles, local
|
||||||
|
resolver tests, and service readiness documentation.
|
||||||
Reference in New Issue
Block a user