Foundation

Root 2.1 kernel API

The frozen 34-symbol ABI 1.4 caller contract, refusal codes and process-crash durability limits.

Final contract for ABI 1.4, annotated tag root-2.1. Source citations are relative to its RURAL commit 4e84fec244a29f348946ec006894c51df87290b7; line numbers describe that commit, not the earlier draft worktrees. RN-08 checked the tag target on 9 November 2026; the earlier pre-rewrite citation is superseded. Signature review remains pending after the history rewrite. This document resolves draft decisions from landed code; it does not close the remaining evidence gates recorded in I-01.

Root 2.1 is process-crash durable, NOT power-loss durable. Successful durable operations sync before acknowledging; uncertain publication poisons the handle. The isolated block-device harness is deferred. SIGKILL/torn-write evidence and bounded formal checks do not qualify physical power-loss ordering (src/storage/receipt_log.zig:2626, src/storage/receipt_log.zig:2720, docs/grove/results/R-12b.md:6, docs/grove/results/R-12.md:212).

Frozen surface and caller contract#

Exactly 34 symbols, with the argument/return declarations, pointer nullability, fixed array extents and struct layouts in the cited header frozen verbatim. rural_grove_abi_version() returns 0x00010004; phase-1 builds remain a distinct 26-symbol ABI 1.0 (src/grove_abi.zig:72, include/rural_grove.h:67). The groups contain 5 lifecycle, 1 memory, 7 arena, 11 map, 4 log, 3 CAS/key and 3 storage operations. The previous draft's “Map (12)” heading was incorrect.

Frozen symbolDeclaration and canonical inventory
rural_grove_abi_versioninclude/rural_grove.h:88; dist/grove-v1/ABI-SYMBOLS.txt:1
rural_grove_arena_getinclude/rural_grove.h:97; dist/grove-v1/ABI-SYMBOLS.txt:2
rural_grove_arena_hasinclude/rural_grove.h:98; dist/grove-v1/ABI-SYMBOLS.txt:3
rural_grove_arena_import_redactioninclude/rural_grove.h:86; dist/grove-v1/ABI-SYMBOLS.txt:4
rural_grove_arena_putinclude/rural_grove.h:96; dist/grove-v1/ABI-SYMBOLS.txt:5
rural_grove_arena_put_sealedinclude/rural_grove.h:99; dist/grove-v1/ABI-SYMBOLS.txt:6
rural_grove_arena_redactinclude/rural_grove.h:68; dist/grove-v1/ABI-SYMBOLS.txt:7
rural_grove_arena_sealed_metainclude/rural_grove.h:100; dist/grove-v1/ABI-SYMBOLS.txt:8
rural_grove_buf_freeinclude/rural_grove.h:93; dist/grove-v1/ABI-SYMBOLS.txt:9
rural_grove_casinclude/rural_grove.h:125; dist/grove-v1/ABI-SYMBOLS.txt:10
rural_grove_cas_allinclude/rural_grove.h:70; dist/grove-v1/ABI-SYMBOLS.txt:11
rural_grove_checkpointinclude/rural_grove.h:73; dist/grove-v1/ABI-SYMBOLS.txt:12
rural_grove_closeinclude/rural_grove.h:90; dist/grove-v1/ABI-SYMBOLS.txt:13
rural_grove_fsckinclude/rural_grove.h:75; dist/grove-v1/ABI-SYMBOLS.txt:14
rural_grove_get_keyinclude/rural_grove.h:126; dist/grove-v1/ABI-SYMBOLS.txt:15
rural_grove_last_errorinclude/rural_grove.h:92; dist/grove-v1/ABI-SYMBOLS.txt:16
rural_grove_log_appendinclude/rural_grove.h:117; dist/grove-v1/ABI-SYMBOLS.txt:17
rural_grove_log_headinclude/rural_grove.h:119; dist/grove-v1/ABI-SYMBOLS.txt:18
rural_grove_log_importinclude/rural_grove.h:120; dist/grove-v1/ABI-SYMBOLS.txt:19
rural_grove_log_readinclude/rural_grove.h:118; dist/grove-v1/ABI-SYMBOLS.txt:20
rural_grove_map_diffinclude/rural_grove.h:113; dist/grove-v1/ABI-SYMBOLS.txt:21
rural_grove_map_dropinclude/rural_grove.h:110; dist/grove-v1/ABI-SYMBOLS.txt:22
rural_grove_map_emptyinclude/rural_grove.h:103; dist/grove-v1/ABI-SYMBOLS.txt:23
rural_grove_map_forkinclude/rural_grove.h:104; dist/grove-v1/ABI-SYMBOLS.txt:24
rural_grove_map_getinclude/rural_grove.h:105; dist/grove-v1/ABI-SYMBOLS.txt:25
rural_grove_map_get_sealedinclude/rural_grove.h:111; dist/grove-v1/ABI-SYMBOLS.txt:26
rural_grove_map_leninclude/rural_grove.h:114; dist/grove-v1/ABI-SYMBOLS.txt:27
rural_grove_map_listinclude/rural_grove.h:107; dist/grove-v1/ABI-SYMBOLS.txt:28
rural_grove_map_list_entriesinclude/rural_grove.h:69; dist/grove-v1/ABI-SYMBOLS.txt:29
rural_grove_map_sealinclude/rural_grove.h:109; dist/grove-v1/ABI-SYMBOLS.txt:30
rural_grove_map_setinclude/rural_grove.h:108; dist/grove-v1/ABI-SYMBOLS.txt:31
rural_grove_openinclude/rural_grove.h:89; dist/grove-v1/ABI-SYMBOLS.txt:32
rural_grove_open_v1include/rural_grove.h:71; dist/grove-v1/ABI-SYMBOLS.txt:33
rural_grove_scaninclude/rural_grove.h:84; dist/grove-v1/ABI-SYMBOLS.txt:34

