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,417 @@
1
+ """Engine ops mixin: Wired query handlers (events, apig, jobs, layers, logs)."""
2
+
3
+ from ..execution.apig.pool import stored_completion_from_events
4
+ from ..execution.errors import ExecutionError
5
+ from ..execution.python_exec import PythonExecService
6
+ from ..project.errors import ProjectError
7
+ from ..project.service import ProjectService
8
+
9
+ #: Severity order for ledger-backed findings pages, mirroring the
10
+ #: execution record's errors-first paging.
11
+ _SEVERITY_RANK = {"error": 0, "warning": 1}
12
+
13
+
14
+ def completion_event_for(events, run_id):
15
+ """Latest run event naming one run, or None.
16
+
17
+ Args:
18
+ events: Ledger events in seq order.
19
+ run_id: Run to find.
20
+
21
+ Returns:
22
+ The latest run_completed/run_failed event data for the
23
+ run, or None when the run never finished.
24
+ """
25
+ found = None
26
+ for event in events or ():
27
+ if event.get("event") not in ("run_completed", "run_failed"):
28
+ continue
29
+ if (event.get("data") or {}).get("run_id") == run_id:
30
+ found = event.get("data") or {}
31
+ return found
32
+
33
+
34
+ def findings_page_for(data, limit=50, cursor=None):
35
+ """Page ledger-stored findings like the execution record does.
36
+
37
+ Args:
38
+ data: Completion event data with findings,
39
+ findings_truncated, and a verdict carrying the full
40
+ finding_counts.
41
+ limit: Maximum findings in this page.
42
+ cursor: Opaque position from a previous page, if any.
43
+
44
+ Returns:
45
+ Finding page with total counts, findings, next cursor,
46
+ and truncation flag.
47
+ """
48
+ stored = list(data.get("findings") or [])
49
+ ordered = sorted(
50
+ stored,
51
+ key=lambda f: _SEVERITY_RANK.get(f.get("severity"), 2),
52
+ )
53
+ counts = dict((data.get("verdict") or {}).get("finding_counts") or {})
54
+ if not counts:
55
+ for finding in ordered:
56
+ severity = finding.get("severity", "unknown")
57
+ counts[severity] = counts.get(severity, 0) + 1
58
+ start = int(cursor) if cursor else 0
59
+ page = ordered[start : start + limit]
60
+ remaining = len(ordered) - (start + len(page))
61
+ return {
62
+ "total_counts": counts,
63
+ "findings": page,
64
+ "next_cursor": str(start + len(page)) if remaining > 0 else None,
65
+ "truncated": bool(data.get("findings_truncated")) or remaining > 0,
66
+ }
67
+
68
+
69
+ class _QueryMixin:
70
+ """Wired query handlers (events, apig, jobs, layers, logs)."""
71
+
72
+ def _describe_op(self, name):
73
+ """Resolve one cataloged name to its live serving handler.
74
+
75
+ The live twin of the MCP server's describe_tool over engine
76
+ objects, so api_search/api_info/api_help report the same
77
+ summaries the generated docs and MCP descriptions serve.
78
+
79
+ Args:
80
+ name: Cataloged operation or query name.
81
+
82
+ Returns:
83
+ (kind, wired, summary, handlers) with the handler
84
+ refs in generated-spec form.
85
+ """
86
+ from ..composition import (
87
+ ENGINE_OP_METHODS,
88
+ QUERY_ENGINE_METHODS,
89
+ QUERY_METHODS,
90
+ _CRUD_OPERATIONS,
91
+ )
92
+ from ..interfaces.vocabulary import OPERATIONS
93
+
94
+ solvers = self._solvers.get(name) or {}
95
+ if solvers:
96
+ adapter = next(iter(solvers.values()))
97
+ cls = type(adapter)
98
+ doc = cls.__doc__ or ""
99
+ summary = doc.splitlines()[0].strip() if doc.splitlines() else ""
100
+ return (
101
+ "operation", True, summary,
102
+ [f"{cls.__module__}.{cls.__qualname__}"],
103
+ )
104
+ if name == "run_job":
105
+ from ..composition import Engine as _Engine
106
+
107
+ return (
108
+ "operation", True,
109
+ (_Engine._run_queued.__doc__ or "").splitlines()[0].strip(),
110
+ [
111
+ "rocket_engine.composition.Engine.run",
112
+ "rocket_engine.composition.Engine._run_queued",
113
+ ],
114
+ )
115
+ if name == "python_execute":
116
+ return (
117
+ "operation", True,
118
+ (PythonExecService.request_execution.__doc__ or ""
119
+ ).splitlines()[0].strip(),
120
+ [
121
+ "rocket_engine.composition.Engine.submit",
122
+ "rocket_engine.execution.python_exec."
123
+ "PythonExecService.request_execution",
124
+ ],
125
+ )
126
+ if name in _CRUD_OPERATIONS and hasattr(ProjectService, name):
127
+ target = getattr(ProjectService, name)
128
+ doc = target.__doc__ or ""
129
+ summary = doc.splitlines()[0].strip() if doc.splitlines() else ""
130
+ return (
131
+ "operation", True, summary,
132
+ [f"rocket_engine.project.service.ProjectService.{name}"],
133
+ )
134
+ method = QUERY_METHODS.get(name)
135
+ if method is not None:
136
+ target = getattr(ProjectService, method)
137
+ doc = target.__doc__ or ""
138
+ summary = doc.splitlines()[0].strip() if doc.splitlines() else ""
139
+ return (
140
+ "query", True, summary,
141
+ [f"rocket_engine.project.service.ProjectService.{method}"],
142
+ )
143
+ emethod = QUERY_ENGINE_METHODS.get(name)
144
+ if emethod is not None:
145
+ from ..composition import Engine as _Engine
146
+
147
+ target = getattr(_Engine, emethod)
148
+ doc = target.__doc__ or ""
149
+ summary = doc.splitlines()[0].strip() if doc.splitlines() else ""
150
+ return (
151
+ "query", True, summary,
152
+ [f"rocket_engine.composition.Engine.{emethod}"],
153
+ )
154
+ kind = "operation" if name in OPERATIONS else "query"
155
+ return (
156
+ kind, False,
157
+ f"Declared {kind} `{name}`; no handler wired yet.", [],
158
+ )
159
+
160
+ def _query_events_since(self, arguments):
161
+ """Serve the wired get_events_since query (cursor + resync)."""
162
+ cursor = arguments.get("cursor", 0)
163
+ if not isinstance(cursor, int) or isinstance(cursor, bool):
164
+ raise ProjectError(
165
+ "invalid_command", f"Cursor must be an int, got {cursor!r}."
166
+ )
167
+ demand = arguments.get("require_contiguous_from")
168
+ if (
169
+ demand is not None
170
+ and (not isinstance(demand, int) or isinstance(demand, bool))
171
+ ):
172
+ raise ProjectError(
173
+ "invalid_command",
174
+ f"require_contiguous_from must be an int, got {demand!r}.",
175
+ )
176
+ events = self._project.events_since(
177
+ cursor, require_contiguous_from=demand
178
+ )
179
+ return {
180
+ "project_id": self._project_id,
181
+ "events": events,
182
+ "cursor": self._project.snapshot()["event_cursor"],
183
+ }
184
+
185
+ def _query_apig(self, name, arguments):
186
+ """Route a catalogue/asset query to its Engine reader."""
187
+ if name == "list_cells":
188
+ return self.catalog_cells(pdk=arguments.get("pdk"))
189
+ if name == "serve_asset":
190
+ return self.serve_asset(
191
+ arguments.get("pdk_id"),
192
+ arguments.get("revision"),
193
+ arguments.get("path"),
194
+ engine_root=arguments.get("engine_root"),
195
+ )
196
+ if name == "list_assets":
197
+ return self.list_assets(
198
+ arguments.get("pdk_id"),
199
+ arguments.get("revision"),
200
+ prefix=arguments.get("prefix"),
201
+ engine_root=arguments.get("engine_root"),
202
+ )
203
+ raise ProjectError("unsupported_operation", f"Operation not wired yet: {name}.")
204
+
205
+ def _query_job(self, name, arguments):
206
+ """Route a job-result query to its Engine reader."""
207
+ if name == "get_job_status":
208
+ return self.job_status(arguments.get("run_id"))
209
+ if name == "get_check_results":
210
+ return self.check_results(arguments.get("operation"))
211
+ if name == "get_logs":
212
+ return self.run_logs(
213
+ arguments.get("run_id"),
214
+ query=arguments.get("query"),
215
+ offset=arguments.get("offset", 0),
216
+ limit=arguments.get("limit", 40),
217
+ )
218
+ if name == "list_job_kinds":
219
+ return self.job_kinds()
220
+ if name == "get_findings":
221
+ return self.findings(
222
+ arguments.get("run_id"),
223
+ limit=arguments.get("limit", 50),
224
+ cursor=arguments.get("cursor"))
225
+ raise ProjectError("unsupported_operation", f"Operation not wired yet: {name}.")
226
+
227
+ def _query_completion(self, name, arguments):
228
+ """Route a completion query to its Engine reader."""
229
+ if name == "plan_checks":
230
+ return self.plan_checks(arguments.get("flow"))
231
+ if name == "get_design_completion":
232
+ return self.design_completion(arguments.get("flow"))
233
+ raise ProjectError("unsupported_operation", f"Operation not wired yet: {name}.")
234
+
235
+ def _query_layer(self, name, arguments):
236
+ """Route a layer-serving query to its Engine reader."""
237
+ if name == "get_layer_stackup":
238
+ return self.layer_stackup(
239
+ arguments.get("pdk_id"),
240
+ revision=arguments.get("revision"),
241
+ engine_root=arguments.get("engine_root"),
242
+ )
243
+ if name == "get_layer_rows":
244
+ return self.layer_rows(
245
+ arguments.get("pdk_id"),
246
+ preset=arguments.get("preset"),
247
+ overrides=arguments.get("overrides"),
248
+ active=arguments.get("active"),
249
+ revision=arguments.get("revision"),
250
+ engine_root=arguments.get("engine_root"),
251
+ )
252
+ if name == "list_layer_presets":
253
+ return self.layer_presets()
254
+ if name == "save_layer_preset":
255
+ return self.layer_preset_save(
256
+ arguments.get("pdk_id"),
257
+ arguments.get("name"),
258
+ arguments.get("layers"),
259
+ render_layers=arguments.get("render_layers"),
260
+ flip=arguments.get("flip", False),
261
+ active=arguments.get("active"),
262
+ overwrite=arguments.get("overwrite", False),
263
+ revision=arguments.get("revision"),
264
+ engine_root=arguments.get("engine_root"),
265
+ )
266
+ if name == "delete_layer_preset":
267
+ return self.layer_preset_delete(arguments.get("name"))
268
+ if name == "apply_layer_preset":
269
+ return self.layer_preset_apply(
270
+ arguments.get("pdk_id"),
271
+ arguments.get("name"),
272
+ revision=arguments.get("revision"),
273
+ engine_root=arguments.get("engine_root"),
274
+ )
275
+ raise ProjectError("unsupported_operation", f"Operation not wired yet: {name}.")
276
+
277
+ def _ledger_findings(self, run_id, limit=50, cursor=None):
278
+ """Findings page from the ledger, after a reboot.
279
+
280
+ In-memory verdicts answer first; across a reboot the
281
+ completion event answers instead. Runs that never
282
+ finished (or never ran) answer unknown_run.
283
+
284
+ Args:
285
+ run_id: Run whose findings to read.
286
+ limit: Maximum findings in this page.
287
+ cursor: Opaque position from a previous page, if any.
288
+
289
+ Returns:
290
+ Finding page for the run.
291
+
292
+ Raises:
293
+ ExecutionError: unknown_run for an id with no
294
+ completion event.
295
+ """
296
+ data = completion_event_for(self._project.events_since(0), run_id)
297
+ if data is None:
298
+ raise ExecutionError("unknown_run", f"Unknown run: {run_id}.")
299
+ return findings_page_for(data, limit=limit, cursor=cursor)
300
+
301
+ def _ledger_logs_page(self, arguments):
302
+ """Log page from the ledger excerpt, after a reboot.
303
+
304
+ Serves the stored tail with true trailing line numbers;
305
+ the unrecoverable head reads as an excerpt, flagged so.
306
+
307
+ Args:
308
+ arguments: run_logs arguments with run_id, query,
309
+ offset, and limit.
310
+
311
+ Returns:
312
+ Page mapping with numbered lines, totals, a more
313
+ flag, and excerpt True.
314
+
315
+ Raises:
316
+ ExecutionError: unknown_run for an id with no
317
+ completion event.
318
+ """
319
+ from ..composition import _LOG_PAGE_MAX # deferred: breaks the ops<->composition cycle
320
+ run_id = arguments.get("run_id")
321
+ data = completion_event_for(self._project.events_since(0), run_id)
322
+ if data is None:
323
+ raise ExecutionError("unknown_run", f"Unknown run: {run_id}.")
324
+ excerpt = (data.get("log_excerpt") or "").splitlines()
325
+ total = data.get("log_lines") or len(excerpt)
326
+ base = total - len(excerpt)
327
+ numbered = [
328
+ (base + index + 1, line) for index, line in enumerate(excerpt)
329
+ ]
330
+ query = arguments.get("query")
331
+ if query:
332
+ numbered = [(n, line) for n, line in numbered if query.lower() in line.lower()]
333
+ try:
334
+ offset = max(0, int(arguments.get("offset", 0)))
335
+ except (TypeError, ValueError):
336
+ raise ProjectError(
337
+ "invalid_command",
338
+ f"get_logs offset must be an int, got {arguments.get('offset')!r}.",
339
+ ) from None
340
+ try:
341
+ limit = max(1, min(int(arguments.get("limit", 40)), _LOG_PAGE_MAX))
342
+ except (TypeError, ValueError):
343
+ raise ProjectError(
344
+ "invalid_command", f"get_logs limit must be an int, got {arguments.get('limit')!r}."
345
+ ) from None
346
+ page = numbered[offset : offset + limit]
347
+ return {
348
+ "run_id": run_id,
349
+ "total_lines": total,
350
+ "matched": len(numbered),
351
+ "offset": offset,
352
+ "lines": [f"{n}: {line}" for n, line in page],
353
+ "more": offset + limit < len(numbered),
354
+ "excerpt": True,
355
+ }
356
+
357
+ def _logs_page(self, arguments):
358
+ from ..composition import _LOG_PAGE_MAX # deferred: breaks the ops<->composition cycle
359
+ run_id = arguments.get("run_id")
360
+ try:
361
+ self._execution.status(run_id) # unknown runs raise unknown_run
362
+ except ExecutionError:
363
+ return self._ledger_logs_page(arguments)
364
+ lines = self._logs.get(run_id, "").splitlines()
365
+ numbered = [(index + 1, line) for index, line in enumerate(lines)]
366
+ query = arguments.get("query")
367
+ if query:
368
+ numbered = [(n, line) for n, line in numbered if query.lower() in line.lower()]
369
+ try:
370
+ offset = max(0, int(arguments.get("offset", 0)))
371
+ except (TypeError, ValueError):
372
+ raise ProjectError(
373
+ "invalid_command",
374
+ f"get_logs offset must be an int, got {arguments.get('offset')!r}.",
375
+ ) from None
376
+ try:
377
+ limit = max(1, min(int(arguments.get("limit", 40)), _LOG_PAGE_MAX))
378
+ except (TypeError, ValueError):
379
+ raise ProjectError(
380
+ "invalid_command", f"get_logs limit must be an int, got {arguments.get('limit')!r}."
381
+ ) from None
382
+ page = numbered[offset : offset + limit]
383
+ return {
384
+ "run_id": run_id,
385
+ "total_lines": len(lines),
386
+ "matched": len(numbered),
387
+ "offset": offset,
388
+ "lines": [f"{n}: {line}" for n, line in page],
389
+ "more": offset + limit < len(numbered),
390
+ }
391
+
392
+ def _stored_completion(self, run_id):
393
+ """Stored completion for a finished run, without executing.
394
+
395
+ In-memory verdicts answer first; across a reboot the
396
+ ledger's run event answers instead. Runs that never
397
+ finished (or never ran) answer None.
398
+ """
399
+ try:
400
+ status = self._execution.status(run_id)
401
+ except ExecutionError:
402
+ status = None
403
+ if (
404
+ status is not None
405
+ and status.get("state") in ("completed", "cancelled")
406
+ and status.get("verdict") is not None
407
+ ):
408
+ pending = self._run_jobs.get(run_id, {})
409
+ return {
410
+ "run_id": run_id,
411
+ "state": status["state"],
412
+ "verdict": status["verdict"],
413
+ "operation": pending.get("operation"),
414
+ }
415
+ return stored_completion_from_events(
416
+ self._project.events_since(0), run_id
417
+ )