From f2e5b9d7aae83671cb9015096794f54d3b6db418 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 3 Jun 2026 02:51:33 -0400 Subject: [PATCH 1/4] feat: add controller result helpers --- README.md | 18 +++++++------ examples/08_controller_preview_diff.py | 22 +++++++++++----- src/context_compiler/__init__.py | 21 +++++++++++++++- src/context_compiler/controller.py | 24 ++++++++++++++++++ tests/test_controller.py | 35 +++++++++++++++++++++++++- 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d1b0f28..6cd5288 100644 --- a/README.md +++ b/README.md @@ -303,10 +303,14 @@ Controller quick example: ```python from context_compiler import ( - get_decision_state, + diff_has_changes, + get_step_decision, + get_step_state, is_update, + get_preview_state_after, create_engine, preview, + preview_would_mutate, state_diff, step, ) @@ -315,16 +319,16 @@ engine = create_engine() before = engine.state dry_run = preview(engine, "prohibit peanuts") -print(dry_run["would_mutate"]) # True -planned_change = state_diff(before, dry_run["state_after"]) -print(planned_change["changed"]) # True +print(preview_would_mutate(dry_run)) # True +planned_change = state_diff(before, get_preview_state_after(dry_run)) +print(diff_has_changes(planned_change)) # True after_preview = engine.state -print(state_diff(before, after_preview)["changed"]) # False (preview does not mutate state) +print(diff_has_changes(state_diff(before, after_preview))) # False (preview does not mutate state) applied = step(engine, "prohibit peanuts") -print(is_update(applied["decision"])) # True -print(get_decision_state(applied["decision"]) is not None) # True +print(is_update(get_step_decision(applied))) # True +print(get_step_state(applied) is not None) # True ``` | API | Description | diff --git a/examples/08_controller_preview_diff.py b/examples/08_controller_preview_diff.py index bfcd36a..4155fe0 100644 --- a/examples/08_controller_preview_diff.py +++ b/examples/08_controller_preview_diff.py @@ -2,7 +2,17 @@ from _util import print_decision_summary, print_state_summary -from context_compiler import create_engine, preview, state_diff, step +from context_compiler import ( + create_engine, + diff_has_changes, + get_preview_decision, + get_step_decision, + get_step_state, + preview, + preview_would_mutate, + state_diff, + step, +) def main() -> None: @@ -13,17 +23,17 @@ def main() -> None: print("\nPreview: prohibit peanuts") preview_result = preview(engine, "prohibit peanuts") - print("would_mutate:", preview_result["would_mutate"]) - print_decision_summary(preview_result["decision"]) + print("would_mutate:", preview_would_mutate(preview_result)) + print_decision_summary(get_preview_decision(preview_result)) state_after_preview = engine.state diff_after_preview = state_diff(state_before, state_after_preview) - print("state changed after preview:", diff_after_preview["changed"]) + print("state changed after preview:", diff_has_changes(diff_after_preview)) print("\nApply: prohibit peanuts") step_result = step(engine, "prohibit peanuts") - print_decision_summary(step_result["decision"]) - print_state_summary(step_result["state"], "state after step") + print_decision_summary(get_step_decision(step_result)) + print_state_summary(get_step_state(step_result), "state after step") if __name__ == "__main__": diff --git a/src/context_compiler/__init__.py b/src/context_compiler/__init__.py index e9624b7..5bc419e 100644 --- a/src/context_compiler/__init__.py +++ b/src/context_compiler/__init__.py @@ -7,7 +7,20 @@ POLICY_PROHIBIT, POLICY_USE, ) -from .controller import PreviewResult, StepResult, StructuralDiff, preview, state_diff, step +from .controller import ( + PreviewResult, + StepResult, + StructuralDiff, + diff_has_changes, + get_preview_decision, + get_preview_state_after, + get_step_decision, + get_step_state, + preview, + preview_would_mutate, + state_diff, + step, +) from .decision_helpers import ( get_clarify_prompt, get_decision_state, @@ -47,16 +60,22 @@ "StructuralDiff", "Transcript", "TranscriptMessage", + "diff_has_changes", "compile_transcript", "create_engine", "get_clarify_prompt", "get_decision_state", + "get_preview_decision", + "get_preview_state_after", "get_premise_value", "get_policy_items", + "get_step_decision", + "get_step_state", "is_clarify", "is_passthrough", "is_update", "preview", + "preview_would_mutate", "state_diff", "step", ] diff --git a/src/context_compiler/controller.py b/src/context_compiler/controller.py index 7617695..a5c3513 100644 --- a/src/context_compiler/controller.py +++ b/src/context_compiler/controller.py @@ -47,6 +47,30 @@ class PreviewResult(TypedDict): would_mutate: bool +def get_step_decision(step_result: StepResult) -> Decision: + return step_result["decision"] + + +def get_step_state(step_result: StepResult) -> State: + return step_result["state"] + + +def get_preview_decision(preview_result: PreviewResult) -> Decision: + return preview_result["decision"] + + +def get_preview_state_after(preview_result: PreviewResult) -> State: + return preview_result["state_after"] + + +def preview_would_mutate(preview_result: PreviewResult) -> bool: + return preview_result["would_mutate"] + + +def diff_has_changes(diff: StructuralDiff) -> bool: + return diff["changed"] + + def state_diff(before: State, after: State) -> StructuralDiff: before_premise = before["premise"] after_premise = after["premise"] diff --git a/tests/test_controller.py b/tests/test_controller.py index 9f543c5..f60849a 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -3,7 +3,15 @@ import pytest -from context_compiler import create_engine +from context_compiler import ( + create_engine, + diff_has_changes, + get_preview_decision, + get_preview_state_after, + get_step_decision, + get_step_state, + preview_would_mutate, +) from context_compiler.controller import preview, state_diff, step _CONTROLLER_FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" / "controller" @@ -198,6 +206,31 @@ def test_controller_result_surface_contract_stability() -> None: assert preview_result["would_mutate"] is preview_result["diff"]["changed"] +def test_controller_helpers_match_public_result_keys() -> None: + engine = create_engine() + + step_result = step(engine, "set premise concise replies") + assert get_step_decision(step_result) is step_result["decision"] + assert get_step_state(step_result) == step_result["state"] + + preview_result = preview(engine, "use docker") + assert get_preview_decision(preview_result) is preview_result["decision"] + assert get_preview_state_after(preview_result) == preview_result["state_after"] + assert preview_would_mutate(preview_result) is preview_result["would_mutate"] + + diff = state_diff(preview_result["state_before"], preview_result["state_after"]) + assert diff_has_changes(diff) is diff["changed"] + + +def test_controller_helpers_are_importable_from_package_root() -> None: + assert callable(get_step_decision) + assert callable(get_step_state) + assert callable(get_preview_decision) + assert callable(get_preview_state_after) + assert callable(preview_would_mutate) + assert callable(diff_has_changes) + + @pytest.mark.parametrize( ("confirmation", "expected_state", "expected_would_mutate"), [ From ff07b554c894ed4f751745ffb12662c8afada553 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 3 Jun 2026 02:54:17 -0400 Subject: [PATCH 2/4] refactor: use controller accessors in repl --- src/context_compiler/repl.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/context_compiler/repl.py b/src/context_compiler/repl.py index f4cab15..9727f11 100644 --- a/src/context_compiler/repl.py +++ b/src/context_compiler/repl.py @@ -6,7 +6,14 @@ from . import __version__, create_engine, get_policy_items, get_premise_value from .const import DECISION_CLARIFY, DECISION_PASSTHROUGH -from .controller import OUTPUT_VERSION, PreviewResult, StepResult +from .controller import ( + OUTPUT_VERSION, + PreviewResult, + StepResult, + get_preview_decision, + get_step_decision, + preview_would_mutate, +) from .controller import preview as controller_preview from .controller import step as controller_step from .engine import Decision, DecisionKind, Engine, State @@ -125,7 +132,7 @@ def _print_decision_lines(decision: Decision, out_stream: TextIO, *, leading_bla def _render_diff_lines(preview_result: PreviewResult) -> list[str]: diff = preview_result["diff"] - lines = [f"would_mutate: {'yes' if preview_result['would_mutate'] else 'no'}", "diff:"] + lines = [f"would_mutate: {'yes' if preview_would_mutate(preview_result) else 'no'}", "diff:"] premise = diff["premise"] if premise["changed"]: @@ -156,7 +163,7 @@ def _print_preview_lines( if leading_blank: print("", file=out_stream) print(command_name, file=out_stream) - for line in _render_decision_lines(preview_result["decision"]): + for line in _render_decision_lines(get_preview_decision(preview_result)): print(line, file=out_stream) for line in _render_diff_lines(preview_result): print(line, file=out_stream) @@ -364,7 +371,7 @@ def run_repl( payload, active_engine, use_preprocessor=use_preprocessor ) result: StepResult = controller_step(active_engine, compile_input) - _print_decision_lines(result["decision"], out_stream, leading_blank=True) + _print_decision_lines(get_step_decision(result), out_stream, leading_blank=True) continue preview_command = None @@ -399,7 +406,7 @@ def run_repl( user_input, active_engine, use_preprocessor=use_preprocessor ) result = controller_step(active_engine, compile_input) - _print_decision_lines(result["decision"], out_stream, leading_blank=True) + _print_decision_lines(get_step_decision(result), out_stream, leading_blank=True) return for line in in_stream: @@ -475,7 +482,9 @@ def run_repl( if json_mode: _write_json_line(out_stream, _json_step_payload(result, command="step")) else: - _print_decision_lines(result["decision"], out_stream, leading_blank=False) + _print_decision_lines( + get_step_decision(result), out_stream, leading_blank=False + ) continue preview_command = None @@ -526,7 +535,7 @@ def run_repl( if json_mode: _write_json_line(out_stream, _json_step_payload(result, command="input")) else: - _print_decision_lines(result["decision"], out_stream, leading_blank=False) + _print_decision_lines(get_step_decision(result), out_stream, leading_blank=False) def main() -> int: # pragma: no cover From 51c9aa3c13e72ad22d9fb63473077b689ff1d7b7 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 3 Jun 2026 02:56:20 -0400 Subject: [PATCH 3/4] docs: track controller helper parity --- tests/fixtures/README.md | 11 +++++++++++ tests/fixtures/conformance/api/public-api-v1.json | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 126e499..90875b1 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -21,6 +21,15 @@ Ports should check equivalent public exports and methods using language-appropri Behavioral semantics remain covered by conformance and structured fixtures. +The API presence contract includes the public controller helper accessors: + +* `get_step_decision` +* `get_step_state` +* `get_preview_decision` +* `get_preview_state_after` +* `preview_would_mutate` +* `diff_has_changes` + ## Step fixtures For [`conformance/step/`](conformance/step/): @@ -84,6 +93,8 @@ Portable controller contract coverage for: * `state_diff(state_before, state_after)` deterministic structural diff output These fixtures keep a minimal, language-neutral contract matrix for controller APIs. +They intentionally validate the raw controller result envelopes; helper accessors +are covered separately by the public API presence contract above. ## Source of truth diff --git a/tests/fixtures/conformance/api/public-api-v1.json b/tests/fixtures/conformance/api/public-api-v1.json index 18a84fc..8df070f 100644 --- a/tests/fixtures/conformance/api/public-api-v1.json +++ b/tests/fixtures/conformance/api/public-api-v1.json @@ -13,6 +13,12 @@ "is_passthrough", "get_clarify_prompt", "get_decision_state", + "get_step_decision", + "get_step_state", + "get_preview_decision", + "get_preview_state_after", + "preview_would_mutate", + "diff_has_changes", "DECISION_PASSTHROUGH", "DECISION_UPDATE", "DECISION_CLARIFY", From 9e9e3772efcac234cc7e89668b72a6b1545979d7 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 3 Jun 2026 03:01:55 -0400 Subject: [PATCH 4/4] test: cover controller helper contract accessors --- tests/test_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_controller.py b/tests/test_controller.py index f60849a..bdf0662 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -206,6 +206,7 @@ def test_controller_result_surface_contract_stability() -> None: assert preview_result["would_mutate"] is preview_result["diff"]["changed"] +@pytest.mark.contract def test_controller_helpers_match_public_result_keys() -> None: engine = create_engine()