pycforge 0.15.2__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 (221) hide show
  1. pycforge/__init__.py +8 -0
  2. pycforge/__main__.py +2 -0
  3. pycforge/_version.py +1 -0
  4. pycforge/converter/__init__.py +0 -0
  5. pycforge/converter/analysis/__init__.py +5 -0
  6. pycforge/converter/analysis/functions.py +723 -0
  7. pycforge/converter/analysis/model.py +158 -0
  8. pycforge/converter/analysis/planning.py +464 -0
  9. pycforge/converter/analysis/stage.py +1976 -0
  10. pycforge/converter/analysis/symbols.py +237 -0
  11. pycforge/converter/analysis/validation.py +2166 -0
  12. pycforge/converter/c_output/__init__.py +2 -0
  13. pycforge/converter/c_output/conformance.py +297 -0
  14. pycforge/converter/c_output/renderer.py +220 -0
  15. pycforge/converter/conditional_regions/__init__.py +71 -0
  16. pycforge/converter/conditional_regions/analysis.py +481 -0
  17. pycforge/converter/conditional_regions/lowering.py +579 -0
  18. pycforge/converter/conditional_regions/model.py +243 -0
  19. pycforge/converter/conditional_regions/validation.py +690 -0
  20. pycforge/converter/containers/__init__.py +19 -0
  21. pycforge/converter/containers/analysis.py +542 -0
  22. pycforge/converter/containers/lowering.py +290 -0
  23. pycforge/converter/containers/model.py +146 -0
  24. pycforge/converter/contracts/__init__.py +0 -0
  25. pycforge/converter/contracts/configuration.py +178 -0
  26. pycforge/converter/contracts/identifiers.py +68 -0
  27. pycforge/converter/contracts/versions.py +71 -0
  28. pycforge/converter/core/__init__.py +0 -0
  29. pycforge/converter/core/artifact_io.py +87 -0
  30. pycforge/converter/core/cancellation.py +7 -0
  31. pycforge/converter/core/canonicalization.py +254 -0
  32. pycforge/converter/core/context.py +13 -0
  33. pycforge/converter/core/diagnostics.py +95 -0
  34. pycforge/converter/core/enums.py +23 -0
  35. pycforge/converter/core/fingerprint.py +22 -0
  36. pycforge/converter/core/pipeline.py +8 -0
  37. pycforge/converter/core/progress.py +25 -0
  38. pycforge/converter/core/request.py +50 -0
  39. pycforge/converter/core/resource_policy.py +31 -0
  40. pycforge/converter/core/result.py +23 -0
  41. pycforge/converter/core/serialization.py +36 -0
  42. pycforge/converter/core/stage.py +10 -0
  43. pycforge/converter/core/stage_artifact.py +80 -0
  44. pycforge/converter/core/stage_outcome.py +15 -0
  45. pycforge/converter/decision_trace/__init__.py +0 -0
  46. pycforge/converter/decision_trace/recorder.py +52 -0
  47. pycforge/converter/facade.py +459 -0
  48. pycforge/converter/frontend/__init__.py +4 -0
  49. pycforge/converter/frontend/normalizer.py +59 -0
  50. pycforge/converter/frontend/parser.py +20 -0
  51. pycforge/converter/frontend/source_document.py +93 -0
  52. pycforge/converter/frontend/stages.py +518 -0
  53. pycforge/converter/frontend/validation.py +76 -0
  54. pycforge/converter/io/__init__.py +0 -0
  55. pycforge/converter/io/atomic_writer.py +33 -0
  56. pycforge/converter/ir/__init__.py +1 -0
  57. pycforge/converter/ir/c_ir/__init__.py +4 -0
  58. pycforge/converter/ir/c_ir/builder.py +26 -0
  59. pycforge/converter/ir/c_ir/model.py +318 -0
  60. pycforge/converter/ir/c_ir/serialization.py +88 -0
  61. pycforge/converter/ir/c_ir/validation.py +834 -0
  62. pycforge/converter/ir/python_ir/__init__.py +17 -0
  63. pycforge/converter/ir/python_ir/nodes.py +86 -0
  64. pycforge/converter/keyword_calls/__init__.py +68 -0
  65. pycforge/converter/keyword_calls/analysis.py +458 -0
  66. pycforge/converter/keyword_calls/lowering.py +271 -0
  67. pycforge/converter/keyword_calls/model.py +304 -0
  68. pycforge/converter/keyword_calls/validation.py +802 -0
  69. pycforge/converter/keyword_only_calls/__init__.py +68 -0
  70. pycforge/converter/keyword_only_calls/analysis.py +640 -0
  71. pycforge/converter/keyword_only_calls/lowering.py +376 -0
  72. pycforge/converter/keyword_only_calls/model.py +328 -0
  73. pycforge/converter/keyword_only_calls/validation.py +1357 -0
  74. pycforge/converter/lowering.py +1000 -0
  75. pycforge/converter/modules/__init__.py +13 -0
  76. pycforge/converter/modules/analysis.py +1250 -0
  77. pycforge/converter/modules/lowering.py +200 -0
  78. pycforge/converter/modules/model.py +73 -0
  79. pycforge/converter/modules/stage.py +192 -0
  80. pycforge/converter/numeric_semantics/__init__.py +23 -0
  81. pycforge/converter/numeric_semantics/analysis.py +420 -0
  82. pycforge/converter/numeric_semantics/lowering.py +153 -0
  83. pycforge/converter/numeric_semantics/model.py +123 -0
  84. pycforge/converter/records/__init__.py +34 -0
  85. pycforge/converter/records/analysis.py +1394 -0
  86. pycforge/converter/records/lowering.py +393 -0
  87. pycforge/converter/records/model.py +319 -0
  88. pycforge/converter/rules/__init__.py +3 -0
  89. pycforge/converter/support_templates/__init__.py +49 -0
  90. pycforge/converter/support_templates/assembly.py +106 -0
  91. pycforge/converter/support_templates/factories.py +348 -0
  92. pycforge/converter/support_templates/model.py +159 -0
  93. pycforge/converter/support_templates/registry.py +388 -0
  94. pycforge/converter/telemetry/__init__.py +0 -0
  95. pycforge/converter/telemetry/sink.py +39 -0
  96. pycforge/ide/__init__.py +19 -0
  97. pycforge/ide/__main__.py +5 -0
  98. pycforge/ide/_worker_event_codec.py +276 -0
  99. pycforge/ide/_worker_protocol_json.py +197 -0
  100. pycforge/ide/_worker_protocol_types.py +90 -0
  101. pycforge/ide/_worker_request_codec.py +200 -0
  102. pycforge/ide/_worker_request_data.py +260 -0
  103. pycforge/ide/_worker_result_data.py +343 -0
  104. pycforge/ide/action_contract.py +469 -0
  105. pycforge/ide/action_validation.py +229 -0
  106. pycforge/ide/command_palette.py +223 -0
  107. pycforge/ide/controller.py +904 -0
  108. pycforge/ide/controller_conversion.py +546 -0
  109. pycforge/ide/controller_io.py +205 -0
  110. pycforge/ide/editor.py +951 -0
  111. pycforge/ide/editor_commands_qt.py +274 -0
  112. pycforge/ide/editor_lexical.py +43 -0
  113. pycforge/ide/editor_sidebars.py +236 -0
  114. pycforge/ide/editor_syntax.py +243 -0
  115. pycforge/ide/find_replace.py +535 -0
  116. pycforge/ide/icons.py +91 -0
  117. pycforge/ide/io_service.py +196 -0
  118. pycforge/ide/model.py +164 -0
  119. pycforge/ide/panels.py +903 -0
  120. pycforge/ide/positions.py +220 -0
  121. pycforge/ide/process_worker.py +130 -0
  122. pycforge/ide/qt.py +230 -0
  123. pycforge/ide/qt_actions.py +418 -0
  124. pycforge/ide/qt_close.py +169 -0
  125. pycforge/ide/qt_command_palette.py +297 -0
  126. pycforge/ide/qt_contract.py +220 -0
  127. pycforge/ide/qt_documents.py +590 -0
  128. pycforge/ide/qt_editor_buffers.py +113 -0
  129. pycforge/ide/qt_editor_surfaces.py +112 -0
  130. pycforge/ide/qt_menus.py +324 -0
  131. pycforge/ide/qt_projection.py +500 -0
  132. pycforge/ide/qt_shell.py +348 -0
  133. pycforge/ide/qt_shell_interactions.py +320 -0
  134. pycforge/ide/qt_state.py +393 -0
  135. pycforge/ide/qt_workspace_features.py +398 -0
  136. pycforge/ide/qt_workspace_navigation.py +208 -0
  137. pycforge/ide/qt_workspace_observers.py +173 -0
  138. pycforge/ide/qt_workspace_panels.py +545 -0
  139. pycforge/ide/qt_workspace_widgets.py +228 -0
  140. pycforge/ide/resources/icons/about.svg +1 -0
  141. pycforge/ide/resources/icons/add-document.svg +1 -0
  142. pycforge/ide/resources/icons/brand-mark.svg +1 -0
  143. pycforge/ide/resources/icons/bundle-search.svg +1 -0
  144. pycforge/ide/resources/icons/cancel.svg +1 -0
  145. pycforge/ide/resources/icons/check.svg +1 -0
  146. pycforge/ide/resources/icons/chevron-right.svg +1 -0
  147. pycforge/ide/resources/icons/close.svg +1 -0
  148. pycforge/ide/resources/icons/collapse-all.svg +1 -0
  149. pycforge/ide/resources/icons/command-palette.svg +1 -0
  150. pycforge/ide/resources/icons/convert.svg +1 -0
  151. pycforge/ide/resources/icons/copy.svg +1 -0
  152. pycforge/ide/resources/icons/cut.svg +1 -0
  153. pycforge/ide/resources/icons/decision-trace.svg +1 -0
  154. pycforge/ide/resources/icons/details.svg +1 -0
  155. pycforge/ide/resources/icons/diagnostics.svg +1 -0
  156. pycforge/ide/resources/icons/duplicate-line.svg +1 -0
  157. pycforge/ide/resources/icons/exit.svg +1 -0
  158. pycforge/ide/resources/icons/expand-all.svg +1 -0
  159. pycforge/ide/resources/icons/export.svg +1 -0
  160. pycforge/ide/resources/icons/find.svg +1 -0
  161. pycforge/ide/resources/icons/go-to-line.svg +1 -0
  162. pycforge/ide/resources/icons/go-to-output.svg +1 -0
  163. pycforge/ide/resources/icons/go-to-source.svg +1 -0
  164. pycforge/ide/resources/icons/history.svg +1 -0
  165. pycforge/ide/resources/icons/indent.svg +1 -0
  166. pycforge/ide/resources/icons/link-c.svg +1 -0
  167. pycforge/ide/resources/icons/mappings.svg +1 -0
  168. pycforge/ide/resources/icons/module.svg +1 -0
  169. pycforge/ide/resources/icons/move-down.svg +1 -0
  170. pycforge/ide/resources/icons/move-line-down.svg +1 -0
  171. pycforge/ide/resources/icons/move-line-up.svg +1 -0
  172. pycforge/ide/resources/icons/move-up.svg +1 -0
  173. pycforge/ide/resources/icons/next.svg +1 -0
  174. pycforge/ide/resources/icons/open.svg +1 -0
  175. pycforge/ide/resources/icons/outdent.svg +1 -0
  176. pycforge/ide/resources/icons/outline.svg +1 -0
  177. pycforge/ide/resources/icons/paste.svg +1 -0
  178. pycforge/ide/resources/icons/previous.svg +1 -0
  179. pycforge/ide/resources/icons/primary-module.svg +1 -0
  180. pycforge/ide/resources/icons/redo.svg +1 -0
  181. pycforge/ide/resources/icons/remove-document.svg +1 -0
  182. pycforge/ide/resources/icons/replace.svg +1 -0
  183. pycforge/ide/resources/icons/save-as.svg +1 -0
  184. pycforge/ide/resources/icons/save.svg +1 -0
  185. pycforge/ide/resources/icons/select-all.svg +1 -0
  186. pycforge/ide/resources/icons/settings.svg +1 -0
  187. pycforge/ide/resources/icons/show-c.svg +1 -0
  188. pycforge/ide/resources/icons/split-view.svg +1 -0
  189. pycforge/ide/resources/icons/summary.svg +1 -0
  190. pycforge/ide/resources/icons/telemetry.svg +1 -0
  191. pycforge/ide/resources/icons/toggle-comment.svg +1 -0
  192. pycforge/ide/resources/icons/toggle-fold.svg +1 -0
  193. pycforge/ide/resources/icons/undo.svg +1 -0
  194. pycforge/ide/resources/icons/whitespace.svg +1 -0
  195. pycforge/ide/revisions.py +400 -0
  196. pycforge/ide/search_service.py +280 -0
  197. pycforge/ide/session_history.py +150 -0
  198. pycforge/ide/source_editing.py +417 -0
  199. pycforge/ide/source_structure.py +558 -0
  200. pycforge/ide/source_structure_async.py +191 -0
  201. pycforge/ide/supervisor.py +503 -0
  202. pycforge/ide/theme.py +119 -0
  203. pycforge/ide/theme_stylesheet.py +598 -0
  204. pycforge/ide/theme_workspace_stylesheet.py +155 -0
  205. pycforge/ide/visual_tokens.py +142 -0
  206. pycforge/ide/worker_protocol.py +80 -0
  207. pycforge/ide/workspace_search.py +462 -0
  208. pycforge/ide/workspace_session.py +256 -0
  209. pycforge/laboratory/__init__.py +6 -0
  210. pycforge/laboratory/audits.py +2244 -0
  211. pycforge/laboratory/checkpoint_e.py +2685 -0
  212. pycforge/laboratory/cli.py +119 -0
  213. pycforge/laboratory/exit_codes.py +11 -0
  214. pycforge/laboratory/keyword_audit.py +661 -0
  215. pycforge/laboratory/keyword_only_audit.py +481 -0
  216. pycforge-0.15.2.dist-info/METADATA +209 -0
  217. pycforge-0.15.2.dist-info/RECORD +221 -0
  218. pycforge-0.15.2.dist-info/WHEEL +5 -0
  219. pycforge-0.15.2.dist-info/entry_points.txt +5 -0
  220. pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
  221. pycforge-0.15.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,343 @@
