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
21 changes: 20 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,36 @@ Public grammar surface:
- `CanonicalDirective`
- `DirectiveKind`
- `ValidatedDirective`
- `match_canonical_directive_start(text, start)`
- `contains_multiple_canonical_directives(text)`
- `decompose_directive(text)`
- `validate_directive(text)`
- `is_canonical_directive(text)`
- `render_directive(kind, /, **operands)`

Use this surface for exact canonical validation, canonical directive syntax
decomposition, 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
- `contains_multiple_canonical_directives(...)` detects compound
directive-shaped structure only; it is not full validation
- decomposition exposes canonical syntax only
- `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
- 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
- rendering is syntax-only and performs no state interpretation
- `engine.step(...)` remains the authority for error, state
transitions, and mutation behavior
Expand All @@ -99,6 +115,9 @@ Boundary notes:
`CanonicalDirective.operands` preserves the grammar-recognized operand text.
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`.

### `engine.premise`

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "context-compiler"
version = "0.9.0dev5"
version = "0.9.0dev6"
description = "Deterministic conversational state engine for LLM applications."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
60 changes: 33 additions & 27 deletions src/context_compiler/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,26 @@ class DirectiveKind(StrEnum):

@dataclass(frozen=True, slots=True)
class ValidatedDirective:
"""Classify text as one canonical directive kind without exposing operands."""
"""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."""
"""Represent one parsed canonical directive and its named operands.

``text`` preserves the original accepted input text. It may retain caller
formatting or casing and is not canonical serialized directive text; use
:func:`render_directive` to produce canonical directive text from semantic
kind and operands.
"""

text: str
kind: DirectiveKind
Expand Down Expand Up @@ -182,10 +193,11 @@ def _operand_starts_with_token(value: str, token: str) -> bool:
def match_canonical_directive_start(text: str, start: int) -> int | None:
"""Locate a canonical directive prefix at a given character position.

This classifies whether canonical directive syntax begins at ``start`` and,
when it does, returns the index immediately after the directive keyword
prefix. It does not parse operands, validate the full directive payload, or
evaluate multi-directive state semantics.
This is a shallow syntax-start matcher: it classifies whether canonical
directive syntax begins at ``start`` and, when it does, returns the index
immediately after the directive keyword prefix. It does not parse operands,
validate the full directive payload, or evaluate multi-directive state
semantics.
"""
if start < 0 or start >= len(text):
return None
Expand Down Expand Up @@ -246,10 +258,11 @@ def _match_directive_token(
def contains_multiple_canonical_directives(text: str) -> bool:
"""Report whether text contains more than one canonical directive start.

This detects compound directive text by looking for multiple canonical
directive prefixes in the same input. It does not parse directive operands,
repair malformed text, or determine whether a whole string should be
accepted as a single directive.
This detects compound directive structure by looking for multiple canonical
directive prefixes in the same input. It is not a replacement for full
directive validation, and it does not parse directive operands, repair
malformed text, or determine whether a whole string should be accepted as a
single directive.
"""
first_start = match_canonical_directive_start(text, 0)
if first_start is None:
Expand Down Expand Up @@ -290,8 +303,10 @@ def decompose_directive(text: str) -> CanonicalDirective | None:

This determines whether ``text`` is a single canonical directive and, when
it is, returns the directive kind plus canonical operand names with the
original operand text preserved. It does not repair input, infer intent, or
evaluate directive effects against compiler state.
original operand text preserved. Callers can determine whether ``text`` is
a complete canonical directive by checking whether this returns a non-`None`
result. It does not repair input, infer intent, or evaluate directive
effects against compiler state.
"""
trimmed_text = _trim_ascii_whitespace(text)
if trimmed_text == "":
Expand Down Expand Up @@ -398,28 +413,20 @@ 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. It
does not expose operands, render directives, repair malformed text, or
evaluate any state transition.
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 is_canonical_directive(text: str) -> bool:
"""Return whether text is exactly one canonical directive.

This provides boolean directive classification for callers that only need a
yes-or-no answer. It does not parse operands, explain rejection reasons, or
perform validation beyond canonical grammar recognition.
"""
return validate_directive(text) is not None


def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
"""Render canonical directive text from a semantic kind and operands.
"""Produce canonical directive text from a semantic kind and operands.

This determines the exact canonical spelling for an existing grammar
capability and rejects operand combinations that would not round-trip as the
Expand Down Expand Up @@ -466,7 +473,6 @@ def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
"ValidatedDirective",
"contains_multiple_canonical_directives",
"decompose_directive",
"is_canonical_directive",
"match_canonical_directive_start",
"render_directive",
"validate_directive",
Expand Down
24 changes: 0 additions & 24 deletions tests/fixtures/conformance/api/public-grammar-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"ValidatedDirective",
"contains_multiple_canonical_directives",
"decompose_directive",
"is_canonical_directive",
"match_canonical_directive_start",
"render_directive",
"validate_directive"
Expand Down Expand Up @@ -91,29 +90,6 @@
}
]
},
"is_canonical_directive": {
"kind": "callable",
"signature": {
"params": [
{
"name": "text",
"kind": "POSITIONAL_OR_KEYWORD",
"has_default": false
}
]
},
"shape_probes": [
{
"kwargs": {
"text": "clear state"
},
"return_shape": {
"type": "boolean",
"const": true
}
}
]
},
"match_canonical_directive_start": {
"kind": "callable",
"signature": {
Expand Down
4 changes: 0 additions & 4 deletions tests/test_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
ValidatedDirective,
contains_multiple_canonical_directives,
decompose_directive,
is_canonical_directive,
match_canonical_directive_start,
render_directive,
validate_directive,
Expand Down Expand Up @@ -83,7 +82,6 @@ def test_validate_directive_accepts_each_canonical_family(
) -> None:
validated = validate_directive(text)
assert validated == ValidatedDirective(text=text, kind=expected_kind)
assert is_canonical_directive(text) is True


@pytest.mark.parametrize(
Expand Down Expand Up @@ -136,7 +134,6 @@ def test_decompose_directive_accepts_each_canonical_family(
def test_validate_directive_rejects_non_canonical_inputs(text: str) -> None:
assert validate_directive(text) is None
assert decompose_directive(text) is None
assert is_canonical_directive(text) is False


@pytest.mark.parametrize(
Expand Down Expand Up @@ -290,7 +287,6 @@ def test_public_grammar_all_includes_semantic_surface() -> None:
"ValidatedDirective",
"contains_multiple_canonical_directives",
"decompose_directive",
"is_canonical_directive",
"match_canonical_directive_start",
"render_directive",
"validate_directive",
Expand Down
2 changes: 0 additions & 2 deletions tests/test_public_grammar_root_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ def test_root_does_not_export_public_grammar_surface() -> None:
"DirectiveKind",
"validate_directive",
"render_directive",
"is_canonical_directive",
):
assert name not in context_compiler.__all__
assert not hasattr(context_compiler, name)
Expand All @@ -19,7 +18,6 @@ def test_grammar_submodule_preserves_public_grammar_surface() -> 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
assert grammar_module.is_canonical_directive is not None


def test_root_does_not_export_private_grammar_implementation() -> None:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading