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,291 @@
1
+ """Circulax v1 normalizer: v0 parse output to a ResultBundle.
2
+
3
+ Maps the ``run_optical`` circulax parse shape (``measurements[]``
4
+ with volt value, target, margin, and pass/fail status plus the
5
+ transient grid) onto a typed measurement table, a transient
6
+ series dataset with a bound trace view, measurements, and
7
+ findings. Pure function of the parsed output: no I/O, no tool
8
+ calls, deterministic ids derived from the run id. PASS
9
+ measurements keep value, target, margin, and status.
10
+ """
11
+
12
+ TASK = "run_optical"
13
+
14
+ _DIAG_CAP = 240
15
+ _INLINE_CAP = 1024
16
+
17
+ _SEVERITY = {"error": "error", "warning": "warning"}
18
+
19
+
20
+ def _cap(text):
21
+ """Cap one diagnostic string at the bundle prose limit.
22
+
23
+ Args:
24
+ text: Raw diagnostic text, possibly unbounded tool prose.
25
+
26
+ Returns:
27
+ The text unchanged when short, else truncated with an
28
+ ellipsis marker.
29
+ """
30
+ text = text if isinstance(text, str) else str(text)
31
+ if len(text) <= _DIAG_CAP:
32
+ return text
33
+ return text[: _DIAG_CAP - 3] + "..."
34
+
35
+
36
+ def _quantity(value, unit):
37
+ """Build one v1 Quantity, mapping unitless to dimensionless.
38
+
39
+ Args:
40
+ value: Numeric value.
41
+ unit: Unit string, possibly empty for unitless measures.
42
+
43
+ Returns:
44
+ Quantity mapping with an explicit unit.
45
+ """
46
+ return {"value": float(value), "unit": unit if unit else "1"}
47
+
48
+
49
+ def _limit(item):
50
+ """Pick the binding target bound for one measurement.
51
+
52
+ Args:
53
+ item: v0 measurement with a ``target`` min/max mapping.
54
+
55
+ Returns:
56
+ The bound closest to the measured value (the margin
57
+ binding bound), or None when the target is empty.
58
+ """
59
+ target = item.get("target") or {}
60
+ scored = []
61
+ if target.get("min") is not None:
62
+ scored.append((float(item["value"]) - float(target["min"]),
63
+ float(target["min"])))
64
+ if target.get("max") is not None:
65
+ scored.append((float(target["max"]) - float(item["value"]),
66
+ float(target["max"])))
67
+ if not scored:
68
+ return None
69
+ return min(scored)[1]
70
+
71
+
72
+ def _downsample(values):
73
+ """Stride-downsample one sample list to the inline cap.
74
+
75
+ Args:
76
+ values: Sample values in stored order.
77
+
78
+ Returns:
79
+ (samples, downsampled) pair with at most _INLINE_CAP
80
+ entries, stride-picked deterministically.
81
+ """
82
+ values = list(values)
83
+ if len(values) <= _INLINE_CAP:
84
+ return values, False
85
+ step = -(-len(values) // _INLINE_CAP)
86
+ return values[::step], True
87
+
88
+
89
+ def _provenance(project_id, run_id, tools, provenance):
90
+ """Build ResultProvenance, defaulting to a placeholder closure.
91
+
92
+ Args:
93
+ project_id: Project the run belongs to.
94
+ run_id: Run the bundle describes.
95
+ tools: Tool ingredient mappings (id, version, digest?).
96
+ provenance: Full provenance mapping when the caller has
97
+ one; a deterministic placeholder otherwise.
98
+
99
+ Returns:
100
+ Provenance mapping valid against the v1 schema.
101
+ """
102
+ if provenance is not None:
103
+ return provenance
104
+ ref = {"scope": "project", "id": project_id, "revision": "r1"}
105
+ return {
106
+ "snapshot": dict(ref),
107
+ "input_lock": dict(ref),
108
+ "closure_hash": "0" * 64,
109
+ "tools": [
110
+ {
111
+ "id": item["id"],
112
+ "version": item.get("version", ""),
113
+ "digest": item.get("digest", "0" * 64),
114
+ }
115
+ for item in tools
116
+ ],
117
+ "parents": [],
118
+ }
119
+
120
+
121
+ def normalize(parsed, *, run_id, project_id="project", target=None,
122
+ tools=(), provenance=None):
123
+ """Normalize one circulax parse output to a v1 ResultBundle.
124
+
125
+ Measurement rows become a table dataset plus a bound table
126
+ view; the transient becomes a time series dataset with a
127
+ bound trace view; violations become findings with capped
128
+ messages. PASS measurements keep value, target, margin, and
129
+ status. A malformed circuit (no transient) reports
130
+ not_converged with its error finding.
131
+
132
+ Args:
133
+ parsed: v0 parse mapping with ``measurements[]``,
134
+ ``transient``, and ``violations[]``.
135
+ run_id: Run the bundle describes (anchors all ids).
136
+ project_id: Project the run belongs to.
137
+ target: v1 target mapping; project kind by default.
138
+ tools: Tool ingredient mappings for provenance.
139
+ provenance: Full provenance mapping when the caller has
140
+ one; a deterministic placeholder otherwise.
141
+
142
+ Returns:
143
+ Schema-valid ResultBundle mapping.
144
+ """
145
+ measurements_in = parsed.get("measurements") or []
146
+ transient = parsed.get("transient") or {}
147
+ violations = parsed.get("violations") or []
148
+ table_id = f"{run_id}:dataset:measurements"
149
+ datasets = [
150
+ {
151
+ "id": table_id,
152
+ "kind": "table",
153
+ "row_count": len(measurements_in),
154
+ "columns": [
155
+ {
156
+ "id": f"{table_id}:name",
157
+ "name": "name",
158
+ "dtype": "utf8",
159
+ "unit": "1",
160
+ "data": [item["name"] for item in measurements_in],
161
+ },
162
+ {
163
+ "id": f"{table_id}:value",
164
+ "name": "value",
165
+ "dtype": "float64",
166
+ "unit": "1",
167
+ "data": [float(item["value"]) for item in measurements_in],
168
+ },
169
+ {
170
+ "id": f"{table_id}:margin",
171
+ "name": "margin",
172
+ "dtype": "float64",
173
+ "unit": "1",
174
+ "data": [float(item["margin"]) for item in measurements_in],
175
+ },
176
+ {
177
+ "id": f"{table_id}:status",
178
+ "name": "status",
179
+ "dtype": "utf8",
180
+ "unit": "1",
181
+ "data": [item["status"] for item in measurements_in],
182
+ },
183
+ ],
184
+ "conditions": [],
185
+ }
186
+ ]
187
+ views = [
188
+ {
189
+ "id": f"{run_id}:view:measurements",
190
+ "kind": "table",
191
+ "dataset_id": table_id,
192
+ "name": "measurements",
193
+ }
194
+ ]
195
+ grid, _ = _downsample(transient.get("t_s") or [])
196
+ curve, _ = _downsample(transient.get("v_V") or [])
197
+ if grid and curve:
198
+ ds_id = f"{run_id}:dataset:transient"
199
+ axis_id = f"{ds_id}:time"
200
+ sig_id = f"{ds_id}:voltage"
201
+ observe = transient.get("observe", "out")
202
+ datasets.append(
203
+ {
204
+ "id": ds_id,
205
+ "kind": "series",
206
+ "analysis": "tran",
207
+ "axes": [
208
+ {
209
+ "id": axis_id,
210
+ "name": "time",
211
+ "quantity": "time",
212
+ "unit": "s",
213
+ "kind": "continuous",
214
+ "scale": "linear",
215
+ "values": [float(v) for v in grid],
216
+ }
217
+ ],
218
+ "signals": [
219
+ {
220
+ "id": sig_id,
221
+ "name": observe,
222
+ "quantity": "voltage",
223
+ "unit": "V",
224
+ "values": {
225
+ "storage": "inline",
226
+ "dtype": "float64",
227
+ "shape": [len(curve)],
228
+ "data": [float(v) for v in curve],
229
+ },
230
+ }
231
+ ],
232
+ "conditions": [],
233
+ }
234
+ )
235
+ views.append(
236
+ {
237
+ "id": f"{run_id}:view:transient",
238
+ "kind": "traces",
239
+ "dataset_id": ds_id,
240
+ "name": observe,
241
+ "x_axis": axis_id,
242
+ "traces": [
243
+ {"signal": sig_id, "transform": "identity",
244
+ "axis": "left"}
245
+ ],
246
+ }
247
+ )
248
+ measurements = []
249
+ for item in measurements_in:
250
+ entry = {
251
+ "name": item["name"],
252
+ "value": _quantity(item["value"], item.get("unit", "")),
253
+ "margin": _quantity(item["margin"], item.get("unit", "")),
254
+ "status": "PASS" if item["status"] == "pass" else "FAIL",
255
+ }
256
+ bound = _limit(item)
257
+ if bound is not None:
258
+ entry["limit"] = _quantity(bound, item.get("unit", ""))
259
+ measurements.append(entry)
260
+ findings = [
261
+ {
262
+ "id": f"{run_id}:finding:{index}",
263
+ "severity": _SEVERITY.get(v.get("severity"), "info"),
264
+ "code": v.get("rule", "circuit"),
265
+ "message": _cap(v.get("message", "")),
266
+ }
267
+ for index, v in enumerate(violations)
268
+ ]
269
+ converged = not any(
270
+ v.get("rule") in ("circuit", "transient") for v in violations
271
+ )
272
+ return {
273
+ "schema": "rocketeda/results/v1",
274
+ "ref": {"scope": "project", "id": run_id, "revision": "r1"},
275
+ "run_id": run_id,
276
+ "task": TASK,
277
+ "target": dict(target)
278
+ if target is not None
279
+ else {"project_id": project_id, "kind": "project"},
280
+ "state": "complete",
281
+ "freshness": "current",
282
+ "convergence": "converged" if converged else "not_converged",
283
+ "provenance": _provenance(project_id, run_id, tools, provenance),
284
+ "datasets": datasets,
285
+ "views": views,
286
+ "measurements": measurements,
287
+ "findings": findings,
288
+ "artifacts": [],
289
+ "logs": [],
290
+ "gaps": [],
291
+ }
@@ -0,0 +1,34 @@
1
+ # Adapters/cli — headless command surface
2
+
3
+ ## Responsibility (project decision)
4
+
5
+ Every engine operation invokable, every event printable: the terminal
6
+ client for debugging the headless engine without RocketIDE. Bare
7
+ args drop into the interactive REPL; `run <op> [key=value ...]`
8
+ runs one op; `--batch FILE`/`-` batch-executes; `--json` gives
9
+ stable machine envelopes. Entry: `python -m rocket_engine` (a
10
+ `[project.scripts]` `rocket-engine` alias can point at
11
+ `adapters.cli:main` once packaging wires it).
12
+
13
+ ## Boundaries
14
+
15
+ Same as every transport: protocol ↔ ports only. No logic. argv
16
+ parsing/formatting lives here; op-call parsing and dispatch live in
17
+ the use-case layer (`execution.dispatch`, the inbound port shared
18
+ with console/MCP over the one catalog). Adapters never call each
19
+ other.
20
+
21
+ ## Routing
22
+
23
+ - Parent rules → `../README.md`.
24
+
25
+ ## Exits (honest)
26
+
27
+ - 0 execution-ok, EVEN when the verdict is FAIL (verdicts live in
28
+ the payload, never in exit codes — same rule as solvers).
29
+ - 1 engine error. 2 usage error (unknown op/args).
30
+
31
+ ## Status
32
+
33
+ Live (CLI-DISPATCH). Console history replays as CLI batch scripts
34
+ by construction — same dispatch, same grammar.
@@ -0,0 +1,305 @@
1
+ """CLI inbound adapter: argv parsing + result formatting over dispatch.
2
+
3
+ `rocket-engine` (this module via `python -m rocket_engine`): bare
4
+ args drop into the interactive REPL; `run <op> [key=value ...]`
5
+ runs one op; `--batch FILE`/`-` batch-executes op lines; `--json`
6
+ gives stable machine output. Op text parses through the one parser
7
+ of record (`execution.dispatch`); this adapter never parses op
8
+ calls itself, never calls another adapter, and holds no rules.
9
+
10
+ Exits: 0 execution-ok EVEN when the verdict is FAIL (verdicts live
11
+ in the payload, never in exit codes); 1 engine error; 2 usage
12
+ error (unknown op/args). Batch aggregates: any usage error -> 2,
13
+ else any engine error -> 1, else 0.
14
+ """
15
+
16
+ import json
17
+ import sys
18
+
19
+ from ...composition import Engine
20
+ from ...execution.dispatch import OpDispatcher, parse_op_call, parse_op_tokens
21
+ from ...execution.errors import UsageError
22
+ from ..errors import AdapterError, error_payload
23
+
24
+ __all__ = ["EXIT_ENGINE_ERROR", "EXIT_OK", "EXIT_USAGE", "USAGE", "main"]
25
+
26
+ EXIT_OK = 0
27
+ EXIT_ENGINE_ERROR = 1
28
+ EXIT_USAGE = 2
29
+
30
+ USAGE = """usage: rocket-engine [--project ID] [--actor NAME] [--request-id ID] [--json] [--batch FILE|-] [run <op> [key=value ...]]
31
+
32
+ No args drops into the interactive REPL (one op call per line,
33
+ blank lines and `#` comments skipped, `quit` to leave). `run`
34
+ executes one op call; `--batch` executes every op-call line of a
35
+ file (`-` reads stdin). `--json` prints stable machine envelopes.
36
+ `--request-id` stamps a caller id on a single `run` (batch and
37
+ REPL lines always mint their own, so repeats stay distinct).
38
+ Op calls are `<operation> [key=value ...]` with YAML-scalar values
39
+ (`benches=[ac,tran]`, `deck={tool: klayout}`); quote values with
40
+ spaces. Every call — queries and operations alike — goes through
41
+ submit, the one admission path.
42
+
43
+ Exits: 0 execution-ok (even when the verdict is FAIL — read the
44
+ payload); 1 engine error; 2 usage error (unknown op/args).
45
+ """
46
+
47
+
48
+ def _split_flag(token, name):
49
+ prefix = f"--{name}="
50
+ if token.startswith(prefix):
51
+ return token[len(prefix):]
52
+ return None
53
+
54
+
55
+ def _parse_argv(argv):
56
+ options = {"json": False, "batch": None, "project": "default",
57
+ "actor": "cli", "request_id": None, "help": False}
58
+ rest = []
59
+ i = 0
60
+ while i < len(argv):
61
+ token = argv[i]
62
+ if token == "--json":
63
+ options["json"] = True
64
+ elif token in ("--help", "-h"):
65
+ options["help"] = True
66
+ elif token == "--batch":
67
+ i += 1
68
+ if i >= len(argv):
69
+ raise UsageError(
70
+ "invalid_command", "`--batch` needs a FILE (`-` for stdin).")
71
+ options["batch"] = argv[i]
72
+ elif token == "--project":
73
+ i += 1
74
+ if i >= len(argv):
75
+ raise UsageError("invalid_command", "`--project` needs an ID.")
76
+ options["project"] = argv[i]
77
+ elif token == "--actor":
78
+ i += 1
79
+ if i >= len(argv):
80
+ raise UsageError("invalid_command", "`--actor` needs a NAME.")
81
+ options["actor"] = argv[i]
82
+ elif token == "--request-id":
83
+ i += 1
84
+ if i >= len(argv):
85
+ raise UsageError(
86
+ "invalid_command", "`--request-id` needs an ID.")
87
+ options["request_id"] = argv[i]
88
+ elif _split_flag(token, "batch") is not None:
89
+ options["batch"] = _split_flag(token, "batch")
90
+ elif _split_flag(token, "project") is not None:
91
+ options["project"] = _split_flag(token, "project")
92
+ elif _split_flag(token, "actor") is not None:
93
+ options["actor"] = _split_flag(token, "actor")
94
+ elif _split_flag(token, "request-id") is not None:
95
+ options["request_id"] = _split_flag(token, "request-id")
96
+ elif token == "--":
97
+ rest.extend(argv[i + 1:])
98
+ break
99
+ elif token.startswith("-"):
100
+ raise UsageError("invalid_command", f"Unknown flag: {token}.")
101
+ else:
102
+ rest.extend(argv[i:])
103
+ break
104
+ i += 1
105
+ return options, rest
106
+
107
+
108
+ def _envelope(operation, ok, payload):
109
+ body = {"operation": operation, "ok": ok}
110
+ body["result" if ok else "error"] = payload
111
+ return json.dumps(body, sort_keys=True, default=str)
112
+
113
+
114
+ def _human_summary(operation, result):
115
+ if isinstance(result, dict):
116
+ verdict = result.get("verdict")
117
+ if isinstance(verdict, dict) and "status" in verdict:
118
+ line = f"{operation}: {verdict['status']}"
119
+ if result.get("run_id"):
120
+ line += f" (run {result['run_id']})"
121
+ return line
122
+ return f"{operation}: ok"
123
+ return f"{operation}: {result}"
124
+
125
+
126
+ def _emit_result(operation, result, as_json, stdout):
127
+ if as_json:
128
+ stdout.write(_envelope(operation, True, result) + "\n")
129
+ else:
130
+ stdout.write(_human_summary(operation, result) + "\n")
131
+
132
+
133
+ def _emit_error(operation, exc, as_json, stdout, stderr):
134
+ payload = error_payload(exc)
135
+ if as_json:
136
+ stdout.write(_envelope(operation, False, payload) + "\n")
137
+ else:
138
+ line = f"error [{payload['code']}]: {payload['message']}"
139
+ if payload.get("fields"):
140
+ line += f" (fields: {', '.join(payload['fields'])})"
141
+ stderr.write(line + "\n")
142
+ if isinstance(exc, UsageError):
143
+ stderr.write(USAGE)
144
+
145
+
146
+ def _classify(exc):
147
+ if isinstance(exc, UsageError):
148
+ return EXIT_USAGE
149
+ return EXIT_ENGINE_ERROR
150
+
151
+
152
+ def _run_single(dispatch, options, op_tokens, stdout, stderr):
153
+ if not op_tokens:
154
+ _emit_error(None, UsageError("invalid_command", "`run` needs an operation."),
155
+ options["json"], stdout, stderr)
156
+ return EXIT_USAGE
157
+ try:
158
+ operation, arguments = parse_op_tokens(op_tokens)
159
+ except UsageError as exc:
160
+ _emit_error(op_tokens[0], exc, options["json"], stdout, stderr)
161
+ return EXIT_USAGE
162
+ try:
163
+ result = dispatch.execute(operation, arguments,
164
+ request_id=options["request_id"])
165
+ except Exception as exc: # engine errors stay data, never tracebacks
166
+ _emit_error(operation, exc, options["json"], stdout, stderr)
167
+ return _classify(exc)
168
+ _emit_result(operation, result, options["json"], stdout)
169
+ return EXIT_OK
170
+
171
+
172
+ def _iter_batch_lines(source, stdin):
173
+ if source == "-":
174
+ return stdin.read().splitlines()
175
+ try:
176
+ with open(source, encoding="utf-8") as handle:
177
+ return handle.read().splitlines()
178
+ except OSError as exc:
179
+ raise AdapterError(
180
+ "batch_unreadable", f"Cannot read batch file {source}: {exc}."
181
+ ) from exc
182
+
183
+
184
+ def _run_batch(dispatch, options, source, stdin, stdout, stderr):
185
+ try:
186
+ lines = _iter_batch_lines(source, stdin)
187
+ except Exception as exc:
188
+ _emit_error(None, exc, options["json"], stdout, stderr)
189
+ return _classify(exc)
190
+ worst = EXIT_OK
191
+ for line in lines:
192
+ stripped = line.strip()
193
+ if not stripped or stripped.startswith("#"):
194
+ continue
195
+ try:
196
+ operation, result = dispatch.execute_line(stripped)
197
+ except UsageError as exc:
198
+ _emit_error(stripped.split()[0] if stripped.split() else None,
199
+ exc, options["json"], stdout, stderr)
200
+ worst = max(worst, EXIT_USAGE)
201
+ continue
202
+ except Exception as exc:
203
+ # execute_line parsed this line already; re-parse only to
204
+ # label the envelope — it cannot fail here.
205
+ operation = parse_op_call(stripped)[0]
206
+ _emit_error(operation, exc, options["json"], stdout, stderr)
207
+ worst = max(worst, EXIT_ENGINE_ERROR)
208
+ continue
209
+ _emit_result(operation, result, options["json"], stdout)
210
+ return worst
211
+
212
+
213
+ def _run_repl(dispatch, options, stdin, stdout, stderr):
214
+ interactive = stdin.isatty() if hasattr(stdin, "isatty") else False
215
+ if interactive:
216
+ stdout.write("rocket> ")
217
+ stdout.flush()
218
+ while True:
219
+ line = stdin.readline()
220
+ if line == "":
221
+ break
222
+ stripped = line.strip()
223
+ if interactive:
224
+ stdout.write("rocket> ")
225
+ stdout.flush()
226
+ if not stripped or stripped.startswith("#"):
227
+ continue
228
+ if stripped in ("exit", "quit"):
229
+ break
230
+ if stripped == "help":
231
+ stdout.write(USAGE)
232
+ continue
233
+ try:
234
+ operation, result = dispatch.execute_line(stripped)
235
+ except UsageError as exc:
236
+ _emit_error(stripped.split()[0] if stripped.split() else None,
237
+ exc, options["json"], stdout, stderr)
238
+ continue
239
+ except Exception as exc:
240
+ operation = parse_op_call(stripped)[0]
241
+ _emit_error(operation, exc, options["json"], stdout, stderr)
242
+ continue
243
+ _emit_result(operation, result, options["json"], stdout)
244
+ return EXIT_OK
245
+
246
+
247
+ def main(argv=None, engine=None, stdin=None, stdout=None, stderr=None):
248
+ """Run the CLI; return the process exit code (never raise for use).
249
+
250
+ Bare args drop into the REPL, `run` executes one op, `--batch`
251
+ executes op lines; usage and engine failures become exit codes,
252
+ never tracebacks.
253
+
254
+ Args:
255
+ argv: Argument vector without the program name; defaults to
256
+ sys.argv[1:].
257
+ engine: Engine to dispatch against; constructed from
258
+ --project/--actor when omitted.
259
+ stdin: Input stream for the REPL and `--batch -`.
260
+ stdout: Stream for results and machine envelopes.
261
+ stderr: Stream for human-readable errors plus usage.
262
+
263
+ Returns:
264
+ Process exit code: 0 execution-ok (even when the verdict
265
+ is FAIL), 1 engine error, 2 usage error.
266
+
267
+ Caution: Exit zero is not PASS — the verdict lives in the
268
+ payload; read it, never the exit code.
269
+ """
270
+ argv = list(sys.argv[1:] if argv is None else argv)
271
+ stdin = sys.stdin if stdin is None else stdin
272
+ stdout = sys.stdout if stdout is None else stdout
273
+ stderr = sys.stderr if stderr is None else stderr
274
+ try:
275
+ options, rest = _parse_argv(argv)
276
+ except UsageError as exc:
277
+ _emit_error(None, exc, False, stdout, stderr)
278
+ return EXIT_USAGE
279
+ if options["help"]:
280
+ stdout.write(USAGE)
281
+ return EXIT_OK
282
+ if options["batch"] is not None and rest[:1] == ["run"]:
283
+ _emit_error(None, UsageError("invalid_command",
284
+ "Cannot combine `run` with `--batch`."),
285
+ options["json"], stdout, stderr)
286
+ return EXIT_USAGE
287
+ if engine is None:
288
+ engine = Engine(options["project"], actor=options["actor"])
289
+ dispatch = OpDispatcher(engine, project_id=options["project"],
290
+ actor=options["actor"])
291
+ if rest[:1] == ["run"]:
292
+ return _run_single(dispatch, options, rest[1:], stdout, stderr)
293
+ if rest:
294
+ _emit_error(None, UsageError("invalid_command",
295
+ f"Unknown command: {rest[0]} (want `run`)."),
296
+ options["json"], stdout, stderr)
297
+ return EXIT_USAGE
298
+ if options["batch"] is not None:
299
+ return _run_batch(dispatch, options, options["batch"], stdin,
300
+ stdout, stderr)
301
+ return _run_repl(dispatch, options, stdin, stdout, stderr)
302
+
303
+
304
+ if __name__ == "__main__":
305
+ raise SystemExit(main())