# mode=apply：校验 + 预览 + 推送远端

> 本文所有 `lpm` 命令都带 `--cwd "<projectRoot>"` 前缀（[`shared.md` 执行约定](shared.md)）。下方各步的 **Checkpoint 写入**走 `lpm --cwd "<projectRoot>" ai state set '<完整 JSON>'`（协议见 [`checkpoint.md`](checkpoint.md)），示例里的 `{ lastStep: ... }` 是写入的内容。

## 前置

- 需要 plan 阶段生成的 `.lpm-cache/config/draft-{timestamp}.json`
- 若无该文件，先执行 `mode=plan`

**Checkpoint 恢复检查**（仅在 `lpm --cwd "<projectRoot>" ai state get` 有输出时）：
- `lastStep === "A0"` + `"success"` → 跳过 A0，从 A1 开始（用户已确认过清单，避免重复打扰）
- `lastCommand` 含 `local-config set` + `"success"` → 跳过 A0+A1，从 A2 开始
- `lastCommand` 含 `local-config diff` + `"success"` → 跳过 A0+A1+A2，从 A3 开始
- `lastCommand` 含 `update --source-type=local` + `"success"` → 跳过 A0+A1+A2+A3，直接执行 A4（输出）
- 其他 → 从 A0 开始

## A0：向用户确认变更（CRITICAL · ADDED/MODIFIED 的最后人工闸口）

> **不可跳过**。A1 的 `local-config set` 写本地前也会对**删除**做强阻塞 gate（draft 丢了远端点位 → exit 2、不写本地）；无删除时才改写 `point.config.local.json` 并触发归一化。A2 diff 同样对**删除**强阻塞，对**新增 / 修改**仅非阻塞展示。A0 与 A2 分工：
>
> - **A0 = [ADDED] / [MODIFIED] 的最后人工闸口**（基于 draft 自比 remote 计算，set 之前）
> - **A2 = [DELETED] 的最后人工闸口**（基于 set 之后的本地文件 + CLI 真值，更可信）
>
> A0 拦的是"AI 写 draft 偏离用户意图"；对"draft 漏写远端已有点位"的兜底由 CLI 在 set / diff / update 三处的删除 gate 完成（见 [`shared.md` §删除点位前置检查协议](shared.md)）。两者不可互替。

**操作**：

1. 列出 draft 的点位清单 + 对比远端，按类型分类输出。**三类必须都列**——无该类时写一行 `（无）`，不要省略类别行：

   ```
   [ADDED]    <type>  [<key>]  — "<name>"

   [MODIFIED] <type>  [<key>]  — "<name>"
              变更字段：
                name:        "旧值" → "新值"
                description: "旧值" → "新值"
              （只列有差异的字段；无差异字段不出现）

   [DELETED]  <type>  [<key>]  — "<name>"
              （此处仅展示，真正的删除确认 gate 在 A2，由 CLI 真值算）
   ```

   切片来源（互不依赖，**支持并行 tool call 的 agent** 一次发起；**不支持的 agent** 顺序执行，结果相同）：
   - `lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/draft-{timestamp}.json --index`
   - `lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json --index`

   禁止整读 JSON 到 context。

2. 把清单完整呈现给用户，明确告知："以上变更确认后会先写本地 `point.config.local.json`，再推送到 Meego 后台修改真实点位配置。请确认是否继续。"

3. 等待用户**明示**同意（"确认" / "OK" / "推送" 等），才能进 A1。

4. 用户明示同意后，写入 Checkpoint：
   ```
   { lastStep: "A0", lastCommandStatus: "success", nextCommand: "local-config set", nextStep: "A1 本地校验+暂存" }
   ```

5. 用户拒绝 / 提出修改 → **回 plan 修正 draft** → 重跑 A0；不要在 apply 阶段就地编辑 draft 绕过 plan。

**正向约束**：

- A1 之前必须完成 A0 的明示同意收集（A1 段落已强制前置）
- 清单必须三类齐全（`[ADDED]` / `[MODIFIED]` / `[DELETED]`），无该类写"（无）"——不要漏 [MODIFIED]，用户可能没意识到字段被改
- A0 和 A2 各管一类变更：A0 管 ADDED/MODIFIED，A2 管 DELETED——不要把两者合并到任何单一闸口（违反 = [`shared.md` §三条根原则](shared.md) 中"数据完整性"根原则违背）

## A1：本地校验 + 暂存 draft（local-config set）

**前置：A0 已获得用户明示确认**。无确认不得执行 A1。

