# Replace Interval Design

## 1. 目标与非目标

### 目标

1. 用 stable `lid` + stable durable `addr` 替换当前 `IntervalMap` exact ownership 路径，消除 `gc_data` / recovery 中的 `NotFound` 与 dangling pointer 问题。
2. 完全删除 interval 作为 exact ownership source-of-truth 的职责，不保留 runtime fallback。
3. 保留当前 `bwe-opt` 的核心收益：
   - per-pid durable cutoff
   - dirty-page checkpoint closure
   - resident incremental checkpoint 语义
   - `PageMap` 仍然是 `AtomicU64 + CAS`
   - data/blob 分文件与现有 victim 选择逻辑
4. 让 normal load / recovery / junk apply / GC rewrite 共享同一套 exact owner 表达。
5. 把当前依赖 `addr` 单调递增和 `stablized_addr` 上界的 durability 判定整体替换掉。
6. 明确不保留“addressable 但未 durable”的中间状态，避免 runtime-only tagged artifact 再次制造悬空引用。

### 非目标

1. 本方案不改变事务、MVCC、`CommitTree::compact`、`Options::sync_on_write` 语义。
2. 本方案不改变当前 bucket 级 dirty-page checkpoint 模型；变化的是 ownership/source-of-truth，而不是 checkpoint 单元。
3. 本方案不引入物理 `FragLoc` graph，不把物理位置写进 durable 引用。
4. 本方案不把 GC victim 选择改成新算法；继续沿用当前 `data_stat/blob_stat + bucket_files` 逻辑。
5. 本方案不保留 mixed mode：同一 bucket 不允许一部分对象走 interval exact lookup、另一部分走 stable-lid 路径。
6. 本方案不考虑兼容旧 manifest / 旧 data/blob 格式；新格式落地后直接替换。

## 2. 当前实现的关键问题

当前失败模式不是 checkpoint payload 本身不完整，而是 durable ownership 的表达层错了。

当前 exact 路径是：

`pid -> addr -> interval -> file_id -> reloc(addr)`

具体问题：

1. `page_table` 只记录裸 `addr`，见 `src/meta/entry.rs`。
2. `load_data/load_blob` 先做 `interval.find(addr)` 再做 `reloc(addr)`，见 `src/meta/mod.rs`。
3. `apply_data_junks/apply_blob_junks` 也走同一条 `addr -> interval -> file_id -> reloc(addr)` 路，见 `src/meta/mod.rs`。
4. `FileBuilder::build_data/build_blob` 当前为每个文件只写一个区间 `[lo, hi]`，见 `src/map/data.rs`。
5. 一旦 file 中的真实 live `addr` 集合是稀疏的，interval 会覆盖空洞，后续 checkpoint/GC rewrite 再写入这些空洞时就会交错 owner。

现象表现为：

1. recovery / reopen 阶段 `Tree::find_leaf` 经由 `load_data()` 命中错误 file，最终在 `reloc(addr)` 上报 `NotFound`。
2. 如果把 exact owner 进一步改成物理 `FragLoc`，GC rewrite 后会把旧页里的物理 pointer 变成 dangling pointer，错误从 `NotFound` 变成 `IoError`。
3. 当前实现中还有一整套基于 `addr` 单调递增的 durability 判定：
   - `CheckpointTask::needs_flush`
   - `collect_remote_addr` / `collect_addressable_target`
   - `Node::is_durable_header`
   - publish 时 `<= stablized_addr` 的 junk 过滤
   - recovery 时基于最大 durable `addr` 回推 `next_addr`
   这套逻辑在 runtime `addr = pointer as u64` 后会整体失效。

根因可以概括成两条：

1. exact owner 不能靠 interval 这类范围近似。
2. durable boundary 不能再靠 `addr` 大小关系表达。

## 3. 必守不变量

### 持久化与 crash-safety 不变量

1. 保持 `data-first, meta-last`。
2. 同一个 dirty pid 的 sibling / remote / junk closure 必须属于同一个 `PublishGroup`。
3. 单个 bucket round 的多个 `PublishGroup` 最终合并成一个 `CheckpointBatch`。
4. 一个 `CheckpointBatch` 可以生成多个 data/blob 文件，并且包含多个 `PublishGroup` 的：
   - `page_table`
   - `data_lid_map/blob_lid_map`
   - junk apply
   - frontier publish
   - orphan clear
   只能在一次 metadata txn 中整体提交。
4. crash before metadata commit：
   - 整个 `CheckpointBatch` 对 recovery 不可见。
5. crash after metadata commit：
   - 整个 `CheckpointBatch` 一次性对 recovery 可见。
6. `retire_after_apply_before_clear` 窗口必须幂等闭合；重启后重复 apply 不能造成 double-retire 或 owner 表缺失。
7. `next_data_lid/next_blob_lid` 必须和引用这些新 `lid` 的 `lid_map/page_table/junk apply/frontier` 在同一个 metadata txn 中提交。
8. `oid` 不持久化；recovery 不允许在旧 `lid` 上续写新的 `oid`。
9. 每次重启后都必须把 `next_data_lid/next_blob_lid` 自动推进到新的 `lid`，即使上次持久化的 `Numerics` 中对应值尚未被实际发布使用，也只能跳过，不能复用。

