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,195 @@
1
+ """Resolve registry blocks against the rocket-blocks layout.
2
+
3
+ The rocket-blocks repo holds one directory per PDK, each with
4
+ one entry folder per prebuilt IP block::
5
+
6
+ <root>/<pdk>/<entry>/rocket/meta.yaml
7
+
8
+ Resolution is per-PDK by construction: the PDK names the
9
+ top-level directory, so a block registered under ``sky130``
10
+ can only ever resolve inside ``<root>/sky130/``. Names that
11
+ try to leave that directory (separators, ``..``, absolute
12
+ paths) are refused before any filesystem touch, and entry
13
+ folder names must be catalogue slugs (lowercase-with-dashes).
14
+
15
+ The entry contract itself lives in ``entry.schema.json`` (a
16
+ byte-identical copy of
17
+ ``contracts/v1/schemas/ip-entry.schema.json``, which is the
18
+ single source). Every resolved entry is verified against that
19
+ schema at runtime: found-but-nonconforming entries fail loudly
20
+ instead of resolving.
21
+ """
22
+
23
+ import json
24
+ from pathlib import Path
25
+
26
+ import yaml
27
+ from jsonschema import Draft202012Validator
28
+ from jsonschema import FormatChecker
29
+
30
+ from ...interfaces.errors import ContractError
31
+
32
+ #: Entry metadata path under a block folder.
33
+ META_PARTS = ("rocket", "meta.yaml")
34
+
35
+ #: Catalogue entry folders are lowercase-with-dashes slugs.
36
+ _SLUG_RE = r"[a-z0-9]+(?:-[a-z0-9]+)*"
37
+
38
+ #: Root export id prefixing intra-bundle references.
39
+ _ROOT_ID = "urn:rocketeda:engine:v1"
40
+
41
+
42
+ def _need_name(value, what, code):
43
+ """Accept one clean, non-escaping segment or raise the code."""
44
+ if not isinstance(value, str) or not value:
45
+ raise ContractError(code, f"{what} must be non-empty text.")
46
+ if value in (".", "..") or "/" in value or "\\" in value:
47
+ raise ContractError(
48
+ code, f"{what} must be one path segment, got {value!r}."
49
+ )
50
+ if Path(value).is_absolute():
51
+ raise ContractError(
52
+ code, f"{what} must be relative, got {value!r}."
53
+ )
54
+ return value
55
+
56
+
57
+ def _load_entry_schema():
58
+ """Read the bundled entry contract (single source copy)."""
59
+ path = Path(__file__).with_name("entry.schema.json")
60
+ try:
61
+ return json.loads(path.read_text(encoding="utf-8"))
62
+ except OSError as exc:
63
+ raise ContractError(
64
+ "unknown_ip",
65
+ f"IP entry contract unreadable: {path.name}.",
66
+ ) from exc
67
+
68
+
69
+ def _inline_root_refs(node, defs):
70
+ """Inline intra-bundle urn refs so the module stands alone."""
71
+ if isinstance(node, dict):
72
+ ref = node.get("$ref")
73
+ if isinstance(ref, str) and ref.startswith(_ROOT_ID + "#/$defs/"):
74
+ name = ref.rsplit("/", 1)[1]
75
+ try:
76
+ target = defs[name]
77
+ except KeyError:
78
+ raise ContractError(
79
+ "unknown_ip",
80
+ f"IP entry contract references unknown {name}.",
81
+ ) from None
82
+ return _inline_root_refs(target, defs)
83
+ return {
84
+ key: _inline_root_refs(value, defs)
85
+ for key, value in node.items()
86
+ }
87
+ if isinstance(node, list):
88
+ return [_inline_root_refs(item, defs) for item in node]
89
+ return node
90
+
91
+
92
+ def _entry_validator():
93
+ """Build the runtime validator for IpEntry mappings."""
94
+ module = _load_entry_schema()
95
+ defs = module.get("$defs", {})
96
+ try:
97
+ entry_schema = _inline_root_refs(defs["IpEntry"], defs)
98
+ except KeyError:
99
+ raise ContractError(
100
+ "unknown_ip", "IP entry contract has no IpEntry."
101
+ ) from None
102
+ return Draft202012Validator(
103
+ entry_schema, format_checker=FormatChecker())
104
+
105
+
106
+ def _check_entry(meta_path, meta, name, version):
107
+ """Refuse found-but-nonconforming entries loudly."""
108
+ if not isinstance(meta, dict):
109
+ raise ContractError(
110
+ "invalid_ip",
111
+ f"IP block metadata malformed: {meta_path}.",
112
+ )
113
+ for error in _entry_validator().iter_errors(meta):
114
+ raise ContractError(
115
+ "invalid_ip",
116
+ f"IP block metadata invalid "
117
+ f"({'/'.join(str(part) for part in error.path) or 'entry'}): "
118
+ f"{error.message}.",
119
+ ) from None
120
+ if meta.get("name") != name:
121
+ raise ContractError(
122
+ "invalid_ip",
123
+ f"IP block metadata names {meta.get('name')!r}, "
124
+ f"addressed {name!r}.",
125
+ )
126
+ declared = meta.get("version")
127
+ if version is not None and declared != version:
128
+ raise ContractError(
129
+ "invalid_ip",
130
+ f"No such IP block version: {name}@{version}.",
131
+ )
132
+ return declared if declared is not None else version
133
+
134
+
135
+ def resolve_block_ref(blocks_root, pdk, name, version=None):
136
+ """Map a registry (pdk, name[, version]) to a blocks entry.
137
+
138
+ Args:
139
+ blocks_root: rocket-blocks checkout root directory.
140
+ pdk: Owning PDK id; selects the ``<root>/<pdk>/`` scope.
141
+ name: Catalogue slug naming the entry folder.
142
+ version: Revision label, or None to accept whatever the
143
+ entry metadata declares.
144
+
145
+ Returns:
146
+ Mapping with pdk, name, entry (directory text), version,
147
+ and meta (the parsed ``rocket/meta.yaml`` mapping).
148
+
149
+ Raises:
150
+ ContractError: invalid_reference for a segment that
151
+ would escape the PDK scope or breaks the catalogue
152
+ slug rule; unknown_ip when the entry or its metadata
153
+ is missing; invalid_ip when the entry is found but
154
+ nonconforming (schema violation, name/version
155
+ disagreement, or an unprovenanced wrapped checkout).
156
+ """
157
+ import re
158
+
159
+ clean_pdk = _need_name(pdk, "IP pdk", "invalid_reference")
160
+ clean_name = _need_name(name, "IP block name", "invalid_reference")
161
+ if not re.fullmatch(_SLUG_RE, clean_name):
162
+ raise ContractError(
163
+ "invalid_reference",
164
+ f"IP block name must be a catalogue slug "
165
+ f"(lowercase-with-dashes), got {clean_name!r}.",
166
+ )
167
+ entry = Path(blocks_root) / clean_pdk / clean_name
168
+ meta_path = entry.joinpath(*META_PARTS)
169
+ if not meta_path.is_file():
170
+ raise ContractError(
171
+ "unknown_ip",
172
+ f"No such IP block entry: {clean_pdk}/{clean_name}.",
173
+ )
174
+ try:
175
+ meta = yaml.safe_load(meta_path.read_text(encoding="utf-8"))
176
+ except OSError as exc:
177
+ raise ContractError(
178
+ "unknown_ip",
179
+ f"IP block entry unreadable: {clean_pdk}/{clean_name}.",
180
+ ) from exc
181
+ resolved_version = _check_entry(
182
+ f"{clean_pdk}/{clean_name}", meta, clean_name, version)
183
+ if (entry / "entry_repo").exists() and not meta.get("upstream"):
184
+ raise ContractError(
185
+ "invalid_ip",
186
+ f"IP block wraps a nested checkout without upstream "
187
+ f"provenance: {clean_pdk}/{clean_name}.",
188
+ )
189
+ return {
190
+ "pdk": clean_pdk,
191
+ "name": clean_name,
192
+ "entry": str(entry),
193
+ "version": resolved_version,
194
+ "meta": meta,
195
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:rocketeda:engine:v1:schemas:ip-entry",
4
+ "title": "Engine v1 IP catalogue entry contract",
5
+ "$comment": "Authoritative meta.yaml shape for rocket-blocks catalogue entries (<root>/<pdk>/<entry>/rocket/meta.yaml). This file is the single source: src/project/ip/blocks.py verifies every entry against this exact document at runtime. Filesystem-level rules the schema cannot see stay in resolve_block_ref: the entry must live inside its owning <root>/<pdk>/ scope, the folder name must be a lowercase-with-dashes slug, the meta name must agree with the folder, and an entry wrapping a nested upstream checkout (entry_repo/) must declare upstream provenance.",
6
+ "$defs": {
7
+ "IpEntryUpstream": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "url": {
12
+ "type": "string",
13
+ "format": "uri",
14
+ "minLength": 1
15
+ },
16
+ "rev": {
17
+ "type": "string",
18
+ "minLength": 1
19
+ },
20
+ "license": {
21
+ "type": "string",
22
+ "minLength": 1
23
+ }
24
+ },
25
+ "required": ["url", "rev"],
26
+ "description": "Provenance of a wrapped nested upstream checkout (entry_repo/): where it came from, which revision, under which license. Mandatory exactly when entry_repo/ is present."
27
+ },
28
+ "IpEntryPort": {
29
+ "type": "object",
30
+ "additionalProperties": true,
31
+ "properties": {
32
+ "name": {
33
+ "type": "string",
34
+ "minLength": 1
35
+ }
36
+ },
37
+ "required": ["name"],
38
+ "description": "One block port (symbol pin or cell terminal). Only the name is contracted today; domain nests standardize later without breaking this shape."
39
+ },
40
+ "IpEntry": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "properties": {
44
+ "name": {
45
+ "type": "string",
46
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
47
+ },
48
+ "version": {
49
+ "type": "string",
50
+ "minLength": 1,
51
+ "pattern": "^\\S+$"
52
+ },
53
+ "kind": {
54
+ "type": "string",
55
+ "minLength": 1
56
+ },
57
+ "ports": {
58
+ "type": "array",
59
+ "items": {
60
+ "$ref": "urn:rocketeda:engine:v1#/$defs/IpEntryPort"
61
+ }
62
+ },
63
+ "links": {
64
+ "type": "object"
65
+ },
66
+ "license": {
67
+ "type": "string",
68
+ "minLength": 1
69
+ },
70
+ "upstream": {
71
+ "$ref": "urn:rocketeda:engine:v1#/$defs/IpEntryUpstream"
72
+ }
73
+ },
74
+ "required": ["name", "version"],
75
+ "description": "Catalogue entry identity plus adapter metadata. Top level is closed (unknown keys fail loudly) so future fields arrive through deliberate schema evolution; port nests stay open until their standard lands. name must equal the entry folder slug; version pins the entry revision a registry block resolves to."
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,402 @@
1
+ """IP lifecycle: per-PDK registries with symbol/footprint linkage.
2
+
3
+ IP blocks are prebuilt HIERARCHICAL schematic blocks on the
4
+ KiCad-catalogue model (buck converters, digital chips): each
5
+ ships a schematic symbol AND a footprint cell LINKED, and
6
+ "instantiate" places a block with that linkage intact.
7
+
8
+ INVARIANT (RULING-01): prebuilt IP is PER-PDK — each PDK
9
+ strictly owns its blocks. Searches scope to one named PDK;
10
+ instantiating a foreign-PDK block fails unless the block opted
11
+ into scope ``shared`` with a recorded reason (digital blocks
12
+ MAY generalize case-by-case; the default stays per-PDK).
13
+ """
14
+
15
+ from ...interfaces.errors import ContractError
16
+
17
+ #: Component name registered IP blocks carry on their entity.
18
+ IP_COMPONENT = "IpBlock"
19
+
20
+ #: Component name placed IP instances carry beside Instance.
21
+ IP_LINK_COMPONENT = "IpLink"
22
+
23
+ #: Registry scopes: per-PDK by default, shared by recorded exception.
24
+ SCOPES = ("pdk", "shared")
25
+
26
+
27
+ def _need_text(value, what, code):
28
+ """Accept non-empty text or raise the named code."""
29
+ if not isinstance(value, str) or not value:
30
+ raise ContractError(code, f"{what} must be non-empty text.")
31
+ return value
32
+
33
+
34
+ def validate_ip_block(block):
35
+ """Validate an IpBlock component mapping at edit time.
36
+
37
+ Args:
38
+ block: Block mapping with pdk, name, version, symbol,
39
+ cell, pin_map, scope, reason, and promoted keys.
40
+
41
+ Returns:
42
+ Normalized IpBlock copy.
43
+
44
+ Raises:
45
+ ContractError: invalid_ip for any shape violation,
46
+ invalid_scope for a shared scope without a reason.
47
+ """
48
+ if not isinstance(block, dict):
49
+ raise ContractError(
50
+ "invalid_ip", f"IpBlock must be a mapping, got {block!r}."
51
+ )
52
+ for key in block:
53
+ if key not in ("pdk", "name", "version", "symbol", "cell",
54
+ "pin_map", "scope", "reason", "promoted"):
55
+ raise ContractError(
56
+ "invalid_ip", f"Unknown IpBlock key: {key}."
57
+ )
58
+ pdk = _need_text(block.get("pdk"), "IP pdk", "invalid_pdk")
59
+ name = _need_text(block.get("name"), "IP name", "invalid_ip")
60
+ version = _need_text(block.get("version"), "IP version", "invalid_ip")
61
+ symbol = _need_text(block.get("symbol"), "IP symbol", "invalid_ip")
62
+ cell = _need_text(block.get("cell"), "IP cell", "invalid_ip")
63
+ pin_map = block.get("pin_map", [])
64
+ if not isinstance(pin_map, list) or not all(
65
+ isinstance(entry, dict)
66
+ and set(entry) == {"symPin", "cellPin"}
67
+ and entry.get("symPin") and entry.get("cellPin")
68
+ for entry in pin_map
69
+ ):
70
+ raise ContractError(
71
+ "invalid_ip",
72
+ "IP pin_map must list {symPin, cellPin} entries.",
73
+ )
74
+ scope = block.get("scope", "pdk")
75
+ if scope not in SCOPES:
76
+ raise ContractError(
77
+ "invalid_scope", f"IP scope must be one of {SCOPES}."
78
+ )
79
+ reason = block.get("reason", "")
80
+ if not isinstance(reason, str):
81
+ raise ContractError(
82
+ "invalid_scope", "IP shared reason must be text."
83
+ )
84
+ if scope == "shared" and not reason:
85
+ raise ContractError(
86
+ "invalid_scope",
87
+ "Shared-scope IP needs a recorded reason naming why "
88
+ "this digital block generalizes across PDKs.",
89
+ )
90
+ promoted = block.get("promoted", False)
91
+ if not isinstance(promoted, bool):
92
+ raise ContractError(
93
+ "invalid_ip", "IP promoted must be a bool."
94
+ )
95
+ return {
96
+ "pdk": pdk, "name": name, "version": version,
97
+ "symbol": symbol, "cell": cell,
98
+ "pin_map": [dict(entry) for entry in pin_map],
99
+ "scope": scope, "reason": reason, "promoted": promoted,
100
+ }
101
+
102
+
103
+ def _all_blocks(store):
104
+ """List every registered block mapping, (pdk, name, version)-sorted."""
105
+ found = []
106
+ for entity in store.entities_with(IP_COMPONENT):
107
+ try:
108
+ raw = store.get_component(entity, IP_COMPONENT)
109
+ except LookupError:
110
+ continue
111
+ try:
112
+ value = validate_ip_block(raw)
113
+ except ContractError:
114
+ continue
115
+ value["id"] = entity
116
+ found.append(value)
117
+ found.sort(key=lambda item: (item["pdk"], item["name"],
118
+ item["version"]))
119
+ return found
120
+
121
+
122
+ def ip_register(store, actor, pdk, name, version, symbol, cell,
123
+ pin_map=None, scope="pdk", reason=""):
124
+ """Register one prebuilt IP block revision under its PDK.
125
+
126
+ Args:
127
+ store: Entity store serving the ENGINE-ECS seam.
128
+ actor: Attribution recorded on the revision and events.
129
+ pdk: Owning PDK id (the per-PDK invariant key).
130
+ name: Block name within the PDK registry.
131
+ version: Revision label ("" forbidden; latest = the
132
+ greatest label in sort order).
133
+ symbol: Linked schematic symbol reference.
134
+ cell: Linked footprint cell reference.
135
+ pin_map: Explicit [{symPin, cellPin}] entries.
136
+ scope: "pdk" (default) or "shared" with a reason.
137
+ reason: Recorded justification for shared scope.
138
+
139
+ Returns:
140
+ Receipt with op, actor, revision, ip id, and scope.
141
+
142
+ Raises:
143
+ ContractError: invalid_pdk for a missing PDK, ip_exists
144
+ for a duplicate (pdk, name, version), invalid_ip
145
+ for a malformed block, invalid_scope for shared
146
+ scope without a reason.
147
+ """
148
+ value = validate_ip_block({
149
+ "pdk": pdk, "name": name, "version": version,
150
+ "symbol": symbol, "cell": cell,
151
+ "pin_map": pin_map or [], "scope": scope,
152
+ "reason": reason, "promoted": False,
153
+ })
154
+ for other in _all_blocks(store):
155
+ if (
156
+ other["pdk"] == value["pdk"]
157
+ and other["name"] == value["name"]
158
+ and other["version"] == value["version"]
159
+ ):
160
+ raise ContractError(
161
+ "ip_exists",
162
+ f"IP already registered: {pdk}/{name}@{version}.",
163
+ )
164
+ made = store.create_entity(actor, f"ip_register {pdk}/{name}")
165
+ store.attach_component(
166
+ actor, made["id"], IP_COMPONENT, value,
167
+ f"ip_register {pdk}/{name}",
168
+ )
169
+ return {
170
+ "op": "ip_register", "actor": actor,
171
+ "revision": made["revision"], "ip": made["id"],
172
+ "pdk": pdk, "name": name, "version": version,
173
+ "scope": scope,
174
+ "events": [
175
+ {"event": "entity_created", "entity": made["id"]},
176
+ {"event": "component_changed", "entity": made["id"],
177
+ "component": IP_COMPONENT},
178
+ ],
179
+ }
180
+
181
+
182
+ def ip_search(store, pdk, name_filter=None):
183
+ """Search one PDK's registry (the per-PDK invariant).
184
+
185
+ Args:
186
+ store: Entity store serving the ENGINE-ECS seam.
187
+ pdk: Owning PDK id scoping the search (required —
188
+ cross-PDK listing is refused by the invariant).
189
+ name_filter: Optional substring the block name must hold.
190
+
191
+ Returns:
192
+ Block summaries (pdk, name, version, symbol, cell,
193
+ scope, promoted), sorted by (name, version).
194
+
195
+ Raises:
196
+ ContractError: invalid_pdk for a missing PDK.
197
+ """
198
+ _need_text(pdk, "IP pdk", "invalid_pdk")
199
+ hits = [
200
+ {
201
+ "id": item["id"], "pdk": item["pdk"], "name": item["name"],
202
+ "version": item["version"], "symbol": item["symbol"],
203
+ "cell": item["cell"], "scope": item["scope"],
204
+ "promoted": item["promoted"],
205
+ }
206
+ for item in _all_blocks(store)
207
+ if item["pdk"] == pdk
208
+ and (name_filter is None or name_filter in item["name"])
209
+ ]
210
+ hits.sort(key=lambda item: (item["name"], item["version"]))
211
+ return hits
212
+
213
+
214
+ def ip_get(store, pdk, name, version=None):
215
+ """Read one registered block; the latest version by default.
216
+
217
+ Args:
218
+ store: Entity store serving the ENGINE-ECS seam.
219
+ pdk: Owning PDK id scoping the lookup.
220
+ name: Block name within the PDK registry.
221
+ version: Revision label, or None for the greatest
222
+ label in sort order.
223
+
224
+ Returns:
225
+ Full block mapping with id.
226
+
227
+ Raises:
228
+ ContractError: invalid_pdk for a missing PDK,
229
+ unknown_ip when no such block (or version) exists
230
+ under the PDK.
231
+ """
232
+ _need_text(pdk, "IP pdk", "invalid_pdk")
233
+ candidates = [
234
+ item for item in _all_blocks(store)
235
+ if item["pdk"] == pdk and item["name"] == name
236
+ ]
237
+ if not candidates:
238
+ candidates = [
239
+ item for item in _all_blocks(store)
240
+ if item["name"] == name and item["scope"] == "shared"
241
+ and (version is None or item["version"] == version)
242
+ ]
243
+ candidates.sort(key=lambda item: (item["pdk"], item["version"]))
244
+ if version is not None:
245
+ for item in candidates:
246
+ if item["version"] == version:
247
+ return item
248
+ elif candidates:
249
+ return sorted(candidates,
250
+ key=lambda item: item["version"])[-1]
251
+ raise ContractError(
252
+ "unknown_ip",
253
+ f"No such IP block: {pdk}/{name}"
254
+ + (f"@{version}." if version is not None else "."),
255
+ )
256
+
257
+
258
+ def ip_instantiate(store, actor, pdk, name, sheet, x, y,
259
+ version=None, rotation=0, mirror=False,
260
+ properties=None):
261
+ """Place one IP block with its symbol/cell linkage intact.
262
+
263
+ Args:
264
+ store: Entity store serving the ENGINE-ECS seam.
265
+ actor: Attribution recorded on the revision and events.
266
+ pdk: Consuming PDK id (must own the block unless the
267
+ block opted into shared scope).
268
+ name: Block name within the PDK registry.
269
+ sheet: Sheet entity id hosting the instance.
270
+ x: X position in schematic units.
271
+ y: Y position in schematic units.
272
+ version: Revision label, or None for the latest.
273
+ rotation: Rotation in degrees (0/90/180/270).
274
+ mirror: Mirror flag.
275
+ properties: Text-to-text instance properties.
276
+
277
+ Returns:
278
+ Receipt with op, actor, revision, instance id, and
279
+ the resolved (pdk, name, version).
280
+
281
+ Raises:
282
+ ContractError: unknown_ip for a missing block,
283
+ pdk_mismatch when a per-PDK block is asked from a
284
+ foreign PDK, unknown_sheet for a missing sheet,
285
+ invalid_instance for a malformed placement.
286
+ """
287
+ block = ip_get(store, pdk, name, version)
288
+ if block["pdk"] != pdk and block["scope"] != "shared":
289
+ raise ContractError(
290
+ "pdk_mismatch",
291
+ f"IP {block['pdk']}/{name} is per-PDK and cannot "
292
+ f"instantiate under {pdk!r}.",
293
+ )
294
+ try:
295
+ store.get_component(sheet, "Sheet")
296
+ except LookupError:
297
+ raise ContractError(
298
+ "unknown_sheet", f"No such sheet entity: {sheet!r}."
299
+ ) from None
300
+ if rotation not in (0, 90, 180, 270):
301
+ raise ContractError(
302
+ "invalid_instance",
303
+ "Instance rotation must be one of (0, 90, 180, 270).",
304
+ )
305
+ if not isinstance(mirror, bool):
306
+ raise ContractError(
307
+ "invalid_instance", "Instance mirror must be a bool."
308
+ )
309
+ clean_props = properties or {}
310
+ if not isinstance(clean_props, dict) or not all(
311
+ isinstance(key, str) and isinstance(val, str)
312
+ for key, val in clean_props.items()
313
+ ):
314
+ raise ContractError(
315
+ "invalid_instance",
316
+ "Instance properties must map text to text.",
317
+ )
318
+ made = store.create_entity(actor, f"ip_instantiate {pdk}/{name}")
319
+ store.attach_component(
320
+ actor, made["id"], "Instance",
321
+ {"sheet": sheet, "symbol": block["symbol"], "x": x, "y": y,
322
+ "rotation": rotation, "mirror": mirror,
323
+ "properties": dict(clean_props), "ref": ""},
324
+ f"ip_instantiate {pdk}/{name}",
325
+ )
326
+ store.attach_component(
327
+ actor, made["id"], IP_LINK_COMPONENT,
328
+ {"pdk": block["pdk"], "name": name,
329
+ "version": block["version"], "symbol": block["symbol"],
330
+ "cell": block["cell"], "pin_map": block["pin_map"],
331
+ "scope": block["scope"]},
332
+ f"ip_instantiate {pdk}/{name}",
333
+ )
334
+ return {
335
+ "op": "ip_instantiate", "actor": actor,
336
+ "revision": made["revision"], "instance": made["id"],
337
+ "pdk": block["pdk"], "name": name,
338
+ "version": block["version"],
339
+ "events": [
340
+ {"event": "entity_created", "entity": made["id"]},
341
+ {"event": "component_changed", "entity": made["id"],
342
+ "component": "Instance"},
343
+ {"event": "component_changed", "entity": made["id"],
344
+ "component": IP_LINK_COMPONENT},
345
+ ],
346
+ }
347
+
348
+
349
+ def ip_promote(store, actor, pdk, name, version):
350
+ """Promote one block version as the PDK registry's recommended one.
351
+
352
+ Args:
353
+ store: Entity store serving the ENGINE-ECS seam.
354
+ actor: Attribution recorded on the revision and events.
355
+ pdk: Owning PDK id scoping the registry.
356
+ name: Block name within the PDK registry.
357
+ version: Revision label to promote.
358
+
359
+ Returns:
360
+ Receipt with op, actor, revision, ip id, and the
361
+ promoted flag.
362
+
363
+ Raises:
364
+ ContractError: unknown_ip when no such block version
365
+ exists under the PDK.
366
+ """
367
+ _need_text(pdk, "IP pdk", "invalid_pdk")
368
+ siblings = [
369
+ item for item in _all_blocks(store)
370
+ if item["pdk"] == pdk and item["name"] == name
371
+ ]
372
+ target = None
373
+ for item in siblings:
374
+ if item["version"] == version:
375
+ target = item
376
+ if target is None:
377
+ raise ContractError(
378
+ "unknown_ip",
379
+ f"No such IP block: {pdk}/{name}@{version}.",
380
+ )
381
+ head = None
382
+ for item in siblings:
383
+ want = item["id"] == target["id"]
384
+ if item["promoted"] != want:
385
+ clean = validate_ip_block({
386
+ "pdk": item["pdk"], "name": item["name"],
387
+ "version": item["version"], "symbol": item["symbol"],
388
+ "cell": item["cell"], "pin_map": item["pin_map"],
389
+ "scope": item["scope"], "reason": item["reason"],
390
+ "promoted": want,
391
+ })
392
+ changed = store.set_component(
393
+ actor, item["id"], IP_COMPONENT, clean,
394
+ f"ip_promote {pdk}/{name}@{version}",
395
+ )
396
+ head = changed["revision"]
397
+ return {
398
+ "op": "ip_promote", "actor": actor,
399
+ "revision": head, "ip": target["id"], "pdk": pdk,
400
+ "name": name, "version": version, "promoted": True,
401
+ "events": [],
402
+ }