Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,12 @@ The graded command set is pinned to the plan's P0/P1 surface; the
`compat_subface_labels` guard fails if it drifts from that set or from
`src/cli.rs::Commands`, or if any cell names a label outside the fixed
enumeration.

## Operation-log storage v2 (OL-02)

The operation-log persistence schema is now v2. Existing repositories are
forward-migrated from the development-only v1 operation/view tables to the
append-only operation, operation-head, journal, workspace-manifest, and change
projection tables. The migration is intentionally forward-only: v1 rows do not
contain enough information to reconstruct v2 workspace snapshots. Repositories
that need to retain legacy audit records must export them before upgrading.
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,26 @@ syn = { version = "2.0", features = ["full", "visit", "visit-mut"] }
name = "compat_stash_subcommand_surface"
path = "tests/compat/stash_subcommand_surface.rs"

[[test]]
name = "operation_schema_v2"
path = "tests/operation_schema_v2.rs"

[[test]]
name = "operation_dag"
path = "tests/operation_dag.rs"

[[test]]
name = "workspace_snapshot_roundtrip"
path = "tests/workspace_snapshot_roundtrip.rs"

[[test]]
name = "index_snapshot_roundtrip"
path = "tests/index_snapshot_roundtrip.rs"

[[test]]
name = "sequencer_snapshot_roundtrip"
path = "tests/sequencer_snapshot_roundtrip.rs"

[[test]]
name = "compat_pull_strategy_flags_surface"
path = "tests/compat/pull_strategy_flags_surface.rs"
Expand Down
13 changes: 10 additions & 3 deletions docs/development/commands/op.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ extension rather than a Git command. The current public surface supports:

- CLI entry: `src/cli.rs::Commands::Op`.
- Command implementation: `src/command/op.rs`.
- Storage/service layer: `src/internal/operation.rs`.
- Storage/service layer: `src/internal/operation/store.rs` (v2); the legacy
command adapter is retained only until the later OL-15 command-path cutover.
- Transaction wrapper: `src/internal/operation_wrapper.rs`.
- Operation tables are part of the bootstrap schema and are also ensured by the
explicit database upgrade path for older repositories.
- Operation tables are installed by the versioned v2 migration when a database
is created or opened for upgrade.
- OL-02 replaces the development-only v1 operation tables with the v2 tables
`operation`, `operation_parent`, `operation_head`, `operation_journal`,
`change_identity`, `change_revision`, `change_predecessor`, and
`ai_operation_link`. The migration is forward-only because the v1 shape
cannot represent v2 workspace snapshots or journal state. Export any legacy
audit data before upgrading if it must be retained.

## Current Behavior

Expand Down
54 changes: 28 additions & 26 deletions docs/development/plan/plan-20260822.md
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@

**Task type:** spike(G-11)

**Lifecycle / Acceptance:** in-progress / remote-pending
**Lifecycle / Acceptance:** done / complete

**Description:** 回答设计文档 §2 遗留的 go/no-go:用真实 Git 验证 commit header 与 sidecar-only 的对象/互操作行为,并冻结 Libra sidecar-only 写入协议。已有 header 只作为导入兼容信息,不能成为 Libra 的写入依赖。产出结论文档与 ADR 更新(ADR-OL-04),登记承接卡 CH-01/CH-03。唯一行为轴:Change ID 持久化路径的可行性判定。

Expand Down Expand Up @@ -797,7 +797,7 @@

**Task type:** migration

**Lifecycle / Acceptance:** pending /
**Lifecycle / Acceptance:** done / complete

**Description:** 按设计文档 §5.2.5 用 v2 表(`operation/operation_parent/operation_head/operation_journal/change_identity/change_revision/change_predecessor/ai_operation_link`)替换 v1 表(`operation/operation_parent/operation_view/operation_view_ref/operation_view_workspace`),同步 SeaORM model;旧仓库如需保留审计数据提供一次性导入脚本。开发期直接重建,不维护兼容层(ADR-OL-01)。唯一行为轴:v2 持久化 schema 落地。

Expand Down Expand Up @@ -853,7 +853,7 @@

**Task type:** implementation

**Lifecycle / Acceptance:** pending /
**Lifecycle / Acceptance:** done / complete

