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,407 @@
1
+ """Raw asset serving (API-GENERALIZE/I29): verbatim PDK bytes, hash-checked.
2
+
3
+ The engine serves PDK store bytes exactly as they sit on disk — a
4
+ single `.sym` as-is, metadata listings, bulk manifests — with no
5
+ conversion and no rendering; parsing and styling stay frontend-side.
6
+ Every serve carries hash-checked identity (sha256 over the raw bytes),
7
+ and every path goes through PATH-GUARD containment, so escapes and
8
+ missing files fail loudly instead of leaking neighboring bytes.
9
+
10
+ Transport note: `serve_asset` base64-encodes the verbatim bytes for
11
+ JSON duplex channels; the sha256 is computed over the raw bytes, so
12
+ decoding and re-hashing always reproduces the served identity.
13
+
14
+ PRD aliases: the PRD names `list_pdk_assets`/`get_pdk_asset` are
15
+ thin wrappers over `list_assets`/`serve_asset` with identical
16
+ payloads — the wire always carries the PDK's own identifiers, and
17
+ humans and agents may query by either spelling. The canonical
18
+ functions stay the transport names until the adapt lane catalogs
19
+ the aliases in the vocabulary and composition wiring (see
20
+ `PRD_ALIASES`); this module pins the alias behavior meanwhile.
21
+
22
+ Serving guards (MC-43): single-asset serves refuse over
23
+ `max_bytes` with `too_large` instead of encoding unbounded bulk,
24
+ and bulk listings page with `limit`/`cursor` following the v1
25
+ Page convention (`next_cursor`, `has_more`-style termination via
26
+ a null cursor).
27
+ """
28
+
29
+ import base64
30
+ import hashlib
31
+
32
+ from ruamel.yaml import YAML
33
+
34
+ try:
35
+ from .errors import ContractError
36
+ from .paths import join_contained, slugify
37
+ from .pdks import resolve_engine_root
38
+ except ImportError: # standalone verification before install
39
+ from rocket_engine.interfaces.errors import ContractError
40
+ from rocket_engine.interfaces.paths import join_contained, slugify
41
+ from rocket_engine.interfaces.pdks import resolve_engine_root
42
+
43
+ ASSET_VERSION = 1
44
+ _HASH_CHUNK_BYTES = 1024 * 1024
45
+
46
+ #: Largest single asset `serve_asset` will encode; larger files
47
+ #: refuse with `too_large` so a blind read never swallows bulk.
48
+ MAX_SERVE_BYTES = 4 * 1024 * 1024
49
+
50
+ #: Largest single `list_assets` page; larger limits refuse loudly.
51
+ MAX_LIST_LIMIT = 1000
52
+
53
+ #: PRD-name alias -> canonical transport name. The adapt lane
54
+ #: catalogs these in the vocabulary and composition wiring; until
55
+ #: then transports serve the canonical names and this table pins
56
+ #: the alias contract at the module layer.
57
+ PRD_ALIASES = {
58
+ "list_pdk_assets": "list_assets",
59
+ "get_pdk_asset": "serve_asset",
60
+ }
61
+
62
+ _yaml = YAML(typ="safe")
63
+
64
+
65
+ def _sha256_file(path):
66
+ digest = hashlib.sha256()
67
+ with open(path, "rb") as handle:
68
+ for chunk in iter(lambda: handle.read(_HASH_CHUNK_BYTES), b""):
69
+ digest.update(chunk)
70
+ return digest.hexdigest()
71
+
72
+
73
+ def _pdk_dir(engine_root, pdk_id, revision):
74
+ root = resolve_engine_root(engine_root)
75
+ try:
76
+ return join_contained(root, "pdks", slugify(pdk_id), revision)
77
+ except Exception as exc:
78
+ raise ContractError(
79
+ "invalid_reference", f"Unknown PDK: {pdk_id} {revision}."
80
+ ) from exc
81
+
82
+
83
+ def _display_id(pdk_dir, pdk_id):
84
+ manifest = pdk_dir / "manifest.yaml"
85
+ try:
86
+ text = manifest.read_text(encoding="utf-8")
87
+ except OSError:
88
+ return slugify(pdk_id)
89
+ try:
90
+ doc = _yaml.load(text)
91
+ except Exception:
92
+ return slugify(pdk_id)
93
+ if not isinstance(doc, dict):
94
+ return slugify(pdk_id)
95
+ return str(doc.get("display_id") or doc.get("id") or slugify(pdk_id))
96
+
97
+
98
+ def _canon_relpath(relpath):
99
+ if not isinstance(relpath, str):
100
+ raise ContractError(
101
+ "invalid_reference",
102
+ f"Asset path must be text, got {relpath!r}.",
103
+ )
104
+ unix = relpath.replace("\\", "/")
105
+ if unix == "" or unix.startswith("/") or unix.endswith("/"):
106
+ raise ContractError(
107
+ "invalid_reference", f"Bad asset path: {relpath!r}."
108
+ )
109
+ parts = [part for part in unix.split("/") if part not in ("", ".")]
110
+ if not parts or ".." in parts:
111
+ raise ContractError(
112
+ "invalid_reference", f"Asset path escapes: {relpath!r}."
113
+ )
114
+ return "/".join(parts)
115
+
116
+
117
+ def _scan_files(pdk_dir):
118
+ """Sorted (relpath, sha256, size) for regular files; symlinks named."""
119
+ files = []
120
+ symlinks = []
121
+ for target in sorted(pdk_dir.rglob("*")):
122
+ if not target.is_symlink() and not target.is_file():
123
+ continue
124
+ try:
125
+ rel = target.relative_to(pdk_dir).as_posix()
126
+ except ValueError:
127
+ continue
128
+ if target.is_symlink():
129
+ try:
130
+ resolves = target.resolve().is_relative_to(pdk_dir.resolve())
131
+ except OSError:
132
+ resolves = False
133
+ symlinks.append({"path": rel, "resolves": resolves})
134
+ continue
135
+ files.append(
136
+ {"path": rel, "sha256": _sha256_file(target),
137
+ "size": target.stat().st_size}
138
+ )
139
+ files.sort(key=lambda e: e["path"])
140
+ symlinks.sort(key=lambda e: e["path"])
141
+ return files, symlinks
142
+
143
+
144
+ def _check_max_bytes(max_bytes):
145
+ if max_bytes is None:
146
+ return None
147
+ if (
148
+ isinstance(max_bytes, bool)
149
+ or not isinstance(max_bytes, int)
150
+ or max_bytes <= 0
151
+ ):
152
+ raise ContractError(
153
+ "invalid_command",
154
+ f"max_bytes must be a positive int, got {max_bytes!r}.",
155
+ )
156
+ return max_bytes
157
+
158
+
159
+ def serve_asset(pdk_id, revision, path, engine_root=None,
160
+ max_bytes=MAX_SERVE_BYTES):
161
+ """Serve one PDK file's bytes verbatim with hash-checked identity.
162
+
163
+ Args:
164
+ pdk_id: PDK id (either display spelling resolves).
165
+ revision: PDK revision pin.
166
+ path: Store-relative file path (single `.sym` as-is, any
167
+ depth).
168
+ engine_root: Explicit store root; resolved when omitted.
169
+ max_bytes: Largest encodable size; larger files refuse
170
+ with too_large instead of encoding bulk. None opts
171
+ out (bulk reads belong to selective calls, never a
172
+ blind loop).
173
+
174
+ Returns:
175
+ Mapping with asset_version, pdk_id, display_id, revision,
176
+ canonical path, size, sha256 over the raw bytes, and
177
+ the verbatim bytes base64-encoded for JSON channels.
178
+
179
+ Raises:
180
+ ContractError: invalid_reference for an unknown PDK, a
181
+ missing file, a directory path, or an escaping path;
182
+ invalid_command for a malformed max_bytes; too_large
183
+ when the file exceeds max_bytes.
184
+ """
185
+ cap = _check_max_bytes(max_bytes)
186
+ pdk_dir = _pdk_dir(engine_root, pdk_id, revision)
187
+ if not pdk_dir.is_dir():
188
+ raise ContractError(
189
+ "invalid_reference", f"Unknown PDK: {pdk_id} {revision}."
190
+ )
191
+ canonical = _canon_relpath(path)
192
+ try:
193
+ target = join_contained(pdk_dir, *canonical.split("/"))
194
+ except Exception as exc:
195
+ raise ContractError(
196
+ "invalid_reference", f"Asset path escapes: {path!r}."
197
+ ) from exc
198
+ if not target.is_file() or target.is_symlink():
199
+ # Symlinks resolve through list_assets metadata, never here:
200
+ # serving follows no link the caller cannot see.
201
+ raise ContractError(
202
+ "invalid_reference", f"No such asset: {path!r}."
203
+ )
204
+ size = target.stat().st_size
205
+ if cap is not None and size > cap:
206
+ raise ContractError(
207
+ "too_large",
208
+ f"Asset {canonical!r} is {size} bytes, over "
209
+ f"max_bytes={cap}; narrow with list_assets prefix "
210
+ "filtering and serve selectively, never in a blind loop.",
211
+ )
212
+ raw = target.read_bytes()
213
+ return {
214
+ "asset_version": ASSET_VERSION,
215
+ "pdk_id": slugify(pdk_id),
216
+ "display_id": _display_id(pdk_dir, pdk_id),
217
+ "revision": revision,
218
+ "path": canonical,
219
+ "size": len(raw),
220
+ "sha256": hashlib.sha256(raw).hexdigest(),
221
+ "content_b64": base64.b64encode(raw).decode("ascii"),
222
+ }
223
+
224
+
225
+ def get_pdk_asset(pdk_id, revision, path, engine_root=None,
226
+ max_bytes=MAX_SERVE_BYTES):
227
+ """Serve one PDK file verbatim under its PRD name.
228
+
229
+ Alias for `serve_asset`: identical arguments, identical
230
+ payload — the wire always carries the PDK's own identifiers.
231
+
232
+ Args:
233
+ pdk_id: PDK id (either display spelling resolves).
234
+ revision: PDK revision pin.
235
+ path: Store-relative file path.
236
+ engine_root: Explicit store root; resolved when omitted.
237
+ max_bytes: Largest encodable size; None opts out.
238
+
239
+ Returns:
240
+ Serve mapping, exactly as `serve_asset` returns.
241
+
242
+ Raises:
243
+ ContractError: Whatever `serve_asset` raises.
244
+ """
245
+ return serve_asset(
246
+ pdk_id, revision, path, engine_root=engine_root,
247
+ max_bytes=max_bytes,
248
+ )
249
+
250
+
251
+ def _check_limit(limit):
252
+ if limit is None:
253
+ return None
254
+ if (
255
+ isinstance(limit, bool)
256
+ or not isinstance(limit, int)
257
+ or not 1 <= limit <= MAX_LIST_LIMIT
258
+ ):
259
+ raise ContractError(
260
+ "invalid_command",
261
+ f"limit must be an int 1..{MAX_LIST_LIMIT}, "
262
+ f"got {limit!r}.",
263
+ )
264
+ return limit
265
+
266
+
267
+ def _check_cursor(cursor, total):
268
+ if cursor is None:
269
+ return 0
270
+ if not isinstance(cursor, str) or not cursor.isdigit():
271
+ raise ContractError(
272
+ "invalid_command",
273
+ f"cursor must be an opaque page token from a prior "
274
+ f"listing, got {cursor!r}.",
275
+ )
276
+ start = int(cursor)
277
+ if start > total:
278
+ raise ContractError(
279
+ "invalid_command",
280
+ f"Unknown cursor {cursor!r} for {total} files.",
281
+ )
282
+ return start
283
+
284
+
285
+ def list_assets(pdk_id, revision, prefix=None, engine_root=None,
286
+ limit=None, cursor=None):
287
+ """List PDK file metadata with per-file hash identity.
288
+
289
+ Args:
290
+ pdk_id: PDK id (either display spelling resolves).
291
+ revision: PDK revision pin.
292
+ prefix: Optional store-relative prefix filter; everything
293
+ when omitted.
294
+ engine_root: Explicit store root; resolved when omitted.
295
+ limit: Maximum file entries in this page; the whole
296
+ filtered set when omitted.
297
+ cursor: Opaque page token from a prior listing; the
298
+ first page when omitted.
299
+
300
+ Returns:
301
+ Mapping with asset_version, pdk_id, display_id, revision,
302
+ sorted file entries for this page (path, size,
303
+ sha256), symlink entries (path, resolves, unpaged),
304
+ the total file count, and the paging echo (limit,
305
+ cursor, next_cursor; next_cursor is None on the last
306
+ page, following the v1 Page convention).
307
+
308
+ Raises:
309
+ ContractError: invalid_reference for an unknown PDK or an
310
+ escaping prefix; invalid_command for a malformed
311
+ limit or cursor.
312
+ """
313
+ page_limit = _check_limit(limit)
314
+ pdk_dir = _pdk_dir(engine_root, pdk_id, revision)
315
+ if not pdk_dir.is_dir():
316
+ raise ContractError(
317
+ "invalid_reference", f"Unknown PDK: {pdk_id} {revision}."
318
+ )
319
+ want = ""
320
+ if prefix:
321
+ want = _canon_relpath(prefix.rstrip("/") or prefix)
322
+ files, symlinks = _scan_files(pdk_dir)
323
+ if want:
324
+ files = [
325
+ entry for entry in files
326
+ if entry["path"] == want
327
+ or entry["path"].startswith(want + "/")
328
+ ]
329
+ symlinks = [
330
+ entry for entry in symlinks
331
+ if entry["path"] == want
332
+ or entry["path"].startswith(want + "/")
333
+ ]
334
+ start = _check_cursor(cursor, len(files))
335
+ if page_limit is None:
336
+ page = files
337
+ next_cursor = None
338
+ else:
339
+ page = files[start:start + page_limit]
340
+ ahead = start + len(page)
341
+ next_cursor = str(ahead) if ahead < len(files) else None
342
+ return {
343
+ "asset_version": ASSET_VERSION,
344
+ "pdk_id": slugify(pdk_id),
345
+ "display_id": _display_id(pdk_dir, pdk_id),
346
+ "revision": revision,
347
+ "files": page,
348
+ "symlinks": symlinks,
349
+ "count": len(files),
350
+ "limit": page_limit,
351
+ "cursor": cursor,
352
+ "next_cursor": next_cursor,
353
+ }
354
+
355
+
356
+ def list_pdk_assets(pdk_id, revision, prefix=None, engine_root=None,
357
+ limit=None, cursor=None):
358
+ """List PDK file metadata under its PRD name.
359
+
360
+ Alias for `list_assets`: identical arguments, identical
361
+ payload — the wire always carries the PDK's own identifiers.
362
+
363
+ Args:
364
+ pdk_id: PDK id (either display spelling resolves).
365
+ revision: PDK revision pin.
366
+ prefix: Optional store-relative prefix filter.
367
+ engine_root: Explicit store root; resolved when omitted.
368
+ limit: Maximum file entries in this page.
369
+ cursor: Opaque page token from a prior listing.
370
+
371
+ Returns:
372
+ Listing mapping, exactly as `list_assets` returns.
373
+
374
+ Raises:
375
+ ContractError: Whatever `list_assets` raises.
376
+ """
377
+ return list_assets(
378
+ pdk_id, revision, prefix=prefix, engine_root=engine_root,
379
+ limit=limit, cursor=cursor,
380
+ )
381
+
382
+
383
+ def asset_manifest(pdk_id, revision, prefix=None, engine_root=None):
384
+ """Build a bulk manifest with tree-level hash identity.
385
+
386
+ Args:
387
+ pdk_id: PDK id (either display spelling resolves).
388
+ revision: PDK revision pin.
389
+ prefix: Optional store-relative prefix filter; the whole
390
+ tree when omitted.
391
+ engine_root: Explicit store root; resolved when omitted.
392
+
393
+ Returns:
394
+ Listing mapping plus tree_sha256 over the sorted
395
+ `sha256:path` lines — one hash pinning the bulk bytes.
396
+
397
+ Raises:
398
+ ContractError: invalid_reference for an unknown PDK or an
399
+ escaping prefix.
400
+ """
401
+ listing = list_assets(
402
+ pdk_id, revision, prefix=prefix, engine_root=engine_root
403
+ )
404
+ digest = hashlib.sha256()
405
+ for entry in listing["files"]:
406
+ digest.update(f"{entry['sha256']}:{entry['path']}\n".encode("utf-8"))
407
+ return {**listing, "tree_sha256": digest.hexdigest()}