myxce/.kiro/specs/ha-repl-hardening/research.md
Rongsong Shen 05febbc9a7 docs(specs): gap re-validation 2026-06-24 (5 HA/raft specs)
/kiro-validate-gap gap-focused sweep. Per-spec gap analysis written to each
spec's research.md (3 new, 2 appended), grounded in file:line evidence:

- config-upgrade-snapshot (new): real open gap — transport + (term,lsn) marker
  + COW substrate exist, but the entity-bearing snapshot PAYLOAD does not
  (16-byte LSN sentinel). HARD-blocked on entity-bearing-snapshots. Closes
  bug-857. Effort L (XL incl. blocker) / Risk High.
- raft-commit-safety (new): CLOSED — G4 Figure-8 commit (raft.c:733) + G6 vote
  freshness (raft.c:157) done, full sim coverage; the "(0,0) stopgap / broken
  build" concern is stale (build green, real vote tuple at cluster.c:435).
- raft-sim-conformance (new): mostly open — raft.tla is reference-only; only
  1 of 4 invariants (Log Matching) checked in code. Missing PRNG/replay,
  message-fault bag, scheduler, 3 invariant checkers, fault switches,
  scenarios. Effort L / Risk Medium.
- ha-repl-hardening (append): RESCOPE/superseded — 31/33 EARS closed; Req 2.3
  + Req 6 obsolete (CTRL_PRIMARY_COMMITTING removed). Residual: docs only.
- ha-convergence-recovery (append): done bar Task 8 (live E2E); bug-1028
  post-failover availability resolved 2026-06-23.

