# mode=plan：理解需求 + 交互补全 + 生成配置文件

## 点位类型速查

| 类型 | 一句话用途 |
|------|---------|
| `customField` | 扩展字段类型（飞项一方字段类型的开发者 SDK 版；admin 装上后享筛/排/度量/流转条件/字段配置/布局配置/OpenAPI 读写）|
| `control` | 自定义 UI 组件 / 面板 / 卡片（嵌表格列 / 详情页 / 节点表单 / 新建页；数据自管，平台不存）|
| `liteAppComponent` | 轻应用组件（搭建器拖拽组件） |
| `dashboard` | **工作项详情页 Tab**（绑定具体工作项实例，展示/组合该工作项数据；运行时 namespace 是 `tab`）|
| `button` | 自定义按钮（工作项详情页 / 节点页）|
| `component` | 节点排期组件（`component_type: schedule`；替换空间所有排期入口）|
| `page` | 空间级独立全屏页（顶部/左侧导航扩展页，**不绑工作项**）|
| `view` | 工作项列表自定义视图 |
| `configuration` | 插件级配置入口 |
| `intercept` | 状态流转拦截器（同步返回放行/拒绝）|
| `listen_event` | 工作项事件异步监听（创建/更新/状态变更等）|

> **必填字段 / 长度 / 枚举合法性以 `lpm schema` 输出为准**，由 `local-config set` 强制校验——避免静态镜像随 schema 迭代漂移。

> **平台命名不一致，别被带偏**：类型名 ≠ key 前缀 ≠ 运行时 namespace。`page` 类型 key 前缀却是 `board_`（叫 board 实为导航页，**不是**看板/仪表盘）；`dashboard` 类型 key 前缀 `dashboard_`、运行时 namespace 是 `tab`（详情页 Tab）。
> **再分清 key vs resourceId**：点位 **key** 形如 `dashboard_xxxxxx`（无 `_web_`，schema 校验对象）；**resourceId** 形如 `dashboard_web_xxxxxx`（带 `_web_` 端标记，是 `platform.web.resourceId.resource` + `src/features/<id>/` 目录名）。文档里满屏的 `board_web_xxx` / `dashboard_web_xxx` 都是 resourceId，**不是 key**。


## P1：识别操作类型 + 粗扫候选点位类型

