rocket-engine 0.0.2b1__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 (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,553 @@
1
+ """Engine ops mixin: Run pumping, batching, replay and solver execution."""
2
+
3
+ from ..adapters.errors import AdapterError
4
+ from ..execution.apig.pool import request_for_run
5
+ from ..execution.apig.submit import merge_ack_completion
6
+ from ..execution.block_runs_scope import (build_scope_record, narrow_job_for_block, stamp_verdict)
7
+ from ..execution.errors import ExecutionError
8
+ from ..execution.replay_safe import content_key_for
9
+ from ..interfaces.yaml_dump import dump_yaml_text
10
+ from ..project.errors import ProjectError
11
+ from .gates import check_submit_gates
12
+
13
+ #: Per-item state rank for the batch worst-state-wins summary —
14
+ #: a failed run outranks a cancelled one, which outranks a refused
15
+ #: (never attempted) one, which outranks a clean completion.
16
+ _STATE_RANK = {"failed": 3, "cancelled": 2, "refused": 1, "completed": 0}
17
+
18
+
19
+ def refuse_smuggled_budget(arguments):
20
+ """Refuse a root-level budget smuggled past the frozen request.
21
+
22
+ The budget travels in the frozen `run.request` only, the way
23
+ caller min/max targets never ride the call. Admission
24
+ refuses it fail-fast; execution stamps the frozen value
25
+ itself, so the stored command never carries a root budget.
26
+
27
+ Args:
28
+ arguments: Solver arguments to inspect.
29
+
30
+ Raises:
31
+ ProjectError: invalid_command for a root-level
32
+ `timeout_s`.
33
+ """
34
+ if "timeout_s" in arguments:
35
+ raise ProjectError(
36
+ "invalid_command",
37
+ "Budget comes from the frozen run.request timeout_s, "
38
+ "not the job root: remove timeout_s.",
39
+ )
40
+
41
+
42
+ def stamp_solver_budget(arguments):
43
+ """Refuse a smuggled root budget; stamp the frozen one.
44
+
45
+ The budget travels in the frozen `run.request` (None means
46
+ "adapter default": a spice sim keeps 120s, a LibreLane flow
47
+ keeps its hour). Called at execution: admission already
48
+ refused smuggling, so the stored command carries none and
49
+ the stamp lands exactly once.
50
+
51
+ Args:
52
+ arguments: Solver arguments with a normalized `request`.
53
+
54
+ Returns:
55
+ Arguments with the frozen budget stamped as the job
56
+ `timeout_s`, or untouched when the request keeps
57
+ the adapter default.
58
+
59
+ Raises:
60
+ ProjectError: invalid_command for a root-level
61
+ `timeout_s`.
62
+ """
63
+ refuse_smuggled_budget(arguments)
64
+ budget = (arguments.get("request") or {}).get("timeout_s")
65
+ if budget is None:
66
+ return arguments
67
+ return {**arguments, "timeout_s": budget}
68
+
69
+
70
+ def _item_state(completion):
71
+ """Batch state for one completion: verdict FAIL reads failed."""
72
+ verdict = (completion or {}).get("verdict") or {}
73
+ if verdict.get("status") == "FAIL":
74
+ return "failed"
75
+ return completion.get("state", "completed")
76
+
77
+
78
+ class _RunMixin:
79
+ """Run pumping, batching, replay and solver execution."""
80
+
81
+ def _await_shared(self, ack, run_id, waiter):
82
+ """Resolve a coalesced envelope to the shared completion.
83
+
84
+ The waiter delivers the owner's completion; a run evicted
85
+ just before attach (or discarded when its drain raised)
86
+ falls back to the stored completion — the run event
87
+ precedes eviction, so fetch cannot miss a finished run.
88
+ """
89
+ completion = None
90
+ if waiter is not None:
91
+ completion = self._coalescer.await_completion(waiter)
92
+ if completion is None:
93
+ completion = self._stored_completion(run_id)
94
+ if completion is None:
95
+ raise ExecutionError(
96
+ "coalesced_run_lost",
97
+ f"Shared run finished with no completion: {run_id}.",
98
+ )
99
+ return merge_ack_completion(ack, completion)
100
+
101
+ def _resolve_pending(self, request_id):
102
+ """Admitted (command, ack) for a queued request, registered.
103
+
104
+ Reboot path: unknown execution records are registered and
105
+ the run job stashed, so the pool runner finds both.
106
+ """
107
+ stored_command, ack = self._project.command_for(request_id)
108
+ run_id = ack.get("run_id")
109
+ if not self._execution.known(run_id):
110
+ self._execution.dispatch(
111
+ stored_command.get("operation"),
112
+ {"cell": (stored_command.get("arguments") or {}).get("cell")},
113
+ ack.get("snapshot_id", ""),
114
+ run_id=run_id,
115
+ )
116
+ self._run_jobs.setdefault(
117
+ run_id,
118
+ {
119
+ "operation": stored_command.get("operation"),
120
+ "arguments": dict(stored_command.get("arguments") or {}),
121
+ "snapshot_id": ack.get("snapshot_id", ""),
122
+ "request_id": str(request_id),
123
+ },
124
+ )
125
+ return stored_command, ack
126
+
127
+ def _pool_execute(self, operation, arguments, snapshot_id, request_id,
128
+ run_id):
129
+ """Execute one pooled run through its solver, contained."""
130
+ try:
131
+ cancelled = (
132
+ self._execution.status(run_id)["state"] == "cancelled"
133
+ )
134
+ except ExecutionError:
135
+ cancelled = False
136
+ if cancelled:
137
+ verdict = {
138
+ "status": "NOT_RUN",
139
+ "reason": "cancelled before dispatch",
140
+ }
141
+ self._project.append_run_event(
142
+ "run_failed", run_id, operation, verdict, request_id
143
+ )
144
+ return {
145
+ "run_id": run_id,
146
+ "state": "cancelled",
147
+ "operation": operation,
148
+ "reason": "cancelled before dispatch",
149
+ "verdict": verdict,
150
+ }
151
+ if not self._solvers.get(operation):
152
+ self._latest[operation] = run_id
153
+ self._logs[run_id] = (
154
+ f"unsupported_operation: No solver registered: {operation}."
155
+ )
156
+ verdict = self._execution.record_not_run(
157
+ run_id, self._logs[run_id]
158
+ )
159
+ self._project.append_run_event(
160
+ "run_failed", run_id, operation, verdict, request_id
161
+ )
162
+ return {
163
+ "run_id": run_id,
164
+ "state": "completed",
165
+ "verdict": verdict,
166
+ "operation": operation,
167
+ }
168
+ run_arguments = dict(arguments)
169
+ run_arguments.pop("expected_revision", None)
170
+ # REPLAY-SAFE: a kill after this mark reads as an
171
+ # interruption at the next boot (AMBIGUOUS, counted toward
172
+ # poison); a kill before it stays RETRYABLE.
173
+ self._project.append_replay_event(
174
+ "run_started",
175
+ request_id,
176
+ {
177
+ "run_id": run_id,
178
+ "content_key": content_key_for(operation, run_arguments),
179
+ "operation": operation,
180
+ },
181
+ )
182
+ return self._run_operation(
183
+ operation,
184
+ run_arguments,
185
+ snapshot_id,
186
+ request_id,
187
+ run_id=run_id,
188
+ contain=True,
189
+ )
190
+
191
+ def _run_batch(self, run_ids, force=False):
192
+ """Pump a batch of admitted runs oldest-listed first.
193
+
194
+ Finished runs contribute their stored completion without
195
+ re-executing; runs cancelled before drain contribute a
196
+ cancelled completion without running; the rest are
197
+ admitted to the pool (pool_full refuses loudly past the
198
+ bound), gate-rechecked at drain time (a verdict that moved
199
+ since submit refuses loud), and drained — concurrently
200
+ when the pool fans out. Every listed id lands in exactly
201
+ one of completions/refused, and states/summary roll the
202
+ per-item outcomes up worst-first.
203
+
204
+ Args:
205
+ run_ids: Admitted run ids to pump, in order.
206
+ force: Bypass drain-time verdict gates only (hard deps
207
+ still refuse).
208
+ """
209
+ if not isinstance(run_ids, list) or not run_ids:
210
+ raise ProjectError(
211
+ "invalid_command",
212
+ "run_job batch needs a non-empty run_ids list.",
213
+ )
214
+ completions = []
215
+ refused = []
216
+ seen = set()
217
+ admitted = []
218
+ for run_id in run_ids:
219
+ if run_id in seen:
220
+ refused.append({
221
+ "run_id": run_id,
222
+ "code": "duplicate_in_batch",
223
+ "message": f"Run listed twice in one batch: {run_id}.",
224
+ })
225
+ continue
226
+ seen.add(run_id)
227
+ stored = self._stored_completion(run_id)
228
+ if stored is not None:
229
+ completions.append(stored)
230
+ continue
231
+ pending = self._run_jobs.get(run_id)
232
+ if pending is not None:
233
+ request_id = pending["request_id"]
234
+ else:
235
+ request_id = request_for_run(
236
+ self._project.events_since(0), run_id
237
+ )
238
+ if request_id is None:
239
+ refused.append({
240
+ "run_id": run_id,
241
+ "code": "unknown_request",
242
+ "message": f"No queued request: {run_id}.",
243
+ })
244
+ continue
245
+ stored_command, ack = self._project.command_for(request_id)
246
+ operation = stored_command.get("operation")
247
+ if not self._solvers.get(operation):
248
+ refused.append({
249
+ "run_id": run_id,
250
+ "code": "unsupported_operation",
251
+ "message": f"No solver registered: {operation}.",
252
+ })
253
+ continue
254
+ if self._is_cancelled(run_id):
255
+ completions.append(
256
+ self._record_cancelled_skip(
257
+ run_id, operation, stored_command, request_id
258
+ )
259
+ )
260
+ continue
261
+ try:
262
+ check_submit_gates(
263
+ operation,
264
+ stored_command.get("arguments") or {},
265
+ self._project,
266
+ force=force,
267
+ )
268
+ except ProjectError as exc:
269
+ refused.append({
270
+ "run_id": run_id,
271
+ "code": exc.code,
272
+ "message": str(exc),
273
+ })
274
+ continue
275
+ self._resolve_pending(request_id)
276
+ try:
277
+ adopted = self._pool.adopt(
278
+ request_id, run_id, stored_command,
279
+ ack.get("snapshot_id", ""),
280
+ )
281
+ except ExecutionError as exc:
282
+ refused.append({
283
+ "run_id": run_id,
284
+ "code": exc.code,
285
+ "message": str(exc),
286
+ })
287
+ continue
288
+ if not adopted:
289
+ stored = self._stored_completion(run_id)
290
+ if stored is not None:
291
+ completions.append(stored)
292
+ continue
293
+ admitted.append(run_id)
294
+ if admitted:
295
+ completions.extend(self._pool.drain_ids(admitted))
296
+ states = {}
297
+ for completion in completions:
298
+ states[completion.get("run_id")] = _item_state(completion)
299
+ for entry in refused:
300
+ states[entry.get("run_id")] = "refused"
301
+ summary = "completed"
302
+ for state in states.values():
303
+ if _STATE_RANK.get(state, 0) > _STATE_RANK.get(summary, 0):
304
+ summary = state
305
+ return {
306
+ "run_ids": list(run_ids),
307
+ "completions": completions,
308
+ "refused": refused,
309
+ "states": states,
310
+ "summary": summary,
311
+ }
312
+
313
+ def _is_cancelled(self, run_id):
314
+ """True when the run was cancelled before this drain."""
315
+ try:
316
+ return self._execution.status(run_id)["state"] == "cancelled"
317
+ except ExecutionError:
318
+ return False
319
+
320
+ def _record_cancelled_skip(self, run_id, operation, stored_command,
321
+ request_id):
322
+ """Record a cancel-before-drain skip as a terminal NOT_RUN."""
323
+ verdict = {
324
+ "status": "NOT_RUN",
325
+ "reason": "cancelled before dispatch",
326
+ }
327
+ self._project.append_run_event(
328
+ "run_failed", run_id, operation, verdict, request_id
329
+ )
330
+ return {
331
+ "run_id": run_id,
332
+ "state": "cancelled",
333
+ "operation": operation,
334
+ "reason": "cancelled before dispatch",
335
+ "verdict": verdict,
336
+ }
337
+
338
+ def _replay_once(self):
339
+ """Build the crash-survivor boot report once per Engine."""
340
+ if self._replayed:
341
+ return None
342
+ self._replayed = True
343
+ return self.boot_report()
344
+
345
+ def _replay_execute(self, operation, arguments, snapshot_id, request_id,
346
+ run_id):
347
+ """Registered pool execution for one explicit resume."""
348
+ self._resolve_pending(request_id)
349
+ return self._pool_execute(
350
+ operation, arguments, snapshot_id, request_id, run_id
351
+ )
352
+
353
+ def _run_pyexec(self, run_id, request_id):
354
+ """Execute an admitted python_execute run (phase 2).
355
+
356
+ Approval lapses propagate as approval errors — approval
357
+ state is never folded into a verdict. Completion carries
358
+ the NOT_RUN verdict (zero exit is never check PASS) and
359
+ appends the run_failed ledger event the NOT_RUN shape
360
+ owns; cancellation returns the cancelled state with no
361
+ ledger event, like every cancelled job.
362
+
363
+ Args:
364
+ run_id: Admitted execution id (the submit ack id).
365
+ request_id: Queued request this run discharges.
366
+
367
+ Returns:
368
+ Completion mapping with run_id, state, verdict, and
369
+ operation.
370
+
371
+ Raises:
372
+ ExecutionError: unknown_run for an id never admitted,
373
+ approval_pending/approval_denied/approval_expired
374
+ when the approval lapses, approval_invalid for
375
+ binding drift.
376
+
377
+ Caution: Runs the approved worker now — read the verdict
378
+ (always NOT_RUN) plus the execution record, never the
379
+ exit path.
380
+ """
381
+ record = self._pyexec.run_job(run_id)
382
+ self._latest["python_execute"] = run_id
383
+ log = record.get("stdout", "")
384
+ if record.get("stderr"):
385
+ log += "\n--- stderr ---\n" + record["stderr"]
386
+ self._logs[run_id] = log
387
+ if record["state"] == "cancelled":
388
+ return {
389
+ "run_id": run_id,
390
+ "state": "cancelled",
391
+ "verdict": record["verdict"],
392
+ "operation": "python_execute",
393
+ }
394
+ self._project.append_run_event(
395
+ "run_failed", run_id, "python_execute", record["verdict"],
396
+ request_id,
397
+ )
398
+ return {
399
+ "run_id": run_id,
400
+ "state": "completed",
401
+ "verdict": record["verdict"],
402
+ "operation": "python_execute",
403
+ }
404
+
405
+ def _run_operation(self, name, arguments, snapshot_id, request_id, run_id=None,
406
+ contain=False, block_scope=None):
407
+ """Select the solver, run it, judge the verdict, log completion."""
408
+ from ..composition import _LOCATION_KEYS # deferred: breaks the ops<->composition cycle
409
+ solvers = self._solvers.get(name) or {}
410
+ if not solvers:
411
+ raise ProjectError(
412
+ "unsupported_operation", f"No solver registered: {name}."
413
+ )
414
+ arguments = stamp_solver_budget(dict(arguments))
415
+ try:
416
+ adapter = self._select_solver(name, arguments)
417
+ except ProjectError as exc:
418
+ if not contain:
419
+ raise
420
+ self._latest[name] = run_id
421
+ self._logs[run_id] = f"{exc.code}: {exc}"
422
+ verdict = self._execution.record_not_run(run_id, f"{exc.code}: {exc}")
423
+ stored_scope = self._run_jobs.get(run_id, {}).get("block_scope")
424
+ if stored_scope is not None:
425
+ verdict = stamp_verdict(verdict, stored_scope["provenance"])
426
+ self._execution.mark_completed(run_id, verdict)
427
+ self._project.append_run_event(
428
+ "run_failed", run_id, name, verdict, request_id
429
+ )
430
+ return {
431
+ "run_id": run_id,
432
+ "state": "completed",
433
+ "verdict": verdict,
434
+ "operation": name,
435
+ }
436
+ if run_id is None:
437
+ issued = self._execution.dispatch(
438
+ name, {"cell": arguments.get("cell")}, snapshot_id
439
+ )
440
+ run_id = issued["run_id"]
441
+ else:
442
+ # Reuses the submit-time record: unknown ids fail as unknown_run.
443
+ self._execution.status(run_id)
444
+ self._execution.mark_running(run_id)
445
+ self._latest[name] = run_id
446
+ scope_record = block_scope
447
+ if scope_record is None:
448
+ scope_record = self._run_jobs.get(run_id, {}).get("block_scope")
449
+ elif "provenance" not in scope_record:
450
+ # Direct runs admit the closure without a run_id; bind it now.
451
+ scope_record = build_scope_record(
452
+ run_id=run_id,
453
+ revision=self._project.snapshot()["current_revision"],
454
+ block_id=scope_record["block_id"],
455
+ deck=arguments.get("deck"),
456
+ closure=scope_record["closure"],
457
+ )
458
+ if scope_record is not None:
459
+ arguments = narrow_job_for_block(arguments, scope_record["closure"])
460
+ try:
461
+ adapter.preflight({"operation": name, **arguments})
462
+ native = adapter.execute({"operation": name, **arguments})
463
+ result = adapter.parse(native)
464
+ except AdapterError as exc:
465
+ self._logs[run_id] = f"{exc.code}: {exc}"
466
+ verdict = self._execution.record_not_run(run_id, f"{exc.code}: {exc}")
467
+ if scope_record is not None:
468
+ verdict = stamp_verdict(verdict, scope_record["provenance"])
469
+ self._execution.mark_completed(run_id, verdict)
470
+ self._project.append_run_event(
471
+ "run_failed", run_id, name, verdict, request_id
472
+ )
473
+ return {
474
+ "run_id": run_id,
475
+ "state": "completed",
476
+ "verdict": verdict,
477
+ "operation": name,
478
+ }
479
+ except Exception as exc:
480
+ # A solver bug in a queued job is NOT_RUN with a reason,
481
+ # never a crash and never a verdict. Direct runs re-raise.
482
+ if not contain:
483
+ raise
484
+ self._logs[run_id] = f"internal_error: {exc!r}"
485
+ verdict = self._execution.record_not_run(run_id, f"internal_error: {exc}")
486
+ if scope_record is not None:
487
+ verdict = stamp_verdict(verdict, scope_record["provenance"])
488
+ self._execution.mark_completed(run_id, verdict)
489
+ self._project.append_run_event(
490
+ "run_failed", run_id, name, verdict, request_id
491
+ )
492
+ return {
493
+ "run_id": run_id,
494
+ "state": "completed",
495
+ "verdict": verdict,
496
+ "operation": name,
497
+ }
498
+ self._logs[run_id] = native.get("log") or "\n".join(
499
+ part for part in (native.get("stdout"), native.get("stderr")) if part
500
+ )
501
+ self._datasets[run_id] = list(native.get("waveforms") or [])
502
+ self._spectra[run_id] = list(result.get("spectra") or [])
503
+ # Solver convention (see ToolAdapter.parse): violations carry
504
+ # rule/severity/message plus flat location keys, nested here.
505
+ findings = []
506
+ for violation in result.get("violations", []):
507
+ location = {}
508
+ for source, dest in _LOCATION_KEYS:
509
+ if violation.get(source) is not None:
510
+ location[dest] = violation[source]
511
+ findings.append(
512
+ {
513
+ "severity": violation.get("severity", "error"),
514
+ "rule": violation["rule"],
515
+ "message": violation.get("message", violation["rule"]),
516
+ "location": location,
517
+ }
518
+ )
519
+ findings_text = dump_yaml_text({"findings": findings})
520
+ verdict = self._execution.evaluate(
521
+ run_id, native.get("exit_code", 0), findings_text
522
+ )
523
+ # Keep measured values on PASS too: a verdict without numbers can't be reviewed.
524
+ if result.get("measurements") is not None:
525
+ verdict = dict(verdict, measurements=result["measurements"])
526
+ self._execution.mark_completed(run_id, verdict)
527
+ # Post-layout evidence rides the same way: the pre/post delta
528
+ # table plus the extraction/schematic provenance are small and
529
+ # reviewable (raw waveform samples stay adapter-level so the
530
+ # ledger never carries bulk vectors).
531
+ if result.get("comparison") is not None:
532
+ verdict = dict(verdict, comparison=result["comparison"])
533
+ self._execution.mark_completed(run_id, verdict)
534
+ if result.get("provenance") is not None:
535
+ verdict = dict(verdict, provenance=result["provenance"])
536
+ self._execution.mark_completed(run_id, verdict)
537
+ if scope_record is not None:
538
+ verdict = stamp_verdict(verdict, scope_record["provenance"])
539
+ self._execution.mark_completed(run_id, verdict)
540
+ if (verdict or {}).get("status") == "NOT_RUN":
541
+ outcome = "run_failed"
542
+ else:
543
+ outcome = "run_completed"
544
+ self._project.append_run_event(
545
+ outcome, run_id, name, verdict, request_id,
546
+ findings=findings, log=self._logs.get(run_id, ""),
547
+ )
548
+ return {
549
+ "run_id": run_id,
550
+ "state": "completed",
551
+ "verdict": verdict,
552
+ "operation": name,
553
+ }