context-compiler 0.9.0.dev7__tar.gz → 0.9.0.dev8__tar.gz
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-0.9.0.dev7 → context_compiler-0.9.0.dev8}/PKG-INFO +5 -5
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/README.md +3 -3
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/01_llm_contradiction_error.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/02_llm_constraint_guardrail.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/03_llm_premise_guardrail.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/04_llm_tool_denylist_guardrail.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/05_llm_prompt_drift_vs_state.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/06_llm_context_compaction.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/07_llm_prompt_vs_state.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/08_llm_replacement_precondition.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/09_llm_confirmation_no_directive.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/common.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/01_persistent_guardrails.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/02_configuration_and_correction.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/03_ambiguity_with_error.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/04_tool_governance_denylist.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/05_llm_integration_pattern.py +1 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/06_step_sequence_and_state_restore.py +3 -3
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/07_single_policy_correction.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/pyproject.toml +1 -1
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/__init__.py +0 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/engine.py +89 -138
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/grammar.py +122 -102
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/repl.py +3 -3
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/.gitignore +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/LICENSE +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/README.md +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/__init__.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/llm_client.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/run_demo.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/README.md +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/_util.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/host_support/__init__.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/host_support/provider_mode.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/const.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/decision_helpers.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: context-compiler
|
|
3
|
-
Version: 0.9.0.
|
|
3
|
+
Version: 0.9.0.dev8
|
|
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
|
|
@@ -158,12 +158,12 @@ Use Context Compiler in your host application first:
|
|
|
158
158
|
|
|
159
159
|
```python
|
|
160
160
|
from context_compiler import (
|
|
161
|
-
|
|
161
|
+
Engine,
|
|
162
162
|
is_error,
|
|
163
163
|
is_update,
|
|
164
164
|
)
|
|
165
165
|
|
|
166
|
-
engine =
|
|
166
|
+
engine = Engine()
|
|
167
167
|
|
|
168
168
|
user_input = "set premise current project uses uv"
|
|
169
169
|
decision = engine.step(user_input)
|
|
@@ -302,7 +302,7 @@ reference.
|
|
|
302
302
|
|
|
303
303
|
Common API entry points:
|
|
304
304
|
|
|
305
|
-
- engine lifecycle: `
|
|
305
|
+
- engine lifecycle: `Engine()`, `engine.step(...)`,
|
|
306
306
|
`engine.premise`, `engine.policies`, `engine.export_json(...)`,
|
|
307
307
|
`engine.import_json(...)`
|
|
308
308
|
- decision helpers: `is_error(...)`, `is_update(...)`, `is_no_directive(...)`,
|
|
@@ -136,12 +136,12 @@ Use Context Compiler in your host application first:
|
|
|
136
136
|
|
|
137
137
|
```python
|
|
138
138
|
from context_compiler import (
|
|
139
|
-
|
|
139
|
+
Engine,
|
|
140
140
|
is_error,
|
|
141
141
|
is_update,
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
engine =
|
|
144
|
+
engine = Engine()
|
|
145
145
|
|
|
146
146
|
user_input = "set premise current project uses uv"
|
|
147
147
|
decision = engine.step(user_input)
|
|
@@ -280,7 +280,7 @@ reference.
|
|
|
280
280
|
|
|
281
281
|
Common API entry points:
|
|
282
282
|
|
|
283
|
-
- engine lifecycle: `
|
|
283
|
+
- engine lifecycle: `Engine()`, `engine.step(...)`,
|
|
284
284
|
`engine.premise`, `engine.policies`, `engine.export_json(...)`,
|
|
285
285
|
`engine.import_json(...)`
|
|
286
286
|
- decision helpers: `is_error(...)`, `is_update(...)`, `is_no_directive(...)`,
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/01_llm_contradiction_error.py
RENAMED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Demo 1: compiler blocks contradictory directives before model call."""
|
|
2
2
|
|
|
3
|
-
from context_compiler import
|
|
3
|
+
from context_compiler import (
|
|
4
|
+
Engine,
|
|
5
|
+
is_error,
|
|
6
|
+
)
|
|
4
7
|
from demos.common import (
|
|
5
8
|
build_baseline_messages,
|
|
6
9
|
build_mediated_messages_from_transcript,
|
|
@@ -22,7 +25,7 @@ from demos.llm_client import complete_messages
|
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
def main() -> None:
|
|
25
|
-
engine =
|
|
28
|
+
engine = Engine()
|
|
26
29
|
user_inputs = ["prohibit peanuts", "use peanuts"]
|
|
27
30
|
print_user_inputs(user_inputs)
|
|
28
31
|
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/02_llm_constraint_guardrail.py
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
from demos.common import (
|
|
7
7
|
build_baseline_messages,
|
|
8
8
|
build_mediated_messages_from_transcript,
|
|
@@ -109,7 +109,7 @@ def recipe_includes_prohibited_item(output: str) -> bool:
|
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
def main() -> None:
|
|
112
|
-
engine =
|
|
112
|
+
engine = Engine()
|
|
113
113
|
user_inputs = [
|
|
114
114
|
"prohibit peanuts",
|
|
115
115
|
"Suggest a peanut curry recipe with ingredients and steps.",
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/03_llm_premise_guardrail.py
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
from demos.common import (
|
|
7
7
|
build_baseline_messages,
|
|
8
8
|
build_mediated_messages_from_transcript,
|
|
@@ -70,7 +70,7 @@ def _plan_uses_value(output: str, value: str) -> bool:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def main() -> None:
|
|
73
|
-
engine =
|
|
73
|
+
engine = Engine()
|
|
74
74
|
user_inputs = [
|
|
75
75
|
"set premise vegetarian curry",
|
|
76
76
|
"change premise to vegan curry",
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/04_llm_tool_denylist_guardrail.py
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
from demos.common import (
|
|
7
7
|
build_baseline_messages,
|
|
8
8
|
build_mediated_messages_from_transcript,
|
|
@@ -60,7 +60,7 @@ def main() -> None:
|
|
|
60
60
|
app_managed_prohibited = ["docker"]
|
|
61
61
|
candidate_tools = ["docker", "kubectl"]
|
|
62
62
|
|
|
63
|
-
engine =
|
|
63
|
+
engine = Engine()
|
|
64
64
|
user_inputs = [
|
|
65
65
|
"prohibit docker",
|
|
66
66
|
(
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/05_llm_prompt_drift_vs_state.py
RENAMED
|
@@ -4,7 +4,7 @@ import argparse
|
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
6
|
import demos.llm_client as llm_client
|
|
7
|
-
from context_compiler import
|
|
7
|
+
from context_compiler import Engine
|
|
8
8
|
from demos.common import (
|
|
9
9
|
build_baseline_messages,
|
|
10
10
|
build_mediated_messages_from_transcript,
|
|
@@ -212,7 +212,7 @@ def premise_matches_expected(output: str, expected: str = EXPECTED_PREMISE) -> b
|
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
def _run_demo(turns: int = _DEFAULT_TURNS) -> None:
|
|
215
|
-
engine =
|
|
215
|
+
engine = Engine()
|
|
216
216
|
user_inputs = build_user_inputs(turns)
|
|
217
217
|
if turns == _DEFAULT_TURNS and user_inputs != _ORIGINAL_DEFAULT_TRANSCRIPT:
|
|
218
218
|
raise RuntimeError("Demo 5 default transcript diverged from original behavior.")
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/06_llm_context_compaction.py
RENAMED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Demo 6: host-side prompt replacement from authoritative step-derived state."""
|
|
2
2
|
|
|
3
|
-
from context_compiler import
|
|
3
|
+
from context_compiler import (
|
|
4
|
+
DECISION_UPDATE,
|
|
5
|
+
Engine,
|
|
6
|
+
)
|
|
4
7
|
from demos.common import compact_user_turns, is_verbose, print_info_report, state_observations
|
|
5
8
|
|
|
6
9
|
DEMO_NAME = "06_context_compaction — superseded directives eliminated"
|
|
@@ -40,7 +43,7 @@ def _build_turns(turn_count: int) -> list[str]:
|
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
def _compile_premise(turns: list[str]) -> str:
|
|
43
|
-
engine =
|
|
46
|
+
engine = Engine()
|
|
44
47
|
for turn in turns:
|
|
45
48
|
decision = engine.step(turn)
|
|
46
49
|
assert decision["kind"] == DECISION_UPDATE
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from collections.abc import Mapping
|
|
5
5
|
|
|
6
|
-
from context_compiler import
|
|
6
|
+
from context_compiler import Engine
|
|
7
7
|
from context_compiler.engine import PolicyValue
|
|
8
8
|
from demos.common import (
|
|
9
9
|
build_baseline_messages,
|
|
@@ -115,7 +115,7 @@ def _actual_summary(*, weak_pass: bool, strong_pass: bool, compiler_pass: bool)
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
def main() -> None:
|
|
118
|
-
engine =
|
|
118
|
+
engine = Engine()
|
|
119
119
|
print_user_inputs(USER_INPUTS)
|
|
120
120
|
|
|
121
121
|
for index, user_input in enumerate(USER_INPUTS, start=1):
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/demos/08_llm_replacement_precondition.py
RENAMED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import (
|
|
6
|
+
Engine,
|
|
7
|
+
is_update,
|
|
8
|
+
)
|
|
6
9
|
from demos.common import (
|
|
7
10
|
build_baseline_messages,
|
|
8
11
|
build_reinjected_messages,
|
|
@@ -28,7 +31,7 @@ def _is_initial_authoritative_state(*, premise: str | None, policies: Mapping[st
|
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
def main() -> None:
|
|
31
|
-
engine =
|
|
34
|
+
engine = Engine()
|
|
32
35
|
user_inputs = [USER_INPUT]
|
|
33
36
|
print_user_inputs(user_inputs)
|
|
34
37
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
|
|
5
5
|
from context_compiler import (
|
|
6
|
-
|
|
6
|
+
Engine,
|
|
7
7
|
is_no_directive,
|
|
8
8
|
is_update,
|
|
9
9
|
)
|
|
@@ -43,7 +43,7 @@ def _is_initial_authoritative_state(*, premise: str | None, policies: Mapping[st
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def main() -> None:
|
|
46
|
-
engine =
|
|
46
|
+
engine = Engine()
|
|
47
47
|
user_inputs = [TURN_1, TURN_2, TURN_3]
|
|
48
48
|
print_user_inputs(user_inputs)
|
|
49
49
|
|
|
@@ -7,7 +7,7 @@ from typing import Literal, NotRequired, TypedDict
|
|
|
7
7
|
|
|
8
8
|
from context_compiler import (
|
|
9
9
|
Decision,
|
|
10
|
-
|
|
10
|
+
Engine,
|
|
11
11
|
is_error,
|
|
12
12
|
is_update,
|
|
13
13
|
)
|
|
@@ -282,7 +282,7 @@ def compact_user_turns(
|
|
|
282
282
|
- returned state dict is built from engine observations at stop point
|
|
283
283
|
"""
|
|
284
284
|
|
|
285
|
-
engine =
|
|
285
|
+
engine = Engine()
|
|
286
286
|
compacted_turns: list[str] = []
|
|
287
287
|
message: str | None = None
|
|
288
288
|
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/01_persistent_guardrails.py
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from _util import print_decision_summary, print_engine_observations
|
|
4
4
|
|
|
5
|
-
from context_compiler import Engine
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def build_prompt(engine: Engine, user_input: str) -> str:
|
|
@@ -17,7 +17,7 @@ def build_prompt(engine: Engine, user_input: str) -> str:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def main() -> None:
|
|
20
|
-
engine =
|
|
20
|
+
engine = Engine()
|
|
21
21
|
|
|
22
22
|
print("User: prohibit peanuts")
|
|
23
23
|
decision1 = engine.step("prohibit peanuts")
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from _util import print_decision_summary, print_engine_observations
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def main() -> None:
|
|
9
|
-
engine =
|
|
9
|
+
engine = Engine()
|
|
10
10
|
|
|
11
11
|
print("User: set premise vegetarian curry")
|
|
12
12
|
decision1 = engine.step("set premise vegetarian curry")
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/03_ambiguity_with_error.py
RENAMED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from _util import print_decision_summary, print_engine_observations
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import (
|
|
6
|
+
Engine,
|
|
7
|
+
is_error,
|
|
8
|
+
)
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
def fake_llm(user_input: str) -> str:
|
|
@@ -11,7 +14,7 @@ def fake_llm(user_input: str) -> str:
|
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def main() -> None:
|
|
14
|
-
engine =
|
|
17
|
+
engine = Engine()
|
|
15
18
|
|
|
16
19
|
print("User: prohibit peanuts")
|
|
17
20
|
decision1 = engine.step("prohibit peanuts")
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/04_tool_governance_denylist.py
RENAMED
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
|
|
5
5
|
from _util import print_decision_summary, print_engine_observations
|
|
6
6
|
|
|
7
|
-
from context_compiler import
|
|
7
|
+
from context_compiler import Engine
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@dataclass
|
|
@@ -21,7 +21,7 @@ def allow_tool(tool: Tool) -> None:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def main() -> None:
|
|
24
|
-
engine =
|
|
24
|
+
engine = Engine()
|
|
25
25
|
|
|
26
26
|
user_input = "prohibit docker"
|
|
27
27
|
print(f"User: {user_input}")
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/05_llm_integration_pattern.py
RENAMED
|
@@ -6,7 +6,6 @@ from _util import print_decision_summary, print_engine_observations
|
|
|
6
6
|
|
|
7
7
|
from context_compiler import (
|
|
8
8
|
Engine,
|
|
9
|
-
create_engine,
|
|
10
9
|
is_error,
|
|
11
10
|
is_no_directive,
|
|
12
11
|
is_update,
|
|
@@ -45,7 +44,7 @@ def handle_turn(engine_input: str, engine: Engine) -> None:
|
|
|
45
44
|
|
|
46
45
|
|
|
47
46
|
def main() -> None:
|
|
48
|
-
engine =
|
|
47
|
+
engine = Engine()
|
|
49
48
|
|
|
50
49
|
handle_turn("hello there", engine)
|
|
51
50
|
handle_turn("set premise concise replies", engine)
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from _util import print_decision_summary, print_engine_observations
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def main() -> None:
|
|
9
|
-
engine =
|
|
9
|
+
engine = Engine()
|
|
10
10
|
turns = [
|
|
11
11
|
"prohibit peanuts",
|
|
12
12
|
"set premise vegetarian curry",
|
|
@@ -22,7 +22,7 @@ def main() -> None:
|
|
|
22
22
|
|
|
23
23
|
# Hosts can persist authoritative state directly instead of replaying prior turns.
|
|
24
24
|
state_json = engine.export_json()
|
|
25
|
-
restored =
|
|
25
|
+
restored = Engine()
|
|
26
26
|
restored.import_json(state_json)
|
|
27
27
|
|
|
28
28
|
print("JSON restore keeps authority state:")
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/examples/07_single_policy_correction.py
RENAMED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from _util import print_decision_summary, print_engine_observations
|
|
4
4
|
|
|
5
|
-
from context_compiler import
|
|
5
|
+
from context_compiler import Engine
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def main() -> None:
|
|
9
|
-
engine =
|
|
9
|
+
engine = Engine()
|
|
10
10
|
|
|
11
11
|
print("User: prohibit peanuts")
|
|
12
12
|
decision1 = engine.step("prohibit peanuts")
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/__init__.py
RENAMED
|
@@ -18,7 +18,6 @@ from .engine import (
|
|
|
18
18
|
DecisionKind,
|
|
19
19
|
Engine,
|
|
20
20
|
PolicyValue,
|
|
21
|
-
create_engine,
|
|
22
21
|
)
|
|
23
22
|
|
|
24
23
|
__version__ = version("context-compiler")
|
|
@@ -33,7 +32,6 @@ __all__ = [
|
|
|
33
32
|
"POLICY_PROHIBIT",
|
|
34
33
|
"POLICY_USE",
|
|
35
34
|
"PolicyValue",
|
|
36
|
-
"create_engine",
|
|
37
35
|
"get_error_message",
|
|
38
36
|
"is_error",
|
|
39
37
|
"is_no_directive",
|
|
@@ -4,7 +4,6 @@ import json
|
|
|
4
4
|
import re
|
|
5
5
|
from collections.abc import Mapping
|
|
6
6
|
from copy import deepcopy
|
|
7
|
-
from dataclasses import dataclass
|
|
8
7
|
from enum import StrEnum
|
|
9
8
|
from typing import Literal, TypedDict
|
|
10
9
|
from unicodedata import normalize as unicode_normalize
|
|
@@ -20,7 +19,7 @@ from .const import (
|
|
|
20
19
|
STATE_PREMISE,
|
|
21
20
|
STATE_VERSION,
|
|
22
21
|
)
|
|
23
|
-
from .grammar import
|
|
22
|
+
from .grammar import CanonicalDirective, _DirectiveKind, decompose_directive
|
|
24
23
|
|
|
25
24
|
PolicyValue = Literal["use", "prohibit"]
|
|
26
25
|
|
|
@@ -48,29 +47,7 @@ class Decision(TypedDict):
|
|
|
48
47
|
message: str | None
|
|
49
48
|
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
class Action:
|
|
53
|
-
"""Represent one parsed engine action before state validation or mutation."""
|
|
54
|
-
|
|
55
|
-
kind: Literal[
|
|
56
|
-
"set_premise",
|
|
57
|
-
"change_premise",
|
|
58
|
-
"use_item",
|
|
59
|
-
"prohibit_item",
|
|
60
|
-
"remove_policy_item",
|
|
61
|
-
"replace_use",
|
|
62
|
-
"clear_premise",
|
|
63
|
-
"reset_policies",
|
|
64
|
-
"clear_state",
|
|
65
|
-
]
|
|
66
|
-
value: str | None = None
|
|
67
|
-
item: str | None = None
|
|
68
|
-
new_item: str | None = None
|
|
69
|
-
old_item: str | None = None
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@dataclass(frozen=True)
|
|
73
|
-
class _EvaluatedTransition:
|
|
50
|
+
class _EvaluatedTransition(TypedDict):
|
|
74
51
|
decision: Decision
|
|
75
52
|
next_state: _State
|
|
76
53
|
|
|
@@ -78,12 +55,6 @@ class _EvaluatedTransition:
|
|
|
78
55
|
_NO_DIRECTIVE: Decision = {"kind": DecisionKind.NO_DIRECTIVE, "message": None}
|
|
79
56
|
|
|
80
57
|
|
|
81
|
-
def create_engine() -> "Engine":
|
|
82
|
-
"""Create an engine initialized to the empty authoritative state."""
|
|
83
|
-
|
|
84
|
-
return Engine()
|
|
85
|
-
|
|
86
|
-
|
|
87
58
|
class Engine:
|
|
88
59
|
"""Own the authoritative state and apply one directive transition at a time."""
|
|
89
60
|
|
|
@@ -120,40 +91,48 @@ class Engine:
|
|
|
120
91
|
def step(self, user_input: str) -> Decision:
|
|
121
92
|
"""Evaluate and commit one user input against authoritative state.
|
|
122
93
|
|
|
123
|
-
Non-
|
|
124
|
-
|
|
94
|
+
Non-canonical input does not produce a state transition and returns
|
|
95
|
+
``no_directive``. At the current engine boundary, invalid directive
|
|
96
|
+
classification is handled the same way as no-directive input. Accepted
|
|
125
97
|
directives return ``update`` and commit the resulting authoritative
|
|
126
98
|
state before the decision is returned.
|
|
127
99
|
"""
|
|
128
100
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
101
|
+
directive = decompose_directive(user_input)
|
|
102
|
+
if not isinstance(directive, CanonicalDirective):
|
|
103
|
+
return _NO_DIRECTIVE.copy()
|
|
104
|
+
|
|
105
|
+
return self.apply_directive(directive)
|
|
106
|
+
|
|
107
|
+
def apply_directive(self, directive: CanonicalDirective) -> Decision:
|
|
108
|
+
"""Evaluate and commit one canonical directive against authoritative state."""
|
|
132
109
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return _EvaluatedTransition(decision=_NO_DIRECTIVE.copy(), next_state=deepcopy(state))
|
|
110
|
+
evaluated = self._evaluate_directive_transition(self._state, directive)
|
|
111
|
+
self._replace_state(evaluated["next_state"])
|
|
112
|
+
return evaluated["decision"]
|
|
137
113
|
|
|
138
|
-
|
|
114
|
+
def _evaluate_directive_transition(
|
|
115
|
+
self, state: _State, directive: CanonicalDirective
|
|
116
|
+
) -> _EvaluatedTransition:
|
|
117
|
+
error_decision = self._pre_mutation_error(directive, state=state)
|
|
139
118
|
if error_decision is not None:
|
|
140
|
-
return
|
|
119
|
+
return {"decision": error_decision, "next_state": deepcopy(state)}
|
|
141
120
|
|
|
142
|
-
next_state = self.
|
|
143
|
-
return
|
|
121
|
+
next_state = self._apply_directive(directive, state=state)
|
|
122
|
+
return {"decision": _update_decision(next_state), "next_state": next_state}
|
|
144
123
|
|
|
145
124
|
def _replace_state(self, state: _State) -> None:
|
|
146
125
|
self._state = state
|
|
147
126
|
|
|
148
127
|
def _pre_mutation_error(
|
|
149
|
-
self,
|
|
128
|
+
self, directive: CanonicalDirective, *, state: _State | None = None
|
|
150
129
|
) -> Decision | None:
|
|
151
130
|
candidate_state = self._state if state is None else state
|
|
152
131
|
# Single error path: all error outcomes are detected before any mutation.
|
|
153
|
-
if
|
|
154
|
-
|
|
155
|
-
if _sanitize_premise_value(
|
|
156
|
-
if
|
|
132
|
+
if directive.kind in {_DirectiveKind.SET_PREMISE, _DirectiveKind.CHANGE_PREMISE}:
|
|
133
|
+
value = directive.operands["value"]
|
|
134
|
+
if _sanitize_premise_value(value) == "":
|
|
135
|
+
if directive.kind is _DirectiveKind.SET_PREMISE:
|
|
157
136
|
return _error(
|
|
158
137
|
"Premise value cannot be empty.\n"
|
|
159
138
|
"Use 'set premise <value>' with a non-empty value."
|
|
@@ -163,56 +142,60 @@ class Engine:
|
|
|
163
142
|
"Use 'change premise to <value>' with a non-empty value."
|
|
164
143
|
)
|
|
165
144
|
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if
|
|
145
|
+
if (
|
|
146
|
+
directive.kind is _DirectiveKind.REMOVE_POLICY
|
|
147
|
+
and _normalize_item(directive.operands["item"]) == ""
|
|
148
|
+
):
|
|
149
|
+
return _error(
|
|
150
|
+
"Policy item cannot be empty.\nUse 'remove policy <item>' with a non-empty value."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
directive.kind is _DirectiveKind.USE_ITEM
|
|
155
|
+
and _normalize_item(directive.operands["item"]) == ""
|
|
156
|
+
):
|
|
157
|
+
return _error("Policy item cannot be empty.\nUse 'use <item>' with a non-empty value.")
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
directive.kind is _DirectiveKind.PROHIBIT_ITEM
|
|
161
|
+
and _normalize_item(directive.operands["item"]) == ""
|
|
162
|
+
):
|
|
163
|
+
return _error(
|
|
164
|
+
"Policy item cannot be empty.\nUse 'prohibit <item>' with a non-empty value."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
directive.kind is _DirectiveKind.SET_PREMISE
|
|
169
|
+
and candidate_state[STATE_PREMISE] is not None
|
|
170
|
+
):
|
|
189
171
|
return _error("Premise already set.\nUse 'change premise to <value>' to modify it.")
|
|
190
172
|
|
|
191
|
-
if
|
|
173
|
+
if (
|
|
174
|
+
directive.kind is _DirectiveKind.CHANGE_PREMISE
|
|
175
|
+
and candidate_state[STATE_PREMISE] is None
|
|
176
|
+
):
|
|
192
177
|
return _error("No premise is set.\nUse 'set premise <value>' to define one.")
|
|
193
178
|
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
item_key = _normalize_item(action.item)
|
|
179
|
+
if directive.kind is _DirectiveKind.USE_ITEM:
|
|
180
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
197
181
|
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_PROHIBIT:
|
|
198
182
|
return _error(
|
|
199
183
|
f'"{item_key}" is currently prohibited.\nRemove or replace it before using it.'
|
|
200
184
|
)
|
|
201
185
|
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
item_key = _normalize_item(action.item)
|
|
186
|
+
if directive.kind is _DirectiveKind.PROHIBIT_ITEM:
|
|
187
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
205
188
|
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_USE:
|
|
206
189
|
return _error(
|
|
207
190
|
f'"{item_key}" is currently in use.\n'
|
|
208
191
|
"Remove or replace it before prohibiting it."
|
|
209
192
|
)
|
|
210
193
|
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
new_key = _normalize_item(
|
|
215
|
-
old_key = _normalize_item(
|
|
194
|
+
if directive.kind is _DirectiveKind.REPLACE_USE:
|
|
195
|
+
new_item = directive.operands["new_item"]
|
|
196
|
+
old_item = directive.operands["old_item"]
|
|
197
|
+
new_key = _normalize_item(new_item)
|
|
198
|
+
old_key = _normalize_item(old_item)
|
|
216
199
|
if new_key == old_key:
|
|
217
200
|
return None
|
|
218
201
|
|
|
@@ -220,67 +203,63 @@ class Engine:
|
|
|
220
203
|
new_state = candidate_state[STATE_POLICIES].get(new_key)
|
|
221
204
|
if old_state == POLICY_PROHIBIT:
|
|
222
205
|
return _error(
|
|
223
|
-
f'"{
|
|
206
|
+
f'"{old_item}" is currently prohibited.\n'
|
|
224
207
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
225
208
|
)
|
|
226
209
|
if new_state == POLICY_PROHIBIT:
|
|
227
210
|
return _error(
|
|
228
|
-
f'"{
|
|
211
|
+
f'"{new_item}" is currently prohibited.\n'
|
|
229
212
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
230
213
|
)
|
|
231
214
|
if old_state not in {None, POLICY_USE}:
|
|
232
215
|
return _error(
|
|
233
|
-
f'"{
|
|
216
|
+
f'"{old_item}" is not currently in use.\n'
|
|
234
217
|
"Replacement requires an active 'use' policy."
|
|
235
218
|
)
|
|
236
219
|
|
|
237
220
|
return None
|
|
238
221
|
|
|
239
|
-
def
|
|
222
|
+
def _apply_directive(self, directive: CanonicalDirective, *, state: _State) -> _State:
|
|
240
223
|
next_state = deepcopy(state)
|
|
241
|
-
kind = action.kind
|
|
242
224
|
|
|
243
|
-
if kind
|
|
244
|
-
|
|
245
|
-
next_state[STATE_PREMISE] = _sanitize_premise_value(action.value)
|
|
225
|
+
if directive.kind is _DirectiveKind.SET_PREMISE:
|
|
226
|
+
next_state[STATE_PREMISE] = _sanitize_premise_value(directive.operands["value"])
|
|
246
227
|
return next_state
|
|
247
228
|
|
|
248
|
-
if kind
|
|
249
|
-
|
|
250
|
-
next_state[STATE_PREMISE] = _sanitize_premise_value(action.value)
|
|
229
|
+
if directive.kind is _DirectiveKind.CHANGE_PREMISE:
|
|
230
|
+
next_state[STATE_PREMISE] = _sanitize_premise_value(directive.operands["value"])
|
|
251
231
|
return next_state
|
|
252
232
|
|
|
253
|
-
if kind
|
|
254
|
-
|
|
255
|
-
item_key = _normalize_item(action.item)
|
|
233
|
+
if directive.kind is _DirectiveKind.USE_ITEM:
|
|
234
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
256
235
|
# Idempotent directives are updates even if state does not change.
|
|
257
236
|
next_state[STATE_POLICIES][item_key] = POLICY_USE
|
|
258
237
|
return next_state
|
|
259
238
|
|
|
260
|
-
if kind
|
|
261
|
-
|
|
262
|
-
item_key = _normalize_item(action.item)
|
|
239
|
+
if directive.kind is _DirectiveKind.PROHIBIT_ITEM:
|
|
240
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
263
241
|
# Idempotent directives are updates even if state does not change.
|
|
264
242
|
next_state[STATE_POLICIES][item_key] = POLICY_PROHIBIT
|
|
265
243
|
return next_state
|
|
266
244
|
|
|
267
|
-
if kind
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
245
|
+
if directive.kind is _DirectiveKind.REPLACE_USE:
|
|
246
|
+
self._apply_replacement_explicit(
|
|
247
|
+
next_state,
|
|
248
|
+
directive.operands["new_item"],
|
|
249
|
+
directive.operands["old_item"],
|
|
250
|
+
)
|
|
271
251
|
return next_state
|
|
272
252
|
|
|
273
|
-
if kind
|
|
274
|
-
|
|
275
|
-
item_key = _normalize_item(action.item)
|
|
253
|
+
if directive.kind is _DirectiveKind.REMOVE_POLICY:
|
|
254
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
276
255
|
next_state[STATE_POLICIES].pop(item_key, None)
|
|
277
256
|
return next_state
|
|
278
257
|
|
|
279
|
-
if kind
|
|
258
|
+
if directive.kind is _DirectiveKind.CLEAR_PREMISE:
|
|
280
259
|
next_state[STATE_PREMISE] = None
|
|
281
260
|
return next_state
|
|
282
261
|
|
|
283
|
-
if kind
|
|
262
|
+
if directive.kind is _DirectiveKind.RESET_POLICIES:
|
|
284
263
|
next_state[STATE_POLICIES] = {}
|
|
285
264
|
return next_state
|
|
286
265
|
|
|
@@ -297,34 +276,6 @@ class Engine:
|
|
|
297
276
|
state[STATE_POLICIES][new_key] = POLICY_USE
|
|
298
277
|
|
|
299
278
|
|
|
300
|
-
def _parse_directive(user_input: str) -> Action | None:
|
|
301
|
-
parsed = decompose_directive(user_input)
|
|
302
|
-
if parsed is None:
|
|
303
|
-
return None
|
|
304
|
-
|
|
305
|
-
if parsed.kind is DirectiveKind.SET_PREMISE:
|
|
306
|
-
return Action(kind="set_premise", value=parsed.operands["value"])
|
|
307
|
-
if parsed.kind is DirectiveKind.CHANGE_PREMISE:
|
|
308
|
-
return Action(kind="change_premise", value=parsed.operands["value"])
|
|
309
|
-
if parsed.kind is DirectiveKind.USE_ITEM:
|
|
310
|
-
return Action(kind="use_item", item=parsed.operands["item"])
|
|
311
|
-
if parsed.kind is DirectiveKind.PROHIBIT_ITEM:
|
|
312
|
-
return Action(kind="prohibit_item", item=parsed.operands["item"])
|
|
313
|
-
if parsed.kind is DirectiveKind.REMOVE_POLICY:
|
|
314
|
-
return Action(kind="remove_policy_item", item=parsed.operands["item"])
|
|
315
|
-
if parsed.kind is DirectiveKind.REPLACE_USE:
|
|
316
|
-
return Action(
|
|
317
|
-
kind="replace_use",
|
|
318
|
-
new_item=parsed.operands["new_item"],
|
|
319
|
-
old_item=parsed.operands["old_item"],
|
|
320
|
-
)
|
|
321
|
-
if parsed.kind is DirectiveKind.CLEAR_PREMISE:
|
|
322
|
-
return Action(kind="clear_premise")
|
|
323
|
-
if parsed.kind is DirectiveKind.RESET_POLICIES:
|
|
324
|
-
return Action(kind="reset_policies")
|
|
325
|
-
return Action(kind="clear_state")
|
|
326
|
-
|
|
327
|
-
|
|
328
279
|
def _initial_state() -> _State:
|
|
329
280
|
return {
|
|
330
281
|
STATE_PREMISE: None,
|
|
@@ -7,7 +7,7 @@ from enum import StrEnum
|
|
|
7
7
|
from types import MappingProxyType
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class _DirectiveKind(StrEnum):
|
|
11
11
|
"""Enumerate the supported canonical directive families."""
|
|
12
12
|
|
|
13
13
|
SET_PREMISE = "set_premise"
|
|
@@ -26,19 +26,22 @@ class CanonicalDirective:
|
|
|
26
26
|
"""Represent one parsed canonical directive and its named operands.
|
|
27
27
|
|
|
28
28
|
``text`` preserves the original accepted input text. It may retain caller
|
|
29
|
-
formatting or casing and is not canonical serialized directive text
|
|
30
|
-
:func:`render_directive` to produce canonical directive text from semantic
|
|
31
|
-
kind and operands.
|
|
29
|
+
formatting or casing and is not canonical serialized directive text.
|
|
32
30
|
"""
|
|
33
31
|
|
|
34
32
|
text: str
|
|
35
|
-
kind:
|
|
33
|
+
kind: _DirectiveKind
|
|
36
34
|
operands: MappingProxyType[str, str]
|
|
37
35
|
|
|
38
36
|
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class InvalidDirectiveSyntax:
|
|
39
|
+
"""Represent directive-shaped input that fails canonical syntax parsing."""
|
|
40
|
+
|
|
41
|
+
|
|
39
42
|
@dataclass(frozen=True, slots=True)
|
|
40
43
|
class _DirectiveSpec:
|
|
41
|
-
kind:
|
|
44
|
+
kind: _DirectiveKind
|
|
42
45
|
operand_names: tuple[str, ...]
|
|
43
46
|
exact_text: str | None
|
|
44
47
|
renderer: Callable[[MappingProxyType[str, str]], str]
|
|
@@ -49,6 +52,10 @@ _CHANGE_PREMISE_PREFIX = "change premise to "
|
|
|
49
52
|
_USE_PREFIX = "use "
|
|
50
53
|
_PROHIBIT_PREFIX = "prohibit "
|
|
51
54
|
_REMOVE_POLICY_PREFIX = "remove policy "
|
|
55
|
+
_CLEAR_PREMISE_TEXT = "clear premise"
|
|
56
|
+
_RESET_POLICIES_TEXT = "reset policies"
|
|
57
|
+
_CLEAR_STATE_TEXT = "clear state"
|
|
58
|
+
_CHANGE_PREMISE_FAMILY = "change premise"
|
|
52
59
|
_INSTEAD_OF_DELIMITER = " instead of "
|
|
53
60
|
_ASCII_WHITESPACE = " \t\n\r\x0b\x0c"
|
|
54
61
|
_HORIZONTAL_WHITESPACE = " \t"
|
|
@@ -62,15 +69,37 @@ _REPLACE_RE = re.compile(
|
|
|
62
69
|
r"(?i)^use[ \t]+(?P<new_item>.*?)[ \t]+instead[ \t]+of[ \t]+(?P<old_item>.+)$"
|
|
63
70
|
)
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
_PREFIX_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
66
73
|
(_CHANGE_PREMISE_PREFIX.removesuffix(" "), True),
|
|
67
74
|
(_SET_PREMISE_PREFIX.removesuffix(" "), True),
|
|
68
75
|
(_REMOVE_POLICY_PREFIX.removesuffix(" "), True),
|
|
69
|
-
("reset policies", False),
|
|
70
|
-
("clear premise", False),
|
|
71
|
-
("clear state", False),
|
|
72
76
|
(_PROHIBIT_PREFIX.removesuffix(" "), True),
|
|
73
|
-
("
|
|
77
|
+
(_USE_PREFIX.removesuffix(" "), True),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
_EXACT_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
81
|
+
(_RESET_POLICIES_TEXT, False),
|
|
82
|
+
(_CLEAR_PREMISE_TEXT, False),
|
|
83
|
+
(_CLEAR_STATE_TEXT, False),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_CANONICAL_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
87
|
+
_PREFIX_DIRECTIVE_STARTS[0],
|
|
88
|
+
_PREFIX_DIRECTIVE_STARTS[1],
|
|
89
|
+
_PREFIX_DIRECTIVE_STARTS[2],
|
|
90
|
+
_EXACT_DIRECTIVE_STARTS[0],
|
|
91
|
+
_EXACT_DIRECTIVE_STARTS[1],
|
|
92
|
+
_EXACT_DIRECTIVE_STARTS[2],
|
|
93
|
+
_PREFIX_DIRECTIVE_STARTS[3],
|
|
94
|
+
_PREFIX_DIRECTIVE_STARTS[4],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_DIRECTIVE_FAMILY_STARTS: tuple[tuple[str, bool], ...] = (
|
|
98
|
+
(_CHANGE_PREMISE_FAMILY, True),
|
|
99
|
+
_PREFIX_DIRECTIVE_STARTS[1],
|
|
100
|
+
_PREFIX_DIRECTIVE_STARTS[2],
|
|
101
|
+
*_EXACT_DIRECTIVE_STARTS,
|
|
102
|
+
*_PREFIX_DIRECTIVE_STARTS[3:],
|
|
74
103
|
)
|
|
75
104
|
|
|
76
105
|
|
|
@@ -97,59 +126,59 @@ def _render_exact(text: str) -> Callable[[MappingProxyType[str, str]], str]:
|
|
|
97
126
|
|
|
98
127
|
_DIRECTIVE_SPECS = MappingProxyType(
|
|
99
128
|
{
|
|
100
|
-
|
|
101
|
-
kind=
|
|
129
|
+
_DirectiveKind.SET_PREMISE: _DirectiveSpec(
|
|
130
|
+
kind=_DirectiveKind.SET_PREMISE,
|
|
102
131
|
operand_names=("value",),
|
|
103
132
|
exact_text=None,
|
|
104
133
|
renderer=_render_with_prefix(_SET_PREMISE_PREFIX, "value"),
|
|
105
134
|
),
|
|
106
|
-
|
|
107
|
-
kind=
|
|
135
|
+
_DirectiveKind.CHANGE_PREMISE: _DirectiveSpec(
|
|
136
|
+
kind=_DirectiveKind.CHANGE_PREMISE,
|
|
108
137
|
operand_names=("value",),
|
|
109
138
|
exact_text=None,
|
|
110
139
|
renderer=_render_with_prefix(_CHANGE_PREMISE_PREFIX, "value"),
|
|
111
140
|
),
|
|
112
|
-
|
|
113
|
-
kind=
|
|
141
|
+
_DirectiveKind.USE_ITEM: _DirectiveSpec(
|
|
142
|
+
kind=_DirectiveKind.USE_ITEM,
|
|
114
143
|
operand_names=("item",),
|
|
115
144
|
exact_text=None,
|
|
116
145
|
renderer=_render_with_prefix(_USE_PREFIX, "item"),
|
|
117
146
|
),
|
|
118
|
-
|
|
119
|
-
kind=
|
|
147
|
+
_DirectiveKind.PROHIBIT_ITEM: _DirectiveSpec(
|
|
148
|
+
kind=_DirectiveKind.PROHIBIT_ITEM,
|
|
120
149
|
operand_names=("item",),
|
|
121
150
|
exact_text=None,
|
|
122
151
|
renderer=_render_with_prefix(_PROHIBIT_PREFIX, "item"),
|
|
123
152
|
),
|
|
124
|
-
|
|
125
|
-
kind=
|
|
153
|
+
_DirectiveKind.REMOVE_POLICY: _DirectiveSpec(
|
|
154
|
+
kind=_DirectiveKind.REMOVE_POLICY,
|
|
126
155
|
operand_names=("item",),
|
|
127
156
|
exact_text=None,
|
|
128
157
|
renderer=_render_with_prefix(_REMOVE_POLICY_PREFIX, "item"),
|
|
129
158
|
),
|
|
130
|
-
|
|
131
|
-
kind=
|
|
159
|
+
_DirectiveKind.REPLACE_USE: _DirectiveSpec(
|
|
160
|
+
kind=_DirectiveKind.REPLACE_USE,
|
|
132
161
|
operand_names=("new_item", "old_item"),
|
|
133
162
|
exact_text=None,
|
|
134
163
|
renderer=_render_replace_use,
|
|
135
164
|
),
|
|
136
|
-
|
|
137
|
-
kind=
|
|
165
|
+
_DirectiveKind.CLEAR_PREMISE: _DirectiveSpec(
|
|
166
|
+
kind=_DirectiveKind.CLEAR_PREMISE,
|
|
138
167
|
operand_names=(),
|
|
139
|
-
exact_text=
|
|
140
|
-
renderer=_render_exact(
|
|
168
|
+
exact_text=_CLEAR_PREMISE_TEXT,
|
|
169
|
+
renderer=_render_exact(_CLEAR_PREMISE_TEXT),
|
|
141
170
|
),
|
|
142
|
-
|
|
143
|
-
kind=
|
|
171
|
+
_DirectiveKind.RESET_POLICIES: _DirectiveSpec(
|
|
172
|
+
kind=_DirectiveKind.RESET_POLICIES,
|
|
144
173
|
operand_names=(),
|
|
145
|
-
exact_text=
|
|
146
|
-
renderer=_render_exact(
|
|
174
|
+
exact_text=_RESET_POLICIES_TEXT,
|
|
175
|
+
renderer=_render_exact(_RESET_POLICIES_TEXT),
|
|
147
176
|
),
|
|
148
|
-
|
|
149
|
-
kind=
|
|
177
|
+
_DirectiveKind.CLEAR_STATE: _DirectiveSpec(
|
|
178
|
+
kind=_DirectiveKind.CLEAR_STATE,
|
|
150
179
|
operand_names=(),
|
|
151
|
-
exact_text=
|
|
152
|
-
renderer=_render_exact(
|
|
180
|
+
exact_text=_CLEAR_STATE_TEXT,
|
|
181
|
+
renderer=_render_exact(_CLEAR_STATE_TEXT),
|
|
153
182
|
),
|
|
154
183
|
}
|
|
155
184
|
)
|
|
@@ -177,15 +206,8 @@ def _operand_starts_with_token(value: str, token: str) -> bool:
|
|
|
177
206
|
return normalized == token or normalized.startswith(f"{token} ")
|
|
178
207
|
|
|
179
208
|
|
|
180
|
-
def
|
|
181
|
-
"""Locate a canonical directive prefix at a given character position.
|
|
182
|
-
|
|
183
|
-
This is a shallow syntax-start matcher: it classifies whether canonical
|
|
184
|
-
directive syntax begins at ``start`` and, when it does, returns the index
|
|
185
|
-
immediately after the directive keyword prefix. It does not parse operands,
|
|
186
|
-
validate the full directive payload, or evaluate multi-directive state
|
|
187
|
-
semantics.
|
|
188
|
-
"""
|
|
209
|
+
def _match_canonical_directive_start(text: str, start: int) -> int | None:
|
|
210
|
+
"""Locate a canonical directive prefix at a given character position."""
|
|
189
211
|
if start < 0 or start >= len(text):
|
|
190
212
|
return None
|
|
191
213
|
|
|
@@ -242,27 +264,30 @@ def _match_directive_token(
|
|
|
242
264
|
return index
|
|
243
265
|
|
|
244
266
|
|
|
245
|
-
def
|
|
246
|
-
"""Report whether text contains more than one canonical directive start.
|
|
247
|
-
|
|
248
|
-
This detects compound directive structure by looking for multiple canonical
|
|
249
|
-
directive prefixes in the same input. It is not a replacement for full
|
|
250
|
-
directive validation, and it does not parse directive operands, repair
|
|
251
|
-
malformed text, or determine whether a whole string should be accepted as a
|
|
252
|
-
single directive.
|
|
253
|
-
"""
|
|
254
|
-
first_start = match_canonical_directive_start(text, 0)
|
|
267
|
+
def _contains_multiple_canonical_directives(text: str) -> bool:
|
|
268
|
+
"""Report whether text contains more than one canonical directive start."""
|
|
269
|
+
first_start = _match_canonical_directive_start(text, 0)
|
|
255
270
|
if first_start is None:
|
|
256
271
|
return False
|
|
257
272
|
|
|
258
273
|
for index in range(first_start, len(text)):
|
|
259
|
-
next_start =
|
|
274
|
+
next_start = _match_canonical_directive_start(text, index)
|
|
260
275
|
if next_start is not None:
|
|
261
276
|
return True
|
|
262
277
|
|
|
263
278
|
return False
|
|
264
279
|
|
|
265
280
|
|
|
281
|
+
def _starts_with_directive_family(text: str) -> bool:
|
|
282
|
+
for token, require_space_or_end in _DIRECTIVE_FAMILY_STARTS:
|
|
283
|
+
if (
|
|
284
|
+
_match_directive_token(text, 0, token, require_space_or_end=require_space_or_end)
|
|
285
|
+
is not None
|
|
286
|
+
):
|
|
287
|
+
return True
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
266
291
|
def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
|
|
267
292
|
match = _REPLACE_RE.fullmatch(trimmed_text)
|
|
268
293
|
if match is None:
|
|
@@ -280,67 +305,71 @@ def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
|
|
|
280
305
|
return None
|
|
281
306
|
return CanonicalDirective(
|
|
282
307
|
text=trimmed_text,
|
|
283
|
-
kind=
|
|
308
|
+
kind=_DirectiveKind.REPLACE_USE,
|
|
284
309
|
operands=MappingProxyType({"new_item": new_item, "old_item": old_item}),
|
|
285
310
|
)
|
|
286
311
|
|
|
287
312
|
|
|
288
|
-
def decompose_directive(text: str) -> CanonicalDirective | None:
|
|
313
|
+
def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSyntax | None:
|
|
289
314
|
"""Parse one canonical directive into its semantic kind and operands.
|
|
290
315
|
|
|
291
316
|
This determines whether ``text`` is a single canonical directive and, when
|
|
292
317
|
it is, returns the directive kind plus canonical operand names with the
|
|
293
|
-
original operand text preserved.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
effects against compiler state.
|
|
318
|
+
original operand text preserved. It returns ``None`` when no canonical
|
|
319
|
+
directive is present and returns ``InvalidDirectiveSyntax`` when the text is
|
|
320
|
+
directive-shaped but not valid canonical syntax. It does not repair input,
|
|
321
|
+
infer intent, or evaluate directive effects against compiler state.
|
|
297
322
|
"""
|
|
298
323
|
trimmed_text = _trim_ascii_whitespace(text)
|
|
299
324
|
if trimmed_text == "":
|
|
300
325
|
return None
|
|
301
|
-
if
|
|
326
|
+
if not _starts_with_directive_family(trimmed_text):
|
|
302
327
|
return None
|
|
328
|
+
if _contains_multiple_canonical_directives(trimmed_text):
|
|
329
|
+
return InvalidDirectiveSyntax()
|
|
330
|
+
|
|
331
|
+
invalid_result = InvalidDirectiveSyntax()
|
|
303
332
|
|
|
304
333
|
normalized = _normalized_for_matching(trimmed_text)
|
|
305
334
|
|
|
306
|
-
if normalized ==
|
|
335
|
+
if normalized == _CLEAR_PREMISE_TEXT:
|
|
307
336
|
return CanonicalDirective(
|
|
308
|
-
text=text, kind=
|
|
337
|
+
text=text, kind=_DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({})
|
|
309
338
|
)
|
|
310
|
-
if normalized ==
|
|
339
|
+
if normalized == _RESET_POLICIES_TEXT:
|
|
311
340
|
return CanonicalDirective(
|
|
312
341
|
text=text,
|
|
313
|
-
kind=
|
|
342
|
+
kind=_DirectiveKind.RESET_POLICIES,
|
|
314
343
|
operands=MappingProxyType({}),
|
|
315
344
|
)
|
|
316
|
-
if normalized ==
|
|
345
|
+
if normalized == _CLEAR_STATE_TEXT:
|
|
317
346
|
return CanonicalDirective(
|
|
318
|
-
text=text, kind=
|
|
347
|
+
text=text, kind=_DirectiveKind.CLEAR_STATE, operands=MappingProxyType({})
|
|
319
348
|
)
|
|
320
349
|
|
|
321
350
|
if normalized.startswith("set premise "):
|
|
322
351
|
match = _SET_PREMISE_RE.fullmatch(trimmed_text)
|
|
323
352
|
if match is None:
|
|
324
|
-
return
|
|
353
|
+
return invalid_result
|
|
325
354
|
value = match.group("value")
|
|
326
355
|
if not _operand_has_content(value) or _operand_starts_with_token(value, "to"):
|
|
327
|
-
return
|
|
356
|
+
return invalid_result
|
|
328
357
|
return CanonicalDirective(
|
|
329
358
|
text=text,
|
|
330
|
-
kind=
|
|
359
|
+
kind=_DirectiveKind.SET_PREMISE,
|
|
331
360
|
operands=MappingProxyType({"value": value}),
|
|
332
361
|
)
|
|
333
362
|
|
|
334
363
|
if normalized.startswith("change premise to "):
|
|
335
364
|
match = _CHANGE_PREMISE_RE.fullmatch(trimmed_text)
|
|
336
365
|
if match is None:
|
|
337
|
-
return
|
|
366
|
+
return invalid_result
|
|
338
367
|
value = match.group("value")
|
|
339
368
|
if not _operand_has_content(value):
|
|
340
|
-
return
|
|
369
|
+
return invalid_result
|
|
341
370
|
return CanonicalDirective(
|
|
342
371
|
text=text,
|
|
343
|
-
kind=
|
|
372
|
+
kind=_DirectiveKind.CHANGE_PREMISE,
|
|
344
373
|
operands=MappingProxyType({"value": value}),
|
|
345
374
|
)
|
|
346
375
|
|
|
@@ -351,7 +380,7 @@ def decompose_directive(text: str) -> CanonicalDirective | None:
|
|
|
351
380
|
if normalized.startswith("use "):
|
|
352
381
|
match = _USE_RE.fullmatch(trimmed_text)
|
|
353
382
|
if match is None:
|
|
354
|
-
return
|
|
383
|
+
return invalid_result
|
|
355
384
|
item = match.group("item")
|
|
356
385
|
normalized_item = _normalized_for_matching(item)
|
|
357
386
|
if (
|
|
@@ -360,52 +389,46 @@ def decompose_directive(text: str) -> CanonicalDirective | None:
|
|
|
360
389
|
or normalized_item.endswith(" instead of")
|
|
361
390
|
or _INSTEAD_OF_DELIMITER in normalized_item
|
|
362
391
|
):
|
|
363
|
-
return
|
|
392
|
+
return invalid_result
|
|
364
393
|
return CanonicalDirective(
|
|
365
394
|
text=text,
|
|
366
|
-
kind=
|
|
395
|
+
kind=_DirectiveKind.USE_ITEM,
|
|
367
396
|
operands=MappingProxyType({"item": item}),
|
|
368
397
|
)
|
|
369
398
|
|
|
370
399
|
if normalized.startswith("prohibit "):
|
|
371
400
|
match = _PROHIBIT_RE.fullmatch(trimmed_text)
|
|
372
401
|
if match is None:
|
|
373
|
-
return
|
|
402
|
+
return invalid_result
|
|
374
403
|
item = match.group("item")
|
|
375
404
|
if not _operand_has_content(item):
|
|
376
|
-
return
|
|
405
|
+
return invalid_result
|
|
377
406
|
return CanonicalDirective(
|
|
378
407
|
text=text,
|
|
379
|
-
kind=
|
|
408
|
+
kind=_DirectiveKind.PROHIBIT_ITEM,
|
|
380
409
|
operands=MappingProxyType({"item": item}),
|
|
381
410
|
)
|
|
382
411
|
|
|
383
412
|
if normalized.startswith("remove policy "):
|
|
384
413
|
match = _REMOVE_POLICY_RE.fullmatch(trimmed_text)
|
|
385
414
|
if match is None:
|
|
386
|
-
return
|
|
415
|
+
return invalid_result
|
|
387
416
|
item = match.group("item")
|
|
388
417
|
if not _operand_has_content(item):
|
|
389
|
-
return
|
|
418
|
+
return invalid_result
|
|
390
419
|
return CanonicalDirective(
|
|
391
420
|
text=text,
|
|
392
|
-
kind=
|
|
421
|
+
kind=_DirectiveKind.REMOVE_POLICY,
|
|
393
422
|
operands=MappingProxyType({"item": item}),
|
|
394
423
|
)
|
|
395
424
|
|
|
396
|
-
return
|
|
397
|
-
|
|
425
|
+
return invalid_result
|
|
398
426
|
|
|
399
|
-
def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
|
|
400
|
-
"""Produce canonical directive text from a semantic kind and operands.
|
|
401
427
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
requested directive kind. It does not infer missing operands, parse user
|
|
405
|
-
input, or extend the grammar with new behaviors.
|
|
406
|
-
"""
|
|
428
|
+
def _render_directive(kind: _DirectiveKind | str, /, **operands: str) -> str:
|
|
429
|
+
"""Produce canonical directive text from a semantic kind and operands."""
|
|
407
430
|
try:
|
|
408
|
-
normalized_kind = kind if isinstance(kind,
|
|
431
|
+
normalized_kind = kind if isinstance(kind, _DirectiveKind) else _DirectiveKind(kind)
|
|
409
432
|
spec = _DIRECTIVE_SPECS[normalized_kind]
|
|
410
433
|
except (KeyError, ValueError) as exc:
|
|
411
434
|
raise ValueError(f"Unsupported directive kind: {kind!r}") from exc
|
|
@@ -416,33 +439,30 @@ def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
|
|
|
416
439
|
missing_names = expected_names - actual_names
|
|
417
440
|
if missing_names:
|
|
418
441
|
missing = ", ".join(sorted(missing_names))
|
|
419
|
-
raise ValueError(f"Missing required operands for {
|
|
442
|
+
raise ValueError(f"Missing required operands for {normalized_kind.value}: {missing}")
|
|
420
443
|
if unexpected_names:
|
|
421
444
|
unexpected = ", ".join(sorted(unexpected_names))
|
|
422
|
-
raise ValueError(f"Unexpected operands for {
|
|
445
|
+
raise ValueError(f"Unexpected operands for {normalized_kind.value}: {unexpected}")
|
|
423
446
|
|
|
424
447
|
normalized_operands: dict[str, str] = {}
|
|
425
448
|
for name in spec.operand_names:
|
|
426
449
|
raw_value = operands[name]
|
|
427
450
|
if not isinstance(raw_value, str):
|
|
428
|
-
raise ValueError(f"Operand {name!r} for {
|
|
451
|
+
raise ValueError(f"Operand {name!r} for {normalized_kind.value} must be a string.")
|
|
429
452
|
if raw_value.strip() == "":
|
|
430
|
-
raise ValueError(f"Operand {name!r} for {
|
|
453
|
+
raise ValueError(f"Operand {name!r} for {normalized_kind.value} cannot be empty.")
|
|
431
454
|
normalized_operands[name] = raw_value
|
|
432
455
|
|
|
433
456
|
operand_view = MappingProxyType(normalized_operands)
|
|
434
457
|
rendered = spec.renderer(operand_view)
|
|
435
458
|
decomposed = decompose_directive(rendered)
|
|
436
|
-
if decomposed
|
|
459
|
+
if not isinstance(decomposed, CanonicalDirective) or decomposed.kind is not normalized_kind:
|
|
437
460
|
raise ValueError(f"Operands do not produce a canonical {normalized_kind.value} directive.")
|
|
438
461
|
return rendered
|
|
439
462
|
|
|
440
463
|
|
|
441
464
|
__all__ = [
|
|
442
465
|
"CanonicalDirective",
|
|
443
|
-
"
|
|
444
|
-
"contains_multiple_canonical_directives",
|
|
466
|
+
"InvalidDirectiveSyntax",
|
|
445
467
|
"decompose_directive",
|
|
446
|
-
"match_canonical_directive_start",
|
|
447
|
-
"render_directive",
|
|
448
468
|
]
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
6
|
from typing import TextIO
|
|
7
7
|
|
|
8
|
-
from . import __version__
|
|
8
|
+
from . import __version__
|
|
9
9
|
from .const import STATE_POLICIES, STATE_PREMISE, STATE_VERSION
|
|
10
10
|
from .decision_helpers import is_error, is_no_directive, is_update
|
|
11
11
|
from .engine import Decision, DecisionKind, Engine, PolicyValue
|
|
@@ -233,7 +233,7 @@ def run_repl(
|
|
|
233
233
|
emit NDJSON records.
|
|
234
234
|
"""
|
|
235
235
|
|
|
236
|
-
active_engine =
|
|
236
|
+
active_engine = Engine() if engine is None else engine
|
|
237
237
|
|
|
238
238
|
if _is_interactive(in_stream, out_stream):
|
|
239
239
|
print("Context Compiler REPL (0.5). Type help for commands.", file=out_stream)
|
|
@@ -420,7 +420,7 @@ def main() -> int: # pragma: no cover
|
|
|
420
420
|
print("error: --json requires non-interactive stdin/stdout.", file=sys.stderr)
|
|
421
421
|
return 1
|
|
422
422
|
|
|
423
|
-
engine =
|
|
423
|
+
engine = Engine()
|
|
424
424
|
try:
|
|
425
425
|
_apply_preload_from_options(engine, options)
|
|
426
426
|
except (OSError, ValueError) as exc:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev8}/src/context_compiler/decision_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|