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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions docs/M1Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions src/context_compiler/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)


Expand Down
28 changes: 15 additions & 13 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"]
Expand Down Expand Up @@ -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 == {
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 18 additions & 4 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading