sprawling-storage 0.0.10

The append-only, hash-chained Ledger of a sprawling city, its content store, checkpoints and worktrees.
Documentation
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-- Copyright (c) 2026 2youg1 and the sprawling contributors

/-!
# storage::error

规定 `error`(`crates/storage/src/` 下同名的文件)。`StorageError` 与 `into_ax`:本 crate 失败词汇的唯一定义点,以及每个变体答哪个码。本文件是 `crates/storage/Spec.lean` 的一个分部;下面每一节保留它在 storage 规格里的标签 §8-n,别处引作 `crates/storage/Spec.lean §8-n`,决定引作 `storage D<n>`。
-/

/-!
### 8-14 storage::error(形状 2 值)

```rust
pub enum StorageError {                      // thiserror;crate 根
    Io { op: &'static str, path: PathBuf, source: io::Error },
    VersionAhead { path: PathBuf, v: u64 }, // 方向感知拒绝的机器面
    Envelope { path: PathBuf, line: u64, source: AxError },  // 段中损坏(断尾候选之外)
    Draft { source: AxError },              // 组 log_truncated 草稿失败(不以死变体粉饰)
    CasMissing / CasCorrupt / RangeOutOfBounds,               // cas
    SeqMissing,                                              // index
    Checkpoint / SecretEgress / Bundle,                      // 各自的模块
    Worktree / WorktreeBusy / MergeStale / MergeWouldDiscard, // worktree
    Alias { op: &'static str, path: PathBuf, kind: alias::AliasKind },  // → E_OUTSIDE_WRITE_DOMAIN(8-25)
    NameTaken { path: PathBuf },            // 新建时名字上已站着东西(8-32)→ E_VERSION_CONFLICT
    LedgerHeld { dir: PathBuf },            // 另一个 JsonlLedger 持着这座城的账本(8-1)→ E_LEDGER_HELD
    LedgerBroken { dir: PathBuf, at: Seq }, // 一波的写或 sync 失败过,重开前拒绝之后每一波(8-1)→ E_STORAGE_FATAL
    ChainHalted { source: AxError },        // 全链审计发现断链,写者停止接新行(8-30);码取审计自己的 source
    Unproven,                               // 开城所依的历史还没证完,写者不接新行(8-30)→ E_HISTORY_UNPROVEN
    Snapshot { op: &'static str, path: PathBuf, source: io::Error },  // 快照读写被盘拒绝(8-26)→ E_STORAGE_FATAL,恢复说的是快照
}
impl StorageError { pub fn into_ax(self) -> AxError; }   // 跨 crate 边界的唯一出口
pub(crate) fn io_err(op: &'static str, path: &Path) -> impl FnOnce(io::Error) -> StorageError;
```

- **为什么是独立模块**:见 §7,全 crate 的模块都用它。
- **为什么带着 `io_err` 走**:它是 `StorageError::Io` 的构造子,而一个值的构造子与它的定义同住。四个模块(cas/bundle/digest_cache/index)只为取它而 import jsonl,那是一条指错了方向的依赖。
- **快照的 I/O 失败有自己的变体**:`Io` 的恢复建议说的是账本(停机,重开会截掉撕裂的尾巴),对快照是错的——快照是账本随时能重建的缓存。`Snapshot` 与 `Io` 同码(盘拒绝了写,多半账本也写不进),恢复则说:删掉这份快照、腾出盘,下次启动从创世折叠。被否:让 `Io` 的恢复按 `op` 分支——一个变体两种建议,读恢复的人得先知道 `op` 的全集。
- **公开名是 `storage::StorageError`**(`lib.rs` 重导出):模块住处不进公共拼写,下游的引用不随它动。
-/

/-!
## 每个变体答哪个码

`StorageError::into_ax` 是本 crate 失败越过 crate 边界的唯一出口。下面的 `code` 列出每个变体答的 `AxCode`;`Draft` 与 `ChainHalted` 不造码,原样交出它们带着的那个 `AxError`(`carried`)。码本身(拼写、语义、装载期白名单)的权威是 kernel 的错误码表(`crates/kernel/Spec.lean` §8-4),这里只说 storage 的哪个失败落在哪个码上,以及为什么它不能被定义掉。

账本介质的失败(`Io`、`LedgerBroken`)与快照的盘拒绝都答 `E_STORAGE_FATAL`,宁停不脏:这是 `code` 的三个分支本身,单独立一条只核对这三格的定理只会重述定义;`only_two_variants_carry` 陈述只有 `Draft` 与 `ChainHalted` 不造码,所以别的变体的码都在这张表里写死。
-/

namespace Storage.Error

/-- `StorageError` 的变体,拼写与 Rust 相同,去掉各自携带的字段。 -/
inductive StorageError where
  | Io
  | VersionAhead
  | Envelope
  | Draft
  | CasMissing
  | CasCorrupt
  | RangeOutOfBounds
  | SeqMissing
  | Checkpoint
  | SecretEgress
  | Bundle
  | Worktree
  | WorktreeBusy
  | MergeStale
  | MergeWouldDiscard
  | Alias
  | NameTaken
  | LedgerHeld
  | LedgerBroken
  | ChainHalted
  | Unproven
  | Snapshot
  deriving DecidableEq, Repr

/-- storage 答出的 kernel `AxCode`,拼写与 Rust 的变体相同。 -/
inductive AxCode where
  | StorageFatal
  | LogVersionUnsupported
  | HistoryUnproven
  | WorktreeBusy
  | VersionConflict
  | PathNotFound
  | CasCorrupt
  | InvalidArgs
  | ConfigInvalid
  | SecretEgress
  | LedgerHeld
  | OutsideWriteDomain
  deriving DecidableEq, Repr