Normative layout: opaque rural_grove_engine; rural_grove_buf is nullable uint8_t *ptr, then size_t len; rural_grove_cas_op is nullable constant key pointer, size_t key_len, int32_t has_expected, uint8_t expected[34], int32_t has_new, uint8_t new_value[34]. Use native C alignment, not packed layouts. Hashes are 34 bytes and canonical leaves 48 bytes (include/rural_grove.h:8, include/rural_grove.h:26, include/rural_grove.h:61).

Calls are serialized per engine. The caller holds a mutex across call plus last_error, an entire paginated scan, and exclusive close. Pointers cover their extents; null byte pointers require zero length. Output objects are valid, aligned and mutually disjoint. Use disjoint input/output storage to satisfy both the general comment and the older CAS-specific restriction. No output borrows an engine. Free each returned buffer exactly once even after failure (including fsck). New APIs clear buffer/handle/more on failure; scan preserves generation. last_error requires a valid handle and cannot diagnose a failed open that published none (include/rural_grove.h:29, include/rural_grove.h:121, src/grove_abi.zig:85, src/grove_abi.zig:371).

Return codes (ABI 1.4)#

Code / nameMeaningRetryableSource
0 OKSuccess; fsck requires complete and clean (CLI exit 0)—include/rural_grove.h:12; src/grove_abi.zig:983
1 NOT_FOUNDMissing content/source/targetNo, unless caller changes the input/statesrc/grove_abi.zig:54
2 IMMUTABLEImmutable operation, including self/unsealed redaction targetNosrc/grove_abi.zig:57; src/storage/arena.zig:440
3 INVALID_PATHInvalid record/path/node/key/leaf/sealed metadata, flags or scan argumentsNo; correct inputsrc/grove_abi.zig:58; src/grove_abi.zig:62
4 UNKNOWN_FORKUnknown or consumed forkNo; obtain a live forksrc/grove_abi.zig:59
5 CHAIN_BROKENBroken receipt chain or malformed v1 importNosrc/grove_abi.zig:60; src/grove_abi.zig:900
6 CAS_MISMATCHComparison outcome, current value outputs filled; not a failed callRe-read and deliberately recompute; no blind retrysrc/grove_abi.zig:63; include/rural_grove.h:121
7 CAPACITYOversize, memory, space, segment/log or other capacity limitNo blind retry; operator/resource actionsrc/grove_abi.zig:64
8 ADAPTERBusy LockUnavailable, ScanChanged, uncertain I/O, unsupported handle operation, other errors, incomplete/refused fsck (CLI exit 3)Busy: retry after owner releases. ScanChanged: restart generation 0. Others: inspect error/report, no blanket retrysrc/grove_abi.zig:65; src/grove_abi.zig:251; include/rural_grove.h:79; src/grove_abi.zig:983
9 REDACTEDRedacted contentNosrc/grove_abi.zig:56
10 CORRUPTStored corruption, internal IdentityMismatch, unsupported version or length overflow; fsck corrupt/unacknowledged (CLI exit 6)Nosrc/grove_abi.zig:61; src/grove_abi.zig:984
11 WRONG_NODEValid supplied local node differs from FORMAT identityNever with the same identityinclude/rural_grove.h:23; src/grove_abi.zig:55; src/grove_abi.zig:325