### `addr` / runtime 生命周期不变量

1. runtime `header.addr` 与 durable `header.addr` 语义不同，必须显式区分：
   - `AddrState::LocalPtr`
   - `AddrState::DurableAddr`
2. resident-only object 可以没有 durable `addr`。
3. 任何 `LocalPtr` 都不能进入：
   - manifest
   - `page_table`
   - `PublishedCutoff`
   - `CheckpointAck`
   - 持久化 retire 集合
4. 一旦对象进入 page checkpoint durable publish 闭包，必须在 frozen scratch copy 上拿到稳定 durable `addr`。
5. runtime `header.addr = pointer as u64` 仅用于本地 identity，不承担 recovery/source-of-truth 职责。
6. runtime retire 集合必须显式拆成两类：
   - `retired_local_ptrs`
   - `retired_durable_addrs`
7. 只有 `retired_durable_addrs` 允许进入：
   - `CheckpointAck.retired_addrs`
   - `PublishedCutoff.retired_addrs`
   - 持久化 retire apply
8. `retired_local_ptrs` 只参与 runtime 本地回收，不跨 publish 边界传播。
9. 所有以 `header.addr` 作为 key 的 runtime catalog，在对象从 `LocalPtr` 过渡到 `DurableAddr` 时都必须显式 rekey，不能长期同时保留 old key 和 new key。
10. `retired_local_ptrs` / `retired_durable_addrs` 必须在 runtime state 中显式分离存储，禁止长期依赖“混合存放 + 使用点过滤”替代状态边界。

### post-publish 过渡层不变量

1. 允许存在一个受控的 post-publish transition layer，用来承接“metadata 已提交，但 live runtime page 还未立刻完成 rebase”的短暂窗口。
2. 这个 transition layer 只能表现为 typed rewrite registry：
   - `data_local_addr -> durable_data_addr`
   - `blob_local_addr -> durable_blob_addr`
3. 禁止使用一张混合 `u64 -> u64` 表同时承接 data/blob rewrite；因为 `data_lid` 与 `blob_lid` 是独立编号空间，数值允许重合。
4. typed rewrite registry 必须被以下 active path 统一消费：
   - checkpoint encode
   - publish-time sidecar apply
   - loader fallback
   - GC protected set / remap
5. 如果某个 old local addr 仍可能被 live runtime page / sidecar / inflight batch 观察到，则对应 rewrite entry 不能提前清理。
6. 只有当 old local addr 已退出：
   - live runtime page
   - `PublishedCutoff`
   - `ArtifactRetireState`
   - inflight `CheckpointBatch`
   后，typed rewrite registry entry 才允许被回收。

### data/blob 命名空间不变量

1. `pack(data_lid, oid)` 与 `pack(blob_lid, oid)` 的原始 `u64` 数值空间允许重合。
2. 因此 runtime cache / pin / loader key 空间必须显式区分 data/blob：
   - data 使用 raw durable addr
   - blob 使用 `RemoteView::tag(durable_blob_addr)` 作为 runtime identity
3. 任何 loader / cache / pin / rewrite registry 实现如果把 data/blob durable addr 当成共享 key 空间处理，都视为违反设计。
4. `Val::get_remote()` 返回的是 untagged durable blob addr；runtime 外层 identity 仍必须通过 `RemoteView::tag(...)` 与 data addr 隔离。

### `PageMap` / CAS / evict 不变量

1. `PageMap` 仍然是 `AtomicU64` slot，CAS 语义不变。
2. `Swip::tagged(x)` 仍然表示 durable side，不改成原子 struct。
3. 明确禁止“addressable 但未 durable”的中间态。
4. clean page 可以直接 evict 到 tagged durable head。
5. dirty page 不能被 evictor materialize 成 runtime-only tagged artifact；必须先进入 page checkpoint 的 `assign -> encode -> publish`。

### GC / recovery 不变量

1. `data_lid_map/blob_lid_map` 是 exact owner 的第一跳。
2. `lid` 也是 future-retire / junk-apply 的迁移原子单元。
3. 一旦某个 `lid` remap 到新文件，rewrite 必须复制该 `lid` 下所有未来仍可能进入 junk apply 的 object。
4. 没有被复制到新文件的同 `lid` object，必须在 remap commit 前完成 durable retire closure，并从所有 future retire source 中消失。
5. remap commit 之后，不允许再出现一个 `addr`：
   - 仍可能出现在 future junk apply 中
   - 但在 `addr -> lid -> current file -> reloc(addr)` 上命不中
6. old file 只有在其上所有 `lid` 的当前 owner 都已切走且 future junk apply 不再依赖它时才能删除。
7. GC protected set 必须是 object-level 集合，而不是 bucket-level 粗粒度跳过。
8. GC protected set 的来源至少包括：
   - inflight `CheckpointBatch` 中已 assign/encode 完但尚未 cleanup 的 durable object
   - runtime 中已安装但尚未被后续 checkpoint 吸收完的 `PublishedCutoff`
   - runtime 中已安装且 future load/retire 仍可能命中的 `ArtifactRetireState`
   - typed rewrite registry 中仍有效的 old/new addr 对应对象
