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,124 @@
|
|
|
1
|
+
"""Submit-immediate (API-GENERALIZE field note 2026-11-01): run by default.
|
|
2
|
+
|
|
3
|
+
Queue-by-default is rejected: `submit()` executes the job inline and
|
|
4
|
+
returns the same `run_id` ack shape plus the verdict payload, while
|
|
5
|
+
`run_job` stays for explicit/deferred/batch pumping plus idempotent
|
|
6
|
+
result fetch. This module is the reference implementation of that
|
|
7
|
+
contract over the public engine port (`submit`/`run`): it behaves
|
|
8
|
+
identically against a queueing engine (submit, then pump once) and a
|
|
9
|
+
wired immediate engine (the ack already carries the verdict, so no
|
|
10
|
+
second call is made). Replay and fetch paths execute at most once per
|
|
11
|
+
run id: the ack's run id keys a completion cache, and `fetch` reads
|
|
12
|
+
back through it.
|
|
13
|
+
"""
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from ..errors import ExecutionError # noqa: F401 — re-exported for callers
|
|
18
|
+
except ImportError: # standalone verification before install
|
|
19
|
+
from rocket_engine.execution.errors import ExecutionError # noqa: F401
|
|
20
|
+
|
|
21
|
+
SUBMIT_VERSION = 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def merge_ack_completion(ack, completion):
|
|
25
|
+
"""Merge an admission ack with its completion (ack shape wins on identity).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
ack: Admission ack with run_id, snapshot_id, state, and
|
|
29
|
+
revision/cursor fields.
|
|
30
|
+
completion: Completion with run_id, state, verdict, and
|
|
31
|
+
operation for the same run.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Merged mapping: every ack identity field (request_id,
|
|
35
|
+
project_id, run_id, snapshot_id, current_revision,
|
|
36
|
+
event_cursor) plus the completion's state, verdict,
|
|
37
|
+
and operation.
|
|
38
|
+
"""
|
|
39
|
+
return {
|
|
40
|
+
**ack,
|
|
41
|
+
"state": completion["state"],
|
|
42
|
+
"verdict": completion.get("verdict"),
|
|
43
|
+
"operation": completion.get("operation"),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SubmitImmediate:
|
|
48
|
+
"""Submit-then-pump-once over the public engine port.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
engine: Duck-typed engine port with `submit(command)` and
|
|
52
|
+
`run(command)` (Engine serves).
|
|
53
|
+
project_id: Project id stamped on every command.
|
|
54
|
+
|
|
55
|
+
Caution: The completion cache is per-instance: share one
|
|
56
|
+
instance per engine port when replays must not re-execute.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, engine, project_id):
|
|
60
|
+
self._engine = engine
|
|
61
|
+
self._project_id = project_id
|
|
62
|
+
self._completed = {}
|
|
63
|
+
|
|
64
|
+
def submit(self, operation, arguments, request_id=None):
|
|
65
|
+
"""Submit an operation and return its completion inline.
|
|
66
|
+
|
|
67
|
+
The ack's run id is the execution id: against a queueing
|
|
68
|
+
engine the job is pumped exactly once via `run_job`; against
|
|
69
|
+
an immediate engine the ack already carries the verdict and
|
|
70
|
+
no second call is made. Replaying a request id returns the
|
|
71
|
+
stored completion without re-executing.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
operation: Cataloged operation name.
|
|
75
|
+
arguments: Operation arguments.
|
|
76
|
+
request_id: Envelope id; generated when omitted.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Merged ack-plus-completion mapping with run_id, queued
|
|
80
|
+
revision fields, completed state, verdict, and
|
|
81
|
+
operation.
|
|
82
|
+
"""
|
|
83
|
+
request_id = request_id or uuid.uuid4().hex
|
|
84
|
+
ack = self._engine.submit({
|
|
85
|
+
"project_id": self._project_id,
|
|
86
|
+
"operation": operation,
|
|
87
|
+
"arguments": dict(arguments),
|
|
88
|
+
"request_id": request_id,
|
|
89
|
+
})
|
|
90
|
+
if ack.get("verdict") is not None and ack.get("state") != "queued":
|
|
91
|
+
self._completed[ack["run_id"]] = dict(ack)
|
|
92
|
+
return dict(ack)
|
|
93
|
+
if ack["run_id"] in self._completed:
|
|
94
|
+
return dict(self._completed[ack["run_id"]])
|
|
95
|
+
completion = self._engine.run({
|
|
96
|
+
"project_id": self._project_id,
|
|
97
|
+
"operation": "run_job",
|
|
98
|
+
"arguments": {"run_id": ack["run_id"]},
|
|
99
|
+
"request_id": f"{request_id}:pump",
|
|
100
|
+
})
|
|
101
|
+
merged = merge_ack_completion(ack, completion)
|
|
102
|
+
self._completed[ack["run_id"]] = merged
|
|
103
|
+
return dict(merged)
|
|
104
|
+
|
|
105
|
+
def fetch(self, run_id):
|
|
106
|
+
"""Fetch a run's completion idempotently.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
run_id: Run submitted through this instance.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Stored completion for the run; repeated fetches return
|
|
113
|
+
the identical mapping without re-executing.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ExecutionError: unknown_run for an id this instance
|
|
117
|
+
never completed.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
return dict(self._completed[run_id])
|
|
121
|
+
except KeyError:
|
|
122
|
+
raise ExecutionError(
|
|
123
|
+
"unknown_run", f"Unknown run: {run_id}."
|
|
124
|
+
) from None
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""Block-scoped run targets (BLOCK-RUNS): run-from-block substrate.
|
|
2
|
+
|
|
3
|
+
Every `run_*` verb targets the project today; this module teaches run
|
|
4
|
+
targets an entity scope so `target: {scope: block, id}` flows through
|
|
5
|
+
submit, preflight, snapshot, and verdict with the same honesty as
|
|
6
|
+
project runs. No new verbs: scope rides the existing `target` shape,
|
|
7
|
+
and any future verb ships with a scope parameter from birth.
|
|
8
|
+
|
|
9
|
+
Pipeline (the wiring patch calls these from the composition root):
|
|
10
|
+
|
|
11
|
+
1. `parse_target` normalizes the caller target (absent means the
|
|
12
|
+
whole project).
|
|
13
|
+
2. `check_operation_scope` gates block scope to the verbs that
|
|
14
|
+
serve it — SPICE + syntax first, decks following.
|
|
15
|
+
3. `resolve_block` resolves the block entity; unknown ids fail
|
|
16
|
+
with the named `unknown_block` reason, never an empty run.
|
|
17
|
+
4. `snapshot_closure` pins the block's component closure
|
|
18
|
+
(sources, CellLink, params, constraints) so the run cannot
|
|
19
|
+
drift mid-flight and sibling edits cannot leak in.
|
|
20
|
+
5. `narrow_job_for_block` rewrites the solver job onto the
|
|
21
|
+
closure sources before adapter preflight.
|
|
22
|
+
6. `build_scope_record` binds `(run_id, revision, block_id,
|
|
23
|
+
deck)` provenance plus the closure hash; `stamp_verdict`
|
|
24
|
+
records that scope on the verdict, and the run event carries
|
|
25
|
+
it into the ledger as provenance.
|
|
26
|
+
|
|
27
|
+
The module never imports the composition root, adapters, or the
|
|
28
|
+
CELL-LINK lane: CellLink resolution arrives as an optional caller
|
|
29
|
+
callable, so this substrate stands alone however the lanes land.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import copy
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
|
|
37
|
+
#: Scopes a run target may name. Absent targets mean `project`.
|
|
38
|
+
PROJECT_SCOPE = "project"
|
|
39
|
+
BLOCK_SCOPE = "block"
|
|
40
|
+
SUPPORTED_SCOPES = (PROJECT_SCOPE, BLOCK_SCOPE)
|
|
41
|
+
|
|
42
|
+
#: Run verbs serving block scope. SPICE + syntax first; every other
|
|
43
|
+
#: verb rejects block targets with `unsupported_scope` until its deck
|
|
44
|
+
#: lane wires narrowing.
|
|
45
|
+
BLOCK_RUN_OPERATIONS = frozenset({"run_spice", "run_syntax"})
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"BLOCK_RUN_OPERATIONS",
|
|
49
|
+
"BLOCK_SCOPE",
|
|
50
|
+
"PROJECT_SCOPE",
|
|
51
|
+
"SUPPORTED_SCOPES",
|
|
52
|
+
"BlockScopeError",
|
|
53
|
+
"build_scope_record",
|
|
54
|
+
"check_operation_scope",
|
|
55
|
+
"closure_hash",
|
|
56
|
+
"narrow_job_for_block",
|
|
57
|
+
"parse_target",
|
|
58
|
+
"resolve_block",
|
|
59
|
+
"scope_provenance",
|
|
60
|
+
"snapshot_closure",
|
|
61
|
+
"stamp_verdict",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BlockScopeError(Exception):
|
|
66
|
+
"""Block-scope violation with a stable machine code.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
code: Stable machine-readable error code
|
|
70
|
+
(unknown_block, invalid_target, unsupported_scope,
|
|
71
|
+
invalid_block).
|
|
72
|
+
message: Human-readable detail.
|
|
73
|
+
fields: Itemized fields, when the error names any.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, code, message, fields=None):
|
|
77
|
+
super().__init__(message)
|
|
78
|
+
self.code = code
|
|
79
|
+
self.fields = fields or []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_target(target):
|
|
83
|
+
"""Normalize a caller run target to (scope, id).
|
|
84
|
+
|
|
85
|
+
Absent targets mean the whole project; block targets name the
|
|
86
|
+
block entity id the run is scoped to.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
target: Caller target mapping ({scope, id}), or None for
|
|
90
|
+
a project run.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Normalized mapping with scope (project or block) and id
|
|
94
|
+
(the block id, or None for project runs).
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
BlockScopeError: invalid_target for a non-mapping target,
|
|
98
|
+
an unknown scope, a block scope without a non-empty
|
|
99
|
+
string id, or a project scope carrying an id.
|
|
100
|
+
"""
|
|
101
|
+
if target is None:
|
|
102
|
+
return {"scope": PROJECT_SCOPE, "id": None}
|
|
103
|
+
if not isinstance(target, Mapping):
|
|
104
|
+
raise BlockScopeError(
|
|
105
|
+
"invalid_target", f"Run target must be a mapping, got {target!r}."
|
|
106
|
+
)
|
|
107
|
+
scope = target.get("scope", PROJECT_SCOPE)
|
|
108
|
+
if scope not in SUPPORTED_SCOPES:
|
|
109
|
+
raise BlockScopeError(
|
|
110
|
+
"invalid_target",
|
|
111
|
+
f"Unknown target scope {scope!r}; supported scopes: "
|
|
112
|
+
f"{list(SUPPORTED_SCOPES)}.",
|
|
113
|
+
)
|
|
114
|
+
block_id = target.get("id")
|
|
115
|
+
if scope == BLOCK_SCOPE:
|
|
116
|
+
if not isinstance(block_id, str) or not block_id:
|
|
117
|
+
raise BlockScopeError(
|
|
118
|
+
"invalid_target",
|
|
119
|
+
"target.scope 'block' needs a non-empty string id.",
|
|
120
|
+
)
|
|
121
|
+
return {"scope": BLOCK_SCOPE, "id": block_id}
|
|
122
|
+
if block_id is not None:
|
|
123
|
+
raise BlockScopeError(
|
|
124
|
+
"invalid_target", "target.scope 'project' takes no id."
|
|
125
|
+
)
|
|
126
|
+
return {"scope": PROJECT_SCOPE, "id": None}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_operation_scope(operation, scope):
|
|
130
|
+
"""Gate one operation against a target scope.
|
|
131
|
+
|
|
132
|
+
Project scope serves every operation; block scope serves only
|
|
133
|
+
the verbs in BLOCK_RUN_OPERATIONS (SPICE + syntax first).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
operation: Cataloged operation about to run.
|
|
137
|
+
scope: Target scope (project or block).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The admitted scope, unchanged.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
BlockScopeError: invalid_target for an unknown scope,
|
|
144
|
+
unsupported_scope when the operation serves no block
|
|
145
|
+
runs yet (the message names the serving verbs).
|
|
146
|
+
"""
|
|
147
|
+
if scope == PROJECT_SCOPE:
|
|
148
|
+
return PROJECT_SCOPE
|
|
149
|
+
if scope != BLOCK_SCOPE:
|
|
150
|
+
raise BlockScopeError(
|
|
151
|
+
"invalid_target",
|
|
152
|
+
f"Unknown target scope {scope!r}; supported scopes: "
|
|
153
|
+
f"{list(SUPPORTED_SCOPES)}.",
|
|
154
|
+
)
|
|
155
|
+
if operation not in BLOCK_RUN_OPERATIONS:
|
|
156
|
+
raise BlockScopeError(
|
|
157
|
+
"unsupported_scope",
|
|
158
|
+
f"{operation} serves no block runs yet "
|
|
159
|
+
f"(block runs serve: {sorted(BLOCK_RUN_OPERATIONS)}).",
|
|
160
|
+
)
|
|
161
|
+
return BLOCK_SCOPE
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def resolve_block(block_id, blocks):
|
|
165
|
+
"""Resolve one block entity from the block store.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
block_id: Block entity id to resolve.
|
|
169
|
+
blocks: Block store: a mapping of id to block entity, or
|
|
170
|
+
a list of block mappings each carrying an id.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The resolved block entity mapping (owned by the store;
|
|
174
|
+
snapshot it before running).
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
BlockScopeError: invalid_target for a non-string or empty
|
|
178
|
+
block id, invalid_block for a malformed store or block
|
|
179
|
+
entry, unknown_block naming the id when no block
|
|
180
|
+
matches.
|
|
181
|
+
"""
|
|
182
|
+
if not isinstance(block_id, str) or not block_id:
|
|
183
|
+
raise BlockScopeError(
|
|
184
|
+
"invalid_target", f"Block id must be a non-empty string, got {block_id!r}."
|
|
185
|
+
)
|
|
186
|
+
if isinstance(blocks, Mapping):
|
|
187
|
+
try:
|
|
188
|
+
return blocks[block_id]
|
|
189
|
+
except KeyError:
|
|
190
|
+
raise BlockScopeError(
|
|
191
|
+
"unknown_block",
|
|
192
|
+
f"Unknown block: {block_id!r}.",
|
|
193
|
+
fields=[block_id],
|
|
194
|
+
) from None
|
|
195
|
+
if isinstance(blocks, (list, tuple)):
|
|
196
|
+
for block in blocks:
|
|
197
|
+
if not isinstance(block, Mapping):
|
|
198
|
+
raise BlockScopeError(
|
|
199
|
+
"invalid_block", f"Block must be a mapping, got {block!r}."
|
|
200
|
+
)
|
|
201
|
+
entry_id = block.get("id")
|
|
202
|
+
if not isinstance(entry_id, str) or not entry_id:
|
|
203
|
+
raise BlockScopeError(
|
|
204
|
+
"invalid_block", "Each block needs a non-empty string id."
|
|
205
|
+
)
|
|
206
|
+
if entry_id == block_id:
|
|
207
|
+
return block
|
|
208
|
+
raise BlockScopeError(
|
|
209
|
+
"unknown_block", f"Unknown block: {block_id!r}.", fields=[block_id]
|
|
210
|
+
)
|
|
211
|
+
raise BlockScopeError(
|
|
212
|
+
"invalid_block", "Blocks must be a mapping or a list of blocks."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def snapshot_closure(block, resolve_celllink=None, variant=None):
|
|
217
|
+
"""Pin a block's component closure for one run.
|
|
218
|
+
|
|
219
|
+
The closure (sources, CellLink, params, constraints) is the
|
|
220
|
+
complete input set the scoped run may read: snapshotting it at
|
|
221
|
+
submit time freezes the run against later edits and keeps
|
|
222
|
+
sibling blocks out by construction.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
block: Block entity mapping with an id plus optional
|
|
226
|
+
sources, CellLink, params, and constraints components.
|
|
227
|
+
resolve_celllink: Optional resolver called as
|
|
228
|
+
`resolve_celllink(block, variant)` returning the
|
|
229
|
+
effective (cell, pinMap, variant) link; the raw stored
|
|
230
|
+
CellLink is always pinned too. None pins the stored
|
|
231
|
+
link only.
|
|
232
|
+
variant: Variant selector passed to the resolver; None
|
|
233
|
+
resolves the stored base link.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Closure mapping with block_id, sources, celllink (the
|
|
237
|
+
stored component, or None), celllink_resolved (the
|
|
238
|
+
resolver output, or None), params, constraints, and
|
|
239
|
+
variant. Every collection is a copy: storage is never
|
|
240
|
+
mutated and never aliased.
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
BlockScopeError: invalid_block for a malformed block or
|
|
244
|
+
component (non-list sources, non-mapping CellLink,
|
|
245
|
+
params, or constraints).
|
|
246
|
+
"""
|
|
247
|
+
if not isinstance(block, Mapping):
|
|
248
|
+
raise BlockScopeError(
|
|
249
|
+
"invalid_block", f"Block must be a mapping, got {block!r}."
|
|
250
|
+
)
|
|
251
|
+
block_id = block.get("id")
|
|
252
|
+
if not isinstance(block_id, str) or not block_id:
|
|
253
|
+
raise BlockScopeError(
|
|
254
|
+
"invalid_block", "Each block needs a non-empty string id."
|
|
255
|
+
)
|
|
256
|
+
sources = block.get("sources", [])
|
|
257
|
+
if not isinstance(sources, list) or any(
|
|
258
|
+
not isinstance(s, str) or not s for s in sources
|
|
259
|
+
):
|
|
260
|
+
raise BlockScopeError(
|
|
261
|
+
"invalid_block", f"Block {block_id!r} sources must be a list of paths."
|
|
262
|
+
)
|
|
263
|
+
raw_link = block.get("CellLink")
|
|
264
|
+
if raw_link is not None and not isinstance(raw_link, Mapping):
|
|
265
|
+
raise BlockScopeError(
|
|
266
|
+
"invalid_block", f"Block {block_id!r} CellLink must be a mapping."
|
|
267
|
+
)
|
|
268
|
+
params = block.get("params", {})
|
|
269
|
+
if not isinstance(params, Mapping):
|
|
270
|
+
raise BlockScopeError(
|
|
271
|
+
"invalid_block", f"Block {block_id!r} params must be a mapping."
|
|
272
|
+
)
|
|
273
|
+
constraints = block.get("constraints", {})
|
|
274
|
+
if not isinstance(constraints, Mapping):
|
|
275
|
+
raise BlockScopeError(
|
|
276
|
+
"invalid_block", f"Block {block_id!r} constraints must be a mapping."
|
|
277
|
+
)
|
|
278
|
+
resolved = None
|
|
279
|
+
if resolve_celllink is not None:
|
|
280
|
+
resolved = resolve_celllink(block, variant)
|
|
281
|
+
return {
|
|
282
|
+
"block_id": block_id,
|
|
283
|
+
"sources": list(sources),
|
|
284
|
+
"celllink": copy.deepcopy(dict(raw_link)) if raw_link is not None else None,
|
|
285
|
+
"celllink_resolved": resolved,
|
|
286
|
+
"params": copy.deepcopy(dict(params)),
|
|
287
|
+
"constraints": copy.deepcopy(dict(constraints)),
|
|
288
|
+
"variant": variant,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def closure_hash(closure):
|
|
293
|
+
"""Hash a closure for provenance and change detection.
|
|
294
|
+
|
|
295
|
+
The hash is content-addressed and key-order stable: equal
|
|
296
|
+
closures hash equally, so a rerun after a sibling edit proves
|
|
297
|
+
isolation by hashing identically.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
closure: Closure mapping from snapshot_closure.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Hex sha256 over the canonical JSON encoding.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
BlockScopeError: invalid_block when the closure is not a
|
|
307
|
+
mapping.
|
|
308
|
+
"""
|
|
309
|
+
if not isinstance(closure, Mapping):
|
|
310
|
+
raise BlockScopeError(
|
|
311
|
+
"invalid_block", f"Closure must be a mapping, got {closure!r}."
|
|
312
|
+
)
|
|
313
|
+
canonical = json.dumps(
|
|
314
|
+
closure, sort_keys=True, separators=(",", ":"), default=str
|
|
315
|
+
)
|
|
316
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def narrow_job_for_block(job, closure):
|
|
320
|
+
"""Rewrite a solver job onto a block closure.
|
|
321
|
+
|
|
322
|
+
Sources are replaced wholesale: the adapter preflights and
|
|
323
|
+
executes only the closure files, so project sources never leak
|
|
324
|
+
into a block run. The closure hash rides along for provenance.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
job: Solver job mapping (operation plus tool inputs).
|
|
328
|
+
closure: Closure mapping from snapshot_closure.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Job copy with sources replaced by the closure sources and
|
|
332
|
+
a block_scope record (block_id plus closure_hash). The
|
|
333
|
+
input job is never mutated.
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
BlockScopeError: invalid_target when the job is not a
|
|
337
|
+
mapping, invalid_block when the closure carries no
|
|
338
|
+
block_id or sources list.
|
|
339
|
+
"""
|
|
340
|
+
if not isinstance(job, Mapping):
|
|
341
|
+
raise BlockScopeError(
|
|
342
|
+
"invalid_target", f"Solver job must be a mapping, got {job!r}."
|
|
343
|
+
)
|
|
344
|
+
if (
|
|
345
|
+
not isinstance(closure, Mapping)
|
|
346
|
+
or not isinstance(closure.get("block_id"), str)
|
|
347
|
+
or not closure.get("block_id")
|
|
348
|
+
or not isinstance(closure.get("sources"), list)
|
|
349
|
+
):
|
|
350
|
+
raise BlockScopeError(
|
|
351
|
+
"invalid_block", "Closure must carry a block_id and a sources list."
|
|
352
|
+
)
|
|
353
|
+
narrowed = dict(job)
|
|
354
|
+
narrowed["sources"] = list(closure["sources"])
|
|
355
|
+
narrowed["block_scope"] = {
|
|
356
|
+
"block_id": closure["block_id"],
|
|
357
|
+
"closure_hash": closure_hash(closure),
|
|
358
|
+
}
|
|
359
|
+
return narrowed
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def scope_provenance(run_id, revision, block_id, deck):
|
|
363
|
+
"""Cite a block run as (run_id, revision, block_id, deck).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
run_id: Run the verdict belongs to.
|
|
367
|
+
revision: Project revision the closure was pinned at.
|
|
368
|
+
block_id: Block entity the run was scoped to.
|
|
369
|
+
deck: Deck mapping the solver ran under (copied
|
|
370
|
+
verbatim), or None for the default deck.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Provenance mapping with run_id, revision, block_id, and
|
|
374
|
+
deck keys.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
BlockScopeError: invalid_target for an empty citation
|
|
378
|
+
field or a deck that is neither a mapping nor None.
|
|
379
|
+
"""
|
|
380
|
+
for name, value in (
|
|
381
|
+
("run_id", run_id),
|
|
382
|
+
("revision", revision),
|
|
383
|
+
("block_id", block_id),
|
|
384
|
+
):
|
|
385
|
+
if not isinstance(value, str) or not value:
|
|
386
|
+
raise BlockScopeError(
|
|
387
|
+
"invalid_target",
|
|
388
|
+
f"Scope citation {name} must be a non-empty string, got {value!r}.",
|
|
389
|
+
)
|
|
390
|
+
if deck is None:
|
|
391
|
+
deck = {}
|
|
392
|
+
if not isinstance(deck, Mapping):
|
|
393
|
+
raise BlockScopeError(
|
|
394
|
+
"invalid_target", f"Scope citation deck must be a mapping, got {deck!r}."
|
|
395
|
+
)
|
|
396
|
+
return {
|
|
397
|
+
"run_id": run_id,
|
|
398
|
+
"revision": revision,
|
|
399
|
+
"block_id": block_id,
|
|
400
|
+
"deck": dict(deck),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def stamp_verdict(verdict, scope):
|
|
405
|
+
"""Record block scope on a verdict, without mutating it.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
verdict: Verdict mapping to cite scope on.
|
|
409
|
+
scope: Provenance mapping from scope_provenance.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Verdict copy with the scope recorded under "scope".
|
|
413
|
+
|
|
414
|
+
Raises:
|
|
415
|
+
BlockScopeError: invalid_target when the verdict is not a
|
|
416
|
+
mapping, invalid_block when the scope misses a
|
|
417
|
+
citation key.
|
|
418
|
+
"""
|
|
419
|
+
if not isinstance(verdict, Mapping):
|
|
420
|
+
raise BlockScopeError(
|
|
421
|
+
"invalid_target", f"Verdict must be a mapping, got {verdict!r}."
|
|
422
|
+
)
|
|
423
|
+
if not isinstance(scope, Mapping):
|
|
424
|
+
raise BlockScopeError(
|
|
425
|
+
"invalid_block", f"Scope must be a mapping, got {scope!r}."
|
|
426
|
+
)
|
|
427
|
+
missing = [
|
|
428
|
+
key
|
|
429
|
+
for key in ("run_id", "revision", "block_id", "deck")
|
|
430
|
+
if key not in scope
|
|
431
|
+
]
|
|
432
|
+
if missing:
|
|
433
|
+
raise BlockScopeError(
|
|
434
|
+
"invalid_block",
|
|
435
|
+
f"Scope misses citation keys: {missing}.",
|
|
436
|
+
)
|
|
437
|
+
stamped = dict(verdict)
|
|
438
|
+
stamped["scope"] = dict(scope)
|
|
439
|
+
return stamped
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def build_scope_record(run_id, revision, block_id, deck, closure):
|
|
443
|
+
"""Bind one block run's provenance, hash, and closure.
|
|
444
|
+
|
|
445
|
+
The record is what the submit path stores per run_id and what
|
|
446
|
+
the verdict path stamps: provenance cites the run, the hash
|
|
447
|
+
detects drift, and the closure replays exactly what ran.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
run_id: Run the verdict belongs to.
|
|
451
|
+
revision: Project revision the closure was pinned at.
|
|
452
|
+
block_id: Block entity the run was scoped to.
|
|
453
|
+
deck: Deck mapping the solver ran under, or None.
|
|
454
|
+
closure: Closure mapping from snapshot_closure.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Scope record with provenance, closure_hash, and a private
|
|
458
|
+
closure copy.
|
|
459
|
+
|
|
460
|
+
Raises:
|
|
461
|
+
BlockScopeError: invalid_block when the closure belongs
|
|
462
|
+
to another block, plus the scope_provenance codes for
|
|
463
|
+
bad citation fields.
|
|
464
|
+
"""
|
|
465
|
+
if not isinstance(closure, Mapping) or closure.get("block_id") != block_id:
|
|
466
|
+
raise BlockScopeError(
|
|
467
|
+
"invalid_block",
|
|
468
|
+
f"Closure is for block {closure.get('block_id')!r} "
|
|
469
|
+
f"when {block_id!r} was requested."
|
|
470
|
+
if isinstance(closure, Mapping)
|
|
471
|
+
else f"Closure must be a mapping, got {closure!r}.",
|
|
472
|
+
)
|
|
473
|
+
return {
|
|
474
|
+
"provenance": scope_provenance(
|
|
475
|
+
run_id=run_id, revision=revision, block_id=block_id, deck=deck
|
|
476
|
+
),
|
|
477
|
+
"closure_hash": closure_hash(closure),
|
|
478
|
+
"closure": copy.deepcopy(dict(closure)),
|
|
479
|
+
}
|