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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
8 changes: 4 additions & 4 deletions docs/M1Design.md → docs/DirectiveGrammarSpec.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -329,7 +329,7 @@ Produces:
state = initial_state
Decision.kind = "update"

### 12. Non-Goals (M1)
### 12. Non-Goals

Not implemented:

Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 21 additions & 7 deletions src/context_compiler/engine.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
119 changes: 119 additions & 0 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down