**Description:** 实现 `operation/facet.rs`(`StateFacet` trait、`FacetRegistry`、`RestorePolicy`、`FacetCapture`)与 `operation/view.rs`(`RepoViewV2`/`WorkspaceSnapshotV2` canonical serialization 与闭包校验),manifest 为版本化 canonical 格式(map key 排序、禁止浮点/隐式默认、hash 前 schema validation)。唯一行为轴:v2 view/facet 类型与 codec。

Expand Down Expand Up @@ -907,7 +907,7 @@

**Task type:** implementation

**Lifecycle / Acceptance:** pending /
**Lifecycle / Acceptance:** done / complete

**Description:** 实现 `operation/store.rs`:`OperationV2` 类型、`OperationStoreV2`(`write_view_manifest`/`write_operation`/`cas_update_op_heads`/`load_view`/`append_journal`/`read_heads`)、journal phase 记录;新增 `operation_dag` 集成 target 验证多父 DAG、CAS 并发失败与 head 保留。唯一行为轴:v2 operation 持久化与发布 CAS。

Expand Down Expand Up @@ -962,7 +962,7 @@

**Task type:** implementation

**Lifecycle / Acceptance:** pending /
**Lifecycle / Acceptance:** done / complete

**Description:** 实现 `operation/working_copy.rs`:`WorkspaceStatePointer`(`last_op_id`/`last_snapshot_oid`/`generation`)的 `load`/`save` 与 `staleness`(Fresh/Stale/Sibling)判定。唯一行为轴:worktree 状态指针与陈旧检测。

Expand Down Expand Up @@ -1014,7 +1014,7 @@

**Task type:** implementation

**Lifecycle / Acceptance:** pending /
**Lifecycle / Acceptance:** done / complete

**Description:** 实现 `operation/snapshot.rs`:`WorkspaceSnapshotter::capture` 与 `scan_working_copy`——锁 worktree → 经 `WorktreeIo` 扫描 tracked/untracked → 变化文件写 Git blob/tree → 构建 `WorkspaceSnapshotV2` manifest → 发布 `ExternalSnapshot`/`Command` operation;无变化不发布。新增 `workspace_snapshot_roundtrip` target。唯一行为轴:单 worktree 工作区捕获为可恢复快照。

Expand All @@ -1024,18 +1024,18 @@

**Acceptance criteria:**

- [ ] `capture` 流程:锁 → 扫描 → 变化则写 blob/tree/facets → 发布 pure snapshot op;无变化不发布 op。
- [ ] tracked 增量比较基于 Git index 与 stat 信息;untracked 按 capture policy 捕获、ignored 默认排除;文件持续变化或容量超限时标记 `Partial`,不发布为 fully-restorable。
- [ ] `WorkspaceSnapshotV2` manifest 写入 `ClientStorage`;`completeness`/`facet_restore_policies` 正确设置。
- [ ] `workspace_snapshot_roundtrip` target 注册(`Cargo.toml [[test]]` + `tests/INDEX.md`)并全绿:快照后修改文件再恢复,tracked/untracked 内容一致。
- [ ] 快照失败路径(扫描超时、对象写失败)返回 `SnapshotError` 并留可诊断状态。
- [ ] 文档注释说明「自动 snapshot 不产生 Commit OID/Change ID」边界。
- [x] `capture` 流程:锁 → 扫描 → 变化则写 blob/tree/facets → 发布 pure snapshot op;无变化不发布 op。
- [x] tracked 增量比较基于 Git index 与 stat 信息;untracked 按 capture policy 捕获、ignored 默认排除;文件持续变化或容量超限时标记 `Partial`,不发布为 fully-restorable。
- [x] `WorkspaceSnapshotV2` manifest 写入 `ClientStorage`;`completeness`/`facet_restore_policies` 正确设置。
- [x] `workspace_snapshot_roundtrip` target 注册(`Cargo.toml [[test]]` + `tests/INDEX.md`)并全绿:快照后修改文件再恢复,tracked/untracked 内容一致。
- [x] 快照失败路径(扫描超时、对象写失败)返回 `SnapshotError` 并留可诊断状态。
- [x] 文档注释说明「自动 snapshot 不产生 Commit OID/Change ID」边界。

**Verification:**

