diff --git a/docs/M1Design.md b/docs/M1Design.md index d066e83..d619ad7 100644 --- a/docs/M1Design.md +++ b/docs/M1Design.md @@ -96,20 +96,19 @@ Accepted patterns: - "don't X" - "do not X" - "never X" -- "avoid X" -- "refrain from X" - "please don't X" Produces: POLICY_ADD(normalize(X)) -`"refrain from X"` is treated as a hard-negative directive and adds -`normalize(X)` to `policies.prohibit`. - For hard-negative directives, `normalize(X)` is applied to each policy payload item before storage. +Soft preference phrases like `"avoid X"` and `"refrain from X"` are +not hard directives in M1. They are treated as passthrough input and do +not mutate authoritative state. + #### 5.2 Hard Positive Directives Accepted patterns: @@ -236,8 +235,8 @@ While pending exists, no other mutation may occur. #### Policies (additive) - avoid parallel octaves - avoid voice crossing + don't use parallel octaves + do not use voice crossing Adding duplicate policy is a no-op. Policies stored in sorted lexical order. diff --git a/src/context_compiler/engine.py b/src/context_compiler/engine.py index 2e2cb84..984efc5 100644 --- a/src/context_compiler/engine.py +++ b/src/context_compiler/engine.py @@ -71,8 +71,6 @@ class NegativeDirectiveRule: NegativeDirectiveRule(starter="don't ", kind="policy_add", strip_leading_use=True), NegativeDirectiveRule(starter="do not ", kind="policy_add", strip_leading_use=True), NegativeDirectiveRule(starter="never ", kind="policy_add", strip_leading_use=True), - NegativeDirectiveRule(starter="refrain from ", kind="policy_add", strip_leading_use=False), - NegativeDirectiveRule(starter="avoid ", kind="policy_add", strip_leading_use=False), ) diff --git a/tests/test_engine.py b/tests/test_engine.py index a6935e1..072c6b5 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -26,13 +26,14 @@ def test_hard_negative_dont_use_stores_item_without_use_prefix() -> None: assert engine.state["policies"]["prohibit"] == ["docker"] -def test_hard_negative_avoid_use_keeps_use_token() -> None: +def test_avoid_is_passthrough_and_does_not_mutate_state() -> None: engine = create_engine() + before = engine.state decision = engine.step("avoid use docker") - assert decision["kind"] == "update" - assert engine.state["policies"]["prohibit"] == ["use docker"] + assert decision["kind"] == "passthrough" + assert engine.state == before def test_correction_replaces_most_recent_exclusive_fact() -> None: @@ -57,9 +58,9 @@ def test_correction_without_prior_exclusive_fact_requires_clarification() -> Non def test_policy_additions_are_sorted_unique_and_removal_is_noop_when_absent() -> None: engine = create_engine() - engine.step("avoid voice crossing") - engine.step("avoid the parallel octaves") - decision = engine.step("avoid voice crossing") + engine.step("don't use voice crossing") + engine.step("never the parallel octaves") + decision = engine.step("do not voice crossing") assert decision["kind"] == "update" assert engine.state["policies"]["prohibit"] == ["parallel octaves", "voice crossing"] @@ -103,7 +104,7 @@ def test_correction_does_not_bypass_pending_clarification() -> None: assert decision1["kind"] == "clarify" # attempt correction while pending - decision2 = engine.step("actually avoid docker") + decision2 = engine.step("actually don't use docker") assert decision2["kind"] == "clarify" assert engine.state == { @@ -169,7 +170,7 @@ def test_reset_commands() -> None: engine = create_engine() engine.step("use Nord Stage 4") - engine.step("avoid parallel octaves") + engine.step("don't use parallel octaves") decision1 = engine.step("reset policies") assert decision1["kind"] == "update" @@ -180,7 +181,7 @@ def test_reset_commands() -> None: } engine.step("use Nord Stage 4") - engine.step("avoid parallel octaves") + engine.step("don't use parallel octaves") decision_constraints = engine.step("clear constraints") assert decision_constraints["kind"] == "update" @@ -191,7 +192,7 @@ def test_reset_commands() -> None: } engine.step("use Nord Stage 4") - engine.step("avoid parallel octaves") + engine.step("don't use parallel octaves") decision2 = engine.step("clear state") assert decision2["kind"] == "update" @@ -318,13 +319,14 @@ def test_unicode_apostrophe_negative_directive_with_please() -> None: assert engine.state["policies"]["prohibit"] == ["docker"] -def test_refrain_from_does_not_strip_use_prefix() -> None: +def test_refrain_from_is_passthrough_and_does_not_mutate_state() -> None: engine = create_engine() + before = engine.state decision = engine.step("refrain from use docker") - assert decision["kind"] == "update" - assert engine.state["policies"]["prohibit"] == ["use docker"] + assert decision["kind"] == "passthrough" + assert engine.state == before def test_hard_negative_dont_without_apostrophe_updates_policy() -> None: diff --git a/tests/test_properties.py b/tests/test_properties.py index 4532003..ebb8eb3 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -27,10 +27,10 @@ def test_determinism_same_input_sequence_same_state(inputs: list[str]) -> None: def test_policy_addition_is_idempotent(item: str) -> None: engine = create_engine() - engine.step(f"avoid {item}") + engine.step(f"don't use {item}") once = list(engine.state["policies"]["prohibit"]) - engine.step(f"avoid {item}") + engine.step(f"don't use {item}") twice = list(engine.state["policies"]["prohibit"]) assert twice == once @@ -59,7 +59,7 @@ def test_policy_persists_across_passthrough_turns(item: str, turns: list[str]) - normalized_item = _split_items(item) assume(len(normalized_item) == 1) - decision = engine.step(f"avoid {item}") + decision = engine.step(f"don't use {item}") expected = normalized_item[0] assert decision["kind"] == "update" assert engine.state["policies"]["prohibit"] == [expected] @@ -116,12 +116,26 @@ def test_safety_near_miss_inputs_do_not_mutate_state(text: str) -> None: def test_hyphenated_and_token_not_split(token: str) -> None: engine = create_engine() - decision = engine.step(f"avoid {token}") + decision = engine.step(f"don't use {token}") assert decision["kind"] == "update" assert engine.state["policies"]["prohibit"] == [token] +@given(st.text(min_size=1, max_size=40)) +def test_soft_negative_phrases_do_not_mutate_state(text: str) -> None: + engine = create_engine() + before = engine.state + + decision_avoid = engine.step(f"avoid {text}") + assert decision_avoid["kind"] == "passthrough" + assert engine.state == before + + decision_refrain = engine.step(f"refrain from {text}") + assert decision_refrain["kind"] == "passthrough" + assert engine.state == before + + @given(st.from_regex(r"[A-Za-z0-9][A-Za-z0-9 ]{0,20}", fullmatch=True)) def test_please_use_is_not_a_supported_positive_directive(value: str) -> None: assume("," not in value) diff --git a/tests/test_repl.py b/tests/test_repl.py index beef478..f9d92b1 100644 --- a/tests/test_repl.py +++ b/tests/test_repl.py @@ -32,7 +32,7 @@ def test_repl_clarify_flow() -> None: def test_repl_state_persists_across_turns() -> None: - decisions = _run_session("avoid docker\nallow docker\nquit\n") + decisions = _run_session("don't use docker\nallow docker\nquit\n") assert len(decisions) == 2 assert decisions[0]["kind"] == "update"