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,552 @@
1
+ """Vivado-style setup surface: provision catalogue + capability reports.
2
+
3
+ The engine side of the setup menu (ENGINE-SETUP): `load_catalogue`
4
+ reads the shipped provision data (`setup_catalogue.yaml` — one
5
+ entry per provisionable unit with detection rules and recipe refs
6
+ into install.sh / provision-plan / owning tickets),
7
+ `catalogue_state` renders it with live install state for the
8
+ checkbox menu, and `report_capabilities` answers the
9
+ `get_capabilities` query (per-solver availability from the
10
+ adapters' own `capabilities()` plus tool resolution, PDK/deck
11
+ state via the store, cloud entries BYOC-gated per I32).
12
+
13
+ Readiness vocabulary is the engine-api appendix: `ready` (some
14
+ registered tool resolves — `deck.tool` selects it), `blocked`
15
+ (fixable: reason + missing items + remedy pointing at the
16
+ catalogue), `unsupported` (no solver registered — terminal for
17
+ this config). `unavailable`/`unknown` are defined but never
18
+ emitted here: transient runner states surface on run verdicts,
19
+ not capability reports.
20
+
21
+ Degradation posture: everything reports honestly; nothing
22
+ installs or phones home. Store reads never create the skeleton
23
+ (`resolve`, never `ensure`); a missing store reports empty with
24
+ the PDK remedy. Cloud entries are static gated rows — no key
25
+ lookup (CLOUD-AUTH is Backlog, so no keys can exist), no
26
+ imports, no network. PDK presence means a valid manifest; deck
27
+ rows verify per-deck hashes (tree-wide rehash stays a run-time
28
+ concern).
29
+ """
30
+
31
+ import os
32
+ import shutil
33
+ from pathlib import Path
34
+
35
+ from ruamel.yaml import YAML
36
+
37
+ from .errors import ContractError
38
+ from .paths import slugify
39
+ from .pdks import list_pdks, resolve_deck, resolve_engine_root
40
+ from .setup_probes import detect_python
41
+ from .vocabulary import OPERATIONS
42
+
43
+ CATALOGUE_VERSION = 1
44
+ _CATALOGUE_PATH = Path(__file__).parent / "setup_catalogue.yaml"
45
+
46
+ # (operation, deck-tool) -> detection spec. Each row cites the
47
+ # adapter's own resolution as oracle: the binaries its preflight
48
+ # `shutil.which`s (or `_resolve`s), the banner probe for netgen
49
+ # (`adapters.lvs_netgen.resolve_netgen`), the import probe for sax.
50
+ # A registered solver with no row fails loudly (unknown_solver),
51
+ # so a new solver cannot slip into the menu undetected. Pure-Python
52
+ # solvers take an empty spec: always available, nothing to detect.
53
+ SOLVER_DEPS = {
54
+ ("run_drc", "magic"): {"binaries": ["magic"]},
55
+ ("run_drc", "klayout"): {"binaries": ["klayout"]},
56
+ ("run_antenna", "klayout"): {"binaries": ["klayout"]},
57
+ ("run_xschem", "xschem"): {"binaries": ["xschem"]},
58
+ ("run_hdl_test", "icarus"): {"binaries": ["iverilog", "vvp"]},
59
+ ("run_optical", "sax"): {"python": ["sax"]},
60
+ ("run_optical", "circulax"): {"python": ["circulax"]},
61
+ ("run_em", "meow"): {"python": ["meow"]},
62
+ ("run_extraction", "magic"): {"binaries": ["magic"]},
63
+ ("run_lvs", "netgen"): {"binaries": ["netgen"], "banner": "lvs"},
64
+ ("run_spice", "ngspice"): {"binaries": ["ngspice"]},
65
+ ("run_post_layout", "ngspice"): {"binaries": ["ngspice"]},
66
+ ("run_syntax", "slang"): {"binaries": ["slang"]},
67
+ ("run_synth", "yosys"): {"binaries": ["yosys"]},
68
+ ("run_connectivity", "connectgraph"): {},
69
+ ("run_erc", "connectgraph"): {},
70
+ ("run_formal", "sby"): {"binaries": ["sby"]},
71
+ ("run_view_sync", "connectgraph"): {},
72
+ ("generate_views", "gdsfactory"): {"python": ["gdsfactory"]},
73
+ ("run_cosim", "cosim"): {"binaries": ["ngspice", "iverilog", "vvp"]},
74
+ ("run_digital_flow", "librelane"): {"binaries": ["librelane"]},
75
+ ("run_timing", "opensta"): {"binaries": ["sta"]},
76
+ }
77
+
78
+
79
+ class SetupError(ContractError):
80
+ """Setup-surface violation with a stable machine code.
81
+
82
+ Args:
83
+ code: Stable machine-readable error code.
84
+ message: Human-readable detail.
85
+ fields: Itemized fields, when the error names any.
86
+ """
87
+
88
+
89
+ def load_catalogue(path=None):
90
+ """Load and structurally validate the provision catalogue.
91
+
92
+ Args:
93
+ path: Catalogue YAML path; the bundled data when omitted.
94
+
95
+ Returns:
96
+ Validated catalogue mapping (version + entries).
97
+
98
+ Raises:
99
+ SetupError: invalid_catalogue for an unloadable file, a
100
+ wrong catalogue_version, a missing entry list, or a
101
+ malformed entry.
102
+ """
103
+ catalogue_path = Path(path) if path is not None else _CATALOGUE_PATH
104
+ try:
105
+ data = YAML(typ="safe").load(catalogue_path.read_text())
106
+ except OSError as exc:
107
+ raise SetupError(
108
+ "invalid_catalogue", f"Cannot read catalogue {catalogue_path}: {exc}."
109
+ ) from exc
110
+ if not isinstance(data, dict) or data.get("catalogue_version") != CATALOGUE_VERSION:
111
+ raise SetupError(
112
+ "invalid_catalogue",
113
+ f"Catalogue {catalogue_path} must carry catalogue_version "
114
+ f"{CATALOGUE_VERSION}.",
115
+ )
116
+ entries = data.get("entries")
117
+ if not isinstance(entries, list) or not entries:
118
+ raise SetupError(
119
+ "invalid_catalogue", f"Catalogue {catalogue_path} has no entries."
120
+ )
121
+ for entry in entries:
122
+ _check_entry(entry, catalogue_path)
123
+ return {"catalogue_version": data["catalogue_version"], "entries": list(entries)}
124
+
125
+
126
+ def _check_entry(entry, catalogue_path):
127
+ if not isinstance(entry, dict):
128
+ raise SetupError(
129
+ "invalid_catalogue", f"Catalogue {catalogue_path} has a malformed entry."
130
+ )
131
+ for field in ("id", "kind", "label", "remedy", "recipe"):
132
+ if not entry.get(field) or not isinstance(entry.get(field), str):
133
+ raise SetupError(
134
+ "invalid_catalogue",
135
+ f"Catalogue entry {entry.get('id')!r} misses text field {field}.",
136
+ )
137
+ kind = entry["kind"]
138
+ if kind == "tool":
139
+ if not entry.get("binaries"):
140
+ raise SetupError(
141
+ "invalid_catalogue",
142
+ f"Catalogue entry {entry['id']!r} needs binaries.",
143
+ )
144
+ elif kind == "python":
145
+ if not entry.get("python"):
146
+ raise SetupError(
147
+ "invalid_catalogue",
148
+ f"Catalogue entry {entry['id']!r} needs python modules.",
149
+ )
150
+ elif kind == "pdk":
151
+ if not entry.get("pdk_id") or not entry.get("decks"):
152
+ raise SetupError(
153
+ "invalid_catalogue",
154
+ f"Catalogue entry {entry['id']!r} needs pdk_id and decks.",
155
+ )
156
+ elif kind == "cloud":
157
+ if not entry.get("service"):
158
+ raise SetupError(
159
+ "invalid_catalogue",
160
+ f"Catalogue entry {entry['id']!r} needs a service name.",
161
+ )
162
+ else:
163
+ raise SetupError(
164
+ "invalid_catalogue",
165
+ f"Catalogue entry {entry['id']!r} has unknown kind {kind!r}.",
166
+ )
167
+
168
+
169
+ def _resolve_netgen():
170
+ """Banner-verified netgen path via the LVS adapter (lazy import)."""
171
+ from ..adapters.lvs_netgen import resolve_netgen
172
+
173
+ return resolve_netgen()
174
+
175
+
176
+ def _checkout_root():
177
+ """Engine checkout root, or None outside a source tree.
178
+
179
+ Returns:
180
+ Checkout root when this file sits inside one (a
181
+ pyproject marker beside src/), else None (installed
182
+ wheels fall back to the legacy home defaults).
183
+ """
184
+ root = Path(__file__).resolve().parents[2]
185
+ if (root / "pyproject.toml").is_file():
186
+ return root
187
+ return None
188
+
189
+
190
+ def default_tools_dir():
191
+ """Default provisioned tools root (BUILD-LOCAL stage 2).
192
+
193
+ Args:
194
+ None: Reads no arguments; honors no env (callers layer
195
+ explicit tools_dir and TOOLS_DIR over this).
196
+
197
+ Returns:
198
+ Repo-local `.tools` under the checkout, or the legacy
199
+ `~/tools` outside a source tree.
200
+ """
201
+ root = _checkout_root()
202
+ if root is not None:
203
+ return root / ".tools"
204
+ return Path(os.path.expanduser("~/tools"))
205
+
206
+
207
+ def prov_bin_dirs(tools_dir=None):
208
+ """bin/ dirs from the provisioned tools root (prov-phase1.env).
209
+
210
+ Parses the KEY="..." lines the provisioner writes: *_PREFIX
211
+ and *_DIR entries contribute their bin/ subdir, executable
212
+ paths (AppImage, venv pythons) contribute their dirname.
213
+ Missing or unreadable env file -> [] (ambient PATH decides).
214
+
215
+ Default root is BUILD-LOCAL stage 2: the repo-local `.tools`
216
+ (explicit tools_dir, then TOOLS_DIR, still win; existing
217
+ `~/tools` bundles keep working via `TOOLS_DIR=~/tools`).
218
+
219
+ Args:
220
+ tools_dir: Explicit tools root; TOOLS_DIR/default when omitted.
221
+
222
+ Returns:
223
+ bin/ dirs from prov-phase1.env under the root ([] when the
224
+ env file is missing or unreadable).
225
+ """
226
+ root = tools_dir or os.environ.get("TOOLS_DIR")
227
+ if root is None:
228
+ root = default_tools_dir()
229
+ try:
230
+ with open(os.path.join(root, "prov-phase1.env")) as handle:
231
+ lines = handle.read().splitlines()
232
+ except OSError:
233
+ return []
234
+ dirs = []
235
+ for line in lines:
236
+ line = line.strip()
237
+ if not line or line.startswith("#") or "=" not in line:
238
+ continue
239
+ key, _, value = line.partition("=")
240
+ value = value.strip().strip('"')
241
+ if not value:
242
+ continue
243
+ if key.endswith("_PREFIX") or key.endswith("_DIR"):
244
+ candidate = os.path.join(value, "bin")
245
+ else:
246
+ candidate = os.path.dirname(value)
247
+ if candidate and os.path.isdir(candidate) \
248
+ and candidate not in dirs:
249
+ dirs.append(candidate)
250
+ return dirs
251
+
252
+
253
+ def _detect_binaries(names, banner=None):
254
+ """Split binary names into found paths and missing names.
255
+
256
+ Resolution order (docs/provision-sources.md "Engine path
257
+ pointing"): provisioned root first (pinned, trusted), then
258
+ ambient PATH. The netgen impostor rule still applies.
259
+ """
260
+ found, missing = {}, []
261
+ prov_dirs = prov_bin_dirs()
262
+ for name in names:
263
+ if banner == "lvs" and name == "netgen":
264
+ # PATH presence is NOT enough (Schoeberl impostor):
265
+ # only the adapter's banner probe admits.
266
+ path = _resolve_netgen()
267
+ else:
268
+ path = None
269
+ for candidate in prov_dirs:
270
+ probe = os.path.join(candidate, name)
271
+ if os.path.isfile(probe) and os.access(probe, os.X_OK):
272
+ path = probe
273
+ break
274
+ if path is None:
275
+ path = shutil.which(name)
276
+ if path is None:
277
+ missing.append(name)
278
+ else:
279
+ found[name] = path
280
+ return found, missing
281
+
282
+
283
+ def _detect_python(names):
284
+ """Split module names into found versions and missing names."""
285
+ # Shadow-proof probe (SETUP-HONESTY): requires a real loader
286
+ # plus installed distribution metadata — see setup_probes.
287
+ return detect_python(names)
288
+
289
+
290
+ def _covering_remedy(catalogue, binaries, python):
291
+ """Join the catalogue remedies covering the missing deps."""
292
+ want_binaries, want_python = set(binaries), set(python)
293
+ remedies = []
294
+ for entry in catalogue["entries"]:
295
+ provides_binaries = set(entry.get("binaries") or [])
296
+ provides_python = set(entry.get("python") or [])
297
+ if (
298
+ want_binaries & provides_binaries or want_python & provides_python
299
+ ) and entry["remedy"] not in remedies:
300
+ remedies.append(entry["remedy"])
301
+ if remedies:
302
+ return " ".join(remedies)
303
+ return "Provision the missing tooling (see docs/provision-plan.md)."
304
+
305
+
306
+ def _pdk_rows(catalogue, root):
307
+ """(pdks, store_present, store_reason, deck_rows) from the store."""
308
+ try:
309
+ pdks = list_pdks(engine_root=root)
310
+ except ContractError as exc:
311
+ # Fresh machine (or a broken entry): report empty with the
312
+ # cause — never raise, never create the skeleton.
313
+ return [], False, str(exc), _missing_deck_rows(catalogue, None)
314
+ rows = []
315
+ for entry in catalogue["entries"]:
316
+ if entry["kind"] != "pdk":
317
+ continue
318
+ slug = slugify(entry["pdk_id"])
319
+ revisions = sorted(
320
+ {p["revision"] for p in pdks if slugify(p["id"]) == slug}
321
+ )
322
+ if not revisions:
323
+ rows.extend(_missing_deck_rows(catalogue, entry))
324
+ continue
325
+ for revision in revisions:
326
+ for rel in entry["decks"]:
327
+ rows.append(_deck_row(entry, revision, rel, root))
328
+ return pdks, True, "", rows
329
+
330
+
331
+ def _missing_deck_rows(catalogue, entry):
332
+ if entry is None:
333
+ rows = []
334
+ for candidate in catalogue["entries"]:
335
+ if candidate["kind"] == "pdk":
336
+ rows.extend(_missing_deck_rows(catalogue, candidate))
337
+ return rows
338
+ return [
339
+ {
340
+ "pdk_id": entry["pdk_id"],
341
+ "revision": None,
342
+ "rel": rel,
343
+ "state": "missing",
344
+ "reason": f"PDK {entry['pdk_id']} not registered.",
345
+ "remedy": entry["remedy"],
346
+ }
347
+ for rel in entry["decks"]
348
+ ]
349
+
350
+
351
+ def _deck_row(entry, revision, rel, root):
352
+ try:
353
+ hit = resolve_deck(entry["pdk_id"], revision, rel, engine_root=root)
354
+ except ContractError as exc:
355
+ state = "drifted" if exc.code == "pdk_drift" else "missing"
356
+ return {
357
+ "pdk_id": entry["pdk_id"],
358
+ "revision": revision,
359
+ "rel": rel,
360
+ "state": state,
361
+ "reason": str(exc),
362
+ "remedy": entry["remedy"],
363
+ }
364
+ return {
365
+ "pdk_id": entry["pdk_id"],
366
+ "revision": revision,
367
+ "rel": rel,
368
+ "state": "installed",
369
+ "sha256": hit["sha256"],
370
+ }
371
+
372
+
373
+ def entry_state(entry, engine_root=None):
374
+ """Render one catalogue entry with its live install state.
375
+
376
+ Args:
377
+ entry: Catalogue entry mapping (from load_catalogue).
378
+ engine_root: Explicit store root; resolved when omitted.
379
+
380
+ Returns:
381
+ Entry fields plus state (installed/missing/gated) and a
382
+ kind-shaped detail mapping.
383
+ """
384
+ root = resolve_engine_root(engine_root)
385
+ base = {
386
+ "id": entry["id"],
387
+ "kind": entry["kind"],
388
+ "label": entry["label"],
389
+ "remedy": entry["remedy"],
390
+ "recipe": entry["recipe"],
391
+ "provisionable": entry.get("provisionable", True),
392
+ }
393
+ if entry["kind"] == "tool":
394
+ found, missing = _detect_binaries(
395
+ entry["binaries"], banner=entry.get("banner")
396
+ )
397
+ base["state"] = "installed" if not missing else "missing"
398
+ base["detail"] = {"found": found, "missing": missing}
399
+ return base
400
+ if entry["kind"] == "python":
401
+ found, missing = _detect_python(entry["python"])
402
+ base["state"] = "installed" if not missing else "missing"
403
+ base["detail"] = {"found": found, "missing": missing}
404
+ return base
405
+ if entry["kind"] == "cloud":
406
+ # Static gated row: no key lookup (no store exists yet),
407
+ # no imports, no network — BYOC per I32.
408
+ base["provisionable"] = False
409
+ base["state"] = "gated"
410
+ base["detail"] = {"key_state": "missing", "service": entry["service"]}
411
+ return base
412
+ try:
413
+ pdks = list_pdks(engine_root=root)
414
+ except ContractError:
415
+ pdks = []
416
+ slug = slugify(entry["pdk_id"])
417
+ revisions = sorted({p["revision"] for p in pdks if slugify(p["id"]) == slug})
418
+ decks = []
419
+ if not revisions:
420
+ decks = _missing_deck_rows({"entries": [entry]}, entry)
421
+ else:
422
+ for revision in revisions:
423
+ for rel in entry["decks"]:
424
+ decks.append(_deck_row(entry, revision, rel, root))
425
+ base["state"] = "installed" if revisions else "missing"
426
+ base["detail"] = {"revisions": revisions, "decks": decks}
427
+ return base
428
+
429
+
430
+ def catalogue_state(engine_root=None):
431
+ """Render the full checkbox menu: entries plus install state.
432
+
433
+ Args:
434
+ engine_root: Explicit store root; resolved when omitted.
435
+
436
+ Returns:
437
+ Catalogue version plus per-entry states.
438
+ """
439
+ catalogue = load_catalogue()
440
+ root = resolve_engine_root(engine_root)
441
+ return {
442
+ "catalogue_version": catalogue["catalogue_version"],
443
+ "entries": [entry_state(e, engine_root=root) for e in catalogue["entries"]],
444
+ }
445
+
446
+
447
+ def report_capabilities(solvers, target=None, engine_root=None, served=()):
448
+ """Report availability, store state, and the provision menu.
449
+
450
+ Args:
451
+ solvers: Operation -> {deck-tool: adapter} registry (the
452
+ Engine solver map; each adapter serves capabilities()).
453
+ target: Target passed to each adapter's capabilities().
454
+ engine_root: Explicit store root; resolved when omitted.
455
+ served: Operations the Engine serves directly without a
456
+ solver (CRUD mutations, run_job); reported ready.
457
+
458
+ Returns:
459
+ Mapping with per-operation readiness (ready/blocked/
460
+ unsupported) plus per-tool availability, the store
461
+ root/presence, registered PDKs, per-deck states,
462
+ BYOC-gated cloud rows, and the checkbox catalogue.
463
+
464
+ Raises:
465
+ SetupError: unknown_solver when a registered (operation,
466
+ tool) has no detection spec — the menu must never
467
+ silently misreport a solver it cannot probe.
468
+ """
469
+ catalogue = load_catalogue()
470
+ root = resolve_engine_root(engine_root)
471
+ pdks, store_present, store_reason, decks = _pdk_rows(catalogue, root)
472
+ cloud = {
473
+ entry["id"]: {
474
+ "label": entry["label"],
475
+ "service": entry["service"],
476
+ "status": "gated",
477
+ "key_state": "missing",
478
+ "remedy": entry["remedy"],
479
+ "recipe": entry["recipe"],
480
+ }
481
+ for entry in catalogue["entries"]
482
+ if entry["kind"] == "cloud"
483
+ }
484
+ operations = {}
485
+ for operation in sorted(OPERATIONS):
486
+ tools = solvers.get(operation) or {}
487
+ if not tools:
488
+ if operation in served:
489
+ operations[operation] = {
490
+ "readiness": "ready",
491
+ "reason": "Served by the Engine directly (no solver needed).",
492
+ "tools": {},
493
+ }
494
+ continue
495
+ operations[operation] = {
496
+ "readiness": "unsupported",
497
+ "reason": f"No solver registered: {operation}.",
498
+ "tools": {},
499
+ }
500
+ continue
501
+ per_tool, missing_all, binaries_all, python_all = {}, [], [], []
502
+ for tool, adapter in tools.items():
503
+ spec = SOLVER_DEPS.get((operation, tool))
504
+ if spec is None:
505
+ raise SetupError(
506
+ "unknown_solver",
507
+ f"No detection spec for solver {operation}/{tool}; "
508
+ "extend the provision catalogue.",
509
+ )
510
+ found_binaries, missing_binaries = _detect_binaries(
511
+ spec.get("binaries") or [], banner=spec.get("banner")
512
+ )
513
+ found_python, missing_python = _detect_python(spec.get("python") or [])
514
+ missing = missing_binaries + missing_python
515
+ detail = {
516
+ "available": not missing,
517
+ "capability": adapter.capabilities(target),
518
+ "found": {**found_binaries, **found_python},
519
+ "missing": missing,
520
+ }
521
+ if missing:
522
+ detail["remedy"] = _covering_remedy(
523
+ catalogue, missing_binaries, missing_python
524
+ )
525
+ per_tool[tool] = detail
526
+ missing_all.extend(missing)
527
+ binaries_all.extend(missing_binaries)
528
+ python_all.extend(missing_python)
529
+ if any(entry["available"] for entry in per_tool.values()):
530
+ operations[operation] = {"readiness": "ready", "tools": per_tool}
531
+ else:
532
+ operations[operation] = {
533
+ "readiness": "blocked",
534
+ "reason": f"No {operation} tool resolves on this machine.",
535
+ "missing": sorted(set(missing_all)),
536
+ "remedy": _covering_remedy(catalogue, binaries_all, python_all),
537
+ "tools": per_tool,
538
+ }
539
+ store = {"root": str(root), "present": store_present}
540
+ if not store_present:
541
+ store["reason"] = store_reason
542
+ store["remedy"] = next(
543
+ e["remedy"] for e in catalogue["entries"] if e["kind"] == "pdk"
544
+ )
545
+ return {
546
+ "operations": operations,
547
+ "store": store,
548
+ "pdks": pdks,
549
+ "decks": decks,
550
+ "cloud": cloud,
551
+ "catalogue": catalogue_state(engine_root=root),
552
+ }