tla: M3 — MVCC.tla (MVCC-1 + MVCC-4) + mvcc wired into tla-check#861
Conversation
Per §8.1 of docs/design/2026_05_28_partial_tla_safety_spec.md (M3 deliverable): the MVCC layer encoded as a per-key version chain with a Compact action that drains older versions, and a ghost variable that preserves the original Write history so MVCC-4 (no lost commit on snapshot install) is checkable. New module: - tla/mvcc/MVCC.tla — Write(k, v) allocates fresh tsCounter and appends to both versions[k] and the ghost originalVersions[k]; Compact(newMin) advances the retention watermark and prunes versions[k], retaining (under EnableSafety) the largest commit_ts below newMin per key so reads at read_ts >= minRetained still see the original value. Gap config drops the retention guard and TLC produces a MVCC-4 counterexample. - tla/mvcc/MCMVCC.tla — TLC model module with key + value symmetry. - tla/mvcc/MCMVCC.cfg — safe config (PASS). - tla/mvcc/MCMVCC_gap.cfg — gap config (FAIL on MVCC4_NoLostCommitOnSnapshotInstall). Invariants encoded: - MVCC-1 uniqueness: no two distinct version records on the same key share a commit_ts. Holds in both safe and gap because Write allocates a fresh tsCounter. - MVCC-4 no lost commit on compaction: for every key k and every read_ts in [minRetained, MaxTs], LatestBefore(versions[k], ts) equals LatestBefore(originalVersions[k], ts). - MVCC_TsMonotonic (PROPERTY): tsCounter weakly increases on every step. - MVCC_GhostMonotonic (PROPERTY): originalVersions[k] only grows. MVCC-2 (no version below the HLC physical ceiling) and MVCC-3 (cross-node read consistency) are deferred to M5: - MVCC-2 is properly an HLC.tla property — the ceiling discipline lives there. M5 will check the cross-spec form after the composed model wires the modules. - MVCC-3 requires a multi-node model; the single-node M3 spec cannot express it. Tooling: - scripts/tla-check.sh: TLA_MODULES gains "mvcc"; gap_invariant_for gains the MVCC-4 pinned string. - tla/README.md: M3 status Not started -> Landed; MVCC module description + invariant table; MCMVCC subsection mirroring the HLC / OCC entries. Verified locally with `make tla-check`: - HLC safe (3,594 states) + HLC gap (HLC-4 ce at depth 5). - OCC safe (150 states) + OCC gap (OCC-1 ce at depth 5). - MVCC safe (79 distinct states) + MVCC gap (MVCC-4 ce at depth 3 — one Write at commit_ts=1, one Compact(newMin=2), and the read at read_ts=2..MaxTs now sees NotFound where the ghost still shows the Write's value).
|
Warning Review limit reached
More reviews will be available in 36 minutes and 5 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds a TLA+ MVCC spec with per-key version chains, compaction behavior, safety invariants, model-check harness and safe/gap TLC configs, plus README updates and test-harness integration to run MVCC checks. ChangesMVCC Formal Specification and Verification
Sequence DiagramsequenceDiagram
participant Harness as Test Harness
participant SafeConfig as MCMVCC.cfg (EnableSafety=TRUE)
participant GapConfig as MCMVCC_gap.cfg (EnableSafety=FALSE)
participant MVCC as MVCC Spec
Harness->>SafeConfig: Run TLC
SafeConfig->>MVCC: Execute Write/Compact transitions
MVCC->>SafeConfig: Verify invariants (MVCC4 holds)
SafeConfig-->>Harness: Success
Harness->>GapConfig: Run TLC (gap)
GapConfig->>MVCC: Execute transitions with EnableSafety=FALSE
MVCC->>GapConfig: MVCC4 counterexample produced
GapConfig-->>Harness: Failure (MVCC4 violated)
Harness->>Harness: Grep for MVCC4 violation string to confirm expected failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements Milestone 3 (M3) by introducing the TLA+ specification for the MVCC layer (tla/mvcc/MVCC.tla) along with its model-checking instances (MCMVCC.tla, MCMVCC.cfg, and MCMVCC_gap.cfg). It also updates the TLA check script (scripts/tla-check.sh) to include the new mvcc module and its expected gap invariant violation, and documents these additions in tla/README.md. There are no review comments to address, and I have no additional feedback to provide on these changes.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tla/README.md (1)
49-51:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the “currently HLC and OCC” wording to include MVCC.
This text is now stale since
make tla-checkincludesmvcc; please align the Run section with current harness behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tla/README.md` around lines 49 - 51, Update the Run section wording to reflect that make tla-check (scripts/tla-check.sh) now includes MVCC: replace the phrase "currently HLC and OCC" with "currently HLC, OCC, and MVCC" (or equivalent) so the README's description of which modules are checked matches the actual harness behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@tla/README.md`:
- Around line 49-51: Update the Run section wording to reflect that make
tla-check (scripts/tla-check.sh) now includes MVCC: replace the phrase
"currently HLC and OCC" with "currently HLC, OCC, and MVCC" (or equivalent) so
the README's description of which modules are checked matches the actual harness
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bafc5492-134a-4fc4-946b-e60bf830072c
📒 Files selected for processing (6)
scripts/tla-check.shtla/README.mdtla/mvcc/MCMVCC.cfgtla/mvcc/MCMVCC.tlatla/mvcc/MCMVCC_gap.cfgtla/mvcc/MVCC.tla
coderabbit Minor on PR #861: the Run section's parenthetical "currently HLC and OCC; M3 / M4 / M5 will be added as they land" was stale once mvcc landed in this PR. Updated to "currently HLC, OCC, and MVCC; M4 / M5 will be added as they land". Doc-only change.
|
Coderabbit Minor fixed in @claude review |
|
Claude finished @bootjp's task in 3m 3s —— View job Code Review — M3 MVCC TLA+ Spec
SummarySolid spec. The ghost-variable approach for MVCC-4, the Finding: stale forward-reference in README (minor)
M3 has now landed, so this should read "future M4 / M5 modules will too." Fix this → Observations (not bugs)
LET below == { v \in S : v.commitTs <= ts /\ v.commitTs > 0 } INThe
The retention of only the single latest-before- One edge worth confirming mentally: when MVCC-1 is only asserted on
MVCC-1 checks uniqueness in the live store only. Since
MVCC_TsMonotonic == [][tsCounter' >= tsCounter]_vars
Symmetry union
Symmetry == KeySymmetry \cup ValSymmetryStandard independent-sort symmetry reduction. Keys and values participate only as abstract indices (no ordering on either in any invariant), so both sorts are genuinely symmetric. Follows the same pattern as MCOCC. Correct. Design choices that are sound
Verdict: LGTM with the one stale README nit. The spec is correct, the invariants are faithfully stated, and the gap evidence is genuine. |
Per §8.1 of docs/design/2026_05_28_partial_tla_safety_spec.md (M4 deliverable): the route catalog + CatalogWatcher layer encoded as a durable `catalogVersion` counter and per-node `engineVersion[n]` that re-syncs via a `CatalogWatcherSync(n)` action. A ghost `engineMaxObserved[n]` tracks the highest version each node has ever observed so Routes-4 (watcher fan-out monotonicity) is a state invariant. New module: - tla/routes/Routes.tla — ProposeRouteChange (single-step catalogVersion bump, models the implementation's `saveSplitResultViaCoordinator` + `applyEngineSnapshot` collapsed into one atomic action) and CatalogWatcherSync(n) (under EnableSafety the watcher must fetch v >= engineMaxObserved[n]; under the gap toggle it can fetch any v in 0..catalogVersion). - tla/routes/MCRoutes.tla — TLC model module with node symmetry. - tla/routes/MCRoutes.cfg — safe config (expected PASS). - tla/routes/MCRoutes_gap.cfg — gap config (expected FAIL on Routes4_NoEngineRegression). Invariants encoded: - Routes-1 catalogVersion in 0..MaxVersions; combined with Routes1_Action (PROPERTY) captures the monotonicity. - Routes-2 vacuously TRUE in M4 — explicit range modelling deferred to M5 (composed), where SplitRange exercising key-boundary changes is in scope. - Routes-3 vacuously TRUE in M4 — ProposeRouteChange is a single TLA+ action so atomicity is structural; async cross-node propagation is captured by Routes-4. - Routes-4 engineVersion[n] >= engineMaxObserved[n] for every node n. - Routes1_Action (PROPERTY): catalogVersion weakly increases on every step. - Routes4_GhostMonotonic (PROPERTY): engineMaxObserved[n] weakly increases on every step. Tooling: - scripts/tla-check.sh: TLA_MODULES gains "routes"; gap_invariant_for gains the pinned Routes-4 string. mc_basename switched from blanket `tr a-z A-Z` to a per-module case statement so the file name MCRoutes.tla (TitleCase, since Routes is a word rather than an acronym like HLC / OCC / MVCC) parses correctly. - tla/README.md: M4 status Not started -> Landed; Routes module description + invariant table + MCRoutes subsection mirroring the HLC / OCC / MVCC entries. Run section updated to enumerate the 4 modules. Verified locally with `make tla-check`: - HLC safe (3,594) + HLC gap (HLC-4 ce at depth 5). - OCC safe (150) + OCC gap (OCC-1 ce at depth 5). - MVCC safe (79) + MVCC gap (MVCC-4 ce at depth 3). - Routes safe (29 distinct states) + Routes gap (Routes-4 ce at depth 3 — one ProposeRouteChange v=0->1, one CatalogWatcherSync(n) fetching 1, one CatalogWatcherSync(n) fetching 0). Stacked on PR #861 (M3 MVCC). Once #861 merges this PR's base auto-updates to main and the diff stays Routes-only.
claude bot M3 review on PR #861: README line 84 still said "future M3 / M4 / M5 modules will too" after M3 landed. Updated to call out that MVCC follows the same pattern in tla/mvcc/ and that M4 / M5 will too.
|
Round 2 fix in All other claude observations were marked LGTM (ghost variable, EnableSafety toggle, RetainOnCompact correctness, MVCC-2/MVCC-3 deferral rationale, symmetry). @claude review |
|
Claude finished @bootjp's task in 1m 42s —— View job Code Review — M3 MVCC TLA+ Spec (Round 2)
Round 2 fixes verified
Old "future M3 / M4 / M5" is gone; MVCC is correctly noted as already landed, and only M4 / M5 remain as "future". ✓
Matches the actual harness. ✓ Fresh spec reviewNo new findings. For completeness, re-confirmed:
Verdict: LGTM. Both Round 1 findings are properly addressed, the Coderabbit note was fixed, and the spec itself remains correct. Ready to merge. |
gemini MEDIUM on PR #862 line 61: without set -e, an exit 64 from mc_basename (added for the routes module) only terminates the subshell. The parent loop would continue with mc=\"\" and the downstream files .tla / .cfg would be syntactically empty file paths that TLC mishandles silently. Same subshell-exit propagation gap that the gap_invariant_for if-check (gemini HIGH on PR #858) closed for the gap-invariant case. Added explicit if-checks at both call sites: - main loop where mc / safe_cfg / gap_cfg are derived - run_tlc inner call (used for both safe and gap runs) Verified make tla-check end-to-end: HLC 3594 / OCC 150 / MVCC 79 / Routes 29 distinct states on safe; all 4 gap configs surface their pinned invariant.
claude bot M4 Round 2 noted that the header said Routes-1 is "strictly monotonic" while Routes1_Action uses >= (weakly non-decreasing) to tolerate stuttering steps. Both are correct from different angles; the parenthetical makes the relationship explicit so a reader does not wonder why the temporal property is not strict.
…eck (#862) ## Summary M4 of the [TLA+ safety spec](https://github.com/bootjp/elastickv/blob/main/docs/design/2026_05_28_partial_tla_safety_spec.md) — the route catalog + CatalogWatcher layer encoded as a durable `catalogVersion` counter and per-node `engineVersion[n]` that re-syncs via a `CatalogWatcherSync(n)` action. **Stacked on #861 (M3 MVCC).** Once #861 merges, this PR's base auto-updates to `main` and the diff stays Routes-only. ## What lands - `tla/routes/Routes.tla` — `ProposeRouteChange` (single-step atomic catalog version bump) + `CatalogWatcherSync(n)` (per-node watcher re-sync, gated by `EnableSafety`). Ghost variable `engineMaxObserved[n]` makes Routes-4 a checkable state invariant. - `tla/routes/MCRoutes.tla` — TLC model module with node symmetry. - `tla/routes/MCRoutes.cfg` / `MCRoutes_gap.cfg` — safe (PASS) + gap (FAIL on Routes-4). - `scripts/tla-check.sh` — `TLA_MODULES` gains `"routes"`; `gap_invariant_for` gains the pinned Routes-4 string. `mc_basename` switched from blanket `tr a-z A-Z` to a per-module case statement so `MCRoutes.tla` (TitleCase) parses correctly — `Routes` is a word, not an acronym like `HLC` / `OCC` / `MVCC`. - `tla/README.md` — M4 status `Not started` → `Landed`; Routes module description + invariant table + MCRoutes subsection. ## Invariants | # | Statement | Form | |---|---|---| | Routes-1 | `catalogVersion ∈ 0..MaxVersions` | INVARIANT (combined with `Routes1_Action` for monotonicity) | | Routes-2 | Vacuously TRUE in M4 — range modelling deferred to M5 | INVARIANT | | Routes-3 | Vacuously TRUE in M4 — `ProposeRouteChange` single-step atomic | INVARIANT | | Routes-4 | `engineVersion[n] ≥ engineMaxObserved[n]` for every node | INVARIANT | | `Routes1_Action` | `catalogVersion` weakly increases across every step | PROPERTY | | `Routes4_GhostMonotonic` | `engineMaxObserved[n]` weakly increases across every step | PROPERTY | ## Deferred to M5 - **Routes-2 (coverage / disjointness)** — M4 does not model individual ranges. M5 will model `routes : Keys → GroupId` whose totality implies both properties. - **Routes-3 (SplitRange atomicity in full form)** — M4 has structural atomicity via the single-step action; M5 will exercise key-boundary changes with explicit ranges. ## Verification (local) ```text $ make tla-check ... HLC safe 3594 distinct states ... HLC gap HLC-4 ce (depth 5) ... OCC safe 150 distinct states ... OCC gap OCC-1 ce (depth 5) ... MVCC safe 79 distinct states ... MVCC gap MVCC-4 ce (depth 3) ... Routes safe 29 distinct states ... Routes gap Routes-4 ce (depth 3) tla-check: all model-check outcomes match the design contract. ``` The Routes-4 counterexample is the canonical 3-step regression: 1. `ProposeRouteChange`: `catalogVersion` 0 → 1. 2. `CatalogWatcherSync(n)`: node `n` fetches version `1` → `engineVersion[n] = 1`, `engineMaxObserved[n] = 1`. 3. `CatalogWatcherSync(n)`: node `n` (gap config) fetches version `0` → `engineVersion[n] = 0`, `engineMaxObserved[n] = 1`. **`engineVersion[n] = 0 < engineMaxObserved[n] = 1` → Routes-4 fails.** ## Modelling notes - **HLC abstraction.** `catalogVersion` is a single monotonic counter — M4 doesn't need the full 48/16 HLC layout because catalog versions are sequence numbers, not timestamps. - **`engineMaxObserved` as ghost.** Carrying the per-node "highest version ever observed" lets Routes-4 be a state invariant rather than a temporal property. Both forms are checked: the state form catches the regressed state; `Routes4_GhostMonotonic` (PROPERTY) catches a buggy ghost-update path. - **Implementation anchor.** `ProposeRouteChange` collapses `adapter/distribution_server.go saveSplitResultViaCoordinator` + `applyEngineSnapshot` into a single TLA+ action — that's Routes-3's structural atomicity. `CatalogWatcherSync(n)` mirrors `distribution/watcher.go SyncOnce`. - **`mc_basename` change.** All other modules (HLC, OCC, MVCC) are acronyms so blanket `tr a-z A-Z` worked. Routes is a word, so the file is `MCRoutes.tla` (TitleCase). Per-module case statement is more readable than per-character tricks. ## Self-review (5-lens per CLAUDE.md) 1. **Data loss** — Routes-4 captures exactly the "a stale watcher overwrite loses the latest catalog state on this node" failure mode. Verified via gap counterexample. 2. **Concurrency** — single-node fan-out; multi-node interactions deferred to M5. 3. **Performance** — `make tla-check` end-to-end runs in <5s; Routes spec adds <1s. 4. **Data consistency** — Routes-1 + Routes-4 + two PROPERTY transitions. Routes-2 / Routes-3 in full form deferred to M5 with rationale. 5. **Test coverage** — no Go tests added (spec-only). `make tla-check` is the new coverage layer. ## Test plan - [ ] `tla-check` CI runs green (watches `tla/**`) - [ ] `tla-spec-ai-review` does NOT fire — spec-only - [ ] Reviewer cross-checks Routes-1 / Routes-4 against §5.4 of the design doc - [ ] Reviewer runs `make tla-check` and confirms 8 outcomes (HLC / OCC / MVCC / Routes safe pass + gap fail) ## Out of scope - Full range modelling for Routes-2 / Routes-3 — M5. - Real `SplitRange` (with key boundary changes) — M5. - M5 composed integration — separate future PR.
…#865) ## Summary M5 of the [TLA+ safety spec](https://github.com/bootjp/elastickv/blob/main/docs/design/2026_05_28_partial_tla_safety_spec.md) — the cross-module safety properties, capping the spec-only roadmap (M1..M5). M1..M4 each modelled a single subsystem in isolation. M5 captures the **seams** between them — the two safety properties that no single-module spec can express. ## What lands - `tla/composed/Composed.tla` — recreates the minimal cross-module state needed for Composed-1 (catalog history `routes[v]`, txn observed catalog version `txnObservedVer[t]`, committing group `txnCommitGroup[t]`) and Composed-3 (monotonic `tsCounter` advancing on every Commit). **Does NOT `INSTANCE` M1..M4** — that would explode the state space well past M5's `<10 min at default bounds` target from §8.1. - `tla/composed/MCComposed.tla` — TLC model with key + group + txn symmetry. - `tla/composed/MCComposed.cfg` / `MCComposed_gap.cfg` — safe (PASS) + gap (FAIL on Composed-1). - `scripts/tla-check.sh` — `TLA_MODULES` gains `"composed"`; `gap_invariant_for` + `mc_basename` gain the corresponding cases. - `tla/README.md` — M5 status `Not started` → `Landed`; Composed module description + invariant table. ## Invariants | # | Statement | Form | |---|---|---| | Composed-1 | Every committed write key was owned by the committing group at the txn's observed catalog version | INVARIANT | | Composed-2 | Vacuously TRUE in this abstraction — `SplitRange` is same-group only per CLAUDE.md | INVARIANT | | Composed-3 | Distinct committed txns have distinct `commit_ts` | INVARIANT | | `Composed_CatalogMonotonic` | `catalogVersion` weakly increases on every step | PROPERTY | | `Composed_TsMonotonic` | `tsCounter` weakly increases on every step | PROPERTY | | `Composed3_TsAction` | Every Commit strictly raises `tsCounter` | PROPERTY | ## Why no `INSTANCE`? Each M1..M4 module tightly bounds its own state for tractable TLC. Bringing all four `INSTANCE`s into one product spec would multiply the state spaces — even a 100x growth lands well past M5's `<10 min at default bounds` target. M5 instead recreates the minimal cross-module state needed to express the two seam invariants. The integration claim is that this projection preserves the invariants the full product would assert; reviewers can sanity-check each Composed action against the corresponding M1..M4 action (e.g., `ProposeRouteChange` mirrors `Routes.tla`, `BeginTxn` + `Commit` mirror `OCC.tla` with a cross-module observed-catalog-version pin). ## Verification (local) ```text $ make tla-check HLC safe 3,594 distinct states + HLC-4 gap ce OCC safe 150 distinct states + OCC-1 gap ce MVCC safe 79 distinct states + MVCC-4 gap ce Routes safe 29 distinct states + Routes-4 gap ce Composed safe 1,684 distinct states + Composed-1 gap ce tla-check: all model-check outcomes match the design contract. ``` The Composed-1 counterexample is the canonical 4-step regression: 1. `BeginTxn(t)` — `txn` observes `catalogVersion = 0`, where `routes[0][k1] = g1`. 2. `WriteIntent(t, k1)`. 3. `ProposeRouteChange(k1, g2)` — `catalogVersion → 1`, `routes[1][k1] = g2`. 4. `Commit(t, g2)` (gap mode) — committing group is `g2`, but `routes[0][k1] = g1 ≠ g2`. **Composed-1 fails.** ## Self-review (5-lens per CLAUDE.md) 1. **Data loss** — out of scope (cross-module invariants check group ownership, not durability). 2. **Concurrency** — Composed-1 is exactly the concurrency seam between OCC and Routes; the gap counterexample is one such interleaving. 3. **Performance** — `make tla-check` end-to-end runs in ~10 s on a dev laptop, well under the 10-min target. 4. **Data consistency** — Composed-1 (cross-module write/route consistency) + Composed-3 (cross-txn ts uniqueness) + 3 PROPERTY transitions. 5. **Test coverage** — no Go tests added (spec-only). `make tla-check` is the coverage layer. ## Roadmap status (post-merge) | Milestone | Status | |---|---| | M1 HLC | Landed (PR #856) | | M2 OCC | Landed (PR #858) | | M3 MVCC | Landed (PR #861) | | M4 Routes | Landed (PR #861 via stacked #862) | | **M5 Composed** | **This PR** | | M6 liveness (OPTIONAL) | Not started — can follow as a separate PR | | M1 Go follow-up (ceiling fence) | Not started — separate Go PR with caller audit | ## Test plan - [ ] `tla-check` CI runs green (watches `tla/**`) - [ ] `tla-spec-ai-review` does NOT fire — spec-only PR - [ ] Reviewer cross-checks Composed-1 / Composed-3 against §5.5 of the design doc - [ ] Reviewer runs `make tla-check` and confirms 10 outcomes (5 safe pass + 5 gap fail) - [ ] Reviewer sanity-checks the `INSTANCE`-free projection claim — each Composed action mirrors the corresponding M1..M4 action ## Out of scope - M6 liveness (OCC-L1, Routes-L1) — separate PR if needed - HLC-4 (iii) ceiling fence Go implementation — separate code PR - Cross-group `SplitRange` in the real implementation (currently same-group only)
Summary
M3 of the TLA+ safety spec — the MVCC layer encoded as a per-key version chain with a
Compactaction and a ghost variable that preserves the originalWritehistory so MVCC-4 is checkable.Follows PR #858 (M2 OCC spec).
What lands
tla/mvcc/MVCC.tla—Write(k, v)allocates a freshtsCounterand appends to bothversions[k]and the ghostoriginalVersions[k];Compact(newMin)advances the retention watermark and (underEnableSafety) retains the largestcommit_tsbelownewMinper key so reads atread_ts ≥ minRetainedstill see the original value.tla/mvcc/MCMVCC.tla— TLC model module with key + value symmetry.tla/mvcc/MCMVCC.cfg/MCMVCC_gap.cfg— safe (PASS) + gap (FAIL on MVCC-4).scripts/tla-check.sh—TLA_MODULESgains"mvcc";gap_invariant_forgains the pinned MVCC-4 string.tla/README.md— M3 statusNot started→Landed; MVCC module description + invariant table.Invariants
commit_tskand everyread_ts ∈ [minRetained, MaxTs],LatestBefore(versions[k], ts) = LatestBefore(originalVersions[k], ts)MVCC_TsMonotonictsCounterweakly increases across every stepMVCC_GhostMonotonicoriginalVersions[k]only grows;Compactleaves itUNCHANGEDDeferred to M5
HLC.tlaproperty — the ceiling discipline lives there. M5 will check the cross-spec form onceHLC.tlaisINSTANCE-d into the composed model.Verification (local)
The MVCC-4 counterexample is short (depth 3): one
Writeatcommit_ts = 1, oneCompact(newMin = 2), and the read atread_ts ∈ [2, MaxTs]now seesNotFoundwhere the ghost still shows theWrite's value.Modelling notes
Compactretention semantics. UnderEnableSafetythe per-key prune keeps every version withcommit_ts ≥ newMinAND the single largest version withcommit_ts < newMin(so any later read in[newMin, MaxTs]still sees the same value the original log would have shown). Under the gap toggle, the prune drops every version withcommit_ts < newMin, including the latest-before-newMin retention.originalVersionsas a ghost. Carrying everyWritein a parallel set thatCompactnever touches lets MVCC-4 be a direct equality between the actual store and the canonical "what should be visible" set. It is the simplest faithful encoding of "no commit is lost" for a single-node spec.tsCounteris a single monotonic counter; M5 will replace it with anINSTANCEofHLC.tla's real 48/16 layout. This keeps M3's state space tight (79 distinct states under the default bounds).MCMVCC.tladeclares both viaPermutations.Self-review (5-lens per CLAUDE.md)
make tla-checkend-to-end runs in under 5 seconds at default bounds.make tla-checkis the new coverage layer.Test plan
tla-checkCI workflow runs green (watchestla/**)tla-spec-ai-reviewdoes NOT fire — spec-only PRmake tla-checkand confirms 6 outcomes (HLC / OCC / MVCC safe pass + gap fail)Out of scope
Summary by CodeRabbit
New Features
Documentation
Tests