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,115 @@
1
+ """Scoped reference resolution and connection checks (I02).
2
+
3
+ Resolution assumes a validated store: files here were accepted by
4
+ validate_document at ingestion. The scan therefore uses a plain safe
5
+ load and skips anything outside the root or unparseable.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from ruamel.yaml import YAML
11
+
12
+ from .documents import validate_document
13
+ from .errors import ContractError
14
+
15
+
16
+ def _matches(doc, ref):
17
+ return (
18
+ isinstance(doc, dict)
19
+ and doc.get("scope") == ref.get("scope")
20
+ and doc.get("id") == ref.get("id")
21
+ and doc.get("revision") == ref.get("revision")
22
+ )
23
+
24
+
25
+ def _scan(root):
26
+ yaml = YAML(typ="safe")
27
+ for path in sorted(root.rglob("*.yaml")):
28
+ try:
29
+ path.resolve().relative_to(root)
30
+ except ValueError:
31
+ continue
32
+ try:
33
+ doc = yaml.load(path.read_text())
34
+ except Exception:
35
+ continue
36
+ if isinstance(doc, dict):
37
+ yield doc
38
+
39
+
40
+ def resolve_reference(ref, root, path=None):
41
+ """Resolve an exact {scope, id, revision} reference inside root.
42
+
43
+ Args:
44
+ ref: Reference mapping with scope, id, and revision.
45
+ root: Store root the resolution stays inside.
46
+ path: Optional direct path under root; scans the store
47
+ when omitted.
48
+
49
+ Returns:
50
+ Matching validated record mapping.
51
+
52
+ Raises:
53
+ ContractError: invalid_reference for an escape, a mismatch
54
+ at path, or an unknown reference; invalid_yaml when a
55
+ direct path cannot load.
56
+ """
57
+ root = Path(root).resolve()
58
+ if path is not None:
59
+ target = (root / path).resolve()
60
+ try:
61
+ target.relative_to(root)
62
+ except ValueError:
63
+ raise ContractError(
64
+ "invalid_reference", f"Reference escapes store root: {path}."
65
+ ) from None
66
+ try:
67
+ doc = YAML(typ="safe").load(target.read_text())
68
+ except Exception as exc:
69
+ raise ContractError("invalid_yaml", f"Cannot load {path}: {exc}") from exc
70
+ if _matches(doc, ref):
71
+ return doc
72
+ raise ContractError("invalid_reference", f"No match at path: {path}.")
73
+ for doc in _scan(root):
74
+ if _matches(doc, ref):
75
+ return doc
76
+ raise ContractError(
77
+ "invalid_reference",
78
+ f"Unknown reference: {ref.get('scope')}/{ref.get('id')}@{ref.get('revision')}.",
79
+ )
80
+
81
+
82
+ def check_connection(text, endpoint_a, endpoint_b):
83
+ """Check two endpoints for unit/port compatibility.
84
+
85
+ Args:
86
+ text: Validated-record YAML text holding the port table.
87
+ endpoint_a: First (instance, port) endpoint.
88
+ endpoint_b: Second (instance, port) endpoint.
89
+
90
+ Returns:
91
+ Compatibility mapping (compatible True).
92
+
93
+ Raises:
94
+ ContractError: invalid_yaml for a bad record,
95
+ invalid_reference for an unknown port,
96
+ incompatible_port for mismatched wavelength units.
97
+ """
98
+ data = validate_document(text)
99
+ ports = data.get("ports", {})
100
+
101
+ def _wavelength_unit(endpoint):
102
+ _, port = endpoint
103
+ if port not in ports:
104
+ raise ContractError("invalid_reference", f"Unknown port: {port}.")
105
+ props = ports[port].get("properties", {})
106
+ return props.get("wavelength", {}).get("unit")
107
+
108
+ unit_a = _wavelength_unit(endpoint_a)
109
+ unit_b = _wavelength_unit(endpoint_b)
110
+ if unit_a and unit_b and unit_a != unit_b:
111
+ raise ContractError(
112
+ "incompatible_port",
113
+ f"Incompatible wavelength units: {unit_a} vs {unit_b}.",
114
+ )
115
+ return {"compatible": True}
@@ -0,0 +1,85 @@
1
+ """I28 ingredient-closure pins for Wave-3 editor and testbench verbs.
2
+
3
+ Pure helpers (no I/O, no logic beyond digests): every Wave-3
4
+ mutation receipt carries an ``i28`` closure pinning the consumed
5
+ revisions (entities, templates, profiles) so a replay can prove
6
+ drift instead of assuming it. Stored digests are canonical-JSON
7
+ sha1, matching the ledger content-key discipline.
8
+ """
9
+
10
+ import copy
11
+ import hashlib
12
+ import json
13
+
14
+
15
+ def digest_mapping(mapping):
16
+ """Digest one JSON-shaped mapping for an I28 pin.
17
+
18
+ Args:
19
+ mapping: JSON-shaped mapping to digest.
20
+
21
+ Returns:
22
+ Twelve hex characters of canonical-JSON sha1.
23
+ """
24
+ text = json.dumps(mapping, sort_keys=True, default=str)
25
+ return hashlib.sha1(text.encode("utf-8")).hexdigest()[:12]
26
+
27
+
28
+ def pin_entities(head, revisions):
29
+ """Pin consumed entity revisions at one head revision.
30
+
31
+ Args:
32
+ head: Ledger head revision the pin rides on.
33
+ revisions: Entity id to live entity revision mapping.
34
+
35
+ Returns:
36
+ Pin with head revision plus the sorted entity map.
37
+ """
38
+ return {
39
+ "revision": head,
40
+ "entities": {key: revisions[key] for key in sorted(revisions)},
41
+ }
42
+
43
+
44
+ def pin_template(provenance):
45
+ """Pin one template provenance into the closure.
46
+
47
+ Args:
48
+ provenance: Template provenance mapping (kind,
49
+ template_version, digest).
50
+
51
+ Returns:
52
+ Deep copy of the provenance plus its digest pin.
53
+ """
54
+ kept = copy.deepcopy(dict(provenance or {}))
55
+ kept["digest"] = kept.get("digest") or digest_mapping(kept)
56
+ return kept
57
+
58
+
59
+ def pin_closure(head, entities=None, template=None, profile=None):
60
+ """Assemble one I28 closure pin for a Wave-3 receipt.
61
+
62
+ Args:
63
+ head: Ledger head revision the pin rides on.
64
+ entities: Entity id to live revision mapping, if any.
65
+ template: Template provenance mapping, if consumed.
66
+ profile: Technology profile pin ({name, version}), if
67
+ consumed.
68
+
69
+ Returns:
70
+ Closure pin with revision, entity pins, template pin,
71
+ and profile pin (each present only when consumed).
72
+ """
73
+ pin = {"revision": head}
74
+ if entities:
75
+ pin["entities"] = {
76
+ key: entities[key] for key in sorted(entities)
77
+ }
78
+ if template is not None:
79
+ pin["template"] = pin_template(template)
80
+ if profile is not None:
81
+ pin["profile"] = copy.deepcopy(dict(profile))
82
+ pin["digest"] = digest_mapping(
83
+ {key: pin[key] for key in sorted(pin) if key != "digest"}
84
+ )
85
+ return pin
@@ -0,0 +1,304 @@
1
+ """Explicit run.request params schema (RUN-PARAMS).
2
+
3
+ Runs are explicit, always — no live-update path — so every knob a
4
+ run needs travels IN the run request, frozen into the run snapshot
5
+ like everything else. xschem keeps netlist knobs as live TCL vars;
6
+ the web split cannot do that without per-event round-trips, so they
7
+ become explicit `request` parameters: netlist type, flat/split
8
+ shape, the LVS-netlist flag, top-is-subckt, deck opts
9
+ passthrough, and the solver budget (null keeps the adapter
10
+ default). Without this schema run reproducibility has a hole:
11
+ two identical-looking runs could differ by a knob nobody recorded.
12
+
13
+ Validated at submit (`Engine.submit` and `Engine.run` normalize
14
+ `arguments["request"]` through `validate_run_request` before the
15
+ command text is frozen), replayed byte-identically through
16
+ `freeze_run_request`, and read back through `parse_run_request`.
17
+ Unknown fields are rejected loudly, never ignored — and every
18
+ caller (IDE, MCP, console) sends the same shape because validation
19
+ lives in the engine, not the transports.
20
+ """
21
+
22
+ import io
23
+
24
+ from ruamel.yaml import YAML
25
+
26
+ # Mirrors xschem's netlist_type TCL var: the netlist dialect the run
27
+ # netlists in. Canonical order is display order (error text, schema).
28
+ NETLIST_TYPES = ("spice", "spectre", "verilog", "vhdl")
29
+
30
+ # Solver-lane budget bounds, mirroring the python lane so one
31
+ # hanging tool cannot pin a drain worker forever. None means
32
+ # "adapter default" (a spice sim keeps 120s, a LibreLane flow
33
+ # keeps its hour); the engine stamps the job only when set.
34
+ MIN_TIMEOUT_S = 1
35
+ MAX_TIMEOUT_S = 1800
36
+ DEFAULT_TIMEOUT_S = None
37
+
38
+ # Canonical knob order: validation output, frozen text, and the
39
+ # schema all follow this order so replays compare byte-identically.
40
+ RUN_REQUEST_FIELDS = (
41
+ "netlist_type",
42
+ "flat_netlist",
43
+ "lvs_netlist",
44
+ "top_is_subckt",
45
+ "deck_opts",
46
+ "timeout_s",
47
+ )
48
+
49
+ RUN_REQUEST_DEFAULTS = {
50
+ "netlist_type": "spice",
51
+ "flat_netlist": False,
52
+ "lvs_netlist": False,
53
+ "top_is_subckt": False,
54
+ "deck_opts": {},
55
+ "timeout_s": DEFAULT_TIMEOUT_S,
56
+ }
57
+
58
+ _BOOL_FLAGS = ("flat_netlist", "lvs_netlist", "top_is_subckt")
59
+
60
+ # Deck opts pass through to the solver deck untouched: flat scalar
61
+ # options only (a nested mapping would smuggle unvalidated shape
62
+ # into the tool call). None means "present but valueless".
63
+ _SCALAR_TYPES = (str, int, float, bool, type(None))
64
+
65
+ # Machine seam for api_spec/contract regen: the integrator injects
66
+ # this `request` property into solver-op schemas from here, the way
67
+ # expected_revision is injected from ProjectService.submit's arg
68
+ # text. additionalProperties False is the schema half of "unknown
69
+ # fields rejected, never ignored".
70
+ RUN_REQUEST_SCHEMA = {
71
+ "type": "object",
72
+ "description": (
73
+ "Explicit run knobs (xschem live-var equivalents), frozen "
74
+ "into the run snapshot. Unknown fields are rejected."
75
+ ),
76
+ "properties": {
77
+ "netlist_type": {
78
+ "type": "string",
79
+ # Literal (not list(NETLIST_TYPES)): api_spec reads this
80
+ # schema via AST without importing; tests pin equality.
81
+ "enum": ["spice", "spectre", "verilog", "vhdl"],
82
+ "default": "spice",
83
+ "description": "Netlist dialect the run netlists in.",
84
+ },
85
+ "flat_netlist": {
86
+ "type": "boolean",
87
+ "default": False,
88
+ "description": "Flatten the hierarchy (else split per cell).",
89
+ },
90
+ "lvs_netlist": {
91
+ "type": "boolean",
92
+ "default": False,
93
+ "description": "Netlist for LVS comparison.",
94
+ },
95
+ "top_is_subckt": {
96
+ "type": "boolean",
97
+ "default": False,
98
+ "description": "Emit the top cell as a subcircuit.",
99
+ },
100
+ "deck_opts": {
101
+ "type": "object",
102
+ "default": {},
103
+ "description": "Solver deck options, flat scalar passthrough.",
104
+ },
105
+ "timeout_s": {
106
+ "type": ["number", "null"],
107
+ "default": None,
108
+ "description": (
109
+ "Solver budget in seconds; null keeps the adapter "
110
+ "default."
111
+ ),
112
+ },
113
+ },
114
+ "additionalProperties": False,
115
+ }
116
+
117
+
118
+ class RunParamsError(Exception):
119
+ """Run.request violation with a stable machine code.
120
+
121
+ Args:
122
+ code: Stable machine-readable error code
123
+ (unknown_run_param, invalid_run_param,
124
+ invalid_run_request).
125
+ message: Human-readable detail naming the field.
126
+ fields: Itemized fields, when the error names any.
127
+ """
128
+
129
+ def __init__(self, code, message, fields=None):
130
+ super().__init__(message)
131
+ self.code = code
132
+ self.fields = fields or []
133
+
134
+
135
+ def _check_deck_opts(opts):
136
+ """Validate deck opts: string keys, flat scalar values."""
137
+ if not isinstance(opts, dict):
138
+ raise RunParamsError(
139
+ "invalid_run_param",
140
+ f"deck_opts must be a mapping, got {opts!r}.",
141
+ fields=["deck_opts"],
142
+ )
143
+ for key, value in opts.items():
144
+ if not isinstance(key, str) or not key:
145
+ raise RunParamsError(
146
+ "invalid_run_param",
147
+ f"deck_opts keys must be non-empty strings, got {key!r}.",
148
+ fields=["deck_opts"],
149
+ )
150
+ if not isinstance(value, _SCALAR_TYPES):
151
+ raise RunParamsError(
152
+ "invalid_run_param",
153
+ f"deck_opts.{key} must be a scalar, got {value!r}.",
154
+ fields=["deck_opts"],
155
+ )
156
+ return dict(opts)
157
+
158
+
159
+ def validate_run_request(params):
160
+ """Validate and normalize one run.request mapping.
161
+
162
+ Missing input (None) and `{}` both mean "all defaults": the
163
+ frozen snapshot always shows every knob explicitly, so old
164
+ callers that send no `request` still record what they ran with.
165
+
166
+ Args:
167
+ params: Caller-supplied `request` mapping, or None when
168
+ the caller sent no `request` key.
169
+
170
+ Returns:
171
+ Normalized mapping with every knob present in canonical
172
+ field order: caller values where given, schema defaults
173
+ elsewhere. A fresh mapping — the caller's dict is never
174
+ mutated.
175
+
176
+ Raises:
177
+ RunParamsError: unknown_run_param naming every unknown
178
+ field, invalid_run_param naming the mistyped field
179
+ (netlist_type outside the enum, a non-bool flag, a
180
+ malformed deck_opts), invalid_run_request for a
181
+ non-mapping `request`.
182
+ """
183
+ if params is None:
184
+ params = {}
185
+ if not isinstance(params, dict):
186
+ raise RunParamsError(
187
+ "invalid_run_request",
188
+ f"request must be a mapping, got {params!r}.",
189
+ )
190
+ unknown = sorted(key for key in params if key not in RUN_REQUEST_FIELDS)
191
+ if unknown:
192
+ raise RunParamsError(
193
+ "unknown_run_param",
194
+ f"Unknown run.request field(s): {', '.join(map(repr, unknown))}; "
195
+ f"known fields: {', '.join(RUN_REQUEST_FIELDS)}.",
196
+ fields=unknown,
197
+ )
198
+ kind = params.get("netlist_type", RUN_REQUEST_DEFAULTS["netlist_type"])
199
+ if kind not in NETLIST_TYPES:
200
+ raise RunParamsError(
201
+ "invalid_run_param",
202
+ f"netlist_type must be one of {', '.join(NETLIST_TYPES)}, "
203
+ f"got {kind!r}.",
204
+ fields=["netlist_type"],
205
+ )
206
+ normalized = {"netlist_type": kind}
207
+ for flag in _BOOL_FLAGS:
208
+ value = params.get(flag, False)
209
+ if not isinstance(value, bool):
210
+ raise RunParamsError(
211
+ "invalid_run_param",
212
+ f"{flag} must be a boolean, got {value!r}.",
213
+ fields=[flag],
214
+ )
215
+ normalized[flag] = value
216
+ normalized["deck_opts"] = _check_deck_opts(
217
+ params.get("deck_opts", RUN_REQUEST_DEFAULTS["deck_opts"])
218
+ )
219
+ normalized["timeout_s"] = _check_timeout(
220
+ params.get("timeout_s", RUN_REQUEST_DEFAULTS["timeout_s"])
221
+ )
222
+ return normalized
223
+
224
+
225
+ def _check_timeout(value):
226
+ """Validate the solver budget: a positive number in bounds, or None.
227
+
228
+ Args:
229
+ value: Caller-supplied `timeout_s`, or None for the
230
+ adapter default.
231
+
232
+ Returns:
233
+ The validated budget, unchanged.
234
+
235
+ Raises:
236
+ RunParamsError: invalid_run_param for a boolean,
237
+ non-number, or out-of-bounds budget.
238
+ """
239
+ if value is None:
240
+ return None
241
+ if (
242
+ isinstance(value, bool)
243
+ or not isinstance(value, (int, float))
244
+ or not MIN_TIMEOUT_S <= value <= MAX_TIMEOUT_S
245
+ ):
246
+ raise RunParamsError(
247
+ "invalid_run_param",
248
+ f"timeout_s must be {MIN_TIMEOUT_S}..{MAX_TIMEOUT_S} seconds "
249
+ f"or null, got {value!r}.",
250
+ fields=["timeout_s"],
251
+ )
252
+ return value
253
+
254
+
255
+ def freeze_run_request(validated):
256
+ """Render validated knobs as canonical inputs.yaml text.
257
+
258
+ Key order is canonical (schema field order, deck opts sorted),
259
+ so the same logical request replays byte-identically whatever
260
+ key order the caller sent.
261
+
262
+ Args:
263
+ validated: Normalized mapping from `validate_run_request`.
264
+
265
+ Returns:
266
+ Deterministic YAML text (`request:` mapping) for the run
267
+ snapshot: same knobs in, same bytes out.
268
+ """
269
+ ordered = {field: validated[field] for field in RUN_REQUEST_FIELDS}
270
+ ordered["deck_opts"] = {
271
+ key: ordered["deck_opts"][key] for key in sorted(ordered["deck_opts"])
272
+ }
273
+ buf = io.StringIO()
274
+ YAML(typ="safe").dump({"request": ordered}, buf)
275
+ return buf.getvalue()
276
+
277
+
278
+ def parse_run_request(text):
279
+ """Read frozen inputs.yaml text back into validated knobs.
280
+
281
+ Args:
282
+ text: YAML text holding a top-level `request:` mapping, as
283
+ rendered by `freeze_run_request`.
284
+
285
+ Returns:
286
+ Validated mapping (see `validate_run_request`).
287
+
288
+ Raises:
289
+ RunParamsError: invalid_run_request for unparsable text or
290
+ a missing/non-mapping `request`, else whatever
291
+ `validate_run_request` raises for the stored knobs.
292
+ """
293
+ try:
294
+ data = YAML(typ="safe").load(text)
295
+ except Exception as exc:
296
+ raise RunParamsError(
297
+ "invalid_run_request", f"Cannot parse inputs.yaml: {exc}."
298
+ ) from exc
299
+ if not isinstance(data, dict) or not isinstance(data.get("request"), dict):
300
+ raise RunParamsError(
301
+ "invalid_run_request",
302
+ "inputs.yaml must hold a top-level request mapping.",
303
+ )
304
+ return validate_run_request(data["request"])