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,461 @@
|
|
|
1
|
+
"""Symbol-to-cell linkage contract (CELL-LINK).
|
|
2
|
+
|
|
3
|
+
A custom block is useless until its schematic symbol binds the
|
|
4
|
+
layout cell that implements it. The CellLink component is that
|
|
5
|
+
binding, stored on the Block entity alongside SymbolRef, with
|
|
6
|
+
KiCad's symbol-to-footprint relationship as the model: the link
|
|
7
|
+
is plain text on the symbol, filters are advisory, and variants
|
|
8
|
+
alter resolution, never storage.
|
|
9
|
+
|
|
10
|
+
Schema (all edit-time validated by validate_celllink, which never
|
|
11
|
+
checks cell existence — an unknown cell is a run error, never an
|
|
12
|
+
edit error):
|
|
13
|
+
|
|
14
|
+
- ``cell``: opaque ``"pdklib:cell"`` string (KiCad LIB_ID analog).
|
|
15
|
+
Missing or empty means unlinked; run/export skips such blocks.
|
|
16
|
+
- ``pinMap``: ordered explicit ``[{symPin, cellPin}]`` list. Pins
|
|
17
|
+
absent from the map resolve to themselves (identity default);
|
|
18
|
+
each symbol pin names a single target (no fanout).
|
|
19
|
+
- ``cellFilters``: advisory ``[glob, ...]`` list with KiCad
|
|
20
|
+
chooser semantics, consulted by the cell picker only.
|
|
21
|
+
- ``variantOverrides``: ``{name: {cell?, pinEdits?}}`` sparse
|
|
22
|
+
differentials. The stored link itself is the one default
|
|
23
|
+
variant (``variant=None``); ``REFERENCE`` always resolves it.
|
|
24
|
+
|
|
25
|
+
Resolution has one chokepoint per question — resolve_pin,
|
|
26
|
+
resolve_cell, resolve_link — and one run/export entry point,
|
|
27
|
+
resolve_run, which emits ``(cell, pinMap, variant)`` per block
|
|
28
|
+
instance only when the resolved cell is non-empty.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
|
|
33
|
+
from .errors import ContractError
|
|
34
|
+
|
|
35
|
+
#: Reserved variant selector resolving the stored base link. It can
|
|
36
|
+
#: never name an override: the reference stays immutable no matter
|
|
37
|
+
#: which variants a link carries.
|
|
38
|
+
REFERENCE = "REFERENCE"
|
|
39
|
+
|
|
40
|
+
_LINK_KEYS = frozenset({"cell", "pinMap", "cellFilters", "variantOverrides"})
|
|
41
|
+
_PIN_KEYS = frozenset({"symPin", "cellPin"})
|
|
42
|
+
_OVERRIDE_KEYS = frozenset({"cell", "pinEdits"})
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"REFERENCE",
|
|
46
|
+
"CellLinkError",
|
|
47
|
+
"collect_erc",
|
|
48
|
+
"match_cell_filters",
|
|
49
|
+
"resolve_cell",
|
|
50
|
+
"resolve_link",
|
|
51
|
+
"resolve_pin",
|
|
52
|
+
"resolve_run",
|
|
53
|
+
"validate_celllink",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CellLinkError(Exception):
|
|
58
|
+
"""Cell-link run failure with a stable machine code.
|
|
59
|
+
|
|
60
|
+
Raised when a stored link cannot run — the cell it names is
|
|
61
|
+
unknown — never at edit time. Editing stays free; the run
|
|
62
|
+
names the block and the cell so the fix is mechanical.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
code: Stable machine-readable error code.
|
|
66
|
+
message: Human-readable detail.
|
|
67
|
+
fields: Itemized fields, when the error names any.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, code, message, fields=None):
|
|
71
|
+
super().__init__(message)
|
|
72
|
+
self.code = code
|
|
73
|
+
self.fields = fields or []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _need_str(value, what):
|
|
77
|
+
if not isinstance(value, str):
|
|
78
|
+
raise ContractError(
|
|
79
|
+
"invalid_celllink", f"{what} must be a string, got {value!r}."
|
|
80
|
+
)
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _need_nonempty_str(value, what):
|
|
85
|
+
_need_str(value, what)
|
|
86
|
+
if not value:
|
|
87
|
+
raise ContractError("invalid_celllink", f"{what} must not be empty.")
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_celllink(link):
|
|
92
|
+
"""Validate a CellLink component mapping at edit time.
|
|
93
|
+
|
|
94
|
+
Checks shape only — key sets, string types, single-target
|
|
95
|
+
pins, well-formed overrides — and returns a normalized copy
|
|
96
|
+
with defaults filled. Cell existence is never checked here:
|
|
97
|
+
any cell text, however unknown, passes validation.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
link: CellLink mapping with cell, pinMap, cellFilters,
|
|
101
|
+
and variantOverrides keys.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Normalized CellLink copy (defaults filled, storage
|
|
105
|
+
untouched).
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ContractError: invalid_celllink for any shape violation.
|
|
109
|
+
"""
|
|
110
|
+
if not isinstance(link, dict):
|
|
111
|
+
raise ContractError(
|
|
112
|
+
"invalid_celllink", f"CellLink must be a mapping, got {link!r}."
|
|
113
|
+
)
|
|
114
|
+
for key in link:
|
|
115
|
+
if key not in _LINK_KEYS:
|
|
116
|
+
raise ContractError("invalid_celllink", f"Unknown CellLink key: {key}.")
|
|
117
|
+
cell = _need_str(link.get("cell", ""), "cell")
|
|
118
|
+
pin_map = link.get("pinMap", [])
|
|
119
|
+
if not isinstance(pin_map, list):
|
|
120
|
+
raise ContractError("invalid_celllink", "pinMap must be a list.")
|
|
121
|
+
seen = set()
|
|
122
|
+
clean_pins = []
|
|
123
|
+
for entry in pin_map:
|
|
124
|
+
if not isinstance(entry, dict):
|
|
125
|
+
raise ContractError(
|
|
126
|
+
"invalid_celllink", f"pinMap entries must be mappings, got {entry!r}."
|
|
127
|
+
)
|
|
128
|
+
for key in entry:
|
|
129
|
+
if key not in _PIN_KEYS:
|
|
130
|
+
raise ContractError(
|
|
131
|
+
"invalid_celllink", f"Unknown pinMap key: {key}."
|
|
132
|
+
)
|
|
133
|
+
sym_pin = _need_nonempty_str(entry.get("symPin"), "symPin")
|
|
134
|
+
cell_pin = _need_nonempty_str(entry.get("cellPin"), "cellPin")
|
|
135
|
+
if sym_pin in seen:
|
|
136
|
+
raise ContractError(
|
|
137
|
+
"invalid_celllink",
|
|
138
|
+
f"symPin {sym_pin!r} names more than one target.",
|
|
139
|
+
)
|
|
140
|
+
seen.add(sym_pin)
|
|
141
|
+
clean_pins.append({"symPin": sym_pin, "cellPin": cell_pin})
|
|
142
|
+
filters = link.get("cellFilters", [])
|
|
143
|
+
if not isinstance(filters, list):
|
|
144
|
+
raise ContractError("invalid_celllink", "cellFilters must be a list.")
|
|
145
|
+
for pattern in filters:
|
|
146
|
+
_need_str(pattern, "cellFilters entry")
|
|
147
|
+
overrides = link.get("variantOverrides", {})
|
|
148
|
+
if not isinstance(overrides, dict):
|
|
149
|
+
raise ContractError("invalid_celllink", "variantOverrides must be a mapping.")
|
|
150
|
+
clean_overrides = {}
|
|
151
|
+
for name, override in overrides.items():
|
|
152
|
+
_need_nonempty_str(name, "variant name")
|
|
153
|
+
if name == REFERENCE:
|
|
154
|
+
raise ContractError(
|
|
155
|
+
"invalid_celllink",
|
|
156
|
+
f"Variant {REFERENCE!r} is reserved for the stored base link.",
|
|
157
|
+
)
|
|
158
|
+
if not isinstance(override, dict):
|
|
159
|
+
raise ContractError(
|
|
160
|
+
"invalid_celllink", f"Variant {name!r} must be a mapping."
|
|
161
|
+
)
|
|
162
|
+
for key in override:
|
|
163
|
+
if key not in _OVERRIDE_KEYS:
|
|
164
|
+
raise ContractError(
|
|
165
|
+
"invalid_celllink", f"Unknown variant key: {key}."
|
|
166
|
+
)
|
|
167
|
+
clean = {}
|
|
168
|
+
if "cell" in override:
|
|
169
|
+
clean["cell"] = _need_nonempty_str(
|
|
170
|
+
override["cell"], f"variant {name!r} cell"
|
|
171
|
+
)
|
|
172
|
+
pin_edits = override.get("pinEdits", {})
|
|
173
|
+
if not isinstance(pin_edits, dict):
|
|
174
|
+
raise ContractError(
|
|
175
|
+
"invalid_celllink", f"Variant {name!r} pinEdits must be a mapping."
|
|
176
|
+
)
|
|
177
|
+
clean_edits = {}
|
|
178
|
+
for sym_pin, cell_pin in pin_edits.items():
|
|
179
|
+
_need_nonempty_str(sym_pin, f"variant {name!r} symPin")
|
|
180
|
+
_need_nonempty_str(cell_pin, f"variant {name!r} cellPin")
|
|
181
|
+
clean_edits[sym_pin] = cell_pin
|
|
182
|
+
clean["pinEdits"] = clean_edits
|
|
183
|
+
clean_overrides[name] = clean
|
|
184
|
+
return {
|
|
185
|
+
"cell": cell,
|
|
186
|
+
"pinMap": clean_pins,
|
|
187
|
+
"cellFilters": list(filters),
|
|
188
|
+
"variantOverrides": clean_overrides,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _block_link(block):
|
|
193
|
+
if not isinstance(block, dict):
|
|
194
|
+
raise ContractError(
|
|
195
|
+
"invalid_celllink", f"Block must be a mapping, got {block!r}."
|
|
196
|
+
)
|
|
197
|
+
link = block.get("CellLink", {})
|
|
198
|
+
return validate_celllink(link)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _override_for(link, variant):
|
|
202
|
+
if variant is None or variant == REFERENCE:
|
|
203
|
+
return None
|
|
204
|
+
if not isinstance(variant, str) or not variant:
|
|
205
|
+
raise ContractError(
|
|
206
|
+
"unknown_variant", f"Variant must be a non-empty string, got {variant!r}."
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
return link["variantOverrides"][variant]
|
|
210
|
+
except KeyError:
|
|
211
|
+
known = sorted(link["variantOverrides"])
|
|
212
|
+
raise ContractError(
|
|
213
|
+
"unknown_variant",
|
|
214
|
+
f"Unknown variant {variant!r}; known variants: {known}.",
|
|
215
|
+
) from None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _effective_pin_map(link, override):
|
|
219
|
+
effective = {entry["symPin"]: entry["cellPin"] for entry in link["pinMap"]}
|
|
220
|
+
if override is not None:
|
|
221
|
+
effective.update(override["pinEdits"])
|
|
222
|
+
ordered = []
|
|
223
|
+
for entry in link["pinMap"]:
|
|
224
|
+
ordered.append(
|
|
225
|
+
{"symPin": entry["symPin"], "cellPin": effective[entry["symPin"]]}
|
|
226
|
+
)
|
|
227
|
+
base_pins = {entry["symPin"] for entry in link["pinMap"]}
|
|
228
|
+
for sym_pin in sorted(effective):
|
|
229
|
+
if sym_pin not in base_pins:
|
|
230
|
+
ordered.append({"symPin": sym_pin, "cellPin": effective[sym_pin]})
|
|
231
|
+
return ordered
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def resolve_pin(block, sym_pin, variant=None):
|
|
235
|
+
"""Resolve one symbol pin to its cell pin (the pin chokepoint).
|
|
236
|
+
|
|
237
|
+
Explicit pinMap entries win, variant pinEdits overlay them,
|
|
238
|
+
and unlisted pins resolve to themselves (identity default).
|
|
239
|
+
A block without a CellLink resolves every pin to itself.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
block: Block mapping carrying the CellLink component.
|
|
243
|
+
sym_pin: Symbol pin to resolve.
|
|
244
|
+
variant: Variant name selecting its overrides; None and
|
|
245
|
+
REFERENCE resolve the stored base link.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Resolved cell-pin name.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ContractError: invalid_celllink for a malformed block or
|
|
252
|
+
link, unknown_variant for a variant the link does not
|
|
253
|
+
define.
|
|
254
|
+
"""
|
|
255
|
+
_need_nonempty_str(sym_pin, "sym_pin")
|
|
256
|
+
link = _block_link(block)
|
|
257
|
+
override = _override_for(link, variant)
|
|
258
|
+
if override is not None and sym_pin in override["pinEdits"]:
|
|
259
|
+
return override["pinEdits"][sym_pin]
|
|
260
|
+
for entry in link["pinMap"]:
|
|
261
|
+
if entry["symPin"] == sym_pin:
|
|
262
|
+
return entry["cellPin"]
|
|
263
|
+
return sym_pin
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def resolve_cell(block, variant=None):
|
|
267
|
+
"""Resolve the cell implementing a block under a variant.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
block: Block mapping carrying the CellLink component.
|
|
271
|
+
variant: Variant name selecting its overrides; None and
|
|
272
|
+
REFERENCE resolve the stored base link.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Effective cell text ("" when the block is unlinked).
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ContractError: invalid_celllink for a malformed block or
|
|
279
|
+
link, unknown_variant for a variant the link does not
|
|
280
|
+
define.
|
|
281
|
+
"""
|
|
282
|
+
link = _block_link(block)
|
|
283
|
+
override = _override_for(link, variant)
|
|
284
|
+
if override is not None and "cell" in override:
|
|
285
|
+
return override["cell"]
|
|
286
|
+
return link["cell"]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def resolve_link(block, variant=None):
|
|
290
|
+
"""Resolve the full (cell, pinMap, variant) link for a block.
|
|
291
|
+
|
|
292
|
+
The variant selector echoes through verbatim so run/export
|
|
293
|
+
records cite which resolution they carry. Resolution copies:
|
|
294
|
+
the stored link is never mutated.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
block: Block mapping carrying the CellLink component.
|
|
298
|
+
variant: Variant name selecting its overrides; None and
|
|
299
|
+
REFERENCE resolve the stored base link.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Mapping with the effective cell, the ordered effective
|
|
303
|
+
pinMap (base order kept, edits applied in place, added
|
|
304
|
+
pins appended sorted), and the variant selector.
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
ContractError: invalid_celllink for a malformed block or
|
|
308
|
+
link, unknown_variant for a variant the link does not
|
|
309
|
+
define.
|
|
310
|
+
"""
|
|
311
|
+
link = _block_link(block)
|
|
312
|
+
override = _override_for(link, variant)
|
|
313
|
+
cell = link["cell"]
|
|
314
|
+
if override is not None and "cell" in override:
|
|
315
|
+
cell = override["cell"]
|
|
316
|
+
return {
|
|
317
|
+
"cell": cell,
|
|
318
|
+
"pinMap": _effective_pin_map(link, override),
|
|
319
|
+
"variant": variant,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _wildcard_to_regex(pattern):
|
|
324
|
+
parts = []
|
|
325
|
+
for char in pattern:
|
|
326
|
+
if char == "*":
|
|
327
|
+
parts.append(".*")
|
|
328
|
+
elif char == "?":
|
|
329
|
+
parts.append(".")
|
|
330
|
+
else:
|
|
331
|
+
parts.append(re.escape(char))
|
|
332
|
+
return "".join(parts)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _match_part(pattern, text):
|
|
336
|
+
return (
|
|
337
|
+
re.fullmatch(_wildcard_to_regex(pattern), text, flags=re.IGNORECASE)
|
|
338
|
+
is not None
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def match_cell_filters(cell, filters):
|
|
343
|
+
"""Match a cell against advisory KiCad chooser filters.
|
|
344
|
+
|
|
345
|
+
Empty filters match everything; otherwise entries combine
|
|
346
|
+
with OR. Matching is case-insensitive and anchored (a bare
|
|
347
|
+
word is not a substring — say ``*word*``). ``:`` splits the
|
|
348
|
+
library from the cell: a pattern without a library part
|
|
349
|
+
matches the cell part only, while ``lib:cell`` requires both
|
|
350
|
+
halves to match. Spaces sanitize to ``_`` on both sides.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
cell: Opaque cell text to test.
|
|
354
|
+
filters: Advisory glob list from the CellLink.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
True when the cell passes the chooser filters.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
ContractError: invalid_celllink for a non-string cell or
|
|
361
|
+
a malformed filter list.
|
|
362
|
+
"""
|
|
363
|
+
_need_str(cell, "cell")
|
|
364
|
+
if not isinstance(filters, list):
|
|
365
|
+
raise ContractError("invalid_celllink", "cellFilters must be a list.")
|
|
366
|
+
if not filters:
|
|
367
|
+
return True
|
|
368
|
+
candidate = cell.replace(" ", "_")
|
|
369
|
+
cand_lib, _, cand_cell = candidate.partition(":")
|
|
370
|
+
if ":" not in candidate:
|
|
371
|
+
cand_lib, cand_cell = "", candidate
|
|
372
|
+
for pattern in filters:
|
|
373
|
+
_need_str(pattern, "cellFilters entry")
|
|
374
|
+
scrubbed = pattern.replace(" ", "_")
|
|
375
|
+
if ":" in scrubbed:
|
|
376
|
+
pat_lib, _, pat_cell = scrubbed.partition(":")
|
|
377
|
+
if _match_part(pat_lib, cand_lib) and _match_part(pat_cell, cand_cell):
|
|
378
|
+
return True
|
|
379
|
+
elif _match_part(scrubbed, cand_cell):
|
|
380
|
+
return True
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def collect_erc(link):
|
|
385
|
+
"""Collect the ERC view of a link: cell text plus filters.
|
|
386
|
+
|
|
387
|
+
ERC checks read this; they never enforce it — collection
|
|
388
|
+
reports unknown cells and filters as-is instead of failing.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
link: CellLink mapping to collect from.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Mapping with the cell text and the advisory filters.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
ContractError: invalid_celllink for a malformed link.
|
|
398
|
+
"""
|
|
399
|
+
clean = validate_celllink(link)
|
|
400
|
+
return {"cell": clean["cell"], "filters": clean["cellFilters"]}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def resolve_run(blocks, known_cells, variant=None):
|
|
404
|
+
"""Resolve run/export records for block instances.
|
|
405
|
+
|
|
406
|
+
Resolves ``(cell, pinMap, variant)`` per block and emits only
|
|
407
|
+
the non-empty links: blocks without a CellLink, or with an
|
|
408
|
+
empty cell, are skipped. A resolved cell outside known_cells
|
|
409
|
+
fails the run with a named reason — never at edit time.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
blocks: Block mappings, each with an id and an optional
|
|
413
|
+
CellLink component.
|
|
414
|
+
known_cells: Available cell names the run may target.
|
|
415
|
+
variant: Variant name applied to every block; None and
|
|
416
|
+
REFERENCE resolve each stored base link.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Emission records in input order, each with block, cell,
|
|
420
|
+
pinMap, and variant keys.
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ContractError: invalid_block for a malformed block
|
|
424
|
+
entry, unknown_variant for a variant a link does not
|
|
425
|
+
define.
|
|
426
|
+
CellLinkError: unknown_cell naming the block and cell
|
|
427
|
+
when a resolved cell is not available to the run.
|
|
428
|
+
"""
|
|
429
|
+
if not isinstance(blocks, (list, tuple)):
|
|
430
|
+
raise ContractError("invalid_block", "Blocks must be a list.")
|
|
431
|
+
available = set(known_cells)
|
|
432
|
+
records = []
|
|
433
|
+
for block in blocks:
|
|
434
|
+
if not isinstance(block, dict):
|
|
435
|
+
raise ContractError(
|
|
436
|
+
"invalid_block", f"Block must be a mapping, got {block!r}."
|
|
437
|
+
)
|
|
438
|
+
block_id = block.get("id")
|
|
439
|
+
if not isinstance(block_id, str) or not block_id:
|
|
440
|
+
raise ContractError(
|
|
441
|
+
"invalid_block", "Each block needs a non-empty string id."
|
|
442
|
+
)
|
|
443
|
+
resolved = resolve_link(block, variant=variant)
|
|
444
|
+
if not resolved["cell"]:
|
|
445
|
+
continue
|
|
446
|
+
if resolved["cell"] not in available:
|
|
447
|
+
raise CellLinkError(
|
|
448
|
+
"unknown_cell",
|
|
449
|
+
f"Block {block_id!r} links unknown cell "
|
|
450
|
+
f"{resolved['cell']!r} (variant {variant!r}); "
|
|
451
|
+
"link it to an available cell and rerun.",
|
|
452
|
+
)
|
|
453
|
+
records.append(
|
|
454
|
+
{
|
|
455
|
+
"block": block_id,
|
|
456
|
+
"cell": resolved["cell"],
|
|
457
|
+
"pinMap": resolved["pinMap"],
|
|
458
|
+
"variant": resolved["variant"],
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
return records
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Strict YAML 1.2 validation (I01). Gate every external byte here."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from ruamel.yaml import YAML
|
|
6
|
+
|
|
7
|
+
from .errors import ContractError
|
|
8
|
+
|
|
9
|
+
MAX_DOCUMENT_BYTES = 1_000_000
|
|
10
|
+
MAX_NESTING_DEPTH = 100
|
|
11
|
+
|
|
12
|
+
REQUIRED_TOP_LEVEL = ("schema", "kind", "id", "revision", "scope", "description")
|
|
13
|
+
ALLOWED_TOP_LEVEL = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"schema", "kind", "id", "revision", "scope", "description",
|
|
16
|
+
"domains", "ports", "parameters", "models", "top", "pdks",
|
|
17
|
+
"constraints", "schematic", "layout", "symbol", "source",
|
|
18
|
+
"objects", "cross_sections", "files", "decks", "license",
|
|
19
|
+
"origin", "tags",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _reject_composition_tricks(node):
|
|
25
|
+
"""Anchors, aliases, and merge keys are forbidden (I01)."""
|
|
26
|
+
if type(node).__name__ == "AliasNode" or getattr(node, "anchor", None):
|
|
27
|
+
raise ContractError("invalid_yaml", "Anchors and aliases are not allowed.")
|
|
28
|
+
value = getattr(node, "value", None)
|
|
29
|
+
if isinstance(value, list) and value and isinstance(value[0], tuple):
|
|
30
|
+
for key, item in value:
|
|
31
|
+
if getattr(key, "value", None) == "<<":
|
|
32
|
+
raise ContractError("invalid_yaml", "Merge keys are not allowed.")
|
|
33
|
+
_reject_composition_tricks(key)
|
|
34
|
+
_reject_composition_tricks(item)
|
|
35
|
+
elif isinstance(value, list):
|
|
36
|
+
for item in value:
|
|
37
|
+
_reject_composition_tricks(item)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _reject_nonfinite_and_depth(data, depth=0):
|
|
41
|
+
if depth > MAX_NESTING_DEPTH:
|
|
42
|
+
raise ContractError("invalid_yaml", "Document nesting exceeds the limit.")
|
|
43
|
+
if isinstance(data, float) and (math.isnan(data) or math.isinf(data)):
|
|
44
|
+
raise ContractError("invalid_yaml", "Non-finite scalars are not allowed.")
|
|
45
|
+
if isinstance(data, dict):
|
|
46
|
+
for item in data.values():
|
|
47
|
+
_reject_nonfinite_and_depth(item, depth + 1)
|
|
48
|
+
elif isinstance(data, list):
|
|
49
|
+
for item in data:
|
|
50
|
+
_reject_nonfinite_and_depth(item, depth + 1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_document(text):
|
|
54
|
+
"""Parse and strictly validate one YAML record.
|
|
55
|
+
|
|
56
|
+
Rejects anchors, aliases, merge keys, non-finite scalars,
|
|
57
|
+
over-deep nesting, oversize bytes, missing required fields, and
|
|
58
|
+
unknown fields — gate every external byte here.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
text: YAML record text to validate.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Validated record mapping.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ContractError: invalid_yaml for any rejection.
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(text, str):
|
|
70
|
+
raise ContractError("invalid_yaml", "Document must be text.")
|
|
71
|
+
if len(text.encode("utf-8")) > MAX_DOCUMENT_BYTES:
|
|
72
|
+
raise ContractError("invalid_yaml", "Document exceeds the size limit.")
|
|
73
|
+
yaml = YAML(typ="safe")
|
|
74
|
+
try:
|
|
75
|
+
node = yaml.compose(text)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
raise ContractError("invalid_yaml", f"YAML composition failed: {exc}") from exc
|
|
78
|
+
_reject_composition_tricks(node)
|
|
79
|
+
try:
|
|
80
|
+
data = yaml.load(text)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
raise ContractError("invalid_yaml", f"YAML load failed: {exc}") from exc
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
raise ContractError("invalid_yaml", "Top level must be a mapping.")
|
|
85
|
+
for key in REQUIRED_TOP_LEVEL:
|
|
86
|
+
if key not in data:
|
|
87
|
+
raise ContractError("invalid_yaml", f"Missing required field: {key}.")
|
|
88
|
+
for key in data:
|
|
89
|
+
if key not in ALLOWED_TOP_LEVEL:
|
|
90
|
+
raise ContractError("invalid_yaml", f"Unknown field: {key}.")
|
|
91
|
+
_reject_nonfinite_and_depth(data)
|
|
92
|
+
return data
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""v0 envelope contracts: the typed shapes of the v0 wire.
|
|
2
|
+
|
|
3
|
+
SubmitCommand, SubmitAck, EngineEvent, FindingPage, Finding, and
|
|
4
|
+
ErrorPayload are the data contracts every gateway speaks today.
|
|
5
|
+
They live here (contracts, no I/O) so gateways, generators, and
|
|
6
|
+
tests share one definition instead of re-declaring the shapes.
|
|
7
|
+
|
|
8
|
+
V0_TO_V1_FIELDS maps each envelope's v0 field names to the v1
|
|
9
|
+
catalog names (contracts/generated/engine-v1.expanded.schema.json;
|
|
10
|
+
v1 design-frozen, runtime migration pending). None marks a field
|
|
11
|
+
v1 restructures rather than renames — the migration owns those,
|
|
12
|
+
never a silent repurpose. Identity entries document that the name
|
|
13
|
+
survives the migration.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SubmitCommand(TypedDict, total=False):
|
|
20
|
+
"""One admitted call: who, what, with what, under which id."""
|
|
21
|
+
|
|
22
|
+
project_id: str
|
|
23
|
+
operation: str
|
|
24
|
+
arguments: Dict[str, Any]
|
|
25
|
+
request_id: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SubmitAck(TypedDict, total=False):
|
|
29
|
+
"""Admission ack: the execution id plus where it admitted."""
|
|
30
|
+
|
|
31
|
+
run_id: str
|
|
32
|
+
snapshot_id: str
|
|
33
|
+
state: str
|
|
34
|
+
approval_state: str
|
|
35
|
+
approval_id: str
|
|
36
|
+
execution_id: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EngineEvent(TypedDict, total=False):
|
|
40
|
+
"""One retained project event, seq-ordered by cursor."""
|
|
41
|
+
|
|
42
|
+
seq: int
|
|
43
|
+
type: str
|
|
44
|
+
actor: str
|
|
45
|
+
data: Dict[str, Any]
|
|
46
|
+
revision: str
|
|
47
|
+
event_cursor: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Finding(TypedDict, total=False):
|
|
51
|
+
"""One judged violation or observation on a completed run."""
|
|
52
|
+
|
|
53
|
+
rule: str
|
|
54
|
+
severity: str
|
|
55
|
+
message: str
|
|
56
|
+
target: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FindingPage(TypedDict, total=False):
|
|
60
|
+
"""One page of findings, errors first, with counts and cursor."""
|
|
61
|
+
|
|
62
|
+
total_counts: Dict[str, int]
|
|
63
|
+
findings: List[Finding]
|
|
64
|
+
next_cursor: Optional[str]
|
|
65
|
+
truncated: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ErrorPayload(TypedDict, total=False):
|
|
69
|
+
"""Wire error: stable code, message, itemized fields when named."""
|
|
70
|
+
|
|
71
|
+
code: str
|
|
72
|
+
message: str
|
|
73
|
+
fields: List[str]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#: v0 envelope -> {v0 field: v1 field or None when restructured}.
|
|
77
|
+
V0_TO_V1_FIELDS = {
|
|
78
|
+
"SubmitCommand": {
|
|
79
|
+
"project_id": "project_id",
|
|
80
|
+
"operation": "task",
|
|
81
|
+
"arguments": "options",
|
|
82
|
+
"request_id": "request_id",
|
|
83
|
+
},
|
|
84
|
+
"SubmitAck": {
|
|
85
|
+
"run_id": "run_id",
|
|
86
|
+
"snapshot_id": "snapshot",
|
|
87
|
+
"state": "state",
|
|
88
|
+
"approval_state": None,
|
|
89
|
+
"approval_id": None,
|
|
90
|
+
"execution_id": None,
|
|
91
|
+
},
|
|
92
|
+
"EngineEvent": {
|
|
93
|
+
"seq": None,
|
|
94
|
+
"type": None,
|
|
95
|
+
"actor": "actor",
|
|
96
|
+
"data": "data",
|
|
97
|
+
"revision": None,
|
|
98
|
+
"event_cursor": "event_cursor",
|
|
99
|
+
},
|
|
100
|
+
"Finding": {
|
|
101
|
+
"rule": "rule",
|
|
102
|
+
"severity": "severity",
|
|
103
|
+
"message": "message",
|
|
104
|
+
"target": "target",
|
|
105
|
+
},
|
|
106
|
+
"FindingPage": {
|
|
107
|
+
"total_counts": None,
|
|
108
|
+
"findings": "findings",
|
|
109
|
+
"next_cursor": "cursor",
|
|
110
|
+
"truncated": None,
|
|
111
|
+
},
|
|
112
|
+
"ErrorPayload": {
|
|
113
|
+
"code": "code",
|
|
114
|
+
"message": "message",
|
|
115
|
+
"fields": "details",
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def v1_field(envelope, v0_field):
|
|
121
|
+
"""v1 name for one v0 envelope field, None when restructured.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
envelope: Envelope name (SubmitCommand, SubmitAck,
|
|
125
|
+
EngineEvent, Finding, FindingPage, ErrorPayload).
|
|
126
|
+
v0_field: v0 field name.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
v1 field name, or None where v1 restructures the field.
|
|
130
|
+
"""
|
|
131
|
+
return V0_TO_V1_FIELDS[envelope][v0_field]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Error type for contract violations. Carries a stable machine code."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContractError(Exception):
|
|
5
|
+
"""Contract violation with a stable machine code.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
code: Stable machine-readable error code.
|
|
9
|
+
message: Human-readable detail.
|
|
10
|
+
fields: Itemized fields, when the error names any.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, code, message, fields=None):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.code = code
|
|
16
|
+
self.fields = fields or []
|