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,685 @@
1
+ """FRONTEND-13 engine touchpoints: annotate, ERC, graph, netlist, xprobe, export.
2
+
3
+ Six pure query/op helpers over the ENGINE-ECS seam so IDE, MCP,
4
+ and script clients share one semantic:
5
+
6
+ - annotate: reference-designator assignment (idempotent).
7
+ - erc_check: ERC_TESTER with severities and pin-conflict config.
8
+ - connection_graph: buildConnectionGraph nodes/edges.
9
+ - write_netlist / update_netlist: DoWriteNetlist/UpdateNetlist.
10
+ - xprobe: ROCKET-XPROBE plus the resolvePin-as-query chokepoint.
11
+ - plot_export: plot/export drawing emission (pure SVG/JSON).
12
+ - flow_gate: binary-gated verdicts (sta/librelane absent ->
13
+ loud BLOCKED-needs-host, never faked green).
14
+
15
+ Net data (net_pins) comes from materialize_nets-style extraction
16
+ and is passed in by the caller: when it is missing the helpers
17
+ say so loudly instead of inventing connectivity.
18
+ """
19
+
20
+ import importlib.util
21
+ import shutil
22
+
23
+ from ..interfaces.errors import ContractError
24
+
25
+ #: Netlist languages write_netlist emits.
26
+ NETLIST_LANGUAGES = ("spice", "verilog")
27
+
28
+ #: Plot/export drawing formats.
29
+ EXPORT_FORMATS = ("svg", "json")
30
+
31
+ #: Tools flow_gate probes: binary on PATH, or importable package.
32
+ GATED_TOOLS = ("sta", "librelane")
33
+
34
+ #: Default ERC severities by rule.
35
+ DEFAULT_ERC_SEVERITIES = {
36
+ "schematic.duplicate_ref": "error",
37
+ "schematic.unknown_symbol": "error",
38
+ "schematic.pin_conflict": "error",
39
+ "schematic.unannotated": "warning",
40
+ "schematic.unmatched_pins": "warning",
41
+ }
42
+
43
+ #: Default pin-conflict policy by "<type>-<type>" pair.
44
+ DEFAULT_PIN_CONFLICT = {"output-output": "error"}
45
+
46
+
47
+ def _all_instances(store):
48
+ """List every instance mapping, id-sorted."""
49
+ found = []
50
+ for entity in store.entities_with("Instance"):
51
+ try:
52
+ raw = store.get_component(entity, "Instance")
53
+ except LookupError:
54
+ continue
55
+ if not isinstance(raw, dict):
56
+ continue
57
+ item = dict(raw)
58
+ item["id"] = entity
59
+ found.append(item)
60
+ found.sort(key=lambda item: item["id"])
61
+ return found
62
+
63
+
64
+ def _symbol_names(store):
65
+ """Return the set of project-owned symbol names."""
66
+ names = set()
67
+ for entity in store.entities_with("Symbol"):
68
+ try:
69
+ raw = store.get_component(entity, "Symbol")
70
+ except LookupError:
71
+ continue
72
+ if isinstance(raw, dict) and raw.get("name"):
73
+ names.add(raw["name"])
74
+ return names
75
+
76
+
77
+ def _symbol_pin_types(store, symbol):
78
+ """Return {pin: pin_type} for one symbol, or None when scoped."""
79
+ if ":" in symbol:
80
+ return None
81
+ for entity in store.entities_with("Symbol"):
82
+ try:
83
+ raw = store.get_component(entity, "Symbol")
84
+ except LookupError:
85
+ continue
86
+ if isinstance(raw, dict) and raw.get("name") == symbol:
87
+ pins = raw.get("pins", [])
88
+ return {
89
+ pin["name"]: pin.get("pin_type", "passive")
90
+ for pin in pins if isinstance(pin, dict)
91
+ }
92
+ return {}
93
+
94
+
95
+ def _linkage_for(store, symbol):
96
+ """Return (cell, pin_map) for one symbol, or (None, {}) unlinked."""
97
+ for entity in store.entities_with("Linkage"):
98
+ try:
99
+ raw = store.get_component(entity, "Linkage")
100
+ except LookupError:
101
+ continue
102
+ if isinstance(raw, dict) and raw.get("symbol") == symbol:
103
+ return raw.get("cell"), {
104
+ entry["symPin"]: entry["cellPin"]
105
+ for entry in raw.get("pin_map", [])
106
+ if isinstance(entry, dict)
107
+ }
108
+ return None, {}
109
+
110
+
111
+ def annotate(store, actor, prefixes=None, sheet=None):
112
+ """Assign reference designators to unannotated instances.
113
+
114
+ Counters continue past the highest numeric suffix already on
115
+ the sheet set, so reruns never collide; already-annotated
116
+ instances keep their refs (idempotent).
117
+
118
+ Args:
119
+ store: Entity store serving the ENGINE-ECS seam.
120
+ actor: Attribution recorded on the revision and events.
121
+ prefixes: Symbol name to ref-prefix mapping (symbols
122
+ absent from it are reported, never guessed).
123
+ sheet: Sheet entity id to scope to, or None for all.
124
+
125
+ Returns:
126
+ Receipt with annotated ([{instance, ref}]) and
127
+ unannotated ([{instance, symbol}]) lists.
128
+
129
+ Raises:
130
+ ContractError: invalid_command for a malformed prefix map.
131
+ """
132
+ prefixes = prefixes or {}
133
+ if not isinstance(prefixes, dict) or not all(
134
+ isinstance(key, str) and isinstance(val, str) and val
135
+ for key, val in prefixes.items()
136
+ ):
137
+ raise ContractError(
138
+ "invalid_command",
139
+ "Annotate prefixes must map symbol text to ref prefix.",
140
+ )
141
+ counters = {}
142
+ for item in _all_instances(store):
143
+ ref = item.get("ref", "")
144
+ for prefix in set(prefixes.values()):
145
+ if ref.startswith(prefix):
146
+ tail = ref[len(prefix):]
147
+ if tail.isdigit():
148
+ counters[prefix] = max(
149
+ counters.get(prefix, 0), int(tail)
150
+ )
151
+ annotated = []
152
+ unannotated = []
153
+ for item in _all_instances(store):
154
+ if sheet is not None and item.get("sheet") != sheet:
155
+ continue
156
+ if item.get("ref"):
157
+ continue
158
+ prefix = prefixes.get(item.get("symbol", ""))
159
+ if not prefix:
160
+ unannotated.append(
161
+ {"instance": item["id"],
162
+ "symbol": item.get("symbol", "")}
163
+ )
164
+ continue
165
+ counters[prefix] = counters.get(prefix, 0) + 1
166
+ ref = f"{prefix}{counters[prefix]}"
167
+ store.set_component(
168
+ actor, item["id"], "Instance",
169
+ {**{k: v for k, v in item.items() if k != "id"},
170
+ "ref": ref},
171
+ f"annotate {item['id']} {ref}",
172
+ )
173
+ annotated.append({"instance": item["id"], "ref": ref})
174
+ return {
175
+ "op": "annotate", "actor": actor,
176
+ "annotated": annotated, "unannotated": unannotated,
177
+ }
178
+
179
+
180
+ def erc_check(store, severities=None, pin_conflict=None,
181
+ net_pins=None):
182
+ """Run the ERC_TESTER rule set over the schematic.
183
+
184
+ Args:
185
+ store: Entity store serving the ENGINE-ECS seam.
186
+ severities: Rule-to-severity overrides over
187
+ DEFAULT_ERC_SEVERITIES.
188
+ pin_conflict: "<type>-<type>" pair to severity overrides
189
+ over DEFAULT_PIN_CONFLICT.
190
+ net_pins: Materialized [{instance, pin, net}] rows for
191
+ the pin-conflict rule; without them that rule is
192
+ skipped loudly (reported, never assumed clean).
193
+
194
+ Returns:
195
+ Mapping with findings ([{rule, severity, message}]) and
196
+ verdict ("fail" when any error-severity finding
197
+ exists, else "pass").
198
+
199
+ Raises:
200
+ ContractError: invalid_command for malformed configs.
201
+ """
202
+ policy = dict(DEFAULT_ERC_SEVERITIES)
203
+ for key, value in (severities or {}).items():
204
+ if key not in DEFAULT_ERC_SEVERITIES:
205
+ raise ContractError(
206
+ "invalid_command", f"Unknown ERC rule: {key!r}."
207
+ )
208
+ if value not in ("error", "warning", "info"):
209
+ raise ContractError(
210
+ "invalid_command",
211
+ f"Bad severity for {key!r}: {value!r}.",
212
+ )
213
+ policy[key] = value
214
+ conflict_policy = dict(DEFAULT_PIN_CONFLICT)
215
+ for key, value in (pin_conflict or {}).items():
216
+ if value not in ("error", "warning", "info"):
217
+ raise ContractError(
218
+ "invalid_command",
219
+ f"Bad pin-conflict severity for {key!r}.",
220
+ )
221
+ conflict_policy[key] = value
222
+ findings = []
223
+ instances = _all_instances(store)
224
+ known = _symbol_names(store)
225
+ seen_refs = {}
226
+ for item in instances:
227
+ ref = item.get("ref", "")
228
+ if ref:
229
+ seen_refs.setdefault(ref, []).append(item["id"])
230
+ symbol = item.get("symbol", "")
231
+ if ":" not in symbol and symbol not in known:
232
+ findings.append({
233
+ "rule": "schematic.unknown_symbol",
234
+ "severity": policy["schematic.unknown_symbol"],
235
+ "message": f"Instance {item['id']!r} references "
236
+ f"unknown symbol {symbol!r}.",
237
+ })
238
+ if not ref:
239
+ findings.append({
240
+ "rule": "schematic.unannotated",
241
+ "severity": policy["schematic.unannotated"],
242
+ "message": f"Instance {item['id']!r} carries no ref.",
243
+ })
244
+ for ref, holders in sorted(seen_refs.items()):
245
+ if len(holders) > 1:
246
+ findings.append({
247
+ "rule": "schematic.duplicate_ref",
248
+ "severity": policy["schematic.duplicate_ref"],
249
+ "message": f"Ref {ref!r} repeats on {holders}.",
250
+ })
251
+ if net_pins is not None:
252
+ by_net = {}
253
+ for row in net_pins:
254
+ by_net.setdefault(row["net"], []).append(row)
255
+ for net in sorted(by_net):
256
+ outputs = [
257
+ row for row in by_net[net]
258
+ if row.get("pin_type") == "output"
259
+ ]
260
+ if len(outputs) >= 2:
261
+ severity = conflict_policy.get(
262
+ "output-output", "error"
263
+ )
264
+ findings.append({
265
+ "rule": "schematic.pin_conflict",
266
+ "severity": severity,
267
+ "message": f"Net {net!r} shorts outputs "
268
+ f"{[(r['instance'], r['pin']) for r in outputs]}.",
269
+ })
270
+ for symbol in sorted(known):
271
+ cell, pin_map = _linkage_for(store, symbol)
272
+ if cell is None:
273
+ continue
274
+ pins = set(_symbol_pin_types(store, symbol) or ())
275
+ problems = sorted(pins - set(pin_map))
276
+ if problems:
277
+ findings.append({
278
+ "rule": "schematic.unmatched_pins",
279
+ "severity": policy["schematic.unmatched_pins"],
280
+ "message": f"Symbol {symbol!r} pins {problems} "
281
+ "have no cell-port map.",
282
+ })
283
+ verdict = (
284
+ "fail" if any(f["severity"] == "error" for f in findings)
285
+ else "pass"
286
+ )
287
+ return {"findings": findings, "verdict": verdict}
288
+
289
+
290
+ def connection_graph(store, sheet=None):
291
+ """Build the connection graph: sheets, instances, bindings.
292
+
293
+ Args:
294
+ store: Entity store serving the ENGINE-ECS seam.
295
+ sheet: Sheet entity id to scope to, or None for all.
296
+
297
+ Returns:
298
+ Mapping with nodes ([{id, kind, name}]) and edges
299
+ ([{from, to, rel}]; "contains" for sheet
300
+ membership, "binds" for hierarchy bindings).
301
+ """
302
+ nodes = []
303
+ edges = []
304
+ want_sheets = set()
305
+ for entity in store.entities_with("Sheet"):
306
+ try:
307
+ raw = store.get_component(entity, "Sheet")
308
+ except LookupError:
309
+ continue
310
+ if not isinstance(raw, dict):
311
+ continue
312
+ if sheet is not None and entity != sheet:
313
+ continue
314
+ want_sheets.add(entity)
315
+ nodes.append(
316
+ {"id": entity, "kind": "sheet",
317
+ "name": raw.get("name", entity)}
318
+ )
319
+ for item in _all_instances(store):
320
+ if sheet is not None and item.get("sheet") != sheet:
321
+ continue
322
+ nodes.append({
323
+ "id": item["id"], "kind": "instance",
324
+ "name": item.get("ref") or item.get("symbol", ""),
325
+ })
326
+ if item.get("sheet") in want_sheets or sheet is None:
327
+ edges.append({
328
+ "from": item.get("sheet"), "to": item["id"],
329
+ "rel": "contains",
330
+ })
331
+ for entity in store.entities_with("Hierarchy"):
332
+ try:
333
+ raw = store.get_component(entity, "Hierarchy")
334
+ except LookupError:
335
+ continue
336
+ if not isinstance(raw, dict):
337
+ continue
338
+ parent, child = (
339
+ raw.get("parent_sheet"), raw.get("child_sheet")
340
+ )
341
+ if sheet is not None and parent != sheet:
342
+ continue
343
+ edges.append({"from": parent, "to": child, "rel": "binds"})
344
+ nodes.sort(key=lambda node: node["id"])
345
+ edges.sort(key=lambda edge: (str(edge["from"]), str(edge["to"])))
346
+ return {"nodes": nodes, "edges": edges}
347
+
348
+
349
+ def net_occurrences(net_pins, net):
350
+ """List every (instance, pin) occurrence of one net.
351
+
352
+ Args:
353
+ net_pins: Materialized [{instance, pin, net}] rows from
354
+ net extraction (materialize_nets output shape).
355
+ net: Net name to collect.
356
+
357
+ Returns:
358
+ Occurrence rows [{instance, pin}] in input order.
359
+ """
360
+ return [
361
+ {"instance": row["instance"], "pin": row["pin"]}
362
+ for row in net_pins
363
+ if isinstance(row, dict) and row.get("net") == net
364
+ ]
365
+
366
+
367
+ def _net_index(net_pins):
368
+ """Index net_pins rows into {(instance, pin): net}."""
369
+ if net_pins is None:
370
+ raise ContractError(
371
+ "invalid_command",
372
+ "Netlist emission needs net_pins from net extraction; "
373
+ "refusing to invent connectivity.",
374
+ )
375
+ index = {}
376
+ for row in net_pins:
377
+ if (
378
+ isinstance(row, dict) and row.get("instance")
379
+ and row.get("pin") and row.get("net")
380
+ ):
381
+ index[(row["instance"], row["pin"])] = row["net"]
382
+ return index
383
+
384
+
385
+ def _instance_nets(store, instance_id, symbol, index):
386
+ """Return (ordered nets, unconnected pins) for one instance."""
387
+ pins = _symbol_pin_types(store, symbol) or {}
388
+ if pins:
389
+ ordered = list(pins)
390
+ else:
391
+ ordered = sorted({
392
+ pin for (inst, pin) in index if inst == instance_id
393
+ })
394
+ nets = []
395
+ unconnected = []
396
+ for pin in ordered:
397
+ net = index.get((instance_id, pin))
398
+ if net is None:
399
+ nets.append(f"NC_{pin}")
400
+ unconnected.append(pin)
401
+ else:
402
+ nets.append(net)
403
+ return nets, unconnected
404
+
405
+
406
+ def write_netlist(store, language="spice", sheet=None, title="top",
407
+ net_pins=None):
408
+ """Write a netlist for the schematic (DoWriteNetlist).
409
+
410
+ Pins with no extracted net emit explicit ``NC_<pin>``
411
+ stubs and are reported under ``unconnected`` — dangling
412
+ connections are named, never dropped.
413
+
414
+ Args:
415
+ store: Entity store serving the ENGINE-ECS seam.
416
+ language: Netlist language ("spice" or "verilog").
417
+ sheet: Sheet entity id to scope to, or None for all.
418
+ title: Netlist/circuit title.
419
+ net_pins: Materialized [{instance, pin, net}] rows
420
+ (required — connectivity is never invented).
421
+
422
+ Returns:
423
+ Mapping with language, text, instance count, nets,
424
+ and unconnected pins.
425
+
426
+ Raises:
427
+ ContractError: invalid_language for an unknown
428
+ language, invalid_command without net_pins.
429
+ """
430
+ if language not in NETLIST_LANGUAGES:
431
+ raise ContractError(
432
+ "invalid_language",
433
+ f"Netlist language must be one of {NETLIST_LANGUAGES}.",
434
+ )
435
+ index = _net_index(net_pins)
436
+ instances = [
437
+ item for item in _all_instances(store)
438
+ if sheet is None or item.get("sheet") == sheet
439
+ ]
440
+ nets = sorted(set(index.values()))
441
+ unconnected = []
442
+ if language == "spice":
443
+ lines = [f"* {title}"]
444
+ for item in instances:
445
+ inst_nets, missing = _instance_nets(
446
+ store, item["id"], item.get("symbol", ""), index
447
+ )
448
+ unconnected.extend(
449
+ {"instance": item["id"], "pin": pin}
450
+ for pin in missing
451
+ )
452
+ tag = item.get("ref") or item["id"]
453
+ lines.append(
454
+ f"X{tag} {' '.join(inst_nets)} "
455
+ f"{item.get('symbol', '')}"
456
+ )
457
+ lines.append(".end")
458
+ else:
459
+ lines = [f"module {title} ();"]
460
+ for item in instances:
461
+ inst_nets, missing = _instance_nets(
462
+ store, item["id"], item.get("symbol", ""), index
463
+ )
464
+ unconnected.extend(
465
+ {"instance": item["id"], "pin": pin}
466
+ for pin in missing
467
+ )
468
+ pins = _symbol_pin_types(store, item.get("symbol", "")) or {}
469
+ ordered = list(pins) or sorted({
470
+ pin for (inst, pin) in index
471
+ if inst == item["id"]
472
+ })
473
+ ports = ", ".join(
474
+ f".{pin}({net})"
475
+ for pin, net in zip(ordered, inst_nets)
476
+ )
477
+ tag = item.get("ref") or item["id"]
478
+ lines.append(
479
+ f" {item.get('symbol', '')} {tag} ({ports});"
480
+ )
481
+ lines.append("endmodule")
482
+ return {
483
+ "language": language, "text": "\n".join(lines) + "\n",
484
+ "title": title, "instances": len(instances), "nets": nets,
485
+ "unconnected": unconnected,
486
+ }
487
+
488
+
489
+ def update_netlist(store, language, prior_text, sheet=None,
490
+ title="top", net_pins=None):
491
+ """Rewrite a netlist and report whether it changed (UpdateNetlist).
492
+
493
+ Args:
494
+ store: Entity store serving the ENGINE-ECS seam.
495
+ language: Netlist language ("spice" or "verilog").
496
+ prior_text: Previously emitted netlist text.
497
+ sheet: Sheet entity id to scope to, or None for all.
498
+ title: Netlist/circuit title.
499
+ net_pins: Materialized [{instance, pin, net}] rows.
500
+
501
+ Returns:
502
+ Mapping with language, text, and changed flag.
503
+
504
+ Raises:
505
+ ContractError: invalid_language for an unknown
506
+ language, invalid_command without net_pins.
507
+ """
508
+ fresh = write_netlist(
509
+ store, language=language, sheet=sheet, title=title,
510
+ net_pins=net_pins,
511
+ )
512
+ return {
513
+ "language": language, "text": fresh["text"],
514
+ "changed": fresh["text"] != prior_text,
515
+ "unconnected": fresh["unconnected"],
516
+ }
517
+
518
+
519
+ def xprobe(store, instance, pin, net_pins=None):
520
+ """Cross-probe one pin: symbol, cell, cell pin, net (ROCKET-XPROBE).
521
+
522
+ The resolvePin-as-query chokepoint: explicit linkage wins,
523
+ unlisted pins resolve to themselves, and the net comes from
524
+ extracted net_pins — without them the probe reports
525
+ ``net_known: False`` instead of guessing.
526
+
527
+ Args:
528
+ store: Entity store serving the ENGINE-ECS seam.
529
+ instance: Instance entity id to probe.
530
+ pin: Symbol pin to probe.
531
+ net_pins: Materialized [{instance, pin, net}] rows, or
532
+ None when no extraction ran.
533
+
534
+ Returns:
535
+ Mapping with instance, pin, symbol, cell (None when
536
+ unlinked), cell_pin, net (None when unknown), and
537
+ net_known flag.
538
+
539
+ Raises:
540
+ ContractError: unknown_instance for a missing
541
+ instance, unknown_pin for a pin the symbol lacks.
542
+ """
543
+ try:
544
+ raw = store.get_component(instance, "Instance")
545
+ except LookupError:
546
+ raise ContractError(
547
+ "unknown_instance",
548
+ f"No such instance entity: {instance!r}.",
549
+ ) from None
550
+ if not isinstance(raw, dict):
551
+ raise ContractError(
552
+ "unknown_instance",
553
+ f"No such instance entity: {instance!r}.",
554
+ )
555
+ symbol = raw.get("symbol", "")
556
+ known_pins = _symbol_pin_types(store, symbol)
557
+ if known_pins is not None and pin not in known_pins:
558
+ raise ContractError(
559
+ "unknown_pin",
560
+ f"Symbol {symbol!r} carries no pin {pin!r}.",
561
+ )
562
+ cell, pin_map = _linkage_for(store, symbol)
563
+ net = None
564
+ for row in net_pins or []:
565
+ if (
566
+ isinstance(row, dict) and row.get("instance") == instance
567
+ and row.get("pin") == pin
568
+ ):
569
+ net = row.get("net")
570
+ break
571
+ return {
572
+ "instance": instance, "pin": pin, "symbol": symbol,
573
+ "cell": cell, "cell_pin": pin_map.get(pin, pin),
574
+ "net": net, "net_known": net is not None,
575
+ }
576
+
577
+
578
+ def plot_export(store, format="svg", sheet=None):
579
+ """Export the schematic drawing (plot/export op).
580
+
581
+ Pure emission — no solver, no display server: SVG frames
582
+ each sheet with instance boxes plus ref/symbol labels, and
583
+ JSON carries the same drawing as structured data.
584
+
585
+ Args:
586
+ store: Entity store serving the ENGINE-ECS seam.
587
+ format: Drawing format ("svg" or "json").
588
+ sheet: Sheet entity id to scope to, or None for all.
589
+
590
+ Returns:
591
+ Mapping with format, content (text or drawing
592
+ mapping), and sheet/instance counts.
593
+
594
+ Raises:
595
+ ContractError: invalid_format for an unknown format.
596
+ """
597
+ if format not in EXPORT_FORMATS:
598
+ raise ContractError(
599
+ "invalid_format",
600
+ f"Export format must be one of {EXPORT_FORMATS}.",
601
+ )
602
+ instances = [
603
+ item for item in _all_instances(store)
604
+ if sheet is None or item.get("sheet") == sheet
605
+ ]
606
+ sheets = [
607
+ node for node in connection_graph(store, sheet)["nodes"]
608
+ if node["kind"] == "sheet"
609
+ ]
610
+ drawing = {
611
+ "sheets": sheets,
612
+ "instances": [
613
+ {"id": item["id"], "sheet": item.get("sheet"),
614
+ "symbol": item.get("symbol"), "ref": item.get("ref"),
615
+ "x": item.get("x"), "y": item.get("y"),
616
+ "rotation": item.get("rotation"),
617
+ "mirror": item.get("mirror")}
618
+ for item in instances
619
+ ],
620
+ }
621
+ if format == "json":
622
+ return {
623
+ "format": format, "content": drawing,
624
+ "sheets": len(sheets), "instances": len(instances),
625
+ }
626
+ parts = [
627
+ '<svg xmlns="http://www.w3.org/2000/svg">',
628
+ f"<!-- sheets={len(sheets)} instances={len(instances)} -->",
629
+ ]
630
+ for order, item in enumerate(drawing["instances"]):
631
+ x = 20 + (order % 8) * 90
632
+ y = 20 + (order // 8) * 70
633
+ label = item["ref"] or item["symbol"]
634
+ parts.append(
635
+ f'<g id="{item["id"]}">'
636
+ f'<rect x="{x}" y="{y}" width="70" height="50" '
637
+ 'fill="none" stroke="black"/>'
638
+ f'<text x="{x + 4}" y="{y + 20}">{label}</text>'
639
+ f"<title>{item['symbol']} {item['ref']}</title></g>"
640
+ )
641
+ parts.append("</svg>")
642
+ return {
643
+ "format": format, "content": "\n".join(parts),
644
+ "sheets": len(sheets), "instances": len(instances),
645
+ }
646
+
647
+
648
+ def flow_gate(tool):
649
+ """Report the availability verdict for a binary-gated flow tool.
650
+
651
+ Absent tools report loud BLOCKED-needs-host with a remedy —
652
+ never a faked green verdict.
653
+
654
+ Args:
655
+ tool: Tool name ("sta" or "librelane").
656
+
657
+ Returns:
658
+ Mapping with tool, available flag, verdict ("ready" or
659
+ "BLOCKED-needs-host"), and remedy text when blocked.
660
+
661
+ Raises:
662
+ ContractError: unknown_tool for an unlisted tool.
663
+ """
664
+ if tool not in GATED_TOOLS:
665
+ raise ContractError(
666
+ "unknown_tool", f"No flow gate for tool {tool!r}."
667
+ )
668
+ if tool == "sta":
669
+ available = shutil.which("sta") is not None
670
+ remedy = (
671
+ "Install OpenSTA on PATH (or provision the timing "
672
+ "profile) and rerun; run_timing stays unwired until then."
673
+ )
674
+ else:
675
+ available = importlib.util.find_spec("librelane") is not None
676
+ remedy = (
677
+ "Provision the digital-flow profile (librelane) and "
678
+ "rerun; run_digital_flow stays unwired until then."
679
+ )
680
+ if available:
681
+ return {"tool": tool, "available": True, "verdict": "ready"}
682
+ return {
683
+ "tool": tool, "available": False,
684
+ "verdict": "BLOCKED-needs-host", "remedy": remedy,
685
+ }