From abf55d3b336996976b38730f69f405b8ba0ff7b5 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:25:58 -0400 Subject: [PATCH 1/6] fix: reject premise directive compounds --- src/context_compiler/grammar.py | 29 ++----------------- ...pose_change_premise_compound_rejected.json | 16 ++++++++++ ..._change_premise_use_compound_rejected.json | 16 ++++++++++ ...ose_opaque_change_premise_conjunction.json | 17 ----------- ..._decompose_opaque_premise_conjunction.json | 17 ----------- ...mar_decompose_opaque_premise_prohibit.json | 17 ----------- .../grammar_decompose_opaque_premise_use.json | 17 ----------- ...emise_clear_premise_compound_rejected.json | 16 ++++++++++ ...premise_clear_state_compound_rejected.json | 16 ++++++++++ ...compose_set_premise_compound_rejected.json | 16 ++++++++++ ...et_premise_prohibit_compound_rejected.json | 16 ++++++++++ ...emise_remove_policy_compound_rejected.json | 16 ++++++++++ ...mise_reset_policies_compound_rejected.json | 16 ++++++++++ ...ose_set_premise_use_compound_rejected.json | 16 ++++++++++ ..._set_premise_and_use_invalid_boundary.json | 5 ++-- tests/test_grammar.py | 4 --- 16 files changed, 148 insertions(+), 102 deletions(-) create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json delete mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json diff --git a/src/context_compiler/grammar.py b/src/context_compiler/grammar.py index 73c993c6..fd49a943 100644 --- a/src/context_compiler/grammar.py +++ b/src/context_compiler/grammar.py @@ -353,18 +353,6 @@ def _contains_multiple_canonical_directives(text: str) -> bool: return False -def _contains_multiple_premise_directives(text: str) -> bool: - """Report premise compounds without inspecting opaque premise payload text.""" - first_start = _match_canonical_directive_start(text, 0) - if first_start is None: - return False - - for index, character in enumerate(text[first_start:], start=first_start): - if character == "\n" and _match_canonical_directive_start(text, index + 1) is not None: - return True - return False - - def _starts_with_directive_family(text: str) -> bool: for token, require_space_or_end in _DIRECTIVE_FAMILY_STARTS: if ( @@ -473,12 +461,7 @@ def _validate_operand_constraints(kind: DirectiveKind, operands: Mapping[str, st def _validate_rendered_canonical_shape(kind: DirectiveKind, operands: Mapping[str, str]) -> None: rendered = _serialize_canonical_directive(kind, MappingProxyType(dict(operands))) - contains_compound = ( - _contains_multiple_premise_directives(rendered) - if kind in {DirectiveKind.SET_PREMISE, DirectiveKind.CHANGE_PREMISE} - else _contains_multiple_canonical_directives(rendered) - ) - if contains_compound: + if _contains_multiple_canonical_directives(rendered): raise ValueError(f"Operands do not produce a canonical {kind.value} directive.") @@ -497,15 +480,7 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta return None if not _starts_with_directive_family(trimmed_text): return None - premise_directive = _match_directive_token( - trimmed_text, 0, _SET_PREMISE_START, require_space_or_end=True - ) or _match_directive_token(trimmed_text, 0, _CHANGE_PREMISE_START, require_space_or_end=True) - contains_compound = ( - _contains_multiple_premise_directives(trimmed_text) - if premise_directive is not None - else _contains_multiple_canonical_directives(trimmed_text) - ) - if contains_compound: + if _contains_multiple_canonical_directives(trimmed_text): return _invalid_directive_syntax(DirectiveSyntaxFailure.COMPOUND_DIRECTIVE) normalized = _normalized_for_matching(trimmed_text) diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json new file mode 100644 index 00000000..093baee2 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to vegetarian and use docker" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json new file mode 100644 index 00000000..1d07bbc7 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_use_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_use_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to deployment target is staging, then use cautious rollout" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json deleted file mode 100644 index f8d9459c..00000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_change_premise_conjunction.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_change_premise_conjunction", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "change premise to vegetarian and use docker" - }, - "expected": { - "directive": { - "text": "change premise to vegetarian and use docker", - "kind": "change_premise", - "operands": { - "value": "vegetarian and use docker" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json deleted file mode 100644 index c6da6f13..00000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_conjunction.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_premise_conjunction", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "set premise vegetarian and use docker" - }, - "expected": { - "directive": { - "text": "set premise vegetarian and use docker", - "kind": "set_premise", - "operands": { - "value": "vegetarian and use docker" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json deleted file mode 100644 index 4a9337a7..00000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_prohibit.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_premise_prohibit", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "set premise users may prohibit unsafe operations" - }, - "expected": { - "directive": { - "text": "set premise users may prohibit unsafe operations", - "kind": "set_premise", - "operands": { - "value": "users may prohibit unsafe operations" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json b/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json deleted file mode 100644 index 27748c3a..00000000 --- a/tests/fixtures/conformance/grammar/grammar_decompose_opaque_premise_use.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "grammar_decompose_opaque_premise_use", - "kind": "grammar", - "action": { - "fn": "decompose_directive", - "text": "set premise we must use gaap" - }, - "expected": { - "directive": { - "text": "set premise we must use gaap", - "kind": "set_premise", - "operands": { - "value": "we must use gaap" - } - } - } -} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json new file mode 100644 index 00000000..938af49d --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_premise_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_clear_premise_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise clear premise before release" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json new file mode 100644 index 00000000..ca8eeae4 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_clear_state_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_clear_state_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise clear state before starting" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json new file mode 100644 index 00000000..4d8f291d --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise concise replies and change premise to formal tone" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json new file mode 100644 index 00000000..70afeb73 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_prohibit_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_prohibit_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise users may prohibit unsafe operations" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json new file mode 100644 index 00000000..89667511 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_remove_policy_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_remove_policy_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise cleanup requires remove policy docker" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json new file mode 100644 index 00000000..557d2334 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_reset_policies_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_reset_policies_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise reset policies after migration" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json new file mode 100644 index 00000000..7a7b4d61 --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_use_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_use_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise we must use gaap" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json b/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json index 73e61ae3..89acb740 100644 --- a/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json +++ b/tests/fixtures/conformance/step/step_compound_set_premise_and_use_invalid_boundary.json @@ -9,11 +9,10 @@ "input": "set premise vegetarian and use docker", "expected": { "decision": { - "kind": "update", - "changed": true + "kind": "no_directive" }, "state": { - "premise": "vegetarian and use docker", + "premise": null, "policies": {}, "version": 2 } diff --git a/tests/test_grammar.py b/tests/test_grammar.py index c4cace7d..18566dd6 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -469,10 +469,6 @@ def test_internal_contains_multiple_canonical_directives_reports_compound_detect assert grammar_module._contains_multiple_canonical_directives(text) is expected -def test_internal_contains_multiple_premise_directives_ignores_non_directive_text() -> None: - assert grammar_module._contains_multiple_premise_directives("hello there") is False - - def test_multiple_premise_directives_are_rejected() -> None: assert decompose_directive("set premise first\nset premise second") == InvalidDirectiveSyntax( failure=DirectiveSyntaxFailure.COMPOUND_DIRECTIVE, From a69371df654fa83c1970b3ce9baa0778b8366319 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:27:52 -0400 Subject: [PATCH 2/6] test: cover premise compound boundaries --- tests/test_compound_directive_properties.py | 17 +++++++++++++++++ tests/test_grammar.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/tests/test_compound_directive_properties.py b/tests/test_compound_directive_properties.py index a20f575c..898ea3d2 100644 --- a/tests/test_compound_directive_properties.py +++ b/tests/test_compound_directive_properties.py @@ -33,6 +33,8 @@ "clear state", ] +PREMISE_STARTS = ["set premise", "change premise to"] + SEPARATOR_CHARS = " \t\n\r,.;:!?-/()[]" LETTER_CHARS = string.ascii_lowercase @@ -62,6 +64,21 @@ def test_compound_separator_robustness(separator: str, second: str) -> None: _assert_compound_no_directive(user_input) +@settings(max_examples=50) +@given( + premise_start=st.sampled_from(PREMISE_STARTS), + separator=st.sampled_from([" ", " and ", ", then ", "\n"]), + second=st.sampled_from(CANONICAL_SECOND_DIRECTIVES), +) +def test_premise_payload_rejects_reserved_directive_starters( + premise_start: str, separator: str, second: str +) -> None: + user_input = f"{premise_start} deployment target is staging{separator}{second}" + + assert isinstance(decompose_directive(user_input), InvalidDirectiveSyntax) + _assert_compound_no_directive(user_input) + + @settings(max_examples=50) @given( chunks=st.lists( diff --git a/tests/test_grammar.py b/tests/test_grammar.py index 18566dd6..395953d9 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -475,6 +475,24 @@ def test_multiple_premise_directives_are_rejected() -> None: ) +@pytest.mark.parametrize( + "text", + [ + "set premise concise replies and change premise to formal tone", + "change premise to deployment target is staging, then use cautious rollout", + "set premise users may prohibit unsafe operations", + "set premise cleanup requires remove policy docker", + "set premise clear premise before release", + "set premise reset policies after migration", + "set premise clear state before starting", + ], +) +def test_premise_directive_starters_are_rejected_as_compound(text: str) -> None: + assert decompose_directive(text) == InvalidDirectiveSyntax( + failure=DirectiveSyntaxFailure.COMPOUND_DIRECTIVE, + ) + + def test_parse_replace_use_rejects_blank_new_item() -> None: assert grammar_module._parse_replace_use("use \t instead of docker") is None From 7bfaf88a2776d91c8e971d54b9b47949e16ef219 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:28:15 -0400 Subject: [PATCH 3/6] docs: clarify premise compound grammar --- docs/DirectiveGrammarSpec.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/DirectiveGrammarSpec.md b/docs/DirectiveGrammarSpec.md index 73969dcc..d433db72 100644 --- a/docs/DirectiveGrammarSpec.md +++ b/docs/DirectiveGrammarSpec.md @@ -333,11 +333,13 @@ Rules: - must contain at least one non-whitespace character; - may contain spaces and punctuation; -- is opaque payload: directive keywords, conjunctions, and multiple sentences - inside `VALUE` are not inspected as embedded directives; +- is opaque for semantic interpretation: the grammar does not infer meaning + from ordinary words or sentence content inside `VALUE`; - has no quote-aware or escape-aware subgrammar; -- a separate canonical directive beginning on a new line is still rejected as - a compound attempt under Section 7.5. +- a canonical directive starter in compound position is not part of `VALUE` and + causes compound-attempt rejection under Section 7.5; +- a separate canonical directive beginning on a new line is also rejected as a + compound attempt. Canonical meaning: @@ -455,23 +457,22 @@ contains more than one attempted directive clause. This includes inputs such as: - `use docker and prohibit peanuts` -- `set premise project deadline is Friday and use docker` is one premise directive; `VALUE` is - opaque and may contain policy words or conjunctions. +- `set premise project deadline is Friday and use docker` +- `set premise deployment target is staging, then use cautious rollout` - `clear state then set premise new project` This rule is lexical and grammar-level. It is not a semantic conflict rule. -Premise `VALUE` is payload rather than a policy identity. The grammar does not -inspect embedded directive words, conjunctions, or sentence boundaries inside -it. A separate canonical directive beginning on a new line is still treated as -a compound attempt. `ITEM` operands retain the compound-detection behavior -described above. +Premise `VALUE` is payload rather than a policy identity, but directive +starters remain reserved in compound position. This is lexical and grammar- +level behavior, not natural-language interpretation. `ITEM` operands retain +the compound-detection behavior described above. Examples: - no_directive: `"use docker and prohibit peanuts"` - directive-shaped invalid: `use "docker and prohibit peanuts"` -- canonical directive: `set premise "use docker and prohibit peanuts"` +- directive-shaped invalid: `set premise "use docker and prohibit peanuts"` ## 8. Parsed Meaning and Semantic Boundary @@ -773,7 +774,7 @@ source material for later conformance fixtures. | `use docker and prohibit peanuts` | directive-shaped invalid input | none | compound attempt | | `clear state then set premise project` | directive-shaped invalid input | none | compound attempt | | `use "docker and prohibit peanuts"` | directive-shaped invalid input | none | quotes do not protect embedded directive text | -| `set premise "use docker and prohibit peanuts"` | canonical directive | set premise | premise `VALUE` is opaque payload, including quote characters | +| `set premise "use docker and prohibit peanuts"` | directive-shaped invalid | none | quotes do not protect embedded directive text | ## 12. Invariants From efa81920b4836a1050a07e3acccf58f65c029bc8 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:35:10 -0400 Subject: [PATCH 4/6] test: cover premise starter compounds --- ...assword_authentication_compound_rejected.json | 16 ++++++++++++++++ ..._premise_users_use_sso_compound_rejected.json | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json create mode 100644 tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json new file mode 100644 index 00000000..d20f2d7a --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_change_premise_users_prohibit_password_authentication_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "change premise to users prohibit password authentication" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} diff --git a/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json new file mode 100644 index 00000000..a4ad59ad --- /dev/null +++ b/tests/fixtures/conformance/grammar/grammar_decompose_set_premise_users_use_sso_compound_rejected.json @@ -0,0 +1,16 @@ +{ + "id": "grammar_decompose_set_premise_users_use_sso_compound_rejected", + "kind": "grammar", + "action": { + "fn": "decompose_directive", + "text": "set premise users use SSO" + }, + "expected": { + "directive": { + "kind": "invalid_directive_syntax", + "failure": "compound_directive", + "directive_kind": null, + "missing_operand": null + } + } +} From 4b5a282ac1c0e5d6c5930abd8bd02ebde79bd8da Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:38:06 -0400 Subject: [PATCH 5/6] docs: consolidate compound directive rules --- docs/DirectiveGrammarSpec.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/DirectiveGrammarSpec.md b/docs/DirectiveGrammarSpec.md index d433db72..89154b3c 100644 --- a/docs/DirectiveGrammarSpec.md +++ b/docs/DirectiveGrammarSpec.md @@ -336,10 +336,7 @@ Rules: - is opaque for semantic interpretation: the grammar does not infer meaning from ordinary words or sentence content inside `VALUE`; - has no quote-aware or escape-aware subgrammar; -- a canonical directive starter in compound position is not part of `VALUE` and - causes compound-attempt rejection under Section 7.5; -- a separate canonical directive beginning on a new line is also rejected as a - compound attempt. +- is subject to the compound-attempt rule in Section 7.5. Canonical meaning: @@ -462,15 +459,17 @@ as: - `clear state then set premise new project` This rule is lexical and grammar-level. It is not a semantic conflict rule. - -Premise `VALUE` is payload rather than a policy identity, but directive -starters remain reserved in compound position. This is lexical and grammar- -level behavior, not natural-language interpretation. `ITEM` operands retain -the compound-detection behavior described above. +For any operand-bearing directive, a recognized directive starter at a token +boundary within the operand makes the full input a compound attempt, even when +ordinary whitespace is the only separator. This applies to premise `VALUE` and +policy `ITEM` operands; no `and`, `then`, punctuation, or newline is required. +It does not interpret natural-language meaning. Examples: - no_directive: `"use docker and prohibit peanuts"` +- directive-shaped invalid: `set premise users use SSO` +- directive-shaped invalid: `change premise to users prohibit password authentication` - directive-shaped invalid: `use "docker and prohibit peanuts"` - directive-shaped invalid: `set premise "use docker and prohibit peanuts"` From 07e961d41059fc8f02f6484cfb9021410bf7a576 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 7 Sep 2026 22:49:52 -0400 Subject: [PATCH 6/6] test: assert premise compound failure --- tests/test_compound_directive_properties.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_compound_directive_properties.py b/tests/test_compound_directive_properties.py index 898ea3d2..669e5d5e 100644 --- a/tests/test_compound_directive_properties.py +++ b/tests/test_compound_directive_properties.py @@ -8,7 +8,11 @@ DECISION_NO_DIRECTIVE, Engine, ) -from context_compiler.grammar import InvalidDirectiveSyntax, decompose_directive +from context_compiler.grammar import ( + DirectiveSyntaxFailure, + InvalidDirectiveSyntax, + decompose_directive, +) CANONICAL_SECOND_DIRECTIVES = [ "set premise concise", @@ -75,7 +79,9 @@ def test_premise_payload_rejects_reserved_directive_starters( ) -> None: user_input = f"{premise_start} deployment target is staging{separator}{second}" - assert isinstance(decompose_directive(user_input), InvalidDirectiveSyntax) + assert decompose_directive(user_input) == InvalidDirectiveSyntax( + failure=DirectiveSyntaxFailure.COMPOUND_DIRECTIVE, + ) _assert_compound_no_directive(user_input)