Skip to content

Hold closure, CTS drive selection and antenna repair

This stream adds a typed netlist ECO, a hold-fix planner driven by RustSTA 0.4 reports, per-level clock-buffer masters, a native LEF process-antenna check with diode repair, and a bounded closure loop. Everything was checked on fixtures/flow/pipeline.v for SKY130 (sky130_fd_sc_hd) and GF180 (gf180mcu_fd_sc_mcu7t5v0). Timing decisions use native SPEF only.

Gates. The loop's final gates (below) require hold >= +0.05 ns and setup >= 0 at every corner, RustSTA and OpenSTA 3.1.0 within 1 ps on the same native SPEF, RustSys check synth, Yosys CEC and 512-vector simulation of the final post-ECO, post-CTS netlist, native-GDS foundry DRC no worse than before the ECO, and planted antenna controls that both the native check and the deck flag and that are then repaired. This is block-level evidence; the scope limits are listed below.

Commands

All four new commands are bounded and deterministic. hold-eco, antenna-eco and apply-eco read and write files only; they never open a workspace. check-antenna opens the workspace read-only.

# Delay-cell profiles from tt/ss/ff Liberty, cell LEF and the cell policy (tracked)
python3 scripts/build_delay_cells.py            # --check verifies the tracked files

# Hold ECO from RustSTA 0.4 reports (target margin 0.05 ns; setup kept >= 0.05 ns at ss)
ruststa -w STA report --section hold_fix --target-margin 0.05 --limit 0 --format json > hold.json
ruststa -w STA report --section endpoints --corner ss --limit 0 --format json > setup_ss.json
rustroad hold-eco --netlist mapped.v --timed-netlist exported.v [--timed-netlist-sha256 HEX] \
  --hold-report hold.json --setup-report setup_ss.json [--setup-report setup_tt.json ...] \
  --hold-corner tt --hold-corner ss --hold-corner ff --setup-corner ss \
  --delay-cells fixtures/pdk/<pdk>-delay-cells.json [--cell-lef LEF] --out eco.v --plan-out plan.json

# Native antenna check (read-only); optional what-if shapes for planted controls
rustroad --workspace W check-antenna [--extra-shapes shapes.json] [--safety-margin 0.10] > antenna.json
rustroad antenna-eco --netlist mapped.v --antenna-report antenna.json \
  --delay-cells fixtures/pdk/<pdk>-delay-cells.json --out eco.v --plan-out plan.json

# Diode-free antenna repair of HEAD by layer jumpers (mutating; new revision)
rustroad --workspace W repair-antenna --base-rev revN [--antenna-report antenna.json] \
  [--safety-margin 0.10] [--max-jumpers-per-net 16] [--max-positions 512] [--max-candidates 32]

# Apply a given rustroad.eco-plan.v1 (used for the controls)
rustroad apply-eco --netlist mapped.v --plan plan.json --delay-cells profile.json [--cell-lef LEF] --out eco.v

# Closure loop and evidence (pinned RustSTA copy in .work/tools/sta, see below)
systemd-run --user --scope -p MemoryMax=8G -p MemorySwapMax=2G /usr/bin/time -v -o run.time \
  timeout 5400s python3 scripts/hold_closure.py --run .work/evidence/final --base-rev empty \
  --signoff --antenna-controls --openrcx
python3 scripts/cts_drive_eval.py --run .work/evidence/cts-eval
python3 scripts/hold_evidence.py --summary .work/evidence/final/summary.json \
  --cts .work/evidence/cts-eval/cts_drive_eval.json --time run.time --out docs/evidence/hold-closure.json

Pinned RustSTA binaries. These are the RustSTA 0.4.0 fix-round-2 binaries (RustSTA d1f61e6). Copy them from .work/sta-signoff/bin/round2 into this checkout's .work/tools/sta/:

  • ruststa: ac76f0f3765ce3c6d44f85257f7d836120a7e0bf6a7283a75357f37fd79aa838
  • rusteda: c8f71b63124577174f8458870d4ec006ab14b35f3b249e6e9d52a1734e759baf

The shared bin/ directory is replaced by later rounds, so the driver checks the hashes on every run.

ECO primitive (rustroad_formats::eco)

