Investigation & fixes (failover committed-job loss): - root-cause bug-851 on an owned 3-node cluster: chaotic failover causes silent committed-prefix divergence (WAL hdr.term always 0; two leaders per term; no (term,lsn) reconcile). Evidence in repro-851/ + INVESTIGATION. - live-validated fixes 848/849/850 (recovery-on-promotion, advertise real applied_lsn, persist-layer contiguity guard); add waldump WAL inspector. Specs (strict raft.tla conformance — library owns the log): - TLA-gap-analysis.md: impl vs raft/raft.tla, gaps G1-G6. - raft-log-replication, raft-commit-safety, raft-sim-conformance: extend src/lib/raft to own log/commit/verification via callbacks (specs generated, cross-spec review passed). - ha-term-safety: controller-side adapter + config marker (R1-R7); design done. - config-upgrade-snapshot: R8 descoped here (deps entity-bearing-snapshots, bug-857). - roadmap + buglog (851/854/857) + .wolf knowledge updated.
24 KiB
Research & Design Decisions: raft-library
Discovery Scope
- Type: Complex new library (brownfield extraction from ha.c)
- Process: Full discovery — codebase analysis of ha.c, ha.h, test files
Key Findings
Existing Election State (ha.c)
- ha_state_t contains: term (atomic uint64), voted_for_term, voted_for_name[XCE_MAXNAME_LEN], role (atomic int32)
- ha_elect_ctx_t holds per-election: n_peers, votes_granted, n_replied, max_resp_tag, max_resp_lsn, done_posted, mutex, semaphore
- Transport coupling: ha_start_election_async directly calls endpoint_connect(), iomgr_monitor(), SSL_new()
- Post-election coupling: ha_elect_evaluate_and_signal calls cluster_init_phase2(), push_config_to_standbys()
Root Cause of Split-Brain
- Self-vote was not recorded atomically at election start; voted_for remained empty until a peer vote arrived
- This allowed node B to grant its vote to node A AND node C in the same term if A and C both started elections before B processed either request
- Fix: record voted_for = self_idx immediately in raft_start_election, before sending any RPC
Build vs. Adopt Decision
- No pure-C Raft library suitable for libxce: hashicorp/raft (Go), etcd/raft (Go), no active C alternative
- Decision: build a minimal Raft election library in src/lib/raft/
- Scope is election only (not log replication), which is simpler than full Raft
Synthesis Outcomes
- Generalization: Requirements 2–6 are all specializations of a single state machine with callbacks. One raft_state_t + raft_callbacks_t pattern covers all cases.
- Build vs. Adopt: Build; no suitable C library exists.
- Simplification: No threads inside the library. Synchronous callbacks. One header, one source, one test file. raft_sim.c compiled only under XCE_TESTING.
Priority Jitter Design
- The delay formula (priority_idx * election_timeout_ms) is a protocol-level decision, not transport
- Placed inside the library as raft_on_leader_lost → on_start_election_after_delay callback
- Caller implements the actual timer/sleep
Tracing Decision
- Added on_trace callback to raft_callbacks_t for full protocol observability
- Simulator dump log enables post-mortem analysis of split-brain scenarios
- raft_snapshot() provides safe point-in-time state read for crash/failure logging
Cross-Spec Gap Audit (2026-06-08)
Batch
/kiro-validate-gapacross 9 specs on persistent_ha branch.
Analysis Summary
- raft-library is ~100% complete. All 7 task groups (1–7), all subtasks (1–2.2, 3.1–3.5, 4.1–4.2, 5.1–5.4, 6.1–6.4, 7) are
[x]in tasks.md. Theready_for_implementationflag is set in spec.json. - No remaining gaps: The library (
src/lib/raft/raft.h,raft.c,raft_sim.c) exposesraft_state_t+raft_callbacks_twith 9 callback hooks. The deterministic simulator (raft_sim.c) verifies split-brain freedom across 100 randomized rounds. The controller adapter in ha.c delegates all election state management (term, voted_for, quorum evaluation) to the raft library. - Cross-spec integration:
election-iomgr-refactorusesraft_start_electioninstead of the oldam_i_primary_quorum. The adapter callbacks in ha.c wireon_send_vote_request→ iomgr async connect,on_become_leader→cluster_init_phase2+ config push,on_become_follower→CTRL_STANDBY. Theon_vote_eligiblecallback implements the freshness gate (config_tag + applied_lsn comparison) and role gate (CTRL_PRIMARY_COMMITTING→ deny). - Test coverage: 34+ Unity test cases across 5 groups (A–H) covering basic election correctness, voted_for invariant, eligibility policy, quorum evaluation, election timeout, RDHUP failover, and split-brain prevention. All pass under ASAN.
- Recommendation: The spec can be marked
implemented.
Gap Analysis — Validation Pass (2026-06-09)
Scope: requirements.md (9 requirement groups, 31 acceptance criteria) vs. on-disk working tree (uncommitted changes to
src/lib/raft/raft.c,src/controller/ha.c,ha.h,myapp.canalyzed as-is). spec.json phase:tasks-generated, all tasks.md checkboxes[x]except parent rollups 2/3/4/5/6.
1. Current State Summary
- Library exists and is build-integrated:
src/lib/raft/raft.h(276 lines),raft.c(290 lines),raft_sim.c(207 lines); wired insrc/lib/meson.build(raft.c at line 108; test_raft.c + raft_sim.c under the test executable at lines 175–176, simulator guarded byXCE_TESTING). - Test suite:
src/lib/testsuite/test_raft.c(2990 lines, 47 test functions, 79 RUN_TEST invocations) covering unit tests, simulator tests, and protocol groups A–H plus F/G split-brain scenarios. - Controller adapter live:
ha_state_tembedsraft_state_t raft(ha.h:183); 9 callbacks implemented inha.c(s_real_cbsat ha.c:735);raft_initcalled in ha init (ha.c:854) and re-initialized with real topology byha_raft_reinit(ha.c:~2981–3008). - Production dispatch fully delegated: vote requests →
raft_on_vote_request(cluster.c:373), vote replies →raft_on_vote_reply(ha.c:2361), RDHUP →raft_on_leader_lost(ha.c:3290), election start →raft_start_election(ha.c:3146).
2. Requirement-to-Asset Map
| Req | Criterion | Asset(s) | Status |
|---|---|---|---|
| 1.1 | Per-node state (term/role/voted_for/n_peers) | raft_state_t (raft.h:23–38) |
Implemented |
| 1.2 | Init defaults term=0/FOLLOWER/-1 | raft_init (raft.c:13–55); H2 test |
Implemented (extended: restores initial_term/initial_voted_for from config) |
| 1.3 | No endpoint_t/SSL/iomgr/cluster_t/capnp in header | raft.h includes only errno/stdint/string (+stdio under XCE_TESTING) | Implemented |
| 2.1 | start_election: term++, self-vote, CANDIDATE, send RPCs | raft.c:57–104 | Implemented (also persists self-vote before RPCs — stronger than spec) |
| 2.2 | Single-node immediate LEADER | raft.c:83–88; tests A1, single_node | Implemented |
| 2.3 | -EALREADY if CANDIDATE/LEADER | raft.c:60–61; tests already_candidate/leader | Implemented |
| 3.1 | term > current: upgrade, clear vote, FOLLOWER, evaluate | raft.c:121–130; B2 | Implemented (extra: fires on_become_follower if was LEADER — split-brain fix, uncommitted change) |
| 3.2 | term < current: deny, return current term | raft.c:110–115; B3 | Implemented |
| 3.3 | One vote per term | raft.c:157–164; B1/B4/B5 | Implemented (extension: idempotent re-grant to the SAME candidate same term — beyond spec text; see Gap-3) |
| 3.4 | Freshness gate (config_tag/LSN) before grant | Split: caller-side gate in cluster.c:352–356 + on_vote_eligible hook (raft.c:167–173) |
Partial — behavior present but located in the dispatcher, not in on_vote_eligible as tasks.md 6.2 states; raft_cb_vote_eligible (ha.c:202) intentionally skips freshness. Doc/code drift. |
| 3.5 | LEADER or COMMITTING deny ALL vote requests regardless of term | raft.c:135–148 (same-term deny only); COMMITTING gate in on_vote_eligible adapter |
Partial (intentional deviation) — higher-term request now steps the leader down instead of denying, to prevent the COMMITTING-timeout split-brain. Requirement text is outdated. |
| 4.1 | Quorum (n/2)+1 → LEADER, on_become_leader once | raft.c:222–234; D1, A4 | Implemented |
| 4.2 | Reply term > current → FOLLOWER + on_become_follower | raft.c:197–206; D4 | Implemented |
| 4.3 | Reply after decided → no-op | raft.c:209–213; D3 | Implemented |
| 4.4 | All replied, no quorum → FOLLOWER | raft.c:237–244; D2 | Implemented |
| 5.1 | leader_lost: clear vote, FOLLOWER, delayed election (priority×timeout) | raft.c:268–276; F1 | Implemented |
| 5.2 | Skip election if voted for non-self during delay | raft_check_and_start_election raft.c:280–284; F2 |
Implemented |
| 5.3 | Else start election internally | raft.c:286–289; F3 | Implemented |
| 6.1 | start_election schedules timeout | raft.c:101; E3 | Implemented |
| 6.2 | Timeout undecided → on_become_follower, cancel tracking | raft.c:260–265; E1 | Implemented |
| 6.3 | Timeout after decided → on_cancel_timeout no-op | raft.c:252–257; E2 | Implemented |
| 7.1 | raft_callbacks_t with 6 required fields | raft.h:66–96 (9 fields: 7 required + 2 optional) | Implemented (superset: adds on_start_election_after_delay as required, on_vote_eligible/on_trace optional) |
| 7.2 | void* ctx passed to every callback | raft_config_t.ctx → s->ctx, all call sites | Implemented |
| 7.3 | NULL required callback → error at init | raft.c:23–38 (-EINVAL); Hnull test | Implemented |
| 8.1 | Deterministic N-node simulator, synchronous routing | raft_sim.c + raft.h:197–274; sim tests | Implemented |
| 8.2 | Invariants: one-leader/3-node, dup deny, single-node, timeout re-election | A2/G1 (100 rounds), B1/B4, A1, E3 + G2 partition | Implemented |
| 8.3 | Compile + all tests pass under ASAN | tasks.md task 7 [x]; commits e10cf9a/361397a fixed ASAN issues |
Unknown (stale) — last verified pre-dating current uncommitted edits to raft.c/ha.c; needs re-run |
| 9.1 | Adapter implements all callbacks via endpoint_t/iomgr | ha.c:107–744 (8 raft_cb_* + s_real_cbs) | Implemented |
| 9.2 | ha_start_election_async delegates; no direct ha->term/voted_for writes outside adapter | ha.c:3038–3161 delegates; only atomic loads + persist/restore remain | Partial — production path clean, but legacy handle_request_vote() (ha.c:1791–1873) still contains a full parallel vote-evaluation path with direct ha->voted_for_term/voted_for_name writes. It is production-dead (only test_ha_vote_gate.c calls it) but duplicates election state management contrary to the spirit of 9.2. |
| 9.3 | ha_phase2_hook / ha_rdhup_election_hook still functional | ha.c:495/631/2068/2147 (phase2), ha.c:3267–3284 (rdhup) | Implemented |
Score: 27 of 31 acceptance criteria Implemented; 3 Partial (3.4, 3.5, 9.2 — all deliberate-deviation or residual-cleanup, not functional regressions); 1 Unknown (8.3 ASAN re-verification against the uncommitted working tree).
Constraints / behavior added beyond requirements
- C1 (Constraint):
raft_initrejects evenn_peers > 1with -EINVAL (raft.c:21–22). Not in requirements. A 2-node or 4-node HA deployment cannot initialize raft at all — verify this matches deployment expectations (deploy-ha.sh, cluster config validation) or it becomes a runtime landmine. - C2 (Constraint):
voted_for_termis unconditionally reset to 0 inraft_init(raft.c:43) even wheninitial_voted_for/initial_termare restored from disk. After a restart, the duplicate-vote check (voted_for_term == term, raft.c:157) cannot match the restored term, so a node that voted for X in term T, restarted, then receives a request from Y in the same term T would fall through Step 4 and could grant a second vote in T (subject to the caller-side freshness gate). Test H1 checksterm/voted_forbut notvoted_for_term. Potential one-vote-per-term violation across restart — needs confirmation.
3. Implementation Approach Options (for the remaining gaps)
Option A: Extend existing components (close gaps in place) — RECOMMENDED
- raft.c: initialize
voted_for_term = (initial_voted_for >= 0) ? initial_term : 0inraft_init(C2); add an H6 restart-double-vote test. - requirements.md: amend 3.5 (higher-term step-down is the intended behavior) and 3.4 (freshness gate enforced by the dispatcher/callback split) so spec matches the as-built design; sync tasks.md 6.2 text.
- ha.c: delete or fold legacy
handle_request_vote()into the raft path; retargettest_ha_vote_gate.catraft_on_vote_request+on_vote_eligible(closes 9.2). - Re-run
/xce-build lib+/xce-build controllerunder ASAN to refresh 8.3. - Trade-offs: ✅ smallest diff, no architectural change, leverages existing test harness; ❌ touches a legacy test file; requirements edit needs human re-approval per workflow.
Option B: Create new components (formal vote-record module)
- Introduce a
raft_persisted_vote_t {term, voted_for}pair persisted/restored atomically, replacing the separate initial_term/initial_voted_for fields; new restart-focused test group. - Trade-offs: ✅ makes the term-binding of a vote impossible to lose by construction; ❌ API churn in raft_config_t and ha_raft_reinit for a problem fixable with one init line; over-engineering (YAGNI).
Option C: Hybrid — fix C2 in place now (Option A), defer handle_request_vote removal to a follow-up
cleanup spec/task since it is production-dead.
- Trade-offs: ✅ de-risks the correctness fix from the test-refactor churn; ❌ leaves known duplication in tree longer; two passes of review.
4. Effort & Risk
| Gap | Effort | Risk | Justification |
|---|---|---|---|
| C2 voted_for_term restore fix + test | S | Low | One-line init change + one Unity test in an established harness |
| Requirements/tasks doc sync (3.4, 3.5) | S | Low | Documentation only; needs approval round-trip |
| Legacy handle_request_vote removal (9.2) | S–M | Medium | Production-dead but test_ha_vote_gate.c must be retargeted; risk of disturbing controller test baseline |
| ASAN re-verification (8.3) | S | Low | Mechanical /xce-build run; fixes only if new findings |
| C1 even-n_peers policy confirmation | S | Medium | If 2-node deployments are expected, this is a hard init failure discovered only at runtime |
Overall remaining work: S–M total, Low–Medium risk — the library and integration are functionally complete.
5. Recommendations for Design/Closure Phase
- Adopt Option A. Fix C2 (
voted_for_termrestore) first — it is the only candidate correctness gap; everything else is documentation or hygiene. - Update requirements.md 3.5 and 3.4 to match the as-built (and better) behavior: leader steps down on
higher term; freshness gate enforced caller-side before
raft_on_vote_requestwith COMMITTING/liveness inon_vote_eligible. Re-approve, then run/kiro-verify-completion raft-library. - Remove or refactor legacy
handle_request_vote(ha.c:1791) to eliminate the last duplicated election state path (Req 9.2), retargetingtest_ha_vote_gate.c. - Re-run task 7 ASAN validation against the current working tree before committing (the verified state predates the uncommitted raft.c Step-3/Step-4 rewrite).
Research Needed
- R1: Confirm intended cluster-size policy — are 2-node (even) HA clusters a supported deployment? If yes, the even-n_peers -EINVAL in raft_init must be relaxed; if no, add config-load validation so the failure surfaces at startup with a clear message rather than as a silent raft_init failure.
- R2: Verify whether the restart double-vote scenario (C2) is reachable in production timing — does
ha_raft_reinitrun before the listener accepts vote requests? If unreachable, document why; fix anyway. - R3: Confirm
raft_check_and_start_electionignoring theraft_start_electionreturn code (raft.c:287–289) is safe in all adapter paths (comment claims -EALREADY unreachable; verify when an inbound vote request flips role between RDHUP and the delayed check).
Gap Re-Audit (2026-06-18)
Post-implementation audit. Spec phase = implemented, tasks 24/24 [x]. This pass re-verifies every EARS requirement against the working tree and reconciles the prior audit's open items (C2, R1–R3). Assets are tagged: [CLOSED] verified met in code, [CLOSED-AS-BUILT] met but behavior differs from literal req text (improvement), [RESIDUAL] doc/hygiene gap, [SCOPE-DRIFT] code beyond spec.
Requirement-to-Asset Map
| Req | Acceptance | Asset (file:line) | Status |
|---|---|---|---|
| 1.1 | per-node state fields | raft.h:28-50 raft_state_t (term/role/voted_for/voted_for_term/n_peers) |
CLOSED |
| 1.2 | init term=0 role=FOLLOWER voted_for=-1 | raft.c:40-56 raft_init; defaults via cfg {0,-1} |
CLOSED |
| 1.3 | no transport types in header | raft.h:4-6 includes only errno/stdint/string; no endpoint_t/SSL_CTX/cluster_t |
CLOSED |
| 2.1 | election: incr term + self-vote + send RPCs | raft.c:68-71,98-102 |
CLOSED |
| 2.2 | single-node → immediate LEADER | raft.c:87-92 returns RAFT_ELECTED_IMMEDIATELY |
CLOSED |
| 2.3 | already CANDIDATE/LEADER → error, no state change | raft.c:64-65 -EALREADY |
CLOSED |
| 3.1 | term>cur → update,clear vote,FOLLOWER,evaluate | raft.c:125-137 (also steps down leader via on_become_follower) |
CLOSED-AS-BUILT |
| 3.2 | term<cur → deny + return cur term | raft.c:114-119 |
CLOSED |
| 3.3 | same term, diff voted_for → deny | raft.c:161-168 (gated on voted_for_term==term) |
CLOSED |
| 3.4 | freshness gate pass → grant + persist | raft.c:170-190; freshness split: tag/lsn floor enforced caller-side (cluster.c:363), COMMITTING/liveness in on_vote_eligible (ha.c:299 raft_cb_vote_eligible) |
CLOSED-AS-BUILT |
| 3.5 | LEADER/COMMITTING → deny all | raft.c:148-152 LEADER deny; COMMITTING enum removed (ha-single-leader-role 93dfc02), now an on_vote_eligible policy |
CLOSED-AS-BUILT |
| 4.1 | quorum ceil((n+1)/2) → LEADER once | raft.c:372-383, quorum=(n_peers/2)+1 == ceil for odd n |
CLOSED |
| 4.2 | reply term>cur → FOLLOWER + on_become_follower | raft.c:346-355 |
CLOSED |
| 4.3 | post-decision no-op | raft.c:358-362 election_decided guard → RAFT_PENDING |
CLOSED |
| 4.4 | all replied, no quorum → FOLLOWER | raft.c:386-393 replies_received==n_peers-1 |
CLOSED |
| 5.1 | leader-lost → clear vote + delay(priority*timeout) | raft.c:417-425 |
CLOSED |
| 5.2 | voted non-self during delay → skip | raft.c:429-433 RAFT_SKIP_ELECTION |
CLOSED |
| 5.3 | not voted / self → start election | raft.c:435-438 RAFT_ELECTION_STARTED |
CLOSED |
| 6.1 | start election → on_schedule_timeout | raft.c:105 |
CLOSED |
| 6.2 | timeout undecided → on_become_follower + cancel | raft.c:409-414 |
CLOSED |
| 6.3 | timeout decided → on_cancel_timeout, no-op | raft.c:401-407 |
CLOSED |
| 7.1 | raft_callbacks_t with required fn ptrs | raft.h:79-117 (9 fields incl. on_send_pre_vote_request) |
CLOSED |
| 7.2 | void* ctx passed unchanged | raft.h ctx threaded; raft.c passes s->ctx everywhere |
CLOSED |
| 7.3 | NULL required callback → non-zero | raft.c:23-38 7 required-cb checks → -EINVAL |
CLOSED |
| 8.1 | deterministic N-node simulator | raft_sim.c, raft.h:264-342 (XCE_TESTING) |
CLOSED |
| 8.2 | 4 invariant cases (a-d) | test_raft.c: A2 one-leader, B1/B4 deny-second, A1 single-node, E*/sim timeout |
CLOSED |
| 8.3 | compiles + tests pass under ASAN (leaks=0) | meson raft/raft.c (l.109) + test_raft.c (l.179); 209 RUN_TEST/test fns |
CLOSED (re-run, see V1) |
| 9.1 | adapter implements all callbacks, no dup state | ha.c:1309-1320 s_real_cbs wires all 10; production state in raft_state_t |
CLOSED |
| 9.2 | ha delegates term/voted_for/quorum to library | ha.c:4382-4522 ha_start_election_async→raft_start_election; cluster.c:418 vote req; ha.c:3237 vote reply |
CLOSED-AS-BUILT |
| 9.3 | XCE_TESTING hooks still functional | ha_rdhup_election_hook (ha.c:4528), ha_phase2 seams retained |
CLOSED |
Score: 24/24 acceptance-criteria groups CLOSED. No requirement is unimplemented. 5 are CLOSED-AS-BUILT (real code is a strict superset/improvement over literal req text — see residual R-DOC).
Reconciliation of Prior Audit Open Items
- C2 (voted_for_term restore double-vote) — NOW FIXED.
raft.c:47setsvoted_for_term = (initial_voted_for != -1) ? initial_term : 0at init, closing the post-restart second-vote window. Regression testtest_proto_H6_restored_vote_blocks_second_vote(test_raft.c:2100, registered l.3978) guards it. Prior research's "only candidate correctness gap" is resolved (commit9821ec4). - R3 / Req 9.2 dual vote path — DOWNGRADED. Legacy
handle_request_vote(ha.c:2608) is now#ifdef XCE_TESTING-only — the production vote path is exclusivelycluster.c:418 → raft_on_vote_request. The duplicated election-state logic no longer runs in production; it remains only as a test fixture fortest_ha_vote_gate.c/test_ha_election.c. No production split-brain risk from the dual path. Residual: the test-only copy can drift from raft.c semantics over time (R-DUP below). - R1 (even-n_peers policy) — STILL OPEN.
raft_initrejects even n_peers>1 with-EINVAL(raft.c:21-22), testtest_raft_init_even_peers_returns_einval. Not stated in requirements; a 2-node HA deploy would silently fail raft_init. Needs requirements note + startup-time config validation.
Residual Gaps (non-blocking)
- R-DOC (Low): requirements.md 3.4/3.5 still describe COMMITTING as an in-library role gate and the freshness gate as in-library; as-built, COMMITTING enum is removed and freshness is split caller-side + on_vote_eligible. Requirements text lags the (better) implementation. Update for traceability.
- R-DUP (Low): test-only
handle_request_voteduplicates vote-gate logic; risk of semantic drift from raft.c. Consider retargetingtest_ha_vote_gate.contoraft_on_vote_requestdirectly. - R1 (Medium): even-n_peers rejection undocumented; surfaces as silent raft_init -EINVAL rather than a clear startup error. Add config-load validation OR a requirement clause.
- R-SCOPE / SCOPE-DRIFT (informational, not a gap): PreVote (raft_begin_pre_vote, raft_on_pre_vote_request, raft_on_pre_vote_reply, on_send_pre_vote_request, RAFT_PRE_VOTE_* codes; raft.c:193-336, raft.h:160-259) lives in the raft-library files but belongs to the separate ha-prevote spec (commits 449b8e9–e6b234a). It is purely additive, mutates no persistent Raft state, and does not violate any raft-library requirement. Flag only so the raft-library boundary doc reflects the co-tenancy: raft.h/raft.c now host two specs' surfaces.
Integration Risks
- IR-1 (Low): PreVote tally arrays are bounded by
RAFT_MAX_PEERS=8(raft.h:18);raft_on_pre_vote_replyrange-guards (raft.c:287). Real-vote path has no equivalent fixed array (uses scalar counters) so it is unbounded-safe. Mismatch is intentional but worth a one-line note in design. - IR-2 (Low):
raft_check_and_start_electiondiscardsraft_start_electionreturn (raft.c:436-437); comment claims -EALREADY unreachable because raft_on_leader_lost forces FOLLOWER. Valid for the current adapter, but an inbound vote/AppendEntries that flips role between RDHUP and the delayed fire could make it reachable. Low likelihood; assert instead of (void) for defense-in-depth.
Open Work — Option Analysis
Option A — Doc-only closure (recommended). Update requirements.md 3.4/3.5/9.2 to as-built; add R1
even-n_peers clause; note PreVote co-tenancy + IR-1/IR-2 in design.md. Then /kiro-verify-completion.
- Effort: S (~1-2h, docs only). Risk: Low. No code change, no regression surface.
Option B — Doc closure + hygiene. Option A plus: (1) startup config validation for even n_peers with a
loud error, (2) retarget test-only handle_request_vote onto raft_on_vote_request (kill R-DUP), (3) convert
IR-2 (void)rc to an assert.
- Effort: M (~half day). Risk: Low-Medium (touches ha.c test seam + config load; rerun full controller + lib test suites).
Option C — Defer, accept as-is. Ship implemented; track R-DOC/R-DUP/R1 as backlog. The only real correctness item (C2) is already fixed and tested, so functional risk of deferring is minimal.
- Effort: none. Risk: Low functionally; Medium for spec/code traceability drift over time.
Validation To Re-Run
- V1:
/xce-build liband/xce-build controllerwithASAN_OPTIONS=detect_leaks=0— no build dir present in the worktree at audit time; the test_raft.c suite (209 cases) and controller tests should be re-run to confirm green on the current tree (which now includes the PreVote additions + C2 fix, post-dating the last recorded task-7 ASAN run).
Bottom line: raft-library is functionally complete — 24/24 met, the sole prior correctness gap (C2) is fixed and regression-tested, and the production dual-vote-path risk is eliminated (legacy path is test-only). Remaining work is documentation/hygiene (Option A, S effort, Low risk) plus a clean ASAN re-run (V1).