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,410 @@
1
+ """Folder-schema parser + validator (LAYOUT-SCHEMA).
2
+
3
+ Checks a tree against layout_schema.yaml, derives and verifies
4
+ .gitignore from tracked flags, and refuses newer-than-known version
5
+ stamps loudly instead of misreading. Policy files use the
6
+ zero-silent-defaults rule: template ships defaults, required entries
7
+ must be present and non-empty.
8
+ """
9
+
10
+ import copy
11
+ import re
12
+ from pathlib import Path
13
+
14
+ from ruamel.yaml import YAML
15
+
16
+ from .errors import ContractError
17
+
18
+ SCHEMA_VERSION = 1
19
+
20
+ _SCHEMA_PATH = Path(__file__).parent / "layout_schema.yaml"
21
+ _VERSION_FILENAME = "layout.version"
22
+ _GITIGNORE_HEADER = (
23
+ "# Engine exhaust (derived from layout_schema.yaml v1; do not edit by hand)."
24
+ )
25
+
26
+
27
+ class LayoutError(ContractError):
28
+ """Layout-schema violation with a stable machine code.
29
+
30
+ Args:
31
+ code: Stable machine-readable error code.
32
+ message: Human-readable detail.
33
+ fields: Itemized fields, when the error names any.
34
+ """
35
+
36
+
37
+ def _yaml():
38
+ return YAML(typ="safe")
39
+
40
+
41
+ def _load_yaml_file(path):
42
+ try:
43
+ return _yaml().load(Path(path).read_text())
44
+ except LayoutError:
45
+ raise
46
+ except Exception as exc:
47
+ raise LayoutError(
48
+ "invalid_yaml", f"Cannot parse {path}: {exc}."
49
+ ) from exc
50
+
51
+
52
+ def _check_entry_struct(entry, where):
53
+ for field in (
54
+ "name", "type", "match", "required", "tracked",
55
+ "created_by", "mutable", "description",
56
+ ):
57
+ if field not in entry:
58
+ raise LayoutError(
59
+ "invalid_schema", f"Entry at {where} misses field: {field}."
60
+ )
61
+ if entry["type"] not in ("dir", "file"):
62
+ raise LayoutError(
63
+ "invalid_schema", f"Entry at {where} has bad type: {entry['type']!r}."
64
+ )
65
+ if entry["match"] not in ("exact", "pattern"):
66
+ raise LayoutError(
67
+ "invalid_schema", f"Entry at {where} has bad match: {entry['match']!r}."
68
+ )
69
+ if entry["match"] == "pattern" and not entry.get("pattern"):
70
+ raise LayoutError(
71
+ "invalid_schema", f"Pattern entry at {where} needs a pattern."
72
+ )
73
+ if entry["type"] == "dir":
74
+ for child in entry.get("children", []) or []:
75
+ _check_entry_struct(child, f"{where}/{entry['name']}")
76
+ else:
77
+ if not entry.get("format"):
78
+ raise LayoutError(
79
+ "invalid_schema", f"File entry at {where} needs a format."
80
+ )
81
+
82
+
83
+ def load_schema(path=None):
84
+ """Load and structurally validate the folder schema.
85
+
86
+ Args:
87
+ path: Schema YAML path; the bundled schema when omitted.
88
+
89
+ Returns:
90
+ Validated schema mapping.
91
+
92
+ Raises:
93
+ LayoutError: invalid_schema for an unloadable file, a wrong
94
+ schema_version, a missing section, a malformed entry,
95
+ or a bad policy_template.
96
+ """
97
+ schema_path = Path(path) if path is not None else _SCHEMA_PATH
98
+ try:
99
+ data = _yaml().load(schema_path.read_text())
100
+ except Exception as exc:
101
+ raise LayoutError(
102
+ "invalid_schema", f"Cannot load schema {schema_path}: {exc}."
103
+ ) from exc
104
+ if not isinstance(data, dict) or data.get("schema_version") != SCHEMA_VERSION:
105
+ raise LayoutError(
106
+ "invalid_schema",
107
+ f"Schema {schema_path} must carry schema_version {SCHEMA_VERSION}.",
108
+ )
109
+ for section in ("engine_root", "linked_project"):
110
+ entries = data.get(section)
111
+ if not isinstance(entries, list):
112
+ raise LayoutError(
113
+ "invalid_schema", f"Schema section missing: {section}."
114
+ )
115
+ for entry in entries:
116
+ _check_entry_struct(entry, section)
117
+ template = data.get("policy_template") or {}
118
+ if not isinstance(template.get("required"), list) or not isinstance(
119
+ template.get("defaults"), dict
120
+ ):
121
+ raise LayoutError("invalid_schema", "policy_template needs required + defaults.")
122
+ return data
123
+
124
+
125
+ def _is_empty(value):
126
+ if value is None:
127
+ return True
128
+ if isinstance(value, str):
129
+ return value.strip() == ""
130
+ if isinstance(value, (list, tuple, dict)):
131
+ return len(value) == 0
132
+ return False
133
+
134
+
135
+ def template_defaults(schema=None):
136
+ """Return a copy of the policy template defaults.
137
+
138
+ Args:
139
+ schema: Schema mapping; loaded when omitted.
140
+
141
+ Returns:
142
+ Deep copy of the policy template defaults.
143
+ """
144
+ data = schema if schema is not None else load_schema()
145
+ return copy.deepcopy(data["policy_template"]["defaults"])
146
+
147
+
148
+ def validate_policy(policy, schema=None):
149
+ """Enforce present + non-empty on every template-required key.
150
+
151
+ Zero-silent-defaults: required entries must be present and
152
+ non-empty; dict defaults recurse one level.
153
+
154
+ Args:
155
+ policy: Policy mapping to validate.
156
+ schema: Schema mapping; loaded when omitted.
157
+
158
+ Returns:
159
+ Ok mapping when every required key passes.
160
+
161
+ Raises:
162
+ LayoutError: invalid_policy for a non-mapping policy,
163
+ missing_policy/empty_policy naming the failed key.
164
+ """
165
+ data = schema if schema is not None else load_schema()
166
+ template = data["policy_template"]
167
+ if not isinstance(policy, dict):
168
+ raise LayoutError("invalid_policy", "Policy must be a mapping.")
169
+ defaults = template["defaults"]
170
+ for key in template["required"]:
171
+ if key not in policy:
172
+ raise LayoutError(
173
+ "missing_policy", f"Missing required policy entry: {key}.",
174
+ fields=[key],
175
+ )
176
+ if _is_empty(policy[key]):
177
+ raise LayoutError(
178
+ "empty_policy",
179
+ f"Required policy entry is empty: {key}.",
180
+ fields=[key],
181
+ )
182
+ default = defaults.get(key)
183
+ value = policy[key]
184
+ if isinstance(default, dict) and isinstance(value, dict):
185
+ for sub in default:
186
+ if sub not in value:
187
+ raise LayoutError(
188
+ "missing_policy",
189
+ f"Missing required policy entry: {key}.{sub}.",
190
+ fields=[f"{key}.{sub}"],
191
+ )
192
+ if _is_empty(value[sub]):
193
+ raise LayoutError(
194
+ "empty_policy",
195
+ f"Required policy entry is empty: {key}.{sub}.",
196
+ fields=[f"{key}.{sub}"],
197
+ )
198
+ return {"ok": True}
199
+
200
+
201
+ def _stamp_path(root, kind):
202
+ root = Path(root)
203
+ if kind == "engine_root":
204
+ return root / _VERSION_FILENAME
205
+ return root / ".rocket" / _VERSION_FILENAME
206
+
207
+
208
+ def _read_folder_version(stamp):
209
+ try:
210
+ text = stamp.read_text()
211
+ except FileNotFoundError:
212
+ raise LayoutError(
213
+ "missing_entry", f"Missing required entry: {stamp}."
214
+ ) from None
215
+ if text.strip() == "":
216
+ raise LayoutError(
217
+ "empty_entry", f"Required entry is empty: {stamp}."
218
+ )
219
+ try:
220
+ data = _yaml().load(text)
221
+ except Exception as exc:
222
+ raise LayoutError(
223
+ "invalid_version", f"Bad version stamp {stamp}: {exc}."
224
+ ) from exc
225
+ version = data.get("schema_version") if isinstance(data, dict) else None
226
+ if not isinstance(version, int) or isinstance(version, bool):
227
+ raise LayoutError(
228
+ "invalid_version",
229
+ f"Bad version stamp {stamp}: need schema_version int.",
230
+ )
231
+ return version
232
+
233
+
234
+ def _check_file(target, entry):
235
+ if target.is_dir():
236
+ raise LayoutError(
237
+ "type_mismatch", f"Entry must be a file, found dir: {target}."
238
+ )
239
+ if target.stat().st_size == 0:
240
+ raise LayoutError("empty_entry", f"Required entry is empty: {target}.")
241
+ if entry.get("format") == "policy":
242
+ data = _load_yaml_file(target)
243
+ if data is None:
244
+ raise LayoutError(
245
+ "empty_entry", f"Required entry is empty: {target}."
246
+ )
247
+ try:
248
+ validate_policy(data)
249
+ except LayoutError as exc:
250
+ raise LayoutError(
251
+ exc.code, f"{exc} ({target})", fields=exc.fields
252
+ ) from None
253
+
254
+
255
+ def _check_entries(base, entries):
256
+ for entry in entries:
257
+ if entry["match"] == "exact":
258
+ target = Path(base) / entry["name"]
259
+ if not target.exists():
260
+ if entry["required"]:
261
+ raise LayoutError(
262
+ "missing_entry", f"Missing required entry: {target}."
263
+ )
264
+ continue
265
+ if entry["type"] == "dir":
266
+ if not target.is_dir():
267
+ raise LayoutError(
268
+ "type_mismatch",
269
+ f"Entry must be a dir, found file: {target}.",
270
+ )
271
+ _check_entries(target, entry.get("children", []) or [])
272
+ else:
273
+ _check_file(target, entry)
274
+ else:
275
+ regex = re.compile(entry["pattern"])
276
+ names = []
277
+ if Path(base).is_dir():
278
+ names = sorted(
279
+ p.name for p in Path(base).iterdir() if regex.match(p.name)
280
+ )
281
+ if entry["required"] and not names:
282
+ raise LayoutError(
283
+ "missing_entry",
284
+ f"Missing required entry matching {entry['pattern']}: {base}.",
285
+ )
286
+ for name in names:
287
+ target = Path(base) / name
288
+ if entry["type"] == "dir":
289
+ if not target.is_dir():
290
+ raise LayoutError(
291
+ "type_mismatch",
292
+ f"Entry must be a dir, found file: {target}.",
293
+ )
294
+ _check_entries(target, entry.get("children", []) or [])
295
+ else:
296
+ _check_file(target, entry)
297
+
298
+
299
+ def validate_tree(root, kind="engine_root", schema=None):
300
+ """Check a tree against the schema; refuse newer stamps first.
301
+
302
+ Args:
303
+ root: Tree root to check.
304
+ kind: Tree kind (engine_root or linked_project).
305
+ schema: Schema mapping; loaded when omitted.
306
+
307
+ Returns:
308
+ Ok mapping with kind, supported schema_version, and folder
309
+ version.
310
+
311
+ Raises:
312
+ LayoutError: invalid_kind for an unknown kind,
313
+ unsupported_version for a newer-than-known stamp,
314
+ missing_entry/empty_entry/type_mismatch for failed
315
+ entries, policy codes for a bad policy file.
316
+ """
317
+ if kind not in ("engine_root", "linked_project"):
318
+ raise LayoutError("invalid_kind", f"Unknown tree kind: {kind}.")
319
+ data = schema if schema is not None else load_schema()
320
+ base = Path(root)
321
+ if not base.is_dir():
322
+ raise LayoutError(
323
+ "missing_entry", f"Root not found or not a dir: {root}."
324
+ )
325
+ stamp = _stamp_path(base, kind)
326
+ folder_version = _read_folder_version(stamp)
327
+ if folder_version > SCHEMA_VERSION:
328
+ raise LayoutError(
329
+ "unsupported_version",
330
+ f"Folder schema version {folder_version} is newer than "
331
+ f"supported {SCHEMA_VERSION}; upgrade the engine, refusing "
332
+ f"to misread {stamp}.",
333
+ )
334
+ _check_entries(base, data[kind])
335
+ return {
336
+ "ok": True,
337
+ "kind": kind,
338
+ "schema_version": SCHEMA_VERSION,
339
+ "folder_version": folder_version,
340
+ }
341
+
342
+
343
+ def _gitignore_patterns(entries, prefix=""):
344
+ patterns = []
345
+ for entry in entries:
346
+ segment = "*" if entry["match"] == "pattern" else entry["name"]
347
+ path = f"{prefix}{segment}"
348
+ if not entry["tracked"]:
349
+ patterns.append(f"{path}/" if entry["type"] == "dir" else path)
350
+ continue
351
+ if entry["type"] == "dir":
352
+ patterns.extend(
353
+ _gitignore_patterns(entry.get("children", []) or [], f"{path}/")
354
+ )
355
+ return patterns
356
+
357
+
358
+ def derive_gitignore(kind=None, schema=None):
359
+ """Derive canonical gitignore lines from tracked=False entries.
360
+
361
+ Args:
362
+ kind: Tree kind to derive for; both kinds when omitted.
363
+ schema: Schema mapping; loaded when omitted.
364
+
365
+ Returns:
366
+ Canonical gitignore text with the derived header.
367
+
368
+ Raises:
369
+ LayoutError: invalid_kind for an unknown kind.
370
+ """
371
+ data = schema if schema is not None else load_schema()
372
+ if kind is not None and kind not in ("engine_root", "linked_project"):
373
+ raise LayoutError("invalid_kind", f"Unknown tree kind: {kind}.")
374
+ sections = [kind] if kind is not None else ["engine_root", "linked_project"]
375
+ patterns = []
376
+ for section in sections:
377
+ patterns.extend(_gitignore_patterns(data[section]))
378
+ lines = [_GITIGNORE_HEADER, *sorted(set(patterns))]
379
+ return "\n".join(lines) + "\n"
380
+
381
+
382
+ def verify_gitignore(text, kind=None, schema=None):
383
+ """Verify a gitignore covers every derived pattern (extras allowed).
384
+
385
+ Args:
386
+ text: Gitignore text to verify.
387
+ kind: Tree kind to verify for; both kinds when omitted.
388
+ schema: Schema mapping; loaded when omitted.
389
+
390
+ Returns:
391
+ Ok mapping when every derived pattern is covered.
392
+
393
+ Raises:
394
+ LayoutError: invalid_kind for an unknown kind,
395
+ gitignore_mismatch naming the missing patterns.
396
+ """
397
+ expected = derive_gitignore(kind=kind, schema=schema)
398
+ want = [line for line in expected.splitlines() if line and not line.startswith("#")]
399
+ have = {
400
+ line.strip()
401
+ for line in (text or "").splitlines()
402
+ if line.strip() and not line.strip().startswith("#")
403
+ }
404
+ missing = [line for line in want if line not in have]
405
+ if missing:
406
+ raise LayoutError(
407
+ "gitignore_mismatch",
408
+ f"Gitignore misses derived patterns: {', '.join(missing)}.",
409
+ )
410
+ return {"ok": True}
@@ -0,0 +1,278 @@
1
+ # LAYOUT-SCHEMA v1: machine-readable ENGINE_ROOT + linked-project layout.
2
+ # Source of truth for dir/file names (ADR-001 consequences), validator
3
+ # input, and migration baseline. Normative prose: docs/storage-policy.md.
4
+ schema_version: 1
5
+ description: >-
6
+ Engine-root and linked-project folder schema. pdks/ is the sole shared
7
+ store; cache/ is disposable derivations (not a store); managed projects
8
+ live at projects/<id>/ with co-located runs/; linked projects keep
9
+ engine state only under .rocket/ (gitignored exhaust).
10
+ engine_root:
11
+ - name: layout.version
12
+ type: file
13
+ match: exact
14
+ required: true
15
+ tracked: true
16
+ created_by: engine
17
+ mutable: false
18
+ format: version
19
+ description: >-
20
+ Version stamp (YAML with schema_version int). Checked first;
21
+ newer-than-known refuses loudly, never misreads.
22
+ - name: policy.yaml
23
+ type: file
24
+ match: exact
25
+ required: true
26
+ tracked: true
27
+ created_by: engine
28
+ mutable: true
29
+ format: policy
30
+ description: >-
31
+ Global defaults (timeouts, threads, flags, deck/tool fallbacks,
32
+ seeds). KiCad-style base; project policy overrides beside runs.
33
+ - name: pdks
34
+ type: dir
35
+ match: exact
36
+ required: true
37
+ tracked: true
38
+ created_by: engine
39
+ mutable: false
40
+ description: >-
41
+ SOLE shared store: verified PDK copies + manifest. Immutable once
42
+ verified; re-fetch re-verifies instead of duplicating.
43
+ children:
44
+ - name: <id>
45
+ type: dir
46
+ match: pattern
47
+ pattern: ^[a-z0-9][a-z0-9_.-]*$
48
+ required: false
49
+ tracked: true
50
+ created_by: engine
51
+ mutable: false
52
+ description: One PDK id (e.g. sky130A lowercased by PATH-GUARD slug).
53
+ children:
54
+ - name: <revision>
55
+ type: dir
56
+ match: pattern
57
+ pattern: ^[A-Za-z0-9][A-Za-z0-9_.-]*$
58
+ required: false
59
+ tracked: true
60
+ created_by: engine
61
+ mutable: false
62
+ description: One PDK revision; manifest pins identity (I28).
63
+ children:
64
+ - name: manifest.yaml
65
+ type: file
66
+ match: exact
67
+ required: true
68
+ tracked: true
69
+ created_by: engine
70
+ mutable: false
71
+ format: manifest
72
+ description: >-
73
+ PDK manifest (hashes, source, date). Format owned by
74
+ PDK-STORE; presence + non-empty enforced here.
75
+ - name: cache
76
+ type: dir
77
+ match: exact
78
+ required: true
79
+ tracked: false
80
+ created_by: engine
81
+ mutable: true
82
+ children: []
83
+ description: >-
84
+ Disposable derivations only. Not a store: nothing trusted is read
85
+ from it; deleting it costs only rebuild time.
86
+ - name: projects
87
+ type: dir
88
+ match: exact
89
+ required: true
90
+ tracked: true
91
+ created_by: engine
92
+ mutable: true
93
+ description: Default home for engine-managed projects.
94
+ children:
95
+ - name: <id>
96
+ type: dir
97
+ match: pattern
98
+ pattern: ^[a-z0-9][a-z0-9_.-]*$
99
+ required: false
100
+ tracked: true
101
+ created_by: engine
102
+ mutable: true
103
+ description: One managed project; runs co-located under runs/.
104
+ children:
105
+ - name: policy.yaml
106
+ type: file
107
+ match: exact
108
+ required: false
109
+ tracked: true
110
+ created_by: engine
111
+ mutable: true
112
+ format: policy
113
+ description: >-
114
+ Optional project-level override beside runs (KiCad-style:
115
+ wins over global, loses to explicit run args).
116
+ - name: events.yaml
117
+ type: file
118
+ match: exact
119
+ required: false
120
+ tracked: true
121
+ created_by: engine
122
+ mutable: true
123
+ format: events
124
+ description: >-
125
+ Durable ordered event ledger (LEDGER-DURABLE/I30):
126
+ seq-ordered actor-attributed events (job_queued,
127
+ run_completed, run_failed). Absent until the first
128
+ engine-mediated mutation; trimmed oldest-first by the
129
+ retention bound, never edited by hand.
130
+ - name: ledger.yaml
131
+ type: file
132
+ match: exact
133
+ required: false
134
+ tracked: true
135
+ created_by: engine
136
+ mutable: true
137
+ format: ledger
138
+ description: >-
139
+ Ledger head (LEDGER-DURABLE/I30): current revision plus
140
+ admitted-request identity for idempotent replay and
141
+ run_job lookup. Absent until the first accepted
142
+ mutation; rewritten atomically on every mutation.
143
+ - name: snapshots
144
+ type: dir
145
+ match: exact
146
+ required: false
147
+ tracked: true
148
+ created_by: engine
149
+ mutable: true
150
+ description: >-
151
+ Restorable per-revision snapshots (REWIND): byte copies
152
+ of managed documents at each revision, restored by
153
+ rewind. Absent until the first snapshotted mutation;
154
+ never hand-edited.
155
+ children:
156
+ - name: <revision>
157
+ type: dir
158
+ match: pattern
159
+ pattern: ^[A-Za-z0-9][A-Za-z0-9_.-]*$
160
+ required: false
161
+ tracked: true
162
+ created_by: engine
163
+ mutable: false
164
+ children: []
165
+ description: >-
166
+ One revision snapshot with the managed bytes as
167
+ restored by rewind. Immutable once written.
168
+ - name: runs
169
+ type: dir
170
+ match: exact
171
+ required: true
172
+ tracked: false
173
+ created_by: engine
174
+ mutable: true
175
+ description: >-
176
+ Co-located run evidence (native outputs, findings,
177
+ verdicts). Gitignored exhaust; travels via CI artifacts.
178
+ children:
179
+ - name: <run_id>
180
+ type: dir
181
+ match: pattern
182
+ pattern: ^[A-Za-z0-9][A-Za-z0-9_.-]*$
183
+ required: false
184
+ tracked: false
185
+ created_by: engine
186
+ mutable: false
187
+ children: []
188
+ description: >-
189
+ One run record with I28 ingredient closure (project
190
+ revision, PDK id+rev+hash, deck, solver, seeds,
191
+ config). Immutable once written.
192
+ linked_project:
193
+ - name: .rocket
194
+ type: dir
195
+ match: exact
196
+ required: true
197
+ tracked: false
198
+ created_by: engine
199
+ mutable: true
200
+ description: >-
201
+ Single carve-out: the engine writes into a caller tree only here,
202
+ never beside sources. Gitignored exhaust. Caller sources outside
203
+ are untouched and unvalidated.
204
+ children:
205
+ - name: layout.version
206
+ type: file
207
+ match: exact
208
+ required: true
209
+ tracked: false
210
+ created_by: engine
211
+ mutable: false
212
+ format: version
213
+ description: Version stamp for the carve-out (same refuse-newer rule).
214
+ - name: policy.yaml
215
+ type: file
216
+ match: exact
217
+ required: false
218
+ tracked: false
219
+ created_by: engine
220
+ mutable: true
221
+ format: policy
222
+ description: Optional project-level override (local, gitignored).
223
+ - name: runs
224
+ type: dir
225
+ match: exact
226
+ required: true
227
+ tracked: false
228
+ created_by: engine
229
+ mutable: true
230
+ description: Co-located run evidence (same I28 closure as managed).
231
+ children:
232
+ - name: <run_id>
233
+ type: dir
234
+ match: pattern
235
+ pattern: ^[A-Za-z0-9][A-Za-z0-9_.-]*$
236
+ required: false
237
+ tracked: false
238
+ created_by: engine
239
+ mutable: false
240
+ children: []
241
+ description: One run record, immutable once written.
242
+ policy_template:
243
+ description: >-
244
+ Zero-silent-defaults: the template ships every default explicitly;
245
+ anything required must be present and non-empty somewhere in the
246
+ chain (run args > project policy > global policy) or startup fails
247
+ loudly. Per-tool audit (2026-10-29) below pins each default's source.
248
+ required:
249
+ - timeout_s
250
+ - preflight_timeout_s
251
+ - threads
252
+ - slang_flags
253
+ - tools
254
+ - decks
255
+ - seeds
256
+ defaults:
257
+ timeout_s: 120
258
+ preflight_timeout_s: 30
259
+ threads: 4
260
+ slang_flags:
261
+ - -Wextra
262
+ tools:
263
+ run_drc: magic
264
+ run_hdl_test: icarus
265
+ run_optical: sax
266
+ run_syntax: slang
267
+ decks:
268
+ run_drc: sky130A
269
+ seeds:
270
+ - 0
271
+ audit:
272
+ timeout_s: All five solver adapters (_TIMEOUT_S=120) plus BwrapRunner.run default (120).
273
+ preflight_timeout_s: BwrapRunner.preflight default (30).
274
+ threads: No existing knob; new canonical name (default 4) for solver/worker parallelism.
275
+ slang_flags: SyntaxAdapter _DEFAULT_FLAGS (["-Wextra"]); other solvers use fixed argv.
276
+ tools: Engine deck.tool fallbacks — run_drc first-registered magic (klayout alt), run_hdl_test icarus, run_optical sax, run_syntax slang.
277
+ decks: Template-owned (PDK-STORE resolves id+rev to manifest-hashed paths); sky130A is the MVP demo PDK.
278
+ seeds: No existing knob; new canonical name (engine-api seeds[]); [0] is the deterministic default.