rustroad.eco-plan.v1 holds a list of typed operations:

  • insert_buffer {cell, instance, net, sink{instance,pin}} moves exactly one sink pin behind a new buffer. The old net drives the buffer's input and keeps all its other sinks.
  • insert_diode {cell, instance, net} attaches a diode. net is a scalar wire/port or a vector bit name[i]; a name that matches both forms is refused.

Names must be new, shell-safe handles [A-Za-z][A-Za-z0-9_]*. Existing instances, wires and register Q nets are never renamed, so CEC state matching by Q wire name is unaffected. A buffer cell is accepted only if its profiled Liberty function (whitespace and enclosing parentheses removed) is exactly its single input pin. An inverting, multi-input or unprofiled cell gives ECO_NOT_BUFFER. Every operation is checked against a bound (max_operations 4096) and the Verilog AST limits.

Grouped macro ports require --cell-buses buses.json on hold-eco and apply-eco. This explicit schema is { "master": { "port": [msb, lsb] } }; bracketed scalar port names are never guessed to be buses. The original LEF must describe every member with one uniform direction, and a moved sink must still be an input. The planner binds report names such as mem/A_DIN[0] to the corresponding bit of the grouped .A_DIN(...) connection. It rewrites only that bit, preserving the grouped port and its siblings. Both descending and ascending declarations, exact-width slices, concatenations and sized constants are supported; unsupported widths or expressions are rejected.

buffer_sinks also follows immutable internal scalar/vector assign aliases when explicit bus metadata is supplied. Escaped bracketed scalar names remain distinct from actual vector bits. The index uses compact IDs, memoized roots, cycle and multiple-driver checks, a 64 MiB memory cap and bounded work; output port assignments remain roots because an ECO can move them. The alias assigns themselves are retained. Schemas are capped at 256 masters, 65,536 member pins and 2 MiB JSON, and expression projection has a separate 64 MiB allowance. The input receipt records the schema hash. These additions retain the spef_rc, propagated-clock and revision-provenance gates for hold planning.

Delay-cell profiles (fixtures/pdk/<pdk>-delay-cells.json)

scripts/build_delay_cells.py reads the three owner-fixed corner Liberty files with a clean-room Liberty group reader (scripts/liberty_lite.py).

  • Candidates are the cell policy's post_synthesis_insertable.cts_hold globs minus the clock-tree cells (clkbuf*, clkinv*), plus the logic buffers buf_1 and buf_2.
  • A cell is kept only if, in every corner, it has exactly one input and one output, an identity function, and a positive_unate combinational arc.
  • Delays are bilinear lookups with no extrapolation, at sky130 0.05 ns / 5 fF and gf180 0.15 ns / 10 fF.
  • Antenna cells are LEF ANTENNACELL masters allowed as physical_only (diode_2, gf180 antenna), stored with their LEF diffusion and gate areas.
  • Each excluded cell is listed with its reason.

The result is 16 sky130 and 14 gf180 buffers. The sky130 values match the earlier black-box recon tables, for example dlygate4sd1_1 at tt/ss/ff: 0.205/0.239, 0.385/0.485, 0.138/0.158 ns.

Hold planner (rustroad hold-eco)

Report requirements. The planner refuses a report if any of these fails:

  • the wire model is spef_rc and the clock model is propagated;
  • the report's target_margin_ns is at least the requested margin;
  • rows are not truncated (listed == shown);
  • every requested hold corner is covered;
  • the setup corner has an endpoints report;
  • slacks that appear in more than one report agree;
  • each endpoint instance and pin exists in the netlist.

