Skip to content

Commit 47eae5e

Browse files
genednaclaude
andcommitted
feat(layer): worktree-scoped layer overlay registry — per-scope registrations/ownership, scope↔workdir binding, conditional purge, fail-closed legacy adopt (plan-20260714 C C.4.1.1 W1-adv slice 2) (v0.19.51)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cbuz56sffqh2u5SUyp7exG Signed-off-by: Eli Ma <eli@patch.sh>
1 parent 64790ff commit 47eae5e

24 files changed

Lines changed: 1198 additions & 151 deletions

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
### Changed
66

7+
- **The layer overlay registry is worktree-scoped (v0.19.51, plan-20260714
8+
Part C §C.4.1.1, W1 advisory slice 2)**: migration `2026072303` re-keys
9+
`layer` to UNIQUE(worktree_id, name) and `layer_path` to
10+
UNIQUE(worktree_id, path); every `LayerStore` method, `apply`/`unapply`,
11+
the `add` staging guard, and the sync ignore-exclusion snapshot carry the
12+
request's one resolved `WorktreeScope`. All `layer` subcommands now run in
13+
linked worktrees against their own registrations — the same layer name and
14+
destination can exist independently per worktree, and `worktree remove`
15+
purges the removed scope's rows when the directory is deleted too (a
16+
retained directory keeps its ownership rows so the still-materialized
17+
overlay files stay un-stageable). Layer ownership is not rebuildable, so
18+
legacy repository-global rows adopt to main only when no linked worktree
19+
exists; otherwise the migration FAILS CLOSED (CHECK guard) and asks for an
20+
explicit `layer unapply`/`layer remove` from the owning worktree. The down
21+
migration equally fails closed while linked-scope rows exist.
722
- **The dirty-set cache is worktree-scoped (v0.19.50, plan-20260714 Part C
823
§C.4.1.1, W1 advisory slice 1)**: migration `2026072302` re-keys
924
`working_dirty` to UNIQUE(worktree_id, path, kind) and

‎COMPATIBILITY.md‎

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

‎Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "libra"
3-
version = "0.19.50"
3+
version = "0.19.51"
44
edition = "2024"
55
license = "MIT"
66
description = "AI agent-native version control system with Git on-disk compatibility, SQLite-backed metadata, and tiered cloud storage"

‎docs/commands/layer.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ invariants:
3030
Precedence on a same-destination collision between two enabled layers:
3131
higher `priority` wins, ties broken by name (last-writer-wins in stack order).
3232

33+
**Per-worktree since W1** (plan-20260714 §C.4.1.1, migration `2026072303`):
34+
the registry and path ownership are scoped by `worktree_id` — the same layer
35+
name (and the same destination path) can exist independently in different
36+
worktrees, every subcommand acts only on the current worktree's rows, and
37+
removing a worktree purges its layer rows only when its directory is deleted
38+
too (a retained directory keeps them, so its overlay files stay
39+
un-stageable). Legacy repository-global rows are
40+
adopted by the main worktree only when no linked worktree exists; otherwise
41+
the schema migration fails closed and asks for an explicit
42+
`layer unapply`/`layer remove` from the owning worktree first.
43+
3344
## Examples
3445

3546
```bash

‎docs/commands/zh-CN/layer.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
两个已启用 layer 之间发生同一目标冲突时的优先级:更高 `priority` 获胜,平局按 name 打破(stack 顺序中的 last-writer-wins)。
1717

18+
**自 W1 起按 worktree 隔离**(plan-20260714 §C.4.1.1,迁移 `2026072303`):注册表与路径 ownership 以 `worktree_id` 为 scope——同名 layer(以及同一目标路径)可在不同 worktree 中独立存在,所有子命令只作用于当前 worktree 的行;仅当 worktree 目录随之删除时移除才清理其 layer 行(保留的目录继续持有 ownership 行,overlay 文件保持不可暂存)。旧的仓库全局行仅在不存在 linked worktree 时归属 main worktree;否则 schema 迁移 fail-closed,要求先在拥有者 worktree 显式执行 `layer unapply`/`layer remove`。
19+
1820
## 示例
1921

2022
```bash

‎docs/development/commands/layer.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,36 @@ invariants:
3030
Precedence on a same-destination collision between two enabled layers:
3131
higher `priority` wins, ties broken by name (last-writer-wins in stack order).
3232

