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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|