9. GC protected set 必须有显式 install/clear 生命周期，不能依赖“看到某个 bucket 正在 flush”这类隐含状态猜测。

## 4. 控制模型总览

### 4.1 新核心对象

1. `durable addr`
   - 编码为 `pack(lid, oid)`
   - 表示稳定 logical object identity
   - 不表示物理位置
   - data object 直接使用 `pack(data_lid, oid)`
   - blob object 继续使用 `RemoteView::tag(pack(blob_lid, oid))`
2. `data_lid_map`
   - `data_lid -> current_data_file_id`
3. `blob_lid_map`
   - `blob_lid -> current_blob_file_id`
4. `next_data_lid/next_blob_lid`
   - 存在 `Numerics`
   - 分别提供 data/blob 新 `lid` 的持久化分配边界
   - recovery 完成后自动各自 `+1`，保证重启后只会使用 fresh `lid`
5. `AddrState`
   - `LocalPtr`
   - `DurableAddr`
6. `PublishGroup`
   - 单个 dirty pid 的完整 sibling / remote / junk durable publish 单元
7. `evict_pending`
   - page checkpoint 可见的 delayed-evict 集合

### 4.2 统一的 exact owner 路径

统一后只有两条 exact 路径：

1. data：
   - `addr -> data_lid -> current_data_file -> reloc(addr)`
2. blob：
   - `addr -> blob_lid -> current_blob_file -> reloc(addr)`

`page_table` 仍然只存：

`pid -> head_addr`

其职责只是告诉系统“这个逻辑页当前 durable head 是谁”，不再参与 exact file ownership 推断。

### 4.3 为什么这个模型能关闭已知失败模式

1. interval 完全退出 exact path，消除了 range over-approx。
2. payload 中只保存稳定 logical `addr`，不保存物理 owner，因此 GC rewrite 不会制造物理 dangling pointer。
3. `CheckpointBatch -> one metadata txn` 保证了同一轮 bucket checkpoint 的可见性原子，而每个 `PublishGroup` 的 closure 又不会被拆进不同 batch。
4. `AddrState` 把 runtime pointer identity 和 durable owner identity 明确分开，替代了旧的 `addr` 单调递增判断。
5. `lid` 作为 remap + future-retire 的闭包单元，保证 `junk apply` 始终能走精确路径。
6. blob/data 继续沿用现有 tag 语义分流，不引入第三套 kind 编码规则。

## 5. 关键时序

### 5.1 正常前台写入

1. resident page 上执行 `link/update/delete/compact/split/merge`。
2. 新生成的 delta/sibling/remote 仍然是 resident-only object。
3. 对这些 resident-only object：
   - `header.addr = pointer as u64`
   - `AddrState = LocalPtr`
4. dirty pid 继续进入 bucket 的 live dirty set。
5. 这一步不分配 durable `addr`。

### 5.2 page checkpoint：snapshot

1. `swap(dirty_pid_set, empty_set)` 冻结本轮 dirty pid 集。
2. 对每个 dirty pid：
   - 读取当前 node
   - 收集 sibling / remote / junk closure
   - 若该 pid 在 `evict_pending` 中，也在这一步一起处理
3. 单个 dirty pid 的 closure 形成一个 `PublishGroup`。
4. 如果本轮一个 bucket 有多个 dirty pid，就有多个 `PublishGroup`。
5. 这些 `PublishGroup` 在本轮 bucket checkpoint 中共同组成一个 `CheckpointBatch`。
6. `CheckpointBatch` 才是最终的 metadata 可见性单元：
   - 一个 bucket round
   - 一次 metadata txn
   - 一次 frontier/watermark/done 推进

resident incremental checkpoint fast path 在语义上保留：

1. 如果某个 resident page 相对当前 durable cutoff 只新增了 suffix，checkpoint 仍然可以只持久化新增部分
2. 旧 durable prefix 不需要重新 materialize 成新的完整 page image
3. 但这条 fast path 不能绕过统一的 planner，仍然必须进入同一个：
   - `PublishGroup`
   - `assign`
   - `encode`
   - `CheckpointBatch publish`
4. 换句话说，保留的是“增量持久化语义”，删除的是“直接绕过 assign/encode 的旧 shortcut”

### 5.3 page checkpoint：assign

1. `assign` 的工作单元是单个 `PublishGroup`，不是单个输出文件。
2. 遍历该 group 中所有需要 durable publish 的 data/blob object。
3. 为首次 durable publish 的 object 分配：
   - `data_lid` 或 `blob_lid`
   - `oid`
   - `DurableAddr`