Busy and wrong-node are distinct in the same C return space (RF-173 resolved). A failed competing open returns 8 without a handle; wrong-node returns 11. Malformed node input is 3, and stored identity damage is 10. Never treat every code 8 as busy. The ABI has no global last_error for open failures (include/rural_grove.h:39, src/grove_abi.zig:78, src/grove_abi.zig:98).

Refusals and admission#

ConditionFrozen resultSource
open_v1 on nonempty unmarked root outside admitted bootstrap shape10, no legacy fallbacksrc/grove_abi.zig:112; src/grove_abi.zig:121; src/grove_abi.zig:301
Empty unmarked root with LOCK and/or root directory .embedder/Init admitted; pending bootstrap may also contain FORMAT.next. Root files GROVE and .cli-lock are unknown entries; init refuses 10src/grove_abi.zig:108; src/grove_abi.zig:114
.embedder is a file/symlink, or arbitrary unknown root entryNo opaque-namespace admission; init refuses 10src/grove_abi.zig:115; src/grove_abi.zig:116
open_v1 on unmarked legacy store10; explicit open retains unmarked legacy compatibilitysrc/grove_abi.zig:301; src/grove_abi.zig:309
Valid local node differs from marked node11, not retryable busysrc/grove_abi.zig:325
Same-root live owner, same or other process8; independent roots may open concurrentlyinclude/rural_grove.h:44; src/grove_abi.zig:252
Invalid LOCK inode after acquisition (non-file or nlink != 1)10; acquisition failures remain 8, not all are diagnosed corruptionsrc/grove_abi.zig:252; src/grove_abi.zig:261; src/grove_abi.zig:65
Missing LOCK on normal open vs read-only fsckOpen may create/sync it; fsck never creates it and reports refused/ABI 8 (CLI exit 3)src/grove_abi.zig:252; src/grove_abi.zig:298; src/storage/grove_fsck.zig:1523
Invalid acknowledged metadata / no valid slot / equal valid slot generations10; no silent legacy downgradesrc/grove_abi.zig:330; src/storage/grove_commit.zig:67; src/storage/grove_commit.zig:80; src/storage/grove_commit.zig:90
RecoveryRequired on marked storeRecovery admission runs; handle published only after successful settlementsrc/grove_abi.zig:90; src/grove_abi.zig:329
Destroyed-slot rebuild needing signed approvalRefuse 8; no approval input seam or fabricated receiptinclude/rural_grove.h:58; src/grove_abi.zig:326
Checkpoint/scan on legacy compatibility handle8, never false successinclude/rural_grove.h:82; src/grove_abi.zig:962
fsck nonzero flags / invalid scan kind, limit or flags3include/rural_grove.h:36; include/rural_grove.h:76; src/grove_abi.zig:968; src/grove_abi.zig:62
cas_all count outside 1..16, duplicate key3 (InvalidRecord)src/grove_abi.zig:478; src/grove_abi.zig:483; src/storage/receipt_log.zig:553
arena_redact self/unsealed target, missing source/target, redacted target2 / 1 / 9 respectively; conflicting link 2; repeat identical link succeedssrc/storage/arena.zig:440; src/storage/arena.zig:453; src/storage/arena.zig:552
arena_import_redaction live destination / non-distinct or invalid ciphertextLive destination immutable 2; helper has separate InvalidRecord/NotFound validationinclude/rural_grove.h:85; src/storage/arena.zig:469; src/storage/arena.zig:489

