Late changes fail differently from early ones: the design is closed, the verification is green, and each edit risks more than it fixes because the re-verification is always narrower than the change deserves. Discipline means every change passes the same three gates no matter how small it looks or how senior its sponsor. This lesson states the gates and shows what each costs on NanoSoC. Its one job is the process; the spare gates that make it cheap are next.
Gate one is reason: the bug, its symptom, its root cause, and why it cannot wait for the next chip or live as errata. A miswired interrupt that kills every DMA completion qualifies; a nicer reset value does not. Gate two is proof that the edit fixes exactly the bug: equivalence between the pre- and post-ECO netlists outside the intended change, plus the failing case now passing. Gate three is re-verification scoped to what the change touches: the affected cone rerun, the must-pass tests green, the manifest and dashboard updated to the new revision. The NanoSoC example is the interrupt miswire, ECO-1: reason in one paragraph, equivalence on the interrupt cone, rerun of the connectivity test that forces each source plus the DMA self-test, manifest moved from revision 214 to 214-e1 (illustrative revision labels) with every result row refreshed.
# The ECO gate checklist: all three gates must pass together.
# Values describe NanoSoC ECO-1, the interrupt re-route (illustrative).
GATES = {
"reason": ["reason"],
"proof": ["equivalence", "failing-case-passes"],
"reverification": ["cone-rerun", "must-pass-green", "manifest-moved"],
}
def blocked(eco):
return sorted(g for g, keys in GATES.items()
if not all(eco.get(k) for k in keys))
eco1 = {"reason": True, "equivalence": True, "failing-case-passes": True,
"cone-rerun": True, "must-pass-green": True, "manifest-moved": True}
assert blocked(eco1) == []
# each gate must be able to block on its own
assert blocked(dict(eco1, reason=False)) == ["reason"]
assert blocked(dict(eco1, equivalence=False)) == ["proof"]
assert blocked(dict(eco1, **{"manifest-moved": False})) == ["reverification"]
# a skipped cone rerun blocks gate three even with equivalence green
assert blocked(dict(eco1, **{"cone-rerun": False})) == ["reverification"]
print("PASS: ECO-1 passes all three gates; each gate blocks when its evidence is missing")The one-paragraph reason with the deferral options considered: erratum, workaround, next chip, and why each fails.
The equivalence report scoped to the change, plus the previously failing case passing on the new netlist.
The manifest moved to the new revision with no stale result rows surviving beside it.
When an ECO breaks something, the cause is almost always a skipped gate three: the cone was drawn too narrow and a coupled path went unverified. Redraw the cone from the changed signals outward through every synchroniser, isolation cell and decoder they touch, then rerun to that boundary. The equivalence proof in Proving an ECO: Equivalence and Gate-Level Simulation sets the standard for gate two.
No: gate three is open. A result row tied to the old revision beside an ECO on the new one is exactly the drift the freeze exists to prevent. Move the manifest and refresh every affected row.