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,123 @@
1
+ """IP editor verbs: per-PDK registry mutations behind ProjectService.
2
+
3
+ First REFACTOR-SERVICE slice: the ip_register/ip_instantiate/
4
+ ip_promote verbs live here as _IpMixin so src/project/service.py
5
+ stays under the architecture line limit. ProjectService binds
6
+ this mixin, so the verbs keep their bound path
7
+ (rocket_engine.project.service.ProjectService.<verb>); the API
8
+ contract generator reads the contract docstrings from this
9
+ module (see scripts/api_spec.py _SERVICE_DOC_HOME).
10
+ """
11
+
12
+ from ...ops import editors as _editors
13
+
14
+ from . import lifecycle as _ip
15
+
16
+
17
+ class _IpMixin:
18
+ """IP registry verbs (per-PDK invariant, RULING-01)."""
19
+
20
+ def ip_register(self, pdk, name, version, symbol, cell,
21
+ request_id, command_text, pin_map=None,
22
+ scope="pdk", reason="",
23
+ expected_revision=None):
24
+ """Register one prebuilt IP block revision under its PDK.
25
+
26
+ Args:
27
+ pdk: Owning PDK id (the per-PDK invariant key).
28
+ name: Block name within the PDK registry.
29
+ version: Revision label ("" forbidden).
30
+ symbol: Linked schematic symbol reference.
31
+ cell: Linked footprint cell reference.
32
+ request_id: Client envelope id for idempotent replay.
33
+ command_text: YAML command text for identity comparison.
34
+ pin_map: Explicit [{symPin, cellPin}] entries.
35
+ scope: "pdk" (default) or "shared" with a reason.
36
+ reason: Recorded justification for shared scope.
37
+ expected_revision: Revision the writer read.
38
+
39
+ Returns:
40
+ Mutation mapping with receipt, events, and cursor.
41
+
42
+ Raises:
43
+ ProjectError: request_conflict, revision_conflict,
44
+ invalid_pdk, ip_exists, invalid_ip, invalid_scope.
45
+ """
46
+ return _editors.editor_mutation(self,
47
+ "ip_register", request_id, command_text,
48
+ expected_revision,
49
+ lambda store, _rid, _rt, _live: _ip.ip_register(
50
+ store, self._actor, pdk, name, version, symbol,
51
+ cell, pin_map=pin_map or [], scope=scope,
52
+ reason=reason),
53
+ lambda receipt: f"registered IP {pdk}/{name}@{version}",
54
+ )
55
+
56
+ def ip_instantiate(self, pdk, name, sheet, x, y,
57
+ request_id, command_text, version=None,
58
+ rotation=0, mirror=False, properties=None,
59
+ expected_revision=None):
60
+ """Place one registered IP block with linkage intact.
61
+
62
+ Args:
63
+ pdk: Consuming PDK id (must own the block unless the
64
+ block opted into shared scope).
65
+ name: Block name within the PDK registry.
66
+ sheet: Sheet entity id hosting the instance.
67
+ x: X position in schematic units.
68
+ y: Y position in schematic units.
69
+ request_id: Client envelope id for idempotent replay.
70
+ command_text: YAML command text for identity comparison.
71
+ version: Revision label, or None for the latest.
72
+ rotation: Rotation in degrees (0/90/180/270).
73
+ mirror: Mirror flag.
74
+ properties: Text-to-text instance properties.
75
+ expected_revision: Revision the writer read.
76
+
77
+ Returns:
78
+ Mutation mapping with receipt, events, and cursor.
79
+
80
+ Raises:
81
+ ProjectError: request_conflict, revision_conflict,
82
+ unknown_ip, pdk_mismatch, unknown_sheet,
83
+ invalid_instance.
84
+ """
85
+ return _editors.editor_mutation(self,
86
+ "ip_instantiate", request_id, command_text,
87
+ expected_revision,
88
+ lambda store, _rid, _rt, _live: _ip.ip_instantiate(
89
+ store, self._actor, pdk, name, sheet, x, y,
90
+ version=version, rotation=rotation, mirror=mirror,
91
+ properties=properties),
92
+ lambda receipt: (
93
+ f"instantiated IP {pdk}/{name}"
94
+ f"@{receipt.get('version')}"),
95
+ )
96
+
97
+ def ip_promote(self, pdk, name, version,
98
+ request_id, command_text,
99
+ expected_revision=None):
100
+ """Promote one block version as the registry's recommended one.
101
+
102
+ Args:
103
+ pdk: Owning PDK id scoping the registry.
104
+ name: Block name within the PDK registry.
105
+ version: Revision label to promote.
106
+ request_id: Client envelope id for idempotent replay.
107
+ command_text: YAML command text for identity comparison.
108
+ expected_revision: Revision the writer read.
109
+
110
+ Returns:
111
+ Mutation mapping with receipt, events, and cursor.
112
+
113
+ Raises:
114
+ ProjectError: request_conflict, revision_conflict,
115
+ unknown_ip, invalid_pdk.
116
+ """
117
+ return _editors.editor_mutation(self,
118
+ "ip_promote", request_id, command_text,
119
+ expected_revision,
120
+ lambda store, _rid, _rt, _live: _ip.ip_promote(
121
+ store, self._actor, pdk, name, version),
122
+ lambda receipt: f"promoted IP {pdk}/{name}@{version}",
123
+ )
@@ -0,0 +1,6 @@
1
+ """Editable project layer catalog bound to a technology profile."""
2
+
3
+ from .bindings import bind_layers
4
+ from .presets import apply_preset, builtin_presets
5
+
6
+ __all__ = ["bind_layers", "apply_preset", "builtin_presets"]
@@ -0,0 +1,115 @@
1
+ """Technology-profile bindings with explicit capability states.
2
+
3
+ Bindings map project layer identities to process purposes and
4
+ report each mapping as supported, missing, or incompatible. A
5
+ missing binding never reinterprets geometry: drawing and saving
6
+ keep working and only process operations report the gap.
7
+ """
8
+
9
+ import copy
10
+
11
+ STATES = ("supported", "missing", "incompatible")
12
+
13
+
14
+ def bind_layers(catalog, profile_layers, source="profile",
15
+ version="unknown"):
16
+ """Bind catalog layers against a profile vocabulary.
17
+
18
+ Args:
19
+ catalog: LayerCatalog to bind.
20
+ profile_layers: Mapping of profile name to layer-name
21
+ lists, or a plain list of known layer names.
22
+ source: Provenance label for the profile source.
23
+ version: Profile version pinned into the report.
24
+
25
+ Returns:
26
+ Binding report with profile pin, catalog revision,
27
+ and one entry per layer (state, purpose, source).
28
+ """
29
+ if isinstance(profile_layers, dict):
30
+ profile = catalog.profile or {}
31
+ known = profile_layers.get(profile.get("name", ""), [])
32
+ else:
33
+ known = list(profile_layers)
34
+ entries = []
35
+ for layer in catalog.list_layers():
36
+ if layer["name"] in known:
37
+ state = "supported"
38
+ elif layer["name"].startswith("custom"):
39
+ state = "missing"
40
+ else:
41
+ state = "missing"
42
+ entries.append({
43
+ "capability": state,
44
+ "layer": layer["lid"],
45
+ "name": layer["name"],
46
+ "purpose": layer["purpose"],
47
+ "source": source,
48
+ "state": state,
49
+ "version": version,
50
+ })
51
+ profile = catalog.profile or {}
52
+ return {
53
+ "catalog_revision": catalog.revision,
54
+ "entries": entries,
55
+ "profile": profile.get("name", "unresolved"),
56
+ "profile_version": profile.get("version", version),
57
+ }
58
+
59
+
60
+ def check_operation(report, layer_lid, operation):
61
+ """Check whether one process operation may consume a layer.
62
+
63
+ Args:
64
+ report: Binding report from bind_layers.
65
+ layer_lid: Stable layer id the operation needs.
66
+ operation: Process operation name (export_gds, drc,
67
+ extraction, or a recipe step).
68
+
69
+ Returns:
70
+ Verdict mapping with state (supported, unsupported,
71
+ or incomplete), layer, operation, and reason. Draw
72
+ and save paths ignore this verdict; only the named
73
+ process operation is gated by it.
74
+
75
+ Raises:
76
+ ValueError: For a layer with no binding entry.
77
+ """
78
+ for entry in report["entries"]:
79
+ if entry["layer"] == layer_lid:
80
+ if entry["state"] == "supported":
81
+ return {
82
+ "layer": layer_lid,
83
+ "operation": operation,
84
+ "reason": "Binding covers the operation.",
85
+ "state": "supported",
86
+ }
87
+ return {
88
+ "layer": layer_lid,
89
+ "operation": operation,
90
+ "reason": (
91
+ f"No {operation} mapping for layer "
92
+ f"{entry['name']!r}; geometry is intact."
93
+ ),
94
+ "state": "unsupported",
95
+ }
96
+ raise ValueError(f"No binding entry for layer: {layer_lid!r}.")
97
+
98
+
99
+ def binding_for(report, layer_lid):
100
+ """Return one layer entry from a binding report.
101
+
102
+ Args:
103
+ report: Binding report from bind_layers.
104
+ layer_lid: Stable layer id to look up.
105
+
106
+ Returns:
107
+ Deep copy of the binding entry.
108
+
109
+ Raises:
110
+ ValueError: For a layer with no binding entry.
111
+ """
112
+ for entry in report["entries"]:
113
+ if entry["layer"] == layer_lid:
114
+ return copy.deepcopy(entry)
115
+ raise ValueError(f"No binding entry for layer: {layer_lid!r}.")
@@ -0,0 +1,228 @@
1
+ """Editable project layer catalog with stable identities.
2
+
3
+ Layers carry identity (lid) separate from display (name, color,
4
+ visibility): renames, saves, and reloads keep the lid, so saved
5
+ geometry addressed by lid is never reinterpreted when a profile
6
+ is missing or changed.
7
+ """
8
+
9
+ import copy
10
+ import itertools
11
+
12
+ _COUNTER = itertools.count(1)
13
+
14
+
15
+ def _mint_lid(existing):
16
+ """Mint one stable layer id.
17
+
18
+ Args:
19
+ existing: Lids already in use.
20
+
21
+ Returns:
22
+ Fresh lid string not present in existing.
23
+ """
24
+ while True:
25
+ candidate = f"lyr-{next(_COUNTER):04d}"
26
+ if candidate not in existing:
27
+ return candidate
28
+
29
+
30
+ class LayerCatalog:
31
+ """Editable layer vocabulary for one project.
32
+
33
+ Args:
34
+ profile: Resolved technology profile mapping (name,
35
+ version) or None when no profile is selected.
36
+ revision: Starting catalog revision for a fresh
37
+ catalog.
38
+ """
39
+
40
+ def __init__(self, profile=None, revision=1):
41
+ self._profile = copy.deepcopy(profile)
42
+ self._revision = revision
43
+ self._layers = {}
44
+
45
+ @property
46
+ def setup_visible(self):
47
+ """Whether missing-profile setup must be shown.
48
+
49
+ Returns:
50
+ True when no technology profile is resolved.
51
+ """
52
+ return self._profile is None
53
+
54
+ @property
55
+ def revision(self):
56
+ """Current catalog revision.
57
+
58
+ Returns:
59
+ Integer revision bumped by every mutation.
60
+ """
61
+ return self._revision
62
+
63
+ @property
64
+ def profile(self):
65
+ """Resolved technology profile, if any.
66
+
67
+ Returns:
68
+ Deep copy of the profile mapping or None.
69
+ """
70
+ return copy.deepcopy(self._profile)
71
+
72
+ def add_layer(self, name, purpose="drawing", color="#ffffff",
73
+ order=0, visible=True, aliases=()):
74
+ """Add one project layer with a stable identity.
75
+
76
+ Args:
77
+ name: Display name for the layer.
78
+ purpose: Process purpose label (display only;
79
+ semantics come from bindings).
80
+ color: Display color for editors.
81
+ order: Stack order for editors.
82
+ visible: Display visibility flag.
83
+ aliases: Extra names resolving to this layer.
84
+
85
+ Returns:
86
+ Deep copy of the created layer record.
87
+
88
+ Raises:
89
+ ValueError: If the name is empty or already used.
90
+ """
91
+ if not name:
92
+ raise ValueError("Layer name must not be empty.")
93
+ if any(layer["name"] == name for layer in
94
+ self._layers.values()):
95
+ raise ValueError(f"Duplicate layer name: {name!r}.")
96
+ lid = _mint_lid(set(self._layers))
97
+ self._layers[lid] = {
98
+ "aliases": list(aliases),
99
+ "color": color,
100
+ "lid": lid,
101
+ "name": name,
102
+ "order": order,
103
+ "purpose": purpose,
104
+ "visible": bool(visible),
105
+ }
106
+ self._revision += 1
107
+ return copy.deepcopy(self._layers[lid])
108
+
109
+ def rename_layer(self, lid, name):
110
+ """Rename one layer without changing its identity.
111
+
112
+ Args:
113
+ lid: Stable layer id to rename.
114
+ name: New display name.
115
+
116
+ Returns:
117
+ Deep copy of the renamed layer record.
118
+
119
+ Raises:
120
+ ValueError: For an unknown lid, an empty name, or
121
+ a name already in use.
122
+ """
123
+ if lid not in self._layers:
124
+ raise ValueError(f"Unknown layer: {lid!r}.")
125
+ if not name:
126
+ raise ValueError("Layer name must not be empty.")
127
+ self._layers[lid]["name"] = name
128
+ self._revision += 1
129
+ return copy.deepcopy(self._layers[lid])
130
+
131
+ def set_display(self, lid, color=None, visible=None, order=None):
132
+ """Change display-only flags of one layer.
133
+
134
+ Args:
135
+ lid: Stable layer id to update.
136
+ color: New display color, if given.
137
+ visible: New visibility flag, if given.
138
+ order: New stack order, if given.
139
+
140
+ Returns:
141
+ Deep copy of the updated layer record.
142
+
143
+ Raises:
144
+ ValueError: For an unknown lid.
145
+ """
146
+ if lid not in self._layers:
147
+ raise ValueError(f"Unknown layer: {lid!r}.")
148
+ layer = self._layers[lid]
149
+ if color is not None:
150
+ layer["color"] = color
151
+ if visible is not None:
152
+ layer["visible"] = bool(visible)
153
+ if order is not None:
154
+ layer["order"] = order
155
+ self._revision += 1
156
+ return copy.deepcopy(layer)
157
+
158
+ def get_layer(self, lid):
159
+ """Return one layer record by stable id.
160
+
161
+ Args:
162
+ lid: Stable layer id to read.
163
+
164
+ Returns:
165
+ Deep copy of the layer record.
166
+
167
+ Raises:
168
+ ValueError: For an unknown lid.
169
+ """
170
+ if lid not in self._layers:
171
+ raise ValueError(f"Unknown layer: {lid!r}.")
172
+ return copy.deepcopy(self._layers[lid])
173
+
174
+ def list_layers(self):
175
+ """List every layer ordered by stack order.
176
+
177
+ Returns:
178
+ List of deep-copied layer records.
179
+ """
180
+ return sorted(
181
+ (copy.deepcopy(layer) for layer in
182
+ self._layers.values()),
183
+ key=lambda layer: (layer["order"], layer["lid"]),
184
+ )
185
+
186
+ def attach_profile(self, profile):
187
+ """Resolve a technology profile without moving geometry.
188
+
189
+ Args:
190
+ profile: Technology profile mapping with at least
191
+ name and version.
192
+
193
+ Returns:
194
+ Deep copy of the attached profile.
195
+ """
196
+ self._profile = copy.deepcopy(profile)
197
+ self._revision += 1
198
+ return copy.deepcopy(self._profile)
199
+
200
+ def to_dict(self):
201
+ """Serialize the catalog including its revision pin.
202
+
203
+ Returns:
204
+ Plain mapping with profile, revision, and layers.
205
+ """
206
+ return {
207
+ "layers": self.list_layers(),
208
+ "profile": copy.deepcopy(self._profile),
209
+ "revision": self._revision,
210
+ }
211
+
212
+ @classmethod
213
+ def from_dict(cls, data):
214
+ """Rebuild a catalog from serialized state.
215
+
216
+ Args:
217
+ data: Mapping produced by to_dict.
218
+
219
+ Returns:
220
+ LayerCatalog with stable lids preserved.
221
+ """
222
+ catalog = cls(
223
+ profile=data.get("profile"),
224
+ revision=data.get("revision", 1),
225
+ )
226
+ for layer in data.get("layers", ()):
227
+ catalog._layers[layer["lid"]] = copy.deepcopy(layer)
228
+ return catalog
@@ -0,0 +1,43 @@
1
+ """Built-in layer visibility presets (viewer aids, not semantics)."""
2
+
3
+ BUILTIN_PRESETS = ("All", "None", "All-metal", "Top", "Bottom")
4
+
5
+
6
+ def builtin_presets():
7
+ """List every built-in preset name.
8
+
9
+ Returns:
10
+ Tuple of preset names.
11
+ """
12
+ return BUILTIN_PRESETS
13
+
14
+
15
+ def apply_preset(name, layer_names):
16
+ """Apply one built-in preset to a layer name list.
17
+
18
+ Args:
19
+ name: Preset name from builtin_presets.
20
+ layer_names: Available layer names to filter.
21
+
22
+ Returns:
23
+ Mapping with preset name and enabled layer names.
24
+
25
+ Raises:
26
+ ValueError: For an unknown preset name.
27
+ """
28
+ if name not in BUILTIN_PRESETS:
29
+ raise ValueError(f"Unknown preset: {name!r}.")
30
+ names = list(layer_names)
31
+ if name == "All":
32
+ enabled = names
33
+ elif name == "None":
34
+ enabled = []
35
+ elif name == "All-metal":
36
+ enabled = [layer for layer in names
37
+ if "met" in layer.lower() or "metal" in
38
+ layer.lower()]
39
+ elif name == "Top":
40
+ enabled = names[-1:] if names else []
41
+ else:
42
+ enabled = names[:1] if names else []
43
+ return {"enabled": enabled, "preset": name}
@@ -0,0 +1 @@
1
+ """Project ledger internals behind ProjectService."""
@@ -0,0 +1,167 @@
1
+ """Document path resolution and byte access."""
2
+
3
+ from ...interfaces.paths import join_contained
4
+ from ..errors import ProjectError
5
+
6
+ from .shared import _RESERVED_DOCS, _SNAPSHOTS_DIRNAME, _atomic_write_text
7
+
8
+ class _DocMixin:
9
+ """Document byte access for ProjectService."""
10
+
11
+ def _doc_path(self, canonical):
12
+ """Resolve one canonical path under the project dir.
13
+
14
+ Args:
15
+ canonical: Canonical document path.
16
+
17
+ Returns:
18
+ Resolved absolute path inside the project dir.
19
+
20
+ Raises:
21
+ ProjectError: path_escape when containment fails.
22
+ """
23
+ try:
24
+ return join_contained(self._dir, canonical)
25
+ except Exception as exc:
26
+ code = getattr(exc, "code", None) or "path_escape"
27
+ raise ProjectError(code, str(exc)) from exc
28
+
29
+ def _read_doc_text(self, canonical, original):
30
+ """Read one document as text, or None when missing.
31
+
32
+ Args:
33
+ canonical: Canonical document path.
34
+ original: Caller-supplied path for error messages.
35
+
36
+ Returns:
37
+ Document text, or None when the document is absent.
38
+
39
+ Raises:
40
+ ProjectError: invalid_command when the path names a
41
+ directory; invalid_document when durable bytes are
42
+ not UTF-8 text.
43
+ """
44
+ if self._dir is None:
45
+ return self._docs.get(canonical)
46
+ target = self._doc_path(canonical)
47
+ if not target.exists():
48
+ return None
49
+ if target.is_dir():
50
+ raise ProjectError(
51
+ "invalid_command",
52
+ f"Document path is a directory: {original!r}.",
53
+ )
54
+ try:
55
+ return target.read_text(encoding="utf-8")
56
+ except UnicodeDecodeError as exc:
57
+ raise ProjectError(
58
+ "invalid_document",
59
+ f"Document is not UTF-8 text: {original!r} ({exc}).",
60
+ ) from exc
61
+ except OSError as exc:
62
+ raise ProjectError(
63
+ "invalid_ledger",
64
+ f"Cannot read document {original!r}: {exc}.",
65
+ ) from exc
66
+
67
+ def _write_doc_text(self, canonical, content, original):
68
+ """Write one document (parents created, atomic rename).
69
+
70
+ Args:
71
+ canonical: Canonical document path.
72
+ content: UTF-8 text to store.
73
+ original: Caller-supplied path for error messages.
74
+
75
+ Raises:
76
+ ProjectError: invalid_command when the path names a
77
+ directory; invalid_ledger when the durable write
78
+ fails.
79
+ """
80
+ if self._dir is None:
81
+ self._docs[canonical] = content
82
+ return
83
+ target = self._doc_path(canonical)
84
+ if target.is_dir():
85
+ raise ProjectError(
86
+ "invalid_command",
87
+ f"Document path is a directory: {original!r}.",
88
+ )
89
+ try:
90
+ target.parent.mkdir(parents=True, exist_ok=True)
91
+ except OSError as exc:
92
+ raise ProjectError(
93
+ "invalid_ledger",
94
+ f"Cannot create document dir for {original!r}: {exc}.",
95
+ ) from exc
96
+ try:
97
+ _atomic_write_text(target, content)
98
+ except OSError as exc:
99
+ raise ProjectError(
100
+ "invalid_ledger",
101
+ f"Cannot write document {original!r}: {exc}.",
102
+ ) from exc
103
+
104
+ def _delete_doc_path(self, canonical, original):
105
+ """Delete one existing document file.
106
+
107
+ Args:
108
+ canonical: Canonical document path.
109
+ original: Caller-supplied path for error messages.
110
+
111
+ Raises:
112
+ ProjectError: invalid_command when the path names a
113
+ directory; invalid_ledger when the delete fails.
114
+ """
115
+ if self._dir is None:
116
+ self._docs.pop(canonical, None)
117
+ return
118
+ target = self._doc_path(canonical)
119
+ if target.is_dir():
120
+ raise ProjectError(
121
+ "invalid_command",
122
+ f"Document path is a directory: {original!r}.",
123
+ )
124
+ try:
125
+ target.unlink()
126
+ except FileNotFoundError:
127
+ pass
128
+ except OSError as exc:
129
+ raise ProjectError(
130
+ "invalid_ledger",
131
+ f"Cannot delete document {original!r}: {exc}.",
132
+ ) from exc
133
+
134
+ def _iter_doc_files(self):
135
+ """Yield (canonical path, byte size) for every document.
136
+
137
+ Returns:
138
+ Sorted (path, bytes) pairs, engine-owned entries
139
+ excluded.
140
+ """
141
+ if self._dir is None:
142
+ return sorted(
143
+ (path, len(text.encode("utf-8")))
144
+ for path, text in self._docs.items()
145
+ )
146
+ if not self._dir.is_dir():
147
+ return []
148
+ found = []
149
+ for target in sorted(self._dir.rglob("*")):
150
+ if not target.is_file():
151
+ continue
152
+ try:
153
+ rel = target.relative_to(self._dir).as_posix()
154
+ except ValueError:
155
+ continue
156
+ if (
157
+ rel in _RESERVED_DOCS
158
+ or rel.startswith("runs/")
159
+ or rel.startswith(_SNAPSHOTS_DIRNAME + "/")
160
+ ):
161
+ continue
162
+ try:
163
+ size = target.stat().st_size
164
+ except OSError:
165
+ continue
166
+ found.append((rel, size))
167
+ return sorted(found)