Fsck vocabulary and opaque namespace#

Read-only report fields include schema_version, format_version, complete, clean, verdict, embedder_namespace, coverage, counts, scratch, first_failure, commit_slots, truncation flags and findings. Finding fields are path, offset, category, severity, invariant. Retained-list truncation does not change verdict/count semantics (src/storage/grove_fsck.zig:67, src/storage/grove_fsck.zig:98, src/storage/grove_fsck.zig:126).

Frozen vocabulary / ruleMeaning / ABI result / CLI exitSource
Verdicts clean, incomplete, corrupt, io_error, unacknowledged, unmarked, unsupported, refusedclean, complete/clean and zero findings -> ABI 0 / CLI 0; corrupt or unacknowledged -> ABI 10 / CLI 6; all others -> ABI 8 / CLI 3src/storage/grove_fsck.zig:17; src/grove_abi.zig:982
Categories corruption, io_failure, unacknowledged, unmarked, unsupported, lock_unavailable, warningStable machine words; severity error or warningsrc/storage/grove_fsck.zig:16; src/storage/grove_fsck.zig:67; src/storage/grove_fsck.zig:144
Verdict aggregationI/O dominates; other non-warning categories assign their verdict in traversal order, except unacknowledged only replaces incompletesrc/storage/grove_fsck.zig:127
complete / cleancomplete = !unverified; clean requires complete and findings == warnings; warnings do not set first_failure, even with report limit zerosrc/storage/grove_fsck.zig:117; src/storage/grove_fsck.zig:137; src/storage/grove_fsck.zig:1580
Root .embedder/embedder namespace: present, opaque; no opening/enumerating/checking contents; may stay complete/cleansrc/storage/grove_fsck.zig:1353; include/rural_grove.h:52
Root GROVE / .cli-lock regular filesOrdinary unknown entries: unsupported object class, category unsupported, severity error; incomplete, not clean; ABI 8 / CLI 3src/storage/grove_fsck.zig:1403
Unknown namespace / interrupted traversalPrevents completeness; a fully inspected corrupt store can still be complete, never cleansrc/storage/grove_fsck.zig:85; src/storage/grove_fsck.zig:1579
Existing LOCK absent / held / multiply linkedLock refusal, incomplete and non-clean; ABI 8 / CLI 3; no mutationsrc/storage/grove_fsck.zig:1523
Bytes beyond selected COMMITunacknowledged, not committed content; ABI 10 / CLI 6src/storage/grove_fsck.zig:1269

Only the root directory .embedder/ is opaque; similarly named nested objects or a root file/symlink are not namespaces. Within fsck's documented coverage, all non-opaque store bytes are kernel-owned and inspected; unknown root entries prevent a clean verdict. Supplied typed tree roots are checked; fsck does not infer application tree roots or semantic supersession edges from arbitrary blobs/CAS digests (src/storage/grove_fsck.zig:156, src/storage/grove_fsck.zig:1353, src/storage/grove_fsck.zig:1477).

Slot publication, best-effort frames and locks#

A/B slots are independent whole envelopes, version 2 kind 11. Select the valid identity-bound slot with greatest generation; equal valid generations or a present pair with no valid member refuse corruption. One valid slot suffices. Both absent is allowed only for first publication, not normal load (src/storage/grove_commit.zig:40, src/storage/grove_commit.zig:59, src/storage/grove_commit.zig:90). The writer validates generation+1, overwrites the other slot at offset zero without truncation or a rename, then follows:

