context-compiler 0.9.0.dev7__tar.gz → 0.9.0.dev9__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.dev9}/PKG-INFO +5 -5
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/README.md +3 -3
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/01_llm_contradiction_error.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/02_llm_constraint_guardrail.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/03_llm_premise_guardrail.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/04_llm_tool_denylist_guardrail.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/05_llm_prompt_drift_vs_state.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/06_llm_context_compaction.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/07_llm_prompt_vs_state.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/08_llm_replacement_precondition.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/09_llm_confirmation_no_directive.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/common.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/01_persistent_guardrails.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/02_configuration_and_correction.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/03_ambiguity_with_error.py +5 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/04_tool_governance_denylist.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/05_llm_integration_pattern.py +1 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/06_step_sequence_and_state_restore.py +3 -3
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/07_single_policy_correction.py +2 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/pyproject.toml +1 -1
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/src/context_compiler/__init__.py +0 -2
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/src/context_compiler/engine.py +63 -147
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/src/context_compiler/grammar.py +207 -77
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/src/context_compiler/repl.py +3 -3
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/.gitignore +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/LICENSE +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/README.md +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/__init__.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/llm_client.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/demos/run_demo.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/README.md +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/examples/_util.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/host_support/__init__.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/host_support/provider_mode.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/src/context_compiler/const.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/src/context_compiler/decision_helpers.py +0 -0
- {context_compiler-0.9.0.dev7 → context_compiler-0.9.0.dev9}/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.dev9
|
|
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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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.dev9}/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 DirectiveKind, decompose_directive
|
|
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,99 +91,76 @@ 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()
|
|
132
104
|
|
|
133
|
-
|
|
134
|
-
action = _parse_directive(user_input)
|
|
135
|
-
if action is None:
|
|
136
|
-
return _EvaluatedTransition(decision=_NO_DIRECTIVE.copy(), next_state=deepcopy(state))
|
|
105
|
+
return self.apply_directive(directive)
|
|
137
106
|
|
|
138
|
-
|
|
107
|
+
def apply_directive(self, directive: CanonicalDirective) -> Decision:
|
|
108
|
+
"""Evaluate and commit one canonical directive against authoritative state."""
|
|
109
|
+
|
|
110
|
+
evaluated = self._evaluate_directive_transition(self._state, directive)
|
|
111
|
+
self._replace_state(evaluated["next_state"])
|
|
112
|
+
return evaluated["decision"]
|
|
113
|
+
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
return _error(
|
|
158
|
-
"Premise value cannot be empty.\n"
|
|
159
|
-
"Use 'set premise <value>' with a non-empty value."
|
|
160
|
-
)
|
|
161
|
-
return _error(
|
|
162
|
-
"Premise value cannot be empty.\n"
|
|
163
|
-
"Use 'change premise to <value>' with a non-empty value."
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
if action.kind == "remove_policy_item":
|
|
167
|
-
assert action.item is not None
|
|
168
|
-
if _normalize_item(action.item) == "":
|
|
169
|
-
return _error(
|
|
170
|
-
"Policy item cannot be empty.\n"
|
|
171
|
-
"Use 'remove policy <item>' with a non-empty value."
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
if action.kind == "use_item":
|
|
175
|
-
assert action.item is not None
|
|
176
|
-
if _normalize_item(action.item) == "":
|
|
177
|
-
return _error(
|
|
178
|
-
"Policy item cannot be empty.\nUse 'use <item>' with a non-empty value."
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
if action.kind == "prohibit_item":
|
|
182
|
-
assert action.item is not None
|
|
183
|
-
if _normalize_item(action.item) == "":
|
|
184
|
-
return _error(
|
|
185
|
-
"Policy item cannot be empty.\nUse 'prohibit <item>' with a non-empty value."
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
if action.kind == "set_premise" and candidate_state[STATE_PREMISE] is not None:
|
|
132
|
+
if (
|
|
133
|
+
directive.kind is DirectiveKind.SET_PREMISE
|
|
134
|
+
and candidate_state[STATE_PREMISE] is not None
|
|
135
|
+
):
|
|
189
136
|
return _error("Premise already set.\nUse 'change premise to <value>' to modify it.")
|
|
190
137
|
|
|
191
|
-
if
|
|
138
|
+
if (
|
|
139
|
+
directive.kind is DirectiveKind.CHANGE_PREMISE
|
|
140
|
+
and candidate_state[STATE_PREMISE] is None
|
|
141
|
+
):
|
|
192
142
|
return _error("No premise is set.\nUse 'set premise <value>' to define one.")
|
|
193
143
|
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
item_key = _normalize_item(action.item)
|
|
144
|
+
if directive.kind is DirectiveKind.USE_ITEM:
|
|
145
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
197
146
|
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_PROHIBIT:
|
|
198
147
|
return _error(
|
|
199
148
|
f'"{item_key}" is currently prohibited.\nRemove or replace it before using it.'
|
|
200
149
|
)
|
|
201
150
|
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
item_key = _normalize_item(action.item)
|
|
151
|
+
if directive.kind is DirectiveKind.PROHIBIT_ITEM:
|
|
152
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
205
153
|
if candidate_state[STATE_POLICIES].get(item_key) == POLICY_USE:
|
|
206
154
|
return _error(
|
|
207
155
|
f'"{item_key}" is currently in use.\n'
|
|
208
156
|
"Remove or replace it before prohibiting it."
|
|
209
157
|
)
|
|
210
158
|
|
|
211
|
-
if
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
new_key = _normalize_item(
|
|
215
|
-
old_key = _normalize_item(
|
|
159
|
+
if directive.kind is DirectiveKind.REPLACE_USE:
|
|
160
|
+
new_item = directive.operands["new_item"]
|
|
161
|
+
old_item = directive.operands["old_item"]
|
|
162
|
+
new_key = _normalize_item(new_item)
|
|
163
|
+
old_key = _normalize_item(old_item)
|
|
216
164
|
if new_key == old_key:
|
|
217
165
|
return None
|
|
218
166
|
|
|
@@ -220,67 +168,63 @@ class Engine:
|
|
|
220
168
|
new_state = candidate_state[STATE_POLICIES].get(new_key)
|
|
221
169
|
if old_state == POLICY_PROHIBIT:
|
|
222
170
|
return _error(
|
|
223
|
-
f'"{
|
|
171
|
+
f'"{old_item}" is currently prohibited.\n'
|
|
224
172
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
225
173
|
)
|
|
226
174
|
if new_state == POLICY_PROHIBIT:
|
|
227
175
|
return _error(
|
|
228
|
-
f'"{
|
|
176
|
+
f'"{new_item}" is currently prohibited.\n'
|
|
229
177
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
230
178
|
)
|
|
231
179
|
if old_state not in {None, POLICY_USE}:
|
|
232
180
|
return _error(
|
|
233
|
-
f'"{
|
|
181
|
+
f'"{old_item}" is not currently in use.\n'
|
|
234
182
|
"Replacement requires an active 'use' policy."
|
|
235
183
|
)
|
|
236
184
|
|
|
237
185
|
return None
|
|
238
186
|
|
|
239
|
-
def
|
|
187
|
+
def _apply_directive(self, directive: CanonicalDirective, *, state: _State) -> _State:
|
|
240
188
|
next_state = deepcopy(state)
|
|
241
|
-
kind = action.kind
|
|
242
189
|
|
|
243
|
-
if kind
|
|
244
|
-
|
|
245
|
-
next_state[STATE_PREMISE] = _sanitize_premise_value(action.value)
|
|
190
|
+
if directive.kind is DirectiveKind.SET_PREMISE:
|
|
191
|
+
next_state[STATE_PREMISE] = _sanitize_premise_value(directive.operands["value"])
|
|
246
192
|
return next_state
|
|
247
193
|
|
|
248
|
-
if kind
|
|
249
|
-
|
|
250
|
-
next_state[STATE_PREMISE] = _sanitize_premise_value(action.value)
|
|
194
|
+
if directive.kind is DirectiveKind.CHANGE_PREMISE:
|
|
195
|
+
next_state[STATE_PREMISE] = _sanitize_premise_value(directive.operands["value"])
|
|
251
196
|
return next_state
|
|
252
197
|
|
|
253
|
-
if kind
|
|
254
|
-
|
|
255
|
-
item_key = _normalize_item(action.item)
|
|
198
|
+
if directive.kind is DirectiveKind.USE_ITEM:
|
|
199
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
256
200
|
# Idempotent directives are updates even if state does not change.
|
|
257
201
|
next_state[STATE_POLICIES][item_key] = POLICY_USE
|
|
258
202
|
return next_state
|
|
259
203
|
|
|
260
|
-
if kind
|
|
261
|
-
|
|
262
|
-
item_key = _normalize_item(action.item)
|
|
204
|
+
if directive.kind is DirectiveKind.PROHIBIT_ITEM:
|
|
205
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
263
206
|
# Idempotent directives are updates even if state does not change.
|
|
264
207
|
next_state[STATE_POLICIES][item_key] = POLICY_PROHIBIT
|
|
265
208
|
return next_state
|
|
266
209
|
|
|
267
|
-
if kind
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
210
|
+
if directive.kind is DirectiveKind.REPLACE_USE:
|
|
211
|
+
self._apply_replacement_explicit(
|
|
212
|
+
next_state,
|
|
213
|
+
directive.operands["new_item"],
|
|
214
|
+
directive.operands["old_item"],
|
|
215
|
+
)
|
|
271
216
|
return next_state
|
|
272
217
|
|
|
273
|
-
if kind
|
|
274
|
-
|
|
275
|
-
item_key = _normalize_item(action.item)
|
|
218
|
+
if directive.kind is DirectiveKind.REMOVE_POLICY:
|
|
219
|
+
item_key = _normalize_item(directive.operands["item"])
|
|
276
220
|
next_state[STATE_POLICIES].pop(item_key, None)
|
|
277
221
|
return next_state
|
|
278
222
|
|
|
279
|
-
if kind
|
|
223
|
+
if directive.kind is DirectiveKind.CLEAR_PREMISE:
|
|
280
224
|
next_state[STATE_PREMISE] = None
|
|
281
225
|
return next_state
|
|
282
226
|
|
|
283
|
-
if kind
|
|
227
|
+
if directive.kind is DirectiveKind.RESET_POLICIES:
|
|
284
228
|
next_state[STATE_POLICIES] = {}
|
|
285
229
|
return next_state
|
|
286
230
|
|
|
@@ -297,34 +241,6 @@ class Engine:
|
|
|
297
241
|
state[STATE_POLICIES][new_key] = POLICY_USE
|
|
298
242
|
|
|
299
243
|
|
|
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
244
|
def _initial_state() -> _State:
|
|
329
245
|
return {
|
|
330
246
|
STATE_PREMISE: None,
|
|
@@ -21,14 +21,20 @@ class DirectiveKind(StrEnum):
|
|
|
21
21
|
CLEAR_STATE = "clear_state"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class DirectiveSyntaxFailure(StrEnum):
|
|
25
|
+
"""Enumerate minimal grammar failure categories for directive-shaped input."""
|
|
26
|
+
|
|
27
|
+
COMPOUND_DIRECTIVE = "compound_directive"
|
|
28
|
+
MISSING_REQUIRED_OPERAND = "missing_required_operand"
|
|
29
|
+
MALFORMED_DIRECTIVE = "malformed_directive"
|
|
30
|
+
|
|
31
|
+
|
|
24
32
|
@dataclass(frozen=True, slots=True)
|
|
25
33
|
class CanonicalDirective:
|
|
26
34
|
"""Represent one parsed canonical directive and its named operands.
|
|
27
35
|
|
|
28
36
|
``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.
|
|
37
|
+
formatting or casing and is not canonical serialized directive text.
|
|
32
38
|
"""
|
|
33
39
|
|
|
34
40
|
text: str
|
|
@@ -36,6 +42,15 @@ class CanonicalDirective:
|
|
|
36
42
|
operands: MappingProxyType[str, str]
|
|
37
43
|
|
|
38
44
|
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class InvalidDirectiveSyntax:
|
|
47
|
+
"""Represent directive-shaped input that fails canonical syntax parsing."""
|
|
48
|
+
|
|
49
|
+
failure: DirectiveSyntaxFailure = DirectiveSyntaxFailure.MALFORMED_DIRECTIVE
|
|
50
|
+
directive_kind: DirectiveKind | None = None
|
|
51
|
+
missing_operand: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
39
54
|
@dataclass(frozen=True, slots=True)
|
|
40
55
|
class _DirectiveSpec:
|
|
41
56
|
kind: DirectiveKind
|
|
@@ -49,6 +64,10 @@ _CHANGE_PREMISE_PREFIX = "change premise to "
|
|
|
49
64
|
_USE_PREFIX = "use "
|
|
50
65
|
_PROHIBIT_PREFIX = "prohibit "
|
|
51
66
|
_REMOVE_POLICY_PREFIX = "remove policy "
|
|
67
|
+
_CLEAR_PREMISE_TEXT = "clear premise"
|
|
68
|
+
_RESET_POLICIES_TEXT = "reset policies"
|
|
69
|
+
_CLEAR_STATE_TEXT = "clear state"
|
|
70
|
+
_CHANGE_PREMISE_FAMILY = "change premise"
|
|
52
71
|
_INSTEAD_OF_DELIMITER = " instead of "
|
|
53
72
|
_ASCII_WHITESPACE = " \t\n\r\x0b\x0c"
|
|
54
73
|
_HORIZONTAL_WHITESPACE = " \t"
|
|
@@ -62,15 +81,37 @@ _REPLACE_RE = re.compile(
|
|
|
62
81
|
r"(?i)^use[ \t]+(?P<new_item>.*?)[ \t]+instead[ \t]+of[ \t]+(?P<old_item>.+)$"
|
|
63
82
|
)
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
_PREFIX_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
66
85
|
(_CHANGE_PREMISE_PREFIX.removesuffix(" "), True),
|
|
67
86
|
(_SET_PREMISE_PREFIX.removesuffix(" "), True),
|
|
68
87
|
(_REMOVE_POLICY_PREFIX.removesuffix(" "), True),
|
|
69
|
-
("reset policies", False),
|
|
70
|
-
("clear premise", False),
|
|
71
|
-
("clear state", False),
|
|
72
88
|
(_PROHIBIT_PREFIX.removesuffix(" "), True),
|
|
73
|
-
("
|
|
89
|
+
(_USE_PREFIX.removesuffix(" "), True),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_EXACT_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
93
|
+
(_RESET_POLICIES_TEXT, False),
|
|
94
|
+
(_CLEAR_PREMISE_TEXT, False),
|
|
95
|
+
(_CLEAR_STATE_TEXT, False),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
_CANONICAL_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
99
|
+
_PREFIX_DIRECTIVE_STARTS[0],
|
|
100
|
+
_PREFIX_DIRECTIVE_STARTS[1],
|
|
101
|
+
_PREFIX_DIRECTIVE_STARTS[2],
|
|
102
|
+
_EXACT_DIRECTIVE_STARTS[0],
|
|
103
|
+
_EXACT_DIRECTIVE_STARTS[1],
|
|
104
|
+
_EXACT_DIRECTIVE_STARTS[2],
|
|
105
|
+
_PREFIX_DIRECTIVE_STARTS[3],
|
|
106
|
+
_PREFIX_DIRECTIVE_STARTS[4],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
_DIRECTIVE_FAMILY_STARTS: tuple[tuple[str, bool], ...] = (
|
|
110
|
+
(_CHANGE_PREMISE_FAMILY, True),
|
|
111
|
+
_PREFIX_DIRECTIVE_STARTS[1],
|
|
112
|
+
_PREFIX_DIRECTIVE_STARTS[2],
|
|
113
|
+
*_EXACT_DIRECTIVE_STARTS,
|
|
114
|
+
*_PREFIX_DIRECTIVE_STARTS[3:],
|
|
74
115
|
)
|
|
75
116
|
|
|
76
117
|
|
|
@@ -136,20 +177,20 @@ _DIRECTIVE_SPECS = MappingProxyType(
|
|
|
136
177
|
DirectiveKind.CLEAR_PREMISE: _DirectiveSpec(
|
|
137
178
|
kind=DirectiveKind.CLEAR_PREMISE,
|
|
138
179
|
operand_names=(),
|
|
139
|
-
exact_text=
|
|
140
|
-
renderer=_render_exact(
|
|
180
|
+
exact_text=_CLEAR_PREMISE_TEXT,
|
|
181
|
+
renderer=_render_exact(_CLEAR_PREMISE_TEXT),
|
|
141
182
|
),
|
|
142
183
|
DirectiveKind.RESET_POLICIES: _DirectiveSpec(
|
|
143
184
|
kind=DirectiveKind.RESET_POLICIES,
|
|
144
185
|
operand_names=(),
|
|
145
|
-
exact_text=
|
|
146
|
-
renderer=_render_exact(
|
|
186
|
+
exact_text=_RESET_POLICIES_TEXT,
|
|
187
|
+
renderer=_render_exact(_RESET_POLICIES_TEXT),
|
|
147
188
|
),
|
|
148
189
|
DirectiveKind.CLEAR_STATE: _DirectiveSpec(
|
|
149
190
|
kind=DirectiveKind.CLEAR_STATE,
|
|
150
191
|
operand_names=(),
|
|
151
|
-
exact_text=
|
|
152
|
-
renderer=_render_exact(
|
|
192
|
+
exact_text=_CLEAR_STATE_TEXT,
|
|
193
|
+
renderer=_render_exact(_CLEAR_STATE_TEXT),
|
|
153
194
|
),
|
|
154
195
|
}
|
|
155
196
|
)
|
|
@@ -177,15 +218,8 @@ def _operand_starts_with_token(value: str, token: str) -> bool:
|
|
|
177
218
|
return normalized == token or normalized.startswith(f"{token} ")
|
|
178
219
|
|
|
179
220
|
|
|
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
|
-
"""
|
|
221
|
+
def _match_canonical_directive_start(text: str, start: int) -> int | None:
|
|
222
|
+
"""Locate a canonical directive prefix at a given character position."""
|
|
189
223
|
if start < 0 or start >= len(text):
|
|
190
224
|
return None
|
|
191
225
|
|
|
@@ -242,27 +276,30 @@ def _match_directive_token(
|
|
|
242
276
|
return index
|
|
243
277
|
|
|
244
278
|
|
|
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)
|
|
279
|
+
def _contains_multiple_canonical_directives(text: str) -> bool:
|
|
280
|
+
"""Report whether text contains more than one canonical directive start."""
|
|
281
|
+
first_start = _match_canonical_directive_start(text, 0)
|
|
255
282
|
if first_start is None:
|
|
256
283
|
return False
|
|
257
284
|
|
|
258
285
|
for index in range(first_start, len(text)):
|
|
259
|
-
next_start =
|
|
286
|
+
next_start = _match_canonical_directive_start(text, index)
|
|
260
287
|
if next_start is not None:
|
|
261
288
|
return True
|
|
262
289
|
|
|
263
290
|
return False
|
|
264
291
|
|
|
265
292
|
|
|
293
|
+
def _starts_with_directive_family(text: str) -> bool:
|
|
294
|
+
for token, require_space_or_end in _DIRECTIVE_FAMILY_STARTS:
|
|
295
|
+
if (
|
|
296
|
+
_match_directive_token(text, 0, token, require_space_or_end=require_space_or_end)
|
|
297
|
+
is not None
|
|
298
|
+
):
|
|
299
|
+
return True
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
|
|
266
303
|
def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
|
|
267
304
|
match = _REPLACE_RE.fullmatch(trimmed_text)
|
|
268
305
|
if match is None:
|
|
@@ -285,59 +322,102 @@ def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
|
|
|
285
322
|
)
|
|
286
323
|
|
|
287
324
|
|
|
288
|
-
def
|
|
325
|
+
def _invalid_directive_syntax(
|
|
326
|
+
failure: DirectiveSyntaxFailure,
|
|
327
|
+
*,
|
|
328
|
+
directive_kind: DirectiveKind | None = None,
|
|
329
|
+
missing_operand: str | None = None,
|
|
330
|
+
) -> InvalidDirectiveSyntax:
|
|
331
|
+
return InvalidDirectiveSyntax(
|
|
332
|
+
failure=failure,
|
|
333
|
+
directive_kind=directive_kind,
|
|
334
|
+
missing_operand=missing_operand,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSyntax | None:
|
|
289
339
|
"""Parse one canonical directive into its semantic kind and operands.
|
|
290
340
|
|
|
291
341
|
This determines whether ``text`` is a single canonical directive and, when
|
|
292
342
|
it is, returns the directive kind plus canonical operand names with the
|
|
293
|
-
original operand text preserved.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
effects against compiler state.
|
|
343
|
+
original operand text preserved. It returns ``None`` when no canonical
|
|
344
|
+
directive is present and returns ``InvalidDirectiveSyntax`` when the text is
|
|
345
|
+
directive-shaped but not valid canonical syntax. It does not repair input,
|
|
346
|
+
infer intent, or evaluate directive effects against compiler state.
|
|
297
347
|
"""
|
|
298
348
|
trimmed_text = _trim_ascii_whitespace(text)
|
|
299
349
|
if trimmed_text == "":
|
|
300
350
|
return None
|
|
301
|
-
if
|
|
351
|
+
if not _starts_with_directive_family(trimmed_text):
|
|
302
352
|
return None
|
|
353
|
+
if _contains_multiple_canonical_directives(trimmed_text):
|
|
354
|
+
return _invalid_directive_syntax(DirectiveSyntaxFailure.COMPOUND_DIRECTIVE)
|
|
303
355
|
|
|
304
356
|
normalized = _normalized_for_matching(trimmed_text)
|
|
305
357
|
|
|
306
|
-
if normalized ==
|
|
358
|
+
if normalized == _CLEAR_PREMISE_TEXT:
|
|
307
359
|
return CanonicalDirective(
|
|
308
360
|
text=text, kind=DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({})
|
|
309
361
|
)
|
|
310
|
-
if normalized ==
|
|
362
|
+
if normalized == _RESET_POLICIES_TEXT:
|
|
311
363
|
return CanonicalDirective(
|
|
312
364
|
text=text,
|
|
313
365
|
kind=DirectiveKind.RESET_POLICIES,
|
|
314
366
|
operands=MappingProxyType({}),
|
|
315
367
|
)
|
|
316
|
-
if normalized ==
|
|
368
|
+
if normalized == _CLEAR_STATE_TEXT:
|
|
317
369
|
return CanonicalDirective(
|
|
318
370
|
text=text, kind=DirectiveKind.CLEAR_STATE, operands=MappingProxyType({})
|
|
319
371
|
)
|
|
320
372
|
|
|
373
|
+
if normalized == "set premise":
|
|
374
|
+
return _invalid_directive_syntax(
|
|
375
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
376
|
+
directive_kind=DirectiveKind.SET_PREMISE,
|
|
377
|
+
missing_operand="value",
|
|
378
|
+
)
|
|
379
|
+
|
|
321
380
|
if normalized.startswith("set premise "):
|
|
322
381
|
match = _SET_PREMISE_RE.fullmatch(trimmed_text)
|
|
323
382
|
if match is None:
|
|
324
|
-
return
|
|
383
|
+
return _invalid_directive_syntax(
|
|
384
|
+
DirectiveSyntaxFailure.MALFORMED_DIRECTIVE,
|
|
385
|
+
directive_kind=DirectiveKind.SET_PREMISE,
|
|
386
|
+
)
|
|
325
387
|
value = match.group("value")
|
|
326
388
|
if not _operand_has_content(value) or _operand_starts_with_token(value, "to"):
|
|
327
|
-
return
|
|
389
|
+
return _invalid_directive_syntax(
|
|
390
|
+
DirectiveSyntaxFailure.MALFORMED_DIRECTIVE,
|
|
391
|
+
directive_kind=DirectiveKind.SET_PREMISE,
|
|
392
|
+
)
|
|
328
393
|
return CanonicalDirective(
|
|
329
394
|
text=text,
|
|
330
395
|
kind=DirectiveKind.SET_PREMISE,
|
|
331
396
|
operands=MappingProxyType({"value": value}),
|
|
332
397
|
)
|
|
333
398
|
|
|
399
|
+
if normalized == "change premise to":
|
|
400
|
+
return _invalid_directive_syntax(
|
|
401
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
402
|
+
directive_kind=DirectiveKind.CHANGE_PREMISE,
|
|
403
|
+
missing_operand="value",
|
|
404
|
+
)
|
|
405
|
+
|
|
334
406
|
if normalized.startswith("change premise to "):
|
|
335
407
|
match = _CHANGE_PREMISE_RE.fullmatch(trimmed_text)
|
|
336
408
|
if match is None:
|
|
337
|
-
return
|
|
409
|
+
return _invalid_directive_syntax(
|
|
410
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
411
|
+
directive_kind=DirectiveKind.CHANGE_PREMISE,
|
|
412
|
+
missing_operand="value",
|
|
413
|
+
)
|
|
338
414
|
value = match.group("value")
|
|
339
415
|
if not _operand_has_content(value):
|
|
340
|
-
return
|
|
416
|
+
return _invalid_directive_syntax(
|
|
417
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
418
|
+
directive_kind=DirectiveKind.CHANGE_PREMISE,
|
|
419
|
+
missing_operand="value",
|
|
420
|
+
)
|
|
341
421
|
return CanonicalDirective(
|
|
342
422
|
text=text,
|
|
343
423
|
kind=DirectiveKind.CHANGE_PREMISE,
|
|
@@ -348,62 +428,113 @@ def decompose_directive(text: str) -> CanonicalDirective | None:
|
|
|
348
428
|
if replacement is not None:
|
|
349
429
|
return replacement
|
|
350
430
|
|
|
431
|
+
if normalized == "use":
|
|
432
|
+
return _invalid_directive_syntax(
|
|
433
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
434
|
+
directive_kind=DirectiveKind.USE_ITEM,
|
|
435
|
+
missing_operand="item",
|
|
436
|
+
)
|
|
437
|
+
|
|
351
438
|
if normalized.startswith("use "):
|
|
352
439
|
match = _USE_RE.fullmatch(trimmed_text)
|
|
353
440
|
if match is None:
|
|
354
|
-
return
|
|
441
|
+
return _invalid_directive_syntax(
|
|
442
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
443
|
+
directive_kind=DirectiveKind.USE_ITEM,
|
|
444
|
+
missing_operand="item",
|
|
445
|
+
)
|
|
355
446
|
item = match.group("item")
|
|
356
447
|
normalized_item = _normalized_for_matching(item)
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
448
|
+
if not _operand_has_content(item):
|
|
449
|
+
return _invalid_directive_syntax(
|
|
450
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
451
|
+
directive_kind=DirectiveKind.USE_ITEM,
|
|
452
|
+
missing_operand="item",
|
|
453
|
+
)
|
|
454
|
+
if normalized_item == "instead of" or normalized_item.startswith("instead of "):
|
|
455
|
+
return _invalid_directive_syntax(
|
|
456
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
457
|
+
directive_kind=DirectiveKind.REPLACE_USE,
|
|
458
|
+
missing_operand="new_item",
|
|
459
|
+
)
|
|
460
|
+
if normalized_item.endswith(" instead of"):
|
|
461
|
+
return _invalid_directive_syntax(
|
|
462
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
463
|
+
directive_kind=DirectiveKind.REPLACE_USE,
|
|
464
|
+
missing_operand="old_item",
|
|
465
|
+
)
|
|
466
|
+
if _INSTEAD_OF_DELIMITER in normalized_item:
|
|
467
|
+
return _invalid_directive_syntax(
|
|
468
|
+
DirectiveSyntaxFailure.MALFORMED_DIRECTIVE,
|
|
469
|
+
directive_kind=DirectiveKind.USE_ITEM,
|
|
470
|
+
)
|
|
364
471
|
return CanonicalDirective(
|
|
365
472
|
text=text,
|
|
366
473
|
kind=DirectiveKind.USE_ITEM,
|
|
367
474
|
operands=MappingProxyType({"item": item}),
|
|
368
475
|
)
|
|
369
476
|
|
|
477
|
+
if normalized == "prohibit":
|
|
478
|
+
return _invalid_directive_syntax(
|
|
479
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
480
|
+
directive_kind=DirectiveKind.PROHIBIT_ITEM,
|
|
481
|
+
missing_operand="item",
|
|
482
|
+
)
|
|
483
|
+
|
|
370
484
|
if normalized.startswith("prohibit "):
|
|
371
485
|
match = _PROHIBIT_RE.fullmatch(trimmed_text)
|
|
372
486
|
if match is None:
|
|
373
|
-
return
|
|
487
|
+
return _invalid_directive_syntax(
|
|
488
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
489
|
+
directive_kind=DirectiveKind.PROHIBIT_ITEM,
|
|
490
|
+
missing_operand="item",
|
|
491
|
+
)
|
|
374
492
|
item = match.group("item")
|
|
375
493
|
if not _operand_has_content(item):
|
|
376
|
-
return
|
|
494
|
+
return _invalid_directive_syntax(
|
|
495
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
496
|
+
directive_kind=DirectiveKind.PROHIBIT_ITEM,
|
|
497
|
+
missing_operand="item",
|
|
498
|
+
)
|
|
377
499
|
return CanonicalDirective(
|
|
378
500
|
text=text,
|
|
379
501
|
kind=DirectiveKind.PROHIBIT_ITEM,
|
|
380
502
|
operands=MappingProxyType({"item": item}),
|
|
381
503
|
)
|
|
382
504
|
|
|
505
|
+
if normalized == "remove policy":
|
|
506
|
+
return _invalid_directive_syntax(
|
|
507
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
508
|
+
directive_kind=DirectiveKind.REMOVE_POLICY,
|
|
509
|
+
missing_operand="item",
|
|
510
|
+
)
|
|
511
|
+
|
|
383
512
|
if normalized.startswith("remove policy "):
|
|
384
513
|
match = _REMOVE_POLICY_RE.fullmatch(trimmed_text)
|
|
385
514
|
if match is None:
|
|
386
|
-
return
|
|
515
|
+
return _invalid_directive_syntax(
|
|
516
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
517
|
+
directive_kind=DirectiveKind.REMOVE_POLICY,
|
|
518
|
+
missing_operand="item",
|
|
519
|
+
)
|
|
387
520
|
item = match.group("item")
|
|
388
521
|
if not _operand_has_content(item):
|
|
389
|
-
return
|
|
522
|
+
return _invalid_directive_syntax(
|
|
523
|
+
DirectiveSyntaxFailure.MISSING_REQUIRED_OPERAND,
|
|
524
|
+
directive_kind=DirectiveKind.REMOVE_POLICY,
|
|
525
|
+
missing_operand="item",
|
|
526
|
+
)
|
|
390
527
|
return CanonicalDirective(
|
|
391
528
|
text=text,
|
|
392
529
|
kind=DirectiveKind.REMOVE_POLICY,
|
|
393
530
|
operands=MappingProxyType({"item": item}),
|
|
394
531
|
)
|
|
395
532
|
|
|
396
|
-
return
|
|
397
|
-
|
|
533
|
+
return _invalid_directive_syntax(DirectiveSyntaxFailure.MALFORMED_DIRECTIVE)
|
|
398
534
|
|
|
399
|
-
def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
|
|
400
|
-
"""Produce canonical directive text from a semantic kind and operands.
|
|
401
535
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
requested directive kind. It does not infer missing operands, parse user
|
|
405
|
-
input, or extend the grammar with new behaviors.
|
|
406
|
-
"""
|
|
536
|
+
def _render_directive(kind: DirectiveKind | str, /, **operands: str) -> str:
|
|
537
|
+
"""Produce canonical directive text from a semantic kind and operands."""
|
|
407
538
|
try:
|
|
408
539
|
normalized_kind = kind if isinstance(kind, DirectiveKind) else DirectiveKind(kind)
|
|
409
540
|
spec = _DIRECTIVE_SPECS[normalized_kind]
|
|
@@ -416,33 +547,32 @@ def render_directive(kind: DirectiveKind, /, **operands: str) -> str:
|
|
|
416
547
|
missing_names = expected_names - actual_names
|
|
417
548
|
if missing_names:
|
|
418
549
|
missing = ", ".join(sorted(missing_names))
|
|
419
|
-
raise ValueError(f"Missing required operands for {
|
|
550
|
+
raise ValueError(f"Missing required operands for {normalized_kind.value}: {missing}")
|
|
420
551
|
if unexpected_names:
|
|
421
552
|
unexpected = ", ".join(sorted(unexpected_names))
|
|
422
|
-
raise ValueError(f"Unexpected operands for {
|
|
553
|
+
raise ValueError(f"Unexpected operands for {normalized_kind.value}: {unexpected}")
|
|
423
554
|
|
|
424
555
|
normalized_operands: dict[str, str] = {}
|
|
425
556
|
for name in spec.operand_names:
|
|
426
557
|
raw_value = operands[name]
|
|
427
558
|
if not isinstance(raw_value, str):
|
|
428
|
-
raise ValueError(f"Operand {name!r} for {
|
|
559
|
+
raise ValueError(f"Operand {name!r} for {normalized_kind.value} must be a string.")
|
|
429
560
|
if raw_value.strip() == "":
|
|
430
|
-
raise ValueError(f"Operand {name!r} for {
|
|
561
|
+
raise ValueError(f"Operand {name!r} for {normalized_kind.value} cannot be empty.")
|
|
431
562
|
normalized_operands[name] = raw_value
|
|
432
563
|
|
|
433
564
|
operand_view = MappingProxyType(normalized_operands)
|
|
434
565
|
rendered = spec.renderer(operand_view)
|
|
435
566
|
decomposed = decompose_directive(rendered)
|
|
436
|
-
if decomposed
|
|
567
|
+
if not isinstance(decomposed, CanonicalDirective) or decomposed.kind is not normalized_kind:
|
|
437
568
|
raise ValueError(f"Operands do not produce a canonical {normalized_kind.value} directive.")
|
|
438
569
|
return rendered
|
|
439
570
|
|
|
440
571
|
|
|
441
572
|
__all__ = [
|
|
442
|
-
"CanonicalDirective",
|
|
443
573
|
"DirectiveKind",
|
|
444
|
-
"
|
|
574
|
+
"DirectiveSyntaxFailure",
|
|
575
|
+
"CanonicalDirective",
|
|
576
|
+
"InvalidDirectiveSyntax",
|
|
445
577
|
"decompose_directive",
|
|
446
|
-
"match_canonical_directive_start",
|
|
447
|
-
"render_directive",
|
|
448
578
|
]
|
|
@@ -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.dev9}/src/context_compiler/decision_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|