From b36a935f482f49898e939edfaffb23f111c4eddc Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 18 May 2026 20:53:16 +0200 Subject: [PATCH] Add profile-driven runtime adapter resolution --- docs/service-readiness.md | 23 ++ src/phase_memory/__init__.py | 5 +- src/phase_memory/service.py | 383 +++++++++++++++++- tests/test_service_readiness.py | 66 +++ workplans/PMEM-MATURITY-SCORECARD.md | 39 +- ...08-profile-driven-runtime-configuration.md | 121 ++++++ 6 files changed, 618 insertions(+), 19 deletions(-) create mode 100644 workplans/PMEM-WP-0008-profile-driven-runtime-configuration.md diff --git a/docs/service-readiness.md b/docs/service-readiness.md index 7a70365..fe54aac 100644 --- a/docs/service-readiness.md +++ b/docs/service-readiness.md @@ -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: diff --git a/src/phase_memory/__init__.py b/src/phase_memory/__init__.py index e0ef244..860afa5 100644 --- a/src/phase_memory/__init__.py +++ b/src/phase_memory/__init__.py @@ -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", ] diff --git a/src/phase_memory/service.py b/src/phase_memory/service.py index 95859d7..bb2b494 100644 --- a/src/phase_memory/service.py +++ b/src/phase_memory/service.py @@ -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) diff --git a/tests/test_service_readiness.py b/tests/test_service_readiness.py index bdf97c5..25f040d 100644 --- a/tests/test_service_readiness.py +++ b/tests/test_service_readiness.py @@ -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() diff --git a/workplans/PMEM-MATURITY-SCORECARD.md b/workplans/PMEM-MATURITY-SCORECARD.md index 575e3d0..f730ea6 100644 --- a/workplans/PMEM-MATURITY-SCORECARD.md +++ b/workplans/PMEM-MATURITY-SCORECARD.md @@ -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 diff --git a/workplans/PMEM-WP-0008-profile-driven-runtime-configuration.md b/workplans/PMEM-WP-0008-profile-driven-runtime-configuration.md new file mode 100644 index 0000000..933af48 --- /dev/null +++ b/workplans/PMEM-WP-0008-profile-driven-runtime-configuration.md @@ -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.