**Checkpoint**：执行前写入 `{ nextCommand: "local-config set ...", nextStep: "A1 本地校验+暂存", lastCommandStatus: "running" }`

```bash
lpm --cwd "<projectRoot>" local-config set --from .lpm-cache/config/draft-{timestamp}.json
```

> `--cwd` 先把 CLI chdir 到插件根，`--from` 的相对路径随之解析到 `<projectRoot>/.lpm-cache/...`（也接受绝对路径）。CLI 完成：schema 本地校验 → URL 校验 → 后端 validator 校验 → 删除闸口 → 归一化 DSL → 写入本地 `point.config.local.json` → 清理 draft + `.lpm-cache/config/remote.json` 基线。**这一步还没推到远端**——推送在 A3。

### 删除闸口（exit 2 · 写本地之前）

draft 相比远端基线丢了点位时，set 在写本地**之前** exit 2，打印 `⚠️ DELETION_REQUIRES_CONFIRMATION` 清单——按下方 [A2 exit 2 分支](#exit-code-分支ai-的核心判定依据) 同一协议**逐字转呈用户取明示确认**。用户明示同意后，才 `local-config set --from <draft> --allow-delete` 重跑。正常按 [config-plan P4.0](feature-config-plan.md) 的 `lpm ai init-draft` 流程从远端基线建 draft，增 / 改不会触发此 gate；触发它通常意味着 draft 没建在基线上。

### 成功

**Checkpoint**：`{ lastCommand: "local-config set", lastCommandStatus: "success", nextCommand: "local-config diff", nextStep: "A2 预览本地 vs 远端差异" }`

继续执行 A2。

### 失败 — 错误分类处理

**Checkpoint**：`{ lastCommand: "local-config set", lastCommandStatus: "failed" }`

| 错误类型 | 处理方式 |
|----------|---------|
| 必填字段缺失 | 向用户补充询问，更新配置文件，**重新执行 A1** |
| 枚举值非法 | 列出合法值，询问用户选择，更新后**重新执行 A1** |
| key 重复冲突 | AI 自动在 key 后追加数字后缀（如 `_2`、`_3`）生成不冲突的 key，更新配置后**重新执行 A1** |
| name 超长 | 提示最大长度限制，要求用户缩短，**重新执行 A1** |
| URL 模式违规（`must match "^https?://"` / URL 缺失 / 为空） | 这类**结构性非法**仍是硬阻止 `exit 1`。把 CLI 报错原文转呈用户，由用户决定提供真实 URL 或其他处理，确认后**重新执行 A1**。注意：换成 example.com / your-server / localhost 之类**占位 URL 并不会被拦下**——CLI 只打印 `⚠️ NOTICE` 提示后照常推送；**AI 应把该 NOTICE 原文转呈用户、提醒上线前替换成真实后端地址**（对齐「Stage Config 先填占位、联调出真公网地址后再 set 替换」的两步流程）。所以**不要拿占位 URL 去「绕过」校验**，那只是把没填真地址的问题推到运行时。AI 也不要自行伪造 URL。（发布前还会由 publish 阶段 A0 的 `lpm check diff` ④段再兜一道） |
| Token 缺失（`must have required property 'token'`） | 当 `table_url.url` / `url` 有值时，对应 `token` 由 CLI 自动生成（36 位 UUID），AI **不需要**手写。若仍报错，说明 CLI 版本过旧或生成链路失败，如实上报用户排查；**切勿**自行伪造 token 值 |
| `table_cell must be object` | `table_cell` 支持 object 或 JSON string 两种形态，CLI 自动归一化。报这个错说明 CLI 版本过旧 → 提示用户升级 CLI 后**重新执行 A1**。AI 不要自行把 string 改成 object 绕过 |
| `platform.web` 字段类型不符（如 `table_url` 用在非 customField）| 不同点位类型有各自的 `PlatformWebFor{Type}` schema：`table_url` 是 customField 专有 / `mode` `init_size` 是 button 专有 / `scene` 是 control 专有 等，**不跨类型混用**。把字段挪回正确点位类型下，或删除误配字段，**重新执行 A1** |
| 未知错误 | 展示原始错误给用户，共同分析后**重新执行 A1** |

循环直至 set 成功。

> **通用原则：** 校验失败时 AI 的默认动作是"停下来问用户"，不是"悄悄发明绕过方案"。任何自主修正都必须（1）不改变被测语义；（2）明确在对话中说明；（3）记入最终产物的 TODO 标记。

## A2：预览 diff（local-config diff · CRITICAL · 根原则 2 数据完整性）

> **不可跳过**。删除点位在 A1（set）/ A2（diff）/ A3（update）三处都会 `exit 2` 硬拦——A2 diff 是**无副作用的纯预览**，最适合让用户看"即将被删的点位清单"。三处任一 exit 2 都按本节协议转呈 + 取确认。

**Checkpoint**：执行前写入 `{ nextCommand: "local-config diff", nextStep: "A2 预览本地 vs 远端差异", lastCommandStatus: "running" }`

```bash
lpm --cwd "<projectRoot>" local-config diff
```

CLI 会对比 A1 写入的 `point.config.local.json` 与远端当前配置，按点位类型 + key 分类输出：

```
[ADDED]    liteAppComponent     [board_web_x9y2]  — "Release Calendar"
[MODIFIED] button               [button_submit]   — "保存"
[DELETED]  page                 [board_abc123]    — "Board Demo"

Summary: 1 added, 1 modified, 1 deleted.
```

### exit code 分支（AI 的核心判定依据）

**exit 0（无删除）** → 安全推进：
- 把 stdout 摘要（最末行 `Summary: ...`）给用户看一眼作为变更预览，直接进 A3
- **Checkpoint**：`{ lastCommand: "local-config diff", lastCommandStatus: "success", nextCommand: "update --source-type=local", nextStep: "A3 推送远端 + 拉回模板" }`

**exit 2（有删除）** → **立即停止，不得自动继续**：

1. 把 CLI 的 stderr 完整内容（含 `⚠️ DELETION_REQUIRES_CONFIRMATION` 标题 + 被删点位清单）**逐字转呈**给用户——不改写、不压缩、不"帮用户总结"
2. 等待用户**明示**同意删除具体的点位（形如"同意删除 page[board_abc123]"或"全部删掉"）
3. 用户确认后进 A3，且 A3 **必须带 `--allow-delete`**（否则 A3 自己的删除闸口会再次 exit 2 拦下推送）；用户拒绝或有疑问 → 回 plan 修正 draft → 重跑 A1 → A2

**绝对禁止**（违反 = [`shared.md` §三条根原则](shared.md) 中"数据完整性"根原则违背，与"自己发布插件"同级）：
- 看到 exit 2 不转呈 stderr，自己判断"这是废弃点位应该删"
- 假设"用户跑了 apply 就是同意所有变更"——用户可能根本没意识到 draft 里缺了某个 key
- 未经用户明示同意就加 `--allow-delete` 绕过任何一处删除闸口——它是"用户确认后的放行开关"，不是"让命令别报错"的消音键

**exit 1（命令错误，非 diff 结果）** → 拉远端 / 读本地失败等，报错给用户分析。

## A3：推送远端 + 拉回模板（update --source-type=local）

**Checkpoint**：执行前写入 `{ nextCommand: "update --source-type=local", nextStep: "A3 推送远端 + 拉回模板", lastCommandStatus: "running" }`

```bash
# 无删除（A2 exit 0）：直接推
lpm --cwd "<projectRoot>" update --source-type=local

# 有删除且用户已在 A2/A1 明示同意：带 --allow-delete 推
lpm --cwd "<projectRoot>" update --source-type=local --allow-delete
```

这一条命令完成两件事：
1. 推：把 A1 写入本地的 `point.config.local.json` 推送到远端
2. 拉：自动回拉远端最新配置 + 后端生成的模板代码，刷新 `plugin.config.json.resources` 和 `src/features/<resourceId>/index.tsx`

> **push 前 update 自己也跑一次删除闸口**：本地配置相比远端丢了点位且**未带** `--allow-delete` → exit 2、打印 `DELETION_REQUIRES_CONFIRMATION`、**不推送**。这不是非阻塞提醒，是硬闸口。所以 A2 检测到删除、用户已明示同意后，A3 必须带 `--allow-delete` 才能推过去；没带 = 推不动。远端不可达时此闸口降级为告警放行（push 命中后端会再校）。

- 成功 → **Checkpoint**：`{ lastCommand: "update --source-type=local", lastCommandStatus: "success", nextStep: "A4 输出" }` → 继续执行 A4
- 失败 → **Checkpoint**：`{ lastCommand: "update --source-type=local", lastCommandStatus: "failed" }` → 展示错误原文给用户；如果是推送阶段失败，本地已保存的 draft 仍在 `point.config.local.json`，修好后重跑本命令即可

## A4：输出

```
✅ 配置已成功推送至远端，新增点位模板已更新
   变更：[添加/修改/删除] page[test_board_v1]
```