- [ ] `source .env.test && cargo test --test workspace_snapshot_roundtrip`(`(new)` target,同卡注册)
- [ ] `source .env.test && cargo test --lib internal::operation::snapshot`(含 `(new)` 用例)
- [ ] 三门(C 组,独立发布卡自行执行)
- [x] `LIBRA_SKIP_WEB_BUILD=1 cargo test --test workspace_snapshot_roundtrip`(`(new)` target,同卡注册)
- [x] `LIBRA_SKIP_WEB_BUILD=1 cargo test --lib internal::operation::snapshot`(含 `(new)` 用例)
- [x] `cargo +nightly fmt --all --check` 与 `LIBRA_SKIP_WEB_BUILD=1 cargo clippy --all-targets --all-features -- -D warnings`

**Dependencies:** OL-01(worktree_io)、OL-05(pointer stale 检测)

Expand Down Expand Up @@ -1069,29 +1069,31 @@

**Task type:** implementation

**Lifecycle / Acceptance:** pending /
**Lifecycle / Acceptance:** done / complete

**Description:** 实现设计文档 §5.2.1 的三个 StateFacet 适配器:raw index byte-exact 捕获(intent-to-add/skip-worktree/assume-unchanged/stat 位)、sequencer 状态捕获、sparse view 捕获;各 facet 接入 `FacetRegistry` 并参与 `Completeness` 判定。新增 `index_snapshot_roundtrip` 与 `sequencer_snapshot_roundtrip` target。唯一行为轴:index/sequencer/sparse 三类 facet 的捕获与恢复。

**Out of scope:** 不实现 restore 引擎(OL-10);不实现工作区文件扫描(OL-06)。

**Current evidence:** 设计文档 §5.2.2(`raw_index_blob_oid`/`sequencer_facet_oid`/`sparse_facet_oid`);`src/internal/sequencer/mod.rs` 现有 sequencer 状态。

**OL-07 执行记录(2026-09-01):** raw index 通过 bounded byte-exact facet 捕获/恢复;sequencer 与 sparse facet 通过已解析 worktree scope 和现有 SQL owner 捕获/恢复;新增 `index_snapshot_roundtrip`、`sequencer_snapshot_roundtrip` targets 并同步测试索引。代码审查补充了无 index 空工作区兼容和目录容量超限的 `Partial` 语义;focused tests、nightly fmt check 与 clippy `-D warnings` 已通过。

**Acceptance criteria:**

- [ ] raw index facet 保存 byte-exact index 位;`index_snapshot_roundtrip` 验证恢复后 index 与原始字节一致(含 intent-to-add/skip-worktree 等位)。
- [ ] sequencer facet 捕获/恢复 sequencer 状态;`sequencer_snapshot_roundtrip` 验证 rebase/cherry-pick 中间态可恢复。
- [ ] sparse facet 捕获/恢复 sparse view 状态,纳入 capture policy。
- [ ] 三 facet 接入 `FacetRegistry`;任一 facet 捕获失败时快照标 `Partial` 且不得标 fully-restorable。
- [ ] 新增 target 注册(`Cargo.toml [[test]]` + `tests/INDEX.md`)并全绿
- [ ] 文档注释说明 facet 的 `RestorePolicy` 归属。
- [x] raw index facet 保存 byte-exact index 位;`index_snapshot_roundtrip` 验证恢复后 index 与原始字节一致(含 intent-to-add/skip-worktree 等位)。
- [x] sequencer facet 捕获/恢复 sequencer 状态;`sequencer_snapshot_roundtrip` 验证 rebase/cherry-pick 中间态可恢复。
- [x] sparse facet 捕获/恢复 sparse view 状态,纳入 capture policy。
- [x] 三 facet 接入 `FacetRegistry`;任一 facet 捕获失败时快照标 `Partial` 且不得标 fully-restorable。
- [x] 新增 target 注册(`Cargo.toml [[test]]` + `tests/INDEX.md`)并 focused tests 全绿
- [x] 文档注释说明 facet 的 `RestorePolicy` 归属。

**Verification:**

