diff --git a/README.md b/README.md index 5394f36..a82a31d 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Use Context Compiler in your host application first: ```python from context_compiler import ( create_engine, - get_error_prompt, + get_error_message, is_error, is_update, ) @@ -148,7 +148,7 @@ user_input = "set premise current project uses uv" decision = engine.step(user_input) if is_error(decision): - show_to_user(get_error_prompt(decision)) + show_to_user(get_error_message(decision)) elif is_update(decision): messages = build_messages(engine.state, user_input) render(call_llm(messages)) @@ -247,10 +247,18 @@ uv run pytest Each user message produces a `Decision`. ```python -class Decision(TypedDict): - kind: Literal["no_directive", "update", "error"] - state: dict | None - prompt_to_user: str | None +class NoDirectiveDecision(TypedDict): + kind: Literal["no_directive"] + +class UpdateDecision(TypedDict): + kind: Literal["update"] + state: State + +class ErrorDecision(TypedDict): + kind: Literal["error"] + message: str + +Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision ``` Meaning: @@ -259,10 +267,10 @@ Meaning: | --- | --- | | no_directive | no canonical directive recognized; no authoritative state change; host decides what to do next | | update | authoritative state mutated; host may use updated state downstream | -| error | show `prompt_to_user` and do not continue normal downstream processing yet | +| error | show `message` and do not continue normal downstream processing yet | For normal app code, prefer the exported decision helpers (`is_error`, -`is_update`, `is_no_directive`, `get_error_prompt`, `get_decision_state`) +`is_update`, `is_no_directive`, `get_error_message`, `get_decision_state`) instead of direct key traversal. See [docs/api-reference.md](docs/api-reference.md) for the full public API @@ -274,7 +282,7 @@ Common API entry points: `engine.premise`, `engine.policies`, `engine.export_json(...)`, `engine.import_json(...)` - decision helpers: `is_error(...)`, `is_update(...)`, `is_no_directive(...)`, - `get_error_prompt(...)`, `get_decision_state(...)` + `get_error_message(...)`, `get_decision_state(...)` - state transport: `engine.export_json(...)`, `engine.import_json(...)` - controller API: `step(...)` - audit APIs: `preview(...)`, `state_diff(...)` diff --git a/demos/01_llm_contradiction_error.py b/demos/01_llm_contradiction_error.py index 479a85b..0003a4b 100644 --- a/demos/01_llm_contradiction_error.py +++ b/demos/01_llm_contradiction_error.py @@ -1,6 +1,6 @@ """Demo 1: compiler blocks contradictory directives before model call.""" -from context_compiler import create_engine +from context_compiler import create_engine, is_error from demos.common import ( build_baseline_messages, build_mediated_messages_from_transcript, @@ -62,9 +62,9 @@ def main() -> None: reinjected_output = complete_messages(reinjected_messages) print_model_output("Reinjected-state", reinjected_output) - if second["kind"] == "error": + if is_error(second): print_messages("compiler-mediated (full)", []) - mediated_output = f"[no call] error required: {second['prompt_to_user']}\nACTION:error" + mediated_output = f"[no call] error required: {second['message']}\nACTION:error" print_model_output("Compiler-mediated (full)", mediated_output) else: mediated_messages = build_mediated_messages_from_transcript(engine.state, user_inputs) @@ -89,7 +89,7 @@ def main() -> None: compact_action = extract_tag_value(compact_output, "ACTION") baseline_respects = baseline_action is not None and baseline_action.lower() == "error" reinjected_respects = reinjected_action is not None and reinjected_action.lower() == "error" - compiler_host_blocked = second["kind"] == "error" + compiler_host_blocked = is_error(second) mediated_respects = compiler_host_blocked compact_respects = compacted_prompt is not None or ( compact_action is not None and compact_action.lower() == "error" diff --git a/demos/06_llm_context_compaction.py b/demos/06_llm_context_compaction.py index 53e53bd..e13f36c 100644 --- a/demos/06_llm_context_compaction.py +++ b/demos/06_llm_context_compaction.py @@ -1,7 +1,6 @@ """Demo 6: host-side prompt replacement from authoritative step-derived state.""" -from context_compiler import create_engine -from context_compiler.engine import DecisionKind +from context_compiler import DECISION_UPDATE, create_engine from demos.common import compact_user_turns, is_verbose, print_info_report DEMO_NAME = "06_context_compaction — superseded directives eliminated" @@ -44,7 +43,7 @@ def _compile_premise(turns: list[str]) -> str: engine = create_engine() for turn in turns: decision = engine.step(turn) - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE compiled_premise = engine.premise assert compiled_premise is not None return compiled_premise diff --git a/demos/08_llm_replacement_precondition.py b/demos/08_llm_replacement_precondition.py index 2455367..3ac0925 100644 --- a/demos/08_llm_replacement_precondition.py +++ b/demos/08_llm_replacement_precondition.py @@ -1,6 +1,6 @@ """Demo 8: missing-source replacement applies deterministically from authoritative state.""" -from context_compiler import DECISION_UPDATE, State, create_engine +from context_compiler import State, create_engine, is_update from demos.common import ( build_baseline_messages, build_reinjected_messages, @@ -61,7 +61,7 @@ def main() -> None: reinjected_output = complete_messages(reinjected_messages) print_model_output("Reinjected-state", reinjected_output) - if decision["kind"] == DECISION_UPDATE: + if is_update(decision): print_messages("compiler-mediated (full)", []) mediated_output = "[no call] authoritative state applied deterministic replacement update" print_model_output("Compiler-mediated (full)", mediated_output) @@ -86,7 +86,7 @@ def main() -> None: baseline_has_authoritative_precondition = False reinjected_has_authoritative_precondition = False - compiler_pass = decision["kind"] == DECISION_UPDATE and state_applied + compiler_pass = is_update(decision) and state_applied compact_pass = compacted_prompt is None and compact_state_applied and compact_no_pending print_host_check( @@ -101,7 +101,7 @@ def main() -> None: ) print_host_check( "COMPILER_BLOCKED_INVALID_REPLACEMENT", - yes_no(decision["kind"] == DECISION_UPDATE), + yes_no(is_update(decision)), context="compiler-mediated", ) print_host_check( diff --git a/demos/09_llm_confirmation_no_directive.py b/demos/09_llm_confirmation_no_directive.py index 4420d4d..ae888a3 100644 --- a/demos/09_llm_confirmation_no_directive.py +++ b/demos/09_llm_confirmation_no_directive.py @@ -1,10 +1,10 @@ """Demo 9: confirmation-style followups remain ordinary no_directive.""" from context_compiler import ( - DECISION_NO_DIRECTIVE, - DECISION_UPDATE, State, create_engine, + is_no_directive, + is_update, ) from demos.common import ( build_baseline_messages, @@ -98,11 +98,9 @@ def main() -> None: ) print_model_output("Compiler-mediated + compact", compact_output) - deterministic_initial_update = first["kind"] == DECISION_UPDATE and state_applied_after_first - unrelated_followup_no_directive = ( - second["kind"] == DECISION_NO_DIRECTIVE and state_preserved_after_second - ) - confirmation_token_not_consumed = third["kind"] == DECISION_NO_DIRECTIVE + deterministic_initial_update = is_update(first) and state_applied_after_first + unrelated_followup_no_directive = is_no_directive(second) and state_preserved_after_second + confirmation_token_not_consumed = is_no_directive(third) deterministic_final_state = _has_podman_use(engine.state) baseline_has_confirmation_state_machine = False diff --git a/demos/common.py b/demos/common.py index 2049a85..959469d 100644 --- a/demos/common.py +++ b/demos/common.py @@ -5,11 +5,11 @@ from typing import Literal, NotRequired, TypedDict from context_compiler import ( - DECISION_ERROR, - DECISION_UPDATE, Decision, State, create_engine, + is_error, + is_update, ) from demos.llm_client import Message @@ -80,14 +80,14 @@ def print_decision(title: str, decision: Decision, state: State) -> None: if not is_verbose(): return print(f"Compiler decision ({title}):") - if decision["kind"] == DECISION_UPDATE: + if is_update(decision): print("result: updated") _print_state_summary(state) - elif decision["kind"] == DECISION_ERROR: + elif is_error(decision): print("result: error") - prompt = decision["prompt_to_user"] - if prompt: - _print_multiline_prompt("error prompt", prompt) + message = decision["message"] + if message: + _print_multiline_prompt("error message", message) _print_state_summary(state) else: print("result: no_directive") @@ -255,24 +255,24 @@ def compact_user_turns( - drop update lines - keep no_directive lines - keep first error line and stop - - return prompt_to_user for error, else None + - return message for error, else None - returned state is engine state at stop point """ engine = create_engine() compacted_turns: list[str] = [] - prompt_to_user: str | None = None + message: str | None = None for turn in user_turns: decision = engine.step(turn) - if decision["kind"] == DECISION_UPDATE: + if is_update(decision): continue compacted_turns.append(turn) - if decision["kind"] == DECISION_ERROR: - prompt_to_user = decision["prompt_to_user"] + if is_error(decision): + message = decision["message"] break - return compacted_turns, engine.state, prompt_to_user + return compacted_turns, engine.state, message def build_mediated_messages_from_transcript( diff --git a/docs/DirectiveGrammarSpec.md b/docs/DirectiveGrammarSpec.md index 5fe2c79..0180870 100644 --- a/docs/DirectiveGrammarSpec.md +++ b/docs/DirectiveGrammarSpec.md @@ -66,10 +66,18 @@ The host: ## 4. Decision API Contract ```python -class Decision(TypedDict): - kind: Literal["no_directive", "update", "error"] - state: dict | None - prompt_to_user: str | None +class NoDirectiveDecision(TypedDict): + kind: Literal["no_directive"] + +class UpdateDecision(TypedDict): + kind: Literal["update"] + state: State + +class ErrorDecision(TypedDict): + kind: Literal["error"] + message: str + +Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision ``` Semantics: diff --git a/docs/api-reference.md b/docs/api-reference.md index 8374557..72eb8ab 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -126,10 +126,18 @@ state through the returned mapping. Each user message produces a `Decision`. ```python -class Decision(TypedDict): - kind: Literal["no_directive", "update", "error"] - state: dict | None - prompt_to_user: str | None +class NoDirectiveDecision(TypedDict): + kind: Literal["no_directive"] + +class UpdateDecision(TypedDict): + kind: Literal["update"] + state: State + +class ErrorDecision(TypedDict): + kind: Literal["error"] + message: str + +Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision ``` Decision kinds: @@ -138,25 +146,25 @@ Decision kinds: | --- | --- | | `no_directive` | no canonical directive recognized; no authoritative state change; host decides what to do next | | `update` | authoritative state changed; host may apply downstream behavior using updated state | -| `error` | show `prompt_to_user`; do not continue normal downstream processing yet | +| `error` | show `message`; do not continue normal downstream processing yet | Helper functions: - `is_no_directive(decision)` - `is_update(decision)` - `is_error(decision)` -- `get_error_prompt(decision)` +- `get_error_message(decision)` - `get_decision_state(decision)` Typical use: ```python -from context_compiler import get_error_prompt, is_error, is_update +from context_compiler import get_error_message, is_error, is_update decision = engine.step(user_input) if is_error(decision): - show_to_user(get_error_prompt(decision)) + show_to_user(get_error_message(decision)) elif is_update(decision): apply_runtime_rules() ``` @@ -294,4 +302,4 @@ Public result and data object names exported at package root include: These names are part of the public package surface. For the exact portable API export contract used by tests and ports, see -[tests/fixtures/conformance/api/public-api-v1.json](../tests/fixtures/conformance/api/public-api-v1.json). +[tests/fixtures/conformance/api/public-api-v2.json](../tests/fixtures/conformance/api/public-api-v2.json). diff --git a/evals/swe-bench/swe-bench.py b/evals/swe-bench/swe-bench.py index e6b7e2d..d9c71de 100644 --- a/evals/swe-bench/swe-bench.py +++ b/evals/swe-bench/swe-bench.py @@ -33,7 +33,7 @@ from pathlib import Path from typing import Any, cast -from context_compiler import create_engine +from context_compiler import create_engine, is_error RUBRIC_WEIGHTS: dict[str, int] = { "Correct fix locus": 2, @@ -701,12 +701,12 @@ def main() -> None: error_result: dict[str, Any] | None = None for index, directive in enumerate(task.directives): decision = engine.step(directive) - if str(decision["kind"]) == "error": + if is_error(decision): error_result = { "error": "compiler_lane_error", "directive_index": index, "directive": directive, - "prompt_to_user": decision.get("prompt_to_user"), + "message": decision["message"], } break diff --git a/examples/03_ambiguity_with_error.py b/examples/03_ambiguity_with_error.py index e58ea15..c9b45f9 100644 --- a/examples/03_ambiguity_with_error.py +++ b/examples/03_ambiguity_with_error.py @@ -2,7 +2,7 @@ from _util import print_decision_summary, print_state_summary -from context_compiler import create_engine, get_error_prompt, is_error +from context_compiler import create_engine, get_error_message, is_error def fake_llm(user_input: str) -> str: @@ -25,7 +25,7 @@ def main() -> None: if is_error(decision2): print("Host behavior: error returned, do NOT call LLM.") - print(f"Error prompt: {get_error_prompt(decision2)}") + print(f"Error message: {get_error_message(decision2)}") else: fake_llm("use peanuts") print() diff --git a/examples/05_llm_integration_pattern.py b/examples/05_llm_integration_pattern.py index 953bfe0..294f4f6 100644 --- a/examples/05_llm_integration_pattern.py +++ b/examples/05_llm_integration_pattern.py @@ -7,7 +7,7 @@ State, create_engine, get_decision_state, - get_error_prompt, + get_error_message, is_error, is_no_directive, is_update, @@ -38,7 +38,7 @@ def handle_turn(engine_input: str, engine: Engine) -> None: fake_llm(get_decision_state(decision), engine_input) elif is_error(decision): print("Host action: error -> show prompt, DO NOT call LLM") - print("error prompt:", get_error_prompt(decision)) + print("error message:", get_error_message(decision)) print() diff --git a/examples/_util.py b/examples/_util.py index 5bcaea1..c1c4079 100644 --- a/examples/_util.py +++ b/examples/_util.py @@ -5,7 +5,7 @@ POLICY_PROHIBIT, POLICY_USE, get_decision_state, - get_error_prompt, + get_error_message, is_error, is_update, ) @@ -46,9 +46,9 @@ def print_decision_summary(decision: Any) -> None: if is_error(decision): print("result: error") - prompt = get_error_prompt(decision) + prompt = get_error_message(decision) if isinstance(prompt, str) and prompt: - print("error prompt:") + print("error message:") for line in prompt.splitlines(): print(f"- {line}") return diff --git a/src/context_compiler/__init__.py b/src/context_compiler/__init__.py index da6112a..a01754e 100644 --- a/src/context_compiler/__init__.py +++ b/src/context_compiler/__init__.py @@ -15,17 +15,19 @@ ) from .decision_helpers import ( get_decision_state, - get_error_prompt, + get_error_message, is_error, is_no_directive, is_update, ) from .engine import ( Decision, - DecisionKind, Engine, + ErrorDecision, + NoDirectiveDecision, PolicyValue, State, + UpdateDecision, create_engine, ) @@ -33,18 +35,20 @@ __all__ = [ "Decision", - "DecisionKind", "DECISION_ERROR", "DECISION_NO_DIRECTIVE", "DECISION_UPDATE", + "ErrorDecision", "Engine", + "NoDirectiveDecision", "POLICY_PROHIBIT", "POLICY_USE", "PolicyValue", "State", "StepResult", + "UpdateDecision", "create_engine", - "get_error_prompt", + "get_error_message", "get_decision_state", "get_step_decision", "get_step_state", diff --git a/src/context_compiler/decision_helpers.py b/src/context_compiler/decision_helpers.py index d1bfd29..afe78d2 100644 --- a/src/context_compiler/decision_helpers.py +++ b/src/context_compiler/decision_helpers.py @@ -1,24 +1,36 @@ """Public helpers for safer decision inspection in host-side code.""" +from typing import TypeGuard + from .const import DECISION_ERROR, DECISION_NO_DIRECTIVE, DECISION_UPDATE -from .engine import Decision, State +from .engine import ( + Decision, + ErrorDecision, + NoDirectiveDecision, + State, + UpdateDecision, +) -def is_update(decision: Decision) -> bool: +def is_update(decision: Decision) -> TypeGuard[UpdateDecision]: return decision["kind"] == DECISION_UPDATE -def is_error(decision: Decision) -> bool: +def is_error(decision: Decision) -> TypeGuard[ErrorDecision]: return decision["kind"] == DECISION_ERROR -def is_no_directive(decision: Decision) -> bool: +def is_no_directive(decision: Decision) -> TypeGuard[NoDirectiveDecision]: return decision["kind"] == DECISION_NO_DIRECTIVE -def get_error_prompt(decision: Decision) -> str | None: - return decision["prompt_to_user"] +def get_error_message(decision: Decision) -> str | None: + if not is_error(decision): + return None + return decision["message"] def get_decision_state(decision: Decision) -> State | None: + if not is_update(decision): + return None return decision["state"] diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index b58e1fa..3bddcb1 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -5,11 +5,13 @@ from collections.abc import Mapping from copy import deepcopy from dataclasses import dataclass -from enum import StrEnum from typing import Literal, TypedDict from unicodedata import normalize as unicode_normalize from .const import ( + DECISION_ERROR, + DECISION_NO_DIRECTIVE, + DECISION_UPDATE, POLICY_PROHIBIT, POLICY_USE, SCHEMA_VERSION, @@ -30,16 +32,21 @@ class State(TypedDict): version: Literal[2] -class DecisionKind(StrEnum): - UPDATE = "update" - NO_DIRECTIVE = "no_directive" - ERROR = "error" +class NoDirectiveDecision(TypedDict): + kind: Literal["no_directive"] -class Decision(TypedDict): - kind: DecisionKind - state: State | None - prompt_to_user: str | None +class UpdateDecision(TypedDict): + kind: Literal["update"] + state: State + + +class ErrorDecision(TypedDict): + kind: Literal["error"] + message: str + + +Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision @dataclass(frozen=True) @@ -61,11 +68,7 @@ class Action: old_item: str | None = None -_NO_DIRECTIVE: Decision = { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, -} +_NO_DIRECTIVE: NoDirectiveDecision = {"kind": DECISION_NO_DIRECTIVE} def create_engine(state: State | None = None) -> "Engine": @@ -355,17 +358,9 @@ def _normalize_item(value: str) -> str: return normalized.strip() -def _error(prompt: str) -> Decision: - return { - "kind": DecisionKind.ERROR, - "state": None, - "prompt_to_user": prompt, - } +def _error(message: str) -> ErrorDecision: + return {"kind": DECISION_ERROR, "message": message} -def _update_decision(state: State) -> Decision: - return { - "kind": DecisionKind.UPDATE, - "state": deepcopy(state), - "prompt_to_user": None, - } +def _update_decision(state: State) -> UpdateDecision: + return {"kind": DECISION_UPDATE, "state": deepcopy(state)} diff --git a/src/context_compiler/repl.py b/src/context_compiler/repl.py index e54b97b..a84dc16 100644 --- a/src/context_compiler/repl.py +++ b/src/context_compiler/repl.py @@ -3,7 +3,7 @@ from typing import TextIO from . import __version__, create_engine -from .const import DECISION_ERROR, DECISION_NO_DIRECTIVE +from .const import DECISION_ERROR from .controller import ( OUTPUT_VERSION, PreviewResult, @@ -14,7 +14,8 @@ ) from .controller import preview as controller_preview from .controller import step as controller_step -from .engine import Decision, DecisionKind, Engine, State +from .decision_helpers import is_error, is_no_directive, is_update +from .engine import Decision, Engine, State _EXIT_TOKENS = {"exit", "quit"} _HELP_TOKENS = {"help", "?"} @@ -44,11 +45,7 @@ def _has_embedded_newline(raw_line: str) -> bool: def _multi_command_decision() -> Decision: - return { - "kind": DecisionKind.ERROR, - "state": None, - "prompt_to_user": _MULTI_COMMAND_PROMPT, - } + return {"kind": DECISION_ERROR, "message": _MULTI_COMMAND_PROMPT} def _print_interactive_help(out_stream: TextIO) -> None: @@ -89,16 +86,15 @@ def _render_state_lines(state: State) -> list[str]: def _render_decision_lines(decision: Decision) -> list[str]: - kind = decision["kind"] - if kind == DECISION_NO_DIRECTIVE: + if is_no_directive(decision): return ["no_directive"] - if kind == DECISION_ERROR: - prompt = decision["prompt_to_user"] or "" - prompt_lines = prompt.splitlines() if prompt else [""] + if is_error(decision): + message = decision["message"] + prompt_lines = message.splitlines() if message else [""] return [f"error: {prompt_lines[0]}", *prompt_lines[1:]] + assert is_update(decision) state = decision["state"] - assert state is not None return ["updated", *_render_state_lines(state)] diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 01f7b6a..44c1f70 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -15,7 +15,7 @@ surface, rather than as Python-only test inputs. ## API contract fixtures -[`conformance/api/public-api-v1.json`](conformance/api/public-api-v1.json) defines the current portable core root public API contract for Python and ports. +[`conformance/api/public-api-v2.json`](conformance/api/public-api-v2.json) defines the current portable core root public API contract for Python and ports. [`conformance/api/public-audit-v1.json`](conformance/api/public-audit-v1.json) defines the current portable audit-module public API contract for Python and ports. @@ -60,6 +60,12 @@ Then asserts: * returned `Decision` * final `engine.state` +The `Decision` payload in this family is a discriminated union: + +* `{"kind":"no_directive"}` +* `{"kind":"update","state": ...}` +* `{"kind":"error","message": ...}` + The current runner enforces a closed fixture shape for this family. Unknown top-level and documented nested fields are rejected. @@ -101,6 +107,9 @@ above. The current runner enforces a closed fixture shape for this family. Unknown top-level, action, expected, and documented result/diff fields are rejected. +Embedded controller `decision` values use the same discriminated union contract +as the core engine step fixtures above. + ## Mutation-isolation fixtures For [`conformance/mutation-isolation/`](conformance/mutation-isolation/): diff --git a/tests/fixtures/conformance/api/public-api-v1.json b/tests/fixtures/conformance/api/public-api-v2.json similarity index 89% rename from tests/fixtures/conformance/api/public-api-v1.json rename to tests/fixtures/conformance/api/public-api-v2.json index 5bde0e9..8bdfd51 100644 --- a/tests/fixtures/conformance/api/public-api-v1.json +++ b/tests/fixtures/conformance/api/public-api-v2.json @@ -1,5 +1,5 @@ { - "id": "public-api-v1", + "id": "public-api-v2", "kind": "api-contract", "target": "context-compiler-ports", "module": "context_compiler", @@ -13,18 +13,20 @@ "mode": "exact", "names": [ "Decision", - "DecisionKind", "DECISION_ERROR", "DECISION_NO_DIRECTIVE", "DECISION_UPDATE", + "ErrorDecision", "Engine", + "NoDirectiveDecision", "POLICY_PROHIBIT", "POLICY_USE", "PolicyValue", "State", "StepResult", + "UpdateDecision", "create_engine", - "get_error_prompt", + "get_error_message", "get_decision_state", "get_step_decision", "get_step_state", @@ -35,10 +37,7 @@ ], "members": { "Decision": { - "kind": "type" - }, - "DecisionKind": { - "kind": "class" + "kind": "type_alias" }, "DECISION_ERROR": { "kind": "constant", @@ -52,9 +51,15 @@ "kind": "constant", "value": "update" }, + "ErrorDecision": { + "kind": "type" + }, "Engine": { "kind": "class" }, + "NoDirectiveDecision": { + "kind": "type" + }, "POLICY_PROHIBIT": { "kind": "constant", "value": "prohibit" @@ -72,6 +77,9 @@ "StepResult": { "kind": "type" }, + "UpdateDecision": { + "kind": "type" + }, "create_engine": { "kind": "callable", "signature": { @@ -92,7 +100,7 @@ } ] }, - "get_error_prompt": { + "get_error_message": { "kind": "callable", "signature": { "params": [ @@ -108,8 +116,7 @@ "kwargs": { "decision": { "kind": "error", - "state": null, - "prompt_to_user": "confirm?" + "message": "confirm?" } }, "return_shape": { @@ -135,7 +142,6 @@ "kwargs": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { @@ -201,8 +207,7 @@ "kwargs": { "decision": { "kind": "error", - "state": null, - "prompt_to_user": "confirm?" + "message": "confirm?" } }, "return_shape": { @@ -227,9 +232,7 @@ { "kwargs": { "decision": { - "kind": "no_directive", - "state": null, - "prompt_to_user": null + "kind": "no_directive" } }, "return_shape": { @@ -255,7 +258,6 @@ "kwargs": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": {}, @@ -323,35 +325,35 @@ "public_members": { "mode": "exact", "members": { - "export_json": { - "kind": "method", - "signature": { - "params": [] - } - }, - "import_json": { - "kind": "method", - "signature": { + "export_json": { + "kind": "method", + "signature": { + "params": [] + } + }, + "import_json": { + "kind": "method", + "signature": { "params": [ { "name": "payload", "kind": "POSITIONAL_OR_KEYWORD", "has_default": false - } - ] - } - }, - "policies": { - "kind": "property" - }, - "premise": { - "kind": "property" - }, - "state": { - "kind": "property" - }, - "step": { - "kind": "method", + } + ] + } + }, + "policies": { + "kind": "property" + }, + "premise": { + "kind": "property" + }, + "state": { + "kind": "property" + }, + "step": { + "kind": "method", "signature": { "params": [ { diff --git a/tests/fixtures/conformance/controller/controller_preview_affirmative_followup_no_directive_after_replace_update.json b/tests/fixtures/conformance/controller/controller_preview_affirmative_followup_no_directive_after_replace_update.json index 9c18328..a153585 100644 --- a/tests/fixtures/conformance/controller/controller_preview_affirmative_followup_no_directive_after_replace_update.json +++ b/tests/fixtures/conformance/controller/controller_preview_affirmative_followup_no_directive_after_replace_update.json @@ -18,9 +18,7 @@ "output_version": 1, "mode": "preview", "decision": { - "kind": "no_directive", - "state": null, - "prompt_to_user": null + "kind": "no_directive" }, "state_before": { "premise": null, diff --git a/tests/fixtures/conformance/controller/controller_preview_error_reports_non_mutating_and_restores_live_state.json b/tests/fixtures/conformance/controller/controller_preview_error_reports_non_mutating_and_restores_live_state.json index d26fbd0..15b74bb 100644 --- a/tests/fixtures/conformance/controller/controller_preview_error_reports_non_mutating_and_restores_live_state.json +++ b/tests/fixtures/conformance/controller/controller_preview_error_reports_non_mutating_and_restores_live_state.json @@ -22,8 +22,7 @@ "kubectl": "use" }, "version": 2 - }, - "prompt_to_user": null + } }, "state_before": { "premise": null, diff --git a/tests/fixtures/conformance/controller/controller_preview_idempotent_update_reports_non_mutating.json b/tests/fixtures/conformance/controller/controller_preview_idempotent_update_reports_non_mutating.json index a7f0915..df62fa3 100644 --- a/tests/fixtures/conformance/controller/controller_preview_idempotent_update_reports_non_mutating.json +++ b/tests/fixtures/conformance/controller/controller_preview_idempotent_update_reports_non_mutating.json @@ -25,8 +25,7 @@ "docker": "use" }, "version": 2 - }, - "prompt_to_user": null + } }, "state_before": { "premise": null, diff --git a/tests/fixtures/conformance/controller/controller_preview_mutating_update_reports_would_mutate_and_no_live_mutation.json b/tests/fixtures/conformance/controller/controller_preview_mutating_update_reports_would_mutate_and_no_live_mutation.json index 49f5cd0..5cb4a39 100644 --- a/tests/fixtures/conformance/controller/controller_preview_mutating_update_reports_would_mutate_and_no_live_mutation.json +++ b/tests/fixtures/conformance/controller/controller_preview_mutating_update_reports_would_mutate_and_no_live_mutation.json @@ -20,8 +20,7 @@ "premise": "concise replies", "policies": {}, "version": 2 - }, - "prompt_to_user": null + } }, "state_before": { "premise": null, diff --git a/tests/fixtures/conformance/controller/controller_preview_replace_prohibited_old_matches_execution_without_mutation.json b/tests/fixtures/conformance/controller/controller_preview_replace_prohibited_old_matches_execution_without_mutation.json index ecdae79..36be470 100644 --- a/tests/fixtures/conformance/controller/controller_preview_replace_prohibited_old_matches_execution_without_mutation.json +++ b/tests/fixtures/conformance/controller/controller_preview_replace_prohibited_old_matches_execution_without_mutation.json @@ -20,8 +20,7 @@ "mode": "preview", "decision": { "kind": "error", - "state": null, - "prompt_to_user": "\"docker\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." + "message": "\"docker\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." }, "state_before": { "premise": null, diff --git a/tests/fixtures/conformance/controller/controller_step_update_envelope_and_state_snapshot.json b/tests/fixtures/conformance/controller/controller_step_update_envelope_and_state_snapshot.json index 6adfcc5..da0d96a 100644 --- a/tests/fixtures/conformance/controller/controller_step_update_envelope_and_state_snapshot.json +++ b/tests/fixtures/conformance/controller/controller_step_update_envelope_and_state_snapshot.json @@ -20,8 +20,7 @@ "premise": "concise replies", "policies": {}, "version": 2 - }, - "prompt_to_user": null + } }, "state": { "premise": "concise replies", diff --git a/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_normalized_token.json b/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_normalized_token.json index bcd7f2b..d61f487 100644 --- a/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_normalized_token.json +++ b/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_normalized_token.json @@ -12,9 +12,7 @@ "input": " YES!!! ", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_punctuation_token.json b/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_punctuation_token.json index 0cb57f2..d3497bb 100644 --- a/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_punctuation_token.json +++ b/tests/fixtures/conformance/step/step_affirmative_followup_no_directive_punctuation_token.json @@ -12,9 +12,7 @@ "input": " okay!!! ", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_boundary_whitespace_trim_use_update.json b/tests/fixtures/conformance/step/step_boundary_whitespace_trim_use_update.json index 83ac24d..cbbb28f 100644 --- a/tests/fixtures/conformance/step/step_boundary_whitespace_trim_use_update.json +++ b/tests/fixtures/conformance/step/step_boundary_whitespace_trim_use_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_change_premise_missing_error.json b/tests/fixtures/conformance/step/step_change_premise_missing_error.json index e8e6daa..89f3afa 100644 --- a/tests/fixtures/conformance/step/step_change_premise_missing_error.json +++ b/tests/fixtures/conformance/step/step_change_premise_missing_error.json @@ -10,8 +10,7 @@ "expected": { "decision": { "kind": "error", - "prompt_to_user": null, - "state": null + "message": "No premise is set.\nUse 'set premise ' to define one." }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_change_premise_update.json b/tests/fixtures/conformance/step/step_change_premise_update.json index 81f3280..70cb89a 100644 --- a/tests/fixtures/conformance/step/step_change_premise_update.json +++ b/tests/fixtures/conformance/step/step_change_premise_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": "concise replies", "policies": {}, diff --git a/tests/fixtures/conformance/step/step_clear_premise_already_null_update.json b/tests/fixtures/conformance/step/step_clear_premise_already_null_update.json index 591da8f..ca5ee8b 100644 --- a/tests/fixtures/conformance/step/step_clear_premise_already_null_update.json +++ b/tests/fixtures/conformance/step/step_clear_premise_already_null_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_clear_premise_populated_update.json b/tests/fixtures/conformance/step/step_clear_premise_populated_update.json index fc05c92..de05a0f 100644 --- a/tests/fixtures/conformance/step/step_clear_premise_populated_update.json +++ b/tests/fixtures/conformance/step/step_clear_premise_populated_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_clear_state_already_empty_update.json b/tests/fixtures/conformance/step/step_clear_state_already_empty_update.json index f7606a5..64110a5 100644 --- a/tests/fixtures/conformance/step/step_clear_state_already_empty_update.json +++ b/tests/fixtures/conformance/step/step_clear_state_already_empty_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/step/step_clear_state_populated_update.json b/tests/fixtures/conformance/step/step_clear_state_populated_update.json index 54d261c..8888198 100644 --- a/tests/fixtures/conformance/step/step_clear_state_populated_update.json +++ b/tests/fixtures/conformance/step/step_clear_state_populated_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/step/step_compound_clear_state_then_set_premise_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_clear_state_then_set_premise_invalid_boundary.json index 7797d62..28e533b 100644 --- a/tests/fixtures/conformance/step/step_compound_clear_state_then_set_premise_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_clear_state_then_set_premise_invalid_boundary.json @@ -11,9 +11,7 @@ "input": "clear state then set premise project", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": "baseline", diff --git a/tests/fixtures/conformance/step/step_compound_remove_policy_and_prohibit_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_remove_policy_and_prohibit_invalid_boundary.json index 6de0e58..9b9b05c 100644 --- a/tests/fixtures/conformance/step/step_compound_remove_policy_and_prohibit_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_remove_policy_and_prohibit_invalid_boundary.json @@ -11,9 +11,7 @@ "input": "remove policy docker and prohibit peanuts", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json index 65bc2a8..89acb74 100644 --- a/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json @@ -9,9 +9,7 @@ "input": "set premise vegetarian and use docker", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_compound_use_and_prohibit_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_use_and_prohibit_invalid_boundary.json index d5fbf8e..ddbe174 100644 --- a/tests/fixtures/conformance/step/step_compound_use_and_prohibit_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_use_and_prohibit_invalid_boundary.json @@ -9,9 +9,7 @@ "input": "use docker and prohibit peanuts", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_compound_use_or_prohibit_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_use_or_prohibit_invalid_boundary.json index ba6d101..491a245 100644 --- a/tests/fixtures/conformance/step/step_compound_use_or_prohibit_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_use_or_prohibit_invalid_boundary.json @@ -9,9 +9,7 @@ "input": "use docker or prohibit peanuts", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_compound_use_punctuation_prohibit_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_use_punctuation_prohibit_invalid_boundary.json index 5f8a6e1..8d21ad7 100644 --- a/tests/fixtures/conformance/step/step_compound_use_punctuation_prohibit_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_use_punctuation_prohibit_invalid_boundary.json @@ -9,9 +9,7 @@ "input": "use docker. prohibit peanuts", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_compound_use_xor_prohibit_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_use_xor_prohibit_invalid_boundary.json index 999fbe1..82099a3 100644 --- a/tests/fixtures/conformance/step/step_compound_use_xor_prohibit_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_use_xor_prohibit_invalid_boundary.json @@ -9,9 +9,7 @@ "input": "use docker xor prohibit peanuts", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_conflict_prohibit_error.json b/tests/fixtures/conformance/step/step_conflict_prohibit_error.json index 38e4c00..6b349e7 100644 --- a/tests/fixtures/conformance/step/step_conflict_prohibit_error.json +++ b/tests/fixtures/conformance/step/step_conflict_prohibit_error.json @@ -12,8 +12,7 @@ "expected": { "decision": { "kind": "error", - "prompt_to_user": null, - "state": null + "message": "\"docker\" is currently in use.\nRemove or replace it before prohibiting it." }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_exact_prefix_no_directive_leading_space.json b/tests/fixtures/conformance/step/step_exact_prefix_no_directive_leading_space.json index 9d99819..c84bd29 100644 --- a/tests/fixtures/conformance/step/step_exact_prefix_no_directive_leading_space.json +++ b/tests/fixtures/conformance/step/step_exact_prefix_no_directive_leading_space.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": "concise", "policies": {}, diff --git a/tests/fixtures/conformance/step/step_independent_followup_no_directive_after_replace_update.json b/tests/fixtures/conformance/step/step_independent_followup_no_directive_after_replace_update.json index db9dbcd..3aec542 100644 --- a/tests/fixtures/conformance/step/step_independent_followup_no_directive_after_replace_update.json +++ b/tests/fixtures/conformance/step/step_independent_followup_no_directive_after_replace_update.json @@ -12,9 +12,7 @@ "input": "sounds good", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_keyword_case_normalization_use_update.json b/tests/fixtures/conformance/step/step_keyword_case_normalization_use_update.json index 7e99ec0..0faa097 100644 --- a/tests/fixtures/conformance/step/step_keyword_case_normalization_use_update.json +++ b/tests/fixtures/conformance/step/step_keyword_case_normalization_use_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_natural_language_request_no_directive.json b/tests/fixtures/conformance/step/step_natural_language_request_no_directive.json index 1f0c719..81cf58f 100644 --- a/tests/fixtures/conformance/step/step_natural_language_request_no_directive.json +++ b/tests/fixtures/conformance/step/step_natural_language_request_no_directive.json @@ -9,9 +9,7 @@ "input": "please use docker", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_near_miss_change_premise_missing_to.json b/tests/fixtures/conformance/step/step_near_miss_change_premise_missing_to.json index 3e2ac85..f86bb72 100644 --- a/tests/fixtures/conformance/step/step_near_miss_change_premise_missing_to.json +++ b/tests/fixtures/conformance/step/step_near_miss_change_premise_missing_to.json @@ -9,9 +9,7 @@ "input": "change premise concise", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_near_miss_set_premise_to.json b/tests/fixtures/conformance/step/step_near_miss_set_premise_to.json index c2a8ff2..a997aa0 100644 --- a/tests/fixtures/conformance/step/step_near_miss_set_premise_to.json +++ b/tests/fixtures/conformance/step/step_near_miss_set_premise_to.json @@ -9,9 +9,7 @@ "input": "set premise to concise", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_negative_followup_no_directive_normalized_token.json b/tests/fixtures/conformance/step/step_negative_followup_no_directive_normalized_token.json index 9fe6556..cdf8587 100644 --- a/tests/fixtures/conformance/step/step_negative_followup_no_directive_normalized_token.json +++ b/tests/fixtures/conformance/step/step_negative_followup_no_directive_normalized_token.json @@ -12,9 +12,7 @@ "input": "no thanks.", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_negative_followup_no_directive_punctuation_token.json b/tests/fixtures/conformance/step/step_negative_followup_no_directive_punctuation_token.json index 63fde86..6c32a48 100644 --- a/tests/fixtures/conformance/step/step_negative_followup_no_directive_punctuation_token.json +++ b/tests/fixtures/conformance/step/step_negative_followup_no_directive_punctuation_token.json @@ -12,9 +12,7 @@ "input": " NOPE?? ", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_non_directive_input_no_directive.json b/tests/fixtures/conformance/step/step_non_directive_input_no_directive.json index 67a2c3c..04dde28 100644 --- a/tests/fixtures/conformance/step/step_non_directive_input_no_directive.json +++ b/tests/fixtures/conformance/step/step_non_directive_input_no_directive.json @@ -9,9 +9,7 @@ "input": "hello there", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_policy_identity_apostrophe_variant_error.json b/tests/fixtures/conformance/step/step_policy_identity_apostrophe_variant_error.json index 14fdc2c..e736b24 100644 --- a/tests/fixtures/conformance/step/step_policy_identity_apostrophe_variant_error.json +++ b/tests/fixtures/conformance/step/step_policy_identity_apostrophe_variant_error.json @@ -9,12 +9,11 @@ "prelude": [ "use don't" ], - "input": "prohibit don’t", + "input": "prohibit don\u2019t", "expected": { "decision": { "kind": "error", - "prompt_to_user": null, - "state": null + "message": "\"don't\" is currently in use.\nRemove or replace it before prohibiting it." }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_policy_identity_case_remove_policy_update.json b/tests/fixtures/conformance/step/step_policy_identity_case_remove_policy_update.json index 3a08571..e34099b 100644 --- a/tests/fixtures/conformance/step/step_policy_identity_case_remove_policy_update.json +++ b/tests/fixtures/conformance/step/step_policy_identity_case_remove_policy_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/step/step_policy_identity_internal_whitespace_remove_policy_update.json b/tests/fixtures/conformance/step/step_policy_identity_internal_whitespace_remove_policy_update.json index fdc0afa..86e0493 100644 --- a/tests/fixtures/conformance/step/step_policy_identity_internal_whitespace_remove_policy_update.json +++ b/tests/fixtures/conformance/step/step_policy_identity_internal_whitespace_remove_policy_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/step/step_prohibit_item_update.json b/tests/fixtures/conformance/step/step_prohibit_item_update.json index 725111e..4ee0893 100644 --- a/tests/fixtures/conformance/step/step_prohibit_item_update.json +++ b/tests/fixtures/conformance/step/step_prohibit_item_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_quoted_leading_directive_text_no_directive.json b/tests/fixtures/conformance/step/step_quoted_leading_directive_text_no_directive.json index d58b727..d846881 100644 --- a/tests/fixtures/conformance/step/step_quoted_leading_directive_text_no_directive.json +++ b/tests/fixtures/conformance/step/step_quoted_leading_directive_text_no_directive.json @@ -9,9 +9,7 @@ "input": "\"use docker and prohibit peanuts\"", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_quoted_payload_compound_invalid_boundary.json b/tests/fixtures/conformance/step/step_quoted_payload_compound_invalid_boundary.json index 4806a56..13a873d 100644 --- a/tests/fixtures/conformance/step/step_quoted_payload_compound_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_quoted_payload_compound_invalid_boundary.json @@ -9,9 +9,7 @@ "input": "use \"docker and prohibit peanuts\"", "expected": { "decision": { - "kind": "no_directive", - "prompt_to_user": null, - "state": null + "kind": "no_directive" }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_remove_policy_missing_idempotent_update.json b/tests/fixtures/conformance/step/step_remove_policy_missing_idempotent_update.json index b99fc07..e77fed2 100644 --- a/tests/fixtures/conformance/step/step_remove_policy_missing_idempotent_update.json +++ b/tests/fixtures/conformance/step/step_remove_policy_missing_idempotent_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_remove_policy_present_update.json b/tests/fixtures/conformance/step/step_remove_policy_present_update.json index 225ca75..026db45 100644 --- a/tests/fixtures/conformance/step/step_remove_policy_present_update.json +++ b/tests/fixtures/conformance/step/step_remove_policy_present_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": {}, diff --git a/tests/fixtures/conformance/step/step_replace_missing_source_error_prompt.json b/tests/fixtures/conformance/step/step_replace_missing_source_error_prompt.json index a7bcfc3..60bf903 100644 --- a/tests/fixtures/conformance/step/step_replace_missing_source_error_prompt.json +++ b/tests/fixtures/conformance/step/step_replace_missing_source_error_prompt.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_replace_prohibited_new_error_without_mutation.json b/tests/fixtures/conformance/step/step_replace_prohibited_new_error_without_mutation.json index 0608d60..1fc23d9 100644 --- a/tests/fixtures/conformance/step/step_replace_prohibited_new_error_without_mutation.json +++ b/tests/fixtures/conformance/step/step_replace_prohibited_new_error_without_mutation.json @@ -14,8 +14,7 @@ "expected": { "decision": { "kind": "error", - "prompt_to_user": "\"kubectl\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item.", - "state": null + "message": "\"kubectl\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_replace_prohibited_old_error_without_mutation.json b/tests/fixtures/conformance/step/step_replace_prohibited_old_error_without_mutation.json index c96f093..a3c1a1a 100644 --- a/tests/fixtures/conformance/step/step_replace_prohibited_old_error_without_mutation.json +++ b/tests/fixtures/conformance/step/step_replace_prohibited_old_error_without_mutation.json @@ -14,8 +14,7 @@ "expected": { "decision": { "kind": "error", - "prompt_to_user": "\"docker\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item.", - "state": null + "message": "\"docker\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." }, "state": { "premise": null, diff --git a/tests/fixtures/conformance/step/step_replace_use_update.json b/tests/fixtures/conformance/step/step_replace_use_update.json index 2c6aeaa..f2eea7f 100644 --- a/tests/fixtures/conformance/step/step_replace_use_update.json +++ b/tests/fixtures/conformance/step/step_replace_use_update.json @@ -12,7 +12,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_reset_policies_already_empty_update.json b/tests/fixtures/conformance/step/step_reset_policies_already_empty_update.json index a8f7825..92d34cf 100644 --- a/tests/fixtures/conformance/step/step_reset_policies_already_empty_update.json +++ b/tests/fixtures/conformance/step/step_reset_policies_already_empty_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": "concise replies", "policies": {}, diff --git a/tests/fixtures/conformance/step/step_reset_policies_populated_update.json b/tests/fixtures/conformance/step/step_reset_policies_populated_update.json index 260e8cf..68b9340 100644 --- a/tests/fixtures/conformance/step/step_reset_policies_populated_update.json +++ b/tests/fixtures/conformance/step/step_reset_policies_populated_update.json @@ -13,7 +13,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": "concise replies", "policies": {}, diff --git a/tests/fixtures/conformance/step/step_set_premise_existing_error.json b/tests/fixtures/conformance/step/step_set_premise_existing_error.json index 88442f8..351a072 100644 --- a/tests/fixtures/conformance/step/step_set_premise_existing_error.json +++ b/tests/fixtures/conformance/step/step_set_premise_existing_error.json @@ -10,8 +10,7 @@ "expected": { "decision": { "kind": "error", - "prompt_to_user": null, - "state": null + "message": "Premise already set.\nUse 'change premise to ' to modify it." }, "state": { "premise": "existing premise", diff --git a/tests/fixtures/conformance/step/step_set_premise_update.json b/tests/fixtures/conformance/step/step_set_premise_update.json index a016bf9..8214515 100644 --- a/tests/fixtures/conformance/step/step_set_premise_update.json +++ b/tests/fixtures/conformance/step/step_set_premise_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": "concise replies", "policies": {}, diff --git a/tests/fixtures/conformance/step/step_tab_separator_use_update.json b/tests/fixtures/conformance/step/step_tab_separator_use_update.json index 57f5a8f..d6419c3 100644 --- a/tests/fixtures/conformance/step/step_tab_separator_use_update.json +++ b/tests/fixtures/conformance/step/step_tab_separator_use_update.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/conformance/step/step_use_item_normalization.json b/tests/fixtures/conformance/step/step_use_item_normalization.json index af8a288..86365bc 100644 --- a/tests/fixtures/conformance/step/step_use_item_normalization.json +++ b/tests/fixtures/conformance/step/step_use_item_normalization.json @@ -10,7 +10,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/engine-regression/structured/README.md b/tests/fixtures/engine-regression/structured/README.md index ce0c065..1e37722 100644 --- a/tests/fixtures/engine-regression/structured/README.md +++ b/tests/fixtures/engine-regression/structured/README.md @@ -28,7 +28,7 @@ Each expected turn uses: * `input` * `decision.kind` -* `decision.prompt_to_user` +* `decision.message` for `error` turns only * `state` `decision.state` is intentionally omitted because the expected authoritative @@ -43,7 +43,7 @@ regressions are visible in: ## Prompt Matching -`decision.prompt_to_user` is matched exactly, including error text. +`decision.message` is matched exactly, including error text. ## Adding a Scenario @@ -56,7 +56,7 @@ regressions are visible in: These fixtures validate **deterministic engine behavior only**: -* `engine.step(...)` outputs (`Decision.kind`, `prompt_to_user`) +* `engine.step(...)` outputs (`Decision.kind`, `message` on `error` only) * post-turn authoritative state snapshot They do **not** cover: diff --git a/tests/fixtures/engine-regression/structured/expected/contradiction_error.json b/tests/fixtures/engine-regression/structured/expected/contradiction_error.json index 9cde747..a04244e 100644 --- a/tests/fixtures/engine-regression/structured/expected/contradiction_error.json +++ b/tests/fixtures/engine-regression/structured/expected/contradiction_error.json @@ -10,8 +10,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "use docker" }, @@ -25,7 +24,7 @@ }, "decision": { "kind": "error", - "prompt_to_user": "\"docker\" is currently in use.\nRemove or replace it before prohibiting it." + "message": "\"docker\" is currently in use.\nRemove or replace it before prohibiting it." }, "input": "prohibit docker" } diff --git a/tests/fixtures/engine-regression/structured/expected/premise_lifecycle.json b/tests/fixtures/engine-regression/structured/expected/premise_lifecycle.json index 36001cd..98a75b9 100644 --- a/tests/fixtures/engine-regression/structured/expected/premise_lifecycle.json +++ b/tests/fixtures/engine-regression/structured/expected/premise_lifecycle.json @@ -8,8 +8,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "set premise concise replies" }, @@ -21,7 +20,7 @@ }, "decision": { "kind": "error", - "prompt_to_user": "Premise already set.\nUse 'change premise to ' to modify it." + "message": "Premise already set.\nUse 'change premise to ' to modify it." }, "input": "set premise formal replies" }, @@ -32,8 +31,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "change premise to concise bullet points" }, @@ -44,8 +42,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "clear premise" }, @@ -57,7 +54,7 @@ }, "decision": { "kind": "error", - "prompt_to_user": "No premise is set.\nUse 'set premise ' to define one." + "message": "No premise is set.\nUse 'set premise ' to define one." }, "input": "change premise to formal tone" } diff --git a/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_new_error.json b/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_new_error.json index d6e2390..0d079a3 100644 --- a/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_new_error.json +++ b/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_new_error.json @@ -10,8 +10,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "use docker" }, @@ -25,8 +24,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "prohibit kubectl" }, @@ -41,7 +39,7 @@ }, "decision": { "kind": "error", - "prompt_to_user": "\"kubectl\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." + "message": "\"kubectl\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." }, "input": "use kubectl instead of docker" }, @@ -55,8 +53,7 @@ "version": 2 }, "decision": { - "kind": "no_directive", - "prompt_to_user": null + "kind": "no_directive" }, "input": "no" } diff --git a/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_old_error.json b/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_old_error.json index 3068f68..0154705 100644 --- a/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_old_error.json +++ b/tests/fixtures/engine-regression/structured/expected/prohibited_replacement_old_error.json @@ -10,8 +10,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "prohibit docker" }, @@ -25,8 +24,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "use pytest" }, @@ -41,7 +39,7 @@ }, "decision": { "kind": "error", - "prompt_to_user": "\"docker\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." + "message": "\"docker\" is currently prohibited.\nSubmit explicit directive(s) to remove it or use a different item." }, "input": "use kubectl instead of docker" }, @@ -55,8 +53,7 @@ "version": 2 }, "decision": { - "kind": "no_directive", - "prompt_to_user": null + "kind": "no_directive" }, "input": "yes" } diff --git a/tests/fixtures/engine-regression/structured/expected/replacement_error.json b/tests/fixtures/engine-regression/structured/expected/replacement_error.json index fc20ab7..e9c8a0a 100644 --- a/tests/fixtures/engine-regression/structured/expected/replacement_error.json +++ b/tests/fixtures/engine-regression/structured/expected/replacement_error.json @@ -10,8 +10,7 @@ "version": 2 }, "decision": { - "kind": "update", - "prompt_to_user": null + "kind": "update" }, "input": "use podman instead of docker" } diff --git a/tests/fixtures/spec-targets/directive-grammar/041_policy_identity_no_article_removal_distinct_update.json b/tests/fixtures/spec-targets/directive-grammar/041_policy_identity_no_article_removal_distinct_update.json index 8a67a16..e04d72c 100644 --- a/tests/fixtures/spec-targets/directive-grammar/041_policy_identity_no_article_removal_distinct_update.json +++ b/tests/fixtures/spec-targets/directive-grammar/041_policy_identity_no_article_removal_distinct_update.json @@ -13,7 +13,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/fixtures/spec-targets/directive-grammar/042_policy_identity_no_dont_rewrite_distinct_update.json b/tests/fixtures/spec-targets/directive-grammar/042_policy_identity_no_dont_rewrite_distinct_update.json index 5da2fb5..bd54812 100644 --- a/tests/fixtures/spec-targets/directive-grammar/042_policy_identity_no_dont_rewrite_distinct_update.json +++ b/tests/fixtures/spec-targets/directive-grammar/042_policy_identity_no_dont_rewrite_distinct_update.json @@ -13,7 +13,6 @@ "expected": { "decision": { "kind": "update", - "prompt_to_user": null, "state": { "premise": null, "policies": { diff --git a/tests/test_04_grammar_edge_cases.py b/tests/test_04_grammar_edge_cases.py index 46c66f7..cf6a6c7 100644 --- a/tests/test_04_grammar_edge_cases.py +++ b/tests/test_04_grammar_edge_cases.py @@ -1,5 +1,4 @@ -from context_compiler import create_engine -from context_compiler.engine import DecisionKind +from context_compiler import DECISION_NO_DIRECTIVE, DECISION_UPDATE, create_engine def test_parser_trims_leading_space_for_canonical_directive() -> None: @@ -8,9 +7,8 @@ def test_parser_trims_leading_space_for_canonical_directive() -> None: decision = engine.step(" set premise concise") assert decision == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": {"premise": "concise", "policies": {}, "version": 2}, - "prompt_to_user": None, } assert engine.state == {"premise": "concise", "policies": {}, "version": 2} @@ -29,7 +27,7 @@ def test_parser_does_not_accept_conversational_aliases() -> None: "set docker", ]: decision = engine.step(text) - assert decision["kind"] == DecisionKind.NO_DIRECTIVE + assert decision["kind"] == DECISION_NO_DIRECTIVE assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -39,19 +37,11 @@ def test_empty_policy_payloads_and_incomplete_replacement_remain_no_directive() before = engine.state for text in ["use", "use ", "use "]: - assert engine.step(text) == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert engine.step(text) == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before for text in ["prohibit", "prohibit ", "prohibit "]: - assert engine.step(text) == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert engine.step(text) == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before for text in [ @@ -61,27 +51,23 @@ def test_empty_policy_payloads_and_incomplete_replacement_remain_no_directive() "use instead of y", "use instead of y", ]: - assert engine.step(text) == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert engine.step(text) == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before - assert engine.step("remove policy\tdocker")["kind"] == DecisionKind.UPDATE + assert engine.step("remove policy\tdocker")["kind"] == DECISION_UPDATE assert engine.state == before def test_lexical_normalization_and_non_directive_near_misses() -> None: engine = create_engine() - assert engine.step("clear premise ")["kind"] == DecisionKind.UPDATE - assert engine.step("reset policies ")["kind"] == DecisionKind.UPDATE - assert engine.step("clear state ")["kind"] == DecisionKind.UPDATE - assert engine.step("remove policy\tdocker")["kind"] == DecisionKind.UPDATE - assert engine.step("Use docker")["kind"] == DecisionKind.UPDATE - assert engine.step("use\tdocker")["kind"] == DecisionKind.UPDATE - assert engine.step("don't Use docker")["kind"] == DecisionKind.NO_DIRECTIVE - assert engine.step("don't use")["kind"] == DecisionKind.NO_DIRECTIVE + assert engine.step("clear premise ")["kind"] == DECISION_UPDATE + assert engine.step("reset policies ")["kind"] == DECISION_UPDATE + assert engine.step("clear state ")["kind"] == DECISION_UPDATE + assert engine.step("remove policy\tdocker")["kind"] == DECISION_UPDATE + assert engine.step("Use docker")["kind"] == DECISION_UPDATE + assert engine.step("use\tdocker")["kind"] == DECISION_UPDATE + assert engine.step("don't Use docker")["kind"] == DECISION_NO_DIRECTIVE + assert engine.step("don't use")["kind"] == DECISION_NO_DIRECTIVE assert engine.state == {"premise": None, "policies": {"docker": "use"}, "version": 2} @@ -93,12 +79,8 @@ def test_premise_to_variant_near_misses_remain_no_directive() -> None: set_variant = engine.step("set premise to concise") change_variant = engine.step("change premise concise") - assert set_variant == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} - assert change_variant == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert set_variant == {"kind": DECISION_NO_DIRECTIVE} + assert change_variant == {"kind": DECISION_NO_DIRECTIVE} assert before == engine.state @@ -109,25 +91,20 @@ def test_remove_policy_missing_or_whitespace_payload_remains_no_directive() -> N first = engine.step("remove policy") second = engine.step("remove policy ") - assert first == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} - assert second == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert first == {"kind": DECISION_NO_DIRECTIVE} + assert second == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before def test_invalid_replacement_does_not_block_following_directives() -> None: engine = create_engine() first = engine.step("use kubectl instead of docker") - assert first["kind"] == DecisionKind.UPDATE + assert first["kind"] == DECISION_UPDATE second = engine.step("set premise concise") assert second == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": {"premise": "concise", "policies": {"kubectl": "use"}, "version": 2}, - "prompt_to_user": None, } assert engine.state == { "premise": "concise", @@ -141,9 +118,5 @@ def test_replace_update_independent_followup_is_no_directive() -> None: first = engine.step("use kubectl instead of docker") second = engine.step("sounds good") - assert first["kind"] == DecisionKind.UPDATE - assert second == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert first["kind"] == DECISION_UPDATE + assert second == {"kind": DECISION_NO_DIRECTIVE} diff --git a/tests/test_api_contract_fixture.py b/tests/test_api_contract_fixture.py index 659bcf1..cd2690b 100644 --- a/tests/test_api_contract_fixture.py +++ b/tests/test_api_contract_fixture.py @@ -12,7 +12,7 @@ import context_compiler _CONTRACT_PATH = ( - Path(__file__).resolve().parent / "fixtures" / "conformance" / "api" / "public-api-v1.json" + Path(__file__).resolve().parent / "fixtures" / "conformance" / "api" / "public-api-v2.json" ) @@ -76,7 +76,6 @@ def test_api_contract_fixture_forbidden_exports_are_not_present() -> None: def test_public_annotation_dependencies_are_importable_from_root() -> None: assert context_compiler.PolicyValue is not None - assert context_compiler.DecisionKind is not None def test_api_contract_fixture_has_unique_entries() -> None: diff --git a/tests/test_compound_directive_properties.py b/tests/test_compound_directive_properties.py index 77fef07..0e5d872 100644 --- a/tests/test_compound_directive_properties.py +++ b/tests/test_compound_directive_properties.py @@ -3,8 +3,7 @@ from hypothesis import assume, given, settings from hypothesis import strategies as st -from context_compiler import create_engine -from context_compiler.engine import DecisionKind +from context_compiler import DECISION_ERROR, DECISION_NO_DIRECTIVE, DECISION_UPDATE, create_engine CANONICAL_SECOND_DIRECTIVES = [ "set premise concise", @@ -39,7 +38,7 @@ def _assert_compound_no_directive(user_input: str) -> None: decision = engine.step(user_input) - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -83,8 +82,8 @@ def test_embedded_canonical_tokens_do_not_trigger_compound_detection( decision = engine.step(f"use docker {prefix}{token}{suffix}") - assert decision["prompt_to_user"] is not None or decision["kind"] != DecisionKind.ERROR - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] != DECISION_ERROR or decision["message"] != "" + assert decision["kind"] == DECISION_UPDATE assert engine.state != before @@ -104,7 +103,7 @@ def test_leading_non_directive_text_disables_compound_detection(prefix: str, sec before = engine.state decision = engine.step(f"{prefix} use docker {second}") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -125,7 +124,7 @@ def test_case_mutated_second_directive_does_not_trigger_compound_detection( decision = engine.step(f"use docker {second_start}") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -157,5 +156,5 @@ def test_fully_quoted_input_remains_no_directive(quote: str, second: str) -> Non decision = engine.step(f"{quote}use docker {second}{quote}") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before diff --git a/tests/test_controller.py b/tests/test_controller.py index 29c4efb..e2bf6dc 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -4,6 +4,9 @@ import pytest from context_compiler import ( + DECISION_ERROR, + DECISION_NO_DIRECTIVE, + DECISION_UPDATE, create_engine, get_step_decision, get_step_state, @@ -17,7 +20,6 @@ state_diff, ) from context_compiler.controller import step -from context_compiler.engine import DecisionKind _CONTROLLER_FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" / "controller" @@ -28,7 +30,7 @@ def test_step_wrapper_returns_state_snapshot_and_contract_shape() -> None: assert result["output_version"] == 1 assert result["mode"] == "step" - assert result["decision"]["kind"] == DecisionKind.UPDATE + assert result["decision"]["kind"] == DECISION_UPDATE assert result["state"] == engine.state assert result["state"] == { "premise": "concise replies", @@ -44,7 +46,7 @@ def test_preview_update_does_not_mutate_engine_state() -> None: result = preview(engine, "set premise concise replies") assert result["mode"] == "preview" - assert result["decision"]["kind"] == DecisionKind.UPDATE + assert result["decision"]["kind"] == DECISION_UPDATE assert result["state_before"] == before assert result["state_after"] == { "premise": "concise replies", @@ -61,9 +63,8 @@ def test_preview_missing_source_replacement_reports_update_without_live_mutation result = preview(engine, "use kubectl instead of docker") assert result["decision"] == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": {"premise": None, "policies": {"kubectl": "use"}, "version": 2}, - "prompt_to_user": None, } assert result["state_before"] == {"premise": None, "policies": {}, "version": 2} assert result["state_after"] == { @@ -75,7 +76,7 @@ def test_preview_missing_source_replacement_reports_update_without_live_mutation assert result["would_mutate"] is True yes = engine.step("yes") - assert yes["kind"] == DecisionKind.NO_DIRECTIVE + assert yes["kind"] == DECISION_NO_DIRECTIVE def test_preview_prohibited_replacement_error_matches_execution_without_mutation() -> None: @@ -86,9 +87,8 @@ def test_preview_prohibited_replacement_error_matches_execution_without_mutation result = preview(engine, "use kubectl instead of docker") assert result["decision"] == { - "kind": DecisionKind.ERROR, - "state": None, - "prompt_to_user": ( + "kind": DECISION_ERROR, + "message": ( '"kubectl" is currently prohibited.\n' "Submit explicit directive(s) to remove it or use a different item." ), @@ -104,7 +104,7 @@ def test_preview_idempotent_update_is_not_a_mutation() -> None: result = preview(engine, "use docker") - assert result["decision"]["kind"] == DecisionKind.UPDATE + assert result["decision"]["kind"] == DECISION_UPDATE assert result["state_before"] == result["state_after"] assert result["diff"]["changed"] is False assert result["would_mutate"] is False @@ -254,17 +254,12 @@ def test_preview_followup_tokens_after_replace_update_are_no_directive( engine = create_engine() initial = engine.step("use kubectl instead of docker") assert initial == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": {"premise": None, "policies": {"kubectl": "use"}, "version": 2}, - "prompt_to_user": None, } preview_result = preview(engine, followup_token) - assert preview_result["decision"] == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert preview_result["decision"] == {"kind": DECISION_NO_DIRECTIVE} assert preview_result["state_after"] == { "premise": None, "policies": {"kubectl": "use"}, @@ -275,9 +270,5 @@ def test_preview_followup_tokens_after_replace_update_are_no_directive( assert engine.state == {"premise": None, "policies": {"kubectl": "use"}, "version": 2} final = step(engine, followup_token) - assert final["decision"] == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert final["decision"] == {"kind": DECISION_NO_DIRECTIVE} assert final["state"] == {"premise": None, "policies": {"kubectl": "use"}, "version": 2} diff --git a/tests/test_decision_constants.py b/tests/test_decision_constants.py index 270646a..a2c11b9 100644 --- a/tests/test_decision_constants.py +++ b/tests/test_decision_constants.py @@ -5,7 +5,7 @@ POLICY_PROHIBIT, POLICY_USE, get_decision_state, - get_error_prompt, + get_error_message, is_error, is_no_directive, is_update, @@ -28,13 +28,12 @@ def test_decision_helpers_for_update_decision() -> None: decision: Decision = { "kind": DECISION_UPDATE, "state": {"premise": "concise replies", "policies": {}, "version": 2}, - "prompt_to_user": None, } assert is_update(decision) is True assert is_error(decision) is False assert is_no_directive(decision) is False - assert get_error_prompt(decision) is None + assert get_error_message(decision) is None assert get_decision_state(decision) == { "premise": "concise replies", "policies": {}, @@ -45,26 +44,21 @@ def test_decision_helpers_for_update_decision() -> None: def test_decision_helpers_for_error_decision() -> None: decision: Decision = { "kind": DECISION_ERROR, - "state": None, - "prompt_to_user": "Use what item?", + "message": "Use what item?", } assert is_update(decision) is False assert is_error(decision) is True assert is_no_directive(decision) is False - assert get_error_prompt(decision) == "Use what item?" + assert get_error_message(decision) == "Use what item?" assert get_decision_state(decision) is None def test_decision_helpers_for_no_directive_decision() -> None: - decision: Decision = { - "kind": DECISION_NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + decision: Decision = {"kind": DECISION_NO_DIRECTIVE} assert is_update(decision) is False assert is_error(decision) is False assert is_no_directive(decision) is True - assert get_error_prompt(decision) is None + assert get_error_message(decision) is None assert get_decision_state(decision) is None diff --git a/tests/test_demo_01_04_behavior.py b/tests/test_demo_01_04_behavior.py index 8f86167..e7cd09d 100644 --- a/tests/test_demo_01_04_behavior.py +++ b/tests/test_demo_01_04_behavior.py @@ -6,12 +6,11 @@ import pytest -from context_compiler.engine import DecisionKind - REPO_ROOT = Path(__file__).resolve().parents[1] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from context_compiler import DECISION_NO_DIRECTIVE, DECISION_UPDATE # noqa: E402 from demos.common import consume_last_report # noqa: E402 @@ -83,8 +82,8 @@ def __init__(self) -> None: def step(self, _text: str) -> dict[str, str]: self._step_count += 1 if self._step_count == 1: - return {"kind": DecisionKind.UPDATE} - return {"kind": DecisionKind.NO_DIRECTIVE} + return {"kind": DECISION_UPDATE} + return {"kind": DECISION_NO_DIRECTIVE} def fake_complete_messages(_messages: object) -> str: nonlocal call_count @@ -119,8 +118,8 @@ def __init__(self) -> None: def step(self, _text: str) -> dict[str, str]: self._step_count += 1 if self._step_count == 1: - return {"kind": DecisionKind.UPDATE} - return {"kind": DecisionKind.NO_DIRECTIVE} + return {"kind": DECISION_UPDATE} + return {"kind": DECISION_NO_DIRECTIVE} monkeypatch.setattr(module, "create_engine", _FakeEngine) monkeypatch.setattr( diff --git a/tests/test_engine.py b/tests/test_engine.py index 1f6d04c..d094ff9 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -3,10 +3,9 @@ import pytest -from context_compiler import create_engine +from context_compiler import DECISION_ERROR, DECISION_NO_DIRECTIVE, DECISION_UPDATE, create_engine from context_compiler.engine import ( Action, - DecisionKind, Engine, _parse_directive, ) @@ -19,13 +18,6 @@ pytestmark = pytest.mark.contract -def test_decision_kind_strenum_behavior() -> None: - for kind in DecisionKind: - assert kind == kind.value - assert str(kind) == kind.value - assert DecisionKind(kind.value) is kind - - def test_parse_directive_delegates_canonical_kinds_to_existing_actions() -> None: assert _parse_directive("set premise concise replies") == Action( kind="set_premise", value="concise replies" @@ -73,37 +65,30 @@ def test_pre_mutation_error_empty_operand_branches_remain_stable() -> None: assert engine._pre_mutation_error(Action(kind="set_premise", value="")) == { "kind": "error", - "state": None, - "prompt_to_user": ( + "message": ( "Premise value cannot be empty.\nUse 'set premise ' with a non-empty value." ), } assert engine._pre_mutation_error(Action(kind="change_premise", value="")) == { "kind": "error", - "state": None, - "prompt_to_user": ( + "message": ( "Premise value cannot be empty.\n" "Use 'change premise to ' with a non-empty value." ), } assert engine._pre_mutation_error(Action(kind="remove_policy_item", item="")) == { "kind": "error", - "state": None, - "prompt_to_user": ( + "message": ( "Policy item cannot be empty.\nUse 'remove policy ' with a non-empty value." ), } assert engine._pre_mutation_error(Action(kind="use_item", item="")) == { "kind": "error", - "state": None, - "prompt_to_user": "Policy item cannot be empty.\nUse 'use ' with a non-empty value.", + "message": "Policy item cannot be empty.\nUse 'use ' with a non-empty value.", } assert engine._pre_mutation_error(Action(kind="prohibit_item", item="")) == { "kind": "error", - "state": None, - "prompt_to_user": ( - "Policy item cannot be empty.\nUse 'prohibit ' with a non-empty value." - ), + "message": ("Policy item cannot be empty.\nUse 'prohibit ' with a non-empty value."), } @@ -307,8 +292,7 @@ def test_replace_use_clarifies_when_old_policy_is_not_use_in_invalid_internal_st assert decision == { "kind": "error", - "state": None, - "prompt_to_user": ( + "message": ( "\"docker\" is not currently in use.\nReplacement requires an active 'use' policy." ), } @@ -404,11 +388,7 @@ def test_non_matching_input_is_no_directive() -> None: "don't use docker", ]: decision = engine.step(text) - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -416,12 +396,12 @@ def test_non_matching_input_is_no_directive() -> None: def test_lexical_normalization_accepts_canonical_directives() -> None: engine = create_engine() - assert engine.step("clear premise ")["kind"] == DecisionKind.UPDATE - assert engine.step(" reset policies")["kind"] == DecisionKind.UPDATE - assert engine.step("clear state\t")["kind"] == DecisionKind.UPDATE - assert engine.step("Use docker")["kind"] == DecisionKind.UPDATE - assert engine.step("use\tdocker")["kind"] == DecisionKind.UPDATE - assert engine.step(" prohibit docker")["kind"] == DecisionKind.ERROR + assert engine.step("clear premise ")["kind"] == DECISION_UPDATE + assert engine.step(" reset policies")["kind"] == DECISION_UPDATE + assert engine.step("clear state\t")["kind"] == DECISION_UPDATE + assert engine.step("Use docker")["kind"] == DECISION_UPDATE + assert engine.step("use\tdocker")["kind"] == DECISION_UPDATE + assert engine.step(" prohibit docker")["kind"] == DECISION_ERROR def test_clear_premise_is_idempotent_update_when_already_null() -> None: @@ -429,7 +409,7 @@ def test_clear_premise_is_idempotent_update_when_already_null() -> None: before = engine.state decision = engine.step("clear premise") - assert decision == {"kind": DecisionKind.UPDATE, "state": before, "prompt_to_user": None} + assert decision == {"kind": DECISION_UPDATE, "state": before} assert engine.state == before @@ -438,7 +418,7 @@ def test_clear_state_is_idempotent_update_when_already_empty() -> None: before = engine.state decision = engine.step("clear state") - assert decision == {"kind": DecisionKind.UPDATE, "state": before, "prompt_to_user": None} + assert decision == {"kind": DECISION_UPDATE, "state": before} assert engine.state == before @@ -446,15 +426,14 @@ def test_set_premise_lifecycle_rules() -> None: engine = create_engine() d1 = engine.step("set premise concise replies") - assert d1["kind"] == DecisionKind.UPDATE + assert d1["kind"] == DECISION_UPDATE assert engine.state["premise"] == "concise replies" before = engine.state d2 = engine.step("set premise new") assert d2 == { - "kind": DecisionKind.ERROR, - "state": None, - "prompt_to_user": ("Premise already set.\nUse 'change premise to ' to modify it."), + "kind": DECISION_ERROR, + "message": ("Premise already set.\nUse 'change premise to ' to modify it."), } assert engine.state == before @@ -463,7 +442,7 @@ def test_set_premise_empty_payload_remains_no_directive() -> None: engine = create_engine() before = engine.state d1 = engine.step("set premise") - assert d1 == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert d1 == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -471,7 +450,7 @@ def test_set_premise_whitespace_payload_remains_no_directive() -> None: engine = create_engine() before = engine.state d1 = engine.step("set premise ") - assert d1 == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert d1 == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -479,11 +458,7 @@ def test_set_premise_to_variant_remains_no_directive() -> None: engine = create_engine() decision = engine.step("set premise to concise replies") - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -492,11 +467,7 @@ def test_set_premise_to_with_whitespace_payload_remains_no_directive() -> None: decision = engine.step("set premise to ") - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -506,14 +477,13 @@ def test_change_premise_requires_existing_premise() -> None: d1 = engine.step("change premise to concise") assert d1 == { "kind": "error", - "state": None, - "prompt_to_user": "No premise is set.\nUse 'set premise ' to define one.", + "message": "No premise is set.\nUse 'set premise ' to define one.", } assert engine.state == {"premise": None, "policies": {}, "version": 2} engine.step("set premise first") d2 = engine.step("change premise to second") - assert d2["kind"] == DecisionKind.UPDATE + assert d2["kind"] == DECISION_UPDATE assert engine.state["premise"] == "second" @@ -523,7 +493,7 @@ def test_change_premise_to_empty_payload_remains_no_directive() -> None: before = engine.state d1 = engine.step("change premise to") - assert d1 == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert d1 == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -533,18 +503,10 @@ def test_change_premise_to_without_space_payload_and_empty_variant_remain_no_dir before = engine.state near_miss = engine.step("change premise baseline") - assert near_miss == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert near_miss == {"kind": DECISION_NO_DIRECTIVE} decision = engine.step("change premise to") - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -554,7 +516,7 @@ def test_change_premise_to_whitespace_payload_remains_no_directive() -> None: before = engine.state d1 = engine.step("change premise to ") - assert d1 == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert d1 == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -563,7 +525,7 @@ def test_change_premise_missing_to_variant_is_no_directive() -> None: before = engine.state decision = engine.step("change premise concise replies") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -572,7 +534,7 @@ def test_change_premise_with_whitespace_after_prefix_remains_no_directive() -> N decision = engine.step("change premise ") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == {"premise": "baseline", "policies": {}, "version": 2} @@ -582,8 +544,8 @@ def test_canonical_premise_forms_still_update_normally() -> None: first = engine.step("set premise concise replies") second = engine.step("change premise to concise bullet points") - assert first["kind"] == DecisionKind.UPDATE - assert second["kind"] == DecisionKind.UPDATE + assert first["kind"] == DECISION_UPDATE + assert second["kind"] == DECISION_UPDATE assert engine.state["premise"] == "concise bullet points" @@ -593,11 +555,11 @@ def test_clear_premise_and_clear_state() -> None: engine.step("use docker") d1 = engine.step("clear premise") - assert d1["kind"] == DecisionKind.UPDATE + assert d1["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {"docker": "use"}, "version": 2} d2 = engine.step("clear state") - assert d2["kind"] == DecisionKind.UPDATE + assert d2["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -605,16 +567,16 @@ def test_policy_directives_and_idempotent_update() -> None: engine = create_engine() d1 = engine.step("use The Docker") - assert d1["kind"] == DecisionKind.UPDATE + assert d1["kind"] == DECISION_UPDATE assert engine.state["policies"] == {"docker": "use"} d2 = engine.step("use docker") - assert d2["kind"] == DecisionKind.UPDATE + assert d2["kind"] == DECISION_UPDATE assert engine.state["policies"] == {"docker": "use"} d3 = engine.step("prohibit docker") assert d3["kind"] == "error" - assert d3["prompt_to_user"] == ( + assert d3["message"] == ( '"docker" is currently in use.\nRemove or replace it before prohibiting it.' ) assert engine.state["policies"] == {"docker": "use"} @@ -627,7 +589,7 @@ def test_policy_directives_and_idempotent_update() -> None: d5 = engine2.step("use docker") assert d5["kind"] == "error" - assert d5["prompt_to_user"] == ( + assert d5["message"] == ( '"docker" is currently prohibited.\nRemove or replace it before using it.' ) assert engine2.state["policies"] == {"docker": "prohibit"} @@ -639,11 +601,7 @@ def test_use_empty_payload_remains_no_directive() -> None: for text in ["use", "use ", "use "]: decision = engine.step(text) - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -653,11 +611,7 @@ def test_prohibit_empty_payload_remains_no_directive() -> None: for text in ["prohibit", "prohibit ", "prohibit "]: decision = engine.step(text) - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -673,23 +627,19 @@ def test_replace_use_incomplete_payload_remains_no_directive() -> None: "use instead of y", ]: decision = engine.step(text) - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before def test_reset_policies_is_update_even_when_already_empty() -> None: engine = create_engine() d1 = engine.step("reset policies") - assert d1["kind"] == DecisionKind.UPDATE + assert d1["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {}, "version": 2} engine.step("use docker") d2 = engine.step("reset policies") - assert d2["kind"] == DecisionKind.UPDATE + assert d2["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -699,7 +649,7 @@ def test_remove_policy_removes_existing_use_policy() -> None: decision = engine.step("remove policy docker") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -709,7 +659,7 @@ def test_remove_policy_removes_existing_prohibit_policy() -> None: decision = engine.step("remove policy docker") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -720,7 +670,7 @@ def test_remove_policy_missing_item_is_idempotent_update() -> None: decision = engine.step("remove policy podman") - assert decision == {"kind": DecisionKind.UPDATE, "state": before, "prompt_to_user": None} + assert decision == {"kind": DECISION_UPDATE, "state": before} assert engine.state == before @@ -730,7 +680,7 @@ def test_remove_policy_empty_payload_remains_no_directive() -> None: decision = engine.step("remove policy") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -740,7 +690,7 @@ def test_remove_policy_whitespace_payload_remains_no_directive() -> None: decision = engine.step("remove policy ") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -750,7 +700,7 @@ def test_replace_use_success() -> None: decision = engine.step("use kubectl instead of docker") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state["policies"] == {"kubectl": "use"} @@ -761,7 +711,7 @@ def test_replace_use_identity_is_noop_update() -> None: decision = engine.step("use the docker instead of docker") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state == before @@ -772,7 +722,6 @@ def test_replace_use_missing_source_applies_as_use_update() -> None: assert d1 == { "kind": "update", "state": {"premise": None, "policies": {"kubectl": "use"}, "version": 2}, - "prompt_to_user": None, } assert engine.state == {"premise": None, "policies": {"kubectl": "use"}, "version": 2} @@ -784,16 +733,11 @@ def test_replace_use_missing_source_yes_followup_is_no_directive() -> None: assert first == { "kind": "update", "state": {"premise": None, "policies": {"kubectl": "use"}, "version": 2}, - "prompt_to_user": None, } assert engine.state == {"premise": None, "policies": {"kubectl": "use"}, "version": 2} second = engine.step("yes") - assert second == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert second == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == {"premise": None, "policies": {"kubectl": "use"}, "version": 2} @@ -803,7 +747,7 @@ def test_replace_use_missing_source_no_followup_has_no_mutation() -> None: before = engine.state decision = engine.step("no") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -816,8 +760,7 @@ def test_replace_use_missing_source_still_reports_target_prohibit_when_new_item_ decision = engine.step("use kubectl instead of docker") assert decision == { "kind": "error", - "state": None, - "prompt_to_user": ( + "message": ( '"kubectl" is currently prohibited.\n' "Submit explicit directive(s) to remove it or use a different item." ), @@ -829,7 +772,7 @@ def test_replace_use_missing_source_ignores_unrelated_existing_policies() -> Non engine.step("use python and docker") decision = engine.step("use kubectl instead of python") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state["policies"] == {"kubectl": "use", "python and docker": "use"} @@ -839,7 +782,7 @@ def test_replace_use_missing_source_ignores_other_conflicting_entries() -> None: engine.step("prohibit python tooling") decision = engine.step("use kubectl instead of python") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state["policies"] == { "kubectl": "use", "python and docker": "use", @@ -852,7 +795,7 @@ def test_replace_use_missing_source_with_empty_probe_uses_invalid_prompt() -> No engine.step("use python and docker") decision = engine.step("use kubectl instead of the") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state == { "premise": None, "policies": {"kubectl": "use", "python and docker": "use"}, @@ -872,8 +815,7 @@ def test_replace_use_ky_prohibit_returns_error_without_mutation() -> None: ) assert first == { "kind": "error", - "state": None, - "prompt_to_user": expected, + "message": expected, } assert engine.state["policies"] == {"docker": "prohibit", "pytest": "use"} @@ -887,7 +829,7 @@ def test_replace_use_ky_prohibit_yes_does_not_authorize_mutation() -> None: assert first["kind"] == "error" decision = engine.step("yes") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -903,8 +845,7 @@ def test_replace_use_kx_prohibit_returns_error_without_mutation() -> None: ) assert first == { "kind": "error", - "state": None, - "prompt_to_user": expected, + "message": expected, } assert engine.state["policies"] == {"docker": "use", "kubectl": "prohibit"} @@ -921,8 +862,7 @@ def test_replace_use_priority_prefers_source_prohibit_error_when_both_prohibit() ) assert first == { "kind": "error", - "state": None, - "prompt_to_user": expected, + "message": expected, } assert engine.state["policies"] == {"docker": "prohibit", "kubectl": "prohibit"} @@ -936,8 +876,7 @@ def test_replace_use_invalid_source_state_prohibit_clarifies_without_mutation() decision = engine.step("use kubectl instead of docker") assert decision == { "kind": "error", - "state": None, - "prompt_to_user": ( + "message": ( '"docker" is currently prohibited.\n' "Submit explicit directive(s) to remove it or use a different item." ), @@ -954,7 +893,7 @@ def test_replace_use_kx_prohibit_no_followup_has_no_mutation() -> None: assert first["kind"] == "error" decision = engine.step("no") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -968,7 +907,7 @@ def test_missing_source_replacement_does_not_block_following_directives() -> Non assert engine.state["policies"] == {"docker": "use", "kubectl": "use"} third = engine.step("yes") - assert third == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert third == {"kind": DECISION_NO_DIRECTIVE} assert engine.state["policies"] == {"docker": "use", "kubectl": "use"} @@ -984,11 +923,7 @@ def test_missing_source_replacement_does_not_suspend_admin_commands() -> None: assert engine.state == {"premise": None, "policies": {}, "version": 2} resolved = engine.step("yes") - assert resolved == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert resolved == {"kind": DECISION_NO_DIRECTIVE} assert engine.state["policies"] == {} @@ -998,7 +933,7 @@ def test_missing_source_replacement_negative_followup_is_no_directive() -> None: decision = engine.step("no") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state["policies"] == {"kubectl": "use"} @@ -1007,7 +942,7 @@ def test_missing_source_replacement_affirmative_followup_tokens_are_no_directive engine.step("use kubectl instead of docker") decision = engine.step(" YES!!! ") - assert decision["kind"] == DecisionKind.NO_DIRECTIVE + assert decision["kind"] == DECISION_NO_DIRECTIVE assert engine.state["policies"] == {"kubectl": "use"} @@ -1016,7 +951,7 @@ def test_missing_source_replacement_affirmative_token_variants_are_no_directive( engine = create_engine() engine.step("use kubectl instead of docker") decision = engine.step(token) - assert decision["kind"] == DecisionKind.NO_DIRECTIVE + assert decision["kind"] == DECISION_NO_DIRECTIVE assert engine.state["policies"] == {"kubectl": "use"} @@ -1026,7 +961,7 @@ def test_missing_source_replacement_negative_tokens_are_no_directive() -> None: before = engine.state decision = engine.step(" NO!!! ") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -1036,7 +971,7 @@ def test_missing_source_replacement_no_thanks_is_no_directive() -> None: before = engine.state decision = engine.step("no thanks.") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -1046,11 +981,7 @@ def test_missing_source_replacement_negative_token_variants_are_no_directive() - engine.step("use kubectl instead of docker") before = engine.state decision = engine.step(token) - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -1060,11 +991,7 @@ def test_missing_source_replacement_unmatched_followup_is_no_directive() -> None before = engine.state second = engine.step("maybe") - assert second == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert second == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -1073,16 +1000,8 @@ def test_missing_source_replacement_unmatched_followups_remain_no_directive() -> engine.step("use kubectl instead of docker") before = engine.state - assert engine.step("later") == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } - assert engine.step("still later") == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert engine.step("later") == {"kind": DECISION_NO_DIRECTIVE} + assert engine.step("still later") == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -1096,32 +1015,24 @@ def test_prohibited_replacement_yes_cannot_override_conflicting_target_polarity( assert engine.state["policies"] == {"docker": "use", "kubectl": "prohibit"} second = engine.step("yes") - assert second["kind"] == DecisionKind.NO_DIRECTIVE + assert second["kind"] == DECISION_NO_DIRECTIVE assert engine.state["policies"] == {"docker": "use", "kubectl": "prohibit"} def test_import_json_does_not_change_independent_yes_no_followup_behavior() -> None: engine = create_engine() first = engine.step("use kubectl instead of docker") - assert first["kind"] == DecisionKind.UPDATE + assert first["kind"] == DECISION_UPDATE imported = {"premise": "baseline", "policies": {"pytest": "use"}, "version": 2} engine.import_json(json.dumps(imported)) yes_decision = engine.step("yes") - assert yes_decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert yes_decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == imported no_decision = engine.step("no") - assert no_decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert no_decision == {"kind": DECISION_NO_DIRECTIVE} def test_remove_policy_uses_normalized_item_matching() -> None: @@ -1129,7 +1040,7 @@ def test_remove_policy_uses_normalized_item_matching() -> None: engine.step("use The Docker") decision = engine.step("remove policy the docker") - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -1192,7 +1103,7 @@ def test_compound_directives_remain_no_directive_without_mutation( decision = engine.step(user_input) - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -1201,7 +1112,7 @@ def test_quoted_non_directive_leading_input_remains_no_directive() -> None: decision = engine.step('"use docker and prohibit peanuts"') - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == {"premise": None, "policies": {}, "version": 2} @@ -1248,7 +1159,7 @@ def test_directive_like_substrings_inside_larger_words_do_not_trigger_compound_r decision = engine.step(user_input) - assert decision["kind"] != DecisionKind.ERROR + assert decision["kind"] != DECISION_ERROR assert decision["kind"] == expected_decision_kind assert engine.state == expected_state @@ -1311,9 +1222,8 @@ def test_valid_single_directives_still_work( decision = engine.step(user_input) assert decision == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": expected_state, - "prompt_to_user": None, } assert engine.state == expected_state @@ -1341,21 +1251,20 @@ def test_all_canonical_directive_starts_remain_single_directive_when_valid( decision = engine.step(directive_start) - assert decision["kind"] != DecisionKind.NO_DIRECTIVE + assert decision["kind"] != DECISION_NO_DIRECTIVE def test_compound_no_directive_after_prior_missing_source_replacement_update() -> None: engine = create_engine() first = engine.step("use kubectl instead of docker") assert first == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": {"premise": None, "policies": {"kubectl": "use"}, "version": 2}, - "prompt_to_user": None, } decision = engine.step("use docker and prohibit peanuts") - assert decision == {"kind": DecisionKind.NO_DIRECTIVE, "state": None, "prompt_to_user": None} + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == {"premise": None, "policies": {"kubectl": "use"}, "version": 2} diff --git a/tests/test_examples_behavior.py b/tests/test_examples_behavior.py index 6c14005..1059673 100644 --- a/tests/test_examples_behavior.py +++ b/tests/test_examples_behavior.py @@ -5,8 +5,6 @@ import pytest -from context_compiler.engine import DecisionKind - REPO_ROOT = Path(__file__).resolve().parents[1] EXAMPLES_DIR = REPO_ROOT / "examples" @@ -33,13 +31,13 @@ def test_example_03_error_gate_blocks_llm_and_allows_later_update( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: module = _load_example_module("03_ambiguity_with_error.py") - decision_kinds: list[DecisionKind] = [] + decision_kinds: list[str] = [] llm_calls: list[str] = [] def capture_decision_summary(decision: object) -> None: assert isinstance(decision, dict) kind = decision.get("kind") - assert isinstance(kind, DecisionKind) + assert isinstance(kind, str) decision_kinds.append(kind) def fake_llm(user_input: str) -> str: @@ -53,9 +51,9 @@ def fake_llm(user_input: str) -> str: output = capsys.readouterr().out assert decision_kinds == [ - DecisionKind.UPDATE, - DecisionKind.ERROR, - DecisionKind.UPDATE, + "update", + "error", + "update", ] assert "Host behavior: error returned, do NOT call LLM." in output assert llm_calls == [] @@ -90,13 +88,13 @@ def test_example_05_dispatches_no_directive_update_and_error_correctly( ) -> None: module = _load_example_module("05_llm_integration_pattern.py") engine = module.create_engine() - decision_kinds: list[DecisionKind] = [] + decision_kinds: list[str] = [] llm_calls: list[tuple[object, str]] = [] def capture_decision_summary(decision: object) -> None: assert isinstance(decision, dict) kind = decision.get("kind") - assert isinstance(kind, DecisionKind) + assert isinstance(kind, str) decision_kinds.append(kind) def capture_fake_llm(state: object, user_input: str) -> str: @@ -112,9 +110,9 @@ def capture_fake_llm(state: object, user_input: str) -> str: module.handle_turn("set premise verbose replies", engine) # error assert decision_kinds == [ - DecisionKind.NO_DIRECTIVE, - DecisionKind.UPDATE, - DecisionKind.ERROR, + "no_directive", + "update", + "error", ] assert len(llm_calls) == calls_before_error assert llm_calls[0][0] is None diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 64a971b..426e5ee 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -4,9 +4,8 @@ import pytest -from context_compiler import create_engine, get_decision_state +from context_compiler import DECISION_ERROR, DECISION_UPDATE, create_engine, get_decision_state from context_compiler.controller import get_step_state, preview, state_diff, step -from context_compiler.engine import DecisionKind from context_compiler.grammar import ( DirectiveKind, decompose_directive, @@ -86,14 +85,7 @@ def _validate_step_fixture(fixture: dict[str, object], fixture_id: object) -> No decision = expected["decision"] assert isinstance(decision, dict), fixture_id - _assert_allowed_keys( - decision, {"kind", "prompt_to_user", "state"}, fixture_id, "expected.decision" - ) - assert isinstance(decision["kind"], str), fixture_id - assert decision["prompt_to_user"] is None or isinstance(decision["prompt_to_user"], str), ( - fixture_id - ) - assert decision["state"] is None or isinstance(decision["state"], dict), fixture_id + _validate_public_decision(decision, fixture_id, "expected.decision") def _validate_state_json_fixture(fixture: dict[str, object], fixture_id: object) -> None: @@ -138,12 +130,30 @@ def _validate_state_json_fixture(fixture: dict[str, object], fixture_id: object) def _validate_controller_result_decision(decision: object, fixture_id: object, label: str) -> None: assert isinstance(decision, dict), fixture_id - _assert_allowed_keys(decision, {"kind", "state", "prompt_to_user"}, fixture_id, label) - assert isinstance(decision["kind"], str), fixture_id - assert decision["state"] is None or isinstance(decision["state"], dict), fixture_id - assert decision["prompt_to_user"] is None or isinstance(decision["prompt_to_user"], str), ( - fixture_id + _validate_public_decision(decision, fixture_id, label) + + +def _validate_public_decision(decision: dict[str, object], fixture_id: object, label: str) -> None: + _assert_allowed_keys( + decision, + {"kind"} | ({"state"} & set(decision)) | ({"message"} & set(decision)), + fixture_id, + label, ) + kind = decision.get("kind") + assert isinstance(kind, str), fixture_id + + if kind == "no_directive": + _assert_allowed_keys(decision, {"kind"}, fixture_id, label) + return + if kind == "update": + _assert_allowed_keys(decision, {"kind", "state"}, fixture_id, label) + assert isinstance(decision["state"], dict), fixture_id + return + + assert kind == "error", fixture_id + _assert_allowed_keys(decision, {"kind", "message"}, fixture_id, label) + assert isinstance(decision["message"], str), fixture_id def _validate_controller_diff(diff: object, fixture_id: object, label: str) -> None: @@ -329,18 +339,17 @@ def test_step_fixtures() -> None: assert decision["kind"] == expected_decision["kind"], fixture_id - if decision["kind"] == DecisionKind.ERROR: - assert decision["state"] == expected_decision["state"], fixture_id - expected_prompt = expected_decision["prompt_to_user"] - actual_prompt = decision["prompt_to_user"] - if expected_prompt is None: - assert isinstance(actual_prompt, str) and actual_prompt != "", fixture_id + if decision["kind"] == DECISION_ERROR: + expected_message = expected_decision.get("message") + actual_message = decision["message"] + if expected_message is None: + assert actual_message != "", fixture_id else: - assert actual_prompt == expected_prompt, fixture_id + assert actual_message == expected_message, fixture_id else: assert decision == expected_decision, fixture_id - if decision["kind"] == DecisionKind.UPDATE: + if decision["kind"] == DECISION_UPDATE: assert decision["state"] == engine.state, fixture_id assert engine.state == expected["state"], fixture_id @@ -661,7 +670,6 @@ def test_step_validator_rejects_unknown_expected_decision_field() -> None: "expected": { "decision": { "kind": "update", - "prompt_to_user": None, "state": {"premise": None, "policies": {"docker": "use"}, "version": 2}, "unexpected": True, }, @@ -815,7 +823,6 @@ def test_controller_validator_rejects_unknown_preview_result_field() -> None: "decision": { "kind": "update", "state": {"premise": None, "policies": {"docker": "use"}, "version": 2}, - "prompt_to_user": None, }, "state_before": {"premise": None, "policies": {}, "version": 2}, "state_after": {"premise": None, "policies": {"docker": "use"}, "version": 2}, @@ -865,7 +872,6 @@ def test_controller_validator_rejects_wrong_branch_action_fields() -> None: "decision": { "kind": "update", "state": {"premise": None, "policies": {"docker": "use"}, "version": 2}, - "prompt_to_user": None, }, "state": {"premise": None, "policies": {"docker": "use"}, "version": 2}, }, diff --git a/tests/test_properties.py b/tests/test_properties.py index 62f0723..ae8b2bc 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -6,9 +6,9 @@ from hypothesis import assume, given from hypothesis import strategies as st -from context_compiler import create_engine +from context_compiler import DECISION_ERROR, DECISION_NO_DIRECTIVE, DECISION_UPDATE, create_engine from context_compiler.controller import preview, state_diff -from context_compiler.engine import DecisionKind, State +from context_compiler.engine import State from context_compiler.grammar import ( DirectiveKind, match_canonical_directive_start, @@ -304,8 +304,8 @@ def test_use_item_with_empty_normalized_payload_clarifies_without_mutation( d2 = engine.step(f"use {item}") expected_prompt = "Policy item cannot be empty.\nUse 'use ' with a non-empty value." - assert d1 == {"kind": DecisionKind.ERROR, "state": None, "prompt_to_user": expected_prompt} - assert d2 == {"kind": DecisionKind.ERROR, "state": None, "prompt_to_user": expected_prompt} + assert d1 == {"kind": DECISION_ERROR, "message": expected_prompt} + assert d2 == {"kind": DECISION_ERROR, "message": expected_prompt} assert engine.state == before @@ -318,8 +318,8 @@ def test_idempotent_prohibit_item_is_update_and_stable_state(item: str) -> None: d1 = engine.step(f"prohibit {item}") d2 = engine.step(f"prohibit {item}") - assert d1["kind"] == DecisionKind.UPDATE - assert d2["kind"] == DecisionKind.UPDATE + assert d1["kind"] == DECISION_UPDATE + assert d2["kind"] == DECISION_UPDATE assert len(engine.state["policies"]) == 1 @@ -340,8 +340,8 @@ def test_prohibit_item_with_empty_normalized_payload_clarifies_without_mutation( d2 = engine.step(f"prohibit {item}") expected_prompt = "Policy item cannot be empty.\nUse 'prohibit ' with a non-empty value." - assert d1 == {"kind": DecisionKind.ERROR, "state": None, "prompt_to_user": expected_prompt} - assert d2 == {"kind": DecisionKind.ERROR, "state": None, "prompt_to_user": expected_prompt} + assert d1 == {"kind": DECISION_ERROR, "message": expected_prompt} + assert d2 == {"kind": DECISION_ERROR, "message": expected_prompt} assert engine.state == before @@ -352,7 +352,7 @@ def test_non_matching_inputs_can_remain_no_directive_only(inputs: list[str]) -> for text in inputs: decision = engine.step(f"please {text}") - assert decision["kind"] == DecisionKind.NO_DIRECTIVE + assert decision["kind"] == DECISION_NO_DIRECTIVE assert engine.state == before @@ -364,11 +364,7 @@ def test_no_directive_sequence_preserves_state_and_decision_kind(inputs: list[st for text in inputs: decision = engine.step(f"prefix {text}") - assert decision == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert decision == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == before @@ -382,7 +378,7 @@ def test_contradiction_use_after_prohibit_always_clarifies(item: str) -> None: before = engine.state decision = engine.step(f"use {item}") - assert decision["kind"] == DecisionKind.ERROR + assert decision["kind"] == DECISION_ERROR assert engine.state == before @@ -399,7 +395,7 @@ def test_contradiction_prohibit_after_use_always_clarifies(item: str) -> None: before = engine.state decision = engine.step(f"prohibit {item}") - assert decision["kind"] == DecisionKind.ERROR + assert decision["kind"] == DECISION_ERROR assert engine.state == before @@ -459,20 +455,15 @@ def test_deterministic_replacement_matches_equivalent_explicit_transition( decision = engine.step(f"use {new_item} instead of {old_item}") assert expected_decision == { - "kind": DecisionKind.UPDATE, + "kind": DECISION_UPDATE, "state": expected_state, - "prompt_to_user": None, } assert decision == expected_decision assert engine.state == expected_state if not old_present: followup = engine.step("yes") - assert followup == { - "kind": DecisionKind.NO_DIRECTIVE, - "state": None, - "prompt_to_user": None, - } + assert followup == {"kind": DECISION_NO_DIRECTIVE} assert engine.state == expected_state diff --git a/tests/test_repl.py b/tests/test_repl.py index 978a1d5..18c9764 100644 --- a/tests/test_repl.py +++ b/tests/test_repl.py @@ -8,8 +8,7 @@ import pytest import context_compiler.repl as repl_module -from context_compiler import __version__, create_engine -from context_compiler.engine import DecisionKind +from context_compiler import DECISION_UPDATE, __version__, create_engine from context_compiler.repl import run_repl pytestmark = pytest.mark.contract @@ -172,8 +171,7 @@ def test_render_decision_lines_uses_error_prefix_for_all_error_prompts() -> None lines = repl_module._render_decision_lines( { "kind": "error", - "state": None, - "prompt_to_user": "Proceed?", + "message": "Proceed?", } ) @@ -186,7 +184,6 @@ def test_render_diff_lines_includes_added_policy_entries() -> None: "decision": { "kind": "update", "state": {"premise": None, "policies": {"docker": "use"}, "version": 2}, - "prompt_to_user": None, }, "diff": { "premise": {"changed": False, "before": None, "after": None}, @@ -517,7 +514,7 @@ def test_repl_non_interactive_json_bare_input_step_result() -> None: assert row["command"] == "input" decision = row["decision"] assert isinstance(decision, dict) - assert decision["kind"] == DecisionKind.UPDATE + assert decision["kind"] == DECISION_UPDATE def test_repl_non_interactive_json_step_and_preview_results() -> None: @@ -606,7 +603,6 @@ def test_repl_non_interactive_json_step_runs_normally_after_replace_update() -> "command": "step", "decision": { "kind": "update", - "prompt_to_user": None, "state": {"premise": "concise", "policies": {"kubectl": "use"}, "version": 2}, }, "mode": "step", @@ -1152,7 +1148,6 @@ def test_repl_interactive_preview_renders_structural_diff_lines( "decision": { "kind": "update", "state": {"premise": "after", "policies": {"docker": "prohibit"}}, - "prompt_to_user": None, }, "state_before": {"premise": "before", "policies": {"docker": "use", "kubectl": "use"}}, "state_after": {"premise": "after", "policies": {"docker": "prohibit"}}, diff --git a/tests/test_repl_coverage.py b/tests/test_repl_coverage.py index cfcccc0..e145aa9 100644 --- a/tests/test_repl_coverage.py +++ b/tests/test_repl_coverage.py @@ -1,5 +1,6 @@ from io import StringIO +from context_compiler import DECISION_UPDATE from context_compiler.repl import ( _print_command_error, _render_diff_lines, @@ -17,9 +18,8 @@ def test_render_diff_lines_covers_premise_removed_and_changed_policy() -> None: "output_version": 1, "mode": "preview", "decision": { - "kind": "update", + "kind": DECISION_UPDATE, "state": {"premise": "next", "policies": {}}, - "prompt_to_user": None, }, "state_before": {"premise": "before", "policies": {"docker": "use"}}, "state_after": {"premise": "next", "policies": {"docker": "prohibit"}}, diff --git a/tests/test_repl_properties.py b/tests/test_repl_properties.py index 34bbedd..51a4331 100644 --- a/tests/test_repl_properties.py +++ b/tests/test_repl_properties.py @@ -4,7 +4,7 @@ from hypothesis import assume, given from hypothesis import strategies as st -from context_compiler import create_engine +from context_compiler import DECISION_ERROR, DECISION_NO_DIRECTIVE, create_engine from context_compiler.repl import run_repl pytestmark = pytest.mark.contract @@ -33,11 +33,11 @@ def _run_repl_lines(lines: list[str]) -> tuple[str, list[str]]: def _oracle_render_decision(decision: dict[str, object]) -> list[str]: kind = decision["kind"] - if kind == "no_directive": + if kind == DECISION_NO_DIRECTIVE: return ["no_directive"] - if kind == "error": - prompt_obj = decision["prompt_to_user"] + if kind == DECISION_ERROR: + prompt_obj = decision["message"] prompt = prompt_obj if isinstance(prompt_obj, str) else "" prompt_lines = prompt.splitlines() if prompt else [""] return [f"error: {prompt_lines[0]}", *prompt_lines[1:]] @@ -116,5 +116,5 @@ def test_repl_emits_human_readable_output_lines(lines: list[str]) -> None: continue assert not output_line.startswith("{") assert '"kind"' not in output_line - assert '"prompt_to_user"' not in output_line + assert '"message"' not in output_line assert '"state"' not in output_line diff --git a/tests/test_structured_regression.py b/tests/test_structured_regression.py index 0a8e252..f258f08 100644 --- a/tests/test_structured_regression.py +++ b/tests/test_structured_regression.py @@ -62,12 +62,16 @@ def _validate_structured_expected_fixture(expected: dict[str, object], fixture_i decision = turn["decision"] assert isinstance(decision, dict), fixture_id _assert_allowed_keys( - decision, {"kind", "prompt_to_user"}, fixture_id, "expected.turn.decision" + decision, {"kind"} | ({"message"} & set(decision)), fixture_id, "expected.turn.decision" ) assert isinstance(decision["kind"], str), fixture_id - assert decision["prompt_to_user"] is None or isinstance(decision["prompt_to_user"], str), ( - fixture_id - ) + if decision["kind"] == "error": + _assert_allowed_keys( + decision, {"kind", "message"}, fixture_id, "expected.turn.decision" + ) + assert isinstance(decision["message"], str), fixture_id + else: + _assert_allowed_keys(decision, {"kind"}, fixture_id, "expected.turn.decision") def _state_diff(expected: object, actual: object) -> str: @@ -121,9 +125,10 @@ def test_structured_regression_scenarios() -> None: assert decision["kind"] == expected_decision["kind"], ( f"{context} decision_kind_mismatch" ) - assert decision["prompt_to_user"] == expected_decision["prompt_to_user"], ( - f"{context} prompt_to_user_mismatch" - ) + if decision["kind"] == "error": + assert decision["message"] == expected_decision["message"], ( + f"{context} message_mismatch" + ) expected_state = expected_turn["state"] if state != expected_state: @@ -139,7 +144,6 @@ def test_structured_expected_validator_rejects_unknown_turn_decision_field() -> "input": "use docker", "decision": { "kind": "update", - "prompt_to_user": None, "unexpected": True, }, "state": {"premise": None, "policies": {"docker": "use"}, "version": 2},