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,276 @@
1
+ """Progress, terminal-result, and worker-failure event codec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pycforge.converter.core.progress import ConversionProgress
6
+ from pycforge.converter.core.result import ConversionResult
7
+
8
+ from ._worker_protocol_json import (
9
+ _contains_surrogate,
10
+ _decode_bounded,
11
+ _encode_bounded,
12
+ _exact_fields,
13
+ _generation,
14
+ _object,
15
+ _receive_frame,
16
+ _send_frame,
17
+ _sha256,
18
+ )
19
+ from ._worker_protocol_types import (
20
+ FAILURE_CLASSIFICATIONS,
21
+ MAX_EVENT_BYTES,
22
+ MAX_FAILURE_MESSAGE_CHARS,
23
+ PROTOCOL_SCHEMA,
24
+ ByteConnection,
25
+ WorkerEvent,
26
+ WorkerProtocolError,
27
+ WorkerRequest,
28
+ )
29
+ from ._worker_request_codec import _validate_worker_request
30
+ from ._worker_result_data import (
31
+ _mappings_from_data,
32
+ _progress_from_data,
33
+ _result_from_data,
34
+ _result_to_data,
35
+ _validate_result_mappings,
36
+ )
37
+
38
+
39
+ _ENVELOPE_FIELDS = frozenset(
40
+ {
41
+ "schema",
42
+ "kind",
43
+ "generation",
44
+ "bundle_fingerprint",
45
+ "transport_fingerprint",
46
+ "progress",
47
+ "result",
48
+ "mappings",
49
+ "failure_classification",
50
+ "message",
51
+ }
52
+ )
53
+
54
+
55
+ def encode_progress(request: WorkerRequest, progress: ConversionProgress) -> bytes:
56
+ _validate_worker_request(request)
57
+ if not isinstance(progress, ConversionProgress):
58
+ raise WorkerProtocolError("progress event must be ConversionProgress")
59
+ return _encode_event(
60
+ request,
61
+ "progress",
62
+ progress={
63
+ "state": progress.state,
64
+ "stage_id": progress.stage_id,
65
+ "completed_stages": progress.completed_stages,
66
+ "total_stages": progress.total_stages,
67
+ },
68
+ )
69
+
70
+
71
+ def encode_terminal(request: WorkerRequest, result: ConversionResult) -> bytes:
72
+ _validate_worker_request(request)
73
+ if not isinstance(result, ConversionResult):
74
+ raise WorkerProtocolError("terminal event must contain ConversionResult")
75
+ artifact = result.stage_artifact
76
+ mappings: list[object] = []
77
+ if artifact is not None:
78
+ raw_mappings = artifact.payload.get("source_output_mappings", ())
79
+ if not isinstance(raw_mappings, (tuple, list)):
80
+ raise WorkerProtocolError("terminal source/output mappings are malformed")
81
+ mappings = list(raw_mappings)
82
+ return _encode_event(
83
+ request,
84
+ "terminal",
85
+ result=_result_to_data(result),
86
+ mappings=mappings,
87
+ )
88
+
89
+
90
+ def encode_failure(
91
+ request: WorkerRequest | None,
92
+ failure_classification: str,
93
+ message: str,
94
+ ) -> bytes:
95
+ if failure_classification not in FAILURE_CLASSIFICATIONS:
96
+ raise WorkerProtocolError("unknown worker failure classification")
97
+ if (
98
+ not isinstance(message, str)
99
+ or not message
100
+ or len(message) > MAX_FAILURE_MESSAGE_CHARS
101
+ or _contains_surrogate(message)
102
+ ):
103
+ raise WorkerProtocolError("worker failure message is malformed")
104
+ return _encode_event(
105
+ request,
106
+ "failure",
107
+ failure_classification=failure_classification,
108
+ message=message,
109
+ )
110
+
111
+
112
+ def decode_event(
113
+ frame: bytes,
114
+ *,
115
+ expected: WorkerRequest | None = None,
116
+ expected_generation: int | None = None,
117
+ expected_bundle_fingerprint: str | None = None,
118
+ expected_transport_fingerprint: str | None = None,
119
+ ) -> WorkerEvent:
120
+ value = _decode_bounded(frame, MAX_EVENT_BYTES, "event")
121
+ data = _object(value, "event envelope")
122
+ _exact_fields(data, _ENVELOPE_FIELDS, "event envelope")
123
+ if data["schema"] != PROTOCOL_SCHEMA:
124
+ raise WorkerProtocolError("incompatible worker event schema")
125
+ kind = data["kind"]
126
+ if kind not in {"progress", "terminal", "failure"}:
127
+ raise WorkerProtocolError("unknown worker event kind")
128
+
129
+ identities = (
130
+ data["generation"],
131
+ data["bundle_fingerprint"],
132
+ data["transport_fingerprint"],
133
+ )
134
+ if all(item is None for item in identities):
135
+ if kind != "failure":
136
+ raise WorkerProtocolError("non-failure event lacks request identity")
137
+ generation = None
138
+ bundle_fingerprint = None
139
+ transport_fingerprint = None
140
+ elif any(item is None for item in identities):
141
+ raise WorkerProtocolError("worker event has partial request identity")
142
+ else:
143
+ generation = _generation(data["generation"])
144
+ bundle_fingerprint = _sha256(
145
+ data["bundle_fingerprint"], "bundle fingerprint"
146
+ )
147
+ transport_fingerprint = _sha256(
148
+ data["transport_fingerprint"], "transport fingerprint"
149
+ )
150
+
151
+ if expected is not None:
152
+ _validate_worker_request(expected)
153
+ expected_generation = expected.generation
154
+ expected_bundle_fingerprint = expected.bundle_fingerprint
155
+ expected_transport_fingerprint = expected.transport_fingerprint
156
+ if generation is not None:
157
+ if (
158
+ expected_generation is not None
159
+ and generation != _generation(expected_generation)
160
+ ):
161
+ raise WorkerProtocolError("worker event generation mismatch")
162
+ if (
163
+ expected_bundle_fingerprint is not None
164
+ and bundle_fingerprint
165
+ != _sha256(expected_bundle_fingerprint, "expected bundle fingerprint")
166
+ ):
167
+ raise WorkerProtocolError("worker event bundle fingerprint mismatch")
168
+ if (
169
+ expected_transport_fingerprint is not None
170
+ and transport_fingerprint
171
+ != _sha256(
172
+ expected_transport_fingerprint,
173
+ "expected transport fingerprint",
174
+ )
175
+ ):
176
+ raise WorkerProtocolError("worker event transport fingerprint mismatch")
177
+
178
+ progress: ConversionProgress | None = None
179
+ result: ConversionResult | None = None
180
+ mappings: tuple[dict[str, object], ...] = ()
181
+ failure_classification: str | None = None
182
+ message: str | None = None
183
+
184
+ if kind == "progress":
185
+ if any(
186
+ data[key] is not None
187
+ for key in ("result", "failure_classification", "message")
188
+ ) or data["mappings"] != []:
189
+ raise WorkerProtocolError("progress event contains terminal fields")
190
+ progress = _progress_from_data(data["progress"])
191
+ elif kind == "terminal":
192
+ if (
193
+ data["progress"] is not None
194
+ or data["failure_classification"] is not None
195
+ or data["message"] is not None
196
+ ):
197
+ raise WorkerProtocolError("terminal event contains non-terminal fields")
198
+ result = _result_from_data(data["result"])
199
+ mappings = _mappings_from_data(data["mappings"])
200
+ _validate_result_mappings(result, mappings)
201
+ else:
202
+ if (
203
+ data["progress"] is not None
204
+ or data["result"] is not None
205
+ or data["mappings"] != []
206
+ ):
207
+ raise WorkerProtocolError("failure event contains conversion fields")
208
+ failure_classification = data["failure_classification"]
209
+ message = data["message"]
210
+ if failure_classification not in FAILURE_CLASSIFICATIONS:
211
+ raise WorkerProtocolError("unknown worker failure classification")
212
+ if (
213
+ not isinstance(message, str)
214
+ or not message
215
+ or len(message) > MAX_FAILURE_MESSAGE_CHARS
216
+ or _contains_surrogate(message)
217
+ ):
218
+ raise WorkerProtocolError("worker failure message is malformed")
219
+
220
+ return WorkerEvent(
221
+ kind,
222
+ generation,
223
+ bundle_fingerprint,
224
+ transport_fingerprint,
225
+ progress,
226
+ result,
227
+ mappings,
228
+ failure_classification,
229
+ message,
230
+ )
231
+
232
+
233
+ def send_event(connection: ByteConnection, frame: bytes) -> None:
234
+ _send_frame(connection, frame, MAX_EVENT_BYTES, "event")
235
+
236
+
237
+ def receive_event(
238
+ connection: ByteConnection,
239
+ *,
240
+ expected: WorkerRequest | None = None,
241
+ ) -> WorkerEvent:
242
+ return decode_event(
243
+ _receive_frame(connection, MAX_EVENT_BYTES, "event"),
244
+ expected=expected,
245
+ )
246
+
247
+
248
+ def _encode_event(
249
+ request: WorkerRequest | None,
250
+ kind: str,
251
+ *,
252
+ progress: object = None,
253
+ result: object = None,
254
+ mappings: list[object] | None = None,
255
+ failure_classification: str | None = None,
256
+ message: str | None = None,
257
+ ) -> bytes:
258
+ if request is not None:
259
+ _validate_worker_request(request)
260
+ data = {
261
+ "schema": PROTOCOL_SCHEMA,
262
+ "kind": kind,
263
+ "generation": None if request is None else request.generation,
264
+ "bundle_fingerprint": (
265
+ None if request is None else request.bundle_fingerprint
266
+ ),
267
+ "transport_fingerprint": (
268
+ None if request is None else request.transport_fingerprint
269
+ ),
270
+ "progress": progress,
271
+ "result": result,
272
+ "mappings": [] if mappings is None else mappings,
273
+ "failure_classification": failure_classification,
274
+ "message": message,
275
+ }
276
+ return _encode_bounded(data, MAX_EVENT_BYTES, "event")
@@ -0,0 +1,197 @@
1
+ """Strict canonical-JSON parsing and byte-only connection framing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import re
8
+ from typing import Any
9
+
10
+ from pycforge.converter.core.fingerprint import canonical_json
11
+
12
+ from ._worker_protocol_types import (
13
+ MAX_JSON_DEPTH,
14
+ ByteConnection,
15
+ WorkerProtocolError,
16
+ )
17
+
18
+
19
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
20
+
21
+
22
+ def _send_frame(
23
+ connection: ByteConnection,
24
+ frame: bytes,
25
+ maximum: int,
26
+ role: str,
27
+ ) -> None:
28
+ if not isinstance(frame, bytes) or len(frame) > maximum:
29
+ raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
30
+ connection.send_bytes(frame)
31
+
32
+
33
+ def _receive_frame(
34
+ connection: ByteConnection,
35
+ maximum: int,
36
+ role: str,
37
+ ) -> bytes:
38
+ try:
39
+ frame = connection.recv_bytes(maximum)
40
+ except OSError as exc:
41
+ raise WorkerProtocolError(
42
+ f"worker {role} frame exceeds protocol bounds or is unreadable"
43
+ ) from exc
44
+ if not isinstance(frame, bytes) or len(frame) > maximum:
45
+ raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
46
+ return frame
47
+
48
+
49
+ def _encode_bounded(value: object, maximum: int, role: str) -> bytes:
50
+ try:
51
+ frame = canonical_json(value)
52
+ except (TypeError, ValueError, UnicodeError) as exc:
53
+ raise WorkerProtocolError(f"worker {role} is not canonical JSON") from exc
54
+ if len(frame) > maximum:
55
+ raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
56
+ return frame
57
+
58
+
59
+ def _decode_bounded(frame: bytes, maximum: int, role: str) -> object:
60
+ if not isinstance(frame, bytes) or len(frame) > maximum:
61
+ raise WorkerProtocolError(f"worker {role} frame exceeds protocol bounds")
62
+ try:
63
+ text = frame.decode("utf-8")
64
+ value = json.loads(
65
+ text,
66
+ object_pairs_hook=_unique_object,
67
+ parse_constant=_reject_constant,
68
+ parse_int=_bounded_json_int,
69
+ parse_float=_finite_json_float,
70
+ )
71
+ _validate_json_tree(value)
72
+ if canonical_json(value) != frame:
73
+ raise WorkerProtocolError(
74
+ f"worker {role} frame is not canonical JSON"
75
+ )
76
+ except WorkerProtocolError:
77
+ raise
78
+ except (UnicodeError, json.JSONDecodeError, TypeError, ValueError) as exc:
79
+ raise WorkerProtocolError(f"worker {role} frame is not valid JSON") from exc
80
+ return value
81
+
82
+
83
+ def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
84
+ result: dict[str, object] = {}
85
+ for key, value in pairs:
86
+ if key in result:
87
+ raise WorkerProtocolError("worker JSON contains duplicate object keys")
88
+ result[key] = value
89
+ return result
90
+
91
+
92
+ def _reject_constant(value: str) -> object:
93
+ raise WorkerProtocolError(f"worker JSON contains forbidden constant {value}")
94
+
95
+
96
+ def _bounded_json_int(value: str) -> int:
97
+ if len(value.lstrip("-")) > 19:
98
+ raise WorkerProtocolError("worker JSON integer exceeds protocol bounds")
99
+ return int(value)
100
+
101
+
102
+ def _finite_json_float(value: str) -> float:
103
+ result = float(value)
104
+ if not math.isfinite(result):
105
+ raise WorkerProtocolError("worker JSON number must be finite")
106
+ return result
107
+
108
+
109
+ def _validate_json_tree(value: object, depth: int = 0) -> None:
110
+ if depth > MAX_JSON_DEPTH:
111
+ raise WorkerProtocolError("worker JSON nesting exceeds protocol bounds")
112
+ if isinstance(value, str):
113
+ if _contains_surrogate(value):
114
+ raise WorkerProtocolError("worker JSON contains invalid Unicode")
115
+ return
116
+ if value is None or isinstance(value, (bool, int)):
117
+ return
118
+ if isinstance(value, float):
119
+ if not math.isfinite(value):
120
+ raise WorkerProtocolError("worker JSON number must be finite")
121
+ return
122
+ if isinstance(value, list):
123
+ for item in value:
124
+ _validate_json_tree(item, depth + 1)
125
+ return
126
+ if isinstance(value, dict):
127
+ for key, item in value.items():
128
+ if not isinstance(key, str) or _contains_surrogate(key):
129
+ raise WorkerProtocolError("worker JSON object key is invalid")
130
+ _validate_json_tree(item, depth + 1)
131
+ return
132
+ raise WorkerProtocolError("worker JSON contains unsupported value")
133
+
134
+
135
+ def _contains_surrogate(value: str) -> bool:
136
+ return any(0xD800 <= ord(character) <= 0xDFFF for character in value)
137
+
138
+
139
+ def _object(value: object, role: str) -> dict[str, Any]:
140
+ if not isinstance(value, dict):
141
+ raise WorkerProtocolError(f"{role} must be an object")
142
+ return value
143
+
144
+
145
+ def _optional_object(value: object, role: str) -> dict[str, Any] | None:
146
+ return None if value is None else _object(value, role)
147
+
148
+
149
+ def _list(value: object, role: str) -> list[Any]:
150
+ if not isinstance(value, list):
151
+ raise WorkerProtocolError(f"{role} must be an array")
152
+ return value
153
+
154
+
155
+ def _exact_fields(
156
+ value: dict[str, Any],
157
+ expected: set[str] | frozenset[str],
158
+ role: str,
159
+ ) -> None:
160
+ if set(value) != set(expected):
161
+ raise WorkerProtocolError(f"{role} fields are incomplete or unknown")
162
+
163
+
164
+ def _string(value: object, role: str) -> str:
165
+ if not isinstance(value, str) or _contains_surrogate(value):
166
+ raise WorkerProtocolError(f"{role} must be valid Unicode text")
167
+ return value
168
+
169
+
170
+ def _nonempty_string(value: object, role: str) -> str:
171
+ result = _string(value, role)
172
+ if not result:
173
+ raise WorkerProtocolError(f"{role} must be non-empty")
174
+ return result
175
+
176
+
177
+ def _string_list(value: object, role: str) -> tuple[str, ...]:
178
+ return tuple(_string(item, role) for item in _list(value, role))
179
+
180
+
181
+ def _nonnegative_integer(value: object, role: str) -> int:
182
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
183
+ raise WorkerProtocolError(f"{role} must be a non-negative integer")
184
+ return value
185
+
186
+
187
+ def _generation(value: object) -> int:
188
+ result = _nonnegative_integer(value, "worker generation")
189
+ if result == 0 or result > 2**63 - 1:
190
+ raise WorkerProtocolError("worker generation is outside protocol bounds")
191
+ return result
192
+
193
+
194
+ def _sha256(value: object, role: str) -> str:
195
+ if not isinstance(value, str) or not _SHA256.fullmatch(value):
196
+ raise WorkerProtocolError(f"{role} must be a lowercase SHA-256 value")
197
+ return value
@@ -0,0 +1,90 @@
1
+ """Shared immutable types and bounds for the worker protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Protocol
7
+
8
+ from pycforge.converter.core.progress import ConversionProgress
9
+ from pycforge.converter.core.request import ConversionRequest, ObservationOptions
10
+ from pycforge.converter.core.result import ConversionResult
11
+
12
+
13
+ PROTOCOL_SCHEMA = "pycforge.worker-protocol/0.1"
14
+ MAX_REQUEST_BYTES = 8 * 1024 * 1024
15
+ MAX_CONTROL_BYTES = 4 * 1024
16
+ MAX_EVENT_BYTES = 128 * 1024 * 1024
17
+ MAX_SOURCE_BYTES = 1_000_000
18
+ MAX_SOURCE_DOCUMENTS = 64
19
+ MAX_JSON_DEPTH = 256
20
+ MAX_DIAGNOSTICS = 1_000
21
+ MAX_FAILURE_MESSAGE_CHARS = 1_000
22
+
23
+ FAILURE_CLASSIFICATIONS = frozenset(
24
+ {
25
+ "protocol-error",
26
+ "request-transport-error",
27
+ "worker-resource-exhaustion",
28
+ "worker-internal-error",
29
+ }
30
+ )
31
+
32
+
33
+ class WorkerProtocolError(ValueError):
34
+ """A frame is malformed, incompatible, oversized, or fails custody checks."""
35
+
36
+
37
+ class ByteConnection(Protocol):
38
+ """The deliberately narrow subset of a multiprocessing connection we use."""
39
+
40
+ def send_bytes(self, buffer: bytes) -> None: ...
41
+
42
+ def recv_bytes(self, maxlength: int | None = None) -> bytes: ...
43
+
44
+ def close(self) -> None: ...
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class WorkerRequest:
49
+ generation: int
50
+ bundle_fingerprint: str
51
+ request: ConversionRequest
52
+ observation: ObservationOptions
53
+ transport_fingerprint: str
54
+
55
+ @classmethod
56
+ def create(
57
+ cls,
58
+ generation: int,
59
+ bundle_fingerprint: str,
60
+ request: ConversionRequest,
61
+ observation: ObservationOptions | None = None,
62
+ ) -> "WorkerRequest":
63
+ from ._worker_request_codec import create_worker_request
64
+
65
+ return create_worker_request(
66
+ generation,
67
+ bundle_fingerprint,
68
+ request,
69
+ observation,
70
+ )
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class WorkerControl:
75
+ generation: int
76
+ bundle_fingerprint: str
77
+ transport_fingerprint: str
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class WorkerEvent:
82
+ kind: str
83
+ generation: int | None
84
+ bundle_fingerprint: str | None
85
+ transport_fingerprint: str | None
86
+ progress: ConversionProgress | None = None
87
+ result: ConversionResult | None = None
88
+ mappings: tuple[dict[str, Any], ...] = ()
89
+ failure_classification: str | None = None
90
+ message: str | None = None