4. 新 `lid` 必须分别从 `Numerics.next_data_lid` / `Numerics.next_blob_lid` 预留。
5. `oid` 只在当前 `assign`/write plan 生命周期内递增，不写入 `Numerics`，也不做 crash recovery。
6. 同一个新输出文件内的 object 共享同一个新 `lid`，其 `oid` 从该文件的局部计数器连续分配。
7. 单个新输出文件中的首次 durable object 数达到 `2^20` 时，必须切换到新的 `lid`。
8. recovery 后不允许继续向 crash 前已经打开但未完整发布语义的 `lid` 追加 `oid`；只能申请新的 `lid`。
9. `assign` 完成后，整个 group 的逻辑 `addr` 才算冻结。
10. `assign` 不能只给“页对象自己”分配 `addr`；凡是嵌在 delta/base value 中、并通过：
   - sibling 指针
   - remote 指针
   - leaf sibling hint
   - leaf remote hint
   指到的 target，只要当前仍是 `LocalPtr`，都必须被纳入同一个 group 并先分配 durable `addr`。
11. `assign` 结束时必须拿到一张完整映射：
   - `LocalPtr -> DurableAddr`
   其中不能留下任何后续 encode 仍会访问到、但没有 durable `addr` 的 sibling/remote target。
12. 这张映射不仅服务于 payload patch，也服务于本轮 publish-time sidecar patch：
   - `CheckpointAck`
   - `PublishedCutoff`
   - `ArtifactRetireState` 中仍需跨 publish 边界保存的地址集合
13. `assign` 结束时还必须冻结最终 write plan：
   - `{object -> target file, lid, oid}`
   - 哪些对象属于增量 suffix
   - 哪些 durable prefix 只复用、不重写
14. 一旦 `assign` 完成，后续 write 只能按这份 plan 落盘，不允许在 encode/write 阶段再做 repack 或重新分配 file partition。
15. 对本轮会跨 publish 边界继续存活的 runtime catalog entry，也必须在这一阶段确定 rekey 计划：
    - `ArtifactRetireState`
    - 其他以 `addr` 为 key 的 runtime sidecar
16. `assign` 必须同时产出 typed rewrite registry 输入：
    - `data_local_addr -> durable_data_addr`
    - `blob_local_addr -> durable_blob_addr`
    这两张表是 publish 后 transition layer 的唯一来源，禁止在 publish 成功后再靠扫描 runtime page 临时猜测生成。

### 5.4 page checkpoint：encode

1. 仅在 frozen scratch copy 上执行。
2. 把 group 中所有引用字段从 `LocalPtr` patch 成 stable durable `addr`。
3. 需要 patch 的至少包括：
   - `page header.addr`
   - `page header.link`
   - delta remote ref
   - delta value 中的 sibling 指针
   - base remote hints
   - base sibling hints
   - base value 中保留存储的 sibling/remote 指针
   - `CheckpointAck.head_addr`
   - `CheckpointAck.remote_addrs`
   - `CheckpointAck.retired_addrs`
   - `PublishedCutoff.head_addr`
   - `PublishedCutoff.remote_addrs`
   - `PublishedCutoff.retired_addrs`
4. 任何 live runtime object 都不在这一步原地改写。
5. `encode` 的前提是 `assign` 已经完成整个 group 的 transitive closure；`encode` 本身不再动态分配新 `addr`。
6. publish-time sidecar patch 必须和 payload encode 使用同一张 `LocalPtr -> DurableAddr` 映射；否则 publish 后 resident remote unpin / retired 清理会因为地址不相等而失配。
7. 对 resident incremental checkpoint fast path 来说，`encode` 只处理本轮新增 suffix 与 sidecar patch；对已 durable prefix 只复用 owner，不重新编码。
8. `CheckpointAck/PublishedCutoff.retired_addrs` 在进入 patch 阶段前就必须已经是 durable-only 集合；local ptr retirees 不得进入这些字段。
9. 任何 runtime catalog rekey 也必须基于同一张映射执行，且必须满足：
   - old key 被移除
   - new key 在 publish 后可见
   - 不允许 old key/new key 同时长期存在
10. 如果 live runtime page 选择不在 publish 时原地改写 payload，那么同一张映射必须转入 typed rewrite registry，直到该 page 完成 rebase/evict。

### 5.5 page checkpoint：write

1. 一个 `PublishGroup` 可以被分散写入多个 data/blob 文件。
2. 每个文件都必须先完成：
   - orphan marker
   - file write
   - file sync
3. write 阶段允许多个输出文件，但它们都还不可见。

### 5.6 page checkpoint：metadata publish

1. 所有属于同一个 `CheckpointBatch` 的文件都完成 write + sync 后，才能进入 metadata publish。
2. `CheckpointBatch` 只允许一次 metadata txn，统一提交：
   - 本轮所有 `PublishGroup` 的 `page_table` 更新
   - 本轮所有 `PublishGroup` 的 `data_lid_map/blob_lid_map`
   - 本轮所有 `PublishGroup` 的 junk apply
   - frontier
   - 若本轮预留了新 `lid`，则同步提交 `Numerics.next_data_lid/next_blob_lid`
   - orphan clear
