diff --git a/AGENTS.md b/AGENTS.md index 0f5d76c2..59f6bdca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 539f9510..695cbd3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/command-domain-model.md b/docs/command-domain-model.md index 30889283..a6cc10bf 100644 --- a/docs/command-domain-model.md +++ b/docs/command-domain-model.md @@ -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 确认,具体业务执行完全自定义 | ## 设计不变量 diff --git a/docs/command-framework-architecture.md b/docs/command-framework-architecture.md index a2faee5f..917a050b 100644 --- a/docs/command-framework-architecture.md +++ b/docs/command-framework-architecture.md @@ -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) diff --git a/docs/flag-help-schema-homology.md b/docs/flag-help-schema-homology.md index 2b004ba0..3f1950eb 100644 --- a/docs/flag-help-schema-homology.md +++ b/docs/flag-help-schema-homology.md @@ -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 --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 --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-*` 与差分门禁)。 diff --git a/docs/rfc-command-framework-convergence.md b/docs/rfc-command-framework-convergence.md index 526effa6..fbb8b936 100644 --- a/docs/rfc-command-framework-convergence.md +++ b/docs/rfc-command-framework-convergence.md @@ -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 里程碑前要求完整删除。 --- diff --git a/internal/cli/command_meta.go b/internal/cli/command_meta.go index 26f09639..7bd0246b 100644 --- a/internal/cli/command_meta.go +++ b/internal/cli/command_meta.go @@ -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 } diff --git a/internal/cli/command_safety.go b/internal/cli/command_safety.go index fd98653c..69212f11 100644 --- a/internal/cli/command_safety.go +++ b/internal/cli/command_safety.go @@ -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. diff --git a/internal/cli/command_safety_test.go b/internal/cli/command_safety_test.go index 5cb62cbf..090bf9e9 100644 --- a/internal/cli/command_safety_test.go +++ b/internal/cli/command_safety_test.go @@ -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") diff --git a/internal/cli/contract_final_test.go b/internal/cli/contract_final_assembly_test.go similarity index 67% rename from internal/cli/contract_final_test.go rename to internal/cli/contract_final_assembly_test.go index 2877a99f..a63b4bdb 100644 --- a/internal/cli/contract_final_test.go +++ b/internal/cli/contract_final_assembly_test.go @@ -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 +} diff --git a/internal/cli/contract_safety_overlay_test.go b/internal/cli/contract_safety_overlay_test.go new file mode 100644 index 00000000..ada164d9 --- /dev/null +++ b/internal/cli/contract_safety_overlay_test.go @@ -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) + } +} diff --git a/internal/cli/contract_schema_embed_test.go b/internal/cli/contract_schema_embed_test.go deleted file mode 100644 index 985a7e3c..00000000 --- a/internal/cli/contract_schema_embed_test.go +++ /dev/null @@ -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") - } -} diff --git a/internal/cli/contractfinal/doc.go b/internal/cli/contractfinal/doc.go new file mode 100644 index 00000000..f32ccc8d --- /dev/null +++ b/internal/cli/contractfinal/doc.go @@ -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 diff --git a/internal/cli/contractfinal/final_test.go b/internal/cli/contractfinal/final_test.go new file mode 100644 index 00000000..bf072d1b --- /dev/null +++ b/internal/cli/contractfinal/final_test.go @@ -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") + } +} diff --git a/internal/cli/contractfinal/homology.go b/internal/cli/contractfinal/homology.go new file mode 100644 index 00000000..11bc339c --- /dev/null +++ b/internal/cli/contractfinal/homology.go @@ -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 +} diff --git a/internal/cli/contractfinal/homology_test.go b/internal/cli/contractfinal/homology_test.go new file mode 100644 index 00000000..29041065 --- /dev/null +++ b/internal/cli/contractfinal/homology_test.go @@ -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") + } +} diff --git a/internal/cli/contract_delivery_seam.go b/internal/cli/contractfinal/param_decls.go similarity index 55% rename from internal/cli/contract_delivery_seam.go rename to internal/cli/contractfinal/param_decls.go index b8ae1468..3ef3c509 100644 --- a/internal/cli/contract_delivery_seam.go +++ b/internal/cli/contractfinal/param_decls.go @@ -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) -} diff --git a/internal/cli/contractfinal/store.go b/internal/cli/contractfinal/store.go new file mode 100644 index 00000000..5ca32df1 --- /dev/null +++ b/internal/cli/contractfinal/store.go @@ -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) + } +} diff --git a/internal/cli/homology/doc.go b/internal/cli/homology/doc.go new file mode 100644 index 00000000..b5c3237a --- /dev/null +++ b/internal/cli/homology/doc.go @@ -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 diff --git a/internal/cli/contract_final_consistency_external_test.go b/internal/cli/homology/final_consistency_test.go similarity index 97% rename from internal/cli/contract_final_consistency_external_test.go rename to internal/cli/homology/final_consistency_test.go index e186124e..b1aa6236 100644 --- a/internal/cli/contract_final_consistency_external_test.go +++ b/internal/cli/homology/final_consistency_test.go @@ -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) { diff --git a/internal/cli/contractfinal_lookup_test.go b/internal/cli/homology/final_lookup_test.go similarity index 82% rename from internal/cli/contractfinal_lookup_test.go rename to internal/cli/homology/final_lookup_test.go index 87cfba27..e92a71af 100644 --- a/internal/cli/contractfinal_lookup_test.go +++ b/internal/cli/homology/final_lookup_test.go @@ -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") } diff --git a/internal/cli/homology_gates_plan_test.go b/internal/cli/homology/gates_plan_test.go similarity index 61% rename from internal/cli/homology_gates_plan_test.go rename to internal/cli/homology/gates_plan_test.go index dc85366e..e0b26a4f 100644 --- a/internal/cli/homology_gates_plan_test.go +++ b/internal/cli/homology/gates_plan_test.go @@ -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) + } + } +} diff --git a/internal/cli/paramdecl_diag_test.go b/internal/cli/homology/paramdecl_diag_test.go similarity index 97% rename from internal/cli/paramdecl_diag_test.go rename to internal/cli/homology/paramdecl_diag_test.go index 8fd99261..ebd082fc 100644 --- a/internal/cli/paramdecl_diag_test.go +++ b/internal/cli/homology/paramdecl_diag_test.go @@ -1,4 +1,4 @@ -package cli_test +package homology import ( "testing" diff --git a/internal/cli/contract_safety_homology_external_test.go b/internal/cli/homology/safety_homology_test.go similarity index 98% rename from internal/cli/contract_safety_homology_external_test.go rename to internal/cli/homology/safety_homology_test.go index f08b3448..f6b712ac 100644 --- a/internal/cli/contract_safety_homology_external_test.go +++ b/internal/cli/homology/safety_homology_test.go @@ -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 } diff --git a/internal/cli/product_decl_test.go b/internal/cli/product_decl_test.go deleted file mode 100644 index 5401c978..00000000 --- a/internal/cli/product_decl_test.go +++ /dev/null @@ -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"}) -} diff --git a/internal/cli/runtime_schema.go b/internal/cli/runtime_schema.go index 86aee5a8..2196011a 100644 --- a/internal/cli/runtime_schema.go +++ b/internal/cli/runtime_schema.go @@ -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) { diff --git a/internal/cli/runtime_schema_coverage_more_test.go b/internal/cli/runtime_schema_coverage_more_test.go index f29acd79..ec9c6239 100644 --- a/internal/cli/runtime_schema_coverage_more_test.go +++ b/internal/cli/runtime_schema_coverage_more_test.go @@ -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) } diff --git a/internal/cli/runtime_schema_seam.go b/internal/cli/runtime_schema_seam.go new file mode 100644 index 00000000..301cbac3 --- /dev/null +++ b/internal/cli/runtime_schema_seam.go @@ -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...) +} diff --git a/internal/cli/runtimeannotate/annotate.go b/internal/cli/runtimeannotate/annotate.go new file mode 100644 index 00000000..83b1be89 --- /dev/null +++ b/internal/cli/runtimeannotate/annotate.go @@ -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 +} diff --git a/internal/cli/runtimeannotate/annotations.go b/internal/cli/runtimeannotate/annotations.go new file mode 100644 index 00000000..77e88be7 --- /dev/null +++ b/internal/cli/runtimeannotate/annotations.go @@ -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" +) diff --git a/internal/cli/runtimeannotate/constraints.go b/internal/cli/runtimeannotate/constraints.go new file mode 100644 index 00000000..8f4df5bc --- /dev/null +++ b/internal/cli/runtimeannotate/constraints.go @@ -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 +} diff --git a/internal/cli/runtimeannotate/doc.go b/internal/cli/runtimeannotate/doc.go new file mode 100644 index 00000000..11c4b203 --- /dev/null +++ b/internal/cli/runtimeannotate/doc.go @@ -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 diff --git a/internal/cli/runtimeannotate/embed_test.go b/internal/cli/runtimeannotate/embed_test.go new file mode 100644 index 00000000..5316d1a3 --- /dev/null +++ b/internal/cli/runtimeannotate/embed_test.go @@ -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") + } +} diff --git a/internal/cli/schema_agent_examples.go b/internal/cli/schema_agent_examples.go index bc3ec0ac..010d6988 100644 --- a/internal/cli/schema_agent_examples.go +++ b/internal/cli/schema_agent_examples.go @@ -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) diff --git a/internal/cli/schema_agent_selection.go b/internal/cli/schema_agent_selection.go index 387f7cc7..df7c7fa9 100644 --- a/internal/cli/schema_agent_selection.go +++ b/internal/cli/schema_agent_selection.go @@ -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 } diff --git a/internal/cli/schema_cobra_binding.go b/internal/cli/schema_cobra_binding.go index 2038a7ca..bee0d05d 100644 --- a/internal/cli/schema_cobra_binding.go +++ b/internal/cli/schema_cobra_binding.go @@ -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) } } diff --git a/internal/cli/schema_registry_test_helpers_test.go b/internal/cli/schema_registry_test_helpers_test.go index d090783d..57a83752 100644 --- a/internal/cli/schema_registry_test_helpers_test.go +++ b/internal/cli/schema_registry_test_helpers_test.go @@ -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}) diff --git a/internal/cli/schema_runtime_registry.go b/internal/cli/schema_runtime_registry.go index 37883656..f3c5f27b 100644 --- a/internal/cli/schema_runtime_registry.go +++ b/internal/cli/schema_runtime_registry.go @@ -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) diff --git a/internal/cli/schema_runtime_registry_contract_test.go b/internal/cli/schema_runtime_registry_contract_test.go index 829454fc..8b411e5c 100644 --- a/internal/cli/schema_runtime_registry_contract_test.go +++ b/internal/cli/schema_runtime_registry_contract_test.go @@ -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{ diff --git a/internal/corecmd/contract/doc.go b/internal/corecmd/contract/doc.go index a9bd5ad9..bcf743ea 100644 --- a/internal/corecmd/contract/doc.go +++ b/internal/corecmd/contract/doc.go @@ -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 diff --git a/internal/corecmd/contract/final.go b/internal/corecmd/contract/final.go index 6a85d6e2..19f3581f 100644 --- a/internal/corecmd/contract/final.go +++ b/internal/corecmd/contract/final.go @@ -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) - } -} diff --git a/internal/corecmd/contract/product_decl_test.go b/internal/corecmd/contract/product_decl_test.go new file mode 100644 index 00000000..d66f47d2 --- /dev/null +++ b/internal/corecmd/contract/product_decl_test.go @@ -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"}) +} diff --git a/internal/corecmd/contract_decl.go b/internal/corecmd/contract_decl.go index adaffd71..8ab6be4b 100644 --- a/internal/corecmd/contract_decl.go +++ b/internal/corecmd/contract_decl.go @@ -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 diff --git a/internal/corecmd/contract_decl_test.go b/internal/corecmd/contract_decl_test.go index ad055109..0978f303 100644 --- a/internal/corecmd/contract_decl_test.go +++ b/internal/corecmd/contract_decl_test.go @@ -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 { diff --git a/internal/corecmd/contract_register_seam_test.go b/internal/corecmd/contract_register_seam_test.go index 1e2ad4ec..22faf95a 100644 --- a/internal/corecmd/contract_register_seam_test.go +++ b/internal/corecmd/contract_register_seam_test.go @@ -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") } } diff --git a/internal/corecmd/corecmd.go b/internal/corecmd/corecmd.go index 6bc04d5b..db59cc14 100644 --- a/internal/corecmd/corecmd.go +++ b/internal/corecmd/corecmd.go @@ -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 diff --git a/internal/corecmd/corecmd_test.go b/internal/corecmd/corecmd_test.go index a40886bf..7d5b70e9 100644 --- a/internal/corecmd/corecmd_test.go +++ b/internal/corecmd/corecmd_test.go @@ -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") } } diff --git a/internal/generator/agentmetadata/contract_final.go b/internal/generator/agentmetadata/contract_final.go index 6a51dfac..1a63cfe2 100644 --- a/internal/generator/agentmetadata/contract_final.go +++ b/internal/generator/agentmetadata/contract_final.go @@ -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 } diff --git a/internal/generator/agentmetadata/manual_hints.go b/internal/generator/agentmetadata/manual_hints.go index ce33c0ab..eb53ef36 100644 --- a/internal/generator/agentmetadata/manual_hints.go +++ b/internal/generator/agentmetadata/manual_hints.go @@ -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) } } diff --git a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go index 4940f013..4cbfebe4 100644 --- a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go +++ b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go @@ -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}, }} diff --git a/internal/generator/agentmetadata/pipeline.go b/internal/generator/agentmetadata/pipeline.go index 06bff06a..119b814b 100644 --- a/internal/generator/agentmetadata/pipeline.go +++ b/internal/generator/agentmetadata/pipeline.go @@ -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) } } diff --git a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go index 1de92236..16cb0c24 100644 --- a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go +++ b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go @@ -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{ diff --git a/internal/helpers/aitable_schema_test.go b/internal/helpers/aitable_schema_test.go index d4c4ef53..4616c889 100644 --- a/internal/helpers/aitable_schema_test.go +++ b/internal/helpers/aitable_schema_test.go @@ -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) } } diff --git a/internal/helpers/cmdcore_phase2_coverage_test.go b/internal/helpers/cmdcore_phase2_coverage_test.go index b0c5cd4e..6aa72065 100644 --- a/internal/helpers/cmdcore_phase2_coverage_test.go +++ b/internal/helpers/cmdcore_phase2_coverage_test.go @@ -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) diff --git a/internal/helpers/contract_defects_test.go b/internal/helpers/contract_defects_test.go index f7fd7ff5..2cfdb535 100644 --- a/internal/helpers/contract_defects_test.go +++ b/internal/helpers/contract_defects_test.go @@ -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") } diff --git a/internal/helpers/leaf.go b/internal/helpers/leaf.go index 13c6fbe2..5d224a86 100644 --- a/internal/helpers/leaf.go +++ b/internal/helpers/leaf.go @@ -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 } diff --git a/internal/shortcut/shortcut_test.go b/internal/shortcut/shortcut_test.go index 3fe4fd46..abff25b9 100644 --- a/internal/shortcut/shortcut_test.go +++ b/internal/shortcut/shortcut_test.go @@ -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") } diff --git a/scripts/policy/check-runtime-confirmation-truth.sh b/scripts/policy/check-runtime-confirmation-truth.sh index 2e64fd0c..41df1d52 100755 --- a/scripts/policy/check-runtime-confirmation-truth.sh +++ b/scripts/policy/check-runtime-confirmation-truth.sh @@ -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 diff --git a/scripts/policy/check-schema-catalog.sh b/scripts/policy/check-schema-catalog.sh index cc5c9d02..c1ed4139 100755 --- a/scripts/policy/check-schema-catalog.sh +++ b/scripts/policy/check-schema-catalog.sh @@ -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)$' \