refactor(schema): break corecmd→cli cycle and split contract seams
Move AnnotateRuntime* into cli/runtimeannotate and the Cobra-keyed ContractFinal store into cli/contractfinal so corecmd depends on thin subpackages instead of the cli delivery root. Keep contract as DTO-only, document Description declare-vs-delivery, and house homology gates under cli/homology. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,8 +30,14 @@ changes must not rewrite it mechanically.
|
||||
- Today: `helpers.LeafSpec` / `shortcut.Shortcut` → `corecmd.Spec` (+ optional `Contract`) → `corecmd.New`
|
||||
- **Declare = final Schema source**: `Flags` / `Constraints` / `Safety` / `ConstParams` / `Contract` (`corecmd.ContractDecl`; nested fields are `contract.*`)
|
||||
- Naming: `ContractDecl` is the authoring leaf declaration. "Schema" means Catalog / `ToolSpec` delivery — do not reintroduce `SchemaDecl`.
|
||||
- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `cli.RegisterRuntimeContractFinal` (annotate + store seam); assembly **pass-throughs** Final. Production code must not call `contract.RegisterRuntimeContractFinal` directly.
|
||||
- Package seam: types/registries → `corecmd/contract` (sole definitions); Cobra annotate+store seam + Catalog/`ResolveMeta` delivery → `internal/cli`. cli does not redefine contract types.
|
||||
- `Safety` uses `contract.SafetySpec` (`internal/corecmd/contract` only — no `cli.*` type alias). Its `confirmation` drives the runtime gate; `effect` / `risk` / `idempotency` are published unchanged. When `Contract` is set, convert once via `contractfinal.RegisterRuntimeContractFinal` (framework) or `cli.RegisterRuntimeContractFinal` (product re-export); assembly **pass-throughs** Final.
|
||||
- Package seam:
|
||||
- types / ProductDecl → `corecmd/contract` (DTO only; **no** Cobra-keyed ContractFinal store)
|
||||
- AnnotateRuntime* writers → `internal/cli/runtimeannotate` (`corecmd` may import; must **not** import `internal/cli` root)
|
||||
- ContractFinal cobra store + Register → `internal/cli/contractfinal`
|
||||
- homology gates → `internal/cli/homology`
|
||||
- Catalog / `ResolveMeta` / go:embed → `internal/cli` root (thin re-exports of annotate/store APIs)
|
||||
- Description declare vs delivery: construction requires `ContractDecl.Description` (evidence). Catalog delivery prefers Cobra Long → provenance `cobra_help`; without Long, declared text → `contract_final`. Title: declared first, then Short, then MCP. Do **not** read this as "declare = wire final" or dual authority.
|
||||
- **Execute** = hooks (`Validate` / `Call` / `RunE` / `PostMount`) — not a second surface authority
|
||||
- Declaration path has **no reviewed parallel fields**; migration-only `runtime_gate` annotate until `Safety` is declared
|
||||
|
||||
@@ -41,7 +47,7 @@ changes must not rewrite it mechanically.
|
||||
- Hard rule: every help/Schema fact is **declared** **or** **annotated**; never inference-only (§1.1–§1.3; framework §5.0).
|
||||
- Embed path: `corecmd.New` → `dws.schema.*` annotations → Schema catalog assembly
|
||||
- MCP metadata must not create CLI flags; optional 1:1 passthrough is a gated subset only.
|
||||
- Planned gate IDs: `HOM-P*`, `HOM-S*`, `HOM-I1`, `HOM-D1` (see that doc §4).
|
||||
- Gate IDs: `HOM-P*`, `HOM-S*`, `HOM-I1`, `HOM-D1` (see that doc §3–§4). `HOM-P1`/`HOM-D1`/`HOM-S1`/`HOM-S2` are on the `check-schema-catalog.sh` policy whitelist; remaining IDs land incrementally.
|
||||
|
||||
## Agent Schema contract
|
||||
|
||||
@@ -379,10 +385,13 @@ lower-precedence source. A higher-precedence declaration may still raise an
|
||||
optional flag to required. `cli_required` continues to mirror the executable
|
||||
Cobra marker.
|
||||
|
||||
For command-level description, Cobra Long wins when present (delivered
|
||||
provenance `cobra_help`, resolution `cobra_help_preferred`); without Long,
|
||||
ContractDecl description is delivered as `contract_final`. Title keeps declared
|
||||
ContractDecl / ContractFinal first, then Cobra Short, then MCP metadata.
|
||||
For command-level description: **declare required, delivery Long may win**.
|
||||
`ContractDecl.Description` is mandatory at construction (declaration evidence).
|
||||
Catalog delivery prefers Cobra Long when present (provenance `cobra_help`,
|
||||
resolution `cobra_help_preferred`); without Long, the declared Description is
|
||||
delivered as `contract_final`. Title keeps declared ContractDecl /
|
||||
ContractFinal first, then Cobra Short, then MCP metadata. This is one authority
|
||||
chain with an explicit delivery preference — not two competing sources.
|
||||
Generic RPC prose may remain an unselected provenance candidate (and
|
||||
parameter-level `interface_description`); it must not overwrite a specialized
|
||||
leaf's title or description.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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 确认,具体业务执行完全自定义 |
|
||||
|
||||
## 设计不变量
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ NewLeafCommand(LeafSpec{
|
||||
|---|---|---|
|
||||
| Identity | 评审源 | `schema_command_registry` |
|
||||
| Parameters.`name/type/required/default/property` | **声明**(或同形 annotate) | `Flags` / `Bind` |
|
||||
| Parameters.`description` | 声明 usage + 可选 hints overlay | `Usage`;hints 不得改 type/required/default |
|
||||
| Parameters.`description` | 声明 usage(`FlagSpec.Usage` / ParamDecl) | `schema_hints/` 已退役;不得用 overlay 改 type/required/default |
|
||||
| Parameters.`interface_*` | 评审源 | MCP meta / bindings;**不造 flag** |
|
||||
| Constraints | **声明** | `Constraints` |
|
||||
| Positionals | **声明** 或显式 annotate | 目标 `Args`;禁止推断 |
|
||||
@@ -192,14 +192,24 @@ Identity 与 selection **刻意不**由 Contract 取代(RFC 决策 8 / schema
|
||||
|
||||
- Flags / ConstParams / Constraints → 注册、校验与 `ConstraintHelp`;SafetySpec → 运行时 `ConfirmSafety`(command);
|
||||
- Call / Execute 作为执行体;业务参数不得在 Call 内装配(helpers 门禁);
|
||||
- **Contract → Schema 嵌入**:参数/约束写原生 annotation,SafetySpec 与 ContractDecl 注册为类型化 Contract Final 并由 Schema 组装透传。
|
||||
- **Contract → Schema 嵌入**:参数/约束写原生 annotation,SafetySpec 与 ContractDecl 注册为类型化 Contract Final 并由 Schema 组装透传;
|
||||
- Selection 权威为 `ContractDecl.Selection` / `ProductDecl`(`contract_final`);`schema_hints/` 已退役。
|
||||
|
||||
仍缺:
|
||||
已进 CI(`make policy` → `check-schema-catalog.sh` / `check-runtime-confirmation-truth.sh`):
|
||||
|
||||
1. `HOM-P*` / `HOM-D1` 门禁:禁止 hints 静默改写 type/required/default;help ≡ schema parameters;
|
||||
2. 重新 `make generate-schema` 后把嵌入结果固化进签入 catalog(注意本机 OOM);identity/selection 仍走评审源。
|
||||
| Gate ID | CI 入口(`-run` 白名单 / 脚本) |
|
||||
|---|---|
|
||||
| `HOM-P1` / `HOM-D1` | `./internal/app`:`TestFinalSchemaParametersMatchExecutableHelpFlags`、`TestEmbeddedSchemaParametersMatchExecutableHelpFlags` |
|
||||
| `HOM-P2`(参数映射/bindings 子集) | `./internal/cli`:`TestSchemaParameterBindingsMatchReviewedBaselineAndEmbeddedCatalog`、`TestEmbeddedCatalogMCPParameterMappingsAreComplete` 等 bindings 门禁 |
|
||||
| `HOM-S1` / `HOM-S2`(confirmation 同源) | `./internal/cli/homology`:`TestUserRequiredSafetyHomologyWithRuntimeGate` + `check-runtime-confirmation-truth.sh`;`./internal/app`:`TestSheetFinalSchemaConfirmationMatchesRuntimeGuards` |
|
||||
| 词汇/决策钉扎 | `./internal/cli/homology`:`TestHomologyDecisionDocPinsPathAAndGateIDs`、`TestMCPPassthroughAdmissionExcludesLeafAndShortcut`、`TestHomologyCIEntrypointsPinned` |
|
||||
|
||||
已落地(写命令确认语义,`HOM-S2` 起点):
|
||||
仍缺(未宣称全量 CI 覆盖):
|
||||
|
||||
1. 独立可执行的 `HOM-P3`(constraints ≡ AnnotateConstraints)与 `HOM-S3`(read 不得误投影 user_required)全量 gate;
|
||||
2. `HOM-I1` 作为单独 gate ID 的显式用例(MCP bindings ⊆ Contract flags 已有映射审计子集,但未钉 `HOM-I1` 标签)。
|
||||
|
||||
已落地(写命令确认语义,`HOM-S2`):
|
||||
|
||||
- `AnnotateRuntimeGate` / `dws.schema.runtime_gate`;Leaf `PostMount: devAppMetaWrite`;手写 delete/robot 等同路径显式标注;
|
||||
- Schema 组装在无 Contract Safety 但有 gate 时 overlay `confirmation=user_required`(`applyContractGateToSafety`);
|
||||
@@ -207,24 +217,24 @@ Identity 与 selection **刻意不**由 Contract 取代(RFC 决策 8 / schema
|
||||
|
||||
## 4. Schema 投影与 Safety 门禁规划
|
||||
|
||||
以下门禁在落地时应作为独立 policy / 测试交付(门禁 ID 稳定,便于 CI 认领)。
|
||||
以下门禁 ID 稳定,便于 CI 认领。§3 表标明哪些已挂入 `check-schema-catalog.sh`。
|
||||
|
||||
| Gate ID | 断言 | 范围 |
|
||||
|---|---|---|
|
||||
| `HOM-P1` | 受管 leaf 的 schema `parameters[].name` 集合 ≡ cobra 本地 flag 名集合(排除全局 persistent) | LeafSpec / 未来 Contract 编译命令 |
|
||||
| `HOM-P2` | schema parameter `type` / `required` / `default` 与 cobra DefValue / MarkFlagRequired / FlagSpec 一致;hints overlay **不得**改写这三项,只可补 description | 同上 |
|
||||
| `HOM-P3` | schema 关系约束(require_one_of / mutually_exclusive)≡ Contract/Leaf `Constraints` 投影(与 `AnnotateConstraints` 同构) | 声明了 Constraints 的命令 |
|
||||
| `HOM-S1` | 若 Contract/Leaf `Risk` ∈ {write, high-risk-write},则 schema `confirmation=user_required`,且 help Safety 行与之同语义 | 使用 Risk 确认的受管命令 |
|
||||
| `HOM-S2` | 若命令走显式 write guard(如 `devAppRequireWriteGuard`)而非 Risk,则必须人工标注 `dws.schema.runtime_gate`(或等价 reviewed Safety);Schema 不得呈 `confirmation=not_required`;符合 §1.1 declare OR annotate | 今日 devapp 写命令 |
|
||||
| `HOM-S3` | `Risk=read`(或空→read)不得投影为 `user_required`,除非有 reviewed exclusion reason | 受管读命令 |
|
||||
| `HOM-I1` | `interface_ref` 存在时,bindings 覆盖的 CLI flag ⊆ Contract flags;MCP meta **不**引入额外 CLI flag | 有 MCP 绑定的命令 |
|
||||
| `HOM-D1` | `dws <path> --help` Flags 段与 schema leaf parameters 零未解释增量 | 抽样 + 受管全集逐步扩大 |
|
||||
| Gate ID | 断言 | 范围 | CI |
|
||||
|---|---|---|---|
|
||||
| `HOM-P1` | 受管 leaf 的 schema `parameters[].name` 集合 ≡ cobra 本地 flag 名集合(排除全局 persistent) | LeafSpec / Contract 编译命令 | **已进**(app help↔schema) |
|
||||
| `HOM-P2` | schema parameter `type` / `required` / `default` 与 cobra DefValue / MarkFlagRequired / FlagSpec 一致;不得用已退役 hints overlay 改写这三项 | 同上 | **部分**(bindings/mapping 门禁) |
|
||||
| `HOM-P3` | schema 关系约束(require_one_of / mutually_exclusive)≡ Contract/Leaf `Constraints` 投影(与 `AnnotateConstraints` 同构) | 声明了 Constraints 的命令 | 规划 |
|
||||
| `HOM-S1` | Contract/Leaf `user_required` Safety 与运行时 Confirm/gate 同源,且 help Safety 行同语义 | 受管写/破坏性命令 | **已进** |
|
||||
| `HOM-S2` | 若命令走显式 write guard(如 `devAppRequireWriteGuard`)而非完整 SafetySpec,则必须人工标注 `dws.schema.runtime_gate`;Schema 不得呈 `confirmation=not_required`;符合 §1.1 declare OR annotate | 今日 devapp 写命令 | **已进**(同源测试含 gate 路径) |
|
||||
| `HOM-S3` | `Risk=read`(或空→read)不得投影为 `user_required`,除非有 reviewed exclusion reason | 受管读命令 | 规划 |
|
||||
| `HOM-I1` | `interface_ref` 存在时,bindings 覆盖的 CLI flag ⊆ Contract flags;MCP meta **不**引入额外 CLI flag | 有 MCP 绑定的命令 | **部分**(mapping 审计) |
|
||||
| `HOM-D1` | `dws <path> --help` Flags 段与 schema leaf parameters 零未解释增量 | 受管公开 leaf | **已进**(与 HOM-P1 同测) |
|
||||
|
||||
落地顺序建议:
|
||||
|
||||
1. 先对 LeafSpec 命令实现 `HOM-P1`/`HOM-P2`/`HOM-P3`(反射 cobra 即可起步,因 flag 已由 Contract 注册);
|
||||
2. 再收 `HOM-S1`–`S3`(需产品确认 devapp 是否升格 Risk,或保持 guard + 诚实 provenance);
|
||||
3. `HOM-I1`/`HOM-D1` 接入 `check-schema-*` / PR 门禁。
|
||||
1. 保持 `HOM-P1`/`HOM-D1`/`HOM-S1`/`HOM-S2` 在 `check-schema-catalog.sh` 白名单中(勿再只靠词汇钉扎);
|
||||
2. 补独立 `HOM-P3` / `HOM-S3` 可执行 gate,并把 `HOM-P2`/`HOM-I1` 从「部分」升到全量标签;
|
||||
3. 新 Leaf 继续走 Contract 嵌入;禁止 `schema_hints/` 回潮。
|
||||
|
||||
## 5. 路径 B 子通道:1:1 MCP 透传叶(可选,非主路径)
|
||||
|
||||
@@ -245,6 +255,6 @@ Identity 与 selection **刻意不**由 Contract 取代(RFC 决策 8 / schema
|
||||
|
||||
## 6. 非目标
|
||||
|
||||
- 不把 selection 文案或 canonical identity 塞进 Contract。
|
||||
- 不把 canonical identity / 导航塞进 Contract(仍归 reviewed `CommandRegistry`);selection 文案已由 `ContractDecl.Selection` / `ProductDecl` 声明(非 hints)。
|
||||
- 不要求删除 LeafSpec 门面。
|
||||
- 不把「生成 catalog 字节一致」当作运行时同源的充分条件(仍需 `HOM-*` 与差分门禁)。
|
||||
|
||||
@@ -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 里程碑前要求完整删除。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
+16
-125
@@ -18,42 +18,18 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestContractFinalTypedRegistryNoJSON(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "x"}
|
||||
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) })
|
||||
|
||||
RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
|
||||
Title: "T",
|
||||
Safety: &contract.SafetySpec{
|
||||
Effect: "write", Confirmation: "user_required", Idempotency: "retryable",
|
||||
},
|
||||
Selection: &contract.SelectionSpec{AgentSummary: "sum", UseWhen: []string{"u"}},
|
||||
Identity: &contract.ToolIdentitySpec{ProductID: "p", Name: "n"},
|
||||
})
|
||||
if cmd.Annotations != nil {
|
||||
if _, ok := cmd.Annotations["dws.schema.final"]; ok {
|
||||
t.Fatal("must not write JSON annotation dws.schema.final")
|
||||
}
|
||||
}
|
||||
got, ok := contract.RuntimeContractFinal(cmd)
|
||||
if !ok || got.Title != "T" || got.Safety == nil || got.Safety.Idempotency != "retryable" {
|
||||
t.Fatalf("got %#v ok=%v", got, ok)
|
||||
}
|
||||
if got.Selection == nil || got.Selection.Reviewed != nil {
|
||||
t.Fatalf("selection must not carry reviewed fields: %#v", got.Selection)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalPassThrough(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "create", Short: "s", Long: "l"}
|
||||
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) })
|
||||
t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(cmd) })
|
||||
cmd.Flags().String("mode", "", "usage")
|
||||
AnnotateRuntimeFlag(cmd, "mode", "mode", "string", false, "")
|
||||
RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
|
||||
runtimeannotate.AnnotateRuntimeFlag(cmd, "mode", "mode", "string", false, "")
|
||||
contractfinal.RegisterRuntimeContractFinal(cmd, contract.ContractFinalPayload{
|
||||
Title: "Final Title",
|
||||
Safety: &contract.SafetySpec{
|
||||
Effect: "write", Confirmation: "user_required", Idempotency: "none",
|
||||
@@ -151,100 +127,6 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalRejectsReviewedSel
|
||||
}
|
||||
}
|
||||
|
||||
func mustFinal(t *testing.T, cmd *cobra.Command) contract.ContractFinalPayload {
|
||||
t.Helper()
|
||||
final, ok := contract.RuntimeContractFinal(cmd)
|
||||
if !ok {
|
||||
t.Fatal("missing final")
|
||||
}
|
||||
return final
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageContractFinalNilCommandGuards(t *testing.T) {
|
||||
// Nil command registration/lookup must be inert no-ops.
|
||||
RegisterRuntimeContractFinal(nil, contract.ContractFinalPayload{Title: "ignored"})
|
||||
if _, ok := contract.RuntimeContractFinal(nil); ok {
|
||||
t.Fatal("contract.RuntimeContractFinal(nil) must report no payload")
|
||||
}
|
||||
if contract.HasRuntimeContractFinal(nil) {
|
||||
t.Fatal("contract.HasRuntimeContractFinal(nil) must be false")
|
||||
}
|
||||
contract.ClearRuntimeContractFinalForTest(nil)
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageApplyParamDeclsSkipsBlankAndAnnotatesEnum(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "apply-params"}
|
||||
cmd.Flags().String("mode", "", "mode")
|
||||
required := false
|
||||
if err := ApplyParamDecls(cmd, []contract.ParamDecl{
|
||||
{Name: " "}, // blank names are skipped
|
||||
{
|
||||
Name: "mode", Property: "mode", Required: &required,
|
||||
InterfaceType: "string", Description: "mode desc",
|
||||
RequiredWhen: "when create", Enum: []string{"a", "b"},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyParamDecls() error = %v", err)
|
||||
}
|
||||
flag := cmd.Flags().Lookup("mode")
|
||||
if flag == nil {
|
||||
t.Fatal("missing mode flag")
|
||||
}
|
||||
if got := flag.Annotations["dws.schema.property"]; len(got) == 0 || got[0] != "mode" {
|
||||
t.Fatalf("property = %#v", flag.Annotations["dws.schema.property"])
|
||||
}
|
||||
if got := flag.Annotations["x-cli-enum"]; len(got) != 2 || got[0] != "a" || got[1] != "b" {
|
||||
t.Fatalf("enum = %#v", flag.Annotations["x-cli-enum"])
|
||||
}
|
||||
if err := ApplyParamDecls(nil, []contract.ParamDecl{{Name: "mode"}}); err != nil {
|
||||
t.Fatalf("ApplyParamDecls(nil) error = %v", err)
|
||||
}
|
||||
if err := ApplyParamDecls(cmd, nil); err != nil {
|
||||
t.Fatalf("ApplyParamDecls(nil decls) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyParamDeclsRejectsUnknownFlag(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "apply-params"}
|
||||
cmd.Flags().String("mode", "", "mode")
|
||||
err := ApplyParamDecls(cmd, []contract.ParamDecl{
|
||||
{Name: "mode", Property: "mode"},
|
||||
{Name: "missing-flag", Property: "missing"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("ApplyParamDecls() error = nil, want unknown flag")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing-flag") {
|
||||
t.Fatalf("ApplyParamDecls() error = %v, want missing-flag", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown flag") {
|
||||
t.Fatalf("ApplyParamDecls() error = %v, want unknown flag", err)
|
||||
}
|
||||
flag := cmd.Flags().Lookup("mode")
|
||||
if flag == nil {
|
||||
t.Fatal("missing mode flag")
|
||||
}
|
||||
if got := flag.Annotations["dws.schema.property"]; len(got) != 0 {
|
||||
t.Fatalf("fail-closed must not annotate before unknown ParamDecl; property = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRuntimeContractFinalRejectsForeignStoredValue(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "x"}
|
||||
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(cmd) })
|
||||
|
||||
// Defensive branch: a stored value that is not a *contract.ContractFinalPayload
|
||||
// (or a typed nil) must fail the read instead of panicking.
|
||||
contract.StoreRuntimeContractFinalRawForTest(cmd, "not-a-payload")
|
||||
if _, ok := contract.RuntimeContractFinal(cmd); ok {
|
||||
t.Fatal("foreign stored value must not decode as contract.ContractFinalPayload")
|
||||
}
|
||||
contract.StoreRuntimeContractFinalRawForTest(cmd, (*contract.ContractFinalPayload)(nil))
|
||||
if _, ok := contract.RuntimeContractFinal(cmd); ok {
|
||||
t.Fatal("typed nil payload must not decode as contract.ContractFinalPayload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalIdentityOverridesApplied(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "create"}
|
||||
entry := runtimeSchemaEntry{
|
||||
@@ -321,7 +203,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFa
|
||||
|
||||
// Safety nil + Contract Risk annotation: Risk overlay wins.
|
||||
riskCmd := &cobra.Command{Use: "create"}
|
||||
AnnotateRuntimeRisk(riskCmd, "write")
|
||||
runtimeannotate.AnnotateRuntimeRisk(riskCmd, "write")
|
||||
entry.Command = riskCmd
|
||||
spec, err := runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{})
|
||||
if err != nil {
|
||||
@@ -336,7 +218,7 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalSafetyAnnotationFa
|
||||
|
||||
// Safety nil + runtime gate annotation: gate overlay wins.
|
||||
gateCmd := &cobra.Command{Use: "create"}
|
||||
AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard")
|
||||
runtimeannotate.AnnotateRuntimeGate(gateCmd, "devAppRequireWriteGuard")
|
||||
entry.Command = gateCmd
|
||||
spec, err = runtimeToolSpecFromContractFinal(entry, contract.ContractFinalPayload{}, runtimeSchemaMetadataSources{})
|
||||
if err != nil {
|
||||
@@ -366,3 +248,12 @@ func TestCrossPlatformCoverageRuntimeToolSpecFromContractFinalParameterResolutio
|
||||
t.Fatalf("parameter resolution error = %v, want resolve Contract Schema parameters", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustFinal(t *testing.T, cmd *cobra.Command) contract.ContractFinalPayload {
|
||||
t.Helper()
|
||||
final, ok := contractfinal.RuntimeContractFinal(cmd)
|
||||
if !ok {
|
||||
t.Fatal("missing final")
|
||||
}
|
||||
return final
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageApplyContractRiskToSafety(t *testing.T) {
|
||||
base := contract.SafetySpec{Idempotency: "unknown", Risk: "low", Confirmation: "not_required", Effect: "read"}
|
||||
got := applyContractRiskToSafety(base, "high-risk-write")
|
||||
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
|
||||
t.Fatalf("high-risk overlay = %#v", got)
|
||||
}
|
||||
if got.Idempotency != "unknown" {
|
||||
t.Fatalf("idempotency should be preserved, got %q", got.Idempotency)
|
||||
}
|
||||
got = applyContractRiskToSafety(base, "write")
|
||||
if got.Effect != "write" || got.Risk != "medium" || got.Confirmation != "user_required" {
|
||||
t.Fatalf("write overlay = %#v", got)
|
||||
}
|
||||
got = applyContractRiskToSafety(base, "read")
|
||||
if got.Effect != "read" || got.Risk != "low" || got.Confirmation != "not_required" {
|
||||
t.Fatalf("read overlay = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageApplyContractGateToSafety(t *testing.T) {
|
||||
base := contract.SafetySpec{Confirmation: "not_required", Effect: "read", Risk: "low"}
|
||||
got := applyContractGateToSafety(base, "devAppRequireWriteGuard")
|
||||
if got.Confirmation != "user_required" {
|
||||
t.Fatalf("gate must force user_required, got %#v", got)
|
||||
}
|
||||
if got.Effect != "write" || got.EffectSource != "corecmd.contract_gate" {
|
||||
t.Fatalf("gate effect overlay = %#v", got)
|
||||
}
|
||||
if got.Risk != "medium" {
|
||||
t.Fatalf("gate risk overlay = %#v", got)
|
||||
}
|
||||
reviewed := contract.SafetySpec{Confirmation: "not_required", Effect: "destructive", Risk: "high", EffectSource: "reviewed"}
|
||||
got = applyContractGateToSafety(reviewed, "devAppRequireWriteGuard")
|
||||
if got.Effect != "destructive" || got.Risk != "high" || got.Confirmation != "user_required" {
|
||||
t.Fatalf("gate must keep reviewed effect/risk: %#v", got)
|
||||
}
|
||||
if got = applyContractGateToSafety(base, " "); got != base {
|
||||
t.Fatalf("blank gate must be a no-op, got %#v", got)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package contractfinal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
|
||||
)
|
||||
|
||||
// HasDeclaredOrAnnotatedConfirmation reports whether confirmation semantics are
|
||||
// covered by typed Contract SafetySpec, legacy Shortcut Risk, or runtime_gate.
|
||||
func HasDeclaredOrAnnotatedConfirmation(cmd *cobra.Command) bool {
|
||||
if final, ok := RuntimeContractFinal(cmd); ok && final.Safety != nil &&
|
||||
strings.TrimSpace(final.Safety.Confirmation) != "" {
|
||||
return true
|
||||
}
|
||||
if _, ok := runtimeannotate.RuntimeContractRisk(cmd); ok {
|
||||
return true
|
||||
}
|
||||
_, ok := runtimeannotate.RuntimeContractGate(cmd)
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package contractfinal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
func TestHasDeclaredOrAnnotatedConfirmationBranches(t *testing.T) {
|
||||
bare := &cobra.Command{Use: "bare"}
|
||||
if HasDeclaredOrAnnotatedConfirmation(bare) {
|
||||
t.Fatal("bare command must not claim confirmation coverage")
|
||||
}
|
||||
|
||||
gated := &cobra.Command{Use: "gated"}
|
||||
runtimeannotate.AnnotateRuntimeGate(gated, "devAppRequireWriteGuard")
|
||||
if !HasDeclaredOrAnnotatedConfirmation(gated) {
|
||||
t.Fatal("annotated gate must satisfy declare-OR-annotate")
|
||||
}
|
||||
|
||||
declared := &cobra.Command{Use: "declared"}
|
||||
t.Cleanup(func() { ClearRuntimeContractFinalForTest(declared) })
|
||||
RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{
|
||||
Safety: &contract.SafetySpec{Confirmation: "not_required"},
|
||||
})
|
||||
if !HasDeclaredOrAnnotatedConfirmation(declared) {
|
||||
t.Fatal("typed Contract SafetySpec confirmation must satisfy declare-OR-annotate")
|
||||
}
|
||||
|
||||
risky := &cobra.Command{Use: "risky"}
|
||||
runtimeannotate.AnnotateRuntimeRisk(risky, "read")
|
||||
if !HasDeclaredOrAnnotatedConfirmation(risky) {
|
||||
t.Fatal("Contract Risk annotation must satisfy declare-OR-annotate")
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
package contractfinal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -19,26 +19,10 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
// Delivery-seam helpers that must live in cli because they emit Cobra
|
||||
// annotations (AnnotateRuntime*). Types and registries live in
|
||||
// corecmd/contract; callers author contract.SafetySpec / contract.ParamDecl /
|
||||
// contract.ContractFinalPayload directly — there is no cli type alias layer.
|
||||
|
||||
// RegisterRuntimeContractFinal is the sole production registration entry for
|
||||
// ContractFinal: annotate (dws.schema.contract) then store the typed payload.
|
||||
// Framework code (corecmd.AttachContract / New) must use this seam rather than
|
||||
// calling contract.RegisterRuntimeContractFinal directly.
|
||||
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload contract.ContractFinalPayload) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
AnnotateRuntimeContract(cmd)
|
||||
contract.RegisterRuntimeContractFinal(cmd, payload)
|
||||
}
|
||||
|
||||
// ApplyParamDecls emits parameter declarations as dws.schema.* annotations on the
|
||||
// command's flags. Called at assembly time when all flags exist on the tree.
|
||||
// Each non-blank ParamDecl.Name must resolve to an existing Cobra flag;
|
||||
@@ -52,7 +36,7 @@ func ApplyParamDecls(cmd *cobra.Command, decls []contract.ParamDecl) error {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if runtimeCommandFlag(cmd, name) == nil {
|
||||
if runtimeannotate.CommandFlag(cmd, name) == nil {
|
||||
return fmt.Errorf("ParamDecl %q references unknown flag on %q", name, cmd.CommandPath())
|
||||
}
|
||||
}
|
||||
@@ -62,27 +46,23 @@ func ApplyParamDecls(cmd *cobra.Command, decls []contract.ParamDecl) error {
|
||||
continue
|
||||
}
|
||||
if prop := strings.TrimSpace(p.Property); prop != "" {
|
||||
AnnotateRuntimeFlagProperty(cmd, name, prop)
|
||||
runtimeannotate.AnnotateRuntimeFlagProperty(cmd, name, prop)
|
||||
}
|
||||
if p.Required != nil {
|
||||
AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required)
|
||||
runtimeannotate.AnnotateRuntimeFlagRequiredValue(cmd, name, *p.Required)
|
||||
}
|
||||
if it := strings.TrimSpace(p.InterfaceType); it != "" {
|
||||
AnnotateRuntimeFlagInterfaceType(cmd, name, it)
|
||||
runtimeannotate.AnnotateRuntimeFlagInterfaceType(cmd, name, it)
|
||||
}
|
||||
if desc := strings.TrimSpace(p.Description); desc != "" {
|
||||
AnnotateRuntimeFlagDescription(cmd, name, desc)
|
||||
runtimeannotate.AnnotateRuntimeFlagDescription(cmd, name, desc)
|
||||
}
|
||||
if rw := strings.TrimSpace(p.RequiredWhen); rw != "" {
|
||||
AnnotateRuntimeFlagRequiredWhen(cmd, name, rw)
|
||||
runtimeannotate.AnnotateRuntimeFlagRequiredWhen(cmd, name, rw)
|
||||
}
|
||||
if len(p.Enum) > 0 {
|
||||
AnnotateRuntimeFlagEnum(cmd, name, p.Enum...)
|
||||
runtimeannotate.AnnotateRuntimeFlagEnum(cmd, name, p.Enum...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolvedFieldProvenance(value any, source, sourceRef, precedence, resolution, reviewReason string) contract.FieldProvenance {
|
||||
return contract.ResolvedFieldProvenance(value, source, sourceRef, precedence, resolution, reviewReason)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+5
-3
@@ -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) {
|
||||
+4
-4
@@ -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")
|
||||
}
|
||||
+56
-3
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
package homology
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -35,12 +35,26 @@ var plannedHomologyGateIDs = []string{
|
||||
"HOM-D1", // --help Flags ≡ schema leaf parameters
|
||||
}
|
||||
|
||||
// ciHomologyEntrypoints are executable gates already wired into
|
||||
// scripts/policy/check-schema-catalog.sh (and confirmation-truth helper).
|
||||
// Vocabulary-only pinning is not enough: these names must stay on the policy
|
||||
// -run whitelist.
|
||||
var ciHomologyEntrypoints = []string{
|
||||
"TestUserRequiredSafetyHomologyWithRuntimeGate",
|
||||
"TestHomologyDecisionDocPinsPathAAndGateIDs",
|
||||
"TestMCPPassthroughAdmissionExcludesLeafAndShortcut",
|
||||
"TestHomologyCIEntrypointsPinned",
|
||||
"TestFinalSchemaParametersMatchExecutableHelpFlags",
|
||||
"TestEmbeddedSchemaParametersMatchExecutableHelpFlags",
|
||||
"TestSheetFinalSchemaConfirmationMatchesRuntimeGuards",
|
||||
}
|
||||
|
||||
func TestHomologyDecisionDocPinsPathAAndGateIDs(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "docs", "flag-help-schema-homology.md"))
|
||||
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "docs", "flag-help-schema-homology.md"))
|
||||
raw, err := os.ReadFile(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("read homology doc: %v", err)
|
||||
@@ -62,6 +76,8 @@ func TestHomologyDecisionDocPinsPathAAndGateIDs(t *testing.T) {
|
||||
"dws.schema.runtime_gate",
|
||||
"mcp_passthrough",
|
||||
"不得覆盖 LeafSpec / Shortcut 主路径",
|
||||
"已进 CI",
|
||||
"check-schema-catalog.sh",
|
||||
} {
|
||||
if !strings.Contains(body, needle) {
|
||||
t.Fatalf("homology doc missing %q", needle)
|
||||
@@ -79,7 +95,7 @@ func TestMCPPassthroughAdmissionExcludesLeafAndShortcut(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "docs", "flag-help-schema-homology.md"))
|
||||
doc := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "docs", "flag-help-schema-homology.md"))
|
||||
raw, err := os.ReadFile(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("read homology doc: %v", err)
|
||||
@@ -96,3 +112,40 @@ func TestMCPPassthroughAdmissionExcludesLeafAndShortcut(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomologyCIEntrypointsPinned(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", ".."))
|
||||
script := filepath.Join(root, "scripts", "policy", "check-schema-catalog.sh")
|
||||
raw, err := os.ReadFile(script)
|
||||
if err != nil {
|
||||
t.Fatalf("read check-schema-catalog.sh: %v", err)
|
||||
}
|
||||
body := string(raw)
|
||||
for _, name := range ciHomologyEntrypoints {
|
||||
if !strings.Contains(body, name) {
|
||||
t.Fatalf("check-schema-catalog.sh missing homology CI entrypoint %q", name)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(body, "./internal/cli/homology") {
|
||||
t.Fatal("check-schema-catalog.sh must run ./internal/cli/homology for HOM gate tests")
|
||||
}
|
||||
doc := filepath.Join(root, "docs", "flag-help-schema-homology.md")
|
||||
docRaw, err := os.ReadFile(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("read homology doc: %v", err)
|
||||
}
|
||||
docBody := string(docRaw)
|
||||
for _, name := range []string{
|
||||
"TestUserRequiredSafetyHomologyWithRuntimeGate",
|
||||
"TestFinalSchemaParametersMatchExecutableHelpFlags",
|
||||
"TestHomologyCIEntrypointsPinned",
|
||||
} {
|
||||
if !strings.Contains(docBody, name) {
|
||||
t.Fatalf("homology doc §3 CI table missing %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cli_test
|
||||
package homology
|
||||
|
||||
import (
|
||||
"testing"
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
func TestProductDeclRegistryRoundTrip(t *testing.T) {
|
||||
t.Cleanup(func() { contract.ClearProductDeclForTest("sample") })
|
||||
contract.ClearProductDeclForTest("sample")
|
||||
|
||||
if contract.HasProductDecl("sample") {
|
||||
t.Fatal("contract.HasProductDecl before register must be false")
|
||||
}
|
||||
contract.RegisterProductDecl(contract.ProductDecl{})
|
||||
if contract.HasProductDecl("") {
|
||||
t.Fatal("empty ID must not register")
|
||||
}
|
||||
|
||||
contract.RegisterProductDecl(contract.ProductDecl{
|
||||
ID: " sample ",
|
||||
Selection: contract.ProductSelectionDecl{
|
||||
AgentSummary: "Manage samples",
|
||||
UseWhen: []string{"target is a sample"},
|
||||
AvoidWhen: []string{"target is another product"},
|
||||
},
|
||||
})
|
||||
if !contract.HasProductDecl("sample") {
|
||||
t.Fatal("contract.HasProductDecl after register must be true")
|
||||
}
|
||||
got, ok := contract.LookupProductDecl("sample")
|
||||
if !ok || got.ID != "sample" || got.Selection.AgentSummary != "Manage samples" {
|
||||
t.Fatalf("contract.LookupProductDecl = %#v, ok=%v", got, ok)
|
||||
}
|
||||
ids := contract.RegisteredProductDeclIDs()
|
||||
found := false
|
||||
for _, id := range ids {
|
||||
if id == "sample" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("contract.RegisteredProductDeclIDs missing sample: %#v", ids)
|
||||
}
|
||||
|
||||
selection, provenance := contract.ProductSelectionFromDecl(got)
|
||||
if selection.AgentSummary != "Manage samples" || selection.AgentSummarySource != contract.ProductDeclSourceRef {
|
||||
t.Fatalf("contract.ProductSelectionFromDecl selection = %#v", selection)
|
||||
}
|
||||
for _, field := range []string{"agent_summary", "use_when", "avoid_when"} {
|
||||
prov, ok := provenance[field]
|
||||
if !ok || prov.Precedence != "contract_final" || prov.Source != contract.ProductDeclProvenanceSource {
|
||||
t.Fatalf("field %s provenance = %#v", field, prov)
|
||||
}
|
||||
}
|
||||
|
||||
contract.ClearProductDeclForTest("sample")
|
||||
if contract.HasProductDecl("sample") {
|
||||
t.Fatal("contract.ClearProductDeclForTest must remove registration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductDeclRegisterPanicsOnIncompleteSelection(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("expected panic for incomplete contract.ProductDecl")
|
||||
}
|
||||
}()
|
||||
contract.RegisterProductDecl(contract.ProductDecl{ID: "broken"})
|
||||
}
|
||||
+37
-447
@@ -25,6 +25,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -34,39 +35,27 @@ import (
|
||||
var embeddedMCPMetadataJSON []byte
|
||||
|
||||
const (
|
||||
runtimeSchemaProductAnnotation = "dws.schema.product"
|
||||
runtimeSchemaToolAnnotation = "dws.schema.tool"
|
||||
runtimeSchemaSourceAnnotation = "dws.schema.source"
|
||||
runtimeSchemaTitleAnnotation = "dws.schema.title"
|
||||
runtimeSchemaDescAnnotation = "dws.schema.description"
|
||||
runtimeSchemaMetaAnnotation = "dws.schema.metadata_source"
|
||||
runtimeSchemaExcludeAnnotation = "dws.schema.exclude"
|
||||
runtimeSchemaRulesAnnotation = "dws.schema.constraints"
|
||||
runtimeSchemaArgsAnnotation = "dws.schema.positionals"
|
||||
|
||||
runtimeSchemaFlagPropertyAnnotation = "dws.schema.property"
|
||||
runtimeSchemaFlagTypeAnnotation = "dws.schema.type"
|
||||
runtimeSchemaFlagDescriptionAnnotation = "dws.schema.description"
|
||||
runtimeSchemaFlagRequiredAnnotation = "dws.schema.required"
|
||||
runtimeSchemaFlagRequiredWhenAnnotation = "dws.schema.required_when"
|
||||
runtimeSchemaFlagExampleAnnotation = "dws.schema.example"
|
||||
|
||||
// Contract surface (command / LeafSpec) embedded onto the live Cobra leaf so
|
||||
// Schema generation can project parameters/constraints/Risk without a second
|
||||
// source of truth. See docs/flag-help-schema-homology.md.
|
||||
runtimeSchemaContractAnnotation = "dws.schema.contract"
|
||||
runtimeSchemaRiskAnnotation = "dws.schema.risk"
|
||||
runtimeSchemaRuntimeGateAnnotation = "dws.schema.runtime_gate"
|
||||
// Annotation keys are owned by runtimeannotate; aliases keep assembly readers stable.
|
||||
runtimeSchemaProductAnnotation = runtimeannotate.AnnotationProduct
|
||||
runtimeSchemaToolAnnotation = runtimeannotate.AnnotationTool
|
||||
runtimeSchemaSourceAnnotation = runtimeannotate.AnnotationSource
|
||||
runtimeSchemaTitleAnnotation = runtimeannotate.AnnotationTitle
|
||||
runtimeSchemaDescAnnotation = runtimeannotate.AnnotationDescription
|
||||
runtimeSchemaMetaAnnotation = runtimeannotate.AnnotationMetaSource
|
||||
runtimeSchemaExcludeAnnotation = runtimeannotate.AnnotationExclude
|
||||
runtimeSchemaRulesAnnotation = runtimeannotate.AnnotationConstraints
|
||||
runtimeSchemaArgsAnnotation = runtimeannotate.AnnotationPositionals
|
||||
runtimeSchemaFlagPropertyAnnotation = runtimeannotate.AnnotationFlagProperty
|
||||
runtimeSchemaFlagTypeAnnotation = runtimeannotate.AnnotationFlagType
|
||||
runtimeSchemaFlagDescriptionAnnotation = runtimeannotate.AnnotationDescription
|
||||
runtimeSchemaFlagRequiredAnnotation = runtimeannotate.AnnotationFlagRequired
|
||||
runtimeSchemaFlagRequiredWhenAnnotation = runtimeannotate.AnnotationFlagReqWhen
|
||||
runtimeSchemaFlagExampleAnnotation = runtimeannotate.AnnotationFlagExample
|
||||
runtimeSchemaContractAnnotation = runtimeannotate.AnnotationContract
|
||||
runtimeSchemaRiskAnnotation = runtimeannotate.AnnotationRisk
|
||||
runtimeSchemaRuntimeGateAnnotation = runtimeannotate.AnnotationRuntimeGate
|
||||
)
|
||||
|
||||
// RuntimeSchemaConstraints describes cross-parameter rules that cannot be
|
||||
// represented by an individual parameter's required bit.
|
||||
type RuntimeSchemaConstraints struct {
|
||||
MutuallyExclusive [][]string `json:"mutually_exclusive,omitempty"`
|
||||
RequireOneOf [][]string `json:"require_one_of,omitempty"`
|
||||
RequireTogether [][]string `json:"require_together,omitempty"`
|
||||
}
|
||||
|
||||
type embeddedMCPMetadata struct {
|
||||
Version int `json:"version"`
|
||||
Source string `json:"source"`
|
||||
@@ -169,258 +158,6 @@ func interfaceMetadataSummaryFrom(metadata embeddedMCPMetadata) map[string]any {
|
||||
return summary
|
||||
}
|
||||
|
||||
// AttachRuntimeSchema records optional implementation-side identity evidence
|
||||
// on a runnable command. Command discovery belongs exclusively to the reviewed
|
||||
// CommandRegistry; the Cobra binder accepts an absent annotation and rejects
|
||||
// an annotation that disagrees with the registry.
|
||||
func AttachRuntimeSchema(cmd *cobra.Command, productID, toolName, source string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
productID = strings.TrimSpace(productID)
|
||||
toolName = strings.TrimSpace(toolName)
|
||||
if productID == "" || toolName == "" {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[runtimeSchemaProductAnnotation] = productID
|
||||
cmd.Annotations[runtimeSchemaToolAnnotation] = toolName
|
||||
if source = strings.TrimSpace(source); source != "" {
|
||||
cmd.Annotations[runtimeSchemaSourceAnnotation] = source
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeToolMetadata preserves MCP-provided tool metadata on a Cobra
|
||||
// leaf so `dws schema` can render richer descriptions without refetching.
|
||||
func AnnotateRuntimeToolMetadata(cmd *cobra.Command, title, description, source string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
if title = strings.TrimSpace(title); title != "" {
|
||||
cmd.Annotations[runtimeSchemaTitleAnnotation] = title
|
||||
}
|
||||
if description = strings.TrimSpace(description); description != "" {
|
||||
cmd.Annotations[runtimeSchemaDescAnnotation] = description
|
||||
}
|
||||
if source = strings.TrimSpace(source); source != "" {
|
||||
cmd.Annotations[runtimeSchemaMetaAnnotation] = source
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlag adds parameter metadata to an already-registered flag.
|
||||
// The metadata mirrors the runtime binding that produced the flag, allowing
|
||||
// schema rendering to preserve MCP parameter names while displaying CLI flags.
|
||||
func AnnotateRuntimeFlag(cmd *cobra.Command, flagName, propertyName, paramType string, required bool, _ string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
flagName = strings.TrimSpace(flagName)
|
||||
if flagName == "" {
|
||||
return
|
||||
}
|
||||
flag := runtimeCommandFlag(cmd, flagName)
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagPropertyAnnotation, strings.TrimSpace(propertyName))
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagTypeAnnotation, strings.TrimSpace(paramType))
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, strconv.FormatBool(required))
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagProperty records only the stable CLI flag to interface
|
||||
// property binding. It intentionally does not copy required or constraints
|
||||
// from an older Catalog into the current executable contract.
|
||||
func AnnotateRuntimeFlagProperty(cmd *cobra.Command, flagName, propertyName string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagPropertyAnnotation, strings.TrimSpace(propertyName))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeRequiredFlags records schema-only required semantics. Unlike
|
||||
// cobra.MarkFlagRequired, it does not require the primary flag itself to be
|
||||
// changed, so helper commands can keep accepting hidden --url/--id aliases.
|
||||
func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
for _, name := range flagNames {
|
||||
flag := runtimeCommandFlag(cmd, name)
|
||||
if flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagRequiredValue sets an explicit required value ("true" or
|
||||
// "false") on a flag's dws.schema.required annotation. Use this when a
|
||||
// declaration needs to override a stale cobra-level or MCP-level required=true
|
||||
// with an explicit false.
|
||||
func AnnotateRuntimeFlagRequiredValue(cmd *cobra.Command, flagName string, required bool) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
v := "false"
|
||||
if required {
|
||||
v = "true"
|
||||
}
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagRequiredAnnotation, v)
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagDescription records the Schema parameter description.
|
||||
func AnnotateRuntimeFlagDescription(cmd *cobra.Command, flagName, description string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagDescriptionAnnotation, strings.TrimSpace(description))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagRequiredWhen records a conditional CLI requirement. The
|
||||
// expression is descriptive metadata and does not alter Cobra validation.
|
||||
func AnnotateRuntimeFlagRequiredWhen(cmd *cobra.Command, flagName, expression string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagRequiredWhenAnnotation, strings.TrimSpace(expression))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagFormat records a machine-readable value format without
|
||||
// changing the Cobra flag type.
|
||||
func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
setFlagAnnotation(flag, "x-cli-format", strings.TrimSpace(format))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagInterfaceType records the wire type for a flag's interface
|
||||
// property (e.g. "string", "integer", "boolean", "array"). The resolver reads
|
||||
// this as a native_annotation candidate for interface_type.
|
||||
func AnnotateRuntimeFlagInterfaceType(cmd *cobra.Command, flagName, interfaceType string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagTypeAnnotation, strings.TrimSpace(interfaceType))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagEnum records the accepted values for a flag.
|
||||
func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
flag := runtimeCommandFlag(cmd, flagName)
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
setFlagAnnotationValues(flag, "x-cli-enum", values...)
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagExample records a valid representative CLI value.
|
||||
func AnnotateRuntimeFlagExample(cmd *cobra.Command, flagName, example string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := runtimeCommandFlag(cmd, flagName); flag != nil {
|
||||
setFlagAnnotation(flag, runtimeSchemaFlagExampleAnnotation, strings.TrimSpace(example))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeContract marks a command as carrying a command/LeafSpec
|
||||
// Contract surface. Schema assembly treats embedded flag annotations and, when
|
||||
// set, AnnotateRuntimeRisk as Contract-authored facts (path A homology).
|
||||
func AnnotateRuntimeContract(cmd *cobra.Command) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaContractAnnotation, "command")
|
||||
}
|
||||
|
||||
// AnnotateRuntimeRisk records the Contract Risk string (read|write|high-risk-write)
|
||||
// for Schema Safety projection. Empty risk is a no-op so write-guard leaves that
|
||||
// leave Risk unset keep reviewed hint safety.
|
||||
func AnnotateRuntimeRisk(cmd *cobra.Command, risk string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
risk = strings.TrimSpace(risk)
|
||||
if risk == "" {
|
||||
return
|
||||
}
|
||||
AnnotateRuntimeContract(cmd)
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaRiskAnnotation, risk)
|
||||
}
|
||||
|
||||
// AnnotateRuntimeGate records a non-Risk confirmation path (e.g.
|
||||
// devAppRequireWriteGuard). Homology rule: every confirmation fact is either
|
||||
// Contract-declared (Risk) or explicitly annotated (this gate / reviewed hints).
|
||||
func AnnotateRuntimeGate(cmd *cobra.Command, gate string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
gate = strings.TrimSpace(gate)
|
||||
if gate == "" {
|
||||
return
|
||||
}
|
||||
AnnotateRuntimeContract(cmd)
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaRuntimeGateAnnotation, gate)
|
||||
}
|
||||
|
||||
// RuntimeContractRisk returns the Contract Risk annotation when present.
|
||||
func RuntimeContractRisk(cmd *cobra.Command) (string, bool) {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return "", false
|
||||
}
|
||||
risk := strings.TrimSpace(cmd.Annotations[runtimeSchemaRiskAnnotation])
|
||||
if risk == "" {
|
||||
return "", false
|
||||
}
|
||||
return risk, true
|
||||
}
|
||||
|
||||
// RuntimeContractGate returns the annotated runtime confirmation gate when present.
|
||||
func RuntimeContractGate(cmd *cobra.Command) (string, bool) {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return "", false
|
||||
}
|
||||
gate := strings.TrimSpace(cmd.Annotations[runtimeSchemaRuntimeGateAnnotation])
|
||||
if gate == "" {
|
||||
return "", false
|
||||
}
|
||||
return gate, true
|
||||
}
|
||||
|
||||
// HasDeclaredOrAnnotatedConfirmation reports whether confirmation semantics are
|
||||
// covered by a typed Contract contract.SafetySpec, the legacy Shortcut Risk bridge, or
|
||||
// an explicit runtime_gate annotation.
|
||||
func HasDeclaredOrAnnotatedConfirmation(cmd *cobra.Command) bool {
|
||||
if final, ok := contract.RuntimeContractFinal(cmd); ok && final.Safety != nil &&
|
||||
strings.TrimSpace(final.Safety.Confirmation) != "" {
|
||||
return true
|
||||
}
|
||||
if _, ok := RuntimeContractRisk(cmd); ok {
|
||||
return true
|
||||
}
|
||||
_, ok := RuntimeContractGate(cmd)
|
||||
return ok
|
||||
}
|
||||
|
||||
// applyContractRiskToSafety overlays Schema Safety fields from an embedded
|
||||
// Contract Risk value. Path A: Contract wins effect/risk/confirmation for the
|
||||
// managed surface; other Safety fields (e.g. idempotency) are preserved.
|
||||
@@ -464,96 +201,6 @@ func applyContractGateToSafety(base contract.SafetySpec, gate string) contract.S
|
||||
return out
|
||||
}
|
||||
|
||||
// AnnotateRuntimeConstraints records command-level parameter relationships.
|
||||
func AnnotateRuntimeConstraints(cmd *cobra.Command, constraints RuntimeSchemaConstraints) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
constraints = normalizeRuntimeSchemaConstraints(constraints)
|
||||
if runtimeSchemaConstraintsEmpty(constraints) {
|
||||
return
|
||||
}
|
||||
if existing := runtimeCommandConstraints(cmd); !runtimeSchemaConstraintsEmpty(existing) {
|
||||
constraints.MutuallyExclusive = append(existing.MutuallyExclusive, constraints.MutuallyExclusive...)
|
||||
constraints.RequireOneOf = append(existing.RequireOneOf, constraints.RequireOneOf...)
|
||||
constraints.RequireTogether = append(existing.RequireTogether, constraints.RequireTogether...)
|
||||
constraints = normalizeRuntimeSchemaConstraints(constraints)
|
||||
}
|
||||
encoded, _ := json.Marshal(constraints)
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaRulesAnnotation, string(encoded))
|
||||
}
|
||||
|
||||
// AnnotateRuntimePositionals records ordered positional arguments for agents.
|
||||
func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...contract.RuntimeSchemaPositional) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
clean := make([]contract.RuntimeSchemaPositional, 0, len(positionals))
|
||||
for _, positional := range positionals {
|
||||
positional.Name = strings.TrimSpace(positional.Name)
|
||||
positional.Type = strings.TrimSpace(positional.Type)
|
||||
positional.Description = strings.TrimSpace(positional.Description)
|
||||
if positional.Name == "" || positional.Index < 0 {
|
||||
continue
|
||||
}
|
||||
if positional.Type == "" {
|
||||
positional.Type = "string"
|
||||
}
|
||||
clean = append(clean, positional)
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
sort.SliceStable(clean, func(i, j int) bool { return clean[i].Index < clean[j].Index })
|
||||
encoded, _ := json.Marshal(clean)
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaArgsAnnotation, string(encoded))
|
||||
}
|
||||
|
||||
// ExcludeFromRuntimeSchema keeps a human-facing hint or redirect in --help
|
||||
// while preventing it from being advertised as an executable agent tool.
|
||||
func ExcludeFromRuntimeSchema(cmd *cobra.Command) {
|
||||
setRuntimeCommandAnnotation(cmd, runtimeSchemaExcludeAnnotation, "true")
|
||||
}
|
||||
|
||||
func setRuntimeCommandAnnotation(cmd *cobra.Command, key, value string) {
|
||||
if cmd == nil || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[key] = value
|
||||
}
|
||||
|
||||
func setFlagAnnotation(flag *pflag.Flag, key, value string) {
|
||||
if flag == nil || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
if flag.Annotations == nil {
|
||||
flag.Annotations = map[string][]string{}
|
||||
}
|
||||
flag.Annotations[key] = []string{value}
|
||||
}
|
||||
|
||||
func setFlagAnnotationValues(flag *pflag.Flag, key string, values ...string) {
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
clean := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
clean = append(clean, value)
|
||||
}
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
if flag.Annotations == nil {
|
||||
flag.Annotations = map[string][]string{}
|
||||
}
|
||||
flag.Annotations[key] = clean
|
||||
}
|
||||
|
||||
type runtimeSchemaEntry struct {
|
||||
ProductID string
|
||||
SourceProductID string
|
||||
@@ -1367,26 +1014,8 @@ func runtimeSchemaRequireOneOfContains(constraints RuntimeSchemaConstraints, nam
|
||||
return false
|
||||
}
|
||||
|
||||
// runtimeCommandFlag resolves local flags plus product/group persistent flags.
|
||||
// Root persistent flags are intentionally available only when explicitly
|
||||
// requested; they are global execution controls, not tool parameters.
|
||||
func runtimeCommandFlag(cmd *cobra.Command, name string) *pflag.Flag {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if flag := cmd.Flags().Lookup(name); flag != nil {
|
||||
return flag
|
||||
}
|
||||
for current := cmd; current != nil; current = current.Parent() {
|
||||
if flag := current.PersistentFlags().Lookup(name); flag != nil {
|
||||
return flag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return runtimeannotate.CommandFlag(cmd, name)
|
||||
}
|
||||
|
||||
func visitRuntimeCommandFlags(cmd *cobra.Command, inheritedNames []string, visit func(*pflag.Flag)) {
|
||||
@@ -1436,74 +1065,35 @@ func visitRuntimeCommandFlags(cmd *cobra.Command, inheritedNames []string, visit
|
||||
}
|
||||
|
||||
func runtimeCommandConstraints(cmd *cobra.Command) RuntimeSchemaConstraints {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return RuntimeSchemaConstraints{}
|
||||
}
|
||||
raw := strings.TrimSpace(cmd.Annotations[runtimeSchemaRulesAnnotation])
|
||||
if raw == "" {
|
||||
return RuntimeSchemaConstraints{}
|
||||
}
|
||||
var constraints RuntimeSchemaConstraints
|
||||
if json.Unmarshal([]byte(raw), &constraints) != nil {
|
||||
return RuntimeSchemaConstraints{}
|
||||
}
|
||||
return normalizeRuntimeSchemaConstraints(constraints)
|
||||
return runtimeannotate.CommandConstraints(cmd)
|
||||
}
|
||||
|
||||
func runtimeCommandPositionals(cmd *cobra.Command) []contract.RuntimeSchemaPositional {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(cmd.Annotations[runtimeSchemaArgsAnnotation])
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var positionals []contract.RuntimeSchemaPositional
|
||||
if json.Unmarshal([]byte(raw), &positionals) != nil {
|
||||
return nil
|
||||
}
|
||||
sort.SliceStable(positionals, func(i, j int) bool { return positionals[i].Index < positionals[j].Index })
|
||||
return positionals
|
||||
return runtimeannotate.CommandPositionals(cmd)
|
||||
}
|
||||
|
||||
func normalizeRuntimeSchemaConstraints(constraints RuntimeSchemaConstraints) RuntimeSchemaConstraints {
|
||||
constraints.MutuallyExclusive = normalizeRuntimeSchemaGroups(constraints.MutuallyExclusive, 2)
|
||||
constraints.RequireOneOf = normalizeRuntimeSchemaGroups(constraints.RequireOneOf, 1)
|
||||
constraints.RequireTogether = normalizeRuntimeSchemaGroups(constraints.RequireTogether, 2)
|
||||
return constraints
|
||||
return runtimeannotate.NormalizeConstraints(constraints)
|
||||
}
|
||||
|
||||
func normalizeRuntimeSchemaGroups(groups [][]string, minimum int) [][]string {
|
||||
out := make([][]string, 0, len(groups))
|
||||
seenGroups := map[string]bool{}
|
||||
for _, group := range groups {
|
||||
clean := make([]string, 0, len(group))
|
||||
seenNames := map[string]bool{}
|
||||
for _, name := range group {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || seenNames[name] {
|
||||
continue
|
||||
}
|
||||
seenNames[name] = true
|
||||
clean = append(clean, name)
|
||||
}
|
||||
if len(clean) < minimum {
|
||||
continue
|
||||
}
|
||||
key := strings.Join(clean, "\x00")
|
||||
if seenGroups[key] {
|
||||
continue
|
||||
}
|
||||
seenGroups[key] = true
|
||||
out = append(out, clean)
|
||||
// Test-facing thin wrapper over NormalizeConstraints group rules.
|
||||
c := RuntimeSchemaConstraints{}
|
||||
switch minimum {
|
||||
case 1:
|
||||
c.RequireOneOf = groups
|
||||
default:
|
||||
c.MutuallyExclusive = groups
|
||||
}
|
||||
return out
|
||||
out := runtimeannotate.NormalizeConstraints(c)
|
||||
if minimum == 1 {
|
||||
return out.RequireOneOf
|
||||
}
|
||||
return out.MutuallyExclusive
|
||||
}
|
||||
|
||||
func runtimeSchemaConstraintsEmpty(constraints RuntimeSchemaConstraints) bool {
|
||||
return len(constraints.MutuallyExclusive) == 0 &&
|
||||
len(constraints.RequireOneOf) == 0 &&
|
||||
len(constraints.RequireTogether) == 0
|
||||
return runtimeannotate.ConstraintsEmpty(constraints)
|
||||
}
|
||||
|
||||
func lookupEmbeddedMCPParam(params map[string]embeddedMCPParamMeta, property, flagName string) (embeddedMCPParamMeta, bool) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/runtimeannotate"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -263,7 +264,9 @@ func TestCrossPlatformCoverageRuntimeSchemaPureHelperEdges(t *testing.T) {
|
||||
if got := runtimeCommandPositionals(cmd); len(got) != 2 || got[0].Name != "first" {
|
||||
t.Fatalf("sorted positionals = %#v", got)
|
||||
}
|
||||
groups := normalizeRuntimeSchemaGroups([][]string{{" "}, {" ", "one", "one"}, {"one"}, {"one"}}, 1)
|
||||
groups := runtimeannotate.NormalizeConstraints(runtimeannotate.RuntimeSchemaConstraints{
|
||||
RequireOneOf: [][]string{{" "}, {" ", "one", "one"}, {"one"}, {"one"}},
|
||||
}).RequireOneOf
|
||||
if !reflect.DeepEqual(groups, [][]string{{"one"}}) {
|
||||
t.Fatalf("normalized groups = %#v", groups)
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package runtimeannotate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
// AttachRuntimeSchema records optional implementation-side identity evidence
|
||||
// on a runnable command. Command discovery belongs exclusively to the reviewed
|
||||
// CommandRegistry; the Cobra binder accepts an absent annotation and rejects
|
||||
// an annotation that disagrees with the registry.
|
||||
func AttachRuntimeSchema(cmd *cobra.Command, productID, toolName, source string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
productID = strings.TrimSpace(productID)
|
||||
toolName = strings.TrimSpace(toolName)
|
||||
if productID == "" || toolName == "" {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[AnnotationProduct] = productID
|
||||
cmd.Annotations[AnnotationTool] = toolName
|
||||
if source = strings.TrimSpace(source); source != "" {
|
||||
cmd.Annotations[AnnotationSource] = source
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeToolMetadata preserves MCP-provided tool metadata on a Cobra
|
||||
// leaf so `dws schema` can render richer descriptions without refetching.
|
||||
func AnnotateRuntimeToolMetadata(cmd *cobra.Command, title, description, source string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
if title = strings.TrimSpace(title); title != "" {
|
||||
cmd.Annotations[AnnotationTitle] = title
|
||||
}
|
||||
if description = strings.TrimSpace(description); description != "" {
|
||||
cmd.Annotations[AnnotationDescription] = description
|
||||
}
|
||||
if source = strings.TrimSpace(source); source != "" {
|
||||
cmd.Annotations[AnnotationMetaSource] = source
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlag adds parameter metadata to an already-registered flag.
|
||||
func AnnotateRuntimeFlag(cmd *cobra.Command, flagName, propertyName, paramType string, required bool, _ string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
flagName = strings.TrimSpace(flagName)
|
||||
if flagName == "" {
|
||||
return
|
||||
}
|
||||
flag := CommandFlag(cmd, flagName)
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
SetFlagAnnotation(flag, AnnotationFlagProperty, strings.TrimSpace(propertyName))
|
||||
SetFlagAnnotation(flag, AnnotationFlagType, strings.TrimSpace(paramType))
|
||||
SetFlagAnnotation(flag, AnnotationFlagRequired, strconv.FormatBool(required))
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagProperty records only the stable CLI flag to interface
|
||||
// property binding.
|
||||
func AnnotateRuntimeFlagProperty(cmd *cobra.Command, flagName, propertyName string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationFlagProperty, strings.TrimSpace(propertyName))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeRequiredFlags records schema-only required semantics.
|
||||
func AnnotateRuntimeRequiredFlags(cmd *cobra.Command, flagNames ...string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
for _, name := range flagNames {
|
||||
flag := CommandFlag(cmd, name)
|
||||
if flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationFlagRequired, "true")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagRequiredValue sets an explicit required value on a flag.
|
||||
func AnnotateRuntimeFlagRequiredValue(cmd *cobra.Command, flagName string, required bool) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
v := "false"
|
||||
if required {
|
||||
v = "true"
|
||||
}
|
||||
SetFlagAnnotation(flag, AnnotationFlagRequired, v)
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagDescription records the Schema parameter description.
|
||||
func AnnotateRuntimeFlagDescription(cmd *cobra.Command, flagName, description string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationDescription, strings.TrimSpace(description))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagRequiredWhen records a conditional CLI requirement.
|
||||
func AnnotateRuntimeFlagRequiredWhen(cmd *cobra.Command, flagName, expression string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationFlagReqWhen, strings.TrimSpace(expression))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagFormat records a machine-readable value format.
|
||||
func AnnotateRuntimeFlagFormat(cmd *cobra.Command, flagName, format string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationFlagFormat, strings.TrimSpace(format))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagInterfaceType records the wire type for a flag's interface
|
||||
// property.
|
||||
func AnnotateRuntimeFlagInterfaceType(cmd *cobra.Command, flagName, interfaceType string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationFlagType, strings.TrimSpace(interfaceType))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagEnum records the accepted values for a flag.
|
||||
func AnnotateRuntimeFlagEnum(cmd *cobra.Command, flagName string, values ...string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
flag := CommandFlag(cmd, flagName)
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
SetFlagAnnotationValues(flag, AnnotationFlagEnum, values...)
|
||||
}
|
||||
|
||||
// AnnotateRuntimeFlagExample records a valid representative CLI value.
|
||||
func AnnotateRuntimeFlagExample(cmd *cobra.Command, flagName, example string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if flag := CommandFlag(cmd, flagName); flag != nil {
|
||||
SetFlagAnnotation(flag, AnnotationFlagExample, strings.TrimSpace(example))
|
||||
}
|
||||
}
|
||||
|
||||
// AnnotateRuntimeContract marks a command as carrying a command/LeafSpec
|
||||
// Contract surface.
|
||||
func AnnotateRuntimeContract(cmd *cobra.Command) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
SetCommandAnnotation(cmd, AnnotationContract, "command")
|
||||
}
|
||||
|
||||
// AnnotateRuntimeRisk records the Contract Risk string for Schema Safety
|
||||
// projection. Empty risk is a no-op so write-guard leaves that leave Risk
|
||||
// unset keep Contract.Safety / runtime_gate provenance (schema_hints/ is
|
||||
// retired and must not be treated as a Safety fallback).
|
||||
func AnnotateRuntimeRisk(cmd *cobra.Command, risk string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
risk = strings.TrimSpace(risk)
|
||||
if risk == "" {
|
||||
return
|
||||
}
|
||||
AnnotateRuntimeContract(cmd)
|
||||
SetCommandAnnotation(cmd, AnnotationRisk, risk)
|
||||
}
|
||||
|
||||
// AnnotateRuntimeGate records a non-Risk confirmation path (e.g.
|
||||
// devAppRequireWriteGuard). Homology rule: every confirmation fact is either
|
||||
// Contract-declared (SafetySpec / Risk) or explicitly annotated (this gate).
|
||||
// schema_hints/ is retired and must not supply confirmation.
|
||||
func AnnotateRuntimeGate(cmd *cobra.Command, gate string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
gate = strings.TrimSpace(gate)
|
||||
if gate == "" {
|
||||
return
|
||||
}
|
||||
AnnotateRuntimeContract(cmd)
|
||||
SetCommandAnnotation(cmd, AnnotationRuntimeGate, gate)
|
||||
}
|
||||
|
||||
// RuntimeContractRisk returns the Contract Risk annotation when present.
|
||||
func RuntimeContractRisk(cmd *cobra.Command) (string, bool) {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return "", false
|
||||
}
|
||||
risk := strings.TrimSpace(cmd.Annotations[AnnotationRisk])
|
||||
if risk == "" {
|
||||
return "", false
|
||||
}
|
||||
return risk, true
|
||||
}
|
||||
|
||||
// RuntimeContractGate returns the annotated runtime confirmation gate when present.
|
||||
func RuntimeContractGate(cmd *cobra.Command) (string, bool) {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return "", false
|
||||
}
|
||||
gate := strings.TrimSpace(cmd.Annotations[AnnotationRuntimeGate])
|
||||
if gate == "" {
|
||||
return "", false
|
||||
}
|
||||
return gate, true
|
||||
}
|
||||
|
||||
// AnnotateRuntimePositionals records ordered positional arguments for agents.
|
||||
func AnnotateRuntimePositionals(cmd *cobra.Command, positionals ...contract.RuntimeSchemaPositional) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
clean := make([]contract.RuntimeSchemaPositional, 0, len(positionals))
|
||||
for _, positional := range positionals {
|
||||
positional.Name = strings.TrimSpace(positional.Name)
|
||||
positional.Type = strings.TrimSpace(positional.Type)
|
||||
positional.Description = strings.TrimSpace(positional.Description)
|
||||
if positional.Name == "" || positional.Index < 0 {
|
||||
continue
|
||||
}
|
||||
if positional.Type == "" {
|
||||
positional.Type = "string"
|
||||
}
|
||||
clean = append(clean, positional)
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
sort.SliceStable(clean, func(i, j int) bool { return clean[i].Index < clean[j].Index })
|
||||
encoded, _ := json.Marshal(clean)
|
||||
SetCommandAnnotation(cmd, AnnotationPositionals, string(encoded))
|
||||
}
|
||||
|
||||
// CommandPositionals reads annotated positionals (nil on miss/error).
|
||||
func CommandPositionals(cmd *cobra.Command) []contract.RuntimeSchemaPositional {
|
||||
if cmd == nil || cmd.Annotations == nil {
|
||||
return nil
|
||||
}
|
||||
raw := strings.TrimSpace(cmd.Annotations[AnnotationPositionals])
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var positionals []contract.RuntimeSchemaPositional
|
||||
if json.Unmarshal([]byte(raw), &positionals) != nil {
|
||||
return nil
|
||||
}
|
||||
sort.SliceStable(positionals, func(i, j int) bool { return positionals[i].Index < positionals[j].Index })
|
||||
return positionals
|
||||
}
|
||||
|
||||
// ExcludeFromRuntimeSchema keeps a human-facing hint or redirect in --help
|
||||
// while preventing it from being advertised as an executable agent tool.
|
||||
func ExcludeFromRuntimeSchema(cmd *cobra.Command) {
|
||||
SetCommandAnnotation(cmd, AnnotationExclude, "true")
|
||||
}
|
||||
|
||||
// CommandFlag resolves local flags plus product/group persistent flags.
|
||||
// Root persistent flags are intentionally available only when explicitly
|
||||
// requested; they are global execution controls, not tool parameters.
|
||||
func CommandFlag(cmd *cobra.Command, name string) *pflag.Flag {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if flag := cmd.Flags().Lookup(name); flag != nil {
|
||||
return flag
|
||||
}
|
||||
for current := cmd; current != nil; current = current.Parent() {
|
||||
if flag := current.PersistentFlags().Lookup(name); flag != nil {
|
||||
return flag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCommandAnnotation writes a non-blank command annotation.
|
||||
func SetCommandAnnotation(cmd *cobra.Command, key, value string) {
|
||||
if cmd == nil || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[key] = value
|
||||
}
|
||||
|
||||
// SetFlagAnnotation writes a single non-blank flag annotation value.
|
||||
func SetFlagAnnotation(flag *pflag.Flag, key, value string) {
|
||||
if flag == nil || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
if flag.Annotations == nil {
|
||||
flag.Annotations = map[string][]string{}
|
||||
}
|
||||
flag.Annotations[key] = []string{value}
|
||||
}
|
||||
|
||||
// SetFlagAnnotationValues writes a trimmed, non-empty flag annotation list.
|
||||
func SetFlagAnnotationValues(flag *pflag.Flag, key string, values ...string) {
|
||||
if flag == nil {
|
||||
return
|
||||
}
|
||||
clean := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
clean = append(clean, value)
|
||||
}
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return
|
||||
}
|
||||
if flag.Annotations == nil {
|
||||
flag.Annotations = map[string][]string{}
|
||||
}
|
||||
flag.Annotations[key] = clean
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package runtimeannotate
|
||||
|
||||
// Cobra annotation keys for Schema homology (path A). Writers and readers must
|
||||
// share these exact strings; do not re-declare literals in callers.
|
||||
const (
|
||||
AnnotationProduct = "dws.schema.product"
|
||||
AnnotationTool = "dws.schema.tool"
|
||||
AnnotationSource = "dws.schema.source"
|
||||
AnnotationTitle = "dws.schema.title"
|
||||
AnnotationDescription = "dws.schema.description"
|
||||
AnnotationMetaSource = "dws.schema.metadata_source"
|
||||
AnnotationExclude = "dws.schema.exclude"
|
||||
AnnotationConstraints = "dws.schema.constraints"
|
||||
AnnotationPositionals = "dws.schema.positionals"
|
||||
AnnotationContract = "dws.schema.contract"
|
||||
AnnotationRisk = "dws.schema.risk"
|
||||
AnnotationRuntimeGate = "dws.schema.runtime_gate"
|
||||
AnnotationFlagProperty = "dws.schema.property"
|
||||
AnnotationFlagType = "dws.schema.type"
|
||||
AnnotationFlagRequired = "dws.schema.required"
|
||||
AnnotationFlagReqWhen = "dws.schema.required_when"
|
||||
AnnotationFlagExample = "dws.schema.example"
|
||||
AnnotationFlagFormat = "x-cli-format"
|
||||
AnnotationFlagEnum = "x-cli-enum"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package runtimeannotate owns Cobra dws.schema.* annotation writers and the
|
||||
// RuntimeSchemaConstraints helpers used by the command framework.
|
||||
//
|
||||
// Package boundary:
|
||||
//
|
||||
// - Types / DTO / ProductDecl → internal/corecmd/contract
|
||||
// - AnnotateRuntime* writers (this package) — no Catalog / go:embed
|
||||
// - ContractFinal cobra store + Register seam → internal/cli/contractfinal
|
||||
// - Catalog assembly / ResolveMeta / go:embed → internal/cli (root)
|
||||
//
|
||||
// Dependency direction: corecmd and cli/contractfinal import this package;
|
||||
// this package must never import internal/cli (root) or contractfinal.
|
||||
package runtimeannotate
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package runtimeannotate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageAnnotateRuntimeRiskEmbedsContractMarker(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "x"}
|
||||
AnnotateRuntimeRisk(cmd, "write")
|
||||
got, ok := RuntimeContractRisk(cmd)
|
||||
if !ok || got != "write" {
|
||||
t.Fatalf("RuntimeContractRisk = %q %v", got, ok)
|
||||
}
|
||||
if cmd.Annotations[AnnotationContract] != "command" {
|
||||
t.Fatalf("contract marker = %q", cmd.Annotations[AnnotationContract])
|
||||
}
|
||||
AnnotateRuntimeRisk(cmd, "")
|
||||
if got, _ = RuntimeContractRisk(cmd); got != "write" {
|
||||
t.Fatalf("empty AnnotateRuntimeRisk must be no-op, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageAnnotateRuntimeGate(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "x"}
|
||||
AnnotateRuntimeGate(cmd, "devAppRequireWriteGuard")
|
||||
got, ok := RuntimeContractGate(cmd)
|
||||
if !ok || got != "devAppRequireWriteGuard" {
|
||||
t.Fatalf("RuntimeContractGate = %q %v", got, ok)
|
||||
}
|
||||
if cmd.Annotations[AnnotationContract] != "command" {
|
||||
t.Fatalf("contract marker = %q", cmd.Annotations[AnnotationContract])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageRuntimeContractAnnotationNilAndBlankGuards(t *testing.T) {
|
||||
AnnotateRuntimeFlagDescription(nil, "flag", "description")
|
||||
AnnotateRuntimeContract(nil)
|
||||
AnnotateRuntimeRisk(nil, "write")
|
||||
AnnotateRuntimeGate(nil, "devAppRequireWriteGuard")
|
||||
|
||||
cmd := &cobra.Command{Use: "x"}
|
||||
AnnotateRuntimeGate(cmd, " ")
|
||||
if _, ok := RuntimeContractGate(cmd); ok {
|
||||
t.Fatal("blank AnnotateRuntimeGate must not record a gate")
|
||||
}
|
||||
if cmd.Annotations != nil && cmd.Annotations[AnnotationContract] == "command" {
|
||||
t.Fatal("blank AnnotateRuntimeGate must not mark the command as Contract")
|
||||
}
|
||||
|
||||
blank := &cobra.Command{Use: "y", Annotations: map[string]string{
|
||||
AnnotationRuntimeGate: " ",
|
||||
}}
|
||||
if _, ok := RuntimeContractGate(blank); ok {
|
||||
t.Fatal("blank runtime_gate annotation must not report a gate")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -163,7 +164,7 @@ func BindEffectiveCommandRegistry(root *cobra.Command, effective EffectiveComman
|
||||
// Index Contract-declared dry_run capabilities at bind time: every
|
||||
// process that resolves the command tree gets the reviewed set, not
|
||||
// only processes that also run Schema assembly.
|
||||
if payload, ok := contract.RuntimeContractFinal(item.PrimaryCommand); ok && payload.DryRun != nil {
|
||||
if payload, ok := contractfinal.RuntimeContractFinal(item.PrimaryCommand); ok && payload.DryRun != nil {
|
||||
recordDeclaredDryRunCapability(item.CanonicalPath, *payload.DryRun)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -77,9 +77,9 @@ func schemaRegistryForTestWithMetadata(root *cobra.Command, agent embeddedAgentM
|
||||
// Clear bind-time ContractFinal so injected fixtures remain authoritative
|
||||
// for the commands under test. Production assembly never takes this path.
|
||||
for _, command := range bound.Commands {
|
||||
contract.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
|
||||
contractfinal.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
|
||||
for _, alias := range command.AliasCommands {
|
||||
contract.ClearRuntimeContractFinalForTest(alias.Command)
|
||||
contractfinal.ClearRuntimeContractFinalForTest(alias.Command)
|
||||
}
|
||||
}
|
||||
return assembleSchemaRegistryFromBoundAllowingLegacy(bound, runtimeSchemaMetadataSources{Agent: agent, MCP: mcp})
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -207,7 +208,7 @@ func assembleSchemaRegistryFromBoundWithOptions(bound BoundCommandRegistry, meta
|
||||
}
|
||||
|
||||
func runtimeToolSpecFromMetadata(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) {
|
||||
if final, ok := contract.RuntimeContractFinal(entry.Command); ok {
|
||||
if final, ok := contractfinal.RuntimeContractFinal(entry.Command); ok {
|
||||
return runtimeToolSpecFromContractFinal(entry, final, metadata)
|
||||
}
|
||||
canonicalPath := entry.ProductID + "." + entry.ToolName
|
||||
@@ -231,7 +232,7 @@ func assembleProductSelection(entry runtimeSchemaEntry, metadata runtimeSchemaMe
|
||||
// runtimeToolSpecAllowingLegacy is the test-isolated overlay path. Prefer
|
||||
// ContractFinal when present; otherwise reopen retired skill/MCP/agent inject.
|
||||
func runtimeToolSpecAllowingLegacy(entry runtimeSchemaEntry, metadata runtimeSchemaMetadataSources) (ToolSpec, error) {
|
||||
if final, ok := contract.RuntimeContractFinal(entry.Command); ok {
|
||||
if final, ok := contractfinal.RuntimeContractFinal(entry.Command); ok {
|
||||
return runtimeToolSpecFromContractFinal(entry, final, metadata)
|
||||
}
|
||||
return runtimeToolSpecFromLegacyMetadata(entry, metadata)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -143,7 +144,7 @@ func TestAssembleSchemaRegistryFailClosedMissingProductDecl(t *testing.T) {
|
||||
Selection: &contract.SelectionSpec{AgentSummary: "orphan leaf"},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
contract.ClearRuntimeContractFinalForTest(leaf)
|
||||
contractfinal.ClearRuntimeContractFinalForTest(leaf)
|
||||
contract.ClearProductDeclForTest("orphan")
|
||||
})
|
||||
product := &cobra.Command{Use: "orphan"}
|
||||
@@ -163,7 +164,7 @@ func TestAssembleSchemaRegistryAllowingLegacyIsolatesOverlayPath(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, command := range bound.Commands {
|
||||
contract.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
|
||||
contractfinal.ClearRuntimeContractFinalForTest(command.PrimaryCommand)
|
||||
}
|
||||
agent := embeddedAgentMetadata{
|
||||
Version: 1,
|
||||
@@ -203,7 +204,7 @@ func TestAssembleSchemaRegistryRequiresContractFinalAndProductDecl(t *testing.T)
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
contract.ClearRuntimeContractFinalForTest(leaf)
|
||||
contractfinal.ClearRuntimeContractFinalForTest(leaf)
|
||||
contract.ClearProductDeclForTest("sample")
|
||||
})
|
||||
contract.RegisterProductDecl(contract.ProductDecl{
|
||||
@@ -333,7 +334,7 @@ func assembleContractFinalTextTool(t *testing.T, short, long, declaredDescriptio
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
contract.ClearRuntimeContractFinalForTest(leaf)
|
||||
contractfinal.ClearRuntimeContractFinalForTest(leaf)
|
||||
contract.ClearProductDeclForTest("sample")
|
||||
})
|
||||
contract.RegisterProductDecl(contract.ProductDecl{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
|
||||
package contract
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ContractFinalPayload is the Contract-authored final Schema leaf overlay.
|
||||
// Registered in-process by the framework; Schema assembly reads it as
|
||||
// pass-through. No JSON bridge. Treat as read-only after Register.
|
||||
// Registered in-process by the framework via runtimeannotate / cli seam;
|
||||
// Schema assembly reads it as pass-through. No JSON bridge. Treat as
|
||||
// read-only after Register.
|
||||
//
|
||||
// The Cobra-keyed runtime store lives in internal/corecmd/runtimeannotate
|
||||
// (not this DTO package). Production product code registers through
|
||||
// cli.RegisterRuntimeContractFinal; framework code may call
|
||||
// runtimeannotate.RegisterRuntimeContractFinal directly.
|
||||
type ContractFinalPayload struct {
|
||||
Title string
|
||||
Description string
|
||||
@@ -33,59 +33,3 @@ type ContractFinalPayload struct {
|
||||
Selection *SelectionSpec
|
||||
Identity *ToolIdentitySpec
|
||||
}
|
||||
|
||||
var contractFinalByCommand sync.Map // *cobra.Command → *ContractFinalPayload
|
||||
|
||||
// RegisterRuntimeContractFinal stores the typed final Schema overlay for a leaf.
|
||||
// Light runtime write: one map store, no JSON, no deep clone.
|
||||
//
|
||||
// Seam-only: production code must call cli.RegisterRuntimeContractFinal so the
|
||||
// dws.schema.contract annotation and the typed store stay atomic. This helper
|
||||
// exists for that seam (and tests that exercise the store). Do not call it from
|
||||
// corecmd.AttachContract / product helpers / shortcuts.
|
||||
func RegisterRuntimeContractFinal(cmd *cobra.Command, payload ContractFinalPayload) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
p := payload
|
||||
contractFinalByCommand.Store(cmd, &p)
|
||||
}
|
||||
|
||||
// RuntimeContractFinal returns the registered final Schema overlay (read-only).
|
||||
func RuntimeContractFinal(cmd *cobra.Command) (ContractFinalPayload, bool) {
|
||||
if cmd == nil {
|
||||
return ContractFinalPayload{}, false
|
||||
}
|
||||
raw, ok := contractFinalByCommand.Load(cmd)
|
||||
if !ok {
|
||||
return ContractFinalPayload{}, false
|
||||
}
|
||||
p, ok := raw.(*ContractFinalPayload)
|
||||
if !ok || p == nil {
|
||||
return ContractFinalPayload{}, false
|
||||
}
|
||||
return *p, true
|
||||
}
|
||||
|
||||
// HasRuntimeContractFinal reports whether the leaf has a registered final overlay.
|
||||
func HasRuntimeContractFinal(cmd *cobra.Command) bool {
|
||||
if cmd == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := contractFinalByCommand.Load(cmd)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ClearRuntimeContractFinalForTest removes a registration (tests only).
|
||||
func ClearRuntimeContractFinalForTest(cmd *cobra.Command) {
|
||||
if cmd != nil {
|
||||
contractFinalByCommand.Delete(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// StoreRuntimeContractFinalRawForTest injects a raw map value (tests only).
|
||||
func StoreRuntimeContractFinalRawForTest(cmd *cobra.Command, raw any) {
|
||||
if cmd != nil {
|
||||
contractFinalByCommand.Store(cmd, raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
package contract
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestProductDeclRegistryRoundTrip(t *testing.T) {
|
||||
t.Cleanup(func() { ClearProductDeclForTest("sample") })
|
||||
ClearProductDeclForTest("sample")
|
||||
|
||||
if HasProductDecl("sample") {
|
||||
t.Fatal("HasProductDecl before register must be false")
|
||||
}
|
||||
RegisterProductDecl(ProductDecl{})
|
||||
if HasProductDecl("") {
|
||||
t.Fatal("empty ID must not register")
|
||||
}
|
||||
|
||||
RegisterProductDecl(ProductDecl{
|
||||
ID: " sample ",
|
||||
Selection: ProductSelectionDecl{
|
||||
AgentSummary: "Manage samples",
|
||||
UseWhen: []string{"target is a sample"},
|
||||
AvoidWhen: []string{"target is another product"},
|
||||
},
|
||||
})
|
||||
if !HasProductDecl("sample") {
|
||||
t.Fatal("HasProductDecl after register must be true")
|
||||
}
|
||||
got, ok := LookupProductDecl("sample")
|
||||
if !ok || got.ID != "sample" || got.Selection.AgentSummary != "Manage samples" {
|
||||
t.Fatalf("LookupProductDecl = %#v, ok=%v", got, ok)
|
||||
}
|
||||
ids := RegisteredProductDeclIDs()
|
||||
found := false
|
||||
for _, id := range ids {
|
||||
if id == "sample" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("RegisteredProductDeclIDs missing sample: %#v", ids)
|
||||
}
|
||||
|
||||
selection, provenance := ProductSelectionFromDecl(got)
|
||||
if selection.AgentSummary != "Manage samples" || selection.AgentSummarySource != ProductDeclSourceRef {
|
||||
t.Fatalf("ProductSelectionFromDecl selection = %#v", selection)
|
||||
}
|
||||
for _, field := range []string{"agent_summary", "use_when", "avoid_when"} {
|
||||
prov, ok := provenance[field]
|
||||
if !ok || prov.Precedence != "contract_final" || prov.Source != ProductDeclProvenanceSource {
|
||||
t.Fatalf("field %s provenance = %#v", field, prov)
|
||||
}
|
||||
}
|
||||
|
||||
ClearProductDeclForTest("sample")
|
||||
if HasProductDecl("sample") {
|
||||
t.Fatal("ClearProductDeclForTest must remove registration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductDeclRegisterPanicsOnIncompleteSelection(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("expected panic for incomplete ProductDecl")
|
||||
}
|
||||
}()
|
||||
RegisterProductDecl(ProductDecl{ID: "broken"})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
@@ -65,7 +66,7 @@ func TestCrossPlatformCoverageNewCommandEmbedsFullContractDeclAsFinalSource(t *t
|
||||
t.Fatal("framework must convert typed ContractDecl; must not write JSON dws.schema.final")
|
||||
}
|
||||
}
|
||||
final, ok := contract.RuntimeContractFinal(cmd)
|
||||
final, ok := contractfinal.RuntimeContractFinal(cmd)
|
||||
if !ok {
|
||||
t.Fatal("expected typed ContractFinal registration")
|
||||
}
|
||||
@@ -135,7 +136,7 @@ func TestNewCommandFallsBackToDeclaredDescriptionWithoutLong(t *testing.T) {
|
||||
},
|
||||
Invoke: func(*Ctx, map[string]any) error { return nil },
|
||||
})
|
||||
final, ok := contract.RuntimeContractFinal(cmd)
|
||||
final, ok := contractfinal.RuntimeContractFinal(cmd)
|
||||
if !ok {
|
||||
t.Fatal("expected typed ContractFinal registration")
|
||||
}
|
||||
@@ -240,7 +241,7 @@ func TestNewCommandSafetySpecPassThrough(t *testing.T) {
|
||||
}
|
||||
build := func(spec Spec) *contract.SafetySpec {
|
||||
cmd := New(spec)
|
||||
final, ok := contract.RuntimeContractFinal(cmd)
|
||||
final, ok := contractfinal.RuntimeContractFinal(cmd)
|
||||
if !ok || final.Safety == nil {
|
||||
t.Fatalf("expected declared safety, final=%#v ok=%v", final, ok)
|
||||
}
|
||||
@@ -273,7 +274,7 @@ func TestContractDeclEmptySkipsFinal(t *testing.T) {
|
||||
Safety: testWriteSafety(),
|
||||
Invoke: func(*Ctx, map[string]any) error { return nil },
|
||||
})
|
||||
if contract.HasRuntimeContractFinal(cmd) {
|
||||
if contractfinal.HasRuntimeContractFinal(cmd) {
|
||||
t.Fatal("Safety without Contract must not register Final (keep runtime write light)")
|
||||
}
|
||||
if _, ok := cmd.Annotations["dws.schema.risk"]; ok {
|
||||
|
||||
@@ -21,10 +21,9 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Production framework code must register ContractFinal through the cli
|
||||
// delivery seam (annotate + store), not by calling the contract store helper
|
||||
// directly.
|
||||
func TestAttachContractUsesCLIRegisterSeam(t *testing.T) {
|
||||
// Production framework code must register ContractFinal through the
|
||||
// contractfinal annotate+store seam, not by importing the cli delivery root.
|
||||
func TestAttachContractUsesContractFinalRegisterSeam(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
@@ -35,11 +34,13 @@ func TestAttachContractUsesCLIRegisterSeam(t *testing.T) {
|
||||
t.Fatalf("read corecmd.go: %v", err)
|
||||
}
|
||||
body := string(raw)
|
||||
if !strings.Contains(body, "cli.RegisterRuntimeContractFinal(") {
|
||||
t.Fatal("AttachContract/New must call cli.RegisterRuntimeContractFinal")
|
||||
if !strings.Contains(body, "contractfinal.RegisterRuntimeContractFinal(") {
|
||||
t.Fatal("AttachContract/New must call contractfinal.RegisterRuntimeContractFinal")
|
||||
}
|
||||
// Disallow the dual-track pattern: annotate then contract.Register in corecmd.
|
||||
if strings.Contains(body, "contract.RegisterRuntimeContractFinal(") {
|
||||
t.Fatal("corecmd must not call contract.RegisterRuntimeContractFinal directly; use cli seam")
|
||||
if strings.Contains(body, `"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"`) {
|
||||
t.Fatal("corecmd must not import internal/cli delivery root")
|
||||
}
|
||||
if strings.Contains(body, "cli.RegisterRuntimeContractFinal(") {
|
||||
t.Fatal("corecmd must not call cli.RegisterRuntimeContractFinal; use contractfinal seam")
|
||||
}
|
||||
}
|
||||
|
||||
+43
-43
@@ -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
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -1559,7 +1560,7 @@ func TestCrossPlatformCoverageEmbedContractCobraProjection(t *testing.T) {
|
||||
t.Fatalf("hidden flag must not be projected, got %#v", got)
|
||||
}
|
||||
// The authored ContractDecl still lands as the typed ContractFinal.
|
||||
final, ok := contract.RuntimeContractFinal(cmd)
|
||||
final, ok := contractfinal.RuntimeContractFinal(cmd)
|
||||
if !ok || final.Description != "desc" {
|
||||
t.Fatalf("cobra projection must still embed ContractDecl, final=%#v ok=%v", final, ok)
|
||||
}
|
||||
@@ -1570,7 +1571,7 @@ func TestCrossPlatformCoverageAttachContractNilAndEmptyGuards(t *testing.T) {
|
||||
AttachContract(nil, testWriteSafety(), ContractDecl{Description: "d"}, "s", "l")
|
||||
cmd := newTestCommand()
|
||||
AttachContract(cmd, testWriteSafety(), ContractDecl{}, "s", "l")
|
||||
if contract.HasRuntimeContractFinal(cmd) {
|
||||
if contractfinal.HasRuntimeContractFinal(cmd) {
|
||||
t.Fatal("empty ContractDecl must not register a ContractFinal")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -151,7 +152,7 @@ func contractFinalProductMetadata(decl contract.ProductDecl) ProductMetadata {
|
||||
// Absent payload sections stay absent (never authored-empty); declared
|
||||
// sections map field-by-field, preserving explicit empty-list authorship.
|
||||
func contractFinalToolMetadata(command *cobra.Command) (ToolMetadata, bool) {
|
||||
payload, ok := contract.RuntimeContractFinal(command)
|
||||
payload, ok := contractfinal.RuntimeContractFinal(command)
|
||||
if !ok {
|
||||
return ToolMetadata{}, false
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
@@ -246,7 +247,7 @@ func validateSelectionAuthoringContracts(opts Options) error {
|
||||
expectedTools := expectedCanonicalToolSet(opts)
|
||||
for canonical := range expectedTools {
|
||||
bound, ok := opts.BoundCommands.ByCanonical[canonical]
|
||||
if ok && contract.HasRuntimeContractFinal(bound.PrimaryCommand) {
|
||||
if ok && contractfinal.HasRuntimeContractFinal(bound.PrimaryCommand) {
|
||||
delete(expectedTools, canonical)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -488,7 +489,7 @@ func TestCrossPlatformCoverageContractFinalDeclarationFailureEdges(t *testing.T)
|
||||
cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{
|
||||
Selection: &contract.SelectionSpec{AgentSummary: "declared summary"},
|
||||
})
|
||||
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) })
|
||||
t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(declared) })
|
||||
bound := cli.BoundCommandRegistry{ByCanonical: map[string]cli.BoundCommandSpec{
|
||||
"sample.run": {PrimaryCommand: declared},
|
||||
}}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ func SelectionHintCoverageTools(projection RegistryProjection) map[string]bool {
|
||||
}
|
||||
for canonical := range expectedTools {
|
||||
bound, ok := projection.Bound.ByCanonical[canonical]
|
||||
if ok && contract.HasRuntimeContractFinal(bound.PrimaryCommand) {
|
||||
if ok && contractfinal.HasRuntimeContractFinal(bound.PrimaryCommand) {
|
||||
delete(expectedTools, canonical)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/agentmetadata"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/outputguard"
|
||||
@@ -283,7 +284,7 @@ func TestCrossPlatformCoverageMetadataRegistryAndSelectionFailureEdges(t *testin
|
||||
func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredTools(t *testing.T) {
|
||||
declared := &cobra.Command{Use: "run"}
|
||||
cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{})
|
||||
t.Cleanup(func() { contract.ClearRuntimeContractFinalForTest(declared) })
|
||||
t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(declared) })
|
||||
|
||||
registry := commandRegistryProjection{
|
||||
CanonicalToolPaths: map[string]string{
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
)
|
||||
|
||||
@@ -80,7 +81,7 @@ func TestDeclareLeafMetadataDoesNotRewriteRunE(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if !contract.HasRuntimeContractFinal(cmd) {
|
||||
if !contractfinal.HasRuntimeContractFinal(cmd) {
|
||||
t.Fatal("expected ContractFinal")
|
||||
}
|
||||
// function values are not comparable; ensure pointer identity via uintptr trick is unnecessary —
|
||||
@@ -128,7 +129,7 @@ func TestAitableDeclareLeafMetadataCoversRegistryHelpers(t *testing.T) {
|
||||
missing = append(missing, tool.CLIPath+" (command missing)")
|
||||
continue
|
||||
}
|
||||
if !contract.HasRuntimeContractFinal(leaf) {
|
||||
if !contractfinal.HasRuntimeContractFinal(leaf) {
|
||||
missing = append(missing, tool.CLIPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,9 +256,10 @@ func TestCrossPlatformCoverageDeclareLeafMetadataConfirmFallbackWithoutCaller(t
|
||||
}
|
||||
|
||||
// TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool
|
||||
// covers the deferred-confirm post-RunE fallback: with a deps.Caller present
|
||||
// and an inner RunE that never dispatches through CallTool, the gate stays
|
||||
// unconfirmed and ConfirmSafety must still run after the inner RunE.
|
||||
// covers the deferred-confirm fail-closed path: with a deps.Caller present and
|
||||
// an inner RunE that never dispatches through CallTool, success-without-confirm
|
||||
// is rejected. Post-RunE ConfirmSafety is intentionally not used — local side
|
||||
// effects may already have run.
|
||||
func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithoutCallTool(t *testing.T) {
|
||||
prev := deps
|
||||
caller := &deferConfirmTestCaller{}
|
||||
@@ -287,24 +288,25 @@ func TestCrossPlatformCoverageDeclareLeafMetadataDeferredConfirmAfterRunEWithout
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
// Without --yes the post-RunE fallback confirmation must fail closed.
|
||||
err := cmd.Execute()
|
||||
if err == nil || (!strings.Contains(err.Error(), "confirmation_required") && !strings.Contains(err.Error(), "需要用户确认")) {
|
||||
t.Fatalf("Execute() without --yes error = %v, want confirmation_required", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "never obtained via CallTool") {
|
||||
t.Fatalf("Execute() without CallTool error = %v, want fail-closed contract error", err)
|
||||
}
|
||||
if ran != 1 {
|
||||
t.Fatalf("inner RunE ran %d times, want 1 (deferred gate confirms after RunE)", ran)
|
||||
t.Fatalf("inner RunE ran %d times, want 1 (fail-closed after successful RunE)", ran)
|
||||
}
|
||||
if caller.calls != 0 {
|
||||
t.Fatalf("CallTool calls = %d, want 0", caller.calls)
|
||||
}
|
||||
|
||||
// With --yes the post-RunE fallback confirmation passes.
|
||||
// --yes must not green-light a post-RunE confirmation: the contract error
|
||||
// remains even when ConfirmSafety would have passed.
|
||||
if err := cmd.Flags().Set("yes", "true"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() with --yes error = %v", err)
|
||||
err = cmd.Execute()
|
||||
if err == nil || !strings.Contains(err.Error(), "never obtained via CallTool") {
|
||||
t.Fatalf("Execute() with --yes but no CallTool error = %v, want fail-closed contract error", err)
|
||||
}
|
||||
if ran != 2 {
|
||||
t.Fatalf("inner RunE ran %d times, want 2", ran)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
@@ -554,7 +555,7 @@ func TestLiveMountExplicitSafetyDrivesRuntimeAndContractFinal(t *testing.T) {
|
||||
cmd := mount(s)
|
||||
root.AddCommand(cmd)
|
||||
|
||||
final, ok := contract.RuntimeContractFinal(cmd)
|
||||
final, ok := contractfinal.RuntimeContractFinal(cmd)
|
||||
if !ok || final.Safety == nil {
|
||||
t.Fatal("mounted Shortcut must publish ContractFinal Safety")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ set -eu
|
||||
# Do NOT compare Catalog fields to Catalog provenance labels: that is a tautology
|
||||
# (both sides read the same embedded snapshot). The real homology gate is
|
||||
# TestUserRequiredSafetyHomologyWithRuntimeGate in
|
||||
# internal/cli/contract_safety_homology_external_test.go — it walks the live
|
||||
# internal/cli/homology/safety_homology_test.go — it walks the live
|
||||
# Cobra tree, reads ContractFinal.Safety, compares to AssembleSchemaRegistry
|
||||
# ToolSpec.Confirmation, and probes the runtime gate.
|
||||
|
||||
@@ -20,7 +20,7 @@ if [ -e internal/cli/schema_hints ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go test ./internal/cli \
|
||||
go test ./internal/cli/homology \
|
||||
-run '^TestUserRequiredSafetyHomologyWithRuntimeGate$' \
|
||||
-count=1
|
||||
|
||||
|
||||
@@ -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)$' \
|
||||
|
||||
Reference in New Issue
Block a user