sync frames and candidate pages -> whole slot overwrite -> file fsync (including fdatasync barrier) -> parent-directory sync -> memory publication -> acknowledgement (src/storage/grove_commit.zig:179, src/storage/grove_commit.zig:195, src/storage/grove_commit.zig:208, src/storage/grove_commit.zig:214, src/storage/receipt_log.zig:2632, src/storage/receipt_log.zig:2644). RuralSlots is bounded evidence for this ordering, with independent-file/checksum/durable-payload assumptions; it is not an unbounded implementation proof (docs/grove/results/F-01.md:7, docs/grove/results/F-01.md:77).

The local durable-best-effort stream appends/syncs a kind-11 frame carrying the next candidate N+1, before publishing its slot. Complete durable but unacknowledged N+1 may be recovered; a torn N+1 falls back while preserving acknowledged N. Durable mode and imported peer streams write zero such frames. Fsck validates their metadata and reports bytes beyond selected COMMIT as unacknowledged (src/storage/receipt_log.zig:2624, src/storage/receipt_log.zig:2708, src/storage/receipt_log.zig:2717, src/storage/receipt_log.zig:2725, src/storage/grove_commit.zig:93, src/storage/grove_fsck.zig:1269). The frozen C open selects explicit durable configuration; the best-effort rule also describes internal storage behavior (src/grove_abi.zig:326).

One lock per store root, held per handle. Rural's root LOCK owns store exclusion, acquired exclusively and nonblocking before classification of FORMAT/bootstrap/legacy data; fsck uses the same existing inode and exclusive fence. Competing opens return busy 8 in both the same and other processes. Legacy compatibility also holds owner.lock against old writers. No lock inode is removed. Close/error unwind explicitly unlocks the owning description before closing it, so fork-inherited descriptors cannot prolong that handle's lock. After close returns, a valid root can be reopened by any process unless another live owner acquired it. An embedder CLI lock only serializes its CLI and never owns the store-use fact (include/rural_grove.h:44, src/grove_abi.zig:244, src/grove_abi.zig:296, src/grove_abi.zig:310, src/grove_abi.zig:365, src/storage/grove_fsck.zig:1523).

Post-tag changes, limits and evidence#

D-R45's change rule: additive only after the tag. A changed symbol gets a _v2 suffix; the old symbol stays. No frozen symbol, argument/struct field, existing enum value, verdict word or category is removed before Root 3. New codes append; additions increment the minor version while major remains 1. The appended WRONG_NODE 11 is part of this freeze, ABI 1.4, not a post-tag addition (include/rural_grove.h:23, src/grove_abi.zig:72; freeze policy recorded in docs/grove/results/I-01.md).

Known limits: the frozen 1.0 binary refusing a format-2 store leaves one empty mode-0600 regular file owner.lock. This is a harness tolerance only; fsck reports it as an unknown root entry, and the operator may delete it.

Known limits: RuralMajority remains a skeleton, outside Root 2.1 proof claims (formal/RuralMajority.tla:6). Chunked writes are not exposed through the GROVE binding; ordinary large puts are not chunked evidence (G-02c result and M-5 transcript, referenced by the collector). R-20 heads journal is post-tag: R-14 measured 470,040 bytes/CAS against 65,536, with idle-host timing and R-21 sync attribution unqualified (docs/grove/results/R-14.md:5, docs/grove/results/R-14.md:169). Power-loss qualification remains deferred.

Run zsh scripts/root21_evidence.zsh <absent-outdir> at the annotated tag commit. It emits EVIDENCE.md/evidence.json with exact commands, actual exits/counts, log hashes, source/archive identities and honest MAIN/reference rows. Set GROVE_WORKTREE for offline fresh GROVE creation/full comparison; ROOT21_DOGFOOD_STORES supplies newline-separated closed roots for read-only fsck, and ROOT21_FORMAL_CACHE supplies a prepopulated verified pinned jar cache. No network download or live-store migration is performed. R-14 numbers and the M-5 transcript are explicitly identified historical references, not relabelled current runs. I-01's retained pre-tag collection does not prove the rewritten tag commit; remaining MAIN items require matching evidence. Tag signature review remains pending. The collector only prints a signing command and never executes it.