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,836 @@
|
|
|
1
|
+
"""Executable conformance-kit contracts for FlowSpec sources and traces.
|
|
2
|
+
|
|
3
|
+
The kit consumes a closed machine-readable corpus. Corpus cases may reference
|
|
4
|
+
an existing JSON fixture by a safe local path and JSON Pointer, then apply
|
|
5
|
+
deterministic set mutations. The runner records the complete checker report,
|
|
6
|
+
canonical IR digests, and caller-visible runtime traces without timestamps or
|
|
7
|
+
private implementation state.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Final, cast
|
|
21
|
+
|
|
22
|
+
from jsonschema import Draft202012Validator
|
|
23
|
+
|
|
24
|
+
from flowspec2.checker import check_flow
|
|
25
|
+
from flowspec2.diagnostics import CompilationStatus, DiagnosticSeverity, FlowCheckReport
|
|
26
|
+
from flowspec2.ir import FlowIR, build_flow_ir
|
|
27
|
+
from flowspec2.json_codec import strict_json_loads, validate_json_value
|
|
28
|
+
from flowspec2.models import ServiceState
|
|
29
|
+
from flowspec2.observability import SNOWFLAKE_EPOCH_MILLISECONDS, SnowflakeIdGenerator
|
|
30
|
+
from flowspec2.profiles import FlowProfile, reference_profile
|
|
31
|
+
from flowspec2.runtime import FlowRuntime
|
|
32
|
+
|
|
33
|
+
CTK_CORPUS_FORMAT: Final[str] = "flowspec2/ctk-corpus"
|
|
34
|
+
CTK_REPORT_FORMAT: Final[str] = "flowspec2/ctk-report"
|
|
35
|
+
CTK_VERSION: Final[str] = "1"
|
|
36
|
+
|
|
37
|
+
_IDENTIFIER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
38
|
+
_DIGEST_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]{64}$")
|
|
39
|
+
|
|
40
|
+
_CORPUS_SCHEMA: Final[dict[str, Any]] = {
|
|
41
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": False,
|
|
44
|
+
"required": ["format", "version", "cases"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"format": {"const": CTK_CORPUS_FORMAT},
|
|
47
|
+
"version": {"const": CTK_VERSION},
|
|
48
|
+
"cases": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"minItems": 1,
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": False,
|
|
54
|
+
"required": ["identifier", "source", "expected"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"identifier": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
|
|
57
|
+
"source": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": False,
|
|
60
|
+
"required": ["path", "pointer"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"path": {"type": "string", "minLength": 1},
|
|
63
|
+
"pointer": {"type": "string", "pattern": r"^(?:|/.*)$"},
|
|
64
|
+
"mutations": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": False,
|
|
69
|
+
"required": ["path", "replacement"],
|
|
70
|
+
"properties": {
|
|
71
|
+
"path": {"type": "string", "pattern": r"^/.+"},
|
|
72
|
+
"replacement": {},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
"expected": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"additionalProperties": False,
|
|
81
|
+
"required": ["compilation", "diagnostics"],
|
|
82
|
+
"properties": {
|
|
83
|
+
"compilation": {
|
|
84
|
+
"enum": ["not_requested", "skipped", "succeeded", "failed"]
|
|
85
|
+
},
|
|
86
|
+
"diagnostics": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": False,
|
|
91
|
+
"required": ["code", "severity", "path"],
|
|
92
|
+
"properties": {
|
|
93
|
+
"code": {"type": "string", "minLength": 1},
|
|
94
|
+
"severity": {"enum": ["warning", "error"]},
|
|
95
|
+
"path": {"type": "string", "pattern": r"^(?:|/.*)$"},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
"ir": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"additionalProperties": False,
|
|
102
|
+
"required": [
|
|
103
|
+
"ir_format",
|
|
104
|
+
"flow",
|
|
105
|
+
"version",
|
|
106
|
+
"source_digest",
|
|
107
|
+
"dependency_digest",
|
|
108
|
+
"digest",
|
|
109
|
+
"canonical_document_digest",
|
|
110
|
+
"canonical_execution_ir_digest",
|
|
111
|
+
],
|
|
112
|
+
"properties": {
|
|
113
|
+
"ir_format": {"type": "string", "minLength": 1},
|
|
114
|
+
"flow": {"type": "string", "minLength": 1},
|
|
115
|
+
"version": {"type": "string", "minLength": 1},
|
|
116
|
+
"source_digest": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"pattern": r"^[0-9a-f]{64}$",
|
|
119
|
+
},
|
|
120
|
+
"profile_digest": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"pattern": r"^[0-9a-f]{64}$",
|
|
123
|
+
},
|
|
124
|
+
"dependency_digest": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"pattern": r"^[0-9a-f]{64}$",
|
|
127
|
+
},
|
|
128
|
+
"digest": {"type": "string", "pattern": r"^[0-9a-f]{64}$"},
|
|
129
|
+
"canonical_document_digest": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"pattern": r"^[0-9a-f]{64}$",
|
|
132
|
+
},
|
|
133
|
+
"canonical_ir_digest": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"pattern": r"^[0-9a-f]{64}$",
|
|
136
|
+
},
|
|
137
|
+
"canonical_execution_ir_digest": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"pattern": r"^[0-9a-f]{64}$",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"turns": {
|
|
146
|
+
"type": "array",
|
|
147
|
+
"items": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"additionalProperties": False,
|
|
150
|
+
"required": ["payload", "expected_trace"],
|
|
151
|
+
"properties": {
|
|
152
|
+
"payload": {"type": "object"},
|
|
153
|
+
"expected_trace": {"type": "object"},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _canonical_json(json_document: object) -> str:
|
|
165
|
+
return json.dumps(
|
|
166
|
+
json_document,
|
|
167
|
+
ensure_ascii=False,
|
|
168
|
+
allow_nan=False,
|
|
169
|
+
separators=(",", ":"),
|
|
170
|
+
sort_keys=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _canonical_clone(json_document: object) -> object:
|
|
175
|
+
return strict_json_loads(_canonical_json(json_document))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _sha256(serialized_contract: str) -> str:
|
|
179
|
+
return hashlib.sha256(serialized_contract.encode("utf-8")).hexdigest()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _fixed_clock() -> datetime:
|
|
183
|
+
return datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _fixed_clock_milliseconds() -> int:
|
|
187
|
+
return SNOWFLAKE_EPOCH_MILLISECONDS
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _require_identifier(identifier: str, contract_name: str) -> None:
|
|
191
|
+
if not _IDENTIFIER_PATTERN.fullmatch(identifier):
|
|
192
|
+
raise ValueError(f"{contract_name} has an invalid identifier: {identifier!r}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _require_digest(contract_digest: str, contract_name: str) -> None:
|
|
196
|
+
if not _DIGEST_PATTERN.fullmatch(contract_digest):
|
|
197
|
+
raise ValueError(f"{contract_name} must be a lowercase SHA-256 digest")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _canonical_object(serialized_contract: str, contract_name: str) -> dict[str, Any]:
|
|
201
|
+
decoded_contract = strict_json_loads(serialized_contract)
|
|
202
|
+
if not isinstance(decoded_contract, dict):
|
|
203
|
+
raise ValueError(f"{contract_name} must encode a JSON object")
|
|
204
|
+
if _canonical_json(decoded_contract) != serialized_contract:
|
|
205
|
+
raise ValueError(f"{contract_name} must be canonical JSON")
|
|
206
|
+
return cast(dict[str, Any], decoded_contract)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _decode_pointer_segment(encoded_segment: str) -> str:
|
|
210
|
+
decoded_characters: list[str] = []
|
|
211
|
+
character_index = 0
|
|
212
|
+
while character_index < len(encoded_segment):
|
|
213
|
+
character = encoded_segment[character_index]
|
|
214
|
+
if character != "~":
|
|
215
|
+
decoded_characters.append(character)
|
|
216
|
+
character_index += 1
|
|
217
|
+
continue
|
|
218
|
+
if character_index + 1 >= len(encoded_segment):
|
|
219
|
+
raise ValueError(f"invalid JSON Pointer escape in segment {encoded_segment!r}")
|
|
220
|
+
escaped_character = encoded_segment[character_index + 1]
|
|
221
|
+
if escaped_character == "0":
|
|
222
|
+
decoded_characters.append("~")
|
|
223
|
+
elif escaped_character == "1":
|
|
224
|
+
decoded_characters.append("/")
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError(f"invalid JSON Pointer escape in segment {encoded_segment!r}")
|
|
227
|
+
character_index += 2
|
|
228
|
+
return "".join(decoded_characters)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _pointer_segments(json_pointer: str) -> tuple[str, ...]:
|
|
232
|
+
if json_pointer == "":
|
|
233
|
+
return ()
|
|
234
|
+
if not json_pointer.startswith("/"):
|
|
235
|
+
raise ValueError(f"JSON Pointer must be empty or start with '/': {json_pointer!r}")
|
|
236
|
+
return tuple(_decode_pointer_segment(segment) for segment in json_pointer[1:].split("/"))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _array_index(pointer_segment: str, *, array_length: int, allow_append: bool) -> int:
|
|
240
|
+
if pointer_segment == "-" and allow_append:
|
|
241
|
+
return array_length
|
|
242
|
+
if not pointer_segment.isdigit() or (
|
|
243
|
+
pointer_segment.startswith("0") and pointer_segment != "0"
|
|
244
|
+
):
|
|
245
|
+
raise ValueError(f"invalid JSON Pointer array index {pointer_segment!r}")
|
|
246
|
+
array_index = int(pointer_segment)
|
|
247
|
+
maximum_index = array_length if allow_append else array_length - 1
|
|
248
|
+
if array_index > maximum_index:
|
|
249
|
+
raise ValueError(f"JSON Pointer array index is out of range: {array_index}")
|
|
250
|
+
return array_index
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _resolve_pointer(json_document: object, json_pointer: str) -> object:
|
|
254
|
+
selected_node = json_document
|
|
255
|
+
for pointer_segment in _pointer_segments(json_pointer):
|
|
256
|
+
if isinstance(selected_node, Mapping):
|
|
257
|
+
if pointer_segment not in selected_node:
|
|
258
|
+
raise ValueError(f"JSON Pointer does not exist: {json_pointer!r}")
|
|
259
|
+
selected_node = selected_node[pointer_segment]
|
|
260
|
+
elif isinstance(selected_node, list):
|
|
261
|
+
selected_node = selected_node[
|
|
262
|
+
_array_index(pointer_segment, array_length=len(selected_node), allow_append=False)
|
|
263
|
+
]
|
|
264
|
+
else:
|
|
265
|
+
raise ValueError(f"JSON Pointer traverses a scalar: {json_pointer!r}")
|
|
266
|
+
return selected_node
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _set_pointer(json_document: object, json_pointer: str, replacement: object) -> None:
|
|
270
|
+
pointer_segments = _pointer_segments(json_pointer)
|
|
271
|
+
if not pointer_segments:
|
|
272
|
+
raise ValueError("a conformance mutation cannot replace the source root")
|
|
273
|
+
parent_pointer = "".join(
|
|
274
|
+
f"/{segment.replace('~', '~0').replace('/', '~1')}" for segment in pointer_segments[:-1]
|
|
275
|
+
)
|
|
276
|
+
parent_node = _resolve_pointer(json_document, parent_pointer)
|
|
277
|
+
final_segment = pointer_segments[-1]
|
|
278
|
+
replacement_copy = copy.deepcopy(replacement)
|
|
279
|
+
if isinstance(parent_node, dict):
|
|
280
|
+
parent_node[final_segment] = replacement_copy
|
|
281
|
+
return
|
|
282
|
+
if isinstance(parent_node, list):
|
|
283
|
+
array_index = _array_index(
|
|
284
|
+
final_segment,
|
|
285
|
+
array_length=len(parent_node),
|
|
286
|
+
allow_append=True,
|
|
287
|
+
)
|
|
288
|
+
if array_index == len(parent_node):
|
|
289
|
+
parent_node.append(replacement_copy)
|
|
290
|
+
else:
|
|
291
|
+
parent_node[array_index] = replacement_copy
|
|
292
|
+
return
|
|
293
|
+
raise ValueError(f"conformance mutation parent is not a container: {json_pointer!r}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@dataclass(frozen=True)
|
|
297
|
+
class CtkDiagnosticOracle:
|
|
298
|
+
"""Stable diagnostic identity used to verify ordering without prose coupling."""
|
|
299
|
+
|
|
300
|
+
code: str
|
|
301
|
+
severity: DiagnosticSeverity
|
|
302
|
+
path: str
|
|
303
|
+
|
|
304
|
+
def __post_init__(self) -> None:
|
|
305
|
+
if not self.code:
|
|
306
|
+
raise ValueError("conformance diagnostic code must be non-empty")
|
|
307
|
+
if self.severity not in {"warning", "error"}:
|
|
308
|
+
raise ValueError("conformance diagnostic severity is invalid")
|
|
309
|
+
if self.path and not self.path.startswith("/"):
|
|
310
|
+
raise ValueError("conformance diagnostic path must be a JSON Pointer")
|
|
311
|
+
|
|
312
|
+
def to_dict(self) -> dict[str, str]:
|
|
313
|
+
return {"code": self.code, "severity": self.severity, "path": self.path}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@dataclass(frozen=True)
|
|
317
|
+
class CtkIrOracle:
|
|
318
|
+
"""Canonical IR identities expected from a valid source."""
|
|
319
|
+
|
|
320
|
+
ir_format: str
|
|
321
|
+
flow: str
|
|
322
|
+
version: str
|
|
323
|
+
source_digest: str
|
|
324
|
+
dependency_digest: str
|
|
325
|
+
digest: str
|
|
326
|
+
canonical_document_digest: str
|
|
327
|
+
canonical_execution_ir_digest: str
|
|
328
|
+
profile_digest: str | None = None
|
|
329
|
+
canonical_ir_digest: str | None = None
|
|
330
|
+
|
|
331
|
+
def __post_init__(self) -> None:
|
|
332
|
+
if not self.ir_format or not self.flow or not self.version:
|
|
333
|
+
raise ValueError("conformance IR format, flow, and version must be non-empty")
|
|
334
|
+
for digest_name, contract_digest in (
|
|
335
|
+
("source digest", self.source_digest),
|
|
336
|
+
("dependency digest", self.dependency_digest),
|
|
337
|
+
("execution digest", self.digest),
|
|
338
|
+
("canonical document digest", self.canonical_document_digest),
|
|
339
|
+
("canonical execution IR digest", self.canonical_execution_ir_digest),
|
|
340
|
+
):
|
|
341
|
+
_require_digest(contract_digest, f"conformance IR {digest_name}")
|
|
342
|
+
if self.profile_digest is not None:
|
|
343
|
+
_require_digest(self.profile_digest, "conformance IR profile digest")
|
|
344
|
+
if self.canonical_ir_digest is not None:
|
|
345
|
+
_require_digest(self.canonical_ir_digest, "conformance canonical IR digest")
|
|
346
|
+
|
|
347
|
+
def to_dict(self) -> dict[str, str]:
|
|
348
|
+
ir_contract = {
|
|
349
|
+
"ir_format": self.ir_format,
|
|
350
|
+
"flow": self.flow,
|
|
351
|
+
"version": self.version,
|
|
352
|
+
"source_digest": self.source_digest,
|
|
353
|
+
"dependency_digest": self.dependency_digest,
|
|
354
|
+
"digest": self.digest,
|
|
355
|
+
"canonical_document_digest": self.canonical_document_digest,
|
|
356
|
+
"canonical_execution_ir_digest": self.canonical_execution_ir_digest,
|
|
357
|
+
}
|
|
358
|
+
if self.profile_digest is not None:
|
|
359
|
+
ir_contract["profile_digest"] = self.profile_digest
|
|
360
|
+
if self.canonical_ir_digest is not None:
|
|
361
|
+
ir_contract["canonical_ir_digest"] = self.canonical_ir_digest
|
|
362
|
+
return ir_contract
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@dataclass(frozen=True)
|
|
366
|
+
class CtkTurn:
|
|
367
|
+
"""One external payload and its exact caller-visible trace oracle."""
|
|
368
|
+
|
|
369
|
+
payload_json: str
|
|
370
|
+
expected_trace_json: str
|
|
371
|
+
|
|
372
|
+
def __post_init__(self) -> None:
|
|
373
|
+
_canonical_object(self.payload_json, "conformance turn payload")
|
|
374
|
+
_canonical_object(self.expected_trace_json, "conformance turn trace oracle")
|
|
375
|
+
|
|
376
|
+
def payload(self) -> dict[str, Any]:
|
|
377
|
+
return _canonical_object(self.payload_json, "conformance turn payload")
|
|
378
|
+
|
|
379
|
+
def expected_trace(self) -> dict[str, Any]:
|
|
380
|
+
return _canonical_object(self.expected_trace_json, "conformance turn trace oracle")
|
|
381
|
+
|
|
382
|
+
def to_dict(self) -> dict[str, object]:
|
|
383
|
+
return {"payload": self.payload(), "expected_trace": self.expected_trace()}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclass(frozen=True)
|
|
387
|
+
class CtkCase:
|
|
388
|
+
"""One immutable source, validation oracle, IR oracle, and trace sequence."""
|
|
389
|
+
|
|
390
|
+
identifier: str
|
|
391
|
+
source_json: str
|
|
392
|
+
expected_compilation: CompilationStatus
|
|
393
|
+
expected_diagnostics: tuple[CtkDiagnosticOracle, ...]
|
|
394
|
+
expected_ir: CtkIrOracle | None = None
|
|
395
|
+
turns: tuple[CtkTurn, ...] = ()
|
|
396
|
+
|
|
397
|
+
def __post_init__(self) -> None:
|
|
398
|
+
_require_identifier(self.identifier, "conformance case")
|
|
399
|
+
_canonical_object(self.source_json, "conformance case source")
|
|
400
|
+
if self.expected_compilation not in {
|
|
401
|
+
"not_requested",
|
|
402
|
+
"skipped",
|
|
403
|
+
"succeeded",
|
|
404
|
+
"failed",
|
|
405
|
+
}:
|
|
406
|
+
raise ValueError("conformance case compilation oracle is invalid")
|
|
407
|
+
if self.turns and self.expected_compilation != "succeeded":
|
|
408
|
+
raise ValueError("runtime trace oracles require successful compilation")
|
|
409
|
+
if self.expected_ir is not None and self.expected_compilation != "succeeded":
|
|
410
|
+
raise ValueError("an IR oracle requires successful compilation")
|
|
411
|
+
|
|
412
|
+
def source_document(self) -> dict[str, Any]:
|
|
413
|
+
return _canonical_object(self.source_json, "conformance case source")
|
|
414
|
+
|
|
415
|
+
def to_dict(self) -> dict[str, object]:
|
|
416
|
+
expected_contract: dict[str, object] = {
|
|
417
|
+
"compilation": self.expected_compilation,
|
|
418
|
+
"diagnostics": [diagnostic.to_dict() for diagnostic in self.expected_diagnostics],
|
|
419
|
+
}
|
|
420
|
+
if self.expected_ir is not None:
|
|
421
|
+
expected_contract["ir"] = self.expected_ir.to_dict()
|
|
422
|
+
case_contract: dict[str, object] = {
|
|
423
|
+
"identifier": self.identifier,
|
|
424
|
+
"source": self.source_document(),
|
|
425
|
+
"expected": expected_contract,
|
|
426
|
+
}
|
|
427
|
+
if self.turns:
|
|
428
|
+
case_contract["turns"] = [turn.to_dict() for turn in self.turns]
|
|
429
|
+
return case_contract
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@dataclass(frozen=True)
|
|
433
|
+
class CtkCorpus:
|
|
434
|
+
"""An immutable, content-addressed conformance corpus."""
|
|
435
|
+
|
|
436
|
+
cases: tuple[CtkCase, ...]
|
|
437
|
+
format_identifier: str = CTK_CORPUS_FORMAT
|
|
438
|
+
version: str = CTK_VERSION
|
|
439
|
+
|
|
440
|
+
def __post_init__(self) -> None:
|
|
441
|
+
if self.format_identifier != CTK_CORPUS_FORMAT or self.version != CTK_VERSION:
|
|
442
|
+
raise ValueError("conformance corpus format or version is unsupported")
|
|
443
|
+
if not self.cases:
|
|
444
|
+
raise ValueError("conformance corpus must contain cases")
|
|
445
|
+
case_identifiers = [conformance_case.identifier for conformance_case in self.cases]
|
|
446
|
+
if len(case_identifiers) != len(set(case_identifiers)):
|
|
447
|
+
raise ValueError("conformance case identifiers must be unique")
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def digest(self) -> str:
|
|
451
|
+
return _sha256(_canonical_json(self.to_dict()))
|
|
452
|
+
|
|
453
|
+
def to_dict(self) -> dict[str, object]:
|
|
454
|
+
return {
|
|
455
|
+
"format": self.format_identifier,
|
|
456
|
+
"version": self.version,
|
|
457
|
+
"cases": [conformance_case.to_dict() for conformance_case in self.cases],
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@dataclass(frozen=True)
|
|
462
|
+
class CtkCaseReport:
|
|
463
|
+
"""Observed contracts and oracle mismatches for one corpus case."""
|
|
464
|
+
|
|
465
|
+
identifier: str
|
|
466
|
+
check_report_json: str
|
|
467
|
+
ir_observation_json: str | None
|
|
468
|
+
runtime_trace_json: str
|
|
469
|
+
failures: tuple[str, ...]
|
|
470
|
+
|
|
471
|
+
def __post_init__(self) -> None:
|
|
472
|
+
_require_identifier(self.identifier, "conformance case report")
|
|
473
|
+
_canonical_object(self.check_report_json, "conformance check report")
|
|
474
|
+
if self.ir_observation_json is not None:
|
|
475
|
+
_canonical_object(self.ir_observation_json, "conformance IR observation")
|
|
476
|
+
runtime_trace = strict_json_loads(self.runtime_trace_json)
|
|
477
|
+
if not isinstance(runtime_trace, list):
|
|
478
|
+
raise ValueError("conformance runtime trace must encode a JSON array")
|
|
479
|
+
if _canonical_json(runtime_trace) != self.runtime_trace_json:
|
|
480
|
+
raise ValueError("conformance runtime trace must be canonical JSON")
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def passed(self) -> bool:
|
|
484
|
+
return not self.failures
|
|
485
|
+
|
|
486
|
+
def to_dict(self) -> dict[str, object]:
|
|
487
|
+
case_report: dict[str, object] = {
|
|
488
|
+
"identifier": self.identifier,
|
|
489
|
+
"passed": self.passed,
|
|
490
|
+
"check": strict_json_loads(self.check_report_json),
|
|
491
|
+
"runtime_trace": strict_json_loads(self.runtime_trace_json),
|
|
492
|
+
"failures": list(self.failures),
|
|
493
|
+
}
|
|
494
|
+
if self.ir_observation_json is not None:
|
|
495
|
+
case_report["ir"] = strict_json_loads(self.ir_observation_json)
|
|
496
|
+
return case_report
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@dataclass(frozen=True)
|
|
500
|
+
class CtkReport:
|
|
501
|
+
"""Deterministic machine-readable result for a complete corpus run."""
|
|
502
|
+
|
|
503
|
+
corpus_digest: str
|
|
504
|
+
profile_identifier: str
|
|
505
|
+
case_reports: tuple[CtkCaseReport, ...]
|
|
506
|
+
format_identifier: str = CTK_REPORT_FORMAT
|
|
507
|
+
version: str = CTK_VERSION
|
|
508
|
+
|
|
509
|
+
def __post_init__(self) -> None:
|
|
510
|
+
if self.format_identifier != CTK_REPORT_FORMAT or self.version != CTK_VERSION:
|
|
511
|
+
raise ValueError("conformance report format or version is unsupported")
|
|
512
|
+
_require_digest(self.corpus_digest, "conformance corpus digest")
|
|
513
|
+
if not self.profile_identifier:
|
|
514
|
+
raise ValueError("conformance report profile identifier must be non-empty")
|
|
515
|
+
if not self.case_reports:
|
|
516
|
+
raise ValueError("conformance report must contain case reports")
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def passed(self) -> bool:
|
|
520
|
+
return all(case_report.passed for case_report in self.case_reports)
|
|
521
|
+
|
|
522
|
+
def _unsigned_dict(self) -> dict[str, object]:
|
|
523
|
+
return {
|
|
524
|
+
"format": self.format_identifier,
|
|
525
|
+
"version": self.version,
|
|
526
|
+
"corpus_digest": self.corpus_digest,
|
|
527
|
+
"profile": self.profile_identifier,
|
|
528
|
+
"passed": self.passed,
|
|
529
|
+
"cases": [case_report.to_dict() for case_report in self.case_reports],
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def digest(self) -> str:
|
|
534
|
+
return _sha256(_canonical_json(self._unsigned_dict()))
|
|
535
|
+
|
|
536
|
+
def to_dict(self) -> dict[str, object]:
|
|
537
|
+
report_contract = self._unsigned_dict()
|
|
538
|
+
report_contract["digest"] = self.digest
|
|
539
|
+
return report_contract
|
|
540
|
+
|
|
541
|
+
def canonical_json(self) -> str:
|
|
542
|
+
return _canonical_json(self.to_dict())
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _load_json_document(json_path: Path) -> object:
|
|
546
|
+
return strict_json_loads(json_path.read_text(encoding="utf-8"))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _safe_corpus_path(corpus_root: Path, relative_path: str) -> Path:
|
|
550
|
+
requested_path = Path(relative_path)
|
|
551
|
+
if requested_path.is_absolute():
|
|
552
|
+
raise ValueError("conformance source paths must be relative")
|
|
553
|
+
resolved_root = corpus_root.resolve()
|
|
554
|
+
resolved_path = (resolved_root / requested_path).resolve()
|
|
555
|
+
try:
|
|
556
|
+
resolved_path.relative_to(resolved_root)
|
|
557
|
+
except ValueError as traversal_error:
|
|
558
|
+
raise ValueError("conformance source path leaves the corpus root") from traversal_error
|
|
559
|
+
if any(path_component.startswith(".env") for path_component in requested_path.parts):
|
|
560
|
+
raise ValueError("conformance source path cannot reference environment files")
|
|
561
|
+
if resolved_path.suffix != ".json":
|
|
562
|
+
raise ValueError("conformance source path must reference a JSON document")
|
|
563
|
+
return resolved_path
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _manifest_schema_errors(manifest_document: object) -> tuple[str, ...]:
|
|
567
|
+
validation_errors = sorted(
|
|
568
|
+
Draft202012Validator(_CORPUS_SCHEMA).iter_errors(cast(Any, manifest_document)),
|
|
569
|
+
key=lambda schema_error: (
|
|
570
|
+
tuple(str(path_segment) for path_segment in schema_error.absolute_path),
|
|
571
|
+
tuple(str(path_segment) for path_segment in schema_error.absolute_schema_path),
|
|
572
|
+
schema_error.message,
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
return tuple(
|
|
576
|
+
f"{'.'.join(str(path_segment) for path_segment in schema_error.absolute_path) or '<root>'}: "
|
|
577
|
+
f"{schema_error.message}"
|
|
578
|
+
for schema_error in validation_errors
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _diagnostic_oracles(
|
|
583
|
+
diagnostic_documents: Sequence[Mapping[str, object]],
|
|
584
|
+
) -> tuple[CtkDiagnosticOracle, ...]:
|
|
585
|
+
return tuple(
|
|
586
|
+
CtkDiagnosticOracle(
|
|
587
|
+
code=cast(str, diagnostic_document["code"]),
|
|
588
|
+
severity=cast(DiagnosticSeverity, diagnostic_document["severity"]),
|
|
589
|
+
path=cast(str, diagnostic_document["path"]),
|
|
590
|
+
)
|
|
591
|
+
for diagnostic_document in diagnostic_documents
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _ir_oracle(ir_document: Mapping[str, object] | None) -> CtkIrOracle | None:
|
|
596
|
+
if ir_document is None:
|
|
597
|
+
return None
|
|
598
|
+
return CtkIrOracle(
|
|
599
|
+
ir_format=cast(str, ir_document["ir_format"]),
|
|
600
|
+
flow=cast(str, ir_document["flow"]),
|
|
601
|
+
version=cast(str, ir_document["version"]),
|
|
602
|
+
source_digest=cast(str, ir_document["source_digest"]),
|
|
603
|
+
profile_digest=cast(str | None, ir_document.get("profile_digest")),
|
|
604
|
+
dependency_digest=cast(str, ir_document["dependency_digest"]),
|
|
605
|
+
digest=cast(str, ir_document["digest"]),
|
|
606
|
+
canonical_document_digest=cast(str, ir_document["canonical_document_digest"]),
|
|
607
|
+
canonical_execution_ir_digest=cast(
|
|
608
|
+
str,
|
|
609
|
+
ir_document["canonical_execution_ir_digest"],
|
|
610
|
+
),
|
|
611
|
+
canonical_ir_digest=cast(str | None, ir_document.get("canonical_ir_digest")),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def load_ctk_corpus(manifest_path: str | Path) -> CtkCorpus:
|
|
616
|
+
"""Load a closed corpus and resolve fixture references within its directory."""
|
|
617
|
+
|
|
618
|
+
resolved_manifest_path = Path(manifest_path).resolve()
|
|
619
|
+
manifest_document = _load_json_document(resolved_manifest_path)
|
|
620
|
+
if manifest_errors := _manifest_schema_errors(manifest_document):
|
|
621
|
+
raise ValueError("invalid conformance corpus: " + "; ".join(manifest_errors))
|
|
622
|
+
manifest_mapping = cast(Mapping[str, object], manifest_document)
|
|
623
|
+
case_documents = cast(Sequence[Mapping[str, object]], manifest_mapping["cases"])
|
|
624
|
+
conformance_cases: list[CtkCase] = []
|
|
625
|
+
for case_document in case_documents:
|
|
626
|
+
source_contract = cast(Mapping[str, object], case_document["source"])
|
|
627
|
+
source_path = _safe_corpus_path(
|
|
628
|
+
resolved_manifest_path.parent,
|
|
629
|
+
cast(str, source_contract["path"]),
|
|
630
|
+
)
|
|
631
|
+
fixture_document = _load_json_document(source_path)
|
|
632
|
+
source_document = _canonical_clone(
|
|
633
|
+
_resolve_pointer(fixture_document, cast(str, source_contract["pointer"]))
|
|
634
|
+
)
|
|
635
|
+
for mutation_contract in cast(
|
|
636
|
+
Sequence[Mapping[str, object]], source_contract.get("mutations", [])
|
|
637
|
+
):
|
|
638
|
+
_set_pointer(
|
|
639
|
+
source_document,
|
|
640
|
+
cast(str, mutation_contract["path"]),
|
|
641
|
+
mutation_contract["replacement"],
|
|
642
|
+
)
|
|
643
|
+
validate_json_value(source_document, boundary="conformance source document")
|
|
644
|
+
if not isinstance(source_document, dict):
|
|
645
|
+
raise ValueError("conformance source pointer must select a JSON object")
|
|
646
|
+
|
|
647
|
+
expected_contract = cast(Mapping[str, object], case_document["expected"])
|
|
648
|
+
turn_contracts = cast(Sequence[Mapping[str, object]], case_document.get("turns", []))
|
|
649
|
+
conformance_cases.append(
|
|
650
|
+
CtkCase(
|
|
651
|
+
identifier=cast(str, case_document["identifier"]),
|
|
652
|
+
source_json=_canonical_json(source_document),
|
|
653
|
+
expected_compilation=cast(CompilationStatus, expected_contract["compilation"]),
|
|
654
|
+
expected_diagnostics=_diagnostic_oracles(
|
|
655
|
+
cast(Sequence[Mapping[str, object]], expected_contract["diagnostics"])
|
|
656
|
+
),
|
|
657
|
+
expected_ir=_ir_oracle(
|
|
658
|
+
cast(Mapping[str, object] | None, expected_contract.get("ir"))
|
|
659
|
+
),
|
|
660
|
+
turns=tuple(
|
|
661
|
+
CtkTurn(
|
|
662
|
+
payload_json=_canonical_json(turn_contract["payload"]),
|
|
663
|
+
expected_trace_json=_canonical_json(turn_contract["expected_trace"]),
|
|
664
|
+
)
|
|
665
|
+
for turn_contract in turn_contracts
|
|
666
|
+
),
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
return CtkCorpus(cases=tuple(conformance_cases))
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _observed_diagnostics(flow_check_report: FlowCheckReport) -> tuple[CtkDiagnosticOracle, ...]:
|
|
673
|
+
return tuple(
|
|
674
|
+
CtkDiagnosticOracle(
|
|
675
|
+
code=flow_diagnostic.code,
|
|
676
|
+
severity=flow_diagnostic.severity,
|
|
677
|
+
path=flow_diagnostic.path,
|
|
678
|
+
)
|
|
679
|
+
for flow_diagnostic in flow_check_report.diagnostics
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _ir_observation(flow_ir: FlowIR) -> dict[str, object]:
|
|
684
|
+
canonical_ir = flow_ir.to_dict()
|
|
685
|
+
canonical_execution_ir = copy.deepcopy(canonical_ir)
|
|
686
|
+
canonical_execution_ir.pop("profile_digest")
|
|
687
|
+
return {
|
|
688
|
+
"ir_format": flow_ir.ir_format,
|
|
689
|
+
"flow": flow_ir.flow,
|
|
690
|
+
"version": flow_ir.version,
|
|
691
|
+
"source_digest": flow_ir.source_digest,
|
|
692
|
+
"profile_digest": flow_ir.profile_digest,
|
|
693
|
+
"dependency_digest": flow_ir.dependency_digest,
|
|
694
|
+
"digest": flow_ir.digest,
|
|
695
|
+
"canonical_document_digest": _sha256(flow_ir.canonical_json),
|
|
696
|
+
"canonical_ir_digest": _sha256(_canonical_json(canonical_ir)),
|
|
697
|
+
"canonical_execution_ir_digest": _sha256(_canonical_json(canonical_execution_ir)),
|
|
698
|
+
"canonical_ir": canonical_ir,
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _runtime_trace(state: ServiceState) -> dict[str, object]:
|
|
703
|
+
trace_contract: dict[str, object] = {
|
|
704
|
+
"status": state.status,
|
|
705
|
+
"data": copy.deepcopy(state.data),
|
|
706
|
+
}
|
|
707
|
+
agent_response = state.agent_response
|
|
708
|
+
if agent_response is not None:
|
|
709
|
+
agent_contract: dict[str, object] = {"description": agent_response.description}
|
|
710
|
+
for response_field in ("payload_schema", "interactive", "error_message"):
|
|
711
|
+
response_contract = getattr(agent_response, response_field)
|
|
712
|
+
if response_contract is not None:
|
|
713
|
+
agent_contract[response_field] = copy.deepcopy(response_contract)
|
|
714
|
+
trace_contract["agent_response"] = agent_contract
|
|
715
|
+
return cast(dict[str, object], _canonical_clone(trace_contract))
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _oracle_failures(
|
|
719
|
+
conformance_case: CtkCase,
|
|
720
|
+
flow_check_report: FlowCheckReport,
|
|
721
|
+
ir_observation: Mapping[str, object] | None,
|
|
722
|
+
runtime_trace: Sequence[Mapping[str, object]],
|
|
723
|
+
) -> tuple[str, ...]:
|
|
724
|
+
oracle_failures: list[str] = []
|
|
725
|
+
if flow_check_report.compilation != conformance_case.expected_compilation:
|
|
726
|
+
oracle_failures.append(
|
|
727
|
+
"compilation mismatch: expected "
|
|
728
|
+
f"{conformance_case.expected_compilation!r}, observed "
|
|
729
|
+
f"{flow_check_report.compilation!r}"
|
|
730
|
+
)
|
|
731
|
+
observed_diagnostics = _observed_diagnostics(flow_check_report)
|
|
732
|
+
if observed_diagnostics != conformance_case.expected_diagnostics:
|
|
733
|
+
oracle_failures.append(
|
|
734
|
+
"diagnostic order mismatch: expected "
|
|
735
|
+
f"{_canonical_json([oracle.to_dict() for oracle in conformance_case.expected_diagnostics])}, "
|
|
736
|
+
f"observed {_canonical_json([oracle.to_dict() for oracle in observed_diagnostics])}"
|
|
737
|
+
)
|
|
738
|
+
if conformance_case.expected_ir is not None:
|
|
739
|
+
if ir_observation is None:
|
|
740
|
+
oracle_failures.append("IR oracle mismatch: no IR was produced")
|
|
741
|
+
else:
|
|
742
|
+
expected_ir_contract = conformance_case.expected_ir.to_dict()
|
|
743
|
+
selected_ir_observation = {
|
|
744
|
+
ir_field: ir_observation.get(ir_field) for ir_field in expected_ir_contract
|
|
745
|
+
}
|
|
746
|
+
if selected_ir_observation != expected_ir_contract:
|
|
747
|
+
oracle_failures.append(
|
|
748
|
+
"IR oracle mismatch: expected "
|
|
749
|
+
f"{_canonical_json(expected_ir_contract)}, observed "
|
|
750
|
+
f"{_canonical_json(selected_ir_observation)}"
|
|
751
|
+
)
|
|
752
|
+
expected_trace = tuple(turn.expected_trace() for turn in conformance_case.turns)
|
|
753
|
+
if tuple(runtime_trace) != expected_trace:
|
|
754
|
+
oracle_failures.append(
|
|
755
|
+
"runtime trace mismatch: expected "
|
|
756
|
+
f"{_canonical_json(expected_trace)}, observed {_canonical_json(runtime_trace)}"
|
|
757
|
+
)
|
|
758
|
+
return tuple(oracle_failures)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
async def _execute_runtime_trace(
|
|
762
|
+
conformance_case: CtkCase,
|
|
763
|
+
runtime_profile: FlowProfile,
|
|
764
|
+
) -> tuple[dict[str, object], ...]:
|
|
765
|
+
if not conformance_case.turns:
|
|
766
|
+
return ()
|
|
767
|
+
runtime = FlowRuntime(
|
|
768
|
+
conformance_case.source_document(),
|
|
769
|
+
tools=runtime_profile.tools,
|
|
770
|
+
subflows=runtime_profile.subflows,
|
|
771
|
+
clock=_fixed_clock,
|
|
772
|
+
log_id_generator=SnowflakeIdGenerator(
|
|
773
|
+
worker_id=0,
|
|
774
|
+
clock_milliseconds=_fixed_clock_milliseconds,
|
|
775
|
+
),
|
|
776
|
+
)
|
|
777
|
+
execution_state = runtime.new_state(f"ctk-{conformance_case.identifier}")
|
|
778
|
+
trace_contracts: list[dict[str, object]] = []
|
|
779
|
+
for conformance_turn in conformance_case.turns:
|
|
780
|
+
execution_state = await runtime.execute(execution_state, conformance_turn.payload())
|
|
781
|
+
trace_contracts.append(_runtime_trace(execution_state))
|
|
782
|
+
return tuple(trace_contracts)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
async def run_ctk_corpus(
|
|
786
|
+
conformance_corpus: CtkCorpus,
|
|
787
|
+
*,
|
|
788
|
+
profile: FlowProfile | None = None,
|
|
789
|
+
) -> CtkReport:
|
|
790
|
+
"""Execute every source, IR, diagnostic-order, and runtime-trace oracle."""
|
|
791
|
+
|
|
792
|
+
runtime_profile = profile or reference_profile()
|
|
793
|
+
case_reports: list[CtkCaseReport] = []
|
|
794
|
+
for conformance_case in conformance_corpus.cases:
|
|
795
|
+
source_document = conformance_case.source_document()
|
|
796
|
+
flow_check_report = check_flow(source_document, profile=runtime_profile)
|
|
797
|
+
ir_observation: dict[str, object] | None = None
|
|
798
|
+
runtime_trace: tuple[dict[str, object], ...] = ()
|
|
799
|
+
execution_failure: str | None = None
|
|
800
|
+
if flow_check_report.compilation == "succeeded":
|
|
801
|
+
try:
|
|
802
|
+
ir_observation = _ir_observation(
|
|
803
|
+
build_flow_ir(source_document, profile=runtime_profile)
|
|
804
|
+
)
|
|
805
|
+
runtime_trace = await _execute_runtime_trace(conformance_case, runtime_profile)
|
|
806
|
+
except Exception as runtime_error: # noqa: BLE001
|
|
807
|
+
execution_failure = (
|
|
808
|
+
"conformance execution failed: "
|
|
809
|
+
f"{runtime_error.__class__.__name__}: {runtime_error}"
|
|
810
|
+
)
|
|
811
|
+
oracle_failures = list(
|
|
812
|
+
_oracle_failures(
|
|
813
|
+
conformance_case,
|
|
814
|
+
flow_check_report,
|
|
815
|
+
ir_observation,
|
|
816
|
+
runtime_trace,
|
|
817
|
+
)
|
|
818
|
+
)
|
|
819
|
+
if execution_failure is not None:
|
|
820
|
+
oracle_failures.append(execution_failure)
|
|
821
|
+
case_reports.append(
|
|
822
|
+
CtkCaseReport(
|
|
823
|
+
identifier=conformance_case.identifier,
|
|
824
|
+
check_report_json=_canonical_json(flow_check_report.to_dict()),
|
|
825
|
+
ir_observation_json=(
|
|
826
|
+
_canonical_json(ir_observation) if ir_observation is not None else None
|
|
827
|
+
),
|
|
828
|
+
runtime_trace_json=_canonical_json(runtime_trace),
|
|
829
|
+
failures=tuple(oracle_failures),
|
|
830
|
+
)
|
|
831
|
+
)
|
|
832
|
+
return CtkReport(
|
|
833
|
+
corpus_digest=conformance_corpus.digest,
|
|
834
|
+
profile_identifier=runtime_profile.identifier,
|
|
835
|
+
case_reports=tuple(case_reports),
|
|
836
|
+
)
|