Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50d7e7d98f |
@@ -0,0 +1,368 @@
|
||||
# 声明式命令框架迁移指南
|
||||
|
||||
- **受众**:把 helpers 叶子 / Shortcut 从 Tier3 / 遗留 annotate 迁到声明式框架的工程师
|
||||
- **规范权威**(本文不重复政策全文,只给可执行路径):
|
||||
- [`rfc-command-framework-convergence.md`](rfc-command-framework-convergence.md) **§5.0**(声明定义、三档路径、Schema 字段权威)
|
||||
- [`flag-help-schema-homology.md`](flag-help-schema-homology.md)(路径 A:Contract 嵌入 Schema)
|
||||
- 仓库根 [`AGENTS.md`](../AGENTS.md)(Authoring tiers、Agent Schema contract、curation)
|
||||
- **架构速览**:[`command-framework-architecture.md`](command-framework-architecture.md)
|
||||
- **实现基线**:PR #830 起 `helpers.LeafSpec` / `shortcut.Shortcut` → `corecmd.Spec` → `corecmd.New`;Schema 经 `RegisterSchemaSourceRoot` → `ResolveSchemaBuild` 运行时组装(**声明即 Catalog**)
|
||||
|
||||
---
|
||||
|
||||
## 1. Why / 终态
|
||||
|
||||
目标不是「再写一份 Catalog JSON」,而是 **一份声明同时驱动**:
|
||||
|
||||
1. Cobra 可执行表面(flags / required / constraints / help)
|
||||
2. 运行时确认门(`confirmation=user_required` → `ConfirmSafety`)
|
||||
3. Agent Schema(`ToolSpec` / `ResolveMeta`)
|
||||
|
||||
**单向数据流(摘要)**:
|
||||
|
||||
```text
|
||||
leaf Safety + Contract / ParamDecl / ProductDecl
|
||||
→ ContractFinal(corecmd.New 或 AttachContract / DeclareLeafMetadata)
|
||||
→ CollectIdentitySpecs(活 Cobra 叶上的 Identity)
|
||||
→ ResolveSchemaBuild → SchemaRegistry
|
||||
→ dws schema / ResolveMeta(同一组装结果)
|
||||
```
|
||||
|
||||
硬约束:
|
||||
|
||||
| 要做 | 不要做 |
|
||||
|---|---|
|
||||
| 在叶子旁声明 `Safety` / `Contract` / `ParamDecl` | 提交 `schema_catalog/`、`schema_meta_index.*`、`schema_agent_metadata/`、`schema_hints/`、`schema_mcp_metadata.json` |
|
||||
| 用 `make generate-schema` 刷新 param aliases + 证明组装确定性 | 把 Catalog dump 当交付权威或手工改 wire |
|
||||
| help/Schema 事实 **declare OR annotate** | 纯推断、或钩子闭包里「顺带」发明表面 |
|
||||
| `Confirmation` 单独驱动运行时门 | 从 `effect`/`risk` 机械推导 `confirmation` |
|
||||
|
||||
`cmd_schema_catalog` 仅供 CI/local dump;生产路径没有 committed Catalog pin。
|
||||
|
||||
---
|
||||
|
||||
## 2. Authoring tiers(何时用哪一档)
|
||||
|
||||
同一 `ContractFinal` 语义;三档不是互相否定。详见 RFC §5.0.2a。
|
||||
|
||||
| 档 | 入口 | 声明什么 | 执行面 | 何时用 |
|
||||
|---|---|---|---|---|
|
||||
| **Tier1** | `corecmd.New` / `helpers.NewLeafCommand(LeafSpec)` | `Flags` / `Constraints` / `Safety` / `ConstParams` / `Contract` 全进 Spec | 框架注册 flag、投影参数、`ConfirmSafety`、派发 | **新命令默认**;执行面可迁入 LeafSpec/`Call` |
|
||||
| **Tier2** | `helpers.DeclareLeafMetadata(cmd, LeafSpec)` | 仅 `Safety` + `Contract`(可选 `Validate`) | **不**注册 flag;确认挂 RunE 包装器 | helpers / Shortcut 迁移态:先补 Agent Schema,执行面暂时手写 `RunE`/`Execute` |
|
||||
| **Tier3** | 裸 `*cobra.Command` | 无框架声明(或仅 annotate / 精确排除) | 调用方自管 | 应收窄;新增裸叶须迁元数据或进 `schema_command_exclusions.go` |
|
||||
|
||||
选用规则(今日):
|
||||
|
||||
1. 新命令 → **Tier1**。
|
||||
2. 既有叶子要进 Schema、但 `RunE`/`Execute` 还不想重写 → **Tier2**(声明写在命令字面量旁)。
|
||||
3. **Shortcut + `DeclareLeafMetadata` 合法**;多数 Shortcut 已走 `FromShortcut` → `corecmd.New`(Tier1 表面 + 自有 `Execute`)。
|
||||
4. Tier2 → Tier1 与 Shortcut → mcpbind 是后续里程碑,**不是**本阶段硬门槛;不得用 Tier2 绕开「业务 flag 必须声明」的纪律(半接管字段会 panic)。
|
||||
|
||||
`DeclareLeafMetadata` 禁止传入:`Flags` / `Constraints` / `ConstParams` / `Call` / `RunE` / `PostMount` / `ConfirmFirst` / `Server`/`Tool`;**唯一允许的执行钩子是 `Validate`**。
|
||||
|
||||
---
|
||||
|
||||
## 3. What to declare(声明清单)
|
||||
|
||||
### 3.1 框架声明面(数据字段才算声明)
|
||||
|
||||
| 字段 | 作用 | Schema / 运行时 |
|
||||
|---|---|---|
|
||||
| `Flags`(Tier1) | kebab-case flag、Kind、Default、Required、`Bind`、Aliases… | parameters;`Bind` → property |
|
||||
| `Constraints`(Tier1) | `at_least_one` / `exactly_one` / `mutually_exclusive` / custom | constraints 段 + 运行时校验 |
|
||||
| `Safety`(`contract.SafetySpec`) | `effect` / `risk` / `confirmation` / `idempotency` **四字段齐全** | ToolSpec Safety;仅 `confirmation` 驱动确认门 |
|
||||
| `ConstParams`(Tier1) | 固定 toolArgs,不上用户 flag 表 | 载荷;非 parameters |
|
||||
| `Contract`(`corecmd.ContractDecl`) | Identity / Description / Selection / Parameters / Interface / DryRun | ContractFinal → Catalog |
|
||||
| `ProductDecl` | 产品级 routing(`contract.RegisterProductDecl`) | 产品 overview selection |
|
||||
|
||||
钩子(`Validate` / `Call` / `Invoke` / `Orchestrate` / `RunE` / `Execute` / `PostMount`)**不算**声明——行为正确也不能替代 Schema 事实。
|
||||
|
||||
### 3.2 Contract 必填要点
|
||||
|
||||
- `Identity`:`ProductID` / `Name` / `CanonicalPath` / `CLIPath` / `PrimaryCLIPath`(与活 Cobra 路径一致;collector 以此为唯一 identity 源)
|
||||
- `Description`:构造期必填(声明证据);Catalog **交付**可优先 Cobra Long(provenance `cobra_help`)
|
||||
- `Selection`:决策向 `AgentSummary` / `UseWhen` / `AvoidWhen` / `Examples`(勿复述 Short;**禁止** `Reviewed` 字段)
|
||||
- `Parameters`(`[]contract.ParamDecl`):CLI flag 名 → RPC `Property`;`interface_type` 等接口事实写这里,不写 MCP pin
|
||||
- `Interface`:`mcp` / `composite` / `local` + `Ref` 或 `Reason`
|
||||
|
||||
### 3.3 ProductDecl
|
||||
|
||||
在产品根命令构造处注册一次,例如 `internal/helpers/drive.go` 的 `newDriveCommand`:
|
||||
|
||||
```go
|
||||
contract.RegisterProductDecl(contract.ProductDecl{
|
||||
ID: "drive",
|
||||
Selection: contract.ProductSelectionDecl{
|
||||
AgentSummary: "…",
|
||||
UseWhen: []string{"…"},
|
||||
AvoidWhen: []string{"…"},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 分步迁移清单
|
||||
|
||||
### 4.1 典型 helpers 叶子(裸 Cobra → Tier2)
|
||||
|
||||
1. **确认 CLI 路径**:`dws <path> --help`,记下 flag 名与 required 组。
|
||||
2. **选定 Identity**:`product_id` / `name` / `canonical_path` / `cli_path` 与兄弟命令不冲突;aliases 若有则写进 Identity(与 collected registry 一致)。
|
||||
3. **声明 Safety**:写操作填完整四字段;删除/破坏性用 `confirmation=user_required`(见 §5)。
|
||||
4. **声明 Contract**:`Description` + `Selection` + `Interface`;flag→RPC 用 `Parameters: []ParamDecl{{Name, Property}}`。
|
||||
5. **挂元数据**:在 flag 注册与 `RunE` 之后调用:
|
||||
|
||||
```go
|
||||
DeclareLeafMetadata(cmd, LeafSpec{
|
||||
Safety: contract.SafetySpec{ /* 四字段 */ },
|
||||
Contract: LeafContract{ /* Identity / Selection / … */ },
|
||||
// 本地副作用且 user_required:补 Validate,避免确认抢先或 fail-closed
|
||||
})
|
||||
```
|
||||
|
||||
6. **Property 缺口**:CLI 有、RPC 无的 selector → `schema_parameter_mapping_ledger.go` 的 `mapping_exclusions`(精确键 + 非空 reason),不要造 binding JSON。
|
||||
7. **产品路由**:若产品尚无 `ProductDecl`,在产品根补注册。
|
||||
8. **验证**(§7)后再开 PR。
|
||||
|
||||
参考:`internal/helpers/drive.go`(`DeclareLeafMetadata` + `RegisterProductDecl`)。
|
||||
|
||||
### 4.2 典型 Shortcut(补 Safety + Contract)
|
||||
|
||||
1. Shortcut 已由 `FromShortcut` → `corecmd.New` 接管 flag/约束/确认;迁移焦点是 **显式 `Safety` + `Contract`**,不要再依赖仅 `Risk` 推断。
|
||||
2. 在 `shortcut.Shortcut{…}` 字面量上增加:
|
||||
|
||||
```go
|
||||
Safety: contract.SafetySpec{
|
||||
Effect: "read", Risk: "low",
|
||||
Confirmation: "not_required", Idempotency: "idempotent",
|
||||
},
|
||||
Contract: corecmd.ContractDecl{
|
||||
Identity: contract.ToolIdentitySpec{ /* … */ },
|
||||
Description: "…",
|
||||
Interface: &contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "…"},
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "…",
|
||||
UseWhen: []string{"…"},
|
||||
AvoidWhen: []string{"…"},
|
||||
Examples: []string{"dws chat +conversation-info --group <openConversationId>"},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
3. `Flags` / `Constraints` / `Validate` / `Execute` 保持 Shortcut 词汇;adapter 映射到 `corecmd.Spec`。
|
||||
4. Examples 用真实可执行路径与 flag;**不要**写 `--yes`。
|
||||
5. 验证(§7)。
|
||||
|
||||
参考:`internal/shortcut/chat/chat_conversation.go`(`ConversationInfo`);adapter:`internal/shortcut/adapter.go`。
|
||||
|
||||
### 4.3 新命令 / 可重写执行面 → Tier1
|
||||
|
||||
优先 `NewLeafCommand(LeafSpec{ Flags, Safety, Contract, Call/… })`,例如 `internal/helpers/devapp.go`。业务 flag 只进 `Flags`,禁止在 `PostMount`/`Validate` 里 `Flags().String` 注册业务面。
|
||||
|
||||
---
|
||||
|
||||
## 5. Safety / confirmation
|
||||
|
||||
### 5.1 声明完整 SafetySpec
|
||||
|
||||
```go
|
||||
Safety: contract.SafetySpec{
|
||||
Effect: "destructive", // read | write | destructive
|
||||
Risk: "high", // low | medium | high
|
||||
Confirmation: "user_required", // not_required | user_required
|
||||
Idempotency: "unknown", // idempotent | retryable | non_idempotent | unknown
|
||||
},
|
||||
```
|
||||
|
||||
非空则四字段必须齐全(构造期校验)。字段独立:`effect=destructive` **不会**自动变成 `user_required`。
|
||||
|
||||
### 5.2 迁离 AnnotateRuntimeRisk / AnnotateRuntimeGate
|
||||
|
||||
| 旧路径 | 新路径 |
|
||||
|---|---|
|
||||
| `AnnotateRuntimeRisk` / 字符串 `dws.schema.risk` | 声明 `Safety.Risk`(经 ContractFinal) |
|
||||
| `AnnotateRuntimeGate` / `runtime_gate`(如 `devAppRequireWriteGuard`) | 声明 `Safety.Confirmation=user_required`;框架 `ConfirmSafety` |
|
||||
| 仅 Shortcut `Risk=write` 隐式确认 | 显式 `Safety`(覆盖 Risk 展开) |
|
||||
|
||||
**禁止新增**生产 `AnnotateRuntimeRisk` / `AnnotateRuntimeGate` 调用点;存量 annotate 可保留至迁完(`HOM-S2`)。同源门禁:`confirmation=user_required` ↔ 运行时 gate(见 `check-runtime-confirmation-truth.sh`、`internal/cli/homology`)。
|
||||
|
||||
### 5.3 Tier2 确认时机
|
||||
|
||||
- 有 `Validate`:`Validate` → `ConfirmSafety` → 原 `RunE`
|
||||
- 无 `Validate`:确认推迟到首次 `CallTool`;无 Caller 的本地副作用叶必须补 `Validate`,否则 fail-closed
|
||||
|
||||
执行前需确认时,用 `--yes` 跳过交互;**Schema examples 永不包含 `--yes`**。
|
||||
|
||||
---
|
||||
|
||||
## 6. Parameters / mapping
|
||||
|
||||
1. **主权威**:Tier1 用 `FlagSpec.Bind`(空则 Name);Tier2 / 混合路径用 `contract.ParamDecl{Name, Property}`。
|
||||
2. **mapping ledger**(`internal/cli/schema_parameter_mapping_ledger.go`):只放 `mapping_exclusions` / removals——CLI flag **无**直接 RPC property 时的精确评审排除;非空 reason。
|
||||
3. **不要**:提交 MCP pin(`schema_mcp_metadata.json` 已退役);不要指望 live MCP 创建 CLI flag;不要用 hints overlay 改 `type`/`required`/`default`。
|
||||
4. **required 地板**:Cobra `MarkFlagRequired` 不得被低优先级源降为 optional。
|
||||
5. 接口事实(`interface_ref` / `interface_type`)声明在 leaf `Contract.Interface` / `ParamDecl`;CLI path ≠ MCP path 时用 `Interface.Ref`(例:`drive delete` → `doc.delete_document`)。
|
||||
|
||||
---
|
||||
|
||||
## 7. Schema 验证
|
||||
|
||||
迁移后至少跑:
|
||||
|
||||
```bash
|
||||
make generate-schema
|
||||
./scripts/policy/check-schema-catalog.sh
|
||||
./scripts/policy/check-runtime-confirmation-truth.sh # 若动了 confirmation / gate
|
||||
# 聚焦:
|
||||
go test ./internal/app -run 'TestSheetFinalSchemaConfirmationMatchesRuntimeGuards|TestFinalSchemaParametersMatchExecutableHelpFlags' -count=1
|
||||
```
|
||||
|
||||
手工抽查:
|
||||
|
||||
```bash
|
||||
dws schema --cli-path "drive mkdir" -f json
|
||||
# 或 ResolveMeta 同源:help Safety 行与 Schema confirmation 一致
|
||||
dws drive mkdir --help
|
||||
```
|
||||
|
||||
Examples 规则(组装/门禁):
|
||||
|
||||
- 每 tool 最多两条;路径与 flag 必须是活 Cobra 可接受的
|
||||
- **禁止** `--yes`;禁止 shell 注释
|
||||
- 缺必填 / 约束失败 = 契约 bug,不是「跳过 example」的理由
|
||||
|
||||
可选:`make test-schema-agent-examples`(合同 + 显式 dry-run 能力子集)。
|
||||
|
||||
---
|
||||
|
||||
## 8. Pitfalls(常见坑)
|
||||
|
||||
1. **Schema source root = declarationOnly**
|
||||
`app.NewSchemaSourceRootCommand` 以 `declarationOnly=true` 建树:**跳过** `helpers.InitDeps` / `injectStaticServers`,避免组装时清掉进程里的 ToolCaller / plugin endpoint。叶子构造与 RunE 包装必须对 `deps == nil` 安全(见 `printDocDeprecationWarning` 注释,`internal/helpers/doc.go`)。
|
||||
|
||||
2. **DryRun `RemoteReads`**
|
||||
声明 `Contract.DryRun` / `DryRunSpec` 时,`RemoteReads: false` 表示预览计划不发起远程读;勿把「有 dry-run 能力」与「会打后端」混为一谈。无 reviewed dry-run 能力时,example 门禁不会魔法升级为 runtime dry-run。
|
||||
|
||||
3. **不要复活退役目录 / pin**
|
||||
`schema_hints/`、`schema_agent_metadata/`、`schema_command_registry/`、committed `schema_catalog/`、`schema_mcp_metadata.json` —— 出现即 policy 失败。
|
||||
|
||||
4. **Tier2 半接管**
|
||||
往 `DeclareLeafMetadata` 塞 `Flags`/`Call`/`RunE` 会 panic;要框架管 flag → 升 Tier1。
|
||||
|
||||
5. **Identity 漂移**
|
||||
`Contract.Identity` 必须与活路径一致;不一致组装失败。排除项用 `schema_command_exclusions.go` 精确路径 + reason,禁止前缀通配。
|
||||
|
||||
6. **Selection 里塞 `Reviewed`**
|
||||
旧 hints 标记;声明载荷携带即组装报错。
|
||||
|
||||
7. **在钩子里发明 flag/property**
|
||||
Schema 看不到;同源门禁(`HOM-P*` / `HOM-D1`)会打回来。
|
||||
|
||||
---
|
||||
|
||||
## 9. Worked mini-example(前后对照)
|
||||
|
||||
### 9.1 helpers:裸叶 → Tier2(模式摘自 `drive mkdir` / `drive delete`)
|
||||
|
||||
**Before(概念)**:手写 `cobra.Command` + `RunE` + `Flags().String`,无 `Safety`/`Contract` → Agent Schema 缺叶子或靠已退役 hints。
|
||||
|
||||
**After(Tier2,缩写)**:
|
||||
|
||||
```go
|
||||
driveMkdirCmd := &cobra.Command{
|
||||
Use: "mkdir",
|
||||
Short: "创建文件夹",
|
||||
RunE: /* 既有 callMCPTool("create_folder", …) */,
|
||||
}
|
||||
driveMkdirCmd.Flags().String("name", "", "…")
|
||||
driveMkdirCmd.Flags().String("folder", "", "…")
|
||||
|
||||
DeclareLeafMetadata(driveMkdirCmd, LeafSpec{
|
||||
Safety: contract.SafetySpec{
|
||||
Effect: "write", Risk: "medium",
|
||||
Confirmation: "not_required", Idempotency: "unknown",
|
||||
},
|
||||
Contract: LeafContract{
|
||||
Identity: contract.ToolIdentitySpec{
|
||||
ProductID: "drive", Name: "create_folder",
|
||||
CanonicalPath: "drive.create_folder",
|
||||
CLIPath: "drive mkdir", PrimaryCLIPath: "drive mkdir",
|
||||
},
|
||||
Description: "创建文件夹",
|
||||
Interface: &contract.InterfaceSpec{
|
||||
Mode: "mcp", Availability: "available",
|
||||
Ref: &contract.InterfaceRefSpec{ProductID: "drive", RPCName: "create_folder"},
|
||||
},
|
||||
Selection: contract.SelectionSpec{
|
||||
AgentSummary: "创建文件夹",
|
||||
UseWhen: []string{"用户要在钉盘下新建普通文件夹时"},
|
||||
AvoidWhen: []string{"知识库内建文件夹改用 dws wiki node create …"},
|
||||
Examples: []string{`dws drive mkdir --name "项目资料" --format json`},
|
||||
},
|
||||
Parameters: []contract.ParamDecl{
|
||||
{Name: "folder", Property: "parentId"},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
破坏性写示例(`drive delete`):`Confirmation: "user_required"`,`Effect: "destructive"`,`Interface.Ref` 可指向 `doc`/`delete_document`。
|
||||
|
||||
路径:`internal/helpers/drive.go`。
|
||||
|
||||
### 9.2 Tier1 LeafSpec(`dev app event list` 缩写)
|
||||
|
||||
```go
|
||||
return NewLeafCommand(LeafSpec{
|
||||
Use: "list", Short: "查询应用已订阅的事件列表",
|
||||
Tool: devAppEventListTool,
|
||||
Safety: /* read / low / not_required / idempotent */,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Bind: "unifiedAppId", Required: true, Trim: true},
|
||||
{Name: "keyword", Bind: "keyword", OmitEmpty: true, Trim: true},
|
||||
},
|
||||
Contract: LeafContract{ /* Identity + Selection + Interface */ },
|
||||
Call: devAppCallCursor(runner),
|
||||
})
|
||||
```
|
||||
|
||||
路径:`internal/helpers/devapp.go`。
|
||||
|
||||
### 9.3 Shortcut(`chat +conversation-info`)
|
||||
|
||||
见 §4.2;完整字面量:`internal/shortcut/chat/chat_conversation.go`。`FromShortcut` 把 `Flags`/`Constraints`/`Safety`/`Contract` 编进 `corecmd.Spec`(`internal/shortcut/adapter.go`)。
|
||||
|
||||
---
|
||||
|
||||
## 10. Done criteria(完成标准)
|
||||
|
||||
迁移 PR 合并前,下列应满足:
|
||||
|
||||
| 检查项 | 证据 |
|
||||
|---|---|
|
||||
| 叶子有完整 `Safety`(或存量 `runtime_gate` 未扩大) | `dws schema --cli-path "…" -f json` 四字段与声明一致 |
|
||||
| `user_required` ↔ 运行时确认 | `./scripts/policy/check-runtime-confirmation-truth.sh`;相关 `HOM-S1`/`HOM-S2` |
|
||||
| Identity / 路径可执行且进 Schema(或精确 exclusion) | `CollectIdentitySpecs` / reverse-completeness;无前缀排除 |
|
||||
| parameters ⊆ help flags;property 有声明或 mapping exclusion | `check-schema-catalog.sh`;`HOM-P1`/`HOM-D1` |
|
||||
| 无退役 pin / hints / agent_metadata / committed catalog | policy 脚本;`git status` 无这些路径 |
|
||||
| `make generate-schema` 通过(aliases + 组装确定性) | CI / 本地 |
|
||||
| Examples 可执行、无 `--yes` | 组装 example 门禁;必要时 `make test-schema-agent-examples` |
|
||||
| gofmt;未无关改动 | PR diff |
|
||||
|
||||
长期可选(非本阶段硬门槛):Tier2 → Tier1;Shortcut `Execute` 中「只为装配参数」的函数体收敛到 mcpbind 形态 1/2(RFC §3.5 / §5.0.2a.5)。
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件速查
|
||||
|
||||
| 主题 | 路径 |
|
||||
|---|---|
|
||||
| Leaf Tier1/Tier2 API | `internal/helpers/leaf.go` |
|
||||
| Leaf → corecmd | `internal/helpers` `FromLeafSpec`;`internal/corecmd` |
|
||||
| Shortcut → corecmd | `internal/shortcut/adapter.go` |
|
||||
| Safety / Contract DTO | `internal/corecmd/contract` |
|
||||
| ContractFinal 注册 | `internal/corecmd/contractfinal` |
|
||||
| Schema 组装入口 | `internal/cli`:`RegisterSchemaSourceRoot`、`ResolveSchemaBuild`、`ResolveMeta` |
|
||||
| 精确排除 | `internal/cli/schema_command_exclusions.go` |
|
||||
| mapping exclusions | `internal/cli/schema_parameter_mapping_ledger.go` |
|
||||
| declaration-only 根 | `internal/app.NewSchemaSourceRootCommand` |
|
||||
Reference in New Issue
Block a user