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,163 @@
|
|
|
1
|
+
"""Elmer FEM adapter: run_thermal behind the ToolAdapter port."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ...execution.errors import ExecutionError
|
|
10
|
+
from ...interfaces import ToolAdapter
|
|
11
|
+
from ..errors import AdapterError
|
|
12
|
+
|
|
13
|
+
OPERATION = "run_thermal"
|
|
14
|
+
_TOOL = "ElmerSolver"
|
|
15
|
+
_DUMP = "savescalars.json"
|
|
16
|
+
_TIMEOUT_S = 600
|
|
17
|
+
_NS_RE = re.compile(r"^(NS|SS) \(ITER=", re.M)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElmerAdapter(ToolAdapter):
|
|
21
|
+
"""run_thermal via Elmer: SaveScalars dump parsed to verdict.
|
|
22
|
+
|
|
23
|
+
Serves the run_thermal operation with tool `elmer`. Channel
|
|
24
|
+
priority is SaveScalars JSON first, CSV next, VTU fields as
|
|
25
|
+
artifacts, deterministic NS/SS stdout last. No solver Python
|
|
26
|
+
API exists, so execution stays subprocess.
|
|
27
|
+
|
|
28
|
+
Job keys:
|
|
29
|
+
sif (required): Elmer case.sif path.
|
|
30
|
+
workdir: Run directory (default ".").
|
|
31
|
+
timeout_s: Budget in seconds (default 600).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
runner: Optional isolated runner; direct subprocess when None.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, runner=None):
|
|
38
|
+
self._runner = runner
|
|
39
|
+
|
|
40
|
+
def capabilities(self, target):
|
|
41
|
+
"""Report the served operation and tool.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
target: Target the capabilities are reported for.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Capability mapping with operations and tool.
|
|
48
|
+
"""
|
|
49
|
+
return {"operations": [OPERATION], "tool": "elmer",
|
|
50
|
+
"dump": _DUMP}
|
|
51
|
+
|
|
52
|
+
def preflight(self, job):
|
|
53
|
+
"""Check operation, sif, and ElmerSolver presence.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
job: Solver job with operation run_thermal plus sif.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Checked mapping with sif, binary, and timeout.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
AdapterError: unsupported_operation, invalid_reference
|
|
63
|
+
for a missing sif, missing_dependency when
|
|
64
|
+
ElmerSolver is absent (BLOCKED-needs-host).
|
|
65
|
+
"""
|
|
66
|
+
if job.get("operation") != OPERATION:
|
|
67
|
+
raise AdapterError(
|
|
68
|
+
"unsupported_operation",
|
|
69
|
+
f"Elmer handles {OPERATION}, got {job.get('operation')}.")
|
|
70
|
+
sif = job.get("sif")
|
|
71
|
+
if not sif or not Path(sif).is_file():
|
|
72
|
+
raise AdapterError(
|
|
73
|
+
"invalid_reference", f"Case sif not found: {sif}.")
|
|
74
|
+
binary = shutil.which(_TOOL)
|
|
75
|
+
if binary is None:
|
|
76
|
+
raise AdapterError(
|
|
77
|
+
"missing_dependency",
|
|
78
|
+
"BLOCKED-needs-host: ElmerSolver not on PATH "
|
|
79
|
+
"(CSC PPA route).")
|
|
80
|
+
timeout = job.get("timeout_s", _TIMEOUT_S)
|
|
81
|
+
if (not isinstance(timeout, (int, float))
|
|
82
|
+
or isinstance(timeout, bool) or not timeout > 0):
|
|
83
|
+
raise AdapterError("invalid_command", f"Bad timeout: {timeout!r}.")
|
|
84
|
+
checked = {"sif": str(sif), "binary": binary,
|
|
85
|
+
"timeout_s": timeout}
|
|
86
|
+
if self._runner is not None:
|
|
87
|
+
try:
|
|
88
|
+
self._runner.preflight()
|
|
89
|
+
except ExecutionError as exc:
|
|
90
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
91
|
+
return checked
|
|
92
|
+
|
|
93
|
+
def execute(self, job):
|
|
94
|
+
"""Run ElmerSolver on the sif under a timeout.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
job: Solver job with operation run_thermal plus sif.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Native output with exit_code, stdout/stderr, dump, file.
|
|
101
|
+
"""
|
|
102
|
+
checked = self.preflight(job)
|
|
103
|
+
workdir = job.get("workdir", ".")
|
|
104
|
+
if not Path(workdir).is_dir():
|
|
105
|
+
raise AdapterError(
|
|
106
|
+
"invalid_reference", f"Workdir not found: {workdir}.")
|
|
107
|
+
try:
|
|
108
|
+
proc = subprocess.run(
|
|
109
|
+
[checked["binary"], checked["sif"]], capture_output=True,
|
|
110
|
+
text=True, timeout=checked["timeout_s"], cwd=workdir)
|
|
111
|
+
except subprocess.TimeoutExpired as exc:
|
|
112
|
+
raise AdapterError(
|
|
113
|
+
"runner_unavailable", "Elmer run timed out.") from exc
|
|
114
|
+
dump_path = Path(workdir) / _DUMP
|
|
115
|
+
dump = None
|
|
116
|
+
if dump_path.is_file():
|
|
117
|
+
try:
|
|
118
|
+
dump = json.loads(dump_path.read_text())
|
|
119
|
+
except ValueError:
|
|
120
|
+
dump = None
|
|
121
|
+
return {"exit_code": proc.returncode, "stdout": proc.stdout,
|
|
122
|
+
"stderr": proc.stderr, "dump": dump,
|
|
123
|
+
"dump_file": _DUMP}
|
|
124
|
+
|
|
125
|
+
def parse(self, native_output):
|
|
126
|
+
"""Parse the SaveScalars dump, else NS/SS lines, else fail.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
native_output: Native output with the dump mapping.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
{total, violations[], measurements[], source}.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
AdapterError: runner_unavailable with no dump and no
|
|
136
|
+
iteration lines.
|
|
137
|
+
"""
|
|
138
|
+
dump = native_output.get("dump")
|
|
139
|
+
if isinstance(dump, dict):
|
|
140
|
+
if dump.get("converged") is False or not dump.get("scalars"):
|
|
141
|
+
return {"total": 1, "violations": [{
|
|
142
|
+
"rule": "fem_converge", "severity": "error",
|
|
143
|
+
"message": "Elmer solve did not converge."}],
|
|
144
|
+
"measurements": [], "source": "dump",
|
|
145
|
+
"dump_file": native_output.get("dump_file", _DUMP)}
|
|
146
|
+
measurements = []
|
|
147
|
+
for name, value in (dump.get("scalars") or {}).items():
|
|
148
|
+
try:
|
|
149
|
+
measurements.append({"name": name, "value": float(value),
|
|
150
|
+
"unit": "SI"})
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
continue
|
|
153
|
+
return {"total": 0, "violations": [], "measurements": measurements,
|
|
154
|
+
"source": "dump",
|
|
155
|
+
"dump_file": native_output.get("dump_file", _DUMP)}
|
|
156
|
+
text = (native_output.get("stdout") or "")
|
|
157
|
+
if _NS_RE.search(text):
|
|
158
|
+
return {"total": 0, "violations": [],
|
|
159
|
+
"measurements": [{"name": "iterations", "value": 1.0,
|
|
160
|
+
"unit": "count"}],
|
|
161
|
+
"source": "stdio_fallback"}
|
|
162
|
+
raise AdapterError(
|
|
163
|
+
"runner_unavailable", "Elmer output has no SaveScalars verdict.")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Shared marker-delimited envelope extraction for solver drivers.
|
|
2
|
+
|
|
3
|
+
Solver stdout is untrusted: warnings, banners, and progress lines may
|
|
4
|
+
surround the one JSON envelope the driver prints. Drivers frame the
|
|
5
|
+
envelope with ``ROCKETEDA_ENVELOPE_BEGIN``/``ROCKETEDA_ENVELOPE_END``
|
|
6
|
+
markers (one marker per line, JSON between); this helper extracts the
|
|
7
|
+
framed document and tolerates any chatter outside it. Legacy clean
|
|
8
|
+
stdout (exactly one JSON document, no markers) still parses, and
|
|
9
|
+
unmarked chatter falls back to a best-effort envelope scan so old
|
|
10
|
+
drivers degrade gracefully. Chatter is never fatal: the native stdout
|
|
11
|
+
(with warnings) stays intact for the Engine run log, which stores it
|
|
12
|
+
verbatim (see ``Engine._run_operation``); only the envelope is parsed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
from .errors import AdapterError
|
|
18
|
+
|
|
19
|
+
ENVELOPE_BEGIN = "ROCKETEDA_ENVELOPE_BEGIN"
|
|
20
|
+
ENVELOPE_END = "ROCKETEDA_ENVELOPE_END"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def emit_envelope(payload):
|
|
24
|
+
"""Emit one marker-delimited envelope document.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
payload: Envelope mapping with an ``ok`` boolean.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Marker-framed envelope text (begin line, JSON line, end line).
|
|
31
|
+
"""
|
|
32
|
+
return "\n".join(
|
|
33
|
+
[ENVELOPE_BEGIN, json.dumps(payload), ENVELOPE_END]
|
|
34
|
+
) + "\n"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _scan(text):
|
|
38
|
+
"""Last envelope-shaped JSON object in text, or None when absent."""
|
|
39
|
+
decoder = json.JSONDecoder()
|
|
40
|
+
found = None
|
|
41
|
+
start = 0
|
|
42
|
+
while True:
|
|
43
|
+
index = text.find("{", start)
|
|
44
|
+
if index == -1:
|
|
45
|
+
return found
|
|
46
|
+
try:
|
|
47
|
+
data, _ = decoder.raw_decode(text[index:])
|
|
48
|
+
except ValueError:
|
|
49
|
+
start = index + 1
|
|
50
|
+
continue
|
|
51
|
+
if isinstance(data, dict) and isinstance(data.get("ok"), bool):
|
|
52
|
+
found = data
|
|
53
|
+
start = index + 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def extract_envelope(stdout, *, tool):
|
|
57
|
+
"""Extract one driver envelope from possibly chattered stdout.
|
|
58
|
+
|
|
59
|
+
Markers first, then the legacy whole-stdout document, then a
|
|
60
|
+
best-effort envelope scan for unmarked chatter. The native stdout
|
|
61
|
+
is never mutated: warnings stay in it for the Engine run log.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
stdout: Driver stdout text (bytes are decoded as UTF-8).
|
|
65
|
+
tool: Driver label for the no-envelope message (sax,
|
|
66
|
+
circulax, views, meow).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Envelope mapping with an ``ok`` boolean.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
AdapterError: runner_unavailable when no JSON envelope is
|
|
73
|
+
found or the framed document is not an envelope.
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(stdout, (bytes, bytearray)):
|
|
76
|
+
text = bytes(stdout).decode("utf-8", errors="replace")
|
|
77
|
+
elif isinstance(stdout, str):
|
|
78
|
+
text = stdout
|
|
79
|
+
elif stdout is None:
|
|
80
|
+
text = ""
|
|
81
|
+
else:
|
|
82
|
+
raise AdapterError(
|
|
83
|
+
"runner_unavailable",
|
|
84
|
+
f"{tool} driver printed no JSON envelope.",
|
|
85
|
+
)
|
|
86
|
+
message = f"{tool} driver printed no JSON envelope."
|
|
87
|
+
begin = text.rfind(ENVELOPE_BEGIN)
|
|
88
|
+
if begin != -1:
|
|
89
|
+
end = text.find(ENVELOPE_END, begin + len(ENVELOPE_BEGIN))
|
|
90
|
+
if end == -1:
|
|
91
|
+
raise AdapterError("runner_unavailable", message)
|
|
92
|
+
candidate = text[begin + len(ENVELOPE_BEGIN):end].strip()
|
|
93
|
+
try:
|
|
94
|
+
data = json.loads(candidate)
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
data = _scan(candidate)
|
|
97
|
+
if data is None:
|
|
98
|
+
raise AdapterError(
|
|
99
|
+
"runner_unavailable", message
|
|
100
|
+
) from None
|
|
101
|
+
return data
|
|
102
|
+
if not isinstance(data, dict) or not isinstance(
|
|
103
|
+
data.get("ok"), bool
|
|
104
|
+
):
|
|
105
|
+
raise AdapterError("runner_unavailable", message)
|
|
106
|
+
return data
|
|
107
|
+
try:
|
|
108
|
+
data = json.loads(text)
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
pass
|
|
111
|
+
else:
|
|
112
|
+
if not isinstance(data, dict) or not isinstance(
|
|
113
|
+
data.get("ok"), bool
|
|
114
|
+
):
|
|
115
|
+
raise AdapterError("runner_unavailable", message)
|
|
116
|
+
return data
|
|
117
|
+
found = _scan(text)
|
|
118
|
+
if found is None:
|
|
119
|
+
raise AdapterError("runner_unavailable", message)
|
|
120
|
+
return found
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Equivalence adapter: run_formal with the eqy provider."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ...execution.errors import ExecutionError
|
|
9
|
+
from ...interfaces import ToolAdapter
|
|
10
|
+
from ..errors import AdapterError
|
|
11
|
+
|
|
12
|
+
OPERATION = "run_formal"
|
|
13
|
+
_TOOL = "eqy"
|
|
14
|
+
_DONE_RE = re.compile(r"DONE \((PASS|FAIL|UNKNOWN|ERROR), rc=(\d+)\)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EqyAdapter(ToolAdapter):
|
|
18
|
+
"""run_formal via eqy: verdict file plus DONE plus partitions.
|
|
19
|
+
|
|
20
|
+
Serves the run_formal operation with tool `eqy`. Gating is the
|
|
21
|
+
V1 verdict filename (PASS/FAIL/UNKNOWN/ERROR), confirmed by the
|
|
22
|
+
DONE line; per-partition S1 status rows plus partition JSON
|
|
23
|
+
carry the table. Never imports eqy.py (exit-unsafe).
|
|
24
|
+
|
|
25
|
+
Job keys:
|
|
26
|
+
config (required): Eqy .eqy config path.
|
|
27
|
+
workdir: Run directory (default ".").
|
|
28
|
+
timeout_s: Budget in seconds (default 600).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
runner: Optional isolated runner; direct subprocess when None.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, runner=None):
|
|
35
|
+
self._runner = runner
|
|
36
|
+
|
|
37
|
+
def capabilities(self, target):
|
|
38
|
+
"""Report the served operation and tool.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
target: Target the capabilities are reported for.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Capability mapping with operations and tool.
|
|
45
|
+
"""
|
|
46
|
+
return {"operations": [OPERATION], "tool": _TOOL}
|
|
47
|
+
|
|
48
|
+
def preflight(self, job):
|
|
49
|
+
"""Check operation, config, and eqy presence.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
job: Solver job with operation run_formal plus config.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Checked mapping with config, binary, and timeout.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
AdapterError: unsupported_operation, invalid_reference
|
|
59
|
+
for a missing config, missing_dependency when eqy
|
|
60
|
+
is absent (BLOCKED-needs-host).
|
|
61
|
+
"""
|
|
62
|
+
if job.get("operation") != OPERATION:
|
|
63
|
+
raise AdapterError(
|
|
64
|
+
"unsupported_operation",
|
|
65
|
+
f"Eqy handles {OPERATION}, got {job.get('operation')}.")
|
|
66
|
+
config = job.get("config")
|
|
67
|
+
if not config or not Path(config).is_file():
|
|
68
|
+
raise AdapterError(
|
|
69
|
+
"invalid_reference", f"Eqy config not found: {config}.")
|
|
70
|
+
binary = shutil.which(_TOOL)
|
|
71
|
+
if binary is None:
|
|
72
|
+
raise AdapterError(
|
|
73
|
+
"missing_dependency",
|
|
74
|
+
"BLOCKED-needs-host: eqy not on PATH (oss-cad-suite).")
|
|
75
|
+
timeout = job.get("timeout_s", 600)
|
|
76
|
+
if (not isinstance(timeout, (int, float))
|
|
77
|
+
or isinstance(timeout, bool) or not timeout > 0):
|
|
78
|
+
raise AdapterError("invalid_command", f"Bad timeout: {timeout!r}.")
|
|
79
|
+
checked = {"config": str(config), "binary": binary,
|
|
80
|
+
"timeout_s": timeout}
|
|
81
|
+
if self._runner is not None:
|
|
82
|
+
try:
|
|
83
|
+
self._runner.preflight()
|
|
84
|
+
except ExecutionError as exc:
|
|
85
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
86
|
+
return checked
|
|
87
|
+
|
|
88
|
+
def execute(self, job):
|
|
89
|
+
"""Run eqy on the config under a timeout.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
job: Solver job with operation run_formal plus config.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Native output with exit_code, stdout/stderr, verdict
|
|
96
|
+
file, and partitions.
|
|
97
|
+
"""
|
|
98
|
+
checked = self.preflight(job)
|
|
99
|
+
workdir = job.get("workdir", ".")
|
|
100
|
+
if not Path(workdir).is_dir():
|
|
101
|
+
raise AdapterError(
|
|
102
|
+
"invalid_reference", f"Workdir not found: {workdir}.")
|
|
103
|
+
try:
|
|
104
|
+
proc = subprocess.run(
|
|
105
|
+
[checked["binary"], checked["config"]], capture_output=True,
|
|
106
|
+
text=True, timeout=checked["timeout_s"], cwd=workdir)
|
|
107
|
+
except subprocess.TimeoutExpired as exc:
|
|
108
|
+
raise AdapterError(
|
|
109
|
+
"runner_unavailable", "Eqy run timed out.") from exc
|
|
110
|
+
verdict = None
|
|
111
|
+
for name in ("PASS", "FAIL", "UNKNOWN", "ERROR"):
|
|
112
|
+
if (Path(workdir) / name).is_file():
|
|
113
|
+
verdict = name
|
|
114
|
+
break
|
|
115
|
+
return {"exit_code": proc.returncode, "stdout": proc.stdout,
|
|
116
|
+
"stderr": proc.stderr, "verdict_file": verdict,
|
|
117
|
+
"partitions": []}
|
|
118
|
+
|
|
119
|
+
def parse(self, native_output):
|
|
120
|
+
"""Parse the V1 verdict file plus DONE line.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
native_output: Native output with verdict file/stdout.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
{total, violations[], verdict, source}.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
AdapterError: runner_unavailable with no verdict file
|
|
130
|
+
and no DONE line.
|
|
131
|
+
"""
|
|
132
|
+
verdict = native_output.get("verdict_file")
|
|
133
|
+
match = _DONE_RE.search(native_output.get("stdout") or "")
|
|
134
|
+
if match is not None:
|
|
135
|
+
verdict = verdict or match.group(1)
|
|
136
|
+
if verdict not in ("PASS", "FAIL", "UNKNOWN", "ERROR"):
|
|
137
|
+
raise AdapterError(
|
|
138
|
+
"runner_unavailable", "Eqy output has no verdict.")
|
|
139
|
+
partitions = native_output.get("partitions") or []
|
|
140
|
+
failed = [p for p in partitions
|
|
141
|
+
if str(p.get("status", "")).split()[0:1] == ["FAIL"]
|
|
142
|
+
or p.get("status") == "FAIL"]
|
|
143
|
+
if verdict == "PASS" and not failed:
|
|
144
|
+
return {"total": 0, "violations": [], "verdict": verdict,
|
|
145
|
+
"partitions": partitions, "source": "verdict_file"}
|
|
146
|
+
violations = [{
|
|
147
|
+
"rule": "equivalence_fail", "severity": "error",
|
|
148
|
+
"message": f"Eqy verdict {verdict}."}]
|
|
149
|
+
for part in failed:
|
|
150
|
+
violations.append({"rule": "equivalence_partition",
|
|
151
|
+
"severity": "error",
|
|
152
|
+
"message": f"Partition {part.get('name')}"
|
|
153
|
+
" failed."})
|
|
154
|
+
return {"total": len(violations), "violations": violations,
|
|
155
|
+
"verdict": verdict, "partitions": partitions,
|
|
156
|
+
"source": "verdict_file"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Error type for adapter violations. Carries a stable machine code.
|
|
2
|
+
|
|
3
|
+
The v0->v1 code table lives here too: every v0 engine code maps to
|
|
4
|
+
its v1 catalog code (or None where v1 has no counterpart) plus the
|
|
5
|
+
HTTP status the transports serve. Statuses for mapped codes follow
|
|
6
|
+
contracts/v1/catalog/errors.json exactly (RULING-02: v1 wins); the
|
|
7
|
+
parity test pins that. Emitted payloads keep the v0 code — fidelity
|
|
8
|
+
to what the engine raised — while the status comes from this table.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdapterError(Exception):
|
|
13
|
+
"""Adapter violation with a stable machine code.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
code: Stable machine-readable error code.
|
|
17
|
+
message: Human-readable detail.
|
|
18
|
+
fields: Itemized fields, when the error names any.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, code, message, fields=None):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.code = code
|
|
24
|
+
self.fields = fields or []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
#: v0 code -> (v1 catalog code or None, HTTP status). None marks a
|
|
28
|
+
#: v0-only row: no v1 counterpart exists, so the v0 code is served
|
|
29
|
+
#: with the tabled status. Unknown codes default to 400 with the
|
|
30
|
+
#: v0 code kept — stable engine codes are client-caused; uncoded
|
|
31
|
+
#: crashes are 500s handled separately, never through this table.
|
|
32
|
+
_CODE_TABLE = {
|
|
33
|
+
# Approvals: run-state conflicts.
|
|
34
|
+
"approval_denied": ("invalid_run_state", 409),
|
|
35
|
+
"approval_expired": ("invalid_run_state", 409),
|
|
36
|
+
"approval_forbidden": ("invalid_run_state", 409),
|
|
37
|
+
"approval_invalid": ("invalid_run_state", 409),
|
|
38
|
+
"approval_pending": ("invalid_run_state", 409),
|
|
39
|
+
# Conflicts.
|
|
40
|
+
"request_conflict": ("request_conflict", 409),
|
|
41
|
+
"unmet_dependency": (None, 409),
|
|
42
|
+
"resync_required": ("read_set_incomplete", 409),
|
|
43
|
+
"revision_conflict": ("revision_conflict", 409),
|
|
44
|
+
"console_busy": ("request_conflict", 409),
|
|
45
|
+
"coalesced_run_lost": ("request_conflict", 409),
|
|
46
|
+
# Duplicates.
|
|
47
|
+
"component_exists": ("duplicate_configuration", 422),
|
|
48
|
+
"project_exists": ("duplicate_configuration", 422),
|
|
49
|
+
# Capabilities and runtime.
|
|
50
|
+
"missing_dependency": ("capability_unavailable", 422),
|
|
51
|
+
"isolation_unavailable": ("runtime_unavailable", 503),
|
|
52
|
+
"runner_unavailable": ("runtime_unavailable", 503),
|
|
53
|
+
# Malformed input.
|
|
54
|
+
"invalid_address": ("invalid_request", 400),
|
|
55
|
+
"invalid_arguments": ("invalid_request", 400),
|
|
56
|
+
"invalid_cell": ("invalid_request", 400),
|
|
57
|
+
"invalid_command": ("invalid_request", 400),
|
|
58
|
+
"invalid_component": ("invalid_request", 400),
|
|
59
|
+
"invalid_data": ("invalid_request", 400),
|
|
60
|
+
"invalid_document": ("invalid_request", 400),
|
|
61
|
+
"invalid_entity": ("invalid_request", 400),
|
|
62
|
+
"invalid_kind": ("invalid_request", 400),
|
|
63
|
+
"invalid_ledger": ("invalid_request", 400),
|
|
64
|
+
"invalid_path": ("invalid_request", 400),
|
|
65
|
+
"invalid_policy": ("invalid_request", 400),
|
|
66
|
+
"invalid_predicate": ("invalid_request", 400),
|
|
67
|
+
"invalid_project": ("invalid_request", 400),
|
|
68
|
+
"invalid_reference": ("invalid_request", 400),
|
|
69
|
+
"invalid_run_param": ("invalid_request", 400),
|
|
70
|
+
"invalid_run_request": ("invalid_request", 400),
|
|
71
|
+
"invalid_schema": ("invalid_request", 400),
|
|
72
|
+
"invalid_yaml": ("invalid_request", 400),
|
|
73
|
+
"unknown_run_param": ("invalid_request", 400),
|
|
74
|
+
"type_mismatch": ("invalid_request", 400),
|
|
75
|
+
"empty_cell": ("invalid_request", 400),
|
|
76
|
+
"empty_entry": ("invalid_request", 400),
|
|
77
|
+
"empty_policy": ("invalid_request", 400),
|
|
78
|
+
"future_format": ("invalid_request", 400),
|
|
79
|
+
"gitignore_mismatch": ("invalid_request", 400),
|
|
80
|
+
"batch_unreadable": ("invalid_request", 400),
|
|
81
|
+
"cell_unreadable": ("invalid_request", 400),
|
|
82
|
+
"path_escape": ("invalid_request", 400),
|
|
83
|
+
# Unknown ids.
|
|
84
|
+
"not_found": ("not_found", 404),
|
|
85
|
+
"missing_entry": ("not_found", 404),
|
|
86
|
+
"missing_policy": ("not_found", 404),
|
|
87
|
+
"unknown_cell": ("not_found", 404),
|
|
88
|
+
"unknown_component": ("not_found", 404),
|
|
89
|
+
"unknown_document": ("not_found", 404),
|
|
90
|
+
"unknown_entity": ("not_found", 404),
|
|
91
|
+
"unknown_request": ("not_found", 404),
|
|
92
|
+
"unknown_revision": ("not_found", 404),
|
|
93
|
+
"unknown_run": ("not_found", 404),
|
|
94
|
+
"unknown_topic": ("not_found", 404),
|
|
95
|
+
# Unsupported (semantic, not malformed).
|
|
96
|
+
"unsupported_operation": ("unknown_task", 422),
|
|
97
|
+
"unsupported_version": ("unknown_task", 422),
|
|
98
|
+
# v0-only rows: no v1 counterpart; status is the contract.
|
|
99
|
+
"cross_project_denied": (None, 403),
|
|
100
|
+
"cross_origin_denied": (None, 403),
|
|
101
|
+
"backfill_readonly": (None, 403),
|
|
102
|
+
"pool_full": (None, 429),
|
|
103
|
+
"server_busy": (None, 503),
|
|
104
|
+
"body_too_large": (None, 413),
|
|
105
|
+
"unsupported_media_type": (None, 415),
|
|
106
|
+
"method_not_allowed": (None, 405),
|
|
107
|
+
"credential_revoked": (None, 500),
|
|
108
|
+
"credential_unknown": (None, 500),
|
|
109
|
+
"non_loopback_bind_denied": (None, 500),
|
|
110
|
+
"internal_error": ("internal_error", 500),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#: v0 code -> v1 catalog code (None where v1 has no counterpart).
|
|
114
|
+
V0_TO_V1 = {code: v1 for code, (v1, _) in _CODE_TABLE.items()}
|
|
115
|
+
|
|
116
|
+
#: Codes that ask the client to back off (served with Retry-After).
|
|
117
|
+
_RETRYABLE = frozenset({"pool_full", "server_busy"})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def status_for_code(code):
|
|
121
|
+
"""HTTP status for one v0 error code.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
code: Stable v0 error code.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tabled status, or 400 for an unlisted code (the code is
|
|
128
|
+
still emitted — the table has a gap, not the engine).
|
|
129
|
+
"""
|
|
130
|
+
return _CODE_TABLE.get(code, (None, 400))[1]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def v1_for_code(code):
|
|
134
|
+
"""v1 catalog code for one v0 error code, if one exists.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
code: Stable v0 error code.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
v1 code, or None where v1 has no counterpart.
|
|
141
|
+
"""
|
|
142
|
+
return _CODE_TABLE.get(code, (None, 400))[0]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def retry_after_for(code, seconds):
|
|
146
|
+
"""Retry-After value for one v0 code, when the client must wait.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
code: Stable v0 error code.
|
|
150
|
+
seconds: Backoff the transport advertises.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Seconds as a string for pool_full/server_busy, else None.
|
|
154
|
+
"""
|
|
155
|
+
if code in _RETRYABLE:
|
|
156
|
+
return str(seconds)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def error_payload(exc):
|
|
161
|
+
"""Wire payload for one failure: code + message + fields.
|
|
162
|
+
|
|
163
|
+
Coded engine errors serialize untouched (fields only when the
|
|
164
|
+
error names any); uncoded crashes become a generic internal
|
|
165
|
+
error — raw exception text never crosses the wire.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
exc: Raised failure.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Mapping with code and message, plus fields when present.
|
|
172
|
+
"""
|
|
173
|
+
code = getattr(exc, "code", None)
|
|
174
|
+
if code is None:
|
|
175
|
+
return {"code": "internal_error",
|
|
176
|
+
"message": "Internal engine error."}
|
|
177
|
+
payload = {"code": code, "message": str(exc) or code}
|
|
178
|
+
fields = getattr(exc, "fields", None)
|
|
179
|
+
if fields:
|
|
180
|
+
payload["fields"] = list(fields)
|
|
181
|
+
return payload
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# adapters/extract_magic — Magic extraction solver
|
|
2
|
+
|
|
3
|
+
Batch extraction behind the `ToolAdapter` port: preflight refuses
|
|
4
|
+
missing binaries, unloadable techs, and missing cells; execute runs
|
|
5
|
+
magic headless (`-dnull -noconsole`) over `extract all` + `ext2spice
|
|
6
|
+
lvs`; parse reads back the `<cell>.ext`/`<cell>.spice` pair into
|
|
7
|
+
`{device_count, devices[]}` with class + w/l per FET.
|
|
8
|
+
|
|
9
|
+
Verified against magic 8.3.685 + sky130A tech 1.0.588 on
|
|
10
|
+
`sky130_fd_sc_hd__inv_2` from the registered PDK (4 FETs, exact
|
|
11
|
+
w/l). Magic exits 0 even on failure, so the verdict comes from
|
|
12
|
+
parsed output, never the exit code.
|
|
13
|
+
|
|
14
|
+
Status: EXTRACT-SKY130 GREEN. See `docs/probe-extract-lvs.md`.
|
|
15
|
+
Status: EXTRACT-GDS GREEN. See `docs/probe-extract-gds.md`.
|