From 8071702c2de501136cc45446a309c33c7d19af6c Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Tue, 10 Mar 2026 01:05:11 -0400 Subject: [PATCH 1/3] Refactor schema key to focus.primary and centralize constants --- README.md | 10 ++--- docs/DescriptionAndMilestones.md | 2 +- docs/M1Design.md | 10 ++--- examples/01_persistent_guardrails.py | 3 +- examples/04_tool_governance_denylist.py | 4 +- src/context_compiler/const.py | 14 +++++++ src/context_compiler/engine.py | 49 +++++++++++++++---------- tests/test_engine.py | 28 +++++++------- tests/test_examples.py | 24 ++++++------ tests/test_properties.py | 2 +- tests/test_repl.py | 6 +-- 11 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 src/context_compiler/const.py diff --git a/README.md b/README.md index a48828c..b8c1c0f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ State becomes: ```json { "facts": { - "focus.device": null + "focus.primary": null }, "policies": { "prohibit": ["docker"] @@ -131,7 +131,7 @@ The compiler maintains an authoritative state snapshot: ```json { "facts": { - "focus.device": null + "focus.primary": null }, "policies": { "prohibit": [] @@ -141,7 +141,7 @@ The compiler maintains an authoritative state snapshot: ``` **Note** -In M1, the fact schema contains a single exclusive slot: `facts["focus.device"]`. +In M1, the fact schema contains a single exclusive slot: `facts["focus.primary"]`. This slot exists to demonstrate deterministic fact replacement and correction semantics. Richer fact schemas may be introduced in future milestones. @@ -184,7 +184,7 @@ User: I'm using MacBook M3 State update: ```text -facts.focus.device = "MacBook M3" +facts.focus.primary = "MacBook M3" ``` Correction: @@ -196,7 +196,7 @@ User: actually MacBook M2 Result: ```text -facts.focus.device = "MacBook M2" +facts.focus.primary = "MacBook M2" ``` Ambiguous mutation: diff --git a/docs/DescriptionAndMilestones.md b/docs/DescriptionAndMilestones.md index 65db51c..9fb47f3 100644 --- a/docs/DescriptionAndMilestones.md +++ b/docs/DescriptionAndMilestones.md @@ -38,7 +38,7 @@ All state transitions originate from explicit user directives. Explicit user commitments persist reliably within a conversation. A correction means replacing a previously set fact, not evaluating conversational accuracy. -In M1 the fact schema intentionally contains a single exclusive slot (`facts["focus.device"]`). +In M1 the fact schema intentionally contains a single exclusive slot (`facts["focus.primary"]`). This slot demonstrates deterministic fact replacement and correction semantics. Policies (`policies.prohibit`) provide the primary mechanism for persistent conversational constraints. diff --git a/docs/M1Design.md b/docs/M1Design.md index d619ad7..1ccd052 100644 --- a/docs/M1Design.md +++ b/docs/M1Design.md @@ -68,7 +68,7 @@ State is a deterministic snapshot: `json { "facts": { - "focus.device": null + "focus.primary": null }, "policies": { "prohibit": [] @@ -120,7 +120,7 @@ Accepted patterns: Produces: - FACT_SET(key="focus.device", value=X) + FACT_SET(key="focus.primary", value=X) “Using” statements set the current discussion focus, not inventory. Hard-positive directives store fact values as opaque strings and do not @@ -228,8 +228,8 @@ While pending exists, no other mutation may occur. #### Facts (exclusive) - focus.device = "nord stage 3" - focus.device = "nord stage 4" + focus.primary = "nord stage 3" + focus.primary = "nord stage 4" →` only stage 4 active` @@ -249,7 +249,7 @@ Example: `json { - "facts": {"focus.device": "nord stage 4"}, + "facts": {"focus.primary": "nord stage 4"}, "policies": {"prohibit": ["parallel octaves"]} } ` diff --git a/examples/01_persistent_guardrails.py b/examples/01_persistent_guardrails.py index d0302aa..e4b087e 100644 --- a/examples/01_persistent_guardrails.py +++ b/examples/01_persistent_guardrails.py @@ -3,10 +3,11 @@ from _util import print_json from context_compiler import State, create_engine +from context_compiler.const import POLICY_PROHIBIT, STATE_POLICIES def build_prompt(state: State, user_input: str) -> str: - prohibit = state["policies"]["prohibit"] + prohibit = state[STATE_POLICIES][POLICY_PROHIBIT] prohibit_text = ", ".join(prohibit) if prohibit else "(none)" return ( "System: Follow authoritative conversation state.\n" diff --git a/examples/04_tool_governance_denylist.py b/examples/04_tool_governance_denylist.py index 94b9967..083ba5a 100644 --- a/examples/04_tool_governance_denylist.py +++ b/examples/04_tool_governance_denylist.py @@ -5,6 +5,7 @@ from _util import print_json from context_compiler import create_engine +from context_compiler.const import POLICY_PROHIBIT, STATE_POLICIES @dataclass @@ -34,9 +35,10 @@ def main() -> None: print() print("Host-side tool denylist behavior:") + prohibit = state[STATE_POLICIES][POLICY_PROHIBIT] tools = [Tool("docker"), Tool("kubectl")] for tool in tools: - if tool.name in state["policies"]["prohibit"]: + if tool.name in prohibit: block_tool(tool) else: allow_tool(tool) diff --git a/src/context_compiler/const.py b/src/context_compiler/const.py new file mode 100644 index 0000000..cd8fd98 --- /dev/null +++ b/src/context_compiler/const.py @@ -0,0 +1,14 @@ +"""Schema key constants used by the context compiler state model.""" + +from typing import Final + +# State schema +STATE_FACTS: Final = "facts" +STATE_POLICIES: Final = "policies" +STATE_VERSION: Final = "version" + +# Fact keys +FOCUS_PRIMARY: Final = "focus.primary" + +# Policy keys +POLICY_PROHIBIT: Final = "prohibit" diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index 984efc5..fc2203e 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -11,7 +11,15 @@ from typing import Literal, TypedDict from unicodedata import normalize as unicode_normalize -FactsState = TypedDict("FactsState", {"focus.device": str | None}) +from .const import ( + FOCUS_PRIMARY, + POLICY_PROHIBIT, + STATE_FACTS, + STATE_POLICIES, + STATE_VERSION, +) + +FactsState = TypedDict("FactsState", {"focus.primary": str | None}) class PoliciesState(TypedDict): @@ -123,14 +131,15 @@ def _classify(self, user_input: str) -> PendingEvent | Decision: if correction is not None: if self._last_exclusive_fact_key is None: return _clarify( - "I don't have a previous focus setting to correct. What should focus.device be?" + "I don't have a previous focus setting to correct. " + "What should focus.primary be?" ) if not correction.strip(): - return _clarify("What should focus.device be?") + return _clarify("What should focus.primary be?") if _has_multiple_values(correction): - return _clarify("Corrections for focus.device must contain one value.") + return _clarify("Corrections for focus.primary must contain one value.") if _invalid_exclusive_value(correction): - return _clarify("Corrections for focus.device must contain one value.") + return _clarify("Corrections for focus.primary must contain one value.") return PendingEvent(kind="fact_set", fact_value=correction) policy_add = _parse_hard_negative(normalized_message) @@ -148,11 +157,11 @@ def _classify(self, user_input: str) -> PendingEvent | Decision: positive_value = _parse_hard_positive(user_input, normalized_message) if positive_value is not None: if not positive_value.strip(): - return _clarify("What should focus.device be?") + return _clarify("What should focus.primary be?") if _has_multiple_values(positive_value): - return _clarify("focus.device accepts one value. Please provide a single value.") + return _clarify("focus.primary accepts one value. Please provide a single value.") if _invalid_exclusive_value(positive_value): - return _clarify("focus.device value is unclear. Please provide one value.") + return _clarify("focus.primary value is unclear. Please provide one value.") return PendingEvent(kind="fact_set", fact_value=positive_value) pending = _parse_ambiguous_mutation(user_input) @@ -197,7 +206,7 @@ def _apply_event(self, event: PendingEvent) -> Decision: return self._remove_policies(list(event.values)) if event.kind == "fact_set": assert event.fact_value is not None - return self._set_focus_device(event.fact_value) + return self._set_focus_primary(event.fact_value) if event.kind == "reset_policies": self._state = _initial_state() self._last_exclusive_fact_key = None @@ -207,31 +216,31 @@ def _apply_event(self, event: PendingEvent) -> Decision: self._last_exclusive_fact_key = None return _update_decision(self._state) - def _set_focus_device(self, value: str) -> Decision: - self._state["facts"]["focus.device"] = _clean_fact_value(value) - self._last_exclusive_fact_key = "focus.device" + def _set_focus_primary(self, value: str) -> Decision: + self._state[STATE_FACTS][FOCUS_PRIMARY] = _clean_fact_value(value) + self._last_exclusive_fact_key = FOCUS_PRIMARY return _update_decision(self._state) def _add_policies(self, values: list[str]) -> Decision: - existing = set(self._state["policies"]["prohibit"]) + existing = set(self._state[STATE_POLICIES][POLICY_PROHIBIT]) for value in values: existing.add(value) - self._state["policies"]["prohibit"] = sorted(existing) + self._state[STATE_POLICIES][POLICY_PROHIBIT] = sorted(existing) return _update_decision(self._state) def _remove_policies(self, values: list[str]) -> Decision: - existing = set(self._state["policies"]["prohibit"]) + existing = set(self._state[STATE_POLICIES][POLICY_PROHIBIT]) for value in values: existing.discard(value) - self._state["policies"]["prohibit"] = sorted(existing) + self._state[STATE_POLICIES][POLICY_PROHIBIT] = sorted(existing) return _update_decision(self._state) def _initial_state() -> State: return { - "facts": {"focus.device": None}, - "policies": {"prohibit": []}, - "version": 1, + STATE_FACTS: {FOCUS_PRIMARY: None}, + STATE_POLICIES: {POLICY_PROHIBIT: []}, + STATE_VERSION: 1, } @@ -368,5 +377,5 @@ def _pending_prompt(pending: PendingEvent) -> str: if pending.kind == "policy_remove" and pending.values: return f"Did you mean to allow '{pending.values[0]}'?" if pending.kind == "fact_set" and pending.fact_value is not None: - return f"Did you mean to set focus.device to '{pending.fact_value}'?" + return f"Did you mean to set focus.primary to '{pending.fact_value}'?" return "Your directive was unclear. Did you mean to change state?" diff --git a/tests/test_engine.py b/tests/test_engine.py index 072c6b5..bf77fbd 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -6,7 +6,7 @@ def test_directive_parsing_positive_negative_and_allow() -> None: decision1 = engine.step("use Nord Stage 4") assert decision1["kind"] == "update" - assert engine.state["facts"]["focus.device"] == "Nord Stage 4" + assert engine.state["facts"]["focus.primary"] == "Nord Stage 4" decision2 = engine.step("please don't use the parallel octaves and a voice crossing") assert decision2["kind"] == "update" @@ -43,7 +43,7 @@ def test_correction_replaces_most_recent_exclusive_fact() -> None: decision = engine.step("actually Nord Stage 4") assert decision["kind"] == "update" - assert engine.state["facts"]["focus.device"] == "Nord Stage 4" + assert engine.state["facts"]["focus.primary"] == "Nord Stage 4" def test_correction_without_prior_exclusive_fact_requires_clarification() -> None: @@ -52,7 +52,7 @@ def test_correction_without_prior_exclusive_fact_requires_clarification() -> Non decision = engine.step("correction: Nord Stage 4") assert decision["kind"] == "clarify" - assert engine.state["facts"]["focus.device"] is None + assert engine.state["facts"]["focus.primary"] is None def test_policy_additions_are_sorted_unique_and_removal_is_noop_when_absent() -> None: @@ -77,7 +77,7 @@ def test_ambiguity_returns_clarify_and_does_not_mutate() -> None: assert decision["kind"] == "clarify" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -90,7 +90,7 @@ def test_ambiguous_multi_item_directive_requires_clarification() -> None: assert decision["kind"] == "clarify" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -108,7 +108,7 @@ def test_correction_does_not_bypass_pending_clarification() -> None: assert decision2["kind"] == "clarify" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -157,13 +157,13 @@ def test_pending_clarification_blocks_other_mutations_until_resolved() -> None: decision2 = engine.step("use Nord Stage 4") assert decision2["kind"] == "clarify" - assert engine.state["facts"]["focus.device"] is None + assert engine.state["facts"]["focus.primary"] is None assert engine.state["policies"]["prohibit"] == [] decision3 = engine.step("yes") assert decision3["kind"] == "update" assert engine.state["policies"]["prohibit"] == ["docker"] - assert engine.state["facts"]["focus.device"] is None + assert engine.state["facts"]["focus.primary"] is None def test_reset_commands() -> None: @@ -175,7 +175,7 @@ def test_reset_commands() -> None: decision1 = engine.step("reset policies") assert decision1["kind"] == "update" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -186,7 +186,7 @@ def test_reset_commands() -> None: decision_constraints = engine.step("clear constraints") assert decision_constraints["kind"] == "update" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -197,7 +197,7 @@ def test_reset_commands() -> None: decision2 = engine.step("clear state") assert decision2["kind"] == "update" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -222,7 +222,7 @@ def test_reset_policies_when_already_empty_resets_state() -> None: assert decision["kind"] == "update" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } @@ -234,7 +234,7 @@ def test_unicode_apostrophe_positive_directive() -> None: decision = engine.step("i’m using Nord Stage 4") assert decision["kind"] == "update" - assert engine.state["facts"]["focus.device"] == "Nord Stage 4" + assert engine.state["facts"]["focus.primary"] == "Nord Stage 4" def test_unicode_apostrophe_negative_directive() -> None: @@ -304,7 +304,7 @@ def test_yes_after_non_pending_clarify_is_passthrough() -> None: assert decision2["kind"] == "passthrough" assert engine.state == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } diff --git a/tests/test_examples.py b/tests/test_examples.py index ba0542d..0088b2d 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -33,7 +33,7 @@ def test_persistent_guardrails_example_output() -> None: "kind": "update", "prompt_to_user": None, "state": { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, }, @@ -45,7 +45,7 @@ def test_persistent_guardrails_example_output() -> None: assert ( _canonical_json( { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, } @@ -67,7 +67,7 @@ def test_configuration_and_correction_example_output() -> None: "kind": "update", "prompt_to_user": None, "state": { - "facts": {"focus.device": "MacBook M3"}, + "facts": {"focus.primary": "MacBook M3"}, "policies": {"prohibit": []}, "version": 1, }, @@ -78,7 +78,7 @@ def test_configuration_and_correction_example_output() -> None: assert ( _canonical_json( { - "facts": {"focus.device": "MacBook M3"}, + "facts": {"focus.primary": "MacBook M3"}, "policies": {"prohibit": []}, "version": 1, } @@ -92,7 +92,7 @@ def test_configuration_and_correction_example_output() -> None: "kind": "update", "prompt_to_user": None, "state": { - "facts": {"focus.device": "MacBook M2"}, + "facts": {"focus.primary": "MacBook M2"}, "policies": {"prohibit": []}, "version": 1, }, @@ -103,7 +103,7 @@ def test_configuration_and_correction_example_output() -> None: assert ( _canonical_json( { - "facts": {"focus.device": "MacBook M2"}, + "facts": {"focus.primary": "MacBook M2"}, "policies": {"prohibit": []}, "version": 1, } @@ -136,7 +136,7 @@ def test_ambiguity_with_clarification_example_output() -> None: "kind": "update", "prompt_to_user": None, "state": { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, }, @@ -147,7 +147,7 @@ def test_ambiguity_with_clarification_example_output() -> None: assert ( _canonical_json( { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, } @@ -167,7 +167,7 @@ def test_tool_governance_denylist_example_output() -> None: "kind": "update", "prompt_to_user": None, "state": { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, }, @@ -179,7 +179,7 @@ def test_tool_governance_denylist_example_output() -> None: assert ( _canonical_json( { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, } @@ -194,12 +194,12 @@ def test_tool_governance_denylist_example_output() -> None: def test_llm_integration_pattern_example_output() -> None: output = _run_example("05_llm_integration_pattern.py") docker_state = { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, } docker_kubernetes_state = { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker", "kubernetes"]}, "version": 1, } diff --git a/tests/test_properties.py b/tests/test_properties.py index ebb8eb3..baa0726 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -85,7 +85,7 @@ def test_exclusive_fact_replacement_last_write_wins(first: str, second: str) -> engine.step(f"use {first}") engine.step(f"use {second}") - assert engine.state["facts"]["focus.device"] == _clean(second) + assert engine.state["facts"]["focus.primary"] == _clean(second) @given( diff --git a/tests/test_repl.py b/tests/test_repl.py index f9d92b1..7966ba1 100644 --- a/tests/test_repl.py +++ b/tests/test_repl.py @@ -17,7 +17,7 @@ def test_repl_update_flow() -> None: assert len(decisions) == 1 assert decisions[0]["kind"] == "update" assert decisions[0]["state"] == { - "facts": {"focus.device": "Nord Stage 4"}, + "facts": {"focus.primary": "Nord Stage 4"}, "policies": {"prohibit": []}, "version": 1, } @@ -37,13 +37,13 @@ def test_repl_state_persists_across_turns() -> None: assert len(decisions) == 2 assert decisions[0]["kind"] == "update" assert decisions[0]["state"] == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": ["docker"]}, "version": 1, } assert decisions[1]["kind"] == "update" assert decisions[1]["state"] == { - "facts": {"focus.device": None}, + "facts": {"focus.primary": None}, "policies": {"prohibit": []}, "version": 1, } From 69f47bfed7a326e549366e623f5e95b3edc6444c Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Tue, 10 Mar 2026 01:10:57 -0400 Subject: [PATCH 2/3] Use StrEnum for DecisionKind --- src/context_compiler/engine.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index fc2203e..c4d72ce 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -8,6 +8,7 @@ import re from copy import deepcopy from dataclasses import dataclass +from enum import StrEnum from typing import Literal, TypedDict from unicodedata import normalize as unicode_normalize @@ -34,8 +35,14 @@ class State(TypedDict): version: Literal[1] +class DecisionKind(StrEnum): + UPDATE = "update" + PASSTHROUGH = "passthrough" + CLARIFY = "clarify" + + class Decision(TypedDict): - kind: Literal["passthrough", "update", "clarify"] + kind: DecisionKind state: State | None prompt_to_user: str | None @@ -69,7 +76,7 @@ class NegativeDirectiveRule: _SPLIT_RE = re.compile(r",|\s+and\s+", re.IGNORECASE) _PASSTHROUGH: Decision = { - "kind": "passthrough", + "kind": DecisionKind.PASSTHROUGH, "state": None, "prompt_to_user": None, } @@ -246,7 +253,7 @@ def _initial_state() -> State: def _clarify(prompt: str) -> Decision: return { - "kind": "clarify", + "kind": DecisionKind.CLARIFY, "state": None, "prompt_to_user": prompt, } @@ -254,7 +261,7 @@ def _clarify(prompt: str) -> Decision: def _update_decision(state: State) -> Decision: return { - "kind": "update", + "kind": DecisionKind.UPDATE, "state": deepcopy(state), "prompt_to_user": None, } From f08da6a83172bc1a5d11bf9f2256e36b079a5e3b Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Tue, 10 Mar 2026 01:13:05 -0400 Subject: [PATCH 3/3] Add DecisionKind StrEnum behavior test --- tests/test_engine.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_engine.py b/tests/test_engine.py index bf77fbd..cccb787 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,4 +1,12 @@ from context_compiler import create_engine +from context_compiler.engine import DecisionKind + + +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_directive_parsing_positive_negative_and_allow() -> None: