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,385 @@
1
+ """Durable revisions, persistence, and rewind snapshots."""
2
+
3
+ import os
4
+ import shutil
5
+ import uuid
6
+ from pathlib import Path
7
+
8
+ from ...interfaces.paths import join_contained
9
+ from ..errors import ProjectError
10
+
11
+ from .shared import (
12
+ _EVENTS_FILENAME,
13
+ _LEDGER_FILENAME,
14
+ _LINKED_CARVEOUT,
15
+ _SNAPSHOTS_DIRNAME,
16
+ _atomic_write_bytes,
17
+ _atomic_write_text,
18
+ _dump_yaml,
19
+ )
20
+
21
+ class _SnapshotMixin:
22
+ """Durable revisions and snapshots for ProjectService."""
23
+
24
+ def _read_ledger_file(self, path):
25
+ """Parse one durable file; missing reads as None, corrupt refuses."""
26
+ try:
27
+ text = Path(path).read_text(encoding="utf-8")
28
+ except FileNotFoundError:
29
+ return None
30
+ except OSError as exc:
31
+ raise ProjectError(
32
+ "invalid_ledger", f"Cannot read ledger file {path}: {exc}."
33
+ ) from exc
34
+ try:
35
+ data = self._yaml.load(text)
36
+ except Exception as exc:
37
+ raise ProjectError(
38
+ "invalid_ledger",
39
+ f"Cannot parse ledger file {path}: {exc}.",
40
+ ) from exc
41
+ if data is None:
42
+ raise ProjectError(
43
+ "invalid_ledger", f"Ledger file is empty: {path}."
44
+ )
45
+ return data
46
+
47
+ def _load(self):
48
+ """Refresh head + log from durable files (absent files read empty)."""
49
+ ledger = self._read_ledger_file(self._dir / _LEDGER_FILENAME)
50
+ if ledger is not None:
51
+ if not isinstance(ledger, dict):
52
+ raise ProjectError(
53
+ "invalid_ledger",
54
+ f"Ledger head must be a mapping: {self._dir}.",
55
+ )
56
+ revision = ledger.get("current_revision")
57
+ requests = ledger.get("requests")
58
+ if not isinstance(revision, str) or not revision:
59
+ raise ProjectError(
60
+ "invalid_ledger",
61
+ f"Ledger head carries no revision: {self._dir}.",
62
+ )
63
+ if not isinstance(requests, dict):
64
+ raise ProjectError(
65
+ "invalid_ledger",
66
+ f"Ledger head carries no request map: {self._dir}.",
67
+ )
68
+ for request_id, entry in requests.items():
69
+ if (
70
+ not isinstance(entry, dict)
71
+ or not isinstance(entry.get("command_text"), str)
72
+ or not isinstance(entry.get("ack"), dict)
73
+ ):
74
+ raise ProjectError(
75
+ "invalid_ledger",
76
+ f"Ledger request is malformed: {request_id}.",
77
+ )
78
+ owner = ledger.get("project_id")
79
+ if owner is not None and owner != self._project_id:
80
+ raise ProjectError(
81
+ "invalid_ledger",
82
+ f"Ledger belongs to {owner}, not {self._project_id}.",
83
+ )
84
+ self._current_revision = revision
85
+ self._requests = {
86
+ request_id: (entry["command_text"], entry["ack"])
87
+ for request_id, entry in requests.items()
88
+ }
89
+ events = self._read_ledger_file(self._dir / _EVENTS_FILENAME)
90
+ if events is not None:
91
+ if not isinstance(events, list):
92
+ raise ProjectError(
93
+ "invalid_ledger",
94
+ f"Event log must be a sequence: {self._dir}.",
95
+ )
96
+ for entry in events:
97
+ if (
98
+ not isinstance(entry, dict)
99
+ or not isinstance(entry.get("seq"), int)
100
+ or isinstance(entry.get("seq"), bool)
101
+ or not isinstance(entry.get("event"), str)
102
+ ):
103
+ raise ProjectError(
104
+ "invalid_ledger",
105
+ f"Event record is malformed in {self._dir}.",
106
+ )
107
+ self._events = events
108
+ self._trim()
109
+
110
+ def _persist(self):
111
+ """Rewrite both durable files; events first, then the head."""
112
+ try:
113
+ self._dir.mkdir(parents=True, exist_ok=True)
114
+ (self._dir / "runs").mkdir(parents=True, exist_ok=True)
115
+ except OSError as exc:
116
+ raise ProjectError(
117
+ "invalid_ledger",
118
+ f"Cannot create ledger dir {self._dir}: {exc}.",
119
+ ) from exc
120
+ try:
121
+ _atomic_write_text(
122
+ self._dir / _EVENTS_FILENAME, _dump_yaml(self._events)
123
+ )
124
+ _atomic_write_text(
125
+ self._dir / _LEDGER_FILENAME,
126
+ _dump_yaml(
127
+ {
128
+ "project_id": self._project_id,
129
+ "current_revision": self._current_revision,
130
+ "requests": {
131
+ request_id: {
132
+ "command_text": command_text,
133
+ "ack": ack,
134
+ }
135
+ for request_id, (command_text, ack)
136
+ in self._requests.items()
137
+ },
138
+ }
139
+ ),
140
+ )
141
+ except OSError as exc:
142
+ raise ProjectError(
143
+ "invalid_ledger",
144
+ f"Cannot persist ledger under {self._dir}: {exc}.",
145
+ ) from exc
146
+
147
+ def _refresh_durable(self):
148
+ if self._dir is not None:
149
+ self._load()
150
+
151
+ def _persist_after_mutation(self):
152
+ if self._dir is not None:
153
+ self._persist()
154
+
155
+ def _snapshot_exists(self, revision):
156
+ """True when a restorable snapshot exists for a revision."""
157
+ if self._dir is None:
158
+ return revision in self._snapshots
159
+ try:
160
+ path = join_contained(
161
+ self._dir, _SNAPSHOTS_DIRNAME, revision
162
+ )
163
+ except Exception:
164
+ return False
165
+ return path.is_dir()
166
+
167
+ def _capture_snapshot(self, revision):
168
+ """Store the current managed bytes under one revision id.
169
+
170
+ Overwrites any previous capture for that id; empty trees
171
+ leave an empty marker dir so the revision stays restorable.
172
+ The copy lands in a sibling temp dir and renames into
173
+ place, so a crash never leaves a partial snapshot behind.
174
+ """
175
+ if self._dir is None:
176
+ self._snapshots[revision] = dict(self._docs)
177
+ return
178
+ try:
179
+ dest = join_contained(
180
+ self._dir, _SNAPSHOTS_DIRNAME, revision
181
+ )
182
+ except Exception as exc:
183
+ raise ProjectError(
184
+ "invalid_ledger",
185
+ f"Cannot snapshot revision {revision}: {exc}.",
186
+ ) from exc
187
+ base = self._dir / _SNAPSHOTS_DIRNAME
188
+ try:
189
+ base.mkdir(parents=True, exist_ok=True)
190
+ except OSError as exc:
191
+ raise ProjectError(
192
+ "invalid_ledger",
193
+ f"Cannot snapshot revision {revision}: {exc}.",
194
+ ) from exc
195
+ for stale in base.glob(".cap.*"):
196
+ try:
197
+ if stale.is_symlink() or stale.is_file():
198
+ stale.unlink()
199
+ elif stale.is_dir():
200
+ shutil.rmtree(stale)
201
+ except OSError:
202
+ pass
203
+ tmp = base / f".cap.{uuid.uuid4().hex}.tmp"
204
+ try:
205
+ tmp.mkdir(parents=True, exist_ok=False)
206
+ except OSError as exc:
207
+ raise ProjectError(
208
+ "invalid_ledger",
209
+ f"Cannot snapshot revision {revision}: {exc}.",
210
+ ) from exc
211
+ try:
212
+ for rel, _size in self._iter_doc_files():
213
+ try:
214
+ data = self._doc_path(rel).read_bytes()
215
+ except OSError as exc:
216
+ raise ProjectError(
217
+ "invalid_ledger",
218
+ f"Cannot snapshot {rel}: {exc}.",
219
+ ) from exc
220
+ target = tmp / rel
221
+ try:
222
+ target.parent.mkdir(parents=True, exist_ok=True)
223
+ _atomic_write_bytes(target, data)
224
+ except OSError as exc:
225
+ raise ProjectError(
226
+ "invalid_ledger",
227
+ f"Cannot snapshot {rel}: {exc}.",
228
+ ) from exc
229
+ if dest.is_symlink() or dest.is_file():
230
+ dest.unlink()
231
+ elif dest.is_dir():
232
+ shutil.rmtree(dest)
233
+ dest.parent.mkdir(parents=True, exist_ok=True)
234
+ os.replace(tmp, dest)
235
+ except ProjectError:
236
+ try:
237
+ shutil.rmtree(tmp, ignore_errors=True)
238
+ except OSError:
239
+ pass
240
+ raise
241
+ except OSError as exc:
242
+ try:
243
+ shutil.rmtree(tmp, ignore_errors=True)
244
+ except OSError:
245
+ pass
246
+ raise ProjectError(
247
+ "invalid_ledger",
248
+ f"Cannot snapshot revision {revision}: {exc}.",
249
+ ) from exc
250
+
251
+ def _ensure_snapshot(self, revision):
252
+ if not self._snapshot_exists(revision):
253
+ self._capture_snapshot(revision)
254
+
255
+ def _load_snapshot(self, revision):
256
+ """Current snapshot bytes, or None when unrestorable."""
257
+ if self._dir is None:
258
+ snap = self._snapshots.get(revision)
259
+ return dict(snap) if snap is not None else None
260
+ try:
261
+ src_dir = join_contained(
262
+ self._dir, _SNAPSHOTS_DIRNAME, revision
263
+ )
264
+ except Exception:
265
+ return None
266
+ if not src_dir.is_dir():
267
+ return None
268
+ found = {}
269
+ for target in sorted(src_dir.rglob("*")):
270
+ if not target.is_file():
271
+ continue
272
+ try:
273
+ rel = target.relative_to(src_dir).as_posix()
274
+ except ValueError:
275
+ continue
276
+ try:
277
+ found[rel] = target.read_bytes()
278
+ except OSError as exc:
279
+ raise ProjectError(
280
+ "invalid_ledger",
281
+ f"Cannot read snapshot {revision}/{rel}: {exc}.",
282
+ ) from exc
283
+ return found
284
+
285
+ def _current_bytes(self):
286
+ """All managed bytes for rollback staging."""
287
+ found = {}
288
+ for rel, _size in self._iter_doc_files():
289
+ try:
290
+ found[rel] = self._doc_path(rel).read_bytes()
291
+ except OSError as exc:
292
+ raise ProjectError(
293
+ "invalid_ledger",
294
+ f"Cannot stage {rel}: {exc}.",
295
+ ) from exc
296
+ return found
297
+
298
+ def _clear_docs(self):
299
+ """Delete every managed file, then prune emptied doc dirs."""
300
+ for rel, _size in self._iter_doc_files():
301
+ target = self._doc_path(rel)
302
+ try:
303
+ target.unlink()
304
+ except FileNotFoundError:
305
+ pass
306
+ except OSError as exc:
307
+ raise ProjectError(
308
+ "invalid_ledger",
309
+ f"Cannot clear {rel}: {exc}.",
310
+ ) from exc
311
+ for target in sorted(self._dir.rglob("*"), reverse=True):
312
+ if not target.is_dir() or target == self._dir:
313
+ continue
314
+ try:
315
+ rel = target.relative_to(self._dir).as_posix()
316
+ except ValueError:
317
+ continue
318
+ if rel == "runs" or rel.startswith("runs/"):
319
+ continue
320
+ if rel == _SNAPSHOTS_DIRNAME or rel.startswith(
321
+ _SNAPSHOTS_DIRNAME + "/"
322
+ ):
323
+ continue
324
+ try:
325
+ target.rmdir()
326
+ except OSError:
327
+ pass
328
+
329
+ def _write_bytes_map(self, mapping):
330
+ for rel, data in mapping.items():
331
+ target = self._doc_path(rel)
332
+ try:
333
+ target.parent.mkdir(parents=True, exist_ok=True)
334
+ _atomic_write_bytes(target, data)
335
+ except OSError as exc:
336
+ raise ProjectError(
337
+ "invalid_ledger",
338
+ f"Cannot restore {rel}: {exc}.",
339
+ ) from exc
340
+
341
+ def _restore_snapshot_bytes(self, snapshot, pre):
342
+ """Replace managed bytes; best-effort rollback to pre."""
343
+ try:
344
+ self._clear_docs()
345
+ self._write_bytes_map(snapshot)
346
+ except ProjectError:
347
+ try:
348
+ self._clear_docs()
349
+ self._write_bytes_map(pre)
350
+ except Exception:
351
+ pass
352
+ raise
353
+
354
+ def _linked_external_files(self):
355
+ """Caller files outside `.rocket/`, sorted, never touched."""
356
+ if self._linked_root is None:
357
+ return []
358
+ root = self._linked_root
359
+ try:
360
+ if not root.is_dir():
361
+ return []
362
+ except OSError:
363
+ return []
364
+ found = []
365
+ try:
366
+ targets = sorted(root.rglob("*"))
367
+ except OSError:
368
+ return []
369
+ for target in targets:
370
+ try:
371
+ if not target.is_file():
372
+ continue
373
+ rel = target.relative_to(root).as_posix()
374
+ except (OSError, ValueError):
375
+ continue
376
+ if rel == _LINKED_CARVEOUT or rel.startswith(
377
+ _LINKED_CARVEOUT + "/"
378
+ ):
379
+ continue
380
+ try:
381
+ size = target.stat().st_size
382
+ except OSError:
383
+ continue
384
+ found.append({"path": rel, "bytes": size})
385
+ return sorted(found, key=lambda e: e["path"])