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,345 @@
1
+ """Argument grammar for the flowspec2 command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from typing import Never
11
+
12
+ from .observability import log_event
13
+
14
+ CommandHandler = Callable[[argparse.Namespace], int]
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class CorrelatedArgumentParser(argparse.ArgumentParser):
19
+ """Argument parser whose user-facing failures carry a correlation ID."""
20
+
21
+ def error(self, message: str) -> Never:
22
+ log_id = log_event(
23
+ logger,
24
+ logging.ERROR,
25
+ "CLI argument parsing failed",
26
+ operation="parse_arguments",
27
+ context={"parser": self.prog},
28
+ )
29
+ self.print_usage(sys.stderr)
30
+ self.exit(2, f"{self.prog}: error [log_id={log_id}]: {message}\n")
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class CliHandlers:
35
+ """Handlers and defaults injected into the declarative argument grammar."""
36
+
37
+ validate: CommandHandler
38
+ check: CommandHandler
39
+ normalize: CommandHandler
40
+ intermediate_representation: CommandHandler
41
+ graph: CommandHandler
42
+ mermaid: CommandHandler
43
+ rasa_export: CommandHandler
44
+ rasa_import: CommandHandler
45
+ open_workflow_export: CommandHandler
46
+ open_workflow_import: CommandHandler
47
+ authoring_benchmark_gemini: CommandHandler
48
+ operational_benchmark_gemini: CommandHandler
49
+ operational_evidence_verify: CommandHandler
50
+ authoring_evidence_verify: CommandHandler
51
+ authoring_evidence_sign: CommandHandler
52
+ authoring_evidence_signature_verify: CommandHandler
53
+ authoring_presentation_review_init: CommandHandler
54
+ authoring_presentation_review_finalize: CommandHandler
55
+ authoring_presentation_review_verify: CommandHandler
56
+ authoring_presentation_review_sign: CommandHandler
57
+ authoring_presentation_review_signature_verify: CommandHandler
58
+ authoring_promotion_verify: CommandHandler
59
+ default_gemini_model: str
60
+
61
+
62
+ def build_parser(cli_handlers: CliHandlers) -> argparse.ArgumentParser:
63
+ """Build a fresh parser bound to the supplied command handlers."""
64
+
65
+ parser = CorrelatedArgumentParser(
66
+ prog="flowspec2",
67
+ description="Validate, inspect, and convert flowspec2 documents.",
68
+ )
69
+ subparsers = parser.add_subparsers(dest="command", required=True)
70
+
71
+ validate_parser = subparsers.add_parser(
72
+ "validate",
73
+ help="validate a flowspec2 document",
74
+ )
75
+ validate_parser.add_argument("path")
76
+ validate_parser.set_defaults(handler=cli_handlers.validate)
77
+
78
+ check_parser = subparsers.add_parser(
79
+ "check",
80
+ help="validate and compile-check a flowspec2 document",
81
+ )
82
+ check_parser.add_argument("path")
83
+ check_parser.add_argument(
84
+ "--json",
85
+ action="store_true",
86
+ dest="json_output",
87
+ help="emit a deterministic machine-readable report",
88
+ )
89
+ check_parser.set_defaults(handler=cli_handlers.check)
90
+
91
+ normalize_parser = subparsers.add_parser(
92
+ "normalize",
93
+ help="emit the canonical normalized flowspec2 document",
94
+ )
95
+ normalize_parser.add_argument("path")
96
+ normalize_parser.set_defaults(handler=cli_handlers.normalize)
97
+
98
+ intermediate_representation_parser = subparsers.add_parser(
99
+ "ir",
100
+ help="emit canonical contracts with subflow topology abstracted",
101
+ )
102
+ intermediate_representation_parser.add_argument("path")
103
+ intermediate_representation_parser.set_defaults(
104
+ handler=cli_handlers.intermediate_representation
105
+ )
106
+
107
+ graph_parser = subparsers.add_parser("graph", help="list compiled graph nodes")
108
+ graph_parser.add_argument("path")
109
+ graph_parser.set_defaults(handler=cli_handlers.graph)
110
+
111
+ mermaid_parser = subparsers.add_parser(
112
+ "mermaid",
113
+ help="render the fully expanded compiled graph as Mermaid",
114
+ )
115
+ mermaid_parser.add_argument("path")
116
+ mermaid_parser.set_defaults(handler=cli_handlers.mermaid)
117
+
118
+ rasa_export_parser = subparsers.add_parser(
119
+ "rasa-export",
120
+ help="export the bounded Rasa CALM portable profile",
121
+ )
122
+ rasa_export_parser.add_argument("path")
123
+ rasa_export_parser.add_argument("--output-dir", required=True)
124
+ rasa_export_parser.add_argument("--allow-lossy", action="store_true")
125
+ rasa_export_parser.set_defaults(handler=cli_handlers.rasa_export)
126
+
127
+ rasa_import_parser = subparsers.add_parser(
128
+ "rasa-import",
129
+ help="import one flow from the bounded Rasa CALM portable profile",
130
+ )
131
+ rasa_import_parser.add_argument("path", help="flows.yml path")
132
+ rasa_import_parser.add_argument("--domain", required=True, help="domain.yml path")
133
+ rasa_import_parser.add_argument("--flow", help="flow id; required for multi-flow files")
134
+ rasa_import_parser.add_argument("--version", default="1.0.0", help="flowspec2 version")
135
+ rasa_import_parser.add_argument("--output", required=True)
136
+ rasa_import_parser.add_argument("--allow-lossy", action="store_true")
137
+ rasa_import_parser.set_defaults(handler=cli_handlers.rasa_import)
138
+
139
+ open_export_parser = subparsers.add_parser(
140
+ "open-workflow-export",
141
+ help="export the lossless Open Workflow conversational profile",
142
+ )
143
+ open_export_parser.add_argument("path")
144
+ open_export_parser.add_argument("--output", required=True)
145
+ open_export_parser.add_argument("--namespace", default="flowspec2")
146
+ open_export_parser.set_defaults(handler=cli_handlers.open_workflow_export)
147
+
148
+ open_import_parser = subparsers.add_parser(
149
+ "open-workflow-import",
150
+ help="import the exact Open Workflow conversational profile",
151
+ )
152
+ open_import_parser.add_argument("path")
153
+ open_import_parser.add_argument("--output", required=True)
154
+ open_import_parser.set_defaults(handler=cli_handlers.open_workflow_import)
155
+
156
+ authoring_benchmark_parser = subparsers.add_parser(
157
+ "authoring-benchmark-gemini",
158
+ help="run the packaged AI-authoring corpus through Gemini",
159
+ )
160
+ authoring_benchmark_parser.add_argument(
161
+ "--allow-network",
162
+ action="store_true",
163
+ required=True,
164
+ help="explicitly permit Gemini API requests for this invocation",
165
+ )
166
+ authoring_benchmark_parser.add_argument(
167
+ "--benchmark-identifier",
168
+ default="gemini_reference",
169
+ )
170
+ authoring_benchmark_parser.add_argument(
171
+ "--model",
172
+ default=cli_handlers.default_gemini_model,
173
+ )
174
+ authoring_benchmark_parser.add_argument("--repository-revision", required=True)
175
+ authoring_benchmark_parser.add_argument("--max-correction-rounds", type=int, default=2)
176
+ authoring_benchmark_parser.add_argument("--output", required=True)
177
+ authoring_benchmark_parser.set_defaults(handler=cli_handlers.authoring_benchmark_gemini)
178
+
179
+ operational_gemini_parser = subparsers.add_parser(
180
+ "operational-benchmark-gemini",
181
+ help="probe final authored flows through Gemini routing and extraction",
182
+ )
183
+ operational_gemini_parser.add_argument("path")
184
+ operational_gemini_parser.add_argument(
185
+ "--allow-network",
186
+ action="store_true",
187
+ required=True,
188
+ help="explicitly permit Gemini API requests for this invocation",
189
+ )
190
+ operational_gemini_parser.add_argument(
191
+ "--model",
192
+ default=cli_handlers.default_gemini_model,
193
+ )
194
+ operational_gemini_parser.add_argument("--repository-revision", required=True)
195
+ operational_gemini_parser.add_argument("--output", required=True)
196
+ operational_gemini_parser.set_defaults(handler=cli_handlers.operational_benchmark_gemini)
197
+
198
+ operational_verify_parser = subparsers.add_parser(
199
+ "operational-evidence-verify",
200
+ help="verify and replay report-only operational model evidence",
201
+ )
202
+ operational_verify_parser.add_argument("path")
203
+ operational_verify_parser.add_argument("--authoring-evidence", required=True)
204
+ operational_verify_parser.add_argument("--repository-revision")
205
+ operational_verify_parser.add_argument(
206
+ "--json",
207
+ action="store_true",
208
+ dest="json_output",
209
+ help="emit a deterministic machine-readable verification summary",
210
+ )
211
+ operational_verify_parser.set_defaults(handler=cli_handlers.operational_evidence_verify)
212
+
213
+ evidence_verify_parser = subparsers.add_parser(
214
+ "authoring-evidence-verify",
215
+ help="verify and deterministically replay an authoring evidence artifact",
216
+ )
217
+ evidence_verify_parser.add_argument("path")
218
+ evidence_verify_parser.add_argument(
219
+ "--repository-revision",
220
+ help="require the artifact to name this repository revision",
221
+ )
222
+ evidence_verify_parser.add_argument(
223
+ "--json",
224
+ action="store_true",
225
+ dest="json_output",
226
+ help="emit a deterministic machine-readable verification summary",
227
+ )
228
+ evidence_verify_parser.set_defaults(handler=cli_handlers.authoring_evidence_verify)
229
+
230
+ evidence_sign_parser = subparsers.add_parser(
231
+ "authoring-evidence-sign",
232
+ help="sign a verified authoring evidence digest with Ed25519",
233
+ )
234
+ evidence_sign_parser.add_argument("path")
235
+ evidence_sign_parser.add_argument("--private-key", required=True)
236
+ evidence_sign_parser.add_argument("--repository-revision")
237
+ evidence_sign_parser.add_argument("--output", required=True)
238
+ evidence_sign_parser.set_defaults(handler=cli_handlers.authoring_evidence_sign)
239
+
240
+ signature_verify_parser = subparsers.add_parser(
241
+ "authoring-evidence-signature-verify",
242
+ help="authenticate authoring evidence with a trusted Ed25519 public key",
243
+ )
244
+ signature_verify_parser.add_argument("path")
245
+ signature_verify_parser.add_argument("--signature", required=True)
246
+ signature_verify_parser.add_argument("--public-key", required=True)
247
+ signature_verify_parser.add_argument("--repository-revision")
248
+ signature_verify_parser.add_argument(
249
+ "--json",
250
+ action="store_true",
251
+ dest="json_output",
252
+ help="emit a deterministic machine-readable authentication summary",
253
+ )
254
+ signature_verify_parser.set_defaults(handler=cli_handlers.authoring_evidence_signature_verify)
255
+
256
+ presentation_review_init_parser = subparsers.add_parser(
257
+ "authoring-presentation-review-init",
258
+ help="create a source-bound human-review packet from verified authoring evidence",
259
+ )
260
+ presentation_review_init_parser.add_argument("path")
261
+ presentation_review_init_parser.add_argument("--repository-revision")
262
+ presentation_review_init_parser.add_argument("--output", required=True)
263
+ presentation_review_init_parser.set_defaults(
264
+ handler=cli_handlers.authoring_presentation_review_init
265
+ )
266
+
267
+ presentation_review_finalize_parser = subparsers.add_parser(
268
+ "authoring-presentation-review-finalize",
269
+ help="finalize human decisions into a canonical presentation review",
270
+ )
271
+ presentation_review_finalize_parser.add_argument("path")
272
+ presentation_review_finalize_parser.add_argument("--draft", required=True)
273
+ presentation_review_finalize_parser.add_argument("--repository-revision")
274
+ presentation_review_finalize_parser.add_argument("--output", required=True)
275
+ presentation_review_finalize_parser.set_defaults(
276
+ handler=cli_handlers.authoring_presentation_review_finalize
277
+ )
278
+
279
+ presentation_review_verify_parser = subparsers.add_parser(
280
+ "authoring-presentation-review-verify",
281
+ help="verify a presentation review against exact authoring evidence",
282
+ )
283
+ presentation_review_verify_parser.add_argument("path")
284
+ presentation_review_verify_parser.add_argument("--review", required=True)
285
+ presentation_review_verify_parser.add_argument("--repository-revision")
286
+ presentation_review_verify_parser.add_argument(
287
+ "--json",
288
+ action="store_true",
289
+ dest="json_output",
290
+ help="emit a deterministic machine-readable verification summary",
291
+ )
292
+ presentation_review_verify_parser.set_defaults(
293
+ handler=cli_handlers.authoring_presentation_review_verify
294
+ )
295
+
296
+ presentation_review_sign_parser = subparsers.add_parser(
297
+ "authoring-presentation-review-sign",
298
+ help="authenticate a verified presentation review with Ed25519",
299
+ )
300
+ presentation_review_sign_parser.add_argument("path")
301
+ presentation_review_sign_parser.add_argument("--review", required=True)
302
+ presentation_review_sign_parser.add_argument("--private-key", required=True)
303
+ presentation_review_sign_parser.add_argument("--repository-revision")
304
+ presentation_review_sign_parser.add_argument("--output", required=True)
305
+ presentation_review_sign_parser.set_defaults(
306
+ handler=cli_handlers.authoring_presentation_review_sign
307
+ )
308
+
309
+ presentation_signature_verify_parser = subparsers.add_parser(
310
+ "authoring-presentation-review-signature-verify",
311
+ help="authenticate a presentation review with a trusted Ed25519 public key",
312
+ )
313
+ presentation_signature_verify_parser.add_argument("path")
314
+ presentation_signature_verify_parser.add_argument("--review", required=True)
315
+ presentation_signature_verify_parser.add_argument("--signature", required=True)
316
+ presentation_signature_verify_parser.add_argument("--public-key", required=True)
317
+ presentation_signature_verify_parser.add_argument("--repository-revision")
318
+ presentation_signature_verify_parser.add_argument(
319
+ "--json",
320
+ action="store_true",
321
+ dest="json_output",
322
+ help="emit a deterministic machine-readable authentication summary",
323
+ )
324
+ presentation_signature_verify_parser.set_defaults(
325
+ handler=cli_handlers.authoring_presentation_review_signature_verify
326
+ )
327
+
328
+ promotion_verify_parser = subparsers.add_parser(
329
+ "authoring-promotion-verify",
330
+ help="evaluate promotion eligibility from authenticated authoring artifacts",
331
+ )
332
+ promotion_verify_parser.add_argument("path")
333
+ promotion_verify_parser.add_argument("--review", required=True)
334
+ promotion_verify_parser.add_argument("--signature", required=True)
335
+ promotion_verify_parser.add_argument("--public-key", required=True)
336
+ promotion_verify_parser.add_argument("--repository-revision")
337
+ promotion_verify_parser.add_argument(
338
+ "--json",
339
+ action="store_true",
340
+ dest="json_output",
341
+ help="emit a deterministic machine-readable promotion summary",
342
+ )
343
+ promotion_verify_parser.set_defaults(handler=cli_handlers.authoring_promotion_verify)
344
+
345
+ return parser
flowspec2/clock.py ADDED
@@ -0,0 +1,23 @@
1
+ """Clock contracts and the real UTC adapter used at runtime boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from datetime import datetime, timezone
7
+
8
+ UtcClock = Callable[[], datetime]
9
+
10
+
11
+ def system_utc_now() -> datetime:
12
+ """Read the real UTC clock for callers that do not inject one."""
13
+
14
+ return datetime.now(timezone.utc)
15
+
16
+
17
+ def utc_timestamp(clock: UtcClock) -> datetime:
18
+ """Read and normalize an aware clock sample, rejecting ambiguous local time."""
19
+
20
+ timestamp = clock()
21
+ if timestamp.tzinfo is None or timestamp.utcoffset() is None:
22
+ raise ValueError("UTC clock must return a timezone-aware datetime")
23
+ return timestamp.astimezone(timezone.utc)
@@ -0,0 +1,47 @@
1
+ """Compatibility profiles for external workflow formats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .models import (
6
+ CompatibilityDiagnostic,
7
+ CompatibilityError,
8
+ CompatibilityReport,
9
+ ConversionOutcome,
10
+ )
11
+ from .open_workflow import (
12
+ OPEN_WORKFLOW_PROFILE_ID,
13
+ OPEN_WORKFLOW_SCHEMA_VERSION,
14
+ export_open_workflow,
15
+ import_open_workflow,
16
+ official_open_workflow_schema,
17
+ validate_official_open_workflow,
18
+ validate_open_workflow_profile,
19
+ )
20
+ from .rasa import (
21
+ RASA_FORMAT,
22
+ RASA_MINIMUM_VERSION,
23
+ RASA_PROFILE_VERSION,
24
+ RasaBundle,
25
+ export_rasa,
26
+ import_rasa,
27
+ )
28
+
29
+ __all__ = [
30
+ "CompatibilityDiagnostic",
31
+ "CompatibilityError",
32
+ "CompatibilityReport",
33
+ "ConversionOutcome",
34
+ "OPEN_WORKFLOW_PROFILE_ID",
35
+ "OPEN_WORKFLOW_SCHEMA_VERSION",
36
+ "RASA_FORMAT",
37
+ "RASA_MINIMUM_VERSION",
38
+ "RASA_PROFILE_VERSION",
39
+ "RasaBundle",
40
+ "export_open_workflow",
41
+ "export_rasa",
42
+ "import_open_workflow",
43
+ "import_rasa",
44
+ "official_open_workflow_schema",
45
+ "validate_official_open_workflow",
46
+ "validate_open_workflow_profile",
47
+ ]
@@ -0,0 +1,82 @@
1
+ """Shared immutable contracts for compatibility conversions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Generic, Literal, TypeVar
7
+
8
+ DiagnosticSeverity = Literal["warning", "error"]
9
+ Artifact = TypeVar("Artifact")
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class CompatibilityDiagnostic:
14
+ """One stable, machine-readable compatibility finding."""
15
+
16
+ severity: DiagnosticSeverity
17
+ code: str
18
+ source_path: str
19
+ message: str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CompatibilityReport:
24
+ """All findings produced by one source-to-target conversion."""
25
+
26
+ source_format: str
27
+ target_format: str
28
+ diagnostics: tuple[CompatibilityDiagnostic, ...] = ()
29
+
30
+ @property
31
+ def has_errors(self) -> bool:
32
+ return any(diagnostic.severity == "error" for diagnostic in self.diagnostics)
33
+
34
+ @property
35
+ def has_warnings(self) -> bool:
36
+ return any(diagnostic.severity == "warning" for diagnostic in self.diagnostics)
37
+
38
+ def blocking_diagnostics(self, *, allow_lossy: bool) -> tuple[CompatibilityDiagnostic, ...]:
39
+ """Return findings that block serialization under the requested policy."""
40
+
41
+ return tuple(
42
+ diagnostic
43
+ for diagnostic in self.diagnostics
44
+ if diagnostic.severity == "error"
45
+ or (diagnostic.severity == "warning" and not allow_lossy)
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ConversionOutcome(Generic[Artifact]):
51
+ """A converted in-memory artifact and its complete compatibility report."""
52
+
53
+ artifact: Artifact
54
+ report: CompatibilityReport
55
+
56
+
57
+ class CompatibilityError(ValueError):
58
+ """Raised when the selected policy blocks a compatibility conversion."""
59
+
60
+ def __init__(
61
+ self,
62
+ report: CompatibilityReport,
63
+ *,
64
+ allow_lossy: bool,
65
+ ) -> None:
66
+ self.report = report
67
+ self.allow_lossy = allow_lossy
68
+ diagnostics = report.blocking_diagnostics(allow_lossy=allow_lossy)
69
+ detail = "; ".join(
70
+ f"{diagnostic.code} at {diagnostic.source_path}: {diagnostic.message}"
71
+ for diagnostic in diagnostics
72
+ )
73
+ super().__init__(
74
+ f"{report.source_format} -> {report.target_format} conversion blocked: {detail}"
75
+ )
76
+
77
+
78
+ def enforce_compatibility_policy(report: CompatibilityReport, *, allow_lossy: bool) -> None:
79
+ """Raise once with the complete report when the conversion is not permitted."""
80
+
81
+ if report.blocking_diagnostics(allow_lossy=allow_lossy):
82
+ raise CompatibilityError(report, allow_lossy=allow_lossy)