From 26e9bcfc218ec28ac4d8082a4a38c16afbc73f22 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Tue, 17 Mar 2026 01:59:48 -0400 Subject: [PATCH 1/2] fix: preserve literal correction payload semantics --- src/context_compiler/engine.py | 24 +++++-- tests/test_engine.py | 119 +++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index 8d5e150..d15a50f 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -552,7 +552,9 @@ def _correction_payload_invokes_other_directive_family(payload: str) -> bool: if normalized_payload in _RESET_POLICY or normalized_payload in _CLEAR_STATE: return True - if _parse_hard_negative(normalized_payload) is not None: + if _parse_hard_negative( + normalized_payload + ) is not None and not _looks_like_literal_fact_payload(normalized_payload): return True if _parse_allow(payload) is not None: return True @@ -595,10 +597,22 @@ def _has_mixed_directive_families(user_input: str) -> bool: return True correction_payload = _parse_correction(user_input) - if correction_payload is not None and correction_payload.strip(): - payload_family = _classify_segment_family(correction_payload) - if payload_family is not None and payload_family != "correction": - return True + return bool( + correction_payload is not None + and correction_payload.strip() + and _correction_payload_invokes_other_directive_family(correction_payload) + ) + + +def _looks_like_literal_fact_payload(normalized_payload: str) -> bool: + for rule in _NEGATIVE_DIRECTIVE_RULES: + if not normalized_payload.startswith(rule.starter): + continue + + tail = normalized_payload[len(rule.starter) :].strip() + # In correction payloads, allow quoted/name-like phrases that begin + # with directive words but continue as literal fact text. + return bool(re.search(r"\s+(?:is|as)\s+", tail)) return False diff --git a/tests/test_engine.py b/tests/test_engine.py index 5c30ade..d190f3f 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -348,6 +348,125 @@ def test_please_use_directive_updates_fact() -> None: assert engine.state["facts"]["focus.primary"] == "Nord Stage 4" +def test_set_directive_updates_fact() -> None: + engine = create_engine() + + decision = engine.step("set Nord Stage 5") + + assert decision["kind"] == "update" + assert engine.state["facts"]["focus.primary"] == "Nord Stage 5" + + +def test_you_can_directive_removes_existing_policy() -> None: + engine = create_engine() + engine.step("don't use docker") + + decision = engine.step("you can docker") + + assert decision["kind"] == "update" + assert engine.state["policies"]["prohibit"] == [] + + +def test_correction_colon_marker_replaces_prior_fact() -> None: + engine = create_engine() + engine.step("use Nord Stage 4") + + decision = engine.step("correction: Nord Stage 5") + + assert decision["kind"] == "update" + assert engine.state["facts"]["focus.primary"] == "Nord Stage 5" + + +def test_hard_negative_comma_splitting_adds_multiple_policies() -> None: + engine = create_engine() + + decision = engine.step("don't use peanuts, shellfish") + + assert decision["kind"] == "update" + assert engine.state["policies"]["prohibit"] == ["peanuts", "shellfish"] + + +@pytest.mark.parametrize("phrase", ["clear state now", "reset policies please"]) +def test_reset_command_near_miss_phrases_are_passthrough_and_do_not_mutate_state( + phrase: str, +) -> None: + engine = create_engine() + engine.step("use Nord Stage 4") + engine.step("don't use docker") + before = engine.state + + decision = engine.step(phrase) + + assert decision["kind"] == "passthrough" + assert engine.state == before + + +@pytest.mark.parametrize( + ("text", "expected_fact"), + [ + ("use Don't Use Me as the project name", "Don't Use Me as the project name"), + ("use Reset Policies as the track title", "Reset Policies as the track title"), + ("use Clear State as the album title", "Clear State as the album title"), + ("use Allow Shellfish as the restaurant name", "Allow Shellfish as the restaurant name"), + ], +) +def test_hard_positive_literals_with_directive_words_do_not_trigger_side_effects( + text: str, expected_fact: str +) -> None: + engine = create_engine() + engine.step("don't use voice crossing") + + decision = engine.step(text) + + assert decision["kind"] == "update" + assert decision["prompt_to_user"] is None + assert engine.state == { + "facts": {"focus.primary": expected_fact}, + "policies": {"prohibit": ["voice crossing"]}, + "version": 1, + } + + +def test_correction_literal_with_directive_words_updates_fact_without_side_effects() -> None: + engine = create_engine() + engine.step("don't use voice crossing") + engine.step("use Nord Stage 4") + + decision = engine.step("actually Don't Use Docker is the codename") + + assert decision["kind"] == "update" + assert decision["prompt_to_user"] is None + assert engine.state == { + "facts": {"focus.primary": "Don't Use Docker is the codename"}, + "policies": {"prohibit": ["voice crossing"]}, + "version": 1, + } + + +@pytest.mark.parametrize( + "correction", + [ + "actually reset policies", + "actually clear state", + "actually allow docker", + "actually docker is fine", + "actually use Nord Stage 5", + ], +) +def test_correction_payloads_from_other_directive_families_clarify_without_mutation( + correction: str, +) -> None: + engine = create_engine() + engine.step("don't use voice crossing") + engine.step("use Nord Stage 4") + before = engine.state + + decision = engine.step(correction) + + assert decision["kind"] == "clarify" + assert engine.state == before + + def test_correction_without_prior_exclusive_fact_requires_clarification() -> None: engine = create_engine() From 9caab02769e057b8f0a15892622d42f20b34631e Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Tue, 17 Mar 2026 02:06:24 -0400 Subject: [PATCH 2/2] docs: rename directive grammar spec references --- README.md | 2 +- docs/{M1Design.md => DirectiveGrammarSpec.md} | 8 ++++---- docs/README.md | 4 ++-- src/context_compiler/engine.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename docs/{M1Design.md => DirectiveGrammarSpec.md} (98%) diff --git a/README.md b/README.md index fd231ae..56c9746 100644 --- a/README.md +++ b/README.md @@ -329,7 +329,7 @@ These invariants are verified through behavioral tests and Hypothesis-based prop More detailed design and milestone documents are available in: - [Project overview](docs/DescriptionAndMilestones.md) -- [M1 design document](docs/M1Design.md) +- [Directive grammar specification](docs/DirectiveGrammarSpec.md) --- diff --git a/docs/M1Design.md b/docs/DirectiveGrammarSpec.md similarity index 98% rename from docs/M1Design.md rename to docs/DirectiveGrammarSpec.md index 058b1cd..c16e6a6 100644 --- a/docs/M1Design.md +++ b/docs/DirectiveGrammarSpec.md @@ -1,10 +1,10 @@ -# Context Compiler — Milestone 1 Design Specification +# Context Compiler — Directive Grammar Specification ## Goal Ensure explicit user corrections and constraints persist reliably within a single conversation. -M1 provides authoritative conversational state.It does **not** perform +This specification provides authoritative conversational state. It does **not** perform reasoning, inference, entity resolution, cross-session memory, or planning. @@ -329,7 +329,7 @@ Produces: state = initial_state Decision.kind = "update" -### 12. Non-Goals (M1) +### 12. Non-Goals Not implemented: @@ -360,7 +360,7 @@ Examples: **Persistence** Policies persist across turns **Idempotency** Duplicate policy adds do nothing -## End of M1 +## End This milestone establishes the authoritative truth layer.All later milestones build on this deterministic contract. diff --git a/docs/README.md b/docs/README.md index dcb6c6b..37eef1c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,5 +2,5 @@ This directory contains the project design and milestone specifications. -- `DescriptionAndMilestones.md` — project overview and milestone roadmap -- `M1Design.md` — authoritative behavioral specification for the deterministic directive engine +- [DescriptionAndMilestones.md](DescriptionAndMilestones.md) — project overview and milestone roadmap +- [DirectiveGrammarSpec.md](DirectiveGrammarSpec.md) — authoritative behavioral specification for the deterministic directive engine diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index d15a50f..9d87ac7 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -1,4 +1,4 @@ -"""Deterministic M1 state engine for explicit user directive handling. +"""Deterministic state engine for explicit user directive handling. This module provides a small, model-independent state machine that parses high-confidence directives and emits host decisions without invoking an LLM. @@ -124,7 +124,7 @@ def compile_transcript(messages: list[dict[str, object]]) -> ApplyResult: class Engine: - """Deterministic state engine implementing M1 directive semantics. + """Deterministic state engine implementing directive semantics. Design note: - ``step()`` is the mutation interface for directive-driven updates.