# 4-33 VSCode Explorer DnD Readonly Conflict v1

> 状态：done
> 创建时间：2026-05-15
> 所属主线：04-tree-domain / VSCode Explorer 文件树与垃圾箱闭环
> 来源：`design/10-review/07-vscode-explorer-filetree-trash-gap-review.md` 阶段 7 DnD 剩余项

## 1. 目标

补齐阶段 7 中尚未闭合的 DnD 剩余项：主 Sidebar Convex filetree 的 readonly 禁止与命名冲突确认。

当前已完成的 DnD 证据是：

- 自拖自身拒绝。
- 父拖到子节点 / 后代拒绝。
- Alt / Ctrl / Meta copy modifier 被识别为 copy。
- 3000 主入口 `/api/tree/filetree/drop-preflight` 已返回 Rust bridge `tree.filetree.drop.preflight` plan。

尚未完成的是：

- 主 Sidebar Convex filetree 下的 readonly 目标 / readonly source 禁止。
- 主 Sidebar Convex filetree 下的重名目标冲突确认，而不是直接 move/copy。

## 2. 当前代码事实

- `bridge-runtime` 的 `FileTreeDropPreflightCommandPayload` 当前只有：
  - `copy`
  - `targetDocumentId / targetRowId / focusedRowId / activeDocumentId`
  - `rowIds`
  - `rows`
  - `documentParents`
- `FileTreeDropPreflightRow` 当前只有：
  - `rowId / rowKind / documentId / assetId / assetDocumentId / assetType / storagePath`
- 现有 `build_filetree_drop_plan` 能判断：
  - 目标页面。
  - doc / asset transfer plan。
  - 父拖子 / 自拖非法 move。
  - copy / move action。
- 现有 payload 不能判断：
  - 目标是否 readonly。
  - source 是否 readonly。
  - target 下是否已有同名页面 / 附件。
  - 冲突应 rename、replace、skip 还是 cancel。

## 3. 设计增量

### 3.1 Drop preflight payload 增量

为 `tree.filetree.drop.preflight` payload 增加以下可选字段：

```json
{
  "sourceCapabilities": ["read", "write", "move", "copy"],
  "targetCapabilities": ["read", "write", "drop"],
  "rows": [
    {
      "rowId": "doc:...",
      "title": "页面标题",
      "operationProfile": "convex_workspace"
    }
  ],
  "targetChildren": [
    {
      "rowKind": "doc",
      "documentId": "...",
      "assetId": null,
      "title": "页面标题"
    }
  ],
  "conflictPolicy": "prompt"
}
```

说明：

- `sourceCapabilities` / `targetCapabilities` 用于表达当前 workspace source 与目标 row 的写权限，不在 UI 层猜测。
- `operationProfile` 允许 local readonly、remote readonly、shared readonly 等来源进入同一 preflight，而不是散落在前端。
- `title` 与 `targetChildren` 用于做同级重名检测。
- `conflictPolicy` 默认 `prompt`，后续可扩展 `rename` / `skip` / `replace`，但本阶段只要求“发现冲突并要求确认”。

### 3.2 Drop preflight plan 增量

为 `FileTreeDropPlan` 增加：

```json
{
  "allowed": true,
  "blockedReason": null,
  "requiresConfirmation": false,
  "conflicts": []
}
```

readonly 场景：

- `allowed=false`
- `blockedReason="readonly_target"` 或 `readonly_source"`
- 不返回可执行 transfer plan，避免 UI 误执行。

命名冲突场景：

- `allowed=true`
- `requiresConfirmation=true`
- `conflicts` 列出冲突对象、目标父级、建议策略。
- UI 必须展示确认，不允许静默执行。

### 3.3 3000 主壳行为

- 内部 drop 触发前继续调用 `/api/tree/filetree/drop-preflight`。
- 若 `allowed=false`，显示阻塞原因，不发 `/api/tree/commands` 或 `/api/media/batch`。
- 若 `requiresConfirmation=true`，显示冲突确认；用户取消时不执行。
- 用户确认后，按 plan 执行 move/copy。

## 4. 验收标准

- readonly target：
  - 真实或构造的 readonly target drop preflight 返回 400 或 `allowed=false`。
  - 浏览器不发执行请求。
  - UI 有“只读”类可解释反馈。
- conflict：
  - 目标父页面已有同名页面或同名附件时，preflight 返回 `requiresConfirmation=true` 与 `conflicts`。
  - 用户取消确认后不执行 move/copy。
  - 用户确认后执行 move/copy，并在结果中记录冲突处理策略。
- no false positive：
  - 非冲突移动不弹冲突确认。
  - copy modifier 仍保留 `copy=true`。
  - 父拖子 / 自拖仍被拒绝。

## 5. 建议执行顺序

1. 扩展 `bridge-runtime` payload / plan 类型，补 readonly 与 conflict 单测。
2. 扩展 3000 `/api/tree/filetree/drop-preflight` route 透传新增字段。
3. 扩展 3000 Rust SSR 主壳内部 drop 逻辑，生成 row title / target children / capability payload。
4. 新增 `scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js`：
   - 一次性用户 / workspace。
   - 构造同名目标，验证 conflict confirmation cancel / confirm。
   - 构造 readonly payload 或 readonly source fixture，验证 blocked。
5. 更新 `design/10-review/07-vscode-explorer-filetree-trash-gap-review.md`，仅在 `task431` 通过后勾选阶段 7 DnD 总项。

## 6. 当前状态

截至 2026-05-15，本文件已完成阶段 7 DnD readonly / conflict 的执行与验证：

- `bridge-runtime` 的 `tree.filetree.drop.preflight` 已支持 source / target capability、target children、conflict policy、`requiresConfirmation` 与 `conflicts`。
- readonly target / source 在 preflight 阶段被拒绝；同名目标返回确认计划；copy modifier 不被 readonly move 规则误拒绝。
- 主 Sidebar 内部 drop 与 cut-paste move 路径在 `requiresConfirmation` 时确认，取消后不执行后续 move/copy。
- 验证命令：
  - `cargo test -p bridge-runtime tree_filetree_drop_preflight -- --nocapture`
  - `pnpm test src/lib/file-tree/shell.test.ts src/lib/file-tree/resource-command-client.test.ts src/components/sidebar/sidebar-paste-preflight-source.test.ts src/components/sidebar/sidebar-dnd-source.test.ts`
  - `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task431-vscode-explorer-dnd-readonly-conflict-smoke.js`

注意：验证时 3000 常驻进程仍是旧 `mnote-web` 二进制，因此 `task431` 对 3000 初次执行复现旧 readonly 放行行为；当前代码在 3001 新编译进程验证通过，3000 需重启后生效。