3. 不允许先让某个 `PublishGroup` 的 sibling 所在文件对应 owner 可见，再让它引用的 target 可见。
4. 也不允许把同一 bucket round 中的多个 `PublishGroup` 分多次 metadata commit。
5. crash before metadata commit 时，本轮预留但未提交的 `lid` 允许整体作废；recovery 通过重启后的 `next_data_lid/next_blob_lid += 1` 保证不会复用到这些 `lid`。
6. `page_table` 对每个 `pid` 的 head_addr 必须来自对应 `PublishGroup` 的显式 planner/ack 结果，禁止在 publish 时通过“文件内最后一个 addr”或“最大 addr”推导 head。
7. metadata txn 成功后，必须先统一安装：
   - `page_table`
   - `lid_map`
   - numerics/frontier
   - typed rewrite registry
   再允许任何后续 loader/runtime path 观察到新 durable head。

### 5.7 evict

1. clean page
   - 已有 durable sealed head
   - 直接切到 tagged durable head
2. dirty page
   - 只加入 `evict_pending`
   - 等 page checkpoint `assign -> encode -> publish` 完成后再做真正 eviction
3. pointer -> tagged durable head 的最终切换
   - 必须在 node lock 下完成
   - 切换前再次校验 page-map entry 未变化
4. 对非 resident/addressable page，如果 publish 后要从 pointer swip 过渡到 tagged durable head，这个切换必须使用与该 batch 相同的：
   - head_addr
   - retire state
   - rewrite registry / published cutoff
   禁止用独立于 batch planner 的后置猜测逻辑完成切换。

### 5.8 GC rewrite

1. 读取 victim 文件中的 live object。
2. live object 原样保留 `addr`。
3. 对应 `lid` remap 到新文件。
4. 同时满足：
   - 同 `lid` 下 future junk apply 仍可能命中的 object 全部被复制
   - 未复制的 object 在 remap commit 前已经完成 durable retire closure
   - 已 snapshot / 已 assign 但尚未 metadata commit 的 inflight `CheckpointBatch` 里引用到的 object 也必须算入 protected set
   - runtime 中已安装但尚未被后续 checkpoint 吸收完的 `PublishedCutoff/ArtifactRetireState` 也必须算入 protected set
5. metadata txn 中统一提交：
   - `data_lid_map/blob_lid_map` 更新
   - stat/bitmap 更新
   - obsolete file delete stage
6. rewrite 判断某个对象是否必须复制时，不能只看 stat bitmap；还必须把 object-level protected set 一并并入 live set。
7. 如果某个 `lid` 仍在 protected set 中，则 rewrite 必须复制该 `lid` 下所有仍可能被 future load/retire 命中的对象，直到该 `lid` 脱离 protected set。

### 5.9 crash 矩阵

| crash window | 磁盘上已完成 | metadata 可见性 | recovery 预期 |
| --- | --- | --- | --- |
| `after_data_sync` | 部分/全部 data/blob 文件已写并 sync | 不可见 | orphan 清理；整个 `CheckpointBatch` 不可见；本轮预留 `lid` 可废弃 |
| `before_manifest_commit` | batch 所有文件已 sync | 不可见 | orphan 清理；整个 `CheckpointBatch` 不可见；本轮预留 `lid` 可废弃 |
| `after_manifest_commit` | batch 所有文件已 sync | 一次性可见 | `page_table + lid_map + junk apply + frontier + numerics` 一致恢复 |
| `retire_after_apply_before_clear` | retire apply 已做 | 可见 | 重启后重复 apply 幂等，不得 double-retire |

## 6. 代码接入点

### 6.1 `src/types/refbox.rs`

1. `BoxRef::alloc/alloc_exact` 需要支持 runtime `header.addr = pointer as u64` 的初始化方式。
2. durable decode 时继续能直接把 `header.addr` 设为 stable durable `addr`。

### 6.2 `src/types/header.rs`

1. 为 `BoxHeader` 增加 `AddrState` 的表达。
2. 明确 `header.addr` 当前是 `LocalPtr` 还是 `DurableAddr`。
3. 不额外引入新的 blob/data kind bit；继续沿用现有 tag 语义。

### 6.3 `src/map/buffer.rs`

1. `Pool::alloc` 不再从 `state.next_addr` 取 runtime `addr`。
2. active resident page catalog 改成以 pointer-style `addr` 做本地 identity。

### 6.4 `src/map/table.rs`

1. `BucketState.next_addr/stablized_addr` 不再承担 durability 语义。
2. 最终需要从 `BucketState` 中移除或弱化这两个字段。

### 6.5 `src/types/node.rs`

1. `Node::is_durable_header` 改为基于 `AddrState`。
2. 依赖 `stabilized_addr` 的 local/durable 判定全部替换。
3. `ArtifactRetireState` / `PublishedCutoff` / `CheckpointAck` 只允许持有 durable `addr`。
4. `apply_published_cutoff()` 依赖的：
   - `head_addr`
   - `remote_addrs`
   - `retired_addrs`
   必须都走 publish-time addr rewrite，不能只 patch head。

### 6.6 `src/types/data.rs` / `src/types/base.rs` / `src/types/delta.rs`

1. 支持在 checkpoint `encode` 阶段 patch：
   - `Val` 内编码的 sibling 指针
   - `Val` 内编码的 remote 指针
   - sibling/link
   - remote ref
   - leaf hints