Skipped ha-elect-ctx-lifecycle (no requirements.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
2026-06-24 10:04:06 +00:00

588 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Gap Analysis: HA Replication Hardening
> Analysis date: 2026-06-04. Phase: `requirements-generated`.
> Purpose: identify what exists, what's missing, and viable implementation approaches.
## Analysis Summary
- **Scope**: 7 requirements — heartbeat-driven state convergence, WAL catch-up wiring, per-standby tracking, snapshot transfer, batch WAL transfer, COMMITTING gate, observability
- **Key finding**: The primary already sends heartbeats (`OPCode_nop`) and receives replies; the `CtrlHelloReply` already carries `committed_lsn` and `config_tag`; `persist_checkpoint` already exists. The gaps are in wiring, not new infrastructure.
- **Largest gaps**: (1) heartbeat reply doesn't carry standby's `applied_lsn`/`config_tag`; (2) WAL catch-up handler is a stub; (3) snapshot transfer path is a stub; (4) no per-standby convergence tracking in the primary.
- **Recommendation**: Extend existing heartbeat reply with state fields; wire the WAL catch-up stub to `apply_wal_entry()`; implement snapshot transfer using existing `persist_checkpoint()`; add per-standby convergence tracking to `ha_state_t` or extend `standby_conn_t`.
## 1. Current State Investigation
### 1.1 Heartbeat Protocol
**Current state**: Asymmetric, unidirectional state push.
- Primary sends `OPCode_nop` via `primary_heartbeat_fn` (ha.c:2880) every `heartbeat_interval_ms` (3s default)
- Standby receives nop in `myreq_handler` (cluster.c:225), sends empty `RequestReply`, calls `ha_on_heartbeat_received` (ha.c:2655) to reset liveness watchdog
- `CtrlHelloRequest`/`CtrlHelloReply` (controller.capnp:835-846) carry `appliedLsn`, `configTag`, `committedLsn` — but only exchanged once on connect, not periodically
**Gap**: Heartbeat reply is empty. Standby's current `(applied_lsn, config_tag)` is not reported back to the primary on each heartbeat cycle.
### 1.2 Per-Standby State Tracking
**Current state**: `standby_conn_t` (repl.h:22-33) tracks per-standby `acked_lsn` and `acked_config_lsn` (for WAL and config quorum), plus `in_catch_up` and `pending_ack_count`. `ha_state_t` has `standby_endpoints` (zarray of endpoints) and `n_peers`, but no per-standby convergence tracking.
**Gap**: No field for the standby's self-reported `applied_lsn` or `config_tag` from heartbeat replies. The primary has no way to know whether a standby has converged without receiving explicit WAL/config ACKs.
### 1.3 WAL Catch-Up Handler
**Current state**: Stub at ha.c dispatch gate (OPCode_walCatchupEntry returns 0). The primary-side streaming loop in config_repl.c:1489-1520 reads WAL segments and sends `OPCode_walCatchupEntry` messages, but the standby does nothing with them.
**Gap**: Standby never applies WAL catch-up entries to local state. `apply_wal_entry()` (persist.c:685) exists and is used during startup WAL replay — can be reused.
### 1.4 Snapshot Transfer
**Current state**: `handle_ctrlhello` (config_repl.c:1359) checks `repl_needs_snapshot()` (repl.c:491) and if true, logs WARN at line 1418 ("snapshot transfer not yet implemented") and skips. `persist_checkpoint()` (persist.c:610) exists and creates a consistent checkpoint.
**Gap**: The snapshot creation and transfer path is entirely unimplemented. Without it, any standby whose LSN predates the WAL retention window is permanently dead.
### 1.5 WAL Entry Batching
**Current state**: The WAL streaming loop in config_repl.c:1481-1521 sends one `xce_request` per WAL entry. `xce_request` enqueues to the endpoint's send buffer (non-blocking), but the loop processes entries sequentially with `pread()` per entry + `malloc`/`free` per payload.
**Gap**: No batching. For a standby catching up on millions of entries, this is CPU-intensive and slow. The wire protocol needs a batch message format.
### 1.6 COMMITTING Gate
**Current state**: `ha_repl_ack_received()` (ha.c:2197) exists with correct quorum math, but is only called from `repl_ack_decode_and_dispatch()` (ha.c:1859) which processes `OPCode_replCatchUpDone`. No standby sends this opcode. The committing timeout (10s) is the only path to PRIMARY.
**Gap**: The majority-convergence check is unreachable. The committing timeout degrades the cluster on every startup.
## 2. Requirement-to-Asset Map
| Req | Key Gap | Key Reusable Asset | Gap Tag |
|-----|---------|-------------------|---------|
| 1 — Heartbeat carries standby state | Reply body is empty; no state fields in nop reply | `OPCode_nop` send/receive loop, `CtrlHelloReply` fields in capnp schema, `xce_reply` pattern in cluster.c:225-228 | **Missing** |
| 2 — Per-standby convergence tracking | No `applied_lsn`/`config_tag` reported per standby | `standby_conn_t` with `acked_lsn`, `ha->standby_endpoints`, `repl_wait_majority_ack` quorum math | **Missing** |
| 3 — WAL catch-up handler | Stub returns 0 (ha.c:882) | `apply_wal_entry()` (persist.c:685), primary WAL streaming loop (config_repl.c:1489) | **Stub** |
| 4 — Snapshot transfer | Stub logs WARN, skips (config_repl.c:1417-1425) | `persist_checkpoint()` (persist.c:610), `repl_needs_snapshot()` (repl.c:491), `CtrlHelloReply.needsSnapshot` field | **Missing** |
| 5 — Batch WAL transfer | One entry per `xce_request` | Existing WAL streaming loop, capnp message format for walCatchupEntry | **Missing** |
| 6 — COMMITTING gate | `ha_repl_ack_received` unreachable; timeout-only path | `ha_repl_ack_received()` (ha.c:2197) with correct quorum math, `ha_promote_to_primary()` | **Missing** |
| 7 — Observability | Logs exist but need new convergence/snapshot events | `INFO`/`WARN` macros in xcelog.h | **Missing (alongside)** |
## 3. Implementation Approach Options
### Option A: Extend heartbeat reply with state fields (Recommended)
Extend the existing `OPCode_nop` reply or create a lightweight heartbeat-ack to carry `(applied_lsn, config_tag)`:
- **Standby side**: In the nop handler (cluster.c:225), populate reply with current `applied_lsn` and `config_tag` before `xce_reply`
- **Primary side**: In the heartbeat send path or a reply handler, read the standby's reported state and update per-standby tracking
- **Why**: Minimal new code — reuses existing heartbeat loop; no new opcodes needed if we extend the reply body; follows Raft AppendEntries pattern
**Trade-offs**:
- ✅ Reuses existing infrastructure; minimal new opcodes
- ✅ Heartbeat already fires every 3s — convergence detected promptly
- ❌ Need capnp schema change if RequestReply needs new fields
- ❌ Primary must process heartbeat replies (currently fire-and-forget)
### Option B: Use CtrlHelloReply periodically
Send `CtrlHelloRequest` periodically (not just on connect) and use the existing `CtrlHelloReply` fields:
- Already carries `committedLsn`, `configTag` in reply
- Already carries `appliedLsn`, `configTag` in request
- **Why**: No capnp schema changes; ctrlhello already carries all needed fields
**Trade-offs**:
- ✅ No schema changes
- ❌ CtrlHelloRequest triggers WAL streaming (heavy) — not suitable for periodic use
- ❌ Semantic mismatch: ctrlhello is a connection-setup message, not a state-reporting message
### Option C: New lightweight opcode
Add `OPCode_stateReport` / `OPCode_stateAck` to capnp schema:
- Dedicated message type with `(applied_lsn, config_tag, in_catch_up)` fields
- Sent by standby in response to heartbeat, or periodically
- **Why**: Clean separation of concerns
**Trade-offs**:
- ✅ Clean protocol design
- ❌ New opcode, new capnp struct, new handler on both sides
## 4. Effort and Risk Summary
| Req | Effort | Risk | Justification |
|-----|--------|------|---------------|
| 1 — Heartbeat state report | S (1-2d) | Low | Extend existing reply; capnp schema change if needed |
| 2 — Per-standby tracking | S (1d) | Low | New fields in ha_state_t or extend standby_conn_t |
| 3 — WAL catch-up wiring | S (1d) | Low | Wire stub to existing apply_wal_entry() |
| 4 — Snapshot transfer | M (3-5d) | Medium | persist_checkpoint exists; transfer protocol is new |
| 5 — Batch WAL transfer | S (1-2d) | Low | Accumulate entries in loop; capnp schema change |
| 6 — COMMITTING gate | S (1d) | Low | Replace ha_repl_ack_received with heartbeat-driven check |
| 7 — Observability | S (alongside) | Low | INFO/WARN log statements |
**Total estimated effort**: 1-2 weeks. Risk: Low-Medium (snapshot transfer is the main risk).
## 5. Key Reusable Assets
| Asset | Location | Reuse in |
|-------|----------|----------|
| `OPCode_nop` send/reply loop | cluster.c:225, ha.c:2880 | Req 1 (heartbeat state report) |
| `CtrlHelloReply` fields | controller.capnp:835-846 | Req 1 (capnp schema template) |
| `standby_conn_t` + `repl_wait_majority_ack` | repl.h:22, repl.c:338 | Req 2 (convergence tracking) |
| `apply_wal_entry()` | persist.c:685 | Req 3 (WAL catch-up) |
| `persist_checkpoint()` | persist.c:610 | Req 4 (snapshot creation) |
| WAL streaming loop | config_repl.c:1481 | Req 5 (batch send) |
| `ha_repl_ack_received()` | ha.c:2197 | Req 6 (quorum math reuse) |
| `INFO`/`WARN` macros | xcelog.h | Req 7 (observability) |
## 6. Research Needed (Design Phase)
- **Capnp schema design**: Whether to extend `RequestReply` with state fields, add a new `stateReport` struct to existing heartbeat messages, or create a new opcode. Evaluate against existing capnp codegen patterns.
- **Heartbeat reply processing**: Currently fire-and-forget; need to add a reply handler on the primary side. Determine best insertion point in the dispatch path.
- **Snapshot wire format**: How to serialize `persist_checkpoint` output for transfer. COW zarray vs raw file transfer.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **ha-repl-hardening** is **~98% complete**. All 6 task groups (1–6), 27 subtasks (1.1–6.7) are `[x]` in tasks.md. Recent commits (aab41fa through e1517bb) confirm active implementation. The git log shows all tasks through 6.7 implemented with bug fixes applied.
- **Remaining gaps**: Task groups 1–4 and 5–6 all show `[ ]` as section headers, but all subtasks within them are `[x]`. The section checkboxes are cosmetic.
- **Deferred items**: None. The original research's "Research Needed" items (capnp schema design, heartbeat reply processing, snapshot wire format) were resolved during the design phase.
- **Cross-spec relationships**: This spec extends `ha-config-bootstrap` Req 5 (majority-commit WAL) and Req 8 (majority-commit config) with heartbeat-driven convergence. It depends on `ha-heartbeat-liveness` for the idle-triggered NOP policy on the primary and activity-based liveness detection on the standby. It depends on `ha-peer-connection-reuse` for the promoted connection model — `handle_ctrlhello` was reduced to connection setup only (task 5.1), with all replication decisions moved to the heartbeat decision loop.
- **Single-node mode**: Verified working — `n_peers=1`, immediate convergence, `CTRL_PRIMARY` with no heartbeat wait.
- **Recommendation**: Run `/xce-build controller` to confirm ASAN-clean build, then mark the spec `implemented`.
---
# Gap Analysis — Validation Pass (2026-06-09)
> Validation-phase gap analysis: requirements.md vs code **as on disk** (including uncommitted
> changes to ha.c/ha.h/myapp.c/raft.c). All 27 subtasks in tasks.md are `[x]`; this pass verifies
> what the code actually does against each acceptance criterion.
## Headline Finding
**The control plane is complete; the data plane is not.** Every protocol mechanism in this spec
(heartbeat stateReport, per-standby tracking, decision loop, batch framing, snapshot chunking,
convergence-gated promotion, safe commit point) is implemented and unit/integration tested. But
**payload bytes are never transmitted or applied**:
- `ha_send_snapshot_to_standby` sends every chunk with `chunk.payload = NULL` —
`config_repl.c:1638` (`/* encoding deferred */`, explained at config_repl.c:1613-1618: the
capnp `Data` ↔ `xce_data_t` tagged-union encode path is not wired).
- The standby snapshot handler passes `payload_bytes=NULL, payload_len=0` —
`cluster.c:191-197`. Consequently `snap.part` is never written, the `is_last` size check
(`ha.c:1094-1106`) fails (or `stat` fails on a missing file), and `persist_load_snapshot` is
never reached in production. Snapshot transfer works only in unit tests via
`xce_snapshot_chunk_hook`.
- WAL catch-up applies entries **header-only**: `ha_apply_single_entry` (ha.c:974-984) builds
`payload_len=0` and calls `apply_wal_entry(p, &hdr, NULL)`. `apply_wal_entry`
(persist.c:691) then only advances the LSN chain — payload-bearing entry types
(`WAL_JOB_SUBMIT` → capnp job, jobstate entries → `wal_jobstate_t`) restore **nothing**.
**Safety implication**: a standby's `applied_lsn` advances without the corresponding state, so it
reports "converged" via stateReport, the primary promotes out of COMMITTING, and a later failover
to that standby would serve from a state missing all payload-bearing entries. The Majority
Consistency Invariant (design §Overview) is currently tracked but not actually established.
**Cross-spec constraint**: the live replication path has the same hole — `repl_broadcast`
(repl.c:193-234) discards `payload` and sends nothing ("TCP send wiring deferred to task 7.6",
owned by ha-config-bootstrap). Today no WAL payload bytes move between controllers on any path.
## Requirement-to-Asset Map
| Req / AC | Asset (file:line) | Status |
|---|---|---|
| **1.1** heartbeat reply carries (tag, lsn) | `cluster.c:273-287` OPCode_nop handler; `ha_nop_reply_state_report` ha.c:1140; `StateReport` controller.capnp:788 | Implemented |
| **1.2** primary records + evaluates | `repl_ack_decode_and_dispatch` ha.c:2456-2547 → `ha_process_standby_state` ha.c:1198 | Implemented |
| **1.3** convergence criteria (tag≥ AND lsn≥) | ha.c:1211-1212 | Implemented |
| **1.4** tag=0 before config push | reads `myconf->config_version_tag` (0 pre-push) | Implemented |
| **2.1** init tracking on COMMITTING entry | Arrays zeroed only in `ha_init` (ha.c:816-819 comment claims the invariant); `raft_cb_become_leader` (ha.c:445-529) resets `repl_ack_count` but **not** `standby_converged[]`/`standby_reported_*[]` → stale convergence flags survive a demote→re-elect cycle | **Partial** |
| **2.2** update + re-evaluate on reply | ha.c:1198-1223, 2525-2533 | Implemented |
| **2.3** majority threshold ceil(n/2)−1 | `ha_check_majority_convergence` ha.c:1244-1278 requires **all** standbys (`n_converged >= n_peers−1`), not majority. Three conflicting formulas: requirements.md 2.3 = ceil(n/2)−1 (1 standby for n=3); tasks.md 5.2 = ceil(n/2) (2 for n=3); code = n−1 (2 for n=3, 4 for n=5) | **Constraint / deviation** (stricter than spec — safe but availability hit) |
| **2.4** INFO on promotion | ha.c:1266-1267 | Implemented |
| **2.5** regression → not-converged | ha.c:1210-1213 | Implemented |
| **3.1** decode + apply WAL entry | Handler wired (`cluster.c:161-173` → `ha_apply_wal_catchup_entry` ha.c:986) but applies **header-only, payload dropped** (ha.c:974-984) | **Partial (CRITICAL)** |
| **3.2** advance applied_lsn | `ha_advance_applied_lsn` ha.c:960 (atomic, monotonic) | Implemented |
| **3.3** ERROR + skip on failure | ha.c:1003-1016, 1032-1037 | Implemented |
| **3.4** stays CTRL_STANDBY, no job ops | `ha_dispatch_gate` CTRL_STANDBY case ha.c:1456-1483 | Implemented |
| **3.5** LSN order | sequential batch iteration | Implemented |
| **4.1** create checkpoint + transfer | `ha_send_snapshot_to_standby` config_repl.c:1578 chunks an **existing** `snap-{checkpoint_lsn}.capnp` — never calls `persist_checkpoint()` to create a fresh one; payload=NULL | **Partial** |
| **4.2** load snapshot, set applied_lsn | `ha_handle_snapshot_chunk` ha.c:1058-1131 complete (verify/rename/load/applied_lsn) but unreachable in production (NULL payload, cluster.c:196) | **Partial** |
| **4.3** no job ops until loaded | dispatch gate (standby role unchanged) | Implemented |
| **4.4** retry via reconnect | Restart-on-reconnect documented (ha.c:1054, cluster.c:178-180) but **no production code unlinks `snap.part`** on reconnect — stale partial file breaks the next transfer's size check | **Partial** |
| **4.5** single-node: no snapshot | no standbys → decision loop never fires | Implemented |
| **5.1** multiple entries per message | `entries :List(WalCatchupEntry)` controller.capnp:850; `send_wal_batch` config_repl.c:1409 | Implemented |
| **5.2** configurable max batch size | `WAL_STREAM_BATCH_ENTRIES`=256 / `WAL_STREAM_BATCH_BYTES`=64KB are compile-time #defines, not runtime config | Implemented (minor constraint) |
| **5.3** apply in order, advance after batch | ha.c:992-1027 (`last_good_lsn` post-batch advance) — but payload gap applies | **Partial** |
| **5.4** partial batch sent immediately | config_repl.c:1556-1563 | Implemented |
| **5.5** ordering + durability | Ordering yes; "durably written" weak — entities restored in-memory, no fsync on standby (applied_lsn in-memory-only is a documented design choice), and payload gap applies | **Partial** |
| **6.1** heartbeats continue in COMMITTING | self-re-arming heartbeat task (fix e1517bb) | Implemented |
| **6.2** NOT_READY for non-repl opcodes | `ha_dispatch_gate` CTRL_PRIMARY_COMMITTING allowlist ha.c:1436-1454 | Implemented |
| **6.3** immediate promotion on majority | ha.c:2529-2533 (before repl dispatch) — subject to 2.3 threshold deviation | Implemented (constraint) |
| **6.4** timeout → WARN + degraded mode | `committing_timeout_fn` ha.c:2885-2914; default 60000 (`HA_DEFAULT_COMMITTING_TIMEOUT_MS` ha.h:131) | Implemented |
| **6.5** single-node immediate | n_peers=1 → threshold 0 (ha.c:1261-1264; fixed in e10cf9a) | Implemented |
| **7.1** catch-up start INFO | config_repl.c:1445-1447 | Implemented |
| **7.2** converged INFO | ha.c:1216-1222 | Implemented |
| **7.3** snapshot start/complete INFO | config_repl.c:1595, 1663 | Implemented |
| **7.4** majority INFO | ha.c:1266 | Implemented |
| **7.5** timeout WARN w/ counts | ha.c:2905-2908 | Implemented |
| **Flow 0** safe commit point (tasks 4.1/4.2) | `ha_compute_safe_committed_lsn` ha.c:373-432; truncation + committed baseline in `raft_cb_become_leader` ha.c:456-485 | Implemented |
**Score**: 26 of 33 acceptance criteria fully implemented. Requirements 1, 6, 7 (and Flow 0
safety) Implemented; Requirement 2 Partial (init-reset + threshold semantics); Requirements 3, 4,
5 Partial — all blocked on the same payload data-plane gap.
**Uncommitted working-tree changes** (ha.c/ha.h/myapp.c/raft.c/deploy-ha.sh) are election-protocol
hardening adjacent to this spec: `ha_init_raft` one-time init (prevents raft state reset on
re-election), term-scoped candidate-agnostic vote denial in `raft_on_vote_request` with idempotent
re-grant, startup self-vote/stale-vote handling, and watchdog arming on the skip-election path.
These strengthen Election Safety per raft-tla-audit.md and should be committed, but they do not
close any of this spec's acceptance criteria.
## Implementation Approach Options (remaining gaps)
### Gap 1 — Payload encoding for `WalCatchupEntry.payload` and `SnapshotTransfer.payload` (the blocker)
**Option A — Extend the typemap codegen to map capnp `Data` ↔ raw buffer for these structs.**
The schema already declares `payload :Data` on both structs; only the C-side encode/decode of the
`xce_data_t` tagged union is missing.
- ✅ Fixes both messages at once; follows the established schema/typemap convention; benefits any
future Data-carrying message
- ❌ Touches generated-code path — regression risk across all opcodes; needs codegen expertise
**Option B — Hand-rolled encode/decode for just these two opcodes.**
Bypass the tagged union: serialize `lsn/type/len/bytes` with direct capn calls (or a length-prefixed
raw layout) in `send_wal_batch` / `ha_send_snapshot_to_standby` and their decode counterparts.
- ✅ Localized, no codegen risk, fastest path to a working data plane
- ❌ Two bespoke wire paths diverging from every other message; long-term maintenance debt
**Option C — Phased hybrid (recommended).**
Phase 1: hand-wire the snapshot payload (raw file bytes — trivially a byte stream, no per-entry
structure) to make Req 4 production-real. Phase 2: solve `Data` encoding properly in the typemap
for `WalCatchupEntry` (structured, batched, used on the hot path) and migrate the snapshot path to
it. Until both land, keep the spec phase below `implemented` or gate promotion on a data-plane
flag.
- ✅ De-risks incrementally; snapshot (rare, big) and WAL (frequent, small) have different shapes
- ❌ Two-step migration; temporary inconsistency between the two paths
### Gap 2 — Convergence threshold semantics (Req 2.3 vs code)
- **Option A**: change code to the requirement's majority formula (`ceil(n_peers/2) − 1` standbys).
Restores availability (one dead standby in a 3-node cluster no longer forces the 60s degraded
timeout) and matches Raft quorum; safe because safe_committed_lsn already derives from a majority.
- **Option B**: keep all-standbys convergence and amend requirements.md + tasks.md with the
rationale (strongest consistency at startup; degraded timeout covers minority loss).
- Either way, **reconcile the three formulas** (requirements 2.3, task 5.2, code) — they currently
disagree for any n ≥ 5.
### Gap 3 — Small correctness fixes (extend existing components)
1. Zero `standby_converged[]` / `standby_reported_*[]` in `raft_cb_become_leader` on COMMITTING
entry (Req 2.1 — re-election staleness).
2. `unlink(snap.part)` on standby reconnect/ctrlhello (Req 4.4 — stale partial file).
3. Create a fresh checkpoint (via delaymgr — `persist_checkpoint` is blocking I/O, must not run in
the vtask/heartbeat-reply context) when SEND_SNAPSHOT fires and the on-disk snapshot is older
than `oldest_wal_lsn` coverage requires (Req 4.1).
4. Consider caching `ha_compute_oldest_wal_lsn` — currently an opendir/readdir scan per heartbeat
reply per standby in vtask context (ha.c:2538-2543).
5. Optionally promote batch limits to `ha_config_t` (Req 5.2 "configurable").
## Effort and Risk
| Gap | Effort | Risk | Justification |
|---|---|---|---|
| 1 — Payload data plane (Option C) | **M–L (5–10d)** | **Medium** | Snapshot raw-bytes path is S; typemap `Data` codegen + batched WAL payload apply + ASAN-clean lifetime mgmt (current `(xce_data_t *)pbuf` cast at config_repl.c:1529 is already suspect) + E2E re-verification is the bulk |
| 2 — Threshold reconciliation | **S (0.5–1d)** | **Low** | One comparison + spec text alignment; existing tests pin the 3-node case |
| 3.1 — Tracking reset on COMMITTING | **S (<0.5d)** | **Low** | memset 3 arrays at ha.c:510; add re-election unit test |
| 3.2 — snap.part discard | **S (<0.5d)** | **Low** | unlink in handle_ctrlhello standby path |
| 3.3 — Fresh checkpoint on SEND_SNAPSHOT | **M (2–3d)** | **Medium** | Blocking checkpoint must hop to delaymgr; interaction with concurrent WAL writes |
| 3.4 — oldest_wal_lsn caching | **S (0.5d)** | **Low** | Cache + invalidate on segment GC |
| 3.5 — Configurable batch limits | **S (0.5d)** | **Low** | Plumb two fields through ha_config_t |
## Recommendations
1. **Do not mark this spec `implemented`.** All tasks are `[x]` but Requirements 3, 4, 5 are only
control-plane complete; the system currently reports convergence it has not achieved. Reopen
targeted tasks (suggest: 7.x series) for the payload data plane rather than re-flagging 2.x/3.5.
2. Land Gap 3.1 and 3.2 immediately (S, Low) — they are latent correctness bugs independent of the
payload work.
3. Decide Gap 2 (threshold) explicitly and record it in the Decision Log; today three documents
disagree.
4. Sequence the payload work after coordinating with **ha-config-bootstrap task 7.6**
(`repl_broadcast` TCP wiring) — the live path and catch-up path should share one entry
serialization, or the two specs will produce divergent wire formats for the same WAL entry.
5. Add an E2E assertion that verifies **entity state** (e.g., a submitted job visible on the
promoted standby), not just `applied_lsn` equality — the current tests 6.5–6.7 pass despite the
data-plane gap because they only observe LSN/log convergence.
## Research Needed
- How the typemap codegen represents capnp `Data` (`xce_data_t` tagged union table dispatch) and
what it takes to encode an arbitrary byte buffer — prerequisite for Gap 1 Option A/C phase 2.
- Whether convergence should additionally verify content (e.g., last-entry CRC chained via
`wal_entry_hdr_t.crc/prev_crc` in stateReport) so header-only application can never report
false convergence again.
- Ownership boundary with ha-config-bootstrap task 7.6: which spec owns live WAL entry
serialization, and can `send_wal_batch` be reused for it.
- Req 5.5 "durably written" on the standby: is in-memory entity restore + in-memory applied_lsn
acceptable (current documented design), or should batch apply fsync a standby-side WAL copy.
---
# Gap Re-Audit (2026-06-18)
> Post-implementation gap audit. spec.json `phase=implemented`; all 41 sub-tasks `[x]`
> (1.1–7.6 incl. the reopened payload data-plane series). This pass re-verifies every EARS
> criterion against the code **as on disk** — specifically whether the two CRITICAL data-plane
> gaps and the small correctness gaps from the 2026-06-09 pass are now closed, and whether the
> COMMITTING-gate requirements still describe the live code after `ha-single-leader-role` landed.
## Headline Finding
**Both 2026-06-09 CRITICAL blockers are CLOSED. The data plane is real.**
- WAL catch-up now applies payload-bearing entries. `ha_apply_single_entry` (ha.c:1558) does
persist-before-apply: `persist_wal_write_replicated(p, e->lsn, type, e->raw_payload, len)`
(fsync, no re-broadcast, idempotent) THEN `apply_wal_entry(p, &hdr, e->raw_payload)`.
`apply_wal_entry` (persist.c:1305) restores entities by type — `WAL_JOB_SUBMIT`→`restore_job`,
job-state→`restore_job_state`, `WAL_WORKER_CTRL`→`worker_from_capnp`,
`WAL_QUEUE_CTRL`→`restore_queue_state` (persist.c:1342-1389). Header-only application is gone.
- Snapshot payload bytes are transmitted. `ha_send_snapshot_to_standby` (config_repl.c:1857)
stats the real file, chunks 64KB, and ships each chunk via `xce_snapshot_chunk_req_raw(ep,
&chunk, buf, chunk_len)` (config_repl.c:1928) — raw `Data` bytes, not NULL. Standby
`ha_handle_snapshot_chunk` (ha.c:1683→ via `handle_snapshot_transfer` cluster.c:196, dispatched
at cluster.c:589) writes chunks to `snap.part`, verifies total size on `is_last`, atomic-renames
to `snap-{lsn}.bin`, loads via `persist_load_snapshot`, sets `applied_lsn`.
**The Majority Consistency Invariant is now actually established, not merely tracked.** A standby
that reports converged holds the entity state behind its `applied_lsn`.
## Architectural supersession (read before re-opening any 6.x task)
`CTRL_PRIMARY_COMMITTING` **no longer exists** — `grep -rn CTRL_PRIMARY_COMMITTING src/controller`
returns zero hits. `ha-single-leader-role` removed the transitional COMMITTING role:
`raft_cb_become_leader` now stores `CTRL_PRIMARY` directly (ha.c:1034) with the comment "there is
no transitional COMMITTING state." New-leader safety is provided by a different, stronger
mechanism than this spec's heartbeat-convergence gate:
1. **Own-term WAL_NOOP + writable_lsn gate** (`ha_arm_own_term_gate` ha.c:2259) — every WAIT
completion is withheld until `committed_lsn >= own-term noop LSN`, i.e. until this term has
committed an entry on a majority.
2. **Per-operation write quorum** (`ha_dispatch_gate` ha.c:2061 → `ha_has_write_quorum` ha.c:2031)
— state-changing opcodes return `-XCE_ERR_NOT_READY` unless `repl_active_standby_count >=
n_peers/2`; replication and read-only/fact-bearing opcodes are always admitted (allowlist at
ha.c:2078-2106).
3. **Vote-derived safe commit point** (`ha_compute_safe_committed_lsn` ha.c:480, called from
raft_cb_become_leader ha.c:908) — `safe_committed_lsn` = majority-threshold element of granted
voters' LSNs; `persist_truncate_wal_above(safe_lsn)` + `committed_lsn=safe_lsn` (ha.c:914).
Net effect: the *intent* of Req 6 (don't serve un-replicated state; gate on majority) is met
**more strongly and continuously** than the original timeout-bounded COMMITTING design — it is now
enforced per-operation for the life of the term, not just at startup. The original Req 2.3 / 6.1–6.4
wording (COMMITTING role, 60s `committing_timeout_ms` degraded fallback, immediate
COMMITTING→PRIMARY on majority) describes a design that was deliberately replaced. This is a
**documentation drift**, not a functional gap.
## Requirement-to-Asset Map (re-verified)
| Req / AC | Asset (file:line) | Status |
|---|---|---|
| **1.1** heartbeat reply carries (tag,lsn) | `OPCode_nop` handler cluster.c:300-301; populates from `myconf->config_version_tag` + `applied_lsn` | CLOSED |
| **1.2** primary records + evaluates | `repl_ack_decode_and_dispatch` → `ha_process_standby_state` ha.c:1851 | CLOSED |
| **1.3** convergence criteria tag≥ AND lsn≥ | ha.c:1864-1865 | CLOSED |
| **1.4** tag=0 before config push | reads live `config_version_tag` (0 pre-push) | CLOSED |
| **2.1** init tracking on term entry | **FIXED** — `raft_cb_become_leader` memsets `standby_reported_tag/_lsn/_converged` (ha.c:1043-1045) on CTRL_PRIMARY entry. Closes 2026-06-09 Gap 3.1. | CLOSED |
| **2.2** update + re-evaluate on reply | `ha_process_standby_state` ha.c:1851-1876 | CLOSED |
| **2.3** majority threshold | **RECONCILED** to `n_peers/2` (C int div) per task 7.5 + Decision Log 2026-06-09. `ha_has_write_quorum` uses `connected >= n_peers/2` (ha.c:2058); `safe_committed_lsn` uses `n_granted/2` (ha.c:527). Three-doc disagreement resolved. | CLOSED |
| **2.4** INFO on promotion/converged | ha.c:1869-1875 (per-standby converged INFO) | CLOSED |
| **2.5** regression → not-converged | ha.c:1863-1866 (recomputed every reply) | CLOSED |
| **3.1** decode + apply WAL entry **with payload** | **FIXED** — `ha_apply_single_entry` ha.c:1558-1589 passes `raw_payload` to `apply_wal_entry`; entities restored persist.c:1342-1389. Closes 2026-06-09 CRITICAL. | CLOSED |
| **3.2** advance applied_lsn | `ha_advance_applied_lsn` ha.c:1541 (atomic monotonic); batch advances after full batch (ha.c:1656-1659) | CLOSED |
| **3.3** ERROR + skip on failure | ha.c:1633-1647, 1666-1672 (NULL/failed sub-entries skipped, no crash) | CLOSED |
| **3.4** stays STANDBY, no job ops | `ha_dispatch_gate` STANDBY case | CLOSED |
| **3.5** LSN order | sequential batch iteration + persist-before-apply | CLOSED |
| **4.1** fresh checkpoint + transfer | **FIXED** — `ha_dispatch_repl_action_for_standby` calls `persist_checkpoint(&c->persist)` before send (ha.c:1961), -EBUSY falls back to existing. Closes 2026-06-09 Gap 3.3. | CLOSED |
| **4.2** load snapshot, set applied_lsn | `ha_handle_snapshot_chunk` ha.c:1683 (verify→rename→`persist_load_snapshot`→applied_lsn); reachable in production (real payload) | CLOSED |
| **4.3** no job ops until loaded | dispatch gate (standby role) | CLOSED |
| **4.4** retry via reconnect / discard stale | **FIXED (cleaner than proposed)** — `chunk_index==0` unlinks any stale `snap.part` (ha.c:1693, 1708-1713) instead of relying on a reconnect-time unlink. Verified by `test_snapshot_recv_reconnect_discards_snappart` (test_config_repl.c:2045). Closes 2026-06-09 Gap 3.2. | CLOSED |
| **4.5** single-node: no snapshot | no standbys → decision loop never fires | CLOSED |
| **5.1** multiple entries/message | `entries :List(WalCatchupEntry)` + `send_wal_batch` (config_repl.c) | CLOSED |
| **5.2** configurable max batch | `WAL_STREAM_BATCH_ENTRIES`=256 / `_BYTES`=64KB compile-time #defines | CLOSED (minor: limits are compile-time, not runtime ha_config_t — Req says "configurable maximum"; defensible) |
| **5.3** apply in order, advance after batch | `ha_apply_wal_catchup_entry` ha.c:1615-1681 (`last_good_lsn` post-batch advance) — now with real payload | CLOSED |
| **5.4** partial batch sent immediately | config_repl.c batch flush on range end | CLOSED |
| **5.5** ordering + durability | Ordering preserved; durability now STRONGER than original design — `ha_apply_single_entry` fsyncs via `persist_wal_write_replicated` BEFORE apply/ACK (ha.c:1570). Supersedes the "in-memory only" caveat for catch-up. | CLOSED (exceeds spec) |
| **6.1** heartbeats continue / gate on majority | superseded: continuous per-op `ha_has_write_quorum` (ha.c:2031) replaces COMMITTING-only check | CLOSED (via supersession) |
| **6.2** NOT_READY for non-repl opcodes | `ha_dispatch_gate` ha.c:2061-2110 (allowlist + `-XCE_ERR_NOT_READY` default) — now enforced for the whole term, not just COMMITTING | CLOSED (stronger) |
| **6.3** immediate promotion on majority | superseded by own-term gate + safe_committed_lsn (no COMMITTING dwell to exit) | CLOSED (via supersession) |
| **6.4** timeout → WARN + degraded | **OBSOLETE** — no COMMITTING state ⇒ no `committing_timeout_ms`. Degraded operation handled by write-quorum gate (rejects writes, keeps serving reads) rather than a one-shot 60s promotion fallback. | N/A (superseded) |
| **6.5** single-node immediate | `ha_has_write_quorum` returns true when `n_peers<=1` (ha.c:2037); safe_committed_lsn single-node path ha.c:486 | CLOSED |
| **7.1** catch-up start INFO | config_repl.c (send_wal_batch start log) | CLOSED |
| **7.2** converged INFO | ha.c:1869-1875 | CLOSED |
| **7.3** snapshot start/complete INFO | config_repl.c:1874-1876, 1939-1941 | CLOSED |
| **7.4** majority INFO | per-standby converged INFO (ha.c:1870); promotion is direct-to-PRIMARY INFO (ha.c:1036) | CLOSED |
| **7.5** timeout WARN | OBSOLETE with COMMITTING removal; quorum-loss edge logged once via `ha_log_quorum_transition` (ha.c:2076) instead | N/A (superseded by quorum-transition logging) |
| **Flow 0** safe commit point | `ha_compute_safe_committed_lsn` ha.c:480-540; truncate+baseline raft_cb_become_leader ha.c:898-923 | CLOSED |
**Score: 31/33 original acceptance criteria CLOSED; 2 (6.4, 7.5) OBSOLETE by deliberate
architectural supersession (CTRL_PRIMARY_COMMITTING removed by ha-single-leader-role).**
Counting the superseded pair as satisfied-by-replacement (their intent — gate on majority, observe
degraded mode — is met more strongly), effective coverage is 33/33. No CRITICAL or HIGH functional
gap remains.
## Residual / Deferred Items (all LOW, none blocking)
1. **Documentation drift (HIGH-doc, LOW-code).** requirements.md Req 6 + design.md still describe
`CTRL_PRIMARY_COMMITTING`, `committing_timeout_ms`, and the 60s degraded-mode timeout, none of
which exist in code. The Introduction (requirements.md:5-7) and Adjacent-expectations also
reference COMMITTING. A reader of the spec will mis-model the live FSM. Code is correct; the
prose is stale.
2. **Req 5.2 "configurable" batch size** is compile-time (`#define`), not runtime config. Minor;
no operator has needed to tune it. Defensible as "configurable at build."
3. **`ha_compute_oldest_wal_lsn`** is an `opendir`/`readdir` scan per heartbeat reply per standby
in vtask context (config_repl.c:1946). Performance-only; fine at current cluster sizes. The
2026-06-09 caching suggestion (Gap 3.4) was never actioned and remains optional.
4. **No covering tests** flagged by codegraph for `ha_process_standby_state`,
`ha_evaluate_repl_action`, `ha_dispatch_repl_action_for_standby`, `ha_send_snapshot_to_standby`
at the unit level (decision-loop logic is exercised via hooks in test_ha_role.c /
test_config_repl.c, and convergence via test_convergence_verification.c). Coverage is
integration-weighted, not absent — but the pure decision functions lack direct unit pins.
5. **E2E entity-state assertion** (2026-06-09 Recommendation 5): tests 6.5–6.7 historically
asserted LSN convergence only. With the data plane now real this is the right place to add a
"submitted job visible on promoted standby" assertion to guard against future regressions to
header-only application. Not done; LOW (data plane verified by unit round-trips per spec.json
note).
## Effort / Risk for Open Work
| Item | Option | Effort | Risk |
|---|---|---|---|
| 1 — Spec/design doc reconciliation (remove COMMITTING; document own-term gate + write-quorum + safe_committed_lsn as the actual Req 6 mechanism) | **A (recommended):** update requirements.md Req 6 + Introduction + design.md to describe the superseding mechanism and cross-link `ha-single-leader-role` / `ha-commit-durability`. **B:** leave a one-line "SUPERSEDED" banner pointing to those specs. | A: S (0.5–1d) · B: XS (<0.5d) | Low (docs only) |
| 2 — Promote batch limits to `ha_config_t` | A: plumb two fields. B: leave as build-time. | S (0.5d) | Low |
| 3 — Cache `oldest_wal_lsn` | A: cache + invalidate on segment GC. B: defer (current scale fine). | S (0.5d) | Low |
| 4 — Direct unit tests for decision functions | A: add `ha_evaluate_repl_action` truth-table + `ha_process_standby_state` convergence-flag tests. B: rely on hook/integration coverage. | S (1d) | Low |
| 5 — E2E entity-state assertion | A: extend test 6.7 to assert a submitted job is present post-failover. B: rely on unit payload round-trips. | S (0.5–1d) | Low |
**Recommendation:** the spec is functionally complete and may remain `implemented`. The only item
worth scheduling is **Item 1 (doc reconciliation)** — the requirements/design now misdescribe the
live FSM after `ha-single-leader-role`, which will mislead future readers and any
`/kiro-verify-completion` run that checks prose against code. Items 2–5 are optional hardening.
---
# Gap Analysis (re-validation) — 2026-06-24
> Re-validation pass. spec.json `phase=implemented` (all 41 sub-tasks `[x]`). This pass re-verifies
> every EARS criterion against the code **as on disk** after the heavy `ha-term-safety` rework
> (commits 004a40b…b77d412, tasks 1.1–4.3) plus bug-828/991/1028/1060/1067 fixes landed since the
> 2026-06-18 audit. Question answered: does this spec still hold, and how much of it is now owned by
> sibling specs?
## Headline Finding
**Still functionally complete; now LARGELY SUPERSEDED in mechanism, but its data-plane code is the
live load-bearing path.** Two distinct things are true at once:
1. **The control-plane *design* of this spec (Req 2.3 / Req 6: `CTRL_PRIMARY_COMMITTING` role +
60 s `committing_timeout_ms` degraded fallback + COMMITTING→PRIMARY-on-majority) is GONE.**
`grep -rn CTRL_PRIMARY_COMMITTING src/controller/*.{c,h}` → 0 hits; `committing_timeout` → 0 hits.
Removed by `ha-single-leader-role` and replaced by a stronger, continuous mechanism
(own-term WAL_NOOP + `writable_lsn` gate `ha_arm_own_term_gate` ha.c:107/1206; per-op write
quorum `ha_has_write_quorum` ha.c:2781 → `-XCE_ERR_NOT_READY`; vote-derived
`ha_compute_safe_committed_lsn` ha.c:510). This is **documentation drift**, not a functional gap —
identical to the 2026-06-18 finding and **still unactioned**.
2. **The data-plane *code* of this spec (Req 1,3,4,5,7) is intact, hardened, and is the path every
sibling spec builds on.** It was NOT rewritten away — `ha-term-safety` routes through it:
- WAL catch-up payload apply: `ha_apply_single_entry` ha.c:2044 → `persist_wal_write_replicated`
(fsync, idempotent) ha.c:2056 THEN `apply_wal_entry` ha.c:2081 (entity restore). Config markers
also flow here (ha.c:2168). Header-only application remains gone.
- Batched streaming: `send_wal_batch` config_repl.c:2005, `WAL_STREAM_BATCH_ENTRIES`=256 /
`_BYTES`=64 KB (config_repl.c:1981-1982); batch flush loop config_repl.c:2071-2173.
- Snapshot raw payload: `ha_send_snapshot_to_standby` config_repl.c:2425 →
`xce_snapshot_chunk_req_raw` config_repl.c:2496 (real bytes).
- Per-standby convergence: `ha_process_standby_state` ha.c:2592 (sets
`standby_reported_tag/_lsn/_converged`, ha.c:2600-2607); reset on term entry ha.c:1115-1117;
fed from `OPCode_reply` stateReport at ha.c:4620.
- `safe_committed_lsn` (this spec's Flow 0, tasks 4.1/4.2) is *explicitly cited by code* as
ha-repl-hardening: ha.c:940 "Task 4.1 (ha-repl-hardening)".
bug-1028 (FIXED 2026-06-23) was inside this very streaming path (`ha_stream_wal_to_standby` gained
a `const persist_t *p` param) — confirming the path is live and maintained, not dead.
## Which sibling spec now owns what
| Concern | Originating in ha-repl-hardening | Now owned / superseded by |
|---|---|---|
| Heartbeat stateReport (Req 1) | yes | retained as-is; consumed by `ha-heartbeat-committed-propagation` (adds committed+term stamp) |
| Per-standby convergence tracking (Req 2.1/2.2/2.5) | yes | retained as-is |
| Majority threshold `n_peers/2` (Req 2.3) | yes | reconciled + **absorbed** into `ha_has_write_quorum` / `safe_committed_lsn` (raft-commit-safety, ha-term-safety) |
| WAL catch-up payload apply (Req 3) | yes | retained; now also drives `ha-term-safety` config-marker apply + bug-991 lsn-advance fix |
| Snapshot transfer (Req 4) | yes | retained as-is |
| Batch WAL transfer (Req 5) | yes | retained; bug-1028 hardened the streamer signature |
| COMMITTING gate (Req 6) | yes | **SUPERSEDED & DELETED** by `ha-single-leader-role` (own-term gate) + `ha-commit-durability` (durable-commit fence) |
| Flow 0 safe commit point (tasks 4.1/4.2) | yes | retained; extended by `raft-commit-safety` (Figure-8) + `ha-term-safety` (per-entry term, (term,lsn) reconcile) — the LSN-only safe point is now term-aware upstream |
## Requirement-to-Asset Map (delta vs 2026-06-18 — only changes noted)
All 31 CLOSED criteria from 2026-06-18 remain CLOSED (assets re-verified, line numbers shifted +~860
in ha.c / +~560 in config_repl.c due to ha-term-safety growth; spot-checked above). No regression.
| Req / AC | Change since 2026-06-18 | Status |
|---|---|---|
| **3.x** payload apply | Path unchanged in shape; bug-991 fix made the three live-apply setters advance applied_lsn to the LAST-held LSN (correctness hardening). | CLOSED (hardened) |
| **5.x** batch streaming | bug-1028 added `const persist_t *p` to `ha_stream_wal_to_standby` (off-by-one / hdr-source fix). | CLOSED (hardened) |
| **Flow 0** safe commit | Now also gated by ha-term-safety per-entry term + bug-1060 promotion-truncate floor `max(safe_lsn,durable_tip)` (prevents new leader truncating its own committed log) + bug-1067 AE header-term separation. | CLOSED (strengthened upstream) |
| **6.4 / 7.5** COMMITTING timeout/WARN | Still OBSOLETE (no COMMITTING). | N/A (superseded) |
**Score: 31/33 CLOSED, 2 OBSOLETE-by-supersession (6.4, 7.5). Effective 33/33. No CRITICAL/HIGH
functional gap. Unchanged from 2026-06-18, now re-confirmed post ha-term-safety.**
## Disposition: RESCOPE (close as data-plane-complete + supersede the control-plane prose)
This spec should **not** be reopened for new feature work and should **not** be deleted. Recommended
disposition:
- **Keep `phase=implemented`.** Every acceptance criterion is satisfied or satisfied-by-replacement.
- **CLOSE-as-superseded the control plane (Req 2.3 + Req 6 entirely).** Their mechanism was
deliberately replaced. Carry a one-line "SUPERSEDED by ha-single-leader-role / ha-commit-durability"
banner on Req 6 and on the Introduction's COMMITTING references.
- **RETAIN the data plane (Req 1,3,4,5,7 + Flow 0) as the canonical owner.** No sibling spec
re-implements WAL-catch-up payload apply, batching, snapshot transfer, or stateReport — they all
consume this code. If anything, this spec is now the *foundation*, not the obsolete layer.
The 2026-06-09 project-memory note ("data plane MISSING, critical, bugs 335-338") is **stale** — it
was closed by the reopened 7.x payload series (audited 2026-06-18) and is unaffected by later work.
## Residual / Deferred (all LOW, none blocking — unchanged + re-confirmed)
1. **Documentation drift (HIGH-doc, LOW-code) — STILL OPEN.** requirements.md Req 6 + Introduction +
design.md describe `CTRL_PRIMARY_COMMITTING` / `committing_timeout_ms` / 60 s degraded fallback,
none of which exist. This is the *only* item worth scheduling. A `/kiro-verify-completion` run that
checks prose-vs-code will flag Req 6 falsely.
2. **Req 5.2 "configurable" batch size** is compile-time `#define`, not `ha_config_t`. Defensible.
3. **`ha_compute_oldest_wal_lsn`** opendir/readdir per heartbeat reply per standby. Perf-only.
4. **Direct unit tests** for `ha_process_standby_state` / `ha_evaluate_repl_action` /
`ha_send_snapshot_to_standby` — coverage is integration-weighted (test_config_repl.c,
test_convergence_verification.c), not absent.
5. **E2E entity-state assertion** (submitted job visible on promoted standby post-failover) — the
live HA soak (`/xce-test-ha`) now exercises this end-to-end, partially discharging the gap; a
pinned unit/E2E assertion is still nice-to-have.
## Effort / Risk for the one schedulable item
| Item | Option | Effort | Risk |
|---|---|---|---|
| Doc reconciliation: remove COMMITTING from requirements.md Req 6 + Introduction + design.md; document own-term gate + write-quorum + safe_committed_lsn as the real Req 6 mechanism; cross-link ha-single-leader-role / ha-commit-durability / ha-term-safety | **A (recommended):** rewrite Req 6 + Introduction (S, 0.5–1d). **B:** one-line SUPERSEDED banner (XS, <0.5d). | A: S · B: XS | Low (docs only) |
## Recommendation
1. **No open implementation work.** Spec is functionally complete and is the live owner of the HA
replication data plane. Mark project-memory note (2026-06-09 "data plane MISSING") as resolved.
2. **Schedule only the doc reconciliation (Residual #1).** It has been flagged in two prior audits
(2026-06-09 doc-drift, 2026-06-18 Item 1) and remains unactioned. Either rewrite Req 6 (Option A)
or banner it (Option B). Everything else is optional hardening.
3. **Treat this spec as RESCOPED:** control plane CLOSED-as-superseded; data plane RETAINED-as-owner.
## Research Needed
- None new. Prior research items (typemap `Data` encoding, snapshot wire format, ownership boundary
with ha-config-bootstrap task 7.6 / live `repl_broadcast`) were all resolved by the payload series
and ha-term-safety task 3.4 (`repl_broadcast` now carries leader term, term-0 live send retired).