context-compiler 0.9.0.dev2__py3-none-any.whl → 0.9.0.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- context_compiler/__init__.py +16 -12
- context_compiler/const.py +2 -2
- context_compiler/controller.py +39 -11
- context_compiler/decision_helpers.py +31 -9
- context_compiler/engine.py +117 -77
- context_compiler/grammar.py +69 -30
- context_compiler/repl.py +24 -16
- {context_compiler-0.9.0.dev2.dist-info → context_compiler-0.9.0.dev4.dist-info}/METADATA +37 -30
- context_compiler-0.9.0.dev4.dist-info/RECORD +14 -0
- context_compiler-0.9.0.dev2.dist-info/RECORD +0 -14
- {context_compiler-0.9.0.dev2.dist-info → context_compiler-0.9.0.dev4.dist-info}/WHEEL +0 -0
- {context_compiler-0.9.0.dev2.dist-info → context_compiler-0.9.0.dev4.dist-info}/entry_points.txt +0 -0
- {context_compiler-0.9.0.dev2.dist-info → context_compiler-0.9.0.dev4.dist-info}/licenses/LICENSE +0 -0
context_compiler/__init__.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from importlib.metadata import version
|
|
2
2
|
|
|
3
3
|
from .const import (
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
DECISION_ERROR,
|
|
5
|
+
DECISION_NO_DIRECTIVE,
|
|
6
6
|
DECISION_UPDATE,
|
|
7
7
|
POLICY_PROHIBIT,
|
|
8
8
|
POLICY_USE,
|
|
@@ -14,18 +14,20 @@ from .controller import (
|
|
|
14
14
|
step,
|
|
15
15
|
)
|
|
16
16
|
from .decision_helpers import (
|
|
17
|
-
get_clarify_prompt,
|
|
18
17
|
get_decision_state,
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
get_error_message,
|
|
19
|
+
is_error,
|
|
20
|
+
is_no_directive,
|
|
21
21
|
is_update,
|
|
22
22
|
)
|
|
23
23
|
from .engine import (
|
|
24
24
|
Decision,
|
|
25
|
-
DecisionKind,
|
|
26
25
|
Engine,
|
|
26
|
+
ErrorDecision,
|
|
27
|
+
NoDirectiveDecision,
|
|
27
28
|
PolicyValue,
|
|
28
29
|
State,
|
|
30
|
+
UpdateDecision,
|
|
29
31
|
create_engine,
|
|
30
32
|
)
|
|
31
33
|
|
|
@@ -33,23 +35,25 @@ __version__ = version("context-compiler")
|
|
|
33
35
|
|
|
34
36
|
__all__ = [
|
|
35
37
|
"Decision",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"DECISION_PASSTHROUGH",
|
|
38
|
+
"DECISION_ERROR",
|
|
39
|
+
"DECISION_NO_DIRECTIVE",
|
|
39
40
|
"DECISION_UPDATE",
|
|
41
|
+
"ErrorDecision",
|
|
40
42
|
"Engine",
|
|
43
|
+
"NoDirectiveDecision",
|
|
41
44
|
"POLICY_PROHIBIT",
|
|
42
45
|
"POLICY_USE",
|
|
43
46
|
"PolicyValue",
|
|
44
47
|
"State",
|
|
45
48
|
"StepResult",
|
|
49
|
+
"UpdateDecision",
|
|
46
50
|
"create_engine",
|
|
47
|
-
"
|
|
51
|
+
"get_error_message",
|
|
48
52
|
"get_decision_state",
|
|
49
53
|
"get_step_decision",
|
|
50
54
|
"get_step_state",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
55
|
+
"is_error",
|
|
56
|
+
"is_no_directive",
|
|
53
57
|
"is_update",
|
|
54
58
|
"step",
|
|
55
59
|
]
|
context_compiler/const.py
CHANGED
|
@@ -12,9 +12,9 @@ POLICY_USE: Final = "use"
|
|
|
12
12
|
POLICY_PROHIBIT: Final = "prohibit"
|
|
13
13
|
|
|
14
14
|
# Decision kinds
|
|
15
|
-
|
|
15
|
+
DECISION_NO_DIRECTIVE: Final = "no_directive"
|
|
16
16
|
DECISION_UPDATE: Final = "update"
|
|
17
|
-
|
|
17
|
+
DECISION_ERROR: Final = "error"
|
|
18
18
|
|
|
19
19
|
# Schema version
|
|
20
20
|
SCHEMA_VERSION: Final = 2
|
context_compiler/controller.py
CHANGED
|
@@ -8,29 +8,39 @@ OUTPUT_VERSION: Literal[1] = 1
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class PremiseDiff(TypedDict):
|
|
11
|
+
"""Describe how the authoritative premise changed between two states."""
|
|
12
|
+
|
|
11
13
|
before: str | None
|
|
12
14
|
after: str | None
|
|
13
15
|
changed: bool
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class ChangedPolicyDiff(TypedDict):
|
|
19
|
+
"""Capture the old and new value for one policy item."""
|
|
20
|
+
|
|
17
21
|
before: Literal["use", "prohibit"]
|
|
18
22
|
after: Literal["use", "prohibit"]
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class PoliciesDiff(TypedDict):
|
|
26
|
+
"""Summarize policy items added, removed, or changed between two states."""
|
|
27
|
+
|
|
22
28
|
added: dict[str, Literal["use", "prohibit"]]
|
|
23
29
|
removed: dict[str, Literal["use", "prohibit"]]
|
|
24
30
|
changed: dict[str, ChangedPolicyDiff]
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
class StructuralDiff(TypedDict):
|
|
34
|
+
"""Describe whether and how authoritative state changed structurally."""
|
|
35
|
+
|
|
28
36
|
changed: bool
|
|
29
37
|
premise: PremiseDiff
|
|
30
38
|
policies: PoliciesDiff
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
class StepResult(TypedDict):
|
|
42
|
+
"""Return the committed outcome of one controller-driven engine step."""
|
|
43
|
+
|
|
34
44
|
output_version: Literal[1]
|
|
35
45
|
mode: Literal["step"]
|
|
36
46
|
decision: Decision
|
|
@@ -38,6 +48,8 @@ class StepResult(TypedDict):
|
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
class PreviewResult(TypedDict):
|
|
51
|
+
"""Return the dry-run outcome of one controller preview evaluation."""
|
|
52
|
+
|
|
41
53
|
output_version: Literal[1]
|
|
42
54
|
mode: Literal["preview"]
|
|
43
55
|
decision: Decision
|
|
@@ -48,30 +60,44 @@ class PreviewResult(TypedDict):
|
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
def get_step_decision(step_result: StepResult) -> Decision:
|
|
63
|
+
"""Return the decision emitted by a committed controller step."""
|
|
64
|
+
|
|
51
65
|
return step_result["decision"]
|
|
52
66
|
|
|
53
67
|
|
|
54
68
|
def get_step_state(step_result: StepResult) -> State:
|
|
69
|
+
"""Return the authoritative state after a committed controller step."""
|
|
70
|
+
|
|
55
71
|
return step_result["state"]
|
|
56
72
|
|
|
57
73
|
|
|
58
74
|
def get_preview_decision(preview_result: PreviewResult) -> Decision:
|
|
75
|
+
"""Return the decision that preview computed without mutating live state."""
|
|
76
|
+
|
|
59
77
|
return preview_result["decision"]
|
|
60
78
|
|
|
61
79
|
|
|
62
80
|
def get_preview_state_after(preview_result: PreviewResult) -> State:
|
|
81
|
+
"""Return the simulated post-transition state from preview."""
|
|
82
|
+
|
|
63
83
|
return preview_result["state_after"]
|
|
64
84
|
|
|
65
85
|
|
|
66
86
|
def preview_would_mutate(preview_result: PreviewResult) -> bool:
|
|
87
|
+
"""Return whether preview observed any structural state change."""
|
|
88
|
+
|
|
67
89
|
return preview_result["would_mutate"]
|
|
68
90
|
|
|
69
91
|
|
|
70
92
|
def diff_has_changes(diff: StructuralDiff) -> bool:
|
|
93
|
+
"""Return whether a structural diff reports any state change."""
|
|
94
|
+
|
|
71
95
|
return diff["changed"]
|
|
72
96
|
|
|
73
97
|
|
|
74
98
|
def state_diff(before: State, after: State) -> StructuralDiff:
|
|
99
|
+
"""Compute a structural diff between two authoritative engine states."""
|
|
100
|
+
|
|
75
101
|
before_premise = before["premise"]
|
|
76
102
|
after_premise = after["premise"]
|
|
77
103
|
premise_changed = before_premise != after_premise
|
|
@@ -112,6 +138,8 @@ def state_diff(before: State, after: State) -> StructuralDiff:
|
|
|
112
138
|
|
|
113
139
|
|
|
114
140
|
def step(engine: Engine, user_input: str) -> StepResult:
|
|
141
|
+
"""Commit one engine transition and package the resulting decision and state."""
|
|
142
|
+
|
|
115
143
|
decision = engine.step(user_input)
|
|
116
144
|
return {
|
|
117
145
|
"output_version": OUTPUT_VERSION,
|
|
@@ -122,19 +150,19 @@ def step(engine: Engine, user_input: str) -> StepResult:
|
|
|
122
150
|
|
|
123
151
|
|
|
124
152
|
def preview(engine: Engine, user_input: str) -> PreviewResult:
|
|
125
|
-
|
|
126
|
-
state_before = engine.state
|
|
153
|
+
"""Evaluate one transition without mutating the engine's live state.
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
state_after = engine.state
|
|
133
|
-
finally:
|
|
134
|
-
engine.import_json(state_json)
|
|
155
|
+
Preview uses the same transition evaluation path as committed execution,
|
|
156
|
+
then returns the simulated decision, before/after states, and a structural
|
|
157
|
+
diff that callers can inspect before deciding whether to step.
|
|
158
|
+
"""
|
|
135
159
|
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
state_before = engine.state
|
|
161
|
+
# Preview intentionally consumes the engine's private evaluator so preview and
|
|
162
|
+
# committed execution share one transition path without making evaluation public.
|
|
163
|
+
evaluated = engine._evaluate_transition(state_before, user_input) # noqa: SLF001
|
|
164
|
+
decision = evaluated.decision
|
|
165
|
+
state_after = evaluated.next_state
|
|
138
166
|
|
|
139
167
|
diff = state_diff(state_before, state_after)
|
|
140
168
|
would_mutate = diff["changed"]
|
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
"""Public helpers for safer decision inspection in host-side code."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from .engine import Decision, State
|
|
3
|
+
from typing import TypeGuard
|
|
5
4
|
|
|
5
|
+
from .const import DECISION_ERROR, DECISION_NO_DIRECTIVE, DECISION_UPDATE
|
|
6
|
+
from .engine import (
|
|
7
|
+
Decision,
|
|
8
|
+
ErrorDecision,
|
|
9
|
+
NoDirectiveDecision,
|
|
10
|
+
State,
|
|
11
|
+
UpdateDecision,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_update(decision: Decision) -> TypeGuard[UpdateDecision]:
|
|
16
|
+
"""Return whether a decision represents a successful state update."""
|
|
6
17
|
|
|
7
|
-
def is_update(decision: Decision) -> bool:
|
|
8
18
|
return decision["kind"] == DECISION_UPDATE
|
|
9
19
|
|
|
10
20
|
|
|
11
|
-
def
|
|
12
|
-
|
|
21
|
+
def is_error(decision: Decision) -> TypeGuard[ErrorDecision]:
|
|
22
|
+
"""Return whether a decision represents an error outcome."""
|
|
23
|
+
|
|
24
|
+
return decision["kind"] == DECISION_ERROR
|
|
13
25
|
|
|
14
26
|
|
|
15
|
-
def
|
|
16
|
-
|
|
27
|
+
def is_no_directive(decision: Decision) -> TypeGuard[NoDirectiveDecision]:
|
|
28
|
+
"""Return whether a decision reports that no directive was recognized."""
|
|
17
29
|
|
|
30
|
+
return decision["kind"] == DECISION_NO_DIRECTIVE
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
|
|
33
|
+
def get_error_message(decision: Decision) -> str | None:
|
|
34
|
+
"""Return the error message for an error decision, if present."""
|
|
35
|
+
|
|
36
|
+
if not is_error(decision):
|
|
37
|
+
return None
|
|
38
|
+
return decision["message"]
|
|
21
39
|
|
|
22
40
|
|
|
23
41
|
def get_decision_state(decision: Decision) -> State | None:
|
|
42
|
+
"""Return the updated authoritative state for an update decision, if present."""
|
|
43
|
+
|
|
44
|
+
if not is_update(decision):
|
|
45
|
+
return None
|
|
24
46
|
return decision["state"]
|
context_compiler/engine.py
CHANGED
|
@@ -5,11 +5,13 @@ import re
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from enum import StrEnum
|
|
9
8
|
from typing import Literal, TypedDict
|
|
10
9
|
from unicodedata import normalize as unicode_normalize
|
|
11
10
|
|
|
12
11
|
from .const import (
|
|
12
|
+
DECISION_ERROR,
|
|
13
|
+
DECISION_NO_DIRECTIVE,
|
|
14
|
+
DECISION_UPDATE,
|
|
13
15
|
POLICY_PROHIBIT,
|
|
14
16
|
POLICY_USE,
|
|
15
17
|
SCHEMA_VERSION,
|
|
@@ -17,7 +19,7 @@ from .const import (
|
|
|
17
19
|
STATE_PREMISE,
|
|
18
20
|
STATE_VERSION,
|
|
19
21
|
)
|
|
20
|
-
from .grammar import DirectiveKind,
|
|
22
|
+
from .grammar import DirectiveKind, decompose_directive
|
|
21
23
|
|
|
22
24
|
PolicyValue = Literal["use", "prohibit"]
|
|
23
25
|
|
|
@@ -30,20 +32,33 @@ class State(TypedDict):
|
|
|
30
32
|
version: Literal[2]
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
class
|
|
34
|
-
|
|
35
|
-
PASSTHROUGH = "passthrough"
|
|
36
|
-
CLARIFY = "clarify"
|
|
35
|
+
class NoDirectiveDecision(TypedDict):
|
|
36
|
+
"""Report that input did not match any recognized canonical directive."""
|
|
37
37
|
|
|
38
|
+
kind: Literal["no_directive"]
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
class UpdateDecision(TypedDict):
|
|
42
|
+
"""Report a successful transition and the resulting authoritative state."""
|
|
43
|
+
|
|
44
|
+
kind: Literal["update"]
|
|
45
|
+
state: State
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ErrorDecision(TypedDict):
|
|
49
|
+
"""Report a rejected transition together with a user-facing error message."""
|
|
50
|
+
|
|
51
|
+
kind: Literal["error"]
|
|
52
|
+
message: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision
|
|
43
56
|
|
|
44
57
|
|
|
45
58
|
@dataclass(frozen=True)
|
|
46
59
|
class Action:
|
|
60
|
+
"""Represent one parsed engine action before state validation or mutation."""
|
|
61
|
+
|
|
47
62
|
kind: Literal[
|
|
48
63
|
"set_premise",
|
|
49
64
|
"change_premise",
|
|
@@ -61,65 +76,100 @@ class Action:
|
|
|
61
76
|
old_item: str | None = None
|
|
62
77
|
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class _EvaluatedTransition:
|
|
81
|
+
decision: Decision
|
|
82
|
+
next_state: State
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_NO_DIRECTIVE: NoDirectiveDecision = {"kind": DECISION_NO_DIRECTIVE}
|
|
69
86
|
|
|
70
87
|
|
|
71
88
|
def create_engine(state: State | None = None) -> "Engine":
|
|
89
|
+
"""Create an engine initialized from validated state or the empty state."""
|
|
90
|
+
|
|
72
91
|
return Engine(state=state)
|
|
73
92
|
|
|
74
93
|
|
|
75
94
|
class Engine:
|
|
95
|
+
"""Own the authoritative state and apply one directive transition at a time."""
|
|
96
|
+
|
|
76
97
|
def __init__(self, state: State | None = None) -> None:
|
|
77
98
|
self._state: State
|
|
78
99
|
self._replace_state(_initial_state() if state is None else _load_state_obj(state))
|
|
79
100
|
|
|
80
101
|
@property
|
|
81
102
|
def premise(self) -> str | None:
|
|
103
|
+
"""Return the current premise from authoritative state."""
|
|
104
|
+
|
|
82
105
|
return self._state[STATE_PREMISE]
|
|
83
106
|
|
|
84
107
|
@property
|
|
85
108
|
def policies(self) -> Mapping[str, PolicyValue]:
|
|
109
|
+
"""Return a defensive copy of the current policy mapping."""
|
|
110
|
+
|
|
86
111
|
return deepcopy(self._state[STATE_POLICIES])
|
|
87
112
|
|
|
88
113
|
@property
|
|
89
114
|
def state(self) -> State:
|
|
115
|
+
"""Return a defensive copy of the full authoritative state."""
|
|
116
|
+
|
|
90
117
|
return deepcopy(self._state)
|
|
91
118
|
|
|
92
119
|
def export_json(self) -> str:
|
|
120
|
+
"""Serialize the current authoritative state to canonical JSON text."""
|
|
121
|
+
|
|
93
122
|
return json.dumps(self._state, sort_keys=True, separators=(",", ":"))
|
|
94
123
|
|
|
95
124
|
def import_json(self, payload: str) -> None:
|
|
125
|
+
"""Replace authoritative state from previously exported JSON text.
|
|
126
|
+
|
|
127
|
+
The payload must match the current state schema and is normalized using
|
|
128
|
+
the same validation rules applied to other engine state inputs.
|
|
129
|
+
"""
|
|
130
|
+
|
|
96
131
|
self._replace_state(_load_state_json(payload))
|
|
97
132
|
|
|
98
133
|
def step(self, user_input: str) -> Decision:
|
|
134
|
+
"""Evaluate and commit one user input against authoritative state.
|
|
135
|
+
|
|
136
|
+
Non-directive input returns ``no_directive`` without changing state.
|
|
137
|
+
Invalid directives return ``error`` without changing state. Accepted
|
|
138
|
+
directives return ``update`` and commit the resulting authoritative
|
|
139
|
+
state before the decision is returned.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
evaluated = self._evaluate_transition(self._state, user_input)
|
|
143
|
+
self._replace_state(evaluated.next_state)
|
|
144
|
+
return evaluated.decision
|
|
145
|
+
|
|
146
|
+
def _evaluate_transition(self, state: State, user_input: str) -> _EvaluatedTransition:
|
|
99
147
|
action = _parse_directive(user_input)
|
|
100
148
|
if action is None:
|
|
101
|
-
return
|
|
149
|
+
return _EvaluatedTransition(decision=_NO_DIRECTIVE.copy(), next_state=deepcopy(state))
|
|
102
150
|
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
return
|
|
151
|
+
error_decision = self._pre_mutation_error(action, state=state)
|
|
152
|
+
if error_decision is not None:
|
|
153
|
+
return _EvaluatedTransition(decision=error_decision, next_state=deepcopy(state))
|
|
106
154
|
|
|
107
|
-
|
|
155
|
+
next_state = self._apply_action(action, state=state)
|
|
156
|
+
return _EvaluatedTransition(decision=_update_decision(next_state), next_state=next_state)
|
|
108
157
|
|
|
109
158
|
def _replace_state(self, state: State) -> None:
|
|
110
159
|
self._state = state
|
|
111
160
|
|
|
112
|
-
def
|
|
113
|
-
|
|
161
|
+
def _pre_mutation_error(self, action: Action, *, state: State | None = None) -> Decision | None:
|
|
162
|
+
candidate_state = self._state if state is None else state
|
|
163
|
+
# Single error path: all error outcomes are detected before any mutation.
|
|
114
164
|
if action.kind in {"set_premise", "change_premise"}:
|
|
115
165
|
assert action.value is not None
|
|
116
166
|
if _sanitize_premise_value(action.value) == "":
|
|
117
167
|
if action.kind == "set_premise":
|
|
118
|
-
return
|
|
168
|
+
return _error(
|
|
119
169
|
"Premise value cannot be empty.\n"
|
|
120
170
|
"Use 'set premise <value>' with a non-empty value."
|
|
121
171
|
)
|
|
122
|
-
return
|
|
172
|
+
return _error(
|
|
123
173
|
"Premise value cannot be empty.\n"
|
|
124
174
|
"Use 'change premise to <value>' with a non-empty value."
|
|
125
175
|
)
|
|
@@ -127,7 +177,7 @@ class Engine:
|
|
|
127
177
|
if action.kind == "remove_policy_item":
|
|
128
178
|
assert action.item is not None
|
|
129
179
|
if _normalize_item(action.item) == "":
|
|
130
|
-
return
|
|
180
|
+
return _error(
|
|
131
181
|
"Policy item cannot be empty.\n"
|
|
132
182
|
"Use 'remove policy <item>' with a non-empty value."
|
|
133
183
|
)
|
|
@@ -135,36 +185,36 @@ class Engine:
|
|
|
135
185
|
if action.kind == "use_item":
|
|
136
186
|
assert action.item is not None
|
|
137
187
|
if _normalize_item(action.item) == "":
|
|
138
|
-
return
|
|
188
|
+
return _error(
|
|
139
189
|
"Policy item cannot be empty.\nUse 'use <item>' with a non-empty value."
|
|
140
190
|
)
|
|
141
191
|
|
|
142
192
|
if action.kind == "prohibit_item":
|
|
143
193
|
assert action.item is not None
|
|
144
194
|
if _normalize_item(action.item) == "":
|
|
145
|
-
return
|
|
195
|
+
return _error(
|
|
146
196
|
"Policy item cannot be empty.\nUse 'prohibit <item>' with a non-empty value."
|
|
147
197
|
)
|
|
148
198
|
|
|
149
|
-
if action.kind == "set_premise" and
|
|
150
|
-
return
|
|
199
|
+
if action.kind == "set_premise" and candidate_state[STATE_PREMISE] is not None:
|
|
200
|
+
return _error("Premise already set.\nUse 'change premise to <value>' to modify it.")
|
|
151
201
|
|
|
152
|
-
if action.kind == "change_premise" and
|
|
153
|
-
return
|
|
202
|
+
if action.kind == "change_premise" and candidate_state[STATE_PREMISE] is None:
|
|
203
|
+
return _error("No premise is set.\nUse 'set premise <value>' to define one.")
|
|
154
204
|
|
|
155
205
|
if action.kind == "use_item":
|
|
156
206
|
assert action.item is not None
|
|
157
207
|
item_key = _normalize_item(action.item)
|
|
158
|
-
if
|
|
159
|
-
return
|
|
208
|
+
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_PROHIBIT:
|
|
209
|
+
return _error(
|
|
160
210
|
f'"{item_key}" is currently prohibited.\nRemove or replace it before using it.'
|
|
161
211
|
)
|
|
162
212
|
|
|
163
213
|
if action.kind == "prohibit_item":
|
|
164
214
|
assert action.item is not None
|
|
165
215
|
item_key = _normalize_item(action.item)
|
|
166
|
-
if
|
|
167
|
-
return
|
|
216
|
+
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_USE:
|
|
217
|
+
return _error(
|
|
168
218
|
f'"{item_key}" is currently in use.\n'
|
|
169
219
|
"Remove or replace it before prohibiting it."
|
|
170
220
|
)
|
|
@@ -177,91 +227,89 @@ class Engine:
|
|
|
177
227
|
if new_key == old_key:
|
|
178
228
|
return None
|
|
179
229
|
|
|
180
|
-
old_state =
|
|
181
|
-
new_state =
|
|
230
|
+
old_state = candidate_state[STATE_POLICIES].get(old_key)
|
|
231
|
+
new_state = candidate_state[STATE_POLICIES].get(new_key)
|
|
182
232
|
if old_state == POLICY_PROHIBIT:
|
|
183
|
-
return
|
|
233
|
+
return _error(
|
|
184
234
|
f'"{action.old_item}" is currently prohibited.\n'
|
|
185
235
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
186
236
|
)
|
|
187
237
|
if new_state == POLICY_PROHIBIT:
|
|
188
|
-
return
|
|
238
|
+
return _error(
|
|
189
239
|
f'"{action.new_item}" is currently prohibited.\n'
|
|
190
240
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
191
241
|
)
|
|
192
242
|
if old_state not in {None, POLICY_USE}:
|
|
193
|
-
return
|
|
243
|
+
return _error(
|
|
194
244
|
f'"{action.old_item}" is not currently in use.\n'
|
|
195
245
|
"Replacement requires an active 'use' policy."
|
|
196
246
|
)
|
|
197
247
|
|
|
198
248
|
return None
|
|
199
249
|
|
|
200
|
-
def _apply_action(self, action: Action) ->
|
|
250
|
+
def _apply_action(self, action: Action, *, state: State) -> State:
|
|
251
|
+
next_state = deepcopy(state)
|
|
201
252
|
kind = action.kind
|
|
202
253
|
|
|
203
254
|
if kind == "set_premise":
|
|
204
255
|
assert action.value is not None
|
|
205
|
-
|
|
206
|
-
return
|
|
256
|
+
next_state[STATE_PREMISE] = _sanitize_premise_value(action.value)
|
|
257
|
+
return next_state
|
|
207
258
|
|
|
208
259
|
if kind == "change_premise":
|
|
209
260
|
assert action.value is not None
|
|
210
|
-
|
|
211
|
-
return
|
|
261
|
+
next_state[STATE_PREMISE] = _sanitize_premise_value(action.value)
|
|
262
|
+
return next_state
|
|
212
263
|
|
|
213
264
|
if kind == "use_item":
|
|
214
265
|
assert action.item is not None
|
|
215
266
|
item_key = _normalize_item(action.item)
|
|
216
267
|
# Idempotent directives are updates even if state does not change.
|
|
217
|
-
|
|
218
|
-
return
|
|
268
|
+
next_state[STATE_POLICIES][item_key] = POLICY_USE
|
|
269
|
+
return next_state
|
|
219
270
|
|
|
220
271
|
if kind == "prohibit_item":
|
|
221
272
|
assert action.item is not None
|
|
222
273
|
item_key = _normalize_item(action.item)
|
|
223
274
|
# Idempotent directives are updates even if state does not change.
|
|
224
|
-
|
|
225
|
-
return
|
|
275
|
+
next_state[STATE_POLICIES][item_key] = POLICY_PROHIBIT
|
|
276
|
+
return next_state
|
|
226
277
|
|
|
227
278
|
if kind == "replace_use":
|
|
228
279
|
assert action.new_item is not None
|
|
229
280
|
assert action.old_item is not None
|
|
230
|
-
self._apply_replacement_explicit(action.new_item, action.old_item)
|
|
231
|
-
return
|
|
281
|
+
self._apply_replacement_explicit(next_state, action.new_item, action.old_item)
|
|
282
|
+
return next_state
|
|
232
283
|
|
|
233
284
|
if kind == "remove_policy_item":
|
|
234
285
|
assert action.item is not None
|
|
235
286
|
item_key = _normalize_item(action.item)
|
|
236
|
-
|
|
237
|
-
return
|
|
287
|
+
next_state[STATE_POLICIES].pop(item_key, None)
|
|
288
|
+
return next_state
|
|
238
289
|
|
|
239
290
|
if kind == "clear_premise":
|
|
240
|
-
|
|
241
|
-
return
|
|
291
|
+
next_state[STATE_PREMISE] = None
|
|
292
|
+
return next_state
|
|
242
293
|
|
|
243
294
|
if kind == "reset_policies":
|
|
244
|
-
|
|
245
|
-
return
|
|
295
|
+
next_state[STATE_POLICIES] = {}
|
|
296
|
+
return next_state
|
|
246
297
|
|
|
247
|
-
|
|
248
|
-
return _update_decision(self._state)
|
|
298
|
+
return _initial_state()
|
|
249
299
|
|
|
250
|
-
def _apply_replacement_explicit(self, new_item: str, old_item: str) -> None:
|
|
300
|
+
def _apply_replacement_explicit(self, state: State, new_item: str, old_item: str) -> None:
|
|
251
301
|
new_key = _normalize_item(new_item)
|
|
252
302
|
old_key = _normalize_item(old_item)
|
|
253
303
|
|
|
254
304
|
if new_key == old_key:
|
|
255
305
|
return
|
|
256
306
|
|
|
257
|
-
|
|
258
|
-
|
|
307
|
+
state[STATE_POLICIES].pop(old_key, None)
|
|
308
|
+
state[STATE_POLICIES][new_key] = POLICY_USE
|
|
259
309
|
|
|
260
310
|
|
|
261
311
|
def _parse_directive(user_input: str) -> Action | None:
|
|
262
|
-
|
|
263
|
-
# This keeps syntax authority in grammar without making the parser a public API.
|
|
264
|
-
parsed = _parse_canonical_directive(user_input)
|
|
312
|
+
parsed = decompose_directive(user_input)
|
|
265
313
|
if parsed is None:
|
|
266
314
|
return None
|
|
267
315
|
|
|
@@ -357,17 +405,9 @@ def _normalize_item(value: str) -> str:
|
|
|
357
405
|
return normalized.strip()
|
|
358
406
|
|
|
359
407
|
|
|
360
|
-
def
|
|
361
|
-
return {
|
|
362
|
-
"kind": DecisionKind.CLARIFY,
|
|
363
|
-
"state": None,
|
|
364
|
-
"prompt_to_user": prompt,
|
|
365
|
-
}
|
|
408
|
+
def _error(message: str) -> ErrorDecision:
|
|
409
|
+
return {"kind": DECISION_ERROR, "message": message}
|
|
366
410
|
|
|
367
411
|
|
|
368
|
-
def _update_decision(state: State) ->
|
|
369
|
-
return {
|
|
370
|
-
"kind": DecisionKind.UPDATE,
|
|
371
|
-
"state": deepcopy(state),
|
|
372
|
-
"prompt_to_user": None,
|
|
373
|
-
}
|
|
412
|
+
def _update_decision(state: State) -> UpdateDecision:
|
|
413
|
+
return {"kind": DECISION_UPDATE, "state": deepcopy(state)}
|
context_compiler/grammar.py
CHANGED
|
@@ -8,6 +8,8 @@ from types import MappingProxyType
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class DirectiveKind(StrEnum):
|
|
11
|
+
"""Enumerate the supported canonical directive families."""
|
|
12
|
+
|
|
11
13
|
SET_PREMISE = "set_premise"
|
|
12
14
|
CHANGE_PREMISE = "change_premise"
|
|
13
15
|
USE_ITEM = "use_item"
|
|
@@ -21,12 +23,16 @@ class DirectiveKind(StrEnum):
|
|
|
21
23
|
|
|
22
24
|
@dataclass(frozen=True, slots=True)
|
|
23
25
|
class ValidatedDirective:
|
|
26
|
+
"""Classify text as one canonical directive kind without exposing operands."""
|
|
27
|
+
|
|
24
28
|
text: str
|
|
25
29
|
kind: DirectiveKind
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
@dataclass(frozen=True, slots=True)
|
|
29
|
-
class
|
|
33
|
+
class CanonicalDirective:
|
|
34
|
+
"""Represent one parsed canonical directive and its named operands."""
|
|
35
|
+
|
|
30
36
|
text: str
|
|
31
37
|
kind: DirectiveKind
|
|
32
38
|
operands: MappingProxyType[str, str]
|
|
@@ -173,7 +179,14 @@ def _operand_starts_with_token(value: str, token: str) -> bool:
|
|
|
173
179
|
return normalized == token or normalized.startswith(f"{token} ")
|
|
174
180
|
|
|
175
181
|
|
|
176
|
-
def
|
|
182
|
+
def match_canonical_directive_start(text: str, start: int) -> int | None:
|
|
183
|
+
"""Locate a canonical directive prefix at a given character position.
|
|
184
|
+
|
|
185
|
+
This classifies whether canonical directive syntax begins at ``start`` and,
|
|
186
|
+
when it does, returns the index immediately after the directive keyword
|
|
187
|
+
prefix. It does not parse operands, validate the full directive payload, or
|
|
188
|
+
evaluate multi-directive state semantics.
|
|
189
|
+
"""
|
|
177
190
|
if start < 0 or start >= len(text):
|
|
178
191
|
return None
|
|
179
192
|
|
|
@@ -230,20 +243,27 @@ def _match_directive_token(
|
|
|
230
243
|
return index
|
|
231
244
|
|
|
232
245
|
|
|
233
|
-
def
|
|
234
|
-
|
|
246
|
+
def contains_multiple_canonical_directives(text: str) -> bool:
|
|
247
|
+
"""Report whether text contains more than one canonical directive start.
|
|
248
|
+
|
|
249
|
+
This detects compound directive text by looking for multiple canonical
|
|
250
|
+
directive prefixes in the same input. It does not parse directive operands,
|
|
251
|
+
repair malformed text, or determine whether a whole string should be
|
|
252
|
+
accepted as a single directive.
|
|
253
|
+
"""
|
|
254
|
+
first_start = match_canonical_directive_start(text, 0)
|
|
235
255
|
if first_start is None:
|
|
236
256
|
return False
|
|
237
257
|
|
|
238
258
|
for index in range(first_start, len(text)):
|
|
239
|
-
next_start =
|
|
259
|
+
next_start = match_canonical_directive_start(text, index)
|
|
240
260
|
if next_start is not None:
|
|
241
261
|
return True
|
|
242
262
|
|
|
243
263
|
return False
|
|
244
264
|
|
|
245
265
|
|
|
246
|
-
def _parse_replace_use(trimmed_text: str) ->
|
|
266
|
+
def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
|
|
247
267
|
match = _REPLACE_RE.fullmatch(trimmed_text)
|
|
248
268
|
if match is None:
|
|
249
269
|
return None
|
|
@@ -258,34 +278,41 @@ def _parse_replace_use(trimmed_text: str) -> _GrammarParsedDirective | None:
|
|
|
258
278
|
normalized_payload = _normalized_for_matching(trimmed_text)
|
|
259
279
|
if normalized_payload.count(_INSTEAD_OF_DELIMITER) != 1:
|
|
260
280
|
return None
|
|
261
|
-
return
|
|
281
|
+
return CanonicalDirective(
|
|
262
282
|
text=trimmed_text,
|
|
263
283
|
kind=DirectiveKind.REPLACE_USE,
|
|
264
284
|
operands=MappingProxyType({"new_item": new_item, "old_item": old_item}),
|
|
265
285
|
)
|
|
266
286
|
|
|
267
287
|
|
|
268
|
-
def
|
|
288
|
+
def decompose_directive(text: str) -> CanonicalDirective | None:
|
|
289
|
+
"""Parse one canonical directive into its semantic kind and operands.
|
|
290
|
+
|
|
291
|
+
This determines whether ``text`` is a single canonical directive and, when
|
|
292
|
+
it is, returns the directive kind plus canonical operand names with the
|
|
293
|
+
original operand text preserved. It does not repair input, infer intent, or
|
|
294
|
+
evaluate directive effects against compiler state.
|
|
295
|
+
"""
|
|
269
296
|
trimmed_text = _trim_ascii_whitespace(text)
|
|
270
297
|
if trimmed_text == "":
|
|
271
298
|
return None
|
|
272
|
-
if
|
|
299
|
+
if contains_multiple_canonical_directives(trimmed_text):
|
|
273
300
|
return None
|
|
274
301
|
|
|
275
302
|
normalized = _normalized_for_matching(trimmed_text)
|
|
276
303
|
|
|
277
304
|
if normalized == "clear premise":
|
|
278
|
-
return
|
|
305
|
+
return CanonicalDirective(
|
|
279
306
|
text=text, kind=DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({})
|
|
280
307
|
)
|
|
281
308
|
if normalized == "reset policies":
|
|
282
|
-
return
|
|
309
|
+
return CanonicalDirective(
|
|
283
310
|
text=text,
|
|
284
311
|
kind=DirectiveKind.RESET_POLICIES,
|
|
285
312
|
operands=MappingProxyType({}),
|
|
286
313
|
)
|
|
287
314
|
if normalized == "clear state":
|
|
288
|
-
return
|
|
315
|
+
return CanonicalDirective(
|
|
289
316
|
text=text, kind=DirectiveKind.CLEAR_STATE, operands=MappingProxyType({})
|
|
290
317
|
)
|
|
291
318
|
|
|
@@ -296,7 +323,7 @@ def _parse_directive(text: str) -> _GrammarParsedDirective | None:
|
|
|
296
323
|
value = match.group("value")
|
|
297
324
|
if not _operand_has_content(value) or _operand_starts_with_token(value, "to"):
|
|
298
325
|
return None
|
|
299
|
-
return
|
|
326
|
+
return CanonicalDirective(
|
|
300
327
|
text=text,
|
|
301
328
|
kind=DirectiveKind.SET_PREMISE,
|
|
302
329
|
operands=MappingProxyType({"value": value}),
|
|
@@ -309,7 +336,7 @@ def _parse_directive(text: str) -> _GrammarParsedDirective | None:
|
|
|
309
336
|
value = match.group("value")
|
|
310
337
|
if not _operand_has_content(value):
|
|
311
338
|
return None
|
|
312
|
-
return
|
|
339
|
+
return CanonicalDirective(
|
|
313
340
|
text=text,
|
|
314
341
|
kind=DirectiveKind.CHANGE_PREMISE,
|
|
315
342
|
operands=MappingProxyType({"value": value}),
|
|
@@ -332,7 +359,7 @@ def _parse_directive(text: str) -> _GrammarParsedDirective | None:
|
|
|
332
359
|
or _INSTEAD_OF_DELIMITER in normalized_item
|
|
333
360
|
):
|
|
334
361
|
return None
|
|
335
|
-
return
|
|
362
|
+
return CanonicalDirective(
|
|
336
363
|
text=text,
|
|
337
364
|
kind=DirectiveKind.USE_ITEM,
|
|
338
365
|
operands=MappingProxyType({"item": item}),
|
|
@@ -345,7 +372,7 @@ def _parse_directive(text: str) -> _GrammarParsedDirective | None:
|
|
|
345
372
|
item = match.group("item")
|
|
346
373
|
if not _operand_has_content(item):
|
|
347
374
|
return None
|
|
348
|
-
return
|
|
375
|
+
return CanonicalDirective(
|
|
349
376
|
text=text,
|
|
350
377
|
kind=DirectiveKind.PROHIBIT_ITEM,
|
|
351
378
|
operands=MappingProxyType({"item": item}),
|
|
@@ -358,7 +385,7 @@ def _parse_directive(text: str) -> _GrammarParsedDirective | None:
|
|
|
358
385
|
item = match.group("item")
|
|
359
386
|
if not _operand_has_content(item):
|
|
360
387
|
return None
|
|
361
|
-
return
|
|
388
|
+
return CanonicalDirective(
|
|
362
389
|
text=text,
|
|
363
390
|
kind=DirectiveKind.REMOVE_POLICY,
|
|
364
391
|
operands=MappingProxyType({"item": item}),
|
|
@@ -368,29 +395,37 @@ def _parse_directive(text: str) -> _GrammarParsedDirective | None:
|
|
|
368
395
|
|
|
369
396
|
|
|
370
397
|
def validate_directive(text: str) -> ValidatedDirective | None:
|
|
371
|
-
|
|
398
|
+
"""Classify whether text is a canonical directive.
|
|
399
|
+
|
|
400
|
+
This determines whether ``text`` belongs to one canonical directive family
|
|
401
|
+
and returns only the normalized semantic kind needed for classification. It
|
|
402
|
+
does not expose operands, render directives, repair malformed text, or
|
|
403
|
+
evaluate any state transition.
|
|
404
|
+
"""
|
|
405
|
+
parsed = decompose_directive(text)
|
|
372
406
|
if parsed is None:
|
|
373
407
|
return None
|
|
374
408
|
return ValidatedDirective(text=parsed.text, kind=parsed.kind)
|
|
375
409
|
|
|
376
410
|
|
|
377
|
-
def _parse_canonical_directive(text: str) -> _GrammarParsedDirective | None:
|
|
378
|
-
return _parse_directive(text)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def match_canonical_directive_start(text: str, start: int) -> int | None:
|
|
382
|
-
return _match_canonical_directive_start(text, start)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def contains_multiple_canonical_directives(text: str) -> bool:
|
|
386
|
-
return _contains_multiple_canonical_directives(text)
|
|
387
|
-
|
|
388
|
-
|
|
389
411
|
def is_canonical_directive(text: str) -> bool:
|
|
412
|
+
"""Return whether text is exactly one canonical directive.
|
|
413
|
+
|
|
414
|
+
This provides boolean directive classification for callers that only need a
|
|
415
|
+
yes-or-no answer. It does not parse operands, explain rejection reasons, or
|
|
416
|
+
perform validation beyond canonical grammar recognition.
|
|
417
|
+
"""
|
|
390
418
|
return validate_directive(text) is not None
|
|
391
419
|
|
|
392
420
|
|
|
393
421
|
def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
|
|
422
|
+
"""Render canonical directive text from a semantic kind and operands.
|
|
423
|
+
|
|
424
|
+
This determines the exact canonical spelling for an existing grammar
|
|
425
|
+
capability and rejects operand combinations that would not round-trip as the
|
|
426
|
+
requested directive kind. It does not infer missing operands, parse user
|
|
427
|
+
input, or extend the grammar with new behaviors.
|
|
428
|
+
"""
|
|
394
429
|
try:
|
|
395
430
|
normalized_kind = kind if isinstance(kind, DirectiveKind) else DirectiveKind(kind)
|
|
396
431
|
spec = _DIRECTIVE_SPECS[normalized_kind]
|
|
@@ -426,9 +461,13 @@ def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
|
|
|
426
461
|
|
|
427
462
|
|
|
428
463
|
__all__ = [
|
|
464
|
+
"CanonicalDirective",
|
|
429
465
|
"DirectiveKind",
|
|
430
466
|
"ValidatedDirective",
|
|
467
|
+
"contains_multiple_canonical_directives",
|
|
468
|
+
"decompose_directive",
|
|
431
469
|
"is_canonical_directive",
|
|
470
|
+
"match_canonical_directive_start",
|
|
432
471
|
"render_directive",
|
|
433
472
|
"validate_directive",
|
|
434
473
|
]
|
context_compiler/repl.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
"""Command-line and REPL entry points for interacting with the engine."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import sys
|
|
3
5
|
from typing import TextIO
|
|
4
6
|
|
|
5
7
|
from . import __version__, create_engine
|
|
6
|
-
from .const import
|
|
8
|
+
from .const import DECISION_ERROR
|
|
7
9
|
from .controller import (
|
|
8
10
|
OUTPUT_VERSION,
|
|
9
11
|
PreviewResult,
|
|
@@ -14,7 +16,8 @@ from .controller import (
|
|
|
14
16
|
)
|
|
15
17
|
from .controller import preview as controller_preview
|
|
16
18
|
from .controller import step as controller_step
|
|
17
|
-
from .
|
|
19
|
+
from .decision_helpers import is_error, is_no_directive, is_update
|
|
20
|
+
from .engine import Decision, Engine, State
|
|
18
21
|
|
|
19
22
|
_EXIT_TOKENS = {"exit", "quit"}
|
|
20
23
|
_HELP_TOKENS = {"help", "?"}
|
|
@@ -44,11 +47,7 @@ def _has_embedded_newline(raw_line: str) -> bool:
|
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
def _multi_command_decision() -> Decision:
|
|
47
|
-
return {
|
|
48
|
-
"kind": DecisionKind.CLARIFY,
|
|
49
|
-
"state": None,
|
|
50
|
-
"prompt_to_user": _MULTI_COMMAND_PROMPT,
|
|
51
|
-
}
|
|
50
|
+
return {"kind": DECISION_ERROR, "message": _MULTI_COMMAND_PROMPT}
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
def _print_interactive_help(out_stream: TextIO) -> None:
|
|
@@ -69,7 +68,7 @@ def _print_interactive_help(out_stream: TextIO) -> None:
|
|
|
69
68
|
print(" clear state", file=out_stream)
|
|
70
69
|
print("Bare input behavior remains unchanged.", file=out_stream)
|
|
71
70
|
print("preview is a deterministic dry-run and never mutates live state.", file=out_stream)
|
|
72
|
-
print("
|
|
71
|
+
print("error results are immediate messages and do not reserve later input.", file=out_stream)
|
|
73
72
|
|
|
74
73
|
|
|
75
74
|
def _render_state_lines(state: State) -> list[str]:
|
|
@@ -89,16 +88,15 @@ def _render_state_lines(state: State) -> list[str]:
|
|
|
89
88
|
|
|
90
89
|
|
|
91
90
|
def _render_decision_lines(decision: Decision) -> list[str]:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
prompt_lines = prompt.splitlines() if prompt else [""]
|
|
91
|
+
if is_no_directive(decision):
|
|
92
|
+
return ["no_directive"]
|
|
93
|
+
if is_error(decision):
|
|
94
|
+
message = decision["message"]
|
|
95
|
+
prompt_lines = message.splitlines() if message else [""]
|
|
98
96
|
return [f"error: {prompt_lines[0]}", *prompt_lines[1:]]
|
|
99
97
|
|
|
98
|
+
assert is_update(decision)
|
|
100
99
|
state = decision["state"]
|
|
101
|
-
assert state is not None
|
|
102
100
|
return ["updated", *_render_state_lines(state)]
|
|
103
101
|
|
|
104
102
|
|
|
@@ -249,11 +247,19 @@ def run_repl(
|
|
|
249
247
|
json_mode: bool = False,
|
|
250
248
|
engine: Engine | None = None,
|
|
251
249
|
) -> None:
|
|
250
|
+
"""Run the interactive or line-oriented REPL against one engine instance.
|
|
251
|
+
|
|
252
|
+
Interactive mode exposes command helpers such as ``state`` and ``preview``.
|
|
253
|
+
Non-interactive mode consumes one input line at a time and can optionally
|
|
254
|
+
emit NDJSON records. Preview requests are evaluated through the shared
|
|
255
|
+
controller preview path and never mutate the live engine state.
|
|
256
|
+
"""
|
|
257
|
+
|
|
252
258
|
active_engine = create_engine() if engine is None else engine
|
|
253
259
|
|
|
254
260
|
if _is_interactive(in_stream, out_stream):
|
|
255
261
|
print("Context Compiler REPL (0.5). Type help for commands.", file=out_stream)
|
|
256
|
-
print("Non-directive input is
|
|
262
|
+
print("Non-directive input is no_directive.", file=out_stream)
|
|
257
263
|
|
|
258
264
|
while True:
|
|
259
265
|
line = in_stream.readline()
|
|
@@ -425,6 +431,8 @@ def run_repl(
|
|
|
425
431
|
|
|
426
432
|
|
|
427
433
|
def main() -> int: # pragma: no cover
|
|
434
|
+
"""Run the command-line entry point and return the process exit status."""
|
|
435
|
+
|
|
428
436
|
args = sys.argv[1:]
|
|
429
437
|
if not args:
|
|
430
438
|
run_repl(sys.stdin, sys.stdout)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: context-compiler
|
|
3
|
-
Version: 0.9.0.
|
|
3
|
+
Version: 0.9.0.dev4
|
|
4
4
|
Summary: Deterministic conversational state engine for LLM applications.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rlippmann/context-compiler
|
|
6
6
|
Project-URL: Repository, https://github.com/rlippmann/context-compiler
|
|
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
|
|
37
37
|
Context Compiler is a deterministic conversational state authority for LLM applications.
|
|
38
38
|
It handles canonical directive execution, semantic validation, deterministic
|
|
39
|
-
|
|
39
|
+
error decisions, runtime semantic continuation boundaries, and structured authoritative state for
|
|
40
40
|
the host.
|
|
41
41
|
|
|
42
42
|
## What Context Compiler provides
|
|
@@ -44,7 +44,7 @@ the host.
|
|
|
44
44
|
Context Compiler gives hosts fixed state rules:
|
|
45
45
|
|
|
46
46
|
- handle canonical explicit state changes with deterministic rules
|
|
47
|
-
-
|
|
47
|
+
- error instead of silent overwrite for blocked/ambiguous changes
|
|
48
48
|
- preserve supported pending continuation when explicit confirmation is required
|
|
49
49
|
- export and import authoritative state for host-managed persistence
|
|
50
50
|
- produce structured authoritative state for downstream host decisions
|
|
@@ -89,7 +89,7 @@ Context Compiler makes state-change rules explicit so behavior stays repeatable.
|
|
|
89
89
|
The architecture has three layers:
|
|
90
90
|
|
|
91
91
|
- syntax classification decides whether input is a canonical directive, invalid
|
|
92
|
-
directive-shaped syntax, or ordinary
|
|
92
|
+
directive-shaped syntax, or ordinary no_directive
|
|
93
93
|
- semantic evaluation decides whether a canonical directive updates state,
|
|
94
94
|
clarifies, or no-ops
|
|
95
95
|
- semantic continuation optionally preserves a deterministic blocked transition
|
|
@@ -123,7 +123,7 @@ use podman instead of docker
|
|
|
123
123
|
- Without explicit state transition rules: behavior depends on host/model handling
|
|
124
124
|
- Context Compiler: applies the deterministic resulting transition when
|
|
125
125
|
`docker` is absent and `use podman` is otherwise valid; other semantic
|
|
126
|
-
conflicts may still
|
|
126
|
+
conflicts may still error
|
|
127
127
|
|
|
128
128
|
### Lifecycle enforcement
|
|
129
129
|
|
|
@@ -133,7 +133,7 @@ change premise to formal tone
|
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
- Without explicit transition checks: behavior depends on host/model handling
|
|
136
|
-
- Context Compiler: asks for
|
|
136
|
+
- Context Compiler: asks for error and keeps saved state unchanged
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
@@ -150,9 +150,9 @@ Decision
|
|
|
150
150
|
│
|
|
151
151
|
▼
|
|
152
152
|
Host Application
|
|
153
|
-
├─
|
|
154
|
-
├─
|
|
155
|
-
└─ update → authoritative state mutated; host may
|
|
153
|
+
├─ error → ask user
|
|
154
|
+
├─ no_directive → no canonical directive recognized; host decides what to do next
|
|
155
|
+
└─ update → authoritative state mutated; host may use compiled state downstream
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
The compiler never calls the LLM. Your app decides what to do with the returned
|
|
@@ -167,8 +167,7 @@ Use Context Compiler in your host application first:
|
|
|
167
167
|
```python
|
|
168
168
|
from context_compiler import (
|
|
169
169
|
create_engine,
|
|
170
|
-
|
|
171
|
-
is_clarify,
|
|
170
|
+
is_error,
|
|
172
171
|
is_update,
|
|
173
172
|
)
|
|
174
173
|
|
|
@@ -177,8 +176,8 @@ engine = create_engine()
|
|
|
177
176
|
user_input = "set premise current project uses uv"
|
|
178
177
|
decision = engine.step(user_input)
|
|
179
178
|
|
|
180
|
-
if
|
|
181
|
-
show_to_user(
|
|
179
|
+
if is_error(decision):
|
|
180
|
+
show_to_user(decision["message"])
|
|
182
181
|
elif is_update(decision):
|
|
183
182
|
messages = build_messages(engine.state, user_input)
|
|
184
183
|
render(call_llm(messages))
|
|
@@ -277,22 +276,30 @@ uv run pytest
|
|
|
277
276
|
Each user message produces a `Decision`.
|
|
278
277
|
|
|
279
278
|
```python
|
|
280
|
-
class
|
|
281
|
-
kind: Literal["
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
class NoDirectiveDecision(TypedDict):
|
|
280
|
+
kind: Literal["no_directive"]
|
|
281
|
+
|
|
282
|
+
class UpdateDecision(TypedDict):
|
|
283
|
+
kind: Literal["update"]
|
|
284
|
+
state: State
|
|
285
|
+
|
|
286
|
+
class ErrorDecision(TypedDict):
|
|
287
|
+
kind: Literal["error"]
|
|
288
|
+
message: str
|
|
289
|
+
|
|
290
|
+
Decision = NoDirectiveDecision | UpdateDecision | ErrorDecision
|
|
284
291
|
```
|
|
285
292
|
|
|
286
293
|
Meaning:
|
|
287
294
|
|
|
288
295
|
| kind | host behavior |
|
|
289
296
|
| --- | --- |
|
|
290
|
-
|
|
|
291
|
-
| update | authoritative state mutated; host may
|
|
292
|
-
|
|
|
297
|
+
| no_directive | no canonical directive recognized; no authoritative state change; host decides what to do next |
|
|
298
|
+
| update | authoritative state mutated; host may use updated state downstream |
|
|
299
|
+
| error | show `message` and do not continue normal downstream processing yet |
|
|
293
300
|
|
|
294
|
-
For normal app code, prefer the exported decision helpers (`
|
|
295
|
-
`is_update`, `
|
|
301
|
+
For normal app code, prefer the exported decision helpers (`is_error`,
|
|
302
|
+
`is_update`, `is_no_directive`, `get_error_message`, `get_decision_state`)
|
|
296
303
|
instead of direct key traversal.
|
|
297
304
|
|
|
298
305
|
See [docs/api-reference.md](docs/api-reference.md) for the full public API
|
|
@@ -303,8 +310,8 @@ Common API entry points:
|
|
|
303
310
|
- engine lifecycle: `create_engine(...)`, `engine.step(...)`, `engine.state`,
|
|
304
311
|
`engine.premise`, `engine.policies`, `engine.export_json(...)`,
|
|
305
312
|
`engine.import_json(...)`
|
|
306
|
-
- decision helpers: `
|
|
307
|
-
`
|
|
313
|
+
- decision helpers: `is_error(...)`, `is_update(...)`, `is_no_directive(...)`,
|
|
314
|
+
`get_error_message(...)`, `get_decision_state(...)`
|
|
308
315
|
- state transport: `engine.export_json(...)`, `engine.import_json(...)`
|
|
309
316
|
- controller API: `step(...)`
|
|
310
317
|
- audit APIs: `preview(...)`, `state_diff(...)`
|
|
@@ -441,14 +448,14 @@ User: clear state
|
|
|
441
448
|
|
|
442
449
|
Grammar invariant: one input may contain at most one canonical directive.
|
|
443
450
|
Directive-shaped invalid input is outside the canonical language, and
|
|
444
|
-
`
|
|
451
|
+
`error` is reserved for canonical directives that later fail semantic
|
|
445
452
|
evaluation against authoritative state.
|
|
446
453
|
|
|
447
454
|
Pending continuation is a separate runtime layer. It may exist only after a
|
|
448
|
-
canonical directive reaches a supported semantic `
|
|
455
|
+
canonical directive reaches a supported semantic `error` case. It never
|
|
449
456
|
repairs malformed syntax or reinterprets non-canonical input as a directive.
|
|
450
457
|
An absent source item in a canonical replacement directive is not, by itself,
|
|
451
|
-
such a `
|
|
458
|
+
such a `error` case.
|
|
452
459
|
|
|
453
460
|
Examples:
|
|
454
461
|
|
|
@@ -496,7 +503,7 @@ boundary, see [DirectiveGrammarSpec.md](docs/DirectiveGrammarSpec.md).
|
|
|
496
503
|
**Isn't this just prompt reinjection?**
|
|
497
504
|
No. Prompt construction is one downstream use of authoritative state.
|
|
498
505
|
Context Compiler is the authority layer that decides when state changes are
|
|
499
|
-
allowed, when
|
|
506
|
+
allowed, when error is required, and how continuation state is
|
|
500
507
|
restored. For runnable application-layer examples, see
|
|
501
508
|
[`context-compiler-example-integrations`](https://github.com/rlippmann/context-compiler-example-integrations).
|
|
502
509
|
|
|
@@ -519,7 +526,7 @@ User: prohibit python_script
|
|
|
519
526
|
|
|
520
527
|
Without an authority layer, the application must invent conflict-resolution and
|
|
521
528
|
continuation rules itself. Context Compiler applies deterministic
|
|
522
|
-
state-transition rules and can return
|
|
529
|
+
state-transition rules and can return error instead of silently
|
|
523
530
|
overwriting state.
|
|
524
531
|
|
|
525
532
|
---
|
|
@@ -531,7 +538,7 @@ overwriting state.
|
|
|
531
538
|
- State changes only through explicit user directives or confirmation.
|
|
532
539
|
- Identical input sequences produce identical compiler state.
|
|
533
540
|
- Model responses never modify compiler state.
|
|
534
|
-
- Ambiguous directives trigger
|
|
541
|
+
- Ambiguous directives trigger error instead of changing state.
|
|
535
542
|
- Syntax errors never create pending continuation.
|
|
536
543
|
|
|
537
544
|
Behavioral tests and Hypothesis-based property tests verify these invariants.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
context_compiler/__init__.py,sha256=acFX-oUeMASVlReQrzmHIL04dy06kdeKaL8yEfE4RwY,1067
|
|
2
|
+
context_compiler/audit.py,sha256=AhDYy1G-xc4Ov9Jnjf_k11-hqWEp5BVIFztBJWbMpBM,476
|
|
3
|
+
context_compiler/const.py,sha256=WeEGDXY8lepFghH9wko0hH51oxoZQBfDnFxSyeIaaWI,465
|
|
4
|
+
context_compiler/controller.py,sha256=Op1-RTKltIhSDLwfhohmH3k7x4q5XdeE7ZtKPJIzqZo,5229
|
|
5
|
+
context_compiler/decision_helpers.py,sha256=1UFg3E3epSjuH_5ErXGigTTacKCs_IgTYV3kxBvdV-4,1302
|
|
6
|
+
context_compiler/engine.py,sha256=igGomL7NTELgdBCB1Z0LpD2H2nA2DC-7BY4-Rb-PxPU,14721
|
|
7
|
+
context_compiler/grammar.py,sha256=vWOK7Hp5_-X_2XfkhosPM4MlIyGZZ6EV3DtGsZUv-8A,16182
|
|
8
|
+
context_compiler/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
context_compiler/repl.py,sha256=OWEfdK8q0dmI3QXdYwxy_pl5gfxwNesqQgkPx3ne62g,17018
|
|
10
|
+
context_compiler-0.9.0.dev4.dist-info/METADATA,sha256=-XLo94NJg0qCC0PalXt92wfbhwPPH1lgPs06bjsHKk0,18346
|
|
11
|
+
context_compiler-0.9.0.dev4.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
12
|
+
context_compiler-0.9.0.dev4.dist-info/entry_points.txt,sha256=xnk4WE9zaC2NAMIF59WD3Y_45E8aA9KH-pZ4lqSzH-U,64
|
|
13
|
+
context_compiler-0.9.0.dev4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
14
|
+
context_compiler-0.9.0.dev4.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
context_compiler/__init__.py,sha256=2nVa-dMpdcT_7BfP6dTBoLWanBItTWBBQ--AXEgNA7I,977
|
|
2
|
-
context_compiler/audit.py,sha256=AhDYy1G-xc4Ov9Jnjf_k11-hqWEp5BVIFztBJWbMpBM,476
|
|
3
|
-
context_compiler/const.py,sha256=DgqVhkaNQ3hx2tToqHi9ZA6X6EYZj618vXacKxU-TDM,467
|
|
4
|
-
context_compiler/controller.py,sha256=U9n9MVjbrIqlrcpqMkDTwvt_WC-FzOE2CZqbwcOXjQg,3858
|
|
5
|
-
context_compiler/decision_helpers.py,sha256=RDl7TYeM7QywEFpWd3d4vw6fRnSfzgWXtgFFv044pe4,659
|
|
6
|
-
context_compiler/engine.py,sha256=june_5yvUF8oyWJQzoL3SINqwoY8BG-MPbjgqETwSMc,13004
|
|
7
|
-
context_compiler/grammar.py,sha256=PGBS5dZHx9GLHd-Emj-9TAna5PlN8MkaBMcr4HsVGLM,14165
|
|
8
|
-
context_compiler/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
context_compiler/repl.py,sha256=ZjjiKSj88KHKYJiaZ_YOkCncsOY8RYrHtvXUkdYVkgs,16549
|
|
10
|
-
context_compiler-0.9.0.dev2.dist-info/METADATA,sha256=Z2kBw2FJGEV2T9IOf3Va-JQv4Rf1nYdAfhnAp2qvnGY,18132
|
|
11
|
-
context_compiler-0.9.0.dev2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
12
|
-
context_compiler-0.9.0.dev2.dist-info/entry_points.txt,sha256=xnk4WE9zaC2NAMIF59WD3Y_45E8aA9KH-pZ4lqSzH-U,64
|
|
13
|
-
context_compiler-0.9.0.dev2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
14
|
-
context_compiler-0.9.0.dev2.dist-info/RECORD,,
|
|
File without changes
|
{context_compiler-0.9.0.dev2.dist-info → context_compiler-0.9.0.dev4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev2.dist-info → context_compiler-0.9.0.dev4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|