refactor(schema): break corecmd→cli cycle and split contract seams

Move AnnotateRuntime* into cli/runtimeannotate and the Cobra-keyed
ContractFinal store into cli/contractfinal so corecmd depends on thin
subpackages instead of the cli delivery root. Keep contract as DTO-only,
document Description declare-vs-delivery, and house homology gates under
cli/homology.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
玉澜
2026-08-01 22:16:17 +08:00
co-authored by Cursor
parent 7fedbea806
commit b7f04fd2a0
59 changed files with 1598 additions and 1079 deletions
+16 -7
View File
@@ -30,8 +30,14 @@ changes must not rewrite it mechanically.
- Today: `helpers.LeafSpec` / `shortcut.Shortcut` → `corecmd.Spec` (+ optional `Contract`) → `corecmd.New`
- **Declare = final Schema source**: `Flags` / `Constraints` / `Safety` / `ConstParams` / `Contract` (`corecmd.ContractDecl`; nested fields are `contract.*`)
- Naming: `ContractDecl` is the authoring leaf declaration. "Schema" means Catalog / `ToolSpec` delivery — do not reintroduce `SchemaDecl`.
- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `cli.RegisterRuntimeContractFinal` (annotate + store seam); assembly **pass-throughs** Final. Production code must not call `contract.RegisterRuntimeContractFinal` directly.
- Package seam: types/registries → `corecmd/contract` (sole definitions); Cobra annotate+store seam + Catalog/`ResolveMeta` delivery → `internal/cli`. cli does not redefine contract types.
- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `contractfinal.RegisterRuntimeContractFinal` (framework) or `cli.RegisterRuntimeContractFinal` (product re-export); assembly **pass-throughs** Final.
- Package seam:
- types / ProductDecl → `corecmd/contract` (DTO only; **no** Cobra-keyed ContractFinal store)
- AnnotateRuntime* writers → `internal/cli/runtimeannotate` (`corecmd` may import; must **not** import `internal/cli` root)
- ContractFinal cobra store + Register → `internal/cli/contractfinal`
- homology gates → `internal/cli/homology`
- Catalog / `ResolveMeta` / go:embed → `internal/cli` root (thin re-exports of annotate/store APIs)
- Description declare vs delivery: construction requires `ContractDecl.Description` (evidence). Catalog delivery prefers Cobra Long → provenance `cobra_help`; without Long, declared text → `contract_final`. Title: declared first, then Short, then MCP. Do **not** read this as "declare = wire final" or dual authority.
- **Execute** = hooks (`Validate` / `Call` / `RunE` / `PostMount`) — not a second surface authority
- Declaration path has **no reviewed parallel fields**; migration-only `runtime_gate` annotate until `Safety` is declared
@@ -41,7 +47,7 @@ changes must not rewrite it mechanically.
- Hard rule: every help/Schema fact is **declared** **or** **annotated**; never inference-only (§1.1–§1.3; framework §5.0).
- Embed path: `corecmd.New` → `dws.schema.*` annotations → Schema catalog assembly
- MCP metadata must not create CLI flags; optional 1:1 passthrough is a gated subset only.
- Planned gate IDs: `HOM-P*`, `HOM-S*`, `HOM-I1`, `HOM-D1` (see that doc §4).
- Gate IDs: `HOM-P*`, `HOM-S*`, `HOM-I1`, `HOM-D1` (see that doc §3–§4). `HOM-P1`/`HOM-D1`/`HOM-S1`/`HOM-S2` are on the `check-schema-catalog.sh` policy whitelist; remaining IDs land incrementally.
## Agent Schema contract
@@ -379,10 +385,13 @@ lower-precedence source. A higher-precedence declaration may still raise an
optional flag to required. `cli_required` continues to mirror the executable
Cobra marker.
For command-level description, Cobra Long wins when present (delivered
provenance `cobra_help`, resolution `cobra_help_preferred`); without Long,
ContractDecl description is delivered as `contract_final`. Title keeps declared
ContractDecl / ContractFinal first, then Cobra Short, then MCP metadata.
For command-level description: **declare required, delivery Long may win**.
`ContractDecl.Description` is mandatory at construction (declaration evidence).
Catalog delivery prefers Cobra Long when present (provenance `cobra_help`,
resolution `cobra_help_preferred`); without Long, the declared Description is
delivered as `contract_final`. Title keeps declared ContractDecl /
ContractFinal first, then Cobra Short, then MCP metadata. This is one authority
chain with an explicit delivery preference — not two competing sources.
Generic RPC prose may remain an unselected provenance candidate (and
parameter-level `interface_description`); it must not overwrite a specialized
leaf's title or description.
+1 -1
View File
@@ -9,7 +9,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
### Changed
- **Hints retired; ContractDecl is the leaf Schema source** (#830) — `schema_hints/`, Manual/Schema hint overlays, and `schema_agent_metadata/` delivery are removed. Selection, safety, parameters, and interface facts declare on ProductDecl / leaf `Contract` (`corecmd.ContractDecl` + `contract.ParamDecl` / `Safety`). Authoring renamed `SchemaDecl` → `ContractDecl`; nested fields reuse `contract.*` directly.
- **Contract package seam** (#830) — types and registries live only under `internal/corecmd/contract`. Production registration is single-entry `cli.RegisterRuntimeContractFinal` (annotate + store); `cli` no longer redefines contract type aliases.
- **Contract package seam** (#830) — types / ProductDecl live under `internal/corecmd/contract` (DTO only). Annotate writers live in `internal/cli/runtimeannotate`; Cobra-keyed ContractFinal store + Register live in `internal/cli/contractfinal`; homology gates in `internal/cli/homology`. `cli` root keeps Catalog/`ResolveMeta` delivery and thin re-exports. `corecmd` no longer imports `internal/cli`.
- **CommandMeta index for ResolveMeta** (#830) — generation publishes `schema_meta_index.json` alongside Catalog ToolSpec wire under `schema_catalog/`. Runtime `ResolveMeta` / `SafetyForCLIPath` read the meta-index only and do not decode the full embedded Catalog.
### Fixed
+4 -4
View File
@@ -49,8 +49,8 @@
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─── 执行体 (恰好一个) ─────────────────────────────────────────┐ │
│ │ Invoke(Ctx, toolArgs) ← 单步:框架装配好 args 后派发 │ │
│ │ Orchestrate(Ctx) ← 多步:自行组装多次调用 │ │
│ │ Invoke(Ctx, toolArgs) ← #830 过渡:单步派发(目标 mcpbind)│ │
│ │ Orchestrate(Ctx) ← #830 过渡:多步编排(目标 Handler)│ │
│ │ RunE(cmd, args) ← 逃生舱:完全自定义 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
@@ -195,8 +195,8 @@ contract.DryRunSpec{
| 执行体 | 适用场景 | 框架做了什么 |
|--------|----------|-------------|
| **Invoke** | 单步 MCP/后端调用 | 框架完成 required→constraint→validate→buildArgs→confirm,传入装配好的 toolArgs |
| **Orchestrate** | 多步编排 | 框架完成 required→constraint→validate→confirm,传入 Ctx 自行组装调用 |
| **Invoke** | #830 过渡单步派发(生产仍用;目标 mcpbind) | 框架完成 required→constraint→validate→buildArgs→confirm,传入装配好的 toolArgs |
| **Orchestrate** | #830 过渡多步编排(生产仍用;目标 Handler) | 框架完成 required→constraint→validate→confirm,传入 Ctx 自行组装调用 |
| **RunE** | 逃生舱 | 框架仍执行 Safety 确认,具体业务执行完全自定义 |
## 设计不变量
+6 -6
View File
@@ -35,9 +35,9 @@ type Spec struct {
ConstParams map[string]any
Contract ContractDecl // 叶子 Contract 声明(非 Catalog Schema)
// 执行面(恰好一个)
Invoke func(c *Ctx, toolArgs map[string]any) error // 单步
Orchestrate func(c *Ctx) error // 多步
// 执行面(恰好一个;Invoke/Orchestrate 为 #830 过渡派发 API,目标 mcpbind+Handler)
Invoke func(c *Ctx, toolArgs map[string]any) error // 过渡:单步
Orchestrate func(c *Ctx) error // 过渡:多步
RunE func(cmd *cobra.Command, args []string) error // 逃生舱
// 钩子
@@ -153,8 +153,8 @@ ConstParams 合并
[!ConfirmFirst? → ConfirmSafety] ← 默认顺序:校验后确认
│
▼
Invoke(ctx, toolArgs) ← 单步派发
或 Orchestrate(ctx) ← 多步编排
Invoke(ctx, toolArgs) ← #830 过渡:单步派发
或 Orchestrate(ctx) ← #830 过渡:多步编排
```
## 消费方式
@@ -218,7 +218,7 @@ DeclareLeafMetadata(baseListCmd, LeafSpec{
})
```
`DeclareLeafMetadata` 调用 `corecmd.AttachContract` 挂 Safety+Contract;不注册 flag、不接管参数投影。当 `Safety.Confirmation=user_required` 时,用**同一份** SafetySpec 包一层 `ConfirmSafety`,保证执行门禁与 Catalog 同源。迁移态入口;新命令仍应走 `NewLeafCommand`。
`DeclareLeafMetadata` 调用 `corecmd.AttachContract` 挂 Safety+Contract;不注册 flag、不接管参数投影。可选 `Validate` 与 `ConfirmSafety` 同挂在 **RunE 包装器**内(不是 PreRunE)。当 `Safety.Confirmation=user_required` 时,用**同一份** SafetySpec 包一层 `ConfirmSafety`,保证执行门禁与 Catalog 同源;无 Validate 时确认推迟到 gated `CallTool`,成功返回却未确认则 fail-closed。迁移态入口;新命令仍应走 `NewLeafCommand`。
### Shortcut(智能快捷方式,已接入 live mount)
+31 -21
View File
@@ -156,7 +156,7 @@ NewLeafCommand(LeafSpec{
|---|---|---|
| Identity | 评审源 | `schema_command_registry` |
| Parameters.`name/type/required/default/property` | **声明**(或同形 annotate) | `Flags` / `Bind` |
| Parameters.`description` | 声明 usage + 可选 hints overlay | `Usage`;hints 不得改 type/required/default |
| Parameters.`description` | 声明 usage(`FlagSpec.Usage` / ParamDecl) | `schema_hints/` 已退役;不得用 overlay 改 type/required/default |
| Parameters.`interface_*` | 评审源 | MCP meta / bindings;**不造 flag** |
| Constraints | **声明** | `Constraints` |
| Positionals | **声明** 或显式 annotate | 目标 `Args`;禁止推断 |
@@ -192,14 +192,24 @@ Identity 与 selection **刻意不**由 Contract 取代(RFC 决策 8 / schema
- Flags / ConstParams / Constraints → 注册、校验与 `ConstraintHelp`;SafetySpec → 运行时 `ConfirmSafety`(command);
- Call / Execute 作为执行体;业务参数不得在 Call 内装配(helpers 门禁);
- **Contract → Schema 嵌入**:参数/约束写原生 annotation,SafetySpec 与 ContractDecl 注册为类型化 Contract Final 并由 Schema 组装透传。
- **Contract → Schema 嵌入**:参数/约束写原生 annotation,SafetySpec 与 ContractDecl 注册为类型化 Contract Final 并由 Schema 组装透传;
- Selection 权威为 `ContractDecl.Selection` / `ProductDecl`(`contract_final`);`schema_hints/` 已退役。
仍缺:
已进 CI(`make policy` → `check-schema-catalog.sh` / `check-runtime-confirmation-truth.sh`):
1. `HOM-P*` / `HOM-D1` 门禁:禁止 hints 静默改写 type/required/default;help ≡ schema parameters;
2. 重新 `make generate-schema` 后把嵌入结果固化进签入 catalog(注意本机 OOM);identity/selection 仍走评审源。
| Gate ID | CI 入口(`-run` 白名单 / 脚本) |
|---|---|
| `HOM-P1` / `HOM-D1` | `./internal/app`:`TestFinalSchemaParametersMatchExecutableHelpFlags`、`TestEmbeddedSchemaParametersMatchExecutableHelpFlags` |
| `HOM-P2`(参数映射/bindings 子集) | `./internal/cli`:`TestSchemaParameterBindingsMatchReviewedBaselineAndEmbeddedCatalog`、`TestEmbeddedCatalogMCPParameterMappingsAreComplete` 等 bindings 门禁 |
| `HOM-S1` / `HOM-S2`(confirmation 同源) | `./internal/cli/homology`:`TestUserRequiredSafetyHomologyWithRuntimeGate` + `check-runtime-confirmation-truth.sh`;`./internal/app`:`TestSheetFinalSchemaConfirmationMatchesRuntimeGuards` |
| 词汇/决策钉扎 | `./internal/cli/homology`:`TestHomologyDecisionDocPinsPathAAndGateIDs`、`TestMCPPassthroughAdmissionExcludesLeafAndShortcut`、`TestHomologyCIEntrypointsPinned` |
已落地(写命令确认语义,`HOM-S2` 起点):
仍缺(未宣称全量 CI 覆盖):
1. 独立可执行的 `HOM-P3`(constraints ≡ AnnotateConstraints)与 `HOM-S3`(read 不得误投影 user_required)全量 gate;
2. `HOM-I1` 作为单独 gate ID 的显式用例(MCP bindings ⊆ Contract flags 已有映射审计子集,但未钉 `HOM-I1` 标签)。
已落地(写命令确认语义,`HOM-S2`):
- `AnnotateRuntimeGate` / `dws.schema.runtime_gate`;Leaf `PostMount: devAppMetaWrite`;手写 delete/robot 等同路径显式标注;
- Schema 组装在无 Contract Safety 但有 gate 时 overlay `confirmation=user_required`(`applyContractGateToSafety`);
@@ -207,24 +217,24 @@ Identity 与 selection **刻意不**由 Contract 取代(RFC 决策 8 / schema
## 4. Schema 投影与 Safety 门禁规划
以下门禁在落地时应作为独立 policy / 测试交付(门禁 ID 稳定,便于 CI 认领)。
以下门禁 ID 稳定,便于 CI 认领。§3 表标明哪些已挂入 `check-schema-catalog.sh`。
| Gate ID | 断言 | 范围 |
|---|---|---|
| `HOM-P1` | 受管 leaf 的 schema `parameters[].name` 集合 ≡ cobra 本地 flag 名集合(排除全局 persistent) | LeafSpec / 未来 Contract 编译命令 |
| `HOM-P2` | schema parameter `type` / `required` / `default` 与 cobra DefValue / MarkFlagRequired / FlagSpec 一致;hints overlay **不得**改写这三项,只可补 description | 同上 |
| `HOM-P3` | schema 关系约束(require_one_of / mutually_exclusive)≡ Contract/Leaf `Constraints` 投影(与 `AnnotateConstraints` 同构) | 声明了 Constraints 的命令 |
| `HOM-S1` | 若 Contract/Leaf `Risk` ∈ {write, high-risk-write},则 schema `confirmation=user_required`,且 help Safety 行与之同语义 | 使用 Risk 确认的受管命令 |
| `HOM-S2` | 若命令走显式 write guard(如 `devAppRequireWriteGuard`)而非 Risk,则必须人工标注 `dws.schema.runtime_gate`(或等价 reviewed Safety);Schema 不得呈 `confirmation=not_required`;符合 §1.1 declare OR annotate | 今日 devapp 写命令 |
| `HOM-S3` | `Risk=read`(或空→read)不得投影为 `user_required`,除非有 reviewed exclusion reason | 受管读命令 |
| `HOM-I1` | `interface_ref` 存在时,bindings 覆盖的 CLI flag ⊆ Contract flags;MCP meta **不**引入额外 CLI flag | 有 MCP 绑定的命令 |
| `HOM-D1` | `dws <path> --help` Flags 段与 schema leaf parameters 零未解释增量 | 抽样 + 受管全集逐步扩大 |
| Gate ID | 断言 | 范围 | CI |
|---|---|---|---|
| `HOM-P1` | 受管 leaf 的 schema `parameters[].name` 集合 ≡ cobra 本地 flag 名集合(排除全局 persistent) | LeafSpec / Contract 编译命令 | **已进**(app help↔schema) |
| `HOM-P2` | schema parameter `type` / `required` / `default` 与 cobra DefValue / MarkFlagRequired / FlagSpec 一致;不得用已退役 hints overlay 改写这三项 | 同上 | **部分**(bindings/mapping 门禁) |
| `HOM-P3` | schema 关系约束(require_one_of / mutually_exclusive)≡ Contract/Leaf `Constraints` 投影(与 `AnnotateConstraints` 同构) | 声明了 Constraints 的命令 | 规划 |
| `HOM-S1` | Contract/Leaf `user_required` Safety 与运行时 Confirm/gate 同源,且 help Safety 行同语义 | 受管写/破坏性命令 | **已进** |
| `HOM-S2` | 若命令走显式 write guard(如 `devAppRequireWriteGuard`)而非完整 SafetySpec,则必须人工标注 `dws.schema.runtime_gate`;Schema 不得呈 `confirmation=not_required`;符合 §1.1 declare OR annotate | 今日 devapp 写命令 | **已进**(同源测试含 gate 路径) |
| `HOM-S3` | `Risk=read`(或空→read)不得投影为 `user_required`,除非有 reviewed exclusion reason | 受管读命令 | 规划 |
| `HOM-I1` | `interface_ref` 存在时,bindings 覆盖的 CLI flag ⊆ Contract flags;MCP meta **不**引入额外 CLI flag | 有 MCP 绑定的命令 | **部分**(mapping 审计) |
| `HOM-D1` | `dws <path> --help` Flags 段与 schema leaf parameters 零未解释增量 | 受管公开 leaf | **已进**(与 HOM-P1 同测) |
落地顺序建议:
1. 先对 LeafSpec 命令实现 `HOM-P1`/`HOM-P2`/`HOM-P3`(反射 cobra 即可起步,因 flag 已由 Contract 注册);
2. 再收 `HOM-S1`–`S3`(需产品确认 devapp 是否升格 Risk,或保持 guard + 诚实 provenance);
3. `HOM-I1`/`HOM-D1` 接入 `check-schema-*` / PR 门禁。
1. 保持 `HOM-P1`/`HOM-D1`/`HOM-S1`/`HOM-S2` 在 `check-schema-catalog.sh` 白名单中(勿再只靠词汇钉扎);
2. 补独立 `HOM-P3` / `HOM-S3` 可执行 gate,并把 `HOM-P2`/`HOM-I1` 从「部分」升到全量标签;
3. 新 Leaf 继续走 Contract 嵌入;禁止 `schema_hints/` 回潮。
## 5. 路径 B 子通道:1:1 MCP 透传叶(可选,非主路径)
@@ -245,6 +255,6 @@ Identity 与 selection **刻意不**由 Contract 取代(RFC 决策 8 / schema
## 6. 非目标
- 不把 selection 文案或 canonical identity 塞进 Contract。
- 不把 canonical identity / 导航塞进 Contract(仍归 reviewed `CommandRegistry`);selection 文案已由 `ContractDecl.Selection` / `ProductDecl` 声明(非 hints)。
- 不要求删除 LeafSpec 门面。
- 不把「生成 catalog 字节一致」当作运行时同源的充分条件(仍需 `HOM-*` 与差分门禁)。
+22 -22
View File
@@ -3,11 +3,11 @@
- **状态**:draft v5.1,征求设计评审
- **范围**:`internal/corecmd`、`internal/helpers`(`LeafSpec`)、`internal/shortcut`
- **实现基线**:PR #830 的 `6fb08c9e`
- **应丢弃的原型**:`0f08484d`(`Invoke` / `Orchestrate` / `Ctx`)
- **#830 过渡派发 API(仍生产使用)**:`Invoke` / `Orchestrate` / `Ctx`(见 §3 / §5.0.3;目标 mcpbind + Handler,**本里程碑不删**)
- **同源决策**:[`flag-help-schema-homology.md`](flag-help-schema-homology.md)(路径 A:Contract 权威)
- **框架声明定义**:本文 **§5.0**(今日 `corecmd.Spec`/`LeafSpec` 与目标 `Contract`)
本文档取代收敛方案的前三版草案。它保留了 PR #830 中有价值的部分——一份声明同时驱动运行时校验、Runtime Schema 和帮助信息,外加严格的漂移门禁——但不把原型提交引入的派发 API 固化下来。
本文档取代收敛方案的前三版草案。它保留了 PR #830 中有价值的部分——一份声明同时驱动运行时校验、Runtime Schema 和帮助信息,外加严格的漂移门禁。`Invoke` / `Orchestrate` / `Ctx` 是 **#830 过渡派发 API**:今日仍在生产路径上,终态目标是 mcpbind 绑定 + Handler;不得在文档里写成「已丢弃」或要求本里程碑完整删派发 API。
v5.1 的变更:
@@ -190,21 +190,21 @@ LeafSpec 已经在生产中证明了声明驱动的执行:27 个源定义每
## 4. 为什么最初的 S1 模型不是目标架构
原型新增了:
#830 过渡派发 API(**仍生产使用**,见文首)形态:
```go
RunE func(*cobra.Command, []string) error
Invoke func(*Ctx, map[string]any) error
Orchestrate func(*Ctx) error
Invoke func(*Ctx, map[string]any) error // 过渡:单步
Orchestrate func(*Ctx) error // 过渡:多步
```
它不稳定,有四个原因。
它不是终态架构,有四个原因。完整替换为 mcpbind + Handler 是后续里程碑,**不是**本 RFC 落地 PR 的删除义务。
### 4.1 `Invoke` 是由下一步就要移除的 MCP 泄漏所定义的
### 4.1 `Invoke` 是由下一步要收敛掉的 MCP 泄漏所定义的
`Invoke` 与 `Orchestrate` 之间唯一本质的区别,是 `command` 会在 `Invoke` 之前构建 `map[string]any`。一旦载荷绑定移出核心,两种形态都变成 `func(*Runtime) error`。
`Invoke` 与 `Orchestrate` 之间唯一本质的区别,是 `corecmd` 会在 `Invoke` 之前构建 `map[string]any`。一旦载荷绑定移出核心,两种形态都变成 `func(*Runtime) error`。
因此旧的 S4 并不是一条独立的清理链。载荷绑定的分离与 handler 模型必须一起设计。
因此旧的 S4 并不是一条独立的清理链。载荷绑定的分离与 handler 模型必须一起设计;在那之前,过渡 API 继续服务 Leaf/Shortcut。
### 4.2 `RunE` 会让已声明的安全契约变成假话
@@ -273,10 +273,10 @@ Definition(仅声明;不可编译)
| 层 | 含义 | 今日落点 |
|---|---|---|
| **声明(declare)** | `corecmd.Spec` / `LeafSpec` / `ContractDecl` **数据字段** = Schema 最终值 | `Flags`/`Constraints`/`Risk`/`ConstParams`/`Contract`;类型真身在 `corecmd/contract`(`SafetySpec`/`ParamDecl`/`ProductDecl`/`ContractFinalPayload`) |
| **框架转换** | 类型转换并注册(**禁止** JSON 注解桥) | `embedContractDecl` → `cli.RegisterRuntimeContractFinal`(annotate + store seam) |
| **注解 seam** | Cobra `dws.schema.*` 写入 | `cli.AnnotateRuntime*`(framework 可调用;**不**在 cli 再定义 contract 类型) |
| **Schema 透传** / 交付 | 组装读取注册表,原样投影为 `ToolSpec`;Catalog embed / `ResolveMeta` | `internal/cli`(交付边界,不搬入 contract) |
| **声明(declare)** | `corecmd.Spec` / `LeafSpec` / `ContractDecl` **数据字段**(声明证据;交付见下) | `Flags`/`Constraints`/`Risk`/`ConstParams`/`Contract`;类型真身在 `corecmd/contract`(DTO:`SafetySpec`/`ParamDecl`/`ProductDecl`/`ContractFinalPayload`;**无** Cobra store) |
| **框架转换** | 类型转换并注册(**禁止** JSON 注解桥) | `embedContractDecl` → `contractfinal.RegisterRuntimeContractFinal`(annotate + store;产品代码经 `cli.RegisterRuntimeContractFinal` 薄 re-export) |
| **注解 seam** | Cobra `dws.schema.*` 写入 | `internal/cli/runtimeannotate.AnnotateRuntime*`(`corecmd` 可依赖;**不** import `cli` 根;cli 根薄 re-export) |
| **Schema 透传** / 交付 | 组装读取注册表,原样投影为 `ToolSpec`;Catalog embed / `ResolveMeta` | `internal/cli` 根(交付边界);ContractFinal store 在 `cli/contractfinal` |
| **执行(execute)** | 钩子不发明表面 | `Validate` / `Call` / `RunE` / `PostMount` |
硬规则:
@@ -329,8 +329,8 @@ Definition(仅声明;不可编译)
```text
今日 LeafSpec / corecmd.Spec.Flags|Constraints|Safety|…
≈ 目标 Definition.Contract
今日 Call / Invoke / Orchestrate / RunE
≈ 目标 mcpbind 派发 或 Handler(形态 1/2/3)
今日 Call / Invoke / Orchestrate / RunE(#830 过渡派发仍生产使用)
≈ 目标 mcpbind 派发 或 Handler(形态 1/2/3;完整删派发 API 属后续里程碑)
今日 Validate / PostMount
≈ 目标 Validators / 收窄后的挂载钩子(不得再充当第二套 flag 权威)
```
@@ -344,11 +344,11 @@ Definition(仅声明;不可编译)
| Schema 字段组 | 子字段 / 内容 | 权威类 | 今日写入面 | 框架声明? |
|---|---|---|---|---|
| **Identity** | `product_id`, `name`, `cli_name`, `canonical_path` / `cli_path` / `primary_cli_path`, `group`, `aliases`, `source`, `source_product_id` | 绑定树 entry(registry 绑定结果) | `schema_command_registry`(+ reviewed manual additions);`ContractDecl.Identity` 可声明但**必须与绑定一致**,不一致组装报错 | 可声明(钉扎/自描述),**不得改绑** |
| **Display / Title / Description** | 产品展示名;工具 title/description | **title**:ContractDecl 声明优先,否则 Cobra Short;**description**:Cobra Long 优先(provenance `cobra_help` / `cobra_help_preferred`),无 Long 时用 ContractDecl description(`contract_final`) | registry 产品名;`ContractDecl` + Cobra Short/Long;组装 stamp 真实 winner | Short/Long 可声明;**canonical 文案不以 Contract 胜 identity**;description 以 Long 为交付权威 |
| **Display / Title / Description** | 产品展示名;工具 title/description | **title**:ContractDecl 声明优先,否则 Cobra Short,再 MCP;**description**:**构造期** `ContractDecl.Description` **必填**(声明证据);**Catalog 交付** Cobra Long 优先(provenance `cobra_help` / `cobra_help_preferred`),无 Long 用声明(`contract_final`) | registry 产品名;`ContractDecl` + Cobra Short/Long;组装 stamp 真实 winner | 非「declare = wire 最终值」、非双权威:声明必填 + 交付 Long 可赢;**canonical 文案不以 Contract 胜 identity** |
| **Parameters** | `name`, `type`, `required` / `cli_required`, `default` | **声明**(或手写 annotate 同形) | `Flags` → `embedContractIntoSchema` / cobra | **是** |
| | `description`(usage 文案) | 声明 usage | `FlagSpec.Usage` / ParamDecl;`schema_hints/` 已退役 | usage **是**;不得用 hint overlay 改 type/required/default |
| | `property`(载荷键) | 声明 | `FlagSpec.Bind`(空则 Name) | **是**(载荷映射) |
| | `enum`, `format`, `example`, `required_when` | 声明或评审 annotate | 今日部分仍 hints/手工;目标进 Contract / reviewed 约束 | 有则须 declare 或 reviewed annotate |
| | `enum`, `format`, `example`, `required_when` | 声明或评审 annotate | 今日部分仍手工 annotate;目标进 Contract / reviewed 约束(`schema_hints/` 已退役) | 有则须 declare 或 reviewed annotate |
| | `interface_description`, `interface_type`, `interface_default` | 评审源(interface) | `schema_mcp_metadata` + bindings | 否;**不得创建 CLI flag**(`HOM-I1`) |
| **Constraints** | `require_one_of`, `mutually_exclusive`, `require_together` | **声明** | `Constraints` → `AnnotateConstraints` | **是** |
| **Positionals** | 位置参数名/必填/说明 | **声明** 或显式 annotate | 目标 `Args`/`PositionalSpec`;今日少量 cobra Args + 注解 | 受管命令应声明,禁止推断 |
@@ -359,7 +359,7 @@ Definition(仅声明;不可编译)
| **Interface** | `interface_mode`, `interface_ref`, `availability`, `reason` | 评审源 | MCP meta + agent metadata 解析 | 否;与 CLI Identity 分离 |
| **Selection** | `agent_summary`, `use_when`, `avoid_when`, `examples`, `prerequisites`, `tips`, `workflow_refs`, … | 声明(`ContractDecl.Selection` / `ProductDecl`) | `ContractDecl` / `ProductDecl`(`schema_hints/` 已退役) | 可声明;声明载荷**不得携带** `Reviewed`(旧路径专用),携带即组装报错 |
| **FieldProvenance** | 各字段 winner / candidates | 组装派生物 | Schema 组装器 | 派生;须与 delivered value 一致 |
| **Extensions / MetadataSource** | 扩展袋;元数据来源标记 | 评审源或组装标记 | hints / embedded MCP / resolver | 不构成 CLI 表面 |
| **Extensions / MetadataSource** | 扩展袋;元数据来源标记 | 评审源或组装标记 | embedded MCP / resolver(禁止 hints 回潮) | 不构成 CLI 表面 |
| **ConstParams**(框架有、Schema parameters 无) | 固定 toolArgs | **声明**(载荷) | `ConstParams` | **是**(故意不上 parameter 表) |
产品级 Schema(`ProductSpec` 的 description / selection)权威在 registry + `ProductDecl`,**不在**单命令 Contract。
@@ -1165,11 +1165,11 @@ cmd.RunE = func(cmd *cobra.Command, args []string) error {
| Runtime Schema / Agent 暴露 | 经评审的 CommandRegistry 加精确排除 | 每个暴露叶子解析到活 Contract;排除显式且不重叠 |
| Safety 与运行时确认 | 可执行 Contract 的完整 `contract.SafetySpec`,或迁移期显式 annotate(如 `runtime_gate`);见 §5.0 | `confirmation` 单独驱动运行时门,`effect` / `risk` / `idempotency` 原样发布,禁止跨字段机械推导;任一 Safety 字段非空时四字段必须齐全,否则构造期 panic;`ConfirmFirst` 只在 `confirmation=user_required` 时合法 |
| 后端 product/tool/载荷绑定 | mcpbind + 后端元数据 | 每个绑定引用真实的 flag/属性 |
| Agent 选择文案(`use_when`、`avoid_when`、摘要) | 经评审的 hints/catalog | 身份解析到活契约 |
| Agent 选择文案(`use_when`、`avoid_when`、摘要) | 声明:`ContractDecl.Selection` / `ProductDecl`(交付 provenance `contract_final`);`schema_hints/` 已退役,禁止回潮 | 身份解析到活契约;选择文案不得创建 CLI 表面 |
冲突在 CI 中失败闭合。手工 hints 可纠正文案或后端事实,但不得静默覆盖可执行 CLI 行为。
冲突在 CI 中失败闭合。Selection / Safety / 参数事实须声明在 ProductDecl 或 owning leaf 的 ContractFinal 上;不得用 hints overlay 静默覆盖可执行 CLI 行为,也不得把 hints 写成 selection 权威。
该权威表不取代现有的多源 Schema 解析器。它使每个字段组的优先级显式:Contract 供给可执行/原生层,而经评审的覆盖、版本化绑定、排除与后端元数据保留其声明职责。
该权威表不取代现有的多源 Schema 解析器。它使每个字段组的优先级显式:Contract / ProductDecl 供给可执行与 Agent 选择层,而经评审的 registry、版本化绑定、排除与后端元数据保留其声明职责。
### 5.11 声明式 dry-run 计划
@@ -1711,7 +1711,7 @@ H0 不属于本 RFC 的实现序列,列在此处只为记录其与 M2/M3 的
| P6c | 一个稳定发布 / 至少 30 天后:移除遗留,运行 M5b 阶段 B(删除已转换 `Execute` 函数体),收窄钩子 | 清理 |
| P7 | 手写迁移与 Schema 权威工作 | 单独项目 |
PR #830 可携带 RFC 供评审,但不得把 `Invoke`/`Orchestrate` 原型原样推送为已接受的 P1 API。
PR #830 可携带 RFC 供评审。`Invoke`/`Orchestrate`/`Ctx` 是已合并的**过渡派发 API**(生产仍用),不得误标为「已接受的终态 P1 API」,也不得在未完成 mcpbind+Handler 里程碑前要求完整删除。
---
+17 -2
View File
@@ -25,6 +25,7 @@
package cli
import (
"fmt"
"sort"
"strings"
"sync"
@@ -62,17 +63,28 @@ var (
// initMetaByCLIPath builds the cli_path → CommandMeta lookup from the embedded
// CommandMeta summary index. It does not decode the full schema_catalog/.
// Decode failure is fail-closed: the error is retained and ResolveMeta panics
// rather than serving an empty map that would silently hide help Safety.
func initMetaByCLIPath() {
runtimeEmbeddedSchemaMetaIndexLazyCount.Add(1)
lookup, err := decodeSchemaMetaIndexLookup(embeddedSchemaMetaIndexJSON)
if err != nil {
runtimeEmbeddedSchemaMetaIndexErr = err
metaByCLIPath = map[string]CommandMeta{}
metaByCLIPath = nil
return
}
metaByCLIPath = lookup
}
// panicIfMetaIndexUnusable fails closed when the embedded CommandMeta summary
// could not be decoded. Callers must not treat this as "command missing".
func panicIfMetaIndexUnusable(err error) {
if err == nil {
return
}
panic(fmt.Sprintf("embedded schema_meta_index.json is unusable: %v", err))
}
// buildMetaByCLIPath constructs the lookup from a loaded catalog.
// Prefer the typed Registry (production cold-start path). Fall back to
// Snapshot.Tools maps for unit tests that synthesize untyped fixtures.
@@ -185,9 +197,12 @@ func registerCommandMetaAliases(lookup map[string]CommandMeta, metas []CommandMe
// meta index (utility commands, hidden commands, shortcuts).
//
// ResolveMeta reads schema_meta_index.json only; it never triggers the full
// embeddedSchemaCatalog() decode used by dws schema / --all.
// embeddedSchemaCatalog() decode used by dws schema / --all. A corrupt or
// undecodable meta index panics (fail-closed) so help Safety cannot silently
// disappear behind an empty lookup.
func ResolveMeta(cliPath string) (CommandMeta, bool) {
metaByCLIPathOnce.Do(initMetaByCLIPath)
panicIfMetaIndexUnusable(runtimeEmbeddedSchemaMetaIndexErr)
m, ok := metaByCLIPath[strings.TrimSpace(cliPath)]
return m, ok
}
+1
View File
@@ -44,6 +44,7 @@ func (s CommandSafety) ShouldRender() bool {
// CLI path (e.g. "dev app delete"). Returns ok=false when the path is absent
// from the embedded schema_meta_index.json (utility commands, hidden commands,
// shortcuts). Delegates to ResolveMeta; it does not decode schema_catalog/.
// A corrupt meta index panics via ResolveMeta (fail-closed).
//
// Deprecated: use ResolveMeta(cliPath).Safety for the complete metadata view.
// Kept for backward compatibility with existing callers.
+22
View File
@@ -14,6 +14,8 @@
package cli
import (
"fmt"
"strings"
"testing"
)
@@ -69,6 +71,26 @@ func TestSafetyForCLIPathTrimsWhitespace(t *testing.T) {
}
}
func TestResolveMetaFailsClosedOnUnusableMetaIndex(t *testing.T) {
// Guard is unit-tested without poisoning the process-wide sync.Once used
// by the real embedded index. Decode failure must panic, not look like a
// missing CLI path (which would silently drop help Safety).
if _, err := decodeSchemaMetaIndexLookup([]byte(`{not-json`)); err == nil {
t.Fatal("decodeSchemaMetaIndexLookup(corrupt) error = nil, want fail-closed decode error")
}
defer func() {
r := recover()
if r == nil {
t.Fatal("panicIfMetaIndexUnusable(err) did not panic")
}
msg, _ := r.(string)
if msg == "" || !strings.Contains(msg, "schema_meta_index.json is unusable") {
t.Fatalf("panic = %#v, want unusable meta-index message", r)
}
}()
panicIfMetaIndexUnusable(fmt.Errorf("decode schema meta index: unexpected EOF"))
}
func TestResolveMetaComplete(t *testing.T) {
// dev app delete: destructive + high + user_required + 有 selection。
m, ok := ResolveMeta("dev app delete")
@@ -18,42 +18,18 @@ import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
)
func TestContractFinalTypedRegistryNoJSON(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) })
RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
Title: "T",
Safety: &contract.SafetySpec{
Effect: "write", Confirmation: "user_required", Idempotency: "retryable",
},
Selection: &contract.SelectionSpec{AgentSummary: "sum", UseWhen: []string{"u"}},
Identity: &contract.ToolIdentitySpec{ProductID: "p", Name: "n"},
})
if cmd.Annotations != nil {
if _, ok := cmd.Annotations["dws.schema.final"]; ok {
t.Fatal("must not write JSON annotation dws.schema.final")
}
}
got, ok := contract.RuntimeContractFinal(cmd)
if !ok || got.Title != "T" || got.Safety == nil || got.Safety.Idempotency != "retryable" {
t.Fatalf("got %#v ok=%v", got, ok)
}
if got.Selection == nil || got.Selection.Reviewed != nil {
t.Fatalf("selection must not carry reviewed fields: %#v", got.Selection)
}
}
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalPassThrough(t *testing.T) {
cmd := &cobra.Command{Use: "create", Short: "s", Long: "l"}
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) })
t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(cmd) })
cmd.Flags().String("mode", "", "usage")
AnnotateRuntimeFlag(cmd, "mode", "mode", "string", false, "")
RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
runtimeannotate.AnnotateRuntimeFlag(cmd, "mode", "mode", "string", false, "")
contractfinal.RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
Title: "Final Title",
Safety: &contract.SafetySpec{
Effect: "write", Confirmation: "user_required", Idempotency: "none",
@@ -151,100 +127,6 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalRejectsReviewedSel
}
}
func mustFinal(t *testing.T, cmd *cobra.Command) contract.ContractFinalPayload {
t.Helper()
final, ok := contract.RuntimeContractFinal(cmd)
if !ok {
t.Fatal("missing final")
}
return final
}
func TestCrossPlatformCoverageContractFinalNilCommandGuards(t *testing.T) {
// Nil command registration/lookup must be inert no-ops.
RegisterRuntimeContractFinal(nil, contract.ContractFinalPayload{Title: "ignored"})
if _, ok := contract.RuntimeContractFinal(nil); ok {
t.Fatal("contract.RuntimeContractFinal(nil) must report no payload")
}
if contract.HasRuntimeContractFinal(nil) {
t.Fatal("contract.HasRuntimeContractFinal(nil) must be false")
}
contract.ClearRuntimeContractFinalForTest(nil)
}
func TestCrossPlatformCoverageApplyParamDeclsSkipsBlankAndAnnotatesEnum(t *testing.T) {
cmd := &cobra.Command{Use: "apply-params"}
cmd.Flags().String("mode", "", "mode")
required := false
if err := ApplyParamDecls(cmd, []contract.ParamDecl{
{Name: " "}, // blank names are skipped
{
Name: "mode", Property: "mode", Required: &required,
InterfaceType: "string", Description: "mode desc",
RequiredWhen: "when create", Enum: []string{"a", "b"},
},
}); err != nil {
t.Fatalf("ApplyParamDecls() error = %v", err)
}
flag := cmd.Flags().Lookup("mode")
if flag == nil {
t.Fatal("missing mode flag")
}
if got := flag.Annotations["dws.schema.property"]; len(got) == 0 || got[0] != "mode" {
t.Fatalf("property = %#v", flag.Annotations["dws.schema.property"])
}
if got := flag.Annotations["x-cli-enum"]; len(got) != 2 || got[0] != "a" || got[1] != "b" {
t.Fatalf("enum = %#v", flag.Annotations["x-cli-enum"])
}
if err := ApplyParamDecls(nil, []contract.ParamDecl{{Name: "mode"}}); err != nil {
t.Fatalf("ApplyParamDecls(nil) error = %v", err)
}
if err := ApplyParamDecls(cmd, nil); err != nil {
t.Fatalf("ApplyParamDecls(nil decls) error = %v", err)
}
}
func TestApplyParamDeclsRejectsUnknownFlag(t *testing.T) {
cmd := &cobra.Command{Use: "apply-params"}
cmd.Flags().String("mode", "", "mode")
err := ApplyParamDecls(cmd, []contract.ParamDecl{
{Name: "mode", Property: "mode"},
{Name: "missing-flag", Property: "missing"},
})
if err == nil {
t.Fatal("ApplyParamDecls() error = nil, want unknown flag")
}
if !strings.Contains(err.Error(), "missing-flag") {
t.Fatalf("ApplyParamDecls() error = %v, want missing-flag", err)
}
if !strings.Contains(err.Error(), "unknown flag") {
t.Fatalf("ApplyParamDecls() error = %v, want unknown flag", err)
}
flag := cmd.Flags().Lookup("mode")
if flag == nil {
t.Fatal("missing mode flag")
}
if got := flag.Annotations["dws.schema.property"]; len(got) != 0 {
t.Fatalf("fail-closed must not annotate before unknown ParamDecl; property = %#v", got)
}
}
func TestCrossPlatformCoverageRuntimeContractFinalRejectsForeignStoredValue(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) })
// Defensive branch: a stored value that is not a *contract.ContractFinalPayload
// (or a typed nil) must fail the read instead of panicking.
contract.StoreRuntimeContractFinalRawForTest(cmd, "not-a-payload")
if _, ok := contract.RuntimeContractFinal(cmd); ok {
t.Fatal("foreign stored value must not decode as contract.ContractFinalPayload")
}
contract.StoreRuntimeContractFinalRawForTest(cmd, (*contract.ContractFinalPayload)(nil))
if _, ok := contract.RuntimeContractFinal(cmd); ok {
t.Fatal("typed nil payload must not decode as contract.ContractFinalPayload")
}
}
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityOverridesApplied(t *testing.T) {
cmd := &cobra.Command{Use: "create"}
entry := runtimeSchemaEntry{
@@ -321,7 +203,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFa
// Safety nil + Contract Risk annotation: Risk overlay wins.
riskCmd := &cobra.Command{Use: "create"}
AnnotateRuntimeRisk(riskCmd, "write")
runtimeannotate.AnnotateRuntimeRisk(riskCmd, "write")
entry.Command = riskCmd
spec, err := runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{})
if err != nil {
@@ -336,7 +218,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFa
// Safety nil + runtime gate annotation: gate overlay wins.
gateCmd := &cobra.Command{Use: "create"}
AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard")
runtimeannotate.AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard")
entry.Command = gateCmd
spec, err = runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{})
if err != nil {
@@ -366,3 +248,12 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalParameterResolutio
t.Fatalf("parameter resolution error = %v, want resolve Contract Schema parameters", err)
}
}
func mustFinal(t *testing.T, cmd *cobra.Command) contract.ContractFinalPayload {
t.Helper()
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok {
t.Fatal("missing final")
}
return final
}
@@ -0,0 +1,61 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
func TestCrossPlatformCoverageApplyContractRiskToSafety(t *testing.T) {
base := contract.SafetySpec{Idempotency: "unknown", Risk: "low", Confirmation: "not_required", Effect: "read"}
got := applyContractRiskToSafety(base, "high-risk-write")
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
t.Fatalf("high-risk overlay = %#v", got)
}
if got.Idempotency != "unknown" {
t.Fatalf("idempotency should be preserved, got %q", got.Idempotency)
}
got = applyContractRiskToSafety(base, "write")
if got.Effect != "write" || got.Risk != "medium" || got.Confirmation != "user_required" {
t.Fatalf("write overlay = %#v", got)
}
got = applyContractRiskToSafety(base, "read")
if got.Effect != "read" || got.Risk != "low" || got.Confirmation != "not_required" {
t.Fatalf("read overlay = %#v", got)
}
}
func TestCrossPlatformCoverageApplyContractGateToSafety(t *testing.T) {
base := contract.SafetySpec{Confirmation: "not_required", Effect: "read", Risk: "low"}
got := applyContractGateToSafety(base, "devAppRequireWriteGuard")
if got.Confirmation != "user_required" {
t.Fatalf("gate must force user_required, got %#v", got)
}
if got.Effect != "write" || got.EffectSource != "corecmd.contract_gate" {
t.Fatalf("gate effect overlay = %#v", got)
}
if got.Risk != "medium" {
t.Fatalf("gate risk overlay = %#v", got)
}
reviewed := contract.SafetySpec{Confirmation: "not_required", Effect: "destructive", Risk: "high", EffectSource: "reviewed"}
got = applyContractGateToSafety(reviewed, "devAppRequireWriteGuard")
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
t.Fatalf("gate must keep reviewed effect/risk: %#v", got)
}
if got = applyContractGateToSafety(base, " "); got != base {
t.Fatalf("blank gate must be a no-op, got %#v", got)
}
}
-138
View File
@@ -1,138 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageAnnotateRuntimeRiskEmbedsContractMarker(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeRisk(cmd, "write")
got, ok := RuntimeContractRisk(cmd)
if !ok || got != "write" {
t.Fatalf("RuntimeContractRisk = %q %v", got, ok)
}
if cmd.Annotations[runtimeSchemaContractAnnotation] != "command" {
t.Fatalf("contract marker = %q", cmd.Annotations[runtimeSchemaContractAnnotation])
}
AnnotateRuntimeRisk(cmd, "")
if got, _ = RuntimeContractRisk(cmd); got != "write" {
t.Fatalf("empty AnnotateRuntimeRisk must be no-op, got %q", got)
}
}
func TestCrossPlatformCoverageApplyContractRiskToSafety(t *testing.T) {
base := contract.SafetySpec{Idempotency: "unknown", Risk: "low", Confirmation: "not_required", Effect: "read"}
got := applyContractRiskToSafety(base, "high-risk-write")
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
t.Fatalf("high-risk overlay = %#v", got)
}
if got.Idempotency != "unknown" {
t.Fatalf("idempotency should be preserved, got %q", got.Idempotency)
}
got = applyContractRiskToSafety(base, "write")
if got.Effect != "write" || got.Risk != "medium" || got.Confirmation != "user_required" {
t.Fatalf("write overlay = %#v", got)
}
got = applyContractRiskToSafety(base, "read")
if got.Effect != "read" || got.Risk != "low" || got.Confirmation != "not_required" {
t.Fatalf("read overlay = %#v", got)
}
}
func TestCrossPlatformCoverageAnnotateRuntimeGateDeclareOrAnnotate(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
if HasDeclaredOrAnnotatedConfirmation(cmd) {
t.Fatal("bare command must not claim confirmation coverage")
}
AnnotateRuntimeGate(cmd, "devAppRequireWriteGuard")
got, ok := RuntimeContractGate(cmd)
if !ok || got != "devAppRequireWriteGuard" {
t.Fatalf("RuntimeContractGate = %q %v", got, ok)
}
if !HasDeclaredOrAnnotatedConfirmation(cmd) {
t.Fatal("annotated gate must satisfy declare-OR-annotate")
}
if cmd.Annotations[runtimeSchemaContractAnnotation] != "command" {
t.Fatalf("contract marker = %q", cmd.Annotations[runtimeSchemaContractAnnotation])
}
}
func TestCrossPlatformCoverageApplyContractGateToSafety(t *testing.T) {
base := contract.SafetySpec{Confirmation: "not_required", Effect: "read", Risk: "low"}
got := applyContractGateToSafety(base, "devAppRequireWriteGuard")
if got.Confirmation != "user_required" {
t.Fatalf("gate must force user_required, got %#v", got)
}
if got.Effect != "write" || got.EffectSource != "corecmd.contract_gate" {
t.Fatalf("gate effect overlay = %#v", got)
}
if got.Risk != "medium" {
t.Fatalf("gate risk overlay = %#v", got)
}
reviewed := contract.SafetySpec{Confirmation: "not_required", Effect: "destructive", Risk: "high", EffectSource: "reviewed"}
got = applyContractGateToSafety(reviewed, "devAppRequireWriteGuard")
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
t.Fatalf("gate must keep reviewed effect/risk: %#v", got)
}
if got = applyContractGateToSafety(base, " "); got != base {
t.Fatalf("blank gate must be a no-op, got %#v", got)
}
}
func TestCrossPlatformCoverageRuntimeContractAnnotationNilAndBlankGuards(t *testing.T) {
// Nil-command annotators must be safe no-ops.
AnnotateRuntimeFlagDescription(nil, "flag", "description")
AnnotateRuntimeContract(nil)
AnnotateRuntimeRisk(nil, "write")
AnnotateRuntimeGate(nil, "devAppRequireWriteGuard")
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeGate(cmd, " ")
if _, ok := RuntimeContractGate(cmd); ok {
t.Fatal("blank AnnotateRuntimeGate must not record a gate")
}
if cmd.Annotations != nil && cmd.Annotations[runtimeSchemaContractAnnotation] == "command" {
t.Fatal("blank AnnotateRuntimeGate must not mark the command as Contract")
}
// A present-but-blank gate annotation must read back as no gate.
blank := &cobra.Command{Use: "y", Annotations: map[string]string{
runtimeSchemaRuntimeGateAnnotation: " ",
}}
if _, ok := RuntimeContractGate(blank); ok {
t.Fatal("blank runtime_gate annotation must not report a gate")
}
}
func TestCrossPlatformCoverageHasDeclaredOrAnnotatedConfirmationDeclaredAndRiskBranches(t *testing.T) {
declared := &cobra.Command{Use: "declared"}
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) })
RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{
Safety: &contract.SafetySpec{Confirmation: "not_required"},
})
if !HasDeclaredOrAnnotatedConfirmation(declared) {
t.Fatal("typed Contract contract.SafetySpec confirmation must satisfy declare-OR-annotate")
}
risky := &cobra.Command{Use: "risky"}
AnnotateRuntimeRisk(risky, "read")
if !HasDeclaredOrAnnotatedConfirmation(risky) {
t.Fatal("Contract Risk annotation must satisfy declare-OR-annotate")
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package contractfinal owns the Cobra-keyed ContractFinal runtime store and
// the annotate+store registration seam.
//
// Production product / helper / shortcut code should call
// cli.RegisterRuntimeContractFinal (thin re-export). Framework code
// (corecmd.AttachContract) calls RegisterRuntimeContractFinal here directly
// so internal/corecmd never imports the cli root delivery package.
//
// Types remain in internal/corecmd/contract (DTO only — no cobra store).
package contractfinal
+133
View File
@@ -0,0 +1,133 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package contractfinal
import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
)
func TestContractFinalTypedRegistryNoJSON(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
Title: "T",
Safety: &contract.SafetySpec{
Effect: "write", Confirmation: "user_required", Idempotency: "retryable",
},
Selection: &contract.SelectionSpec{AgentSummary: "sum", UseWhen: []string{"u"}},
Identity: &contract.ToolIdentitySpec{ProductID: "p", Name: "n"},
})
if cmd.Annotations != nil {
if _, ok := cmd.Annotations["dws.schema.final"]; ok {
t.Fatal("must not write JSON annotation dws.schema.final")
}
}
got, ok := RuntimeContractFinal(cmd)
if !ok || got.Title != "T" || got.Safety == nil || got.Safety.Idempotency != "retryable" {
t.Fatalf("got %#v ok=%v", got, ok)
}
if got.Selection == nil || got.Selection.Reviewed != nil {
t.Fatalf("selection must not carry reviewed fields: %#v", got.Selection)
}
}
func TestCrossPlatformCoverageContractFinalNilCommandGuards(t *testing.T) {
// Nil command registration/lookup must be inert no-ops.
RegisterRuntimeContractFinal(nil, contract.ContractFinalPayload{Title: "ignored"})
if _, ok := RuntimeContractFinal(nil); ok {
t.Fatal("RuntimeContractFinal(nil) must report no payload")
}
if HasRuntimeContractFinal(nil) {
t.Fatal("HasRuntimeContractFinal(nil) must be false")
}
ClearRuntimeContractFinalForTest(nil)
}
func TestCrossPlatformCoverageApplyParamDeclsSkipsBlankAndAnnotatesEnum(t *testing.T) {
cmd := &cobra.Command{Use: "apply-params"}
cmd.Flags().String("mode", "", "mode")
required := false
if err := ApplyParamDecls(cmd, []contract.ParamDecl{
{Name: " "}, // blank names are skipped
{
Name: "mode", Property: "mode", Required: &required,
InterfaceType: "string", Description: "mode desc",
RequiredWhen: "when create", Enum: []string{"a", "b"},
},
}); err != nil {
t.Fatalf("ApplyParamDecls() error = %v", err)
}
flag := cmd.Flags().Lookup("mode")
if flag == nil {
t.Fatal("missing mode flag")
}
if got := flag.Annotations["dws.schema.property"]; len(got) == 0 || got[0] != "mode" {
t.Fatalf("property = %#v", flag.Annotations["dws.schema.property"])
}
if got := flag.Annotations["x-cli-enum"]; len(got) != 2 || got[0] != "a" || got[1] != "b" {
t.Fatalf("enum = %#v", flag.Annotations["x-cli-enum"])
}
if err := ApplyParamDecls(nil, []contract.ParamDecl{{Name: "mode"}}); err != nil {
t.Fatalf("ApplyParamDecls(nil) error = %v", err)
}
if err := ApplyParamDecls(cmd, nil); err != nil {
t.Fatalf("ApplyParamDecls(nil decls) error = %v", err)
}
}
func TestApplyParamDeclsRejectsUnknownFlag(t *testing.T) {
cmd := &cobra.Command{Use: "apply-params"}
cmd.Flags().String("mode", "", "mode")
err := ApplyParamDecls(cmd, []contract.ParamDecl{
{Name: "mode", Property: "mode"},
{Name: "missing-flag", Property: "missing"},
})
if err == nil {
t.Fatal("ApplyParamDecls() error = nil, want unknown flag")
}
if !strings.Contains(err.Error(), "missing-flag") {
t.Fatalf("ApplyParamDecls() error = %v, want missing-flag", err)
}
if !strings.Contains(err.Error(), "unknown flag") {
t.Fatalf("ApplyParamDecls() error = %v, want unknown flag", err)
}
flag := cmd.Flags().Lookup("mode")
if flag == nil {
t.Fatal("missing mode flag")
}
if got := flag.Annotations["dws.schema.property"]; len(got) != 0 {
t.Fatalf("fail-closed must not annotate before unknown ParamDecl; property = %#v", got)
}
}
func TestCrossPlatformCoverageRuntimeContractFinalRejectsForeignStoredValue(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(cmd) })
// Defensive branch: a stored value that is not a *contract.ContractFinalPayload
// (or a typed nil) must fail the read instead of panicking.
StoreRuntimeContractFinalRawForTest(cmd, "not-a-payload")
if _, ok := RuntimeContractFinal(cmd); ok {
t.Fatal("foreign stored value must not decode as contract.ContractFinalPayload")
}
StoreRuntimeContractFinalRawForTest(cmd, (*contract.ContractFinalPayload)(nil))
if _, ok := RuntimeContractFinal(cmd); ok {
t.Fatal("typed nil payload must not decode as contract.ContractFinalPayload")
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package contractfinal
import (
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
)
// HasDeclaredOrAnnotatedConfirmation reports whether confirmation semantics are
// covered by typed Contract SafetySpec, legacy Shortcut Risk, or runtime_gate.
func HasDeclaredOrAnnotatedConfirmation(cmd *cobra.Command) bool {
if final, ok := RuntimeContractFinal(cmd); ok && final.Safety != nil &&
strings.TrimSpace(final.Safety.Confirmation) != "" {
return true
}
if _, ok := runtimeannotate.RuntimeContractRisk(cmd); ok {
return true
}
_, ok := runtimeannotate.RuntimeContractGate(cmd)
return ok
}
@@ -0,0 +1,51 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package contractfinal
import (
"testing"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
func TestHasDeclaredOrAnnotatedConfirmationBranches(t *testing.T) {
bare := &cobra.Command{Use: "bare"}
if HasDeclaredOrAnnotatedConfirmation(bare) {
t.Fatal("bare command must not claim confirmation coverage")
}
gated := &cobra.Command{Use: "gated"}
runtimeannotate.AnnotateRuntimeGate(gated, "devAppRequireWriteGuard")
if !HasDeclaredOrAnnotatedConfirmation(gated) {
t.Fatal("annotated gate must satisfy declare-OR-annotate")
}
declared := &cobra.Command{Use: "declared"}
t.Cleanup(func() { ClearRuntimeContractFinalForTest(declared) })
RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{
Safety: &contract.SafetySpec{Confirmation: "not_required"},
})
if !HasDeclaredOrAnnotatedConfirmation(declared) {
t.Fatal("typed Contract SafetySpec confirmation must satisfy declare-OR-annotate")
}
risky := &cobra.Command{Use: "risky"}
runtimeannotate.AnnotateRuntimeRisk(risky, "read")
if !HasDeclaredOrAnnotatedConfirmation(risky) {
t.Fatal("Contract Risk annotation must satisfy declare-OR-annotate")
}
}
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
package contractfinal
import (
"fmt"
@@ -19,26 +19,10 @@ import (
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
// Delivery-seam helpers that must live in cli because they emit Cobra
// annotations (AnnotateRuntime*). Types and registries live in
// corecmd/contract; callers author contract.SafetySpec / contract.ParamDecl /
// contract.ContractFinalPayload directly — there is no cli type alias layer.
// RegisterRuntimeContractFinal is the sole production registration entry for
// ContractFinal: annotate (dws.schema.contract) then store the typed payload.
// Framework code (corecmd.AttachContract / New) must use this seam rather than
// calling contract.RegisterRuntimeContractFinal directly.
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload contract.ContractFinalPayload) {
if cmd == nil {
return
}
AnnotateRuntimeContract(cmd)
contract.RegisterRuntimeContractFinal(cmd, payload)
}
// ApplyParamDecls emits parameter declarations as dws.schema.* annotations on the
// command's flags. Called at assembly time when all flags exist on the tree.
// Each non-blank ParamDecl.Name must resolve to an existing Cobra flag;
@@ -52,7 +36,7 @@ func ApplyParamDecls(cmd *cobra.Command, decls []contract.ParamDecl) error {
if name == "" {
continue
}
if runtimeCommandFlag(cmd, name) == nil {
if runtimeannotate.CommandFlag(cmd, name) == nil {
return fmt.Errorf("ParamDecl %q references unknown flag on %q", name, cmd.CommandPath())
}
}
@@ -62,27 +46,23 @@ func ApplyParamDecls(cmd *cobra.Command, decls []contract.ParamDecl) error {
continue
}
if prop := strings.TrimSpace(p.Property); prop != "" {
AnnotateRuntimeFlagProperty(cmd, name, prop)
runtimeannotate.AnnotateRuntimeFlagProperty(cmd, name, prop)
}
if p.Required != nil {
AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required)
runtimeannotate.AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required)
}
if it := strings.TrimSpace(p.InterfaceType); it != "" {
AnnotateRuntimeFlagInterfaceType(cmd, name, it)
runtimeannotate.AnnotateRuntimeFlagInterfaceType(cmd, name, it)
}
if desc := strings.TrimSpace(p.Description); desc != "" {
AnnotateRuntimeFlagDescription(cmd, name, desc)
runtimeannotate.AnnotateRuntimeFlagDescription(cmd, name, desc)
}
if rw := strings.TrimSpace(p.RequiredWhen); rw != "" {
AnnotateRuntimeFlagRequiredWhen(cmd, name, rw)
runtimeannotate.AnnotateRuntimeFlagRequiredWhen(cmd, name, rw)
}
if len(p.Enum) > 0 {
AnnotateRuntimeFlagEnum(cmd, name, p.Enum...)
runtimeannotate.AnnotateRuntimeFlagEnum(cmd, name, p.Enum...)
}
}
return nil
}
func resolvedFieldProvenance(value any, source, sourceRef, precedence, resolution, reviewReason string) contract.FieldProvenance {
return contract.ResolvedFieldProvenance(value, source, sourceRef, precedence, resolution, reviewReason)
}
+78
View File
@@ -0,0 +1,78 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package contractfinal
import (
"sync"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
var contractFinalByCommand sync.Map // *cobra.Command → *contract.ContractFinalPayload
// RegisterRuntimeContractFinal annotates dws.schema.contract then stores the
// typed final Schema overlay. This is the atomic annotate+store implementation.
//
// cli.RegisterRuntimeContractFinal is the delivery-facing name for product code;
// corecmd.AttachContract calls this function directly.
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload contract.ContractFinalPayload) {
if cmd == nil {
return
}
runtimeannotate.AnnotateRuntimeContract(cmd)
p := payload
contractFinalByCommand.Store(cmd, &p)
}
// RuntimeContractFinal returns the registered final Schema overlay (read-only).
func RuntimeContractFinal(cmd *cobra.Command) (contract.ContractFinalPayload, bool) {
if cmd == nil {
return contract.ContractFinalPayload{}, false
}
raw, ok := contractFinalByCommand.Load(cmd)
if !ok {
return contract.ContractFinalPayload{}, false
}
p, ok := raw.(*contract.ContractFinalPayload)
if !ok || p == nil {
return contract.ContractFinalPayload{}, false
}
return *p, true
}
// HasRuntimeContractFinal reports whether the leaf has a registered final overlay.
func HasRuntimeContractFinal(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
_, ok := contractFinalByCommand.Load(cmd)
return ok
}
// ClearRuntimeContractFinalForTest removes a registration (tests only).
func ClearRuntimeContractFinalForTest(cmd *cobra.Command) {
if cmd != nil {
contractFinalByCommand.Delete(cmd)
}
}
// StoreRuntimeContractFinalRawForTest injects a raw map value (tests only).
func StoreRuntimeContractFinalRawForTest(cmd *cobra.Command, raw any) {
if cmd != nil {
contractFinalByCommand.Store(cmd, raw)
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package homology holds Schema/CLI homology and ContractFinal consistency
// gates that exercise the live command tree and delivered Catalog.
//
// These tests intentionally live outside the cli root so contract / homology
// policy noise does not clutter the delivery package surface. They import
// internal/cli and related packages; they do not redefine delivery types.
package homology
@@ -1,7 +1,7 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli_test
package homology
import (
"encoding/json"
@@ -16,6 +16,7 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
@@ -82,7 +83,7 @@ func TestAllCommandsContractFinalConsistentWithLiveAndEmbeddedCatalog(t *testing
rows = append(rows, row{canonical, cliPath, "FAIL", "absent from embedded full leaf shard"})
continue
}
final, has := contract.RuntimeContractFinal(cmd.PrimaryCommand)
final, has := contractfinal.RuntimeContractFinal(cmd.PrimaryCommand)
if !has {
failCount++
rows = append(rows, row{canonical, cliPath, "FAIL", "no contract.RuntimeContractFinal on PrimaryCommand"})
@@ -207,7 +208,8 @@ func repoCLIDir() string {
if !ok {
return "."
}
return filepath.Dir(file)
// This file lives in internal/cli/homology; schema_catalog is under internal/cli.
return filepath.Clean(filepath.Join(filepath.Dir(file), ".."))
}
func loadEmbeddedFullLeafTools() (map[string]embedToolView, error) {
@@ -1,10 +1,10 @@
package cli_test
package homology
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
)
func TestContractFinalFoundForHrbrain(t *testing.T) {
@@ -13,7 +13,7 @@ func TestContractFinalFoundForHrbrain(t *testing.T) {
if err != nil || hrbrain == nil {
t.Fatalf("hrbrain search employees not found: %v", err)
}
final, ok := contract.RuntimeContractFinal(hrbrain)
final, ok := contractfinal.RuntimeContractFinal(hrbrain)
if !ok {
t.Fatal("ContractFinal NOT found for hrbrain search employees — this is the root cause of contract.ParamDecl not working")
}
@@ -26,7 +26,7 @@ func TestContractFinalFoundForAisearch(t *testing.T) {
if err != nil || enterprise == nil {
t.Fatalf("aisearch enterprise not found: %v", err)
}
final, ok := contract.RuntimeContractFinal(enterprise)
final, ok := contractfinal.RuntimeContractFinal(enterprise)
if !ok {
t.Fatal("ContractFinal NOT found for aisearch enterprise")
}
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
package homology
import (
"os"
@@ -35,12 +35,26 @@ var plannedHomologyGateIDs = []string{
"HOM-D1", // --help Flags ≡ schema leaf parameters
}
// ciHomologyEntrypoints are executable gates already wired into
// scripts/policy/check-schema-catalog.sh (and confirmation-truth helper).
// Vocabulary-only pinning is not enough: these names must stay on the policy
// -run whitelist.
var ciHomologyEntrypoints = []string{
"TestUserRequiredSafetyHomologyWithRuntimeGate",
"TestHomologyDecisionDocPinsPathAAndGateIDs",
"TestMCPPassthroughAdmissionExcludesLeafAndShortcut",
"TestHomologyCIEntrypointsPinned",
"TestFinalSchemaParametersMatchExecutableHelpFlags",
"TestEmbeddedSchemaParametersMatchExecutableHelpFlags",
"TestSheetFinalSchemaConfirmationMatchesRuntimeGuards",
}
func TestHomologyDecisionDocPinsPathAAndGateIDs(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "docs", "flag-help-schema-homology.md"))
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "docs", "flag-help-schema-homology.md"))
raw, err := os.ReadFile(doc)
if err != nil {
t.Fatalf("read homology doc: %v", err)
@@ -62,6 +76,8 @@ func TestHomologyDecisionDocPinsPathAAndGateIDs(t *testing.T) {
"dws.schema.runtime_gate",
"mcp_passthrough",
"不得覆盖 LeafSpec / Shortcut 主路径",
"已进 CI",
"check-schema-catalog.sh",
} {
if !strings.Contains(body, needle) {
t.Fatalf("homology doc missing %q", needle)
@@ -79,7 +95,7 @@ func TestMCPPassthroughAdmissionExcludesLeafAndShortcut(t *testing.T) {
if !ok {
t.Fatal("runtime.Caller failed")
}
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "docs", "flag-help-schema-homology.md"))
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "docs", "flag-help-schema-homology.md"))
raw, err := os.ReadFile(doc)
if err != nil {
t.Fatalf("read homology doc: %v", err)
@@ -96,3 +112,40 @@ func TestMCPPassthroughAdmissionExcludesLeafAndShortcut(t *testing.T) {
}
}
}
func TestHomologyCIEntrypointsPinned(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
root := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", ".."))
script := filepath.Join(root, "scripts", "policy", "check-schema-catalog.sh")
raw, err := os.ReadFile(script)
if err != nil {
t.Fatalf("read check-schema-catalog.sh: %v", err)
}
body := string(raw)
for _, name := range ciHomologyEntrypoints {
if !strings.Contains(body, name) {
t.Fatalf("check-schema-catalog.sh missing homology CI entrypoint %q", name)
}
}
if !strings.Contains(body, "./internal/cli/homology") {
t.Fatal("check-schema-catalog.sh must run ./internal/cli/homology for HOM gate tests")
}
doc := filepath.Join(root, "docs", "flag-help-schema-homology.md")
docRaw, err := os.ReadFile(doc)
if err != nil {
t.Fatalf("read homology doc: %v", err)
}
docBody := string(docRaw)
for _, name := range []string{
"TestUserRequiredSafetyHomologyWithRuntimeGate",
"TestFinalSchemaParametersMatchExecutableHelpFlags",
"TestHomologyCIEntrypointsPinned",
} {
if !strings.Contains(docBody, name) {
t.Fatalf("homology doc §3 CI table missing %q", name)
}
}
}
@@ -1,4 +1,4 @@
package cli_test
package homology
import (
"testing"
@@ -1,7 +1,7 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli_test
package homology
import (
"errors"
@@ -15,8 +15,8 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
)
@@ -87,7 +87,7 @@ func TestUserRequiredSafetyHomologyWithRuntimeGate(t *testing.T) {
fails = append(fails, row{canonical, cmd.PrimaryCLIPath, "", "nil PrimaryCommand"})
continue
}
final, hasFinal := contract.RuntimeContractFinal(leaf)
final, hasFinal := contractfinal.RuntimeContractFinal(leaf)
if !hasFinal || final.Safety == nil {
continue
}
-75
View File
@@ -1,75 +0,0 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package cli
import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
func TestProductDeclRegistryRoundTrip(t *testing.T) {
t.Cleanup(func() { contract.ClearProductDeclForTest("sample") })
contract.ClearProductDeclForTest("sample")
if contract.HasProductDecl("sample") {
t.Fatal("contract.HasProductDecl before register must be false")
}
contract.RegisterProductDecl(contract.ProductDecl{})
if contract.HasProductDecl("") {
t.Fatal("empty ID must not register")
}
contract.RegisterProductDecl(contract.ProductDecl{
ID: " sample ",
Selection: contract.ProductSelectionDecl{
AgentSummary: "Manage samples",
UseWhen: []string{"target is a sample"},
AvoidWhen: []string{"target is another product"},
},
})
if !contract.HasProductDecl("sample") {
t.Fatal("contract.HasProductDecl after register must be true")
}
got, ok := contract.LookupProductDecl("sample")
if !ok || got.ID != "sample" || got.Selection.AgentSummary != "Manage samples" {
t.Fatalf("contract.LookupProductDecl = %#v, ok=%v", got, ok)
}
ids := contract.RegisteredProductDeclIDs()
found := false
for _, id := range ids {
if id == "sample" {
found = true
break
}
}
if !found {
t.Fatalf("contract.RegisteredProductDeclIDs missing sample: %#v", ids)
}
selection, provenance := contract.ProductSelectionFromDecl(got)
if selection.AgentSummary != "Manage samples" || selection.AgentSummarySource != contract.ProductDeclSourceRef {
t.Fatalf("contract.ProductSelectionFromDecl selection = %#v", selection)
}
for _, field := range []string{"agent_summary", "use_when", "avoid_when"} {
prov, ok := provenance[field]
if !ok || prov.Precedence != "contract_final" || prov.Source != contract.ProductDeclProvenanceSource {
t.Fatalf("field %s provenance = %#v", field, prov)
}
}
contract.ClearProductDeclForTest("sample")
if contract.HasProductDecl("sample") {
t.Fatal("contract.ClearProductDeclForTest must remove registration")
}
}
func TestProductDeclRegisterPanicsOnIncompleteSelection(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic for incomplete contract.ProductDecl")
}
}()
contract.RegisterProductDecl(contract.ProductDecl{ID: "broken"})
}
+37 -447
View File
@@ -25,6 +25,7 @@ import (
"sync"
"sync/atomic"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -34,39 +35,27 @@ import (
var embeddedMCPMetadataJSON []byte
const (
runtimeSchemaProductAnnotation = "dws.schema.product"
runtimeSchemaToolAnnotation = "dws.schema.tool"
runtimeSchemaSourceAnnotation = "dws.schema.source"
runtimeSchemaTitleAnnotation = "dws.schema.title"
runtimeSchemaDescAnnotation = "dws.schema.description"
runtimeSchemaMetaAnnotation = "dws.schema.metadata_source"
runtimeSchemaExcludeAnnotation = "dws.schema.exclude"
runtimeSchemaRulesAnnotation = "dws.schema.constraints"
runtimeSchemaArgsAnnotation = "dws.schema.positionals"
runtimeSchemaFlagPropertyAnnotation = "dws.schema.property"
runtimeSchemaFlagTypeAnnotation = "dws.schema.type"
runtimeSchemaFlagDescriptionAnnotation = "dws.schema.description"
runtimeSchemaFlagRequiredAnnotation = "dws.schema.required"
runtimeSchemaFlagRequiredWhenAnnotation = "dws.schema.required_when"
runtimeSchemaFlagExampleAnnotation = "dws.schema.example"
// Contract surface (command / LeafSpec) embedded onto the live Cobra leaf so
// Schema generation can project parameters/constraints/Risk without a second
// source of truth. See docs/flag-help-schema-homology.md.
runtimeSchemaContractAnnotation = "dws.schema.contract"
runtimeSchemaRiskAnnotation = "dws.schema.risk"
runtimeSchemaRuntimeGateAnnotation = "dws.schema.runtime_gate"
// Annotation keys are owned by runtimeannotate; aliases keep assembly readers stable.
runtimeSchemaProductAnnotation = runtimeannotate.AnnotationProduct
runtimeSchemaToolAnnotation = runtimeannotate.AnnotationTool
runtimeSchemaSourceAnnotation = runtimeannotate.AnnotationSource
runtimeSchemaTitleAnnotation = runtimeannotate.AnnotationTitle
runtimeSchemaDescAnnotation = runtimeannotate.AnnotationDescription
runtimeSchemaMetaAnnotation = runtimeannotate.AnnotationMetaSource
runtimeSchemaExcludeAnnotation = runtimeannotate.AnnotationExclude
runtimeSchemaRulesAnnotation = runtimeannotate.AnnotationConstraints
runtimeSchemaArgsAnnotation = runtimeannotate.AnnotationPositionals
runtimeSchemaFlagPropertyAnnotation = runtimeannotate.AnnotationFlagProperty
runtimeSchemaFlagTypeAnnotation = runtimeannotate.AnnotationFlagType
runtimeSchemaFlagDescriptionAnnotation = runtimeannotate.AnnotationDescription
runtimeSchemaFlagRequiredAnnotation = runtimeannotate.AnnotationFlagRequired
runtimeSchemaFlagRequiredWhenAnnotation = runtimeannotate.AnnotationFlagReqWhen
runtimeSchemaFlagExampleAnnotation = runtimeannotate.AnnotationFlagExample
runtimeSchemaContractAnnotation = runtimeannotate.AnnotationContract
runtimeSchemaRiskAnnotation = runtimeannotate.AnnotationRisk
runtimeSchemaRuntimeGateAnnotation = runtimeannotate.AnnotationRuntimeGate
)
// RuntimeSchemaConstraints describes cross-parameter rules that cannot be
// represented by an individual parameter's required bit.
type RuntimeSchemaConstraints struct {
MutuallyExclusive [][]string `json:"mutually_exclusive,omitempty"`
RequireOneOf [][]string `json:"require_one_of,omitempty"`
RequireTogether [][]string `json:"require_together,omitempty"`
}
type embeddedMCPMetadata struct {
Version int `json:"version"`
Source string `json:"source"`
@@ -169,258 +158,6 @@ func interfaceMetadataSummaryFrom(metadata embeddedMCPMetadata) map[string]any {
return summary
}
// AttachRuntimeSchema records optional implementation-side identity evidence
// on a runnable command. Command discovery belongs exclusively to the reviewed
// CommandRegistry; the Cobra binder accepts an absent annotation and rejects
// an annotation that disagrees with the registry.
func AttachRuntimeSchema(cmd *cobra.Command, productID, toolName, source string) {
if cmd == nil {
return
}
productID = strings.TrimSpace(productID)
toolName = strings.TrimSpace(toolName)
if productID == "" || toolName == "" {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations[runtimeSchemaProductAnnotation] = productID
cmd.Annotations[runtimeSchemaToolAnnotation] = toolName
if source = strings.TrimSpace(source); source != "" {
cmd.Annotations[runtimeSchemaSourceAnnotation] = source
}
}
// AnnotateRuntimeToolMetadata preserves MCP-provided tool metadata on a Cobra
// leaf so `dws schema` can render richer descriptions without refetching.
func AnnotateRuntimeToolMetadata(cmd *cobra.Command, title, description, source string) {
if cmd == nil {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
if title = strings.TrimSpace(title); title != "" {
cmd.Annotations[runtimeSchemaTitleAnnotation] = title
}
if description = strings.TrimSpace(description); description != "" {
cmd.Annotations[runtimeSchemaDescAnnotation] = description
}
if source = strings.TrimSpace(source); source != "" {
cmd.Annotations[runtimeSchemaMetaAnnotation] = source
}
}
// AnnotateRuntimeFlag adds parameter metadata to an already-registered flag.
// The metadata mirrors the runtime binding that produced the flag, allowing
// schema rendering to preserve MCP parameter names while displaying CLI flags.
func AnnotateRuntimeFlag(cmd *cobra.Command, flagName, propertyName, paramType string, required bool, _ string) {
if cmd == nil {
return
}
flagName = strings.TrimSpace(flagName)
if flagName == "" {
return
}
flag := runtimeCommandFlag(cmd, flagName)
if flag == nil {
return
}
setFlagAnnotation(flag, runtimeSchemaFlagPropertyAnnotation, strings.TrimSpace(propertyName))
setFlagAnnotation(flag, runtimeSchemaFlagTypeAnnotation, strings.TrimSpace(paramType))
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, strconv.FormatBool(required))
}
// AnnotateRuntimeFlagProperty records only the stable CLI flag to interface
// property binding. It intentionally does not copy required or constraints
// from an older Catalog into the current executable contract.
func AnnotateRuntimeFlagProperty(cmd *cobra.Command, flagName, propertyName string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagPropertyAnnotation, strings.TrimSpace(propertyName))
}
}
// AnnotateRuntimeRequiredFlags records schema-only required semantics. Unlike
// cobra.MarkFlagRequired, it does not require the primary flag itself to be
// changed, so helper commands can keep accepting hidden --url/--id aliases.
func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) {
if cmd == nil {
return
}
for _, name := range flagNames {
flag := runtimeCommandFlag(cmd, name)
if flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, "true")
}
}
}
// AnnotateRuntimeFlagRequiredValue sets an explicit required value ("true" or
// "false") on a flag's dws.schema.required annotation. Use this when a
// declaration needs to override a stale cobra-level or MCP-level required=true
// with an explicit false.
func AnnotateRuntimeFlagRequiredValue(cmd *cobra.Command, flagName string, required bool) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
v := "false"
if required {
v = "true"
}
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, v)
}
}
// AnnotateRuntimeFlagDescription records the Schema parameter description.
func AnnotateRuntimeFlagDescription(cmd *cobra.Command, flagName, description string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagDescriptionAnnotation, strings.TrimSpace(description))
}
}
// AnnotateRuntimeFlagRequiredWhen records a conditional CLI requirement. The
// expression is descriptive metadata and does not alter Cobra validation.
func AnnotateRuntimeFlagRequiredWhen(cmd *cobra.Command, flagName, expression string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagRequiredWhenAnnotation, strings.TrimSpace(expression))
}
}
// AnnotateRuntimeFlagFormat records a machine-readable value format without
// changing the Cobra flag type.
func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, "x-cli-format", strings.TrimSpace(format))
}
}
// AnnotateRuntimeFlagInterfaceType records the wire type for a flag's interface
// property (e.g. "string", "integer", "boolean", "array"). The resolver reads
// this as a native_annotation candidate for interface_type.
func AnnotateRuntimeFlagInterfaceType(cmd *cobra.Command, flagName, interfaceType string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagTypeAnnotation, strings.TrimSpace(interfaceType))
}
}
// AnnotateRuntimeFlagEnum records the accepted values for a flag.
func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...string) {
if cmd == nil {
return
}
flag := runtimeCommandFlag(cmd, flagName)
if flag == nil {
return
}
setFlagAnnotationValues(flag, "x-cli-enum", values...)
}
// AnnotateRuntimeFlagExample records a valid representative CLI value.
func AnnotateRuntimeFlagExample(cmd *cobra.Command, flagName, example string) {
if cmd == nil {
return
}
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
setFlagAnnotation(flag, runtimeSchemaFlagExampleAnnotation, strings.TrimSpace(example))
}
}
// AnnotateRuntimeContract marks a command as carrying a command/LeafSpec
// Contract surface. Schema assembly treats embedded flag annotations and, when
// set, AnnotateRuntimeRisk as Contract-authored facts (path A homology).
func AnnotateRuntimeContract(cmd *cobra.Command) {
if cmd == nil {
return
}
setRuntimeCommandAnnotation(cmd, runtimeSchemaContractAnnotation, "command")
}
// AnnotateRuntimeRisk records the Contract Risk string (read|write|high-risk-write)
// for Schema Safety projection. Empty risk is a no-op so write-guard leaves that
// leave Risk unset keep reviewed hint safety.
func AnnotateRuntimeRisk(cmd *cobra.Command, risk string) {
if cmd == nil {
return
}
risk = strings.TrimSpace(risk)
if risk == "" {
return
}
AnnotateRuntimeContract(cmd)
setRuntimeCommandAnnotation(cmd, runtimeSchemaRiskAnnotation, risk)
}
// AnnotateRuntimeGate records a non-Risk confirmation path (e.g.
// devAppRequireWriteGuard). Homology rule: every confirmation fact is either
// Contract-declared (Risk) or explicitly annotated (this gate / reviewed hints).
func AnnotateRuntimeGate(cmd *cobra.Command, gate string) {
if cmd == nil {
return
}
gate = strings.TrimSpace(gate)
if gate == "" {
return
}
AnnotateRuntimeContract(cmd)
setRuntimeCommandAnnotation(cmd, runtimeSchemaRuntimeGateAnnotation, gate)
}
// RuntimeContractRisk returns the Contract Risk annotation when present.
func RuntimeContractRisk(cmd *cobra.Command) (string, bool) {
if cmd == nil || cmd.Annotations == nil {
return "", false
}
risk := strings.TrimSpace(cmd.Annotations[runtimeSchemaRiskAnnotation])
if risk == "" {
return "", false
}
return risk, true
}
// RuntimeContractGate returns the annotated runtime confirmation gate when present.
func RuntimeContractGate(cmd *cobra.Command) (string, bool) {
if cmd == nil || cmd.Annotations == nil {
return "", false
}
gate := strings.TrimSpace(cmd.Annotations[runtimeSchemaRuntimeGateAnnotation])
if gate == "" {
return "", false
}
return gate, true
}
// HasDeclaredOrAnnotatedConfirmation reports whether confirmation semantics are
// covered by a typed Contract contract.SafetySpec, the legacy Shortcut Risk bridge, or
// an explicit runtime_gate annotation.
func HasDeclaredOrAnnotatedConfirmation(cmd *cobra.Command) bool {
if final, ok := contract.RuntimeContractFinal(cmd); ok && final.Safety != nil &&
strings.TrimSpace(final.Safety.Confirmation) != "" {
return true
}
if _, ok := RuntimeContractRisk(cmd); ok {
return true
}
_, ok := RuntimeContractGate(cmd)
return ok
}
// applyContractRiskToSafety overlays Schema Safety fields from an embedded
// Contract Risk value. Path A: Contract wins effect/risk/confirmation for the
// managed surface; other Safety fields (e.g. idempotency) are preserved.
@@ -464,96 +201,6 @@ func applyContractGateToSafety(base contract.SafetySpec, gate string) contract.S
return out
}
// AnnotateRuntimeConstraints records command-level parameter relationships.
func AnnotateRuntimeConstraints(cmd *cobra.Command, constraints RuntimeSchemaConstraints) {
if cmd == nil {
return
}
constraints = normalizeRuntimeSchemaConstraints(constraints)
if runtimeSchemaConstraintsEmpty(constraints) {
return
}
if existing := runtimeCommandConstraints(cmd); !runtimeSchemaConstraintsEmpty(existing) {
constraints.MutuallyExclusive = append(existing.MutuallyExclusive, constraints.MutuallyExclusive...)
constraints.RequireOneOf = append(existing.RequireOneOf, constraints.RequireOneOf...)
constraints.RequireTogether = append(existing.RequireTogether, constraints.RequireTogether...)
constraints = normalizeRuntimeSchemaConstraints(constraints)
}
encoded, _ := json.Marshal(constraints)
setRuntimeCommandAnnotation(cmd, runtimeSchemaRulesAnnotation, string(encoded))
}
// AnnotateRuntimePositionals records ordered positional arguments for agents.
func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...contract.RuntimeSchemaPositional) {
if cmd == nil {
return
}
clean := make([]contract.RuntimeSchemaPositional, 0, len(positionals))
for _, positional := range positionals {
positional.Name = strings.TrimSpace(positional.Name)
positional.Type = strings.TrimSpace(positional.Type)
positional.Description = strings.TrimSpace(positional.Description)
if positional.Name == "" || positional.Index < 0 {
continue
}
if positional.Type == "" {
positional.Type = "string"
}
clean = append(clean, positional)
}
if len(clean) == 0 {
return
}
sort.SliceStable(clean, func(i, j int) bool { return clean[i].Index < clean[j].Index })
encoded, _ := json.Marshal(clean)
setRuntimeCommandAnnotation(cmd, runtimeSchemaArgsAnnotation, string(encoded))
}
// ExcludeFromRuntimeSchema keeps a human-facing hint or redirect in --help
// while preventing it from being advertised as an executable agent tool.
func ExcludeFromRuntimeSchema(cmd *cobra.Command) {
setRuntimeCommandAnnotation(cmd, runtimeSchemaExcludeAnnotation, "true")
}
func setRuntimeCommandAnnotation(cmd *cobra.Command, key, value string) {
if cmd == nil || strings.TrimSpace(value) == "" {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations[key] = value
}
func setFlagAnnotation(flag *pflag.Flag, key, value string) {
if flag == nil || strings.TrimSpace(value) == "" {
return
}
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[key] = []string{value}
}
func setFlagAnnotationValues(flag *pflag.Flag, key string, values ...string) {
if flag == nil {
return
}
clean := make([]string, 0, len(values))
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
clean = append(clean, value)
}
}
if len(clean) == 0 {
return
}
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[key] = clean
}
type runtimeSchemaEntry struct {
ProductID string
SourceProductID string
@@ -1367,26 +1014,8 @@ func runtimeSchemaRequireOneOfContains(constraints RuntimeSchemaConstraints, nam
return false
}
// runtimeCommandFlag resolves local flags plus product/group persistent flags.
// Root persistent flags are intentionally available only when explicitly
// requested; they are global execution controls, not tool parameters.
func runtimeCommandFlag(cmd *cobra.Command, name string) *pflag.Flag {
if cmd == nil {
return nil
}
name = strings.TrimSpace(name)
if name == "" {
return nil
}
if flag := cmd.Flags().Lookup(name); flag != nil {
return flag
}
for current := cmd; current != nil; current = current.Parent() {
if flag := current.PersistentFlags().Lookup(name); flag != nil {
return flag
}
}
return nil
return runtimeannotate.CommandFlag(cmd, name)
}
func visitRuntimeCommandFlags(cmd *cobra.Command, inheritedNames []string, visit func(*pflag.Flag)) {
@@ -1436,74 +1065,35 @@ func visitRuntimeCommandFlags(cmd *cobra.Command, inheritedNames []string, visit
}
func runtimeCommandConstraints(cmd *cobra.Command) RuntimeSchemaConstraints {
if cmd == nil || cmd.Annotations == nil {
return RuntimeSchemaConstraints{}
}
raw := strings.TrimSpace(cmd.Annotations[runtimeSchemaRulesAnnotation])
if raw == "" {
return RuntimeSchemaConstraints{}
}
var constraints RuntimeSchemaConstraints
if json.Unmarshal([]byte(raw), &constraints) != nil {
return RuntimeSchemaConstraints{}
}
return normalizeRuntimeSchemaConstraints(constraints)
return runtimeannotate.CommandConstraints(cmd)
}
func runtimeCommandPositionals(cmd *cobra.Command) []contract.RuntimeSchemaPositional {
if cmd == nil || cmd.Annotations == nil {
return nil
}
raw := strings.TrimSpace(cmd.Annotations[runtimeSchemaArgsAnnotation])
if raw == "" {
return nil
}
var positionals []contract.RuntimeSchemaPositional
if json.Unmarshal([]byte(raw), &positionals) != nil {
return nil
}
sort.SliceStable(positionals, func(i, j int) bool { return positionals[i].Index < positionals[j].Index })
return positionals
return runtimeannotate.CommandPositionals(cmd)
}
func normalizeRuntimeSchemaConstraints(constraints RuntimeSchemaConstraints) RuntimeSchemaConstraints {
constraints.MutuallyExclusive = normalizeRuntimeSchemaGroups(constraints.MutuallyExclusive, 2)
constraints.RequireOneOf = normalizeRuntimeSchemaGroups(constraints.RequireOneOf, 1)
constraints.RequireTogether = normalizeRuntimeSchemaGroups(constraints.RequireTogether, 2)
return constraints
return runtimeannotate.NormalizeConstraints(constraints)
}
func normalizeRuntimeSchemaGroups(groups [][]string, minimum int) [][]string {
out := make([][]string, 0, len(groups))
seenGroups := map[string]bool{}
for _, group := range groups {
clean := make([]string, 0, len(group))
seenNames := map[string]bool{}
for _, name := range group {
name = strings.TrimSpace(name)
if name == "" || seenNames[name] {
continue
}
seenNames[name] = true
clean = append(clean, name)
}
if len(clean) < minimum {
continue
}
key := strings.Join(clean, "\x00")
if seenGroups[key] {
continue
}
seenGroups[key] = true
out = append(out, clean)
// Test-facing thin wrapper over NormalizeConstraints group rules.
c := RuntimeSchemaConstraints{}
switch minimum {
case 1:
c.RequireOneOf = groups
default:
c.MutuallyExclusive = groups
}
return out
out := runtimeannotate.NormalizeConstraints(c)
if minimum == 1 {
return out.RequireOneOf
}
return out.MutuallyExclusive
}
func runtimeSchemaConstraintsEmpty(constraints RuntimeSchemaConstraints) bool {
return len(constraints.MutuallyExclusive) == 0 &&
len(constraints.RequireOneOf) == 0 &&
len(constraints.RequireTogether) == 0
return runtimeannotate.ConstraintsEmpty(constraints)
}
func lookupEmbeddedMCPParam(params map[string]embeddedMCPParamMeta, property, flagName string) (embeddedMCPParamMeta, bool) {
@@ -10,6 +10,7 @@ import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -263,7 +264,9 @@ func TestCrossPlatformCoverageRuntimeSchemaPureHelperEdges(t *testing.T) {
if got := runtimeCommandPositionals(cmd); len(got) != 2 || got[0].Name != "first" {
t.Fatalf("sorted positionals = %#v", got)
}
groups := normalizeRuntimeSchemaGroups([][]string{{" "}, {" ", "one", "one"}, {"one"}, {"one"}}, 1)
groups := runtimeannotate.NormalizeConstraints(runtimeannotate.RuntimeSchemaConstraints{
RequireOneOf: [][]string{{" "}, {" ", "one", "one"}, {"one"}, {"one"}},
}).RequireOneOf
if !reflect.DeepEqual(groups, [][]string{{"one"}}) {
t.Fatalf("normalized groups = %#v", groups)
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
// Thin re-exports so helpers / app / shortcut keep the cli. prefix while the
// implementations live in focused subpackages. corecmd imports those
// subpackages directly and must not import this delivery root.
type RuntimeSchemaConstraints = runtimeannotate.RuntimeSchemaConstraints
var (
AttachRuntimeSchema = runtimeannotate.AttachRuntimeSchema
AnnotateRuntimeToolMetadata = runtimeannotate.AnnotateRuntimeToolMetadata
AnnotateRuntimeFlag = runtimeannotate.AnnotateRuntimeFlag
AnnotateRuntimeFlagProperty = runtimeannotate.AnnotateRuntimeFlagProperty
AnnotateRuntimeRequiredFlags = runtimeannotate.AnnotateRuntimeRequiredFlags
AnnotateRuntimeFlagRequiredValue = runtimeannotate.AnnotateRuntimeFlagRequiredValue
AnnotateRuntimeFlagDescription = runtimeannotate.AnnotateRuntimeFlagDescription
AnnotateRuntimeFlagRequiredWhen = runtimeannotate.AnnotateRuntimeFlagRequiredWhen
AnnotateRuntimeFlagFormat = runtimeannotate.AnnotateRuntimeFlagFormat
AnnotateRuntimeFlagInterfaceType = runtimeannotate.AnnotateRuntimeFlagInterfaceType
AnnotateRuntimeFlagEnum = runtimeannotate.AnnotateRuntimeFlagEnum
AnnotateRuntimeFlagExample = runtimeannotate.AnnotateRuntimeFlagExample
AnnotateRuntimeContract = runtimeannotate.AnnotateRuntimeContract
AnnotateRuntimeRisk = runtimeannotate.AnnotateRuntimeRisk
AnnotateRuntimeGate = runtimeannotate.AnnotateRuntimeGate
RuntimeContractRisk = runtimeannotate.RuntimeContractRisk
RuntimeContractGate = runtimeannotate.RuntimeContractGate
AnnotateRuntimeConstraints = runtimeannotate.AnnotateRuntimeConstraints
AnnotateRuntimePositionals = runtimeannotate.AnnotateRuntimePositionals
ExcludeFromRuntimeSchema = runtimeannotate.ExcludeFromRuntimeSchema
HasDeclaredOrAnnotatedConfirmation = contractfinal.HasDeclaredOrAnnotatedConfirmation
RuntimeContractFinal = contractfinal.RuntimeContractFinal
HasRuntimeContractFinal = contractfinal.HasRuntimeContractFinal
ClearRuntimeContractFinalForTest = contractfinal.ClearRuntimeContractFinalForTest
StoreRuntimeContractFinalRawForTest = contractfinal.StoreRuntimeContractFinalRawForTest
ApplyParamDecls = contractfinal.ApplyParamDecls
)
// RegisterRuntimeContractFinal is the sole production registration entry for
// product / helper / shortcut code (annotate + store). Framework code calls
// contractfinal.RegisterRuntimeContractFinal directly.
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload contract.ContractFinalPayload) {
contractfinal.RegisterRuntimeContractFinal(cmd, payload)
}
func resolvedFieldProvenance(value any, source, sourceRef, precedence, resolution, reviewReason string) contract.FieldProvenance {
return contract.ResolvedFieldProvenance(value, source, sourceRef, precedence, resolution, reviewReason)
}
func setRuntimeCommandAnnotation(cmd *cobra.Command, key, value string) {
runtimeannotate.SetCommandAnnotation(cmd, key, value)
}
func setFlagAnnotation(flag *pflag.Flag, key, value string) {
runtimeannotate.SetFlagAnnotation(flag, key, value)
}
func setFlagAnnotationValues(flag *pflag.Flag, key string, values ...string) {
runtimeannotate.SetFlagAnnotationValues(flag, key, values...)
}
+366
View File
@@ -0,0 +1,366 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtimeannotate
import (
"encoding/json"
"sort"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
// AttachRuntimeSchema records optional implementation-side identity evidence
// on a runnable command. Command discovery belongs exclusively to the reviewed
// CommandRegistry; the Cobra binder accepts an absent annotation and rejects
// an annotation that disagrees with the registry.
func AttachRuntimeSchema(cmd *cobra.Command, productID, toolName, source string) {
if cmd == nil {
return
}
productID = strings.TrimSpace(productID)
toolName = strings.TrimSpace(toolName)
if productID == "" || toolName == "" {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations[AnnotationProduct] = productID
cmd.Annotations[AnnotationTool] = toolName
if source = strings.TrimSpace(source); source != "" {
cmd.Annotations[AnnotationSource] = source
}
}
// AnnotateRuntimeToolMetadata preserves MCP-provided tool metadata on a Cobra
// leaf so `dws schema` can render richer descriptions without refetching.
func AnnotateRuntimeToolMetadata(cmd *cobra.Command, title, description, source string) {
if cmd == nil {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
if title = strings.TrimSpace(title); title != "" {
cmd.Annotations[AnnotationTitle] = title
}
if description = strings.TrimSpace(description); description != "" {
cmd.Annotations[AnnotationDescription] = description
}
if source = strings.TrimSpace(source); source != "" {
cmd.Annotations[AnnotationMetaSource] = source
}
}
// AnnotateRuntimeFlag adds parameter metadata to an already-registered flag.
func AnnotateRuntimeFlag(cmd *cobra.Command, flagName, propertyName, paramType string, required bool, _ string) {
if cmd == nil {
return
}
flagName = strings.TrimSpace(flagName)
if flagName == "" {
return
}
flag := CommandFlag(cmd, flagName)
if flag == nil {
return
}
SetFlagAnnotation(flag, AnnotationFlagProperty, strings.TrimSpace(propertyName))
SetFlagAnnotation(flag, AnnotationFlagType, strings.TrimSpace(paramType))
SetFlagAnnotation(flag, AnnotationFlagRequired, strconv.FormatBool(required))
}
// AnnotateRuntimeFlagProperty records only the stable CLI flag to interface
// property binding.
func AnnotateRuntimeFlagProperty(cmd *cobra.Command, flagName, propertyName string) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
SetFlagAnnotation(flag, AnnotationFlagProperty, strings.TrimSpace(propertyName))
}
}
// AnnotateRuntimeRequiredFlags records schema-only required semantics.
func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) {
if cmd == nil {
return
}
for _, name := range flagNames {
flag := CommandFlag(cmd, name)
if flag != nil {
SetFlagAnnotation(flag, AnnotationFlagRequired, "true")
}
}
}
// AnnotateRuntimeFlagRequiredValue sets an explicit required value on a flag.
func AnnotateRuntimeFlagRequiredValue(cmd *cobra.Command, flagName string, required bool) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
v := "false"
if required {
v = "true"
}
SetFlagAnnotation(flag, AnnotationFlagRequired, v)
}
}
// AnnotateRuntimeFlagDescription records the Schema parameter description.
func AnnotateRuntimeFlagDescription(cmd *cobra.Command, flagName, description string) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
SetFlagAnnotation(flag, AnnotationDescription, strings.TrimSpace(description))
}
}
// AnnotateRuntimeFlagRequiredWhen records a conditional CLI requirement.
func AnnotateRuntimeFlagRequiredWhen(cmd *cobra.Command, flagName, expression string) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
SetFlagAnnotation(flag, AnnotationFlagReqWhen, strings.TrimSpace(expression))
}
}
// AnnotateRuntimeFlagFormat records a machine-readable value format.
func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
SetFlagAnnotation(flag, AnnotationFlagFormat, strings.TrimSpace(format))
}
}
// AnnotateRuntimeFlagInterfaceType records the wire type for a flag's interface
// property.
func AnnotateRuntimeFlagInterfaceType(cmd *cobra.Command, flagName, interfaceType string) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
SetFlagAnnotation(flag, AnnotationFlagType, strings.TrimSpace(interfaceType))
}
}
// AnnotateRuntimeFlagEnum records the accepted values for a flag.
func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...string) {
if cmd == nil {
return
}
flag := CommandFlag(cmd, flagName)
if flag == nil {
return
}
SetFlagAnnotationValues(flag, AnnotationFlagEnum, values...)
}
// AnnotateRuntimeFlagExample records a valid representative CLI value.
func AnnotateRuntimeFlagExample(cmd *cobra.Command, flagName, example string) {
if cmd == nil {
return
}
if flag := CommandFlag(cmd, flagName); flag != nil {
SetFlagAnnotation(flag, AnnotationFlagExample, strings.TrimSpace(example))
}
}
// AnnotateRuntimeContract marks a command as carrying a command/LeafSpec
// Contract surface.
func AnnotateRuntimeContract(cmd *cobra.Command) {
if cmd == nil {
return
}
SetCommandAnnotation(cmd, AnnotationContract, "command")
}
// AnnotateRuntimeRisk records the Contract Risk string for Schema Safety
// projection. Empty risk is a no-op so write-guard leaves that leave Risk
// unset keep Contract.Safety / runtime_gate provenance (schema_hints/ is
// retired and must not be treated as a Safety fallback).
func AnnotateRuntimeRisk(cmd *cobra.Command, risk string) {
if cmd == nil {
return
}
risk = strings.TrimSpace(risk)
if risk == "" {
return
}
AnnotateRuntimeContract(cmd)
SetCommandAnnotation(cmd, AnnotationRisk, risk)
}
// AnnotateRuntimeGate records a non-Risk confirmation path (e.g.
// devAppRequireWriteGuard). Homology rule: every confirmation fact is either
// Contract-declared (SafetySpec / Risk) or explicitly annotated (this gate).
// schema_hints/ is retired and must not supply confirmation.
func AnnotateRuntimeGate(cmd *cobra.Command, gate string) {
if cmd == nil {
return
}
gate = strings.TrimSpace(gate)
if gate == "" {
return
}
AnnotateRuntimeContract(cmd)
SetCommandAnnotation(cmd, AnnotationRuntimeGate, gate)
}
// RuntimeContractRisk returns the Contract Risk annotation when present.
func RuntimeContractRisk(cmd *cobra.Command) (string, bool) {
if cmd == nil || cmd.Annotations == nil {
return "", false
}
risk := strings.TrimSpace(cmd.Annotations[AnnotationRisk])
if risk == "" {
return "", false
}
return risk, true
}
// RuntimeContractGate returns the annotated runtime confirmation gate when present.
func RuntimeContractGate(cmd *cobra.Command) (string, bool) {
if cmd == nil || cmd.Annotations == nil {
return "", false
}
gate := strings.TrimSpace(cmd.Annotations[AnnotationRuntimeGate])
if gate == "" {
return "", false
}
return gate, true
}
// AnnotateRuntimePositionals records ordered positional arguments for agents.
func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...contract.RuntimeSchemaPositional) {
if cmd == nil {
return
}
clean := make([]contract.RuntimeSchemaPositional, 0, len(positionals))
for _, positional := range positionals {
positional.Name = strings.TrimSpace(positional.Name)
positional.Type = strings.TrimSpace(positional.Type)
positional.Description = strings.TrimSpace(positional.Description)
if positional.Name == "" || positional.Index < 0 {
continue
}
if positional.Type == "" {
positional.Type = "string"
}
clean = append(clean, positional)
}
if len(clean) == 0 {
return
}
sort.SliceStable(clean, func(i, j int) bool { return clean[i].Index < clean[j].Index })
encoded, _ := json.Marshal(clean)
SetCommandAnnotation(cmd, AnnotationPositionals, string(encoded))
}
// CommandPositionals reads annotated positionals (nil on miss/error).
func CommandPositionals(cmd *cobra.Command) []contract.RuntimeSchemaPositional {
if cmd == nil || cmd.Annotations == nil {
return nil
}
raw := strings.TrimSpace(cmd.Annotations[AnnotationPositionals])
if raw == "" {
return nil
}
var positionals []contract.RuntimeSchemaPositional
if json.Unmarshal([]byte(raw), &positionals) != nil {
return nil
}
sort.SliceStable(positionals, func(i, j int) bool { return positionals[i].Index < positionals[j].Index })
return positionals
}
// ExcludeFromRuntimeSchema keeps a human-facing hint or redirect in --help
// while preventing it from being advertised as an executable agent tool.
func ExcludeFromRuntimeSchema(cmd *cobra.Command) {
SetCommandAnnotation(cmd, AnnotationExclude, "true")
}
// CommandFlag resolves local flags plus product/group persistent flags.
// Root persistent flags are intentionally available only when explicitly
// requested; they are global execution controls, not tool parameters.
func CommandFlag(cmd *cobra.Command, name string) *pflag.Flag {
if cmd == nil {
return nil
}
name = strings.TrimSpace(name)
if name == "" {
return nil
}
if flag := cmd.Flags().Lookup(name); flag != nil {
return flag
}
for current := cmd; current != nil; current = current.Parent() {
if flag := current.PersistentFlags().Lookup(name); flag != nil {
return flag
}
}
return nil
}
// SetCommandAnnotation writes a non-blank command annotation.
func SetCommandAnnotation(cmd *cobra.Command, key, value string) {
if cmd == nil || strings.TrimSpace(value) == "" {
return
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations[key] = value
}
// SetFlagAnnotation writes a single non-blank flag annotation value.
func SetFlagAnnotation(flag *pflag.Flag, key, value string) {
if flag == nil || strings.TrimSpace(value) == "" {
return
}
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[key] = []string{value}
}
// SetFlagAnnotationValues writes a trimmed, non-empty flag annotation list.
func SetFlagAnnotationValues(flag *pflag.Flag, key string, values ...string) {
if flag == nil {
return
}
clean := make([]string, 0, len(values))
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
clean = append(clean, value)
}
}
if len(clean) == 0 {
return
}
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[key] = clean
}
@@ -0,0 +1,38 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtimeannotate
// Cobra annotation keys for Schema homology (path A). Writers and readers must
// share these exact strings; do not re-declare literals in callers.
const (
AnnotationProduct = "dws.schema.product"
AnnotationTool = "dws.schema.tool"
AnnotationSource = "dws.schema.source"
AnnotationTitle = "dws.schema.title"
AnnotationDescription = "dws.schema.description"
AnnotationMetaSource = "dws.schema.metadata_source"
AnnotationExclude = "dws.schema.exclude"
AnnotationConstraints = "dws.schema.constraints"
AnnotationPositionals = "dws.schema.positionals"
AnnotationContract = "dws.schema.contract"
AnnotationRisk = "dws.schema.risk"
AnnotationRuntimeGate = "dws.schema.runtime_gate"
AnnotationFlagProperty = "dws.schema.property"
AnnotationFlagType = "dws.schema.type"
AnnotationFlagRequired = "dws.schema.required"
AnnotationFlagReqWhen = "dws.schema.required_when"
AnnotationFlagExample = "dws.schema.example"
AnnotationFlagFormat = "x-cli-format"
AnnotationFlagEnum = "x-cli-enum"
)
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtimeannotate
import (
"encoding/json"
"strings"
"github.com/spf13/cobra"
)
// RuntimeSchemaConstraints describes cross-parameter rules that cannot be
// represented by an individual parameter's required bit.
type RuntimeSchemaConstraints struct {
MutuallyExclusive [][]string `json:"mutually_exclusive,omitempty"`
RequireOneOf [][]string `json:"require_one_of,omitempty"`
RequireTogether [][]string `json:"require_together,omitempty"`
}
// AnnotateRuntimeConstraints records command-level parameter relationships.
func AnnotateRuntimeConstraints(cmd *cobra.Command, constraints RuntimeSchemaConstraints) {
if cmd == nil {
return
}
constraints = NormalizeConstraints(constraints)
if ConstraintsEmpty(constraints) {
return
}
if existing := CommandConstraints(cmd); !ConstraintsEmpty(existing) {
constraints.MutuallyExclusive = append(existing.MutuallyExclusive, constraints.MutuallyExclusive...)
constraints.RequireOneOf = append(existing.RequireOneOf, constraints.RequireOneOf...)
constraints.RequireTogether = append(existing.RequireTogether, constraints.RequireTogether...)
constraints = NormalizeConstraints(constraints)
}
encoded, _ := json.Marshal(constraints)
SetCommandAnnotation(cmd, AnnotationConstraints, string(encoded))
}
// CommandConstraints reads the annotated constraint payload (empty on miss/error).
func CommandConstraints(cmd *cobra.Command) RuntimeSchemaConstraints {
if cmd == nil || cmd.Annotations == nil {
return RuntimeSchemaConstraints{}
}
raw := strings.TrimSpace(cmd.Annotations[AnnotationConstraints])
if raw == "" {
return RuntimeSchemaConstraints{}
}
var constraints RuntimeSchemaConstraints
if json.Unmarshal([]byte(raw), &constraints) != nil {
return RuntimeSchemaConstraints{}
}
return NormalizeConstraints(constraints)
}
// NormalizeConstraints trims, deduplicates, and drops undersized groups.
func NormalizeConstraints(constraints RuntimeSchemaConstraints) RuntimeSchemaConstraints {
constraints.MutuallyExclusive = normalizeGroups(constraints.MutuallyExclusive, 2)
constraints.RequireOneOf = normalizeGroups(constraints.RequireOneOf, 1)
constraints.RequireTogether = normalizeGroups(constraints.RequireTogether, 2)
return constraints
}
// ConstraintsEmpty reports whether no constraint groups remain.
func ConstraintsEmpty(constraints RuntimeSchemaConstraints) bool {
return len(constraints.MutuallyExclusive) == 0 &&
len(constraints.RequireOneOf) == 0 &&
len(constraints.RequireTogether) == 0
}
func normalizeGroups(groups [][]string, minimum int) [][]string {
out := make([][]string, 0, len(groups))
seenGroups := map[string]bool{}
for _, group := range groups {
clean := make([]string, 0, len(group))
seenNames := map[string]bool{}
for _, name := range group {
name = strings.TrimSpace(name)
if name == "" || seenNames[name] {
continue
}
seenNames[name] = true
clean = append(clean, name)
}
if len(clean) < minimum {
continue
}
key := strings.Join(clean, "\x00")
if seenGroups[key] {
continue
}
seenGroups[key] = true
out = append(out, clean)
}
return out
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package runtimeannotate owns Cobra dws.schema.* annotation writers and the
// RuntimeSchemaConstraints helpers used by the command framework.
//
// Package boundary:
//
// - Types / DTO / ProductDecl → internal/corecmd/contract
// - AnnotateRuntime* writers (this package) — no Catalog / go:embed
// - ContractFinal cobra store + Register seam → internal/cli/contractfinal
// - Catalog assembly / ResolveMeta / go:embed → internal/cli (root)
//
// Dependency direction: corecmd and cli/contractfinal import this package;
// this package must never import internal/cli (root) or contractfinal.
package runtimeannotate
@@ -0,0 +1,71 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package runtimeannotate
import (
"testing"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageAnnotateRuntimeRiskEmbedsContractMarker(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeRisk(cmd, "write")
got, ok := RuntimeContractRisk(cmd)
if !ok || got != "write" {
t.Fatalf("RuntimeContractRisk = %q %v", got, ok)
}
if cmd.Annotations[AnnotationContract] != "command" {
t.Fatalf("contract marker = %q", cmd.Annotations[AnnotationContract])
}
AnnotateRuntimeRisk(cmd, "")
if got, _ = RuntimeContractRisk(cmd); got != "write" {
t.Fatalf("empty AnnotateRuntimeRisk must be no-op, got %q", got)
}
}
func TestCrossPlatformCoverageAnnotateRuntimeGate(t *testing.T) {
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeGate(cmd, "devAppRequireWriteGuard")
got, ok := RuntimeContractGate(cmd)
if !ok || got != "devAppRequireWriteGuard" {
t.Fatalf("RuntimeContractGate = %q %v", got, ok)
}
if cmd.Annotations[AnnotationContract] != "command" {
t.Fatalf("contract marker = %q", cmd.Annotations[AnnotationContract])
}
}
func TestCrossPlatformCoverageRuntimeContractAnnotationNilAndBlankGuards(t *testing.T) {
AnnotateRuntimeFlagDescription(nil, "flag", "description")
AnnotateRuntimeContract(nil)
AnnotateRuntimeRisk(nil, "write")
AnnotateRuntimeGate(nil, "devAppRequireWriteGuard")
cmd := &cobra.Command{Use: "x"}
AnnotateRuntimeGate(cmd, " ")
if _, ok := RuntimeContractGate(cmd); ok {
t.Fatal("blank AnnotateRuntimeGate must not record a gate")
}
if cmd.Annotations != nil && cmd.Annotations[AnnotationContract] == "command" {
t.Fatal("blank AnnotateRuntimeGate must not mark the command as Contract")
}
blank := &cobra.Command{Use: "y", Annotations: map[string]string{
AnnotationRuntimeGate: " ",
}}
if _, ok := RuntimeContractGate(blank); ok {
t.Fatal("blank runtime_gate annotation must not report a gate")
}
}
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -186,7 +187,7 @@ func buildAgentExampleExecutionPlan(bound BoundCommandRegistry, typedTools map[s
canonicalPaths := make([]string, 0, len(bound.Commands))
for _, command := range bound.Commands {
canonical := strings.TrimSpace(command.CanonicalPath)
if !contract.HasRuntimeContractFinal(command.PrimaryCommand) {
if !contractfinal.HasRuntimeContractFinal(command.PrimaryCommand) {
return ManualAgentExampleExecutionPlan{}, fmt.Errorf("bound tool %q has no ContractFinal declaration; Schema examples require leaf Schema.Selection", canonical)
}
canonicalPaths = append(canonicalPaths, canonical)
+3 -3
View File
@@ -21,7 +21,7 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/spf13/cobra"
)
@@ -73,7 +73,7 @@ func BuildAgentSelectionEvalFixture(bound BoundCommandRegistry) (AgentSelectionF
for _, command := range bound.Commands {
canonical := strings.TrimSpace(command.CanonicalPath)
expectedTools[canonical] = true
if !contract.HasRuntimeContractFinal(command.PrimaryCommand) {
if !contractfinal.HasRuntimeContractFinal(command.PrimaryCommand) {
return fixture, report, fmt.Errorf("agent selection expected canonical %q has no ContractFinal declaration", canonical)
}
productID, _, ok := strings.Cut(canonical, ".")
@@ -169,7 +169,7 @@ func ValidateAgentSelectionContract(bound BoundCommandRegistry) (AgentSelectionR
// tool from its ContractFinal overlay.
func contractFinalSelectionHint(command *cobra.Command) ManualAgentToolHint {
hint := ManualAgentToolHint{Reviewed: true, Revision: "contract", Reason: "Contract final declaration (corecmd.ContractDecl)"}
payload, ok := contract.RuntimeContractFinal(command)
payload, ok := contractfinal.RuntimeContractFinal(command)
if !ok || payload.Selection == nil {
return hint
}
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -163,7 +164,7 @@ func BindEffectiveCommandRegistry(root *cobra.Command, effective EffectiveComman
// Index Contract-declared dry_run capabilities at bind time: every
// process that resolves the command tree gets the reviewed set, not
// only processes that also run Schema assembly.
if payload, ok := contract.RuntimeContractFinal(item.PrimaryCommand); ok && payload.DryRun != nil {
if payload, ok := contractfinal.RuntimeContractFinal(item.PrimaryCommand); ok && payload.DryRun != nil {
recordDeclaredDryRunCapability(item.CanonicalPath, *payload.DryRun)
}
}
@@ -8,7 +8,7 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/spf13/cobra"
)
@@ -77,9 +77,9 @@ func schemaRegistryForTestWithMetadata(root *cobra.Command, agent embeddedAgentM
// Clear bind-time ContractFinal so injected fixtures remain authoritative
// for the commands under test. Production assembly never takes this path.
for _, command := range bound.Commands {
contract.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
contractfinal.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
for _, alias := range command.AliasCommands {
contract.ClearRuntimeContractFinalForTest(alias.Command)
contractfinal.ClearRuntimeContractFinalForTest(alias.Command)
}
}
return assembleSchemaRegistryFromBoundAllowingLegacy(bound, runtimeSchemaMetadataSources{Agent: agent, MCP: mcp})
+3 -2
View File
@@ -9,6 +9,7 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
@@ -207,7 +208,7 @@ func assembleSchemaRegistryFromBoundWithOptions(bound BoundCommandRegistry, meta
}
func runtimeToolSpecFromMetadata(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) {
if final, ok := contract.RuntimeContractFinal(entry.Command); ok {
if final, ok := contractfinal.RuntimeContractFinal(entry.Command); ok {
return runtimeToolSpecFromContractFinal(entry, final, metadata)
}
canonicalPath := entry.ProductID + "." + entry.ToolName
@@ -231,7 +232,7 @@ func assembleProductSelection(entry runtimeSchemaEntry, metadata runtimeSchemaMe
// runtimeToolSpecAllowingLegacy is the test-isolated overlay path. Prefer
// ContractFinal when present; otherwise reopen retired skill/MCP/agent inject.
func runtimeToolSpecAllowingLegacy(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) {
if final, ok := contract.RuntimeContractFinal(entry.Command); ok {
if final, ok := contractfinal.RuntimeContractFinal(entry.Command); ok {
return runtimeToolSpecFromContractFinal(entry, final, metadata)
}
return runtimeToolSpecFromLegacyMetadata(entry, metadata)
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
)
@@ -143,7 +144,7 @@ func TestAssembleSchemaRegistryFailClosedMissingProductDecl(t *testing.T) {
Selection: &contract.SelectionSpec{AgentSummary: "orphan leaf"},
})
t.Cleanup(func() {
contract.ClearRuntimeContractFinalForTest(leaf)
contractfinal.ClearRuntimeContractFinalForTest(leaf)
contract.ClearProductDeclForTest("orphan")
})
product := &cobra.Command{Use: "orphan"}
@@ -163,7 +164,7 @@ func TestAssembleSchemaRegistryAllowingLegacyIsolatesOverlayPath(t *testing.T) {
t.Fatal(err)
}
for _, command := range bound.Commands {
contract.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
contractfinal.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
}
agent := embeddedAgentMetadata{
Version: 1,
@@ -203,7 +204,7 @@ func TestAssembleSchemaRegistryRequiresContractFinalAndProductDecl(t *testing.T)
},
})
t.Cleanup(func() {
contract.ClearRuntimeContractFinalForTest(leaf)
contractfinal.ClearRuntimeContractFinalForTest(leaf)
contract.ClearProductDeclForTest("sample")
})
contract.RegisterProductDecl(contract.ProductDecl{
@@ -333,7 +334,7 @@ func assembleContractFinalTextTool(t *testing.T, short, long, declaredDescriptio
},
})
t.Cleanup(func() {
contract.ClearRuntimeContractFinalForTest(leaf)
contractfinal.ClearRuntimeContractFinalForTest(leaf)
contract.ClearProductDeclForTest("sample")
})
contract.RegisterProductDecl(contract.ProductDecl{
+25 -17
View File
@@ -11,32 +11,40 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package contract owns the command-framework declaration registries and the
// leaf/product types that form ContractFinal pass-through.
// Package contract owns command-framework declaration DTOs and the
// ProductDecl registry. It is intentionally free of Cobra-keyed runtime
// stores and Catalog / go:embed delivery code.
//
// This package is the sole definition point for:
// - ContractFinalPayload (+ registry store)
// - ProductDecl (+ registry)
// - ContractFinalPayload (DTO only — store lives in cli/contractfinal)
// - ProductDecl (+ string-keyed registry; not Cobra-keyed)
// - SafetySpec / SelectionSpec / InterfaceSpec / DryRunSpec / identity /
// positionals / ParamDecl
// - FieldProvenance / FieldCandidateProvenance
//
// Package boundary (corecmd → cli seam):
// Package boundary:
//
// - Types and registries → corecmd/contract (this package). Callers author
// contract.SafetySpec / contract.ParamDecl / contract.ProductDecl /
// contract.ContractFinalPayload / contract.InterfaceSpec directly.
// - Types / ProductDecl → corecmd/contract (this package).
// - Authoring wrapper → corecmd.ContractDecl (leaf-facing; nested fields are
// these contract types). Name is ContractDecl, not SchemaDecl: "Schema" in
// this repo means Catalog / ToolSpec delivery, not the author declaration.
// - Cobra annotation + store seam → internal/cli.RegisterRuntimeContractFinal
// (AnnotateRuntimeContract + RegisterRuntimeContractFinal). Production
// framework code (corecmd.New / AttachContract) must use that seam and must
// not call this package's store helper directly.
// - Catalog assembly / ResolveMeta / go:embed → internal/cli (delivery
// boundary; not moved into contract).
// - AnnotateRuntime* writers → internal/cli/runtimeannotate
// (corecmd may import; must not import cli root).
// - ContractFinal cobra store + Register seam → internal/cli/contractfinal
// (framework calls RegisterRuntimeContractFinal here; product code uses
// cli.RegisterRuntimeContractFinal re-export).
// - Catalog assembly / ResolveMeta / go:embed → internal/cli (delivery root).
//
// Authoring path: corecmd.ContractDecl → ContractFinalPayload (via cli seam);
// ProductDecl for product-level Agent routing. Provenance stamp for declared
// leaf Safety remains "corecmd.contract".
// Description declare vs delivery (not dual authority):
//
// - Construction requires ContractDecl.Description (declaration evidence).
// - Catalog delivery prefers Cobra Long when present → provenance cobra_help;
// without Long, declared Description is delivered → contract_final.
// - Title prefers declared ContractDecl/ContractFinal, then Cobra Short, then
// MCP metadata. Declare is not "wire final value" for description when Long
// exists; assembly stamps the real winner.
//
// Authoring path: corecmd.ContractDecl → ContractFinalPayload (via
// contractfinal seam); ProductDecl for product-level Agent routing.
// Provenance stamp for declared leaf Safety remains "corecmd.contract".
package contract
+8 -64
View File
@@ -13,15 +13,15 @@
package contract
import (
"sync"
"github.com/spf13/cobra"
)
// ContractFinalPayload is the Contract-authored final Schema leaf overlay.
// Registered in-process by the framework; Schema assembly reads it as
// pass-through. No JSON bridge. Treat as read-only after Register.
// Registered in-process by the framework via runtimeannotate / cli seam;
// Schema assembly reads it as pass-through. No JSON bridge. Treat as
// read-only after Register.
//
// The Cobra-keyed runtime store lives in internal/corecmd/runtimeannotate
// (not this DTO package). Production product code registers through
// cli.RegisterRuntimeContractFinal; framework code may call
// runtimeannotate.RegisterRuntimeContractFinal directly.
type ContractFinalPayload struct {
Title string
Description string
@@ -33,59 +33,3 @@ type ContractFinalPayload struct {
Selection *SelectionSpec
Identity *ToolIdentitySpec
}
var contractFinalByCommand sync.Map // *cobra.Command → *ContractFinalPayload
// RegisterRuntimeContractFinal stores the typed final Schema overlay for a leaf.
// Light runtime write: one map store, no JSON, no deep clone.
//
// Seam-only: production code must call cli.RegisterRuntimeContractFinal so the
// dws.schema.contract annotation and the typed store stay atomic. This helper
// exists for that seam (and tests that exercise the store). Do not call it from
// corecmd.AttachContract / product helpers / shortcuts.
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload ContractFinalPayload) {
if cmd == nil {
return
}
p := payload
contractFinalByCommand.Store(cmd, &p)
}
// RuntimeContractFinal returns the registered final Schema overlay (read-only).
func RuntimeContractFinal(cmd *cobra.Command) (ContractFinalPayload, bool) {
if cmd == nil {
return ContractFinalPayload{}, false
}
raw, ok := contractFinalByCommand.Load(cmd)
if !ok {
return ContractFinalPayload{}, false
}
p, ok := raw.(*ContractFinalPayload)
if !ok || p == nil {
return ContractFinalPayload{}, false
}
return *p, true
}
// HasRuntimeContractFinal reports whether the leaf has a registered final overlay.
func HasRuntimeContractFinal(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
_, ok := contractFinalByCommand.Load(cmd)
return ok
}
// ClearRuntimeContractFinalForTest removes a registration (tests only).
func ClearRuntimeContractFinalForTest(cmd *cobra.Command) {
if cmd != nil {
contractFinalByCommand.Delete(cmd)
}
}
// StoreRuntimeContractFinalRawForTest injects a raw map value (tests only).
func StoreRuntimeContractFinalRawForTest(cmd *cobra.Command, raw any) {
if cmd != nil {
contractFinalByCommand.Store(cmd, raw)
}
}
@@ -0,0 +1,71 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package contract
import "testing"
func TestProductDeclRegistryRoundTrip(t *testing.T) {
t.Cleanup(func() { ClearProductDeclForTest("sample") })
ClearProductDeclForTest("sample")
if HasProductDecl("sample") {
t.Fatal("HasProductDecl before register must be false")
}
RegisterProductDecl(ProductDecl{})
if HasProductDecl("") {
t.Fatal("empty ID must not register")
}
RegisterProductDecl(ProductDecl{
ID: " sample ",
Selection: ProductSelectionDecl{
AgentSummary: "Manage samples",
UseWhen: []string{"target is a sample"},
AvoidWhen: []string{"target is another product"},
},
})
if !HasProductDecl("sample") {
t.Fatal("HasProductDecl after register must be true")
}
got, ok := LookupProductDecl("sample")
if !ok || got.ID != "sample" || got.Selection.AgentSummary != "Manage samples" {
t.Fatalf("LookupProductDecl = %#v, ok=%v", got, ok)
}
ids := RegisteredProductDeclIDs()
found := false
for _, id := range ids {
if id == "sample" {
found = true
break
}
}
if !found {
t.Fatalf("RegisteredProductDeclIDs missing sample: %#v", ids)
}
selection, provenance := ProductSelectionFromDecl(got)
if selection.AgentSummary != "Manage samples" || selection.AgentSummarySource != ProductDeclSourceRef {
t.Fatalf("ProductSelectionFromDecl selection = %#v", selection)
}
for _, field := range []string{"agent_summary", "use_when", "avoid_when"} {
prov, ok := provenance[field]
if !ok || prov.Precedence != "contract_final" || prov.Source != ProductDeclProvenanceSource {
t.Fatalf("field %s provenance = %#v", field, prov)
}
}
ClearProductDeclForTest("sample")
if HasProductDecl("sample") {
t.Fatal("ClearProductDeclForTest must remove registration")
}
}
func TestProductDeclRegisterPanicsOnIncompleteSelection(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic for incomplete ProductDecl")
}
}()
RegisterProductDecl(ProductDecl{ID: "broken"})
}
+9 -1
View File
@@ -28,9 +28,17 @@ import (
// contract.ContractFinalPayload; Catalog assembly pass-throughs that payload.
// Nested fields reuse contract.* types directly so authoring cannot drift from
// the registry model.
//
// Description declare vs delivery (not dual authority, not "declare = wire"):
// - Construction requires Description (declaration evidence / fail-closed).
// - Catalog delivery: Cobra Long wins when present → provenance cobra_help;
// without Long, declared Description is delivered → contract_final.
// - Title: declared Title/ContractFinal first, then Cobra Short, then MCP.
//
// The payload stores the declared text; assembly stamps the real winner.
type ContractDecl struct {
Title string
Description string
Description string // required at construction; Catalog may prefer Cobra Long
Positionals []contract.RuntimeSchemaPositional
Parameters []contract.ParamDecl
DryRun *contract.DryRunSpec
+5 -4
View File
@@ -17,6 +17,7 @@ import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
@@ -65,7 +66,7 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
t.Fatal("framework must convert typed ContractDecl; must not write JSON dws.schema.final")
}
}
final, ok := contract.RuntimeContractFinal(cmd)
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok {
t.Fatal("expected typed ContractFinal registration")
}
@@ -135,7 +136,7 @@ func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) {
},
Invoke: func(*Ctx, map[string]any) error { return nil },
})
final, ok := contract.RuntimeContractFinal(cmd)
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok {
t.Fatal("expected typed ContractFinal registration")
}
@@ -240,7 +241,7 @@ func TestNewCommandSafetySpecPassThrough(t *testing.T) {
}
build := func(spec Spec) *contract.SafetySpec {
cmd := New(spec)
final, ok := contract.RuntimeContractFinal(cmd)
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok || final.Safety == nil {
t.Fatalf("expected declared safety, final=%#v ok=%v", final, ok)
}
@@ -273,7 +274,7 @@ func TestContractDeclEmptySkipsFinal(t *testing.T) {
Safety: testWriteSafety(),
Invoke: func(*Ctx, map[string]any) error { return nil },
})
if contract.HasRuntimeContractFinal(cmd) {
if contractfinal.HasRuntimeContractFinal(cmd) {
t.Fatal("Safety without Contract must not register Final (keep runtime write light)")
}
if _, ok := cmd.Annotations["dws.schema.risk"]; ok {
@@ -21,10 +21,9 @@ import (
"testing"
)
// Production framework code must register ContractFinal through the cli
// delivery seam (annotate + store), not by calling the contract store helper
// directly.
func TestAttachContractUsesCLIRegisterSeam(t *testing.T) {
// Production framework code must register ContractFinal through the
// contractfinal annotate+store seam, not by importing the cli delivery root.
func TestAttachContractUsesContractFinalRegisterSeam(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
@@ -35,11 +34,13 @@ func TestAttachContractUsesCLIRegisterSeam(t *testing.T) {
t.Fatalf("read corecmd.go: %v", err)
}
body := string(raw)
if !strings.Contains(body, "cli.RegisterRuntimeContractFinal(") {
t.Fatal("AttachContract/New must call cli.RegisterRuntimeContractFinal")
if !strings.Contains(body, "contractfinal.RegisterRuntimeContractFinal(") {
t.Fatal("AttachContract/New must call contractfinal.RegisterRuntimeContractFinal")
}
// Disallow the dual-track pattern: annotate then contract.Register in corecmd.
if strings.Contains(body, "contract.RegisterRuntimeContractFinal(") {
t.Fatal("corecmd must not call contract.RegisterRuntimeContractFinal directly; use cli seam")
if strings.Contains(body, `"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"`) {
t.Fatal("corecmd must not import internal/cli delivery root")
}
if strings.Contains(body, "cli.RegisterRuntimeContractFinal(") {
t.Fatal("corecmd must not call cli.RegisterRuntimeContractFinal; use contractfinal seam")
}
}
+43 -43
View File
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package command is the shared, dispatch-agnostic base for building leaf
// Package corecmd is the shared, dispatch-agnostic base for building leaf
// commands. It concentrates flag registration, the alias/env/default effective
// value fallback chain, required validation, cross-flag constraint declaration
// checks + runtime enforcement, SafetySpec-driven confirmation, toolArgs
@@ -20,33 +20,31 @@
// Declaration vs execution (framework rule):
//
// - Declare = Spec data fields (Flags, Constraints, Safety,
// ConstParams, Use/Short/Long/Example). NewCommand registers, validates,
// ConstParams, Use/Short/Long/Example). New registers, validates,
// confirms, and embeds those facts into dws.schema.*.
// - Execute = Validate / Invoke / Orchestrate / RunE / PostMount. Hooks
// consume assembled args; they must not invent the CLI surface.
// - Annotate = explicit cobra annotations when a fact is not (yet) a Contract
// field (e.g. write-guard runtime_gate). Inference-only Schema/help is
// forbidden.
// - Reviewed non-Contract Schema (identity, selection, interface_*, dry-run,
// idempotency) has its own sources and must not create CLI flags.
// - Selection / product routing prose is declared on ContractDecl /
// ProductDecl (delivered as contract_final). schema_hints/ is retired.
// Identity remains the reviewed CommandRegistry. Interface / dry-run
// reviewed sources must not create CLI flags.
//
// Full ToolSpec field authority: RFC §5.0.4 / homology §1.4.
//
// It is deliberately dispatch-agnostic: it never calls an MCP tool. The
// LeafSpec framework (internal/helpers) and, later, the Shortcut framework wrap
// these primitives and supply their own dispatch (single-step MCP / multi-step
// orchestration / escape hatch). Extracting the primitives here lets both
// frameworks share one flag + constraint + safety + schema base, differing only
// in how they dispatch — the first step toward a single typed command registry.
// It is deliberately dispatch-agnostic: it never calls an MCP tool. LeafSpec
// (internal/helpers) and Shortcut (internal/shortcut) wrap these primitives and
// supply dispatch. Invoke / Orchestrate / Ctx are the #830 transitional
// dispatch API still used in production; the RFC target is mcpbind + Handler
// (do not treat removal of Invoke/Orchestrate as already landed).
//
// Behavioral contract: this package is a pure extraction of the logic that
// previously lived in internal/helpers/leaf.go, so flag registration, value
// fallback, required/constraint semantics, confirmation behavior, and schema projection
// stay semantically identical. The evidence is split: check-generated-drift
// (catalog unchanged) proves only the build-time projection — identity, flags,
// help and annotations — while the runtime pipeline (required validation,
// toolArgs assembly, write confirmation, dispatch order) is evidenced solely by
// this package's own tests plus the leaf/risk/constraint unit tests.
// Behavioral contract: flag registration, value fallback, required/constraint
// semantics, confirmation behavior, and schema projection stay shared across
// Leaf and Shortcut. Evidence is split: check-generated-drift proves build-time
// projection; runtime pipeline order is covered by this package's tests plus
// leaf/risk/constraint unit tests.
package corecmd
import (
@@ -61,7 +59,8 @@ import (
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
@@ -214,16 +213,17 @@ const (
//
// - RunE — full escape hatch: the framework only registers flags/constraints/
// help and hands control over.
// - Invoke — single-step: runs after required/constraint/Validate checks, args
// assembly and the Safety confirmation gate, receiving the assembled toolArgs.
// - Orchestrate — multi-step: runs after the same checks and confirmation but
// receives only the Ctx, so it can chain several backend calls itself.
// - Invoke — #830 transitional single-step dispatch: runs after required/
// constraint/Validate checks, args assembly and the Safety confirmation
// gate, receiving the assembled toolArgs. Target: mcpbind Bind.
// - Orchestrate — #830 transitional multi-step dispatch: same checks and
// confirmation, receives only the Ctx. Target: Handler / orchestration.
// - Validate / PostMount — orchestration only; must not register business flags
// or assemble business params that belong in Flags/ConstParams.
//
// Exactly one of RunE / Invoke / Orchestrate must be set; NewCommand validates
// this at construction time. command itself stays dispatch-agnostic and never
// calls a backend: the adapters (FromLeafSpec / FromShortcut) supply the body.
// Exactly one of RunE / Invoke / Orchestrate must be set; New validates this at
// construction time. corecmd stays dispatch-agnostic and never calls a backend:
// the adapters (FromLeafSpec / FromShortcut) supply the body.
type Spec struct {
Use string
Short string
@@ -1139,23 +1139,23 @@ func embedContractIntoSchema(cmd *cobra.Command, spec Spec) {
continue
}
if flag.Required || flag.MarkRequired {
cli.AnnotateRuntimeRequiredFlags(cmd, name)
runtimeannotate.AnnotateRuntimeRequiredFlags(cmd, name)
}
if len(flag.Enum) > 0 {
cli.AnnotateRuntimeFlagEnum(cmd, name, flag.Enum...)
runtimeannotate.AnnotateRuntimeFlagEnum(cmd, name, flag.Enum...)
}
// Same class as Enum: a declared rule the reviewed registry cannot
// derive. Without it a credential that is mandatory under one
// identity is published as plainly optional.
if flag.RequiredWhen != "" {
cli.AnnotateRuntimeFlagRequiredWhen(cmd, name, flag.RequiredWhen)
runtimeannotate.AnnotateRuntimeFlagRequiredWhen(cmd, name, flag.RequiredWhen)
}
}
embedContractDecl(cmd, spec)
return
}
cli.AnnotateRuntimeContract(cmd)
runtimeannotate.AnnotateRuntimeContract(cmd)
required := make([]string, 0, len(spec.Flags))
for _, flag := range spec.Flags {
name := strings.TrimSpace(flag.Name)
@@ -1163,36 +1163,36 @@ func embedContractIntoSchema(cmd *cobra.Command, spec Spec) {
continue
}
requiredFlag := flag.Required || flag.MarkRequired
cli.AnnotateRuntimeFlag(cmd, name, strings.TrimSpace(flag.Bind), flagKindSchemaType(flag.Kind), requiredFlag, "")
runtimeannotate.AnnotateRuntimeFlag(cmd, name, strings.TrimSpace(flag.Bind), flagKindSchemaType(flag.Kind), requiredFlag, "")
desc := strings.TrimSpace(flag.SchemaDescription)
if desc == "" {
desc = strings.TrimSpace(flag.Usage)
}
if desc != "" {
cli.AnnotateRuntimeFlagDescription(cmd, name, desc)
runtimeannotate.AnnotateRuntimeFlagDescription(cmd, name, desc)
}
if flag.RequiredWhen != "" {
cli.AnnotateRuntimeFlagRequiredWhen(cmd, name, flag.RequiredWhen)
runtimeannotate.AnnotateRuntimeFlagRequiredWhen(cmd, name, flag.RequiredWhen)
}
if flag.Format != "" {
cli.AnnotateRuntimeFlagFormat(cmd, name, flag.Format)
runtimeannotate.AnnotateRuntimeFlagFormat(cmd, name, flag.Format)
}
if flag.Example != "" {
cli.AnnotateRuntimeFlagExample(cmd, name, flag.Example)
runtimeannotate.AnnotateRuntimeFlagExample(cmd, name, flag.Example)
}
if len(flag.Enum) > 0 {
cli.AnnotateRuntimeFlagEnum(cmd, name, flag.Enum...)
runtimeannotate.AnnotateRuntimeFlagEnum(cmd, name, flag.Enum...)
}
if requiredFlag {
required = append(required, name)
}
}
cli.AnnotateRuntimeRequiredFlags(cmd, required...)
runtimeannotate.AnnotateRuntimeRequiredFlags(cmd, required...)
embedContractDecl(cmd, spec)
}
// embedContractDecl does a light runtime write: only when ContractDecl is authored,
// convert once through the cli delivery seam (annotate + store).
// convert once through contractfinal.RegisterRuntimeContractFinal (annotate + store).
func embedContractDecl(cmd *cobra.Command, spec Spec) {
if spec.Contract.empty() {
return
@@ -1205,7 +1205,7 @@ func embedContractDecl(cmd *cobra.Command, spec Spec) {
// while keeping execution substance frozen. Overwrites any prior ContractFinal
// on cmd; does not alter an already-installed ConfirmSafety closure.
//
// Production registration always goes through cli.RegisterRuntimeContractFinal
// Production registration always goes through contractfinal.RegisterRuntimeContractFinal
// (annotate + store). Do not call contract.RegisterRuntimeContractFinal from
// framework/product code — that store helper is seam-only.
//
@@ -1279,7 +1279,7 @@ func AttachContract(cmd *cobra.Command, safety contract.SafetySpec, decl Contrac
if len(decl.Parameters) > 0 {
payload.Parameters = append([]contract.ParamDecl(nil), decl.Parameters...)
}
cli.RegisterRuntimeContractFinal(cmd, payload)
contractfinal.RegisterRuntimeContractFinal(cmd, payload)
}
func firstNonEmpty(values ...string) string {
@@ -1332,7 +1332,7 @@ func flagKindSchemaType(kind FlagKind) string {
// Runtime Schema: exactly_one decomposes into require_one_of + mutually_exclusive
// (matching the handwritten commands' use of AnnotateRuntimeConstraints).
func AnnotateConstraints(cmd *cobra.Command, constraints []Constraint) {
var projected cli.RuntimeSchemaConstraints
var projected runtimeannotate.RuntimeSchemaConstraints
var required []string
for _, constraint := range constraints {
flags := make([]string, 0, len(constraint.Flags))
@@ -1362,8 +1362,8 @@ func AnnotateConstraints(cmd *cobra.Command, constraints []Constraint) {
}
}
}
cli.AnnotateRuntimeRequiredFlags(cmd, required...)
cli.AnnotateRuntimeConstraints(cmd, projected)
runtimeannotate.AnnotateRuntimeRequiredFlags(cmd, required...)
runtimeannotate.AnnotateRuntimeConstraints(cmd, projected)
}
// ConstraintHelp renders the --help "参数约束" section, matching the shortcut
+3 -2
View File
@@ -20,6 +20,7 @@ import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
@@ -1559,7 +1560,7 @@ func TestCrossPlatformCoverageEmbedContractCobraProjection(t *testing.T) {
t.Fatalf("hidden flag must not be projected, got %#v", got)
}
// The authored ContractDecl still lands as the typed ContractFinal.
final, ok := contract.RuntimeContractFinal(cmd)
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok || final.Description != "desc" {
t.Fatalf("cobra projection must still embed ContractDecl, final=%#v ok=%v", final, ok)
}
@@ -1570,7 +1571,7 @@ func TestCrossPlatformCoverageAttachContractNilAndEmptyGuards(t *testing.T) {
AttachContract(nil, testWriteSafety(), ContractDecl{Description: "d"}, "s", "l")
cmd := newTestCommand()
AttachContract(cmd, testWriteSafety(), ContractDecl{}, "s", "l")
if contract.HasRuntimeContractFinal(cmd) {
if contractfinal.HasRuntimeContractFinal(cmd) {
t.Fatal("empty ContractDecl must not register a ContractFinal")
}
}
@@ -18,6 +18,7 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
)
@@ -151,7 +152,7 @@ func contractFinalProductMetadata(decl contract.ProductDecl) ProductMetadata {
// Absent payload sections stay absent (never authored-empty); declared
// sections map field-by-field, preserving explicit empty-list authorship.
func contractFinalToolMetadata(command *cobra.Command) (ToolMetadata, bool) {
payload, ok := contract.RuntimeContractFinal(command)
payload, ok := contractfinal.RuntimeContractFinal(command)
if !ok {
return ToolMetadata{}, false
}
@@ -13,6 +13,7 @@ import (
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
@@ -246,7 +247,7 @@ func validateSelectionAuthoringContracts(opts Options) error {
expectedTools := expectedCanonicalToolSet(opts)
for canonical := range expectedTools {
bound, ok := opts.BoundCommands.ByCanonical[canonical]
if ok && contract.HasRuntimeContractFinal(bound.PrimaryCommand) {
if ok && contractfinal.HasRuntimeContractFinal(bound.PrimaryCommand) {
delete(expectedTools, canonical)
}
}
@@ -11,6 +11,7 @@ import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/spf13/cobra"
)
@@ -488,7 +489,7 @@ func TestCrossPlatformCoverageContractFinalDeclarationFailureEdges(t *testing.T)
cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{
Selection: &contract.SelectionSpec{AgentSummary: "declared summary"},
})
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) })
t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(declared) })
bound := cli.BoundCommandRegistry{ByCanonical: map[string]cli.BoundCommandSpec{
"sample.run": {PrimaryCommand: declared},
}}
+2 -2
View File
@@ -20,7 +20,7 @@ import (
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/spf13/cobra"
)
@@ -78,7 +78,7 @@ func SelectionHintCoverageTools(projection RegistryProjection) map[string]bool {
}
for canonical := range expectedTools {
bound, ok := projection.Bound.ByCanonical[canonical]
if ok && contract.HasRuntimeContractFinal(bound.PrimaryCommand) {
if ok && contractfinal.HasRuntimeContractFinal(bound.PrimaryCommand) {
delete(expectedTools, canonical)
}
}
@@ -13,6 +13,7 @@ import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/agentmetadata"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/outputguard"
@@ -283,7 +284,7 @@ func TestCrossPlatformCoverageMetadataRegistryAndSelectionFailureEdges(t *testin
func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredTools(t *testing.T) {
declared := &cobra.Command{Use: "run"}
cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{})
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) })
t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(declared) })
registry := commandRegistryProjection{
CanonicalToolPaths: map[string]string{
+3 -2
View File
@@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
@@ -80,7 +81,7 @@ func TestDeclareLeafMetadataDoesNotRewriteRunE(t *testing.T) {
},
},
})
if !contract.HasRuntimeContractFinal(cmd) {
if !contractfinal.HasRuntimeContractFinal(cmd) {
t.Fatal("expected ContractFinal")
}
// function values are not comparable; ensure pointer identity via uintptr trick is unnecessary —
@@ -128,7 +129,7 @@ func TestAitableDeclareLeafMetadataCoversRegistryHelpers(t *testing.T) {
missing = append(missing, tool.CLIPath+" (command missing)")
continue
}
if !contract.HasRuntimeContractFinal(leaf) {
if !contractfinal.HasRuntimeContractFinal(leaf) {
missing = append(missing, tool.CLIPath)
}
}
@@ -256,9 +256,10 @@ func TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller(t
}
// TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool
// covers the deferred-confirm post-RunE fallback: with a deps.Caller present
// and an inner RunE that never dispatches through CallTool, the gate stays
// unconfirmed and ConfirmSafety must still run after the inner RunE.
// covers the deferred-confirm fail-closed path: with a deps.Caller present and
// an inner RunE that never dispatches through CallTool, success-without-confirm
// is rejected. Post-RunE ConfirmSafety is intentionally not used — local side
// effects may already have run.
func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool(t *testing.T) {
prev := deps
caller := &deferConfirmTestCaller{}
@@ -287,24 +288,25 @@ func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithout
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{})
// Without --yes the post-RunE fallback confirmation must fail closed.
err := cmd.Execute()
if err == nil || (!strings.Contains(err.Error(), "confirmation_required") && !strings.Contains(err.Error(), "需要用户确认")) {
t.Fatalf("Execute() without --yes error = %v, want confirmation_required", err)
if err == nil || !strings.Contains(err.Error(), "never obtained via CallTool") {
t.Fatalf("Execute() without CallTool error = %v, want fail-closed contract error", err)
}
if ran != 1 {
t.Fatalf("inner RunE ran %d times, want 1 (deferred gate confirms after RunE)", ran)
t.Fatalf("inner RunE ran %d times, want 1 (fail-closed after successful RunE)", ran)
}
if caller.calls != 0 {
t.Fatalf("CallTool calls = %d, want 0", caller.calls)
}
// With --yes the post-RunE fallback confirmation passes.
// --yes must not green-light a post-RunE confirmation: the contract error
// remains even when ConfirmSafety would have passed.
if err := cmd.Flags().Set("yes", "true"); err != nil {
t.Fatal(err)
}
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() with --yes error = %v", err)
err = cmd.Execute()
if err == nil || !strings.Contains(err.Error(), "never obtained via CallTool") {
t.Fatalf("Execute() with --yes but no CallTool error = %v, want fail-closed contract error", err)
}
if ran != 2 {
t.Fatalf("inner RunE ran %d times, want 2", ran)
+2 -2
View File
@@ -12,7 +12,7 @@ import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
@@ -156,7 +156,7 @@ func TestDocVersionRevertPublishesRuntimeSafety(t *testing.T) {
if err != nil || len(remaining) != 0 {
t.Fatalf("find doc version revert: command=%v remaining=%v err=%v", cmd, remaining, err)
}
final, ok := contract.RuntimeContractFinal(cmd)
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok || final.Safety == nil {
t.Fatal("doc version revert must publish ContractFinal Safety")
}
+21 -8
View File
@@ -46,10 +46,11 @@ const contractConfirmDeferredAnnotation = "dws.contract.confirm_deferred"
// - 完全托管模式 NewLeafCommand:声明 + 执行都归 corecmd(flag 注册、
// 参数投影、ConfirmSafety、派发)。新命令默认走此模式。
// - 声明元数据模式 DeclareLeafMetadata:声明 Safety + Contract(AttachContract),
// 不注册 flag、不接管参数投影;可选 Validate 挂到 PreRunE(确认前)。
// 当 Safety.Confirmation=user_required 时用**同一份** SafetySpec 包一层
// ConfirmSafety(在 PreRunE 之后),保证执行门禁与 Catalog 同源。
// 用于执行体必须冻结的既有命令补声明,是迁移态。
// 不注册 flag、不接管参数投影;可选 Validate 与 ConfirmSafety 同挂在
// RunE 包装器内(Validate 在前,不是 PreRunE——直接调 RunE /
// proxySubCmd 会跳过 PreRunE)。当 Safety.Confirmation=user_required 时
// 用**同一份** SafetySpec 包一层 ConfirmSafety,保证执行门禁与 Catalog
// 同源。用于执行体必须冻结的既有命令补声明,是迁移态。
//
// 每个 API 各自声明:
//
@@ -199,8 +200,10 @@ func NewLeafCommand(spec LeafSpec) *cobra.Command {
// user_required 确认时机:
// - 提供 Validate:Validate → ConfirmSafety → 原 RunE(本地副作用命令)
// - 未提供 Validate:原 RunE 先跑(含缺参校验),ConfirmSafety 推迟到
// 首次 deps.Caller.CallTool;若无 Caller 可挂门禁,则回退为
// ConfirmSafety → 原 RunE(此类命令应补 Validate,见同源门禁)
// 首次 deps.Caller.CallTool。无 Caller 时回退 ConfirmSafety → 原 RunE。
// 有 Caller 但 RunE 成功返回且从未 CallTool:fail-closed(禁止「成功
// 返回却未确认」——此时副作用可能已发生,事后 Confirm 太晚)。本地
// 副作用叶必须补 Validate,或把副作用放进 gated CallTool。
//
// 该模式是迁移态而非终态:命令具备条件时应升级为 NewLeafCommand。传入
// Flags/Constraints/ConstParams/Call/RunE/PostMount 或空 Contract 会 panic,
@@ -280,7 +283,9 @@ func installContractRunEPipeline(cmd *cobra.Command, rt *contractRuntime) {
if !rt.confirm {
return inner(c, args)
}
if deps != nil && deps.Caller != nil && deps.Caller.DryRun() {
// OR across flagsets (root persistent --dry-run must not be shadowed by
// a leaf-local --dry-run defaulting to false).
if corecmd.BoolFlag(c, "dry-run") || (deps != nil && deps.Caller != nil && deps.Caller.DryRun()) {
return inner(c, args)
}
if rt.validate != nil {
@@ -307,7 +312,15 @@ func installContractRunEPipeline(cmd *cobra.Command, rt *contractRuntime) {
return err
}
if !gate.confirmed {
return corecmd.ConfirmSafety(c, rt.safety)
// Fail closed: RunE already returned successfully. A post-RunE
// ConfirmSafety cannot undo local side effects, and --yes would
// falsely green-light them after the fact. Side-effect leaves must
// declare Validate (confirm-before-RunE) or dispatch through the
// gated CallTool path. Dry-run previews may finish without CallTool.
if corecmd.BoolFlag(c, "dry-run") {
return nil
}
return fmt.Errorf("contract: user_required confirmation was never obtained via CallTool for %q; add Validate for local side effects or dispatch through deps.Caller.CallTool", c.Name())
}
return nil
}
+2 -1
View File
@@ -20,6 +20,7 @@ import (
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
@@ -554,7 +555,7 @@ func TestLiveMountExplicitSafetyDrivesRuntimeAndContractFinal(t *testing.T) {
cmd := mount(s)
root.AddCommand(cmd)
final, ok := contract.RuntimeContractFinal(cmd)
final, ok := contractfinal.RuntimeContractFinal(cmd)
if !ok || final.Safety == nil {
t.Fatal("mounted Shortcut must publish ContractFinal Safety")
}
@@ -8,7 +8,7 @@ set -eu
# Do NOT compare Catalog fields to Catalog provenance labels: that is a tautology
# (both sides read the same embedded snapshot). The real homology gate is
# TestUserRequiredSafetyHomologyWithRuntimeGate in
# internal/cli/contract_safety_homology_external_test.go — it walks the live
# internal/cli/homology/safety_homology_test.go — it walks the live
# Cobra tree, reads ContractFinal.Safety, compares to AssembleSchemaRegistry
# ToolSpec.Confirmation, and probes the runtime gate.
@@ -20,7 +20,7 @@ if [ -e internal/cli/schema_hints ]; then
exit 1
fi
go test ./internal/cli \
go test ./internal/cli/homology \
-run '^TestUserRequiredSafetyHomologyWithRuntimeGate$' \
-count=1
+9 -2
View File
@@ -251,11 +251,18 @@ fi
# Run the typed content gates as policy, rather than treating non-empty
# correction/exclusion maps as proof that their exact keys and winners are
# valid against the shipped Catalog and pinned MCP metadata.
# Homology (HOM-*) CI entrypoints: Safety confirmation truth + vocabulary /
# whitelist pins live in ./internal/cli/homology; parameter/help set equality
# is in ./internal/app below; bindings/mapping subset remains in ./internal/cli.
# Keep TestHomologyCIEntrypointsPinned in sync when adding gate IDs to policy.
go test ./internal/cli \
-run '^(TestEmbeddedSchemaCatalog.*|TestEmbeddedSchemaAllPayload.*|TestRuntimeSchemaAllPayload.*|TestSchemaAllReturnsCompleteEmbeddedLeafSchemas|TestSchemaCatalogDeliveryCompleteness.*|TestValidateSchemaDeliveryInvariants.*|TestSchemaAliasViewProblem.*|TestSchemaDeliveryToolsByCanonical.*|TestSchemaUsesEmbeddedCatalogWithoutRuntimeLoad|TestWalkLeafCommandsTraversesAnnotatedHiddenSubtree|TestSchemaParameterBindingsMatchReviewedBaselineAndEmbeddedCatalog|TestDecodeSchemaParameterBindingsFailsClosed|TestSchemaParameterBindingManifestHashIsExactContentNotCount|TestBuildEffectiveCommandRegistryFailsClosedOnInvalidParameterBindingSource|TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys|TestEmbeddedCatalogMCPParameterMappingsAreComplete|TestSchemaParameterMappingAuditExclusionRules|TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty|TestRuntimeCommandParameterSpecsPreserveReviewedEmptyPropertyProvenance|TestSchemaParameterBindingCorrectionsAreReviewed|TestUserRequiredSafetyHomologyWithRuntimeGate)$' \
-run '^(TestEmbeddedSchemaCatalog.*|TestEmbeddedSchemaAllPayload.*|TestRuntimeSchemaAllPayload.*|TestSchemaAllReturnsCompleteEmbeddedLeafSchemas|TestSchemaCatalogDeliveryCompleteness.*|TestValidateSchemaDeliveryInvariants.*|TestSchemaAliasViewProblem.*|TestSchemaDeliveryToolsByCanonical.*|TestSchemaUsesEmbeddedCatalogWithoutRuntimeLoad|TestWalkLeafCommandsTraversesAnnotatedHiddenSubtree|TestSchemaParameterBindingsMatchReviewedBaselineAndEmbeddedCatalog|TestDecodeSchemaParameterBindingsFailsClosed|TestSchemaParameterBindingManifestHashIsExactContentNotCount|TestBuildEffectiveCommandRegistryFailsClosedOnInvalidParameterBindingSource|TestValidateSchemaParameterBindingDeliveryRejectsStaleReviewedKeys|TestEmbeddedCatalogMCPParameterMappingsAreComplete|TestSchemaParameterMappingAuditExclusionRules|TestRuntimeSchemaReviewedMappingExclusionSelectsEmptyProperty|TestRuntimeCommandParameterSpecsPreserveReviewedEmptyPropertyProvenance|TestSchemaParameterBindingCorrectionsAreReviewed|TestResolveMetaFailsClosedOnUnusableMetaIndex)$' \
-count=1
go test ./internal/cli/homology \
-run '^(TestUserRequiredSafetyHomologyWithRuntimeGate|TestHomologyDecisionDocPinsPathAAndGateIDs|TestMCPPassthroughAdmissionExcludesLeafAndShortcut|TestHomologyCIEntrypointsPinned)$' \
-count=1
go test ./internal/helpers \
-run '^TestSheetConfirmationGuardCoversEveryProtectedLeaf$' \
-run '^(TestSheetConfirmationGuardCoversEveryProtectedLeaf|TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool)$' \
-count=1
go test ./internal/app \
-run '^(TestEmbeddedSchemaContractMapsToExecutableTree|TestFinalSchemaParametersMatchExecutableHelpFlags|TestEmbeddedSchemaParametersMatchExecutableHelpFlags|TestSchemaHelpFlagCompletenessRejects.*|TestRuntimeSchemaCompletenessCoversPublicCommandTree|TestReviewedRoutedInterfacesReachFinalSchema|TestViewGetWrappersUsePinnedGetViewsInterface|TestReviewedInterfaceDispositionSourceOwnsRuntimeSurface|TestSheetFinalSchemaConfirmationMatchesRuntimeGuards|TestRegisterPluginHTTPServerDoesNotProbeEndpoint|TestRegisterStdioServerFromManifestDoesNotStartProcess)$' \