diff --git a/docs/api-reference.md b/docs/api-reference.md index d5530c8..c0ef436 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -70,34 +70,30 @@ Public grammar surface: - `CanonicalDirective` - `DirectiveKind` -- `ValidatedDirective` - `match_canonical_directive_start(text, start)` - `contains_multiple_canonical_directives(text)` - `decompose_directive(text)` -- `validate_directive(text)` - `render_directive(kind, /, **operands)` -Use this surface for exact canonical validation, canonical directive syntax -decomposition, or canonical directive string construction only. +Use this surface for exact canonical directive decomposition and +classification via `decompose_directive(...)`, shallow syntax-start detection, +or canonical directive string construction only. Boundary notes: - `match_canonical_directive_start(...)` only matches a canonical directive - prefix at a position; it does not validate a whole directive + prefix at a position; it does not decompose or accept a whole directive - `contains_multiple_canonical_directives(...)` detects compound - directive-shaped structure only; it is not full validation -- decomposition exposes canonical syntax only + directive-shaped structure only; it is not full decomposition +- use `decompose_directive(text)` to determine whether text is a complete + canonical directive +- a non-`None` decomposition returns a `CanonicalDirective` with `kind`, + `operands`, and preserved accepted `text` - `CanonicalDirective.text` preserves the original accepted input text, so caller casing or formatting may remain visible there - `CanonicalDirective.text` is not canonical serialized directive text -- callers can treat `decompose_directive(...) is not None` as the complete - canonical-directive check when operand access is needed +- `match_canonical_directive_start(...)` is only for shallow syntax detection - operands are grammar-level text, not normalized semantic values -- `ValidatedDirective.text` preserves the accepted input text used for - classification -- callers can treat `validate_directive(...) is not None` as the - canonical-directive check when only classification is needed -- validation returns `None` for any non-canonical input - decomposition returns `None` for any non-canonical input - `render_directive(...)` produces canonical directive text from semantic kind and operands @@ -116,8 +112,7 @@ Boundary notes: Core does not lowercase operands, collapse internal operand whitespace, or convert operand text into engine/domain identifiers at the grammar layer. Canonical serialized directive output comes from -`render_directive(kind, /, **operands)`, not from `CanonicalDirective.text` or -`ValidatedDirective.text`. +`render_directive(kind, /, **operands)`, not from `CanonicalDirective.text`. ### `engine.premise` diff --git a/pyproject.toml b/pyproject.toml index 64d8a30..c9d98b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "context-compiler" -version = "0.9.0dev6" +version = "0.9.0dev7" description = "Deterministic conversational state engine for LLM applications." readme = "README.md" requires-python = ">=3.11" diff --git a/src/context_compiler/grammar.py b/src/context_compiler/grammar.py index 735f109..cfe6d8b 100644 --- a/src/context_compiler/grammar.py +++ b/src/context_compiler/grammar.py @@ -21,19 +21,6 @@ class DirectiveKind(StrEnum): CLEAR_STATE = "clear_state" -@dataclass(frozen=True, slots=True) -class ValidatedDirective: - """Classify accepted input text as one canonical directive kind. - - ``text`` preserves the accepted input text used for classification rather - than a canonical rendered representation, so caller casing and formatting - may remain visible here. - """ - - text: str - kind: DirectiveKind - - @dataclass(frozen=True, slots=True) class CanonicalDirective: """Represent one parsed canonical directive and its named operands. @@ -409,22 +396,6 @@ def decompose_directive(text: str) -> CanonicalDirective | None: return None -def validate_directive(text: str) -> ValidatedDirective | None: - """Classify whether text is a canonical directive. - - This determines whether ``text`` belongs to one canonical directive family - and returns only the normalized semantic kind needed for classification. - Callers can determine whether ``text`` is a canonical directive by checking - whether this returns a non-`None` result while only receiving - classification information. It does not expose operands, render directives, - repair malformed text, or evaluate any state transition. - """ - parsed = decompose_directive(text) - if parsed is None: - return None - return ValidatedDirective(text=parsed.text, kind=parsed.kind) - - def render_directive(kind: DirectiveKind, /, **operands: str) -> str: """Produce canonical directive text from a semantic kind and operands. @@ -461,8 +432,8 @@ def render_directive(kind: DirectiveKind, /, **operands: str) -> str: operand_view = MappingProxyType(normalized_operands) rendered = spec.renderer(operand_view) - validated = validate_directive(rendered) - if validated is None or validated.kind is not normalized_kind: + decomposed = decompose_directive(rendered) + if decomposed is None or decomposed.kind is not normalized_kind: raise ValueError(f"Operands do not produce a canonical {normalized_kind.value} directive.") return rendered @@ -470,10 +441,8 @@ def render_directive(kind: DirectiveKind, /, **operands: str) -> str: __all__ = [ "CanonicalDirective", "DirectiveKind", - "ValidatedDirective", "contains_multiple_canonical_directives", "decompose_directive", "match_canonical_directive_start", "render_directive", - "validate_directive", ] diff --git a/tests/_api_contract_harness.py b/tests/_api_contract_harness.py index 4679a98..c9f5c03 100644 --- a/tests/_api_contract_harness.py +++ b/tests/_api_contract_harness.py @@ -64,10 +64,6 @@ def assert_shape( assert actual_members == sorted(expected_members.keys()) return - if "kind" in shape and shape["kind"] == "validated_directive": - assert value == grammar.validate_directive(shape["text"]) - return - if "kind" in shape and shape["kind"] == "canonical_directive": assert value == grammar.decompose_directive(shape["text"]) return @@ -370,12 +366,6 @@ def _validate_shape_spec(shape: object, label: str) -> None: if kind == "engine_instance": _assert_closed_keys(shape, {"kind"}, label) return - if kind == "validated_directive": - _assert_closed_keys(shape, {"kind", "text", "directive_kind"}, label) - _require_fields(shape, {"kind", "text", "directive_kind"}, label) - _assert_type(shape["text"], str, f"{label}.text") - _assert_type(shape["directive_kind"], str, f"{label}.directive_kind") - return if kind == "canonical_directive": _assert_closed_keys(shape, {"kind", "text", "directive_kind", "operands"}, label) _require_fields(shape, {"kind", "text", "directive_kind", "operands"}, label) diff --git a/tests/fixtures/conformance/api/public-grammar-v1.json b/tests/fixtures/conformance/api/public-grammar-v1.json index fe8ecd9..865e562 100644 --- a/tests/fixtures/conformance/api/public-grammar-v1.json +++ b/tests/fixtures/conformance/api/public-grammar-v1.json @@ -6,12 +6,10 @@ "names": [ "CanonicalDirective", "DirectiveKind", - "ValidatedDirective", "contains_multiple_canonical_directives", "decompose_directive", "match_canonical_directive_start", - "render_directive", - "validate_directive" + "render_directive" ], "members": { "CanonicalDirective": { @@ -20,9 +18,6 @@ "DirectiveKind": { "kind": "class" }, - "ValidatedDirective": { - "kind": "class" - }, "contains_multiple_canonical_directives": { "kind": "callable", "signature": { @@ -155,38 +150,6 @@ } } ] - }, - "validate_directive": { - "kind": "callable", - "signature": { - "params": [ - { - "name": "text", - "kind": "POSITIONAL_OR_KEYWORD", - "has_default": false - } - ] - }, - "shape_probes": [ - { - "kwargs": { - "text": "use docker" - }, - "return_shape": { - "kind": "validated_directive", - "text": "use docker", - "directive_kind": "use_item" - } - }, - { - "kwargs": { - "text": "please use docker" - }, - "return_shape": { - "type": "null" - } - } - ] } } } diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_boundary_whitespace_trim_use.json b/tests/fixtures/conformance/grammar/grammar_decompose_boundary_whitespace_trim_use.json new file mode 100644 index 0000000..562a69e --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_boundary_whitespace_trim_use.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_boundary_whitespace_trim_use", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": " use docker " + }, + "expected": { + "directive": { + "text": " use docker ", + "kind": "use_item", + "operands": { + "item": "docker" + } + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise.json new file mode 100644 index 0000000..73470ff --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_change_premise", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to concise replies" + }, + "expected": { + "directive": { + "text": "change premise to concise replies", + "kind": "change_premise", + "operands": { + "value": "concise replies" + } + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_compound_rejected.json similarity index 51% rename from tests/fixtures/conformance/grammar/grammar_validate_compound_rejected.json rename to tests/fixtures/conformance/grammar/grammar_decompose_compound_rejected.json index 4bac6db..cc77118 100644 --- a/tests/fixtures/conformance/grammar/grammar_validate_compound_rejected.json +++ b/tests/fixtures/conformance/grammar/grammar_decompose_compound_rejected.json @@ -1,11 +1,11 @@ { - "id": "grammar_validate_compound_rejected", + "id": "grammar_decompose_compound_rejected", "kind": "grammar", "action": { - "fn": "validate_directive", + "fn": "decompose_directive", "text": "use docker and prohibit peanuts" }, "expected": { - "validated": null + "directive": null } } diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_compound_use_and_prohibit_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_compound_use_and_prohibit_rejected.json new file mode 100644 index 0000000..bd06aad --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_compound_use_and_prohibit_rejected.json @@ -0,0 +1,11 @@ +{ + "id": "grammar_decompose_invalid_compound_use_and_prohibit_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use docker and prohibit peanuts" + }, + "expected": { + "directive": null + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_missing_use_operand_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_missing_use_operand_rejected.json new file mode 100644 index 0000000..7e2390b --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_missing_use_operand_rejected.json @@ -0,0 +1,11 @@ +{ + "id": "grammar_decompose_invalid_missing_use_operand_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use" + }, + "expected": { + "directive": null + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_quoted_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_quoted_compound_rejected.json new file mode 100644 index 0000000..5c50618 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_quoted_compound_rejected.json @@ -0,0 +1,11 @@ +{ + "id": "grammar_decompose_invalid_quoted_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use \"docker and prohibit peanuts\"" + }, + "expected": { + "directive": null + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_replacement_missing_new_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_replacement_missing_new_rejected.json new file mode 100644 index 0000000..86f916b --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_replacement_missing_new_rejected.json @@ -0,0 +1,11 @@ +{ + "id": "grammar_decompose_invalid_replacement_missing_new_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use instead of docker" + }, + "expected": { + "directive": null + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_replacement_missing_old_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_replacement_missing_old_rejected.json new file mode 100644 index 0000000..6454cd8 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_replacement_missing_old_rejected.json @@ -0,0 +1,11 @@ +{ + "id": "grammar_decompose_invalid_replacement_missing_old_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use podman instead of" + }, + "expected": { + "directive": null + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_invalid_set_premise_to_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_set_premise_to_rejected.json new file mode 100644 index 0000000..6ada6f0 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_invalid_set_premise_to_rejected.json @@ -0,0 +1,11 @@ +{ + "id": "grammar_decompose_invalid_set_premise_to_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise to concise" + }, + "expected": { + "directive": null + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_keyword_case_normalization_use.json b/tests/fixtures/conformance/grammar/grammar_decompose_keyword_case_normalization_use.json new file mode 100644 index 0000000..2e6e19e --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_keyword_case_normalization_use.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_keyword_case_normalization_use", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "Use Docker" + }, + "expected": { + "directive": { + "text": "Use Docker", + "kind": "use_item", + "operands": { + "item": "Docker" + } + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise.json new file mode 100644 index 0000000..27f8c9f --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_set_premise", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise concise replies" + }, + "expected": { + "directive": { + "text": "set premise concise replies", + "kind": "set_premise", + "operands": { + "value": "concise replies" + } + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_tab_separator_use.json b/tests/fixtures/conformance/grammar/grammar_decompose_tab_separator_use.json new file mode 100644 index 0000000..2f1998b --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_tab_separator_use.json @@ -0,0 +1,17 @@ +{ + "id": "grammar_decompose_tab_separator_use", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "use\tdocker" + }, + "expected": { + "directive": { + "text": "use\tdocker", + "kind": "use_item", + "operands": { + "item": "docker" + } + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_render_replace_use.json b/tests/fixtures/conformance/grammar/grammar_render_replace_use.json index 43b908d..c0059ca 100644 --- a/tests/fixtures/conformance/grammar/grammar_render_replace_use.json +++ b/tests/fixtures/conformance/grammar/grammar_render_replace_use.json @@ -11,6 +11,6 @@ }, "expected": { "text": "use podman instead of docker", - "validated_kind": "replace_use" + "directive_kind": "replace_use" } } diff --git a/tests/fixtures/conformance/grammar/grammar_validate_boundary_whitespace_trim_use.json b/tests/fixtures/conformance/grammar/grammar_validate_boundary_whitespace_trim_use.json deleted file mode 100644 index eb821f7..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_boundary_whitespace_trim_use.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "grammar_validate_boundary_whitespace_trim_use", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": " use docker " - }, - "expected": { - "validated": { - "text": " use docker ", - "kind": "use_item" - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_change_premise.json b/tests/fixtures/conformance/grammar/grammar_validate_change_premise.json deleted file mode 100644 index e30d6e7..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_change_premise.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "grammar_validate_change_premise", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "change premise to concise replies" - }, - "expected": { - "validated": { - "text": "change premise to concise replies", - "kind": "change_premise" - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_clear_state.json b/tests/fixtures/conformance/grammar/grammar_validate_clear_state.json deleted file mode 100644 index 84a2508..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_clear_state.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "grammar_validate_clear_state", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "clear state" - }, - "expected": { - "validated": { - "text": "clear state", - "kind": "clear_state" - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_invalid_compound_use_and_prohibit_rejected.json b/tests/fixtures/conformance/grammar/grammar_validate_invalid_compound_use_and_prohibit_rejected.json deleted file mode 100644 index 80de798..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_invalid_compound_use_and_prohibit_rejected.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "grammar_validate_invalid_compound_use_and_prohibit_rejected", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "use docker and prohibit peanuts" - }, - "expected": { - "validated": null - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_invalid_missing_use_operand_rejected.json b/tests/fixtures/conformance/grammar/grammar_validate_invalid_missing_use_operand_rejected.json deleted file mode 100644 index 4c5a505..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_invalid_missing_use_operand_rejected.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "grammar_validate_invalid_missing_use_operand_rejected", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "use" - }, - "expected": { - "validated": null - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_invalid_quoted_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_validate_invalid_quoted_compound_rejected.json deleted file mode 100644 index 1f0f2a0..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_invalid_quoted_compound_rejected.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "grammar_validate_invalid_quoted_compound_rejected", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "use \"docker and prohibit peanuts\"" - }, - "expected": { - "validated": null - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_invalid_replacement_missing_new_rejected.json b/tests/fixtures/conformance/grammar/grammar_validate_invalid_replacement_missing_new_rejected.json deleted file mode 100644 index e3c3e10..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_invalid_replacement_missing_new_rejected.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "grammar_validate_invalid_replacement_missing_new_rejected", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "use instead of docker" - }, - "expected": { - "validated": null - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_invalid_replacement_missing_old_rejected.json b/tests/fixtures/conformance/grammar/grammar_validate_invalid_replacement_missing_old_rejected.json deleted file mode 100644 index dcff464..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_invalid_replacement_missing_old_rejected.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "grammar_validate_invalid_replacement_missing_old_rejected", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "use podman instead of" - }, - "expected": { - "validated": null - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_invalid_set_premise_to_rejected.json b/tests/fixtures/conformance/grammar/grammar_validate_invalid_set_premise_to_rejected.json deleted file mode 100644 index 09153fe..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_invalid_set_premise_to_rejected.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "grammar_validate_invalid_set_premise_to_rejected", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "set premise to concise" - }, - "expected": { - "validated": null - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_keyword_case_normalization_use.json b/tests/fixtures/conformance/grammar/grammar_validate_keyword_case_normalization_use.json deleted file mode 100644 index 1062373..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_keyword_case_normalization_use.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "grammar_validate_keyword_case_normalization_use", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "Use Docker" - }, - "expected": { - "validated": { - "text": "Use Docker", - "kind": "use_item" - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_set_premise.json b/tests/fixtures/conformance/grammar/grammar_validate_set_premise.json deleted file mode 100644 index e7573c8..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_set_premise.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "grammar_validate_set_premise", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "set premise concise replies" - }, - "expected": { - "validated": { - "text": "set premise concise replies", - "kind": "set_premise" - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_validate_tab_separator_use.json b/tests/fixtures/conformance/grammar/grammar_validate_tab_separator_use.json deleted file mode 100644 index 633fd7d..0000000 --- a/tests/fixtures/conformance/grammar/grammar_validate_tab_separator_use.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "grammar_validate_tab_separator_use", - "kind": "grammar", - "action": { - "fn": "validate_directive", - "text": "use\tdocker" - }, - "expected": { - "validated": { - "text": "use\tdocker", - "kind": "use_item" - } - } -} diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 7f57e6f..24f6f44 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -8,7 +8,6 @@ DirectiveKind, decompose_directive, render_directive, - validate_directive, ) _STEP_FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" / "conformance" / "step" @@ -128,21 +127,17 @@ def _validate_grammar_fixture(fixture: dict[str, object], fixture_id: object) -> expected = fixture["expected"] assert isinstance(expected, dict), fixture_id fn = action["fn"] - assert fn in {"decompose_directive", "validate_directive", "render_directive"}, fixture_id + assert fn in {"decompose_directive", "render_directive"}, fixture_id if fn == "decompose_directive": _assert_allowed_keys(action, {"fn", "text"}, fixture_id, "action") assert isinstance(action["text"], str), fixture_id _assert_allowed_keys(expected, {"directive"}, fixture_id, "expected") - elif fn == "validate_directive": - _assert_allowed_keys(action, {"fn", "text"}, fixture_id, "action") - assert isinstance(action["text"], str), fixture_id - _assert_allowed_keys(expected, {"validated"}, fixture_id, "expected") else: _assert_allowed_keys(action, {"fn", "kind", "operands"}, fixture_id, "action") assert isinstance(action["kind"], str), fixture_id assert isinstance(action["operands"], dict), fixture_id - _assert_allowed_keys(expected, {"text", "validated_kind"}, fixture_id, "expected") + _assert_allowed_keys(expected, {"text", "directive_kind"}, fixture_id, "expected") def _apply_prelude(engine: object, prelude: object) -> None: @@ -246,21 +241,12 @@ def test_grammar_fixtures() -> None: assert directive.text == expected_directive["text"], fixture_id assert directive.kind.value == expected_directive["kind"], fixture_id assert dict(directive.operands) == expected_directive["operands"], fixture_id - elif fn == "validate_directive": - validated = validate_directive(action["text"]) - expected_validated = expected["validated"] - if expected_validated is None: - assert validated is None, fixture_id - else: - assert validated is not None, fixture_id - assert validated.text == expected_validated["text"], fixture_id - assert validated.kind.value == expected_validated["kind"], fixture_id else: rendered = render_directive(DirectiveKind(action["kind"]), **action["operands"]) assert rendered == expected["text"], fixture_id - validated = validate_directive(rendered) - assert validated is not None, fixture_id - assert validated.kind.value == expected["validated_kind"], fixture_id + directive = decompose_directive(rendered) + assert directive is not None, fixture_id + assert directive.kind.value == expected["directive_kind"], fixture_id def test_mutation_isolation_fixtures() -> None: diff --git a/tests/test_grammar.py b/tests/test_grammar.py index fde4ee0..ca090a2 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -7,12 +7,10 @@ from context_compiler.grammar import ( CanonicalDirective, DirectiveKind, - ValidatedDirective, contains_multiple_canonical_directives, decompose_directive, match_canonical_directive_start, render_directive, - validate_directive, ) @@ -42,16 +40,6 @@ def test_directive_kind_members_and_values() -> None: assert DirectiveKind("set_premise") is DirectiveKind.SET_PREMISE -def test_validated_directive_is_frozen_and_slotted() -> None: - validated = ValidatedDirective( - text="set premise concise replies", - kind=DirectiveKind.SET_PREMISE, - ) - assert validated.__slots__ == ("text", "kind") - with pytest.raises(FrozenInstanceError): - validated.text = "change premise to concise replies" # type: ignore[misc] - - def test_canonical_directive_is_frozen_and_slotted() -> None: directive = CanonicalDirective( text="use docker", @@ -63,27 +51,6 @@ def test_canonical_directive_is_frozen_and_slotted() -> None: directive.kind = DirectiveKind.PROHIBIT_ITEM # type: ignore[misc] -@pytest.mark.parametrize( - ("text", "expected_kind"), - [ - ("set premise concise replies", DirectiveKind.SET_PREMISE), - ("change premise to formal tone", DirectiveKind.CHANGE_PREMISE), - ("use docker", DirectiveKind.USE_ITEM), - ("prohibit peanuts", DirectiveKind.PROHIBIT_ITEM), - ("remove policy docker", DirectiveKind.REMOVE_POLICY), - ("use podman instead of docker", DirectiveKind.REPLACE_USE), - ("clear premise", DirectiveKind.CLEAR_PREMISE), - ("reset policies", DirectiveKind.RESET_POLICIES), - ("clear state", DirectiveKind.CLEAR_STATE), - ], -) -def test_validate_directive_accepts_each_canonical_family( - text: str, expected_kind: DirectiveKind -) -> None: - validated = validate_directive(text) - assert validated == ValidatedDirective(text=text, kind=expected_kind) - - @pytest.mark.parametrize( ("text", "expected_kind", "expected_operands"), [ @@ -131,26 +98,10 @@ def test_decompose_directive_accepts_each_canonical_family( '"use docker and prohibit peanuts"', ], ) -def test_validate_directive_rejects_non_canonical_inputs(text: str) -> None: - assert validate_directive(text) is None +def test_decompose_directive_rejects_non_canonical_inputs(text: str) -> None: assert decompose_directive(text) is None -@pytest.mark.parametrize( - ("text", "expected_kind"), - [ - (" set premise concise", DirectiveKind.SET_PREMISE), - ("Use docker", DirectiveKind.USE_ITEM), - ("use\tdocker", DirectiveKind.USE_ITEM), - ], -) -def test_validate_directive_accepts_lexically_normalized_canonical_input( - text: str, expected_kind: DirectiveKind -) -> None: - validated = validate_directive(text) - assert validated == ValidatedDirective(text=text, kind=expected_kind) - - @pytest.mark.parametrize( ("text", "expected_operands"), [ @@ -196,9 +147,9 @@ def test_render_directive_outputs_exact_canonical_syntax( ) -> None: rendered = render_directive(kind, **operands) assert rendered == expected - validated = validate_directive(rendered) - assert validated is not None - assert validated.kind is kind + directive = decompose_directive(rendered) + assert directive is not None + assert directive.kind is kind @pytest.mark.parametrize( @@ -284,18 +235,16 @@ def test_public_grammar_all_includes_semantic_surface() -> None: assert grammar_module.__all__ == [ "CanonicalDirective", "DirectiveKind", - "ValidatedDirective", "contains_multiple_canonical_directives", "decompose_directive", "match_canonical_directive_start", "render_directive", - "validate_directive", ] -def test_validate_directive_rejects_near_miss_without_required_delimiter() -> None: - assert validate_directive("clear statex") is None - assert validate_directive("usex docker") is None +def test_decompose_directive_rejects_near_miss_without_required_delimiter() -> None: + assert decompose_directive("clear statex") is None + assert decompose_directive("usex docker") is None def test_render_directive_rejects_non_string_operands() -> None: @@ -342,12 +291,13 @@ def test_decompose_directive_returns_canonical_operands_for_use_item() -> None: assert parsed.operands == {"item": "docker"} -def test_validate_directive_is_projection_of_decomposition() -> None: +def test_decompose_directive_returns_text_kind_and_operands_without_projection_layer() -> None: decomposed = decompose_directive("use docker") - validated = validate_directive("use docker") assert decomposed is not None - assert validated == ValidatedDirective(text=decomposed.text, kind=decomposed.kind) + assert decomposed.text == "use docker" + assert decomposed.kind is DirectiveKind.USE_ITEM + assert decomposed.operands == {"item": "docker"} def test_internal_match_directive_token_rejects_truncated_and_non_whitespace_separator() -> None: @@ -423,14 +373,14 @@ def fullmatch(self, text: str) -> _FakeMatch | None: ("_REMOVE_POLICY_RE", "remove policy docker"), ], ) -def test_validate_directive_defensively_rejects_when_branch_regex_match_is_missing( +def test_decompose_directive_defensively_rejects_when_branch_regex_match_is_missing( monkeypatch: pytest.MonkeyPatch, pattern_name: str, text: str, ) -> None: monkeypatch.setattr(grammar_module, pattern_name, _FakePattern(None)) - assert validate_directive(text) is None + assert decompose_directive(text) is None @pytest.mark.parametrize( @@ -441,7 +391,7 @@ def test_validate_directive_defensively_rejects_when_branch_regex_match_is_missi ("_REMOVE_POLICY_RE", "remove policy docker", {"item": " \t "}), ], ) -def test_validate_directive_defensively_rejects_whitespace_only_operands_after_match( +def test_decompose_directive_defensively_rejects_whitespace_only_operands_after_match( monkeypatch: pytest.MonkeyPatch, pattern_name: str, text: str, @@ -449,4 +399,4 @@ def test_validate_directive_defensively_rejects_whitespace_only_operands_after_m ) -> None: monkeypatch.setattr(grammar_module, pattern_name, _FakePattern(_FakeMatch(groups))) - assert validate_directive(text) is None + assert decompose_directive(text) is None diff --git a/tests/test_properties.py b/tests/test_properties.py index b1fa933..e12adf1 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -9,9 +9,9 @@ from context_compiler import DECISION_ERROR, DECISION_NO_DIRECTIVE, DECISION_UPDATE, create_engine from context_compiler.grammar import ( DirectiveKind, + decompose_directive, match_canonical_directive_start, render_directive, - validate_directive, ) @@ -90,11 +90,11 @@ def _sanitize_premise_like_engine(value: str) -> str: ) VALID_USE_ITEM_TEXT = VALID_NONEMPTY_ITEM_TEXT.filter( - lambda value: validate_directive(f"use {value}") is not None + lambda value: decompose_directive(f"use {value}") is not None ) VALID_PROHIBIT_ITEM_TEXT = VALID_NONEMPTY_ITEM_TEXT.filter( - lambda value: validate_directive(f"prohibit {value}") is not None + lambda value: decompose_directive(f"prohibit {value}") is not None ) VALID_PREMISE_TEXT = NORMALIZATION_SENSITIVE_TEXT.filter( @@ -106,17 +106,17 @@ def _sanitize_premise_like_engine(value: str) -> str: ).filter( lambda value: ( value != "" - and validate_directive(f"set premise {value}") is not None - and validate_directive(f"change premise to {value}") is not None + and decompose_directive(f"set premise {value}") is not None + and decompose_directive(f"change premise to {value}") is not None ) ) CANONICAL_GRAMMAR_ITEM_TEXT = NORMALIZATION_SENSITIVE_TEXT.map(_normalize_item_like_engine).filter( lambda value: ( value != "" - and validate_directive(f"use {value}") is not None - and validate_directive(f"prohibit {value}") is not None - and validate_directive(f"remove policy {value}") is not None + and decompose_directive(f"use {value}") is not None + and decompose_directive(f"prohibit {value}") is not None + and decompose_directive(f"remove policy {value}") is not None ) ) @@ -166,7 +166,7 @@ def _build_deterministic_replacement_case( .filter( lambda args: ( _normalize_item_like_engine(args[2]) != _normalize_item_like_engine(args[3]) - and validate_directive(f"use {args[2]} instead of {args[3]}") is not None + and decompose_directive(f"use {args[2]} instead of {args[3]}") is not None and not any( key in {_normalize_item_like_engine(args[2]), _normalize_item_like_engine(args[3])} and value == "prohibit" @@ -234,7 +234,7 @@ def test_determinism_same_input_sequence_same_state(inputs: list[str]) -> None: @given(GRAMMAR_RENDER_CASES) -def test_grammar_helper_render_validate_round_trip_is_stable( +def test_grammar_helper_render_decompose_round_trip_is_stable( case: dict[str, DirectiveKind | dict[str, str]], ) -> None: kind = case["kind"] @@ -244,12 +244,13 @@ def test_grammar_helper_render_validate_round_trip_is_stable( assert isinstance(operands, dict) rendered = render_directive(kind, **operands) - validated = validate_directive(rendered) + directive = decompose_directive(rendered) - assert validated is not None - assert validated.kind is kind - assert validated.text == rendered - assert validate_directive(validated.text) == validated + assert directive is not None + assert directive.kind is kind + assert directive.text == rendered + assert dict(directive.operands) == operands + assert decompose_directive(directive.text) == directive assert render_directive(kind, **operands) == rendered @@ -260,7 +261,7 @@ def test_idempotent_use_item_is_update_and_stable_state(item: str) -> None: assume(not item.endswith(" instead of")) assume(_normalize_item_like_engine(item) != "") assume(not _contains_canonical_start_fragment(item)) - assume(validate_directive(f"use {item}") is not None) + assume(decompose_directive(f"use {item}") is not None) engine = create_engine() d1 = engine.step(f"use {item}") d2 = engine.step(f"use {item}") @@ -288,7 +289,7 @@ def test_use_item_with_whitespace_only_payload_remains_no_directive(item: str) - def test_idempotent_prohibit_item_is_update_and_stable_state(item: str) -> None: assume(_normalize_item_like_engine(item) != "") assume(not _contains_canonical_start_fragment(item)) - assume(validate_directive(f"prohibit {item}") is not None) + assume(decompose_directive(f"prohibit {item}") is not None) engine = create_engine() d1 = engine.step(f"prohibit {item}") d2 = engine.step(f"prohibit {item}") @@ -338,8 +339,8 @@ def test_no_directive_sequence_preserves_state_and_decision_kind(inputs: list[st @given(st.text(min_size=1, max_size=30)) def test_contradiction_use_after_prohibit_always_clarifies(item: str) -> None: assume(not _contains_canonical_start_fragment(item)) - assume(validate_directive(f"prohibit {item}") is not None) - assume(validate_directive(f"use {item}") is not None) + assume(decompose_directive(f"prohibit {item}") is not None) + assume(decompose_directive(f"use {item}") is not None) engine = create_engine() engine.step(f"prohibit {item}") before = _observations(engine) @@ -355,8 +356,8 @@ def test_contradiction_prohibit_after_use_always_clarifies(item: str) -> None: assume(not item.startswith("instead of ")) assume(not item.endswith(" instead of")) assume(not _contains_canonical_start_fragment(item)) - assume(validate_directive(f"use {item}") is not None) - assume(validate_directive(f"prohibit {item}") is not None) + assume(decompose_directive(f"use {item}") is not None) + assume(decompose_directive(f"prohibit {item}") is not None) engine = create_engine() engine.step(f"use {item}") before = _observations(engine) diff --git a/tests/test_public_grammar_root_exports.py b/tests/test_public_grammar_root_exports.py index 6b3076f..24690f7 100644 --- a/tests/test_public_grammar_root_exports.py +++ b/tests/test_public_grammar_root_exports.py @@ -5,7 +5,6 @@ def test_root_does_not_export_public_grammar_surface() -> None: for name in ( "DirectiveKind", - "validate_directive", "render_directive", ): assert name not in context_compiler.__all__ @@ -16,7 +15,6 @@ def test_grammar_submodule_preserves_public_grammar_surface() -> None: assert grammar_module.CanonicalDirective is not None assert grammar_module.DirectiveKind is not None assert grammar_module.decompose_directive is not None - assert grammar_module.validate_directive is not None assert grammar_module.render_directive is not None diff --git a/uv.lock b/uv.lock index 3538eae..ba79235 100644 --- a/uv.lock +++ b/uv.lock @@ -296,7 +296,7 @@ wheels = [ [[package]] name = "context-compiler" -version = "0.9.0.dev6" +version = "0.9.0.dev7" source = { editable = "." } [package.optional-dependencies]