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,352 @@
1
+ """Project-owned schematic symbols: pins, graphics, properties, revisions."""
2
+
3
+ from ...interfaces.errors import ContractError
4
+
5
+ #: Component name symbols carry on their entity.
6
+ SYMBOL_COMPONENT = "Symbol"
7
+
8
+ #: Electrical pin types a symbol pin may carry.
9
+ PIN_TYPES = ("input", "output", "inout", "passive", "power")
10
+
11
+
12
+ def _is_int(value):
13
+ """True for real ints (bools excluded)."""
14
+ return isinstance(value, int) and not isinstance(value, bool)
15
+
16
+
17
+ def validate_pin(pin):
18
+ """Validate one symbol pin record at edit time.
19
+
20
+ Args:
21
+ pin: Pin mapping with name, pin_type, x, and y keys.
22
+
23
+ Returns:
24
+ Normalized pin copy.
25
+
26
+ Raises:
27
+ ContractError: invalid_symbol for any shape violation.
28
+ """
29
+ if not isinstance(pin, dict):
30
+ raise ContractError(
31
+ "invalid_symbol", f"Symbol pin must be a mapping, got {pin!r}."
32
+ )
33
+ for key in pin:
34
+ if key not in ("name", "pin_type", "x", "y"):
35
+ raise ContractError(
36
+ "invalid_symbol", f"Unknown symbol pin key: {key}."
37
+ )
38
+ name = pin.get("name")
39
+ if not isinstance(name, str) or not name:
40
+ raise ContractError(
41
+ "invalid_symbol", "Symbol pin name must be non-empty text."
42
+ )
43
+ if pin.get("pin_type") not in PIN_TYPES:
44
+ raise ContractError(
45
+ "invalid_symbol",
46
+ f"Symbol pin type must be one of {PIN_TYPES}.",
47
+ )
48
+ for axis in ("x", "y"):
49
+ if not _is_int(pin.get(axis)):
50
+ raise ContractError(
51
+ "invalid_symbol",
52
+ f"Symbol pin {axis} must be an int, got "
53
+ f"{pin.get(axis)!r}.",
54
+ )
55
+ return {
56
+ "name": name, "pin_type": pin["pin_type"],
57
+ "x": pin["x"], "y": pin["y"],
58
+ }
59
+
60
+
61
+ def validate_symbol(symbol):
62
+ """Validate a Symbol component mapping at edit time.
63
+
64
+ Args:
65
+ symbol: Symbol mapping with name, pins, graphics,
66
+ properties, and revision keys.
67
+
68
+ Returns:
69
+ Normalized Symbol copy.
70
+
71
+ Raises:
72
+ ContractError: invalid_symbol for any shape violation.
73
+ """
74
+ if not isinstance(symbol, dict):
75
+ raise ContractError(
76
+ "invalid_symbol", f"Symbol must be a mapping, got {symbol!r}."
77
+ )
78
+ for key in symbol:
79
+ if key not in ("name", "pins", "graphics", "properties",
80
+ "revision"):
81
+ raise ContractError(
82
+ "invalid_symbol", f"Unknown Symbol key: {key}."
83
+ )
84
+ name = symbol.get("name")
85
+ if not isinstance(name, str) or not name:
86
+ raise ContractError(
87
+ "invalid_symbol", "Symbol name must be non-empty text."
88
+ )
89
+ pins = symbol.get("pins", [])
90
+ if not isinstance(pins, list):
91
+ raise ContractError(
92
+ "invalid_symbol", "Symbol pins must be a list."
93
+ )
94
+ clean_pins = [validate_pin(pin) for pin in pins]
95
+ seen = [pin["name"] for pin in clean_pins]
96
+ if len(set(seen)) != len(seen):
97
+ raise ContractError(
98
+ "invalid_symbol", "Symbol pin names must be distinct."
99
+ )
100
+ graphics = symbol.get("graphics", [])
101
+ if not isinstance(graphics, list) or not all(
102
+ isinstance(item, dict) and isinstance(item.get("kind"), str)
103
+ for item in graphics
104
+ ):
105
+ raise ContractError(
106
+ "invalid_symbol",
107
+ "Symbol graphics must be a list of {kind, ...} items.",
108
+ )
109
+ properties = symbol.get("properties", {})
110
+ if not isinstance(properties, dict) or not all(
111
+ isinstance(key, str) and isinstance(val, str)
112
+ for key, val in properties.items()
113
+ ):
114
+ raise ContractError(
115
+ "invalid_symbol",
116
+ "Symbol properties must map text to text.",
117
+ )
118
+ revision = symbol.get("revision", 1)
119
+ if not _is_int(revision) or revision < 1:
120
+ raise ContractError(
121
+ "invalid_symbol", "Symbol revision must be a positive int."
122
+ )
123
+ return {
124
+ "name": name, "pins": clean_pins,
125
+ "graphics": [dict(item) for item in graphics],
126
+ "properties": dict(properties), "revision": revision,
127
+ }
128
+
129
+
130
+ def _read_symbol(store, symbol_id):
131
+ """Return the normalized Symbol for one entity id."""
132
+ try:
133
+ raw = store.get_component(symbol_id, SYMBOL_COMPONENT)
134
+ except LookupError:
135
+ raise ContractError(
136
+ "unknown_symbol",
137
+ f"No such symbol entity: {symbol_id!r}.",
138
+ ) from None
139
+ value = validate_symbol(raw)
140
+ value["id"] = symbol_id
141
+ return value
142
+
143
+
144
+ def _all_symbols(store):
145
+ """List every symbol mapping, name-sorted."""
146
+ found = []
147
+ for entity in store.entities_with(SYMBOL_COMPONENT):
148
+ try:
149
+ found.append(_read_symbol(store, entity))
150
+ except ContractError:
151
+ continue
152
+ found.sort(key=lambda item: item["name"])
153
+ return found
154
+
155
+
156
+ def _check_name_free(store, name, except_id=None):
157
+ """Fail loudly when a symbol name is already taken."""
158
+ for item in _all_symbols(store):
159
+ if item["name"] == name and item["id"] != except_id:
160
+ raise ContractError(
161
+ "symbol_exists", f"Symbol already exists: {name!r}."
162
+ )
163
+
164
+
165
+ def create_symbol(store, actor, name, pins, graphics=None,
166
+ properties=None):
167
+ """Create one project-owned symbol definition.
168
+
169
+ Args:
170
+ store: Entity store serving the ENGINE-ECS seam.
171
+ actor: Attribution recorded on the revision and events.
172
+ name: Unique symbol name.
173
+ pins: Pin records with name, pin_type, x, and y.
174
+ graphics: Editable graphics items ({kind, ...}).
175
+ properties: Text-to-text symbol properties.
176
+
177
+ Returns:
178
+ Receipt with op, actor, revision, symbol id, and events.
179
+
180
+ Raises:
181
+ ContractError: invalid_symbol for a malformed symbol,
182
+ symbol_exists for a duplicate name.
183
+ """
184
+ value = validate_symbol({
185
+ "name": name, "pins": pins, "graphics": graphics or [],
186
+ "properties": properties or {}, "revision": 1,
187
+ })
188
+ _check_name_free(store, value["name"])
189
+ made = store.create_entity(actor, f"create_symbol {value['name']}")
190
+ store.attach_component(
191
+ actor, made["id"], SYMBOL_COMPONENT, value,
192
+ f"create_symbol {value['name']}",
193
+ )
194
+ return {
195
+ "op": "create_symbol", "actor": actor,
196
+ "revision": made["revision"], "symbol": made["id"],
197
+ "events": [
198
+ {"event": "entity_created", "entity": made["id"]},
199
+ {"event": "component_changed", "entity": made["id"],
200
+ "component": SYMBOL_COMPONENT},
201
+ ],
202
+ }
203
+
204
+
205
+ def copy_symbol(store, actor, symbol_id, name):
206
+ """Copy one symbol definition under a new name at revision 1.
207
+
208
+ Args:
209
+ store: Entity store serving the ENGINE-ECS seam.
210
+ actor: Attribution recorded on the revision and events.
211
+ symbol_id: Source symbol entity id.
212
+ name: New unique symbol name.
213
+
214
+ Returns:
215
+ Receipt with op, actor, revision, symbol id, and events.
216
+
217
+ Raises:
218
+ ContractError: unknown_symbol for a missing source,
219
+ symbol_exists for a duplicate name.
220
+ """
221
+ source = _read_symbol(store, symbol_id)
222
+ return create_symbol(
223
+ store, actor, name, source["pins"],
224
+ graphics=source["graphics"], properties=source["properties"],
225
+ )
226
+
227
+
228
+ def rename_symbol(store, actor, symbol_id, name):
229
+ """Rename one symbol; refuse while instances reference it.
230
+
231
+ Args:
232
+ store: Entity store serving the ENGINE-ECS seam.
233
+ actor: Attribution recorded on the revision and events.
234
+ symbol_id: Symbol entity id to rename.
235
+ name: New unique symbol name.
236
+
237
+ Returns:
238
+ Receipt with op, actor, revision, symbol id, and name.
239
+
240
+ Raises:
241
+ ContractError: unknown_symbol for a missing symbol,
242
+ symbol_exists for a duplicate name, symbol_in_use
243
+ when instances reference the old name.
244
+ """
245
+ value = _read_symbol(store, symbol_id)
246
+ if not isinstance(name, str) or not name:
247
+ raise ContractError(
248
+ "invalid_symbol", "Symbol name must be non-empty text."
249
+ )
250
+ _check_name_free(store, name, except_id=symbol_id)
251
+ for entity in store.entities_with("Instance"):
252
+ try:
253
+ instance = store.get_component(entity, "Instance")
254
+ except LookupError:
255
+ continue
256
+ if (
257
+ isinstance(instance, dict)
258
+ and instance.get("symbol") == value["name"]
259
+ ):
260
+ raise ContractError(
261
+ "symbol_in_use",
262
+ f"Symbol {value['name']!r} is placed on instance "
263
+ f"{entity!r}.",
264
+ )
265
+ clean = validate_symbol({
266
+ "name": name, "pins": value["pins"],
267
+ "graphics": value["graphics"],
268
+ "properties": value["properties"],
269
+ "revision": value["revision"],
270
+ })
271
+ changed = store.set_component(
272
+ actor, symbol_id, SYMBOL_COMPONENT, clean,
273
+ f"rename_symbol {name}",
274
+ )
275
+ return {
276
+ "op": "rename_symbol", "actor": actor,
277
+ "revision": changed["revision"], "symbol": symbol_id,
278
+ "name": name, "events": [],
279
+ }
280
+
281
+
282
+ def revise_symbol(store, actor, symbol_id, pins=None, graphics=None,
283
+ properties=None):
284
+ """Revise one symbol definition, bumping its revision.
285
+
286
+ Args:
287
+ store: Entity store serving the ENGINE-ECS seam.
288
+ actor: Attribution recorded on the revision and events.
289
+ symbol_id: Symbol entity id to revise.
290
+ pins: Replacement pin records, or None to keep.
291
+ graphics: Replacement graphics items, or None to keep.
292
+ properties: Replacement properties, or None to keep.
293
+
294
+ Returns:
295
+ Receipt with op, actor, revision, symbol id, and the
296
+ new symbol revision.
297
+
298
+ Raises:
299
+ ContractError: unknown_symbol for a missing symbol,
300
+ invalid_symbol for a malformed revision.
301
+ """
302
+ value = _read_symbol(store, symbol_id)
303
+ clean = validate_symbol({
304
+ "name": value["name"],
305
+ "pins": pins if pins is not None else value["pins"],
306
+ "graphics": (
307
+ graphics if graphics is not None else value["graphics"]
308
+ ),
309
+ "properties": (
310
+ properties if properties is not None
311
+ else value["properties"]
312
+ ),
313
+ "revision": value["revision"] + 1,
314
+ })
315
+ changed = store.set_component(
316
+ actor, symbol_id, SYMBOL_COMPONENT, clean,
317
+ f"revise_symbol {value['name']}",
318
+ )
319
+ return {
320
+ "op": "revise_symbol", "actor": actor,
321
+ "revision": changed["revision"], "symbol": symbol_id,
322
+ "symbol_revision": clean["revision"], "events": [],
323
+ }
324
+
325
+
326
+ def get_symbol(store, symbol_id):
327
+ """Read one symbol definition by entity id.
328
+
329
+ Args:
330
+ store: Entity store serving the ENGINE-ECS seam.
331
+ symbol_id: Symbol entity id to read.
332
+
333
+ Returns:
334
+ Symbol mapping with id, name, pins, graphics,
335
+ properties, and revision.
336
+
337
+ Raises:
338
+ ContractError: unknown_symbol for a missing symbol.
339
+ """
340
+ return _read_symbol(store, symbol_id)
341
+
342
+
343
+ def list_symbols(store):
344
+ """List every symbol definition, name-sorted.
345
+
346
+ Args:
347
+ store: Entity store serving the ENGINE-ECS seam.
348
+
349
+ Returns:
350
+ Symbol mappings with ids, name-sorted.
351
+ """
352
+ return _all_symbols(store)
@@ -0,0 +1,303 @@
1
+ """Runtime-modifiable project templates (TEMPLATE-RUNTIME).
2
+
3
+ The engine ships one versioned base shape; PDKs and power users
4
+ extend it with extra named areas and custom config without forking
5
+ the engine. Extensions land only where allowed: unknown top-level
6
+ keys fail loudly naming the key, area entries admit exactly name
7
+ plus label, and the `engine.` config namespace stays reserved for
8
+ the engine. Older templates migrate through registered one-step
9
+ links; newer-than-known templates refuse with future_format and
10
+ unlinked steps name their missing_migration.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+ import re
17
+
18
+ from ruamel.yaml import YAML
19
+
20
+ from .errors import TemplateError
21
+
22
+ TEMPLATE_VERSION = 1
23
+
24
+ BASE_TEMPLATE_ID = "default"
25
+
26
+ _TOP_LEVEL_KEYS = frozenset(
27
+ {"template_version", "id", "base", "description", "areas", "config"}
28
+ )
29
+
30
+ _AREA_KEYS = frozenset({"name", "label"})
31
+
32
+ _SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]*$")
33
+
34
+ _RESERVED_CONFIG_PREFIX = "engine."
35
+
36
+ _SCALARS = (str, int, float, bool)
37
+
38
+ _MIGRATIONS = {}
39
+
40
+
41
+ def register_migration(old, new, fn):
42
+ """Register one chained migration link between template versions.
43
+
44
+ Links chain one step at a time (new must be old + 1); migrate
45
+ walks them from the template version to the head. Registration
46
+ is additive: engine upgrades add links, never rewrite old ones.
47
+
48
+ Args:
49
+ old: Source template_version this link migrates from.
50
+ new: Target template_version (must be old + 1).
51
+ fn: Callable taking the old mapping and returning the new
52
+ mapping.
53
+
54
+ Raises:
55
+ TemplateError: invalid_migration for a skipped version
56
+ step or a conflicting link, invalid_template when fn
57
+ is not callable.
58
+ """
59
+ if not callable(fn):
60
+ raise TemplateError(
61
+ "invalid_template",
62
+ f"Template migrator {old}->{new} is not callable.",
63
+ )
64
+ if new != old + 1:
65
+ raise TemplateError(
66
+ "invalid_migration",
67
+ f"Template migration must step one version, got {old}->{new}.",
68
+ )
69
+ if old in _MIGRATIONS:
70
+ raise TemplateError(
71
+ "invalid_migration",
72
+ f"Template already migrates from version {old}.",
73
+ )
74
+ _MIGRATIONS[old] = (new, fn)
75
+
76
+
77
+ def clear_migrations():
78
+ """Drop registered migration links (a test seam, not a feature).
79
+
80
+ Returns:
81
+ Count of dropped links.
82
+ """
83
+ count = len(_MIGRATIONS)
84
+ _MIGRATIONS.clear()
85
+ return count
86
+
87
+
88
+ def _check_id(value, what):
89
+ if not isinstance(value, str) or not _SLUG_RE.match(value):
90
+ raise TemplateError(
91
+ "invalid_template", f"Template {what} must be a slug: {value!r}."
92
+ )
93
+
94
+
95
+ def _check_areas(areas):
96
+ if not isinstance(areas, list):
97
+ raise TemplateError(
98
+ "invalid_template",
99
+ f"Template areas must be a list: {areas!r}.",
100
+ )
101
+ for index, area in enumerate(areas):
102
+ if not isinstance(area, dict):
103
+ raise TemplateError(
104
+ "invalid_template",
105
+ f"Template areas[{index}] must be a mapping: {area!r}.",
106
+ )
107
+ unknown = set(area) - _AREA_KEYS
108
+ if unknown:
109
+ raise TemplateError(
110
+ "invalid_template",
111
+ f"Template areas[{index}] allows only "
112
+ f"{sorted(_AREA_KEYS)}, got: {sorted(unknown)}.",
113
+ )
114
+ name = area.get("name")
115
+ if not isinstance(name, str) or not _SLUG_RE.match(name):
116
+ raise TemplateError(
117
+ "invalid_template",
118
+ f"Template areas[{index}] needs a slug name: {area!r}.",
119
+ )
120
+ label = area.get("label")
121
+ if label is not None and not isinstance(label, str):
122
+ raise TemplateError(
123
+ "invalid_template",
124
+ f"Template areas[{index}] label must be text: {area!r}.",
125
+ )
126
+
127
+
128
+ def _check_config(config):
129
+ if not isinstance(config, dict):
130
+ raise TemplateError(
131
+ "invalid_template",
132
+ f"Template config must be a mapping: {config!r}.",
133
+ )
134
+ for key, value in config.items():
135
+ if not isinstance(key, str) or not key:
136
+ raise TemplateError(
137
+ "invalid_template",
138
+ f"Template config keys must be text: {key!r}.",
139
+ )
140
+ if key.startswith(_RESERVED_CONFIG_PREFIX):
141
+ raise TemplateError(
142
+ "invalid_template",
143
+ f"Template config key {key!r} is engine-reserved "
144
+ f"({_RESERVED_CONFIG_PREFIX}* namespace).",
145
+ )
146
+ if not isinstance(value, _SCALARS):
147
+ raise TemplateError(
148
+ "invalid_template",
149
+ f"Template config {key!r} must be a scalar: {value!r}.",
150
+ )
151
+
152
+
153
+ def validate_template(data):
154
+ """Check one head-version template mapping against the base.
155
+
156
+ Extensions land only where allowed: the fixed top-level key
157
+ set, name-plus-label area entries, and scalar config outside
158
+ the engine-reserved namespace.
159
+
160
+ Args:
161
+ data: Template mapping already at TEMPLATE_VERSION.
162
+
163
+ Returns:
164
+ Validated deep copy of the template mapping.
165
+
166
+ Raises:
167
+ TemplateError: invalid_template for a non-mapping,
168
+ unknown key, bad id/base, or malformed area/config.
169
+ """
170
+ if not isinstance(data, dict):
171
+ raise TemplateError(
172
+ "invalid_template",
173
+ f"Template must be a mapping: {data!r}.",
174
+ )
175
+ unknown = set(data) - _TOP_LEVEL_KEYS
176
+ if unknown:
177
+ raise TemplateError(
178
+ "invalid_template",
179
+ f"Template allows only {sorted(_TOP_LEVEL_KEYS)}, "
180
+ f"got: {sorted(unknown)}.",
181
+ )
182
+ _check_id(data.get("id"), "id")
183
+ base = data.get("base")
184
+ if not isinstance(base, str) or not base:
185
+ raise TemplateError(
186
+ "invalid_template",
187
+ f"Template base must name its base template: {base!r}.",
188
+ )
189
+ description = data.get("description")
190
+ if description is not None and not isinstance(description, str):
191
+ raise TemplateError(
192
+ "invalid_template",
193
+ f"Template description must be text: {description!r}.",
194
+ )
195
+ _check_areas(data.get("areas", []))
196
+ _check_config(data.get("config", {}))
197
+ return copy.deepcopy(data)
198
+
199
+
200
+ def migrate_template(data):
201
+ """Migrate one template mapping to the known head version.
202
+
203
+ Walks registered one-step links from the mapping's version
204
+ upward; each step's output re-enters the chain so multi-step
205
+ upgrades compose. Migration is pure (no I/O).
206
+
207
+ Args:
208
+ data: Template mapping at some older template_version.
209
+
210
+ Returns:
211
+ Migrated mapping stamped at TEMPLATE_VERSION.
212
+
213
+ Raises:
214
+ TemplateError: invalid_template for a non-mapping or a
215
+ non-integer version, future_format for a mapping
216
+ newer than the head, missing_migration for a version
217
+ step with no registered link, invalid_migration when
218
+ a migrator returns a non-mapping or raises.
219
+ """
220
+ if not isinstance(data, dict):
221
+ raise TemplateError(
222
+ "invalid_template",
223
+ f"Template must be a mapping: {data!r}.",
224
+ )
225
+ version = data.get("template_version")
226
+ if isinstance(version, bool) or not isinstance(version, int):
227
+ raise TemplateError(
228
+ "invalid_template",
229
+ f"Template version must be an integer: {version!r}.",
230
+ )
231
+ if version > TEMPLATE_VERSION:
232
+ raise TemplateError(
233
+ "future_format",
234
+ f"Template is at version {version}; "
235
+ f"this engine knows {TEMPLATE_VERSION}.",
236
+ )
237
+ current = copy.deepcopy(data)
238
+ while version < TEMPLATE_VERSION:
239
+ link = _MIGRATIONS.get(version)
240
+ if link is None:
241
+ raise TemplateError(
242
+ "missing_migration",
243
+ f"No template migration from version {version}.",
244
+ )
245
+ new, fn = link
246
+ try:
247
+ current = fn(current)
248
+ except TemplateError:
249
+ raise
250
+ except Exception as exc:
251
+ raise TemplateError(
252
+ "invalid_migration",
253
+ f"Template migrator {version}->{new} failed: {exc}.",
254
+ ) from exc
255
+ if not isinstance(current, dict):
256
+ raise TemplateError(
257
+ "invalid_migration",
258
+ f"Template migrator {version}->{new} "
259
+ "did not return a mapping.",
260
+ )
261
+ current["template_version"] = new
262
+ version = new
263
+ return current
264
+
265
+
266
+ def load_template(data):
267
+ """Migrate then validate one custom template mapping.
268
+
269
+ Args:
270
+ data: Custom template mapping at any known version.
271
+
272
+ Returns:
273
+ Validated template mapping at TEMPLATE_VERSION.
274
+
275
+ Raises:
276
+ TemplateError: future_format, missing_migration,
277
+ invalid_migration, or invalid_template naming the
278
+ violating key, area, or config entry.
279
+ """
280
+ return validate_template(migrate_template(data))
281
+
282
+
283
+ def loads_template(text):
284
+ """Parse YAML text then migrate and validate one template.
285
+
286
+ Args:
287
+ text: YAML text of a custom template document.
288
+
289
+ Returns:
290
+ Validated template mapping at TEMPLATE_VERSION.
291
+
292
+ Raises:
293
+ TemplateError: invalid_template when the text is not a
294
+ YAML mapping, plus every load_template verdict.
295
+ """
296
+ try:
297
+ data = YAML(typ="safe").load(text)
298
+ except Exception as exc:
299
+ raise TemplateError(
300
+ "invalid_template",
301
+ f"Template is not YAML: {exc}.",
302
+ ) from exc
303
+ return load_template(data)
@@ -0,0 +1,25 @@
1
+ """Custom project template errors (TEMPLATE-RUNTIME)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class TemplateError(Exception):
7
+ """Typed custom-template failure; `code` names the verdict.
8
+
9
+ Codes: invalid_template for a non-mapping, bad YAML, unknown
10
+ key, or malformed area/config entry; future_format for a
11
+ template newer than this engine; missing_migration for a
12
+ version step with no registered link; invalid_migration for
13
+ a skipped step, a conflicting link, or a migrator that does
14
+ not return a mapping.
15
+ """
16
+
17
+ def __init__(self, code, message):
18
+ """Bind one machine-readable verdict code to its message.
19
+
20
+ Args:
21
+ code: Verdict code naming the failure.
22
+ message: Human-readable detail naming the violation.
23
+ """
24
+ super().__init__(message)
25
+ self.code = code