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.
Files changed (92) hide show
  1. flowspec2/__init__.py +105 -0
  2. flowspec2/authoring/__init__.py +275 -0
  3. flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
  4. flowspec2/authoring/authoring-evidence.schema.json +455 -0
  5. flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
  6. flowspec2/authoring/benchmark.py +1409 -0
  7. flowspec2/authoring/corpus/await_correction.case.json +220 -0
  8. flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
  9. flowspec2/authoring/corpus/gated_derive.case.json +100 -0
  10. flowspec2/authoring/corpus/linear.case.json +55 -0
  11. flowspec2/authoring/corpus/manifest.json +31 -0
  12. flowspec2/authoring/corpus/subflow.case.json +66 -0
  13. flowspec2/authoring/corpus/terminal.case.json +94 -0
  14. flowspec2/authoring/corpus.py +307 -0
  15. flowspec2/authoring/ctk.py +836 -0
  16. flowspec2/authoring/detached_signature.py +120 -0
  17. flowspec2/authoring/evidence.py +311 -0
  18. flowspec2/authoring/evidence_signature.py +187 -0
  19. flowspec2/authoring/evidence_verification.py +229 -0
  20. flowspec2/authoring/gemini.py +215 -0
  21. flowspec2/authoring/operational-corpus.json +61 -0
  22. flowspec2/authoring/operational.py +956 -0
  23. flowspec2/authoring/operational_providers.py +167 -0
  24. flowspec2/authoring/presentation_review.py +1013 -0
  25. flowspec2/authoring/presentation_review_signature.py +305 -0
  26. flowspec2/authoring/projection.py +299 -0
  27. flowspec2/authoring/provider_prompt.py +83 -0
  28. flowspec2/backends/__init__.py +82 -0
  29. flowspec2/backends/http.py +189 -0
  30. flowspec2/checker.py +238 -0
  31. flowspec2/cli.py +789 -0
  32. flowspec2/cli_parser.py +345 -0
  33. flowspec2/clock.py +23 -0
  34. flowspec2/compat/__init__.py +47 -0
  35. flowspec2/compat/models.py +82 -0
  36. flowspec2/compat/open_workflow.py +616 -0
  37. flowspec2/compat/rasa.py +19 -0
  38. flowspec2/compat/rasa_export.py +876 -0
  39. flowspec2/compat/rasa_import.py +992 -0
  40. flowspec2/compat/rasa_shared.py +270 -0
  41. flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
  42. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
  43. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
  44. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
  45. flowspec2/compat/tool_profiles.py +149 -0
  46. flowspec2/compat/yaml.py +147 -0
  47. flowspec2/compiler.py +957 -0
  48. flowspec2/compiler_contracts.py +17 -0
  49. flowspec2/compiler_resume_contracts.py +594 -0
  50. flowspec2/compiler_schema_relations.py +1830 -0
  51. flowspec2/compiler_tool_contracts.py +245 -0
  52. flowspec2/compiler_value_contracts.py +447 -0
  53. flowspec2/derive.py +32 -0
  54. flowspec2/diagnostics.py +94 -0
  55. flowspec2/domains.py +489 -0
  56. flowspec2/experimental/__init__.py +57 -0
  57. flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
  58. flowspec2/experimental/v3-preview-loss-policy.json +161 -0
  59. flowspec2/experimental/v3_lowering.py +928 -0
  60. flowspec2/experimental/v3_preview.py +2460 -0
  61. flowspec2/flowspec-2.schema.json +635 -0
  62. flowspec2/interactive.py +300 -0
  63. flowspec2/ir.py +1459 -0
  64. flowspec2/json_codec.py +120 -0
  65. flowspec2/llm.py +366 -0
  66. flowspec2/models.py +121 -0
  67. flowspec2/nodes.py +1537 -0
  68. flowspec2/observability.py +151 -0
  69. flowspec2/predicates.py +89 -0
  70. flowspec2/profiles.py +118 -0
  71. flowspec2/py.typed +0 -0
  72. flowspec2/runtime.py +1059 -0
  73. flowspec2/schema.py +54 -0
  74. flowspec2/schema_contracts.py +203 -0
  75. flowspec2/semantic_derive_contracts.py +530 -0
  76. flowspec2/semantic_path_contracts.py +528 -0
  77. flowspec2/semantic_predicate_contracts.py +355 -0
  78. flowspec2/semantic_schema_contracts.py +137 -0
  79. flowspec2/semantic_source_contracts.py +21 -0
  80. flowspec2/semantic_state_contracts.py +450 -0
  81. flowspec2/semantic_support.py +40 -0
  82. flowspec2/semantics.py +410 -0
  83. flowspec2/state_migration.py +1034 -0
  84. flowspec2/subflows/__init__.py +1117 -0
  85. flowspec2/subflows/address.py +328 -0
  86. flowspec2/subflows/identification.py +1031 -0
  87. flowspec2/tools.py +1154 -0
  88. flowspec2-1.0.0.dist-info/METADATA +482 -0
  89. flowspec2-1.0.0.dist-info/RECORD +92 -0
  90. flowspec2-1.0.0.dist-info/WHEEL +4 -0
  91. flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
  92. 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
+ )