myxce/.kiro/specs/raft-sim-conformance/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

165 lines
14 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 — raft-sim-conformance (2026-06-24)
Analyzed per `.claude/skills/kiro-validate-gap/rules/gap-analysis.md`. Scope: a single
feature — an in-process, deterministic, seed-driven harness that drives N `src/lib/raft`
library instances through `raft.tla`-`Next`-equivalent transitions and asserts the four
Raft safety invariants every step (the "use the verified spec as an oracle" payoff;
acceptance bar = catch a bug-851-class divergence on a deliberately-broken build, pass on
the correct build).
## 1. Current State Investigation
### Authoritative spec
- `raft/raft.tla` — Diego Ongaro's canonical Raft (abstract spec; ends `raft.tla:471`).
All `Next` actions present and located: `Restart` (167), `Timeout` (178),
`RequestVote` (190), `AppendEntries` (204), `BecomeLeader` (229), `ClientRequest` (246),
`AdvanceCommitIndex` (259), `Receive` (421) with `UpdateTerm` (406) and
`DropStaleResponse` (415), `DuplicateMessage` (443), `DropMessage` (448),
`Next` disjunction (454). The `.tla` file carries NO embedded invariant `THEOREM` block;
the four safety invariants (Election Safety, Log Matching, Leader Completeness, State
Machine Safety) are tracked prose-side in `src/controller/CLAUDE.md`. **No TLC config
(`*.cfg`), no `tla2tools`/TLC tooling exists in-repo** — the spec is a reference
document, not a model-checked artifact today.
### How the C library is checked against raft.tla today
- **It is not, automatically.** Conformance is enforced by hand: `src/controller/CLAUDE.md`
holds the TLA+ variable→code and action→code mapping plus a manual "Protocol
Verification Checklist." There is no trace generation, no invariant checker that runs
per-step, and no randomized simulation that asserts the four invariants.
- `src/controller/raft/` does **not** exist; the library lives at `src/lib/raft/`
(`raft.c` 33 KB, `raft.h` 30 KB, `raft_sim.c` 18 KB). Per `src/controller/CLAUDE.md`,
the controller's `raft.c` was election-only; the full election+log+commit protocol now
lives in the **library** `src/lib/raft` (deps `raft-log-replication` +
`raft-commit-safety` landed — callbacks present and exercised in `test_raft.c`).
### Existing harness (`src/lib/raft/raft_sim.c`, `XCE_TESTING`-gated)
Already present (the election + partial log layer it was grown to for the dependency
specs), all consuming published library entry points verbatim:
- `raft_sim_init`, `raft_sim_deliver_pending` (vote RPC routing via
`raft_on_vote_request`/`_reply`), `raft_sim_deliver_append_entries` (AE routing via
`raft_on_append_entries_request`/`_reply`), `raft_sim_start_all_elections`,
`raft_sim_fire_timeout`, `raft_sim_partition`, `raft_sim_advance_commit`
(`raft_advance_commit`), `raft_sim_restart_node` (models Restart, preserves persisted
term/votedFor/log/commit_index), and **one** invariant checker:
`raft_sim_check_log_matching` (`raft_sim.c:424`).
- In-memory adapter callbacks already back the four log callbacks + `on_commit_advanced`
into per-node `raft_sim_node_t` (`log[]`, `log_count`, `commit_index`,
`commit_advanced_count`, `trace_buf`). The library holds no entry bytes — the simulator
owns storage, exactly as the dependency design anticipated.
- `raft.h` `XCE_TESTING` section (`raft.h:432+`) already defines `raft_sim_rpc_t`,
`raft_sim_entry_t`, `raft_sim_ae_t`, `raft_sim_node_t`, `raft_sim_cluster_t`, and bounds
`RAFT_SIM_MAX_NODES=8`, `RAFT_SIM_MAX_LOG=64`, `RAFT_SIM_ENTRY_LEN=16`,
`RAFT_SIM_MAX_AE=64`.
- `test_raft.c` (7011 lines) already exercises election + AE + commit + a
hand-built partition/catch-up convergence path and both directions of
`raft_sim_check_log_matching` (pass-on-converged / fail-on-divergence).
### All library entry points the harness needs already exist in `raft.h`
`raft_log_append`, `raft_send_append_entries`, `raft_advance_commit`,
`raft_on_append_entries_request`/`_reply`, `raft_on_vote_request`/`_reply`, `raft_init`,
`raft_start_election`, `raft_on_election_timeout`, `raft_snapshot` — all declared.
**Constraint:** `raft_snapshot_t` (`raft.h:69-76`) exposes term/role/voted_for/votes only —
NOT log or commit_index. So Election Safety can read from `raft_snapshot`, but Log
Matching / Leader Completeness / State Machine Safety MUST read the harness-owned
in-memory `log[]`/`commit_index`/`applied[]` (Req 3.6 "observable per-node state only" is
satisfied by the in-memory mirror, not by reaching into `raft.c`).
### Spec phase state
`spec.json` = `tasks-generated`, all 3 gates auto-approved (`-y`), `ready_for_implementation`.
`tasks.md` has 8 task groups (1.1–8.1); **every task box is unchecked `[ ]`** — none of
the conformance machinery is implemented. The brief/requirements/design are complete and
internally consistent.
## 2. Requirement-to-Asset Map (gaps tagged Missing / Unknown / Constraint)
| Req | Need | Existing asset | Gap |
|-----|------|----------------|-----|
| 1.1–1.6 Deterministic seeded driver | Seeded PRNG (SplitMix64), `(N,seed)` byte-identical replay, message bag, action trace buffer, no threads/sockets/wall-clock | `raft_sim_init` (no seed/PRNG); synchronous already | **Missing**: PRNG, `seed`, `raft_sim_conformance_init`, bag (`bag[]`/`bag_count`), trace buffer, `raft_sim_run` replay. Constraint: must extend the existing `raft_sim_cluster_t`/`_node_t`, not rewrite. |
| 2.1–2.6 Drive `raft.tla` Next actions via library entry points | `Restart`/`Timeout`/`RequestVote`/`BecomeLeader`/`ClientRequest`/`AppendEntries`/`AdvanceCommitIndex`/`Receive`(UpdateTerm+DropStaleResponse)/`Duplicate`/`Drop` through published API | Vote+AE delivery, restart, advance-commit, partition all wired verbatim to library handlers | **Missing**: unified `raft_sim_step` scheduler selecting only *enabled* actions; `ClientRequest` via `raft_log_append`; bag-based `Duplicate`/`Drop` (current delivery drains a queue, no per-message drop/dup/reorder). UpdateTerm/DropStaleResponse are already inside the library handlers (consumed, not re-modeled). |
| 3.1–3.6 Per-step assertion of all four invariants | Election Safety, Log Matching, Leader Completeness, State Machine Safety; first-failure halt; observable-state-only | Only `raft_sim_check_log_matching` exists | **Missing**: `raft_sim_check_election_safety`, `_leader_completeness`, `_state_machine_safety`, `_check_all` (first-failure composer); `applied[]` per-node state machine view. |
| 4.1–4.5 Targeted adversarial scenarios | partition/heal, restart-churn, drop/dup/reorder, Figure-8, stale-rejoin (bug-851 G5) | A hand-built partition/catch-up test exists in `test_raft.c` (not a reusable named scenario) | **Missing**: 5 named `raft_sim_scenario_*` entry points asserting all four invariants throughout. |
| 5.1–5.4 Failure-trace capture/replay | seed+N+failing-invariant+ordered action trace dump; quiet-on-success; replay reproducibility | per-node `trace_buf` (string events only, not structured actions) | **Missing**: structured `raft_sim_action_t` trace + `raft_sim_dump_trace`; replay tie-in to `raft_sim_run`. |
| 6.1–6.4 CI-runnable target | M-seed sweep + all scenarios in `src/lib/testsuite`, ASAN/UBSAN-clean, bounded runtime | `test_raft.c` already builds/runs under `XCE_TESTING` + sanitizers | **Missing**: the sweep harness + bounds; mechanics (test entry point, sanitizer build) already exist. Low-risk. |
| 7.1–7.4 Deliberately-broken-build negative test | compile-time fault switches (`RAFT_SIM_BREAK_TERM_STAMP`, `RAFT_SIM_BREAK_TRUNCATE`) confined to adapter; both-directions assert | none | **Missing**. **Constraint**: switches must live in the *adapter* (`raft_sim.c` callbacks), never `raft.c` — the boundary forbids touching protocol code. This is the acceptance teeth. |
| 8.1–8.3 Optional TLC-consumable trace export | spec-named machine-readable record stream, off by default | none | **Missing** (optional `[ ]*`, deferred). **Unknown/Research**: no TLC tooling in-repo to actually consume it — export format can be written but cross-checking against `raft.tla` via TLC is unproven. |
### Cross-cutting findings
- **Currently CHECKED against raft.tla (in code):** Log Matching only (one checker,
`raft_sim.c:424`), and only on hand-driven clusters — not per-step, not over randomized
runs. Election Safety / Leader Completeness / State Machine Safety are **UNCHECKED in
code** (asserted only by prose checklist). Of the `Next` actions, Restart / Timeout /
RequestVote / AppendEntries / AdvanceCommitIndex / Receive are *driven* by the existing
sim; `ClientRequest`, per-message `Duplicate`/`Drop`/reorder, and a unified
enabled-action scheduler are **not yet driven**.
- **raft.tla does NOT model durability** (restores `log` intact on Restart,
`raft.tla:165-174`) — so the harness, like the spec, cannot and should not assert the
commit-durability invariant (that rests on `ha-commit-durability` tests). This correctly
scopes the harness to the four safety invariants only. Confirmed against memory.
## 3. Implementation Approach Options
### Option A — Extend `raft_sim.c` / `test_raft.c` in place (spec's chosen path)
Add PRNG, bag, action trace, three new checkers + composer, `raft_sim_step`/`_run`, five
scenarios, fault switches — all inside the existing `XCE_TESTING` sim section and test
file. Reuse `raft_sim_node_t`/`_cluster_t` (extend with `bag[]`, `applied[]`, `seed`,
`trace[]`).
- ✅ Matches the spec boundary exactly (owns `raft_sim.c` + `raft.h` XCE_TESTING + `test_raft.c`); reuses the verbatim-library-entry-point pattern already proven by the dependency specs; no protocol change; sanitizer/CI plumbing already in place.
- ❌ `raft_sim.c` grows well past the existing 18 KB (toward the 800-line guideline ceiling); `test_raft.c` is already 7011 lines.
### Option B — New `raft_conformance.c` / `test_raft_conformance.c` modules
Put the harness (PRNG, bag, scheduler, checkers, scenarios, fault switches) in dedicated
files; keep `raft_sim.c` as the thin election/AE adapter.
- ✅ Honors "many small files"; isolates the oracle from the dependency-era sim; easier to read/maintain; keeps `test_raft.c` from ballooning further.
- ❌ Deviates from the spec's stated file boundary ("owns `raft_sim.c` … ONLY"); needs a meson build wiring change and a boundary-note update; minor duplication of the in-memory adapter.
### Option C — Hybrid (recommended)
Keep the in-memory adapter callbacks, bag, PRNG, and the **four checkers** in
`raft_sim.c`/`raft.h` (they are genuinely "simulator" primitives and reuse existing node
state). Lift the **scheduler + five scenarios + M-seed sweep + negative test** into a
new `test_raft_conformance.c` (or a clearly-fenced section) so `test_raft.c` does not grow
unbounded. Fault switches stay in `raft_sim.c` (adapter) per boundary.
- ✅ Best file-size/cohesion balance; preserves the "harness drives only published entry points" boundary; checkers stay reusable; the heavy scenario/sweep code is isolated.
- ❌ Splits ownership across two test files — needs a one-line boundary amendment in `tasks.md`/`design.md` (Allowed Dependencies already permit a new test file under `src/lib/testsuite`).
## 4. Effort & Risk
- **Effort: L (1–2 weeks).** Eight task groups; the hard parts are the seeded scheduler
with enabled-action selection (4.2), deterministic byte-identical replay (4.4/1.6), the
three new invariant checkers (3.1/3.3/3.4) and the bug-851 Figure-8 / stale-rejoin
scenarios (5.4/5.5). Substantial scaffolding already exists (adapter callbacks,
partition, restart, Log Matching, AE/vote delivery), which pulls it down from XL.
- **Risk: Medium.** Mitigants: all library entry points exist and are stable; deps landed;
no protocol change; synchronous/no-I/O design removes concurrency risk; the negative
test self-validates checker sensitivity. Residual risk: (a) determinism leaks (pointer
values / iteration order in action selection) — must be audited (Req 1.2); (b) the
Figure-8 and stale-rejoin scenarios must be precise enough to actually exercise the
bug-851 mechanism (else the oracle's "teeth" are blunt); (c) keeping fixed-size bounds
(`RAFT_SIM_MAX_*`) large enough for randomized runs without blowing the CI time/ASAN
budget. The optional TLC export (Req 8) is **Unknown** — no in-repo TLC to validate it.
## 5. Recommendations for Design Phase
- **Preferred approach: Option C (hybrid)** — checkers + bag + PRNG + fault switches in
`raft_sim.c`/`raft.h`; scheduler + scenarios + sweep + negative test in a new
`src/lib/testsuite/test_raft_conformance.c`. Amend the `tasks.md`/`design.md` file
boundary with that one new test file (design "Allowed Dependencies" already permits it).
If strict adherence to the stated single-file boundary is required, fall back to
Option A and accept the file-size growth (flag a `clang-format`/size exception).
- **Key decisions to lock:** (1) PRNG = SplitMix64 inlined, single `cluster.seed` source —
forbid any wall-clock/address input (Req 1.2); (2) invariant checkers read ONLY
harness-owned in-memory state (`log[]`/`commit_index`/`applied[]`) + `raft_snapshot`
(role/term) — never `raft.c` internals (Req 3.6); (3) `ClientRequest` drives
`raft_log_append` on a forced leader; `Duplicate`/`Drop`/reorder mutate ONLY the bag
(Req 2.5); (4) fault switches confined to adapter callbacks, default-off, never set in
the default CI run (Req 7.3).
- **Research items to carry forward:** (a) **TLC consumability (Req 8)** — no
`tla2tools`/`.cfg` exists in-repo; decide whether to add TLC tooling or keep Req 8 as a
documented-format-only deferral. (b) confirm `RAFT_SIM_MAX_LOG=64` / step bounds are
large enough for randomized convergence yet inside the ASAN/UBSAN CI budget (Req 6.4).
(c) verify the existing per-node `applied[]`/state-machine view needs adding (not present
in current `raft_sim_node_t`) for State Machine Safety (Req 3.4).
- **Dependency watch:** harness consumes `raft-log-replication` + `raft-commit-safety`
callback signatures verbatim; per design "Revalidation Triggers," any change to
`on_persist_entry`/`on_get_entry_term`/`on_send_append_entries`/`on_truncate_from`/
`on_commit_advanced` or the `(lastLogTerm,lastLogIndex)` freshness contract re-validates
this spec.