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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- 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.
|