# Implementation Plan > **TDD-mandatory** (`testing.md`): every implementation sub-task is preceded by a RED test in `src/lib/testsuite/test_raft.c` (and the simulator backing in `raft_sim.c`) that fails before the `raft.c` change and passes after. Build/run library tests via `/xce-build lib`. > > **Spec boundary** (applies to ALL tasks): this spec owns the commit-index advance (`raft.tla` `AdvanceCommitIndex` + the current-term Figure-8 rule, closes G4) and the `(lastLogTerm, lastLogIndex)` vote/PreVote freshness (`raft.tla` `HandleRequestVoteRequest` `logOk`, closes G6) **in `src/lib/raft` only**. It does **NOT** own the log model / AppendEntries / per-peer `match_index[]` (those come from `raft-log-replication` and are reused verbatim), the synchronous commit-barrier / durability mechanism (controller — only TRIGGERED via `on_commit_advanced`), config markers / upgrade snapshot (`ha-term-safety`), or the conformance harness (`raft-sim-conformance`). No task here edits any controller file; the controller adapter is described, not built. > > **Depends (HARD): `raft-log-replication`** — needs `log_len`, `match_index[]`, and `on_get_entry_term` already present in `raft.{c,h}` and the in-memory per-node log + AppendEntries routing already in `raft_sim.c`. ## 1. Header contract: commit_index, on_commit_advanced, raft_advance_commit, freshness signatures - [x] 1.1 Extend `raft.h` with the commit-index state and restore field - Add `uint64_t commit_index;` to `raft_state_t` - Add `uint64_t initial_commit_index;` to `raft_config_t` (restored committed LSN at boot, default 0; library never reads storage — consistent with `initial_term`/`initial_log_len`) - Observable completion: `raft.h` compiles; `sizeof(raft_state_t)` grows by the new field; no entry-byte storage added - _Requirements: 1.1, 1.5_ - _Boundary: raft.h_ - [x] 1.2 Add the `on_commit_advanced` REQUIRED callback to `raft_callbacks_t` (verbatim contract) - Declare `void on_commit_advanced(void *ctx, uint64_t commit_index)` with the exact signature from design.md / brief - Document it as REQUIRED, fired only on strict advance, and as a TRIGGER for the existing commit barrier (never performs durability itself) - Observable completion: the function-pointer member exists in `raft_callbacks_t` with the verbatim signature; header doc comment matches the Authoritative shared contract - _Requirements: 2.1, 2.4, 5.1_ - _Boundary: raft.h_ - _Depends: 1.1_ - [x] 1.3 Declare `raft_advance_commit` and extend the vote/PreVote signatures with last_log_term/last_log_index - Declare `int raft_advance_commit(raft_state_t *s)` - Extend `raft_on_vote_request` and `raft_on_pre_vote_request` to accept `cand_last_log_term` and `cand_last_log_index` (the chosen "library owns logOk" plumbing — Option B in design.md) - Update the header doc comments to describe the new logOk gate and the commit-advance action - Observable completion: header declares `raft_advance_commit` and the extended vote/PreVote signatures; a stub TU including `raft.h` compiles - _Requirements: 1.2, 2.2, 3.2, 4.2_ - _Boundary: raft.h_ - _Depends: 1.2_ ## 2. Init validation and commit-index restore - [x] 2.1 RED+GREEN: `raft_init` requires `on_commit_advanced` and restores `initial_commit_index` - Test: `raft_init` returns `-EINVAL` when `on_commit_advanced` is NULL; returns 0 when present; `s->commit_index == cfg->initial_commit_index` after init - Implement the NULL check in `raft_init` (mirroring the existing required-callback checks); restore `s->commit_index = cfg->initial_commit_index` - Observable completion: new `-EINVAL` test case passes; restore assertion passes; existing election + log-replication init tests still green - _Requirements: 2.5, 1.1_ - _Boundary: raft.c, test_raft.c_ - _Depends: 1.3_ ## 3. AdvanceCommitIndex with the current-term Figure-8 rule (raft.tla:259/270, closes G4) - [x] 3.1 RED: commit-advance tests (current-term advance, Figure-8 hold, monotonic, non-leader) - Test (advance): leader with `match_index[]` quorum-agreeing through index `k`, entry at `k` from `currentTerm` → `commit_index` advances to `k`, `on_commit_advanced(k)` fired once - Test (Figure-8 hold): quorum agrees through a previous-term entry at `k` with no current-term entry above → `commit_index` unchanged, `on_commit_advanced` NOT fired - Test (empty agreeIndexes): no index reaches quorum → unchanged, no callback - Test (monotonic): a later call computing a smaller `Max(agreeIndexes)` never lowers `commit_index` - Test (non-leader): `raft_advance_commit` returns `-EPERM`, no change, no callback - Observable completion: tests compile and FAIL (no implementation yet) - _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 2.2, 2.3_ - _Boundary: test_raft.c_ - _Depends: 2.1_ - [x] 3.2 GREEN: implement `raft_advance_commit` - Leader-only guard (`-EPERM` otherwise); compute `Max(agreeIndexes)` by scanning candidate indexes from `log_len` downward, seeding the agree-count with self, until a quorum `((n_peers/2)+1)` is reached - Apply the current-term rule: advance to `Max(agreeIndexes)` only when `on_get_entry_term(Max) == s->term`; else hold - Monotonic: only ever move `commit_index` forward; fire `on_commit_advanced(commit_index)` exactly once on a strict advance; read entry terms only via `on_get_entry_term` - Observable completion: 3.1 tests pass; the current-term check is unmistakably present before the advance in `raft.c`; `commit_index <= log_len` holds - _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, 2.3_ - _Boundary: raft.c_ - _Depends: 3.1_ - [x] 3.3 RED+GREEN: commit-advance is a separate action from append-entries reply - Test: a `raft_on_append_entries_reply` that updates `match_index[]` to a committable quorum does NOT by itself change `commit_index` or fire `on_commit_advanced`; only a subsequent `raft_advance_commit` does - Verify (no implementation change expected in `raft_on_append_entries_reply` — it is owned upstream and must stay untouched); if a regression is found, the fix belongs upstream, not here - Observable completion: test passes; `raft_on_append_entries_reply` remains free of any commit-advance side effect - _Requirements: 2.6_ - _Boundary: test_raft.c_ - _Depends: 3.2_ ## 4. (lastLogTerm, lastLogIndex) vote freshness (raft.tla:285, closes G6) - [x] 4.1 RED: binding-vote freshness tests - Test: candidate with stale `last_log_term` denied even when `last_log_index` is greater; candidate with fresher `last_log_term` granted with a smaller `last_log_index`; tie on term grants only when `cand_last_log_index >= log_len`; the voter's own last-log term/index come from `on_get_entry_term(log_len)`/`log_len` (term 0 when empty) - Test: `!logOk` denies even when `on_vote_eligible` would grant (logOk applied BEFORE the application gate); persist-before-grant unchanged on the grant path - Observable completion: tests compile and FAIL - _Requirements: 3.1, 3.3, 3.4, 3.5, 3.6_ - _Boundary: test_raft.c_ - _Depends: 1.3_ - [x] 4.2 GREEN: implement the logOk gate in `raft_on_vote_request` - Compute `local_last_log_term = (log_len > 0) ? on_get_entry_term(log_len) : 0`; `logOk = (cand_last_log_term > local_last_log_term) || (cand_last_log_term == local_last_log_term && cand_last_log_index >= log_len)` - Insert the logOk deny BEFORE `on_vote_eligible`, after the existing stale-term/UpdateTerm/one-vote-per-term steps; leave persist-before-grant and all existing invariants in place - Observable completion: 4.1 tests pass; no regression in existing vote tests; the logOk block is clearly before `on_vote_eligible` in `raft.c` - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_ - _Boundary: raft.c_ - _Depends: 4.1_ ## 5. (lastLogTerm, lastLogIndex) PreVote freshness (raft.tla:285, side-effect-free) - [x] 5.1 RED: PreVote freshness tests (identical rule, no side effects) - Test: `raft_on_pre_vote_request` applies the SAME `(lastLogTerm, lastLogIndex)` logOk as the binding vote; a stale tuple is denied; the existing stale-term deny and `on_vote_eligible` gate remain in force - Test: across grant and deny, no mutation of `term`/`voted_for`/`voted_for_term`/`role`/`commit_index` and no `on_persist` call (PreVote invariant) - Observable completion: tests compile and FAIL - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ - _Boundary: test_raft.c_ - _Depends: 1.3_ - [x] 5.2 GREEN: implement the logOk gate in `raft_on_pre_vote_request` - Apply the identical logOk predicate (factored to match 4.2) before/with the existing eligibility gate; mutate nothing; no `on_persist` - Observable completion: 5.1 tests pass; no regression in existing PreVote tests; the predicate matches the binding-vote path exactly - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ - _Boundary: raft.c_ - _Depends: 5.1_ ## 6. Simulator backing: commit-advance driver + freshness plumbing - [x] 6.1 Wire `on_commit_advanced` and a commit-advance driver in `raft_sim.c` - Add a per-`raft_sim_node_t` `commit_index` mirror updated from `on_commit_advanced`; implement the callback (record the value + count fires for assertions) - Add a sim helper to drive `raft_advance_commit` on a given node (or all leaders) after delivery - Pass each node's restored `initial_commit_index` (default 0) in `raft_sim_init`; wire `on_commit_advanced` for every node - Observable completion: after replication + delivery, driving the helper advances the leader's mirrored `commit_index`; the callback fire-count is observable - _Requirements: 6.1, 6.6, 2.5_ - _Boundary: raft_sim.c_ - _Depends: 3.2_ - [x] 6.2 Plumb `(last_log_term, last_log_index)` through the sim vote/PreVote routing - Update the sim's vote and PreVote delivery to read the candidate's last-log `(term, index)` from its in-memory log (`term at log_len` / `log_len`) and pass them to the extended `raft_on_vote_request` / `raft_on_pre_vote_request` - Observable completion: existing election sim tests still pass with the extended signatures; a node with a stale last-log term is denied a vote it would previously have won on LSN alone - _Requirements: 6.5, 3.2, 4.2_ - _Boundary: raft_sim.c_ - _Depends: 4.2, 5.2_ ## 7. Simulator invariant tests (Figure-8, Leader Completeness, freshness, trigger semantics) - [x] 7.1 Figure-8 commit-refusal test (G4) - Test: construct a previous-term entry replicated to a quorum but not covered by a committed current-term entry; drive `raft_advance_commit` and assert `commit_index` does NOT move over it; then replicate a current-term entry above it to a quorum, drive `raft_advance_commit`, and assert `commit_index` advances over BOTH (indirect commit) - Observable completion: test passes end-to-end via the sim; the held phase and the released phase are both asserted - _Requirements: 6.2, 6.3_ - _Boundary: test_raft.c, raft_sim.c_ - _Depends: 6.1_ - [x] 7.2 Leader Completeness across forced leader changes - Test: drive replication to a commit, then force leader changes (timeout / partition); assert any entry committed (≤ `commit_index` on the committing leader) is present in the log of every node that subsequently becomes leader - Observable completion: test passes; no future leader is missing a committed entry across the churn - _Requirements: 6.4_ - _Boundary: test_raft.c, raft_sim.c_ - _Depends: 6.2, 7.1_ - [x] 7.3 Freshness-gate lexicographic exercise - Test: drive votes with mismatched `(last_log_term, last_log_index)` tuples and assert the lexicographic outcome — stale-term-but-higher-index denied; fresher-term-but-lower-index granted; tie-term decided by index - Observable completion: test passes for both vote and PreVote paths - _Requirements: 6.5_ - _Boundary: test_raft.c, raft_sim.c_ - _Depends: 6.2_ - [x] 7.4 Trigger-semantics test for `on_commit_advanced` - Test: `on_commit_advanced` fires exactly once on a strict advance and does NOT fire while the current-term rule holds the commit index (Figure-8 held phase) or when not leader - Observable completion: test passes; the callback fire-count matches the number of strict advances - _Requirements: 6.6, 2.2, 2.3_ - _Boundary: test_raft.c, raft_sim.c_ - _Depends: 6.1, 7.1_ ## 8. Verification - [x] 8.1 Full library suite green under ASAN - Run `/xce-build lib`; all existing election + log-replication tests plus the new commit-safety and freshness tests pass under ASAN with zero leaks - Observable completion: test binary exits 0; ASAN clean; no regression in the election-core or log-replication tests - _Requirements: 1.2, 1.3, 2.2, 3.1, 4.1, 6.2, 6.4_ - _Boundary: src/lib/raft, src/lib/testsuite_ - _Depends: 7.1, 7.2, 7.3, 7.4_