2. 当前 `Val::encode_preserving_storage()` 只是保留旧存储形态，不足以做 pointer->durable-addr 重写；需要新增显式 patch helper，按 `LocalPtr -> DurableAddr` 映射改写 value 内字段。
3. `Base::new_leaf` / `Builder::add_leaf_preserving_storage` 路径必须允许基于 patch 后的 `Val` 重新生成 durable payload，而不是直接把旧 pointer-style `addr` 原样带入文件。
4. blob durable ref 的 encode 规则必须明确为：
   - `Val::get_remote()` 对 durable blob 返回 `pack(blob_lid, oid)`
   - 对外容器通过 `RemoteView::tag(...)` 继续表达“这是 blob ref”

### 6.7 `src/map/data.rs`

1. `CheckpointTask::snapshot` 增加 `PublishGroup` 抽象。
2. bucket round 级别增加 `CheckpointBatch` 抽象，作为最终 publish 单位。
3. `assign -> encode` 变成 checkpoint 的显式阶段。
4. `needs_flush`、`collect_remote_addr`、`collect_addressable_target` 不再比较 `addr` 大小。
5. snapshot 必须同时产出：
   - payload closure
   - publish-time sidecar patch 需要的 `LocalPtr -> DurableAddr` 重写输入
6. `FlushResult` 需要从“单 data_ivl / 单 blob_ivl”升级为“多文件结果集合 + batch 级 publish 元数据”。
7. `CheckpointTask::snapshot` 产出的 `PublishGroup` 必须显式记录每个 pid 的：
   - head addr
   - page / junk / ack slice
   - typed rewrite registry 输入
   禁止在 flush publish 阶段再靠遍历 batch 内容反推 head 或 group 边界。

### 6.8 `src/map/flush.rs`

1. 接收多文件 write 结果。
2. 保证单个 bucket round 的多个 `PublishGroup` 最终合并成一个 `CheckpointBatch` publish。
3. observer/publish 侧只允许一次 metadata txn。
4. `flush` 必须把 typed rewrite registry 与 `CheckpointBatch` 一起向下传递；禁止 publish/runtime/GC 自己重新扫描 page payload 构造第二套 rewrite 映射。

### 6.9 `src/map/publish.rs`

1. 删除 `<= stablized_addr` 的 junk 过滤。
2. 改成只处理 snapshot/freeze 中显式收集出来的 `retired_durable_addrs`。
3. 在 publish 成功后负责执行 runtime catalog rekey：
   - `ArtifactRetireState`
   - 其他以 `addr` 为 key 的 sidecar
4. rekey 必须与本轮 `LocalPtr -> DurableAddr` 映射一致；失败默认保守处理：延迟清理，而不是产生 dangling runtime 引用。
5. publish 成功后，如果 live runtime page 尚未完成 rebase，则必须安装 typed rewrite registry，而不是假设后续路径再也看不到 old local addr。

### 6.10 `src/map/evictor.rs`

1. 删除 runtime-only tagged artifact 路径。
2. 增加 `evict_pending` 处理。
3. clean page 与 dirty page eviction 分流。
4. 如果 `evict_pending` 通过 `addr` 间接关联页状态或 sidecar，则 publish 后也必须按同一映射完成 rekey 或清理。
5. evictor 不允许自己生成一套脱离 batch planner 的 data/blob rewrite 规则。

### 6.11 `src/meta/entry.rs`

1. 新增 `data_lid_map/blob_lid_map` 的元数据记录格式。
2. `page_table` 继续保留 `pid -> head_addr`。
3. `Numerics` 新增：
   - `next_data_lid`
   - `next_blob_lid`
4. 删除 interval 相关元数据记录格式。
5. 不新增 data/blob 混合 rewrite 元数据格式；typed rewrite registry 只能作为 runtime 过渡层存在，不能反向变成新的 durable source-of-truth。

### 6.12 `src/meta/mod.rs`

1. `load_data/load_blob` 改成 `addr -> lid_map -> file -> reloc(addr)`。
2. `apply_data_junks/apply_blob_junks` 改成同一路径。
3. recovery 不再恢复 interval，而恢复：
   - `page_table`
   - `data_lid_map`
   - `blob_lid_map`
   - `data_stat/blob_stat`
4. recovery 完成后把 `Numerics.next_data_lid/next_blob_lid` 各自自动推进 `+1`，确保重启后新分配只落到 fresh `lid`，不尝试恢复旧 `lid` 内的 `oid` 续写。
5. loader/cache/pin 必须显式区分 data/blob runtime key 空间，不能把 untagged blob durable addr 和 data durable addr 共享一套 key。

### 6.13 `src/store/gc.rs`

1. `rewrite_data/rewrite_blob` 保持原 `addr`。
2. `lid` 作为 remap + future-retire 的闭包单元。
3. remap commit 后不允许 future junk apply 的 `reloc(addr)` miss。
4. GC protected set 不能只看 manifest 当前可见 owner，还必须包含：
   - inflight `CheckpointBatch`
   - runtime 已安装但未吸收完的 `PublishedCutoff/ArtifactRetireState`
5. GC protected set 必须是 object-level install/clear 机制，不能退化为 bucket-level skip。

### 6.14 `src/store/store.rs`

