context-compiler 0.9.0.dev9__tar.gz → 0.9.0.dev11__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.dev9 → context_compiler-0.9.0.dev11}/PKG-INFO +6 -7
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/README.md +5 -6
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/08_llm_replacement_precondition.py +24 -24
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/09_llm_confirmation_no_directive.py +24 -27
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/pyproject.toml +1 -1
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/engine.py +11 -2
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/grammar.py +193 -86
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/.gitignore +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/LICENSE +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/01_llm_contradiction_error.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/02_llm_constraint_guardrail.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/03_llm_premise_guardrail.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/04_llm_tool_denylist_guardrail.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/05_llm_prompt_drift_vs_state.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/06_llm_context_compaction.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/07_llm_prompt_vs_state.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/README.md +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/__init__.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/common.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/llm_client.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/run_demo.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/01_persistent_guardrails.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/02_configuration_and_correction.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/03_ambiguity_with_error.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/04_tool_governance_denylist.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/05_llm_integration_pattern.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/06_step_sequence_and_state_restore.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/07_single_policy_correction.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/README.md +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/_util.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/host_support/__init__.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/host_support/provider_mode.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/__init__.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/const.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/decision_helpers.py +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/py.typed +0 -0
- {context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/repl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: context-compiler
|
|
3
|
-
Version: 0.9.0.
|
|
3
|
+
Version: 0.9.0.dev11
|
|
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
|
|
@@ -414,10 +414,9 @@ Replacement:
|
|
|
414
414
|
User: use podman instead of docker
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
-
If `docker` is absent from saved state, that
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
semantically valid.
|
|
417
|
+
If `docker` is absent from saved state, that is a semantic `error`.
|
|
418
|
+
Canonical replacement requires an active existing source `use` policy and
|
|
419
|
+
does not degrade to plain `use podman`.
|
|
421
420
|
|
|
422
421
|
Removal and reset:
|
|
423
422
|
|
|
@@ -435,8 +434,8 @@ evaluation against authoritative state.
|
|
|
435
434
|
Pending continuation is a separate runtime layer. It may exist only after a
|
|
436
435
|
canonical directive reaches a supported semantic `error` case. It never
|
|
437
436
|
repairs malformed syntax or reinterprets non-canonical input as a directive.
|
|
438
|
-
An absent source item in a canonical replacement directive is
|
|
439
|
-
|
|
437
|
+
An absent source item in a canonical replacement directive is itself a
|
|
438
|
+
semantic `error` case and does not authorize degradation to plain `use`.
|
|
440
439
|
|
|
441
440
|
Examples:
|
|
442
441
|
|
|
@@ -392,10 +392,9 @@ Replacement:
|
|
|
392
392
|
User: use podman instead of docker
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
-
If `docker` is absent from saved state, that
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
semantically valid.
|
|
395
|
+
If `docker` is absent from saved state, that is a semantic `error`.
|
|
396
|
+
Canonical replacement requires an active existing source `use` policy and
|
|
397
|
+
does not degrade to plain `use podman`.
|
|
399
398
|
|
|
400
399
|
Removal and reset:
|
|
401
400
|
|
|
@@ -413,8 +412,8 @@ evaluation against authoritative state.
|
|
|
413
412
|
Pending continuation is a separate runtime layer. It may exist only after a
|
|
414
413
|
canonical directive reaches a supported semantic `error` case. It never
|
|
415
414
|
repairs malformed syntax or reinterprets non-canonical input as a directive.
|
|
416
|
-
An absent source item in a canonical replacement directive is
|
|
417
|
-
|
|
415
|
+
An absent source item in a canonical replacement directive is itself a
|
|
416
|
+
semantic `error` case and does not authorize degradation to plain `use`.
|
|
418
417
|
|
|
419
418
|
Examples:
|
|
420
419
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Demo 8: missing-source replacement
|
|
1
|
+
"""Demo 8: missing-source replacement fails without mutating authoritative state."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
|
|
5
5
|
from context_compiler import (
|
|
6
6
|
Engine,
|
|
7
|
-
|
|
7
|
+
is_error,
|
|
8
8
|
)
|
|
9
9
|
from demos.common import (
|
|
10
10
|
build_baseline_messages,
|
|
@@ -22,7 +22,7 @@ from demos.common import (
|
|
|
22
22
|
)
|
|
23
23
|
from demos.llm_client import complete_messages
|
|
24
24
|
|
|
25
|
-
DEMO_NAME = "08_replacement_precondition — missing-source replacement
|
|
25
|
+
DEMO_NAME = "08_replacement_precondition — missing-source replacement requires active source"
|
|
26
26
|
USER_INPUT = "use podman instead of docker"
|
|
27
27
|
|
|
28
28
|
|
|
@@ -69,38 +69,38 @@ def main() -> None:
|
|
|
69
69
|
reinjected_output = complete_messages(reinjected_messages)
|
|
70
70
|
print_model_output("Reinjected-state", reinjected_output)
|
|
71
71
|
|
|
72
|
-
if
|
|
72
|
+
if is_error(decision):
|
|
73
73
|
print_messages("compiler-mediated (full)", [])
|
|
74
|
-
mediated_output = "[no call] authoritative state
|
|
74
|
+
mediated_output = "[no call] authoritative state blocked replacement without source use"
|
|
75
75
|
print_model_output("Compiler-mediated (full)", mediated_output)
|
|
76
76
|
else:
|
|
77
77
|
print_messages("compiler-mediated (full)", [])
|
|
78
|
-
mediated_output = "[no call] expected
|
|
78
|
+
mediated_output = "[no call] expected semantic replacement error was not produced"
|
|
79
79
|
print_model_output("Compiler-mediated (full)", mediated_output)
|
|
80
80
|
|
|
81
81
|
compacted_turns, compacted_state, compacted_prompt = compact_user_turns(user_inputs)
|
|
82
|
-
if compacted_prompt is None:
|
|
82
|
+
if compacted_prompt is not None:
|
|
83
83
|
print_messages("compiler-mediated + compact", [])
|
|
84
|
-
compact_output = "[no call]
|
|
84
|
+
compact_output = "[no call] unexpected error was produced during compaction"
|
|
85
85
|
print_model_output("Compiler-mediated + compact", compact_output)
|
|
86
86
|
else:
|
|
87
87
|
print_messages("compiler-mediated + compact", [])
|
|
88
|
-
compact_output = "[no call]
|
|
88
|
+
compact_output = "[no call] compaction preserved replacement error without state mutation"
|
|
89
89
|
print_model_output("Compiler-mediated + compact", compact_output)
|
|
90
90
|
|
|
91
91
|
premise, policies = observe_engine(engine)
|
|
92
92
|
compacted_premise, compacted_policies = state_observations(compacted_state)
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
state_preserved = _is_initial_authoritative_state(premise=premise, policies=policies)
|
|
94
|
+
compact_state_preserved = _is_initial_authoritative_state(
|
|
95
95
|
premise=compacted_premise,
|
|
96
96
|
policies=compacted_policies,
|
|
97
97
|
)
|
|
98
|
-
|
|
98
|
+
compact_error_preserved = compacted_prompt is not None
|
|
99
99
|
|
|
100
100
|
baseline_has_authoritative_precondition = False
|
|
101
101
|
reinjected_has_authoritative_precondition = False
|
|
102
|
-
compiler_pass =
|
|
103
|
-
compact_pass =
|
|
102
|
+
compiler_pass = is_error(decision) and state_preserved
|
|
103
|
+
compact_pass = compact_error_preserved and compact_state_preserved
|
|
104
104
|
|
|
105
105
|
print_host_check(
|
|
106
106
|
"BASELINE_AUTHORITATIVE_PRECONDITION",
|
|
@@ -114,12 +114,12 @@ def main() -> None:
|
|
|
114
114
|
)
|
|
115
115
|
print_host_check(
|
|
116
116
|
"COMPILER_BLOCKED_INVALID_REPLACEMENT",
|
|
117
|
-
yes_no(
|
|
117
|
+
yes_no(is_error(decision)),
|
|
118
118
|
context="compiler-mediated",
|
|
119
119
|
)
|
|
120
120
|
print_host_check(
|
|
121
|
-
"
|
|
122
|
-
yes_no(
|
|
121
|
+
"COMPILER_STATE_PRESERVED",
|
|
122
|
+
yes_no(state_preserved),
|
|
123
123
|
context="compiler-mediated",
|
|
124
124
|
)
|
|
125
125
|
|
|
@@ -130,18 +130,18 @@ def main() -> None:
|
|
|
130
130
|
compiler_pass=compiler_pass,
|
|
131
131
|
compiler_compact_pass=compact_pass,
|
|
132
132
|
expected=(
|
|
133
|
-
"missing-source replacement should
|
|
134
|
-
"
|
|
133
|
+
"missing-source replacement should return semantic error without mutating "
|
|
134
|
+
"authoritative state"
|
|
135
135
|
),
|
|
136
136
|
actual=(
|
|
137
|
-
"compiler
|
|
138
|
-
"still lack authoritative state enforcement"
|
|
137
|
+
"compiler blocked missing-source replacement without mutating state; baseline and "
|
|
138
|
+
"reinjected paths still lack authoritative state enforcement"
|
|
139
139
|
if compiler_pass and compact_pass
|
|
140
|
-
else "compiler did not consistently
|
|
140
|
+
else "compiler did not consistently enforce the replacement source precondition"
|
|
141
141
|
),
|
|
142
142
|
passed=compiler_pass and compact_pass,
|
|
143
|
-
result_pass="missing-source replacement
|
|
144
|
-
result_fail="missing-source replacement
|
|
143
|
+
result_pass="missing-source replacement was rejected without mutation",
|
|
144
|
+
result_fail="missing-source replacement was not rejected correctly",
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Demo 9: confirmation-style
|
|
1
|
+
"""Demo 9: replacement errors do not create confirmation-style followup state."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
|
|
5
5
|
from context_compiler import (
|
|
6
6
|
Engine,
|
|
7
|
+
is_error,
|
|
7
8
|
is_no_directive,
|
|
8
|
-
is_update,
|
|
9
9
|
)
|
|
10
10
|
from demos.common import (
|
|
11
11
|
build_baseline_messages,
|
|
@@ -24,8 +24,7 @@ from demos.common import (
|
|
|
24
24
|
from demos.llm_client import complete_messages
|
|
25
25
|
|
|
26
26
|
DEMO_NAME = (
|
|
27
|
-
"09_confirmation_no_directive_boundary — "
|
|
28
|
-
"missing-source replacement does not create a confirmation state"
|
|
27
|
+
"09_confirmation_no_directive_boundary — replacement errors do not create a confirmation state"
|
|
29
28
|
)
|
|
30
29
|
TURN_1 = "use podman instead of docker"
|
|
31
30
|
TURN_2 = "maybe"
|
|
@@ -34,10 +33,6 @@ INITIAL_PREMISE: str | None = None
|
|
|
34
33
|
INITIAL_POLICIES: dict[str, str] = {}
|
|
35
34
|
|
|
36
35
|
|
|
37
|
-
def _has_podman_use(policies: Mapping[str, str]) -> bool:
|
|
38
|
-
return policies.get("podman") == "use"
|
|
39
|
-
|
|
40
|
-
|
|
41
36
|
def _is_initial_authoritative_state(*, premise: str | None, policies: Mapping[str, str]) -> bool:
|
|
42
37
|
return premise == INITIAL_PREMISE and dict(policies) == INITIAL_POLICIES
|
|
43
38
|
|
|
@@ -50,12 +45,17 @@ def main() -> None:
|
|
|
50
45
|
first = engine.step(TURN_1)
|
|
51
46
|
premise, policies = observe_engine(engine)
|
|
52
47
|
print_decision("turn 1", first, premise=premise, policies=policies)
|
|
53
|
-
|
|
48
|
+
state_preserved_after_first = _is_initial_authoritative_state(
|
|
49
|
+
premise=premise, policies=policies
|
|
50
|
+
)
|
|
54
51
|
|
|
55
52
|
second = engine.step(TURN_2)
|
|
56
53
|
premise, policies = observe_engine(engine)
|
|
57
54
|
print_decision("turn 2", second, premise=premise, policies=policies)
|
|
58
|
-
state_preserved_after_second =
|
|
55
|
+
state_preserved_after_second = _is_initial_authoritative_state(
|
|
56
|
+
premise=premise,
|
|
57
|
+
policies=policies,
|
|
58
|
+
)
|
|
59
59
|
|
|
60
60
|
third = engine.step(TURN_3)
|
|
61
61
|
premise, policies = observe_engine(engine)
|
|
@@ -96,7 +96,7 @@ def main() -> None:
|
|
|
96
96
|
compacted_turns, compacted_state, compacted_prompt = compact_user_turns(user_inputs)
|
|
97
97
|
if compacted_prompt is not None:
|
|
98
98
|
print_messages("compiler-mediated + compact", [])
|
|
99
|
-
compact_output = f"[no call] error
|
|
99
|
+
compact_output = f"[no call] replacement error preserved: {compacted_prompt}"
|
|
100
100
|
print_model_output("Compiler-mediated + compact", compact_output)
|
|
101
101
|
else:
|
|
102
102
|
print_messages("compiler-mediated + compact", [])
|
|
@@ -105,32 +105,30 @@ def main() -> None:
|
|
|
105
105
|
)
|
|
106
106
|
print_model_output("Compiler-mediated + compact", compact_output)
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
deterministic_initial_error = is_error(first) and state_preserved_after_first
|
|
109
109
|
unrelated_followup_no_directive = is_no_directive(second) and state_preserved_after_second
|
|
110
110
|
confirmation_token_not_consumed = is_no_directive(third)
|
|
111
111
|
premise, policies = observe_engine(engine)
|
|
112
|
-
deterministic_final_state =
|
|
112
|
+
deterministic_final_state = _is_initial_authoritative_state(premise=premise, policies=policies)
|
|
113
113
|
_, compacted_policies = state_observations(compacted_state)
|
|
114
114
|
|
|
115
115
|
baseline_has_confirmation_state_machine = False
|
|
116
116
|
reinjected_has_confirmation_state_machine = False
|
|
117
117
|
|
|
118
118
|
compiler_pass = (
|
|
119
|
-
|
|
119
|
+
deterministic_initial_error
|
|
120
120
|
and unrelated_followup_no_directive
|
|
121
121
|
and confirmation_token_not_consumed
|
|
122
122
|
and deterministic_final_state
|
|
123
123
|
)
|
|
124
124
|
|
|
125
125
|
compact_pass = (
|
|
126
|
-
compacted_prompt is None
|
|
127
|
-
and compacted_turns == [TURN_2, TURN_3]
|
|
128
|
-
and _has_podman_use(compacted_policies)
|
|
126
|
+
compacted_prompt is not None and compacted_turns == [TURN_1] and compacted_policies == {}
|
|
129
127
|
)
|
|
130
128
|
|
|
131
129
|
print_host_check(
|
|
132
|
-
"
|
|
133
|
-
yes_no(
|
|
130
|
+
"DETERMINISTIC_INITIAL_ERROR",
|
|
131
|
+
yes_no(deterministic_initial_error),
|
|
134
132
|
context="compiler-mediated",
|
|
135
133
|
)
|
|
136
134
|
print_host_check(
|
|
@@ -144,7 +142,7 @@ def main() -> None:
|
|
|
144
142
|
context="compiler-mediated",
|
|
145
143
|
)
|
|
146
144
|
print_host_check(
|
|
147
|
-
"
|
|
145
|
+
"FINAL_STATE_UNCHANGED",
|
|
148
146
|
yes_no(deterministic_final_state),
|
|
149
147
|
context="compiler-mediated",
|
|
150
148
|
)
|
|
@@ -156,19 +154,18 @@ def main() -> None:
|
|
|
156
154
|
compiler_pass=compiler_pass,
|
|
157
155
|
compiler_compact_pass=compact_pass,
|
|
158
156
|
expected=(
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"no_directive"
|
|
157
|
+
"replacement error should not create an engine-owned confirmation state, "
|
|
158
|
+
"and later yes/no-style input should remain ordinary no_directive"
|
|
162
159
|
),
|
|
163
160
|
actual=(
|
|
164
|
-
"compiler
|
|
165
|
-
"
|
|
161
|
+
"compiler returned semantic error and treated later inputs as ordinary "
|
|
162
|
+
"no_directive without mutating state"
|
|
166
163
|
if compiler_pass and compact_pass
|
|
167
164
|
else "compiler did not consistently preserve the confirmation-no_directive boundary"
|
|
168
165
|
),
|
|
169
166
|
passed=compiler_pass and compact_pass,
|
|
170
|
-
result_pass="
|
|
171
|
-
result_fail="
|
|
167
|
+
result_pass="replacement error stayed outside engine-owned confirmation state",
|
|
168
|
+
result_fail="replacement error still behaved like engine-owned confirmation state",
|
|
172
169
|
)
|
|
173
170
|
|
|
174
171
|
|
|
@@ -176,7 +176,7 @@ class Engine:
|
|
|
176
176
|
f'"{new_item}" is currently prohibited.\n'
|
|
177
177
|
"Submit explicit directive(s) to remove it or use a different item."
|
|
178
178
|
)
|
|
179
|
-
if old_state
|
|
179
|
+
if old_state != POLICY_USE:
|
|
180
180
|
return _error(
|
|
181
181
|
f'"{old_item}" is not currently in use.\n'
|
|
182
182
|
"Replacement requires an active 'use' policy."
|
|
@@ -276,7 +276,12 @@ def _load_state_obj(raw: object) -> _State:
|
|
|
276
276
|
if not isinstance(policies, dict):
|
|
277
277
|
raise ValueError("Invalid state payload.")
|
|
278
278
|
|
|
279
|
+
sanitized_premise = None if premise is None else _sanitize_premise_value(premise)
|
|
280
|
+
if premise is not None and sanitized_premise == "":
|
|
281
|
+
raise ValueError("Invalid state payload.")
|
|
282
|
+
|
|
279
283
|
normalized_policies: dict[str, PolicyValue] = {}
|
|
284
|
+
seen_policy_sources: dict[str, str] = {}
|
|
280
285
|
for key, value in policies.items():
|
|
281
286
|
if not isinstance(key, str):
|
|
282
287
|
raise ValueError("Invalid state payload.")
|
|
@@ -285,10 +290,14 @@ def _load_state_obj(raw: object) -> _State:
|
|
|
285
290
|
normalized_key = _normalize_item(key)
|
|
286
291
|
if normalized_key == "":
|
|
287
292
|
raise ValueError("Invalid state payload.")
|
|
293
|
+
prior_source = seen_policy_sources.get(normalized_key)
|
|
294
|
+
if prior_source is not None and prior_source != key:
|
|
295
|
+
raise ValueError("Invalid state payload.")
|
|
296
|
+
seen_policy_sources[normalized_key] = key
|
|
288
297
|
normalized_policies[normalized_key] = value
|
|
289
298
|
|
|
290
299
|
return {
|
|
291
|
-
STATE_PREMISE:
|
|
300
|
+
STATE_PREMISE: sanitized_premise,
|
|
292
301
|
STATE_POLICIES: dict(sorted(normalized_policies.items())),
|
|
293
302
|
STATE_VERSION: SCHEMA_VERSION,
|
|
294
303
|
}
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/grammar.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Immutable canonical grammar helpers for Context Compiler directives."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from collections.abc import Callable
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from enum import StrEnum
|
|
7
7
|
from types import MappingProxyType
|
|
@@ -33,14 +33,24 @@ class DirectiveSyntaxFailure(StrEnum):
|
|
|
33
33
|
class CanonicalDirective:
|
|
34
34
|
"""Represent one parsed canonical directive and its named operands.
|
|
35
35
|
|
|
36
|
-
``text``
|
|
37
|
-
|
|
36
|
+
``text`` is the canonical serialized directive text derived from ``kind``
|
|
37
|
+
and ``operands``.
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
text: str
|
|
41
40
|
kind: DirectiveKind
|
|
42
41
|
operands: MappingProxyType[str, str]
|
|
43
42
|
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
normalized_kind = _normalize_directive_kind(self.kind)
|
|
45
|
+
normalized_operands = _normalize_canonical_operands(normalized_kind, self.operands)
|
|
46
|
+
object.__setattr__(self, "kind", normalized_kind)
|
|
47
|
+
object.__setattr__(self, "operands", MappingProxyType(normalized_operands))
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def text(self) -> str:
|
|
51
|
+
"""Return the canonical serialized directive text."""
|
|
52
|
+
return _serialize_canonical_directive(self.kind, self.operands)
|
|
53
|
+
|
|
44
54
|
|
|
45
55
|
@dataclass(frozen=True, slots=True)
|
|
46
56
|
class InvalidDirectiveSyntax:
|
|
@@ -54,16 +64,31 @@ class InvalidDirectiveSyntax:
|
|
|
54
64
|
@dataclass(frozen=True, slots=True)
|
|
55
65
|
class _DirectiveSpec:
|
|
56
66
|
kind: DirectiveKind
|
|
67
|
+
canonical_start: str
|
|
57
68
|
operand_names: tuple[str, ...]
|
|
58
69
|
exact_text: str | None
|
|
59
70
|
renderer: Callable[[MappingProxyType[str, str]], str]
|
|
60
71
|
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class DirectiveMetadata:
|
|
75
|
+
"""Describe one canonical directive family without exposing parser internals."""
|
|
76
|
+
|
|
77
|
+
kind: DirectiveKind
|
|
78
|
+
canonical_start: str
|
|
79
|
+
operand_names: tuple[str, ...]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_SET_PREMISE_START = "set premise"
|
|
83
|
+
_CHANGE_PREMISE_START = "change premise to"
|
|
84
|
+
_USE_START = "use"
|
|
85
|
+
_PROHIBIT_START = "prohibit"
|
|
86
|
+
_REMOVE_POLICY_START = "remove policy"
|
|
87
|
+
_SET_PREMISE_PREFIX = f"{_SET_PREMISE_START} "
|
|
88
|
+
_CHANGE_PREMISE_PREFIX = f"{_CHANGE_PREMISE_START} "
|
|
89
|
+
_USE_PREFIX = f"{_USE_START} "
|
|
90
|
+
_PROHIBIT_PREFIX = f"{_PROHIBIT_START} "
|
|
91
|
+
_REMOVE_POLICY_PREFIX = f"{_REMOVE_POLICY_START} "
|
|
67
92
|
_CLEAR_PREMISE_TEXT = "clear premise"
|
|
68
93
|
_RESET_POLICIES_TEXT = "reset policies"
|
|
69
94
|
_CLEAR_STATE_TEXT = "clear state"
|
|
@@ -81,39 +106,6 @@ _REPLACE_RE = re.compile(
|
|
|
81
106
|
r"(?i)^use[ \t]+(?P<new_item>.*?)[ \t]+instead[ \t]+of[ \t]+(?P<old_item>.+)$"
|
|
82
107
|
)
|
|
83
108
|
|
|
84
|
-
_PREFIX_DIRECTIVE_STARTS: tuple[tuple[str, bool], ...] = (
|
|
85
|
-
(_CHANGE_PREMISE_PREFIX.removesuffix(" "), True),
|
|
86
|
-
(_SET_PREMISE_PREFIX.removesuffix(" "), True),
|
|
87
|
-
(_REMOVE_POLICY_PREFIX.removesuffix(" "), True),
|
|
88
|
-
(_PROHIBIT_PREFIX.removesuffix(" "), True),
|
|
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:],
|
|
115
|
-
)
|
|
116
|
-
|
|
117
109
|
|
|
118
110
|
def _render_with_prefix(
|
|
119
111
|
prefix: str, operand_name: str
|
|
@@ -140,54 +132,63 @@ _DIRECTIVE_SPECS = MappingProxyType(
|
|
|
140
132
|
{
|
|
141
133
|
DirectiveKind.SET_PREMISE: _DirectiveSpec(
|
|
142
134
|
kind=DirectiveKind.SET_PREMISE,
|
|
135
|
+
canonical_start=_SET_PREMISE_START,
|
|
143
136
|
operand_names=("value",),
|
|
144
137
|
exact_text=None,
|
|
145
138
|
renderer=_render_with_prefix(_SET_PREMISE_PREFIX, "value"),
|
|
146
139
|
),
|
|
147
140
|
DirectiveKind.CHANGE_PREMISE: _DirectiveSpec(
|
|
148
141
|
kind=DirectiveKind.CHANGE_PREMISE,
|
|
142
|
+
canonical_start=_CHANGE_PREMISE_START,
|
|
149
143
|
operand_names=("value",),
|
|
150
144
|
exact_text=None,
|
|
151
145
|
renderer=_render_with_prefix(_CHANGE_PREMISE_PREFIX, "value"),
|
|
152
146
|
),
|
|
153
147
|
DirectiveKind.USE_ITEM: _DirectiveSpec(
|
|
154
148
|
kind=DirectiveKind.USE_ITEM,
|
|
149
|
+
canonical_start=_USE_START,
|
|
155
150
|
operand_names=("item",),
|
|
156
151
|
exact_text=None,
|
|
157
152
|
renderer=_render_with_prefix(_USE_PREFIX, "item"),
|
|
158
153
|
),
|
|
159
154
|
DirectiveKind.PROHIBIT_ITEM: _DirectiveSpec(
|
|
160
155
|
kind=DirectiveKind.PROHIBIT_ITEM,
|
|
156
|
+
canonical_start=_PROHIBIT_START,
|
|
161
157
|
operand_names=("item",),
|
|
162
158
|
exact_text=None,
|
|
163
159
|
renderer=_render_with_prefix(_PROHIBIT_PREFIX, "item"),
|
|
164
160
|
),
|
|
165
161
|
DirectiveKind.REMOVE_POLICY: _DirectiveSpec(
|
|
166
162
|
kind=DirectiveKind.REMOVE_POLICY,
|
|
163
|
+
canonical_start=_REMOVE_POLICY_START,
|
|
167
164
|
operand_names=("item",),
|
|
168
165
|
exact_text=None,
|
|
169
166
|
renderer=_render_with_prefix(_REMOVE_POLICY_PREFIX, "item"),
|
|
170
167
|
),
|
|
171
168
|
DirectiveKind.REPLACE_USE: _DirectiveSpec(
|
|
172
169
|
kind=DirectiveKind.REPLACE_USE,
|
|
170
|
+
canonical_start=_USE_START,
|
|
173
171
|
operand_names=("new_item", "old_item"),
|
|
174
172
|
exact_text=None,
|
|
175
173
|
renderer=_render_replace_use,
|
|
176
174
|
),
|
|
177
175
|
DirectiveKind.CLEAR_PREMISE: _DirectiveSpec(
|
|
178
176
|
kind=DirectiveKind.CLEAR_PREMISE,
|
|
177
|
+
canonical_start=_CLEAR_PREMISE_TEXT,
|
|
179
178
|
operand_names=(),
|
|
180
179
|
exact_text=_CLEAR_PREMISE_TEXT,
|
|
181
180
|
renderer=_render_exact(_CLEAR_PREMISE_TEXT),
|
|
182
181
|
),
|
|
183
182
|
DirectiveKind.RESET_POLICIES: _DirectiveSpec(
|
|
184
183
|
kind=DirectiveKind.RESET_POLICIES,
|
|
184
|
+
canonical_start=_RESET_POLICIES_TEXT,
|
|
185
185
|
operand_names=(),
|
|
186
186
|
exact_text=_RESET_POLICIES_TEXT,
|
|
187
187
|
renderer=_render_exact(_RESET_POLICIES_TEXT),
|
|
188
188
|
),
|
|
189
189
|
DirectiveKind.CLEAR_STATE: _DirectiveSpec(
|
|
190
190
|
kind=DirectiveKind.CLEAR_STATE,
|
|
191
|
+
canonical_start=_CLEAR_STATE_TEXT,
|
|
191
192
|
operand_names=(),
|
|
192
193
|
exact_text=_CLEAR_STATE_TEXT,
|
|
193
194
|
renderer=_render_exact(_CLEAR_STATE_TEXT),
|
|
@@ -196,6 +197,64 @@ _DIRECTIVE_SPECS = MappingProxyType(
|
|
|
196
197
|
)
|
|
197
198
|
|
|
198
199
|
|
|
200
|
+
def _starts_with_descriptor(spec: _DirectiveSpec) -> tuple[str, bool]:
|
|
201
|
+
return (spec.canonical_start, bool(spec.operand_names))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _unique_start_descriptors(specs: tuple[_DirectiveSpec, ...]) -> tuple[tuple[str, bool], ...]:
|
|
205
|
+
descriptors: list[tuple[str, bool]] = []
|
|
206
|
+
seen: set[tuple[str, bool]] = set()
|
|
207
|
+
for spec in specs:
|
|
208
|
+
descriptor = _starts_with_descriptor(spec)
|
|
209
|
+
if descriptor not in seen:
|
|
210
|
+
seen.add(descriptor)
|
|
211
|
+
descriptors.append(descriptor)
|
|
212
|
+
return tuple(descriptors)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_CANONICAL_START_ORDER = (
|
|
216
|
+
DirectiveKind.CHANGE_PREMISE,
|
|
217
|
+
DirectiveKind.SET_PREMISE,
|
|
218
|
+
DirectiveKind.REMOVE_POLICY,
|
|
219
|
+
DirectiveKind.RESET_POLICIES,
|
|
220
|
+
DirectiveKind.CLEAR_PREMISE,
|
|
221
|
+
DirectiveKind.CLEAR_STATE,
|
|
222
|
+
DirectiveKind.PROHIBIT_ITEM,
|
|
223
|
+
DirectiveKind.USE_ITEM,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
_CANONICAL_DIRECTIVE_STARTS = _unique_start_descriptors(
|
|
227
|
+
tuple(_DIRECTIVE_SPECS[kind] for kind in _CANONICAL_START_ORDER)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
_DIRECTIVE_FAMILY_STARTS = (
|
|
231
|
+
(_CHANGE_PREMISE_FAMILY, True),
|
|
232
|
+
*_unique_start_descriptors(
|
|
233
|
+
tuple(
|
|
234
|
+
_DIRECTIVE_SPECS[kind]
|
|
235
|
+
for kind in (
|
|
236
|
+
DirectiveKind.SET_PREMISE,
|
|
237
|
+
DirectiveKind.REMOVE_POLICY,
|
|
238
|
+
DirectiveKind.RESET_POLICIES,
|
|
239
|
+
DirectiveKind.CLEAR_PREMISE,
|
|
240
|
+
DirectiveKind.CLEAR_STATE,
|
|
241
|
+
DirectiveKind.PROHIBIT_ITEM,
|
|
242
|
+
DirectiveKind.USE_ITEM,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
_PUBLIC_DIRECTIVE_METADATA = tuple(
|
|
249
|
+
DirectiveMetadata(
|
|
250
|
+
kind=spec.kind,
|
|
251
|
+
canonical_start=spec.canonical_start,
|
|
252
|
+
operand_names=spec.operand_names,
|
|
253
|
+
)
|
|
254
|
+
for spec in _DIRECTIVE_SPECS.values()
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
199
258
|
def _trim_ascii_whitespace(text: str) -> str:
|
|
200
259
|
return text.strip(_ASCII_WHITESPACE)
|
|
201
260
|
|
|
@@ -316,7 +375,6 @@ def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
|
|
|
316
375
|
if normalized_payload.count(_INSTEAD_OF_DELIMITER) != 1:
|
|
317
376
|
return None
|
|
318
377
|
return CanonicalDirective(
|
|
319
|
-
text=trimmed_text,
|
|
320
378
|
kind=DirectiveKind.REPLACE_USE,
|
|
321
379
|
operands=MappingProxyType({"new_item": new_item, "old_item": old_item}),
|
|
322
380
|
)
|
|
@@ -335,6 +393,74 @@ def _invalid_directive_syntax(
|
|
|
335
393
|
)
|
|
336
394
|
|
|
337
395
|
|
|
396
|
+
def _normalize_directive_kind(kind: DirectiveKind | str) -> DirectiveKind:
|
|
397
|
+
try:
|
|
398
|
+
return kind if isinstance(kind, DirectiveKind) else DirectiveKind(kind)
|
|
399
|
+
except ValueError as exc:
|
|
400
|
+
raise ValueError(f"Unsupported directive kind: {kind!r}") from exc
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _normalize_canonical_operands(
|
|
404
|
+
kind: DirectiveKind,
|
|
405
|
+
operands: Mapping[str, str],
|
|
406
|
+
) -> dict[str, str]:
|
|
407
|
+
spec = _DIRECTIVE_SPECS[kind]
|
|
408
|
+
expected_names = set(spec.operand_names)
|
|
409
|
+
actual_names = set(operands)
|
|
410
|
+
unexpected_names = actual_names - expected_names
|
|
411
|
+
missing_names = expected_names - actual_names
|
|
412
|
+
if missing_names:
|
|
413
|
+
missing = ", ".join(sorted(missing_names))
|
|
414
|
+
raise ValueError(f"Missing required operands for {kind.value}: {missing}")
|
|
415
|
+
if unexpected_names:
|
|
416
|
+
unexpected = ", ".join(sorted(unexpected_names))
|
|
417
|
+
raise ValueError(f"Unexpected operands for {kind.value}: {unexpected}")
|
|
418
|
+
|
|
419
|
+
normalized_operands: dict[str, str] = {}
|
|
420
|
+
for name in spec.operand_names:
|
|
421
|
+
raw_value = operands[name]
|
|
422
|
+
if not isinstance(raw_value, str):
|
|
423
|
+
raise ValueError(f"Operand {name!r} for {kind.value} must be a string.")
|
|
424
|
+
if not _operand_has_content(raw_value):
|
|
425
|
+
raise ValueError(f"Operand {name!r} for {kind.value} cannot be empty.")
|
|
426
|
+
normalized_operands[name] = raw_value
|
|
427
|
+
|
|
428
|
+
_validate_operand_constraints(kind, normalized_operands)
|
|
429
|
+
_validate_rendered_canonical_shape(kind, normalized_operands)
|
|
430
|
+
return normalized_operands
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _validate_operand_constraints(kind: DirectiveKind, operands: Mapping[str, str]) -> None:
|
|
434
|
+
if kind is DirectiveKind.SET_PREMISE:
|
|
435
|
+
value = operands["value"]
|
|
436
|
+
if _operand_starts_with_token(value, "to"):
|
|
437
|
+
raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
if kind is DirectiveKind.USE_ITEM:
|
|
441
|
+
item = operands["item"]
|
|
442
|
+
normalized_item = _normalized_for_matching(item)
|
|
443
|
+
if normalized_item == "instead of" or normalized_item.startswith("instead of "):
|
|
444
|
+
raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
|
|
445
|
+
if _INSTEAD_OF_DELIMITER in normalized_item:
|
|
446
|
+
raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
if kind is DirectiveKind.REPLACE_USE:
|
|
450
|
+
new_item = operands["new_item"]
|
|
451
|
+
old_item = operands["old_item"]
|
|
452
|
+
if _INSTEAD_OF_DELIMITER in _normalized_for_matching(
|
|
453
|
+
new_item
|
|
454
|
+
) or _INSTEAD_OF_DELIMITER in _normalized_for_matching(old_item):
|
|
455
|
+
raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _validate_rendered_canonical_shape(kind: DirectiveKind, operands: Mapping[str, str]) -> None:
|
|
459
|
+
rendered = _serialize_canonical_directive(kind, MappingProxyType(dict(operands)))
|
|
460
|
+
if _contains_multiple_canonical_directives(rendered):
|
|
461
|
+
raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
|
|
462
|
+
|
|
463
|
+
|
|
338
464
|
def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSyntax | None:
|
|
339
465
|
"""Parse one canonical directive into its semantic kind and operands.
|
|
340
466
|
|
|
@@ -356,19 +482,14 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
356
482
|
normalized = _normalized_for_matching(trimmed_text)
|
|
357
483
|
|
|
358
484
|
if normalized == _CLEAR_PREMISE_TEXT:
|
|
359
|
-
return CanonicalDirective(
|
|
360
|
-
text=text, kind=DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({})
|
|
361
|
-
)
|
|
485
|
+
return CanonicalDirective(kind=DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({}))
|
|
362
486
|
if normalized == _RESET_POLICIES_TEXT:
|
|
363
487
|
return CanonicalDirective(
|
|
364
|
-
text=text,
|
|
365
488
|
kind=DirectiveKind.RESET_POLICIES,
|
|
366
489
|
operands=MappingProxyType({}),
|
|
367
490
|
)
|
|
368
491
|
if normalized == _CLEAR_STATE_TEXT:
|
|
369
|
-
return CanonicalDirective(
|
|
370
|
-
text=text, kind=DirectiveKind.CLEAR_STATE, operands=MappingProxyType({})
|
|
371
|
-
)
|
|
492
|
+
return CanonicalDirective(kind=DirectiveKind.CLEAR_STATE, operands=MappingProxyType({}))
|
|
372
493
|
|
|
373
494
|
if normalized == "set premise":
|
|
374
495
|
return _invalid_directive_syntax(
|
|
@@ -391,7 +512,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
391
512
|
directive_kind=DirectiveKind.SET_PREMISE,
|
|
392
513
|
)
|
|
393
514
|
return CanonicalDirective(
|
|
394
|
-
text=text,
|
|
395
515
|
kind=DirectiveKind.SET_PREMISE,
|
|
396
516
|
operands=MappingProxyType({"value": value}),
|
|
397
517
|
)
|
|
@@ -419,7 +539,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
419
539
|
missing_operand="value",
|
|
420
540
|
)
|
|
421
541
|
return CanonicalDirective(
|
|
422
|
-
text=text,
|
|
423
542
|
kind=DirectiveKind.CHANGE_PREMISE,
|
|
424
543
|
operands=MappingProxyType({"value": value}),
|
|
425
544
|
)
|
|
@@ -469,7 +588,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
469
588
|
directive_kind=DirectiveKind.USE_ITEM,
|
|
470
589
|
)
|
|
471
590
|
return CanonicalDirective(
|
|
472
|
-
text=text,
|
|
473
591
|
kind=DirectiveKind.USE_ITEM,
|
|
474
592
|
operands=MappingProxyType({"item": item}),
|
|
475
593
|
)
|
|
@@ -497,7 +615,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
497
615
|
missing_operand="item",
|
|
498
616
|
)
|
|
499
617
|
return CanonicalDirective(
|
|
500
|
-
text=text,
|
|
501
618
|
kind=DirectiveKind.PROHIBIT_ITEM,
|
|
502
619
|
operands=MappingProxyType({"item": item}),
|
|
503
620
|
)
|
|
@@ -525,7 +642,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
525
642
|
missing_operand="item",
|
|
526
643
|
)
|
|
527
644
|
return CanonicalDirective(
|
|
528
|
-
text=text,
|
|
529
645
|
kind=DirectiveKind.REMOVE_POLICY,
|
|
530
646
|
operands=MappingProxyType({"item": item}),
|
|
531
647
|
)
|
|
@@ -533,46 +649,37 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
|
|
|
533
649
|
return _invalid_directive_syntax(DirectiveSyntaxFailure.MALFORMED_DIRECTIVE)
|
|
534
650
|
|
|
535
651
|
|
|
536
|
-
def
|
|
537
|
-
"""
|
|
538
|
-
try:
|
|
539
|
-
normalized_kind = kind if isinstance(kind, DirectiveKind) else DirectiveKind(kind)
|
|
540
|
-
spec = _DIRECTIVE_SPECS[normalized_kind]
|
|
541
|
-
except (KeyError, ValueError) as exc:
|
|
542
|
-
raise ValueError(f"Unsupported directive kind: {kind!r}") from exc
|
|
652
|
+
def get_directive_metadata() -> tuple[DirectiveMetadata, ...]:
|
|
653
|
+
"""Return immutable public directive metadata derived from internal specs."""
|
|
543
654
|
|
|
544
|
-
|
|
545
|
-
actual_names = set(operands)
|
|
546
|
-
unexpected_names = actual_names - expected_names
|
|
547
|
-
missing_names = expected_names - actual_names
|
|
548
|
-
if missing_names:
|
|
549
|
-
missing = ", ".join(sorted(missing_names))
|
|
550
|
-
raise ValueError(f"Missing required operands for {normalized_kind.value}: {missing}")
|
|
551
|
-
if unexpected_names:
|
|
552
|
-
unexpected = ", ".join(sorted(unexpected_names))
|
|
553
|
-
raise ValueError(f"Unexpected operands for {normalized_kind.value}: {unexpected}")
|
|
655
|
+
return _PUBLIC_DIRECTIVE_METADATA
|
|
554
656
|
|
|
555
|
-
normalized_operands: dict[str, str] = {}
|
|
556
|
-
for name in spec.operand_names:
|
|
557
|
-
raw_value = operands[name]
|
|
558
|
-
if not isinstance(raw_value, str):
|
|
559
|
-
raise ValueError(f"Operand {name!r} for {normalized_kind.value} must be a string.")
|
|
560
|
-
if raw_value.strip() == "":
|
|
561
|
-
raise ValueError(f"Operand {name!r} for {normalized_kind.value} cannot be empty.")
|
|
562
|
-
normalized_operands[name] = raw_value
|
|
563
657
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
658
|
+
def _serialize_canonical_directive(
|
|
659
|
+
kind: DirectiveKind | str, operands: MappingProxyType[str, str]
|
|
660
|
+
) -> str:
|
|
661
|
+
"""Serialize a validated semantic directive without reparsing it."""
|
|
662
|
+
normalized_kind = _normalize_directive_kind(kind)
|
|
663
|
+
spec = _DIRECTIVE_SPECS[normalized_kind]
|
|
664
|
+
|
|
665
|
+
return spec.renderer(operands)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _render_directive(kind: DirectiveKind | str, /, **operands: str) -> str:
|
|
669
|
+
"""Produce canonical directive text from a semantic kind and operands."""
|
|
670
|
+
normalized_kind = _normalize_directive_kind(kind)
|
|
671
|
+
return CanonicalDirective(
|
|
672
|
+
kind=normalized_kind,
|
|
673
|
+
operands=MappingProxyType(dict(operands)),
|
|
674
|
+
).text
|
|
570
675
|
|
|
571
676
|
|
|
572
677
|
__all__ = [
|
|
573
678
|
"DirectiveKind",
|
|
574
679
|
"DirectiveSyntaxFailure",
|
|
680
|
+
"DirectiveMetadata",
|
|
575
681
|
"CanonicalDirective",
|
|
576
682
|
"InvalidDirectiveSyntax",
|
|
683
|
+
"get_directive_metadata",
|
|
577
684
|
"decompose_directive",
|
|
578
685
|
]
|
|
File without changes
|
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/01_llm_contradiction_error.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/02_llm_constraint_guardrail.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/03_llm_premise_guardrail.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/04_llm_tool_denylist_guardrail.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/05_llm_prompt_drift_vs_state.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/06_llm_context_compaction.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/demos/07_llm_prompt_vs_state.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/01_persistent_guardrails.py
RENAMED
|
File without changes
|
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/03_ambiguity_with_error.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/04_tool_governance_denylist.py
RENAMED
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/05_llm_integration_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/examples/07_single_policy_correction.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{context_compiler-0.9.0.dev9 → context_compiler-0.9.0.dev11}/src/context_compiler/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|