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,30 @@
1
+ # execution — run lifecycle, isolation, and verdicts
2
+
3
+ `execution/` provides run-state services and execution helpers used by the engine composition root. It covers admission/backpressure, solver process isolation, the Python execution approval surface, normalized evidence, and verdict evaluation. It contains no UI code and owns no solver-specific parsing.
4
+
5
+ ## Main pieces
6
+
7
+ - `service.py`: in-memory run and approval records, cancellation state, evidence/findings access, and verdict evaluation. Run records live for the lifetime of the `Engine` instance.
8
+ - `apig_pool.py`, `coalesce.py`, and `block_runs_scope.py`: bounded dispatch, duplicate-submit coalescing, and block-scoped run input handling.
9
+ - `runner.py`: subprocess helpers and `BwrapRunner` for OS-level tool isolation when the host supports the required sandbox.
10
+ - `replay_safe.py`: reports interrupted runs after restart and requires an explicit resume or discard decision; it does not silently re-execute survivors.
11
+ - `python_exec.py` and `console.py`: project-script/console request handling and approval-binding semantics. `python_execute` requests admission; the caller cannot approve its own code.
12
+ - `dispatch.py`: shared operation-call parsing and dispatch used by the CLI and console paths.
13
+
14
+ ## Run flow
15
+
16
+ `Engine.submit` validates and admits a solver operation, captures its project context, dispatches the selected adapter, and normally returns the completed result inline. `run_job` is available for deferred work, pool draining, batch pumping, and idempotent retrieval of an admitted run's completion. The two surfaces share the same run IDs and verdict rules.
17
+
18
+ Solver adapters return structured findings, measurements, and artifacts. The engine evaluates those results into `PASS`, `FAIL`, `NOT_RUN`, or `UNSUPPORTED`; a zero process exit does not establish `PASS`. Missing isolation support disables isolated work instead of falling back to an unrestricted host process.
19
+
20
+ ## Persistence boundary
21
+
22
+ The project ledger can persist when `engine_root` is configured, but `ExecutionService` run state and captured logs are in-memory per engine instance. The MCP launcher can write completed run evidence under the bound project's `results/engine-runs/`. These are separate lifecycles; do not assume every queue item or log survives an engine restart.
23
+
24
+ ## Routing
25
+
26
+ - Project revisions/events → [`project/`](../project/README.md).
27
+ - Result and command records → [`interfaces/`](../interfaces/README.md).
28
+ - Tool invocation and parsing → [`adapters/`](../adapters/README.md).
29
+ - Process ordering and the fully-verified gate → [`workflows/`](../workflows/README.md).
30
+ - Isolation and console contracts → [Python console spec](../../../PRD/tools-research/python-console-spec.md).
@@ -0,0 +1,150 @@
1
+ """Admission, queue, dispatch, isolated workers, cancel, evidence, evaluation. Owns execution."""
2
+
3
+ from .apig.pool import (
4
+ ApigPool,
5
+ pending_from_events,
6
+ request_for_run,
7
+ stored_completion_from_events,
8
+ )
9
+ from .apig.submit import SubmitImmediate, merge_ack_completion
10
+ from .block_runs_scope import (
11
+ BLOCK_RUN_OPERATIONS,
12
+ BLOCK_SCOPE,
13
+ PROJECT_SCOPE,
14
+ SUPPORTED_SCOPES,
15
+ BlockScopeError,
16
+ build_scope_record,
17
+ check_operation_scope,
18
+ closure_hash,
19
+ narrow_job_for_block,
20
+ parse_target,
21
+ resolve_block,
22
+ scope_provenance,
23
+ snapshot_closure,
24
+ stamp_verdict,
25
+ )
26
+ from .coalesce import (
27
+ Coalescer,
28
+ content_key,
29
+ deck_hash,
30
+ normalize_arguments,
31
+ )
32
+ from .console.apply_session import ConsoleSession, get_help
33
+ from .routing.calls import (
34
+ CONSOLE_TOOL_VERBS,
35
+ TIER_2_OPERATIONS,
36
+ admission_envelope,
37
+ build_effects_report,
38
+ collect_run_ids,
39
+ entity_doc_path,
40
+ format_address,
41
+ is_tier2,
42
+ parse_address,
43
+ report_completions,
44
+ stamp_tier2_call,
45
+ tier_for,
46
+ tool_call,
47
+ )
48
+ from .routing.namespace import ConsoleNamespace
49
+ from .routing.tools import ConsoleTools
50
+ from .dispatch import OpDispatcher, parse_op_call, parse_op_tokens
51
+ from .errors import ExecutionError, UsageError
52
+ from .python_exec import (
53
+ MCP_INPUT_SCHEMA,
54
+ PythonExecRunner,
55
+ PythonExecService,
56
+ build_worker_source,
57
+ not_run_verdict,
58
+ parse_exception,
59
+ prelude_digest_for,
60
+ resolve_script_path,
61
+ )
62
+ from .replay_safe import (
63
+ AMBIGUOUS,
64
+ POISON,
65
+ POISON_THRESHOLD,
66
+ REPLAY_SAFE_VERSION,
67
+ RETRYABLE,
68
+ ReplaySafe,
69
+ boot_message,
70
+ content_key_for,
71
+ interrupt_counts_from_events,
72
+ survivors_from_events,
73
+ user_message_for,
74
+ )
75
+ from .runner import BwrapRunner
76
+ from .service import ExecutionService
77
+ from .worker import CellRunner, CredentialGate, worker_profile
78
+
79
+ __all__ = [
80
+ "AMBIGUOUS",
81
+ "BLOCK_RUN_OPERATIONS",
82
+ "BLOCK_SCOPE",
83
+ "POISON",
84
+ "POISON_THRESHOLD",
85
+ "PROJECT_SCOPE",
86
+ "REPLAY_SAFE_VERSION",
87
+ "RETRYABLE",
88
+ "SUPPORTED_SCOPES",
89
+ "ApigPool",
90
+ "BlockScopeError",
91
+ "BwrapRunner",
92
+ "CellRunner",
93
+ "Coalescer",
94
+ "CONSOLE_TOOL_VERBS",
95
+ "ConsoleNamespace",
96
+ "ConsoleSession",
97
+ "ConsoleTools",
98
+ "CredentialGate",
99
+ "ExecutionError",
100
+ "ExecutionService",
101
+ "MCP_INPUT_SCHEMA",
102
+ "OpDispatcher",
103
+ "PythonExecRunner",
104
+ "PythonExecService",
105
+ "ReplaySafe",
106
+ "SubmitImmediate",
107
+ "TIER_2_OPERATIONS",
108
+ "UsageError",
109
+ "admission_envelope",
110
+ "boot_message",
111
+ "build_effects_report",
112
+ "build_scope_record",
113
+ "build_worker_source",
114
+ "check_operation_scope",
115
+ "closure_hash",
116
+ "collect_run_ids",
117
+ "content_key",
118
+ "deck_hash",
119
+ "content_key_for",
120
+ "entity_doc_path",
121
+ "format_address",
122
+ "get_help",
123
+ "interrupt_counts_from_events",
124
+ "is_tier2",
125
+ "merge_ack_completion",
126
+ "narrow_job_for_block",
127
+ "normalize_arguments",
128
+ "not_run_verdict",
129
+ "parse_address",
130
+ "parse_exception",
131
+ "parse_op_call",
132
+ "parse_op_tokens",
133
+ "prelude_digest_for",
134
+ "report_completions",
135
+ "resolve_script_path",
136
+ "parse_target",
137
+ "pending_from_events",
138
+ "request_for_run",
139
+ "resolve_block",
140
+ "scope_provenance",
141
+ "snapshot_closure",
142
+ "stamp_tier2_call",
143
+ "stamp_verdict",
144
+ "stored_completion_from_events",
145
+ "survivors_from_events",
146
+ "tier_for",
147
+ "tool_call",
148
+ "user_message_for",
149
+ "worker_profile",
150
+ ]
@@ -0,0 +1 @@
1
+ """Admission pool: backlog, backpressure, and submit-immediate."""
@@ -0,0 +1,463 @@
1
+ """Worker pool (API-GENERALIZE): in-order drain, backpressure, boot replay.
2
+
3
+ The durable request log (ProjectService) owns admission truth; the pool
4
+ owns execution order around it. `submit_request` delegates admission to
5
+ the ledger (same ack bytes, same `request_id` dedupe), then holds the
6
+ admitted run in a FIFO backlog until `drain`/`drain_ids` runs it through
7
+ the caller-supplied runner. The backlog is bounded: a full pool refuses
8
+ new admissions with a typed `pool_full` error BEFORE touching the
9
+ ledger, so a refusal never advances the revision and nothing is ever
10
+ silently dropped. Replays (same `request_id`, same text) return the
11
+ stored ack without re-enqueueing and are never refused, even when full.
12
+
13
+ Boot replay rebuilds the backlog from ledger events alone: `replay`
14
+ takes `job_queued` run ids in seq order, minus every run id with a
15
+ `run_completed`/`run_failed` event, resolves each survivor through the
16
+ caller, and drains them — so a kill mid-queue still completes every
17
+ request exactly once after reboot.
18
+ """
19
+
20
+ import threading
21
+ from concurrent.futures import ThreadPoolExecutor
22
+
23
+ from ruamel.yaml import YAML
24
+
25
+ try:
26
+ from ..errors import ExecutionError
27
+ except ImportError: # standalone verification before install
28
+ from rocket_engine.execution.errors import ExecutionError
29
+
30
+ POOL_VERSION = 1
31
+ DEFAULT_MAX_DEPTH = 64
32
+ DEFAULT_MAX_WORKERS = 4
33
+
34
+ _yaml = YAML(typ="safe")
35
+
36
+
37
+ def pending_from_events(events):
38
+ """Order admitted-but-uncompleted requests oldest first.
39
+
40
+ Args:
41
+ events: Ledger events with seq, event, request_id, and
42
+ data.run_id entries.
43
+
44
+ Returns:
45
+ (request_id, run_id) pairs for every `job_queued` event
46
+ without a later `run_completed`/`run_failed` event for
47
+ the same run id, in seq order.
48
+ """
49
+ queued = {}
50
+ order = []
51
+ completed = set()
52
+ for event in sorted(events, key=lambda e: e.get("seq", 0)):
53
+ data = event.get("data") or {}
54
+ run_id = data.get("run_id")
55
+ if run_id is None:
56
+ continue
57
+ if event.get("event") == "job_queued":
58
+ if run_id not in queued:
59
+ order.append(run_id)
60
+ queued[run_id] = event.get("request_id")
61
+ elif event.get("event") in ("run_completed", "run_failed"):
62
+ completed.add(run_id)
63
+ return [
64
+ (queued[run_id], run_id) for run_id in order if run_id not in completed
65
+ ]
66
+
67
+
68
+ def stored_completion_from_events(events, run_id):
69
+ """Read a run's stored completion from ledger events, if any.
70
+
71
+ Args:
72
+ events: Ledger events with event and data entries.
73
+ run_id: Run to look up.
74
+
75
+ Returns:
76
+ Completion mapping (run_id, completed state, verdict,
77
+ operation) from the run's `run_completed`/`run_failed`
78
+ event, or None when the run never completed.
79
+ """
80
+ for event in events:
81
+ if event.get("event") not in ("run_completed", "run_failed"):
82
+ continue
83
+ data = event.get("data") or {}
84
+ if data.get("run_id") != run_id:
85
+ continue
86
+ return {
87
+ "run_id": run_id,
88
+ "state": "completed",
89
+ "verdict": data.get("verdict"),
90
+ "operation": data.get("operation"),
91
+ }
92
+ return None
93
+
94
+
95
+ def request_for_run(events, run_id):
96
+ """Find the admitting request id for a run, if any.
97
+
98
+ Args:
99
+ events: Ledger events with event, request_id, and
100
+ data.run_id entries.
101
+ run_id: Run to trace back to its admission.
102
+
103
+ Returns:
104
+ Request id from the run's `job_queued` event, or None
105
+ when no admission event names the run.
106
+ """
107
+ for event in events:
108
+ if event.get("event") != "job_queued":
109
+ continue
110
+ data = event.get("data") or {}
111
+ if data.get("run_id") == run_id:
112
+ return event.get("request_id")
113
+ return None
114
+
115
+
116
+ class ApigPool:
117
+ """Bounded FIFO execution pool over a durable request ledger.
118
+
119
+ Args:
120
+ project: Project port with `submit(command_text,
121
+ request_id, expected_revision)`, `events_since(cursor)`,
122
+ and `command_for(request_id)` (ProjectService serves).
123
+ runner: Callable `(operation, arguments, snapshot_id,
124
+ request_id, run_id)` returning a completion mapping.
125
+ Runner exceptions are recorded as failed completions,
126
+ never raised, and the drain continues.
127
+ max_depth: Maximum undrained backlog; admissions past it
128
+ are refused with `pool_full`, never queued, never
129
+ dropped silently.
130
+ max_workers: Drain fan-out for independent items
131
+ (BATCH-PARALLEL). Reads and sims run concurrently;
132
+ same-entity mutations serialize upstream through
133
+ ENTITY-CAS. 1 keeps the legacy sequential drain.
134
+
135
+ Caution: The bound applies to new admissions only — replay
136
+ re-enqueues crash survivors past the bound rather than
137
+ abandoning admitted work.
138
+ """
139
+
140
+ def __init__(self, project, runner, max_depth=DEFAULT_MAX_DEPTH,
141
+ max_workers=1):
142
+ self._project = project
143
+ self._runner = runner
144
+ self._max_depth = max_depth
145
+ self._max_workers = max(1, int(max_workers))
146
+ self._lock = threading.Lock()
147
+ self._backlog = []
148
+ self._queued = {}
149
+ self._acks = {}
150
+ self._completions = {}
151
+ self._completed_runs = None
152
+ self._discarded = set()
153
+
154
+ @property
155
+ def max_depth(self):
156
+ """Backlog bound refused past, not queued past.
157
+
158
+ Returns:
159
+ Maximum undrained backlog.
160
+ """
161
+ return self._max_depth
162
+
163
+ @property
164
+ def depth(self):
165
+ """Current undrained backlog size.
166
+
167
+ Returns:
168
+ Number of admitted runs awaiting drain.
169
+ """
170
+ with self._lock:
171
+ return len(self._backlog)
172
+
173
+ def _seed_completed(self):
174
+ if self._completed_runs is None:
175
+ self._completed_runs = {
176
+ run_id
177
+ for _request_id, run_id in ()
178
+ }
179
+ for event in self._project.events_since(0):
180
+ if event.get("event") in ("run_completed", "run_failed"):
181
+ data = event.get("data") or {}
182
+ if data.get("run_id") is not None:
183
+ self._completed_runs.add(data["run_id"])
184
+
185
+ def submit_request(self, command_text, request_id, expected_revision=None):
186
+ """Admit one request through the ledger, then enqueue it.
187
+
188
+ Replays return the stored ack without re-enqueueing and are
189
+ never refused. A full pool raises before admission, so the
190
+ ledger never advances for a refused request.
191
+
192
+ Args:
193
+ command_text: YAML command mapping with an operation.
194
+ request_id: Client envelope id; resubmitting the
195
+ identical text returns the identical ack.
196
+ expected_revision: Revision the writer read.
197
+
198
+ Returns:
199
+ Admission ack with run_id, snapshot_id, queued state,
200
+ current revision, and event cursor.
201
+
202
+ Raises:
203
+ ExecutionError: pool_full when the undrained backlog
204
+ is at its bound — names the request, the depth,
205
+ and the bound.
206
+ ProjectError: invalid_yaml, revision_conflict, or
207
+ request_conflict from the ledger, untouched.
208
+ """
209
+ with self._lock:
210
+ if request_id in self._acks:
211
+ return dict(self._acks[request_id])
212
+ if len(self._backlog) >= self._max_depth:
213
+ raise ExecutionError(
214
+ "pool_full",
215
+ f"Pool saturated: {request_id} refused at "
216
+ f"depth {len(self._backlog)}/{self._max_depth}; "
217
+ "drain before submitting more.",
218
+ fields=[{"depth": len(self._backlog),
219
+ "max_depth": self._max_depth}],
220
+ )
221
+ ack = self._project.submit(
222
+ command_text, request_id, expected_revision=expected_revision
223
+ )
224
+ self._seed_completed()
225
+ command = _yaml.load(command_text)
226
+ with self._lock:
227
+ self._acks[request_id] = dict(ack)
228
+ if ack["run_id"] in self._completed_runs:
229
+ return dict(ack)
230
+ self._enqueue_locked(
231
+ request_id, ack["run_id"], command, ack["snapshot_id"]
232
+ )
233
+ return dict(ack)
234
+
235
+ def adopt(self, request_id, run_id, command, snapshot_id):
236
+ """Enqueue an already-admitted request without touching the ledger.
237
+
238
+ Batch pumping and boot replay admit through this: the
239
+ request was admitted before (its ack lives in the ledger),
240
+ so only the backlog bound and dedupe apply here.
241
+
242
+ Args:
243
+ request_id: Admitting envelope id.
244
+ run_id: Admitted run id.
245
+ command: Parsed command mapping with operation and
246
+ arguments.
247
+ snapshot_id: Evidence snapshot the run pins.
248
+
249
+ Returns:
250
+ True when the run was enqueued, False when it was
251
+ already queued or drained.
252
+
253
+ Raises:
254
+ ExecutionError: pool_full when the undrained backlog
255
+ is at its bound.
256
+ """
257
+ with self._lock:
258
+ if request_id in self._queued or request_id in self._completions:
259
+ return False
260
+ self._seed_completed()
261
+ if run_id in self._completed_runs:
262
+ return False
263
+ if len(self._backlog) >= self._max_depth:
264
+ raise ExecutionError(
265
+ "pool_full",
266
+ f"Pool saturated: {request_id} refused at "
267
+ f"depth {len(self._backlog)}/{self._max_depth}; "
268
+ "drain before submitting more.",
269
+ fields=[{"depth": len(self._backlog),
270
+ "max_depth": self._max_depth}],
271
+ )
272
+ self._enqueue_locked(request_id, run_id, command, snapshot_id)
273
+ return True
274
+
275
+ def _enqueue(self, request_id, run_id, command, snapshot_id):
276
+ with self._lock:
277
+ self._enqueue_locked(request_id, run_id, command, snapshot_id)
278
+
279
+ def _enqueue_locked(self, request_id, run_id, command, snapshot_id):
280
+ entry = {
281
+ "request_id": request_id,
282
+ "run_id": run_id,
283
+ "operation": command.get("operation"),
284
+ "arguments": dict(command.get("arguments") or {}),
285
+ "snapshot_id": snapshot_id,
286
+ }
287
+ self._backlog.append(entry)
288
+ self._queued[request_id] = entry
289
+
290
+ def pending(self):
291
+ """List undrained run ids, oldest first.
292
+
293
+ Returns:
294
+ Run ids in drain order.
295
+ """
296
+ with self._lock:
297
+ return [entry["run_id"] for entry in self._backlog]
298
+
299
+ def drain(self, limit=None):
300
+ """Run undrained entries oldest first.
301
+
302
+ Args:
303
+ limit: Maximum completions; the whole backlog when
304
+ omitted.
305
+
306
+ Returns:
307
+ Completion mappings in drain order. A raising runner
308
+ becomes a failed completion carrying the error,
309
+ and the drain continues past it.
310
+ """
311
+ with self._lock:
312
+ run_ids = [entry["run_id"] for entry in self._backlog]
313
+ if limit is not None:
314
+ run_ids = run_ids[: max(0, limit)]
315
+ return self.drain_ids(run_ids)
316
+
317
+ def discard(self, run_id):
318
+ """Drop one undrained backlog entry without running it.
319
+
320
+ Cancel-before-drain uses this: the admission stands in the
321
+ ledger, but the run never executes here. A later drain of
322
+ the same id returns a cancelled (NOT_RUN) completion; a
323
+ drain of an id never admitted still raises
324
+ unknown_request.
325
+
326
+ Args:
327
+ run_id: Backlog run id to drop.
328
+
329
+ Returns:
330
+ True when an entry was dropped, False when no backlog
331
+ entry names the id.
332
+ """
333
+ with self._lock:
334
+ for entry in self._backlog:
335
+ if entry["run_id"] == run_id:
336
+ self._backlog.remove(entry)
337
+ del self._queued[entry["request_id"]]
338
+ self._discarded.add(run_id)
339
+ return True
340
+ return False
341
+
342
+ def drain_ids(self, run_ids):
343
+ """Run named backlog entries in the given order.
344
+
345
+ With max_workers > 1 the runners overlap (BATCH-PARALLEL);
346
+ results still return in the given order.
347
+
348
+ Args:
349
+ run_ids: Backlog run ids to drain, in order.
350
+
351
+ Returns:
352
+ Completion mappings in the given order.
353
+
354
+ Raises:
355
+ ExecutionError: unknown_request for a run id with no
356
+ backlog entry.
357
+ """
358
+ cancelled_ids = []
359
+ with self._lock:
360
+ entries = []
361
+ for run_id in run_ids:
362
+ entry = None
363
+ for candidate in self._backlog:
364
+ if candidate["run_id"] == run_id:
365
+ entry = candidate
366
+ break
367
+ if entry is None:
368
+ if run_id in self._discarded:
369
+ cancelled_ids.append(run_id)
370
+ continue
371
+ raise ExecutionError(
372
+ "unknown_request", f"No queued request: {run_id}."
373
+ )
374
+ self._backlog.remove(entry)
375
+ del self._queued[entry["request_id"]]
376
+ entries.append(entry)
377
+ by_id = {}
378
+ for run_id in cancelled_ids:
379
+ by_id[run_id] = {
380
+ "run_id": run_id,
381
+ "state": "cancelled",
382
+ "reason": "cancelled before dispatch",
383
+ "verdict": {
384
+ "status": "NOT_RUN",
385
+ "reason": "cancelled before dispatch",
386
+ },
387
+ }
388
+ if entries:
389
+ if self._max_workers <= 1 or len(entries) <= 1:
390
+ drained = [self._drain_one(entry) for entry in entries]
391
+ else:
392
+ with ThreadPoolExecutor(
393
+ max_workers=min(self._max_workers, len(entries)),
394
+ thread_name_prefix="apig-drain",
395
+ ) as pool:
396
+ drained = list(pool.map(self._drain_one, entries))
397
+ for entry, completion in zip(entries, drained):
398
+ by_id.setdefault(entry["run_id"], completion)
399
+ return [by_id[run_id] for run_id in run_ids]
400
+
401
+ def _drain_one(self, entry):
402
+ """Run one backlog entry, recording its completion."""
403
+ try:
404
+ completion = self._runner(
405
+ entry["operation"],
406
+ dict(entry["arguments"]),
407
+ entry["snapshot_id"],
408
+ entry["request_id"],
409
+ entry["run_id"],
410
+ )
411
+ except Exception as exc: # noqa: BLE001 — recorded, never raised
412
+ code = getattr(exc, "code", type(exc).__name__)
413
+ completion = {
414
+ "run_id": entry["run_id"],
415
+ "state": "failed",
416
+ "error": f"{code}: {exc}",
417
+ "operation": entry["operation"],
418
+ }
419
+ with self._lock:
420
+ self._completions[entry["request_id"]] = completion
421
+ self._seed_completed()
422
+ self._completed_runs.add(entry["run_id"])
423
+ return completion
424
+
425
+ def replay(self, events, resolve):
426
+ """Re-enqueue crash survivors from ledger events, then drain them.
427
+
428
+ Args:
429
+ events: Ledger events; survivors are `job_queued` run
430
+ ids without a `run_completed`/`run_failed` event.
431
+ resolve: Callable `request_id -> (command, ack)` for
432
+ each survivor (ProjectService.command_for serves).
433
+
434
+ Returns:
435
+ Report with replayed run ids (seq order), their
436
+ completions, and skipped entries (each with
437
+ request_id, run_id, and reason) for survivors the
438
+ resolver cannot serve. Skips are reported, never
439
+ silent, and the drain continues past them.
440
+ """
441
+ replayed = []
442
+ skipped = []
443
+ for request_id, run_id in pending_from_events(events):
444
+ if request_id in self._queued or request_id in self._completions:
445
+ continue
446
+ try:
447
+ command, ack = resolve(request_id)
448
+ except Exception as exc: # noqa: BLE001 — reported per request
449
+ skipped.append({
450
+ "request_id": request_id,
451
+ "run_id": run_id,
452
+ "reason": f"{getattr(exc, 'code', type(exc).__name__)}: {exc}",
453
+ })
454
+ continue
455
+ self._acks[request_id] = dict(ack)
456
+ self._enqueue(request_id, run_id, command, ack["snapshot_id"])
457
+ replayed.append(run_id)
458
+ completions = self.drain_ids(replayed)
459
+ return {
460
+ "replayed": replayed,
461
+ "completions": completions,
462
+ "skipped": skipped,
463
+ }