1. page checkpoint 的 publish 结果从单文件扩展为 `CheckpointBatch` 多文件。
2. metadata txn 一次性提交整个 batch。
3. `Store` 必须在 publish 成功后统一安装：
   - page_table / lid_map / numerics/frontier
   - typed rewrite registry
   - runtime sidecar rekey
   禁止把这些动作拆成多套无序的后置补丁。

### 6.15 `src/store/recovery.rs`

1. recovery 不再依赖 interval exact lookup。
2. WAL replay 中的 tree load 也要走新 owner 路径。

## 7. 与现有机制关系

### 与 dirty-page checkpoint 的关系

1. 不改变“单个 dirty pid 的 closure 同进同出”。
2. 进一步加强为“同一个 closure 只能在一次 metadata txn 中整体可见”。

### 与 `PublishedCutoff` / `ArtifactRetireState` 的关系

1. 继续保留这两层 sidecar。
2. 但 sidecar 中不允许再出现 runtime-only pointer identity。
3. sidecar 只保存 durable `addr`。

### 与 `PageMap` 的关系

1. `PageMap` 的 resident/pointer swip 与 tagged durable swip 模型保留。
2. 只是 tagged side 现在严格只允许 durable head。

### 与 blob 的关系

1. blob 不再是 data 路径的附属品。
2. `blob_lid_map` 和 `data_lid_map` 对称存在。

## 8. 配置项建议

### 建议新增的临时开关

1. `replace_interval_enable`
   - 阶段 A/B 默认 `false`
   - 阶段 C 起默认 `true`
2. `replace_interval_strict`
   - 默认 `false`
   - phase C 末尾 / phase D 设为 `true`
   - 打开后禁止任何旧路径入口

这些开关只服务于开发/验证阶段：

1. 不用于线上双格式兼容
2. 不用于在线升级旧 manifest / 旧 data/blob 文件
3. 因为本方案明确不考虑兼容性，新格式的启用边界就是“直接替换”

### 建议禁止的 bypass

1. 不允许增加 `allow_interval_exact_lookup`
2. 不允许增加 `allow_partial_publishgroup_commit`
3. 不允许增加 `allow_reloc_miss_continue`
4. 不允许在新格式 bucket 上回退到 interval exact ownership

## 9. 可观测性

建议新增：

1. Counter
   - `ReplaceIntervalAssignGroup`
   - `ReplaceIntervalEncodeGroup`
   - `ReplaceIntervalLidRemap`
   - `ReplaceIntervalEvictPending`
2. Gauge
   - `ReplaceIntervalPendingEvictPids`
   - `ReplaceIntervalPublishGroupFiles`
3. Histogram
   - `ReplaceIntervalAssignMicros`
   - `ReplaceIntervalEncodeMicros`
   - `ReplaceIntervalPublishGroupFilesPerTxn`
4. Failure-oriented metrics
   - `ReplaceIntervalOwnerLookupMiss`
   - `ReplaceIntervalRelocMissAfterRemap`
   - `ReplaceIntervalUnexpectedLocalPtrPersist`

## 10. 性能影响与约束

### 热路径

1. resident write path 不再消耗 `next_addr`。
2. runtime `header.addr = pointer as u64`，不会增加前台 owner lookup 开销。
3. 读 path 多一跳 `lid_map` lookup，但去掉了 interval 锁与 interval 查找。

### 慢路径

1. checkpoint 新增 `assign -> encode` 两阶段。
2. GC rewrite 需要维护 `lid` 闭包，不允许用“reloc miss 忽略”偷过。
3. evictor 对 dirty page 变成 delayed eviction，更依赖 checkpoint 推进速度。

### 可度量验收

1. 正确性
   - `tests/gc.rs::gc_data`
   - `tests/gc.rs::gc_blob`
   - recovery / failpoint 相关用例
   必须全部通过。
2. 性能
   - `benches/perf.rs` 中 point read 回归不超过 5%
   - 顺序写/随机写回归不超过 10%
   - GC rewrite wall time 回归不超过 10%
3. 运行时
   - `ReplaceIntervalOwnerLookupMiss` 必须长期为 0
   - `ReplaceIntervalRelocMissAfterRemap` 必须长期为 0

## 11. 失败与边界场景

### `after_data_sync`

1. 部分或全部 data/blob 文件已 durable。
2. 但 `page_table/lid_map` 尚未提交。
3. recovery 只做 orphan 清理，整个 `CheckpointBatch` 不可见。

### `before_manifest_commit`

1. batch 所有文件都已 sync。
2. metadata 仍不可见。
3. recovery 结果与 `after_data_sync` 相同。

### `after_manifest_commit`

1. 整个 `CheckpointBatch` 一次性变为可见。
2. recovery 必须同时看到：
   - `page_table`
   - `data_lid_map/blob_lid_map`
   - retire apply
   - frontier

### `retire_after_apply_before_clear`

1. retire apply 已完成。
2. clear 尚未完成。
3. 重启后重复 apply 必须幂等，不得导致 owner 表缺失或重复统计。

### reloc miss

