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,461 @@
1
+ """Symbol-to-cell linkage contract (CELL-LINK).
2
+
3
+ A custom block is useless until its schematic symbol binds the
4
+ layout cell that implements it. The CellLink component is that
5
+ binding, stored on the Block entity alongside SymbolRef, with
6
+ KiCad's symbol-to-footprint relationship as the model: the link
7
+ is plain text on the symbol, filters are advisory, and variants
8
+ alter resolution, never storage.
9
+
10
+ Schema (all edit-time validated by validate_celllink, which never
11
+ checks cell existence — an unknown cell is a run error, never an
12
+ edit error):
13
+
14
+ - ``cell``: opaque ``"pdklib:cell"`` string (KiCad LIB_ID analog).
15
+ Missing or empty means unlinked; run/export skips such blocks.
16
+ - ``pinMap``: ordered explicit ``[{symPin, cellPin}]`` list. Pins
17
+ absent from the map resolve to themselves (identity default);
18
+ each symbol pin names a single target (no fanout).
19
+ - ``cellFilters``: advisory ``[glob, ...]`` list with KiCad
20
+ chooser semantics, consulted by the cell picker only.
21
+ - ``variantOverrides``: ``{name: {cell?, pinEdits?}}`` sparse
22
+ differentials. The stored link itself is the one default
23
+ variant (``variant=None``); ``REFERENCE`` always resolves it.
24
+
25
+ Resolution has one chokepoint per question — resolve_pin,
26
+ resolve_cell, resolve_link — and one run/export entry point,
27
+ resolve_run, which emits ``(cell, pinMap, variant)`` per block
28
+ instance only when the resolved cell is non-empty.
29
+ """
30
+
31
+ import re
32
+
33
+ from .errors import ContractError
34
+
35
+ #: Reserved variant selector resolving the stored base link. It can
36
+ #: never name an override: the reference stays immutable no matter
37
+ #: which variants a link carries.
38
+ REFERENCE = "REFERENCE"
39
+
40
+ _LINK_KEYS = frozenset({"cell", "pinMap", "cellFilters", "variantOverrides"})
41
+ _PIN_KEYS = frozenset({"symPin", "cellPin"})
42
+ _OVERRIDE_KEYS = frozenset({"cell", "pinEdits"})
43
+
44
+ __all__ = [
45
+ "REFERENCE",
46
+ "CellLinkError",
47
+ "collect_erc",
48
+ "match_cell_filters",
49
+ "resolve_cell",
50
+ "resolve_link",
51
+ "resolve_pin",
52
+ "resolve_run",
53
+ "validate_celllink",
54
+ ]
55
+
56
+
57
+ class CellLinkError(Exception):
58
+ """Cell-link run failure with a stable machine code.
59
+
60
+ Raised when a stored link cannot run — the cell it names is
61
+ unknown — never at edit time. Editing stays free; the run
62
+ names the block and the cell so the fix is mechanical.
63
+
64
+ Args:
65
+ code: Stable machine-readable error code.
66
+ message: Human-readable detail.
67
+ fields: Itemized fields, when the error names any.
68
+ """
69
+
70
+ def __init__(self, code, message, fields=None):
71
+ super().__init__(message)
72
+ self.code = code
73
+ self.fields = fields or []
74
+
75
+
76
+ def _need_str(value, what):
77
+ if not isinstance(value, str):
78
+ raise ContractError(
79
+ "invalid_celllink", f"{what} must be a string, got {value!r}."
80
+ )
81
+ return value
82
+
83
+
84
+ def _need_nonempty_str(value, what):
85
+ _need_str(value, what)
86
+ if not value:
87
+ raise ContractError("invalid_celllink", f"{what} must not be empty.")
88
+ return value
89
+
90
+
91
+ def validate_celllink(link):
92
+ """Validate a CellLink component mapping at edit time.
93
+
94
+ Checks shape only — key sets, string types, single-target
95
+ pins, well-formed overrides — and returns a normalized copy
96
+ with defaults filled. Cell existence is never checked here:
97
+ any cell text, however unknown, passes validation.
98
+
99
+ Args:
100
+ link: CellLink mapping with cell, pinMap, cellFilters,
101
+ and variantOverrides keys.
102
+
103
+ Returns:
104
+ Normalized CellLink copy (defaults filled, storage
105
+ untouched).
106
+
107
+ Raises:
108
+ ContractError: invalid_celllink for any shape violation.
109
+ """
110
+ if not isinstance(link, dict):
111
+ raise ContractError(
112
+ "invalid_celllink", f"CellLink must be a mapping, got {link!r}."
113
+ )
114
+ for key in link:
115
+ if key not in _LINK_KEYS:
116
+ raise ContractError("invalid_celllink", f"Unknown CellLink key: {key}.")
117
+ cell = _need_str(link.get("cell", ""), "cell")
118
+ pin_map = link.get("pinMap", [])
119
+ if not isinstance(pin_map, list):
120
+ raise ContractError("invalid_celllink", "pinMap must be a list.")
121
+ seen = set()
122
+ clean_pins = []
123
+ for entry in pin_map:
124
+ if not isinstance(entry, dict):
125
+ raise ContractError(
126
+ "invalid_celllink", f"pinMap entries must be mappings, got {entry!r}."
127
+ )
128
+ for key in entry:
129
+ if key not in _PIN_KEYS:
130
+ raise ContractError(
131
+ "invalid_celllink", f"Unknown pinMap key: {key}."
132
+ )
133
+ sym_pin = _need_nonempty_str(entry.get("symPin"), "symPin")
134
+ cell_pin = _need_nonempty_str(entry.get("cellPin"), "cellPin")
135
+ if sym_pin in seen:
136
+ raise ContractError(
137
+ "invalid_celllink",
138
+ f"symPin {sym_pin!r} names more than one target.",
139
+ )
140
+ seen.add(sym_pin)
141
+ clean_pins.append({"symPin": sym_pin, "cellPin": cell_pin})
142
+ filters = link.get("cellFilters", [])
143
+ if not isinstance(filters, list):
144
+ raise ContractError("invalid_celllink", "cellFilters must be a list.")
145
+ for pattern in filters:
146
+ _need_str(pattern, "cellFilters entry")
147
+ overrides = link.get("variantOverrides", {})
148
+ if not isinstance(overrides, dict):
149
+ raise ContractError("invalid_celllink", "variantOverrides must be a mapping.")
150
+ clean_overrides = {}
151
+ for name, override in overrides.items():
152
+ _need_nonempty_str(name, "variant name")
153
+ if name == REFERENCE:
154
+ raise ContractError(
155
+ "invalid_celllink",
156
+ f"Variant {REFERENCE!r} is reserved for the stored base link.",
157
+ )
158
+ if not isinstance(override, dict):
159
+ raise ContractError(
160
+ "invalid_celllink", f"Variant {name!r} must be a mapping."
161
+ )
162
+ for key in override:
163
+ if key not in _OVERRIDE_KEYS:
164
+ raise ContractError(
165
+ "invalid_celllink", f"Unknown variant key: {key}."
166
+ )
167
+ clean = {}
168
+ if "cell" in override:
169
+ clean["cell"] = _need_nonempty_str(
170
+ override["cell"], f"variant {name!r} cell"
171
+ )
172
+ pin_edits = override.get("pinEdits", {})
173
+ if not isinstance(pin_edits, dict):
174
+ raise ContractError(
175
+ "invalid_celllink", f"Variant {name!r} pinEdits must be a mapping."
176
+ )
177
+ clean_edits = {}
178
+ for sym_pin, cell_pin in pin_edits.items():
179
+ _need_nonempty_str(sym_pin, f"variant {name!r} symPin")
180
+ _need_nonempty_str(cell_pin, f"variant {name!r} cellPin")
181
+ clean_edits[sym_pin] = cell_pin
182
+ clean["pinEdits"] = clean_edits
183
+ clean_overrides[name] = clean
184
+ return {
185
+ "cell": cell,
186
+ "pinMap": clean_pins,
187
+ "cellFilters": list(filters),
188
+ "variantOverrides": clean_overrides,
189
+ }
190
+
191
+
192
+ def _block_link(block):
193
+ if not isinstance(block, dict):
194
+ raise ContractError(
195
+ "invalid_celllink", f"Block must be a mapping, got {block!r}."
196
+ )
197
+ link = block.get("CellLink", {})
198
+ return validate_celllink(link)
199
+
200
+
201
+ def _override_for(link, variant):
202
+ if variant is None or variant == REFERENCE:
203
+ return None
204
+ if not isinstance(variant, str) or not variant:
205
+ raise ContractError(
206
+ "unknown_variant", f"Variant must be a non-empty string, got {variant!r}."
207
+ )
208
+ try:
209
+ return link["variantOverrides"][variant]
210
+ except KeyError:
211
+ known = sorted(link["variantOverrides"])
212
+ raise ContractError(
213
+ "unknown_variant",
214
+ f"Unknown variant {variant!r}; known variants: {known}.",
215
+ ) from None
216
+
217
+
218
+ def _effective_pin_map(link, override):
219
+ effective = {entry["symPin"]: entry["cellPin"] for entry in link["pinMap"]}
220
+ if override is not None:
221
+ effective.update(override["pinEdits"])
222
+ ordered = []
223
+ for entry in link["pinMap"]:
224
+ ordered.append(
225
+ {"symPin": entry["symPin"], "cellPin": effective[entry["symPin"]]}
226
+ )
227
+ base_pins = {entry["symPin"] for entry in link["pinMap"]}
228
+ for sym_pin in sorted(effective):
229
+ if sym_pin not in base_pins:
230
+ ordered.append({"symPin": sym_pin, "cellPin": effective[sym_pin]})
231
+ return ordered
232
+
233
+
234
+ def resolve_pin(block, sym_pin, variant=None):
235
+ """Resolve one symbol pin to its cell pin (the pin chokepoint).
236
+
237
+ Explicit pinMap entries win, variant pinEdits overlay them,
238
+ and unlisted pins resolve to themselves (identity default).
239
+ A block without a CellLink resolves every pin to itself.
240
+
241
+ Args:
242
+ block: Block mapping carrying the CellLink component.
243
+ sym_pin: Symbol pin to resolve.
244
+ variant: Variant name selecting its overrides; None and
245
+ REFERENCE resolve the stored base link.
246
+
247
+ Returns:
248
+ Resolved cell-pin name.
249
+
250
+ Raises:
251
+ ContractError: invalid_celllink for a malformed block or
252
+ link, unknown_variant for a variant the link does not
253
+ define.
254
+ """
255
+ _need_nonempty_str(sym_pin, "sym_pin")
256
+ link = _block_link(block)
257
+ override = _override_for(link, variant)
258
+ if override is not None and sym_pin in override["pinEdits"]:
259
+ return override["pinEdits"][sym_pin]
260
+ for entry in link["pinMap"]:
261
+ if entry["symPin"] == sym_pin:
262
+ return entry["cellPin"]
263
+ return sym_pin
264
+
265
+
266
+ def resolve_cell(block, variant=None):
267
+ """Resolve the cell implementing a block under a variant.
268
+
269
+ Args:
270
+ block: Block mapping carrying the CellLink component.
271
+ variant: Variant name selecting its overrides; None and
272
+ REFERENCE resolve the stored base link.
273
+
274
+ Returns:
275
+ Effective cell text ("" when the block is unlinked).
276
+
277
+ Raises:
278
+ ContractError: invalid_celllink for a malformed block or
279
+ link, unknown_variant for a variant the link does not
280
+ define.
281
+ """
282
+ link = _block_link(block)
283
+ override = _override_for(link, variant)
284
+ if override is not None and "cell" in override:
285
+ return override["cell"]
286
+ return link["cell"]
287
+
288
+
289
+ def resolve_link(block, variant=None):
290
+ """Resolve the full (cell, pinMap, variant) link for a block.
291
+
292
+ The variant selector echoes through verbatim so run/export
293
+ records cite which resolution they carry. Resolution copies:
294
+ the stored link is never mutated.
295
+
296
+ Args:
297
+ block: Block mapping carrying the CellLink component.
298
+ variant: Variant name selecting its overrides; None and
299
+ REFERENCE resolve the stored base link.
300
+
301
+ Returns:
302
+ Mapping with the effective cell, the ordered effective
303
+ pinMap (base order kept, edits applied in place, added
304
+ pins appended sorted), and the variant selector.
305
+
306
+ Raises:
307
+ ContractError: invalid_celllink for a malformed block or
308
+ link, unknown_variant for a variant the link does not
309
+ define.
310
+ """
311
+ link = _block_link(block)
312
+ override = _override_for(link, variant)
313
+ cell = link["cell"]
314
+ if override is not None and "cell" in override:
315
+ cell = override["cell"]
316
+ return {
317
+ "cell": cell,
318
+ "pinMap": _effective_pin_map(link, override),
319
+ "variant": variant,
320
+ }
321
+
322
+
323
+ def _wildcard_to_regex(pattern):
324
+ parts = []
325
+ for char in pattern:
326
+ if char == "*":
327
+ parts.append(".*")
328
+ elif char == "?":
329
+ parts.append(".")
330
+ else:
331
+ parts.append(re.escape(char))
332
+ return "".join(parts)
333
+
334
+
335
+ def _match_part(pattern, text):
336
+ return (
337
+ re.fullmatch(_wildcard_to_regex(pattern), text, flags=re.IGNORECASE)
338
+ is not None
339
+ )
340
+
341
+
342
+ def match_cell_filters(cell, filters):
343
+ """Match a cell against advisory KiCad chooser filters.
344
+
345
+ Empty filters match everything; otherwise entries combine
346
+ with OR. Matching is case-insensitive and anchored (a bare
347
+ word is not a substring — say ``*word*``). ``:`` splits the
348
+ library from the cell: a pattern without a library part
349
+ matches the cell part only, while ``lib:cell`` requires both
350
+ halves to match. Spaces sanitize to ``_`` on both sides.
351
+
352
+ Args:
353
+ cell: Opaque cell text to test.
354
+ filters: Advisory glob list from the CellLink.
355
+
356
+ Returns:
357
+ True when the cell passes the chooser filters.
358
+
359
+ Raises:
360
+ ContractError: invalid_celllink for a non-string cell or
361
+ a malformed filter list.
362
+ """
363
+ _need_str(cell, "cell")
364
+ if not isinstance(filters, list):
365
+ raise ContractError("invalid_celllink", "cellFilters must be a list.")
366
+ if not filters:
367
+ return True
368
+ candidate = cell.replace(" ", "_")
369
+ cand_lib, _, cand_cell = candidate.partition(":")
370
+ if ":" not in candidate:
371
+ cand_lib, cand_cell = "", candidate
372
+ for pattern in filters:
373
+ _need_str(pattern, "cellFilters entry")
374
+ scrubbed = pattern.replace(" ", "_")
375
+ if ":" in scrubbed:
376
+ pat_lib, _, pat_cell = scrubbed.partition(":")
377
+ if _match_part(pat_lib, cand_lib) and _match_part(pat_cell, cand_cell):
378
+ return True
379
+ elif _match_part(scrubbed, cand_cell):
380
+ return True
381
+ return False
382
+
383
+
384
+ def collect_erc(link):
385
+ """Collect the ERC view of a link: cell text plus filters.
386
+
387
+ ERC checks read this; they never enforce it — collection
388
+ reports unknown cells and filters as-is instead of failing.
389
+
390
+ Args:
391
+ link: CellLink mapping to collect from.
392
+
393
+ Returns:
394
+ Mapping with the cell text and the advisory filters.
395
+
396
+ Raises:
397
+ ContractError: invalid_celllink for a malformed link.
398
+ """
399
+ clean = validate_celllink(link)
400
+ return {"cell": clean["cell"], "filters": clean["cellFilters"]}
401
+
402
+
403
+ def resolve_run(blocks, known_cells, variant=None):
404
+ """Resolve run/export records for block instances.
405
+
406
+ Resolves ``(cell, pinMap, variant)`` per block and emits only
407
+ the non-empty links: blocks without a CellLink, or with an
408
+ empty cell, are skipped. A resolved cell outside known_cells
409
+ fails the run with a named reason — never at edit time.
410
+
411
+ Args:
412
+ blocks: Block mappings, each with an id and an optional
413
+ CellLink component.
414
+ known_cells: Available cell names the run may target.
415
+ variant: Variant name applied to every block; None and
416
+ REFERENCE resolve each stored base link.
417
+
418
+ Returns:
419
+ Emission records in input order, each with block, cell,
420
+ pinMap, and variant keys.
421
+
422
+ Raises:
423
+ ContractError: invalid_block for a malformed block
424
+ entry, unknown_variant for a variant a link does not
425
+ define.
426
+ CellLinkError: unknown_cell naming the block and cell
427
+ when a resolved cell is not available to the run.
428
+ """
429
+ if not isinstance(blocks, (list, tuple)):
430
+ raise ContractError("invalid_block", "Blocks must be a list.")
431
+ available = set(known_cells)
432
+ records = []
433
+ for block in blocks:
434
+ if not isinstance(block, dict):
435
+ raise ContractError(
436
+ "invalid_block", f"Block must be a mapping, got {block!r}."
437
+ )
438
+ block_id = block.get("id")
439
+ if not isinstance(block_id, str) or not block_id:
440
+ raise ContractError(
441
+ "invalid_block", "Each block needs a non-empty string id."
442
+ )
443
+ resolved = resolve_link(block, variant=variant)
444
+ if not resolved["cell"]:
445
+ continue
446
+ if resolved["cell"] not in available:
447
+ raise CellLinkError(
448
+ "unknown_cell",
449
+ f"Block {block_id!r} links unknown cell "
450
+ f"{resolved['cell']!r} (variant {variant!r}); "
451
+ "link it to an available cell and rerun.",
452
+ )
453
+ records.append(
454
+ {
455
+ "block": block_id,
456
+ "cell": resolved["cell"],
457
+ "pinMap": resolved["pinMap"],
458
+ "variant": resolved["variant"],
459
+ }
460
+ )
461
+ return records
@@ -0,0 +1,92 @@
1
+ """Strict YAML 1.2 validation (I01). Gate every external byte here."""
2
+
3
+ import math
4
+
5
+ from ruamel.yaml import YAML
6
+
7
+ from .errors import ContractError
8
+
9
+ MAX_DOCUMENT_BYTES = 1_000_000
10
+ MAX_NESTING_DEPTH = 100
11
+
12
+ REQUIRED_TOP_LEVEL = ("schema", "kind", "id", "revision", "scope", "description")
13
+ ALLOWED_TOP_LEVEL = frozenset(
14
+ {
15
+ "schema", "kind", "id", "revision", "scope", "description",
16
+ "domains", "ports", "parameters", "models", "top", "pdks",
17
+ "constraints", "schematic", "layout", "symbol", "source",
18
+ "objects", "cross_sections", "files", "decks", "license",
19
+ "origin", "tags",
20
+ }
21
+ )
22
+
23
+
24
+ def _reject_composition_tricks(node):
25
+ """Anchors, aliases, and merge keys are forbidden (I01)."""
26
+ if type(node).__name__ == "AliasNode" or getattr(node, "anchor", None):
27
+ raise ContractError("invalid_yaml", "Anchors and aliases are not allowed.")
28
+ value = getattr(node, "value", None)
29
+ if isinstance(value, list) and value and isinstance(value[0], tuple):
30
+ for key, item in value:
31
+ if getattr(key, "value", None) == "<<":
32
+ raise ContractError("invalid_yaml", "Merge keys are not allowed.")
33
+ _reject_composition_tricks(key)
34
+ _reject_composition_tricks(item)
35
+ elif isinstance(value, list):
36
+ for item in value:
37
+ _reject_composition_tricks(item)
38
+
39
+
40
+ def _reject_nonfinite_and_depth(data, depth=0):
41
+ if depth > MAX_NESTING_DEPTH:
42
+ raise ContractError("invalid_yaml", "Document nesting exceeds the limit.")
43
+ if isinstance(data, float) and (math.isnan(data) or math.isinf(data)):
44
+ raise ContractError("invalid_yaml", "Non-finite scalars are not allowed.")
45
+ if isinstance(data, dict):
46
+ for item in data.values():
47
+ _reject_nonfinite_and_depth(item, depth + 1)
48
+ elif isinstance(data, list):
49
+ for item in data:
50
+ _reject_nonfinite_and_depth(item, depth + 1)
51
+
52
+
53
+ def validate_document(text):
54
+ """Parse and strictly validate one YAML record.
55
+
56
+ Rejects anchors, aliases, merge keys, non-finite scalars,
57
+ over-deep nesting, oversize bytes, missing required fields, and
58
+ unknown fields — gate every external byte here.
59
+
60
+ Args:
61
+ text: YAML record text to validate.
62
+
63
+ Returns:
64
+ Validated record mapping.
65
+
66
+ Raises:
67
+ ContractError: invalid_yaml for any rejection.
68
+ """
69
+ if not isinstance(text, str):
70
+ raise ContractError("invalid_yaml", "Document must be text.")
71
+ if len(text.encode("utf-8")) > MAX_DOCUMENT_BYTES:
72
+ raise ContractError("invalid_yaml", "Document exceeds the size limit.")
73
+ yaml = YAML(typ="safe")
74
+ try:
75
+ node = yaml.compose(text)
76
+ except Exception as exc:
77
+ raise ContractError("invalid_yaml", f"YAML composition failed: {exc}") from exc
78
+ _reject_composition_tricks(node)
79
+ try:
80
+ data = yaml.load(text)
81
+ except Exception as exc:
82
+ raise ContractError("invalid_yaml", f"YAML load failed: {exc}") from exc
83
+ if not isinstance(data, dict):
84
+ raise ContractError("invalid_yaml", "Top level must be a mapping.")
85
+ for key in REQUIRED_TOP_LEVEL:
86
+ if key not in data:
87
+ raise ContractError("invalid_yaml", f"Missing required field: {key}.")
88
+ for key in data:
89
+ if key not in ALLOWED_TOP_LEVEL:
90
+ raise ContractError("invalid_yaml", f"Unknown field: {key}.")
91
+ _reject_nonfinite_and_depth(data)
92
+ return data
@@ -0,0 +1,131 @@
1
+ """v0 envelope contracts: the typed shapes of the v0 wire.
2
+
3
+ SubmitCommand, SubmitAck, EngineEvent, FindingPage, Finding, and
4
+ ErrorPayload are the data contracts every gateway speaks today.
5
+ They live here (contracts, no I/O) so gateways, generators, and
6
+ tests share one definition instead of re-declaring the shapes.
7
+
8
+ V0_TO_V1_FIELDS maps each envelope's v0 field names to the v1
9
+ catalog names (contracts/generated/engine-v1.expanded.schema.json;
10
+ v1 design-frozen, runtime migration pending). None marks a field
11
+ v1 restructures rather than renames — the migration owns those,
12
+ never a silent repurpose. Identity entries document that the name
13
+ survives the migration.
14
+ """
15
+
16
+ from typing import Any, Dict, List, Optional, TypedDict
17
+
18
+
19
+ class SubmitCommand(TypedDict, total=False):
20
+ """One admitted call: who, what, with what, under which id."""
21
+
22
+ project_id: str
23
+ operation: str
24
+ arguments: Dict[str, Any]
25
+ request_id: str
26
+
27
+
28
+ class SubmitAck(TypedDict, total=False):
29
+ """Admission ack: the execution id plus where it admitted."""
30
+
31
+ run_id: str
32
+ snapshot_id: str
33
+ state: str
34
+ approval_state: str
35
+ approval_id: str
36
+ execution_id: str
37
+
38
+
39
+ class EngineEvent(TypedDict, total=False):
40
+ """One retained project event, seq-ordered by cursor."""
41
+
42
+ seq: int
43
+ type: str
44
+ actor: str
45
+ data: Dict[str, Any]
46
+ revision: str
47
+ event_cursor: int
48
+
49
+
50
+ class Finding(TypedDict, total=False):
51
+ """One judged violation or observation on a completed run."""
52
+
53
+ rule: str
54
+ severity: str
55
+ message: str
56
+ target: str
57
+
58
+
59
+ class FindingPage(TypedDict, total=False):
60
+ """One page of findings, errors first, with counts and cursor."""
61
+
62
+ total_counts: Dict[str, int]
63
+ findings: List[Finding]
64
+ next_cursor: Optional[str]
65
+ truncated: bool
66
+
67
+
68
+ class ErrorPayload(TypedDict, total=False):
69
+ """Wire error: stable code, message, itemized fields when named."""
70
+
71
+ code: str
72
+ message: str
73
+ fields: List[str]
74
+
75
+
76
+ #: v0 envelope -> {v0 field: v1 field or None when restructured}.
77
+ V0_TO_V1_FIELDS = {
78
+ "SubmitCommand": {
79
+ "project_id": "project_id",
80
+ "operation": "task",
81
+ "arguments": "options",
82
+ "request_id": "request_id",
83
+ },
84
+ "SubmitAck": {
85
+ "run_id": "run_id",
86
+ "snapshot_id": "snapshot",
87
+ "state": "state",
88
+ "approval_state": None,
89
+ "approval_id": None,
90
+ "execution_id": None,
91
+ },
92
+ "EngineEvent": {
93
+ "seq": None,
94
+ "type": None,
95
+ "actor": "actor",
96
+ "data": "data",
97
+ "revision": None,
98
+ "event_cursor": "event_cursor",
99
+ },
100
+ "Finding": {
101
+ "rule": "rule",
102
+ "severity": "severity",
103
+ "message": "message",
104
+ "target": "target",
105
+ },
106
+ "FindingPage": {
107
+ "total_counts": None,
108
+ "findings": "findings",
109
+ "next_cursor": "cursor",
110
+ "truncated": None,
111
+ },
112
+ "ErrorPayload": {
113
+ "code": "code",
114
+ "message": "message",
115
+ "fields": "details",
116
+ },
117
+ }
118
+
119
+
120
+ def v1_field(envelope, v0_field):
121
+ """v1 name for one v0 envelope field, None when restructured.
122
+
123
+ Args:
124
+ envelope: Envelope name (SubmitCommand, SubmitAck,
125
+ EngineEvent, Finding, FindingPage, ErrorPayload).
126
+ v0_field: v0 field name.
127
+
128
+ Returns:
129
+ v1 field name, or None where v1 restructures the field.
130
+ """
131
+ return V0_TO_V1_FIELDS[envelope][v0_field]
@@ -0,0 +1,16 @@
1
+ """Error type for contract violations. Carries a stable machine code."""
2
+
3
+
4
+ class ContractError(Exception):
5
+ """Contract violation with a stable machine code.
6
+
7
+ Args:
8
+ code: Stable machine-readable error code.
9
+ message: Human-readable detail.
10
+ fields: Itemized fields, when the error names any.
11
+ """
12
+
13
+ def __init__(self, code, message, fields=None):
14
+ super().__init__(message)
15
+ self.code = code
16
+ self.fields = fields or []