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,2279 @@
|
|
|
1
|
+
"""Composition root (B9): the Engine wires core modules for adapters.
|
|
2
|
+
|
|
3
|
+
Adapters construct against this, never against services directly. The
|
|
4
|
+
root owns no domain logic: it routes cataloged names (see
|
|
5
|
+
`interfaces.vocabulary`) to live module state. Operations queue through
|
|
6
|
+
the project service with revision gating and envelope-id idempotency;
|
|
7
|
+
CRUD mutations (CRUD-API/I31) and rewind (REWIND) execute immediately
|
|
8
|
+
through the same admission machinery with no queue and no solver;
|
|
9
|
+
every accepted mutation advances the revision and appends an
|
|
10
|
+
actor-attributed event to the durable ledger (LEDGER-DURABLE/I30),
|
|
11
|
+
and every run appends its completion with a verdict link. Only wired
|
|
12
|
+
queries read state, the rest honestly report unwired.
|
|
13
|
+
Submit-immediate execution (API-GENERALIZE): `submit(op)` admits and
|
|
14
|
+
runs inline, returning the ack shape plus the verdict payload under
|
|
15
|
+
the one run id; `run_job` stays for explicit/deferred/batch pumping
|
|
16
|
+
plus idempotent result fetch — over `run()` directly or over
|
|
17
|
+
submit-only transports via `submit(run_job)`. Replays return the
|
|
18
|
+
stored completion without re-executing, and a fresh Engine reports
|
|
19
|
+
crash survivors once (boot report, never executed) on its first
|
|
20
|
+
mutating call; every replay surface stays gated behind an explicit
|
|
21
|
+
resume decision (REPLAY-SAFE).
|
|
22
|
+
With `constraints` (metric -> {min, max, unit[, expr, control]}),
|
|
23
|
+
run_spice targets come from the project, never the caller:
|
|
24
|
+
caller-supplied min/max — or a caller expr where the project defines
|
|
25
|
+
one — is refused.
|
|
26
|
+
`run()` executes registered solvers synchronously. Each operation
|
|
27
|
+
maps to named solvers; `deck.tool` selects one, defaulting to the
|
|
28
|
+
first registered (run_drc: magic, then klayout; run_optical: sax,
|
|
29
|
+
then circulax; run_em: meow; run_extraction: magic; run_lvs:
|
|
30
|
+
netgen; run_spice: ngspice;
|
|
31
|
+
run_post_layout: ngspice; run_syntax: slang; run_hdl_test: icarus;
|
|
32
|
+
run_synth: yosys; run_connectivity: connectgraph; run_erc: connectgraph;
|
|
33
|
+
run_formal: sby; run_view_sync: connectgraph;
|
|
34
|
+
generate_views: gdsfactory; run_cosim: cosim; run_timing: opensta;
|
|
35
|
+
run_digital_flow: librelane; run_xschem: xschem; run_antenna: klayout).
|
|
36
|
+
Unregistered operations and unknown deck tools raise instead of
|
|
37
|
+
faking a verdict.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import uuid
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from .adapters.circulax.adapter import CirculaxAdapter
|
|
45
|
+
from .adapters.connectivity import ConnectivityAdapter
|
|
46
|
+
from .adapters.cosim import CosimAdapter
|
|
47
|
+
from .adapters.digital_flow import LibrelaneFlowAdapter
|
|
48
|
+
from .adapters.drc.magic import MagicDrcAdapter
|
|
49
|
+
from .adapters.drc.klayout import AntennaKlayoutAdapter, KlayoutDrcAdapter
|
|
50
|
+
from .adapters.extract_magic import MagicExtractAdapter
|
|
51
|
+
from .adapters.formal_sby import FormalSbyAdapter
|
|
52
|
+
from .adapters.gdsfactory.adapter import GdsfactoryViewsAdapter
|
|
53
|
+
from .adapters.hdl_sim import HdlSimAdapter
|
|
54
|
+
from .adapters.lvs_netgen import NetgenLvsAdapter
|
|
55
|
+
from .adapters.meow.adapter import MeowAdapter
|
|
56
|
+
from .adapters.ngspice import NgspiceAdapter
|
|
57
|
+
from .adapters.postlayout.adapter import PostLayoutAdapter
|
|
58
|
+
from .adapters.sax import SaxAdapter
|
|
59
|
+
from .adapters.syntax import SyntaxAdapter
|
|
60
|
+
from .adapters.synth_yosys import YosysSynthAdapter
|
|
61
|
+
from .adapters.timing_sta import OpenStaTimingAdapter
|
|
62
|
+
from .adapters.view_sync import ViewSyncAdapter
|
|
63
|
+
from .adapters.xschem import XschemAdapter
|
|
64
|
+
from .execution.console.lifecycle import ConsoleLifecycle
|
|
65
|
+
from .execution.apig.pool import (
|
|
66
|
+
DEFAULT_MAX_WORKERS,
|
|
67
|
+
ApigPool,
|
|
68
|
+
request_for_run,
|
|
69
|
+
stored_completion_from_events,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
#: Batch drain fan-out for independent solver items (BATCH-PARALLEL).
|
|
73
|
+
DEFAULT_BATCH_WORKERS = DEFAULT_MAX_WORKERS
|
|
74
|
+
from .execution.apig.submit import merge_ack_completion
|
|
75
|
+
from .execution.block_runs_scope import (
|
|
76
|
+
BlockScopeError,
|
|
77
|
+
build_scope_record,
|
|
78
|
+
check_operation_scope,
|
|
79
|
+
narrow_job_for_block,
|
|
80
|
+
parse_target,
|
|
81
|
+
resolve_block,
|
|
82
|
+
snapshot_closure,
|
|
83
|
+
stamp_verdict,
|
|
84
|
+
)
|
|
85
|
+
from .execution.coalesce import Coalescer, content_key
|
|
86
|
+
from .execution.dispatch import OpDispatcher
|
|
87
|
+
from .execution.errors import ExecutionError
|
|
88
|
+
from .execution.python_exec import PythonExecService
|
|
89
|
+
from .execution.replay_safe import ReplaySafe, content_key_for
|
|
90
|
+
from .execution.service import ExecutionService
|
|
91
|
+
from .interfaces import apig_assets
|
|
92
|
+
from .interfaces.celllink import resolve_run as resolve_celllink_run
|
|
93
|
+
from .interfaces.errors import ContractError
|
|
94
|
+
from .stores.layers import (
|
|
95
|
+
PresetStore,
|
|
96
|
+
get_stackup,
|
|
97
|
+
viewer_rows,
|
|
98
|
+
viewer_rows_for_preset,
|
|
99
|
+
)
|
|
100
|
+
from .interfaces.runparams import RunParamsError, validate_run_request
|
|
101
|
+
from .interfaces.setup import report_capabilities
|
|
102
|
+
from .interfaces.vocabulary import OPERATIONS, QUERIES
|
|
103
|
+
from .interfaces.yaml_dump import dump_yaml_text
|
|
104
|
+
from .library.apig_catalog import (
|
|
105
|
+
ApigCatalog,
|
|
106
|
+
gdsfactory_provider,
|
|
107
|
+
static_provider,
|
|
108
|
+
)
|
|
109
|
+
from .library.catalog import Catalog
|
|
110
|
+
from .library.errors import LibraryError
|
|
111
|
+
from .execution.ports import BOOT_PROFILE, PortBus
|
|
112
|
+
from .execution.runtime import EngineRuntime
|
|
113
|
+
from .project.errors import ProjectError
|
|
114
|
+
from .project.service import ProjectService
|
|
115
|
+
from .workflows.completion import design_completion_report, plan_checks_report
|
|
116
|
+
from .ops.deferred import _DeferredMixin
|
|
117
|
+
from .ops.editors import _EditorMixin
|
|
118
|
+
from .ops.submit import _SubmitMixin
|
|
119
|
+
from .ops.queries import _QueryMixin
|
|
120
|
+
from .ops.runs import _RunMixin, refuse_smuggled_budget
|
|
121
|
+
from .ops.gates import _GateMixin, check_submit_gates
|
|
122
|
+
from .ops.evidence import (
|
|
123
|
+
confine_spice_arguments as _confine_spice_arguments,
|
|
124
|
+
)
|
|
125
|
+
from .ops.evidence import (
|
|
126
|
+
evidence_source_state as _evidence_source_state,
|
|
127
|
+
)
|
|
128
|
+
from .ops.evidence import (
|
|
129
|
+
maybe_record_evidence as _maybe_record_evidence,
|
|
130
|
+
)
|
|
131
|
+
from .ops.evidence import (
|
|
132
|
+
record_run_evidence as _record_run_evidence,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
_WIRED_QUERIES = frozenset(
|
|
136
|
+
{
|
|
137
|
+
"get_project_snapshot",
|
|
138
|
+
"get_capabilities",
|
|
139
|
+
"get_events_since",
|
|
140
|
+
"list_projects",
|
|
141
|
+
"list_docs",
|
|
142
|
+
"read_doc",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Wired query -> ProjectService method serving it. Engine.submit keeps
|
|
147
|
+
# its explicit dispatch (argument checks differ per query); this map
|
|
148
|
+
# is the docs/help seam — scripts/api_spec.py and the MCP server read
|
|
149
|
+
# handler docstrings through it, so a rewired query updates here.
|
|
150
|
+
QUERY_METHODS = {
|
|
151
|
+
"get_project_snapshot": "snapshot",
|
|
152
|
+
"get_events_since": "events_since",
|
|
153
|
+
"list_projects": "list_projects",
|
|
154
|
+
"list_docs": "list_docs",
|
|
155
|
+
"read_doc": "read_doc",
|
|
156
|
+
"get_component": "get_component",
|
|
157
|
+
"components_of": "components_of",
|
|
158
|
+
"entities_with": "entities_with",
|
|
159
|
+
"get_world_snapshot": "world_snapshot",
|
|
160
|
+
"read_settings": "read_settings",
|
|
161
|
+
"get_resolved_config": "resolved_config",
|
|
162
|
+
"read_entity_revision": "entity_revision",
|
|
163
|
+
"list_sheets": "entities_with",
|
|
164
|
+
"get_sheet": "get_component",
|
|
165
|
+
"get_instance": "get_component",
|
|
166
|
+
"instances_on_sheet": "entities_with",
|
|
167
|
+
"list_symbols": "entities_with",
|
|
168
|
+
"get_symbol": "get_component",
|
|
169
|
+
"get_cell": "get_component",
|
|
170
|
+
"list_layers": "read_doc",
|
|
171
|
+
"check_capability": "capability_report",
|
|
172
|
+
"ip_search": "entities_with",
|
|
173
|
+
"ip_get": "get_component",
|
|
174
|
+
"connection_graph": "graph_report",
|
|
175
|
+
"net_occurrences": "graph_report",
|
|
176
|
+
"xprobe": "probe_report",
|
|
177
|
+
"resolve_pin": "probe_report",
|
|
178
|
+
"plot_export": "plot_report",
|
|
179
|
+
"erc_check": "erc_report",
|
|
180
|
+
"write_netlist": "netlist_report",
|
|
181
|
+
"update_netlist": "netlist_report",
|
|
182
|
+
"list_testbenches": "entities_with",
|
|
183
|
+
"describe_testbench": "testbench_report",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Wired query -> Engine method serving it. Same docs/help seam as
|
|
187
|
+
# QUERY_METHODS, for the queries the composition root answers
|
|
188
|
+
# itself (solver + store introspection the project ledger cannot
|
|
189
|
+
# serve). scripts/api_spec.py and the MCP server read Engine
|
|
190
|
+
# handler docstrings through this map.
|
|
191
|
+
QUERY_ENGINE_METHODS = {
|
|
192
|
+
"get_capabilities": "capabilities",
|
|
193
|
+
"get_python_console": "python_console",
|
|
194
|
+
"api_search": "api_search",
|
|
195
|
+
"api_info": "api_info",
|
|
196
|
+
"api_help": "api_help",
|
|
197
|
+
"get_job_status": "job_status",
|
|
198
|
+
"get_check_results": "check_results",
|
|
199
|
+
"get_logs": "run_logs",
|
|
200
|
+
"get_findings": "findings",
|
|
201
|
+
"list_job_kinds": "job_kinds",
|
|
202
|
+
"get_python_console": "python_console",
|
|
203
|
+
"list_cells": "catalog_cells",
|
|
204
|
+
"serve_asset": "serve_asset",
|
|
205
|
+
"list_assets": "list_assets",
|
|
206
|
+
"list_artifacts": "list_artifacts",
|
|
207
|
+
"get_layer_stackup": "layer_stackup",
|
|
208
|
+
"get_layer_rows": "layer_rows",
|
|
209
|
+
"list_layer_presets": "layer_presets",
|
|
210
|
+
"save_layer_preset": "layer_preset_save",
|
|
211
|
+
"delete_layer_preset": "layer_preset_delete",
|
|
212
|
+
"apply_layer_preset": "layer_preset_apply",
|
|
213
|
+
"list_survivors": "boot_report",
|
|
214
|
+
"plan_checks": "plan_checks",
|
|
215
|
+
"get_design_completion": "design_completion",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Job-result queries served from execution state (plus the captured run log).
|
|
219
|
+
_JOB_QUERIES = frozenset(
|
|
220
|
+
{"get_job_status", "get_check_results", "get_logs", "get_findings",
|
|
221
|
+
"list_job_kinds"}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Catalogue + raw-asset queries served from engine state (the cell
|
|
225
|
+
# index and the PDK store); read-only, never replay the backlog.
|
|
226
|
+
_APIG_QUERIES = frozenset({"list_cells", "serve_asset", "list_assets"})
|
|
227
|
+
|
|
228
|
+
# Replay-safety query (REPLAY-SAFE): the boot survivor report, served
|
|
229
|
+
# from ledger state. Report-only — never executes, only records
|
|
230
|
+
# idempotent interruption marks for newly found outages.
|
|
231
|
+
_REPLAY_QUERIES = frozenset({"list_survivors"})
|
|
232
|
+
|
|
233
|
+
# Layer-serving queries (LAYER-SERVING): stackup/rows/presets served
|
|
234
|
+
# from the PDK store plus Engine-memory user presets (PresetStore).
|
|
235
|
+
_LAYER_QUERIES = frozenset(
|
|
236
|
+
{
|
|
237
|
+
"get_layer_stackup",
|
|
238
|
+
"get_layer_rows",
|
|
239
|
+
"list_layer_presets",
|
|
240
|
+
"save_layer_preset",
|
|
241
|
+
"delete_layer_preset",
|
|
242
|
+
"apply_layer_preset",
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Completion queries (I27-GATE): stage plans plus the fully-verified
|
|
247
|
+
# gate, served from the solver registry and the latest check verdicts.
|
|
248
|
+
_COMPLETION_QUERIES = frozenset({"plan_checks", "get_design_completion"})
|
|
249
|
+
|
|
250
|
+
# A get_logs page never dumps more than this many lines at once.
|
|
251
|
+
_LOG_PAGE_MAX = 200
|
|
252
|
+
|
|
253
|
+
# Engine-served operations: submit-branch job control with no solver.
|
|
254
|
+
# The api_spec seam reads handler docstrings through this map, like
|
|
255
|
+
# QUERY_ENGINE_METHODS for queries — a rewired op updates here, and
|
|
256
|
+
# scripts/api_spec.py plus the MCP server follow with no edits.
|
|
257
|
+
ENGINE_OP_METHODS = {
|
|
258
|
+
"resume_run": "resume_survivor",
|
|
259
|
+
"discard_run": "discard_survivor",
|
|
260
|
+
"resume_all_retryable": "resume_all_retryable",
|
|
261
|
+
"cancel_job": "cancel_job",
|
|
262
|
+
"python_open": "open_console",
|
|
263
|
+
"python_reset": "reset_console",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# CRUD mutations + rewind execute immediately inside submit()
|
|
267
|
+
# (run_job precedent): revision gate + idempotency + event logging,
|
|
268
|
+
# no queue, no solver. Engine.run delegates them back to submit so
|
|
269
|
+
# the CLI `run` path shares the one implementation. Wire ops
|
|
270
|
+
# (WIRE-PLACE) ride the same immediate path once the ENGINE-ECS
|
|
271
|
+
# store lands; without it they report unsupported.
|
|
272
|
+
_CRUD_OPERATIONS = frozenset(
|
|
273
|
+
{
|
|
274
|
+
"create_project",
|
|
275
|
+
"write_doc",
|
|
276
|
+
"delete_doc",
|
|
277
|
+
"apply_mutations",
|
|
278
|
+
"rewind",
|
|
279
|
+
"create_entity",
|
|
280
|
+
"set_component",
|
|
281
|
+
"attach_component",
|
|
282
|
+
"detach_component",
|
|
283
|
+
"delete_entity",
|
|
284
|
+
"write_settings",
|
|
285
|
+
"place_wire",
|
|
286
|
+
"place_junction",
|
|
287
|
+
"connect_endpoint",
|
|
288
|
+
"move_wire",
|
|
289
|
+
"split_wire",
|
|
290
|
+
"delete_wire",
|
|
291
|
+
"delete_junction",
|
|
292
|
+
"materialize_nets",
|
|
293
|
+
"apply_transaction",
|
|
294
|
+
"create_shape",
|
|
295
|
+
"create_sheet",
|
|
296
|
+
"place_instance",
|
|
297
|
+
"bind_block",
|
|
298
|
+
"create_symbol",
|
|
299
|
+
"create_cell",
|
|
300
|
+
"map_pins",
|
|
301
|
+
"ip_register", "ip_instantiate", "ip_promote",
|
|
302
|
+
"bind_layers",
|
|
303
|
+
"annotate",
|
|
304
|
+
"create_testbench",
|
|
305
|
+
"regenerate_testbench",
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Wave-3 reads needing typed Engine dispatch (argument defaults
|
|
310
|
+
# and assembly the generic component seams do not provide). The
|
|
311
|
+
# docs seam stays QUERY_METHODS; this set only routes submit to
|
|
312
|
+
# the _EditorMixin handler.
|
|
313
|
+
_EDITOR_READS = frozenset(
|
|
314
|
+
{
|
|
315
|
+
"read_entity_revision",
|
|
316
|
+
"list_sheets",
|
|
317
|
+
"get_sheet",
|
|
318
|
+
"get_instance",
|
|
319
|
+
"instances_on_sheet",
|
|
320
|
+
"list_symbols",
|
|
321
|
+
"get_symbol",
|
|
322
|
+
"get_cell",
|
|
323
|
+
"list_layers",
|
|
324
|
+
"check_capability",
|
|
325
|
+
"ip_search",
|
|
326
|
+
"ip_get",
|
|
327
|
+
"connection_graph",
|
|
328
|
+
"net_occurrences",
|
|
329
|
+
"xprobe",
|
|
330
|
+
"resolve_pin",
|
|
331
|
+
"plot_export",
|
|
332
|
+
"erc_check",
|
|
333
|
+
"write_netlist",
|
|
334
|
+
"update_netlist",
|
|
335
|
+
"list_testbenches",
|
|
336
|
+
"describe_testbench",
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Adapter violation key -> finding location key. bbox keeps its legacy
|
|
341
|
+
# lambda-units name; everything else passes through unchanged.
|
|
342
|
+
_LOCATION_KEYS = (
|
|
343
|
+
("bbox", "bbox_lambda"),
|
|
344
|
+
("bbox_um", "bbox_um"),
|
|
345
|
+
("area", "area"),
|
|
346
|
+
("file", "file"),
|
|
347
|
+
("line", "line"),
|
|
348
|
+
("col", "col"),
|
|
349
|
+
("sim_time", "sim_time"),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class Engine(
|
|
354
|
+
_SubmitMixin,
|
|
355
|
+
_QueryMixin,
|
|
356
|
+
_RunMixin,
|
|
357
|
+
_GateMixin,
|
|
358
|
+
_EditorMixin,
|
|
359
|
+
_DeferredMixin,
|
|
360
|
+
):
|
|
361
|
+
"""Composition root: one bound project plus its solvers and services.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
project_id: Project this engine is bound to; cross-project
|
|
365
|
+
envelopes are denied, never routed.
|
|
366
|
+
actor: Attribution for mutations this engine submits
|
|
367
|
+
(human / rocket-fuel / system); passed through to every
|
|
368
|
+
ledger event verbatim.
|
|
369
|
+
catalog_root: Root the component catalog resolves against.
|
|
370
|
+
engine_root: Engine root for ledger durability; the log
|
|
371
|
+
persists under `projects/<project_id>/` beneath it.
|
|
372
|
+
None keeps the in-memory ledger.
|
|
373
|
+
linked_root: Caller tree for linked projects; rewind
|
|
374
|
+
reports files outside `.rocket/` and never writes
|
|
375
|
+
there. None means a managed project.
|
|
376
|
+
constraints: Optional run_spice metric targets
|
|
377
|
+
({metric: {min, max, unit[, expr, control]}}); caller
|
|
378
|
+
targets are refused, unknown metrics fail loudly.
|
|
379
|
+
blocks: Optional block store for block-scoped runs (a
|
|
380
|
+
mapping of id to block entity, or a list of block
|
|
381
|
+
mappings); runs without a block target never read it.
|
|
382
|
+
pool_max_depth: Maximum undrained execution backlog;
|
|
383
|
+
admissions past it are refused, never queued.
|
|
384
|
+
project_dir: Project folder python_execute path mode
|
|
385
|
+
resolves inside; path mode refuses loudly without one.
|
|
386
|
+
The same folder also scopes run_spice source
|
|
387
|
+
confinement and the results/engine-runs evidence trail,
|
|
388
|
+
so every transport confines and records identically.
|
|
389
|
+
None keeps the engine in-memory (no confinement, no
|
|
390
|
+
evidence files).
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, project_id, actor, catalog_root=".",
|
|
394
|
+
engine_root=None, linked_root=None, constraints=None,
|
|
395
|
+
blocks=None, pool_max_depth=64, project_dir=None):
|
|
396
|
+
self._project_id = project_id
|
|
397
|
+
self._actor = actor
|
|
398
|
+
self._project_dir = project_dir
|
|
399
|
+
self._blocks = blocks if blocks is not None else {}
|
|
400
|
+
self._project = ProjectService(
|
|
401
|
+
project_id, actor, engine_root=engine_root,
|
|
402
|
+
linked_root=linked_root,
|
|
403
|
+
)
|
|
404
|
+
self._execution = ExecutionService(project_id)
|
|
405
|
+
self._coalescer = Coalescer()
|
|
406
|
+
self._layer_presets = PresetStore()
|
|
407
|
+
self._catalog = Catalog(catalog_root)
|
|
408
|
+
self._solvers = {
|
|
409
|
+
"run_drc": {"magic": MagicDrcAdapter(), "klayout": KlayoutDrcAdapter()},
|
|
410
|
+
"run_hdl_test": {"icarus": HdlSimAdapter()},
|
|
411
|
+
"run_optical": {"sax": SaxAdapter(), "circulax": CirculaxAdapter()},
|
|
412
|
+
"run_em": {"meow": MeowAdapter()},
|
|
413
|
+
"run_extraction": {"magic": MagicExtractAdapter()},
|
|
414
|
+
"run_lvs": {"netgen": NetgenLvsAdapter()},
|
|
415
|
+
"run_spice": {"ngspice": NgspiceAdapter()},
|
|
416
|
+
"run_post_layout": {"ngspice": PostLayoutAdapter()},
|
|
417
|
+
"run_syntax": {"slang": SyntaxAdapter()},
|
|
418
|
+
"run_synth": {"yosys": YosysSynthAdapter()},
|
|
419
|
+
"run_connectivity": {"connectgraph": ConnectivityAdapter()},
|
|
420
|
+
"run_erc": {"connectgraph": ConnectivityAdapter()},
|
|
421
|
+
"run_formal": {"sby": FormalSbyAdapter()},
|
|
422
|
+
"run_view_sync": {"connectgraph": ViewSyncAdapter()},
|
|
423
|
+
"generate_views": {"gdsfactory": GdsfactoryViewsAdapter()},
|
|
424
|
+
"run_cosim": {"cosim": CosimAdapter()},
|
|
425
|
+
"run_digital_flow": {"librelane": LibrelaneFlowAdapter()},
|
|
426
|
+
"run_timing": {"opensta": OpenStaTimingAdapter()},
|
|
427
|
+
"run_xschem": {"xschem": XschemAdapter()},
|
|
428
|
+
"run_antenna": {"klayout": AntennaKlayoutAdapter()},
|
|
429
|
+
}
|
|
430
|
+
self._constraints = dict(constraints) if constraints is not None else None
|
|
431
|
+
self._logs = {}
|
|
432
|
+
self._datasets = {}
|
|
433
|
+
self._spectra = {}
|
|
434
|
+
self._latest = {}
|
|
435
|
+
self._run_jobs = {}
|
|
436
|
+
self._pool = ApigPool(
|
|
437
|
+
self._project, self._pool_execute, max_depth=pool_max_depth,
|
|
438
|
+
max_workers=DEFAULT_BATCH_WORKERS,
|
|
439
|
+
)
|
|
440
|
+
self._replay = ReplaySafe(self._project, self._replay_execute)
|
|
441
|
+
self._cells = ApigCatalog(
|
|
442
|
+
catalog_root, providers=[gdsfactory_provider()]
|
|
443
|
+
)
|
|
444
|
+
self._replayed = False
|
|
445
|
+
self._pyexec = PythonExecService(
|
|
446
|
+
project_id, actor,
|
|
447
|
+
execution=self._execution,
|
|
448
|
+
project_root=project_dir,
|
|
449
|
+
dispatcher=OpDispatcher(
|
|
450
|
+
self, project_id=project_id, actor=actor),
|
|
451
|
+
)
|
|
452
|
+
self._consoles = ConsoleLifecycle()
|
|
453
|
+
self._deferred_receipts = {}
|
|
454
|
+
self._deferred = EngineRuntime(
|
|
455
|
+
project_id, runner=self._deferred_runner,
|
|
456
|
+
auto_dispatch=False,
|
|
457
|
+
)
|
|
458
|
+
# Step 7A: one shared Engine per process with many
|
|
459
|
+
# attached ports. Boot profile is stdio alone; the
|
|
460
|
+
# runtime toggle attaches MCP / loopback-HTTP live.
|
|
461
|
+
self._port_bus = PortBus()
|
|
462
|
+
for boot_port in BOOT_PROFILE:
|
|
463
|
+
self._port_bus.attach(boot_port, push=True)
|
|
464
|
+
self._port_cursor = 0
|
|
465
|
+
|
|
466
|
+
def snapshot(self):
|
|
467
|
+
"""Read the current project snapshot.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Snapshot mapping for the bound project.
|
|
471
|
+
"""
|
|
472
|
+
return self._project.snapshot()
|
|
473
|
+
|
|
474
|
+
def events_since(self, cursor, require_contiguous_from=None):
|
|
475
|
+
"""Read ledger events after a cursor.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
cursor: Opaque position from a previous read.
|
|
479
|
+
require_contiguous_from: Assert contiguous history from
|
|
480
|
+
this seq; unmet demands fail instead of gap-hiding.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Events appended after the cursor, oldest first.
|
|
484
|
+
|
|
485
|
+
Raises:
|
|
486
|
+
ProjectError: resync_required when the contiguity
|
|
487
|
+
demand cannot be met — re-read the snapshot plus
|
|
488
|
+
the full retained log and reset the cursor.
|
|
489
|
+
"""
|
|
490
|
+
return self._project.events_since(
|
|
491
|
+
cursor, require_contiguous_from=require_contiguous_from
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def sync_ports(self):
|
|
495
|
+
"""Fan newly appended ledger events out to the ports.
|
|
496
|
+
|
|
497
|
+
Pulls the project ledger since the last synced cursor
|
|
498
|
+
and ingests the batch into the shared port bus. Every
|
|
499
|
+
successful Engine.submit mutation path syncs
|
|
500
|
+
automatically; direct ProjectService or batch callers
|
|
501
|
+
sync explicitly through this call.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Sync record with the ingested count, per-port
|
|
505
|
+
enqueue counts, and the bus cursor.
|
|
506
|
+
|
|
507
|
+
Caution: Sync is pull-and-fan-out, never a merge — a
|
|
508
|
+
quarantined port converges only through
|
|
509
|
+
port_resync, never through this call.
|
|
510
|
+
"""
|
|
511
|
+
events = self._project.events_since(self._port_cursor)
|
|
512
|
+
counts = self._port_bus.ingest(events)
|
|
513
|
+
if events:
|
|
514
|
+
self._port_cursor = max(
|
|
515
|
+
event["seq"] for event in events)
|
|
516
|
+
return {"ingested": len(events), "per_port": counts,
|
|
517
|
+
"cursor": self._port_bus.cursor}
|
|
518
|
+
|
|
519
|
+
def attach_port(self, name, push=True, max_depth=None):
|
|
520
|
+
"""Attach one port to the shared world.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
name: Port name (stdio, mcp, ide, loopback-http).
|
|
524
|
+
push: True for async push delivery; False leaves
|
|
525
|
+
the port pull-only over get_events_since.
|
|
526
|
+
max_depth: Per-port queue ceiling; the bus default
|
|
527
|
+
when omitted.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Attached port descriptor.
|
|
531
|
+
|
|
532
|
+
Raises:
|
|
533
|
+
ExecutionError: invalid_command for a bad name,
|
|
534
|
+
port_exists for an attached name.
|
|
535
|
+
"""
|
|
536
|
+
return self._port_bus.attach(name, push=push,
|
|
537
|
+
max_depth=max_depth)
|
|
538
|
+
|
|
539
|
+
def detach_port(self, name):
|
|
540
|
+
"""Detach one port; its backlog is dropped with it.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
name: Attached port name to remove.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Detached port descriptor.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
ExecutionError: unknown_port for a detached name.
|
|
550
|
+
"""
|
|
551
|
+
return self._port_bus.detach(name)
|
|
552
|
+
|
|
553
|
+
def port_state(self, name):
|
|
554
|
+
"""Read one port's engine-side bookkeeping.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
name: Attached port name.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Descriptor with name, push, enabled, state,
|
|
561
|
+
resync_required, backlog, cursor, acked, and
|
|
562
|
+
max_depth.
|
|
563
|
+
|
|
564
|
+
Raises:
|
|
565
|
+
ExecutionError: unknown_port for a detached name.
|
|
566
|
+
"""
|
|
567
|
+
return self._port_bus.port_state(name)
|
|
568
|
+
|
|
569
|
+
def port_pending(self, name):
|
|
570
|
+
"""Read one port's queued ledger events.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
name: Attached port name.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Queued event copies, oldest first.
|
|
577
|
+
|
|
578
|
+
Raises:
|
|
579
|
+
ExecutionError: unknown_port for a detached name.
|
|
580
|
+
"""
|
|
581
|
+
return self._port_bus.pending(name)
|
|
582
|
+
|
|
583
|
+
def port_ack(self, name, seq):
|
|
584
|
+
"""Acknowledge delivery through one ledger position.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
name: Attached port name.
|
|
588
|
+
seq: Ledger position acknowledged.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Ack record with port, acked position, and backlog.
|
|
592
|
+
|
|
593
|
+
Raises:
|
|
594
|
+
ExecutionError: unknown_port for a detached name,
|
|
595
|
+
invalid_command for a non-integer position.
|
|
596
|
+
"""
|
|
597
|
+
return self._port_bus.ack(name, seq)
|
|
598
|
+
|
|
599
|
+
def port_drain(self, name, handler, limit=None,
|
|
600
|
+
guarded=True):
|
|
601
|
+
"""Deliver one port's backlog through a handler.
|
|
602
|
+
|
|
603
|
+
Every position routes through the port's bound
|
|
604
|
+
position guard, so redelivery applies once while
|
|
605
|
+
every delivery still advances the ack.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
name: Attached port name.
|
|
609
|
+
handler: Callable taking one event mapping.
|
|
610
|
+
limit: Maximum events to deliver; the whole
|
|
611
|
+
backlog when omitted.
|
|
612
|
+
guarded: True applies each position once through
|
|
613
|
+
the port's bound guard.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Drain record with delivered and applied counts,
|
|
617
|
+
the acked position, and handler errors.
|
|
618
|
+
|
|
619
|
+
Raises:
|
|
620
|
+
ExecutionError: unknown_port for a detached name.
|
|
621
|
+
"""
|
|
622
|
+
return self._port_bus.drain(name, handler, limit=limit,
|
|
623
|
+
guarded=guarded)
|
|
624
|
+
|
|
625
|
+
def port_evaluate(self):
|
|
626
|
+
"""Run the staged no-ack ladder over every port.
|
|
627
|
+
|
|
628
|
+
Past 10s a silent port warns, past 15s its backlog is
|
|
629
|
+
dropped and it quarantines, past 20s the engine-side
|
|
630
|
+
resync directive fires. The engine flags, never kills.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
Directive mappings in attach order.
|
|
634
|
+
"""
|
|
635
|
+
return self._port_bus.port_evaluate()
|
|
636
|
+
|
|
637
|
+
def port_resync(self, name, head=None):
|
|
638
|
+
"""Converge one port onto the current ledger head.
|
|
639
|
+
|
|
640
|
+
The client re-runs its idempotent onboarding ritual
|
|
641
|
+
from this position; flags clear and delivery resumes.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
name: Attached port name.
|
|
645
|
+
head: Position to converge on; the bus head when
|
|
646
|
+
omitted.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Resync receipt with port, cursor, and state.
|
|
650
|
+
|
|
651
|
+
Raises:
|
|
652
|
+
ExecutionError: unknown_port for a detached name,
|
|
653
|
+
invalid_command for a non-integer head.
|
|
654
|
+
"""
|
|
655
|
+
return self._port_bus.port_resync(name, head=head)
|
|
656
|
+
|
|
657
|
+
def set_port_enabled(self, name, enabled, local=False):
|
|
658
|
+
"""Enable or disable one port live (local-only toggle).
|
|
659
|
+
|
|
660
|
+
Disabling quarantines like a dead ship: the backlog is
|
|
661
|
+
dropped and the resync flag is set.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
name: Attached port name.
|
|
665
|
+
enabled: True for live delivery, False to park
|
|
666
|
+
the port.
|
|
667
|
+
local: Caller asserts a local (loopback/stdio)
|
|
668
|
+
caller; anything else is refused loudly per
|
|
669
|
+
LOCAL-SINGLE-USER.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Updated port descriptor.
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
ExecutionError: unknown_port for a detached name,
|
|
676
|
+
remote_toggle_denied for a non-local caller.
|
|
677
|
+
"""
|
|
678
|
+
return self._port_bus.set_enabled(name, enabled,
|
|
679
|
+
local=local)
|
|
680
|
+
|
|
681
|
+
def set_port_profile(self, names, local=False):
|
|
682
|
+
"""Swap the live port profile (local-only toggle).
|
|
683
|
+
|
|
684
|
+
Missing names attach as push ports; every other port
|
|
685
|
+
disables like a dead ship. The boot profile is stdio
|
|
686
|
+
alone.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
names: Port names forming the live profile.
|
|
690
|
+
local: Caller asserts a local caller; anything
|
|
691
|
+
else is refused loudly per LOCAL-SINGLE-USER.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Profile mapping with the enabled names sorted.
|
|
695
|
+
|
|
696
|
+
Raises:
|
|
697
|
+
ExecutionError: remote_toggle_denied for a
|
|
698
|
+
non-local caller, invalid_command for a bad
|
|
699
|
+
profile.
|
|
700
|
+
"""
|
|
701
|
+
return self._port_bus.set_profile(names, local=local)
|
|
702
|
+
|
|
703
|
+
def capabilities(self, target=None, engine_root=None):
|
|
704
|
+
"""Report per-solver availability, store state, and the provision menu.
|
|
705
|
+
|
|
706
|
+
The engine side of the Vivado-style setup surface: every
|
|
707
|
+
cataloged operation with readiness (ready when some
|
|
708
|
+
registered tool resolves, or when the Engine serves the
|
|
709
|
+
operation directly; blocked with reason + missing items
|
|
710
|
+
+ remedy when no tool resolves; unsupported when no
|
|
711
|
+
solver is registered), each tool's availability from its
|
|
712
|
+
own detection probe plus its adapter capabilities(),
|
|
713
|
+
PDK/deck install state from the store, BYOC-gated cloud
|
|
714
|
+
rows, and the checkbox provision catalogue with live
|
|
715
|
+
install state.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
target: Target passed to each adapter's capabilities().
|
|
719
|
+
engine_root: Explicit store root; ROCKETEDA_HOME
|
|
720
|
+
resolution when omitted.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Capability mapping with operations, store, pdks,
|
|
724
|
+
decks, cloud, and catalogue sections.
|
|
725
|
+
|
|
726
|
+
Raises:
|
|
727
|
+
SetupError: unknown_solver when a registered
|
|
728
|
+
(operation, tool) has no detection spec — the
|
|
729
|
+
report fails instead of misreporting a solver it
|
|
730
|
+
cannot probe.
|
|
731
|
+
|
|
732
|
+
Caution: Read-only: never creates the store skeleton and
|
|
733
|
+
never attempts network — cloud rows report gated with
|
|
734
|
+
key_state missing until CLOUD-AUTH supplies keys plus
|
|
735
|
+
an explicit opt-in (F12/I32 BYOC).
|
|
736
|
+
"""
|
|
737
|
+
return report_capabilities(
|
|
738
|
+
self._solvers,
|
|
739
|
+
target=target,
|
|
740
|
+
engine_root=engine_root,
|
|
741
|
+
served=_CRUD_OPERATIONS | {"run_job", "place_cell", "python_execute"},
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def submit(self, command):
|
|
745
|
+
"""Admit and run an operation inline, or serve a wired query.
|
|
746
|
+
|
|
747
|
+
Solver operations admit through the project service with
|
|
748
|
+
revision gating and envelope-id idempotency, then execute
|
|
749
|
+
immediately: the returned mapping is the admission ack
|
|
750
|
+
shape plus the completion (state, verdict, operation) under
|
|
751
|
+
the one run id. CRUD mutations, rewind, and place_cell
|
|
752
|
+
execute immediately through the same admission machinery
|
|
753
|
+
(no queue, no solver); only wired queries read state, the
|
|
754
|
+
rest honestly report unwired.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
command: Envelope with project_id, operation, arguments,
|
|
758
|
+
and request_id.
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
Merged ack-plus-completion for solver operations (ack
|
|
762
|
+
identity fields plus state, verdict, operation), the
|
|
763
|
+
immediate mutation mapping (new revision + events +
|
|
764
|
+
approval-UI fields) for CRUD/rewind/place_cell
|
|
765
|
+
operations, the snapshot for the wired
|
|
766
|
+
get_project_snapshot query, the capability mapping
|
|
767
|
+
for get_capabilities, the discovery record for
|
|
768
|
+
api_search/api_info/api_help, the event page for
|
|
769
|
+
get_events_since, the project/document listing for
|
|
770
|
+
the CRUD queries, the status/checks/logs/kinds for
|
|
771
|
+
the job-result queries, the stage plan or completion
|
|
772
|
+
report for the completion queries, the cell listing
|
|
773
|
+
or asset bytes for the catalogue/asset queries, the
|
|
774
|
+
evidence listing for list_artifacts, the
|
|
775
|
+
stackup/rows/presets for the layer-serving queries,
|
|
776
|
+
the stored-or-fresh completion for run_job, the
|
|
777
|
+
boot survivor report for list_survivors, the resume
|
|
778
|
+
completion for resume_run, the discard record for
|
|
779
|
+
discard_run, the gated batch for resume_all_retryable,
|
|
780
|
+
the approval handle for python_execute, or the console
|
|
781
|
+
view for get_python_console.
|
|
782
|
+
|
|
783
|
+
Raises:
|
|
784
|
+
ProjectError: cross_project_denied for a foreign
|
|
785
|
+
envelope, unsupported_operation for an unwired or
|
|
786
|
+
unknown name, unknown_block for a block target
|
|
787
|
+
naming no block, invalid_target for a malformed
|
|
788
|
+
target, unsupported_scope for a block target on a
|
|
789
|
+
verb serving no block runs yet, unknown_run_param
|
|
790
|
+
for an unknown run.request field, invalid_run_param
|
|
791
|
+
for a mistyped run.request knob, invalid_run_request
|
|
792
|
+
for a non-mapping run.request, invalid_command
|
|
793
|
+
for caller-supplied run_spice targets,
|
|
794
|
+
revision_conflict for a stale
|
|
795
|
+
expected_revision, invalid_command for a malformed
|
|
796
|
+
get_events_since cursor, a malformed mutation, a
|
|
797
|
+
place_cell without a cell, a serve_asset without
|
|
798
|
+
pdk id/revision/path, a rewind that lacks a target
|
|
799
|
+
or targets the live head, a write_settings missing
|
|
800
|
+
kind or settings, an unknown settings kind, or
|
|
801
|
+
settings that break the kind schema, future_format
|
|
802
|
+
for a settings write over a newer on-disk file,
|
|
803
|
+
or a layer query missing its pdk_id/name, a
|
|
804
|
+
completion query without a flow or with an unknown
|
|
805
|
+
flow, a resume_run or discard_run without a run_id,
|
|
806
|
+
a component verb missing entity, component, or
|
|
807
|
+
data, or a world snapshot with a non-bool
|
|
808
|
+
flag, unknown_entity or unknown_component for
|
|
809
|
+
a missing entity or component, component_exists
|
|
810
|
+
for a duplicate attach, backfill_readonly for a
|
|
811
|
+
write over a read-only backfilled id,
|
|
812
|
+
resync_required when a get_events_since contiguity
|
|
813
|
+
demand cannot be met, invalid_project
|
|
814
|
+
for a bad project id,
|
|
815
|
+
project_exists for a duplicate create,
|
|
816
|
+
unknown_document for a missing document,
|
|
817
|
+
unknown_revision for a rewind target with no
|
|
818
|
+
snapshot, invalid_path or path_escape for a bad
|
|
819
|
+
document path, request_conflict for a reused
|
|
820
|
+
envelope id with new text, unknown_cell,
|
|
821
|
+
ambiguous_cell, unknown_param, incompatible_port,
|
|
822
|
+
or instance_exists for a bad placement,
|
|
823
|
+
invalid_reference for a bad asset address,
|
|
824
|
+
unknown_request for an id never admitted.
|
|
825
|
+
ExecutionError: pool_full when the execution backlog
|
|
826
|
+
is saturated — drain (run_job) before submitting
|
|
827
|
+
more; nothing was admitted. coalesced_run_lost
|
|
828
|
+
when a joined run finished with no completion
|
|
829
|
+
stored (its owner died mid-drain). Resume refusals
|
|
830
|
+
carry unknown_survivor, already_completed,
|
|
831
|
+
already_decided, or poison_quarantined with a
|
|
832
|
+
user-facing message (full context is logged
|
|
833
|
+
dev-side).
|
|
834
|
+
|
|
835
|
+
Caution: Bound to one project: cross-project envelopes are
|
|
836
|
+
denied, never routed — and a stale expected_revision
|
|
837
|
+
fails instead of merging. The first mutating call per
|
|
838
|
+
Engine also builds the crash-survivor boot report
|
|
839
|
+
(report-only — nothing executes without an explicit
|
|
840
|
+
resume decision); queries never do.
|
|
841
|
+
"""
|
|
842
|
+
envelope = command.get("project_id")
|
|
843
|
+
if envelope != self._project_id:
|
|
844
|
+
raise ProjectError(
|
|
845
|
+
"cross_project_denied",
|
|
846
|
+
f"Bound to {self._project_id}, got {envelope}.",
|
|
847
|
+
)
|
|
848
|
+
name = command.get("operation")
|
|
849
|
+
arguments = dict(command.get("arguments") or {})
|
|
850
|
+
arguments = self._apply_targets(name, arguments)
|
|
851
|
+
arguments = self._apply_run_request(arguments)
|
|
852
|
+
if name == "run_spice":
|
|
853
|
+
arguments = self.confine_spice_arguments(
|
|
854
|
+
arguments, self._project_dir
|
|
855
|
+
)
|
|
856
|
+
if name == "get_capabilities":
|
|
857
|
+
return self.capabilities(
|
|
858
|
+
target=arguments.get("target"),
|
|
859
|
+
engine_root=arguments.get("engine_root"),
|
|
860
|
+
)
|
|
861
|
+
if name == "get_python_console":
|
|
862
|
+
return self.python_console()
|
|
863
|
+
if name == "api_search":
|
|
864
|
+
return self.api_search(query=arguments.get("query"))
|
|
865
|
+
if name == "api_info":
|
|
866
|
+
if "name" not in arguments:
|
|
867
|
+
raise ProjectError(
|
|
868
|
+
"invalid_command", "api_info needs a name argument."
|
|
869
|
+
)
|
|
870
|
+
return self.api_info(arguments["name"])
|
|
871
|
+
if name == "api_help":
|
|
872
|
+
return self.api_help(name=arguments.get("name"))
|
|
873
|
+
if name == "get_events_since":
|
|
874
|
+
return self._query_events_since(arguments)
|
|
875
|
+
if name == "list_projects":
|
|
876
|
+
return self._project.list_projects()
|
|
877
|
+
if name == "list_docs":
|
|
878
|
+
return self._project.list_docs(prefix=arguments.get("prefix"))
|
|
879
|
+
if name == "read_doc":
|
|
880
|
+
if "path" not in arguments:
|
|
881
|
+
raise ProjectError(
|
|
882
|
+
"invalid_command", "read_doc needs a path argument."
|
|
883
|
+
)
|
|
884
|
+
return self._project.read_doc(arguments["path"])
|
|
885
|
+
if name == "read_settings":
|
|
886
|
+
if "kind" not in arguments:
|
|
887
|
+
raise ProjectError(
|
|
888
|
+
"invalid_command", "read_settings needs a kind argument."
|
|
889
|
+
)
|
|
890
|
+
return self._project.read_settings(arguments["kind"])
|
|
891
|
+
if name == "get_resolved_config":
|
|
892
|
+
return self._project.resolved_config(
|
|
893
|
+
session_id=arguments.get("session_id"),
|
|
894
|
+
run_id=arguments.get("run_id"),
|
|
895
|
+
)
|
|
896
|
+
if name == "get_component":
|
|
897
|
+
if "entity" not in arguments or "component" not in arguments:
|
|
898
|
+
raise ProjectError(
|
|
899
|
+
"invalid_command",
|
|
900
|
+
"get_component needs entity and component arguments.",
|
|
901
|
+
)
|
|
902
|
+
return self._project.get_component(
|
|
903
|
+
arguments["entity"], arguments["component"]
|
|
904
|
+
)
|
|
905
|
+
if name == "components_of":
|
|
906
|
+
if "entity" not in arguments:
|
|
907
|
+
raise ProjectError(
|
|
908
|
+
"invalid_command",
|
|
909
|
+
"components_of needs an entity argument.",
|
|
910
|
+
)
|
|
911
|
+
return self._project.components_of(arguments["entity"])
|
|
912
|
+
if name == "entities_with":
|
|
913
|
+
if "component" not in arguments:
|
|
914
|
+
raise ProjectError(
|
|
915
|
+
"invalid_command",
|
|
916
|
+
"entities_with needs a component argument.",
|
|
917
|
+
)
|
|
918
|
+
return self._project.entities_with(
|
|
919
|
+
arguments["component"],
|
|
920
|
+
predicate=arguments.get("predicate"),
|
|
921
|
+
)
|
|
922
|
+
if name == "get_world_snapshot":
|
|
923
|
+
return self._project.world_snapshot(
|
|
924
|
+
include_backfill=arguments.get("include_backfill", True)
|
|
925
|
+
)
|
|
926
|
+
if name in _EDITOR_READS:
|
|
927
|
+
return self._query_editor(name, arguments)
|
|
928
|
+
if name in _JOB_QUERIES:
|
|
929
|
+
return self._query_job(name, arguments)
|
|
930
|
+
if name == "list_artifacts":
|
|
931
|
+
return self.list_artifacts()
|
|
932
|
+
if name in _APIG_QUERIES:
|
|
933
|
+
return self._query_apig(name, arguments)
|
|
934
|
+
if name in _LAYER_QUERIES:
|
|
935
|
+
return self._query_layer(name, arguments)
|
|
936
|
+
if name in _COMPLETION_QUERIES:
|
|
937
|
+
return self._query_completion(name, arguments)
|
|
938
|
+
if name in _REPLAY_QUERIES:
|
|
939
|
+
return self.boot_report()
|
|
940
|
+
if name in _WIRED_QUERIES:
|
|
941
|
+
return self._project.snapshot()
|
|
942
|
+
if name in QUERIES or name not in OPERATIONS:
|
|
943
|
+
raise ProjectError(
|
|
944
|
+
"unsupported_operation", f"Operation not wired yet: {name}."
|
|
945
|
+
)
|
|
946
|
+
self._replay_once()
|
|
947
|
+
if name == "run_job":
|
|
948
|
+
# Deferred/batch pumping plus idempotent fetch: admission
|
|
949
|
+
# (and its revision gate) already happened at submit time.
|
|
950
|
+
return self.run(command)
|
|
951
|
+
if name == "resume_run":
|
|
952
|
+
if "run_id" not in arguments:
|
|
953
|
+
raise ProjectError(
|
|
954
|
+
"invalid_command",
|
|
955
|
+
"resume_run needs a run_id argument.",
|
|
956
|
+
)
|
|
957
|
+
return self.resume_survivor(
|
|
958
|
+
arguments["run_id"],
|
|
959
|
+
force=bool(arguments.get("force", False)),
|
|
960
|
+
)
|
|
961
|
+
if name == "discard_run":
|
|
962
|
+
if "run_id" not in arguments:
|
|
963
|
+
raise ProjectError(
|
|
964
|
+
"invalid_command",
|
|
965
|
+
"discard_run needs a run_id argument.",
|
|
966
|
+
)
|
|
967
|
+
return self.discard_survivor(arguments["run_id"])
|
|
968
|
+
if name == "resume_all_retryable":
|
|
969
|
+
return self.resume_all_retryable()
|
|
970
|
+
if name == "cancel_job":
|
|
971
|
+
# Job control, not admission: cancel rides submit so every
|
|
972
|
+
# transport reaches it, but it never enters the solver lane
|
|
973
|
+
# (no ledger admit, no revision gate — it names a live run).
|
|
974
|
+
return self.cancel_job(arguments.get("run_id"))
|
|
975
|
+
if name in _CRUD_OPERATIONS:
|
|
976
|
+
out = self._submit_crud(name, arguments, command)
|
|
977
|
+
self._deferred.apply_edit()
|
|
978
|
+
self.sync_ports()
|
|
979
|
+
return out
|
|
980
|
+
if name == "place_cell":
|
|
981
|
+
out = self._submit_place_cell(arguments, command)
|
|
982
|
+
self._deferred.apply_edit()
|
|
983
|
+
self.sync_ports()
|
|
984
|
+
return out
|
|
985
|
+
if name == "python_execute":
|
|
986
|
+
out = self._submit_pyexec(arguments, command)
|
|
987
|
+
self._deferred.apply_edit()
|
|
988
|
+
self.sync_ports()
|
|
989
|
+
return out
|
|
990
|
+
if name in ("python_open", "python_reset"):
|
|
991
|
+
return self._submit_console(name, arguments, command)
|
|
992
|
+
if not self._solvers.get(name):
|
|
993
|
+
raise ProjectError(
|
|
994
|
+
"unsupported_operation", f"No solver registered: {name}."
|
|
995
|
+
)
|
|
996
|
+
refuse_smuggled_budget(arguments)
|
|
997
|
+
check_submit_gates(
|
|
998
|
+
name, arguments, self._project,
|
|
999
|
+
force=command.get("force") is True,
|
|
1000
|
+
)
|
|
1001
|
+
scope_request = self._admit_block_scope(name, arguments)
|
|
1002
|
+
command_text = dump_yaml_text(
|
|
1003
|
+
{"operation": name, "arguments": arguments}
|
|
1004
|
+
)
|
|
1005
|
+
request_id = command.get("request_id") or uuid.uuid4().hex
|
|
1006
|
+
# COALESCE-I33: identical concurrent content on an unchanged
|
|
1007
|
+
# revision executes once. The guard holds the whole admission
|
|
1008
|
+
# section — lookup, ledger admit, register — so two identical
|
|
1009
|
+
# submits cannot both miss; drains and completion waits run
|
|
1010
|
+
# outside it. Force bypasses the lane both ways: a forced run
|
|
1011
|
+
# neither joins a live run nor registers as one.
|
|
1012
|
+
force = command.get("force") is True
|
|
1013
|
+
key = content_key(name, arguments)
|
|
1014
|
+
with self._coalescer.guard:
|
|
1015
|
+
head = self._project.snapshot()["current_revision"]
|
|
1016
|
+
hit = None if force else self._coalescer.lookup(key, head)
|
|
1017
|
+
if hit is not None:
|
|
1018
|
+
ack = self._project.log_coalesced_request(
|
|
1019
|
+
command_text, str(request_id), hit["run_id"],
|
|
1020
|
+
hit["snapshot_id"],
|
|
1021
|
+
)
|
|
1022
|
+
waiter = self._coalescer.attach(hit["run_id"], str(request_id))
|
|
1023
|
+
else:
|
|
1024
|
+
ack = self._pool.submit_request(
|
|
1025
|
+
command_text,
|
|
1026
|
+
str(request_id),
|
|
1027
|
+
expected_revision=arguments.get("expected_revision"),
|
|
1028
|
+
)
|
|
1029
|
+
# SUBMIT-IMMEDIATE: the ack run_id is the execution id. A
|
|
1030
|
+
# replay of a completed run returns the stored completion
|
|
1031
|
+
# without re-executing; otherwise register the record (a
|
|
1032
|
+
# replay returns the same ack, so register it only once)
|
|
1033
|
+
# and drain this run inline.
|
|
1034
|
+
stored = self._stored_completion(ack["run_id"])
|
|
1035
|
+
if stored is not None:
|
|
1036
|
+
return merge_ack_completion(ack, stored)
|
|
1037
|
+
if not self._execution.known(ack["run_id"]):
|
|
1038
|
+
target = {"cell": arguments.get("cell")}
|
|
1039
|
+
if scope_request is not None:
|
|
1040
|
+
target["scope"] = "block"
|
|
1041
|
+
target["block_id"] = scope_request["block_id"]
|
|
1042
|
+
self._execution.dispatch(
|
|
1043
|
+
name, target, ack["snapshot_id"], run_id=ack["run_id"]
|
|
1044
|
+
)
|
|
1045
|
+
self._run_jobs.setdefault(
|
|
1046
|
+
ack["run_id"],
|
|
1047
|
+
{
|
|
1048
|
+
"operation": name,
|
|
1049
|
+
"arguments": arguments,
|
|
1050
|
+
"snapshot_id": ack["snapshot_id"],
|
|
1051
|
+
"request_id": str(request_id),
|
|
1052
|
+
},
|
|
1053
|
+
)
|
|
1054
|
+
if scope_request is not None:
|
|
1055
|
+
pending = self._run_jobs[ack["run_id"]]
|
|
1056
|
+
if "block_scope" not in pending:
|
|
1057
|
+
pending["block_scope"] = build_scope_record(
|
|
1058
|
+
run_id=ack["run_id"],
|
|
1059
|
+
revision=self._project.snapshot()["current_revision"],
|
|
1060
|
+
block_id=scope_request["block_id"],
|
|
1061
|
+
deck=arguments.get("deck"),
|
|
1062
|
+
closure=scope_request["closure"],
|
|
1063
|
+
)
|
|
1064
|
+
waiter = None
|
|
1065
|
+
if not force:
|
|
1066
|
+
self._coalescer.register(
|
|
1067
|
+
key, ack["current_revision"], str(request_id),
|
|
1068
|
+
ack["run_id"], ack["snapshot_id"],
|
|
1069
|
+
)
|
|
1070
|
+
if hit is not None:
|
|
1071
|
+
out = self._await_shared(ack, hit["run_id"], waiter)
|
|
1072
|
+
self.sync_ports()
|
|
1073
|
+
return out
|
|
1074
|
+
try:
|
|
1075
|
+
completion = self._pool.drain_ids([ack["run_id"]])[0]
|
|
1076
|
+
except BaseException:
|
|
1077
|
+
if not force:
|
|
1078
|
+
self._coalescer.discard(ack["run_id"])
|
|
1079
|
+
raise
|
|
1080
|
+
if not force:
|
|
1081
|
+
self._coalescer.complete(ack["run_id"], completion)
|
|
1082
|
+
self._deferred.apply_edit()
|
|
1083
|
+
self._maybe_record_evidence(name, completion, arguments)
|
|
1084
|
+
self.sync_ports()
|
|
1085
|
+
return merge_ack_completion(ack, completion)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def catalog_cells(self, pdk=None):
|
|
1089
|
+
"""List versioned component cells, optionally for one PDK.
|
|
1090
|
+
|
|
1091
|
+
The engine introspects cell libraries upstream and serves
|
|
1092
|
+
plain data (cell, PDK, ports, default params), so the
|
|
1093
|
+
frontend never imports gdsfactory.
|
|
1094
|
+
|
|
1095
|
+
Args:
|
|
1096
|
+
pdk: Restrict to one PDK label; all entries when
|
|
1097
|
+
omitted.
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Listing with catalog_version and cells sorted by
|
|
1101
|
+
(pdk, cell).
|
|
1102
|
+
"""
|
|
1103
|
+
return self._cells.list_cells(pdk=pdk)
|
|
1104
|
+
|
|
1105
|
+
def register_cells(self, entries, source="static"):
|
|
1106
|
+
"""Seed the component catalogue with static entries.
|
|
1107
|
+
|
|
1108
|
+
Args:
|
|
1109
|
+
entries: Entry mappings with cell, pdk, pdk_revision,
|
|
1110
|
+
ports, and default params.
|
|
1111
|
+
source: Source label stamped on every entry.
|
|
1112
|
+
|
|
1113
|
+
Returns:
|
|
1114
|
+
Mapping with the cell count now served.
|
|
1115
|
+
"""
|
|
1116
|
+
self._cells.add_provider(static_provider(entries, source=source))
|
|
1117
|
+
return {"cells": len(self._cells.list_cells()["cells"])}
|
|
1118
|
+
|
|
1119
|
+
def serve_asset(self, pdk_id=None, revision=None, path=None,
|
|
1120
|
+
engine_root=None):
|
|
1121
|
+
"""Serve one PDK file's bytes verbatim with hash identity.
|
|
1122
|
+
|
|
1123
|
+
No conversion, no rendering: the sha256 is computed over
|
|
1124
|
+
the raw bytes, base64 is transport encoding only.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
pdk_id: PDK id (either display spelling resolves).
|
|
1128
|
+
revision: PDK revision pin.
|
|
1129
|
+
path: Store-relative file path.
|
|
1130
|
+
engine_root: Explicit store root; ROCKETEDA_HOME
|
|
1131
|
+
resolution when omitted.
|
|
1132
|
+
|
|
1133
|
+
Returns:
|
|
1134
|
+
Mapping with asset_version, pdk_id, display_id,
|
|
1135
|
+
revision, canonical path, size, sha256, and the
|
|
1136
|
+
verbatim bytes base64-encoded.
|
|
1137
|
+
|
|
1138
|
+
Raises:
|
|
1139
|
+
ProjectError: invalid_command when pdk id, revision,
|
|
1140
|
+
or path is missing; invalid_reference for an
|
|
1141
|
+
unknown PDK, a missing file, or an escaping path.
|
|
1142
|
+
|
|
1143
|
+
Caution: No size cap — bulk reads belong to list_assets
|
|
1144
|
+
plus selective serve_asset calls, never a blind loop.
|
|
1145
|
+
"""
|
|
1146
|
+
if pdk_id is None or revision is None or path is None:
|
|
1147
|
+
raise ProjectError(
|
|
1148
|
+
"invalid_command",
|
|
1149
|
+
"serve_asset needs pdk_id, revision, and path arguments.",
|
|
1150
|
+
)
|
|
1151
|
+
try:
|
|
1152
|
+
return apig_assets.serve_asset(
|
|
1153
|
+
pdk_id, revision, path, engine_root=engine_root
|
|
1154
|
+
)
|
|
1155
|
+
except ContractError as exc:
|
|
1156
|
+
raise ProjectError(exc.code, str(exc)) from exc
|
|
1157
|
+
|
|
1158
|
+
def list_assets(self, pdk_id=None, revision=None, prefix=None,
|
|
1159
|
+
engine_root=None):
|
|
1160
|
+
"""List PDK file metadata with per-file hash identity.
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
pdk_id: PDK id (either display spelling resolves).
|
|
1164
|
+
revision: PDK revision pin.
|
|
1165
|
+
prefix: Optional store-relative prefix filter.
|
|
1166
|
+
engine_root: Explicit store root; ROCKETEDA_HOME
|
|
1167
|
+
resolution when omitted.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
Mapping with asset_version, pdk_id, display_id,
|
|
1171
|
+
revision, sorted file entries (path, size,
|
|
1172
|
+
sha256), symlink entries, and the file count.
|
|
1173
|
+
|
|
1174
|
+
Raises:
|
|
1175
|
+
ProjectError: invalid_command when pdk id or revision
|
|
1176
|
+
is missing; invalid_reference for an unknown PDK
|
|
1177
|
+
or an escaping prefix.
|
|
1178
|
+
"""
|
|
1179
|
+
if pdk_id is None or revision is None:
|
|
1180
|
+
raise ProjectError(
|
|
1181
|
+
"invalid_command",
|
|
1182
|
+
"list_assets needs pdk_id and revision arguments.",
|
|
1183
|
+
)
|
|
1184
|
+
try:
|
|
1185
|
+
return apig_assets.list_assets(
|
|
1186
|
+
pdk_id, revision, prefix=prefix, engine_root=engine_root
|
|
1187
|
+
)
|
|
1188
|
+
except ContractError as exc:
|
|
1189
|
+
raise ProjectError(exc.code, str(exc)) from exc
|
|
1190
|
+
|
|
1191
|
+
def list_artifacts(self):
|
|
1192
|
+
"""List recorded run-evidence files, newest first.
|
|
1193
|
+
|
|
1194
|
+
The WIRE-DECLARED-OPS wiring for the declared
|
|
1195
|
+
`list_artifacts` query: every transport shares the one
|
|
1196
|
+
`results/engine-runs` evidence trail the engine records
|
|
1197
|
+
itself, so this lists run ids with their operation,
|
|
1198
|
+
verdict, finish time, and evidence path. Unreadable
|
|
1199
|
+
files are skipped, never fatal — the trail is evidence,
|
|
1200
|
+
not a ledger.
|
|
1201
|
+
|
|
1202
|
+
Returns:
|
|
1203
|
+
Mapping with the artifact count plus entries sorted
|
|
1204
|
+
by path (each with run_id, operation, verdict,
|
|
1205
|
+
finished_at, and path). Empty when the engine is
|
|
1206
|
+
unbound or no run has been recorded yet.
|
|
1207
|
+
|
|
1208
|
+
Caution: Read-only: reports recorded evidence, never
|
|
1209
|
+
executes anything and never replays the backlog.
|
|
1210
|
+
"""
|
|
1211
|
+
folder = None
|
|
1212
|
+
if self._project_dir is not None:
|
|
1213
|
+
folder = Path(self._project_dir).resolve() / "results" / "engine-runs"
|
|
1214
|
+
entries = []
|
|
1215
|
+
if folder is not None and folder.is_dir():
|
|
1216
|
+
for path in sorted(folder.glob("*.json")):
|
|
1217
|
+
try:
|
|
1218
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
1219
|
+
except (OSError, ValueError):
|
|
1220
|
+
continue
|
|
1221
|
+
if not isinstance(record, dict):
|
|
1222
|
+
continue
|
|
1223
|
+
entries.append(
|
|
1224
|
+
{
|
|
1225
|
+
"run_id": record.get("run_id", path.stem),
|
|
1226
|
+
"operation": record.get("operation", ""),
|
|
1227
|
+
"verdict": record.get("verdict", "UNKNOWN"),
|
|
1228
|
+
"finished_at": record.get("finished_at"),
|
|
1229
|
+
"path": str(path),
|
|
1230
|
+
}
|
|
1231
|
+
)
|
|
1232
|
+
entries.sort(
|
|
1233
|
+
key=lambda e: (e["finished_at"] or "", e["path"]), reverse=True
|
|
1234
|
+
)
|
|
1235
|
+
return {"artifacts": entries, "count": len(entries)}
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def boot_report(self):
|
|
1239
|
+
"""Report crash survivors without executing anything (boot report).
|
|
1240
|
+
|
|
1241
|
+
Survivors are ledger admissions without a run event —
|
|
1242
|
+
typically a kill between admit and drain. Each carries its
|
|
1243
|
+
taxonomy kind (retryable, ambiguous, poison), quarantine
|
|
1244
|
+
state, and per-run Resume/Discard actions.
|
|
1245
|
+
|
|
1246
|
+
Returns:
|
|
1247
|
+
Report with the session-ended message, the unfinished
|
|
1248
|
+
count, survivors in admit order, quarantined run
|
|
1249
|
+
ids, and unresolved entries.
|
|
1250
|
+
|
|
1251
|
+
Caution: Report-only by construction: nothing executes
|
|
1252
|
+
here — every replay surface needs an explicit resume
|
|
1253
|
+
decision (resume_run, discard_run, resume_all_retryable).
|
|
1254
|
+
"""
|
|
1255
|
+
return self._replay.boot()
|
|
1256
|
+
|
|
1257
|
+
def resume_survivor(self, run_id, force=False):
|
|
1258
|
+
"""Explicitly resume one crash survivor through its solver.
|
|
1259
|
+
|
|
1260
|
+
Args:
|
|
1261
|
+
run_id: Survivor to resume.
|
|
1262
|
+
force: Required for quarantined (poison) runs; force
|
|
1263
|
+
bypasses quarantine only — the run still executes
|
|
1264
|
+
for real.
|
|
1265
|
+
|
|
1266
|
+
Returns:
|
|
1267
|
+
Executor completion for the run.
|
|
1268
|
+
|
|
1269
|
+
Raises:
|
|
1270
|
+
ExecutionError: unknown_survivor for an id with no
|
|
1271
|
+
unfinished run, already_completed for a finished
|
|
1272
|
+
run, already_decided for a run handled this
|
|
1273
|
+
session, poison_quarantined for a quarantined run
|
|
1274
|
+
resumed without force.
|
|
1275
|
+
|
|
1276
|
+
Caution: This executes the run — the one replay surface
|
|
1277
|
+
that does, and only on this explicit call.
|
|
1278
|
+
"""
|
|
1279
|
+
return self._replay.resume(run_id, force=force)
|
|
1280
|
+
|
|
1281
|
+
def discard_survivor(self, run_id):
|
|
1282
|
+
"""Explicitly discard one crash survivor, never executing it.
|
|
1283
|
+
|
|
1284
|
+
Args:
|
|
1285
|
+
run_id: Survivor to discard.
|
|
1286
|
+
|
|
1287
|
+
Returns:
|
|
1288
|
+
Discard record with run_id, discarded state, and the
|
|
1289
|
+
run's operation.
|
|
1290
|
+
|
|
1291
|
+
Raises:
|
|
1292
|
+
ExecutionError: unknown_survivor for an id with no
|
|
1293
|
+
unfinished run, already_completed for a finished
|
|
1294
|
+
run, already_decided for a run handled this
|
|
1295
|
+
session.
|
|
1296
|
+
"""
|
|
1297
|
+
return self._replay.discard(run_id)
|
|
1298
|
+
|
|
1299
|
+
def resume_all_retryable(self):
|
|
1300
|
+
"""Resume every RETRYABLE survivor, oldest first (gated).
|
|
1301
|
+
|
|
1302
|
+
Ambiguous runs need individual review and poison runs need
|
|
1303
|
+
per-run force, so both are skipped loudly — never swept up.
|
|
1304
|
+
|
|
1305
|
+
Returns:
|
|
1306
|
+
Batch with resumed run ids, their completions, and
|
|
1307
|
+
skipped entries (each with run_id, typed code, and
|
|
1308
|
+
user-facing message).
|
|
1309
|
+
|
|
1310
|
+
Raises:
|
|
1311
|
+
ExecutionError: Propagates the first executor failure
|
|
1312
|
+
unchanged — earlier resumes stand, later ones never
|
|
1313
|
+
fire.
|
|
1314
|
+
|
|
1315
|
+
Caution: This executes runs — only the RETRYABLE,
|
|
1316
|
+
non-quarantined survivors, and only on this call.
|
|
1317
|
+
"""
|
|
1318
|
+
return self._replay.resume_all_retryable()
|
|
1319
|
+
|
|
1320
|
+
def resolve_python_approval(self, approval_id, decision, actor=None):
|
|
1321
|
+
"""Resolve a python_execute approval via the human UI route.
|
|
1322
|
+
|
|
1323
|
+
Agent calls request approval; they never grant it — this
|
|
1324
|
+
method is the trusted UI route, and no MCP tool maps to it.
|
|
1325
|
+
|
|
1326
|
+
Args:
|
|
1327
|
+
approval_id: Approval bound by the submit call.
|
|
1328
|
+
decision: `approved` to admit the queued run, `denied`
|
|
1329
|
+
to lapse the approval with no run.
|
|
1330
|
+
actor: Human actor the decision is attributed to; the
|
|
1331
|
+
engine actor when omitted.
|
|
1332
|
+
|
|
1333
|
+
Returns:
|
|
1334
|
+
Resolution with approval_id, approval_state,
|
|
1335
|
+
execution_id, and the run_id once admitted.
|
|
1336
|
+
|
|
1337
|
+
Raises:
|
|
1338
|
+
ExecutionError: approval_invalid/approval_denied/
|
|
1339
|
+
approval_expired/approval_forbidden per the
|
|
1340
|
+
approval ledger.
|
|
1341
|
+
|
|
1342
|
+
Caution: Human route only — MCP agents must never call
|
|
1343
|
+
this; there is deliberately no MCP tool for it.
|
|
1344
|
+
"""
|
|
1345
|
+
return self._pyexec.resolve_approval(
|
|
1346
|
+
approval_id, decision, via="ui",
|
|
1347
|
+
actor=actor if actor is not None else self._actor,
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
def python_console(self):
|
|
1351
|
+
"""Read the python_execute console view.
|
|
1352
|
+
|
|
1353
|
+
Returns:
|
|
1354
|
+
Console view with project_id, generation, env_digest,
|
|
1355
|
+
history cursor, and pending approvals.
|
|
1356
|
+
|
|
1357
|
+
Caution: Read-only: reports approval queue state, never
|
|
1358
|
+
resolves anything.
|
|
1359
|
+
"""
|
|
1360
|
+
return self._pyexec.console_view()
|
|
1361
|
+
|
|
1362
|
+
def open_console(self, workspace_root=None):
|
|
1363
|
+
"""Open a console, reusing the live one for its workspace.
|
|
1364
|
+
|
|
1365
|
+
Consoles are Engine-memory and workspace-bound: one live
|
|
1366
|
+
handle per workspace root, unbound when omitted. Opening
|
|
1367
|
+
never touches the project ledger, so no expected_revision
|
|
1368
|
+
rides along; the registry itself is the idempotency.
|
|
1369
|
+
|
|
1370
|
+
Args:
|
|
1371
|
+
workspace_root: Panel workspace binding; one live
|
|
1372
|
+
handle per root, unbound when omitted.
|
|
1373
|
+
|
|
1374
|
+
Returns:
|
|
1375
|
+
Opening with console_id, project_id, generation,
|
|
1376
|
+
and the workspace_root binding.
|
|
1377
|
+
"""
|
|
1378
|
+
return self._consoles.open_console(
|
|
1379
|
+
self._project_id, self._actor,
|
|
1380
|
+
workspace_root=workspace_root)
|
|
1381
|
+
|
|
1382
|
+
def reset_console(self, console_id=None):
|
|
1383
|
+
"""Retire a console generation; kill outstanding approvals.
|
|
1384
|
+
|
|
1385
|
+
Reset is a lifecycle event, never a re-run: the generation
|
|
1386
|
+
retires, pending approvals die through the session, and
|
|
1387
|
+
live workers stay cancel territory. Unknown ids fail
|
|
1388
|
+
loudly; nothing is admitted on failure.
|
|
1389
|
+
|
|
1390
|
+
Args:
|
|
1391
|
+
console_id: Console from `python_open`.
|
|
1392
|
+
|
|
1393
|
+
Returns:
|
|
1394
|
+
Mapping with the console_id and the fresh generation.
|
|
1395
|
+
|
|
1396
|
+
Raises:
|
|
1397
|
+
ProjectError: invalid_command without a console_id.
|
|
1398
|
+
ExecutionError: unknown_console for an id never
|
|
1399
|
+
opened or already closed.
|
|
1400
|
+
"""
|
|
1401
|
+
if console_id is None:
|
|
1402
|
+
raise ProjectError(
|
|
1403
|
+
"invalid_command",
|
|
1404
|
+
"python_reset needs a console_id argument.",
|
|
1405
|
+
)
|
|
1406
|
+
return self._consoles.reset_console(console_id)
|
|
1407
|
+
|
|
1408
|
+
def api_search(self, query=None):
|
|
1409
|
+
"""Search cataloged operations and queries by name or summary.
|
|
1410
|
+
|
|
1411
|
+
The discovery entrypoint: substring match over every
|
|
1412
|
+
cataloged name plus its handler summary, so callers find the
|
|
1413
|
+
verb without reading the whole reference.
|
|
1414
|
+
|
|
1415
|
+
Args:
|
|
1416
|
+
query: Substring matched against names and handler
|
|
1417
|
+
summaries (case-insensitive); every cataloged name
|
|
1418
|
+
when omitted.
|
|
1419
|
+
|
|
1420
|
+
Returns:
|
|
1421
|
+
Search mapping with the query and matches sorted by
|
|
1422
|
+
name (each with name, kind, wired, and summary).
|
|
1423
|
+
"""
|
|
1424
|
+
needle = query.lower() if query else None
|
|
1425
|
+
matches = []
|
|
1426
|
+
for name in sorted(OPERATIONS | QUERIES):
|
|
1427
|
+
kind, wired, summary, _handlers = self._describe_op(name)
|
|
1428
|
+
if (
|
|
1429
|
+
needle is not None
|
|
1430
|
+
and needle not in name.lower()
|
|
1431
|
+
and needle not in summary.lower()
|
|
1432
|
+
):
|
|
1433
|
+
continue
|
|
1434
|
+
matches.append({
|
|
1435
|
+
"name": name, "kind": kind,
|
|
1436
|
+
"wired": wired, "summary": summary,
|
|
1437
|
+
})
|
|
1438
|
+
return {"query": query, "matches": matches}
|
|
1439
|
+
|
|
1440
|
+
def api_info(self, name):
|
|
1441
|
+
"""Describe one cataloged operation or query.
|
|
1442
|
+
|
|
1443
|
+
Args:
|
|
1444
|
+
name: Cataloged operation or query name.
|
|
1445
|
+
|
|
1446
|
+
Returns:
|
|
1447
|
+
Record with name, kind, wired, summary, and handlers
|
|
1448
|
+
in generated-spec form.
|
|
1449
|
+
|
|
1450
|
+
Raises:
|
|
1451
|
+
ProjectError: invalid_command for a missing name,
|
|
1452
|
+
unsupported_operation for an uncataloged name.
|
|
1453
|
+
"""
|
|
1454
|
+
if name is None:
|
|
1455
|
+
raise ProjectError(
|
|
1456
|
+
"invalid_command", "api_info needs a name argument."
|
|
1457
|
+
)
|
|
1458
|
+
if name not in OPERATIONS and name not in QUERIES:
|
|
1459
|
+
raise ProjectError(
|
|
1460
|
+
"unsupported_operation",
|
|
1461
|
+
f"Operation not wired yet: {name}.",
|
|
1462
|
+
)
|
|
1463
|
+
kind, wired, summary, handlers = self._describe_op(name)
|
|
1464
|
+
return {
|
|
1465
|
+
"name": name, "kind": kind, "wired": wired,
|
|
1466
|
+
"summary": summary, "handlers": handlers,
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
def api_help(self, name=None):
|
|
1470
|
+
"""Explain one cataloged name, or outline the whole catalog.
|
|
1471
|
+
|
|
1472
|
+
Args:
|
|
1473
|
+
name: Cataloged operation or query name; outlines
|
|
1474
|
+
every cataloged name when omitted.
|
|
1475
|
+
|
|
1476
|
+
Returns:
|
|
1477
|
+
Help mapping with name, kind, wired, summary, and a
|
|
1478
|
+
text block (handler plus where the full reference
|
|
1479
|
+
section lives).
|
|
1480
|
+
|
|
1481
|
+
Raises:
|
|
1482
|
+
ProjectError: unsupported_operation for an uncataloged
|
|
1483
|
+
name.
|
|
1484
|
+
"""
|
|
1485
|
+
if name is None:
|
|
1486
|
+
lines = [
|
|
1487
|
+
"Cataloged operations and queries "
|
|
1488
|
+
f"({len(OPERATIONS)} operations, {len(QUERIES)} queries).",
|
|
1489
|
+
"Name one with api_info for its handler record.",
|
|
1490
|
+
"",
|
|
1491
|
+
]
|
|
1492
|
+
for entry in self.api_search()["matches"]:
|
|
1493
|
+
lines.append(f"- `{entry['name']}`: {entry['summary']}")
|
|
1494
|
+
text = "\n".join(lines) + "\n"
|
|
1495
|
+
return {
|
|
1496
|
+
"name": None, "kind": "query", "wired": True,
|
|
1497
|
+
"summary": "Outline of every cataloged operation and query.",
|
|
1498
|
+
"text": text,
|
|
1499
|
+
}
|
|
1500
|
+
record = self.api_info(name)
|
|
1501
|
+
handlers = ", ".join(
|
|
1502
|
+
f"`{handler}`" for handler in record["handlers"]
|
|
1503
|
+
) or "no handler wired yet"
|
|
1504
|
+
text = (
|
|
1505
|
+
f"{record['summary']}\n\nHandler: {handlers}.\n"
|
|
1506
|
+
f"Full reference: docs/engine-api.md ### {name}."
|
|
1507
|
+
)
|
|
1508
|
+
return {
|
|
1509
|
+
"name": name, "kind": record["kind"],
|
|
1510
|
+
"wired": record["wired"], "summary": record["summary"],
|
|
1511
|
+
"text": text,
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
def cancel_job(self, run_id=None):
|
|
1515
|
+
"""Cancel a queued or running job through the job surface.
|
|
1516
|
+
|
|
1517
|
+
python_execute cells route to the cell runner (a live worker
|
|
1518
|
+
tree is killed promptly); solver-lane runs route to the
|
|
1519
|
+
execution record plus the pool backlog, so a queued batch
|
|
1520
|
+
item never executes after cancel. Finished runs keep their
|
|
1521
|
+
terminal state, truthfully.
|
|
1522
|
+
|
|
1523
|
+
Args:
|
|
1524
|
+
run_id: Admitted execution id to cancel.
|
|
1525
|
+
|
|
1526
|
+
Returns:
|
|
1527
|
+
Mapping with run_id and state.
|
|
1528
|
+
|
|
1529
|
+
Raises:
|
|
1530
|
+
ProjectError: invalid_command without a run_id.
|
|
1531
|
+
ExecutionError: unknown_run for an id never admitted.
|
|
1532
|
+
"""
|
|
1533
|
+
if run_id is None:
|
|
1534
|
+
raise ProjectError(
|
|
1535
|
+
"invalid_command", "cancel_job needs a run_id argument."
|
|
1536
|
+
)
|
|
1537
|
+
try:
|
|
1538
|
+
return self._pyexec.cancel(run_id)
|
|
1539
|
+
except ExecutionError as exc:
|
|
1540
|
+
if exc.code != "unknown_run" or not self._execution.known(
|
|
1541
|
+
run_id
|
|
1542
|
+
):
|
|
1543
|
+
raise
|
|
1544
|
+
cancelled = self._execution.cancel(run_id)
|
|
1545
|
+
self._pool.discard(run_id)
|
|
1546
|
+
return cancelled
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def job_status(self, run_id=None):
|
|
1550
|
+
"""Read one run's state, verdict, and evidence links.
|
|
1551
|
+
|
|
1552
|
+
Args:
|
|
1553
|
+
run_id: Run to inspect (the submit-ack id, shared
|
|
1554
|
+
with the execution record).
|
|
1555
|
+
|
|
1556
|
+
Returns:
|
|
1557
|
+
Run state mapping with state, verdict, and log links.
|
|
1558
|
+
|
|
1559
|
+
Raises:
|
|
1560
|
+
ExecutionError: unknown_run for an unregistered id.
|
|
1561
|
+
"""
|
|
1562
|
+
return self._execution.status(run_id)
|
|
1563
|
+
|
|
1564
|
+
def check_results(self, operation=None):
|
|
1565
|
+
"""Read the latest check verdict per operation.
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
operation: Restrict to one operation; all latest
|
|
1569
|
+
verdicts when omitted.
|
|
1570
|
+
|
|
1571
|
+
Returns:
|
|
1572
|
+
Mapping of operation to run state plus the current
|
|
1573
|
+
project revision.
|
|
1574
|
+
"""
|
|
1575
|
+
checks = {}
|
|
1576
|
+
for op, run_id in self._latest.items():
|
|
1577
|
+
if operation and op != operation:
|
|
1578
|
+
continue
|
|
1579
|
+
status = self._execution.status(run_id)
|
|
1580
|
+
verdict = status.get("verdict") or {}
|
|
1581
|
+
checks[op] = {
|
|
1582
|
+
"run_id": run_id,
|
|
1583
|
+
"state": status["state"],
|
|
1584
|
+
"status": verdict.get("status"),
|
|
1585
|
+
"verdict": verdict,
|
|
1586
|
+
}
|
|
1587
|
+
return {
|
|
1588
|
+
"checks": checks,
|
|
1589
|
+
"current_revision": self._project.snapshot()["current_revision"],
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
def plan_checks(self, flow):
|
|
1593
|
+
"""Plan one flow's checks against the registered solvers.
|
|
1594
|
+
|
|
1595
|
+
The stage plan behind the check-plan display: every required
|
|
1596
|
+
stage with its readiness (ready when all tool entrypoints are
|
|
1597
|
+
registered, otherwise required-but-blocked with the missing
|
|
1598
|
+
operations named), so a blocked M14 cites run_thermal instead
|
|
1599
|
+
of vanishing.
|
|
1600
|
+
|
|
1601
|
+
Args:
|
|
1602
|
+
flow: Flow inventory to plan (digital/analog/photonic/mixed).
|
|
1603
|
+
|
|
1604
|
+
Returns:
|
|
1605
|
+
Plan mapping with the flow, per-stage readiness plus
|
|
1606
|
+
missing operations, blocked/ready id lists, the sorted
|
|
1607
|
+
missing operations, and the current revision.
|
|
1608
|
+
|
|
1609
|
+
Raises:
|
|
1610
|
+
ProjectError: invalid_command for a missing or unknown flow.
|
|
1611
|
+
"""
|
|
1612
|
+
if not flow:
|
|
1613
|
+
raise ProjectError(
|
|
1614
|
+
"invalid_command", "plan_checks needs a flow argument."
|
|
1615
|
+
)
|
|
1616
|
+
try:
|
|
1617
|
+
plan = plan_checks_report(flow, set(self._solvers))
|
|
1618
|
+
except ValueError as exc:
|
|
1619
|
+
raise ProjectError("invalid_command", str(exc)) from exc
|
|
1620
|
+
plan["current_revision"] = self._project.snapshot()["current_revision"]
|
|
1621
|
+
return plan
|
|
1622
|
+
|
|
1623
|
+
def design_completion(self, flow):
|
|
1624
|
+
"""Judge one flow's completion; refuse while blockers stand.
|
|
1625
|
+
|
|
1626
|
+
Layers the latest check verdicts over the stage plan: any
|
|
1627
|
+
blocked, unverified, or failing stage becomes a named blocker
|
|
1628
|
+
and the fully-verified claim is refused (claim None with a
|
|
1629
|
+
refusal naming the count); only a blocker-free plan claims
|
|
1630
|
+
fully verified with per-operation evidence run ids. Mixed M14
|
|
1631
|
+
plus the vocab-only run_thermal operation always appear while
|
|
1632
|
+
no thermal solver is registered.
|
|
1633
|
+
|
|
1634
|
+
Args:
|
|
1635
|
+
flow: Flow inventory to judge (digital/analog/photonic/mixed).
|
|
1636
|
+
|
|
1637
|
+
Returns:
|
|
1638
|
+
Completion mapping with the fully_verified flag, claim or
|
|
1639
|
+
refusal, named blockers, missing operations, evidence
|
|
1640
|
+
run ids, per-stage statuses, and the current revision.
|
|
1641
|
+
|
|
1642
|
+
Raises:
|
|
1643
|
+
ProjectError: invalid_command for a missing or unknown flow.
|
|
1644
|
+
"""
|
|
1645
|
+
if not flow:
|
|
1646
|
+
raise ProjectError(
|
|
1647
|
+
"invalid_command",
|
|
1648
|
+
"get_design_completion needs a flow argument.",
|
|
1649
|
+
)
|
|
1650
|
+
seen = self.check_results()
|
|
1651
|
+
try:
|
|
1652
|
+
return design_completion_report(
|
|
1653
|
+
flow,
|
|
1654
|
+
set(self._solvers),
|
|
1655
|
+
seen["checks"],
|
|
1656
|
+
seen["current_revision"],
|
|
1657
|
+
)
|
|
1658
|
+
except ValueError as exc:
|
|
1659
|
+
raise ProjectError("invalid_command", str(exc)) from exc
|
|
1660
|
+
|
|
1661
|
+
def run_logs(self, run_id=None, query=None, offset=0, limit=40):
|
|
1662
|
+
"""Search and page one run's captured native log.
|
|
1663
|
+
|
|
1664
|
+
Args:
|
|
1665
|
+
run_id: Run whose log to read.
|
|
1666
|
+
query: Case-insensitive substring filter; all lines
|
|
1667
|
+
when omitted.
|
|
1668
|
+
offset: First matching line to return (0-based).
|
|
1669
|
+
limit: Maximum lines per page (capped server-side).
|
|
1670
|
+
|
|
1671
|
+
Returns:
|
|
1672
|
+
Page mapping with numbered lines, totals, and a
|
|
1673
|
+
more flag.
|
|
1674
|
+
|
|
1675
|
+
Raises:
|
|
1676
|
+
ExecutionError: unknown_run for an unregistered id.
|
|
1677
|
+
ProjectError: invalid_command for a non-int
|
|
1678
|
+
offset or limit.
|
|
1679
|
+
"""
|
|
1680
|
+
return self._logs_page(
|
|
1681
|
+
{"run_id": run_id, "query": query, "offset": offset, "limit": limit}
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
def job_kinds(self):
|
|
1685
|
+
"""List registered operations with their deck tools.
|
|
1686
|
+
|
|
1687
|
+
Returns:
|
|
1688
|
+
Mapping of job kinds with operation names and the
|
|
1689
|
+
tool keys each accepts.
|
|
1690
|
+
"""
|
|
1691
|
+
return {
|
|
1692
|
+
"job_kinds": [
|
|
1693
|
+
{"operation": operation, "tools": list(solvers)}
|
|
1694
|
+
for operation, solvers in sorted(self._solvers.items())
|
|
1695
|
+
]
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def layer_stackup(self, pdk_id, revision=None, engine_root=None):
|
|
1700
|
+
"""Serve one PDK's read-only layer stackup.
|
|
1701
|
+
|
|
1702
|
+
The viewer/3D authority: ordered layers, metal/tech
|
|
1703
|
+
grouping, enabled set, colors, and Z heights parsed from
|
|
1704
|
+
the registered PDK's KLayout tech files (hash-verified
|
|
1705
|
+
through the PDK store).
|
|
1706
|
+
|
|
1707
|
+
Args:
|
|
1708
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
1709
|
+
revision: Revision pin; the newest registered revision
|
|
1710
|
+
when omitted.
|
|
1711
|
+
engine_root: Explicit store root; resolved when omitted.
|
|
1712
|
+
|
|
1713
|
+
Returns:
|
|
1714
|
+
Stackup record with layers, enabled set, groups, and
|
|
1715
|
+
Z completeness.
|
|
1716
|
+
|
|
1717
|
+
Raises:
|
|
1718
|
+
ProjectError: invalid_command for a missing pdk_id.
|
|
1719
|
+
PdkError: unknown_pdk for a missing store/id/revision
|
|
1720
|
+
or .lyp deck, pdk_drift for a tampered file.
|
|
1721
|
+
LayerServingError: unknown_stackup for a PDK with no
|
|
1722
|
+
stackup mapping.
|
|
1723
|
+
"""
|
|
1724
|
+
if not pdk_id:
|
|
1725
|
+
raise ProjectError(
|
|
1726
|
+
"invalid_command", "get_layer_stackup needs a pdk_id argument."
|
|
1727
|
+
)
|
|
1728
|
+
return get_stackup(pdk_id, revision=revision, engine_root=engine_root)
|
|
1729
|
+
|
|
1730
|
+
def layer_rows(self, pdk_id, preset=None, overrides=None, active=None,
|
|
1731
|
+
revision=None, engine_root=None):
|
|
1732
|
+
"""Render viewer rows for one PDK's stackup.
|
|
1733
|
+
|
|
1734
|
+
Row visibility defaults to the PDK flags; an explicit
|
|
1735
|
+
preset applies first, then per-layer overrides, then the
|
|
1736
|
+
active layer.
|
|
1737
|
+
|
|
1738
|
+
Args:
|
|
1739
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
1740
|
+
preset: Built-in or user preset to apply; PDK defaults
|
|
1741
|
+
when omitted.
|
|
1742
|
+
overrides: Sparse {layer name: visible} wins over the
|
|
1743
|
+
preset map.
|
|
1744
|
+
active: Active layer override; the preset active when
|
|
1745
|
+
omitted.
|
|
1746
|
+
revision: Revision pin; the newest registered revision
|
|
1747
|
+
when omitted.
|
|
1748
|
+
engine_root: Explicit store root; resolved when omitted.
|
|
1749
|
+
|
|
1750
|
+
Returns:
|
|
1751
|
+
Page with stackup_ref, preset, flip, active, and
|
|
1752
|
+
file-order rows.
|
|
1753
|
+
|
|
1754
|
+
Raises:
|
|
1755
|
+
ProjectError: invalid_command for a missing pdk_id.
|
|
1756
|
+
PdkError: unknown_pdk for a missing store/id/revision.
|
|
1757
|
+
LayerServingError: unknown_stackup for an unmapped
|
|
1758
|
+
PDK, unknown_preset for a missing preset,
|
|
1759
|
+
invalid_preset for a stale user preset,
|
|
1760
|
+
invalid_visibility for bad overrides/active.
|
|
1761
|
+
"""
|
|
1762
|
+
if not pdk_id:
|
|
1763
|
+
raise ProjectError(
|
|
1764
|
+
"invalid_command", "get_layer_rows needs a pdk_id argument."
|
|
1765
|
+
)
|
|
1766
|
+
stackup = get_stackup(pdk_id, revision=revision, engine_root=engine_root)
|
|
1767
|
+
if preset is None:
|
|
1768
|
+
return viewer_rows(stackup, visibility=overrides, active=active)
|
|
1769
|
+
return viewer_rows_for_preset(
|
|
1770
|
+
self._layer_presets, stackup, preset,
|
|
1771
|
+
overrides=overrides, active=active,
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
def layer_presets(self):
|
|
1775
|
+
"""List built-in plus user layer preset names.
|
|
1776
|
+
|
|
1777
|
+
Returns:
|
|
1778
|
+
Mapping with preset name records (name + builtin),
|
|
1779
|
+
built-ins first in pinned order.
|
|
1780
|
+
"""
|
|
1781
|
+
return {"presets": self._layer_presets.list_presets()}
|
|
1782
|
+
|
|
1783
|
+
def layer_preset_save(self, pdk_id, name, layers, render_layers=None,
|
|
1784
|
+
flip=False, active=None, overwrite=False,
|
|
1785
|
+
revision=None, engine_root=None):
|
|
1786
|
+
"""Save a user layer preset against one PDK's stackup.
|
|
1787
|
+
|
|
1788
|
+
Missing layer names default to hidden; apply() resolves
|
|
1789
|
+
the full map.
|
|
1790
|
+
|
|
1791
|
+
Args:
|
|
1792
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
1793
|
+
name: Preset name (built-in names are reserved).
|
|
1794
|
+
layers: Sparse {layer name: visible} map.
|
|
1795
|
+
render_layers: {toggle: bool} viewer aids; the default
|
|
1796
|
+
set when omitted.
|
|
1797
|
+
flip: View-from-bottom flag.
|
|
1798
|
+
active: Active layer name, or None.
|
|
1799
|
+
overwrite: Replace an existing user preset of the same
|
|
1800
|
+
name instead of conflicting.
|
|
1801
|
+
revision: Revision pin; the newest registered revision
|
|
1802
|
+
when omitted.
|
|
1803
|
+
engine_root: Explicit store root; resolved when omitted.
|
|
1804
|
+
|
|
1805
|
+
Returns:
|
|
1806
|
+
Stored preset record (builtin False).
|
|
1807
|
+
|
|
1808
|
+
Raises:
|
|
1809
|
+
ProjectError: invalid_command for a missing pdk_id.
|
|
1810
|
+
PdkError: unknown_pdk for a missing store/id/revision.
|
|
1811
|
+
LayerServingError: unknown_stackup for an unmapped
|
|
1812
|
+
PDK, invalid_preset for a bad name/shape,
|
|
1813
|
+
preset_conflict for a built-in name or an
|
|
1814
|
+
existing preset without overwrite.
|
|
1815
|
+
|
|
1816
|
+
Caution: View state, not project state: presets live in
|
|
1817
|
+
Engine memory (no revision advance, no ledger event)
|
|
1818
|
+
and are lost on restart until a durable store lands.
|
|
1819
|
+
"""
|
|
1820
|
+
if not pdk_id:
|
|
1821
|
+
raise ProjectError(
|
|
1822
|
+
"invalid_command", "save_layer_preset needs a pdk_id argument."
|
|
1823
|
+
)
|
|
1824
|
+
stackup = get_stackup(pdk_id, revision=revision, engine_root=engine_root)
|
|
1825
|
+
return self._layer_presets.save(
|
|
1826
|
+
stackup, name, layers, render_layers=render_layers,
|
|
1827
|
+
flip=flip, active=active, overwrite=overwrite,
|
|
1828
|
+
)
|
|
1829
|
+
|
|
1830
|
+
def layer_preset_delete(self, name):
|
|
1831
|
+
"""Delete one user layer preset (built-ins are undeletable).
|
|
1832
|
+
|
|
1833
|
+
Args:
|
|
1834
|
+
name: Preset name to delete.
|
|
1835
|
+
|
|
1836
|
+
Returns:
|
|
1837
|
+
Deletion record with the deleted name.
|
|
1838
|
+
|
|
1839
|
+
Raises:
|
|
1840
|
+
ProjectError: invalid_command for a missing name.
|
|
1841
|
+
LayerServingError: preset_conflict for a built-in
|
|
1842
|
+
name; unknown_preset for a missing user preset.
|
|
1843
|
+
|
|
1844
|
+
Caution: In-memory only: deletes the Engine-memory preset,
|
|
1845
|
+
no revision advance, no ledger event.
|
|
1846
|
+
"""
|
|
1847
|
+
if not name:
|
|
1848
|
+
raise ProjectError(
|
|
1849
|
+
"invalid_command", "delete_layer_preset needs a name argument."
|
|
1850
|
+
)
|
|
1851
|
+
return self._layer_presets.delete(name)
|
|
1852
|
+
|
|
1853
|
+
def layer_preset_apply(self, pdk_id, name, revision=None, engine_root=None):
|
|
1854
|
+
"""Explicitly apply one layer preset to a Stackup.
|
|
1855
|
+
|
|
1856
|
+
Args:
|
|
1857
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
1858
|
+
name: Built-in or user preset name.
|
|
1859
|
+
revision: Revision pin; the newest registered revision
|
|
1860
|
+
when omitted.
|
|
1861
|
+
engine_root: Explicit store root; resolved when omitted.
|
|
1862
|
+
|
|
1863
|
+
Returns:
|
|
1864
|
+
Full preset record with the complete layers map,
|
|
1865
|
+
renderLayers, flip, and active.
|
|
1866
|
+
|
|
1867
|
+
Raises:
|
|
1868
|
+
ProjectError: invalid_command for a missing pdk_id
|
|
1869
|
+
or name.
|
|
1870
|
+
PdkError: unknown_pdk for a missing store/id/revision.
|
|
1871
|
+
LayerServingError: unknown_stackup for an unmapped
|
|
1872
|
+
PDK, unknown_preset for a missing preset,
|
|
1873
|
+
invalid_preset for a stale user preset.
|
|
1874
|
+
"""
|
|
1875
|
+
if not pdk_id:
|
|
1876
|
+
raise ProjectError(
|
|
1877
|
+
"invalid_command", "apply_layer_preset needs a pdk_id argument."
|
|
1878
|
+
)
|
|
1879
|
+
if not name:
|
|
1880
|
+
raise ProjectError(
|
|
1881
|
+
"invalid_command", "apply_layer_preset needs a name argument."
|
|
1882
|
+
)
|
|
1883
|
+
stackup = get_stackup(pdk_id, revision=revision, engine_root=engine_root)
|
|
1884
|
+
return self._layer_presets.apply(stackup, name)
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def log_text(self, run_id):
|
|
1888
|
+
"""Return the captured native log for a run.
|
|
1889
|
+
|
|
1890
|
+
Args:
|
|
1891
|
+
run_id: Run whose native log to read.
|
|
1892
|
+
|
|
1893
|
+
Returns:
|
|
1894
|
+
Captured log text, or "" when the run logged nothing.
|
|
1895
|
+
"""
|
|
1896
|
+
return self._logs.get(run_id, "")
|
|
1897
|
+
|
|
1898
|
+
def datasets(self, run_id):
|
|
1899
|
+
"""List the waveform files a run wrote (e.g. ngspice rawfiles).
|
|
1900
|
+
|
|
1901
|
+
Args:
|
|
1902
|
+
run_id: Run whose waveform files to list.
|
|
1903
|
+
|
|
1904
|
+
Returns:
|
|
1905
|
+
List of {analysis, path} mappings; empty when the solver
|
|
1906
|
+
wrote none.
|
|
1907
|
+
|
|
1908
|
+
Caution: Paths point into the solver workdir, which the next
|
|
1909
|
+
run there overwrites; move them to keep them.
|
|
1910
|
+
"""
|
|
1911
|
+
return list(self._datasets.get(run_id, []))
|
|
1912
|
+
|
|
1913
|
+
def spectra(self, run_id):
|
|
1914
|
+
"""Return the transmission spectra an optical run computed.
|
|
1915
|
+
|
|
1916
|
+
Args:
|
|
1917
|
+
run_id: Run whose spectra to read.
|
|
1918
|
+
|
|
1919
|
+
Returns:
|
|
1920
|
+
List of {from, to, wavelength_nm[], transmission_db[]}
|
|
1921
|
+
mappings, one per target port pair; empty for other runs.
|
|
1922
|
+
"""
|
|
1923
|
+
return list(self._spectra.get(run_id, []))
|
|
1924
|
+
|
|
1925
|
+
@staticmethod
|
|
1926
|
+
def confine_spice_arguments(arguments, project_dir):
|
|
1927
|
+
"""Confine run_spice arguments (TRANSPORT-PARITY seam).
|
|
1928
|
+
|
|
1929
|
+
Args:
|
|
1930
|
+
arguments: Submitted run_spice arguments.
|
|
1931
|
+
project_dir: Project folder, or None when unbound.
|
|
1932
|
+
|
|
1933
|
+
Returns:
|
|
1934
|
+
Confined arguments (unchanged when unbound).
|
|
1935
|
+
|
|
1936
|
+
Raises:
|
|
1937
|
+
AdapterError: Invalid source or caller workdir.
|
|
1938
|
+
"""
|
|
1939
|
+
return _confine_spice_arguments(arguments, project_dir)
|
|
1940
|
+
|
|
1941
|
+
def record_run_evidence(self, outcome, arguments, project_dir=None):
|
|
1942
|
+
"""Persist one finished run (TRANSPORT-PARITY seam).
|
|
1943
|
+
|
|
1944
|
+
Args:
|
|
1945
|
+
outcome: Completion mapping with run_id, operation,
|
|
1946
|
+
and verdict.
|
|
1947
|
+
arguments: Submitted arguments for the source list.
|
|
1948
|
+
project_dir: Project folder; engine default when
|
|
1949
|
+
omitted.
|
|
1950
|
+
|
|
1951
|
+
Returns:
|
|
1952
|
+
Evidence path string, or None when unbound.
|
|
1953
|
+
"""
|
|
1954
|
+
root = project_dir if project_dir is not None else self._project_dir
|
|
1955
|
+
return _record_run_evidence(
|
|
1956
|
+
outcome, arguments, root, self.log_text)
|
|
1957
|
+
|
|
1958
|
+
@staticmethod
|
|
1959
|
+
def _evidence_source_state(root):
|
|
1960
|
+
"""Probe (commit, dirty) of the project tree.
|
|
1961
|
+
|
|
1962
|
+
Args:
|
|
1963
|
+
root: Project folder to probe.
|
|
1964
|
+
|
|
1965
|
+
Returns:
|
|
1966
|
+
(commit, dirty) pair, (None, None) outside git.
|
|
1967
|
+
"""
|
|
1968
|
+
return _evidence_source_state(root)
|
|
1969
|
+
|
|
1970
|
+
def _maybe_record_evidence(self, operation, completion, arguments):
|
|
1971
|
+
"""Record one completion when project-bound (seam).
|
|
1972
|
+
|
|
1973
|
+
Args:
|
|
1974
|
+
operation: Operation hint when the completion
|
|
1975
|
+
omits it.
|
|
1976
|
+
completion: Completion mapping, batch mapping,
|
|
1977
|
+
or ack.
|
|
1978
|
+
arguments: Submitted arguments for the source
|
|
1979
|
+
list.
|
|
1980
|
+
|
|
1981
|
+
Returns:
|
|
1982
|
+
Evidence path string, or None when skipped.
|
|
1983
|
+
"""
|
|
1984
|
+
return _maybe_record_evidence(
|
|
1985
|
+
self._project_dir, self._run_jobs, self.log_text,
|
|
1986
|
+
operation, completion, arguments,
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
def register_solver(self, operation, adapter, tool=None):
|
|
1990
|
+
"""Register a solver adapter under an operation name.
|
|
1991
|
+
|
|
1992
|
+
Args:
|
|
1993
|
+
operation: Cataloged operation the adapter serves.
|
|
1994
|
+
adapter: ToolAdapter owning the preflight/execute/parse
|
|
1995
|
+
round trip.
|
|
1996
|
+
tool: Deck-tool key selecting this adapter; defaults to
|
|
1997
|
+
the adapter class name.
|
|
1998
|
+
|
|
1999
|
+
Returns:
|
|
2000
|
+
Mapping with the operation and tool key registered.
|
|
2001
|
+
"""
|
|
2002
|
+
name = tool if tool is not None else type(adapter).__name__
|
|
2003
|
+
self._solvers.setdefault(operation, {})[name] = adapter
|
|
2004
|
+
return {"operation": operation, "tool": name}
|
|
2005
|
+
|
|
2006
|
+
def solvers_for(self, operation):
|
|
2007
|
+
"""Return the tool-keyed solvers serving one operation.
|
|
2008
|
+
|
|
2009
|
+
Introspection for docs and MCP help: per-operation prose
|
|
2010
|
+
derives from the serving adapters' docstrings, so this map
|
|
2011
|
+
is the seam — never a second source. Deck-tool selection
|
|
2012
|
+
reads the keys; the default is the first registered.
|
|
2013
|
+
|
|
2014
|
+
Args:
|
|
2015
|
+
operation: Cataloged operation name.
|
|
2016
|
+
|
|
2017
|
+
Returns:
|
|
2018
|
+
Mapping of deck-tool key to serving adapter; empty
|
|
2019
|
+
when no solver is registered.
|
|
2020
|
+
"""
|
|
2021
|
+
return dict(self._solvers.get(operation) or {})
|
|
2022
|
+
|
|
2023
|
+
def findings(self, run_id, limit=50, cursor=None):
|
|
2024
|
+
"""Read a page of findings for a completed run.
|
|
2025
|
+
|
|
2026
|
+
In-memory verdicts answer first; across a reboot the
|
|
2027
|
+
completion event answers instead.
|
|
2028
|
+
|
|
2029
|
+
Args:
|
|
2030
|
+
run_id: Run whose findings to read.
|
|
2031
|
+
limit: Maximum findings in this page.
|
|
2032
|
+
cursor: Opaque position from a previous page, if any.
|
|
2033
|
+
|
|
2034
|
+
Returns:
|
|
2035
|
+
Finding page for the run.
|
|
2036
|
+
|
|
2037
|
+
Raises:
|
|
2038
|
+
ExecutionError: unknown_run for an id with no
|
|
2039
|
+
completion event.
|
|
2040
|
+
"""
|
|
2041
|
+
try:
|
|
2042
|
+
return self._execution.findings(run_id, limit=limit, cursor=cursor)
|
|
2043
|
+
except ExecutionError:
|
|
2044
|
+
return self._ledger_findings(run_id, limit=limit, cursor=cursor)
|
|
2045
|
+
|
|
2046
|
+
def resolve_celllinks(self, blocks, known_cells, variant=None):
|
|
2047
|
+
"""Resolve run/export cell-link records for block instances.
|
|
2048
|
+
|
|
2049
|
+
CELL-LINK seam: resolves (cell, pinMap, variant) per block
|
|
2050
|
+
through interfaces.celllink, emitting only non-empty links.
|
|
2051
|
+
Editing stays free — unknown cells fail here, at run time.
|
|
2052
|
+
|
|
2053
|
+
Args:
|
|
2054
|
+
blocks: Block mappings, each with an id and an optional
|
|
2055
|
+
CellLink component.
|
|
2056
|
+
known_cells: Available cell names the run may target.
|
|
2057
|
+
variant: Variant name applied to every block; None and
|
|
2058
|
+
REFERENCE resolve each stored base link.
|
|
2059
|
+
|
|
2060
|
+
Returns:
|
|
2061
|
+
Emission records in input order, each with block, cell,
|
|
2062
|
+
pinMap, and variant keys.
|
|
2063
|
+
|
|
2064
|
+
Raises:
|
|
2065
|
+
ContractError: invalid_block for a malformed block
|
|
2066
|
+
entry, unknown_variant for an undefined variant.
|
|
2067
|
+
CellLinkError: unknown_cell naming the block and cell
|
|
2068
|
+
when a resolved cell is unavailable to the run.
|
|
2069
|
+
"""
|
|
2070
|
+
return resolve_celllink_run(blocks, known_cells, variant=variant)
|
|
2071
|
+
|
|
2072
|
+
def _run_queued(self, command):
|
|
2073
|
+
"""Execute admitted requests by id: deferred singles, fetch, batches.
|
|
2074
|
+
|
|
2075
|
+
Submit already ran inline, so this is the explicit path:
|
|
2076
|
+
deferred execution for admissions drained elsewhere
|
|
2077
|
+
(crash survivors, staged backlog), idempotent fetch for
|
|
2078
|
+
finished runs, and ordered batch pumping.
|
|
2079
|
+
|
|
2080
|
+
Request keys:
|
|
2081
|
+
run_id: Admitted run to execute or fetch.
|
|
2082
|
+
request_id: Envelope id admitted by an earlier submit
|
|
2083
|
+
(resolves to its run when run_id is absent).
|
|
2084
|
+
run_ids: Batch form — admitted runs to pump oldest
|
|
2085
|
+
listed first.
|
|
2086
|
+
force: Batch form only — bypass drain-time verdict
|
|
2087
|
+
gates (hard deps still refuse).
|
|
2088
|
+
|
|
2089
|
+
Args:
|
|
2090
|
+
command: Envelope whose arguments carry run_id,
|
|
2091
|
+
request_id, or run_ids.
|
|
2092
|
+
|
|
2093
|
+
Returns:
|
|
2094
|
+
Completion mapping with run_id, state, verdict, and
|
|
2095
|
+
operation for a single id (stored, never
|
|
2096
|
+
re-executed, when the run already finished); batch
|
|
2097
|
+
mapping with run_ids, completions, refused, per-item
|
|
2098
|
+
states, and worst-state-wins summary for the batch
|
|
2099
|
+
form (every listed id lands in exactly one of
|
|
2100
|
+
completions/refused).
|
|
2101
|
+
|
|
2102
|
+
Raises:
|
|
2103
|
+
ProjectError: invalid_command when the arguments lack a
|
|
2104
|
+
run id or the batch list is malformed,
|
|
2105
|
+
unknown_request for an id never admitted,
|
|
2106
|
+
unsupported_operation when the admitted
|
|
2107
|
+
operation has no registered solver.
|
|
2108
|
+
"""
|
|
2109
|
+
arguments = dict(command.get("arguments") or {})
|
|
2110
|
+
if arguments.get("run_ids") is not None:
|
|
2111
|
+
return self._run_batch(
|
|
2112
|
+
arguments.get("run_ids"),
|
|
2113
|
+
force=arguments.get("force") is True,
|
|
2114
|
+
)
|
|
2115
|
+
run_id = arguments.get("run_id")
|
|
2116
|
+
if run_id is not None:
|
|
2117
|
+
# Idempotent fetch: a finished run returns its stored
|
|
2118
|
+
# completion instead of executing again.
|
|
2119
|
+
stored = self._stored_completion(run_id)
|
|
2120
|
+
if stored is not None:
|
|
2121
|
+
return stored
|
|
2122
|
+
pending = self._run_jobs.get(run_id)
|
|
2123
|
+
if pending is None:
|
|
2124
|
+
# Reboot path: the admitting request survives in
|
|
2125
|
+
# the ledger even though this process never saw it.
|
|
2126
|
+
queued_request = request_for_run(
|
|
2127
|
+
self._project.events_since(0), run_id
|
|
2128
|
+
)
|
|
2129
|
+
if queued_request is None:
|
|
2130
|
+
raise ProjectError(
|
|
2131
|
+
"unknown_request", f"No queued request: {run_id}."
|
|
2132
|
+
) from None
|
|
2133
|
+
stored_command, ack = self._project.command_for(
|
|
2134
|
+
queued_request
|
|
2135
|
+
)
|
|
2136
|
+
pending = {
|
|
2137
|
+
"operation": stored_command.get("operation"),
|
|
2138
|
+
"arguments": dict(stored_command.get("arguments") or {}),
|
|
2139
|
+
"snapshot_id": ack.get("snapshot_id", ""),
|
|
2140
|
+
"request_id": str(queued_request),
|
|
2141
|
+
}
|
|
2142
|
+
self._run_jobs[run_id] = pending
|
|
2143
|
+
stored_operation = pending["operation"]
|
|
2144
|
+
run_arguments = dict(pending["arguments"])
|
|
2145
|
+
snapshot_id = pending["snapshot_id"]
|
|
2146
|
+
request_id = pending["request_id"]
|
|
2147
|
+
else:
|
|
2148
|
+
request_id = arguments.get("request_id")
|
|
2149
|
+
if request_id is None:
|
|
2150
|
+
raise ProjectError(
|
|
2151
|
+
"invalid_command",
|
|
2152
|
+
"run_job needs a run_id, request_id, or run_ids argument.",
|
|
2153
|
+
)
|
|
2154
|
+
stored_command, ack = self._project.command_for(request_id)
|
|
2155
|
+
stored_operation = stored_command.get("operation")
|
|
2156
|
+
run_arguments = dict(stored_command.get("arguments") or {})
|
|
2157
|
+
snapshot_id = ack.get("snapshot_id", "")
|
|
2158
|
+
request_id = str(request_id)
|
|
2159
|
+
run_id = ack.get("run_id")
|
|
2160
|
+
completion = self._stored_completion(run_id)
|
|
2161
|
+
if completion is not None:
|
|
2162
|
+
return completion
|
|
2163
|
+
if stored_operation == "python_execute":
|
|
2164
|
+
return self._run_pyexec(run_id, request_id)
|
|
2165
|
+
if not self._solvers.get(stored_operation):
|
|
2166
|
+
raise ProjectError(
|
|
2167
|
+
"unsupported_operation", f"No solver registered: {stored_operation}."
|
|
2168
|
+
)
|
|
2169
|
+
if not self._execution.known(run_id):
|
|
2170
|
+
# Reboot path: the ledger admitted this run, but this
|
|
2171
|
+
# process holds no execution record for it yet.
|
|
2172
|
+
self._execution.dispatch(
|
|
2173
|
+
stored_operation,
|
|
2174
|
+
{"cell": run_arguments.get("cell")},
|
|
2175
|
+
snapshot_id,
|
|
2176
|
+
run_id=run_id,
|
|
2177
|
+
)
|
|
2178
|
+
# Submit-time admission data stays out of the solver job.
|
|
2179
|
+
# Targets were already filled at submit time; never re-fill
|
|
2180
|
+
# here (filled min/max would read as caller targets).
|
|
2181
|
+
run_arguments.pop("expected_revision", None)
|
|
2182
|
+
return self._run_operation(
|
|
2183
|
+
stored_operation,
|
|
2184
|
+
run_arguments,
|
|
2185
|
+
snapshot_id,
|
|
2186
|
+
request_id,
|
|
2187
|
+
run_id=run_id,
|
|
2188
|
+
contain=True,
|
|
2189
|
+
)
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
def run(self, command):
|
|
2193
|
+
"""Execute an operation synchronously through its solver.
|
|
2194
|
+
|
|
2195
|
+
`deck.tool` selects the registered solver, defaulting to
|
|
2196
|
+
the first registered; solver violations nest into findings
|
|
2197
|
+
and the verdict is judged from findings alone — exit zero
|
|
2198
|
+
is not PASS. The native exit code is recorded on the run,
|
|
2199
|
+
never the verdict.
|
|
2200
|
+
CRUD/rewind mutations delegate to submit's immediate path
|
|
2201
|
+
so the CLI `run` spelling shares the one implementation.
|
|
2202
|
+
|
|
2203
|
+
Args:
|
|
2204
|
+
command: Envelope with project_id, operation, and
|
|
2205
|
+
arguments (deck.tool selects the solver).
|
|
2206
|
+
|
|
2207
|
+
Returns:
|
|
2208
|
+
Completion mapping with run_id, state, verdict, and
|
|
2209
|
+
operation. Measured values ride on the verdict (PASS
|
|
2210
|
+
included), never only on failure. Adapter failures become not_run verdicts,
|
|
2211
|
+
never exceptions. Every judged run appends
|
|
2212
|
+
run_completed (PASS/FAIL) or run_failed (NOT_RUN) to
|
|
2213
|
+
the ledger with the engine's actor and the verdict.
|
|
2214
|
+
CRUD/rewind/place_cell operations return the immediate
|
|
2215
|
+
mutation mapping instead (new revision + events +
|
|
2216
|
+
approval-UI fields).
|
|
2217
|
+
|
|
2218
|
+
Raises:
|
|
2219
|
+
ProjectError: cross_project_denied for a foreign
|
|
2220
|
+
envelope, unsupported_operation for an unregistered
|
|
2221
|
+
operation or unknown deck tool, unknown_block for
|
|
2222
|
+
a block target naming no block, invalid_target for
|
|
2223
|
+
a malformed target, unsupported_scope for a block
|
|
2224
|
+
target on a verb serving no block runs yet,
|
|
2225
|
+
unknown_run_param for an unknown run.request field,
|
|
2226
|
+
invalid_run_param for a mistyped run.request knob,
|
|
2227
|
+
invalid_run_request for a non-mapping run.request,
|
|
2228
|
+
invalid_command when run_job lacks a
|
|
2229
|
+
run_id/request_id argument, for caller-supplied
|
|
2230
|
+
run_spice targets, when a CRUD
|
|
2231
|
+
mutation or place_cell is malformed, or a rewind
|
|
2232
|
+
lacks a target or targets the live head,
|
|
2233
|
+
revision_conflict for a stale CRUD/rewind/
|
|
2234
|
+
place_cell expected_revision,
|
|
2235
|
+
unknown_document for a missing CRUD document,
|
|
2236
|
+
unknown_revision for a rewind target with no
|
|
2237
|
+
snapshot.
|
|
2238
|
+
|
|
2239
|
+
Caution: Runs the solver now and records the verdict —
|
|
2240
|
+
exit zero is not PASS; read the verdict, never the
|
|
2241
|
+
exit path.
|
|
2242
|
+
"""
|
|
2243
|
+
envelope = command.get("project_id")
|
|
2244
|
+
if envelope != self._project_id:
|
|
2245
|
+
raise ProjectError(
|
|
2246
|
+
"cross_project_denied",
|
|
2247
|
+
f"Bound to {self._project_id}, got {envelope}.",
|
|
2248
|
+
)
|
|
2249
|
+
self._replay_once()
|
|
2250
|
+
name = command.get("operation")
|
|
2251
|
+
if name == "run_job":
|
|
2252
|
+
completion = self._run_queued(command)
|
|
2253
|
+
self._maybe_record_evidence(name, completion,
|
|
2254
|
+
dict(command.get("arguments") or {}))
|
|
2255
|
+
return completion
|
|
2256
|
+
if name in _CRUD_OPERATIONS or name == "place_cell":
|
|
2257
|
+
return self.submit(command)
|
|
2258
|
+
if name == "python_execute":
|
|
2259
|
+
raise ProjectError(
|
|
2260
|
+
"invalid_command",
|
|
2261
|
+
"python_execute needs submit, human approval, then "
|
|
2262
|
+
"run_job; direct run cannot approve.",
|
|
2263
|
+
)
|
|
2264
|
+
arguments = self._apply_targets(name, dict(command.get("arguments") or {}))
|
|
2265
|
+
arguments = self._apply_run_request(arguments)
|
|
2266
|
+
if name == "run_spice":
|
|
2267
|
+
arguments = self.confine_spice_arguments(
|
|
2268
|
+
arguments, self._project_dir
|
|
2269
|
+
)
|
|
2270
|
+
block_scope = self._admit_block_scope(name, arguments)
|
|
2271
|
+
completion = self._run_operation(
|
|
2272
|
+
name,
|
|
2273
|
+
arguments,
|
|
2274
|
+
command.get("snapshot_id", ""),
|
|
2275
|
+
None,
|
|
2276
|
+
block_scope=block_scope,
|
|
2277
|
+
)
|
|
2278
|
+
self._maybe_record_evidence(name, completion, arguments)
|
|
2279
|
+
return completion
|