| name | sta-review |
| description | Read Static Timing Analysis reports, triage setup/hold/recovery/removal violations, and propose fixes (RTL pipelining, sizing, useful skew, buffer insertion). Use when the user says "STA", "timing report", "timing violation", "setup fail", "hold fail", "close timing", "WNS", "TNS". |
STA Review
Doctrine (v0.1.50): ๆไฟฎๆณๅฏซ้ฒๅทฅๅ
ท๏ผ่้ๅฏซ้ฒ promptใ
Mandatory program preflight first; AI is the backstop, not the lead.
STA signoff runs in a commercial tool (PrimeTime, Tempus, OpenSTA). The
deterministic triage (5 endpoint categories โ fix strategies) is now in
programs/sta_triage_classify.py; this skill is the wrapper that runs
it, narrates residual interpretation, and refuses to override the
program's category counts.
Mandatory Deterministic Preflight
Before any narrative:
python3 plugins/vibe-ic/programs/sta_report_check.py \
--rpt <sta.rpt> --json /tmp/sta_check.json
python3 plugins/vibe-ic/programs/sta_triage_classify.py \
--endpoints-json <endpoints.json> \
--wns <wns_ns> --tns <tns_ns> \
--out-md /tmp/sta_triage.md --out-json /tmp/sta_triage.json
The program returns counts per category (cell_delay_limited /
net_delay_limited / logic_depth_limited / clock_skew_limited /
hold_violation) plus the fix strategy per category. Do not author
these counts by reading the report.
python3 plugins/vibe-ic/programs/pnr_timing_repair_completeness_check.py \
<pnr.tcl> --json /tmp/pnr_repair.json
pnr_timing_repair_completeness_check.py FAILs any OpenROAD PnR script
that runs only repair_timing -hold without set_wire_rc +
repair_design + repair_timing -setup (the sha256 silicon-DOA
anti-pattern). The phase3 runner already emits the full chain; this
gate is the static backstop when reviewing a hand-authored script.
When to use
- After synthesis, after CTS, after routing, after ECO
- Any time WNS (Worst Negative Slack) or TNS (Total Negative Slack) goes positive
- When hold violations appear after routing
- Multi-corner sign-off (ss/tt/ff ร low/high temp ร low/high Vdd)
Inputs
- STA report(s) โ setup + hold + recovery/removal + min-pulse-width
- SDC constraints
- Top violating paths (usually
-max_paths 100)
- Corner/mode matrix
- Optional: RTL source for the violating module
Workflow
- Categorize every violating endpoint:
- Cell delay limited โ upsize driver, Vt swap
- Net delay limited โ insert buffer, reroute, shorten wire
- Logic depth limited โ pipeline / restructure RTL
- Clock skew limited โ useful skew, CTS re-balance
- Hold โ buffer insertion on min paths
- Propose fix per endpoint with an estimated slack gain
- Flag paths that cannot be fixed by ECO โ require RTL change
- Cross-corner view: if a path fails only at one corner, adjust margin first
- Summarize: WNS/TNS before โ after, number of endpoints touched
Output format
sta/sta_review.md:
- Summary (WNS, TNS, #failing endpoints, corner)
- Top 20 paths with classification + fix + slack delta
- RTL change recommendations (handoff to
/rtl-repair)
- ECO fix list (handoff to
/eco-plan)
Technical basis
Static timing analysis is governed by setup/hold relationships around launch/capture flops. Standard references: Bhasker & Chadha "Static Timing Analysis for Nanometer Designs". OpenSTA is the open-source signoff-class engine (https://github.com/The-OpenROAD-Project/OpenSTA).
Handoff
- RTL change โ
/rtl-repair
- Metal-only fix โ
/eco-plan
- Power impact of sizing โ
/ir-drop-triage
Compliance gate (mandatory)
After producing your output, save it to a file and run:
python3 plugins/vibe-ic/_shared/skill_compliance_check.py \
--requirements plugins/vibe-ic/skills/sta-review/compliance.yaml \
<your_output_file>
Exit 0 = PASS, exit 1 = FAIL with specific missing elements listed.
compliance.yaml in the corresponding skill directory enumerates
every required element of your output: section headers, metadata fields,
handoff lines, tool invocations.
Your task is not complete until the audit returns PASS. Missing
elements are the single largest source of skill-execution non-determinism
across different agents.
Captured rule โ PnR must run set_wire_rc + repair_design + repair_timing -setup (not hold-fix-only)
Enforced by two programs โ no longer prose judgment:
- The full deterministic command sequence (
set_wire_rc -signal/-clock with
NONFATAL fallbacks โ estimate_parasitics โ repair_design โ
repair_timing -setup โ detailed_placement; then post-CTS repair_timing -hold;
then post-global-route re-estimate + repair) is emitted by
programs/phase3_one_shot_runner.py (phase3 PnR template).
programs/pnr_timing_repair_completeness_check.py is the static gate that
FAILs any PnR script missing that chain (the hold-only silicon-DOA shape).
Worked example (kept for context): benchmark_clean/sha256 v0.1.25 post-route
WNS = -102.76 ns, mis-blamed on the SHA round; real cause was the missing
set_wire_rc / repair_design / repair_timing -setup (STA optimistic,
RSZ-0089). After adding the chain (v0.1.26): WNS = +10.95 ns MET on the same RTL.
General across all OpenROAD-driven PnR โ template-level, not chip-specific.
Captured by benchmark-enhancement-capture 2026-05-28; extracted to programs in M4.
Captured rule โ a corner that is unreported is not a corner that met
Enforced by a program โ no longer prose judgment.
This skill already said, in prose, "Multi-corner sign-off (ss/tt/ff ร low/high
temp ร low/high Vdd)" and "Cross-corner view: if a path fails only at one
corner, adjust margin first." Prose cannot fail a run. Measured consequence: a
campaign ledger carried no STA column at all while two ICs violated setup at
the slow sign-off corner (worst setup slack -4.33 ns, TNS -259.13; and
-2.35 ns, TNS -10.36), both persisting unchanged into the post-fix verify
runs. Because timing was simply absent from the record, "the phase-3 failures
were LVS tooling artifacts" โ true of the LVS part โ was allowed to stand as the
whole explanation.
python3 plugins/vibe-ic/programs/sta_corner_record_completeness_check.py \
<run_dir> --json /tmp/sta_corner_record.json
sta_corner_record_completeness_check.py FAILs when:
- R1 a reported corner is unnamed, or a corner carrying no sign-off role is
characterised only half-way (setup without hold, or the reverse);
- R2 a corner the flow DECLARED it would analyse has no datapoint for the
role it was declared to serve โ including the case where an STA verdict JSON
outlives the report it cites;
- R3 any sign-off corner violates. A typ-only "MET" is a misleading pass
and never carries the run; when the typ corner met while a sign-off corner
violated, the finding says so in those words;
- R4 a multi-corner claim is not backed by multiple corner LIBRARIES โ
either the collapse is undisclosed (
R4_MULTI_CORNER_CLAIM_UNSUPPORTED) or
the run records no corner-to-liberty resolution at all
(R4_LIBRARY_RESOLUTION_UNRECORDED);
- R5 DRV is violated (
R5_DRV_VIOLATION) or was never queried
(R5_DRV_UNQUERIED).
R4: a "multi-corner" run that read one library
Measured: a flow's "multi-corner" STA read the typ liberty three times,
varying only the SPEF. Three corners were reported; one library was analysed,
and the report did not say so. The degradation path is designed โ the runner's
own docstring says that when corner libs are unavailable "it degrades to the
single-corner read_liberty (byte-identical to the pre-multi-corner
emission)". The degradation is not the defect; the silence is. A degraded
run was byte-indistinguishable from a genuine one. What that silence cost: real
PVT STA run by hand closed setup+hold at three corners with TNS=0, but carried
139 max_slew DRV violations at the slow corner, entirely invisible to the
flow's single-corner check.
R4 is judged at run level, not per axis: a single-library RC axis is not a
degradation (the RC axis varies parasitics, and one process library across its
corners is correct by design). The degradation is when the run as a whole reports
several corners while never analysing more than one distinct library on any
axis. Judging per axis would label every healthy run degraded โ the mirror image
of the defect.
A disclosed collapse is not a FAIL. It yields the verdict
SINGLE_CORNER_ONLY (exit 0): a PDK that genuinely ships one library cannot
do better, and failing it would fabricate a violation on every single-library
PDK. But the verdict string carries the limitation, so no downstream summary
can quote a bare "PASS" and have it read as multi-corner closure. Never record
a SINGLE_CORNER_ONLY run as multi-corner sign-off.
R5: DRV must actually be asked for
report_check_types -max_slew -max_capacitance is what surfaces slew/cap
violations. If the flow never emits it, the report cannot show a slew
violation however many exist โ an unqueried limit is indistinguishable from a
met one, which is the same disease R2 names for unreported slack. The emitter
attests the query with a SIGNOFF_CHECK_TYPES_REPORTED marker; its absence
is what separates "queried and clean" from "never asked".
It learns which corners are sign-off from the run's OWN declarations โ
mcorner_ocv_stance.json (setup_process_corner / hold_process_corner),
multi_corner_spef_stance.json (setup_corner / hold_corner) and
pvt_matrix.json (primary_corner) โ so no corner name is hardcoded and a PDK
with a different corner vocabulary is judged the same way. The cornerโliberty
resolution comes from the same stances' corner_library_resolution block, and
from the # corner_liberty: header lines the STA reports now carry; a corner
name is never taken as evidence of which file was read, because believing the
name is how this defect survived. It always emits the full per-corner table
(corner, axis, role, setup WNS, hold WNS, TNS, source log) plus the per-axis
library-resolution and DRV table, on PASS as well as FAIL: the absence of that
evidence is the defect itself.
Distinct from post_route_signoff_corner_check (#147), which judges the slack
inside the multicorner report and is wired condition_files_exist on that very
report โ so it self-skips when the report is absent. Do not read a
NOT_APPLICABLE from it as "timing is fine".
Captured by benchmark-enhancement-capture 2026-07-21 (Bucket A, prose โ program).
Captured rule โ a single-register iterative self-loop is loop-bound: retiming can't help, the fix is a multi-cycle round (never a relaxed clock)
Enforced by a program โ no longer prose judgment.
python3 plugins/vibe-ic/programs/iterative_recurrence_timing_diagnosis.py \
--sta-report <worst_setup_path.rpt> \
[--retiming-wns-delta <ns_from_a_retiming_experiment>] \
[--spec-microarch-free] [--spec-latency-unconstrained] \
[--target-period-ns <hard_period>] --json /tmp/iter_diag.json
The diagnosis. An iterative datapath computes one iteration per clock by
feeding a state register back to itself: X <= f(..., X, ...). The stateโstate
cycle then contains exactly one register, and two facts hold for ANY such
loop on ANY PDK:
- Loop-bound period. Min period of a cyclic path = (ฮฃ combinational delay on
the cycle) / (registers on the cycle). One register โ the floor is the whole
cone delay. You cannot clock it below one full iteration.
- Retiming is powerless. Retiming preserves the register count on every
cycle, so a one-register cycle stays one register and its bound is invariant.
It shows up as
abc dretime returning a byte-identical netlist / ~0 ns WNS
improvement. Do not keep retrying it, and do not reach for pure logic
restructuring (dch -f; dc2) either โ it can break the ripple but inflate
area/die so the routed slow-corner WNS gets worse from added wire delay.
The fix, when the spec authorises it. If the design spec leaves the microarch
as a free choice (iterative / unrolled / pipelined / multi-cycle) and does not
fix the latency-cycle count, split the recurrence cone over Nโฅ2 clocked
sub-stages (a multi-cycle round). The loop now spans N registers; each cycle
is a genuine single-cycle path at the same HARD period, with per-cycle depth
~1/N. Latency in cycles grows ~Nร; the period is what closes. The SDC stays the
honest single-cycle SDC โ a multi-cycle round is N real single-cycle stages, not
a multicycle timing exception, so no set_multicycle_path masking is introduced.
A synth-level timing win is NOT a post-route win โ measure the shipped corner
The only number that counts is post-route on the shipped SPEF. A synthesis
knob that clearly improves the pre-PnR critical path can degrade the routed
slow corner, because area bought at synth becomes wire delay after placement โ
and modern PnR already resizes for you.
Measured, twice, in opposite tools, same disease:
| change | pre-PnR effect | post-route effect |
|---|
dch -f; dc2 restructuring | breaks the ripple | +26% area, routed SS WORSE |
| delay-driven mapping + gate sizing | arrival 65.507 โ 50.954 ns (โ22.2%) | SS WNS โ2.519 โ โ4.386 ns (1.87 ns WORSE), +4.6% cells |
The second was a controlled A/B: same RTL, same run-dir shape, both freshly
synthesised, differing only in the mapper flags. A 22% pre-PnR improvement
became a 1.87 ns post-route regression. Mechanism: OpenROAD's repair_design /
repair_timing already resize after placement, so synth-time sizing is
largely redundant with them โ you keep its area cost and lose the wire delay.
So: never accept a pre-PnR delta as evidence. Re-run the post-route A/B on
the shipped SPEF before adopting any synth QoR knob, and be prepared for the
sign to flip.
A related trap: a timing lever that was never actually engaged
Separately worth knowing, because it invites exactly the "fix" above: a delay
target can be passed and silently ignored. Measured โ abc -liberty <lib> -D <period> produced a netlist byte-identical to no -D at all (identical
worst-path arrival, cell count and area), because on the non--constr path the
default &nf mapper ignores -D; the fork's own help documents a -sizing
flag that exists precisely to make -D bind.
Two lessons, and the second matters more:
- Verify a flag does something. Byte-identical output with and without it
is proof it is inert โ a cheap, conclusive diff worth running on any knob you
believe is protecting you.
- Inert is not automatically wrong. Here, engaging the ignored lever made
the shipped corner worse, so leaving it inert was the measured-better
configuration. Do not "repair" an unbound knob on the assumption that binding
it must help.
Background worth having: yosys techmaps $lcu to a Brent-Kung parallel-prefix
adder (techmap.v, _90_lcu_brent_kung), so a fast adder is present by
default and a long carry chain in the report is the mapper's area-mode choice,
not what techmap built. That makes "the arithmetic is the floor" a conclusion to
reach last, not first โ but as the table above shows, forcing delay-mode mapping
is not the remedy either.
Only once the flow's own knobs are measured out does an architectural change
earn its keep. If a re-measured worst path is still one full-width carry-propagate
add, then the per-cycle arithmetic cost is real and the remedies are: register
the carry across sub-cycles (split the add into half-width slices โ a register in
the middle is the one split synthesis cannot merge back), or use a
carry-select/carry-lookahead adder. Both are pure microarch choices, in-spec
exactly when the multi-cycle split is. Re-measure after every change: the
critical path moves, and the next binding constraint is often a different one.
If the spec instead forbids the microarch change, that is an honest floor:
report the violated corner as violated, never dress it as a pass, and never
relax the target clock (the program FAILs hard on --relax-clock-proposed).
The program reads the worst path's start/end register bank (instance path
minus the bit-index) to spot the self-loop directly; when the netlist names are
synthesis-anonymised (_1234_) it returns INCONCLUSIVE_NAMES_ANONYMIZED and
asks for a name-preserving netlist or a retiming-experiment WNS delta rather than
guessing. A measured retiming improvement overrides the name heuristic
(RETIMING_EFFECTIVE_APPLY) โ ground truth beats the heuristic.
Worked example (context, not detection logic) โ and an honest open case. A
crypto hash accelerator on an open PDK at a HARD 25.907 ns period: the reference
iterative single-cycle round is an a_regโa_reg / e_regโe_reg self-loop
that misses the slow sign-off corner at post-route WNS โ2.519 ns. Everything
tried so far has failed, and each failure is informative:
abc dretime โ byte-identical netlist (loop-bound, as predicted above).
- logic restructuring (
dch -f; dc2) โ breaks the ripple, +26% area, routed
SS worse.
- delay-driven mapping + gate sizing โ โ22% pre-PnR, +4.6% cells, post-route
SS โ4.386 ns (1.87 ns WORSE).
- multi-cycle round (2-cycle, and a 16-bit carry-split variant) โ functionally
verified only (NIST vectors 100%, 300/300 random blocks bit-identical to the
reference). Its timing was never validly measured: the runs that appeared
to measure it were reusing a cached netlist of the ORIGINAL design, so those
numbers were retracted. See the netlist-cache rule โ an RTL edit that never
reaches synthesis produces confident numbers describing RTL nobody read.
So this cell does not converge, and no fix is claimed for it. What the case
does establish is the ordering: rule out flow defects and measure every knob
post-route before re-architecting, and treat any number produced by a run
whose RTL may not have been synthesised as void.
Captured by repo-gatekeeper 2026-07-25 (prose โ program; diagnosis + remedy routing).