Report binding (hardening, stream gate). RustSTA reports name their workspace and revision but carry no hash of the netlist they timed. That record is in the timing engine's own revision metadata, so hold-eco binds the reports to it and refuses a mismatch with STALE_REPORT (never INVALID_WORKSPACE; the file-only commands report their own codes):

  • Provenance. The revision the reports name (<workspace>/revisions/rev<NNNN>/meta.json, schema rusteda.revision.v1, read under a shared flock on the STA workspace's workspace.lock) must record exactly one netlist source, and its xxh3-64 must equal that of the bytes hold-eco parses as --timed-netlist. The hash is the binding; the recorded path is reported (source_path_matches) but a copy with the same bytes is accepted. A missing, unreadable, malformed or ambiguous record (no or several netlist sources, a record of another revision, a revision handle other than rev<N>) fails REPORT_PROVENANCE; a writer holding the lock for more than about 2 s fails STA_WORKSPACE_LOCKED. Committed revisions are immutable; nothing in the STA workspace is written.
  • --timed-netlist is the netlist the timing engine imported (the post-CTS export). With --timed-netlist-sha256 (scripts/hold_closure.py records the hash when it hands the file to rusteda import) a different file is refused;
  • every hold_fix row must match the timed netlist: endpoint instance and master, endpoint net, net driver and every worst-path stage pin/net (assign aliases are merged);
  • every endpoints row must name a pin or port of the timed netlist, and all reports must come from one STA workspace revision;
  • the edited netlist must be refined by the timed netlist: same instances and masters, and every changed connection reaches the original net through inserted buffers (clock tree) or tie cells. An ECO-prefixed instance present only in the timed netlist means the reports belong to a later iteration.

The phase-B1 counterexample (iteration-0 reports against the iteration-1 netlist) fails with STALE_REPORT. So does the round-1 verifier's counterexample, which the content checks alone accepted: the iteration-1 reports (whose rows also exist in the iteration-0 netlist) with the iteration-0 netlist and its own correct sha256. On the gate run's data:

STALE_REPORT: the reports come from .../sky130_i1_sta_ws rev2, which timed
.../sky130_i1_exported.v (xxh3:ccbbad2cbcf4f9d8); --timed-netlist
.../sky130_i0_exported.v is xxh3:5e03463facbccb2b: the reports were not
produced from this netlist

(gf180: xxh3:3a80821eca7dfeba vs xxh3:7fada0d8136b8ec9.) The matching pairing reproduces the gate's iteration-1 netlist byte for byte on both PDKs. The plan records the binding (inputs.binding: sta_record with the revision record, its sha256, the recorded netlist source and both xxh3 values; revision; rows and stages bound; inserted instances).

Sink pins. insert_buffer (hold-eco and apply-eco) accepts only a sink pin whose cell-LEF DIRECTION is INPUT. The directions come from the profile's cell_lef source, re-verified by sha256 (--cell-lef may only relocate the same file). An output, inout or unknown pin fails with ECO_INVALID; the phase-B1 r0/Q plan is now refused.

Chain selection. For each sink below the margin the planner needs margin - slack at every hold corner where the sink is listed. It enumerates multisets of up to --max-chain profiled cells, at most --max-combinations. It picks the smallest total area, then the fewest cells, then names. The hold side is credited with min(rise, fall) at each corner. The setup side is charged max(rise, fall) at the setup corner, and the estimated setup slack must stay at or above --setup-margin.

Existing chains. Earlier rr_eco_hold_* cells that already feed a sink are found by walking back along the chain. New cells extend that chain, up to --max-total-chain.

Path-aware mode (--pins-report, closure stream). With per-pin setup slack reports of the same STA revision (bound like the other reports: pin_rows_bound), the chain goes on the endpoint's worst hold path instead of in front of the endpoint: on a sink pin of the path, or in front of every input-pin sink of one small net (--max-group-sinks, e.g. an input port), wherever the setup slack of every path through the point (and of the delay already planned on the same path) leaves room at every setup corner; a point shared by several violating endpoints is credited to all of them. --allow-partial applies the feasible part (status partial) and lists the infeasible endpoints with the setup budget of each point of their path. Details and the ranking in docs/closure.md. Without --pins-report the planner below is unchanged (same netlists; the plan JSON carries three more parameter fields).

Infeasible cases. These write the plan with status infeasible and exit 2 with HOLD_ECO_INFEASIBLE:

  • no chain meets the need without breaking the setup margin;
  • the chain would exceed its length cap;
  • the violating endpoint is a top-level port (endpoint planner; the path-aware planner can delay an upstream pin of its path instead).

The estimate is not a timing result. The loop re-times every ECO with new parasitics.

Per-level CTS masters

clock.level_cells in the physical profile is optional. Index 0 is the level that drives the sinks; levels past the end of the list reuse its last entry. Without the field nothing changes: tracked profiles and existing workspaces are unaffected.

  • Import keeps every configured master.
  • Placement uses each master's own size.
  • validate_tree checks each buffer's master against its level.
  • SDC set_max_fanout still caps branching.

LEF ANTENNACELL diodes may stay on the clock source net as passive loads. They still count toward the fanout limit.

Hilbert clustering and buffer-tree regrouping

clock.clustering is raster (default, unchanged) or hilbert. Raster order sorts sinks by row then x, so a group of max_fanout sinks can span the whole die width. Hilbert order sorts by position on a square Hilbert curve over the sink bounding box, so consecutive sinks, and therefore each group, stay compact at every level. In Hilbert mode the placement seed drops the CTS buffers, which then start from their own pins' centroid.

clock.regroup_prefixes (Hilbert only, at most 16 names) names non-clock buffer trees written as <label>_<depth>_<index>, depth 0 driving the sinks. Buffers of one label and depth carry the same signal, so their sinks may be exchanged freely. From the leaves up, each level's sinks are re-split into contiguous Hilbert chunks, keeping every buffer's sink count. Buffer inputs are never changed. A level is skipped when a buffer has other than one output, its output net is a port, appears in an assign, or is used through a bit/part-select or concatenation. Connection work is capped at 4M. The regrouped names are listed in the clock report as regrouped.

CTS drive evaluation (scripts/cts_drive_eval.py)

The script builds iteration 0 with alternative per-level clock-buffer masters and reports, at ss+max in RustSTA, each variant's clock-sink latency, clock slew and worst hold slack, and whether the closure loop then closes. Its receipt is folded into docs/evidence/hold-closure.json, kept as historical evidence. The tracked profiles keep clkbuf_1; the stronger GF180 variants are limited by the lattice router's per-sink search budget (router owner).

Native antenna check (rustroad check-antenna)

LEF data. The LEF reader keeps the antenna statements exactly in LefDocument.antenna:

  • THICKNESS;
  • the OXIDE1 AREA/SIDE and DIFF ratios, as scalars or PWL;
  • GATEPLUSDIFF;
  • the cumulative and AREAMINUSDIFF forms;
  • pin ANTENNAGATEAREA and ANTENNADIFFAREA, with an optional LAYER.

Other oxide models, unknown forms and inexact values go to unsupported. They never fail the import, and the import diagnostics are unchanged.

Model. The check re-reads the workspace's own LEF sources and verifies them by sha256. The LEF/DEF 5.8 partial-net model is applied bottom-up over the LEF layer stack:

  • at a routing layer, the partial net is the conductors at or below it plus the cuts below it;
  • at a cut layer, it is the routing layers below it plus the cuts up to it;
  • the conductors are wires (with LEF width), vias, same-net patches and the routing-layer pin shapes.

For each component that touches a gate:

  • ratio = (union side area, i.e. perimeter x THICKNESS, or union area) / (gate area + GATEPLUSDIFF x diffusion area);
  • the limit is the DIFF rule evaluated at the connected diffusion area. With no diffusion, the non-DIFF rule applies, or the DIFF rule at zero when the LEF states only that one (sky130 metals, gf180 Metal2 and above). With diffusion but only a non-DIFF rule (gf180 Metal1), the non-DIFF rule applies, which ignores the diode's protection and is conservative.

Verdict. Cumulative or AREAMINUSDIFF rules, other oxides, or LAYER-qualified pin areas make the verdict partial; none of these occurs in either PDK. Top ports carry no gate or diffusion area. Cell-internal metal beyond the pin shapes is not modeled.

Safety margin (hardening, stream gate). The native ratio reads low against the deck (Magic) on the measured sky130 met3 cases. A row is therefore violated when ratio > (1 - m) x limit with m = --safety-margin (default 0.10, above the measured under-read); exceeds_lef_limit keeps the raw comparison, effective_limit the threshold, and the report carries safety_margin and lef_violations. antenna-eco sizes diodes against the same effective limit and carries a partial input forward (status partial, input_status, diagnostic ANTENNA_INPUT_PARTIAL) instead of reporting clean. The foundry deck stays the signoff gate; scripts/rtl2gds.py re-measures the margin on a planted wire in every run (native/(1-m) vs the Magic ratio, docs/rtl2gds.md).

Budgets. max_shapes_per_net, max_pair_checks and max_union_cells bound the work.

Planted controls

Each control is a wire of minimum I/O-layer width attached to input port a[1] and leaving the die: 600 um of met3 on SKY130, 2000 um of Metal3 on GF180. It is drawn in the native check through --extra-shapes, and in the native GDS through kl_plant.py rect on the layer map's net layer. Both the native check and the deck (SKY130 Magic antennacheck, GF180 KLayout antenna deck) must flag it. The repair runs antenna-eco (a diode_2 on SKY130, an antenna cell on GF180), then a fresh import, place, CTS and route, and then the same plant; both checks must then be clean, and the repaired layout is re-timed at every corner.

Layer jumpers (rustroad repair-antenna)

Why not diodes on sky130. The antenna ECO's sky130_fd_sc_hd__diode_2 cannot pass the frozen LVS setup (E3 uart_tx and alu, root-caused with the pinned image 4c48415d8bd0 on the E3 logs):

  • the reference (powered) netlist is correct: the diode is present with the right connections (DIODE on the port net, VGND/VNB on VSS, VPB/VPWR on VDD), the XOR is clean and Netgen ends with Circuits match uniquely;
  • Netgen prints Cell sky130_fd_sc_hd__diode_2 (0|1) disconnected node: VGND/VPWR/VPB inside the cell comparison, for both netlists: the PDK cell has one device (XD0 VNB DIODE sky130_fd_pr__diode_pw2nd_05v5) and three device-less supply pins. The frozen harness fails any disconnected node (the rule that catches a planted extra pin);
  • the sky130 KLayout deck extracts no device from the diode_2 layout (diff 65/20, nsdm 93/44 and areaid.diode 81/23 are all there; the deck's diode derivations do not recognise it): the extracted subckt is empty, the cell is NoMatch and the top is skipped.

Neither is a netlist error, so the deck/setup stays frozen and the repair avoids diodes.

Method. The native check is per layer and non-cumulative (partial net: the conductors that exist when the layer is etched). A jumper over layer L cuts one straight L run of the violating partial net and bridges the gap on the next routing layer up: two L/L+1 vias and a short L+1 wire (a non-preferred-direction jog between two upper-layer track positions). While L is etched the bridge does not exist, so the gates keep only the L metal between them and the cut; when L+1 is etched the bridge is the only L+1 metal it adds. Netlist and placement are unchanged; only the violating net's routes change.

  • Per violating net, the lowest violating layer first. The net graph holds the layers up to L (wires split at every node, vias, access points and one virtual node per terminal joined to every node inside its pin shapes: the access point is often not on a via). Dijkstra from the gates of the violating partial nets gives the gate side of every straight run (interior nodes without via, access point or branch).
  • Candidates: positions on the upper layer's tracks (lattice steps for the legacy router) inside the run, nearest the gate end first, bridges of one to three pitches; up to 512 positions per run. A candidate must clear every other-net shape (both vias on all their layers and the bridge, spatial index of the router's validation), leave no same-net pin or patch across the cut, and add no same-net rule violation (route_rules: notch spacing, minimum area, holes, floating patches) to the net.
  • Strategy A cuts the longest runs; the first candidate that clears the net wins, else the one with the largest drop. Strategy B (multi-sink trees, where one cut can raise the other side's ratio) isolates every gate of the violating partial net on its nearest run in one batch. A choice is accepted only when the layer's worst ratio over the effective limit drops and no row of the net becomes newly violated; at most 16 jumpers per net.
  • The native margin is kept: acceptance uses the same (1 - 0.10) x limit as check-antenna. The edited routes pass the full routing::validate (other-net spacing, connectivity, same-net rules) before the commit and again inside it. --antenna-report binds a check-antenna report of the base revision (revision, margin and violated row set must equal the recomputed ones, else STALE_REPORT). With nothing violated, no revision is written (status: clean).
  • Not repairable: a violation on the top routing layer (met4 / Metal4: no layer above in the routing profiles), cut-layer rules, runs whose every position is blocked. They are listed in unrepaired with the reason; the closure loop's diode ECO remains the fallback (sky130 LVS then fails, as before).

scripts/hold_closure.py runs repair-antenna right after routing on every layout of the closure loop (legacy and track recipes), so the antenna check, extraction, timing and signoff of that iteration see the jumpered routes.

Tests: rustroad-core jumper::tests (gate-side cut on a routed lattice fixture that re-validates with unchanged wire length; other-net metal on the bridge layer moves the jumper; the cut partial net has no further position; bounds), crates/rustroad-workspace/tests/antenna.rs (layer_jumpers_repair_partial_net_violations_without_diodes: violated -> repaired with M2/M3 jumpers, REVISION_CONFLICT, STALE_REPORT, the committed revision re-validates, check-antenna agrees, netlist unchanged).

Closure loop (scripts/hold_closure.py)

Loop. At most 4 iterations, each recorded:

  1. netlist -> import-pdk -> place -> clock-tree -> route clock -> route all
  2. native extraction at min/nom/max
  3. RustSTA 0.4 with three Liberty corners and three SPEF corners (--require-wire-model spef_rc)
  4. hold_fix report with a 0.05 ns target margin, plus endpoints per corner
  5. native check-antenna
  6. hold-eco while timing is open; antenna-eco only on a timing-closed layout

Repairing antenna on a layout that the next hold ECO will replace was tried first and dropped: the perturbed sky130 layout hit the lattice router's budget.

Corners. The corners are fixed by the owner decision:

  • sky130: tt_025C_1v80+nom, ss_100C_1v60+max, ff_n40C_1v95+min
  • gf180: tt_025C_3v30+nom, ss_125C_3v00+max, ff_n40C_3v60+min

The SDC is cts.sdc with the same post-route lines that verify_routing.py appends. The Liberty files are the unmodified PDK files.

Final gates per PDK:

  • RustSTA and OpenSTA hold >= margin and setup >= 0 at all three corners;
  • per-endpoint parity <= 1 ps on the same native SPEF;
  • RustSys check synth of the RTL against the final exported netlist;
  • Yosys CEC, plus a negative control;
  • 512-vector Verilator simulation;
  • Yosys sat proofs of every inserted buffer master and every clock master at each corner Liberty, each with a wrong-polarity control;
  • a native ECO_NOT_BUFFER refusal of a profile that lists clkinv_1 with its real Liberty function (!A) / (!I), plus a positive control;
  • native antenna passed;
  • with --signoff: native-GDS Magic/KLayout DRC and antenna no worse than iteration 0, and the deck antenna check passing;
  • with --antenna-controls: the planted controls;
  • the frozen hashes of 19 inputs unchanged: Liberty x3, RC profiles x3, OpenRCX rules x3, both SDCs, the delay-cell profile, the cell policy, the routing and physical profiles, the technology/cell LEF, the cell GDS and the GDS layer map.

Receipts and determinism

docs/evidence/hold-closure.json holds the per-iteration slacks, inserted cells, parity, signoff counts and resource receipts of this stream's runs. It is historical evidence kept for reproducibility, not a current benchmark. The determinism check repeats the full run in a different directory and compares netlists, exported/powered Verilog, DEF, routes and min/nom/max SPEF of every iteration byte for byte. The harness fails any process above 2 GiB.

OpenRCX timing on the collinear-merged DEF (its RECT patch lines are dropped first; OpenRCX ignores them anyway) is reported, not gating.

Scope and limitations

  • Block or macro level only. IR, EM, SI, OCV and CPPR are unchecked. Density is not part of this gate.
  • The hold estimates use one representative slew/load point. Final acceptance is always a re-timed, re-extracted layout.
  • Port-endpoint hold ECO is unsupported (explicit infeasible).
  • Antenna diodes can be planned only on nets of the mapped netlist. Nets generated by CTS are reported as infeasible.
  • Antenna model limits:
  • OXIDE1 partial-net only; the cumulative forms are not modeled (neither PDK states them);
  • ports carry no diffusion;
  • cell-internal metal beyond the pin shapes is not modeled;
  • the diode is assumed to join the violating partial net, and the re-layout plus both checks confirm it.
  • OpenRCX reference timing on the merged DEF is reported only, not gated.
  • The lattice router's hard per-sink search budget blocks the stronger GF180 clock-buffer profiles and some layout perturbations (router owner, W15).
  • No reference tool source was read. OpenSTA, Yosys, Verilator, Magic, KLayout, Netgen and OpenRCX ran only as pinned black-box executables.

Large netlists

File-only hold-eco, apply-eco and antenna-eco use the same bounded streaming Verilog reader as physical import. The ordinary 16 MiB, 250,000-cell and 4,096-operation defaults remain in force. Larger inputs require explicit global --max-input-mib, --max-cells, --max-nets and --max-pins allowances, plus --eco-memory-mib and --max-eco-operations on the command.

Before reading, the command checks a conservative allowance for both ASTs, indexes, reports and temporary output against its memory budget and available host RAM. SHA-256 and the timing engine's xxh3 identity are calculated from the same byte stream; the timed source is not retained as a second text buffer. Output Verilog is streamed through a byte- and memory-checked atomic writer. A limit failure leaves no new output. These storage limits do not relax the planner's work, chain-length, timing, provenance or equivalence requirements.