flowspec2 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowspec2/__init__.py +105 -0
- flowspec2/authoring/__init__.py +275 -0
- flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
- flowspec2/authoring/authoring-evidence.schema.json +455 -0
- flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
- flowspec2/authoring/benchmark.py +1409 -0
- flowspec2/authoring/corpus/await_correction.case.json +220 -0
- flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
- flowspec2/authoring/corpus/gated_derive.case.json +100 -0
- flowspec2/authoring/corpus/linear.case.json +55 -0
- flowspec2/authoring/corpus/manifest.json +31 -0
- flowspec2/authoring/corpus/subflow.case.json +66 -0
- flowspec2/authoring/corpus/terminal.case.json +94 -0
- flowspec2/authoring/corpus.py +307 -0
- flowspec2/authoring/ctk.py +836 -0
- flowspec2/authoring/detached_signature.py +120 -0
- flowspec2/authoring/evidence.py +311 -0
- flowspec2/authoring/evidence_signature.py +187 -0
- flowspec2/authoring/evidence_verification.py +229 -0
- flowspec2/authoring/gemini.py +215 -0
- flowspec2/authoring/operational-corpus.json +61 -0
- flowspec2/authoring/operational.py +956 -0
- flowspec2/authoring/operational_providers.py +167 -0
- flowspec2/authoring/presentation_review.py +1013 -0
- flowspec2/authoring/presentation_review_signature.py +305 -0
- flowspec2/authoring/projection.py +299 -0
- flowspec2/authoring/provider_prompt.py +83 -0
- flowspec2/backends/__init__.py +82 -0
- flowspec2/backends/http.py +189 -0
- flowspec2/checker.py +238 -0
- flowspec2/cli.py +789 -0
- flowspec2/cli_parser.py +345 -0
- flowspec2/clock.py +23 -0
- flowspec2/compat/__init__.py +47 -0
- flowspec2/compat/models.py +82 -0
- flowspec2/compat/open_workflow.py +616 -0
- flowspec2/compat/rasa.py +19 -0
- flowspec2/compat/rasa_export.py +876 -0
- flowspec2/compat/rasa_import.py +992 -0
- flowspec2/compat/rasa_shared.py +270 -0
- flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
- flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
- flowspec2/compat/tool_profiles.py +149 -0
- flowspec2/compat/yaml.py +147 -0
- flowspec2/compiler.py +957 -0
- flowspec2/compiler_contracts.py +17 -0
- flowspec2/compiler_resume_contracts.py +594 -0
- flowspec2/compiler_schema_relations.py +1830 -0
- flowspec2/compiler_tool_contracts.py +245 -0
- flowspec2/compiler_value_contracts.py +447 -0
- flowspec2/derive.py +32 -0
- flowspec2/diagnostics.py +94 -0
- flowspec2/domains.py +489 -0
- flowspec2/experimental/__init__.py +57 -0
- flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
- flowspec2/experimental/v3-preview-loss-policy.json +161 -0
- flowspec2/experimental/v3_lowering.py +928 -0
- flowspec2/experimental/v3_preview.py +2460 -0
- flowspec2/flowspec-2.schema.json +635 -0
- flowspec2/interactive.py +300 -0
- flowspec2/ir.py +1459 -0
- flowspec2/json_codec.py +120 -0
- flowspec2/llm.py +366 -0
- flowspec2/models.py +121 -0
- flowspec2/nodes.py +1537 -0
- flowspec2/observability.py +151 -0
- flowspec2/predicates.py +89 -0
- flowspec2/profiles.py +118 -0
- flowspec2/py.typed +0 -0
- flowspec2/runtime.py +1059 -0
- flowspec2/schema.py +54 -0
- flowspec2/schema_contracts.py +203 -0
- flowspec2/semantic_derive_contracts.py +530 -0
- flowspec2/semantic_path_contracts.py +528 -0
- flowspec2/semantic_predicate_contracts.py +355 -0
- flowspec2/semantic_schema_contracts.py +137 -0
- flowspec2/semantic_source_contracts.py +21 -0
- flowspec2/semantic_state_contracts.py +450 -0
- flowspec2/semantic_support.py +40 -0
- flowspec2/semantics.py +410 -0
- flowspec2/state_migration.py +1034 -0
- flowspec2/subflows/__init__.py +1117 -0
- flowspec2/subflows/address.py +328 -0
- flowspec2/subflows/identification.py +1031 -0
- flowspec2/tools.py +1154 -0
- flowspec2-1.0.0.dist-info/METADATA +482 -0
- flowspec2-1.0.0.dist-info/RECORD +92 -0
- flowspec2-1.0.0.dist-info/WHEEL +4 -0
- flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
- flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Entry, state-writer, and rail-reference semantic contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from jsonschema import Draft202012Validator
|
|
8
|
+
from jsonschema.exceptions import SchemaError
|
|
9
|
+
|
|
10
|
+
from .diagnostics import DiagnosticLocation, FlowDiagnostic
|
|
11
|
+
from .semantic_support import json_pointer, semantic_diagnostic
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def entry_schema_contracts(
|
|
15
|
+
document: dict[str, Any],
|
|
16
|
+
*,
|
|
17
|
+
external_slots: frozenset[str] | None,
|
|
18
|
+
) -> list[FlowDiagnostic]:
|
|
19
|
+
entry_schema = cast(dict[str, Any] | None, document["route"].get("entry_args_schema"))
|
|
20
|
+
if entry_schema is None:
|
|
21
|
+
return []
|
|
22
|
+
try:
|
|
23
|
+
Draft202012Validator.check_schema(entry_schema)
|
|
24
|
+
except SchemaError as schema_error:
|
|
25
|
+
return [
|
|
26
|
+
semantic_diagnostic(
|
|
27
|
+
"FLOWSPEC_SEMANTIC_INVALID_ENTRY_SCHEMA",
|
|
28
|
+
"/route/entry_args_schema",
|
|
29
|
+
f"entry_args_schema is not a valid Draft 2020-12 schema: {schema_error.message}",
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
34
|
+
if entry_schema.get("type") != "object":
|
|
35
|
+
diagnostics.append(
|
|
36
|
+
semantic_diagnostic(
|
|
37
|
+
"FLOWSPEC_SEMANTIC_ENTRY_SCHEMA_NOT_OBJECT",
|
|
38
|
+
"/route/entry_args_schema/type",
|
|
39
|
+
"entry_args_schema must describe one object of initial slot values",
|
|
40
|
+
suggested_fix="Set type to object and declare the permitted slot properties.",
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
if entry_schema.get("additionalProperties") is not False:
|
|
44
|
+
diagnostics.append(
|
|
45
|
+
semantic_diagnostic(
|
|
46
|
+
"FLOWSPEC_SEMANTIC_ENTRY_SCHEMA_OPEN",
|
|
47
|
+
"/route/entry_args_schema/additionalProperties",
|
|
48
|
+
"entry_args_schema must reject undeclared entry fields",
|
|
49
|
+
suggested_fix="Set additionalProperties to false.",
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
available_slots = set(document.get("slots", {}))
|
|
53
|
+
if external_slots is not None:
|
|
54
|
+
available_slots.update(external_slots)
|
|
55
|
+
for property_name in cast(dict[str, Any], entry_schema.get("properties", {})):
|
|
56
|
+
if property_name not in available_slots:
|
|
57
|
+
diagnostics.append(
|
|
58
|
+
semantic_diagnostic(
|
|
59
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_ENTRY_SLOT",
|
|
60
|
+
json_pointer("route", "entry_args_schema", "properties", property_name),
|
|
61
|
+
f"entry argument {property_name!r} is not a declared or exposed slot",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return diagnostics
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def state_writer_contracts(
|
|
68
|
+
document: dict[str, Any],
|
|
69
|
+
*,
|
|
70
|
+
external_slots: frozenset[str] | None,
|
|
71
|
+
) -> list[FlowDiagnostic]:
|
|
72
|
+
"""Reject state keys whose ownership or write phase is ambiguous."""
|
|
73
|
+
|
|
74
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
75
|
+
declared_slots = set(document.get("slots", {}))
|
|
76
|
+
state_slots = declared_slots | set(external_slots or ())
|
|
77
|
+
derive_locations = {
|
|
78
|
+
cast(str, derive_definition["writes"]): json_pointer("derive", derive_index, "writes")
|
|
79
|
+
for derive_index, derive_definition in enumerate(document.get("derive", []))
|
|
80
|
+
}
|
|
81
|
+
await_locations: dict[str, str] = {}
|
|
82
|
+
await_definition = cast(
|
|
83
|
+
dict[str, Any],
|
|
84
|
+
cast(dict[str, Any], document.get("capabilities", {})).get("await_external", {}),
|
|
85
|
+
)
|
|
86
|
+
if await_definition:
|
|
87
|
+
on_resume = cast(dict[str, Any], await_definition.get("on_resume", {}))
|
|
88
|
+
for state_key in cast(dict[str, Any], on_resume.get("set", {})):
|
|
89
|
+
await_locations[state_key] = json_pointer(
|
|
90
|
+
"capabilities", "await_external", "on_resume", "set", state_key
|
|
91
|
+
)
|
|
92
|
+
enrichment = on_resume.get("enrich")
|
|
93
|
+
if isinstance(enrichment, dict):
|
|
94
|
+
for state_key in cast(dict[str, Any], enrichment.get("set", {})):
|
|
95
|
+
await_locations[state_key] = json_pointer(
|
|
96
|
+
"capabilities",
|
|
97
|
+
"await_external",
|
|
98
|
+
"on_resume",
|
|
99
|
+
"enrich",
|
|
100
|
+
"set",
|
|
101
|
+
state_key,
|
|
102
|
+
)
|
|
103
|
+
for transition_name, transition in {
|
|
104
|
+
"timeout": await_definition.get("timeout"),
|
|
105
|
+
**cast(dict[str, Any], await_definition.get("recovery", {})),
|
|
106
|
+
}.items():
|
|
107
|
+
if not isinstance(transition, dict):
|
|
108
|
+
continue
|
|
109
|
+
for state_key in cast(dict[str, Any], transition.get("set", {})):
|
|
110
|
+
await_locations.setdefault(
|
|
111
|
+
state_key,
|
|
112
|
+
json_pointer(
|
|
113
|
+
"capabilities",
|
|
114
|
+
"await_external",
|
|
115
|
+
transition_name if transition_name == "timeout" else "recovery",
|
|
116
|
+
*(() if transition_name == "timeout" else (transition_name,)),
|
|
117
|
+
"set",
|
|
118
|
+
state_key,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
terminal_locations: dict[str, str] = {}
|
|
123
|
+
terminal = cast(dict[str, Any], document.get("terminal", {}))
|
|
124
|
+
for state_key in terminal.get("outputs", {}):
|
|
125
|
+
terminal_locations[state_key] = json_pointer("terminal", "outputs", state_key)
|
|
126
|
+
success_set = cast(
|
|
127
|
+
dict[str, Any],
|
|
128
|
+
cast(dict[str, Any], terminal.get("outcomes", {})).get("success", {}).get("set", {}),
|
|
129
|
+
)
|
|
130
|
+
for state_key in success_set:
|
|
131
|
+
if state_key in terminal_locations:
|
|
132
|
+
diagnostics.append(
|
|
133
|
+
semantic_diagnostic(
|
|
134
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_TERMINAL_WRITE",
|
|
135
|
+
json_pointer("terminal", "outcomes", "success", "set", state_key),
|
|
136
|
+
f"terminal writes state key {state_key!r} through outputs and success.set",
|
|
137
|
+
related=(DiagnosticLocation(path=terminal_locations[state_key]),),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
terminal_locations.setdefault(
|
|
141
|
+
state_key,
|
|
142
|
+
json_pointer("terminal", "outcomes", "success", "set", state_key),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
entry = cast(dict[str, Any], document.get("entry", {}))
|
|
146
|
+
entry_key = cast(str | None, entry.get("writes"))
|
|
147
|
+
protected_keys = (
|
|
148
|
+
state_slots | set(derive_locations) | set(await_locations) | set(terminal_locations)
|
|
149
|
+
)
|
|
150
|
+
reserved_service_key = {"service"} if document.get("service") else set()
|
|
151
|
+
if entry_key is not None and entry_key in protected_keys | reserved_service_key:
|
|
152
|
+
diagnostics.append(
|
|
153
|
+
semantic_diagnostic(
|
|
154
|
+
"FLOWSPEC_SEMANTIC_ENTRY_WRITE_COLLISION",
|
|
155
|
+
"/entry/writes",
|
|
156
|
+
f"entry result key {entry_key!r} has another state owner",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if document.get("service") and "service" in state_slots | set(derive_locations) | set(
|
|
161
|
+
await_locations
|
|
162
|
+
) | set(terminal_locations):
|
|
163
|
+
diagnostics.append(
|
|
164
|
+
semantic_diagnostic(
|
|
165
|
+
"FLOWSPEC_SEMANTIC_SERVICE_STATE_COLLISION",
|
|
166
|
+
"/service",
|
|
167
|
+
"state key 'service' is reserved for namespaced service metadata",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for derived_key, derive_path in derive_locations.items():
|
|
172
|
+
conflicting_path = await_locations.get(derived_key) or terminal_locations.get(derived_key)
|
|
173
|
+
if conflicting_path is not None:
|
|
174
|
+
diagnostics.append(
|
|
175
|
+
semantic_diagnostic(
|
|
176
|
+
"FLOWSPEC_SEMANTIC_DERIVE_WRITE_COLLISION",
|
|
177
|
+
derive_path,
|
|
178
|
+
f"derived key {derived_key!r} is also written by another phase",
|
|
179
|
+
related=(DiagnosticLocation(path=conflicting_path),),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
for terminal_key, terminal_path in terminal_locations.items():
|
|
184
|
+
if terminal_key in state_slots:
|
|
185
|
+
diagnostics.append(
|
|
186
|
+
semantic_diagnostic(
|
|
187
|
+
"FLOWSPEC_SEMANTIC_TERMINAL_SLOT_COLLISION",
|
|
188
|
+
terminal_path,
|
|
189
|
+
f"terminal write {terminal_key!r} would bypass its slot domain",
|
|
190
|
+
suggested_fix="Write a distinct result key.",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
if terminal_key in await_locations:
|
|
194
|
+
diagnostics.append(
|
|
195
|
+
semantic_diagnostic(
|
|
196
|
+
"FLOWSPEC_SEMANTIC_PHASE_WRITE_COLLISION",
|
|
197
|
+
terminal_path,
|
|
198
|
+
f"state key {terminal_key!r} is also written by await_external",
|
|
199
|
+
related=(DiagnosticLocation(path=await_locations[terminal_key]),),
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return diagnostics
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def rail_reference_contracts(
|
|
206
|
+
document: dict[str, Any],
|
|
207
|
+
identifiers: dict[str, str],
|
|
208
|
+
slot_positions: dict[str, int],
|
|
209
|
+
*,
|
|
210
|
+
external_slots: frozenset[str] | None,
|
|
211
|
+
) -> list[FlowDiagnostic]:
|
|
212
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
213
|
+
declared_slots = set(document.get("slots", {}))
|
|
214
|
+
available_slots = set(declared_slots)
|
|
215
|
+
if external_slots is not None:
|
|
216
|
+
available_slots.update(external_slots)
|
|
217
|
+
derive_targets = {definition["writes"] for definition in document.get("derive", [])}
|
|
218
|
+
available_inputs = available_slots | derive_targets
|
|
219
|
+
|
|
220
|
+
for path_index, path_step in enumerate(document["path"]):
|
|
221
|
+
for step_kind in ("slot", "confirm"):
|
|
222
|
+
if step_kind in path_step and path_step[step_kind] not in declared_slots:
|
|
223
|
+
diagnostics.append(
|
|
224
|
+
semantic_diagnostic(
|
|
225
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_NATIVE_PATH_SLOT",
|
|
226
|
+
json_pointer("path", path_index, step_kind),
|
|
227
|
+
f"native {step_kind} step must reference a top-level declared slot",
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
if "confirm" in path_step and path_step["confirm"] in declared_slots:
|
|
231
|
+
confirmation_slot = cast(dict[str, Any], document["slots"][path_step["confirm"]])
|
|
232
|
+
confirmation_domain = cast(
|
|
233
|
+
dict[str, Any] | None,
|
|
234
|
+
cast(dict[str, Any], document.get("domains", {})).get(confirmation_slot["domain"]),
|
|
235
|
+
)
|
|
236
|
+
if confirmation_domain is not None and confirmation_domain["type"] != "bool":
|
|
237
|
+
diagnostics.append(
|
|
238
|
+
semantic_diagnostic(
|
|
239
|
+
"FLOWSPEC_SEMANTIC_CONFIRM_DOMAIN_NOT_BOOLEAN",
|
|
240
|
+
json_pointer("path", path_index, "confirm"),
|
|
241
|
+
f"confirmation slot {path_step['confirm']!r} must use a bool domain",
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
gates = cast(
|
|
245
|
+
dict[str, Any],
|
|
246
|
+
cast(dict[str, Any], document.get("overrides", {})).get("gates", {}),
|
|
247
|
+
)
|
|
248
|
+
gateable_identifiers = {
|
|
249
|
+
cast(str, path_step["step"])
|
|
250
|
+
for path_step in document["path"]
|
|
251
|
+
if "slot" in path_step or "confirm" in path_step
|
|
252
|
+
}
|
|
253
|
+
for gate_identifier in gates:
|
|
254
|
+
if gate_identifier not in gateable_identifiers:
|
|
255
|
+
diagnostics.append(
|
|
256
|
+
semantic_diagnostic(
|
|
257
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_GATE_STEP",
|
|
258
|
+
json_pointer("overrides", "gates", gate_identifier),
|
|
259
|
+
f"gate target {gate_identifier!r} is not a native collect or confirm step",
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
continue
|
|
263
|
+
gated_step = next(
|
|
264
|
+
path_step
|
|
265
|
+
for path_step in document["path"]
|
|
266
|
+
if path_step.get("step") == gate_identifier
|
|
267
|
+
and ("slot" in path_step or "confirm" in path_step)
|
|
268
|
+
)
|
|
269
|
+
if "ask_when" in gated_step:
|
|
270
|
+
diagnostics.append(
|
|
271
|
+
semantic_diagnostic(
|
|
272
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_GATE_SOURCE",
|
|
273
|
+
json_pointer("overrides", "gates", gate_identifier),
|
|
274
|
+
"step has both inline ask_when and an override gate",
|
|
275
|
+
related=(
|
|
276
|
+
DiagnosticLocation(
|
|
277
|
+
path=json_pointer(
|
|
278
|
+
"path", document["path"].index(gated_step), "ask_when"
|
|
279
|
+
)
|
|
280
|
+
),
|
|
281
|
+
),
|
|
282
|
+
suggested_fix="Keep one gate source for the step.",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
if "confirm" in gated_step and (
|
|
286
|
+
gated_step.get("correctable") is True or "on_reject" in gated_step
|
|
287
|
+
):
|
|
288
|
+
diagnostics.append(
|
|
289
|
+
semantic_diagnostic(
|
|
290
|
+
"FLOWSPEC_SEMANTIC_GATE_UNSUPPORTED_FOR_CONFIRMATION_VARIANT",
|
|
291
|
+
json_pointer("overrides", "gates", gate_identifier),
|
|
292
|
+
"summary and correction-hub confirmations do not support override gates",
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if (confirmation := document.get("confirm")) is not None:
|
|
297
|
+
if confirmation["step"] not in identifiers:
|
|
298
|
+
diagnostics.append(
|
|
299
|
+
semantic_diagnostic(
|
|
300
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_CONFIRM_STEP",
|
|
301
|
+
"/confirm/step",
|
|
302
|
+
f"confirmation hub step {confirmation['step']!r} is not in path",
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
if confirmation["slot"] not in declared_slots:
|
|
306
|
+
diagnostics.append(
|
|
307
|
+
semantic_diagnostic(
|
|
308
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_CONFIRM_SLOT",
|
|
309
|
+
"/confirm/slot",
|
|
310
|
+
f"confirmation hub slot {confirmation['slot']!r} is not declared",
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
confirmation_slot = cast(dict[str, Any], document["slots"][confirmation["slot"]])
|
|
315
|
+
confirmation_domain = cast(
|
|
316
|
+
dict[str, Any] | None,
|
|
317
|
+
cast(dict[str, Any], document.get("domains", {})).get(confirmation_slot["domain"]),
|
|
318
|
+
)
|
|
319
|
+
if confirmation_domain is not None and confirmation_domain["type"] != "bool":
|
|
320
|
+
diagnostics.append(
|
|
321
|
+
semantic_diagnostic(
|
|
322
|
+
"FLOWSPEC_SEMANTIC_CONFIRM_DOMAIN_NOT_BOOLEAN",
|
|
323
|
+
"/confirm/slot",
|
|
324
|
+
f"confirmation slot {confirmation['slot']!r} must use a bool domain",
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
matching_confirmation_steps = [
|
|
328
|
+
path_step
|
|
329
|
+
for path_step in document["path"]
|
|
330
|
+
if path_step.get("step") == confirmation["step"] and "confirm" in path_step
|
|
331
|
+
]
|
|
332
|
+
if matching_confirmation_steps:
|
|
333
|
+
matching_step = matching_confirmation_steps[0]
|
|
334
|
+
if matching_step["confirm"] != confirmation["slot"]:
|
|
335
|
+
diagnostics.append(
|
|
336
|
+
semantic_diagnostic(
|
|
337
|
+
"FLOWSPEC_SEMANTIC_CONFIRM_SLOT_MISMATCH",
|
|
338
|
+
"/confirm/slot",
|
|
339
|
+
"top-level confirmation slot differs from its path step",
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
if matching_step.get("correctable") is not True:
|
|
343
|
+
diagnostics.append(
|
|
344
|
+
semantic_diagnostic(
|
|
345
|
+
"FLOWSPEC_SEMANTIC_CONFIRM_HUB_NOT_CORRECTABLE",
|
|
346
|
+
"/confirm/step",
|
|
347
|
+
"top-level confirmation must bind a path step marked correctable",
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
if (
|
|
351
|
+
on_confirm := confirmation.get("on_confirm")
|
|
352
|
+
) is not None and on_confirm not in identifiers:
|
|
353
|
+
diagnostics.append(
|
|
354
|
+
semantic_diagnostic(
|
|
355
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_CONFIRM_TARGET",
|
|
356
|
+
"/confirm/on_confirm",
|
|
357
|
+
f"confirmation target {on_confirm!r} is not an executable step",
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
elif on_confirm is not None:
|
|
361
|
+
confirmation_path = identifiers.get(confirmation["step"], "")
|
|
362
|
+
target_path = identifiers[on_confirm]
|
|
363
|
+
if confirmation_path.startswith("/path/") and target_path.startswith("/path/"):
|
|
364
|
+
confirmation_index = int(confirmation_path.split("/")[2])
|
|
365
|
+
target_index = int(target_path.split("/")[2])
|
|
366
|
+
if target_index <= confirmation_index:
|
|
367
|
+
diagnostics.append(
|
|
368
|
+
semantic_diagnostic(
|
|
369
|
+
"FLOWSPEC_SEMANTIC_CONFIRM_TARGET_NOT_FORWARD",
|
|
370
|
+
"/confirm/on_confirm",
|
|
371
|
+
"confirmation target must execute after the confirmation hub",
|
|
372
|
+
suggested_fix="Target a later path step, normally the terminal step.",
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
for correctable_index, correctable_slot in enumerate(confirmation["correctable"]):
|
|
376
|
+
if external_slots is not None and correctable_slot not in available_slots:
|
|
377
|
+
diagnostics.append(
|
|
378
|
+
semantic_diagnostic(
|
|
379
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_CORRECTABLE_SLOT",
|
|
380
|
+
json_pointer("confirm", "correctable", correctable_index),
|
|
381
|
+
f"correctable slot {correctable_slot!r} is not declared or exposed",
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
confirmation_source = identifiers.get(confirmation["step"], "")
|
|
385
|
+
if (
|
|
386
|
+
confirmation_source.startswith("/path/")
|
|
387
|
+
and correctable_slot in slot_positions
|
|
388
|
+
and slot_positions[correctable_slot] >= int(confirmation_source.split("/")[2])
|
|
389
|
+
):
|
|
390
|
+
diagnostics.append(
|
|
391
|
+
semantic_diagnostic(
|
|
392
|
+
"FLOWSPEC_SEMANTIC_CORRECTABLE_SLOT_ORDER",
|
|
393
|
+
json_pointer("confirm", "correctable", correctable_index),
|
|
394
|
+
f"correctable slot {correctable_slot!r} is not produced before the hub",
|
|
395
|
+
suggested_fix="Move its producer before the confirmation hub.",
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if (terminal := document.get("terminal")) is not None:
|
|
400
|
+
for binding_index, binding in enumerate(terminal.get("input", [])):
|
|
401
|
+
if external_slots is not None and binding["slot"] not in available_inputs:
|
|
402
|
+
diagnostics.append(
|
|
403
|
+
semantic_diagnostic(
|
|
404
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_TERMINAL_INPUT",
|
|
405
|
+
json_pointer("terminal", "input", binding_index, "slot"),
|
|
406
|
+
f"terminal input slot {binding['slot']!r} is not declared, derived, or exposed",
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if (auto_flow := document.get("auto_flow")) is not None:
|
|
411
|
+
native_path_identifiers = {
|
|
412
|
+
identifier
|
|
413
|
+
for identifier, source_path in identifiers.items()
|
|
414
|
+
if source_path.startswith("/path/")
|
|
415
|
+
}
|
|
416
|
+
if auto_flow.get("resume_at") not in {None, *native_path_identifiers}:
|
|
417
|
+
diagnostics.append(
|
|
418
|
+
semantic_diagnostic(
|
|
419
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_AUTO_FLOW_RESUME_STEP",
|
|
420
|
+
"/auto_flow/resume_at",
|
|
421
|
+
f"auto_flow.resume_at {auto_flow['resume_at']!r} is not a native path step",
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
for prefill_index, prefill_slot in enumerate(auto_flow.get("prefill_from", [])):
|
|
425
|
+
if prefill_slot not in declared_slots:
|
|
426
|
+
diagnostics.append(
|
|
427
|
+
semantic_diagnostic(
|
|
428
|
+
"FLOWSPEC_SEMANTIC_NON_NATIVE_AUTO_FLOW_PREFILL_SLOT",
|
|
429
|
+
json_pointer("auto_flow", "prefill_from", prefill_index),
|
|
430
|
+
f"auto-flow prefill slot {prefill_slot!r} is not a top-level slot",
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
for flow_field, mapping in auto_flow.get("alias_map", {}).items():
|
|
434
|
+
candidate_mappings = (
|
|
435
|
+
[mapping]
|
|
436
|
+
if all(not isinstance(value, dict) for value in mapping.values())
|
|
437
|
+
else [value for value in mapping.values() if isinstance(value, dict)]
|
|
438
|
+
)
|
|
439
|
+
for candidate_mapping in candidate_mappings:
|
|
440
|
+
for destination_slot in candidate_mapping:
|
|
441
|
+
if destination_slot not in declared_slots:
|
|
442
|
+
diagnostics.append(
|
|
443
|
+
semantic_diagnostic(
|
|
444
|
+
"FLOWSPEC_SEMANTIC_NON_NATIVE_AUTO_FLOW_DESTINATION",
|
|
445
|
+
json_pointer("auto_flow", "alias_map", flow_field),
|
|
446
|
+
f"auto-flow alias destination {destination_slot!r} is not a "
|
|
447
|
+
"top-level slot",
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
return diagnostics
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Shared diagnostic and JSON projection helpers for semantic linking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .diagnostics import DiagnosticLocation, FlowDiagnostic
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def json_pointer(*segments: str | int) -> str:
|
|
12
|
+
return "".join(f"/{str(segment).replace('~', '~0').replace('/', '~1')}" for segment in segments)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def semantic_diagnostic(
|
|
16
|
+
code: str,
|
|
17
|
+
path: str,
|
|
18
|
+
message: str,
|
|
19
|
+
*,
|
|
20
|
+
related: tuple[DiagnosticLocation, ...] = (),
|
|
21
|
+
suggested_fix: str | None = None,
|
|
22
|
+
) -> FlowDiagnostic:
|
|
23
|
+
return FlowDiagnostic(
|
|
24
|
+
code=code,
|
|
25
|
+
severity="error",
|
|
26
|
+
path=path,
|
|
27
|
+
message=message,
|
|
28
|
+
related_locations=related,
|
|
29
|
+
suggested_fix=suggested_fix,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def mutable_json(value: Any) -> Any:
|
|
34
|
+
if isinstance(value, Mapping):
|
|
35
|
+
return {key: mutable_json(nested_value) for key, nested_value in value.items()}
|
|
36
|
+
if isinstance(value, tuple):
|
|
37
|
+
return [mutable_json(nested_value) for nested_value in value]
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
return [mutable_json(nested_value) for nested_value in value]
|
|
40
|
+
return value
|