/-- 一个变体越过边界时的码:自己的码,或原样交出它带着的那个错误的码。 -/
inductive Answer where
  | own (code : AxCode)
  | carried
  deriving DecidableEq, Repr

/-! D3 `VersionAhead`→`E_LOG_VERSION_UNSUPPORTED`

`VersionAhead`→`E_LOG_VERSION_UNSUPPORTED`:不可定义掉——二进制升级与数据寿命天然错位;方向感知拒绝即其最小语义。
-/

/-! D4 `CasCorrupt`→`E_CAS_CORRUPT`

`CasCorrupt`→`E_CAS_CORRUPT`:不可定义掉——位腐烂与外部改动在本设计边界外;能定义掉的部分(写路径半成品)已由 tmp+rename 定义掉。
-/

/-! D5 `CasMissing`→`E_PATH_NOT_FOUND`

`CasMissing`→`E_PATH_NOT_FOUND`:不可定义掉——Locator 是跨会话引用,对象可被更早的介质事故清除;nearby 给同前缀既存对象。
-/

/-! D6 `RangeOutOfBounds`→`E_INVALID_ARGS`

`RangeOutOfBounds`→`E_INVALID_ARGS`:可部分定义掉——`Range` 构造已保 `from<=to`;对象长度只在读时可知,读时校验是剩余的不可消部分。
-/

/-! D7 `Io`→`E_STORAGE_FATAL`

`Io`→`E_STORAGE_FATAL`(宁停不脏路径;不可定义掉——介质失败在设计边界外)。
-/

/-! D8 `WorktreeBusy`→`E_WORKTREE_BUSY`

`WorktreeBusy`→`E_WORKTREE_BUSY`:可定义掉但尚未做——当「领节点」本身变成取租约(`storage::queue` 已有队列),busy 就从错误变成排队。它同时承担「该节点的树被占」与「再开一棵就越上限」两个情形:两者的可执行替代同为「先归还一棵」,而区分它们的是 subject 不是码。
-/

/-! D9 `MergeStale`→`E_VERSION_CONFLICT`

`MergeStale`→`E_VERSION_CONFLICT`:不可定义掉——两个节点同时开工就会有一个后到;能定义掉的那部分(“合到一半失败”)已由 fast-forward 判定在动手之前定义掉。
-/

/-! D10 `MergeWouldDiscard`→`E_VERSION_CONFLICT`

`MergeWouldDiscard`→`E_VERSION_CONFLICT`:不可定义掉——人的未提交改动在城市目录里,机器无权决定它与节点的活谁留下;能定义掉的「静默覆盖」已由 SAFE 检出定义掉。
-/

/-! D11 `Worktree`→`E_STORAGE_FATAL`

`Worktree`→`E_STORAGE_FATAL`:不可定义掉——仓库与文件系统是外部世界;能定义掉的那部分(名字走出目录)已由 `WorktreeName` 在构造点定义掉。
-/

/-! D12 `Alias`→`E_OUTSIDE_WRITE_DOMAIN`

`Alias`→`E_OUTSIDE_WRITE_DOMAIN`:不可定义掉——名字与它指向的文件之间隔着一个链接是外部文件系统的事实;能定义掉的那部分(一次写入经链接穿透)已由 `WriteTarget` 在构造点定义掉,recovery 恒为「换成普通文件后重试」,故被拒的 run 不会卡死。硬链接臂在两个平台上都在这个码下(链接计数大于 1 即拒,§3.5)。
-/

/-! D17 `Envelope`→`E_LOG_VERSION_UNSUPPORTED` 同族拒读

`Envelope`→`E_LOG_VERSION_UNSUPPORTED` 同族拒读(段中损坏非尾部=不可自动修复,指出路径交人决定)。
-/

/-- `StorageError::into_ax` 的码。`Checkpoint` 答 `E_WORKTREE_BUSY`(恢复:理顺仓库的状态,再跑这一波),`Bundle` 答 `E_CONFIG_INVALID`(I/O 本身无误、得到的却不是一座城:目标目录被占、清单与到达的东西不符、链有缺口;恢复:恢复进空目录,bundle 短了就再拷一次),`NameTaken` 答 `E_VERSION_CONFLICT`(§8-32),`LedgerHeld` 见 storage D18,`LedgerBroken` 见 storage D16,`Unproven` 见 storage D19。 -/
def code : StorageError → Answer
  | .Io => .own .StorageFatal
  | .VersionAhead => .own .LogVersionUnsupported
  | .Envelope => .own .LogVersionUnsupported
  | .Draft => .carried
  | .CasMissing => .own .PathNotFound
  | .CasCorrupt => .own .CasCorrupt
  | .RangeOutOfBounds => .own .InvalidArgs
  | .SeqMissing => .own .InvalidArgs
  | .Checkpoint => .own .WorktreeBusy
  | .SecretEgress => .own .SecretEgress
  | .Bundle => .own .ConfigInvalid
  | .Worktree => .own .StorageFatal
  | .WorktreeBusy => .own .WorktreeBusy
  | .MergeStale => .own .VersionConflict
  | .MergeWouldDiscard => .own .VersionConflict
  | .Alias => .own .OutsideWriteDomain
  | .NameTaken => .own .VersionConflict
  | .LedgerHeld => .own .LedgerHeld
  | .LedgerBroken => .own .StorageFatal
  | .ChainHalted => .carried
  | .Unproven => .own .HistoryUnproven
  | .Snapshot => .own .StorageFatal

theorem only_two_variants_carry (e : StorageError) :
    code e = .carried ↔ e = .Draft ∨ e = .ChainHalted := by
  cases e <;> decide

end Storage.Error