Binary DB v0:工作流记录
展开 Task、Change、worktree、Line、Stash、Tag、Land 以及内联工作流链接记录的布局与不变量。
适用人群: 内核实现者与工作流集成方
这些 layout 是本地与远端工作流状态的定长记录和关联记录。阅读每个字节布局时,要连同后面的身份、变更、校验和遗留转换规则一起读;仅凭记录大小常量并不构成完整契约。
Task 记录#
LOCAL_TASK_RECORD_SIZE = 64
LocalTaskRecord — task.bin:
u8 task_meta
u8 local_meta
u16 payload_len
u64 payload_offset
u32 origin_plan_revision_index_plus1
u32 plan_item_index_plus1
u32 published_remote_task_index
u64 created_at_s
u64 updated_at_s
u64 plan_linked_at_s
u64 published_at_s
u64 closed_at_sREMOTE_TASK_RECORD_SIZE = 60
RemoteTaskRecord — task.bin:
u8 task_meta
u8 remote_meta
u16 payload_len
u64 payload_offset
u32 origin_plan_revision_index_plus1
u32 plan_item_index_plus1
u64 created_at_s
u64 updated_at_s
u64 plan_linked_at_s
u64 fetched_at_s
u64 closed_at_sTask 的身份是推导出来的,不是存储的:
task_index = record_number
task_seq = task_index + 1
task_id = derive(authority_scope, repo_namespace, "T", task_seq)Task 记录只追加,绝不能在物理上重排。遗留的 SQLite 导入可以对稀疏的遗留序列做 一次压紧;它不得通过插入填充记录来保留空洞。
closed_at_s 是 LocalTaskRecord 和 RemoteTaskRecord 的固定尾部字段, 不是 payload,也不是单独的记录或文件。零表示:要么有效的 Task 状态不是终态, 要么离线遗留转换对某个终态 Task 没有拿到源的关闭时间。后一种情况下 Task 已 关闭,而它的历史关闭时间未知。非零值即关闭时间;当且仅当在 TaskRecord.task_meta 之下(对本地权威还要加上 LocalTaskRecord.local_meta)有效的 Task 状态为终态时,该值才有效。后续的 发布或关联更新可以改变 updated_at_s,但绝不替换关闭时间。
创建 Task 时写入 closed_at_s = 0。关闭操作先写入并 fsync closed_at_s, 之后才写入并 fsync 作为提交标记的终态状态位。原生写入方绝不会清除终态状态来 重新打开一个 Task。读取方把非终态 Task 上残留的非零时间视为缺失,恢复流程会把 它修复为零。原生 v0 写入方绝不能创建关闭时间为零的终态 Task。由于不存在迁移 来源位或字段,读取方、校验器和恢复流程把"终态加零"接受为未知的历史关闭时间, 不把这种表示归类为损坏。
修正前的 layout-1 本地 40 字节和远端 36 字节 Task 记录只能作为离线转换输入。 转换器必须显式收到源记录大小,绝不能仅凭文件大小可整除性来推断。在独占的权威 锁之下,它用 44 字节或 40 字节记录把完整的 task.bin 重写到一个单独的文件, 并保留源权威提供的每一个关闭时间。对于遗留源格式可证明没有记录关闭时间的终态 Task,它写入 closed_at_s = 0,不得以创建时间、更新时间、转换时间或任何其他 推断出的时间戳来替代。定义了关闭时间但提供了非法值的源,仍然失败拒绝。转换器 校验整个重写后的文件,并在激活之前原子地替换旧文件。一个活动的 task.bin 绝不混用修正前和修正后的记录大小。
对于 bin 到 bin 的转换,源的 planning 状态 unplanned 和 explicit_unplanned 都编码为 TaskRecord.task_meta 位 0 未置位;planned 编码为位 0 置位。这就是 v0 完整的 planning 状态编码:v0 有意不保留这两种 unplanned 源写法之间的第三种区分。数值形式的 Plan revision 与 item 绑定会被 独立解析并保留。只有当文本形式的 plan_section_ref 或 plan_drift_state 缺失、或者能被精确推导时,才在不新增字段的情况下被接受:section ref 来自解析 出的 Plan Item 的 heading_path_bytes,而 drift 状态来自数值绑定以及当前的 Plan/Plan-revision 元数据。非推导得出的值或有歧义的值失败拒绝。
被接纳的源 Task 状态 abandoned 映射到 TaskRecord.task_meta 位 7 canceled。它不会新建另一个 Task 状态字段,也不会 从服务端源推断 LocalTaskRecord.local_meta 位 1。在单独列举的、确切的锁定源 门禁之下,遗留源写法 canceled 映射到同一个已有位,不映射到其他任何状态。 显式携带独立的本地放弃状态的本地源,按其既有语义保留该本地位。
对于从可证明没有更新时间或抓取时间的遗留 Remote Task 格式做的离线转换, 转换器相应地写入 updated_at_s = 0 或 fetched_at_s = 0。每个零表示未知的 历史时间,而不是转换时间。提供的合法时间被原样保留;定义了其中任一字段却提供 非法值的源格式失败拒绝。原生 v0 的 Remote Task 写入方继续写入它们实际的事件 时间,不使用这条遗留例外。
Change 记录#
TASK_CHANGE_INDEX_RECORD_SIZE = 8
TaskChangeIndexRecord — task_change_index.bin:
u32 latest_change_index_plus1
u16 change_count
u8 next_change_ordinal
u8 reserved0LOCAL_CHANGE_RECORD_SIZE = 68
LocalChangeRecord — change.bin:
u8 change_meta
u8 local_meta
u16 payload_len
u8 change_ordinal
u8 change_state
u16 reserved1
u64 payload_offset
u32 task_index
u32 previous_change_index_plus1
u32 fork_snapshot_index_plus1
u8 published_remote_change_ordinal_plus1
u8 reserved2
u16 reserved3
u64 created_at_s
u64 updated_at_s
u64 published_at_s
u32 base_line_index_plus1
u64 archived_at_sREMOTE_CHANGE_RECORD_SIZE = 68
RemoteChangeRecord — change.bin:
u8 change_meta
u8 remote_meta
u16 payload_len
u8 change_ordinal
u8 change_state
u16 reserved1
u64 payload_offset
u32 task_index
u32 previous_change_index_plus1
u32 selected_patchset_index_plus1
u32 fork_snapshot_index_plus1
u64 created_at_s
u64 updated_at_s
u64 fetched_at_s
u32 base_line_index_plus1
u64 archived_at_sChange 记录既不存放文本形式的 Change ID,也不存放文本形式的 Task ID。它的 身份是 (authority_scope, task_index, change_ordinal)。
已发布的 Local Change 的 Remote 身份,是由其所属 Local Task 的 published_remote_task_index 和 published_remote_change_ordinal_plus1 - 1 组成的二元组。Task 字段选定确切的 Remote Task ordinal;Change 字段在该 Task 内部选定 C-01..C-64。 没有任何 Local Change 存储或需要一个全局的 Remote change_index。
对于 Remote Change, ChangePatchsetIndexRecord.latest_patchset_index_plus1 是最新/当前的 Patchset,而 selected_patchset_index_plus1 是唯一持久化的、可变的 selected-Patchset 权威,供评审、就绪判定和 Land 使用。选定是 Change 的状态, 不是 Patchset 的不可变属性。零表示没有选定 Patchset。非零的 selected 指针必须 引用一个由该 Change 拥有的存活 Patchset。 源的 current_patchset_number 通过 latest 索引解析; 源的 selected_patchset_number 通过 selected 指针解析。两者可以不同,绝不能 被合并成一个值。选定操作更新这个已有的指针,不改动任何 Patchset 记录。这只是 对已有四字节槽位的一次语义重命名,不会移动该槽位,也不会移动 44 字节前缀中的 任何字段。后来的内联生命周期扩展只追加两个已声明的尾部字段。
仅对被接纳的遗留服务端格式而言,一个已 land 且有一个或多个存活 Patchset 的 Change,可能提供 current_patchset_number = 0,尽管 latest/current 仍然可以 重建。只有当源的 selected 指针非零,且解析到该 Change 拥有的、patch_ordinal 最大的那个存活 Patchset 时,转换才接受这个零;此时它把那个 Patchset 写为 ChangePatchsetIndexRecord.latest_patchset_index_plus1。其他任何为零、缺失、 有歧义、非本 Change 拥有、被省略或互相矛盾的 latest 证据,都失败拒绝。selected 指针仍然独立存储,不会从这条例外推断出来。
ChangeRecord.change_state 位 0 是 canceled;位 1 到位 7 保留且为零。 canceled 位只有在 archived 生命周期下才有效。源的 Change 状态 abandoned 映射为 archived 生命周期、canceled 置位、以及已有的 superseded 位未置位。 它不会新建另一条 Change 记录或 payload。对于可证明没有抓取时间的遗留 Remote Change 格式,转换写入 fetched_at_s = 0;提供的合法值被保留,提供的非法值 失败拒绝。
published_remote_change_ordinal_plus1 = 0 表示缺失。值 1..64 编码 Remote Change 的 ordinal 0..63,所以 C-01 存为 1,C-64 存为 64;值 65..255 非法。reserved2 和 reserved3 必须为零。当且仅当 ordinal 字段为零时,LocalChangeRecord.local_meta 位 0 才未置位。该位置位时,ordinal 字段和 published_at_s 都非零,并且所属的 Local Task 也必须置位它的 published 位。Remote 的 ordinal 可以与 Local 的 change_ordinal 不同;发布时存的是 Remote 权威返回的、确切的 Task 作用域 ordinal。
读取遗留的显式文本 published_change_id 的离线转换器,解析出确切的 C-01..C-64 后缀,并直接写入它的 ordinal 加一的值;它绝不伪造全局的 Remote Change 索引。按旧有的固定 u32 published_remote_change_index 解释来读取的转换器,必须显式选择那个源 schema, 并在写入本字段之前,把被引用的 Remote 记录解析到它所属的 Task 及 Task 作用域 的 ordinal。发布权威缺失、不匹配或有歧义时失败拒绝。
内联 Change 生命周期字段#
base_line_index_plus1 和 archived_at_s 是每条 68 字节的 Local 或 Remote Change 记录中必需的固定字段。它们不是 payload,不是单独的记录,也不是单独的 文件。 base_line_index_plus1 是必需的,引用创建该 Change 时所依据的那条确切的 LineRecord。Change 绝不重复存放 base-Line 名称或文本形式的 Line ID。若 fork_snapshot_index_plus1 非零,它引用创建该 Change 时被观测为该 base Line 的 head 的那个确切的存活 Snapshot。Snapshot 的 line_index_plus1 是它不可变的 创作 Line 身份,而不是 Line-head 归属;当在另一条 Line 上创作的 Snapshot 成为 base Line 的 head 之后,它可能与 base_line_index_plus1 不同,并且不得被重写, 也不得拿来做相等比较。公开的 forked_from_line 值由数值形式的 base-Line 身份 推导而来。
当且仅当 Change 的生命周期为 archived 时,archived_at_s 才非零;例外是:从 可证明没有记录归档时间的遗留源格式转换而来的 archived Change,保留零作为未知 的历史归档时间。ChangeRecord.change_meta 位 7 在该 archived 生命周期内区分 出被取代的 Change,而 ChangeRecord.change_state 位 0 区分取消。归档和重新 打开各自重写并持久提交一条完整的 68 字节 Change 记录;生命周期位与 archived_at_s 一起变化,不涉及旁路文件排序,也不涉及记录内部的状态最后写入 协议。恢复流程拒绝非 archived 却带有非零归档时间的 Change。原生 v0 的归档写入 要求归档时间非零。由于没有新增迁移位,读取方、校验器和恢复流程把"archived 加零"接受为未知的遗留时间。公开的 review 状态是 active 生命周期加上 review_pending;landed_at 由成功的 Land 记录的 updated_at_s 推导,不会在 Change 上再存一份。
创建 Change 时,通过精确的 payload 比较来解析归一化的 base-Line 名称,把该 Line 当前的 head 观测为可选的 fork Snapshot,然后追加完整的 Change 提交记录。 对于已提交的 Change,恢复流程会拒绝:base Line 为零或缺失、Line 缺失或有歧义、 fork Snapshot 缺失或已打墓碑、生命周期与时间不匹配,或 superseded 位非法。 恢复流程不会把 fork Snapshot 的创作 Line 与 Change 的 base Line 做比较。
紧邻的上一版活动 layout-1 表示,即 44 字节的 change.bin 加上按 ordinal 对齐 的 8 字节 change_lifecycle.bin,只能作为离线转换输入。转换器必须显式收到那个 源表示,要求两个文件的记录数相同,保留每个 Change 完整的 44 字节前缀,追加 配对的确切 base-Line 索引和归档时间,校验得到的完整 52 字节文件,并在激活的 目标中省略 change_lifecycle.bin。它不得推断缺失的 base Line,也不得按任何 文本身份来对齐行。
更早的、旁路文件出现之前的 layout-1 generation,只有在它声明的源 schema 独立 提供确切的 base_line 时,才同样只能作为离线转换输入。转换把该值解析为权威 本地的 line_index,要求任何提供的 forked_from_line 都等于那条 base Line, 并保留确切引用的存活 fork Snapshot,不比较也不重写它的创作 Line。它保留提供的 合法源归档时间,只有当被接纳的源格式可证明没有归档时间字段时,才写入 archived_at_s = 0。提供的时间非法,或任何必需的非时间值不可得或有歧义,都会 在物化完整的 68 字节 Change 文件之前失败拒绝。
Worktree 游标#
WORKTREE_CURSOR_RECORD_SIZE = 28
WorktreeCursorRecord — .ait-worktree/cursor.bin:
u8 cursor_meta
u8 reserved0
u16 reserved1
u32 task_index
u32 selected_change_index_plus1
u32 pending_pre_land_target_snapshot_index_plus1
u32 pending_landed_snapshot_index_plus1
u64 updated_at_s该游标只在本地存在,是可丢弃的临时状态。它不是工作流历史,必须排除在内容 snapshot 之外。
Line 与 Stash 记录#
LINE_RECORD_SIZE = 40
LineRecord — line.bin:
u8 line_meta
u8 reserved0
u16 line_name_len
u64 line_name_offset
u32 head_snapshot_index_plus1
u64 created_at_s
u64 updated_at_s
u64 archived_at_sSTASH_RECORD_SIZE = 8
StashRecord — stash.bin:
u8 stash_meta
u8 reserved0
u16 reserved1
u32 stash_snapshot_indexStash 存储只在本地。stash_snapshot_index 必须引用一个种类为 stash 的内容 snapshot。删除 Stash 会给记录打上墓碑,绝不移动后面的索引。
本地 Tag 记录#
TAG_RECORD_SIZE = 24
TagRecord — tag.bin:
u8 tag_meta
u8 reserved0
u16 payload_len
u64 payload_offset
u32 snapshot_index
u64 created_at_stag_index 是稳定的内部 Tag 身份,也就是 TagRecord 在 tag.bin 中从零开始 的 ordinal。公开的 Tag 身份仍然是它归一化的、非空的 UTF-8 名称。 snapshot_index 引用一个存活的内容 Snapshot。
创建 Tag 时先追加它带类型的 payload,再写它的固定记录。强制替换一个已有名称会 保留 tag_index,追加一份新的 payload,然后更新固定记录;替换过程被打断时, 可能只留下一份无人引用的 payload。删除会设置墓碑位,绝不删除被引用的 Snapshot, 也不移动 Tag 的 ordinal。重新创建同一个名称只会清除墓碑位,同时保留同一个 tag_index。
Land 记录#
TASK_LAND_INDEX_RECORD_SIZE = 8
TaskLandIndexRecord — task_land_index.bin:
u32 latest_land_index_plus1
u16 land_count
u16 reserved0
CHANGE_LAND_INDEX_RECORD_SIZE = 8
ChangeLandIndexRecord — change_land_index.bin:
u32 latest_land_index_plus1
u16 land_count
u8 next_land_ordinal
u8 reserved0LOCAL_LAND_RECORD_SIZE = 44
LocalLandRecord — land.bin:
u8 land_meta
u8 land_ordinal
u8 change_ordinal
u8 failure_kind
u32 change_index
u32 previous_task_land_index_plus1
u32 previous_change_land_index_plus1
u32 pre_land_target_snapshot_index_plus1
u32 landed_snapshot_index_plus1
u64 submitted_at_s
u64 updated_at_s
u32 target_line_index_plus1SERVER_LAND_RECORD_SIZE = 48
ServerLandRecord — land.bin:
u8 land_meta
u8 land_ordinal
u8 change_ordinal
u8 failure_kind
u32 change_index
u32 patchset_index
u32 previous_task_land_index_plus1
u32 previous_change_land_index_plus1
u32 pre_land_target_snapshot_index_plus1
u32 landed_snapshot_index_plus1
u64 submitted_at_s
u64 updated_at_s
u32 target_line_index_plus1本地 Land 权威就是存储的那对 snapshot。服务端 Land 权威还存储确切的被接受的 Patchset。保持同一个 L-## 的 Land 状态更新,采用状态最后写入的两阶段写。
land_ordinal 在 (change_index, land_ordinal) 内唯一。值 0..63 在完整的 所属 Change 身份之下渲染为 L-01..L-64。服务端 Land 的 patchset_index 是那次尝试所接受的不可变 Patchset;它不会让 Land 身份变成 Patchset 作用域的。因此一个 Change 可以同时保留针对某个 Patchset 的 L-01 和针对之后某个 Patchset 的 L-02。 ChangeLandIndexRecord 负责下一个 ordinal 的分配,它的 latest/previous 链接 遵循 Change 作用域的 Land ordinal。TaskLandIndexRecord 和 previous_task_land_index_plus1 只保留物理上的 Task 清单。
Land 模式存放在 land_meta 的位 5 到位 6。源的 direct、merge 和 ff-only 映射到下面的固定编码;未知模式失败拒绝。源的结果对象不会变成 payload。目标 Line 身份以及 pre-land/landed 的 Snapshot 值来自固定的 Land 记录;被阻塞结果的 blocker_class 映射到 failure_kind。BASE_STALE 和 POLICY_BLOCKED 分别映射到 base_stale 和 policy_blocked。任何无法从该 固定记录精确推导出来的、无法识别的 blocker 或结果值,都失败拒绝。
原生 v0 的 Land 提交要求 submitted_at_s 非零。仅在离线遗留转换中,一个已 land 的 Change,如果其源权威记录了确切的 land 结果却从未记录提交时间,可以把 它第一个也是唯一一个成功的 Land 物化为:land_ordinal = 0、两个 previous-Land 链接均为零、submitted_at_s = 0,以及 updated_at_s 等于被保留的源 landed_at。零表示未知的历史提交时间;转换器不得用 Change 的创建/更新时间或 转换时间来替代。当目标 Line、landed Snapshot、必要时的被接受 Patchset,或者 land 时间缺失或有歧义时,本规则不允许重建;原生 v0 写入方也绝不能创建提交时间 为零的新 Land。
内联 Land 目标 Line 权威#
所属 Land 记录的 target_line_index_plus1 是必需的,引用提交 Land 时选定的那 条确切的目标 LineRecord。它属于固定 schema,不是 payload。Land 绝不重复存放 目标 Line 名称或文本形式的 Line ID。
存在时,pre_land_target_snapshot_index_plus1 引用目标 Line 更新之前被观测为 其 head 的那个确切的存活 Snapshot,而 landed_snapshot_index_plus1 引用被装入 为该 Line 新 head 的那个确切的存活 Snapshot。被引用 Snapshot 的 line_index_plus1 仍然是它不可变的创作 Line 身份,可能与 target_line_index_plus1 不同;Land 绝不给它重新贴标签,也不把这两个 Line 索引拿来做相等比较。已 land 的 Change 的公开 target_line 和 landed_at 值, 由它成功的那次 Land 的目标 Line 和 updated_at_s 推导;排队中、运行中、被 阻塞、失败、已取消和更新中的 Land 尝试,即使两个 Snapshot 都还不存在,也保留 它们的目标 Line。
被接纳的遗留服务端格式可能为一个 Change 保留不止一次成功的 Land,也可能在一次 成功之后追加被阻塞、失败、已取消或更新中的尝试。转换保留每一行 Land 及其确切 的被接受 Patchset。只要有任何一次存活的 Land 成功过,无论重复的源 Change 状态 如何,Change 的生命周期都归一化为 landed。land_ordinal 最大的那次成功 Land 提供公开的目标 Line 和 land 时间;之后未成功的尝试仍然是不可变的历史,不会 撤销已完成的 land。在那次成功之后创建的 selected/current Patchset,仍然是独立 的可变 Change 权威,不必等于被接受的 Patchset。声称已 land 却没有可证明的成功 Land 的源 Change,只能遵循下文明确的重建规则或点名的省略规则;它绝不会被隐式 地补上一个伪造的 Land。
当存在确切的 Land 权威时,遗留 Change 的 landed_at 是一份非权威的重复投影。 转换会把提供的值作为时间戳来校验,但不要求它等于任何 Land 时间,也不存储它; 权威的公开时间是最近一次成功 Land 的 updated_at_s。同样地,提交之后,Change 的可变 selected-Patchset 指针与某次历史 Land 确切的被接受 Patchset 是各自独立 的权威。转换保留两者,并不要求它们保持相等。
提交 Land 时通过精确的 payload 比较来解析归一化的 Line 名称,并追加和 fsync 一条完整的 Land 提交记录,其中包含必需的目标 Line 引用。对于已提交的 Land, 恢复流程会拒绝:目标 Line 为零或缺失、被引用的 Snapshot 缺失或已打墓碑,或者 状态与 Snapshot 存在与否不一致。恢复流程不会把被引用 Snapshot 的创作 Line 与 Land 的目标 Line 做比较。
紧邻的上一版 layout-1 表示,即 32 字节的 Local 或 36 字节的 Server Land 记录, 加上按 ordinal 对齐的 4 字节 land_target_line.bin,只能作为离线转换输入。 转换要求头部匹配且记录数完全一致,把每个确切的目标 Line 值追加到未改动的 Land 前缀之后,校验每一条加宽后的记录,省略退役的旁路文件,并在输入为零、缺失、 多余、错位或有歧义时失败拒绝。源文件不被修改。
更早的、旁路文件出现之前、没有 land_target_line.bin 的 layout-1 generation, 同样只能作为离线转换输入。转换把确切的源 target_line 解析为权威本地的 line_index,保留每一处确切存储的存活 Snapshot 引用,不比较也不重写它的创作 Line;如果在物化完整的加宽 Land 记录之前,目标或某个被引用的 Snapshot 不可得 或有歧义,则失败拒绝。