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,193 @@
1
+ """Versioned technology-profile semantics and capability gates.
2
+
3
+ A TechProfile pins the exact technology semantics process
4
+ operations consume (name, version, source hash, units, grid,
5
+ contact/connectivity notes, capability map). Nothing is inferred
6
+ from display names: a missing mapping, grid, rule, or tool
7
+ yields a named unsupported/incomplete verdict and authored
8
+ geometry stays intact. Binary-gated backends (opensta,
9
+ librelane, klayout, docker) probe loudly and report
10
+ BLOCKED-needs-host instead of faking a result.
11
+ """
12
+
13
+ import copy
14
+ import shutil
15
+
16
+ GATED_TOOLS = ("opensta", "librelane", "klayout", "docker")
17
+ CAPABILITIES = (
18
+ "drc", "extraction", "export_gds", "export_oasis",
19
+ "connectivity",
20
+ )
21
+
22
+
23
+ class TechProfile:
24
+ """Versioned technology semantics for one process.
25
+
26
+ Args:
27
+ name: Technology profile name (a PDK or another
28
+ explicitly supported technology).
29
+ version: Profile version string.
30
+ source_hash: Hash of the profile source the pin came
31
+ from (deck hash, file sha, or manifest hash).
32
+ units: Coordinate units label.
33
+ grid: Manufacturing grid pitch in storage units.
34
+ deck: Backend deck/tool version label, if any.
35
+ contacts: Contact/residue and overlap notes.
36
+ connectivity: Connectivity relation notes.
37
+ capabilities: Mapping of capability name to state
38
+ (supported, missing, or incompatible).
39
+ """
40
+
41
+ def __init__(self, name, version, source_hash, units="nm",
42
+ grid=1, deck=None, contacts=None,
43
+ connectivity=None, capabilities=None):
44
+ self.name = name
45
+ self.version = version
46
+ self.source_hash = source_hash
47
+ self.units = units
48
+ self.grid = grid
49
+ self.deck = deck
50
+ self.contacts = copy.deepcopy(contacts or {})
51
+ self.connectivity = copy.deepcopy(connectivity or {})
52
+ self.capabilities = dict(capabilities or {})
53
+
54
+ def pin(self):
55
+ """Return the I28 ingredient pin for this profile.
56
+
57
+ Returns:
58
+ Mapping with name, version, source hash, deck,
59
+ units, and grid.
60
+ """
61
+ return {
62
+ "deck": self.deck,
63
+ "grid": self.grid,
64
+ "name": self.name,
65
+ "source_hash": self.source_hash,
66
+ "units": self.units,
67
+ "version": self.version,
68
+ }
69
+
70
+ def label(self):
71
+ """Return the short name@version label.
72
+
73
+ Returns:
74
+ Profile label string.
75
+ """
76
+ return f"{self.name}@{self.version}"
77
+
78
+
79
+ def bind_layer(profile, layer, purpose, gds=None,
80
+ capability="supported"):
81
+ """Bind one project layer to process destinations.
82
+
83
+ Args:
84
+ profile: TechProfile the binding resolves under.
85
+ layer: Stable project layer lid.
86
+ purpose: Process purpose (drawing, contact, marker).
87
+ gds: Optional (layer, datatype) destination tuple.
88
+ capability: Binding state (supported, missing, or
89
+ incompatible).
90
+
91
+ Returns:
92
+ Binding mapping with source pin, destination,
93
+ purpose, and capability state.
94
+
95
+ Raises:
96
+ ValueError: For an unknown capability state.
97
+ """
98
+ if capability not in ("supported", "missing",
99
+ "incompatible"):
100
+ raise ValueError(
101
+ f"Unknown capability state: {capability!r}.")
102
+ return {
103
+ "capability": capability,
104
+ "destination": gds,
105
+ "layer": layer,
106
+ "profile": profile.label(),
107
+ "purpose": purpose,
108
+ "source_hash": profile.source_hash,
109
+ }
110
+
111
+
112
+ def check_capability(profile, operation, bindings):
113
+ """Check whether one operation may run under a profile.
114
+
115
+ Args:
116
+ profile: TechProfile the operation would consume.
117
+ operation: Operation name (drc, extraction,
118
+ export_gds, export_oasis, connectivity).
119
+ bindings: Layer bindings the design needs (empty
120
+ means the design needs no mapped layer).
121
+
122
+ Returns:
123
+ Verdict with state supported, unsupported, or
124
+ incomplete, naming the exact profile/deck pin.
125
+ Unsupported results never imply a valid run.
126
+ """
127
+ state = profile.capabilities.get(operation, "missing")
128
+ if state == "supported" and bindings:
129
+ missing = [binding for binding in bindings if
130
+ binding.get("capability") != "supported"]
131
+ if missing:
132
+ return {
133
+ "operation": operation,
134
+ "profile": profile.label(),
135
+ "reason": (
136
+ f"{len(missing)} layer binding(s) are not "
137
+ "supported; authored geometry is intact."
138
+ ),
139
+ "state": "incomplete",
140
+ }
141
+ return {
142
+ "operation": operation,
143
+ "profile": profile.label(),
144
+ "reason": "Profile and bindings cover the run.",
145
+ "state": "supported",
146
+ }
147
+ if state == "supported":
148
+ return {
149
+ "operation": operation,
150
+ "profile": profile.label(),
151
+ "reason": "Profile covers the run.",
152
+ "state": "supported",
153
+ }
154
+ return {
155
+ "operation": operation,
156
+ "profile": profile.label(),
157
+ "reason": (
158
+ f"Profile {profile.label()} reports {operation} "
159
+ f"as {state}; deck {profile.deck}."
160
+ ),
161
+ "state": "unsupported",
162
+ }
163
+
164
+
165
+ def probe_tool(tool):
166
+ """Probe one binary-gated backend without faking results.
167
+
168
+ Args:
169
+ tool: Tool binary name (opensta, librelane, klayout,
170
+ docker, or another backend binary).
171
+
172
+ Returns:
173
+ Probe mapping with tool, state (available or
174
+ BLOCKED-needs-host), path, and reason. Absent
175
+ binaries report BLOCKED-needs-host loudly.
176
+ """
177
+ path = shutil.which(tool)
178
+ if path is None:
179
+ return {
180
+ "path": None,
181
+ "reason": (
182
+ f"{tool} is not on PATH in this environment; "
183
+ "the operation is BLOCKED-needs-host."
184
+ ),
185
+ "state": "BLOCKED-needs-host",
186
+ "tool": tool,
187
+ }
188
+ return {
189
+ "path": path,
190
+ "reason": f"{tool} resolved at {path}.",
191
+ "state": "available",
192
+ "tool": tool,
193
+ }
@@ -0,0 +1,121 @@
1
+ """Declarative derived-geometry recipes with loud failure modes.
2
+
3
+ Recipes describe deterministic GDS/OASIS derivation steps
4
+ (copy, offset, size, boolean and/or/not) pinned to a profile
5
+ revision. Round-trip validation replays the recipe against a
6
+ reference layout; unsupported transformations fail loudly
7
+ instead of emitting a silent partial output.
8
+ """
9
+
10
+ import copy
11
+
12
+ SUPPORTED_OPS = ("copy", "offset", "size", "and", "or", "not")
13
+
14
+
15
+ class DerivedRecipe:
16
+ """Named deterministic derivation pinned to a profile.
17
+
18
+ Args:
19
+ name: Recipe name.
20
+ profile: Profile pin label (name@version).
21
+ steps: List of step mappings, each with an op from
22
+ SUPPORTED_OPS plus op arguments.
23
+ """
24
+
25
+ def __init__(self, name, profile, steps):
26
+ self.name = name
27
+ self.profile = profile
28
+ self.steps = copy.deepcopy(list(steps))
29
+
30
+ def to_dict(self):
31
+ """Serialize the recipe for revision pinning.
32
+
33
+ Returns:
34
+ Mapping with name, profile, and steps.
35
+ """
36
+ return {
37
+ "name": self.name,
38
+ "profile": self.profile,
39
+ "steps": copy.deepcopy(self.steps),
40
+ }
41
+
42
+
43
+ def _apply_step(shape, step):
44
+ """Apply one derivation step to one shape copy.
45
+
46
+ Args:
47
+ shape: Shape mapping with shape_type and data.
48
+ step: Step mapping with an op name.
49
+
50
+ Returns:
51
+ Derived shape mapping.
52
+
53
+ Raises:
54
+ ValueError: unsupported_transformation for an op
55
+ outside SUPPORTED_OPS.
56
+ """
57
+ op = step.get("op")
58
+ if op not in SUPPORTED_OPS:
59
+ raise ValueError(
60
+ f"unsupported_transformation: {op!r} is not a "
61
+ f"derivable recipe op for {shape.get('shape_type')}.")
62
+ out = copy.deepcopy(shape)
63
+ data = out["data"]
64
+ if op == "copy":
65
+ return out
66
+ if op == "offset":
67
+ dx, dy = step.get("dx", 0), step.get("dy", 0)
68
+ if out["shape_type"] in ("rect", "label"):
69
+ data["x"] += dx
70
+ data["y"] += dy
71
+ else:
72
+ data["points"] = [[px + dx, py + dy] for px, py in
73
+ data["points"]]
74
+ return out
75
+ if op == "size":
76
+ delta = step.get("delta", 0)
77
+ if out["shape_type"] == "rect":
78
+ data["x"] -= delta
79
+ data["y"] -= delta
80
+ data["w"] += 2 * delta
81
+ data["h"] += 2 * delta
82
+ return out
83
+ out["derived_boolean"] = op
84
+ return out
85
+
86
+
87
+ def apply_recipe(shapes, recipe):
88
+ """Derive output geometry deterministically.
89
+
90
+ Args:
91
+ shapes: List of input shape mappings.
92
+ recipe: DerivedRecipe to apply in step order.
93
+
94
+ Returns:
95
+ New list with derived shape mappings; inputs are
96
+ never mutated.
97
+
98
+ Raises:
99
+ ValueError: unsupported_transformation for any step
100
+ outside SUPPORTED_OPS.
101
+ """
102
+ current = copy.deepcopy(list(shapes))
103
+ for step in recipe.steps:
104
+ current = [_apply_step(shape, step) for shape in
105
+ current]
106
+ return current
107
+
108
+
109
+ def round_trip_ok(reference, recipe):
110
+ """Validate a recipe replays deterministically.
111
+
112
+ Args:
113
+ reference: Reference shape list to replay.
114
+ recipe: DerivedRecipe to validate.
115
+
116
+ Returns:
117
+ True when two applications agree byte-for-byte.
118
+ """
119
+ first = apply_recipe(reference, recipe)
120
+ second = apply_recipe(reference, recipe)
121
+ return first == second
@@ -0,0 +1 @@
1
+ """Schematic package marker: sheets, instances, hierarchy authoring."""
@@ -0,0 +1,312 @@
1
+ """Hierarchy: parent-sheet to child-sheet bindings through block instances."""
2
+
3
+ from ...interfaces.errors import ContractError
4
+
5
+ #: Component name hierarchy bindings carry on their entity.
6
+ HIERARCHY_COMPONENT = "Hierarchy"
7
+
8
+
9
+ def validate_hierarchy(binding):
10
+ """Validate a Hierarchy binding mapping at edit time.
11
+
12
+ Args:
13
+ binding: Binding mapping with parent_sheet, child_sheet,
14
+ instance, and port_map keys.
15
+
16
+ Returns:
17
+ Normalized binding copy.
18
+
19
+ Raises:
20
+ ContractError: invalid_hierarchy for any shape violation.
21
+ """
22
+ if not isinstance(binding, dict):
23
+ raise ContractError(
24
+ "invalid_hierarchy",
25
+ f"Hierarchy must be a mapping, got {binding!r}.",
26
+ )
27
+ for key in binding:
28
+ if key not in ("parent_sheet", "child_sheet", "instance",
29
+ "port_map"):
30
+ raise ContractError(
31
+ "invalid_hierarchy", f"Unknown Hierarchy key: {key}."
32
+ )
33
+ parent = binding.get("parent_sheet")
34
+ child = binding.get("child_sheet")
35
+ instance = binding.get("instance")
36
+ for label, value in (("parent_sheet", parent),
37
+ ("child_sheet", child),
38
+ ("instance", instance)):
39
+ if not isinstance(value, str) or not value:
40
+ raise ContractError(
41
+ "invalid_hierarchy",
42
+ f"Hierarchy {label} must be an entity id.",
43
+ )
44
+ if parent == child:
45
+ raise ContractError(
46
+ "invalid_hierarchy",
47
+ "Hierarchy parent and child must differ.",
48
+ )
49
+ port_map = binding.get("port_map", [])
50
+ if not isinstance(port_map, list):
51
+ raise ContractError(
52
+ "invalid_hierarchy", "Hierarchy port_map must be a list."
53
+ )
54
+ clean_map = []
55
+ seen = set()
56
+ for entry in port_map:
57
+ if (
58
+ not isinstance(entry, dict)
59
+ or set(entry) != {"parent", "child"}
60
+ or not entry["parent"]
61
+ or not isinstance(entry["parent"], str)
62
+ or not entry["child"]
63
+ or not isinstance(entry["child"], str)
64
+ ):
65
+ raise ContractError(
66
+ "invalid_hierarchy",
67
+ "port_map entries need parent + child text.",
68
+ )
69
+ if entry["child"] in seen:
70
+ raise ContractError(
71
+ "invalid_hierarchy",
72
+ f"Child port {entry['child']!r} maps more than once.",
73
+ )
74
+ seen.add(entry["child"])
75
+ clean_map.append(
76
+ {"parent": entry["parent"], "child": entry["child"]}
77
+ )
78
+ return {
79
+ "parent_sheet": parent, "child_sheet": child,
80
+ "instance": instance, "port_map": clean_map,
81
+ }
82
+
83
+
84
+ def _read_binding(store, binding_id):
85
+ """Return the normalized binding for one entity id."""
86
+ try:
87
+ raw = store.get_component(binding_id, HIERARCHY_COMPONENT)
88
+ except LookupError:
89
+ raise ContractError(
90
+ "unknown_binding",
91
+ f"No such hierarchy binding: {binding_id!r}.",
92
+ ) from None
93
+ value = validate_hierarchy(raw)
94
+ value["id"] = binding_id
95
+ return value
96
+
97
+
98
+ def _check_sheet(store, sheet_id):
99
+ """Fail loudly when a sheet entity is missing."""
100
+ try:
101
+ store.get_component(sheet_id, "Sheet")
102
+ except LookupError:
103
+ raise ContractError(
104
+ "unknown_sheet", f"No such sheet entity: {sheet_id!r}."
105
+ ) from None
106
+
107
+
108
+ def _check_instance(store, instance_id):
109
+ """Fail loudly when an instance entity is missing."""
110
+ try:
111
+ store.get_component(instance_id, "Instance")
112
+ except LookupError:
113
+ raise ContractError(
114
+ "unknown_instance",
115
+ f"No such instance entity: {instance_id!r}.",
116
+ ) from None
117
+
118
+
119
+ def _all_bindings(store):
120
+ """List every binding mapping, id-sorted."""
121
+ found = []
122
+ for entity in store.entities_with(HIERARCHY_COMPONENT):
123
+ try:
124
+ found.append(_read_binding(store, entity))
125
+ except ContractError:
126
+ continue
127
+ found.sort(key=lambda item: item["id"])
128
+ return found
129
+
130
+
131
+ def bind_block(store, actor, parent_sheet, child_sheet, instance):
132
+ """Bind a child sheet under a parent through a block instance.
133
+
134
+ Args:
135
+ store: Entity store serving the ENGINE-ECS seam.
136
+ actor: Attribution recorded on the revision and events.
137
+ parent_sheet: Parent sheet entity id.
138
+ child_sheet: Child sheet entity id.
139
+ instance: Block instance entity id on the parent sheet.
140
+
141
+ Returns:
142
+ Receipt with op, actor, revision, binding id, and events.
143
+
144
+ Raises:
145
+ ContractError: unknown_sheet for a missing sheet,
146
+ unknown_instance for a missing instance,
147
+ binding_exists for a duplicate binding,
148
+ invalid_hierarchy for a self-binding.
149
+ """
150
+ value = validate_hierarchy({
151
+ "parent_sheet": parent_sheet, "child_sheet": child_sheet,
152
+ "instance": instance, "port_map": [],
153
+ })
154
+ _check_sheet(store, parent_sheet)
155
+ _check_sheet(store, child_sheet)
156
+ _check_instance(store, instance)
157
+ for other in _all_bindings(store):
158
+ if (
159
+ other["parent_sheet"] == parent_sheet
160
+ and other["child_sheet"] == child_sheet
161
+ and other["instance"] == instance
162
+ ):
163
+ raise ContractError(
164
+ "binding_exists",
165
+ f"Sheet {child_sheet!r} is already bound under "
166
+ f"{parent_sheet!r} through {instance!r}.",
167
+ )
168
+ made = store.create_entity(actor, f"bind_block {child_sheet}")
169
+ store.attach_component(
170
+ actor, made["id"], HIERARCHY_COMPONENT, value,
171
+ f"bind_block {child_sheet}",
172
+ )
173
+ return {
174
+ "op": "bind_block", "actor": actor,
175
+ "revision": made["revision"], "binding": made["id"],
176
+ "events": [
177
+ {"event": "entity_created", "entity": made["id"]},
178
+ {"event": "component_changed", "entity": made["id"],
179
+ "component": HIERARCHY_COMPONENT},
180
+ ],
181
+ }
182
+
183
+
184
+ def set_port_map(store, actor, binding_id, port_map):
185
+ """Replace one binding's parent-to-child port map.
186
+
187
+ Args:
188
+ store: Entity store serving the ENGINE-ECS seam.
189
+ actor: Attribution recorded on the revision and events.
190
+ binding_id: Binding entity id to update.
191
+ port_map: New [{parent, child}] port mappings.
192
+
193
+ Returns:
194
+ Receipt with op, actor, revision, binding id, and
195
+ the new port map.
196
+
197
+ Raises:
198
+ ContractError: unknown_binding for a missing binding,
199
+ invalid_hierarchy for a malformed map.
200
+ """
201
+ value = _read_binding(store, binding_id)
202
+ clean = validate_hierarchy({
203
+ "parent_sheet": value["parent_sheet"],
204
+ "child_sheet": value["child_sheet"],
205
+ "instance": value["instance"], "port_map": port_map,
206
+ })
207
+ changed = store.set_component(
208
+ actor, binding_id, HIERARCHY_COMPONENT, clean,
209
+ f"set_port_map {binding_id}",
210
+ )
211
+ return {
212
+ "op": "set_port_map", "actor": actor,
213
+ "revision": changed["revision"], "binding": binding_id,
214
+ "port_map": clean["port_map"], "events": [],
215
+ }
216
+
217
+
218
+ def unbind_block(store, actor, binding_id):
219
+ """Delete one hierarchy binding.
220
+
221
+ Args:
222
+ store: Entity store serving the ENGINE-ECS seam.
223
+ actor: Attribution recorded on the revision and events.
224
+ binding_id: Binding entity id to delete.
225
+
226
+ Returns:
227
+ Receipt with op, actor, revision, and binding id.
228
+
229
+ Raises:
230
+ ContractError: unknown_binding for a missing binding.
231
+ """
232
+ _read_binding(store, binding_id)
233
+ done = store.delete_entity(
234
+ actor, binding_id, f"unbind_block {binding_id}"
235
+ )
236
+ return {
237
+ "op": "unbind_block", "actor": actor,
238
+ "revision": done["revision"], "binding": binding_id,
239
+ "events": [{"event": "entity_deleted", "entity": binding_id}],
240
+ }
241
+
242
+
243
+ def children_of(store, parent_sheet):
244
+ """List child sheet ids bound under one parent, id-sorted.
245
+
246
+ Args:
247
+ store: Entity store serving the ENGINE-ECS seam.
248
+ parent_sheet: Parent sheet entity id.
249
+
250
+ Returns:
251
+ Child sheet entity ids, id-sorted.
252
+ """
253
+ return sorted(
254
+ item["child_sheet"] for item in _all_bindings(store)
255
+ if item["parent_sheet"] == parent_sheet
256
+ )
257
+
258
+
259
+ def parent_of(store, child_sheet):
260
+ """List parent sheet ids binding one child, id-sorted.
261
+
262
+ Args:
263
+ store: Entity store serving the ENGINE-ECS seam.
264
+ child_sheet: Child sheet entity id.
265
+
266
+ Returns:
267
+ Parent sheet entity ids, id-sorted.
268
+ """
269
+ return sorted(
270
+ item["parent_sheet"] for item in _all_bindings(store)
271
+ if item["child_sheet"] == child_sheet
272
+ )
273
+
274
+
275
+ def hierarchy_tree(store, root_sheet):
276
+ """Render the hierarchy subtree rooted at one sheet.
277
+
278
+ Args:
279
+ store: Entity store serving the ENGINE-ECS seam.
280
+ root_sheet: Root sheet entity id.
281
+
282
+ Returns:
283
+ Nested mapping with sheet plus children subtrees.
284
+ """
285
+ return {
286
+ "sheet": root_sheet,
287
+ "children": [
288
+ hierarchy_tree(store, child)
289
+ for child in children_of(store, root_sheet)
290
+ ],
291
+ }
292
+
293
+
294
+ def sheet_path(store, sheet):
295
+ """Return the root-to-sheet chain through first parents.
296
+
297
+ Args:
298
+ store: Entity store serving the ENGINE-ECS seam.
299
+ sheet: Sheet entity id to locate.
300
+
301
+ Returns:
302
+ Sheet ids from the root down to the sheet itself.
303
+ """
304
+ chain = [sheet]
305
+ seen = {sheet}
306
+ while True:
307
+ parents = parent_of(store, chain[0])
308
+ if not parents or parents[0] in seen:
309
+ break
310
+ chain.insert(0, parents[0])
311
+ seen.add(parents[0])
312
+ return chain