P1 做两件事：
1. 操作类型识别（增 / 改 / 删）
2. 粗扫候选点位类型——按文件顶部 [点位类型速查](#点位类型速查) 一句话用途表 + 下文 [术语→点位类型映射](#术语点位类型映射组件一词歧义) 表识别（不重述）

**特例**：字段语义需求（"字段值是 X" / "挂工作项上的字段"）→ candidate 同时含 `customField` + `control`（二选，P2.5 决策树定）。
**特例**：词面歧义触发双候选——用户原话含"组件 / 控件 / 筛选器 / 卡片 / 看板"等点位类别字面通用词时（典型 trap："筛选器**组件**" 既可 `control` 字段控件又可 `liteAppComponent` 独立组件），P1 必须输出 ≥2 候选，禁止单候选退化；进 P2.5 走 step 3 第三分支（其他双候选）拍板（**不**走 §P2.5.1，§P2.5.1 仅限 customField + control 对）。
**跨点位协作**（详情页表单联动 等）→ 加读 [`point-types/shared-scenes.md`](feature-point-types/shared-scenes.md)。

P1 **不做精细选型**（customField vs control 二选等），精细选型放到 P2.5（schema + candidate index.md 都读完后做）。

用户原话的读取方式：
- **被 workflow phase 编排时**：用 `lpm --cwd "<projectRoot>" ai state get` 取 checkpoint 的 `context.originalRequirement`（输出为空 = 无 checkpoint；Phase 1 尾回录的版本，逐字保留、不受会话压缩影响）
- **独立调用时**：从当前对话上下文读

输出：N 个独立需求 + 各自候选点位列表，等 P2 + P2.5 精细选型。

**MCP 缓存协议**：见 [`shared.md`](shared.md) 「MCP 检索技巧」末尾。`.lpm-cache/` 由 CLI 自动 gitignore + 在 `local-config set` / `publish` 成功后自动清理，AI 只写不清理。

## P2：获取 Schema 和当前完整配置

```bash
lpm --cwd "<projectRoot>" schema
lpm --cwd "<projectRoot>" local-config get --remote
```

- 两条命令都是**文件优先**：CLI 把结果写到 `.lpm-cache/schema/point-schema.json` 和 `.lpm-cache/config/remote.json`，stdout 只回一行相对路径
- **两个命令并行执行**，不要等第一个完成再跑第二个
- 若 `local-config get` 对应的远端为空，`remote.json` 内容是 `{}`，作为后续增改的基础
- 后续所有消费（切片、写 draft、diff 对比）都走文件，**不要整读 JSON 到 context**

### 按点位分片读取 schema

`.lpm-cache/schema/point-schema.json` 约 2400+ 行（~30K tokens），用 `lpm ai peek` 切片：

```bash
# 取某个点位完整定义 + 自动追 $ref（BuilderLayout / PointKey 等子 schema 一次拿全）
lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint

# 选型阶段：列所有点位类型（一句话描述）
lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json --index

# 只要 description 字段（看 AI 行为指引，忽略 pattern/enum 校验噪音）
lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint --descriptions-only

# 不追 $ref（仅主 schema）
lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint --no-follow
```

`lpm ai peek` 默认行为：结构模式自动取完整子树 + 追 `$ref` 闭包 + 循环检测，不会截断。**只取当前要操作的点位类型**，不要用 `--index` 当整读 schema 的借口。

**`--summary` / `--index` 的使用边界**（防止基于目录级信息下结论）：

- `--summary` / `--index` / `--list` 只用于**导航和选型**（决定"我要改哪个点位 / 用哪种类型"）
- 一旦确定了目标点位类型或 key，**落笔前必须再 peek 一次到具体符号**取完整定义：
  - 写配置 → `lpm --cwd "<projectRoot>" ai peek point-schema.json <PointName>` 取完整字段约束
  - 写 key 级改动 → `lpm --cwd "<projectRoot>" ai peek remote.json 'data[point_type=xxx].point_config[key=yyy]'` 取完整当前值
- "我看了 summary 知道有这个点位" ≠ "我知道这个点位的字段长什么样"——前者只够**选**，不够**改**

### 涉及 DSL 字段（table_cell / table_layout）的额外引导

触达以下字段时，schema 给的只是**字段约束**（type enum / style 字段 / 嵌套深度 / event action 枚举 等），**靠约束写不出有意义的 DSL 值**——DSL 的表达式语法（`{{...}}`、`$container` / `$colorTokens` / `$i18n` / `$fieldValue` 等平台变量、`data` 变量和数据接口返回字段的对应、事件 action 的 params 结构、色值多语言 token 的运行时行为）都在 MCP 里。

| 字段 | 归属点位 |
|---|---|
| `platform.web.table_cell` | `control`（scene 包含 1 时必填）|
| `platform.web.table_layout` | `customField`（必填）|

**MUST 先**走 MCP fallback 关键词 `开发表格页控件` / `表格列控件 DSL` 拉原文，按 MCP 缓存协议写到 `.lpm-cache/mcp/table-cell-dsl-spec.md`；**然后**对照 schema 的 `dslSchema` + MCP 里的使用示例 + 用户需求组合出合规 DSL。

- schema 告诉你**什么字段合法**（type 枚举 / style 支持哪些键 / 嵌套深度）
- MCP 告诉你**怎么用才有意义**（表达式语法 / 平台变量含义 / data 变量对接数据接口的链路 / 事件 params 形态）

**冲突裁决**：schema > MCP。字段命名不一致时以 schema 为准（例：schema 写 `$colorTokens.light`/`dark`，MCP 原文示例曾拼作 `lignt_mode`/`dark_mode`；写配置时用 schema 的拼写）。

**Mustache 表达式的能力边界**：schema 和 MCP 都只明确列出 5 类操作符（算术 `+-*/%` / 比较 `== != >= <= > <` / 三元 `x?y:z` / 属性访问 `x.y` / 数组访问 `a[i]`）；**调 JS 方法**（`.toFixed()` / `.length` / `.slice()` / `.toUpperCase()` 等）两边都没说支持——**保守不用**，需要格式化就在**数据接口返回端预处理成字符串**（type: string 变量）或**走 MCP fallback** 进一步确认。

### 按需读取 remote.json

`.lpm-cache/config/remote.json` 是远端全量配置，同样用 `lpm ai peek`：

```bash
# 某类型下的某点位
lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json 'liteAppComponent[0].key'

# 类型 + key 列表（diff / 删除确认最小视野）
lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json --index
```

或者 remote.json 文件较小时直接 Read 整文件也 OK（判断标准：空 `{}` 或 <10 个点位时整读）。

### 术语→点位类型映射（"组件"一词歧义）

schema 中 `liteAppComponent` 叫"轻应用组件"、`component` 叫"组件位"，但用户日常口语里"组件"可以指任何东西。识别规则：**先从用户描述里抽核心名词（拓展字段 / 控件 / 轻应用 / 排期），再映射点位类型。"组件"二字是修饰语，不是判断依据。**

| 用户可能的说法 | 核心名词 | 正确点位类型 | 易错映射 |
|--------------|---------|------------|---------|
| "拓展字段"/"拓展字段组件"/"字段模板"/"自定义字段" | **拓展字段** | `customField` | ~~liteAppComponent~~ |
| "控件"/"控件组件" | **控件** | `control` | ~~liteAppComponent~~ |
| "轻应用组件"/"概览组件"/"构建器组件"/"拖拽组件" | **轻应用/概览/构建器** | `liteAppComponent` | — |
| "排期组件"/"节点排期" | **排期** | `component` | ~~liteAppComponent~~ |
| "详情页 Tab"/"详情页加 Tab"/"标签页"/"详情页区块" | **详情页 Tab** | `dashboard` | ~~page~~（page 是空间级独立页、不绑工作项；详情页 Tab 必须绑工作项实例 → dashboard）|

## P2.5：精细点位选型 + 方案确认

**前置**：P2 已拉 schema + remote.json。

**主公式**（4 步）：

1. **Read 候选点位 index.md**：含独立 doc 的 6 类（customField / control / liteAppComponent / dashboard / button / componentSchedule）；其余 5 类（page / view / configuration / intercept / listen_event）走 [`code-plan.md`](feature-code-plan.md) Step 2 MCP fallback。**字段语义候选 customField + control 两份必读**。
2. **拉 schema 决策依据**：候选 = customField + control 是**唯一双候选选型边界**——必跑 `lpm --cwd "<projectRoot>" ai peek .lpm-cache/schema/point-schema.json ControlPoint` 拿完整 $comment（含完整能力对比表 + 三层决策树）；其他点位单候选选型靠 index.md 即可。
3. **按候选数分支**：
   - **单候选** → "已选 [类型]，理由：[X]。确认吗？" 等用户确认
   - **候选 = customField + control 双候选** → 走 §P2.5.1，**不要走单候选话术**
   - **其他双候选**（如 P1 词面歧义触发的 control + liteAppComponent）→ echo 两候选并列：每个写一句话能力 + 利弊 + 倾向理由，等用户答 "确认 / 改 / 不知道"；不要单候选话术绕过
4. **用户答完进 P3**——选型不对在这里 catch 比生成 draft 后再发现成本低。

### P2.5.1：customField vs control 选型边界（特例分支）

**触发**：P1 候选**同时含** customField + control 两份。其他点位选型**不进入此节**。

**为什么是特例**：customField vs control 是 schema 里唯一一对"语义重叠 + 多维度可换路径"的点位，需要"列双候选 + 多维度对比 + 用户拍板"。其他点位是单选，不需要这个开销。

**强制动作**（按顺序，每步缺失 = HARD 失败）：

1. **echo schema $comment 完整能力对比表**（peek 拿到的内容逐字搬到本 plan 输出）——这是双候选 derive 路径差异的事实依据，不 echo 等于没看
2. **走 schema $comment 三层决策树**（Step 0 字面术语 → Step 1 独家信号 → Step 2 歧义停下问）
3. Step 0/1 命中 → 一行话术告知用户 "已选 [类型]，理由：[命中信号原文]。确认吗？" + 等回复
4. **Step 2 命中 → 强制四动作**：
   - 扫题面 3 类信号：① 数据来源（CRM / ERP / 不落地）② 数据存储归属（落地飞项 vs 业务后端）③ 数据形态（表格列展示 / 表单交互）——3 类必扫，无命中写"无"。**端支持差异不在此列**（mobile 不展示两路径都能搞定，非真歧义）
   - 每条命中信号配 1 个对用户的关键决策问题（不漏）
   - 列两候选并列展开（基于完整能力对比表 derive 各自路径下针对本需求的实施 + 代价）
   - 全填完才向用户拍板

**铁律**（违反 = HARD）：
- 禁止预先表态选型（"应选 customField" / "候选点位类型 customField" / "走 control 路径" 都算）
- 必两候选并列展开（单个不算"列"）
- 3 类信号必扫完才能拍板
- 拍板前禁止写产物代码

## P3：交互补全缺失字段

**规则：一次只问一个问题**，语义化表达（例：不说"key 是什么"，而说"这个点位的唯一标识符打算用什么？建议格式如 my_board_point"）。

**外部数据源识别**：补字段时遇用户提到外部系统（CRM / ERP / 自建后端 / oncall 值班人 / 任何非飞项内的数据源）→ stop-and-ask 数据来源："这个值是飞项内字段还是外部系统？外部需自建后端代理调（前端不裸调外部 API，token 进 bundle = 公开发布凭据）"。**不要硬编默认值或编造 OpenAPI**。

### 对于修改操作

1. `lpm ai peek` 定位目标点位（不要 Read 整文件）：`lpm --cwd "<projectRoot>" ai peek .lpm-cache/config/remote.json '<type>[key=<key>]'`
2. 仅询问需要修改的字段
3. 其余字段保持原值

## P4：生成完整配置文件

**draft 不是从零写，而是从远端基线改出来的。** set 是全量替换（规则定义见 [`shared.md`](shared.md) 的"全量提交约束"）——draft 必须是「远端全量配置 + 本次改动」，漏写任一现有点位 = 推送时永久删除它。

### P4.0：从远端基线建 draft 起点（不可跳过）

```bash
# 从 remote.json 一步建 draft：自动拼 draft-<ts>.json 文件名 + 路径校验，stdout 回显 draft 路径
DRAFT=$(lpm --cwd "<projectRoot>" ai init-draft)
```

`remote.json` 与 draft 同构（CLI 已反向转换成本地 DSL 形态），`init-draft` 直接把它快照成 draft 起点；远端为空时是 `{}`，draft 也从 `{}` 起。

**这一步让"保留所有现有点位"成为构造的天然结果**——下面的增 / 改 / 删都是**在这份 draft 副本上动具体条目**，不是凭记忆把全量 JSON 重新拼一遍。先判能不能改现有点位满足需求（改它、不新建）；只有用户明确要删时，才从副本里移除——减少点位才会触发 A2/set 的删除闸口。

> 改 draft 用 `lpm ai patch-json <draft> --set-path/--add-path/--delete/--merge-path ...`：磁盘层按 path 精准动几处，draft 在盘上保持全量、context 里只装 delta，不必整读整写全量 JSON。下面三类操作即用它落地。

### 添加操作（在副本上）

在对应类型数组中 append 新点位对象，**保留所有其他类型和其他点位不变**。

```
远端: { "page": [A], "button": [B1, B2] }
用户: 新增一个 liteAppComponent
结果: { "page": [A], "button": [B1, B2], "liteAppComponent": [新点位] }
```

对于 Single 类型（page/view/dashboard/config/intercept/listen_event/component），如果远端已有该类型的点位，不能再添加（maxItems=1，schema 会拒绝）。应提示用户是要替换还是修改现有点位。

### 修改操作（在副本上）

找到匹配 key 的点位，合并更新字段，**其余字段和其他点位原样保留**。

```
远端: { "button": [{ "key": "button_x1", "name": "旧名" }, { "key": "button_x2", ... }] }
用户: 把 button_x1 的名字改成"新名"
结果: { "button": [{ "key": "button_x1", "name": "新名" }, { "key": "button_x2", 原样 }] }
```

### 删除操作（在副本上 · CRITICAL · 根原则 2 数据完整性 · config.plan 实施点）

从对应类型数组中移除匹配 key 的点位，**其余原样保留**。

```
远端: { "page": [A], "button": [B1, B2] }
用户: 删除 B2
结果: { "page": [A], "button": [B1] }

用户: 删除整个 button 类型的所有点位
结果: { "page": [A] }
```

（如果只传 `{ "liteAppComponent": [新点位] }` 而不包含 page 和 button，远端的 page 和 button 会被清除。）

**删除确认规则**：draft 相比远端基线减少了任何点位时，先向用户列出被删除的点位清单（key + name）并获得确认，再执行 set——不要静默删除。

### 生成前自检（仅语义级，schema 强校项让 A1 `lpm local-config set` 兜底）

- [ ] **从副本改出**：draft 由 P4.0 的 `remote.json` 副本编辑而来（含远端所有现有类型），非从零拼。只需复核没误删——而非"是否凑齐了全量"

> 其他规则（key 唯一 / Single 类型 maxItems / 必填字段 / 枚举合法 / name 长度 / platform 字段类型限定）schema 强校，A1 阶段 lpm 拒就拒，按 [`config-apply.md`](feature-config-apply.md) §A1「错误分类处理」修复重跑。AI **不要在 P4 复述**——校两次浪费 token。

## P5：判定前端点位 / webhook 形态点位（结构性，下游 stage 用）

从这次 draft（+ 远端基线）的点位 config 里，机械判出两组——下游 Stage Code 用它决定要不要跑、（可能的）后端那一半用它知道要接哪些 webhook 端点：

- **有前端的点位** = 在 `plugin.config.json` 里有 `resource` / `entry` 的点位（渲染点位）。这组非空 → Stage Code 要跑；这组为空 → 这个 feature 没有前端渲染点位，Stage Code 整段跳过。
- **webhook 形态的点位**（= 一定有后端）：

  | 点位类型 | webhook url/token 在哪 |
  |---|---|
  | `ai_node` / `ai_field`（`app_type=ai_node` / `ai_field` 工程）| url/token 在一个 `listen_event` extension 里 |
  | `intercept` / `listen_event` | 点位顶层有 `url`（+ `token`）|
  | `control`（嵌新建页等场景）| 点位顶层 `control[].url` |

  `ai_node` 可两半都有（节点卡片 resource + 算结果的 webhook）；`ai_field` 通常只后端那半。这组非空 → 这个 feature 需要后端那一半（Stage Config 之后走 feature 末尾的「→ 后端那一半」产交接包）。

webhook 形态点位的 url/token 在 Stage Config 这步可以先填占位 / localhost——真公网地址在后端那一半的联调收口（第 2 次 `local-config set`）才指过去。

### P5.1：后端可行性粗核（可选，命中 webhook 形态点位时）

写前端前可选跑 `lpm perm check --apis <按业务意图粗判的接口中文名>` 核一次：两类工程都极少"做不了"（AI 应用恒 `satisfied`；`normal` 的 `needApply` 可申请、不阻塞），唯一要 catch 的是 `unknown`（接口名写错 → 重查；此 app 确实没有 → 停下告知用户）。完整 perm 核对在「→ 后端那一半（relay）」A2 确认权限就绪，这里不替代。

## P6：输出

plan 阶段完成后输出：
- 生成的配置文件路径：`.lpm-cache/config/draft-{timestamp}.json`
- 变更摘要：添加/修改/删除了哪些点位（类型 + key）
- 前端 / 后端判定：有前端的点位有哪些（→ Stage Code 是否跑）、有 webhook 形态点位吗（→ 是否需要后端那一半）

> 中间产物（`.lpm-cache/config/remote.json` / `draft-*.json` / `schema/point-schema.json` / `mcp/*.md`）由 CLI 在 `local-config set` / `publish` 成功后自动清理，skill 不负责清理。
