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,49 @@
1
+ """Scoped testbench blocks with editable templates."""
2
+
3
+ from .blocks import (
4
+ SOURCE_PREFIX,
5
+ TESTBENCH_COMPONENT,
6
+ assert_runnable,
7
+ check_fresh,
8
+ create_block,
9
+ describe_block,
10
+ design_net_pins,
11
+ dut_digest,
12
+ excluded_harness_instances,
13
+ harness_instances,
14
+ materialize_interface,
15
+ normalize_probes,
16
+ normalize_targets,
17
+ probe_simulator,
18
+ regenerate_block,
19
+ )
20
+ from .templates import (
21
+ TEMPLATE_EXTENSIONS,
22
+ TEMPLATE_KINDS,
23
+ TEMPLATE_VERSION,
24
+ describe_template,
25
+ render,
26
+ )
27
+
28
+ __all__ = [
29
+ "SOURCE_PREFIX",
30
+ "TESTBENCH_COMPONENT",
31
+ "TEMPLATE_EXTENSIONS",
32
+ "TEMPLATE_KINDS",
33
+ "TEMPLATE_VERSION",
34
+ "assert_runnable",
35
+ "check_fresh",
36
+ "create_block",
37
+ "describe_block",
38
+ "describe_template",
39
+ "design_net_pins",
40
+ "dut_digest",
41
+ "excluded_harness_instances",
42
+ "harness_instances",
43
+ "materialize_interface",
44
+ "normalize_probes",
45
+ "normalize_targets",
46
+ "probe_simulator",
47
+ "regenerate_block",
48
+ "render",
49
+ ]
@@ -0,0 +1,619 @@
1
+ """Scoped testbench blocks over the ENGINE-ECS entity seam.
2
+
3
+ One block names a device under test (a primary circuit-instance
4
+ occurrence plus explicit nonoverlapping additional targets),
5
+ probes inside the DUT closure, an explicit harness binding, and
6
+ an editable source rendered from the materialized interface. At
7
+ occurrence ``P+[bench]`` the legal DUTs are sibling circuit
8
+ instances plus descendants: ancestors, the bench scope itself,
9
+ unrelated scopes, overlapping targets, and out-of-closure probes
10
+ are refused. Stable entity ids disambiguate reused definitions —
11
+ never names or positions.
12
+
13
+ Renames preserve bindings; hierarchy moves, deletes, interface
14
+ changes, and template upgrades mark the link stale and block the
15
+ run. Regeneration renders a candidate the client adopts
16
+ explicitly and never overwrites an edited source. Harness
17
+ components stay out of the production netlist unless the design
18
+ adopts them. Simulator runs stay binary-gated: absent tools
19
+ report BLOCKED-needs-host, never a faked green.
20
+ """
21
+
22
+ import importlib.util
23
+ import shutil
24
+
25
+ from ...interfaces.errors import ContractError
26
+ from ...interfaces.refs import digest_mapping
27
+ from ..edit.cas import read_entity_revision
28
+ from .templates import (
29
+ TEMPLATE_EXTENSIONS,
30
+ TEMPLATE_VERSION,
31
+ describe_template,
32
+ render,
33
+ )
34
+
35
+ #: Entity component carrying the testbench record.
36
+ TESTBENCH_COMPONENT = "Testbench"
37
+
38
+ #: Saved source document prefix per block name.
39
+ SOURCE_PREFIX = "testbench/"
40
+
41
+ #: Simulator binary probed per testbench kind.
42
+ SIMULATOR_TOOLS = {"spice": "ngspice", "hdl": "icarus", "cocotb": "cocotb"}
43
+
44
+
45
+ def _instance_of(service, entity):
46
+ """Read one instance payload, or None when not an instance.
47
+
48
+ Args:
49
+ service: ProjectService handle to read through.
50
+ entity: Entity id to inspect.
51
+
52
+ Returns:
53
+ Instance payload mapping, or None for a missing entity
54
+ or a non-instance holder.
55
+ """
56
+ from ..errors import ProjectError as _ProjectError
57
+
58
+ try:
59
+ data = service.get_component(entity, "Instance")["data"]
60
+ except _ProjectError:
61
+ return None
62
+ except LookupError:
63
+ return None
64
+ return data if isinstance(data, dict) else None
65
+
66
+
67
+ def _bindings(service):
68
+ """List every hierarchy binding payload, id-sorted.
69
+
70
+ Args:
71
+ service: ProjectService handle to read through.
72
+
73
+ Returns:
74
+ Binding mappings with id, parent_sheet, child_sheet,
75
+ and instance.
76
+ """
77
+ found = []
78
+ for entity in service.entities_with("Hierarchy")["entities"]:
79
+ try:
80
+ raw = service.get_component(entity, "Hierarchy")["data"]
81
+ except Exception: # noqa: BLE001 — skip unreadable rows
82
+ continue
83
+ if isinstance(raw, dict):
84
+ item = dict(raw)
85
+ item["id"] = entity
86
+ found.append(item)
87
+ found.sort(key=lambda item: item["id"])
88
+ return found
89
+
90
+
91
+ def _descendant_sheets(service, root):
92
+ """Collect a sheet plus every sheet bound beneath it.
93
+
94
+ Args:
95
+ service: ProjectService handle to read through.
96
+ root: Root sheet entity id.
97
+
98
+ Returns:
99
+ Sheet ids in the root closure (root first).
100
+ """
101
+ seen = [root]
102
+ changed = True
103
+ bindings = _bindings(service)
104
+ while changed:
105
+ changed = False
106
+ for binding in bindings:
107
+ if (
108
+ binding.get("parent_sheet") in seen
109
+ and binding.get("child_sheet") not in seen
110
+ ):
111
+ seen.append(binding.get("child_sheet"))
112
+ changed = True
113
+ return seen
114
+
115
+
116
+ def _ancestor_sheets(service, sheet):
117
+ """Collect sheets above one sheet through hierarchy bindings.
118
+
119
+ Args:
120
+ service: ProjectService handle to read through.
121
+ sheet: Sheet entity id to walk up from.
122
+
123
+ Returns:
124
+ Ancestor sheet ids, nearest first.
125
+ """
126
+ parents = {
127
+ binding.get("child_sheet"): binding.get("parent_sheet")
128
+ for binding in _bindings(service)
129
+ }
130
+ seen = []
131
+ current = parents.get(sheet)
132
+ while current is not None and current not in seen:
133
+ seen.append(current)
134
+ current = parents.get(current)
135
+ return seen
136
+
137
+
138
+ def _closure_instances(service, targets):
139
+ """Collect DUT targets plus instances on descendant sheets.
140
+
141
+ Args:
142
+ service: ProjectService handle to read through.
143
+ targets: Normalized targets with primary and
144
+ additional entity ids.
145
+
146
+ Returns:
147
+ Entity ids in the DUT closure (targets plus
148
+ descendant-scope instances).
149
+ """
150
+ sheets = set()
151
+ for entity in [targets["primary"], *targets["additional"]]:
152
+ payload = _instance_of(service, entity) or {}
153
+ if payload.get("sheet"):
154
+ sheets.update(
155
+ _descendant_sheets(service, payload["sheet"]))
156
+ found = {targets["primary"], *targets["additional"]}
157
+ for entity in service.entities_with("Instance")["entities"]:
158
+ payload = _instance_of(service, entity) or {}
159
+ if payload.get("sheet") in sheets:
160
+ found.add(entity)
161
+ return found
162
+
163
+
164
+ def normalize_targets(service, targets):
165
+ """Validate and normalize one DUT target set.
166
+
167
+ The primary must be a circuit-instance occurrence;
168
+ additional targets must be nonoverlapping sibling or
169
+ descendant instances — never ancestors, the scope itself,
170
+ or unrelated scopes.
171
+
172
+ Args:
173
+ service: ProjectService handle to read through.
174
+ targets: Mapping with primary plus additional ids.
175
+
176
+ Returns:
177
+ Normalized mapping with primary and additional ids.
178
+
179
+ Raises:
180
+ ContractError: invalid_target for a missing primary or
181
+ a non-instance target, overlapping_targets for a
182
+ repeated DUT, illegal_scope for an ancestor or an
183
+ unrelated scope.
184
+ """
185
+ if not isinstance(targets, dict) or not targets.get("primary"):
186
+ raise ContractError(
187
+ "invalid_target", "Testbench targets need a primary.")
188
+ primary = targets["primary"]
189
+ primary_payload = _instance_of(service, primary)
190
+ if primary_payload is None:
191
+ raise ContractError(
192
+ "invalid_target",
193
+ f"Primary DUT is not a circuit instance: {primary!r}.")
194
+ additional = list(targets.get("additional") or [])
195
+ if len(set(additional)) != len(additional) or primary in additional:
196
+ raise ContractError(
197
+ "overlapping_targets",
198
+ "Additional DUT targets must not overlap the primary.")
199
+ home = primary_payload.get("sheet")
200
+ allowed = set(_descendant_sheets(service, home)) if home else set()
201
+ ancestors = set(_ancestor_sheets(service, home)) if home else set()
202
+ for entity in additional:
203
+ payload = _instance_of(service, entity)
204
+ if payload is None:
205
+ raise ContractError(
206
+ "invalid_target",
207
+ f"Additional DUT is not an instance: {entity!r}.")
208
+ sheet = payload.get("sheet")
209
+ if sheet in ancestors:
210
+ raise ContractError(
211
+ "illegal_scope",
212
+ f"DUT {entity!r} is an ancestor of the primary.")
213
+ if sheet not in allowed:
214
+ raise ContractError(
215
+ "illegal_scope",
216
+ f"DUT {entity!r} is outside the primary scope.")
217
+ return {"primary": primary, "additional": additional}
218
+
219
+
220
+ def normalize_probes(service, targets, probes):
221
+ """Validate probes against the DUT closure.
222
+
223
+ Args:
224
+ service: ProjectService handle to read through.
225
+ targets: Normalized DUT targets.
226
+ probes: Probe mappings with an entity each.
227
+
228
+ Returns:
229
+ Normalized probe list.
230
+
231
+ Raises:
232
+ ContractError: illegal_scope for a probe outside the
233
+ DUT closure.
234
+ """
235
+ closure = _closure_instances(service, targets)
236
+ kept = []
237
+ for probe in probes or []:
238
+ entity = (probe or {}).get("entity")
239
+ if entity not in closure:
240
+ raise ContractError(
241
+ "illegal_scope",
242
+ f"Probe {entity!r} is outside the DUT closure.")
243
+ kept.append({"entity": entity})
244
+ return kept
245
+
246
+
247
+ def materialize_interface(service, targets):
248
+ """Materialize the target interface from symbol definitions.
249
+
250
+ Only symbol-defined pins appear with their occurrence; symbols
251
+ that do not resolve stay pin-less instead of invented.
252
+
253
+ Args:
254
+ service: ProjectService handle to read through.
255
+ targets: Normalized DUT targets.
256
+
257
+ Returns:
258
+ Interface with pins ([{occurrence, pin}]) and digest.
259
+ """
260
+ symbols = {}
261
+ for entity in service.entities_with("Symbol")["entities"]:
262
+ try:
263
+ raw = service.get_component(entity, "Symbol")["data"]
264
+ except Exception: # noqa: BLE001 — skip unreadable rows
265
+ continue
266
+ if isinstance(raw, dict) and raw.get("name"):
267
+ symbols[raw["name"]] = raw.get("pins") or []
268
+ pins = []
269
+ for entity in [targets["primary"], *targets["additional"]]:
270
+ payload = _instance_of(service, entity) or {}
271
+ for pin in symbols.get(payload.get("symbol", ""), []):
272
+ if isinstance(pin, dict) and pin.get("name"):
273
+ pins.append(
274
+ {"occurrence": entity, "pin": pin["name"]})
275
+ interface = {"pins": pins}
276
+ interface["digest"] = digest_mapping(interface)
277
+ return interface
278
+
279
+
280
+ def dut_digest(service, targets):
281
+ """Digest the DUT occurrence set plus its live revisions.
282
+
283
+ Args:
284
+ service: ProjectService handle to read through.
285
+ targets: Normalized DUT targets.
286
+
287
+ Returns:
288
+ Digest pinning occurrences, payloads, and revisions.
289
+ """
290
+ members = [targets["primary"], *targets["additional"]]
291
+ payloads = {
292
+ entity: (_instance_of(service, entity) or {}) for entity in members
293
+ }
294
+ revisions = {
295
+ entity: read_entity_revision(service, entity)
296
+ for entity in members
297
+ }
298
+ return digest_mapping(
299
+ {"payloads": payloads, "revisions": revisions})
300
+
301
+
302
+ def _record(service, bench):
303
+ """Read one testbench record payload.
304
+
305
+ Args:
306
+ service: ProjectService handle to read through.
307
+ bench: Testbench entity id.
308
+
309
+ Returns:
310
+ Testbench record mapping.
311
+ """
312
+ return service.get_component(bench, TESTBENCH_COMPONENT)["data"]
313
+
314
+
315
+ def check_fresh(service, bench):
316
+ """Check whether one testbench link is still fresh.
317
+
318
+ Args:
319
+ service: ProjectService handle to read through.
320
+ bench: Testbench entity id.
321
+
322
+ Returns:
323
+ (fresh, reasons) with stale reason strings (dut,
324
+ template, hierarchy) when the link drifted.
325
+ """
326
+ record = _record(service, bench)
327
+ reasons = []
328
+ targets = record.get("targets") or {}
329
+ try:
330
+ current = dut_digest(service, targets)
331
+ except Exception: # noqa: BLE001 — unreadable DUT reads stale
332
+ return False, ["dut"]
333
+ if current != record.get("dut_digest"):
334
+ reasons.append("dut")
335
+ members = [targets.get("primary"), *(targets.get("additional") or [])]
336
+ for entity in members:
337
+ try:
338
+ live = read_entity_revision(service, entity)
339
+ except Exception: # noqa: BLE001 — a lost DUT reads stale
340
+ reasons.append("hierarchy")
341
+ break
342
+ pinned = (record.get("dut_revisions") or {}).get(entity)
343
+ if pinned is not None and pinned != live:
344
+ reasons.append("hierarchy")
345
+ break
346
+ if (record.get("template") or {}).get(
347
+ "template_version") != TEMPLATE_VERSION:
348
+ reasons.append("template")
349
+ return (not reasons), sorted(set(reasons))
350
+
351
+
352
+ def assert_runnable(service, bench):
353
+ """Refuse to run a stale testbench link.
354
+
355
+ Args:
356
+ service: ProjectService handle to read through.
357
+ bench: Testbench entity id.
358
+
359
+ Returns:
360
+ True when the link is fresh.
361
+
362
+ Raises:
363
+ ContractError: stale_link naming the drift reasons.
364
+ """
365
+ fresh, reasons = check_fresh(service, bench)
366
+ if not fresh:
367
+ raise ContractError(
368
+ "stale_link",
369
+ f"Testbench {bench!r} drifted ({', '.join(reasons)}); "
370
+ "regenerate before running.")
371
+ return True
372
+
373
+
374
+ def create_block(service, request_id, command_text, name, kind,
375
+ targets, probes, harness, template=None,
376
+ expected_revision=None):
377
+ """Create one scoped testbench block with editable source.
378
+
379
+ Validates the DUT scope, materializes the interface, renders
380
+ the template source, stores the record, and saves the source
381
+ as a project document. The harness binding rides verbatim —
382
+ never ambient parent inclusion.
383
+
384
+ Args:
385
+ service: ProjectService handle to write through.
386
+ request_id: Inner client envelope id.
387
+ command_text: Inner command text for identity comparison.
388
+ name: Block name (source saved as testbench/<name>.<ext>).
389
+ kind: Template kind (spice, hdl, or cocotb).
390
+ targets: DUT target set (primary plus additional).
391
+ probes: Probe mappings inside the DUT closure.
392
+ harness: Explicit harness binding mapping.
393
+ template: Reserved template override (unused; the kind
394
+ selects the template).
395
+ expected_revision: Revision the writer read.
396
+
397
+ Returns:
398
+ Receipt with testbench id, name, kind, source path,
399
+ DUT digest, and freshness.
400
+
401
+ Raises:
402
+ ContractError: invalid_kind, invalid_target,
403
+ overlapping_targets, illegal_scope, or whatever the
404
+ service raises.
405
+ """
406
+ del template
407
+ describe_template(kind)
408
+ normalized = normalize_targets(service, targets)
409
+ kept_probes = normalize_probes(service, normalized, probes)
410
+ digest = dut_digest(service, normalized)
411
+ interface = materialize_interface(service, normalized)
412
+ rendered = render(kind, interface, dict(harness or {}))
413
+ path = f"{SOURCE_PREFIX}{name}.{TEMPLATE_EXTENSIONS[kind]}"
414
+ members = [normalized["primary"], *normalized["additional"]]
415
+ revisions = {
416
+ entity: read_entity_revision(service, entity)
417
+ for entity in members
418
+ }
419
+ created = service.create_entity(
420
+ request_id, command_text, kind="testbench",
421
+ components={
422
+ TESTBENCH_COMPONENT: {
423
+ "name": name, "kind": kind,
424
+ "targets": normalized, "probes": kept_probes,
425
+ "harness": dict(harness or {}),
426
+ "interface": interface, "dut_digest": digest,
427
+ "dut_revisions": revisions,
428
+ "template": rendered["provenance"],
429
+ "source": rendered["source"],
430
+ "source_path": path,
431
+ "candidate_source": None,
432
+ "candidate_digest": None,
433
+ "adopted": False, "fresh": True,
434
+ }
435
+ },
436
+ expected_revision=expected_revision,
437
+ )
438
+ service.write_doc(
439
+ path, rendered["source"],
440
+ f"{request_id}:source", f"{command_text}\n#inner source",
441
+ expected_revision=created["current_revision"],
442
+ )
443
+ return {
444
+ "testbench": created["entity"], "name": name, "kind": kind,
445
+ "source_path": path, "dut_digest": digest,
446
+ "current_revision": created["current_revision"],
447
+ "fresh": True,
448
+ }
449
+
450
+
451
+ def regenerate_block(service, request_id, command_text, testbench,
452
+ expected_revision=None):
453
+ """Regenerate one block to a candidate without overwriting edits.
454
+
455
+ A fresh link returns a noop receipt; a drifted link renders
456
+ the candidate from the current interface and stores it
457
+ beside the edited live source for explicit adoption.
458
+
459
+ Args:
460
+ service: ProjectService handle to write through.
461
+ request_id: Inner client envelope id.
462
+ command_text: Inner command text for identity comparison.
463
+ testbench: Testbench entity id.
464
+ expected_revision: Revision the writer read.
465
+
466
+ Returns:
467
+ Noop receipt when fresh, else the candidate source,
468
+ digest, drift reasons, and live-source diff flag.
469
+
470
+ Raises:
471
+ Exception: Whatever the service raises.
472
+ """
473
+ record = _record(service, testbench)
474
+ fresh, reasons = check_fresh(service, testbench)
475
+ normalized = normalize_targets(service, record.get("targets"))
476
+ if fresh:
477
+ return {"testbench": testbench, "noop": True, "fresh": True}
478
+ interface = materialize_interface(service, normalized)
479
+ rendered = render(
480
+ record.get("kind"), interface, record.get("harness") or {})
481
+ digest = dut_digest(service, normalized)
482
+ service.set_component(
483
+ testbench, TESTBENCH_COMPONENT,
484
+ {**record, "candidate_source": rendered["source"],
485
+ "candidate_digest": rendered["digest"],
486
+ "dut_digest": digest, "fresh": False},
487
+ request_id, command_text,
488
+ expected_revision=expected_revision,
489
+ )
490
+ return {
491
+ "testbench": testbench, "fresh": False,
492
+ "stale_reasons": reasons,
493
+ "candidate_source": rendered["source"],
494
+ "candidate_digest": rendered["digest"],
495
+ "differs_from_live":
496
+ rendered["source"] != record.get("source"),
497
+ }
498
+
499
+
500
+ def describe_block(service, bench):
501
+ """Describe one block with freshness and closure pins.
502
+
503
+ Args:
504
+ service: ProjectService handle to read through.
505
+ bench: Testbench entity id.
506
+
507
+ Returns:
508
+ Record plus freshness, stale reasons, port bindings,
509
+ interface digest, template provenance, and the I28
510
+ closure pin.
511
+ """
512
+ record = dict(_record(service, bench))
513
+ fresh, reasons = check_fresh(service, bench)
514
+ record["fresh"] = fresh
515
+ record["stale_reasons"] = reasons
516
+ record["port_bindings"] = [
517
+ {"occurrence": pin.get("occurrence"), "pin": pin.get("pin")}
518
+ for pin in (record.get("interface") or {}).get("pins", [])
519
+ ]
520
+ record["interface_digest"] = (
521
+ record.get("interface") or {}).get("digest")
522
+ record["i28"] = {
523
+ "revision": service.snapshot()["current_revision"],
524
+ "dut_revisions": record.get("dut_revisions"),
525
+ "template": record.get("template"),
526
+ "digest": digest_mapping({
527
+ "dut": record.get("dut_digest"),
528
+ "template": record.get("template"),
529
+ "source": record.get("source"),
530
+ }),
531
+ }
532
+ return record
533
+
534
+
535
+ def harness_instances(record):
536
+ """List harness-owned instances excluded from production output.
537
+
538
+ Args:
539
+ record: Testbench record mapping.
540
+
541
+ Returns:
542
+ Harness instance ids (explicit binding only).
543
+ """
544
+ harness = record.get("harness") or {}
545
+ return list(harness.get("instances") or [])
546
+
547
+
548
+ def design_net_pins(record, net_pins):
549
+ """Filter harness instances out of production net pins.
550
+
551
+ Args:
552
+ record: Testbench record mapping.
553
+ net_pins: Materialized [{instance, pin, net}] rows.
554
+
555
+ Returns:
556
+ Rows excluding harness-owned instances (adopted blocks
557
+ return every row unchanged).
558
+ """
559
+ if record.get("adopted"):
560
+ return list(net_pins or [])
561
+ excluded = set(harness_instances(record))
562
+ return [
563
+ row for row in net_pins or []
564
+ if row.get("instance") not in excluded
565
+ ]
566
+
567
+
568
+ def excluded_harness_instances(service):
569
+ """Collect harness-owned instances across all testbenches.
570
+
571
+ Args:
572
+ service: ProjectService handle to read through.
573
+
574
+ Returns:
575
+ Harness instance ids excluded from production output.
576
+ """
577
+ from ..errors import ProjectError as _ProjectError
578
+
579
+ excluded = set()
580
+ for bench in service.entities_with(
581
+ TESTBENCH_COMPONENT)["entities"]:
582
+ try:
583
+ record = service.get_component(
584
+ bench, TESTBENCH_COMPONENT)["data"]
585
+ except _ProjectError:
586
+ continue
587
+ excluded.update(harness_instances(record))
588
+ return excluded
589
+
590
+
591
+ def probe_simulator(kind):
592
+ """Probe the simulator binary for one testbench kind.
593
+
594
+ Absent simulators report BLOCKED-needs-host with a remedy —
595
+ never a faked PASS.
596
+
597
+ Args:
598
+ kind: Testbench kind (spice, hdl, or cocotb).
599
+
600
+ Returns:
601
+ Probe with kind, tool, available flag, and verdict
602
+ (ready or BLOCKED-needs-host) plus remedy text.
603
+ """
604
+ tool = SIMULATOR_TOOLS.get(kind, kind)
605
+ if tool == "cocotb":
606
+ available = importlib.util.find_spec("cocotb") is not None
607
+ else:
608
+ available = shutil.which(tool) is not None
609
+ if available:
610
+ return {"kind": kind, "tool": tool, "available": True,
611
+ "verdict": "ready"}
612
+ return {
613
+ "kind": kind, "tool": tool, "available": False,
614
+ "verdict": "BLOCKED-needs-host",
615
+ "remedy": (
616
+ f"Install {tool} on PATH (or provision the sim profile) "
617
+ "and rerun; testbench simulation stays gated until then."
618
+ ),
619
+ }