Add profile-driven runtime adapter resolution

This commit is contained in:
2026-05-18 20:53:16 +02:00
parent 850979ccf1
commit b36a935f48
6 changed files with 618 additions and 19 deletions

View File

@@ -36,6 +36,29 @@ web framework binding.
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_report` emits:

View File

@@ -45,7 +45,7 @@ from .retrieval import (
retrieve_graph_neighborhood,
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 .runtime import PhaseMemoryRuntime
@@ -98,7 +98,10 @@ __all__ = [
"retrieve_graph_neighborhood",
"select_event_path",
"RuntimeConfig",
"RuntimeAdapterBundle",
"health_report",
"resolve_runtime_adapters",
"runtime_from_config",
"service_contracts",
]

View File

@@ -3,14 +3,18 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from .adapters import (
AllowAllPolicyGateway,
FileBackedMemoryGraphStore,
InMemoryMemoryEventLog,
InMemoryMemoryGraphStore,
InMemoryRuntimeRegistry,
InMemorySemanticIndex,
JsonlAuditSink,
JsonlMemoryEventLog,
NoopContextPackageCompiler,
RecordingAuditSink,
)
@@ -37,11 +41,22 @@ SERVICE_OPERATIONS = {
@dataclass(frozen=True)
class RuntimeConfig:
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"
audit_sink_mode: str = "recording"
package_compiler_mode: str = "noop"
semantic_index_mode: str = "disabled"
runtime_registry_mode: str = "memory"
dry_run_default: bool = True
trust_zone_labels: tuple[str, ...] = ("local",)
@@ -49,14 +64,108 @@ class RuntimeConfig:
def local_default(cls) -> "RuntimeConfig":
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, ...]:
diagnostics: list[Diagnostic] = []
if not self.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"}:
diagnostics.append(Diagnostic("error", "unsupported_policy_mode", "Unsupported policy mode.", "policy_mode", {"policy_mode": self.policy_mode}))
if self.semantic_index_mode not in {"disabled", "external"}:
diagnostics.append(Diagnostic("error", "unsupported_semantic_index_mode", "Unsupported semantic index mode.", "semantic_index_mode", {"semantic_index_mode": self.semantic_index_mode}))
for adapter, allowed_modes in _SUPPORTED_ADAPTER_MODES.items():
mode = self.adapter_mode(adapter)
if mode not in allowed_modes:
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)
def to_dict(self) -> dict[str, Any]:
@@ -67,20 +176,134 @@ class RuntimeConfig:
"audit_sink_mode": self.audit_sink_mode,
"package_compiler_mode": self.package_compiler_mode,
"semantic_index_mode": self.semantic_index_mode,
"runtime_registry_mode": self.runtime_registry_mode,
"dry_run_default": self.dry_run_default,
"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]:
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()
# First service-ready slice keeps adapters dependency-light. External
# adapter resolution belongs behind the registry in later deployments.
return PhaseMemoryRuntime()
external_adapters = dict(external_adapters or {})
diagnostics = list(config.diagnostics())
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]:
@@ -112,9 +335,15 @@ def health_report(runtime: PhaseMemoryRuntime, *, config: RuntimeConfig | None =
class LocalServiceRunner:
"""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.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]:
payload = payload or {}
@@ -213,3 +442,135 @@ def default_conformance_adapters() -> dict[str, Any]:
"semantic_index": InMemorySemanticIndex(),
"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)

View File

@@ -1,3 +1,6 @@
import json
from pathlib import Path
from phase_memory.models import LifecycleState, MemoryNode
from phase_memory.service import (
HEALTH_REPORT_SCHEMA,
@@ -15,9 +18,17 @@ from phase_memory.service import (
default_conformance_adapters,
health_report,
kontextual_delegation_envelope,
resolve_runtime_adapters,
runtime_from_config,
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:
contracts = service_contracts()
@@ -47,6 +58,61 @@ def test_service_runner_handles_health() -> None:
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:
adapters = default_conformance_adapters()

View File

@@ -44,22 +44,22 @@ not what adjacent repositories may already provide.
## 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
foundation, a usable local runtime facade, a CLI, a file-backed local
workspace, first-slice policy/review/audit gates, a concrete Markitect package
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 |
| --- | ---: | ---: | --- | --- |
| 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. |
| 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. |
| 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. |
| 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. |
@@ -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. |
| 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. |
| 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. |
| 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. |
| Developer experience | 3.3 | 4.0 | README quick start, package map, runtime facade docs, CLI examples, local persistence guide | Add troubleshooting and richer examples. |
| 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.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.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
@@ -152,6 +152,27 @@ Remaining maturity blockers:
- Optional framework-specific service bindings.
- 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
Closed on 2026-05-18:
@@ -215,6 +236,7 @@ flowchart TD
WP5["PMEM-WP-0005\nMarkitect package bridge"]
WP6["PMEM-WP-0006\nRetrieval and activation quality"]
WP7["PMEM-WP-0007\nService readiness and adapters"]
WP8["PMEM-WP-0008\nProfile-driven runtime config"]
WP1 --> WP2
WP2 --> WP3
@@ -227,6 +249,9 @@ flowchart TD
WP4 --> WP7
WP5 --> WP7
WP6 --> WP7
WP3 --> WP8
WP5 --> WP8
WP7 --> WP8
```
## Next Tracking Cadence

View 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.