Raft replicates state machines: multiple nodes agree on a committed order of commands, then execute deterministic state transitions in that same order. It tolerates crashes, restarts, and network anomalies within the protocol's assumptions, but not arbitrary malicious nodes.
When learning, distinguish three facts: the log is stored on a node, the log is committed by the protocol, and the command is applied to the state machine. These moments usually differ. Raft guarantees that committed history will not be overwritten by a later legitimate leader, allowing uncommitted tails to diverge temporarily and be replaced upon recovery.
Terms and Roles: Freshness is Not Physical Time
A term is a monotonically increasing logical term number. A Follower enters the Candidate state after an election timeout, increments the term, and initiates a vote; upon receiving a majority of votes required by the configuration, it becomes the Leader. Random timeouts reduce the probability of simultaneous contention, do not guarantee a successful election in one attempt, and there is no fixed millisecond value applicable to all networks.
Upon receiving a protocol message with a higher term, a node updates its term and reverts to follower. An isolated old leader may not immediately receive this message and might still consider itself in office; safety relies on its inability to obtain the required quorum to complete a new legitimate commit, rather than all nodes immediately knowing who the leader is. Access to external resources also requires appropriate fencing and cannot rely solely on the role variable in the process.
State That Must Be Preserved
State
Purpose
Recovery Requirement
currentTerm
Reject requests from old terms
Must not regress after restart
votedFor
Vote target for this term
Prevents voting for different candidates in the same term
log: index, term, command
Replicated command sequence
Confirmed persistent logs must not be forgotten
commitIndex
Known commit position
Can be recovered or advanced by the protocol
lastApplied
State machine application position
Must be consistent with state machine persistence and recovery methods
nextIndex, matchIndex
Leader tracks replication progress of each replica
Can be re-established
When persistent state changes, the persistence requirement must be met before the successful acknowledgment depending on that change; this does not mean every heartbeat without state change requires a separate fsync. If the state machine has already persisted the result, the corresponding command must not be blindly executed a second time during recovery.
Election: One Vote Per Person Is Not Enough
The candidate sends the (lastLogTerm, lastLogIndex) of its tail log. The voter, upon satisfying term and voting conditions, must also confirm that the candidate's log is at least as up-to-date as its own: first compare the term of the last log entry, and if terms are equal, compare the index. Looking only at log length is insufficient; a longer tail from an old term is not necessarily more qualified.
Each node votes for at most one candidate in the same term. Since two majorities intersect, it is impossible to have two leaders both receiving a legal majority in the same term. The retention of committed logs across terms requires log matching, election restrictions, and commit rules to prove together. You cannot directly equate "log update" with "necessarily containing any old entry."
Log Replication: Find the Common Prefix First
The Leader's AppendEntries carries the index and term of the previous log entry. The Follower only accepts subsequent entries after this prefix check passes; upon encountering a conflict with the same index but different term, it deletes the conflicting entry and its subsequent tail, then appends the new content.
After rejection, the leader adjusts nextIndex to re-find the common prefix; implementations can use conflict term information to accelerate rollback. Do not truncate the follower's log just because a shorter heartbeat was received: when there are no entry conflicts, the absence of a specific tail in the message is not a reason to delete it.
The log matching property states: if two logs have the same term at the same index, their preceding prefixes are consistent. This allows the leader to gradually establish a consistent log with short prefix checks, without comparing the entire history each time.
Why Empty Heartbeats Cannot Truncate the Tail
The following simulates only the log and commit position parts of AppendEntries, assuming the request term and leader identity have already passed protocol checks. Logs are represented as (term, command), with a sentinel at position 0; this does not implement election, persistence, or index conversion after snapshots.
defappend_entries(log,committed,prev_index,prev_term,entries,leader_commit):ifnot0<=prev_index<len(log)orlog[prev_index][0]!=prev_term:returnFalse, list(log), committedupdated=list(log)forindex,entryinenumerate(entries,start=prev_index+1):ifindex<len(updated)andupdated[index][0]!=entry[0]:ifindex<=committed:raiseValueError("Legal Raft history cannot conflict with committed prefix")delupdated[index:]ifindex==len(updated):updated.append(entry)elifupdated[index]!=entry:raiseValueError("Same index, same term, but different command, violating input assumptions")matched_end=prev_index+len(entries)new_commit=max(committed,min(leader_commit,matched_end))returnTrue, updated, new_commitlog=[(0,"sentinel"),(1,"a"),(2,"b"),(2,"c"),(2,"d")]ok, same, commit=append_entries(log,1,2,2,[],4)assertokandsame==logandcommit==2ok, fixed, commit=append_entries(log,2,2,2,[(3,"new-c")],3)assertokandfixed==log[:3]+[(3,"new-c")]andcommit==3assertappend_entries(fixed,3,2,2,[(3,"new-c")],3)==(True,fixed,3)assertappend_entries(log,2,2,99,[],4)==(False,log,2)
The first call only proves that the prefix at position 2 matches; you cannot delete entries 3 and 4 just because the message did not carry them, nor can you advance the commit to 4 just because the follower locally happens to have 3 and 4. The second call carries position 3, term 3, which truly conflicts with the local term 2, so it deletes the old tail from 3 onwards, then appends the new entry.
Repeating the second request will not append a second copy of the command. Implementations need to distinguish between "already existing matching entries," "new entries," and "conflicting entries"; you cannot unconditionally overwrite the local log with the entire suffix received. The above processing corresponds to Raft Figure 2's AppendEntries rules.
Commit: Why You Must Look at the Current Term
In a basic majority configuration, a leader can advance commitIndex to N, provided that: N is greater than the current commit position, at least a majority of nodes have matchIndex >= N, and log[N].term == currentTerm. Committing N simultaneously commits the log prefix preceding it.
Old term entries, even if temporarily present on a majority of nodes, cannot be declared committed solely based on the number of replicas currently seen. The reason is that voting prioritizes the tail term; another candidate with a higher term tail may still win subsequent elections in some executions and overwrite uncommitted entries. Majority replication of current term entries provides the additional condition required to safely advance the commit prefix.
A practical readable example: A new leader's current term is 7, inheriting an entry at index 40, term 6. It writes a no-op for term 7 at index 41 and safely commits it; index 40 is then committed along with the prefix. There is no need to replace the old command with a new term and re-execute it. For strict proofs and counterexamples, see Raft Paper Section 5.4 and Figure 8.
Majority Position Reached 40, But May Still Only Commit to 39
A five-node leader's current term is 7; log 40 belongs to term 6, and 41 is a no-op for term 7. It is committed up to 39. If the replication progress is [41,40,40,39,38], although 40 has three replicas, the current term condition is still not met; only after 41 also reaches the majority can it advance together with 40.
defadvance_commit(committed,current_term,terms,matches):majority=len(matches)//2+1eligible=[indexforindex,terminterms.items()ifindex>committedandterm==current_termandsum(position>=indexforpositioninmatches)>=majority]returnmax([committed]+eligible)terms={39:6,40:6,41:7}assertadvance_commit(39,7,terms,[41,40,40,39,38])==39assertadvance_commit(39,7,terms,[41,41,41,39,38])==41assertadvance_commit(41,7,terms,[41,41,41,39,38])==41assert(7,10)>(6,100)# Election compares tail term first, not log length
Here, matches includes the leader itself and comes from the same fixed five-node configuration, with no missing nodes or treating duplicate ACKs as multiple replicas. If membership changes are in progress, you cannot continue to apply a fixed majority formula, but must use the quorum rules of the current configuration. This filtering function demonstrates the advancement condition and does not undertake a complete safety proof.
Why does majority replication not move commit?
Place replication evidence beside the commit condition to avoid treating replica count as a complete proof. Each slider selects an independent progress snapshot, recomputing from committed index 39; lowering it does not model loss of acknowledged durable logs. The separate apply action shows that a newly committed prefix still needs execution.
#{i:matchIndexi≥n}≥3∧term(n)=7
Preparing the visual
Why does majority replication not move commit?
Advancing the committed prefix requires a current-term entry replicated to a majority. Committing it includes earlier entries. Applying remains a separate action.
{"id":"raft-commit-frontier","title":"Why does majority replication not move commit?","summary":"Advancing the committed prefix requires a current-term entry replicated to a majority. Committing it includes earlier entries. Applying remains a separate action.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"track\"><div class=\"cell\">39<br>term 6</div><div class=\"cell\">40<br>term 6</div><div class=\"cell gold\">41<br>term 7</div></div><p class=\"hint\">A = 41</p><label class=\"replica\">B<input aria-label=\"B\" id=\"p0\" type=\"range\" min=\"38\" max=\"41\" value=\"40\"><output id=\"p0v\"></output></label><label class=\"replica\">C<input aria-label=\"C\" id=\"p1\" type=\"range\" min=\"38\" max=\"41\" value=\"40\"><output id=\"p1v\"></output></label><label class=\"replica\">D<input aria-label=\"D\" id=\"p2\" type=\"range\" min=\"38\" max=\"41\" value=\"39\"><output id=\"p2v\"></output></label><label class=\"replica\">E<input aria-label=\"E\" id=\"p3\" type=\"range\" min=\"38\" max=\"41\" value=\"38\"><output id=\"p3v\"></output></label><div id=\"copies\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"commit\"></span><strong id=\"commit\"></strong></div><div><span data-i18n=\"applied\"></span><strong id=\"applied\"></strong></div></div><div class=\"actions\"><button id=\"apply\" data-i18n=\"apply\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blocked\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"advanced\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.replica{display:grid;grid-template-columns:18px minmax(0,1fr) 28px;gap:10px;align-items:center;margin:18px 0;font-size:13px}.replica input{width:100%}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let applied=39,commit=39;function draw(reset=false){if(reset)applied=39;const p=[41,...[0,1,2,3].map(i=>+$('#p'+i).value)];for(let i=0;i<4;i++)set('p'+i+'v',p[i+1]);const c40=p.filter(x=>x>=40).length,c41=p.filter(x=>x>=41).length;commit=c41>=3?41:39;set('commit',commit);set('applied',applied);cells('copies',[{text:'40: '+c40+' / 5',cls:c40>=3?'on':''},{text:'41: '+c41+' / 5',cls:c41>=3?'gold':''}]);$('#apply').disabled=applied===commit;set('explanation',t(commit===41?'advanced':'blocked'));}document.querySelectorAll('input').forEach(e=>e.oninput=()=>draw(true));$('#apply').onclick=()=>{applied=commit;draw();};draw();","audio":false,"strings":{"advanced":"Index 41 is from term 7 and has a majority; index 40 commits with the prefix.","applied":"Applied through","apply":"Apply committed prefix","blocked":"Index 40 may have a majority, but is from term 6. Without a majority for 41, commit stays at 39.","commit":"Commit can reach","heading":"Why does majority replication not move commit?","intro":"The current term is 7. Change each replica’s durable log position; compare the evidence for indices 40 and 41.","position":"Durable end","reset":"Reset","scope":"Fixed five-member configuration. Leader A is durable through 41; 39 is committed and applied. Sliders select independent replication snapshots and recompute the condition, not a complete election.","scopeLabel":"Model scope and assumptions"}}
Application and Client Retries
Nodes advance lastApplied in committed order. State machine execution must be deterministic: if random numbers, current time, or external query results affect the state, they should be incorporated into commands or consistent inputs via explicit protocols, rather than allowing each replica to freely take different values.
Even if the Raft log is safe, the client may still lose the response after the command has been committed, leading to a re-submission of the same business operation. Replicated state machines typically require request IDs and result deduplication. Raft itself does not automatically treat two duplicate business requests as the same command.
Linearizable Reads: Being the Leader Is Not Enough
An isolated old leader may possess stale state. For safe reading, it is necessary to confirm that the leader authority relied upon for this read is still valid, and wait for the state machine to apply to the corresponding safe commit position.
Quorum-based read methods usually contact a majority of nodes to establish a read barrier after the leader has confirmed the commit boundary of the current term, and then wait for the local lastApplied to reach the safe position. Lease-based reads can reduce some communication, but their correctness requires time and lease assumptions specified by the protocol. See Practical Systems for details.
Snapshots: Preserve State and Its Corresponding History Position
A snapshot records the state of an applied prefix, along with metadata such as lastIncludedIndex, lastIncludedTerm, and the configuration required for recovery. Only after the snapshot is reliably saved can the corresponding old logs be reclaimed according to the protocol.
If a lagging follower's required logs have been reclaimed, it can recover the prefix via InstallSnapshot and then catch up on subsequent logs. Installation must not allow a node to revert to a view older than the applied state; whether to retain subsequent logs also requires checking if the boundaries match. A snapshot is not an arbitrary copy of a changing data directory.
Membership Changes: The Definition of Quorum Also Changes
Directly splitting the old set into a new set may allow two disjoint majorities to act independently. Joint consensus requires the majority of the old set and the majority of the new set to be satisfied simultaneously, before transitioning to the new configuration. It is not combining the two sets of nodes and taking a majority of the total number.
Another class of implementations restricts changes to safe single-member steps and specifies when the configuration takes effect. Specific implementations may support different interfaces; you cannot skip their application order and recovery rules just because you see "single-node change." New replicas often start as non-voting learners to catch up, avoiding a new voting node with no data immediately raising the quorum threshold.
Joint Consensus Is Not Choosing Any Three Out of Five
Old configuration {A,B,C}, new configuration {C,D,E}. The old majority {A,B} and the new majority {D,E} can be completely disjoint, indicating that "each configuration using its own majority" is not enough. The joint phase requires a single decision to obtain confirmation from at least two nodes of the old configuration AND at least two nodes of the new configuration simultaneously.
fromitertoolsimportcombinationsold, new=set("ABC"), set("CDE")defmajority(acks,members):returnlen(acks&members)>=len(members)//2+1defjoint(acks):returnmajority(acks,old)andmajority(acks,new)assertmajority(set("ABC"),old|new)# Three out of five
assertnotjoint(set("ABC"))# New config only has C, one ack is not enough
assertjoint(set("ACD"))# Old config A, C; New config C, D
assertnot(set("AB")&set("DE"))all_sets=[set(nodes)forsizeinrange(6)fornodesincombinations("ABCDE",size)]valid=[acksforacksinall_setsifjoint(acks)]assertall(a&bforainvalidforbinvalid)
Acknowledgments from the shared node C can count for both sides, but acknowledgments for A and B from the old side cannot be treated as D and E from the new side. This rule affects availability: when the old group can form a majority but the new group cannot, the joint phase still cannot complete decisions requiring joint quorum. When configuration entries take effect and how to recover must follow the complete reconfiguration protocol; you cannot just replace a majority judgment function. Raft Membership Changes
Three out of five may still be insufficient
Joint consensus is the conjunction of two predicates, not a majority over the union. A,B,C provide three votes in total but only C in the new configuration; adding D satisfies both. Conversely, C,D,E lack an old-configuration vote. The static figure shows membership; the selectable acknowledgement set tests the boundary.
∣A∩Cold∣≥2∧∣A∩Cnew∣≥2
Preparing the visual
Three out of five may still be insufficient
Joint consensus requires majorities of both old and new configurations. C counts in both; surplus acknowledgements on one side cannot replace the other.
{"id":"raft-joint-quorum","title":"Three out of five may still be insufficient","summary":"Joint consensus requires majorities of both old and new configurations. C counts in both; surplus acknowledgements on one side cannot replace the other.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div id=\"nodes\" class=\"choices\"></div><div class=\"track-row\"><span data-i18n=\"old\"></span><div id=\"oldrow\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"new\"></span><div id=\"newrow\" class=\"track\"></div></div><div id=\"count\" class=\"formula\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"yes\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"no\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.choices{grid-template-columns:repeat(5,minmax(0,1fr))}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let ack=new Set(['A','B','C']);for(const n of 'ABCDE'){const b=document.createElement('button');b.textContent=n;b.dataset.node=n;b.onclick=()=>{ack.has(n)?ack.delete(n):ack.add(n);draw();};$('#nodes').append(b);}function draw(){const old=[...'ABC'].filter(n=>ack.has(n)).length,ne=[...'CDE'].filter(n=>ack.has(n)).length;for(const [id,ns]of [['oldrow','ABC'],['newrow','CDE']])cells(id,[...ns].map(n=>({text:n,cls:ack.has(n)?'on':'empty'})));document.querySelectorAll('[data-node]').forEach(b=>b.setAttribute('aria-pressed',String(ack.has(b.dataset.node))));set('count',old+' ≥ 2 ∧ '+ne+' ≥ 2');set('explanation',t(old>=2&&ne>=2?'yes':'no'));$('#count').dataset.valid=String(old>=2&&ne>=2);}draw();","audio":false,"strings":{"heading":"Three out of five may still be insufficient","intro":"Old configuration ABC; new configuration CDE. Select acknowledgements and see their contribution to each group.","new":"New CDE","no":"At least one side has fewer than 2 votes; a union majority is insufficient.","old":"Old ABC","reset":"Reset","scope":"Only the joint-quorum predicate is modeled. Acknowledgements concern the same decision. Configuration activation and recovery require the full protocol.","scopeLabel":"Model scope and assumptions","yes":"Both sides have at least 2 votes: joint quorum reached."}}
Quick Reference: Several Non-Negotiable Restrictions
Direct commit judgment based on majority persistence must satisfy the current term condition.
Election compares the tail term of logs first, then the index.
Leader identity alone cannot prove that local reads are fresh enough.
Snapshots, deduplication state, and membership configurations are part of complete recovery.
Safety can be maintained during communication anomalies, but continuous progress still requires a communicable quorum and appropriate timing conditions.