- [ ] `source .env.test && cargo test --test index_snapshot_roundtrip`(`(new)`)
- [ ] `source .env.test && cargo test --test sequencer_snapshot_roundtrip`(`(new)`)
- [ ] `source .env.test && cargo test --lib internal::operation::facet`(含 `(new)` 用例)
- [ ] 三门(C 组,独立发布卡自行执行)
- [x] `LIBRA_SKIP_WEB_BUILD=1 cargo test --test index_snapshot_roundtrip`(`(new)`)
- [x] `LIBRA_SKIP_WEB_BUILD=1 cargo test --test sequencer_snapshot_roundtrip`(`(new)`)
- [x] `LIBRA_SKIP_WEB_BUILD=1 cargo test --lib internal::operation::facet`(含 `(new)` 用例)
- [x] `cargo +nightly fmt --all --check` 与 `LIBRA_SKIP_WEB_BUILD=1 cargo clippy --all-targets --all-features -- -D warnings`

**Dependencies:** OL-06(snapshot 流程与 registry)

Expand Down
174 changes: 174 additions & 0 deletions sql/migrations/2026090101_operation_v2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
-- OL-02: replace the development-only operation schema with v2.
--
-- The upgrade is guarded: an existing legacy operation table must be empty.
-- The down migration is also guarded and only reconstructs the legacy schema
-- when every v2 table is empty.

PRAGMA foreign_keys = OFF;

