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,364 @@
1
+ """Tier-2 call shapes, effects reports, and addressing."""
2
+
3
+ import io
4
+ import uuid
5
+
6
+ from ruamel.yaml import YAML
7
+
8
+ from ...stores import ecs
9
+ from ..errors import ExecutionError
10
+
11
+
12
+ #: Doc writes: the CRUD document verbs, newest revision wins by gate.
13
+ DOC_VERBS = ("apply_mutations", "delete_doc", "write_doc")
14
+
15
+ #: Cell add/place: catalogue instances landing as component docs.
16
+ CELL_VERBS = ("place_cell",)
17
+
18
+ #: Entity store verbs (ENGINE-ECS): components over projected docs.
19
+ ECS_VERBS = (
20
+ "attach_component",
21
+ "create_entity",
22
+ "delete_entity",
23
+ "detach_component",
24
+ "set_component",
25
+ )
26
+
27
+ #: Wire/junction verbs (WIRE-PLACE): connectivity as durable data.
28
+ WIRE_VERBS = (
29
+ "connect_endpoint",
30
+ "delete_junction",
31
+ "delete_wire",
32
+ "materialize_nets",
33
+ "move_wire",
34
+ "place_junction",
35
+ "place_wire",
36
+ "split_wire",
37
+ )
38
+
39
+ #: Sibling project mutations sharing the identical admission path.
40
+ PROJECT_VERBS = ("create_project", "rewind", "write_settings")
41
+
42
+ #: Every console tier-2 operation: project-state mutations admitted
43
+ #: through the ledger. Reads, solver runs, and unknown names stay
44
+ #: tier-1 (dispatch and the vocabulary gate own those).
45
+ TIER_2_OPERATIONS = frozenset(
46
+ list(DOC_VERBS) + list(CELL_VERBS) + list(ECS_VERBS)
47
+ + list(WIRE_VERBS) + list(PROJECT_VERBS)
48
+ )
49
+
50
+ #: Console namespace verbs, sorted: one wrapper per name on both
51
+ #: ConsoleNamespace (host-side admission) and ConsoleTools
52
+ #: (worker-side recording).
53
+ CONSOLE_TOOL_VERBS = tuple(sorted(TIER_2_OPERATIONS))
54
+
55
+
56
+ def tier_for(operation):
57
+ """Classify one operation into its console tier.
58
+
59
+ Args:
60
+ operation: Operation name to classify (any value tolerated).
61
+
62
+ Returns:
63
+ 2 for project-state mutations routing through admission, 1
64
+ for reads, solver runs, unknown names, and non-text.
65
+ """
66
+ if isinstance(operation, str) and operation in TIER_2_OPERATIONS:
67
+ return 2
68
+ return 1
69
+
70
+
71
+ def is_tier2(operation):
72
+ """True when an operation is a tier-2 admitted mutation.
73
+
74
+ Args:
75
+ operation: Operation name to test (any value tolerated).
76
+
77
+ Returns:
78
+ True for tier-2 verbs, False for everything else.
79
+ """
80
+ return tier_for(operation) == 2
81
+
82
+
83
+ def admission_envelope(operation, arguments):
84
+ """Render one admission identity text for a tier-2 call.
85
+
86
+ The text carries the operation plus its arguments through the
87
+ same YAML dumper the Engine uses, so identical calls admit with
88
+ identical identity and resubmits replay instead of duplicating.
89
+
90
+ Args:
91
+ operation: Tier-2 operation name for the envelope.
92
+ arguments: Operation arguments carried on the envelope.
93
+
94
+ Returns:
95
+ YAML command text identifying this exact call.
96
+
97
+ Raises:
98
+ ExecutionError: invalid_command for a non-text operation,
99
+ non-mapping arguments, or values no YAML dumper spans.
100
+ """
101
+ if not isinstance(operation, str) or not operation:
102
+ raise ExecutionError(
103
+ "invalid_command",
104
+ f"Envelope needs a text operation, got {operation!r}.",
105
+ )
106
+ if not isinstance(arguments, dict):
107
+ raise ExecutionError(
108
+ "invalid_command",
109
+ f"Envelope needs mapping arguments, got {arguments!r}.",
110
+ )
111
+ # Fresh dumper per call: a failed ruamel dump poisons the
112
+ # instance (it then emits empty text forever), which would
113
+ # collapse distinct calls onto one identity.
114
+ buf = io.StringIO()
115
+ try:
116
+ YAML(typ="safe").dump(
117
+ {"operation": operation, "arguments": arguments}, buf
118
+ )
119
+ except Exception as exc:
120
+ raise ExecutionError(
121
+ "invalid_command",
122
+ f"Envelope arguments do not serialize: {exc}.",
123
+ ) from exc
124
+ return buf.getvalue()
125
+
126
+
127
+ def stamp_tier2_call(operation, arguments, expected_revision,
128
+ request_id=None):
129
+ """Bind live admission identity onto one recorded tier-2 call.
130
+
131
+ Console replay stamps what the cell left open: a tier-2 call
132
+ without a caller binding gains the live head revision, and a
133
+ call without an envelope id gains a fresh one. Caller bindings
134
+ win verbatim — a cell that pinned its own revision replays at
135
+ that revision and fails loudly when it drifted. Tier-1 calls
136
+ pass through untouched.
137
+
138
+ Args:
139
+ operation: Recorded operation name.
140
+ arguments: Recorded arguments mapping.
141
+ expected_revision: Live head revision to bind when the
142
+ caller bound none.
143
+ request_id: Envelope id to keep, or None for a fresh one
144
+ on tier-2 calls.
145
+
146
+ Returns:
147
+ (arguments, request_id) pair: a new arguments mapping with
148
+ the binding filled when it was missing, plus the kept
149
+ or minted envelope id.
150
+
151
+ Raises:
152
+ ExecutionError: invalid_command for non-mapping arguments.
153
+ """
154
+ if not isinstance(arguments, dict):
155
+ raise ExecutionError(
156
+ "invalid_command",
157
+ f"Stamp needs mapping arguments, got {arguments!r}.",
158
+ )
159
+ if not is_tier2(operation):
160
+ return dict(arguments), request_id
161
+ stamped = dict(arguments)
162
+ if "expected_revision" not in stamped:
163
+ stamped["expected_revision"] = expected_revision
164
+ return stamped, request_id or uuid.uuid4().hex
165
+
166
+
167
+ def tool_call(operation, arguments):
168
+ """Build one canonical worker-side op-call record.
169
+
170
+ Args:
171
+ operation: Operation name for the record.
172
+ arguments: Op arguments copied onto the record.
173
+
174
+ Returns:
175
+ Op-call mapping with operation plus a copy of arguments —
176
+ the prelude JSONL shape, host replayable.
177
+
178
+ Raises:
179
+ ExecutionError: invalid_command for a non-text operation
180
+ or non-mapping arguments.
181
+ """
182
+ if not isinstance(operation, str) or not operation:
183
+ raise ExecutionError(
184
+ "invalid_command",
185
+ f"Op call needs a text operation, got {operation!r}.",
186
+ )
187
+ if not isinstance(arguments, dict):
188
+ raise ExecutionError(
189
+ "invalid_command",
190
+ f"Op call needs mapping arguments, got {arguments!r}.",
191
+ )
192
+ return {"operation": operation, "arguments": dict(arguments)}
193
+
194
+
195
+ def collect_run_ids(op_results):
196
+ """Lift child run ids off replayed op results.
197
+
198
+ Args:
199
+ op_results: Per-op replay outcomes (ok plus result, or ok
200
+ False plus error); None reads as no results.
201
+
202
+ Returns:
203
+ Run ids in replay order: one per ok result carrying a text
204
+ run_id, everything else skipped.
205
+ """
206
+ run_ids = []
207
+ for item in op_results or []:
208
+ if not isinstance(item, dict) or not item.get("ok"):
209
+ continue
210
+ result = item.get("result")
211
+ if isinstance(result, dict) and isinstance(
212
+ result.get("run_id"), str
213
+ ):
214
+ run_ids.append(result["run_id"])
215
+ return run_ids
216
+
217
+
218
+ def build_effects_report(ops, op_results, ops_skipped=0, replayed=True):
219
+ """Pack replayed ops as the shared effects shape.
220
+
221
+ Args:
222
+ ops: Recorded op calls in replay order.
223
+ op_results: Per-op replay outcomes aligned with ops.
224
+ ops_skipped: Harvest lines dropped as malformed.
225
+ replayed: True once a dispatcher replayed the calls; an
226
+ unreplayed bundle is ok only when it holds no ops.
227
+
228
+ Returns:
229
+ Effects mapping with ops, per-op results, the ok flag,
230
+ the skip count, child run ids, and the replayed flag.
231
+ """
232
+ results = list(op_results or [])
233
+ calls = list(ops or [])
234
+ if replayed:
235
+ ops_ok = all(item.get("ok") for item in results)
236
+ else:
237
+ ops_ok = not calls
238
+ return {
239
+ "ops": calls,
240
+ "op_results": results,
241
+ "ops_ok": ops_ok,
242
+ "ops_skipped": ops_skipped,
243
+ "run_ids": collect_run_ids(results),
244
+ "replayed": bool(replayed),
245
+ }
246
+
247
+
248
+ def report_completions(run_ids, fetch):
249
+ """Report stored completions for run ids without executing.
250
+
251
+ A read-only batch read-back: every id resolves through fetch
252
+ (a stored-completion reader such as the job surface), unknown
253
+ ids land in refused with their code, and ids listed twice are
254
+ refused as duplicates — nothing runs, nothing advances, and a
255
+ second report over the same ids reads the same values.
256
+
257
+ Args:
258
+ run_ids: Non-empty list of run ids to report.
259
+ fetch: Callable mapping one run id to its stored
260
+ completion; per-id failures land in refused.
261
+
262
+ Returns:
263
+ Report mapping with the asked run ids, their completions
264
+ in order, and refused entries (run_id, code, message).
265
+
266
+ Raises:
267
+ ExecutionError: invalid_command for an empty or
268
+ non-list run_ids, a non-text id, or a non-callable
269
+ fetch.
270
+ """
271
+ if not isinstance(run_ids, list) or not run_ids:
272
+ raise ExecutionError(
273
+ "invalid_command",
274
+ "report_completions needs a non-empty run_ids list.",
275
+ )
276
+ for run_id in run_ids:
277
+ if not isinstance(run_id, str) or not run_id:
278
+ raise ExecutionError(
279
+ "invalid_command",
280
+ f"Run id must be non-empty text, got {run_id!r}.",
281
+ )
282
+ if not callable(fetch):
283
+ raise ExecutionError(
284
+ "invalid_command", "report_completions needs a fetch reader."
285
+ )
286
+ completions = []
287
+ refused = []
288
+ seen = set()
289
+ for run_id in run_ids:
290
+ if run_id in seen:
291
+ refused.append({
292
+ "run_id": run_id,
293
+ "code": "duplicate_in_batch",
294
+ "message": f"Run listed twice in one report: {run_id}.",
295
+ })
296
+ continue
297
+ seen.add(run_id)
298
+ try:
299
+ completions.append(fetch(run_id))
300
+ except Exception as exc: # per-id truth: record, never raise
301
+ refused.append({
302
+ "run_id": run_id,
303
+ "code": getattr(exc, "code", None) or "internal_error",
304
+ "message": str(exc),
305
+ })
306
+ return {"run_ids": list(run_ids), "completions": completions,
307
+ "refused": refused}
308
+
309
+
310
+ def entity_doc_path(entity):
311
+ """Derive the one doc path storing an entity.
312
+
313
+ Shared ENGINE-ECS addressing: this wrapper names the same
314
+ function the store and the wired verbs use, so console paths
315
+ cannot drift onto a second projection.
316
+
317
+ Args:
318
+ entity: Entity id to project.
319
+
320
+ Returns:
321
+ Canonical entities path for the id.
322
+
323
+ Raises:
324
+ EcsError: invalid_entity for a malformed id (propagated
325
+ unchanged from the shared substrate).
326
+ """
327
+ return ecs.entity_doc_path(entity)
328
+
329
+
330
+ def parse_address(address):
331
+ """Split one canonical entity.component address.
332
+
333
+ Shared ENGINE-ECS addressing, same function as the store.
334
+
335
+ Args:
336
+ address: Address text with exactly one dot.
337
+
338
+ Returns:
339
+ (entity, component) pair, both validated.
340
+
341
+ Raises:
342
+ EcsError: invalid_address for a malformed address
343
+ (propagated unchanged from the shared substrate).
344
+ """
345
+ return ecs.parse_address(address)
346
+
347
+
348
+ def format_address(entity, component):
349
+ """Render one canonical entity.component address.
350
+
351
+ Shared ENGINE-ECS addressing, same function as the store.
352
+
353
+ Args:
354
+ entity: Entity id part.
355
+ component: Component name part.
356
+
357
+ Returns:
358
+ Canonical address text.
359
+
360
+ Raises:
361
+ EcsError: invalid_entity or invalid_component for
362
+ malformed parts (propagated unchanged).
363
+ """
364
+ return ecs.format_address(entity, component)