33+
**Worktree scoping (W1, plan-20260714 §C.4.1.1, migration `2026072303`)**:
34+
`layer` is keyed by `(worktree_id, name)` and `layer_path` by
35+
`(worktree_id, path)`. Every `LayerStore` method, `apply`/`unapply`, and the
36+
`add` staging guard take the request's ONE resolved `WorktreeScope`
37+
explicitly. Scope↔workdir binding: `add` derives its scope from the CAPTURED
38+
working directory (`WorktreeScope::for_workdir`), so the hard staging guard
39+
always checks the ownership rows of the very tree it stages into — immune to
40+
a concurrent process-cwd switch; the mutation entry points `apply`/`unapply`
41+
re-derive the scope from their resolved workdir and FAIL CLOSED on a
42+
mismatch (`verify_scope_matches_workdir`). The sync ignore-exclusion
43+
snapshot stores `(scope_key, set)` and stays advisory — a stale consult can
44+
only misclassify `status`/`clean` cosmetically; the commit invariant is held
45+
by the DB-scoped staging guard, and NO destructive gate may consult the
46+
snapshot (the `worktree remove --delete-dir` dirty check reads the target
47+
scope's overlay set straight from the DB, fail-closed, and subtracts it
48+
explicitly from the unstaged side only). `worktree remove` purges the
49+
removed scope's layer rows ONLY when the directory is actually gone
50+
(`--delete-dir`, or already vanished), and `worktree prune` GCs each pruned
51+
scope's rows the same way: a RETAINED directory still operates as a
52+
repository, so its ownership rows survive to keep the still-materialized
53+
overlay files un-stageable (it cannot be re-registered while non-empty —
54+
`worktree add` refuses — so the rows guard it until the directory is
55+
cleared; orphaned rows are reclaimed by the W3 worktree doctor). Legacy
56+
migration is FAIL-CLOSED by a CHECK guard: rows adopt to main (`''`) only
57+
when no linked HEAD row exists in `reference`; otherwise the migration
58+
refuses and asks for explicit `layer unapply`/`layer remove` from the owning
59+
worktree (layer ownership is not rebuildable — clearing it would make
60+
overlay files committable). The down migration equally fails closed while
61+
any linked-scope row exists.
62+
3363
## Examples
3464

