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,546 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import CancelledError, Future
4
+ from dataclasses import replace
5
+
6
+ from pycforge.converter.core.enums import ResultStatus, Severity
7
+ from pycforge.converter.core.progress import ConversionProgress
8
+ from pycforge.converter.core.request import SourceBundle
9
+ from pycforge.converter.core.stage_artifact import freeze_value
10
+
11
+ from .model import WorkspaceState
12
+ from .revisions import WorkspaceRevision
13
+ from .session_history import (
14
+ ConversionHistoryEntry,
15
+ append_conversion_history,
16
+ )
17
+ from .supervisor import (
18
+ ConversionCancelled,
19
+ ConversionSuperseded,
20
+ WorkerFailure,
21
+ )
22
+
23
+
24
+ class ConversionControllerMixin:
25
+ """Isolated-conversion lifecycle mixed into the workspace controller."""
26
+
27
+ def convert_async(self) -> Future:
28
+ with self._lock:
29
+ if self._closed:
30
+ raise RuntimeError("workspace controller is closed")
31
+ if not self.snapshot.revision_authenticated:
32
+ revision_future = self._revision_future
33
+ if revision_future is None:
34
+ raise RuntimeError(
35
+ "workspace revision is not authenticated"
36
+ )
37
+ deferred: Future = Future()
38
+ revision_future.add_done_callback(
39
+ lambda completed: self._submit_after_revision(
40
+ completed,
41
+ deferred,
42
+ )
43
+ )
44
+ return deferred
45
+ return self._submit_authenticated_revision()
46
+
47
+ def convert(self):
48
+ return self.convert_async().result()
49
+
50
+ def cancel(self) -> None:
51
+ with self._lock:
52
+ current = self.snapshot
53
+ if current.state is not WorkspaceState.CONVERTING:
54
+ return
55
+ self._publish(
56
+ replace(
57
+ current,
58
+ state=WorkspaceState.CANCEL_REQUESTED,
59
+ stale_reason=(
60
+ "cancel-requested"
61
+ if current.generated_c is not None
62
+ else None
63
+ ),
64
+ worker_failure_reason=None,
65
+ )
66
+ )
67
+ self._supervisor.cancel(current.request_sequence)
68
+
69
+ def _submit_after_revision(
70
+ self,
71
+ revision_future: Future[WorkspaceRevision],
72
+ deferred: Future,
73
+ ) -> None:
74
+ if deferred.done():
75
+ return
76
+ try:
77
+ revision_future.result()
78
+ submitted = self.convert_async()
79
+ except CancelledError:
80
+ deferred.set_exception(
81
+ ConversionSuperseded(
82
+ "workspace revision was superseded before conversion"
83
+ )
84
+ )
85
+ return
86
+ except BaseException as exc:
87
+ deferred.set_exception(exc)
88
+ return
89
+
90
+ def bridge(completed: Future) -> None:
91
+ if deferred.done():
92
+ return
93
+ try:
94
+ deferred.set_result(completed.result())
95
+ except BaseException as exc:
96
+ deferred.set_exception(exc)
97
+
98
+ submitted.add_done_callback(bridge)
99
+
100
+ def _submit_authenticated_revision(self) -> Future:
101
+ current = self.snapshot
102
+ revision = self._current_revision
103
+ if (
104
+ revision.generation != current.revision_generation
105
+ or revision.bundle_fingerprint != current.bundle_fingerprint
106
+ ):
107
+ raise RuntimeError("workspace revision identity is unsettled")
108
+ self._request_sequence += 1
109
+ sequence = self._request_sequence
110
+ revision_generation = revision.generation
111
+ bundle_fingerprint = revision.bundle_fingerprint
112
+ self._publish(
113
+ replace(
114
+ current,
115
+ state=WorkspaceState.CONVERTING,
116
+ request_sequence=sequence,
117
+ stale_reason=None,
118
+ worker_failure_reason=None,
119
+ active_stage=None,
120
+ completed_stages=0,
121
+ total_stages=0,
122
+ )
123
+ )
124
+ public: Future = Future()
125
+ try:
126
+ isolated = self._supervisor.submit(
127
+ generation=sequence,
128
+ bundle_fingerprint=bundle_fingerprint,
129
+ request=revision.request,
130
+ progress=self._report_progress,
131
+ )
132
+ except BaseException as exc:
133
+ self._fail_conversion(
134
+ sequence,
135
+ revision_generation,
136
+ bundle_fingerprint,
137
+ revision.request_fingerprint.value,
138
+ "worker-startup-failure",
139
+ )
140
+ public.set_exception(exc)
141
+ return public
142
+ isolated.add_done_callback(
143
+ lambda completed: self._conversion_finished(
144
+ completed,
145
+ public,
146
+ sequence,
147
+ revision_generation,
148
+ bundle_fingerprint,
149
+ revision,
150
+ )
151
+ )
152
+ return public
153
+
154
+ def _conversion_finished(
155
+ self,
156
+ isolated: Future,
157
+ public: Future,
158
+ sequence: int,
159
+ revision_generation: int,
160
+ bundle_fingerprint: str,
161
+ revision: WorkspaceRevision,
162
+ ) -> None:
163
+ try:
164
+ result = isolated.result()
165
+ except (ConversionCancelled, ConversionSuperseded, CancelledError) as exc:
166
+ self._finish_cancellation(
167
+ sequence,
168
+ revision_generation,
169
+ bundle_fingerprint,
170
+ revision.request_fingerprint.value,
171
+ )
172
+ if not public.done():
173
+ public.set_exception(exc)
174
+ return
175
+ except BaseException as exc:
176
+ reason = (
177
+ exc.classification
178
+ if isinstance(exc, WorkerFailure)
179
+ else "worker-failure"
180
+ )
181
+ self._fail_conversion(
182
+ sequence,
183
+ revision_generation,
184
+ bundle_fingerprint,
185
+ revision.request_fingerprint.value,
186
+ reason,
187
+ )
188
+ if not public.done():
189
+ public.set_exception(exc)
190
+ return
191
+ if result.request_fingerprint != revision.request_fingerprint:
192
+ failure = WorkerFailure("request-fingerprint-mismatch")
193
+ self._fail_conversion(
194
+ sequence,
195
+ revision_generation,
196
+ bundle_fingerprint,
197
+ revision.request_fingerprint.value,
198
+ failure.classification,
199
+ )
200
+ if not public.done():
201
+ public.set_exception(failure)
202
+ return
203
+ self._publish_result(
204
+ sequence,
205
+ revision_generation,
206
+ bundle_fingerprint,
207
+ revision.source_bundle,
208
+ result,
209
+ )
210
+ if not public.done():
211
+ public.set_result(result)
212
+
213
+ def _publish_result(
214
+ self,
215
+ sequence: int,
216
+ revision_generation: int,
217
+ bundle_fingerprint: str,
218
+ source_bundle: SourceBundle,
219
+ result,
220
+ ) -> None:
221
+ generated_c = result.generated_c
222
+ index_job: tuple[int, str] | None = None
223
+ with self._lock:
224
+ current = self.snapshot
225
+ if (
226
+ sequence != self._request_sequence
227
+ or sequence != current.request_sequence
228
+ or revision_generation != current.revision_generation
229
+ or current.bundle_fingerprint != bundle_fingerprint
230
+ ):
231
+ return
232
+ state = self._state_for(result)
233
+ payload = (
234
+ result.stage_artifact.payload
235
+ if result.stage_artifact
236
+ else {}
237
+ )
238
+ conversion_summary = result.conversion_summary or {}
239
+ summary = (
240
+ ("status", result.status.value),
241
+ ("last_completed_stage", result.last_completed_stage or ""),
242
+ (
243
+ "request_fingerprint",
244
+ result.request_fingerprint.value
245
+ if result.request_fingerprint
246
+ else "",
247
+ ),
248
+ (
249
+ "output_fingerprint",
250
+ result.output_fingerprint.value
251
+ if result.output_fingerprint
252
+ else "",
253
+ ),
254
+ ("modules", str(len(source_bundle.companions) + 1)),
255
+ ("functions", str(len(conversion_summary.get("functions", ())))),
256
+ ("calls", str(len(conversion_summary.get("calls", ())))),
257
+ ("helpers", str(len(conversion_summary.get("helpers", ())))),
258
+ )
259
+ published = result.generated_c is not None
260
+ completed_stage_count = len(result.stage_order)
261
+ total_stage_count = max(
262
+ current.total_stages,
263
+ completed_stage_count,
264
+ )
265
+ history = append_conversion_history(
266
+ current.conversion_history,
267
+ ConversionHistoryEntry(
268
+ request_sequence=sequence,
269
+ revision_generation=revision_generation,
270
+ bundle_fingerprint=bundle_fingerprint,
271
+ request_fingerprint=(
272
+ result.request_fingerprint.value
273
+ if result.request_fingerprint
274
+ else None
275
+ ),
276
+ output_fingerprint=(
277
+ result.output_fingerprint.value
278
+ if result.output_fingerprint
279
+ else None
280
+ ),
281
+ status=result.status.value,
282
+ diagnostic_count=len(result.diagnostics),
283
+ completed_stage_count=completed_stage_count,
284
+ total_stage_count=total_stage_count,
285
+ stage=result.last_completed_stage,
286
+ published=published,
287
+ reason=self._result_history_reason(state, published),
288
+ ),
289
+ )
290
+ if published:
291
+ assert generated_c is not None
292
+ self._result_output_index = None
293
+ self._output_index_generation += 1
294
+ index_job = (
295
+ self._output_index_generation,
296
+ generated_c,
297
+ )
298
+ snapshot = replace(
299
+ current,
300
+ generated_c=(
301
+ result.generated_c if published else current.generated_c
302
+ ),
303
+ state=state,
304
+ diagnostics=tuple(
305
+ freeze_value(diagnostic.to_dict())
306
+ for diagnostic in result.diagnostics
307
+ ),
308
+ summary=summary,
309
+ decision_trace=result.decision_trace,
310
+ telemetry=result.telemetry,
311
+ conversion_summary=result.conversion_summary,
312
+ mappings=(
313
+ tuple(payload.get("source_output_mappings", ()))
314
+ if published
315
+ else current.mappings
316
+ ),
317
+ conversion_history=history,
318
+ result_source_fingerprint=(
319
+ bundle_fingerprint
320
+ if published
321
+ else current.result_source_fingerprint
322
+ ),
323
+ result_bundle_fingerprint=(
324
+ bundle_fingerprint
325
+ if published
326
+ else current.result_bundle_fingerprint
327
+ ),
328
+ result_revision_generation=(
329
+ revision_generation
330
+ if published
331
+ else current.result_revision_generation
332
+ ),
333
+ result_state=state if published else current.result_state,
334
+ stale_reason=(
335
+ None
336
+ if published
337
+ else (
338
+ "conversion-not-published"
339
+ if current.generated_c is not None
340
+ else None
341
+ )
342
+ ),
343
+ active_stage=None,
344
+ completed_stages=completed_stage_count,
345
+ total_stages=total_stage_count,
346
+ worker_failure_reason=(
347
+ "converter-internal-failure"
348
+ if state is WorkspaceState.FAILED
349
+ else None
350
+ ),
351
+ )
352
+ self._publish(snapshot)
353
+ if index_job is not None:
354
+ try:
355
+ self._output_index_service.submit(*index_job)
356
+ except RuntimeError:
357
+ # Close may retire deferred presentation work immediately
358
+ # after the terminal result is safely published.
359
+ pass
360
+
361
+ def _fail_conversion(
362
+ self,
363
+ sequence: int,
364
+ revision_generation: int,
365
+ bundle_fingerprint: str,
366
+ request_fingerprint: str,
367
+ reason: str,
368
+ ) -> None:
369
+ with self._lock:
370
+ current = self.snapshot
371
+ if (
372
+ sequence != self._request_sequence
373
+ or current.request_sequence != sequence
374
+ or current.revision_generation != revision_generation
375
+ or current.bundle_fingerprint != bundle_fingerprint
376
+ or current.state
377
+ not in {
378
+ WorkspaceState.CONVERTING,
379
+ WorkspaceState.CANCEL_REQUESTED,
380
+ }
381
+ ):
382
+ return
383
+ history = append_conversion_history(
384
+ current.conversion_history,
385
+ ConversionHistoryEntry(
386
+ request_sequence=sequence,
387
+ revision_generation=revision_generation,
388
+ bundle_fingerprint=bundle_fingerprint,
389
+ request_fingerprint=request_fingerprint,
390
+ output_fingerprint=None,
391
+ status="WorkerFailure",
392
+ diagnostic_count=0,
393
+ completed_stage_count=current.completed_stages,
394
+ total_stage_count=current.total_stages,
395
+ stage=current.active_stage,
396
+ published=False,
397
+ reason=reason,
398
+ ),
399
+ )
400
+ self._publish(
401
+ replace(
402
+ current,
403
+ state=WorkspaceState.FAILED,
404
+ diagnostics=(),
405
+ summary=(),
406
+ decision_trace=None,
407
+ telemetry=None,
408
+ conversion_summary=None,
409
+ stale_reason=(
410
+ "conversion-failed"
411
+ if current.generated_c is not None
412
+ else None
413
+ ),
414
+ active_stage=None,
415
+ completed_stages=0,
416
+ total_stages=0,
417
+ worker_failure_reason=reason,
418
+ conversion_history=history,
419
+ )
420
+ )
421
+
422
+ def _finish_cancellation(
423
+ self,
424
+ sequence: int,
425
+ revision_generation: int,
426
+ bundle_fingerprint: str,
427
+ request_fingerprint: str,
428
+ ) -> None:
429
+ with self._lock:
430
+ current = self.snapshot
431
+ if (
432
+ sequence != self._request_sequence
433
+ or current.request_sequence != sequence
434
+ or current.revision_generation != revision_generation
435
+ or current.bundle_fingerprint != bundle_fingerprint
436
+ or current.state
437
+ not in {
438
+ WorkspaceState.CONVERTING,
439
+ WorkspaceState.CANCEL_REQUESTED,
440
+ }
441
+ ):
442
+ return
443
+ history = append_conversion_history(
444
+ current.conversion_history,
445
+ ConversionHistoryEntry(
446
+ request_sequence=sequence,
447
+ revision_generation=revision_generation,
448
+ bundle_fingerprint=bundle_fingerprint,
449
+ request_fingerprint=request_fingerprint,
450
+ output_fingerprint=None,
451
+ status=ResultStatus.CANCELED.value,
452
+ diagnostic_count=0,
453
+ completed_stage_count=current.completed_stages,
454
+ total_stage_count=current.total_stages,
455
+ stage=current.active_stage,
456
+ published=False,
457
+ reason="conversion-canceled",
458
+ ),
459
+ )
460
+ self._publish(
461
+ replace(
462
+ current,
463
+ state=WorkspaceState.CANCELED,
464
+ diagnostics=(),
465
+ summary=(),
466
+ decision_trace=None,
467
+ telemetry=None,
468
+ conversion_summary=None,
469
+ stale_reason=(
470
+ "conversion-canceled"
471
+ if current.generated_c is not None
472
+ else None
473
+ ),
474
+ active_stage=None,
475
+ completed_stages=0,
476
+ total_stages=0,
477
+ worker_failure_reason=None,
478
+ conversion_history=history,
479
+ )
480
+ )
481
+
482
+ def _report_progress(
483
+ self,
484
+ sequence: int,
485
+ bundle_fingerprint: str,
486
+ event: ConversionProgress,
487
+ ) -> None:
488
+ with self._lock:
489
+ current = self.snapshot
490
+ if (
491
+ sequence != self._request_sequence
492
+ or current.request_sequence != sequence
493
+ or current.bundle_fingerprint != bundle_fingerprint
494
+ or current.state is not WorkspaceState.CONVERTING
495
+ ):
496
+ return
497
+ self._publish(
498
+ replace(
499
+ current,
500
+ active_stage=event.stage_id,
501
+ completed_stages=event.completed_stages,
502
+ total_stages=event.total_stages,
503
+ )
504
+ )
505
+
506
+ @staticmethod
507
+ def _state_for(result) -> WorkspaceState:
508
+ if result.status is ResultStatus.CANCELED:
509
+ return WorkspaceState.CANCELED
510
+ if result.status is ResultStatus.REJECTED:
511
+ return WorkspaceState.REJECTED
512
+ if result.status is ResultStatus.INTERNAL_FAILURE:
513
+ return WorkspaceState.FAILED
514
+ severities = {diagnostic.severity for diagnostic in result.diagnostics}
515
+ if Severity.APPROXIMATION in severities:
516
+ return WorkspaceState.APPROXIMATION
517
+ trace = result.decision_trace or {}
518
+ telemetry = result.telemetry or {}
519
+ if (
520
+ trace.get("truncated")
521
+ or trace.get("observer_failed")
522
+ or telemetry.get("dropped")
523
+ or telemetry.get("observer_failed")
524
+ ):
525
+ return WorkspaceState.OBSERVER_INCOMPLETE
526
+ if Severity.WARNING in severities:
527
+ return WorkspaceState.WARNING
528
+ return WorkspaceState.CONVERTED
529
+
530
+ @staticmethod
531
+ def _result_history_reason(
532
+ state: WorkspaceState,
533
+ published: bool,
534
+ ) -> str | None:
535
+ if published:
536
+ return None
537
+ if state is WorkspaceState.REJECTED:
538
+ return "converter-rejected"
539
+ if state is WorkspaceState.FAILED:
540
+ return "converter-internal-failure"
541
+ if state is WorkspaceState.CANCELED:
542
+ return "converter-canceled"
543
+ return "conversion-not-published"
544
+
545
+
546
+ __all__ = ["ConversionControllerMixin"]