1
+ """Strict plain-data mapping for converter results and progress."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pycforge.converter.contracts.versions import RESULT_SCHEMA_VERSION
8
+ from pycforge.converter.core.artifact_io import (
9
+ ArtifactCompatibilityError,
10
+ artifact_from_dict,
11
+ artifact_to_dict,
12
+ )
13
+ from pycforge.converter.core.diagnostics import Diagnostic
14
+ from pycforge.converter.core.enums import ResultStatus, Severity
15
+ from pycforge.converter.core.fingerprint import Fingerprint, fingerprint
16
+ from pycforge.converter.core.progress import ConversionProgress
17
+ from pycforge.converter.core.result import ConversionResult
18
+ from pycforge.converter.core.stage_artifact import freeze_value
19
+
20
+ from ._worker_protocol_json import (
21
+ _exact_fields,
22
+ _list,
23
+ _nonempty_string,
24
+ _nonnegative_integer,
25
+ _object,
26
+ _optional_object,
27
+ _string,
28
+ _string_list,
29
+ )
30
+ from ._worker_protocol_types import MAX_DIAGNOSTICS, WorkerProtocolError
31
+
32
+
33
+ _FINGERPRINT_FIELDS = frozenset(
34
+ {"domain", "schema_version", "canonicalization_version", "algorithm", "value"}
35
+ )
36
+ _DIAGNOSTIC_FIELDS = frozenset(
37
+ {
38
+ "diagnostic_id",
39
+ "code",
40
+ "severity",
41
+ "stage",
42
+ "message",
43
+ "effect_on_status",
44
+ "source_span",
45
+ "related_spans",
46
+ "causal_diagnostic_id",
47
+ "target_contract",
48
+ "semantic_policy",
49
+ "rule_id",
50
+ "fact_references",
51
+ "obligation_references",
52
+ "explanation",
53
+ "remediation",
54
+ "approximation_code",
55
+ "semantic_delta",
56
+ "source_module_id",
57
+ "source_logical_name",
58
+ }
59
+ )
60
+ _RESULT_FIELDS = frozenset(
61
+ {
62
+ "schema_version",
63
+ "status",
64
+ "generated_c",
65
+ "diagnostics",
66
+ "request_fingerprint",
67
+ "resource_fingerprint",
68
+ "output_fingerprint",
69
+ "last_completed_stage",
70
+ "stage_order",
71
+ "decision_trace",
72
+ "telemetry",
73
+ "stage_artifact",
74
+ "conversion_summary",
75
+ }
76
+ )
77
+ _PUBLISHABLE_STATUSES = frozenset(
78
+ {
79
+ ResultStatus.CONVERTED,
80
+ ResultStatus.CONVERTED_WITH_WARNINGS,
81
+ ResultStatus.CONVERTED_WITH_APPROXIMATIONS,
82
+ }
83
+ )
84
+
85
+
86
+ def _result_to_data(result: ConversionResult) -> dict[str, object]:
87
+ return {
88
+ "schema_version": RESULT_SCHEMA_VERSION,
89
+ "status": result.status.value,
90
+ "generated_c": result.generated_c,
91
+ "diagnostics": [item.to_dict() for item in result.diagnostics],
92
+ "request_fingerprint": (
93
+ None
94
+ if result.request_fingerprint is None
95
+ else result.request_fingerprint.to_dict()
96
+ ),
97
+ "resource_fingerprint": (
98
+ None
99
+ if result.resource_fingerprint is None
100
+ else result.resource_fingerprint.to_dict()
101
+ ),
102
+ "output_fingerprint": (
103
+ None
104
+ if result.output_fingerprint is None
105
+ else result.output_fingerprint.to_dict()
106
+ ),
107
+ "last_completed_stage": result.last_completed_stage,
108
+ "stage_order": list(result.stage_order),
109
+ "decision_trace": result.decision_trace,
110
+ "telemetry": result.telemetry,
111
+ "stage_artifact": (
112
+ None if result.stage_artifact is None else artifact_to_dict(result.stage_artifact)
113
+ ),
114
+ "conversion_summary": result.conversion_summary,
115
+ }
116
+
117
+
118
+ def _result_from_data(value: object) -> ConversionResult:
119
+ data = _object(value, "conversion result")
120
+ _exact_fields(data, _RESULT_FIELDS, "conversion result")
121
+ if data["schema_version"] != RESULT_SCHEMA_VERSION:
122
+ raise WorkerProtocolError("incompatible conversion result schema")
123
+ try:
124
+ status = ResultStatus(data["status"])
125
+ except (TypeError, ValueError) as exc:
126
+ raise WorkerProtocolError("unknown conversion result status") from exc
127
+ generated_c = data["generated_c"]
128
+ if generated_c is not None:
129
+ generated_c = _string(generated_c, "generated C")
130
+ raw_diagnostics = _list(data["diagnostics"], "conversion diagnostics")
131
+ if len(raw_diagnostics) > MAX_DIAGNOSTICS:
132
+ raise WorkerProtocolError("conversion diagnostic count exceeds protocol bounds")
133
+ diagnostics = tuple(_diagnostic_from_data(item) for item in raw_diagnostics)
134
+ request_fingerprint = _fingerprint_from_data(
135
+ data["request_fingerprint"], "request", "conversion-request", False
136
+ )
137
+ resource_fingerprint = _fingerprint_from_data(
138
+ data["resource_fingerprint"], "resource", "resource-policy", False
139
+ )
140
+ output_fingerprint = _fingerprint_from_data(
141
+ data["output_fingerprint"], "output", "generated-output", False
142
+ )
143
+ last_completed_stage = data["last_completed_stage"]
144
+ if last_completed_stage is not None:
145
+ last_completed_stage = _nonempty_string(
146
+ last_completed_stage, "last completed stage"
147
+ )
148
+ stage_order = _string_list(data["stage_order"], "stage order")
149
+ decision_trace = _optional_object(data["decision_trace"], "decision trace")
150
+ telemetry = _optional_object(data["telemetry"], "telemetry")
151
+ conversion_summary = _optional_object(
152
+ data["conversion_summary"], "conversion summary"
153
+ )
154
+ artifact_data = data["stage_artifact"]
155
+ if artifact_data is None:
156
+ artifact = None
157
+ else:
158
+ try:
159
+ artifact = artifact_from_dict(
160
+ _object(artifact_data, "stage artifact")
161
+ )
162
+ except ArtifactCompatibilityError as exc:
163
+ raise WorkerProtocolError("conversion stage artifact is invalid") from exc
164
+
165
+ if generated_c is None:
166
+ if output_fingerprint is not None:
167
+ raise WorkerProtocolError(
168
+ "unpublished conversion carries an output fingerprint"
169
+ )
170
+ if status in _PUBLISHABLE_STATUSES:
171
+ raise WorkerProtocolError(
172
+ "publishable conversion result lacks generated C"
173
+ )
174
+ else:
175
+ expected_output = fingerprint("generated-output", generated_c)
176
+ if output_fingerprint != expected_output:
177
+ raise WorkerProtocolError("conversion output fingerprint mismatch")
178
+ if status not in _PUBLISHABLE_STATUSES:
179
+ raise WorkerProtocolError(
180
+ "non-publishable conversion carries generated C"
181
+ )
182
+ if artifact is not None:
183
+ artifact_order = artifact.payload.get("stage_order", ())
184
+ if tuple(artifact_order) != stage_order:
185
+ raise WorkerProtocolError("conversion stage order mismatch")
186
+ if (
187
+ request_fingerprint is None
188
+ or artifact.conversion_id != request_fingerprint.value
189
+ ):
190
+ raise WorkerProtocolError("conversion artifact request identity mismatch")
191
+ artifact_c = artifact.payload.get("generated_c")
192
+ if generated_c is not None and artifact_c != generated_c:
193
+ raise WorkerProtocolError("conversion artifact output mismatch")
194
+ elif stage_order:
195
+ raise WorkerProtocolError("conversion without artifact carries stage order")
196
+
197
+ return ConversionResult(
198
+ status,
199
+ generated_c,
200
+ diagnostics,
201
+ request_fingerprint,
202
+ resource_fingerprint,
203
+ output_fingerprint,
204
+ last_completed_stage,
205
+ stage_order,
206
+ freeze_value(decision_trace),
207
+ freeze_value(telemetry),
208
+ artifact,
209
+ freeze_value(conversion_summary),
210
+ )
211
+
212
+
213
+ def _diagnostic_from_data(value: object) -> Diagnostic:
214
+ data = _object(value, "diagnostic")
215
+ _exact_fields(data, _DIAGNOSTIC_FIELDS, "diagnostic")
216
+ try:
217
+ severity = Severity(data["severity"])
218
+ except (TypeError, ValueError) as exc:
219
+ raise WorkerProtocolError("diagnostic severity is invalid") from exc
220
+ for key in ("code", "stage", "message"):
221
+ _nonempty_string(data[key], f"diagnostic {key}")
222
+ if not isinstance(data["effect_on_status"], bool):
223
+ raise WorkerProtocolError("diagnostic effect_on_status must be Boolean")
224
+ source_span = data["source_span"]
225
+ if source_span is not None:
226
+ source_span = _object(source_span, "diagnostic source span")
227
+ raw_related = _list(data["related_spans"], "diagnostic related spans")
228
+ related = tuple(_object(item, "diagnostic related span") for item in raw_related)
229
+ optional_strings = {}
230
+ for key in (
231
+ "causal_diagnostic_id",
232
+ "target_contract",
233
+ "semantic_policy",
234
+ "rule_id",
235
+ "explanation",
236
+ "remediation",
237
+ "approximation_code",
238
+ "semantic_delta",
239
+ "source_module_id",
240
+ "source_logical_name",
241
+ ):
242
+ raw = data[key]
243
+ optional_strings[key] = (
244
+ None if raw is None else _string(raw, f"diagnostic {key}")
245
+ )
246
+ diagnostic = Diagnostic(
247
+ code=data["code"],
248
+ severity=severity,
249
+ stage=data["stage"],
250
+ message=data["message"],
251
+ effect_on_status=data["effect_on_status"],
252
+ source_span=source_span,
253
+ related_spans=related,
254
+ causal_diagnostic_id=optional_strings["causal_diagnostic_id"],
255
+ target_contract=optional_strings["target_contract"],
256
+ semantic_policy=optional_strings["semantic_policy"],
257
+ rule_id=optional_strings["rule_id"],
258
+ fact_references=_string_list(
259
+ data["fact_references"], "diagnostic fact references"
260
+ ),
261
+ obligation_references=_string_list(
262
+ data["obligation_references"], "diagnostic obligation references"
263
+ ),
264
+ explanation=optional_strings["explanation"],
265
+ remediation=optional_strings["remediation"],
266
+ approximation_code=optional_strings["approximation_code"],
267
+ semantic_delta=optional_strings["semantic_delta"],
268
+ source_module_id=optional_strings["source_module_id"],
269
+ source_logical_name=optional_strings["source_logical_name"],
270
+ )
271
+ supplied_id = _nonempty_string(data["diagnostic_id"], "diagnostic ID")
272
+ if supplied_id != diagnostic.diagnostic_id:
273
+ raise WorkerProtocolError("diagnostic identity mismatch")
274
+ return diagnostic
275
+
276
+
277
+ def _fingerprint_from_data(
278
+ value: object,
279
+ role: str,
280
+ domain: str,
281
+ required: bool,
282
+ ) -> Fingerprint | None:
283
+ if value is None:
284
+ if required:
285
+ raise WorkerProtocolError(f"missing {role} fingerprint")
286
+ return None
287
+ data = _object(value, f"{role} fingerprint")
288
+ _exact_fields(data, _FINGERPRINT_FIELDS, f"{role} fingerprint")
289
+ if not all(isinstance(item, str) for item in data.values()):
290
+ raise WorkerProtocolError(f"{role} fingerprint fields must be strings")
291
+ result = Fingerprint(**data)
292
+ if (
293
+ result.domain != domain
294
+ or result.schema_version != "0.1"
295
+ or result.canonicalization_version != "canonical-json-v1"
296
+ or result.algorithm != "sha256"
297
+ or len(result.value) != 64
298
+ or any(character not in "0123456789abcdef" for character in result.value)
299
+ ):
300
+ raise WorkerProtocolError(f"{role} fingerprint metadata is incompatible")
301
+ return result
302
+
303
+
304
+ def _progress_from_data(value: object) -> ConversionProgress:
305
+ data = _object(value, "conversion progress")
306
+ _exact_fields(
307
+ data,
308
+ {"state", "stage_id", "completed_stages", "total_stages"},
309
+ "conversion progress",
310
+ )
311
+ state = _nonempty_string(data["state"], "conversion progress state")
312
+ stage_id = data["stage_id"]
313
+ if stage_id is not None:
314
+ stage_id = _nonempty_string(stage_id, "conversion progress stage")
315
+ completed = _nonnegative_integer(
316
+ data["completed_stages"], "completed conversion stages"
317
+ )
318
+ total = _nonnegative_integer(data["total_stages"], "total conversion stages")
319
+ try:
320
+ return ConversionProgress(state, stage_id, completed, total)
321
+ except ValueError as exc:
322
+ raise WorkerProtocolError("conversion progress is inconsistent") from exc
323
+
324
+
325
+ def _mappings_from_data(value: object) -> tuple[dict[str, Any], ...]:
326
+ items = _list(value, "source/output mappings")
327
+ return tuple(
328
+ freeze_value(_object(item, "source/output mapping")) for item in items
329
+ )
330
+
331
+
332
+ def _validate_result_mappings(
333
+ result: ConversionResult,
334
+ mappings: tuple[dict[str, Any], ...],
335
+ ) -> None:
336
+ artifact = result.stage_artifact
337
+ if artifact is None:
338
+ if mappings:
339
+ raise WorkerProtocolError("conversion without artifact carries mappings")
340
+ return
341
+ raw = artifact.payload.get("source_output_mappings", ())
342
+ if not isinstance(raw, (tuple, list)) or tuple(raw) != mappings:
343
+ raise WorkerProtocolError("terminal source/output mappings mismatch")