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,528 @@
|
|
|
1
|
+
"""Path, domain, and slot semantic contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from .diagnostics import DiagnosticLocation, FlowDiagnostic
|
|
9
|
+
from .domains import categorical_number_bindings, normalize_text, parse_affirmation
|
|
10
|
+
from .semantic_support import json_pointer, semantic_diagnostic
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def step_identifier(path_step: Mapping[str, Any], terminal_identifier: str) -> str | None:
|
|
14
|
+
if "slot" in path_step:
|
|
15
|
+
return cast(str, path_step["step"])
|
|
16
|
+
if "confirm" in path_step:
|
|
17
|
+
return cast(str, path_step["step"])
|
|
18
|
+
if "derive" in path_step:
|
|
19
|
+
return cast(str, path_step["step"])
|
|
20
|
+
if "terminal" in path_step:
|
|
21
|
+
return terminal_identifier
|
|
22
|
+
if "await_external" in path_step:
|
|
23
|
+
return cast(str, path_step["step"])
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def path_contracts(
|
|
28
|
+
document: dict[str, Any],
|
|
29
|
+
) -> tuple[list[FlowDiagnostic], dict[str, str], dict[str, int]]:
|
|
30
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
31
|
+
terminal = cast(dict[str, Any], document.get("terminal", {}))
|
|
32
|
+
terminal_identifier = cast(str, terminal.get("step", "terminal"))
|
|
33
|
+
identifiers: dict[str, str] = {"__init__": ""}
|
|
34
|
+
slot_positions: dict[str, int] = {}
|
|
35
|
+
terminal_markers: list[int] = []
|
|
36
|
+
subflow_anchors: dict[str, list[int]] = {}
|
|
37
|
+
external_wait_anchors: list[int] = []
|
|
38
|
+
|
|
39
|
+
for path_index, path_step in enumerate(document["path"]):
|
|
40
|
+
source_path = json_pointer("path", path_index)
|
|
41
|
+
if "terminal" in path_step:
|
|
42
|
+
terminal_markers.append(path_index)
|
|
43
|
+
if "use" in path_step:
|
|
44
|
+
subflow_anchors.setdefault(path_step["use"], []).append(path_index)
|
|
45
|
+
if "await_external" in path_step:
|
|
46
|
+
external_wait_anchors.append(path_index)
|
|
47
|
+
if "slot" in path_step or "confirm" in path_step:
|
|
48
|
+
slot_name = cast(str, path_step.get("slot", path_step.get("confirm")))
|
|
49
|
+
slot_positions[slot_name] = path_index
|
|
50
|
+
identifier = step_identifier(path_step, terminal_identifier)
|
|
51
|
+
if identifier is None:
|
|
52
|
+
continue
|
|
53
|
+
if (first_path := identifiers.get(identifier)) is not None:
|
|
54
|
+
diagnostics.append(
|
|
55
|
+
semantic_diagnostic(
|
|
56
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_STEP_ID",
|
|
57
|
+
source_path,
|
|
58
|
+
f"step id {identifier!r} is already bound",
|
|
59
|
+
related=(DiagnosticLocation(path=first_path, message="First binding."),),
|
|
60
|
+
suggested_fix="Assign a unique stable step id.",
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
identifiers[identifier] = source_path
|
|
65
|
+
|
|
66
|
+
if terminal_markers and not terminal:
|
|
67
|
+
diagnostics.extend(
|
|
68
|
+
semantic_diagnostic(
|
|
69
|
+
"FLOWSPEC_SEMANTIC_TERMINAL_DEFINITION_MISSING",
|
|
70
|
+
json_pointer("path", path_index, "terminal"),
|
|
71
|
+
"terminal path marker has no top-level terminal definition",
|
|
72
|
+
suggested_fix="Declare the terminal action or remove its path marker.",
|
|
73
|
+
)
|
|
74
|
+
for path_index in terminal_markers
|
|
75
|
+
)
|
|
76
|
+
if terminal and not terminal_markers:
|
|
77
|
+
diagnostics.append(
|
|
78
|
+
semantic_diagnostic(
|
|
79
|
+
"FLOWSPEC_SEMANTIC_TERMINAL_MARKER_MISSING",
|
|
80
|
+
"/terminal",
|
|
81
|
+
"terminal action is unreachable because path has no terminal marker",
|
|
82
|
+
suggested_fix='Add {"terminal": true} at the intended path position.',
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
if len(terminal_markers) > 1:
|
|
86
|
+
diagnostics.extend(
|
|
87
|
+
semantic_diagnostic(
|
|
88
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_TERMINAL_MARKER",
|
|
89
|
+
json_pointer("path", path_index, "terminal"),
|
|
90
|
+
"only one terminal path marker is executable",
|
|
91
|
+
related=(
|
|
92
|
+
DiagnosticLocation(
|
|
93
|
+
path=json_pointer("path", terminal_markers[0], "terminal"),
|
|
94
|
+
message="First marker.",
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
suggested_fix="Keep a single terminal marker.",
|
|
98
|
+
)
|
|
99
|
+
for path_index in terminal_markers[1:]
|
|
100
|
+
)
|
|
101
|
+
if terminal_markers and terminal_markers[0] != len(document["path"]) - 1:
|
|
102
|
+
diagnostics.append(
|
|
103
|
+
semantic_diagnostic(
|
|
104
|
+
"FLOWSPEC_SEMANTIC_UNREACHABLE_PATH_AFTER_TERMINAL",
|
|
105
|
+
json_pointer("path", terminal_markers[0], "terminal"),
|
|
106
|
+
"path entries after the terminal marker can never execute",
|
|
107
|
+
suggested_fix="Move the terminal marker to the end or remove unreachable entries.",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
use_declarations: dict[str, list[int]] = {}
|
|
112
|
+
for use_index, use_definition in enumerate(document.get("uses", [])):
|
|
113
|
+
use_declarations.setdefault(use_definition["ref"], []).append(use_index)
|
|
114
|
+
for reference, declaration_indexes in use_declarations.items():
|
|
115
|
+
for duplicate_index in declaration_indexes[1:]:
|
|
116
|
+
diagnostics.append(
|
|
117
|
+
semantic_diagnostic(
|
|
118
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_USE_DECLARATION",
|
|
119
|
+
json_pointer("uses", duplicate_index, "ref"),
|
|
120
|
+
f"subflow {reference!r} is declared more than once",
|
|
121
|
+
related=(
|
|
122
|
+
DiagnosticLocation(
|
|
123
|
+
path=json_pointer("uses", declaration_indexes[0], "ref"),
|
|
124
|
+
message="First declaration.",
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
suggested_fix="Keep one declaration and one configuration per subflow.",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
if reference not in subflow_anchors:
|
|
131
|
+
diagnostics.append(
|
|
132
|
+
semantic_diagnostic(
|
|
133
|
+
"FLOWSPEC_SEMANTIC_ORPHAN_USE_DECLARATION",
|
|
134
|
+
json_pointer("uses", declaration_indexes[0], "ref"),
|
|
135
|
+
f"subflow {reference!r} is declared but has no path anchor",
|
|
136
|
+
suggested_fix="Add its use step or remove the unused declaration.",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
for reference, anchor_indexes in subflow_anchors.items():
|
|
140
|
+
if reference not in use_declarations:
|
|
141
|
+
diagnostics.extend(
|
|
142
|
+
semantic_diagnostic(
|
|
143
|
+
"FLOWSPEC_SEMANTIC_USE_DECLARATION_MISSING",
|
|
144
|
+
json_pointer("path", path_index, "use"),
|
|
145
|
+
f"subflow anchor {reference!r} has no uses declaration",
|
|
146
|
+
suggested_fix="Declare the versioned subflow and its configuration in uses.",
|
|
147
|
+
)
|
|
148
|
+
for path_index in anchor_indexes
|
|
149
|
+
)
|
|
150
|
+
for duplicate_index in anchor_indexes[1:]:
|
|
151
|
+
diagnostics.append(
|
|
152
|
+
semantic_diagnostic(
|
|
153
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_USE_ANCHOR",
|
|
154
|
+
json_pointer("path", duplicate_index, "use"),
|
|
155
|
+
f"subflow {reference!r} is anchored more than once and would duplicate node ids",
|
|
156
|
+
related=(
|
|
157
|
+
DiagnosticLocation(
|
|
158
|
+
path=json_pointer("path", anchor_indexes[0], "use"),
|
|
159
|
+
message="First anchor.",
|
|
160
|
+
),
|
|
161
|
+
),
|
|
162
|
+
suggested_fix="Use one anchor per declared subflow instance.",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
await_definition = cast(
|
|
167
|
+
dict[str, Any] | None,
|
|
168
|
+
cast(dict[str, Any], document.get("capabilities", {})).get("await_external"),
|
|
169
|
+
)
|
|
170
|
+
if external_wait_anchors and await_definition is None:
|
|
171
|
+
diagnostics.append(
|
|
172
|
+
semantic_diagnostic(
|
|
173
|
+
"FLOWSPEC_SEMANTIC_AWAIT_DEFINITION_MISSING",
|
|
174
|
+
json_pointer("path", external_wait_anchors[0], "await_external"),
|
|
175
|
+
"external-wait marker has no capabilities.await_external contract",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
if len(external_wait_anchors) > 1:
|
|
179
|
+
diagnostics.extend(
|
|
180
|
+
semantic_diagnostic(
|
|
181
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_AWAIT_MARKER",
|
|
182
|
+
json_pointer("path", path_index, "await_external"),
|
|
183
|
+
"the format supports one external-wait binding per flow",
|
|
184
|
+
)
|
|
185
|
+
for path_index in external_wait_anchors[1:]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return diagnostics, identifiers, slot_positions
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def domain_contracts(document: dict[str, Any]) -> list[FlowDiagnostic]:
|
|
192
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
193
|
+
domains = cast(dict[str, dict[str, Any]], document.get("domains", {}))
|
|
194
|
+
for domain_name, domain_definition in domains.items():
|
|
195
|
+
domain_path = json_pointer("domains", domain_name)
|
|
196
|
+
if (
|
|
197
|
+
"minimum" in domain_definition
|
|
198
|
+
and "maximum" in domain_definition
|
|
199
|
+
and domain_definition["minimum"] > domain_definition["maximum"]
|
|
200
|
+
):
|
|
201
|
+
diagnostics.append(
|
|
202
|
+
semantic_diagnostic(
|
|
203
|
+
"FLOWSPEC_SEMANTIC_INVALID_NUMERIC_RANGE",
|
|
204
|
+
json_pointer("domains", domain_name, "minimum"),
|
|
205
|
+
"numeric domain minimum exceeds maximum",
|
|
206
|
+
related=(
|
|
207
|
+
DiagnosticLocation(path=json_pointer("domains", domain_name, "maximum")),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
values = cast(list[Any], domain_definition.get("values", []))
|
|
212
|
+
for value_index, domain_value in enumerate(values):
|
|
213
|
+
if domain_value in values[:value_index]:
|
|
214
|
+
diagnostics.append(
|
|
215
|
+
semantic_diagnostic(
|
|
216
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_DOMAIN_VALUE",
|
|
217
|
+
json_pointer("domains", domain_name, "values", value_index),
|
|
218
|
+
f"domain value {domain_value!r} is duplicated",
|
|
219
|
+
suggested_fix="Keep each canonical value once.",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if domain_definition.get("type") == "categorical":
|
|
224
|
+
normalization = cast(dict[str, Any], domain_definition.get("normalize", {}))
|
|
225
|
+
accent_fold = bool(normalization.get("accent_fold", False))
|
|
226
|
+
normalized_inputs: dict[str, tuple[Any, str]] = {}
|
|
227
|
+
for value_index, domain_value in enumerate(values):
|
|
228
|
+
if domain_value is None:
|
|
229
|
+
continue
|
|
230
|
+
normalized_value = normalize_text(domain_value, accent_fold=accent_fold)
|
|
231
|
+
if not normalized_value:
|
|
232
|
+
diagnostics.append(
|
|
233
|
+
semantic_diagnostic(
|
|
234
|
+
"FLOWSPEC_SEMANTIC_EMPTY_NORMALIZED_VALUE",
|
|
235
|
+
json_pointer("domains", domain_name, "values", value_index),
|
|
236
|
+
"canonical categorical value normalizes to an empty input",
|
|
237
|
+
suggested_fix="Use a non-whitespace canonical value.",
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
continue
|
|
241
|
+
if (first_binding := normalized_inputs.get(normalized_value)) is not None:
|
|
242
|
+
diagnostics.append(
|
|
243
|
+
semantic_diagnostic(
|
|
244
|
+
"FLOWSPEC_SEMANTIC_AMBIGUOUS_NORMALIZED_VALUE",
|
|
245
|
+
json_pointer("domains", domain_name, "values", value_index),
|
|
246
|
+
"canonical values collapse to the same normalized input",
|
|
247
|
+
related=(
|
|
248
|
+
DiagnosticLocation(
|
|
249
|
+
path=first_binding[1],
|
|
250
|
+
message="Conflicting canonical value.",
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
suggested_fix="Choose canonical values that remain distinct after configured normalization.",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
normalized_inputs[normalized_value] = (
|
|
258
|
+
domain_value,
|
|
259
|
+
json_pointer("domains", domain_name, "values", value_index),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if normalization.get("number_words"):
|
|
263
|
+
for number_input, target in categorical_number_bindings(values).items():
|
|
264
|
+
normalized_number_input = normalize_text(number_input, accent_fold=accent_fold)
|
|
265
|
+
number_words_path = json_pointer(
|
|
266
|
+
"domains", domain_name, "normalize", "number_words"
|
|
267
|
+
)
|
|
268
|
+
first_binding = normalized_inputs.get(normalized_number_input)
|
|
269
|
+
if first_binding is not None and (
|
|
270
|
+
type(first_binding[0]) is not type(target) or first_binding[0] != target
|
|
271
|
+
):
|
|
272
|
+
diagnostics.append(
|
|
273
|
+
semantic_diagnostic(
|
|
274
|
+
"FLOWSPEC_SEMANTIC_NUMBER_INPUT_COLLISION",
|
|
275
|
+
number_words_path,
|
|
276
|
+
"positional number input collides with a different canonical value",
|
|
277
|
+
related=(DiagnosticLocation(path=first_binding[1]),),
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
normalized_inputs.setdefault(
|
|
282
|
+
normalized_number_input,
|
|
283
|
+
(target, number_words_path),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for synonym, target in cast(dict[str, Any], normalization.get("synonyms", {})).items():
|
|
287
|
+
synonym_path = json_pointer(
|
|
288
|
+
"domains", domain_name, "normalize", "synonyms", synonym
|
|
289
|
+
)
|
|
290
|
+
normalized_synonym = normalize_text(synonym, accent_fold=accent_fold)
|
|
291
|
+
if not normalized_synonym:
|
|
292
|
+
diagnostics.append(
|
|
293
|
+
semantic_diagnostic(
|
|
294
|
+
"FLOWSPEC_SEMANTIC_EMPTY_NORMALIZED_ALIAS",
|
|
295
|
+
synonym_path,
|
|
296
|
+
"categorical alias normalizes to an empty input and is unreachable",
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
first_binding = normalized_inputs.get(normalized_synonym)
|
|
301
|
+
if first_binding is not None and (
|
|
302
|
+
type(first_binding[0]) is not type(target) or first_binding[0] != target
|
|
303
|
+
):
|
|
304
|
+
diagnostics.append(
|
|
305
|
+
semantic_diagnostic(
|
|
306
|
+
"FLOWSPEC_SEMANTIC_AMBIGUOUS_NORMALIZED_ALIAS",
|
|
307
|
+
synonym_path,
|
|
308
|
+
"alias collides with an input bound to a different canonical value",
|
|
309
|
+
related=(DiagnosticLocation(path=first_binding[1]),),
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
normalized_inputs.setdefault(normalized_synonym, (target, synonym_path))
|
|
314
|
+
|
|
315
|
+
if domain_definition.get("type") == "bool":
|
|
316
|
+
normalization = cast(dict[str, Any], domain_definition.get("normalize", {}))
|
|
317
|
+
accent_fold = bool(normalization.get("accent_fold", False))
|
|
318
|
+
normalized_aliases: dict[str, tuple[bool, str]] = {}
|
|
319
|
+
for synonym, target in cast(dict[str, bool], normalization.get("synonyms", {})).items():
|
|
320
|
+
synonym_path = json_pointer(
|
|
321
|
+
"domains", domain_name, "normalize", "synonyms", synonym
|
|
322
|
+
)
|
|
323
|
+
normalized_synonym = normalize_text(synonym, accent_fold=accent_fold)
|
|
324
|
+
if not normalized_synonym:
|
|
325
|
+
diagnostics.append(
|
|
326
|
+
semantic_diagnostic(
|
|
327
|
+
"FLOWSPEC_SEMANTIC_EMPTY_NORMALIZED_ALIAS",
|
|
328
|
+
synonym_path,
|
|
329
|
+
"boolean alias normalizes to an empty input and is unreachable",
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
continue
|
|
333
|
+
if (
|
|
334
|
+
first_alias := normalized_aliases.get(normalized_synonym)
|
|
335
|
+
) is not None and first_alias[0] is not target:
|
|
336
|
+
diagnostics.append(
|
|
337
|
+
semantic_diagnostic(
|
|
338
|
+
"FLOWSPEC_SEMANTIC_AMBIGUOUS_NORMALIZED_ALIAS",
|
|
339
|
+
synonym_path,
|
|
340
|
+
"boolean aliases normalize to the same input with different targets",
|
|
341
|
+
related=(DiagnosticLocation(path=first_alias[1]),),
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
normalized_aliases.setdefault(normalized_synonym, (target, synonym_path))
|
|
346
|
+
built_in_target = (
|
|
347
|
+
parse_affirmation(
|
|
348
|
+
synonym,
|
|
349
|
+
emoji_veto=bool(normalization.get("emoji_veto", False)),
|
|
350
|
+
)
|
|
351
|
+
if normalization.get("affirmation")
|
|
352
|
+
else {
|
|
353
|
+
"true": True,
|
|
354
|
+
"1": True,
|
|
355
|
+
"yes": True,
|
|
356
|
+
"y": True,
|
|
357
|
+
"false": False,
|
|
358
|
+
"0": False,
|
|
359
|
+
"no": False,
|
|
360
|
+
"n": False,
|
|
361
|
+
}.get(normalized_synonym)
|
|
362
|
+
)
|
|
363
|
+
if built_in_target is not None and built_in_target is not target:
|
|
364
|
+
diagnostics.append(
|
|
365
|
+
semantic_diagnostic(
|
|
366
|
+
"FLOWSPEC_SEMANTIC_ALIAS_CONFLICTS_WITH_BOOLEAN_NORMALIZATION",
|
|
367
|
+
synonym_path,
|
|
368
|
+
"boolean alias conflicts with the configured built-in interpretation",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
row_values: dict[Any, int] = {}
|
|
373
|
+
for row_index, row_definition in enumerate(domain_definition.get("rows", [])):
|
|
374
|
+
row_value = row_definition["value"]
|
|
375
|
+
if row_value not in values:
|
|
376
|
+
diagnostics.append(
|
|
377
|
+
semantic_diagnostic(
|
|
378
|
+
"FLOWSPEC_SEMANTIC_ROW_VALUE_OUTSIDE_DOMAIN",
|
|
379
|
+
json_pointer("domains", domain_name, "rows", row_index, "value"),
|
|
380
|
+
f"row value {row_value!r} is not a canonical domain value",
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
if (first_row := row_values.get(row_value)) is not None:
|
|
384
|
+
diagnostics.append(
|
|
385
|
+
semantic_diagnostic(
|
|
386
|
+
"FLOWSPEC_SEMANTIC_DUPLICATE_DOMAIN_ROW",
|
|
387
|
+
json_pointer("domains", domain_name, "rows", row_index, "value"),
|
|
388
|
+
f"row value {row_value!r} has more than one presentation row",
|
|
389
|
+
related=(
|
|
390
|
+
DiagnosticLocation(
|
|
391
|
+
path=json_pointer(
|
|
392
|
+
"domains", domain_name, "rows", first_row, "value"
|
|
393
|
+
)
|
|
394
|
+
),
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
row_values[row_value] = row_index
|
|
400
|
+
|
|
401
|
+
synonyms = cast(
|
|
402
|
+
dict[str, Any],
|
|
403
|
+
cast(dict[str, Any], domain_definition.get("normalize", {})).get("synonyms", {}),
|
|
404
|
+
)
|
|
405
|
+
allowed_targets = {True, False} if domain_definition.get("type") == "bool" else set(values)
|
|
406
|
+
for synonym, target in synonyms.items():
|
|
407
|
+
if target not in allowed_targets:
|
|
408
|
+
diagnostics.append(
|
|
409
|
+
semantic_diagnostic(
|
|
410
|
+
"FLOWSPEC_SEMANTIC_SYNONYM_TARGET_OUTSIDE_DOMAIN",
|
|
411
|
+
json_pointer("domains", domain_name, "normalize", "synonyms", synonym),
|
|
412
|
+
f"synonym target {target!r} is not accepted by domain {domain_name!r}",
|
|
413
|
+
related=(DiagnosticLocation(path=domain_path),),
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
return diagnostics
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def slot_contracts(
|
|
420
|
+
document: dict[str, Any],
|
|
421
|
+
slot_positions: dict[str, int],
|
|
422
|
+
*,
|
|
423
|
+
external_slots: frozenset[str] | None,
|
|
424
|
+
) -> list[FlowDiagnostic]:
|
|
425
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
426
|
+
slots = cast(dict[str, dict[str, Any]], document.get("slots", {}))
|
|
427
|
+
domains = cast(dict[str, Any], document.get("domains", {}))
|
|
428
|
+
available_slots = set(slots)
|
|
429
|
+
if external_slots is not None:
|
|
430
|
+
available_slots.update(external_slots)
|
|
431
|
+
derive_targets = {
|
|
432
|
+
cast(str, derive_definition["writes"]) for derive_definition in document.get("derive", [])
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for slot_name, slot_definition in slots.items():
|
|
436
|
+
if slot_definition["domain"] not in domains:
|
|
437
|
+
diagnostics.append(
|
|
438
|
+
semantic_diagnostic(
|
|
439
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_DOMAIN",
|
|
440
|
+
json_pointer("slots", slot_name, "domain"),
|
|
441
|
+
f"domain {slot_definition['domain']!r} is not declared",
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
if (
|
|
445
|
+
slot_definition.get("required", True)
|
|
446
|
+
and slot_name not in slot_positions
|
|
447
|
+
and slot_name not in derive_targets
|
|
448
|
+
):
|
|
449
|
+
diagnostics.append(
|
|
450
|
+
semantic_diagnostic(
|
|
451
|
+
"FLOWSPEC_SEMANTIC_REQUIRED_SLOT_WITHOUT_PRODUCER",
|
|
452
|
+
json_pointer("slots", slot_name),
|
|
453
|
+
f"required slot {slot_name!r} has no collection or derive producer",
|
|
454
|
+
suggested_fix=(
|
|
455
|
+
"Add its collection step, derive it deterministically, or make it optional."
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
for requirement_index, requirement in enumerate(slot_definition.get("requires", [])):
|
|
460
|
+
requirement_path = json_pointer("slots", slot_name, "requires", requirement_index)
|
|
461
|
+
if requirement == slot_name:
|
|
462
|
+
diagnostics.append(
|
|
463
|
+
semantic_diagnostic(
|
|
464
|
+
"FLOWSPEC_SEMANTIC_SELF_DEPENDENCY",
|
|
465
|
+
requirement_path,
|
|
466
|
+
f"slot {slot_name!r} cannot require itself",
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
elif external_slots is not None and requirement not in available_slots:
|
|
470
|
+
diagnostics.append(
|
|
471
|
+
semantic_diagnostic(
|
|
472
|
+
"FLOWSPEC_SEMANTIC_UNKNOWN_REQUIRED_SLOT",
|
|
473
|
+
requirement_path,
|
|
474
|
+
f"required slot {requirement!r} is not declared or exposed by the profile",
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
if (
|
|
478
|
+
requirement in slot_positions
|
|
479
|
+
and slot_name in slot_positions
|
|
480
|
+
and slot_positions[requirement] >= slot_positions[slot_name]
|
|
481
|
+
):
|
|
482
|
+
diagnostics.append(
|
|
483
|
+
semantic_diagnostic(
|
|
484
|
+
"FLOWSPEC_SEMANTIC_REQUIREMENT_ORDER",
|
|
485
|
+
requirement_path,
|
|
486
|
+
f"required slot {requirement!r} is not collected before {slot_name!r}",
|
|
487
|
+
suggested_fix="Move the required slot earlier in path.",
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
dependency_graph = {
|
|
492
|
+
slot_name: [
|
|
493
|
+
requirement
|
|
494
|
+
for requirement in slot_definition.get("requires", [])
|
|
495
|
+
if requirement in slots
|
|
496
|
+
]
|
|
497
|
+
for slot_name, slot_definition in slots.items()
|
|
498
|
+
}
|
|
499
|
+
active: list[str] = []
|
|
500
|
+
completed: set[str] = set()
|
|
501
|
+
reported_cycles: set[frozenset[str]] = set()
|
|
502
|
+
|
|
503
|
+
def visit(slot_name: str) -> None:
|
|
504
|
+
if slot_name in completed:
|
|
505
|
+
return
|
|
506
|
+
if slot_name in active:
|
|
507
|
+
cycle = active[active.index(slot_name) :] + [slot_name]
|
|
508
|
+
cycle_members = frozenset(cycle)
|
|
509
|
+
if cycle_members not in reported_cycles:
|
|
510
|
+
reported_cycles.add(cycle_members)
|
|
511
|
+
diagnostics.append(
|
|
512
|
+
semantic_diagnostic(
|
|
513
|
+
"FLOWSPEC_SEMANTIC_SLOT_DEPENDENCY_CYCLE",
|
|
514
|
+
json_pointer("slots", slot_name, "requires"),
|
|
515
|
+
f"slot dependency cycle: {' -> '.join(cycle)}",
|
|
516
|
+
suggested_fix="Make requires a directed acyclic precedence relation.",
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
return
|
|
520
|
+
active.append(slot_name)
|
|
521
|
+
for requirement in dependency_graph[slot_name]:
|
|
522
|
+
visit(requirement)
|
|
523
|
+
active.pop()
|
|
524
|
+
completed.add(slot_name)
|
|
525
|
+
|
|
526
|
+
for slot_name in sorted(slots):
|
|
527
|
+
visit(slot_name)
|
|
528
|
+
return diagnostics
|