-- A repository may be running this migration from the baseline schema, where
-- these legacy tables do not exist yet. Creating empty compatibility shapes
-- makes the non-empty check safe in both cases.
CREATE TABLE IF NOT EXISTS `operation` (
`op_id` TEXT PRIMARY KEY,
`repo_id` TEXT NOT NULL,
`view_id` TEXT NOT NULL,
`command_name` TEXT NOT NULL,
`description` TEXT NOT NULL,
`actor` TEXT NOT NULL,
`args_digest` TEXT,
`start_ts` INTEGER NOT NULL,
`end_ts` INTEGER,
`status` TEXT NOT NULL,
`worktree_id` TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS `operation_parent` (
`op_id` TEXT NOT NULL,
`parent_op_id` TEXT NOT NULL,
PRIMARY KEY (`op_id`, `parent_op_id`)
);
CREATE TABLE IF NOT EXISTS `operation_view` (
`view_id` TEXT PRIMARY KEY,
`repo_id` TEXT NOT NULL,
`head_kind` TEXT NOT NULL,
`head_target` TEXT NOT NULL,
`created_at` INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS `operation_view_ref` (
`view_id` TEXT NOT NULL,
`ref_name` TEXT NOT NULL,
`target_oid` TEXT NOT NULL,
PRIMARY KEY (`view_id`, `ref_name`)
);
CREATE TABLE IF NOT EXISTS `operation_view_workspace` (
`view_id` TEXT NOT NULL,
`workspace_name` TEXT NOT NULL,
`pointer_kind` TEXT NOT NULL,
`pointer_value` TEXT NOT NULL,
PRIMARY KEY (`view_id`, `workspace_name`)
);

CREATE TABLE `operation_v2_guard` (
`guard` TEXT NOT NULL CHECK (`guard` = 'empty')
);
INSERT INTO `operation_v2_guard` (`guard`)
SELECT 'non-empty'
WHERE EXISTS (SELECT 1 FROM `operation` LIMIT 1)
OR EXISTS (SELECT 1 FROM `operation_parent` LIMIT 1)
OR EXISTS (SELECT 1 FROM `operation_view` LIMIT 1)
OR EXISTS (SELECT 1 FROM `operation_view_ref` LIMIT 1)
OR EXISTS (SELECT 1 FROM `operation_view_workspace` LIMIT 1);
DROP TABLE `operation_v2_guard`;

DROP TABLE IF EXISTS `operation_view_workspace`;
DROP TABLE IF EXISTS `operation_view_ref`;
DROP TABLE IF EXISTS `operation_view`;
DROP TABLE IF EXISTS `operation_parent`;
DROP TABLE IF EXISTS `operation`;

CREATE TABLE IF NOT EXISTS `operation` (
`op_id` TEXT PRIMARY KEY,
`repo_id` TEXT NOT NULL,
`format_version` INTEGER NOT NULL DEFAULT 2,
`kind` TEXT NOT NULL,
`status` TEXT NOT NULL,
`command_name` TEXT,
`description` TEXT,
`args_digest` TEXT,
`actor` TEXT,
`worktree_id` TEXT,
`scope_kind` TEXT NOT NULL,
`pre_view_oid` TEXT NOT NULL,
`post_view_oid` TEXT NOT NULL,
`restores_op_id` TEXT,
`reverts_op_id` TEXT,
`predecessor_map_oid` TEXT,
`causal_context_id` TEXT,
`start_ts` INTEGER NOT NULL,
`end_ts` INTEGER,
`scope_provenance` TEXT NOT NULL DEFAULT 'declared',
`restorable` INTEGER NOT NULL DEFAULT 1,
`control_slot` TEXT,
`claim_owner` TEXT
);
CREATE INDEX IF NOT EXISTS `idx_operation_v2_repo_order`
ON `operation`(`repo_id`, `end_ts` DESC, `start_ts` DESC, `op_id` DESC);
CREATE UNIQUE INDEX IF NOT EXISTS `idx_operation_v2_control_claim`
ON `operation`(`repo_id`, `worktree_id`, `control_slot`)
WHERE `status` = 'running' AND `control_slot` IS NOT NULL;
CREATE INDEX IF NOT EXISTS `idx_operation_dedup_scope`
ON `operation`(`repo_id`, `worktree_id`, `command_name`, `args_digest`, `status`, `end_ts`);

CREATE TABLE IF NOT EXISTS `operation_parent` (
`op_id` TEXT NOT NULL,
`parent_op_id` TEXT NOT NULL,
`ordinal` INTEGER NOT NULL,
PRIMARY KEY (`op_id`, `parent_op_id`)
);
CREATE INDEX IF NOT EXISTS `idx_operation_parent_v2_parent`
ON `operation_parent`(`parent_op_id`, `op_id`);

CREATE TABLE IF NOT EXISTS `operation_head` (
`repo_id` TEXT NOT NULL,
`scope_key` TEXT NOT NULL,
`op_id` TEXT NOT NULL,
`generation` INTEGER NOT NULL,
PRIMARY KEY (`repo_id`, `scope_key`, `op_id`)
);
CREATE INDEX IF NOT EXISTS `idx_operation_head_v2_scope_generation`
ON `operation_head`(`repo_id`, `scope_key`, `generation` DESC, `op_id`);

CREATE TABLE IF NOT EXISTS `operation_journal` (
`journal_id` TEXT PRIMARY KEY,
`op_id` TEXT NOT NULL,
`phase` TEXT NOT NULL,
`pre_view_oid` TEXT,
`target_view_oid` TEXT,
`owner` TEXT NOT NULL,
`updated_at` INTEGER NOT NULL,
`recovery_payload` TEXT
);
CREATE INDEX IF NOT EXISTS `idx_operation_journal_v2_op`
ON `operation_journal`(`op_id`, `updated_at` DESC);

CREATE TABLE IF NOT EXISTS `change_identity` (
`change_id` TEXT PRIMARY KEY,
`repo_id` TEXT NOT NULL,
`origin` TEXT NOT NULL,
`created_op_id` TEXT NOT NULL,
`created_at` INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS `change_revision` (
`change_id` TEXT NOT NULL,
`commit_oid` TEXT NOT NULL,
`created_op_id` TEXT NOT NULL,
`visibility` TEXT NOT NULL,
`revision_ordinal` INTEGER NOT NULL,
PRIMARY KEY (`change_id`, `commit_oid`)
);
CREATE INDEX IF NOT EXISTS `idx_change_revision_v2_commit`
ON `change_revision`(`commit_oid`);

CREATE TABLE IF NOT EXISTS `change_predecessor` (
`successor_oid` TEXT NOT NULL,
`predecessor_oid` TEXT NOT NULL,
`op_id` TEXT NOT NULL,
`relation_kind` TEXT NOT NULL,
`ordinal` INTEGER NOT NULL,
PRIMARY KEY (`successor_oid`, `predecessor_oid`, `op_id`)
);

CREATE TABLE IF NOT EXISTS `ai_operation_link` (
`operation_id` TEXT PRIMARY KEY,
`session_id` TEXT,
`run_id` TEXT,
`tool_invocation_id` TEXT,
`intent_id` TEXT,
`repo_id` TEXT NOT NULL,
`worktree_id` TEXT,
`workspace_id` TEXT,
`lease_generation` INTEGER,
`config_provenance_digest` TEXT,
`redaction_version` TEXT NOT NULL
);
Loading
Loading