3565
```bash

‎docs/development/commands/worktree.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 命令实现目标
44

5-
`libra worktree` 的目标是管理同一仓库状态的附加工作目录。实现需要明确 Libra worktree 的隔离布局:多个 worktree 共享同一个 `.libra` SQLite 数据库、对象存储、branch/tag/remote refs 和配置,但每个 linked worktree 拥有自己真实的 `.libra` gitdir(本地目录,非 symlink),保存私有 HEAD、index 和 HEAD reflog,并记录 `commondir` 指针与稳定 `worktree_id`;sequencer 家族(cherry-pick/am/revert/merge/bisect/rebase)自 W1(v0.19.42)起在 linked worktree 以 per-worktree DB scope 运行,`bisect_state` 的 lazy DDL 已由正式迁移 `2026072301` 收编;dirty-set cache 自 W1(迁移 `2026072302`)起 per-worktree(`dirty`/`status --scan/--cached/--check-dirty` 在 linked 可用且互不干扰;service 的无 scope dirty-mark 请求在多 worktree 仓库拒绝);stash/layer/sparse-view 仍拒绝(各自 scope 化切片前)。由更早版本创建的 worktree 可能仍是旧共享 `.libra` symlink 布局。remove 默认保留磁盘目录,只有 `--delete-dir` 执行 Git 风格删除。
5+
`libra worktree` 的目标是管理同一仓库状态的附加工作目录。实现需要明确 Libra worktree 的隔离布局:多个 worktree 共享同一个 `.libra` SQLite 数据库、对象存储、branch/tag/remote refs 和配置,但每个 linked worktree 拥有自己真实的 `.libra` gitdir(本地目录,非 symlink),保存私有 HEAD、index 和 HEAD reflog,并记录 `commondir` 指针与稳定 `worktree_id`;sequencer 家族(cherry-pick/am/revert/merge/bisect/rebase)自 W1(v0.19.42)起在 linked worktree 以 per-worktree DB scope 运行,`bisect_state` 的 lazy DDL 已由正式迁移 `2026072301` 收编;dirty-set cache 自 W1(迁移 `2026072302`)起 per-worktree(`dirty`/`status --scan/--cached/--check-dirty` 在 linked 可用且互不干扰;service 的无 scope dirty-mark 请求在多 worktree 仓库拒绝);layer 注册表自 W1(迁移 `2026072303`)起 per-worktree(同名 layer/同路径可在不同 worktree 独立存在;仅当目录随 `--delete-dir` 删除或已消失时 remove/prune 才清理该 scope 的 layer 行——保留的目录继续持有 ownership 行、overlay 文件保持不可暂存;legacy 全局行仅在无 linked worktree 时归 main、否则迁移 fail-closed);stash/sparse-view 仍拒绝(各自 scope 化切片前)。由更早版本创建的 worktree 可能仍是旧共享 `.libra` symlink 布局。remove 默认保留磁盘目录,只有 `--delete-dir` 执行 Git 风格删除。
66

77
## 对比 Git 与兼容性
88

‎install.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ INSTALL_DIR="${LIBRA_INSTALL_DIR:-$LIBRA_HOME/bin}"
1818
# user opts in with LIBRA_ALLOW_FALLBACK=1. Default behaviour is fail-fast so
1919
# offline installs cannot silently regress to a stale version. Bump this on
2020
# every release so the opt-in fallback remains useful.
21-
DEFAULT_VERSION="v0.19.50"
21+
DEFAULT_VERSION="v0.19.51"
2222

2323
# ─── theme (Dusk) ────────────────────────────────────────────────────────────
2424
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ -z "${LIBRA_NO_TUI:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
-- 2026072303_layer_worktree_scope
2+
--
3+
-- plan-20260714 Part C W1 (§C.4.1.1): scope the layer overlay registry per
4+
-- worktree. `layer` gains `worktree_id` with UNIQUE(worktree_id, name) and
5+
-- `layer_path` gains `worktree_id` with UNIQUE(worktree_id, path) — the same
6+
-- layer name and the same materialized destination may exist independently
7+
-- in different worktrees.
8+
--
9+
-- Legacy-row rule (§C.4.1.1): layer registration/ownership is NOT rebuildable
10+
-- advisory state — clearing it would make materialized overlay files
11+
-- committable (breaking never-enters-commit), and guessing an owner is
12+
-- forbidden. Legacy global rows are adopted to the MAIN scope ('') ONLY when
13+
-- the guard below proves no linked worktree exists (no linked HEAD row in
14+
-- `reference`): layer mutations have been main-only since the W0 transition
15+
-- guard, so absent linked worktrees the rows can only belong to main. If
16+
-- linked worktrees coexist with legacy rows, this migration FAILS CLOSED —
17+
-- run `libra layer unapply` / `libra layer remove <name>` from the owning
18+
-- worktree on the previous binary (or main), then upgrade.
19+
20+
-- Defensive: guarantee the legacy shapes exist so the rebuild below is
21+
-- well-defined even on a database that somehow skipped 2026070501.
22+
CREATE TABLE IF NOT EXISTS `layer` (
23+
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
24+
`name` TEXT NOT NULL UNIQUE,
25+
`source` TEXT NOT NULL,
26+
`priority` INTEGER NOT NULL DEFAULT 0,
27+
`enabled` INTEGER NOT NULL DEFAULT 1,
28+
`created_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
29+
`updated_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
30+
);
31+
CREATE TABLE IF NOT EXISTS `layer_path` (
32+
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
33+
`layer_name` TEXT NOT NULL,
34+
`path` TEXT NOT NULL UNIQUE,
35+
`content_hash` TEXT NOT NULL,
36+
`materialized_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
37+
);
38+
39+
-- Fail-closed guard: legacy global layer rows + linked worktree evidence
40+
-- (a linked HEAD row) is ambiguous ownership — refuse the whole migration
41+
-- (CHECK violation rolls back this transaction, including the version claim).
42+
CREATE TABLE `layer__legacy_rows_need_explicit_adopt_2026072303` (
43+
`blocked` INTEGER NOT NULL CHECK (`blocked` = 0)
44+
);
45+
INSERT INTO `layer__legacy_rows_need_explicit_adopt_2026072303` (`blocked`)
46+
SELECT CASE
47+
WHEN ((SELECT COUNT(*) FROM `layer`) + (SELECT COUNT(*) FROM `layer_path`)) > 0
48+
AND EXISTS (SELECT 1 FROM `reference`
49+
WHERE `worktree_id` IS NOT NULL AND `worktree_id` <> '')
50+
THEN 1 ELSE 0 END;
51+
DROP TABLE `layer__legacy_rows_need_explicit_adopt_2026072303`;
52+
53+
ALTER TABLE `layer` RENAME TO `layer__old_2026072303`;
54+
CREATE TABLE `layer` (
55+
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
56+
`worktree_id` TEXT NOT NULL DEFAULT '',
57+
`name` TEXT NOT NULL,
58+
`source` TEXT NOT NULL,
59+
`priority` INTEGER NOT NULL DEFAULT 0,
60+
`enabled` INTEGER NOT NULL DEFAULT 1,
61+
`created_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
62+
`updated_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
63+
UNIQUE(`worktree_id`, `name`)
64+
);
65+
INSERT INTO `layer`
66+
(`id`, `worktree_id`, `name`, `source`, `priority`, `enabled`,
67+
`created_at`, `updated_at`)
68+
SELECT `id`, '', `name`, `source`, `priority`, `enabled`,
69+
`created_at`, `updated_at`
70+
FROM `layer__old_2026072303`;
71+
DROP TABLE `layer__old_2026072303`;
72+
73+
ALTER TABLE `layer_path` RENAME TO `layer_path__old_2026072303`;
74+
CREATE TABLE `layer_path` (
75+
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
76+
`worktree_id` TEXT NOT NULL DEFAULT '',
77+
`layer_name` TEXT NOT NULL,
78+
`path` TEXT NOT NULL,
79+
`content_hash` TEXT NOT NULL,
80+
`materialized_at` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
81+
UNIQUE(`worktree_id`, `path`)
82+
);
83+
INSERT INTO `layer_path`
84+
(`id`, `worktree_id`, `layer_name`, `path`, `content_hash`,
85+
`materialized_at`)
86+
SELECT `id`, '', `layer_name`, `path`, `content_hash`, `materialized_at`
87+
FROM `layer_path__old_2026072303`;
88+
DROP TABLE `layer_path__old_2026072303`;

0 commit comments

Comments
 (0)