The alias would be a footnote if one owner could remove it: widen the DMA address port to 12 bits and every image but the first disappears. But the port belongs to the DMA owner, the slot width to the integrator, the headers to software, and the tests to verification, so the one-line fix touches four parties and every consumer they feed. Without a named decider the change either dies in a thread or lands quietly and breaks the headers. Naming one decider per facet, and one path by which a frozen contract may change, removes that thread.
RACI names four positions per contract: Responsible does the work, Accountable answers for it, Consulted is asked first, Informed is told after. The rule that makes it work is singularity: exactly one A per row. Two accountables means no decider; zero means the facet belongs to everyone, which is nobody. On NanoSoC the integrator holds the A for the shared tables and the top-level structure, block owners hold the A for behaviour inside their wrappers, software holds the A for drivers built on frozen tables, and verification holds the A for the proof that the assembled chip meets the specification.
Contract | Responsible | Accountable | Consulted | Informed |
|---|---|---|---|---|
Address map and decode | Integrator | Integrator | Software, PD | Verification, docs |
Fabric protocol and config | Integrator | Integrator | Block owners | Verification |
Block behaviour and collateral | Block owner | Block owner | Integrator | Software, verification |
Clock, reset, power intent | Integrator | Integrator | PD, block owners | Software |
Constraints per block | Block owner | Block owner | PD, integrator | Signoff board |
Top-level constraint merge | Integrator | Integrator | PD, block owners | Signoff board |
Drivers against frozen tables | Software | Software | Integrator | Verification |
Chip-level proof | Verification | Verification | Integrator, software | Signoff board |
Physical closure | PD | PD | Integrator | Signoff board |
Change control is the only legal path across a freeze, and it has five steps: request with the reason, impact listing every consumer and test touched, decision by the Accountable, regeneration of every consumer from the changed source, and requalification proving the change and nothing else. The worked example below runs the alias-removal request through it: widening the port touches the DMA RTL, the wrapper, the headers, the aliases used in tests, and the alias checker itself. The Accountable defers it to the next spin and approves the cheap companion instead, a lint rule plus the canonical-address check, which is how change control earns its keep: most requests are correctly refused or shrunk.
The script below holds the table above as data and enforces the two rules that keep RACI honest: every contract names exactly one Accountable, and every contract names someone Responsible for doing the work. It passes the NanoSoC table and demonstrates it catching a row where two parties both claim accountability.
CONTRACTS = ["map", "fabric", "block", "clock_reset_power",
"constraints", "top_constraints", "drivers", "proof", "closure"]
RACI = { # name: parties per position; NanoSoC teaching values
"map": {"R": ["integrator"], "A": ["integrator"]},
"fabric": {"R": ["integrator"], "A": ["integrator"]},
"block": {"R": ["block-owner"], "A": ["block-owner"]},
"clock_reset_power": {"R": ["integrator"], "A": ["integrator"]},
"constraints": {"R": ["block-owner"], "A": ["block-owner"]},
"top_constraints": {"R": ["integrator"], "A": ["integrator"]},
"drivers": {"R": ["software"], "A": ["software"]},
"proof": {"R": ["verification"], "A": ["verification"]},
"closure": {"R": ["pd"], "A": ["pd"]},
}
def check(raci):
problems = []
for name in CONTRACTS:
row = raci.get(name, {})
if len(row.get("A", [])) != 1:
problems.append(name + ": needs exactly one Accountable")
if len(row.get("R", [])) < 1:
problems.append(name + ": needs someone Responsible")
return problems
assert check(RACI) == []
shared = dict(RACI)
shared["drivers"] = {"R": ["software"], "A": ["software", "integrator"]}
assert check(shared) == ["drivers: needs exactly one Accountable"]
orphan = dict(RACI)
orphan["closure"] = {"R": ["pd"], "A": []}
assert check(orphan) == ["closure: needs exactly one Accountable"]
print("PASS: 9 contracts singly accountable; shared-A and empty-A caught")The RACI table is versioned with the project plan, and no row has zero or two Accountables.
Every change after a freeze carries the five steps with the Accountable's name on the decision.
Refused changes are recorded with the reason, so they are not re-proposed monthly.
When two parties each claim the other owns a failure, read the RACI row for the facet, not the org chart: the A decides and the R acts. When a change lands without the five steps, treat every consumer as suspect and re-run the stamp checker from the flow topic before debugging any single symptom.
The top-level constraint merge row: the integrator is Responsible and Accountable, PD and the block owners are Consulted. Block owners sign their own per-block constraints, and PD signs closure, not ownership of the merged source constraints.