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
7 changes: 7 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ submodule.

Public grammar surface:

- `DirectiveKind`
- `DirectiveSyntaxFailure`
- `CanonicalDirective`
- `InvalidDirectiveSyntax`
- `decompose_directive(text)`
Expand All @@ -89,6 +91,11 @@ Boundary notes:
- `decompose_directive(...)` returns `InvalidDirectiveSyntax` for
directive-shaped input that is not valid canonical syntax
- `decompose_directive(...)` returns `None` when no directive is present
- `CanonicalDirective.kind` uses `DirectiveKind`
- `InvalidDirectiveSyntax.failure` uses `DirectiveSyntaxFailure`
- `InvalidDirectiveSyntax.directive_kind`, when present, uses `DirectiveKind`
- `InvalidDirectiveSyntax.missing_operand`, when present, names the missing
grammar operand without introducing user-facing message 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
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.0dev8"
version = "0.9.0dev9"
description = "Deterministic conversational state engine for LLM applications."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
63 changes: 14 additions & 49 deletions src/context_compiler/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
STATE_PREMISE,
STATE_VERSION,
)
from .grammar import CanonicalDirective, _DirectiveKind, decompose_directive
from .grammar import CanonicalDirective, DirectiveKind, decompose_directive

PolicyValue = Literal["use", "prohibit"]

Expand Down Expand Up @@ -129,69 +129,34 @@ def _pre_mutation_error(
) -> Decision | None:
candidate_state = self._state if state is None else state
# Single error path: all error outcomes are detected before any mutation.
if directive.kind in {_DirectiveKind.SET_PREMISE, _DirectiveKind.CHANGE_PREMISE}:
value = directive.operands["value"]
if _sanitize_premise_value(value) == "":
if directive.kind is _DirectiveKind.SET_PREMISE:
return _error(
"Premise value cannot be empty.\n"
"Use 'set premise <value>' with a non-empty value."
)
return _error(
"Premise value cannot be empty.\n"
"Use 'change premise to <value>' with a non-empty value."
)

if (
directive.kind is _DirectiveKind.REMOVE_POLICY
and _normalize_item(directive.operands["item"]) == ""
):
return _error(
"Policy item cannot be empty.\nUse 'remove policy <item>' with a non-empty value."
)

if (
directive.kind is _DirectiveKind.USE_ITEM
and _normalize_item(directive.operands["item"]) == ""
):
return _error("Policy item cannot be empty.\nUse 'use <item>' with a non-empty value.")

if (
directive.kind is _DirectiveKind.PROHIBIT_ITEM
and _normalize_item(directive.operands["item"]) == ""
):
return _error(
"Policy item cannot be empty.\nUse 'prohibit <item>' with a non-empty value."
)

if (
directive.kind is _DirectiveKind.SET_PREMISE
directive.kind is DirectiveKind.SET_PREMISE
and candidate_state[STATE_PREMISE] is not None
):
return _error("Premise already set.\nUse 'change premise to <value>' to modify it.")

if (
directive.kind is _DirectiveKind.CHANGE_PREMISE
directive.kind is DirectiveKind.CHANGE_PREMISE
and candidate_state[STATE_PREMISE] is None
):
return _error("No premise is set.\nUse 'set premise <value>' to define one.")

if directive.kind is _DirectiveKind.USE_ITEM:
if directive.kind is DirectiveKind.USE_ITEM:
item_key = _normalize_item(directive.operands["item"])
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_PROHIBIT:
return _error(
f'"{item_key}" is currently prohibited.\nRemove or replace it before using it.'
)

if directive.kind is _DirectiveKind.PROHIBIT_ITEM:
if directive.kind is DirectiveKind.PROHIBIT_ITEM:
item_key = _normalize_item(directive.operands["item"])
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_USE:
return _error(
f'"{item_key}" is currently in use.\n'
"Remove or replace it before prohibiting it."
)

if directive.kind is _DirectiveKind.REPLACE_USE:
if directive.kind is DirectiveKind.REPLACE_USE:
new_item = directive.operands["new_item"]
old_item = directive.operands["old_item"]
new_key = _normalize_item(new_item)
Expand Down Expand Up @@ -222,44 +187,44 @@ def _pre_mutation_error(
def _apply_directive(self, directive: CanonicalDirective, *, state: _State) -> _State:
next_state = deepcopy(state)

if directive.kind is _DirectiveKind.SET_PREMISE:
if directive.kind is DirectiveKind.SET_PREMISE:
next_state[STATE_PREMISE] = _sanitize_premise_value(directive.operands["value"])
return next_state

if directive.kind is _DirectiveKind.CHANGE_PREMISE:
if directive.kind is DirectiveKind.CHANGE_PREMISE:
next_state[STATE_PREMISE] = _sanitize_premise_value(directive.operands["value"])
return next_state

if directive.kind is _DirectiveKind.USE_ITEM:
if directive.kind is DirectiveKind.USE_ITEM:
item_key = _normalize_item(directive.operands["item"])
# Idempotent directives are updates even if state does not change.
next_state[STATE_POLICIES][item_key] = POLICY_USE
return next_state

if directive.kind is _DirectiveKind.PROHIBIT_ITEM:
if directive.kind is DirectiveKind.PROHIBIT_ITEM:
item_key = _normalize_item(directive.operands["item"])
# Idempotent directives are updates even if state does not change.
next_state[STATE_POLICIES][item_key] = POLICY_PROHIBIT
return next_state

if directive.kind is _DirectiveKind.REPLACE_USE:
if directive.kind is DirectiveKind.REPLACE_USE:
self._apply_replacement_explicit(
next_state,
directive.operands["new_item"],
directive.operands["old_item"],
)
return next_state

if directive.kind is _DirectiveKind.REMOVE_POLICY:
if directive.kind is DirectiveKind.REMOVE_POLICY:
item_key = _normalize_item(directive.operands["item"])
next_state[STATE_POLICIES].pop(item_key, None)
return next_state

if directive.kind is _DirectiveKind.CLEAR_PREMISE:
if directive.kind is DirectiveKind.CLEAR_PREMISE:
next_state[STATE_PREMISE] = None
return next_state

if directive.kind is _DirectiveKind.RESET_POLICIES:
if directive.kind is DirectiveKind.RESET_POLICIES:
next_state[STATE_POLICIES] = {}
return next_state

Expand Down
Loading