From 481b73e3c457d5ee99ce1cc28761a19bf655bcf3 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Tue, 10 Mar 2026 03:43:40 -0400 Subject: [PATCH] feat: add JSON state import/export --- src/context_compiler/engine.py | 54 +++++++++++ tests/test_engine.py | 160 +++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index 8636976..028d1b2 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -5,6 +5,7 @@ Only explicit user directives can mutate authoritative state. """ +import json import re from copy import deepcopy from dataclasses import dataclass @@ -108,6 +109,20 @@ def state(self) -> State: """Return a defensive copy of the current authoritative state snapshot.""" return deepcopy(self._state) + def export_json(self) -> str: + """Serialize authoritative state to canonical JSON.""" + return json.dumps(self._state, sort_keys=True, separators=(",", ":")) + + def import_json(self, payload: str) -> None: + """Replace authoritative state from a JSON payload.""" + state = _load_state_json(payload) + self._state = state + self._pending = None + self._pending_prompt = None + self._last_exclusive_fact_key = ( + FOCUS_PRIMARY if state[STATE_FACTS][FOCUS_PRIMARY] is not None else None + ) + def step(self, user_input: str) -> Decision: """Process one user input and return a deterministic Decision.""" if self._pending is not None: @@ -247,6 +262,45 @@ def _initial_state() -> State: } +def _load_state_json(payload: str) -> State: + try: + raw = json.loads(payload) + except json.JSONDecodeError as exc: + raise ValueError("Invalid JSON payload.") from exc + + if not isinstance(raw, dict): + raise ValueError("Invalid state payload.") + if set(raw.keys()) != {STATE_FACTS, STATE_POLICIES, STATE_VERSION}: + raise ValueError("Invalid state payload.") + + if raw[STATE_VERSION] != 1: + raise ValueError(f"Unsupported state version: {raw[STATE_VERSION]!r}") + + facts = raw[STATE_FACTS] + policies = raw[STATE_POLICIES] + if not isinstance(facts, dict) or not isinstance(policies, dict): + raise ValueError("Invalid state payload.") + if set(facts.keys()) != {FOCUS_PRIMARY} or set(policies.keys()) != {POLICY_PROHIBIT}: + raise ValueError("Invalid state payload.") + + focus_value = facts[FOCUS_PRIMARY] + prohibit_value = policies[POLICY_PROHIBIT] + if focus_value is not None and not isinstance(focus_value, str): + raise ValueError("Invalid state payload.") + if not isinstance(prohibit_value, list) or any( + not isinstance(item, str) for item in prohibit_value + ): + raise ValueError("Invalid state payload.") + + return { + STATE_FACTS: { + FOCUS_PRIMARY: None if focus_value is None else _clean_fact_value(focus_value) + }, + STATE_POLICIES: {POLICY_PROHIBIT: sorted(set(prohibit_value))}, + STATE_VERSION: 1, + } + + def _clarify(prompt: str) -> Decision: return { "kind": DecisionKind.CLARIFY, diff --git a/tests/test_engine.py b/tests/test_engine.py index 8e08bfc..f485556 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,3 +1,5 @@ +import json + from context_compiler import create_engine from context_compiler.engine import DecisionKind @@ -9,6 +11,164 @@ def test_decision_kind_strenum_behavior() -> None: assert DecisionKind(kind.value) is kind +def test_export_json_returns_complete_representation_of_state() -> None: + engine = create_engine() + engine.step("use Nord Stage 4") + engine.step("don't use docker") + + payload = engine.export_json() + assert json.loads(payload) == { + "facts": {"focus.primary": "Nord Stage 4"}, + "policies": {"prohibit": ["docker"]}, + "version": 1, + } + + +def test_import_json_restores_state_exactly() -> None: + engine = create_engine() + engine.step("use Nord Stage 4") + engine.step("don't use docker") + snapshot = engine.export_json() + + engine.step("clear state") + engine.import_json(snapshot) + + assert engine.state == { + "facts": {"focus.primary": "Nord Stage 4"}, + "policies": {"prohibit": ["docker"]}, + "version": 1, + } + + +def test_export_import_round_trip_preserves_state() -> None: + source = create_engine() + source.step("use Nord Stage 4") + source.step("don't use docker") + + target = create_engine() + target.import_json(source.export_json()) + + assert target.state == source.state + + +def test_imported_state_matches_live_subsequent_behavior() -> None: + live = create_engine() + live.step("use Nord Stage 4") + live.step("don't use parallel octaves") + + imported = create_engine() + imported.import_json(live.export_json()) + + turns = [ + "actually Nord Stage 3", + "don't use voice crossing", + "reset policies", + "allow docker", + ] + for turn in turns: + assert imported.step(turn) == live.step(turn) + assert imported.state == live.state + + +def test_import_json_invalid_json_and_unsupported_version_are_rejected() -> None: + engine = create_engine() + + try: + engine.import_json("{") + raise AssertionError("expected ValueError for invalid JSON") + except ValueError as exc: + assert "Invalid JSON payload" in str(exc) + + try: + engine.import_json( + json.dumps( + { + "facts": {"focus.primary": None}, + "policies": {"prohibit": []}, + "version": 2, + } + ) + ) + raise AssertionError("expected ValueError for unsupported version") + except ValueError as exc: + assert "Unsupported state version" in str(exc) + + +def test_import_json_canonicalizes_duplicate_unsorted_prohibit_values() -> None: + engine = create_engine() + engine.import_json( + json.dumps( + { + "facts": {"focus.primary": None}, + "policies": {"prohibit": ["kubernetes", "docker", "docker"]}, + "version": 1, + } + ) + ) + + assert engine.state == { + "facts": {"focus.primary": None}, + "policies": {"prohibit": ["docker", "kubernetes"]}, + "version": 1, + } + + +def test_import_json_sanitizes_fact_value_like_live_fact_write() -> None: + engine = create_engine() + engine.import_json( + json.dumps( + { + "facts": {"focus.primary": " MacBook M3` "}, + "policies": {"prohibit": []}, + "version": 1, + } + ) + ) + + assert engine.state["facts"]["focus.primary"] == "MacBook M3'" + + +def test_import_json_clears_pending_clarification_state() -> None: + engine = create_engine() + decision = engine.step("no use docker") + assert decision["kind"] == "clarify" + + engine.import_json( + json.dumps( + { + "facts": {"focus.primary": "Nord Stage 4"}, + "policies": {"prohibit": ["kubernetes"]}, + "version": 1, + } + ) + ) + + decision_after_import = engine.step("yes") + assert decision_after_import["kind"] == "passthrough" + assert engine.state == { + "facts": {"focus.primary": "Nord Stage 4"}, + "policies": {"prohibit": ["kubernetes"]}, + "version": 1, + } + + +def test_imported_fact_state_restores_correction_behavior() -> None: + engine = create_engine() + engine.import_json( + json.dumps( + { + "facts": {"focus.primary": "Nord Stage 4"}, + "policies": {"prohibit": []}, + "version": 1, + } + ) + ) + + decision = engine.step("actually Nord Stage 3") + assert decision["kind"] == "update" + assert engine.state["facts"]["focus.primary"] == "Nord Stage 3" + + def test_directive_parsing_positive_negative_and_allow() -> None: engine = create_engine()