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
flowspec2/semantics.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Aggregate semantic linking for structurally valid FlowSpec2 documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
8
|
+
|
|
9
|
+
from .diagnostics import DiagnosticLocation, FlowDiagnostic
|
|
10
|
+
from .ir import normalize_flow
|
|
11
|
+
from .profiles import FlowProfile, capability_is_requested
|
|
12
|
+
from .semantic_source_contracts import (
|
|
13
|
+
derive_contracts,
|
|
14
|
+
domain_contracts,
|
|
15
|
+
entry_schema_contracts,
|
|
16
|
+
path_contracts,
|
|
17
|
+
predicate_contracts,
|
|
18
|
+
rail_reference_contracts,
|
|
19
|
+
slot_contracts,
|
|
20
|
+
state_writer_contracts,
|
|
21
|
+
)
|
|
22
|
+
from .semantic_support import json_pointer, mutable_json, semantic_diagnostic
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FlowLinkError(ValueError):
|
|
26
|
+
"""Raised when a structurally valid flow fails semantic or profile linking."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, diagnostics: tuple[FlowDiagnostic, ...]) -> None:
|
|
29
|
+
self.diagnostics = diagnostics
|
|
30
|
+
rendered_diagnostics = "; ".join(
|
|
31
|
+
f"{diagnostic.code} at {diagnostic.path or '/'}: {diagnostic.message}"
|
|
32
|
+
for diagnostic in diagnostics
|
|
33
|
+
)
|
|
34
|
+
super().__init__(f"FlowSpec2 linking failed: {rendered_diagnostics}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def profile_contracts(
|
|
38
|
+
document: dict[str, Any],
|
|
39
|
+
profile: FlowProfile,
|
|
40
|
+
) -> tuple[list[FlowDiagnostic], frozenset[str] | None, dict[str, int]]:
|
|
41
|
+
diagnostics: list[FlowDiagnostic] = []
|
|
42
|
+
exposed_slots: set[str] = set()
|
|
43
|
+
exposed_slot_positions: dict[str, int] = {}
|
|
44
|
+
exposed_slot_owners: dict[str, tuple[str, int]] = {}
|
|
45
|
+
state_key_owners: dict[str, tuple[str, int, bool]] = {}
|
|
46
|
+
has_open_manifest = False
|
|
47
|
+
required_capabilities: set[tuple[str, str]] = set()
|
|
48
|
+
use_anchor_positions = {
|
|
49
|
+
cast(str, path_step["use"]): path_index
|
|
50
|
+
for path_index, path_step in enumerate(document["path"])
|
|
51
|
+
if "use" in path_step
|
|
52
|
+
}
|
|
53
|
+
authored_state_writers: dict[str, list[tuple[str, str]]] = {}
|
|
54
|
+
|
|
55
|
+
def add_authored_state_writer(state_key: str, path: str, writer_kind: str) -> None:
|
|
56
|
+
authored_state_writers.setdefault(state_key, []).append((path, writer_kind))
|
|
57
|
+
|
|
58
|
+
for slot_name in document.get("slots", {}):
|
|
59
|
+
add_authored_state_writer(slot_name, json_pointer("slots", slot_name), "slot")
|
|
60
|
+
for derive_index, derive_definition in enumerate(document.get("derive", [])):
|
|
61
|
+
add_authored_state_writer(
|
|
62
|
+
cast(str, derive_definition["writes"]),
|
|
63
|
+
json_pointer("derive", derive_index, "writes"),
|
|
64
|
+
"derive",
|
|
65
|
+
)
|
|
66
|
+
if (entry_definition := document.get("entry")) is not None and entry_definition.get("writes"):
|
|
67
|
+
add_authored_state_writer(
|
|
68
|
+
cast(str, entry_definition["writes"]),
|
|
69
|
+
"/entry/writes",
|
|
70
|
+
"entry",
|
|
71
|
+
)
|
|
72
|
+
if (terminal_definition := document.get("terminal")) is not None:
|
|
73
|
+
for output_key in terminal_definition.get("outputs", {}):
|
|
74
|
+
add_authored_state_writer(
|
|
75
|
+
output_key,
|
|
76
|
+
json_pointer("terminal", "outputs", output_key),
|
|
77
|
+
"terminal",
|
|
78
|
+
)
|
|
79
|
+
success_set = cast(
|
|
80
|
+
dict[str, Any],
|
|
81
|
+
cast(dict[str, Any], terminal_definition.get("outcomes", {}))
|
|
82
|
+
.get("success", {})
|
|
83
|
+
.get("set", {}),
|
|
84
|
+
)
|
|
85
|
+
for state_key in success_set:
|
|
86
|
+
add_authored_state_writer(
|
|
87
|
+
state_key,
|
|
88
|
+
json_pointer("terminal", "outcomes", "success", "set", state_key),
|
|
89
|
+
"terminal",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
for domain_name, domain_definition in document.get("domains", {}).items():
|
|
93
|
+
domain_type = domain_definition["type"]
|
|
94
|
+
if domain_type not in profile.domain_types:
|
|
95
|
+
diagnostics.append(
|
|
96
|
+
semantic_diagnostic(
|
|
97
|
+
"FLOWSPEC_PROFILE_DOMAIN_UNSUPPORTED",
|
|
98
|
+
json_pointer("domains", domain_name, "type"),
|
|
99
|
+
f"profile {profile.identifier!r} does not implement domain type {domain_type!r}",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for use_index, use_definition in enumerate(document.get("uses", [])):
|
|
104
|
+
reference = use_definition["ref"]
|
|
105
|
+
if not profile.subflows.has(reference):
|
|
106
|
+
diagnostics.append(
|
|
107
|
+
semantic_diagnostic(
|
|
108
|
+
"FLOWSPEC_PROFILE_SUBFLOW_UNAVAILABLE",
|
|
109
|
+
json_pointer("uses", use_index, "ref"),
|
|
110
|
+
f"profile {profile.identifier!r} does not register subflow {reference!r}",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
has_open_manifest = True
|
|
114
|
+
continue
|
|
115
|
+
definition = profile.subflows.definition(reference)
|
|
116
|
+
if definition.legacy_manifest and not profile.allow_legacy_contracts:
|
|
117
|
+
diagnostics.append(
|
|
118
|
+
semantic_diagnostic(
|
|
119
|
+
"FLOWSPEC_PROFILE_LEGACY_SUBFLOW_MANIFEST_FORBIDDEN",
|
|
120
|
+
json_pointer("uses", use_index, "ref"),
|
|
121
|
+
f"profile {profile.identifier!r} requires a typed manifest for "
|
|
122
|
+
f"subflow {reference!r}",
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
if definition.exposed_slots is None:
|
|
126
|
+
has_open_manifest = True
|
|
127
|
+
else:
|
|
128
|
+
exposed_slots.update(definition.exposed_slots)
|
|
129
|
+
for exposed_slot in definition.exposed_slots:
|
|
130
|
+
if (first_owner := exposed_slot_owners.get(exposed_slot)) is not None:
|
|
131
|
+
diagnostics.append(
|
|
132
|
+
semantic_diagnostic(
|
|
133
|
+
"FLOWSPEC_PROFILE_SUBFLOW_SLOT_COLLISION",
|
|
134
|
+
json_pointer("uses", use_index, "ref"),
|
|
135
|
+
f"subflow slot {exposed_slot!r} is also owned by {first_owner[0]!r}",
|
|
136
|
+
related=(
|
|
137
|
+
DiagnosticLocation(
|
|
138
|
+
path=json_pointer("uses", first_owner[1], "ref"),
|
|
139
|
+
message="First owning subflow declaration.",
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
exposed_slot_owners[exposed_slot] = (reference, use_index)
|
|
146
|
+
if reference in use_anchor_positions:
|
|
147
|
+
exposed_slot_positions[exposed_slot] = use_anchor_positions[reference]
|
|
148
|
+
for colliding_slot in sorted(set(document.get("slots", {})) & definition.exposed_slots):
|
|
149
|
+
diagnostics.append(
|
|
150
|
+
semantic_diagnostic(
|
|
151
|
+
"FLOWSPEC_PROFILE_SUBFLOW_SLOT_COLLISION",
|
|
152
|
+
json_pointer("slots", colliding_slot),
|
|
153
|
+
f"slot {colliding_slot!r} is owned by subflow {reference!r}",
|
|
154
|
+
related=(
|
|
155
|
+
DiagnosticLocation(
|
|
156
|
+
path=json_pointer("uses", use_index, "ref"),
|
|
157
|
+
message="Owning subflow declaration.",
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
suggested_fix="Remove the duplicate top-level slot declaration.",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
for state_key in sorted(definition.owned_state_keys):
|
|
164
|
+
state_key_is_exposed = (
|
|
165
|
+
definition.exposed_slots is not None and state_key in definition.exposed_slots
|
|
166
|
+
)
|
|
167
|
+
if (first_state_owner := state_key_owners.get(state_key)) is not None:
|
|
168
|
+
if not (state_key_is_exposed and first_state_owner[2]):
|
|
169
|
+
diagnostics.append(
|
|
170
|
+
semantic_diagnostic(
|
|
171
|
+
"FLOWSPEC_PROFILE_SUBFLOW_STATE_COLLISION",
|
|
172
|
+
json_pointer("uses", use_index, "ref"),
|
|
173
|
+
f"subflow state key {state_key!r} is also owned by "
|
|
174
|
+
f"{first_state_owner[0]!r}",
|
|
175
|
+
related=(
|
|
176
|
+
DiagnosticLocation(
|
|
177
|
+
path=json_pointer("uses", first_state_owner[1], "ref"),
|
|
178
|
+
message="First owning subflow declaration.",
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
state_key_owners[state_key] = (
|
|
185
|
+
reference,
|
|
186
|
+
use_index,
|
|
187
|
+
state_key_is_exposed,
|
|
188
|
+
)
|
|
189
|
+
for writer_path, writer_kind in authored_state_writers.get(state_key, []):
|
|
190
|
+
if writer_kind == "slot" and state_key_is_exposed:
|
|
191
|
+
continue
|
|
192
|
+
diagnostics.append(
|
|
193
|
+
semantic_diagnostic(
|
|
194
|
+
"FLOWSPEC_PROFILE_SUBFLOW_STATE_COLLISION",
|
|
195
|
+
writer_path,
|
|
196
|
+
f"{writer_kind} writes state key {state_key!r}, which is owned by "
|
|
197
|
+
f"subflow {reference!r}",
|
|
198
|
+
related=(
|
|
199
|
+
DiagnosticLocation(
|
|
200
|
+
path=json_pointer("uses", use_index, "ref"),
|
|
201
|
+
message="Owning subflow declaration.",
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
suggested_fix="Remove the duplicate writer or use a distinct state key.",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
required_capabilities.update(
|
|
208
|
+
(capability, json_pointer("uses", use_index, "ref"))
|
|
209
|
+
for capability in definition.capabilities
|
|
210
|
+
)
|
|
211
|
+
for tool_name, required_version in definition.required_tools.items():
|
|
212
|
+
tool_path = json_pointer("uses", use_index, "ref")
|
|
213
|
+
if not profile.tools.has(tool_name):
|
|
214
|
+
diagnostics.append(
|
|
215
|
+
semantic_diagnostic(
|
|
216
|
+
"FLOWSPEC_PROFILE_SUBFLOW_TOOL_UNAVAILABLE",
|
|
217
|
+
tool_path,
|
|
218
|
+
f"subflow {reference!r} requires tool {tool_name!r} at version "
|
|
219
|
+
f"{required_version!r}, but the profile does not register it",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
continue
|
|
223
|
+
actual_version = profile.tools.definition(tool_name).version
|
|
224
|
+
if (
|
|
225
|
+
profile.tools.definition(tool_name).legacy_contract
|
|
226
|
+
and not profile.allow_legacy_contracts
|
|
227
|
+
):
|
|
228
|
+
diagnostics.append(
|
|
229
|
+
semantic_diagnostic(
|
|
230
|
+
"FLOWSPEC_PROFILE_LEGACY_TOOL_CONTRACT_FORBIDDEN",
|
|
231
|
+
tool_path,
|
|
232
|
+
f"profile {profile.identifier!r} requires a typed contract for "
|
|
233
|
+
f"tool {tool_name!r}",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
if actual_version != required_version:
|
|
237
|
+
diagnostics.append(
|
|
238
|
+
semantic_diagnostic(
|
|
239
|
+
"FLOWSPEC_PROFILE_SUBFLOW_TOOL_VERSION_MISMATCH",
|
|
240
|
+
tool_path,
|
|
241
|
+
f"subflow {reference!r} requires tool {tool_name!r} at version "
|
|
242
|
+
f"{required_version!r}, but the profile provides {actual_version!r}",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
configuration_schema = mutable_json(definition.configuration_schema)
|
|
246
|
+
configuration_validator = Draft202012Validator(
|
|
247
|
+
configuration_schema,
|
|
248
|
+
format_checker=FormatChecker(),
|
|
249
|
+
)
|
|
250
|
+
for configuration_error in sorted(
|
|
251
|
+
configuration_validator.iter_errors(use_definition.get("with", {})),
|
|
252
|
+
key=lambda error: tuple(str(segment) for segment in error.absolute_path),
|
|
253
|
+
):
|
|
254
|
+
error_path = json_pointer(
|
|
255
|
+
"uses",
|
|
256
|
+
use_index,
|
|
257
|
+
"with",
|
|
258
|
+
*tuple(configuration_error.absolute_path),
|
|
259
|
+
)
|
|
260
|
+
diagnostics.append(
|
|
261
|
+
semantic_diagnostic(
|
|
262
|
+
"FLOWSPEC_PROFILE_SUBFLOW_CONFIGURATION_INVALID",
|
|
263
|
+
error_path,
|
|
264
|
+
f"configuration for {definition.ref!r}: {configuration_error.message}",
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
tool_references: list[tuple[str, str]] = []
|
|
269
|
+
if (entry := document.get("entry")) is not None:
|
|
270
|
+
tool_references.append((entry["tool"], "/entry/tool"))
|
|
271
|
+
if (terminal := document.get("terminal")) is not None:
|
|
272
|
+
tool_references.append((terminal["tool"], "/terminal/tool"))
|
|
273
|
+
await_external = cast(
|
|
274
|
+
dict[str, Any] | None,
|
|
275
|
+
cast(dict[str, Any], document.get("capabilities", {})).get("await_external"),
|
|
276
|
+
)
|
|
277
|
+
if await_external is not None:
|
|
278
|
+
enrichment = cast(
|
|
279
|
+
str | dict[str, Any] | None,
|
|
280
|
+
cast(dict[str, Any], await_external.get("on_resume", {})).get("enrich"),
|
|
281
|
+
)
|
|
282
|
+
if not profile.allow_legacy_contracts and (
|
|
283
|
+
not isinstance(await_external.get("resume"), dict) or isinstance(enrichment, str)
|
|
284
|
+
):
|
|
285
|
+
diagnostics.append(
|
|
286
|
+
semantic_diagnostic(
|
|
287
|
+
"FLOWSPEC_PROFILE_LEGACY_AWAIT_CONTRACT_FORBIDDEN",
|
|
288
|
+
"/capabilities/await_external",
|
|
289
|
+
f"profile {profile.identifier!r} requires a versioned typed resume "
|
|
290
|
+
"contract and object-form enrichment",
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
if isinstance(enrichment, str):
|
|
294
|
+
tool_references.append((enrichment, "/capabilities/await_external/on_resume/enrich"))
|
|
295
|
+
elif isinstance(enrichment, dict):
|
|
296
|
+
tool_references.append(
|
|
297
|
+
(
|
|
298
|
+
enrichment["tool"],
|
|
299
|
+
"/capabilities/await_external/on_resume/enrich/tool",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
for tool_name, tool_path in tool_references:
|
|
303
|
+
if not profile.tools.has(tool_name):
|
|
304
|
+
diagnostics.append(
|
|
305
|
+
semantic_diagnostic(
|
|
306
|
+
"FLOWSPEC_PROFILE_TOOL_UNAVAILABLE",
|
|
307
|
+
tool_path,
|
|
308
|
+
f"tool {tool_name!r} is not registered in profile {profile.identifier!r}",
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
elif (
|
|
312
|
+
profile.tools.definition(tool_name).legacy_contract
|
|
313
|
+
and not profile.allow_legacy_contracts
|
|
314
|
+
):
|
|
315
|
+
diagnostics.append(
|
|
316
|
+
semantic_diagnostic(
|
|
317
|
+
"FLOWSPEC_PROFILE_LEGACY_TOOL_CONTRACT_FORBIDDEN",
|
|
318
|
+
tool_path,
|
|
319
|
+
f"profile {profile.identifier!r} requires a typed contract for "
|
|
320
|
+
f"tool {tool_name!r}",
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
for capability_name, capability_value in document.get("capabilities", {}).items():
|
|
325
|
+
if capability_is_requested(capability_value):
|
|
326
|
+
required_capabilities.add(
|
|
327
|
+
(capability_name, json_pointer("capabilities", capability_name))
|
|
328
|
+
)
|
|
329
|
+
if "auto_flow" in document:
|
|
330
|
+
required_capabilities.add(("auto_flow", "/auto_flow"))
|
|
331
|
+
for capability_name, capability_path in sorted(required_capabilities):
|
|
332
|
+
if capability_name not in profile.capabilities:
|
|
333
|
+
diagnostics.append(
|
|
334
|
+
semantic_diagnostic(
|
|
335
|
+
"FLOWSPEC_PROFILE_CAPABILITY_UNAVAILABLE",
|
|
336
|
+
capability_path,
|
|
337
|
+
f"profile {profile.identifier!r} does not provide capability {capability_name!r}",
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
diagnostics,
|
|
343
|
+
None if has_open_manifest else frozenset(exposed_slots),
|
|
344
|
+
exposed_slot_positions,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def semantic_diagnostics(
|
|
349
|
+
flow_document: dict[str, Any],
|
|
350
|
+
*,
|
|
351
|
+
external_slots: frozenset[str] | None = None,
|
|
352
|
+
external_steps: frozenset[str] = frozenset(),
|
|
353
|
+
profile: FlowProfile | None = None,
|
|
354
|
+
) -> tuple[FlowDiagnostic, ...]:
|
|
355
|
+
"""Link all source contracts and return every deterministic finding.
|
|
356
|
+
|
|
357
|
+
``external_slots=None`` defers profile-owned references to compilation. A
|
|
358
|
+
profile-aware caller passes the complete exposed-slot set to make unknown
|
|
359
|
+
references an aggregate semantic error instead.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
document = normalize_flow(flow_document)
|
|
363
|
+
profile_diagnostics: list[FlowDiagnostic] = []
|
|
364
|
+
profile_slot_positions: dict[str, int] = {}
|
|
365
|
+
if profile is not None:
|
|
366
|
+
profile_diagnostics, profile_slots, profile_slot_positions = profile_contracts(
|
|
367
|
+
document, profile
|
|
368
|
+
)
|
|
369
|
+
if profile_slots is not None:
|
|
370
|
+
external_slots = profile_slots
|
|
371
|
+
diagnostics, identifiers, slot_positions = path_contracts(document)
|
|
372
|
+
slot_positions = {**profile_slot_positions, **slot_positions}
|
|
373
|
+
diagnostics.extend(profile_diagnostics)
|
|
374
|
+
diagnostics.extend(domain_contracts(document))
|
|
375
|
+
diagnostics.extend(slot_contracts(document, slot_positions, external_slots=external_slots))
|
|
376
|
+
diagnostics.extend(
|
|
377
|
+
derive_contracts(
|
|
378
|
+
document,
|
|
379
|
+
identifiers,
|
|
380
|
+
external_slots=external_slots,
|
|
381
|
+
external_steps=external_steps,
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
diagnostics.extend(entry_schema_contracts(document, external_slots=external_slots))
|
|
385
|
+
diagnostics.extend(state_writer_contracts(document, external_slots=external_slots))
|
|
386
|
+
diagnostics.extend(
|
|
387
|
+
predicate_contracts(
|
|
388
|
+
document,
|
|
389
|
+
external_slots=external_slots,
|
|
390
|
+
profile=profile,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
diagnostics.extend(
|
|
394
|
+
rail_reference_contracts(
|
|
395
|
+
document,
|
|
396
|
+
identifiers,
|
|
397
|
+
slot_positions,
|
|
398
|
+
external_slots=external_slots,
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
return tuple(
|
|
402
|
+
sorted(
|
|
403
|
+
set(diagnostics),
|
|
404
|
+
key=lambda diagnostic: (
|
|
405
|
+
diagnostic.path,
|
|
406
|
+
diagnostic.code,
|
|
407
|
+
diagnostic.message,
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
)
|