rocket-engine 0.0.2b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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
+ }