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,956 @@
1
+ """Report-only evidence for model-mediated routing and extraction behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import hashlib
7
+ import json
8
+ import re
9
+ from collections.abc import Callable, Mapping
10
+ from dataclasses import dataclass
11
+ from importlib.metadata import version as package_version
12
+ from importlib.resources import files
13
+ from pathlib import Path
14
+ from typing import Any, Final, Literal, cast
15
+
16
+ import jsonschema
17
+
18
+ from flowspec2.domains import make_slot_model
19
+ from flowspec2.json_codec import StrictJsonError, strict_json_loads, validate_json_value
20
+ from flowspec2.llm import (
21
+ StructuredOutputRequest,
22
+ build_extraction_request,
23
+ build_route_request,
24
+ )
25
+ from flowspec2.models import AgentResponse
26
+
27
+ from .evidence import AuthoringProviderProvenance
28
+ from .evidence_verification import AuthoringEvidenceVerification, verify_authoring_evidence
29
+
30
+ AUTHORING_OPERATIONAL_EVIDENCE_FORMAT: Final[str] = "flowspec2/authoring-operational-evidence@2"
31
+ OPERATIONAL_EVIDENCE_CLASSIFICATION: Final[str] = "report_only"
32
+ OPERATIONAL_CORPUS_FORMAT: Final[str] = "flowspec2/operational-corpus@2"
33
+ OPERATIONAL_PROBE_FORMAT: Final[str] = "flowspec2/operational-probe@2"
34
+ REFERENCE_OPERATIONAL_CORPUS_IDENTIFIER: Final[str] = "flowspec2_reference_operational"
35
+ OPERATIONAL_PROMPT_FORMAT: Final[str] = "flowspec2/operational-structured-output@1"
36
+
37
+ _CORPUS_PATH: Final[Path] = Path(__file__).with_name("operational-corpus.json")
38
+ _EVIDENCE_SCHEMA_PATH: Final[Path] = Path(__file__).with_name(
39
+ "authoring-operational-evidence.schema.json"
40
+ )
41
+ _IDENTIFIER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
42
+ _DIGEST_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]{64}$")
43
+ _SECRET_KEY_PATTERN: Final[re.Pattern[str]] = re.compile(
44
+ r"(?:api[_-]?key|authorization|credential|password|secret|access[_-]?token)",
45
+ re.IGNORECASE,
46
+ )
47
+
48
+ OperationalKind = Literal["route", "extract"]
49
+ OperationalSubjectMode = Literal["authored", "counterfactual"]
50
+ OperationalDiagnostic = Literal[
51
+ "invalid_json",
52
+ "schema_mismatch",
53
+ "semantic_mismatch",
54
+ ]
55
+
56
+
57
+ def _canonical_json(json_document: object) -> str:
58
+ return json.dumps(
59
+ json_document,
60
+ ensure_ascii=False,
61
+ allow_nan=False,
62
+ separators=(",", ":"),
63
+ sort_keys=True,
64
+ )
65
+
66
+
67
+ def _sha256(serialized_contract: str) -> str:
68
+ return hashlib.sha256(serialized_contract.encode("utf-8")).hexdigest()
69
+
70
+
71
+ def _request_contract(structured_request: StructuredOutputRequest) -> dict[str, object]:
72
+ return {
73
+ "system_instruction": structured_request.system,
74
+ "prompt": structured_request.prompt,
75
+ "response_schema": structured_request.response_schema,
76
+ }
77
+
78
+
79
+ def _operational_prompt_contract() -> dict[str, object]:
80
+ route_request = build_route_request(
81
+ "reference citizen route input",
82
+ [
83
+ {
84
+ "flow": "reference_flow",
85
+ "route": {
86
+ "description": "Reference route description.",
87
+ "trigger_phrases": ["reference trigger phrase"],
88
+ },
89
+ }
90
+ ],
91
+ )
92
+ extraction_request = build_extraction_request(
93
+ "reference citizen extraction input",
94
+ AgentResponse(
95
+ description="Reference extraction prompt.",
96
+ payload_schema={
97
+ "type": "object",
98
+ "additionalProperties": False,
99
+ "properties": {
100
+ "reference_slot": {
101
+ "description": "Reference extraction hint.",
102
+ "enum": ["reference_token"],
103
+ }
104
+ },
105
+ "required": ["reference_slot"],
106
+ },
107
+ ),
108
+ )
109
+ return {
110
+ "format": OPERATIONAL_PROMPT_FORMAT,
111
+ "request_fields": ["system_instruction", "prompt", "response_schema"],
112
+ "route_request": _request_contract(route_request),
113
+ "extraction_request": _request_contract(extraction_request),
114
+ }
115
+
116
+
117
+ OPERATIONAL_PROMPT_DIGEST: Final[str] = _sha256(_canonical_json(_operational_prompt_contract()))
118
+
119
+
120
+ def _contains_secret_key(json_document: object) -> bool:
121
+ if isinstance(json_document, Mapping):
122
+ return any(
123
+ _SECRET_KEY_PATTERN.search(str(property_name)) is not None
124
+ or _contains_secret_key(property_value)
125
+ for property_name, property_value in json_document.items()
126
+ )
127
+ if isinstance(json_document, list):
128
+ return any(_contains_secret_key(element_value) for element_value in json_document)
129
+ return False
130
+
131
+
132
+ def _decode_pointer_segment(pointer_segment: str) -> str:
133
+ return pointer_segment.replace("~1", "/").replace("~0", "~")
134
+
135
+
136
+ def _resolve_pointer(json_document: object, json_pointer: str) -> object:
137
+ if not json_pointer.startswith("/"):
138
+ raise ValueError("operational subject pointer must be an absolute JSON Pointer")
139
+ current_value = json_document
140
+ for encoded_segment in json_pointer[1:].split("/"):
141
+ pointer_segment = _decode_pointer_segment(encoded_segment)
142
+ if isinstance(current_value, Mapping):
143
+ if pointer_segment not in current_value:
144
+ raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
145
+ current_value = current_value[pointer_segment]
146
+ elif isinstance(current_value, list):
147
+ if not pointer_segment.isdigit() or int(pointer_segment) >= len(current_value):
148
+ raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
149
+ current_value = current_value[int(pointer_segment)]
150
+ else:
151
+ raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
152
+ return current_value
153
+
154
+
155
+ def _replace_pointer(json_document: object, json_pointer: str, replacement: object) -> None:
156
+ if not json_pointer.startswith("/"):
157
+ raise ValueError("operational subject pointer must be an absolute JSON Pointer")
158
+ encoded_segments = json_pointer[1:].split("/")
159
+ if not encoded_segments or encoded_segments == [""]:
160
+ raise ValueError("operational subject pointer cannot replace the document root")
161
+ parent_value = json_document
162
+ for encoded_segment in encoded_segments[:-1]:
163
+ pointer_segment = _decode_pointer_segment(encoded_segment)
164
+ if isinstance(parent_value, dict) and pointer_segment in parent_value:
165
+ parent_value = parent_value[pointer_segment]
166
+ elif (
167
+ isinstance(parent_value, list)
168
+ and pointer_segment.isdigit()
169
+ and int(pointer_segment) < len(parent_value)
170
+ ):
171
+ parent_value = parent_value[int(pointer_segment)]
172
+ else:
173
+ raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
174
+ final_segment = _decode_pointer_segment(encoded_segments[-1])
175
+ if isinstance(parent_value, dict) and final_segment in parent_value:
176
+ parent_value[final_segment] = replacement
177
+ return
178
+ if (
179
+ isinstance(parent_value, list)
180
+ and final_segment.isdigit()
181
+ and int(final_segment) < len(parent_value)
182
+ ):
183
+ parent_value[int(final_segment)] = replacement
184
+ return
185
+ raise ValueError(f"operational subject pointer does not exist: {json_pointer}")
186
+
187
+
188
+ @dataclass(frozen=True)
189
+ class OperationalProbe:
190
+ """One public model-mediated observation bound to an authoring case."""
191
+
192
+ identifier: str
193
+ pair_identifier: str
194
+ subject_mode: OperationalSubjectMode
195
+ operation: OperationalKind
196
+ authoring_case_identifier: str
197
+ subject_pointer: str
198
+ citizen_input: str
199
+ expected_json: str
200
+ counterfactual_json: str | None = None
201
+
202
+ def __post_init__(self) -> None:
203
+ if not _IDENTIFIER_PATTERN.fullmatch(self.identifier):
204
+ raise ValueError("operational probe identifier is invalid")
205
+ if not _IDENTIFIER_PATTERN.fullmatch(self.pair_identifier):
206
+ raise ValueError("operational probe pair identifier is invalid")
207
+ if self.subject_mode not in {"authored", "counterfactual"}:
208
+ raise ValueError("operational probe subject mode is unsupported")
209
+ if self.operation not in {"route", "extract"}:
210
+ raise ValueError("operational probe operation is unsupported")
211
+ if not _IDENTIFIER_PATTERN.fullmatch(self.authoring_case_identifier):
212
+ raise ValueError("operational probe authoring case identifier is invalid")
213
+ if not self.subject_pointer.startswith("/"):
214
+ raise ValueError("operational probe subject pointer must be a JSON Pointer")
215
+ if not self.citizen_input.strip():
216
+ raise ValueError("operational probe citizen input must be non-empty")
217
+ expected_output = strict_json_loads(self.expected_json)
218
+ validate_json_value(expected_output, boundary="operational expected output")
219
+ if self.expected_json != _canonical_json(expected_output):
220
+ raise ValueError("operational probe expected output must be canonical JSON")
221
+ if self.subject_mode == "authored" and self.counterfactual_json is not None:
222
+ raise ValueError("authored operational probes cannot replace their subject")
223
+ if self.subject_mode == "counterfactual" and self.counterfactual_json is None:
224
+ raise ValueError("counterfactual operational probes require a replacement subject")
225
+ if self.counterfactual_json is not None:
226
+ counterfactual_subject = strict_json_loads(self.counterfactual_json)
227
+ validate_json_value(
228
+ counterfactual_subject,
229
+ boundary="operational counterfactual subject",
230
+ )
231
+ if self.counterfactual_json != _canonical_json(counterfactual_subject):
232
+ raise ValueError("operational counterfactual subject must be canonical JSON")
233
+
234
+ @property
235
+ def expected_output(self) -> object:
236
+ return strict_json_loads(self.expected_json)
237
+
238
+ @property
239
+ def counterfactual_subject(self) -> object | None:
240
+ return (
241
+ None
242
+ if self.counterfactual_json is None
243
+ else strict_json_loads(self.counterfactual_json)
244
+ )
245
+
246
+ def to_dict(self) -> dict[str, object]:
247
+ return {
248
+ "identifier": self.identifier,
249
+ "pair_identifier": self.pair_identifier,
250
+ "subject_mode": self.subject_mode,
251
+ "operation": self.operation,
252
+ "authoring_case_identifier": self.authoring_case_identifier,
253
+ "subject_pointer": self.subject_pointer,
254
+ "citizen_input": self.citizen_input,
255
+ "expected": self.expected_output,
256
+ "counterfactual": self.counterfactual_subject,
257
+ }
258
+
259
+
260
+ @dataclass(frozen=True)
261
+ class OperationalCorpus:
262
+ """Closed packaged operational probes and their content identity."""
263
+
264
+ identifier: str
265
+ digest: str
266
+ probes: tuple[OperationalProbe, ...]
267
+ format_identifier: str = OPERATIONAL_CORPUS_FORMAT
268
+ probe_format_identifier: str = OPERATIONAL_PROBE_FORMAT
269
+
270
+ def __post_init__(self) -> None:
271
+ if self.format_identifier != OPERATIONAL_CORPUS_FORMAT:
272
+ raise ValueError("operational corpus format is unsupported")
273
+ if self.probe_format_identifier != OPERATIONAL_PROBE_FORMAT:
274
+ raise ValueError("operational probe format is unsupported")
275
+ if self.identifier != REFERENCE_OPERATIONAL_CORPUS_IDENTIFIER:
276
+ raise ValueError("operational corpus identifier is unsupported")
277
+ if not _DIGEST_PATTERN.fullmatch(self.digest):
278
+ raise ValueError("operational corpus digest must be lowercase SHA-256")
279
+ identifiers = tuple(probe.identifier for probe in self.probes)
280
+ if not identifiers or identifiers != tuple(sorted(identifiers)):
281
+ raise ValueError("operational probes must be non-empty and sorted")
282
+ if len(set(identifiers)) != len(identifiers):
283
+ raise ValueError("operational probe identifiers must be unique")
284
+ probes_by_pair: dict[str, list[OperationalProbe]] = {}
285
+ for probe in self.probes:
286
+ probes_by_pair.setdefault(probe.pair_identifier, []).append(probe)
287
+ for pair_identifier, paired_probes in probes_by_pair.items():
288
+ if len(paired_probes) != 2 or {probe.subject_mode for probe in paired_probes} != {
289
+ "authored",
290
+ "counterfactual",
291
+ }:
292
+ raise ValueError(
293
+ f"operational pair must contain authored and counterfactual probes: "
294
+ f"{pair_identifier}"
295
+ )
296
+ comparison_fields = {
297
+ (
298
+ probe.operation,
299
+ probe.authoring_case_identifier,
300
+ probe.subject_pointer,
301
+ probe.citizen_input,
302
+ )
303
+ for probe in paired_probes
304
+ }
305
+ if len(comparison_fields) != 1:
306
+ raise ValueError(f"operational pair inputs are inconsistent: {pair_identifier}")
307
+ if len({probe.expected_json for probe in paired_probes}) != 2:
308
+ raise ValueError(
309
+ f"operational pair must expect distinct outcomes: {pair_identifier}"
310
+ )
311
+
312
+ def metadata(self) -> dict[str, object]:
313
+ return {
314
+ "format": self.format_identifier,
315
+ "probe_format": self.probe_format_identifier,
316
+ "identifier": self.identifier,
317
+ "digest": self.digest,
318
+ "probe_identifiers": [probe.identifier for probe in self.probes],
319
+ }
320
+
321
+
322
+ _operational_corpus_cache: OperationalCorpus | None = None
323
+
324
+
325
+ def load_reference_operational_corpus() -> OperationalCorpus:
326
+ """Load the immutable public operational probe corpus."""
327
+
328
+ global _operational_corpus_cache
329
+ if _operational_corpus_cache is not None:
330
+ return _operational_corpus_cache
331
+ corpus_resource = files("flowspec2.authoring").joinpath("operational-corpus.json")
332
+ serialized_corpus = corpus_resource.read_text(encoding="utf-8")
333
+ decoded_corpus = strict_json_loads(serialized_corpus)
334
+ if not isinstance(decoded_corpus, dict):
335
+ raise ValueError("operational corpus must be a JSON object")
336
+ if set(decoded_corpus) != {"format", "case_format", "identifier", "probes"}:
337
+ raise ValueError("operational corpus keys do not match the closed contract")
338
+ if decoded_corpus["format"] != OPERATIONAL_CORPUS_FORMAT:
339
+ raise ValueError("operational corpus format is unsupported")
340
+ if decoded_corpus["case_format"] != OPERATIONAL_PROBE_FORMAT:
341
+ raise ValueError("operational probe format is unsupported")
342
+ raw_probes = decoded_corpus["probes"]
343
+ if not isinstance(raw_probes, list):
344
+ raise ValueError("operational corpus probes must be an array")
345
+ probes: list[OperationalProbe] = []
346
+ for raw_probe in raw_probes:
347
+ if not isinstance(raw_probe, dict) or set(raw_probe) != {
348
+ "identifier",
349
+ "pair_identifier",
350
+ "subject_mode",
351
+ "operation",
352
+ "authoring_case_identifier",
353
+ "subject_pointer",
354
+ "citizen_input",
355
+ "expected",
356
+ "counterfactual",
357
+ }:
358
+ raise ValueError("operational probe keys do not match the closed contract")
359
+ probes.append(
360
+ OperationalProbe(
361
+ identifier=cast(str, raw_probe["identifier"]),
362
+ pair_identifier=cast(str, raw_probe["pair_identifier"]),
363
+ subject_mode=cast(OperationalSubjectMode, raw_probe["subject_mode"]),
364
+ operation=cast(OperationalKind, raw_probe["operation"]),
365
+ authoring_case_identifier=cast(str, raw_probe["authoring_case_identifier"]),
366
+ subject_pointer=cast(str, raw_probe["subject_pointer"]),
367
+ citizen_input=cast(str, raw_probe["citizen_input"]),
368
+ expected_json=_canonical_json(raw_probe["expected"]),
369
+ counterfactual_json=(
370
+ None
371
+ if raw_probe["counterfactual"] is None
372
+ else _canonical_json(raw_probe["counterfactual"])
373
+ ),
374
+ )
375
+ )
376
+ canonical_corpus = _canonical_json(decoded_corpus)
377
+ _operational_corpus_cache = OperationalCorpus(
378
+ identifier=cast(str, decoded_corpus["identifier"]),
379
+ digest=_sha256(canonical_corpus),
380
+ probes=tuple(probes),
381
+ )
382
+ return _operational_corpus_cache
383
+
384
+
385
+ @dataclass(frozen=True)
386
+ class OperationalProbeRequest:
387
+ """Exact provider-neutral inputs for one operational probe."""
388
+
389
+ probe_identifier: str
390
+ operation: OperationalKind
391
+ system_instruction: str
392
+ prompt: str
393
+ response_schema_json: str
394
+
395
+ def __post_init__(self) -> None:
396
+ if not _IDENTIFIER_PATTERN.fullmatch(self.probe_identifier):
397
+ raise ValueError("operational request probe identifier is invalid")
398
+ if self.operation not in {"route", "extract"}:
399
+ raise ValueError("operational request operation is unsupported")
400
+ if not self.system_instruction or not self.prompt:
401
+ raise ValueError("operational request instructions must be non-empty")
402
+ response_schema = strict_json_loads(self.response_schema_json)
403
+ if not isinstance(response_schema, dict):
404
+ raise ValueError("operational response schema must be an object")
405
+ if self.response_schema_json != _canonical_json(response_schema):
406
+ raise ValueError("operational response schema must be canonical JSON")
407
+ jsonschema.Draft202012Validator.check_schema(response_schema)
408
+
409
+ @classmethod
410
+ def from_structured_request(
411
+ cls,
412
+ probe: OperationalProbe,
413
+ structured_request: StructuredOutputRequest,
414
+ ) -> OperationalProbeRequest:
415
+ return cls(
416
+ probe_identifier=probe.identifier,
417
+ operation=probe.operation,
418
+ system_instruction=structured_request.system,
419
+ prompt=structured_request.prompt,
420
+ response_schema_json=_canonical_json(structured_request.response_schema),
421
+ )
422
+
423
+ def response_schema(self) -> dict[str, Any]:
424
+ return cast(dict[str, Any], strict_json_loads(self.response_schema_json))
425
+
426
+ def to_dict(self) -> dict[str, object]:
427
+ return {
428
+ "system_instruction": self.system_instruction,
429
+ "prompt": self.prompt,
430
+ "response_schema": self.response_schema(),
431
+ }
432
+
433
+
434
+ @dataclass(frozen=True)
435
+ class OperationalModelResponse:
436
+ """Raw completed provider turn and optional effective model identity."""
437
+
438
+ raw_output: str
439
+ effective_model_version: str | None = None
440
+
441
+ def __post_init__(self) -> None:
442
+ if self.effective_model_version is not None:
443
+ normalized_model = self.effective_model_version.strip()
444
+ if not normalized_model:
445
+ raise ValueError("operational effective model version must be non-empty")
446
+ object.__setattr__(self, "effective_model_version", normalized_model)
447
+
448
+
449
+ @dataclass(frozen=True)
450
+ class _PreparedProbe:
451
+ probe: OperationalProbe
452
+ correction_round: int
453
+ source_sha256: str
454
+ subject_sha256: str
455
+ request: OperationalProbeRequest
456
+ expected_output_json: str
457
+
458
+ @property
459
+ def expected_output(self) -> object:
460
+ return strict_json_loads(self.expected_output_json)
461
+
462
+
463
+ @dataclass(frozen=True)
464
+ class OperationalCapture:
465
+ """One exact request, raw model turn, and deterministic observation."""
466
+
467
+ probe_identifier: str
468
+ pair_identifier: str
469
+ subject_mode: OperationalSubjectMode
470
+ operation: OperationalKind
471
+ authoring_case_identifier: str
472
+ correction_round: int
473
+ source_sha256: str
474
+ subject_pointer: str
475
+ subject_sha256: str
476
+ request_json: str
477
+ expected_output_json: str
478
+ raw_output: str
479
+ raw_output_sha256: str
480
+ parsed_output_json: str | None
481
+ effective_model_version: str | None
482
+ matched: bool
483
+ diagnostic: OperationalDiagnostic | None
484
+
485
+ def to_dict(self) -> dict[str, object]:
486
+ return {
487
+ "probe_identifier": self.probe_identifier,
488
+ "pair_identifier": self.pair_identifier,
489
+ "subject_mode": self.subject_mode,
490
+ "operation": self.operation,
491
+ "authoring_case_identifier": self.authoring_case_identifier,
492
+ "correction_round": self.correction_round,
493
+ "source_sha256": self.source_sha256,
494
+ "subject_pointer": self.subject_pointer,
495
+ "subject_sha256": self.subject_sha256,
496
+ "request": strict_json_loads(self.request_json),
497
+ "expected_output": strict_json_loads(self.expected_output_json),
498
+ "raw_output": self.raw_output,
499
+ "raw_output_sha256": self.raw_output_sha256,
500
+ "parsed_output": (
501
+ strict_json_loads(self.parsed_output_json)
502
+ if self.parsed_output_json is not None
503
+ else None
504
+ ),
505
+ "effective_model_version": self.effective_model_version,
506
+ "matched": self.matched,
507
+ "diagnostic": self.diagnostic,
508
+ }
509
+
510
+
511
+ def _completed_sources(
512
+ serialized_authoring_evidence: str,
513
+ verified_authoring_evidence: AuthoringEvidenceVerification,
514
+ ) -> dict[str, dict[str, Any]]:
515
+ recomputed_verification = verify_authoring_evidence(
516
+ serialized_authoring_evidence,
517
+ expected_repository_revision=verified_authoring_evidence.repository_revision,
518
+ )
519
+ if recomputed_verification != verified_authoring_evidence:
520
+ raise ValueError("authoring evidence verification does not match its exact document")
521
+ evidence_document = strict_json_loads(serialized_authoring_evidence)
522
+ if not isinstance(evidence_document, dict):
523
+ raise ValueError("authoring evidence must be a JSON object")
524
+ if evidence_document.get("digest") != verified_authoring_evidence.digest:
525
+ raise ValueError("verified authoring evidence digest does not match its document")
526
+ captures = cast(list[dict[str, Any]], evidence_document["captures"])
527
+ captures_by_key = {
528
+ (capture["case_identifier"], capture["correction_round"]): capture for capture in captures
529
+ }
530
+ completed_sources: dict[str, dict[str, Any]] = {}
531
+ report = cast(dict[str, Any], evidence_document["report"])
532
+ for case_result in cast(list[dict[str, Any]], report["case_results"]):
533
+ if case_result["succeeded"] is not True:
534
+ continue
535
+ attempts = cast(list[dict[str, Any]], case_result["attempts"])
536
+ final_round = cast(int, attempts[-1]["correction_round"])
537
+ case_identifier = cast(str, case_result["case_identifier"])
538
+ capture = captures_by_key[(case_identifier, final_round)]
539
+ source_document = strict_json_loads(cast(str, capture["authored_source"]))
540
+ if not isinstance(source_document, dict):
541
+ raise ValueError("successful authored source must be a JSON object")
542
+ completed_sources[case_identifier] = {
543
+ "correction_round": final_round,
544
+ "source_sha256": capture["source_sha256"],
545
+ "source": source_document,
546
+ }
547
+ return completed_sources
548
+
549
+
550
+ def _catalog_contract(completed_sources: Mapping[str, Mapping[str, Any]]) -> dict[str, object]:
551
+ source_closure = [
552
+ {
553
+ "case_identifier": case_identifier,
554
+ "correction_round": completed_sources[case_identifier]["correction_round"],
555
+ "source_sha256": completed_sources[case_identifier]["source_sha256"],
556
+ }
557
+ for case_identifier in sorted(completed_sources)
558
+ ]
559
+ return {
560
+ "source_closure": source_closure,
561
+ "digest": _sha256(_canonical_json(source_closure)),
562
+ }
563
+
564
+
565
+ def _extraction_agent_response(source_document: dict[str, Any], path_index: int) -> AgentResponse:
566
+ path = cast(list[dict[str, Any]], source_document["path"])
567
+ path_step = path[path_index]
568
+ slot_name = cast(str, path_step["slot"])
569
+ slots = cast(dict[str, dict[str, Any]], source_document["slots"])
570
+ slot_contract = slots[slot_name]
571
+ prompt_contract = cast(dict[str, Any], path_step["prompt"])
572
+ slot_model = make_slot_model(
573
+ slot_name,
574
+ cast(str, slot_contract["domain"]),
575
+ cast(dict[str, Any], source_document["domains"]),
576
+ nullable=cast(bool, slot_contract.get("nullable", False)),
577
+ extract_hint=cast(str | None, prompt_contract.get("extract_hint")),
578
+ )
579
+ return AgentResponse(
580
+ description=cast(str, prompt_contract["text"]),
581
+ payload_schema=slot_model.model_json_schema(),
582
+ interactive=cast(dict[str, Any] | None, path_step.get("interactive")),
583
+ )
584
+
585
+
586
+ def _prepare_probes(
587
+ serialized_authoring_evidence: str,
588
+ verified_authoring_evidence: AuthoringEvidenceVerification,
589
+ ) -> tuple[tuple[_PreparedProbe, ...], dict[str, object]]:
590
+ completed_sources = _completed_sources(
591
+ serialized_authoring_evidence,
592
+ verified_authoring_evidence,
593
+ )
594
+ corpus = load_reference_operational_corpus()
595
+ prepared_probes: list[_PreparedProbe] = []
596
+ for probe in corpus.probes:
597
+ source_contract = completed_sources.get(probe.authoring_case_identifier)
598
+ if source_contract is None:
599
+ raise ValueError(
600
+ f"operational probe requires successful authoring case: "
601
+ f"{probe.authoring_case_identifier}"
602
+ )
603
+ source_document = cast(dict[str, Any], source_contract["source"])
604
+ effective_source_document = copy.deepcopy(source_document)
605
+ if probe.subject_mode == "counterfactual":
606
+ _replace_pointer(
607
+ effective_source_document,
608
+ probe.subject_pointer,
609
+ probe.counterfactual_subject,
610
+ )
611
+ subject_value = _resolve_pointer(effective_source_document, probe.subject_pointer)
612
+ expected_output: object
613
+ if probe.operation == "route":
614
+ if probe.subject_pointer != "/route/trigger_phrases":
615
+ raise ValueError("route probe must bind route.trigger_phrases")
616
+ catalog_flows = [
617
+ (
618
+ effective_source_document
619
+ if case_identifier == probe.authoring_case_identifier
620
+ else cast(dict[str, Any], completed_sources[case_identifier]["source"])
621
+ )
622
+ for case_identifier in sorted(completed_sources)
623
+ ]
624
+ structured_request = build_route_request(probe.citizen_input, catalog_flows)
625
+ expected_output = {
626
+ "service": effective_source_document["flow"]
627
+ if cast(dict[str, Any], probe.expected_output)["selected"] is True
628
+ else None
629
+ }
630
+ else:
631
+ pointer_segments = probe.subject_pointer.strip("/").split("/")
632
+ if (
633
+ len(pointer_segments) != 4
634
+ or pointer_segments[0] != "path"
635
+ or not pointer_segments[1].isdigit()
636
+ or pointer_segments[2:] != ["prompt", "extract_hint"]
637
+ ):
638
+ raise ValueError("extract probe must bind a path prompt.extract_hint")
639
+ agent_response = _extraction_agent_response(
640
+ effective_source_document,
641
+ int(pointer_segments[1]),
642
+ )
643
+ structured_request = build_extraction_request(probe.citizen_input, agent_response)
644
+ expected_output = probe.expected_output
645
+ prepared_probes.append(
646
+ _PreparedProbe(
647
+ probe=probe,
648
+ correction_round=cast(int, source_contract["correction_round"]),
649
+ source_sha256=cast(str, source_contract["source_sha256"]),
650
+ subject_sha256=_sha256(_canonical_json(subject_value)),
651
+ request=OperationalProbeRequest.from_structured_request(
652
+ probe,
653
+ structured_request,
654
+ ),
655
+ expected_output_json=_canonical_json(expected_output),
656
+ )
657
+ )
658
+ return tuple(prepared_probes), _catalog_contract(completed_sources)
659
+
660
+
661
+ def _capture(
662
+ prepared_probe: _PreparedProbe,
663
+ model_response: OperationalModelResponse,
664
+ ) -> OperationalCapture:
665
+ parsed_output: object | None = None
666
+ diagnostic: OperationalDiagnostic | None = None
667
+ try:
668
+ parsed_output = strict_json_loads(model_response.raw_output)
669
+ except (json.JSONDecodeError, StrictJsonError):
670
+ diagnostic = "invalid_json"
671
+ if diagnostic is None and not jsonschema.Draft202012Validator(
672
+ prepared_probe.request.response_schema()
673
+ ).is_valid(cast(Any, parsed_output)):
674
+ diagnostic = "schema_mismatch"
675
+ if diagnostic is None and parsed_output != prepared_probe.expected_output:
676
+ diagnostic = "semantic_mismatch"
677
+ matched = diagnostic is None
678
+ return OperationalCapture(
679
+ probe_identifier=prepared_probe.probe.identifier,
680
+ pair_identifier=prepared_probe.probe.pair_identifier,
681
+ subject_mode=prepared_probe.probe.subject_mode,
682
+ operation=prepared_probe.probe.operation,
683
+ authoring_case_identifier=prepared_probe.probe.authoring_case_identifier,
684
+ correction_round=prepared_probe.correction_round,
685
+ source_sha256=prepared_probe.source_sha256,
686
+ subject_pointer=prepared_probe.probe.subject_pointer,
687
+ subject_sha256=prepared_probe.subject_sha256,
688
+ request_json=_canonical_json(prepared_probe.request.to_dict()),
689
+ expected_output_json=prepared_probe.expected_output_json,
690
+ raw_output=model_response.raw_output,
691
+ raw_output_sha256=_sha256(model_response.raw_output),
692
+ parsed_output_json=(
693
+ _canonical_json(parsed_output) if diagnostic != "invalid_json" else None
694
+ ),
695
+ effective_model_version=model_response.effective_model_version,
696
+ matched=matched,
697
+ diagnostic=diagnostic,
698
+ )
699
+
700
+
701
+ @dataclass(frozen=True)
702
+ class AuthoringOperationalEvidence:
703
+ """Canonical report-only operational evidence bound to authoring evidence."""
704
+
705
+ package_version: str
706
+ repository_revision: str
707
+ benchmark_evidence_digest: str
708
+ corpus: OperationalCorpus
709
+ provider: AuthoringProviderProvenance
710
+ catalog_json: str
711
+ captures: tuple[OperationalCapture, ...]
712
+
713
+ def __post_init__(self) -> None:
714
+ if not self.package_version or not self.repository_revision:
715
+ raise ValueError("operational evidence package and revision must be non-empty")
716
+ if not _DIGEST_PATTERN.fullmatch(self.benchmark_evidence_digest):
717
+ raise ValueError("operational evidence authoring digest is invalid")
718
+ catalog = strict_json_loads(self.catalog_json)
719
+ if not isinstance(catalog, dict) or self.catalog_json != _canonical_json(catalog):
720
+ raise ValueError("operational evidence catalog must be canonical JSON")
721
+ capture_identifiers = tuple(capture.probe_identifier for capture in self.captures)
722
+ expected_identifiers = tuple(probe.identifier for probe in self.corpus.probes)
723
+ if capture_identifiers != expected_identifiers:
724
+ raise ValueError("operational evidence capture closure does not match its corpus")
725
+
726
+ @property
727
+ def matched_probes(self) -> int:
728
+ return sum(capture.matched for capture in self.captures)
729
+
730
+ @property
731
+ def total_probes(self) -> int:
732
+ return len(self.captures)
733
+
734
+ @property
735
+ def all_matched(self) -> bool:
736
+ return self.matched_probes == self.total_probes
737
+
738
+ def _unsigned_dict(self) -> dict[str, object]:
739
+ return {
740
+ "format": AUTHORING_OPERATIONAL_EVIDENCE_FORMAT,
741
+ "classification": OPERATIONAL_EVIDENCE_CLASSIFICATION,
742
+ "package_version": self.package_version,
743
+ "repository_revision": self.repository_revision,
744
+ "benchmark_evidence_digest": self.benchmark_evidence_digest,
745
+ "corpus": self.corpus.metadata(),
746
+ "provider": self.provider.to_dict(),
747
+ "catalog": strict_json_loads(self.catalog_json),
748
+ "captures": [capture.to_dict() for capture in self.captures],
749
+ "matched_probes": self.matched_probes,
750
+ "total_probes": self.total_probes,
751
+ "all_matched": self.all_matched,
752
+ }
753
+
754
+ @property
755
+ def digest(self) -> str:
756
+ return _sha256(_canonical_json(self._unsigned_dict()))
757
+
758
+ def to_dict(self) -> dict[str, object]:
759
+ evidence_document = self._unsigned_dict()
760
+ evidence_document["digest"] = self.digest
761
+ return evidence_document
762
+
763
+ def to_json(self) -> str:
764
+ return _canonical_json(self.to_dict())
765
+
766
+
767
+ def run_authoring_operational_evidence(
768
+ serialized_authoring_evidence: str,
769
+ verified_authoring_evidence: AuthoringEvidenceVerification,
770
+ *,
771
+ provider: AuthoringProviderProvenance,
772
+ executor: Callable[[OperationalProbeRequest], OperationalModelResponse],
773
+ ) -> AuthoringOperationalEvidence:
774
+ """Run every packaged probe and retain completed mismatches as evidence."""
775
+
776
+ prepared_probes, catalog = _prepare_probes(
777
+ serialized_authoring_evidence,
778
+ verified_authoring_evidence,
779
+ )
780
+ captures = tuple(
781
+ _capture(prepared_probe, executor(prepared_probe.request))
782
+ for prepared_probe in prepared_probes
783
+ )
784
+ return AuthoringOperationalEvidence(
785
+ package_version=verified_authoring_evidence.package_version,
786
+ repository_revision=verified_authoring_evidence.repository_revision,
787
+ benchmark_evidence_digest=verified_authoring_evidence.digest,
788
+ corpus=load_reference_operational_corpus(),
789
+ provider=provider,
790
+ catalog_json=_canonical_json(catalog),
791
+ captures=captures,
792
+ )
793
+
794
+
795
+ @dataclass(frozen=True)
796
+ class AuthoringOperationalEvidenceVerification:
797
+ """Offline integrity and replay summary for operational evidence."""
798
+
799
+ digest: str
800
+ benchmark_evidence_digest: str
801
+ repository_revision: str
802
+ provider_identifier: str
803
+ requested_model: str
804
+ effective_model_versions: tuple[str, ...]
805
+ matched_probes: int
806
+ total_probes: int
807
+ all_matched: bool
808
+ classification: str = OPERATIONAL_EVIDENCE_CLASSIFICATION
809
+
810
+ def to_dict(self) -> dict[str, object]:
811
+ return {
812
+ "digest": self.digest,
813
+ "benchmark_evidence_digest": self.benchmark_evidence_digest,
814
+ "repository_revision": self.repository_revision,
815
+ "provider_identifier": self.provider_identifier,
816
+ "requested_model": self.requested_model,
817
+ "effective_model_versions": list(self.effective_model_versions),
818
+ "matched_probes": self.matched_probes,
819
+ "total_probes": self.total_probes,
820
+ "all_matched": self.all_matched,
821
+ "classification": self.classification,
822
+ }
823
+
824
+
825
+ _operational_evidence_schema_cache: dict[str, Any] | None = None
826
+
827
+
828
+ def _operational_evidence_schema() -> dict[str, Any]:
829
+ global _operational_evidence_schema_cache
830
+ if _operational_evidence_schema_cache is None:
831
+ decoded_schema = strict_json_loads(_EVIDENCE_SCHEMA_PATH.read_text(encoding="utf-8"))
832
+ if not isinstance(decoded_schema, dict):
833
+ raise ValueError("operational evidence schema must be a JSON object")
834
+ jsonschema.Draft202012Validator.check_schema(decoded_schema)
835
+ _operational_evidence_schema_cache = cast(dict[str, Any], decoded_schema)
836
+ return _operational_evidence_schema_cache
837
+
838
+
839
+ def authoring_operational_evidence_schema() -> dict[str, Any]:
840
+ """Return an owned closed schema for operational evidence."""
841
+
842
+ return copy.deepcopy(_operational_evidence_schema())
843
+
844
+
845
+ def _provider_from_document(provider_document: dict[str, Any]) -> AuthoringProviderProvenance:
846
+ if _contains_secret_key(provider_document.get("generation_configuration")):
847
+ raise ValueError("operational provider configuration contains a secret field")
848
+ return AuthoringProviderProvenance.from_configuration(
849
+ identifier=cast(str, provider_document["identifier"]),
850
+ model=cast(str, provider_document["model"]),
851
+ sdk=cast(str, provider_document["sdk"]),
852
+ sdk_version=cast(str, provider_document["sdk_version"]),
853
+ prompt_format=cast(str, provider_document["prompt_format"]),
854
+ prompt_digest=cast(str, provider_document["prompt_digest"]),
855
+ generation_configuration=cast(
856
+ dict[str, object], provider_document["generation_configuration"]
857
+ ),
858
+ )
859
+
860
+
861
+ def verify_authoring_operational_evidence(
862
+ serialized_operational_evidence: str,
863
+ serialized_authoring_evidence: str,
864
+ verified_authoring_evidence: AuthoringEvidenceVerification,
865
+ ) -> AuthoringOperationalEvidenceVerification:
866
+ """Verify exact closure and replay captured model outputs without network access."""
867
+
868
+ decoded_evidence = strict_json_loads(serialized_operational_evidence)
869
+ jsonschema.Draft202012Validator(_operational_evidence_schema()).validate(decoded_evidence)
870
+ if not isinstance(decoded_evidence, dict):
871
+ raise AssertionError("operational evidence schema accepted a non-object")
872
+ evidence_document = cast(dict[str, Any], decoded_evidence)
873
+ if serialized_operational_evidence.strip() != _canonical_json(evidence_document):
874
+ raise ValueError("operational evidence must use canonical compact JSON")
875
+ unsigned_document = dict(evidence_document)
876
+ recorded_digest = cast(str, unsigned_document.pop("digest"))
877
+ if recorded_digest != _sha256(_canonical_json(unsigned_document)):
878
+ raise ValueError("operational evidence digest does not match its content")
879
+ if evidence_document["package_version"] != package_version("flowspec2"):
880
+ raise ValueError("operational evidence package version does not match verifier")
881
+ if evidence_document["benchmark_evidence_digest"] != verified_authoring_evidence.digest:
882
+ raise ValueError("operational evidence is bound to different authoring evidence")
883
+ if evidence_document["repository_revision"] != verified_authoring_evidence.repository_revision:
884
+ raise ValueError("operational evidence repository revision is inconsistent")
885
+ corpus = load_reference_operational_corpus()
886
+ if evidence_document["corpus"] != corpus.metadata():
887
+ raise ValueError("operational evidence corpus does not match installed corpus")
888
+ prepared_probes, expected_catalog = _prepare_probes(
889
+ serialized_authoring_evidence,
890
+ verified_authoring_evidence,
891
+ )
892
+ if evidence_document["catalog"] != expected_catalog:
893
+ raise ValueError("operational evidence source catalog does not match authoring evidence")
894
+ provider_document = cast(dict[str, Any], evidence_document["provider"])
895
+ provider = _provider_from_document(provider_document)
896
+ if (
897
+ provider.prompt_format != OPERATIONAL_PROMPT_FORMAT
898
+ or provider.prompt_digest != OPERATIONAL_PROMPT_DIGEST
899
+ ):
900
+ raise ValueError("operational provider prompt contract is unsupported")
901
+ capture_documents = cast(list[dict[str, Any]], evidence_document["captures"])
902
+ if len(capture_documents) != len(prepared_probes):
903
+ raise ValueError("operational evidence capture count is inconsistent")
904
+ replayed_captures: list[OperationalCapture] = []
905
+ for capture_document, prepared_probe in zip(
906
+ capture_documents,
907
+ prepared_probes,
908
+ strict=True,
909
+ ):
910
+ replayed_capture = _capture(
911
+ prepared_probe,
912
+ OperationalModelResponse(
913
+ raw_output=cast(str, capture_document["raw_output"]),
914
+ effective_model_version=cast(
915
+ str | None, capture_document["effective_model_version"]
916
+ ),
917
+ ),
918
+ )
919
+ if replayed_capture.to_dict() != capture_document:
920
+ raise ValueError(
921
+ f"operational capture does not match offline replay: "
922
+ f"{prepared_probe.probe.identifier}"
923
+ )
924
+ replayed_captures.append(replayed_capture)
925
+ if provider.identifier == "google_gemini" and any(
926
+ capture.effective_model_version is None for capture in replayed_captures
927
+ ):
928
+ raise ValueError("Gemini operational evidence requires effective model versions")
929
+ matched_probes = sum(capture.matched for capture in replayed_captures)
930
+ total_probes = len(replayed_captures)
931
+ if (
932
+ evidence_document["matched_probes"] != matched_probes
933
+ or evidence_document["total_probes"] != total_probes
934
+ or evidence_document["all_matched"] != (matched_probes == total_probes)
935
+ ):
936
+ raise ValueError("operational evidence summary does not match replay")
937
+ effective_models = tuple(
938
+ sorted(
939
+ {
940
+ capture.effective_model_version
941
+ for capture in replayed_captures
942
+ if capture.effective_model_version is not None
943
+ }
944
+ )
945
+ )
946
+ return AuthoringOperationalEvidenceVerification(
947
+ digest=recorded_digest,
948
+ benchmark_evidence_digest=verified_authoring_evidence.digest,
949
+ repository_revision=verified_authoring_evidence.repository_revision,
950
+ provider_identifier=provider.identifier,
951
+ requested_model=provider.model,
952
+ effective_model_versions=effective_models,
953
+ matched_probes=matched_probes,
954
+ total_probes=total_probes,
955
+ all_matched=matched_probes == total_probes,
956
+ )