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,194 @@
1
+ """Run evidence trail: confinement + per-run evidence files.
2
+
3
+ Pure helpers behind the Engine evidence seam (TRANSPORT-PARITY):
4
+ sources resolve inside the project, the engine chooses the workdir,
5
+ and every transport leaves the identical evidence file. The Engine
6
+ keeps thin wrappers so the MCP server's ``Engine.confine_spice_arguments``
7
+ seam and the submit/run call sites do not move.
8
+ """
9
+
10
+ import json
11
+ import subprocess
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ from ..adapters.errors import AdapterError
16
+
17
+
18
+ def confine_spice_arguments(arguments, project_dir):
19
+ """Resolve run_spice sources inside the project; fix the workdir.
20
+
21
+ The engine-side confinement every transport shares
22
+ (TRANSPORT-PARITY): sources resolve inside the project dir,
23
+ a caller workdir is refused, and the workdir is the
24
+ results/engine-runs folder the evidence trail records
25
+ into. Unbound engines (no project dir) pass arguments
26
+ through untouched.
27
+
28
+ Args:
29
+ arguments: Submitted run_spice arguments.
30
+ project_dir: Project folder sources resolve inside, or
31
+ None for an unbound engine.
32
+
33
+ Returns:
34
+ Arguments with absolute confined sources and the
35
+ engine-chosen workdir (unchanged when unbound).
36
+
37
+ Raises:
38
+ AdapterError: invalid_reference for a source outside
39
+ the project, invalid_command for a caller workdir.
40
+ """
41
+ if project_dir is None:
42
+ return arguments
43
+ root = Path(project_dir).resolve()
44
+ confined = dict(arguments)
45
+ workdir = root / "results" / "engine-runs"
46
+ sources = confined.get("sources")
47
+ if isinstance(sources, list):
48
+ resolved = []
49
+ for source in sources:
50
+ path = (root / str(source)).resolve()
51
+ if not path.is_relative_to(root):
52
+ raise AdapterError(
53
+ "invalid_reference",
54
+ f"Source outside the project: {source}.",
55
+ )
56
+ resolved.append(str(path))
57
+ confined["sources"] = resolved
58
+ if "workdir" in confined and confined["workdir"] != str(workdir):
59
+ raise AdapterError(
60
+ "invalid_command",
61
+ "workdir is chosen by the engine, not the caller.",
62
+ )
63
+ workdir.mkdir(parents=True, exist_ok=True)
64
+ confined["workdir"] = str(workdir)
65
+ return confined
66
+
67
+
68
+ def evidence_source_state(root):
69
+ """Probe (short commit, dirty) of the project tree.
70
+
71
+ Twin of the MCP server's source probe (kept local so the
72
+ composition root never imports an adapter): git metadata
73
+ when readable, (None, None) otherwise.
74
+
75
+ Args:
76
+ root: Project folder to probe.
77
+
78
+ Returns:
79
+ (commit, dirty) pair, (None, None) outside git.
80
+ """
81
+ git = ["git", "-C", str(root)]
82
+ head = subprocess.run(
83
+ [*git, "rev-parse", "--short", "HEAD"],
84
+ capture_output=True, text=True,
85
+ )
86
+ if head.returncode != 0:
87
+ return None, None
88
+ status = subprocess.run(
89
+ [*git, "status", "--porcelain", "--",
90
+ "design", "tests", "constraints.yaml"],
91
+ capture_output=True, text=True,
92
+ )
93
+ return head.stdout.strip(), bool(status.stdout.strip())
94
+
95
+
96
+ def record_run_evidence(outcome, arguments, project_dir, log_text):
97
+ """Persist one finished run under results/engine-runs.
98
+
99
+ The same record shape the MCP server writes, recorded by
100
+ the engine itself so every transport leaves the identical
101
+ evidence file (TRANSPORT-PARITY): IDE buttons and agent
102
+ calls share one evidence trail whoever submitted.
103
+
104
+ Args:
105
+ outcome: Completion mapping with run_id, operation,
106
+ and verdict.
107
+ arguments: Submitted arguments (for the run's source
108
+ list).
109
+ project_dir: Project folder, or None for an unbound
110
+ engine (no file is written).
111
+ log_text: Callable taking a run_id and returning the
112
+ run's log text.
113
+
114
+ Returns:
115
+ Evidence file path string, or None when the engine is
116
+ unbound (no project dir anywhere).
117
+ """
118
+ if project_dir is None:
119
+ return None
120
+ folder = Path(project_dir).resolve() / "results" / "engine-runs"
121
+ folder.mkdir(parents=True, exist_ok=True)
122
+ commit, dirty = evidence_source_state(Path(project_dir).resolve())
123
+ sources = []
124
+ for source in arguments.get("sources") or []:
125
+ path = Path(str(source))
126
+ if path.is_absolute():
127
+ try:
128
+ sources.append(str(path.relative_to(
129
+ Path(project_dir).resolve())))
130
+ continue
131
+ except ValueError:
132
+ pass
133
+ sources.append(str(source))
134
+ record = {
135
+ "producer": "rocket-engine",
136
+ "run_id": outcome["run_id"],
137
+ "operation": outcome["operation"],
138
+ "verdict": outcome["verdict"],
139
+ "finished_at": datetime.now(timezone.utc).strftime(
140
+ "%Y-%m-%dT%H:%M:%SZ"),
141
+ "source_commit": commit,
142
+ "source_dirty": dirty,
143
+ "sources": sources,
144
+ "log": log_text(outcome["run_id"]),
145
+ }
146
+ path = folder / f"{outcome['run_id']}.json"
147
+ path.write_text(json.dumps(record, indent=2), encoding="utf-8")
148
+ return str(path)
149
+
150
+
151
+ def maybe_record_evidence(project_dir, run_jobs, log_text,
152
+ operation, completion, arguments):
153
+ """Record one completion when the engine is project-bound.
154
+
155
+ No-op for unbound engines, non-completions, and
156
+ verdict-less payloads (acks, batch mappings, queries).
157
+ run_job fetches resolve sources from the admitted
158
+ arguments, never the fetch envelope.
159
+
160
+ Args:
161
+ project_dir: Engine project folder, or None when
162
+ unbound.
163
+ run_jobs: Admitted run-job records by run_id.
164
+ log_text: Callable taking a run_id and returning the
165
+ run's log text.
166
+ operation: Operation hint when the completion omits it.
167
+ completion: Completion mapping, batch mapping, or ack.
168
+ arguments: Submitted arguments for the source list.
169
+
170
+ Returns:
171
+ Evidence file path string, or None when skipped.
172
+ """
173
+ if project_dir is None:
174
+ return None
175
+ if not isinstance(completion, dict):
176
+ return None
177
+ run_id = completion.get("run_id")
178
+ verdict = completion.get("verdict")
179
+ if run_id is None or verdict is None:
180
+ return None
181
+ pending = run_jobs.get(run_id, {})
182
+ sources = pending.get("arguments", arguments)
183
+ if not isinstance(sources, dict):
184
+ sources = arguments
185
+ return record_run_evidence(
186
+ {
187
+ "run_id": run_id,
188
+ "operation": completion.get("operation", operation),
189
+ "verdict": verdict,
190
+ },
191
+ sources,
192
+ project_dir,
193
+ log_text,
194
+ )
@@ -0,0 +1,262 @@
1
+ """Engine ops mixin: Solver selection, target/constraint admission and block scope."""
2
+
3
+ from ..execution.block_runs_scope import (BlockScopeError, check_operation_scope, parse_target, resolve_block, snapshot_closure)
4
+ from ..interfaces.runparams import RunParamsError, validate_run_request
5
+ from ..project.errors import ProjectError
6
+
7
+
8
+ class _GateMixin:
9
+ """Solver selection, target/constraint admission and block scope."""
10
+
11
+ def _select_solver(self, name, arguments):
12
+ solvers = self._solvers.get(name) or {}
13
+ if not solvers:
14
+ raise ProjectError(
15
+ "unsupported_operation", f"No solver registered: {name}."
16
+ )
17
+ hint = (arguments.get("deck") or {}).get("tool")
18
+ if hint is None:
19
+ return next(iter(solvers.values()))
20
+ try:
21
+ return solvers[hint]
22
+ except KeyError:
23
+ raise ProjectError(
24
+ "unsupported_operation",
25
+ f"No {name} solver for deck tool: {hint}.",
26
+ ) from None
27
+
28
+ def _apply_targets(self, name, arguments):
29
+ """Fill run_spice metric targets from project constraints.
30
+
31
+ Caller-supplied min/max are refused (invalid_command):
32
+ the agent must not grade its own homework. Metrics
33
+ without a constraint entry fail loudly, listing the
34
+ constrained ones.
35
+ """
36
+ if name != "run_spice" or self._constraints is None:
37
+ return arguments
38
+ measurements = arguments.get("measurements")
39
+ if not isinstance(measurements, list):
40
+ return arguments # the adapter's preflight reports the shape error
41
+ filled = []
42
+ control = []
43
+ for item in measurements:
44
+ if not isinstance(item, dict):
45
+ filled.append(item)
46
+ continue
47
+ metric = item.get("name")
48
+ if "min" in item or "max" in item:
49
+ raise ProjectError(
50
+ "invalid_command",
51
+ "Targets come from the project's constraints, not the call: "
52
+ f"remove min/max from {metric!r}.",
53
+ )
54
+ target = self._constraints.get(metric)
55
+ if target is None:
56
+ raise ProjectError(
57
+ "invalid_command",
58
+ f"No target for {metric!r} in the project's constraints; "
59
+ f"constrained metrics: {sorted(self._constraints)}.",
60
+ )
61
+ bounds = {key: target[key] for key in ("min", "max") if target.get(key) is not None}
62
+ unit = item.get("unit") or target.get("unit", "")
63
+ item = {**item, **bounds, "unit": unit}
64
+ # How a metric is measured is part of the target: a caller formula could read anything.
65
+ if target.get("expr") is not None:
66
+ if item.get("expr") not in (None, "", target["expr"]):
67
+ raise ProjectError(
68
+ "invalid_command",
69
+ f"How {metric!r} is measured comes from the project's constraints; "
70
+ "omit expr.",
71
+ )
72
+ item["expr"] = target["expr"]
73
+ for line in target.get("control") or []:
74
+ if line not in control:
75
+ control.append(line)
76
+ filled.append(item)
77
+ updated = {**arguments, "measurements": filled}
78
+ if control:
79
+ updated["analyses"] = list(arguments.get("analyses") or []) + control
80
+ return updated
81
+
82
+ def _apply_run_request(self, arguments):
83
+ """Validate and freeze the explicit run.request knobs (RUN-PARAMS).
84
+
85
+ Every submit carries its knobs explicitly: a missing
86
+ `request` freezes to all defaults, a partial one fills
87
+ defaults, and unknown fields fail loudly — never
88
+ ignored. All callers (IDE, MCP, console) share this one
89
+ path, so the shape is identical whoever submits.
90
+
91
+ Args:
92
+ arguments: Submitted arguments, maybe with `request`.
93
+
94
+ Returns:
95
+ Arguments with `request` normalized to every knob.
96
+
97
+ Raises:
98
+ ProjectError: unknown_run_param for an unknown
99
+ field, invalid_run_param for a mistyped knob,
100
+ invalid_run_request for a non-mapping
101
+ `request`.
102
+ """
103
+ try:
104
+ normalized = validate_run_request(arguments.get("request"))
105
+ except RunParamsError as exc:
106
+ raise ProjectError(exc.code, str(exc), fields=exc.fields) from exc
107
+ return {**arguments, "request": normalized}
108
+
109
+ def _admit_block_scope(self, name, arguments):
110
+ """Validate a block run target and snapshot its closure.
111
+
112
+ Arguments without a target run at project scope and skip
113
+ this path untouched; block targets resolve their entity
114
+ and pin the component closure the verdict later cites.
115
+
116
+ Args:
117
+ name: Cataloged operation about to run.
118
+ arguments: Operation arguments (target rides here).
119
+
120
+ Returns:
121
+ None for project runs, else the admitted scope with
122
+ block_id and closure keys (the run_id-bound record
123
+ is built once the ack id exists).
124
+
125
+ Raises:
126
+ ProjectError: unknown_block for a block target naming
127
+ no block, invalid_target for a malformed target,
128
+ unsupported_scope for a block target on a verb
129
+ serving no block runs yet.
130
+ """
131
+ if "target" not in arguments:
132
+ return None
133
+ target = arguments["target"]
134
+ if (
135
+ isinstance(target, dict)
136
+ and target.get("scope", "project") == "project"
137
+ ):
138
+ # Project scope serves every operation and carries no
139
+ # block: established callers (scenario harness, adapter
140
+ # tests) address cells at project scope, so pass those
141
+ # targets through untouched — only block scope engages.
142
+ return None
143
+ try:
144
+ parsed = parse_target(target)
145
+ check_operation_scope(name, parsed["scope"])
146
+ if parsed["scope"] != "block":
147
+ return None
148
+ block = resolve_block(parsed["id"], self._blocks)
149
+ closure = snapshot_closure(block)
150
+ except BlockScopeError as exc:
151
+ raise ProjectError(exc.code, str(exc)) from exc
152
+ return {"block_id": parsed["id"], "closure": closure}
153
+
154
+
155
+ #: (operation, deck tool) -> prerequisite operations that must each
156
+ #: hold at least one PASS verdict in this project. Declared by engine
157
+ #: maintainers here — never by caller arguments. Hard deps are
158
+ #: structural (LVS after extraction) and never forceable.
159
+ HARD_DEPS = {
160
+ ("run_lvs", "netgen"): ("run_extraction",),
161
+ ("run_post_layout", "ngspice"): ("run_spice",),
162
+ ("run_timing", "opensta"): ("run_synth",),
163
+ ("run_digital_flow", "librelane"): ("run_syntax",),
164
+ ("run_formal", "sby"): ("run_synth",),
165
+ }
166
+
167
+ #: operation -> gate operations whose latest verdict must be PASS.
168
+ #: `force=True` on the command bypasses these (and only these).
169
+ VERDICT_GATES = {
170
+ "run_post_layout": ("run_spice",),
171
+ "run_timing": ("run_synth",),
172
+ "run_formal": ("run_synth",),
173
+ }
174
+
175
+ _PASS = "PASS"
176
+
177
+ _COMPLETION_EVENTS = ("run_completed", "run_failed")
178
+
179
+
180
+ def hard_dependencies(operation, deck_tool=None, caller_depends_on=None):
181
+ """Prerequisite operations for one solver lane.
182
+
183
+ Args:
184
+ operation: Solver operation about to be admitted.
185
+ deck_tool: Deck tool qualifier, or None for any deck.
186
+ caller_depends_on: Accepted and ignored — callers never
187
+ shape the gate table.
188
+
189
+ Returns:
190
+ Prerequisite operation names (possibly empty).
191
+ """
192
+ return HARD_DEPS.get((operation, deck_tool), ()) or HARD_DEPS.get(
193
+ (operation, None), ()
194
+ )
195
+
196
+
197
+ def _completions(project):
198
+ """(seq, operation, status) triples from the durable ledger."""
199
+ try:
200
+ events = project.events_since(0)
201
+ except Exception:
202
+ return []
203
+ out = []
204
+ for event in events or []:
205
+ if event.get("event") not in _COMPLETION_EVENTS:
206
+ continue
207
+ data = event.get("data") or {}
208
+ operation = data.get("operation")
209
+ verdict = data.get("verdict") or {}
210
+ if operation is None:
211
+ continue
212
+ out.append((event.get("seq", 0), operation, verdict.get("status")))
213
+ return out
214
+
215
+
216
+ def check_submit_gates(operation, arguments, project, force=False):
217
+ """Refuse an illegal solver sequence before admission.
218
+
219
+ Args:
220
+ operation: Solver operation about to be admitted.
221
+ arguments: Submit arguments (deck tool read from
222
+ `deck.tool` when it is a mapping).
223
+ project: Project port with `events_since(cursor)`.
224
+ force: Bypass verdict gates only; hard deps still refuse.
225
+
226
+ Returns:
227
+ None when every gate holds.
228
+
229
+ Raises:
230
+ ProjectError: unmet_dependency naming the unmet gate —
231
+ for an unmet hard dep (never forceable) or a
232
+ non-PASS latest verdict (forceable).
233
+ """
234
+ deck = (arguments or {}).get("deck")
235
+ deck_tool = deck.get("tool") if isinstance(deck, dict) else None
236
+ completions = _completions(project)
237
+
238
+ for prereq in hard_dependencies(operation, deck_tool):
239
+ if any(op == prereq and status == _PASS for _, op, status in completions):
240
+ continue
241
+ raise ProjectError(
242
+ "unmet_dependency",
243
+ f"{operation} needs {prereq} PASS before dispatch "
244
+ f"(gate: {prereq}).",
245
+ fields=[{"gate": prereq}],
246
+ )
247
+
248
+ if force:
249
+ return None
250
+ latest = {}
251
+ for seq, op, status in completions:
252
+ if seq >= latest.get(op, (-1, None))[0]:
253
+ latest[op] = (seq, status)
254
+ for gate in VERDICT_GATES.get(operation, ()):
255
+ if latest.get(gate, (None, None))[1] != _PASS:
256
+ raise ProjectError(
257
+ "unmet_dependency",
258
+ f"{operation} needs latest {gate} PASS before dispatch "
259
+ f"(gate: {gate}); resubmit with force to bypass.",
260
+ fields=[{"gate": gate}],
261
+ )
262
+ return None
@@ -0,0 +1,76 @@
1
+ """Store port seam: protocols plus the default concrete bindings.
2
+
3
+ DECOMP-CORE moves the four project stores behind ports: service
4
+ and engine code binds stores through this module, never by
5
+ importing ``stores`` ad hoc. Protocols declare the seam each
6
+ store serves; ``default_ports`` documents the concrete binding
7
+ the engine ships. Behavior is byte-identical to the direct
8
+ store imports — the seam exists so the next slice can inject
9
+ fakes without touching call sites.
10
+ """
11
+
12
+ from typing import Protocol
13
+
14
+ from ..stores import ecs
15
+ from ..stores import layers
16
+ from ..stores import settings
17
+ from ..stores import wireplace
18
+
19
+
20
+ class EntityPort(Protocol):
21
+ """Entity-store seam the project ledger writes through."""
22
+
23
+ def validate_component_name(self, component):
24
+ """Validate one component name, raising on misuse.
25
+
26
+ Args:
27
+ component: Component name to validate.
28
+ """
29
+
30
+
31
+ class SettingsPort(Protocol):
32
+ """Settings-store seam the project service persists through."""
33
+
34
+ def validate_kind(self, kind):
35
+ """Validate one settings kind, raising on misuse.
36
+
37
+ Args:
38
+ kind: Settings kind to validate.
39
+ """
40
+
41
+
42
+ class WirePort(Protocol):
43
+ """Wireplace seam for wire/junction admission."""
44
+
45
+ def validate_wire(self, wire):
46
+ """Validate one wire payload, raising on misuse.
47
+
48
+ Args:
49
+ wire: Wire payload to validate.
50
+ """
51
+
52
+
53
+ class LayerPort(Protocol):
54
+ """Layer-store seam for stackup and preset serving."""
55
+
56
+ def stackup(self, pdk_id):
57
+ """Return the stackup rows for one PDK id.
58
+
59
+ Args:
60
+ pdk_id: PDK id to serve the stackup for.
61
+ """
62
+
63
+
64
+ def default_ports():
65
+ """Return the concrete store bindings the engine ships.
66
+
67
+ Returns:
68
+ Mapping of port name to the concrete store module
69
+ serving it (ecs, settings, wireplace, layers).
70
+ """
71
+ return {
72
+ "ecs": ecs,
73
+ "settings": settings,
74
+ "wireplace": wireplace,
75
+ "layers": layers,
76
+ }