Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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 |
Expand Down
22 changes: 16 additions & 6 deletions examples/08_controller_preview_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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__":
Expand Down
21 changes: 20 additions & 1 deletion src/context_compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
]
24 changes: 24 additions & 0 deletions src/context_compiler/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
23 changes: 16 additions & 7 deletions src/context_compiler/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tests/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/):
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/conformance/api/public-api-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 35 additions & 1 deletion tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -198,6 +206,32 @@ 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()

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"),
[
Expand Down