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,550 @@
|
|
|
1
|
+
"""PDK store: local-dir adoption into pdks/ + manifest + verify (PDK-STORE).
|
|
2
|
+
|
|
3
|
+
The engine owns its root; callers own their files. `register_pdk`
|
|
4
|
+
adopts a caller-owned directory into the sole shared store
|
|
5
|
+
`pdks/<id>/<revision>/` (copy + manifest + verify). Entries are
|
|
6
|
+
immutable once verified: re-register re-verifies instead of
|
|
7
|
+
duplicating. Every store path goes through PATH-GUARD containment
|
|
8
|
+
(`join_contained`) and slug helpers; every op validates the store
|
|
9
|
+
layout via LAYOUT-SCHEMA (`validate_tree`). Runs pin the I28
|
|
10
|
+
ingredient closure (id + revision + manifest hash) via
|
|
11
|
+
`pdk_ingredient`. MVP scope: local-dir adoption only.
|
|
12
|
+
|
|
13
|
+
Identity (PDK-DISPLAY): slug on disk, canonical display id in
|
|
14
|
+
records. The store directory and manifest `id` stay the lookup slug
|
|
15
|
+
(`sky130A` → `sky130a`); the manifest `display_id` records the
|
|
16
|
+
canonical id as passed at registration, and `register_pdk` /
|
|
17
|
+
`verify_pdk` / `list_pdks` / `pdk_ingredient` plus drift/conflict
|
|
18
|
+
messages report it. Lookup stays slug-based (either spelling
|
|
19
|
+
resolves); pre-display manifests without `display_id` load with the
|
|
20
|
+
slug as fallback. `display_id` is fixed at first registration:
|
|
21
|
+
re-register never rewrites the manifest.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import io
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import shutil
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from ruamel.yaml import YAML
|
|
33
|
+
|
|
34
|
+
from .errors import ContractError
|
|
35
|
+
from .layout import SCHEMA_VERSION, template_defaults, validate_tree
|
|
36
|
+
from .paths import join_contained, slugify, to_unix
|
|
37
|
+
|
|
38
|
+
MANIFEST_VERSION = 1
|
|
39
|
+
_MANIFEST_FILENAME = "manifest.yaml"
|
|
40
|
+
_REVISION_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
|
41
|
+
|
|
42
|
+
_HASH_CHUNK_BYTES = 1024 * 1024
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PdkError(ContractError):
|
|
46
|
+
"""PDK-store violation with a stable machine code.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
code: Stable machine-readable error code.
|
|
50
|
+
message: Human-readable detail.
|
|
51
|
+
fields: Itemized fields, when the error names any.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _checkout_root():
|
|
56
|
+
"""Engine checkout root, or None outside a source tree.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Checkout root when this file sits inside one (a
|
|
60
|
+
pyproject marker beside src/), else None (installed
|
|
61
|
+
wheels fall back to the legacy home default).
|
|
62
|
+
"""
|
|
63
|
+
root = Path(__file__).resolve().parents[2]
|
|
64
|
+
if (root / "pyproject.toml").is_file():
|
|
65
|
+
return root
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def default_engine_root():
|
|
70
|
+
"""Default engine store root (BUILD-LOCAL stage 2).
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
None: Reads no arguments; honors no env (callers layer
|
|
74
|
+
explicit roots, ROCKETEDA_HOME, and XDG over this).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Repo-local `.stores/rocketeda` under the checkout, or the
|
|
78
|
+
legacy `~/.local/share/rocketeda` outside a source tree.
|
|
79
|
+
"""
|
|
80
|
+
root = _checkout_root()
|
|
81
|
+
if root is not None:
|
|
82
|
+
return root / ".stores" / "rocketeda"
|
|
83
|
+
return Path.home() / ".local" / "share" / "rocketeda"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve_engine_root(explicit=None):
|
|
87
|
+
"""Resolve ENGINE_ROOT: explicit -> $ROCKETEDA_HOME -> XDG -> default.
|
|
88
|
+
|
|
89
|
+
The default is BUILD-LOCAL stage 2: the repo-local
|
|
90
|
+
`.stores/rocketeda` inside a checkout (explicit env still
|
|
91
|
+
wins; existing `~/.local/share/rocketeda` stores keep working
|
|
92
|
+
via `ROCKETEDA_HOME=~/.local/share/rocketeda`).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
explicit: Explicit root path; env/default when omitted.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Resolved engine root path.
|
|
99
|
+
"""
|
|
100
|
+
if explicit is not None:
|
|
101
|
+
return Path(explicit)
|
|
102
|
+
env_home = os.environ.get("ROCKETEDA_HOME")
|
|
103
|
+
if env_home:
|
|
104
|
+
return Path(env_home)
|
|
105
|
+
xdg_data = os.environ.get("XDG_DATA_HOME")
|
|
106
|
+
if xdg_data:
|
|
107
|
+
return Path(xdg_data) / "rocketeda"
|
|
108
|
+
return default_engine_root()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _dump_yaml(data):
|
|
112
|
+
buf = io.StringIO()
|
|
113
|
+
YAML(typ="safe").dump(data, buf)
|
|
114
|
+
return buf.getvalue()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def ensure_engine_root(root=None):
|
|
118
|
+
"""Create a missing engine-root skeleton, then validate it.
|
|
119
|
+
|
|
120
|
+
Idempotent: existing stamps, policy, and dirs are kept.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
root: Explicit root path; resolved when omitted.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Validated engine root path.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
LayoutError: Any tree-validation failure after creation.
|
|
130
|
+
"""
|
|
131
|
+
resolved = resolve_engine_root(root)
|
|
132
|
+
resolved.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
stamp = resolved / "layout.version"
|
|
134
|
+
if not stamp.exists():
|
|
135
|
+
stamp.write_text(_dump_yaml({"schema_version": SCHEMA_VERSION}))
|
|
136
|
+
policy = resolved / "policy.yaml"
|
|
137
|
+
if not policy.exists():
|
|
138
|
+
policy.write_text(_dump_yaml(template_defaults()))
|
|
139
|
+
for dirname in ("pdks", "cache", "projects"):
|
|
140
|
+
(resolved / dirname).mkdir(parents=True, exist_ok=True)
|
|
141
|
+
validate_tree(resolved, kind="engine_root")
|
|
142
|
+
return resolved
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _check_revision(revision):
|
|
146
|
+
if not isinstance(revision, str) or _REVISION_RE.match(revision) is None:
|
|
147
|
+
raise PdkError("invalid_pdk", f"Bad PDK revision: {revision!r}.")
|
|
148
|
+
return revision
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _pdk_dir(engine_root, pdk_id, revision):
|
|
152
|
+
slug = slugify(pdk_id)
|
|
153
|
+
_check_revision(revision)
|
|
154
|
+
return join_contained(engine_root, "pdks", slug, revision)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _sha256_file(path):
|
|
158
|
+
digest = hashlib.sha256()
|
|
159
|
+
with open(path, "rb") as handle:
|
|
160
|
+
while True:
|
|
161
|
+
chunk = handle.read(_HASH_CHUNK_BYTES)
|
|
162
|
+
if not chunk:
|
|
163
|
+
break
|
|
164
|
+
digest.update(chunk)
|
|
165
|
+
return digest.hexdigest()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _scan_tree(root):
|
|
169
|
+
"""Hash every regular file; record symlink targets. No following."""
|
|
170
|
+
root = Path(root)
|
|
171
|
+
files = {}
|
|
172
|
+
symlinks = {}
|
|
173
|
+
stack = [root]
|
|
174
|
+
while stack:
|
|
175
|
+
current = stack.pop()
|
|
176
|
+
with os.scandir(current) as iterator:
|
|
177
|
+
for entry in iterator:
|
|
178
|
+
rel = to_unix(str(Path(entry.path).relative_to(root)))
|
|
179
|
+
if rel == _MANIFEST_FILENAME:
|
|
180
|
+
continue
|
|
181
|
+
if entry.is_symlink():
|
|
182
|
+
symlinks[rel] = os.readlink(entry.path)
|
|
183
|
+
elif entry.is_dir(follow_symlinks=False):
|
|
184
|
+
stack.append(Path(entry.path))
|
|
185
|
+
elif entry.is_file(follow_symlinks=False):
|
|
186
|
+
files[rel] = _sha256_file(entry.path)
|
|
187
|
+
else:
|
|
188
|
+
raise PdkError(
|
|
189
|
+
"invalid_pdk",
|
|
190
|
+
f"Unsupported file type in PDK tree: {rel}.",
|
|
191
|
+
)
|
|
192
|
+
return files, symlinks
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _tree_hash(files, symlinks):
|
|
196
|
+
lines = [f"file {rel} {files[rel]}" for rel in files]
|
|
197
|
+
lines += [f"link {rel} {symlinks[rel]}" for rel in symlinks]
|
|
198
|
+
canonical = "".join(f"{line}\n" for line in sorted(lines))
|
|
199
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _load_manifest(path):
|
|
203
|
+
try:
|
|
204
|
+
data = YAML(typ="safe").load(Path(path).read_text())
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
raise PdkError(
|
|
207
|
+
"invalid_pdk", f"Cannot parse PDK manifest {path}: {exc}."
|
|
208
|
+
) from exc
|
|
209
|
+
if not isinstance(data, dict):
|
|
210
|
+
raise PdkError(
|
|
211
|
+
"invalid_pdk", f"PDK manifest must be a mapping: {path}."
|
|
212
|
+
)
|
|
213
|
+
for field in ("manifest_version", "id", "revision", "files", "tree_hash"):
|
|
214
|
+
if field not in data:
|
|
215
|
+
raise PdkError(
|
|
216
|
+
"invalid_pdk", f"PDK manifest misses field {field}: {path}."
|
|
217
|
+
)
|
|
218
|
+
if data["manifest_version"] != MANIFEST_VERSION:
|
|
219
|
+
raise PdkError(
|
|
220
|
+
"invalid_pdk",
|
|
221
|
+
f"PDK manifest version {data['manifest_version']} is newer than "
|
|
222
|
+
f"supported {MANIFEST_VERSION}; refusing to misread {path}.",
|
|
223
|
+
)
|
|
224
|
+
return data
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _display_of(manifest):
|
|
228
|
+
"""Canonical display id; pre-display manifests fall back to the slug."""
|
|
229
|
+
return manifest.get("display_id") or manifest["id"]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _manifest_record(slug, display_id, revision, dest, source, files, symlinks):
|
|
233
|
+
manifest = {
|
|
234
|
+
"manifest_version": MANIFEST_VERSION,
|
|
235
|
+
"id": slug,
|
|
236
|
+
"display_id": display_id,
|
|
237
|
+
"revision": revision,
|
|
238
|
+
"source": str(source),
|
|
239
|
+
"registered_at": datetime.now(timezone.utc).isoformat(),
|
|
240
|
+
"file_count": len(files),
|
|
241
|
+
"symlink_count": len(symlinks),
|
|
242
|
+
"tree_hash": _tree_hash(files, symlinks),
|
|
243
|
+
"files": {rel: files[rel] for rel in sorted(files)},
|
|
244
|
+
"symlinks": {rel: symlinks[rel] for rel in sorted(symlinks)},
|
|
245
|
+
}
|
|
246
|
+
manifest_path = dest / _MANIFEST_FILENAME
|
|
247
|
+
manifest_path.write_text(_dump_yaml(manifest))
|
|
248
|
+
manifest_hash = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
|
|
249
|
+
return manifest, manifest_hash
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _drift(pdk_id, revision, rel, detail):
|
|
253
|
+
raise PdkError(
|
|
254
|
+
"pdk_drift",
|
|
255
|
+
f"PDK drift in {pdk_id}@{revision}: {rel} {detail}.",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _verify_against(dest, manifest):
|
|
260
|
+
pdk_id = _display_of(manifest)
|
|
261
|
+
revision = manifest["revision"]
|
|
262
|
+
files, symlinks = _scan_tree(dest)
|
|
263
|
+
want_files = manifest.get("files") or {}
|
|
264
|
+
want_links = manifest.get("symlinks") or {}
|
|
265
|
+
for rel in sorted(set(files) | set(want_files)):
|
|
266
|
+
if rel not in want_files:
|
|
267
|
+
_drift(pdk_id, revision, rel, "is extra on disk (not in manifest)")
|
|
268
|
+
if rel not in files:
|
|
269
|
+
_drift(pdk_id, revision, rel, "is missing on disk")
|
|
270
|
+
if files[rel] != want_files[rel]:
|
|
271
|
+
_drift(pdk_id, revision, rel, "hash mismatch")
|
|
272
|
+
for rel in sorted(set(symlinks) | set(want_links)):
|
|
273
|
+
if rel not in want_links:
|
|
274
|
+
_drift(pdk_id, revision, rel, "is an extra symlink on disk")
|
|
275
|
+
if rel not in symlinks:
|
|
276
|
+
_drift(pdk_id, revision, rel, "symlink is missing on disk")
|
|
277
|
+
if symlinks[rel] != want_links[rel]:
|
|
278
|
+
_drift(pdk_id, revision, rel, "symlink target changed")
|
|
279
|
+
if _tree_hash(files, symlinks) != manifest["tree_hash"]:
|
|
280
|
+
raise PdkError(
|
|
281
|
+
"pdk_drift",
|
|
282
|
+
f"PDK drift in {pdk_id}@{revision}: tree hash mismatch.",
|
|
283
|
+
)
|
|
284
|
+
return files, symlinks
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def register_pdk(source_dir, pdk_id, revision, engine_root=None):
|
|
288
|
+
"""Adopt a local dir into pdks/ (copy + manifest + verify).
|
|
289
|
+
|
|
290
|
+
Re-register re-verifies the existing entry instead of duplicating.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
source_dir: Caller-owned directory to adopt (outside its
|
|
294
|
+
store entry).
|
|
295
|
+
pdk_id: Canonical display id (slug on disk).
|
|
296
|
+
revision: PDK revision pin.
|
|
297
|
+
engine_root: Explicit root; resolved when omitted.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Registration record (id, revision, path, hashes, counts,
|
|
301
|
+
verified, reverified).
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
PdkError: invalid_pdk for a bad source/revision/layout,
|
|
305
|
+
pdk_conflict when the entry is registered from another
|
|
306
|
+
source, pdk_drift when re-verify finds changes.
|
|
307
|
+
"""
|
|
308
|
+
root = ensure_engine_root(engine_root)
|
|
309
|
+
source = Path(source_dir)
|
|
310
|
+
if not source.is_dir():
|
|
311
|
+
raise PdkError(
|
|
312
|
+
"invalid_pdk", f"PDK source must be a directory: {source_dir}."
|
|
313
|
+
)
|
|
314
|
+
source_resolved = source.resolve()
|
|
315
|
+
slug = slugify(pdk_id)
|
|
316
|
+
_check_revision(revision)
|
|
317
|
+
dest = join_contained(root, "pdks", slug, revision)
|
|
318
|
+
if dest == source_resolved or source_resolved in dest.parents:
|
|
319
|
+
raise PdkError(
|
|
320
|
+
"invalid_pdk",
|
|
321
|
+
f"PDK source must be outside its store entry: {source_dir}.",
|
|
322
|
+
)
|
|
323
|
+
if dest in source_resolved.parents:
|
|
324
|
+
raise PdkError(
|
|
325
|
+
"invalid_pdk",
|
|
326
|
+
f"PDK store entry must be outside its source: {source_dir}.",
|
|
327
|
+
)
|
|
328
|
+
manifest_path = dest / _MANIFEST_FILENAME
|
|
329
|
+
if dest.exists():
|
|
330
|
+
if not manifest_path.is_file():
|
|
331
|
+
raise PdkError(
|
|
332
|
+
"invalid_pdk",
|
|
333
|
+
f"PDK entry without manifest (remove and retry): {dest}.",
|
|
334
|
+
)
|
|
335
|
+
manifest = _load_manifest(manifest_path)
|
|
336
|
+
if manifest.get("source") != str(source_resolved):
|
|
337
|
+
raise PdkError(
|
|
338
|
+
"pdk_conflict",
|
|
339
|
+
f"PDK {_display_of(manifest)}@{revision} is already registered "
|
|
340
|
+
f"from {manifest.get('source')}, not {source_resolved}.",
|
|
341
|
+
)
|
|
342
|
+
_verify_against(dest, manifest)
|
|
343
|
+
manifest_hash = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
|
|
344
|
+
return {
|
|
345
|
+
"id": _display_of(manifest),
|
|
346
|
+
"revision": revision,
|
|
347
|
+
"path": str(dest),
|
|
348
|
+
"manifest_hash": manifest_hash,
|
|
349
|
+
"tree_hash": manifest["tree_hash"],
|
|
350
|
+
"file_count": manifest.get("file_count", len(manifest["files"])),
|
|
351
|
+
"verified": True,
|
|
352
|
+
"reverified": True,
|
|
353
|
+
}
|
|
354
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
shutil.copytree(source_resolved, dest, symlinks=True, copy_function=shutil.copy2)
|
|
356
|
+
files, symlinks = _scan_tree(dest)
|
|
357
|
+
manifest, manifest_hash = _manifest_record(
|
|
358
|
+
slug, pdk_id, revision, dest, source_resolved, files, symlinks
|
|
359
|
+
)
|
|
360
|
+
_verify_against(dest, manifest)
|
|
361
|
+
return {
|
|
362
|
+
"id": pdk_id,
|
|
363
|
+
"revision": revision,
|
|
364
|
+
"path": str(dest),
|
|
365
|
+
"manifest_hash": manifest_hash,
|
|
366
|
+
"tree_hash": manifest["tree_hash"],
|
|
367
|
+
"file_count": len(files),
|
|
368
|
+
"verified": True,
|
|
369
|
+
"reverified": False,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def verify_pdk(pdk_id, revision, engine_root=None):
|
|
374
|
+
"""Re-hash a registered PDK; drift fails loudly naming the file.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
378
|
+
revision: PDK revision pin.
|
|
379
|
+
engine_root: Explicit root; resolved when omitted.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Verification record (ok, id, revision, path, hashes,
|
|
383
|
+
file_count).
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
PdkError: invalid_pdk for a bad revision, unknown_pdk for
|
|
387
|
+
a missing store entry, pdk_drift for any change.
|
|
388
|
+
"""
|
|
389
|
+
root = resolve_engine_root(engine_root)
|
|
390
|
+
validate_tree(root, kind="engine_root")
|
|
391
|
+
slug = slugify(pdk_id)
|
|
392
|
+
_check_revision(revision)
|
|
393
|
+
dest = _pdk_dir(root, slug, revision)
|
|
394
|
+
manifest_path = dest / _MANIFEST_FILENAME
|
|
395
|
+
if not dest.is_dir() or not manifest_path.is_file():
|
|
396
|
+
raise PdkError(
|
|
397
|
+
"unknown_pdk", f"Unknown PDK: {pdk_id}@{revision} (no store entry)."
|
|
398
|
+
)
|
|
399
|
+
manifest = _load_manifest(manifest_path)
|
|
400
|
+
_verify_against(dest, manifest)
|
|
401
|
+
return {
|
|
402
|
+
"ok": True,
|
|
403
|
+
"id": _display_of(manifest),
|
|
404
|
+
"revision": revision,
|
|
405
|
+
"path": str(dest),
|
|
406
|
+
"manifest_hash": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
|
|
407
|
+
"tree_hash": manifest["tree_hash"],
|
|
408
|
+
"file_count": manifest.get("file_count", len(manifest["files"])),
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def list_pdks(engine_root=None):
|
|
413
|
+
"""List registered PDKs in canonical (id, revision) order.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
engine_root: Explicit root; resolved when omitted.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Entry records (id, revision, path, hashes, counts,
|
|
420
|
+
registered_at), sorted by (id, revision).
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
PdkError: invalid_pdk for a store entry without a manifest.
|
|
424
|
+
"""
|
|
425
|
+
root = resolve_engine_root(engine_root)
|
|
426
|
+
validate_tree(root, kind="engine_root")
|
|
427
|
+
pdks_root = join_contained(root, "pdks")
|
|
428
|
+
entries = []
|
|
429
|
+
if not pdks_root.is_dir():
|
|
430
|
+
return entries
|
|
431
|
+
for id_dir in sorted(pdks_root.iterdir(), key=lambda p: p.name):
|
|
432
|
+
if id_dir.name.startswith(".") or not id_dir.is_dir():
|
|
433
|
+
continue
|
|
434
|
+
for rev_dir in sorted(id_dir.iterdir(), key=lambda p: p.name):
|
|
435
|
+
if rev_dir.name.startswith(".") or not rev_dir.is_dir():
|
|
436
|
+
continue
|
|
437
|
+
manifest_path = rev_dir / _MANIFEST_FILENAME
|
|
438
|
+
if not manifest_path.is_file():
|
|
439
|
+
raise PdkError(
|
|
440
|
+
"invalid_pdk",
|
|
441
|
+
f"PDK entry without manifest (remove and retry): {rev_dir}.",
|
|
442
|
+
)
|
|
443
|
+
manifest = _load_manifest(manifest_path)
|
|
444
|
+
entries.append({
|
|
445
|
+
"id": _display_of(manifest),
|
|
446
|
+
"revision": manifest["revision"],
|
|
447
|
+
"path": str(rev_dir.resolve()),
|
|
448
|
+
"manifest_hash": hashlib.sha256(
|
|
449
|
+
manifest_path.read_bytes()
|
|
450
|
+
).hexdigest(),
|
|
451
|
+
"tree_hash": manifest["tree_hash"],
|
|
452
|
+
"file_count": manifest.get("file_count", len(manifest["files"])),
|
|
453
|
+
"registered_at": manifest.get("registered_at"),
|
|
454
|
+
})
|
|
455
|
+
return sorted(entries, key=lambda e: (e["id"], e["revision"]))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def resolve_deck(pdk_id, revision, deck, engine_root=None):
|
|
459
|
+
"""Resolve a deck relpath to its manifest-hashed store path.
|
|
460
|
+
|
|
461
|
+
The file's current hash must match the manifest; drift, unknown
|
|
462
|
+
decks, and unknown id/rev all raise (never silent substitute).
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
466
|
+
revision: PDK revision pin.
|
|
467
|
+
deck: Deck relpath under the store entry.
|
|
468
|
+
engine_root: Explicit root; resolved when omitted.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Deck record with store path and current sha256.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
PdkError: invalid_pdk for a bad revision, unknown_pdk for
|
|
475
|
+
a missing entry or deck, pdk_drift for hash/symlink
|
|
476
|
+
changes or a missing file, path codes for an escaping
|
|
477
|
+
deck relpath.
|
|
478
|
+
"""
|
|
479
|
+
root = resolve_engine_root(engine_root)
|
|
480
|
+
validate_tree(root, kind="engine_root")
|
|
481
|
+
slug = slugify(pdk_id)
|
|
482
|
+
_check_revision(revision)
|
|
483
|
+
dest = _pdk_dir(root, slug, revision)
|
|
484
|
+
manifest_path = dest / _MANIFEST_FILENAME
|
|
485
|
+
if not dest.is_dir() or not manifest_path.is_file():
|
|
486
|
+
raise PdkError(
|
|
487
|
+
"unknown_pdk",
|
|
488
|
+
f"Unknown PDK: {pdk_id}@{revision} (no store entry).",
|
|
489
|
+
)
|
|
490
|
+
manifest = _load_manifest(manifest_path)
|
|
491
|
+
display = _display_of(manifest)
|
|
492
|
+
# Containment first (resolved), then manifest lookup on the LEXICAL
|
|
493
|
+
# rel: resolving before lookup would make every symlink deck
|
|
494
|
+
# misdiagnose as its target and let swapped link targets pass.
|
|
495
|
+
target = join_contained(dest, deck)
|
|
496
|
+
lexical = Path(dest, to_unix(deck))
|
|
497
|
+
rel = to_unix(str(lexical.relative_to(dest)))
|
|
498
|
+
want_files = manifest.get("files") or {}
|
|
499
|
+
want_links = manifest.get("symlinks") or {}
|
|
500
|
+
if rel in want_links:
|
|
501
|
+
if not lexical.is_symlink() or os.readlink(lexical) != want_links[rel]:
|
|
502
|
+
_drift(display, revision, rel, "symlink target changed")
|
|
503
|
+
try:
|
|
504
|
+
digest = _sha256_file(target)
|
|
505
|
+
except OSError:
|
|
506
|
+
_drift(display, revision, rel, "symlink target missing on disk")
|
|
507
|
+
return {"path": str(lexical), "sha256": digest}
|
|
508
|
+
if rel not in want_files:
|
|
509
|
+
raise PdkError(
|
|
510
|
+
"unknown_pdk",
|
|
511
|
+
f"Unknown deck for {display}@{revision}: {rel} (not in manifest).",
|
|
512
|
+
)
|
|
513
|
+
if lexical.is_symlink():
|
|
514
|
+
_drift(display, revision, rel, "changed from file to symlink")
|
|
515
|
+
if not target.is_file():
|
|
516
|
+
_drift(display, revision, rel, "is missing on disk")
|
|
517
|
+
current_hash = _sha256_file(target)
|
|
518
|
+
if current_hash != want_files[rel]:
|
|
519
|
+
_drift(display, revision, rel, "hash mismatch")
|
|
520
|
+
return {"path": str(target), "sha256": current_hash}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def pdk_ingredient(pdk_id, revision, engine_root=None):
|
|
524
|
+
"""Return the I28 PDK ingredient closure for run records.
|
|
525
|
+
|
|
526
|
+
Verifies freshness first: a drifted store fails loudly instead
|
|
527
|
+
of recording a stale hash. `pdk_tree_hash` is the
|
|
528
|
+
content-addressed cross-machine identity (unlike
|
|
529
|
+
`pdk_manifest_hash`, it ignores `registered_at`/source).
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
533
|
+
revision: PDK revision pin.
|
|
534
|
+
engine_root: Explicit root; resolved when omitted.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Ingredient closure (pdk_id, pdk_revision,
|
|
538
|
+
pdk_manifest_hash, pdk_tree_hash).
|
|
539
|
+
|
|
540
|
+
Raises:
|
|
541
|
+
PdkError: unknown_pdk for a missing entry, pdk_drift when
|
|
542
|
+
the store changed since registration.
|
|
543
|
+
"""
|
|
544
|
+
result = verify_pdk(pdk_id, revision, engine_root=engine_root)
|
|
545
|
+
return {
|
|
546
|
+
"pdk_id": result["id"],
|
|
547
|
+
"pdk_revision": result["revision"],
|
|
548
|
+
"pdk_manifest_hash": result["manifest_hash"],
|
|
549
|
+
"pdk_tree_hash": result["tree_hash"],
|
|
550
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Outbound ports: contracts the engine depends on, adapters implement."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ToolAdapter(ABC):
|
|
7
|
+
"""One solver family behind four calls. No transport knowledge."""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def capabilities(self, target):
|
|
11
|
+
"""What this tool can do for the target (operations, limits).
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
target: Target the capabilities are reported for.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Capability mapping naming the served operations.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def preflight(self, job):
|
|
22
|
+
"""Refuse early when inputs, deck, or environment disqualify.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
job: Solver job mapping (operation plus tool inputs).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Checked job inputs the execute call runs with.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
AdapterError: Implementations raise stable codes
|
|
32
|
+
(unsupported_operation, missing_dependency,
|
|
33
|
+
invalid_reference, invalid_command) — never a
|
|
34
|
+
silent pass.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def execute(self, job):
|
|
39
|
+
"""Run the tool; return native artifacts and exit state.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
job: Solver job mapping (operation plus tool inputs).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Native output mapping with exit state and artifacts.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
AdapterError: Implementations raise stable codes
|
|
49
|
+
(runner_unavailable, missing_dependency,
|
|
50
|
+
invalid_reference) — never a faked result.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def parse(self, native_output):
|
|
55
|
+
"""Normalize native output into findings and measurements.
|
|
56
|
+
|
|
57
|
+
Violation convention: rule/severity/message plus FLAT location
|
|
58
|
+
keys (file/line/col/sim_time/bbox/bbox_um/area). run() nests
|
|
59
|
+
them into finding.location; nested location dicts are ignored.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
native_output: Native output mapping from execute.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Normalized result with total, violations, and any
|
|
66
|
+
measurements.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
AdapterError: Implementations raise runner_unavailable
|
|
70
|
+
when the output carries no parseable verdict —
|
|
71
|
+
unparseable never parses as clean.
|
|
72
|
+
"""
|