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,100 @@
1
+ {
2
+ "format": "flowspec2/authoring-case@3",
3
+ "identifier": "gated_derivation",
4
+ "prompt": "Author the ordered steps collect_issue_category, collect_issue_detail, and derive_backend_classification. Collect required issue_category from the categorical IssueCategory values Pothole and Obstruction. Collect required issue_detail from categorical IssueDetail values Small and Large, require issue_category first, and ask only when slots.issue_category equals Pothole. Derive backend_classification from issue_category followed by issue_detail with Pothole|Small to POTHOLE_SMALL, Pothole|Large to POTHOLE_LARGE, Obstruction|null to STREET_OBSTRUCTION, and default $from[0]. Add no other semantic behavior. Flow identity, version, route prose, and non-verbatim prompt wording may vary.",
5
+ "feature_tags": ["gate", "derive", "closed_domain"],
6
+ "required_constructs": [
7
+ {
8
+ "identifier": "conditional-gate",
9
+ "pointer_pattern": "/path/*/ask_when",
10
+ "expected": {
11
+ "eq": [
12
+ "slots.issue_category",
13
+ "Pothole"
14
+ ]
15
+ }
16
+ },
17
+ {
18
+ "identifier": "derive-step",
19
+ "pointer_pattern": "/path/*/derive",
20
+ "expected": "backend_classification"
21
+ },
22
+ {
23
+ "identifier": "derive-definition",
24
+ "pointer_pattern": "/derive/*/writes",
25
+ "expected": "backend_classification"
26
+ },
27
+ {
28
+ "identifier": "derive-sources",
29
+ "pointer_pattern": "/derive/*/from",
30
+ "expected": [
31
+ "issue_category",
32
+ "issue_detail"
33
+ ]
34
+ }
35
+ ],
36
+ "source": {
37
+ "schema": "flowspec/2",
38
+ "flow": "gated_issue_classification",
39
+ "version": "1.0.0",
40
+ "route": {
41
+ "description": "Classify a street issue with a conditional detail."
42
+ },
43
+ "domains": {
44
+ "IssueCategory": {
45
+ "type": "categorical",
46
+ "values": ["Pothole", "Obstruction"]
47
+ },
48
+ "IssueDetail": {
49
+ "type": "categorical",
50
+ "values": ["Small", "Large"]
51
+ }
52
+ },
53
+ "slots": {
54
+ "issue_category": {
55
+ "domain": "IssueCategory",
56
+ "required": true
57
+ },
58
+ "issue_detail": {
59
+ "domain": "IssueDetail",
60
+ "required": true,
61
+ "requires": ["issue_category"]
62
+ }
63
+ },
64
+ "path": [
65
+ {
66
+ "step": "collect_issue_category",
67
+ "slot": "issue_category",
68
+ "prompt": {
69
+ "text": "Which type of issue are you reporting?"
70
+ }
71
+ },
72
+ {
73
+ "step": "collect_issue_detail",
74
+ "slot": "issue_detail",
75
+ "ask_when": {
76
+ "eq": ["slots.issue_category", "Pothole"]
77
+ },
78
+ "prompt": {
79
+ "text": "How large is the pothole?"
80
+ }
81
+ },
82
+ {
83
+ "step": "derive_backend_classification",
84
+ "derive": "backend_classification"
85
+ }
86
+ ],
87
+ "derive": [
88
+ {
89
+ "writes": "backend_classification",
90
+ "from": ["issue_category", "issue_detail"],
91
+ "lookup": {
92
+ "Pothole|Small": "POTHOLE_SMALL",
93
+ "Pothole|Large": "POTHOLE_LARGE",
94
+ "Obstruction|null": "STREET_OBSTRUCTION"
95
+ },
96
+ "default": "$from[0]"
97
+ }
98
+ ]
99
+ }
100
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "format": "flowspec2/authoring-case@3",
3
+ "identifier": "linear_collection",
4
+ "prompt": "Author a flow with the single ordered step collect_request_description. It collects the required request_description slot through the free-text RequestDescription domain and asks the citizen for a short maintenance-request description. Include route trigger examples and an extraction hint. Add no other domain, slot, path step, derive, subflow, confirmation, capability, or terminal behavior. Flow identity, version, route prose, trigger examples, non-verbatim prompt wording, and extraction guidance may vary.",
5
+ "feature_tags": ["linear", "collection"],
6
+ "required_constructs": [
7
+ {
8
+ "identifier": "collect-step",
9
+ "pointer_pattern": "/path/*/slot",
10
+ "expected": "request_description"
11
+ },
12
+ {
13
+ "identifier": "route-trigger-examples",
14
+ "pointer_pattern": "/route/trigger_phrases"
15
+ },
16
+ {
17
+ "identifier": "request-description-extraction-hint",
18
+ "pointer_pattern": "/path/*/prompt/extract_hint"
19
+ }
20
+ ],
21
+ "source": {
22
+ "schema": "flowspec/2",
23
+ "flow": "linear_maintenance_request",
24
+ "version": "1.0.0",
25
+ "route": {
26
+ "description": "Collect a short municipal maintenance request.",
27
+ "trigger_phrases": [
28
+ "cobalt lantern protocol",
29
+ "describe a maintenance request",
30
+ "report municipal maintenance"
31
+ ]
32
+ },
33
+ "domains": {
34
+ "RequestDescription": {
35
+ "type": "free_text"
36
+ }
37
+ },
38
+ "slots": {
39
+ "request_description": {
40
+ "domain": "RequestDescription",
41
+ "required": true
42
+ }
43
+ },
44
+ "path": [
45
+ {
46
+ "step": "collect_request_description",
47
+ "slot": "request_description",
48
+ "prompt": {
49
+ "text": "Describe the maintenance request.",
50
+ "extract_hint": "Set request_description to only the text inside `ZETA=[...]`; ignore `ALPHA=[...]`."
51
+ }
52
+ }
53
+ ]
54
+ }
55
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "format": "flowspec2/authoring-corpus@3",
3
+ "identifier": "flowspec2_reference",
4
+ "cases": [
5
+ {
6
+ "identifier": "address_subflow",
7
+ "path": "subflow.case.json",
8
+ "sha256": "e7fe2fb903cc6557443fd8a9c37bbf2a68aa6ed914fa51952a7960179546ed31"
9
+ },
10
+ {
11
+ "identifier": "await_and_correction",
12
+ "path": "await_correction.case.json",
13
+ "sha256": "d36e8f98ce9832029595600b093a062e29ff34d37bd84c5a3c9ada87a5745711"
14
+ },
15
+ {
16
+ "identifier": "gated_derivation",
17
+ "path": "gated_derive.case.json",
18
+ "sha256": "643c7bbf085e685fa2237578d5940bb89f1343f44d92bdc9d4033c53bc0259cd"
19
+ },
20
+ {
21
+ "identifier": "linear_collection",
22
+ "path": "linear.case.json",
23
+ "sha256": "dbdb6535bfba1400513748fbbdd7527dcd3db6025492c166e0674cbce1e11048"
24
+ },
25
+ {
26
+ "identifier": "terminal_fulfillment",
27
+ "path": "terminal.case.json",
28
+ "sha256": "988b703c5d595346b9a3c0c0fdded54aea74b02d967a6c3330553c183d18da91"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "format": "flowspec2/authoring-case@3",
3
+ "identifier": "address_subflow",
4
+ "prompt": "Author an ordered flow that first runs collect_reference_note for optional reference_note in the optional free-text ReferenceNote domain, then delegates address collection to address@1. Declare address@1 exactly once with required and needs_confirmation both true. Add no other semantic behavior. Flow identity, version, route prose, and non-verbatim prompt wording may vary.",
5
+ "feature_tags": ["subflow", "address"],
6
+ "required_constructs": [
7
+ {
8
+ "identifier": "subflow-anchor",
9
+ "pointer_pattern": "/path/*/use",
10
+ "expected": "address@1"
11
+ },
12
+ {
13
+ "identifier": "subflow-declaration",
14
+ "pointer_pattern": "/uses/*/ref",
15
+ "expected": "address@1"
16
+ },
17
+ {
18
+ "identifier": "subflow-configuration",
19
+ "pointer_pattern": "/uses/*/with",
20
+ "expected": {
21
+ "required": true,
22
+ "needs_confirmation": true
23
+ }
24
+ }
25
+ ],
26
+ "source": {
27
+ "schema": "flowspec/2",
28
+ "flow": "address_collection",
29
+ "version": "1.0.0",
30
+ "route": {
31
+ "description": "Collect and confirm the address for a municipal request."
32
+ },
33
+ "domains": {
34
+ "ReferenceNote": {
35
+ "type": "free_text",
36
+ "optional": true
37
+ }
38
+ },
39
+ "slots": {
40
+ "reference_note": {
41
+ "domain": "ReferenceNote"
42
+ }
43
+ },
44
+ "path": [
45
+ {
46
+ "step": "collect_reference_note",
47
+ "slot": "reference_note",
48
+ "prompt": {
49
+ "text": "Add an optional nearby reference point."
50
+ }
51
+ },
52
+ {
53
+ "use": "address@1"
54
+ }
55
+ ],
56
+ "uses": [
57
+ {
58
+ "ref": "address@1",
59
+ "with": {
60
+ "required": true,
61
+ "needs_confirmation": true
62
+ }
63
+ }
64
+ ]
65
+ }
66
+ }
@@ -0,0 +1,94 @@
1
+ {
2
+ "format": "flowspec2/authoring-case@3",
3
+ "identifier": "terminal_fulfillment",
4
+ "prompt": "Author an ordered flow that collects required problem_description through the free-text ProblemDescription domain in collect_problem_description, then executes the open_ticket terminal. Call open_service_request idempotently with problem_description as problem and store result.protocol_id as protocol_id. On success and fatal reset next; on retryable preserve state. Add no other semantic behavior. Flow identity, version, route prose, and non-verbatim prompt wording may vary.",
5
+ "feature_tags": ["terminal", "tool_contract", "idempotency"],
6
+ "required_constructs": [
7
+ {
8
+ "identifier": "terminal-marker",
9
+ "pointer_pattern": "/path/*/terminal",
10
+ "expected": true
11
+ },
12
+ {
13
+ "identifier": "terminal-tool",
14
+ "pointer_pattern": "/terminal/tool",
15
+ "expected": "open_service_request"
16
+ },
17
+ {
18
+ "identifier": "terminal-idempotency",
19
+ "pointer_pattern": "/terminal/idempotent",
20
+ "expected": true
21
+ },
22
+ {
23
+ "identifier": "terminal-input",
24
+ "pointer_pattern": "/terminal/input",
25
+ "expected": [
26
+ {
27
+ "param": "problem",
28
+ "slot": "problem_description"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "identifier": "terminal-output",
34
+ "pointer_pattern": "/terminal/outputs/protocol_id",
35
+ "expected": "result.protocol_id"
36
+ }
37
+ ],
38
+ "source": {
39
+ "schema": "flowspec/2",
40
+ "flow": "terminal_ticket_fulfillment",
41
+ "version": "1.0.0",
42
+ "route": {
43
+ "description": "Collect a problem and create a municipal service ticket."
44
+ },
45
+ "domains": {
46
+ "ProblemDescription": {
47
+ "type": "free_text"
48
+ }
49
+ },
50
+ "slots": {
51
+ "problem_description": {
52
+ "domain": "ProblemDescription",
53
+ "required": true
54
+ }
55
+ },
56
+ "path": [
57
+ {
58
+ "step": "collect_problem_description",
59
+ "slot": "problem_description",
60
+ "prompt": {
61
+ "text": "Describe the service problem."
62
+ }
63
+ },
64
+ {
65
+ "terminal": true
66
+ }
67
+ ],
68
+ "terminal": {
69
+ "step": "open_ticket",
70
+ "tool": "open_service_request",
71
+ "idempotent": true,
72
+ "input": [
73
+ {
74
+ "param": "problem",
75
+ "slot": "problem_description"
76
+ }
77
+ ],
78
+ "outputs": {
79
+ "protocol_id": "result.protocol_id"
80
+ },
81
+ "outcomes": {
82
+ "success": {
83
+ "reset_next": true
84
+ },
85
+ "retryable": {
86
+ "preserve_state": true
87
+ },
88
+ "fatal": {
89
+ "reset_next": true
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,307 @@
1
+ """Versioned, packaged benchmark cases for AI flow authoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from dataclasses import dataclass
9
+ from importlib.resources import files
10
+ from importlib.resources.abc import Traversable
11
+ from typing import Any, Final, cast
12
+
13
+ from jsonschema import Draft202012Validator
14
+ from jsonschema.exceptions import ValidationError
15
+
16
+ from flowspec2.json_codec import strict_json_loads
17
+
18
+ from .benchmark import AuthoringBenchmarkCase, RequiredFlowConstruct
19
+
20
+ AUTHORING_CASE_FORMAT: Final[str] = "flowspec2/authoring-case@3"
21
+ AUTHORING_CORPUS_FORMAT: Final[str] = "flowspec2/authoring-corpus@3"
22
+ REFERENCE_AUTHORING_CORPUS_ID: Final[str] = "flowspec2_reference"
23
+
24
+ _IDENTIFIER_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
25
+ _CASE_PATH_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z][a-z0-9_]*\.case\.json$")
26
+ _DIGEST_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]{64}$")
27
+ _MANIFEST_SCHEMA: Final[dict[str, Any]] = {
28
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
29
+ "type": "object",
30
+ "additionalProperties": False,
31
+ "required": ["format", "identifier", "cases"],
32
+ "properties": {
33
+ "format": {"const": AUTHORING_CORPUS_FORMAT},
34
+ "identifier": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
35
+ "cases": {
36
+ "type": "array",
37
+ "minItems": 1,
38
+ "items": {
39
+ "type": "object",
40
+ "additionalProperties": False,
41
+ "required": ["identifier", "path", "sha256"],
42
+ "properties": {
43
+ "identifier": {
44
+ "type": "string",
45
+ "pattern": _IDENTIFIER_PATTERN.pattern,
46
+ },
47
+ "path": {"type": "string", "pattern": _CASE_PATH_PATTERN.pattern},
48
+ "sha256": {"type": "string", "pattern": _DIGEST_PATTERN.pattern},
49
+ },
50
+ },
51
+ },
52
+ },
53
+ }
54
+ _CASE_SCHEMA: Final[dict[str, Any]] = {
55
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
56
+ "type": "object",
57
+ "additionalProperties": False,
58
+ "required": [
59
+ "format",
60
+ "identifier",
61
+ "prompt",
62
+ "feature_tags",
63
+ "required_constructs",
64
+ "source",
65
+ ],
66
+ "properties": {
67
+ "format": {"const": AUTHORING_CASE_FORMAT},
68
+ "identifier": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
69
+ "prompt": {"type": "string", "minLength": 1},
70
+ "feature_tags": {
71
+ "type": "array",
72
+ "uniqueItems": True,
73
+ "items": {"type": "string", "pattern": _IDENTIFIER_PATTERN.pattern},
74
+ },
75
+ "required_constructs": {
76
+ "type": "array",
77
+ "items": {
78
+ "type": "object",
79
+ "additionalProperties": False,
80
+ "required": ["identifier", "pointer_pattern"],
81
+ "properties": {
82
+ "identifier": {
83
+ "type": "string",
84
+ "pattern": _IDENTIFIER_PATTERN.pattern,
85
+ },
86
+ "pointer_pattern": {"type": "string", "pattern": r"^/.+"},
87
+ "expected": {},
88
+ },
89
+ },
90
+ },
91
+ "source": {"type": "object"},
92
+ },
93
+ }
94
+ _manifest_validator: Final[Draft202012Validator] = Draft202012Validator(_MANIFEST_SCHEMA)
95
+ _case_validator: Final[Draft202012Validator] = Draft202012Validator(_CASE_SCHEMA)
96
+ _reference_corpus_cache: AuthoringCorpus | None = None
97
+
98
+
99
+ def _canonical_json(json_document: object) -> str:
100
+ return json.dumps(
101
+ json_document,
102
+ ensure_ascii=False,
103
+ allow_nan=False,
104
+ separators=(",", ":"),
105
+ sort_keys=True,
106
+ )
107
+
108
+
109
+ def _validation_errors(
110
+ json_document: object,
111
+ validator: Draft202012Validator,
112
+ ) -> tuple[str, ...]:
113
+ validation_errors = sorted(
114
+ validator.iter_errors(cast(Any, json_document)),
115
+ key=lambda validation_error: (
116
+ tuple(str(path_segment) for path_segment in validation_error.absolute_path),
117
+ validation_error.message,
118
+ ),
119
+ )
120
+ return tuple(
121
+ f"{'.'.join(str(path_segment) for path_segment in validation_error.absolute_path) or '<root>'}: "
122
+ f"{validation_error.message}"
123
+ for validation_error in validation_errors
124
+ )
125
+
126
+
127
+ def _load_case_document(case_resource: Traversable) -> dict[str, Any]:
128
+ decoded_case = strict_json_loads(case_resource.read_text(encoding="utf-8"))
129
+ if validation_errors := _validation_errors(decoded_case, _case_validator):
130
+ raise ValueError(
131
+ f"invalid packaged authoring case {case_resource.name}: " + "; ".join(validation_errors)
132
+ )
133
+ return cast(dict[str, Any], decoded_case)
134
+
135
+
136
+ def _load_manifest(corpus_directory: Traversable) -> dict[str, Any]:
137
+ manifest_resource = corpus_directory.joinpath("manifest.json")
138
+ decoded_manifest = strict_json_loads(manifest_resource.read_text(encoding="utf-8"))
139
+ if validation_errors := _validation_errors(decoded_manifest, _manifest_validator):
140
+ raise ValueError(
141
+ "invalid packaged authoring corpus manifest: " + "; ".join(validation_errors)
142
+ )
143
+ return cast(dict[str, Any], decoded_manifest)
144
+
145
+
146
+ def _benchmark_case(case_document: dict[str, Any]) -> AuthoringBenchmarkCase:
147
+ required_constructs = tuple(
148
+ (
149
+ RequiredFlowConstruct.expecting(
150
+ cast(str, required_construct["identifier"]),
151
+ cast(str, required_construct["pointer_pattern"]),
152
+ required_construct["expected"],
153
+ )
154
+ if "expected" in required_construct
155
+ else RequiredFlowConstruct(
156
+ identifier=cast(str, required_construct["identifier"]),
157
+ pointer_pattern=cast(str, required_construct["pointer_pattern"]),
158
+ )
159
+ )
160
+ for required_construct in cast(
161
+ list[dict[str, Any]],
162
+ case_document["required_constructs"],
163
+ )
164
+ )
165
+ return AuthoringBenchmarkCase.expecting_flow(
166
+ identifier=cast(str, case_document["identifier"]),
167
+ prompt=cast(str, case_document["prompt"]),
168
+ expected_flow=case_document["source"],
169
+ feature_tags=tuple(cast(list[str], case_document["feature_tags"])),
170
+ required_constructs=required_constructs,
171
+ )
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class AuthoringCorpus:
176
+ """Immutable cases and content identity for one benchmark corpus."""
177
+
178
+ format_identifier: str
179
+ case_format_identifier: str
180
+ identifier: str
181
+ digest: str
182
+ cases: tuple[AuthoringBenchmarkCase, ...]
183
+
184
+ def __post_init__(self) -> None:
185
+ if self.format_identifier != AUTHORING_CORPUS_FORMAT:
186
+ raise ValueError("unknown authoring corpus format")
187
+ if self.case_format_identifier != AUTHORING_CASE_FORMAT:
188
+ raise ValueError("unknown authoring case format")
189
+ if not _IDENTIFIER_PATTERN.fullmatch(self.identifier):
190
+ raise ValueError("authoring corpus identifier is invalid")
191
+ if not _DIGEST_PATTERN.fullmatch(self.digest):
192
+ raise ValueError("authoring corpus digest must be lowercase SHA-256")
193
+ case_identifiers = tuple(benchmark_case.identifier for benchmark_case in self.cases)
194
+ if not case_identifiers:
195
+ raise ValueError("authoring corpus requires at least one case")
196
+ if case_identifiers != tuple(sorted(case_identifiers)):
197
+ raise ValueError("authoring corpus cases must be sorted by identifier")
198
+ if len(set(case_identifiers)) != len(case_identifiers):
199
+ raise ValueError("authoring corpus case identifiers must be unique")
200
+
201
+ def metadata(self) -> dict[str, object]:
202
+ """Return deterministic corpus provenance without reference answers."""
203
+
204
+ return {
205
+ "format": self.format_identifier,
206
+ "case_format": self.case_format_identifier,
207
+ "identifier": self.identifier,
208
+ "digest": self.digest,
209
+ "case_identifiers": [benchmark_case.identifier for benchmark_case in self.cases],
210
+ }
211
+
212
+
213
+ def _load_authoring_corpus(
214
+ corpus_directory: Traversable,
215
+ *,
216
+ expected_identifier: str,
217
+ ) -> AuthoringCorpus:
218
+ manifest_document = _load_manifest(corpus_directory)
219
+ if manifest_document["identifier"] != expected_identifier:
220
+ raise ValueError("authoring corpus manifest identifier is not the expected contract")
221
+ manifest_cases = cast(list[dict[str, Any]], manifest_document["cases"])
222
+ manifest_identifiers = tuple(
223
+ cast(str, case_contract["identifier"]) for case_contract in manifest_cases
224
+ )
225
+ manifest_paths = tuple(cast(str, case_contract["path"]) for case_contract in manifest_cases)
226
+ if manifest_identifiers != tuple(sorted(manifest_identifiers)):
227
+ raise ValueError("authoring corpus manifest cases must be sorted by identifier")
228
+ if len(set(manifest_identifiers)) != len(manifest_identifiers):
229
+ raise ValueError("authoring corpus manifest case identifiers must be unique")
230
+ if len(set(manifest_paths)) != len(manifest_paths):
231
+ raise ValueError("authoring corpus manifest case paths must be unique")
232
+ packaged_case_paths = {
233
+ child_resource.name
234
+ for child_resource in corpus_directory.iterdir()
235
+ if child_resource.is_file() and child_resource.name.endswith(".case.json")
236
+ }
237
+ if packaged_case_paths != set(manifest_paths):
238
+ missing_paths = sorted(set(manifest_paths) - packaged_case_paths)
239
+ unlisted_paths = sorted(packaged_case_paths - set(manifest_paths))
240
+ raise ValueError(
241
+ "authoring corpus manifest membership mismatch: "
242
+ f"missing={missing_paths}, unlisted={unlisted_paths}"
243
+ )
244
+
245
+ case_documents: list[dict[str, Any]] = []
246
+ for case_contract in manifest_cases:
247
+ case_path = cast(str, case_contract["path"])
248
+ case_resource = corpus_directory.joinpath(case_path)
249
+ serialized_case = case_resource.read_bytes()
250
+ observed_digest = hashlib.sha256(serialized_case).hexdigest()
251
+ expected_digest = cast(str, case_contract["sha256"])
252
+ if observed_digest != expected_digest:
253
+ raise ValueError(f"packaged authoring case failed integrity verification: {case_path}")
254
+ case_document = _load_case_document(case_resource)
255
+ if case_document["identifier"] != case_contract["identifier"]:
256
+ raise ValueError(
257
+ f"packaged authoring case identifier disagrees with manifest: {case_path}"
258
+ )
259
+ case_documents.append(case_document)
260
+
261
+ try:
262
+ benchmark_cases = tuple(
263
+ sorted(
264
+ (_benchmark_case(case_document) for case_document in case_documents),
265
+ key=lambda benchmark_case: benchmark_case.identifier,
266
+ )
267
+ )
268
+ except (ValidationError, ValueError) as case_contract_error:
269
+ raise ValueError(
270
+ "authoring corpus contains an invalid reference flow"
271
+ ) from case_contract_error
272
+ contract_document = {
273
+ "format": AUTHORING_CORPUS_FORMAT,
274
+ "case_format": AUTHORING_CASE_FORMAT,
275
+ "identifier": manifest_document["identifier"],
276
+ "cases": case_documents,
277
+ "resolved_acceptance": [
278
+ {
279
+ "identifier": benchmark_case.identifier,
280
+ "contract": benchmark_case.authoring_task().acceptance.to_dict(),
281
+ }
282
+ for benchmark_case in benchmark_cases
283
+ ],
284
+ }
285
+ corpus_digest = hashlib.sha256(_canonical_json(contract_document).encode("utf-8")).hexdigest()
286
+ return AuthoringCorpus(
287
+ format_identifier=AUTHORING_CORPUS_FORMAT,
288
+ case_format_identifier=AUTHORING_CASE_FORMAT,
289
+ identifier=cast(str, manifest_document["identifier"]),
290
+ digest=corpus_digest,
291
+ cases=benchmark_cases,
292
+ )
293
+
294
+
295
+ def load_reference_authoring_corpus() -> AuthoringCorpus:
296
+ """Load and validate the immutable corpus distributed with flowspec2."""
297
+
298
+ global _reference_corpus_cache
299
+ if _reference_corpus_cache is not None:
300
+ return _reference_corpus_cache
301
+
302
+ corpus_directory = files("flowspec2.authoring").joinpath("corpus")
303
+ _reference_corpus_cache = _load_authoring_corpus(
304
+ corpus_directory,
305
+ expected_identifier=REFERENCE_AUTHORING_CORPUS_ID,
306
+ )
307
+ return _reference_corpus_cache