1. 在新方案里，future junk apply 的 `reloc(addr)` miss 被定义为 bug，不再允许 continue。
2. 只要一个 `addr` 未来还可能 retire，它就必须能经 `lid_map` 命中当前 file 并在 `reloc(addr)` 中命中。

### dirty eviction starvation

1. dirty page eviction 依赖 checkpoint。
2. 当内存压力高而 dirty page 多时，应优先推进 checkpoint，而不是制造 runtime-only tagged artifact。
3. 默认失败策略是返回 `Again`/施加 backpressure，不允许跳过 invariant。

## 12. 分阶段落地计划

### phase A

目标：

1. 把文档中的核心类型和边界钉死到代码接口层。

工作：

1. 引入 `AddrState`
2. runtime alloc 改成 `header.addr = pointer as u64`
3. 删掉 durability 判定里对 `addr` 单调递增的依赖
4. 定义 `data_lid_map/blob_lid_map` 元数据格式
5. 定义 `Numerics.next_data_lid/next_blob_lid` 与 recovery `+1` 规则

完成条件：

1. runtime / durable `addr` 语义分离
2. `needs_flush` / `is_durable_header` 不再比较 `addr`
3. `lid` 分配与 recovery 规则固定

### phase B

目标：

1. 打通 page checkpoint 的 `assign -> encode -> publish`。

工作：

1. `PublishGroup` 实装
2. checkpoint 支持多 data/blob 文件 + 一次 metadata txn
3. `page_table + lid_map + junk apply + frontier` 的 group publish

完成条件：

1. dirty pid closure 同收集、同 publish、同可见
2. 不再存在 runtime-only tagged artifact

### phase C

目标：

1. 切换 normal load / recovery / junk apply / GC rewrite 到 stable-lid 路径。

工作：

1. `load_data/load_blob` 改走 `lid_map`
2. `apply_data_junks/apply_blob_junks` 改走 `lid_map`
3. `rewrite_data/rewrite_blob` 保 addr、改 lid map
4. interval 元数据与相关代码完全退出 active path

完成条件：

1. `gc_data/gc_blob` 与 recovery failpoint 全部通过
2. `ReplaceIntervalOwnerLookupMiss = 0`

### phase D

目标：

1. 清理旧路径与 rollout 开关，进入 strict mode。

工作：

1. 删除 interval exact ownership 路径
2. 删除 `stablized_addr/next_addr` 的旧语义
3. 打开 `replace_interval_strict`

完成条件：

1. 新 bucket 只允许新格式
2. 没有旧路径入口

## 13. 测试矩阵建议

### 单元测试

1. `addr = pack(lid, oid)` 编解码
2. `AddrState` 状态转换
3. `data_lid_map/blob_lid_map` 编解码
4. assign 后 patch 的：
   - `link`
   - sibling hint
   - remote hint
   - `CheckpointAck.head_addr`

### 集成测试

1. `tests/gc.rs::gc_data`
2. `tests/gc.rs::gc_blob`
3. reopen after checkpoint
4. reopen after gc rewrite
5. mixed data + blob page closure
6. dirty page delayed eviction

### failpoint / crash

1. `after_data_sync`
2. `before_manifest_commit`
3. `after_manifest_commit`
4. `retire_after_apply_before_clear`
5. `gc_data_rewrite_before_meta_commit`
6. `gc_blob_rewrite_before_meta_commit`

### 幂等与恢复

1. orphan 清理幂等
2. retire apply 幂等
3. remap 后 future junk apply 不 miss
4. old file 删除后 load/junk apply 仍可命中当前 owner

## 14. 与替代方案对比

### 主方案：`addr -> lid -> current file`

优点：

1. 元数据成本相对可控
2. 与 `0.0.13` 的成功部分一致
3. 保持当前 checkpoint closure 模型，改动面可控

缺点：

1. `lid` 必须承担 remap + future-retire 的闭包单元职责
2. GC rewrite 不能随意拆散同一个 `lid`

### 备选方案：per-object owner table

路径：

1. data：`addr -> data_owner -> {file_id, seq}`
2. blob：`addr -> blob_owner -> {file_id, seq}`

优点：

1. 不再要求 `lid` 是迁移原子单元
2. 同一个 `lid` 是否分散到多个文件不影响 exact owner

缺点：

1. metadata 量更大
2. checkpoint / GC 都要批量更新 per-object owner table
3. 对当前实现侵入性更强

当前建议仍然优先走主方案，只有在主方案无法稳定保证：

1. remap 后 future junk apply 永远不 miss
2. `lid` 作为迁移单元不会把 checkpoint / GC 复杂度推高到不可接受

才切换到备选方案。

## 15. 实施顺序建议

1. 先做 `AddrState + runtime pointer addr`，把 `next_addr/stablized_addr` 的 durability 语义拆掉。
2. 再做 `data_lid_map/blob_lid_map` 的元数据格式与 load/junk apply helper。
3. 再做 page checkpoint 的 `PublishGroup + assign -> encode + one metadata txn`。
4. 然后改 evictor，删除 runtime-only tagged artifact。
5. 再改 `rewrite_data/rewrite_blob`，把 `lid` 闭包规则做实。
6. 最后删 interval exact path 与 rollout 开关。
