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,229 @@
1
+ """Strict offline verification and deterministic replay of authoring evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import hashlib
7
+ import json
8
+ import re
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+ from importlib.metadata import version as package_version
12
+ from pathlib import Path
13
+ from typing import Any, Final, cast
14
+
15
+ import jsonschema
16
+
17
+ from flowspec2.json_codec import strict_json_loads
18
+ from flowspec2.profiles import reference_profile
19
+
20
+ from .benchmark import replay_authoring_benchmark
21
+ from .corpus import load_reference_authoring_corpus
22
+ from .evidence import AUTHORING_EVIDENCE_FORMAT, SOURCE_ADAPTER_CONTRACT
23
+
24
+ _EVIDENCE_SCHEMA_PATH: Final[Path] = Path(__file__).with_name("authoring-evidence.schema.json")
25
+ _SECRET_KEY_PATTERN: Final[re.Pattern[str]] = re.compile(
26
+ r"(?:api[_-]?key|authorization|credential|password|secret|access[_-]?token)",
27
+ re.IGNORECASE,
28
+ )
29
+ _evidence_schema_cache: dict[str, Any] | None = None
30
+
31
+
32
+ def _canonical_json(json_document: object) -> str:
33
+ return json.dumps(
34
+ json_document,
35
+ ensure_ascii=False,
36
+ allow_nan=False,
37
+ separators=(",", ":"),
38
+ sort_keys=True,
39
+ )
40
+
41
+
42
+ def _sha256(serialized_contract: str) -> str:
43
+ return hashlib.sha256(serialized_contract.encode("utf-8")).hexdigest()
44
+
45
+
46
+ def _contains_secret_key(json_document: object) -> bool:
47
+ if isinstance(json_document, Mapping):
48
+ return any(
49
+ _SECRET_KEY_PATTERN.search(str(property_name)) is not None
50
+ or _contains_secret_key(property_value)
51
+ for property_name, property_value in json_document.items()
52
+ )
53
+ if isinstance(json_document, list):
54
+ return any(_contains_secret_key(element_value) for element_value in json_document)
55
+ return False
56
+
57
+
58
+ def _evidence_schema() -> dict[str, Any]:
59
+ global _evidence_schema_cache
60
+ cached_schema = _evidence_schema_cache
61
+ if cached_schema is None:
62
+ decoded_schema = strict_json_loads(_EVIDENCE_SCHEMA_PATH.read_text(encoding="utf-8"))
63
+ if not isinstance(decoded_schema, dict):
64
+ raise ValueError("authoring evidence schema must be a JSON object")
65
+ jsonschema.Draft202012Validator.check_schema(decoded_schema)
66
+ cached_schema = cast(dict[str, Any], decoded_schema)
67
+ _evidence_schema_cache = cached_schema
68
+ return cached_schema
69
+
70
+
71
+ def authoring_evidence_schema() -> dict[str, Any]:
72
+ """Return an owned copy of the closed evidence schema."""
73
+
74
+ return copy.deepcopy(_evidence_schema())
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class AuthoringEvidenceVerification:
79
+ """Verified deterministic identity and replay summary for one artifact."""
80
+
81
+ digest: str
82
+ benchmark_identifier: str
83
+ repository_revision: str
84
+ package_version: str
85
+ provider_identifier: str
86
+ requested_model: str
87
+ effective_model_versions: tuple[str, ...]
88
+ successful_cases: int
89
+ total_cases: int
90
+ total_attempts: int
91
+
92
+ def to_dict(self) -> dict[str, object]:
93
+ """Return the stable machine-readable verification summary."""
94
+
95
+ return {
96
+ "digest": self.digest,
97
+ "benchmark_identifier": self.benchmark_identifier,
98
+ "repository_revision": self.repository_revision,
99
+ "package_version": self.package_version,
100
+ "provider_identifier": self.provider_identifier,
101
+ "requested_model": self.requested_model,
102
+ "effective_model_versions": list(self.effective_model_versions),
103
+ "successful_cases": self.successful_cases,
104
+ "total_cases": self.total_cases,
105
+ "total_attempts": self.total_attempts,
106
+ }
107
+
108
+
109
+ def verify_authoring_evidence(
110
+ serialized_evidence: str,
111
+ *,
112
+ expected_repository_revision: str | None = None,
113
+ ) -> AuthoringEvidenceVerification:
114
+ """Validate provenance and replay every exact capture against packaged contracts."""
115
+
116
+ decoded_evidence = strict_json_loads(serialized_evidence)
117
+ jsonschema.Draft202012Validator(_evidence_schema()).validate(decoded_evidence)
118
+ if not isinstance(decoded_evidence, dict):
119
+ raise AssertionError("the evidence schema accepted a non-object document")
120
+ evidence_document = cast(dict[str, Any], decoded_evidence)
121
+ if serialized_evidence.strip() != _canonical_json(evidence_document):
122
+ raise ValueError("authoring evidence must use canonical compact JSON")
123
+ if evidence_document["format"] != AUTHORING_EVIDENCE_FORMAT:
124
+ raise ValueError("authoring evidence format is unsupported")
125
+
126
+ unsigned_document = dict(evidence_document)
127
+ recorded_digest = cast(str, unsigned_document.pop("digest"))
128
+ computed_digest = _sha256(_canonical_json(unsigned_document))
129
+ if recorded_digest != computed_digest:
130
+ raise ValueError("authoring evidence digest does not match its content")
131
+
132
+ installed_package_version = package_version("flowspec2")
133
+ recorded_package_version = cast(str, evidence_document["package_version"])
134
+ if recorded_package_version != installed_package_version:
135
+ raise ValueError("authoring evidence package version does not match the installed verifier")
136
+ repository_revision = cast(str, evidence_document["repository_revision"])
137
+ if (
138
+ expected_repository_revision is not None
139
+ and repository_revision != expected_repository_revision
140
+ ):
141
+ raise ValueError("authoring evidence repository revision does not match the expected value")
142
+
143
+ reference_corpus = load_reference_authoring_corpus()
144
+ if evidence_document["corpus"] != reference_corpus.metadata():
145
+ raise ValueError("authoring evidence corpus does not match the installed corpus")
146
+ flow_profile = reference_profile()
147
+ expected_profile = {
148
+ "identifier": flow_profile.identifier,
149
+ "digest": flow_profile.digest,
150
+ }
151
+ if evidence_document["profile"] != expected_profile:
152
+ raise ValueError("authoring evidence profile does not match the installed profile")
153
+ if evidence_document["source_adapter"] != SOURCE_ADAPTER_CONTRACT:
154
+ raise ValueError("authoring evidence source adapter contract is unsupported")
155
+
156
+ provider_document = cast(dict[str, Any], evidence_document["provider"])
157
+ if _contains_secret_key(provider_document["generation_configuration"]):
158
+ raise ValueError("authoring evidence provider configuration contains a secret field")
159
+ capture_documents = cast(list[dict[str, Any]], evidence_document["captures"])
160
+ if provider_document["identifier"] == "google_gemini" and any(
161
+ capture_document["effective_model_version"] is None
162
+ for capture_document in capture_documents
163
+ ):
164
+ raise ValueError("Gemini authoring evidence requires effective model versions")
165
+
166
+ report_document = cast(dict[str, Any], evidence_document["report"])
167
+ benchmark_document = cast(dict[str, int], evidence_document["benchmark"])
168
+ case_result_documents = cast(list[dict[str, Any]], report_document["case_results"])
169
+ if any(
170
+ len(cast(list[dict[str, Any]], case_result_document["attempts"]))
171
+ > benchmark_document["max_correction_rounds"] + 1
172
+ for case_result_document in case_result_documents
173
+ ):
174
+ raise ValueError("authoring evidence report exceeds its correction-round limit")
175
+ expected_capture_keys = tuple(
176
+ (case_result_document["case_identifier"], attempt_document["correction_round"])
177
+ for case_result_document in case_result_documents
178
+ for attempt_document in cast(list[dict[str, Any]], case_result_document["attempts"])
179
+ )
180
+ actual_capture_keys = tuple(
181
+ (capture_document["case_identifier"], capture_document["correction_round"])
182
+ for capture_document in capture_documents
183
+ )
184
+ if actual_capture_keys != expected_capture_keys:
185
+ raise ValueError("authoring evidence captures do not align with report attempts")
186
+ for capture_document in capture_documents:
187
+ authored_source = cast(str, capture_document["authored_source"])
188
+ if capture_document["source_sha256"] != _sha256(authored_source):
189
+ raise ValueError("authoring evidence capture source digest does not match its source")
190
+
191
+ captured_sources: dict[str, list[str]] = {
192
+ benchmark_case.identifier: [] for benchmark_case in reference_corpus.cases
193
+ }
194
+ for capture_document in capture_documents:
195
+ case_identifier = cast(str, capture_document["case_identifier"])
196
+ if case_identifier not in captured_sources:
197
+ raise ValueError("authoring evidence capture case is not in the installed corpus")
198
+ captured_sources[case_identifier].append(cast(str, capture_document["authored_source"]))
199
+
200
+ replayed_report = replay_authoring_benchmark(
201
+ cast(str, report_document["benchmark_identifier"]),
202
+ reference_corpus.cases,
203
+ captured_sources,
204
+ profile=flow_profile,
205
+ )
206
+ if replayed_report.to_dict() != report_document:
207
+ raise ValueError("authoring evidence report does not match deterministic replay")
208
+
209
+ effective_model_versions = tuple(
210
+ sorted(
211
+ {
212
+ cast(str, capture_document["effective_model_version"])
213
+ for capture_document in capture_documents
214
+ if capture_document["effective_model_version"] is not None
215
+ }
216
+ )
217
+ )
218
+ return AuthoringEvidenceVerification(
219
+ digest=recorded_digest,
220
+ benchmark_identifier=replayed_report.benchmark_identifier,
221
+ repository_revision=repository_revision,
222
+ package_version=recorded_package_version,
223
+ provider_identifier=cast(str, provider_document["identifier"]),
224
+ requested_model=cast(str, provider_document["model"]),
225
+ effective_model_versions=effective_model_versions,
226
+ successful_cases=replayed_report.successful_cases,
227
+ total_cases=replayed_report.total_cases,
228
+ total_attempts=replayed_report.total_attempts,
229
+ )
@@ -0,0 +1,215 @@
1
+ """Optional Gemini transport for the provider-neutral authoring benchmark."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from importlib.metadata import PackageNotFoundError, version
8
+ from typing import Final, Protocol, cast
9
+
10
+ from jsonschema import Draft202012Validator
11
+
12
+ from flowspec2.json_codec import StrictJsonError, strict_json_loads
13
+
14
+ from .benchmark import AuthoredSource, AuthoringRequest
15
+ from .evidence import AuthoringProviderProvenance
16
+ from .projection import authoring_projection
17
+ from .provider_prompt import (
18
+ AUTHORING_SYSTEM_INSTRUCTION,
19
+ authoring_prompt,
20
+ authoring_prompt_digest,
21
+ canonical_json,
22
+ )
23
+
24
+ DEFAULT_GEMINI_AUTHOR_MODEL: Final[str] = "gemini-2.5-flash"
25
+ DEFAULT_GEMINI_AUTHOR_TIMEOUT_MILLISECONDS: Final[int] = 120_000
26
+ GEMINI_AUTHOR_PROMPT_FORMAT: Final[str] = "flowspec2/gemini-author-prompt@3"
27
+
28
+ _PROVIDER_IDENTIFIER: Final[str] = "google_gemini"
29
+ _SDK_NAME: Final[str] = "google-genai"
30
+ _SYSTEM_INSTRUCTION: Final[str] = AUTHORING_SYSTEM_INSTRUCTION
31
+
32
+
33
+ def _canonical_json(json_document: object) -> str:
34
+ return canonical_json(json_document)
35
+
36
+
37
+ GEMINI_AUTHOR_PROMPT_DIGEST: Final[str] = authoring_prompt_digest(GEMINI_AUTHOR_PROMPT_FORMAT)
38
+
39
+
40
+ class _GeminiModels(Protocol):
41
+ def generate_content(
42
+ self,
43
+ *,
44
+ model: str,
45
+ contents: str,
46
+ config: dict[str, object],
47
+ ) -> object: ...
48
+
49
+
50
+ class _GeminiClient(Protocol):
51
+ @property
52
+ def models(self) -> _GeminiModels: ...
53
+
54
+ def close(self) -> None: ...
55
+
56
+
57
+ class GeminiAuthorError(RuntimeError):
58
+ """The optional provider boundary could not return a usable source envelope."""
59
+
60
+
61
+ def _authoring_prompt(authoring_request: AuthoringRequest) -> str:
62
+ return authoring_prompt(authoring_request, GEMINI_AUTHOR_PROMPT_FORMAT)
63
+
64
+
65
+ def _default_client(api_key: str | None) -> _GeminiClient:
66
+ resolved_api_key = api_key or os.environ.get("GEMINI_API_KEY")
67
+ if not resolved_api_key:
68
+ raise GeminiAuthorError(
69
+ "Gemini authoring requires GEMINI_API_KEY after explicit network opt-in."
70
+ )
71
+ try:
72
+ from google import genai
73
+ except ImportError as import_error:
74
+ raise GeminiAuthorError(
75
+ "Gemini authoring requires the optional flowspec2[llm] dependency."
76
+ ) from import_error
77
+ try:
78
+ return cast(
79
+ _GeminiClient,
80
+ genai.Client(
81
+ api_key=resolved_api_key,
82
+ http_options={"timeout": DEFAULT_GEMINI_AUTHOR_TIMEOUT_MILLISECONDS},
83
+ ),
84
+ )
85
+ except Exception as client_error:
86
+ raise GeminiAuthorError("Gemini author client initialization failed") from client_error
87
+
88
+
89
+ def _sdk_version() -> str:
90
+ try:
91
+ return version(_SDK_NAME)
92
+ except PackageNotFoundError:
93
+ return "injected-client"
94
+
95
+
96
+ class GeminiAuthor:
97
+ """Generate benchmark sources through a closed Gemini projection envelope."""
98
+
99
+ def __init__(
100
+ self,
101
+ model: str = DEFAULT_GEMINI_AUTHOR_MODEL,
102
+ *,
103
+ api_key: str | None = None,
104
+ client: _GeminiClient | None = None,
105
+ ) -> None:
106
+ if not model.strip():
107
+ raise ValueError("Gemini author model must be non-empty")
108
+ if api_key is not None and client is not None:
109
+ raise ValueError("Gemini author cannot combine an API key with an injected client")
110
+ self.model = model
111
+ self._client = client if client is not None else _default_client(api_key)
112
+ self._owns_client = client is None
113
+ self._closed = False
114
+
115
+ @property
116
+ def generation_configuration(self) -> dict[str, object]:
117
+ projection_artifact = authoring_projection()
118
+ return {
119
+ "response_mime_type": "application/json",
120
+ "projection_schema_digest": projection_artifact.schema_digest,
121
+ "seed": 0,
122
+ "temperature": 0.0,
123
+ "timeout_milliseconds": DEFAULT_GEMINI_AUTHOR_TIMEOUT_MILLISECONDS,
124
+ }
125
+
126
+ def provenance(self) -> AuthoringProviderProvenance:
127
+ return AuthoringProviderProvenance.from_configuration(
128
+ identifier=_PROVIDER_IDENTIFIER,
129
+ model=self.model,
130
+ sdk=_SDK_NAME,
131
+ sdk_version=_sdk_version(),
132
+ prompt_format=GEMINI_AUTHOR_PROMPT_FORMAT,
133
+ prompt_digest=GEMINI_AUTHOR_PROMPT_DIGEST,
134
+ generation_configuration=self.generation_configuration,
135
+ )
136
+
137
+ def __call__(self, authoring_request: AuthoringRequest) -> AuthoredSource:
138
+ if self._closed:
139
+ raise GeminiAuthorError("Gemini author is closed")
140
+ if authoring_request.format_identifier != "flowspec/2":
141
+ raise GeminiAuthorError("Gemini author supports only the flowspec/2 source adapter")
142
+ projection_artifact = authoring_projection()
143
+ provider_configuration: dict[str, object] = {
144
+ "system_instruction": _SYSTEM_INSTRUCTION,
145
+ "response_mime_type": "application/json",
146
+ "response_json_schema": projection_artifact.schema(),
147
+ "temperature": 0.0,
148
+ "seed": 0,
149
+ }
150
+ try:
151
+ provider_response = self._client.models.generate_content(
152
+ model=self.model,
153
+ contents=_authoring_prompt(authoring_request),
154
+ config=provider_configuration,
155
+ )
156
+ except Exception as provider_error:
157
+ status_code = getattr(
158
+ provider_error,
159
+ "status_code",
160
+ getattr(provider_error, "code", None),
161
+ )
162
+ status_suffix = f", status={status_code}" if isinstance(status_code, int) else ""
163
+ raise GeminiAuthorError(
164
+ "Gemini author request failed for "
165
+ f"{authoring_request.task.identifier!r} correction round "
166
+ f"{authoring_request.correction_round} "
167
+ f"({type(provider_error).__name__}{status_suffix})."
168
+ ) from provider_error
169
+
170
+ try:
171
+ response_text = getattr(provider_response, "text", None)
172
+ effective_model_version = getattr(provider_response, "model_version", None)
173
+ except Exception as response_error:
174
+ raise GeminiAuthorError("Gemini author response could not be read") from response_error
175
+ if not isinstance(response_text, str) or not response_text:
176
+ raise GeminiAuthorError("Gemini author returned an empty projection envelope")
177
+ if not isinstance(effective_model_version, str) or not effective_model_version.strip():
178
+ raise GeminiAuthorError("Gemini author response omitted its effective model version")
179
+ try:
180
+ projected_document = strict_json_loads(response_text)
181
+ except (json.JSONDecodeError, StrictJsonError) as decoding_error:
182
+ raise GeminiAuthorError(
183
+ "Gemini author returned an invalid projection envelope"
184
+ ) from decoding_error
185
+ envelope_errors = tuple(
186
+ Draft202012Validator(projection_artifact.schema()).iter_errors(projected_document)
187
+ )
188
+ if envelope_errors:
189
+ raise GeminiAuthorError("Gemini author returned an incompatible projection envelope")
190
+ if not isinstance(projected_document, dict):
191
+ raise AssertionError("projection schema accepted a non-object response")
192
+ return AuthoredSource(
193
+ source=cast(str, projected_document["flow_document_json"]),
194
+ effective_model_version=effective_model_version,
195
+ )
196
+
197
+ def close(self) -> None:
198
+ if self._closed:
199
+ return
200
+ if self._owns_client:
201
+ try:
202
+ self._client.close()
203
+ except Exception as close_error:
204
+ raise GeminiAuthorError(
205
+ "Gemini author client could not close cleanly"
206
+ ) from close_error
207
+ self._closed = True
208
+
209
+ def __enter__(self) -> GeminiAuthor:
210
+ if self._closed:
211
+ raise GeminiAuthorError("Gemini author is closed")
212
+ return self
213
+
214
+ def __exit__(self, _exception_type: object, _exception: object, _traceback: object) -> None:
215
+ self.close()
@@ -0,0 +1,61 @@
1
+ {
2
+ "format": "flowspec2/operational-corpus@2",
3
+ "case_format": "flowspec2/operational-probe@2",
4
+ "identifier": "flowspec2_reference_operational",
5
+ "probes": [
6
+ {
7
+ "identifier": "linear-extract-authored",
8
+ "pair_identifier": "linear-extract-hint",
9
+ "subject_mode": "authored",
10
+ "operation": "extract",
11
+ "authoring_case_identifier": "linear_collection",
12
+ "subject_pointer": "/path/0/prompt/extract_hint",
13
+ "citizen_input": "ALPHA=[broken streetlight]; ZETA=[playground swing is broken]",
14
+ "expected": {
15
+ "request_description": "playground swing is broken"
16
+ },
17
+ "counterfactual": null
18
+ },
19
+ {
20
+ "identifier": "linear-extract-counterfactual",
21
+ "pair_identifier": "linear-extract-hint",
22
+ "subject_mode": "counterfactual",
23
+ "operation": "extract",
24
+ "authoring_case_identifier": "linear_collection",
25
+ "subject_pointer": "/path/0/prompt/extract_hint",
26
+ "citizen_input": "ALPHA=[broken streetlight]; ZETA=[playground swing is broken]",
27
+ "expected": {
28
+ "request_description": "broken streetlight"
29
+ },
30
+ "counterfactual": "Set request_description to only the text inside `ALPHA=[...]`; ignore `ZETA=[...]`."
31
+ },
32
+ {
33
+ "identifier": "linear-route-authored",
34
+ "pair_identifier": "linear-route-triggers",
35
+ "subject_mode": "authored",
36
+ "operation": "route",
37
+ "authoring_case_identifier": "linear_collection",
38
+ "subject_pointer": "/route/trigger_phrases",
39
+ "citizen_input": "cobalt lantern protocol",
40
+ "expected": {
41
+ "selected": true
42
+ },
43
+ "counterfactual": null
44
+ },
45
+ {
46
+ "identifier": "linear-route-counterfactual",
47
+ "pair_identifier": "linear-route-triggers",
48
+ "subject_mode": "counterfactual",
49
+ "operation": "route",
50
+ "authoring_case_identifier": "linear_collection",
51
+ "subject_pointer": "/route/trigger_phrases",
52
+ "citizen_input": "cobalt lantern protocol",
53
+ "expected": {
54
+ "selected": false
55
+ },
56
+ "counterfactual": [
57
+ "amber compass protocol"
58
+ ]
59
+ }
60
+ ]
61
+ }