Compare commits

...
Author SHA1 Message Date
chichuan 65ab48ae5a ci: govern whole-product CLI and Schema retirement 2026-08-25 17:03:53 +08:00
10 changed files with 1258 additions and 63 deletions
+12 -3
View File
@@ -242,8 +242,11 @@ flag 迁移。
Schema compatibility 使用同一组 base、stable、candidate refs,以及 base-owned flag
与 command migration ledgers。merge-base-owned checker 分别规范化 merge-base 与
stable 的完整 Schema,并让 candidate 对两份历史 contract 独立执行检查;它只把已通过
Interface lifecycle 的 exact rename、command move 或 flag extraction 规范化到当前历史
副本,不会维护第二份 allowlist,也不会放宽其他 Schema 历史字段。
Interface lifecycle 的 exact rename、command move、flag extraction、availability
hardening 或完整 product retirement 规范化到当前历史副本,不会维护第二份 allowlist,
也不会放宽其他 Schema 历史字段。产品退役必须在共享 command ledger 中精确列出
merge-base / stable 的全部 historical command 与 Schema tool 并集;candidate 不能用同一
PR 新增记录并删除产品。
For a release-seal branch that archives rendered fragments:
@@ -353,6 +356,11 @@ helper、fixture 或在同一 PR 新增 self-approval 记录来放行 breaking c
bootstrap 仍由 merge-base 已有的 modern helper 做无豁免比较,并只接受 candidate
提交中的规范空清单;完整边界见下方治理文档。
首次增加旧 merge-base 不认识的 migration `kind` 必须拆成三个 PR:先只落 parser、
lifecycle、CLI/Schema adapters、wrapper capability 与 hostile tests;再在 surface 不变时
新增 `pending`;最后才由产品 PR 消费为 `consumed`。CLI 与 Schema wrappers 会在旧
authority 首次看到 `product_retirement` 时提前拒绝,不能把机制与审批记录塞进同一 PR。
精确的两阶段 flag 迁移生命周期见
[CLI flag 兼容迁移治理](cli-interface-flag-migrations.md)。治理 PR 只能在
surface 未变化时新增 `pending`;后续产品 PR 达到审批的精确 surface 后,才能
@@ -363,7 +371,8 @@ shorthand、no-opt 和任何无关漂移仍然阻塞。Schema 可以新增;历
tool、parameter、mapping、positional execution、constraint 与 safety 语义继续
受保护。`alias_of` 只是一项由 `FlagSpec.Aliases` 产生的框架关系证据,不是 payload
等价证明;产品 PR 仍须证明 canonical 与 legacy 的最终运行 payload 等价并在 transport
前拒绝冲突输入。当前迁移清单为空,不授权 PR #904。
前拒绝冲突输入。`product_retirement` 机制 PR 不含任何 pending 退役记录,因此自身不授权
删除产品。
## Required GitHub repository settings
+51 -7
View File
@@ -7,7 +7,7 @@
两种原语都只放行清单精确登记的变化,不是通用 breaking-change 豁免,也不得在同一 command/flag 上叠加以绕过 rename 的 requiredness 保持规则。
同一套 base-owned lifecycle 也治理两类跨命令迁移:旧命令保留执行能力但从 Help / Schema 导航隐藏,并迁到新的公开命令路径;或把旧命令中的一个可选 flag 拆成新的专用命令。跨命令迁移只允许清单精确声明的 `command_became_hidden` / `flag_became_hidden` 及其 Schema 投影,不是通用 command-path breaking-change 豁免。
同一套 base-owned lifecycle 也治理跨命令迁移和完整产品退役:旧命令保留执行能力但从 Help / Schema 导航隐藏,并迁到新的公开命令路径;把旧命令中的一个可选 flag 拆成新的专用命令;或在独立审批后整体删除一个 product root 及其全部 CLI / Schema surface。清单只允许每种 `kind` 精确声明的 finding 与 Schema 投影,不是通用 command-path breaking-change 豁免。
同名 flag 的精确类型迁移属于另一类评审机制,只能进入
`internal/interfacesnapshot/reviewed.go` 与 legacy smoke helper 的镜像表;flag rename
@@ -38,7 +38,7 @@ Smoke fixture,不参与迁移审批。
同时提供 `--base` 与 `--stable`;核心 lifecycle 也拒绝缺失 stable 的非空清单,避免
调用方因漏传历史参考而提前清理 consumed receipt。
PR merge-base 同时拥有快照生成器、比较器和已审批清单。门禁用这套 base-owned helper 检查同一个已提交 candidate revision、merge-base 与 stable,candidate 不能通过修改自己的 Go 比较 helper 来放宽规则。candidate 中的清单只参与迁移状态流转,不能批准同一个 PR 引入的接口变化。首次引入 flag 机制时,merge-base 尚无迁移解析器;bootstrap 会用 merge-base 已有的 modern Interface Snapshot 做不带豁免的普通比较,并只接受 candidate 中逐字匹配的空 flag 清单。后续引入 command migration 扩展时,base 已拥有 flag comparator;bootstrap 仍只执行 base-owned 普通比较,不向旧 helper 传入新的 command ledger,因此允许随治理 PR 提交仍处于 before 的 pending 计划,也不会授予任何迁移豁免。bootstrap 无法让旧 helper 证明新治理实现本身正确,因此本治理 PR 的新 parser、lifecycle、launcher 与 hostile tests 仍是必须由真人评审的受保护策略变更;它们合入后才成为后续 PR 的 base-owned authority。
PR merge-base 同时拥有快照生成器、比较器和已审批清单。门禁用这套 base-owned helper 检查同一个已提交 candidate revision、merge-base 与 stable,candidate 不能通过修改自己的 Go 比较 helper 来放宽规则。candidate 中的清单只参与迁移状态流转,不能批准同一个 PR 引入的接口变化。首次引入 flag 机制时,merge-base 尚无迁移解析器;bootstrap 会用 merge-base 已有的 modern Interface Snapshot 做不带豁免的普通比较,并只接受 candidate 中逐字匹配的空 flag 清单。首次引入整个 command ledger 时,base 已拥有 flag comparator;bootstrap 仍只执行 base-owned 普通比较,不向旧 helper 传入新的 command ledger,也不会授予任何迁移豁免。bootstrap 无法让旧 helper 证明新治理实现本身正确,因此新 parser、lifecycle、launcher 与 hostile tests 必须先由真人评审,合入后才成为后续 PR 的 base-owned authority。
这条边界保护比较规则和审批数据,不是任意代码沙箱。GitHub workflow / launcher 的变更仍由仓库保护规则和真人评审负责;candidate Cobra 构建也会执行 candidate 代码,因此对同一 runner 上的主动恶意代码,需要独立进程或文件系统隔离,不能把本门禁描述成已经解决。
@@ -51,16 +51,56 @@ scripts/policy/interface-migrations/approved-command-migrations-v1.json
清单使用严格 JSON 解析:版本、字段名大小写、JSON 值类型、命令路径和 flag 名都必须精确;拒绝重复键、未知键、scalar `null` 与尾随 JSON 值,`reason` 不能为空;禁止 `*`、`?`、前缀规则或其他 wildcard。历史未声明 `kind` 的记录按 `flag_rename` 解释;新增同名 requiredness 迁移必须显式写 `kind: requiredness_change` 和单一 `flag` before/after。清单中的 `pending` 记录只记录已评审计划,并授权其精确列出的后续产品迁移;候选与 merge-base 仍必须精确匹配 `before`,不能授权同一个提交中的接口变化,也不能作为其他命令或参数的通配豁免。
首次引入一个旧 merge-base 不认识的新 `kind` 时,机制 PR 不得同时写入该 kind 的 pending 记录,因为旧的 base-owned 严格解析器会拒绝未知字段。必须先合入 parser、lifecycle、CLI/Schema adapter 与 hostile tests;待这些实现成为新的 merge-base authority 后,再用独立治理审批 PR 新增 pending,最后才由产品 PR 消费。
首次引入一个旧 merge-base 不认识的新 `kind` 时,必须使用三个独立 PR:机制 PR 只合入 parser、lifecycle、CLI/Schema adapter、wrapper capability 与 hostile tests,清单保持不含该 kind;待实现成为新的 merge-base authority 后,治理审批 PR 只新增 `pending` 且产品 surface 不变;最后产品 PR 才能删除或迁移 surface 并把记录改成 `consumed`。两个 authoritative wrapper 都会在旧 authority 首次收到 `product_retirement` 时提前拒绝并提示三阶段流程;旧严格 parser 的 unknown-kind 拒绝仍是兜底。
## 跨命令迁移原语
`approved-command-migrations-v1.json` 只接受两种 `kind`:
`approved-command-migrations-v1.json` 接受以下四种 `kind`:
| kind | CLI after 状态 | Schema 允许的精确投影 |
|---|---|---|
| `command_move` | legacy 命令仍 runnable、由 visible 变 hidden;replacement 由 absent 变 visible runnable | 同一 stable tool identity 的 `primary_cli_path` 改到 replacement;只允许清单列出的参数改名,参数类型、property、requiredness、default 等必须等价 |
| `flag_extraction` | legacy 命令保持 visible runnable;指定 legacy flag 仍可执行但由 visible 变 hidden;replacement 由 absent 变 visible runnable | source tool 只删除指定参数;replacement tool 必须位于精确的新路径,并保持 source 的 interface 与 safety identity;清单必须完整列出每个 source 参数到 replacement 参数或常量 property 的承接关系 |
| `schema_availability_hardening` | 精确命令保持 runnable,并按记录变 hidden 或保持 compatibility-visible | 同一 tool 只能从记录的 `availability.before` 变为 `availability.after`,其他字段仍由普通兼容检查约束 |
| `product_retirement` | 一个精确 product root 及其全部后代从已声明的 before surface 整体变为 absent | 删除同名 Schema product;`commands` 与 `schema.tools` 必须分别完整覆盖 merge-base / stable 的历史并集 |
### 完整产品退役
`product_retirement` 的 `legacy.command` 必须是 `dws <product_id>` 这一层的精确 product root;不接受子路径、alias、prefix 或 wildcard。root 的 before 必须是 runnable,并精确声明当时的 `hidden` 值,因此既能治理公开 root,也能治理真实存在的 hidden runnable root;after 只能是 absent。该 kind 不声明 replacement、legacy flag、参数映射或 availability。
记录必须包含两个已排序、无重复的完整集合:
- `commands`:merge-base 与 stable Interface Snapshot 中该 root 本身和全部后代的 canonical path 并集;root 必须包含在内。任一历史快照出现未登记后代,或登记了两份历史都不存在的命令,都会失败。
- `schema.tools`:merge-base 与 stable normalized Schema 中同名 product 的全部 canonical tool ID 并集;每个 ID 必须以 `<product_id>.` 开头。stable 可以没有该产品或只含较早子集,但两份历史都必须受同一记录约束。
治理审批 PR 中,candidate 的完整命令对象集合和 Schema product 必须与 merge-base 深度相等;只删一个 child、改 flag / alias / safety、改 tool contract,或在同一 PR 删除产品都会被视为 self-approval。产品 PR 消费 base-owned pending 时,只接受 CLI subtree 和 Schema product 同时整体 absent。普通 Interface Compare 会把整棵树的删除折叠为 root `command_removed`,但 lifecycle 会在过滤该 finding 前独立核对每一个历史 command,因此折叠不会扩大授权。
下面只是结构示例,不代表已审批产品;首次落地该 kind 的机制 PR 也不得加入这条记录:
```json
{
"kind": "product_retirement",
"legacy": {
"command": "dws edu-app",
"before": {"present": true, "runnable": true, "hidden": true},
"after": {"present": false}
},
"commands": [
"dws edu-app",
"dws edu-app task",
"dws edu-app task list"
],
"schema": {
"product_id": "edu-app",
"tools": [
"edu-app.query_all_task",
"edu-app.query_publish_task"
]
},
"state": "pending",
"reason": "Retire the reviewed education application product surface."
}
```
`command_move` 只能隐藏没有子命令的 legacy leaf,且 legacy 与 replacement
不得互为祖先路径;整棵命令树的迁移需要单独设计逐叶治理,不能复用这一原语。
@@ -102,7 +142,7 @@ bool 常量证据属于 bootstrap;一旦任一历史快照已记录该
replacement 必须保留 source 已发布的 dry-run 能力:历史 `dry_run` 非空时不得删除或改值;
历史未声明时允许 replacement 新增 dry-run。这与普通 Schema 兼容规则保持同一单调边界。
两种迁移都要求旧 argv 继续可执行。删除旧命令、删除旧 flag、把 legacy 改成 non-runnable、改变未登记的历史参数、改变 interface / safety,或只完成部分 before → after 转换都会 fail closed。命令别名会先规范到 reference 的 canonical path,但清单本身仍只能记录精确 canonical 命令,不能用 alias 或前缀扩大授权。
`command_move` 与 `flag_extraction` 都要求旧 argv 继续可执行。删除旧命令、删除旧 flag、把 legacy 改成 non-runnable、改变未登记的历史参数、改变 interface / safety,或只完成部分 before → after 转换都会 fail closed。命令别名会先规范到 reference 的 canonical path,但清单本身仍只能记录精确 canonical 命令,不能用 alias 或前缀扩大授权。`product_retirement` 是唯一允许整体删除命令的 kind,并受上一节的完整集合证明约束。
跨命令清单复用下文同一套 `pending → consumed → inert/cleanup` 生命周期。治理 PR 只能新增 `pending` 且产品 surface 必须仍是 before;后续产品 PR 才能一次性切到 after 并改为 `consumed`。candidate 新增的 pending 记录不能批准自己的改动。
@@ -114,8 +154,8 @@ rename 以 `(kind, command, legacy flag, canonical flag)` 为唯一精确键;r
| 阶段 | PR 可以做什么 | 必须满足的快照状态 |
|---|---|---|
| 1. 治理审批 | 新增 `state: pending` 的精确记录;不得在同一个 PR 修改产品 surface | candidate 和 merge-base 都与记录中的 `before` 完全一致;该记录不改变 stable 的判断 |
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | rename 的 legacy 仍存在但由 visible 变 hidden,且声明 `alias_of`,canonical requiredness 保持不变;requiredness change 只把同名 flag 从 optional 提升为 required |
| 1. 治理审批 | 新增 `state: pending` 的精确记录;不得在同一个 PR 修改产品 surface | candidate 和 merge-base 都与记录中的 `before` 完全一致;产品退役还要求 command subtree 与 Schema product 深度相等 |
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | rename 的 legacy 仍存在但由 visible 变 hidden;requiredness change 只提升同名 flag;产品退役则要求完整 CLI subtree 与 Schema product 同时 absent |
| 3. 保留回执 | 产品 PR 合入后,如果 stable 仍是 `before`,继续保留 `consumed` | merge-base 或 stable 仍有任一份尚未达到 `after` |
| 4. 惰性保留或清理 | 当 merge-base 和 stable 都已经是 `after`,该记录不再提供任何授权;后续 PR 可以原样保留或删除 | 两份参考快照均精确匹配 `after`;保留时仍必须是不可改写的 `consumed`,接口偏离 `after` 继续失败 |
@@ -211,6 +251,8 @@ rename 以 `(kind, command, legacy flag, canonical flag)` 为唯一精确键;r
2. required legacy 被新增的 required canonical 替代时产生的 `required_flag_added`;如果 canonical 在 before 阶段只是 hidden 占位符,则允许它在转为公开拼写时继承 legacy 的 requiredness。已有的 visible canonical 不允许借 rename 改变 requiredness。
3. `requiredness_change` 中同名 flag 从 optional 提升为 required 时产生的 `flag_became_required`。
跨命令记录另有各自的窄授权。`product_retirement` 只有在 lifecycle 已证明 current 整棵 subtree absent、且每个 merge-base / stable 历史命令都被完整登记后,才会移除该 product root 的 `command_removed`(以及同一 root 的 alias removal)finding;它不会按 prefix 过滤任意 child finding,也不会授权其他产品。
以下变化仍按普通兼容规则阻塞,不能被迁移记录掩盖:
- 删除 legacy、canonical、命令或其他 flag;
@@ -251,6 +293,8 @@ canonical-only `after` 状态时不需要再次投影;adapter 保持 baseline
format、enum、`required_when`、constraints、positionals 与 safety 等全部字段仍交给原 checker,
任何不相干漂移继续阻塞。
`product_retirement` 的 Schema adapter 读取同一条 command ledger 记录,不维护第二份 allowlist。checker 必须同时收到 merge-base 与 stable 两份 normalized Schema:pending 的 `schema.tools` 等于两份历史的精确并集,candidate-added pending 还要求 current product 与 merge-base 深度相等;消费时 current 不得再发布该 product。adapter 只从当前被检查的历史副本删除该完整 product,再交给普通兼容检查,其他 product 不变。
## 本地验证
先确保 merge-base 和 stable tag 已在本地,然后运行与 CI 相同的权威门禁:
+165 -14
View File
@@ -9,24 +9,31 @@ import (
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
)
const CommandMigrationManifestVersion = 1
// productRetirementMigrationCapability is inspected only in the trusted
// merge-base source by the authoritative compatibility wrappers. A mechanism
// PR must land this capability before a later PR may add a pending retirement.
const productRetirementMigrationCapability = "dws.command-migration.product-retirement.v1"
const (
CommandMigrationPending = "pending"
CommandMigrationConsumed = "consumed"
CommandMigrationMove = "command_move"
CommandMigrationFlagExtraction = "flag_extraction"
CommandMigrationAvailability = "schema_availability_hardening"
CommandMigrationMove = "command_move"
CommandMigrationFlagExtraction = "flag_extraction"
CommandMigrationAvailability = "schema_availability_hardening"
CommandMigrationProductRetirement = "product_retirement"
)
// CommandMigrationManifest governs compatibility-preserving surface moves that
// cannot be represented as an in-command flag rename. The merge-base owns the
// authorization; the candidate copy is only a lifecycle receipt.
// CommandMigrationManifest governs reviewed cross-command transitions and
// whole-product retirement. The merge-base owns the authorization; the
// candidate copy is only a lifecycle receipt.
type CommandMigrationManifest struct {
Version int `json:"version"`
Migrations []CommandMigration `json:"migrations"`
@@ -37,6 +44,7 @@ type CommandMigration struct {
Legacy CommandMigrationSide `json:"legacy"`
Replacement CommandMigrationSide `json:"replacement"`
LegacyFlag CommandMigrationFlag `json:"legacy_flag"`
Commands []string `json:"commands,omitempty"`
Schema CommandMigrationSchema `json:"schema"`
State string `json:"state"`
Reason string `json:"reason"`
@@ -64,6 +72,7 @@ type CommandMigrationSchema struct {
ProductID string `json:"product_id"`
SourceToolID string `json:"source_tool_id"`
ReplacementToolID string `json:"replacement_tool_id"`
Tools []string `json:"tools,omitempty"`
Parameters []CommandParameterMigration `json:"parameters"`
Availability *CommandAvailabilityChange `json:"availability,omitempty"`
}
@@ -133,9 +142,22 @@ func (m CommandMigrationManifest) Validate() error {
}
func (m CommandMigration) validate() error {
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction && m.Kind != CommandMigrationAvailability {
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction &&
m.Kind != CommandMigrationAvailability && m.Kind != CommandMigrationProductRetirement {
return fmt.Errorf("invalid kind %q", m.Kind)
}
if strings.TrimSpace(m.Reason) == "" || m.Reason != strings.TrimSpace(m.Reason) {
return fmt.Errorf("migration must include a non-empty trimmed reason")
}
if m.State != CommandMigrationPending && m.State != CommandMigrationConsumed {
return fmt.Errorf("invalid state %q", m.State)
}
if m.Kind == CommandMigrationProductRetirement {
return m.validateProductRetirement()
}
if m.Commands != nil {
return fmt.Errorf("%s must not declare commands", m.Kind)
}
if m.Kind == CommandMigrationAvailability {
if !isExactCommandPath(m.Legacy.Command) {
return fmt.Errorf("legacy must be an exact command path rooted at dws")
@@ -151,12 +173,6 @@ func (m CommandMigration) validate() error {
return fmt.Errorf("legacy and replacement command paths must differ")
}
}
if strings.TrimSpace(m.Reason) == "" || m.Reason != strings.TrimSpace(m.Reason) {
return fmt.Errorf("migration must include a non-empty trimmed reason")
}
if m.State != CommandMigrationPending && m.State != CommandMigrationConsumed {
return fmt.Errorf("invalid state %q", m.State)
}
states := map[string]CommandMigrationState{
"legacy before": m.Legacy.Before,
"legacy after": m.Legacy.After,
@@ -246,6 +262,42 @@ func (m CommandMigration) validate() error {
return nil
}
func (m CommandMigration) validateProductRetirement() error {
if !isExactCommandPath(m.Legacy.Command) || m.Legacy.Command == "dws" {
return fmt.Errorf("product_retirement legacy must be an exact product root below dws")
}
if m.Replacement != (CommandMigrationSide{}) {
return fmt.Errorf("product_retirement must not declare replacement")
}
if m.LegacyFlag != (CommandMigrationFlag{}) {
return fmt.Errorf("product_retirement must not declare legacy_flag")
}
if !m.Legacy.Before.Present || !m.Legacy.Before.Runnable || m.Legacy.After != (CommandMigrationState{}) {
return fmt.Errorf("product_retirement root must migrate exactly from its declared runnable visibility to absent")
}
if len(strings.Fields(m.Legacy.Command)) != 2 || strings.TrimPrefix(m.Legacy.Command, "dws ") != m.Schema.ProductID {
return fmt.Errorf("product_retirement root must exactly match schema product_id")
}
if len(m.Commands) == 0 {
return fmt.Errorf("product_retirement commands must be a non-empty array")
}
rootFound := false
for index, command := range m.Commands {
if !isExactCommandPath(command) ||
(command != m.Legacy.Command && !strings.HasPrefix(command, m.Legacy.Command+" ")) {
return fmt.Errorf("product_retirement command %d must be an exact path in root %q", index, m.Legacy.Command)
}
if index > 0 && m.Commands[index-1] >= command {
return fmt.Errorf("product_retirement commands must be sorted and unique")
}
rootFound = rootFound || command == m.Legacy.Command
}
if !rootFound {
return fmt.Errorf("product_retirement commands must include root %q", m.Legacy.Command)
}
return m.Schema.validate(m.Kind)
}
func (s CommandMigrationState) validate(label string) error {
if !s.Present {
if s.Runnable || s.Hidden {
@@ -286,6 +338,30 @@ func (f CommandMigrationFlag) validate() error {
}
func (s CommandMigrationSchema) validate(kind string) error {
if kind == CommandMigrationProductRetirement {
if !isExactSchemaIdentifier(s.ProductID) {
return fmt.Errorf("schema product_id must be an exact identifier")
}
if s.SourceToolID != "" || s.ReplacementToolID != "" || s.Parameters != nil || s.Availability != nil {
return fmt.Errorf("product_retirement schema must declare only product_id and tools")
}
if len(s.Tools) == 0 {
return fmt.Errorf("product_retirement schema tools must be a non-empty array")
}
prefix := s.ProductID + "."
for index, tool := range s.Tools {
if !isExactSchemaIdentifier(tool) || !strings.HasPrefix(tool, prefix) || len(tool) == len(prefix) {
return fmt.Errorf("product_retirement schema tool %d must be an exact canonical path in product %q", index, s.ProductID)
}
if index > 0 && s.Tools[index-1] >= tool {
return fmt.Errorf("product_retirement schema tools must be sorted and unique")
}
}
return nil
}
if s.Tools != nil {
return fmt.Errorf("%s must not declare schema tools", kind)
}
for label, value := range map[string]string{
"product_id": s.ProductID,
"source_tool_id": s.SourceToolID,
@@ -386,7 +462,7 @@ func (m CommandMigration) key() string {
}
func (m CommandMigration) displayKey() string {
if m.Kind == CommandMigrationAvailability {
if m.Kind == CommandMigrationAvailability || m.Kind == CommandMigrationProductRetirement {
return fmt.Sprintf("%s %q", m.Kind, m.Legacy.Command)
}
return fmt.Sprintf("%s %q -> %q", m.Kind, m.Legacy.Command, m.Replacement.Command)
@@ -481,6 +557,11 @@ func evaluateCommandMigrationLifecycle(
candidateByKey := commandMigrationIndex(candidate)
authorizations := make([]CommandMigration, 0, len(authority.Migrations))
for _, approved := range authority.Migrations {
if approved.Kind == CommandMigrationProductRetirement && approved.State == CommandMigrationPending {
if err := validateProductRetirementCommandScope(references, approved); err != nil {
return nil, fmt.Errorf("approved command migration %s: %w", approved.displayKey(), err)
}
}
basePhase := matchCommandMigrationPhase(mergeBase, approved)
wantBase := commandMigrationBefore
if approved.State == CommandMigrationConsumed && !isCompatibilityVisibleAvailabilityMigration(approved) {
@@ -495,6 +576,10 @@ func evaluateCommandMigrationLifecycle(
return nil, fmt.Errorf("candidate modified base-owned command migration %s", approved.displayKey())
}
currentPhase := matchCommandMigrationPhase(current, approved)
if approved.Kind == CommandMigrationProductRetirement && currentPhase == commandMigrationBefore && exists &&
!reflect.DeepEqual(productRetirementCommands(current, approved), productRetirementCommands(mergeBase, approved)) {
return nil, fmt.Errorf("candidate changed pending product retirement surface %s", approved.displayKey())
}
if isCompatibilityVisibleAvailabilityMigration(approved) {
if currentPhase != commandMigrationBefore {
return nil, fmt.Errorf("candidate drifted from compatibility-visible command migration %s", approved.displayKey())
@@ -567,12 +652,21 @@ func evaluateCommandMigrationLifecycle(
if proposed.State != CommandMigrationPending {
return nil, fmt.Errorf("candidate-added command migration %s must start pending", proposed.displayKey())
}
if proposed.Kind == CommandMigrationProductRetirement {
if err := validateProductRetirementCommandScope(references, proposed); err != nil {
return nil, fmt.Errorf("candidate-added command migration %s: %w", proposed.displayKey(), err)
}
}
if matchCommandMigrationPhase(mergeBase, proposed) != commandMigrationBefore {
return nil, fmt.Errorf("candidate-added command migration %s does not match the merge-base before state", proposed.displayKey())
}
if matchCommandMigrationPhase(current, proposed) != commandMigrationBefore {
return nil, fmt.Errorf("candidate-added command migration %s cannot authorize its own interface change", proposed.displayKey())
}
if proposed.Kind == CommandMigrationProductRetirement &&
!reflect.DeepEqual(productRetirementCommands(current, proposed), productRetirementCommands(mergeBase, proposed)) {
return nil, fmt.Errorf("candidate-added command migration %s changed the product surface in its approval PR", proposed.displayKey())
}
}
return authorizations, nil
}
@@ -614,6 +708,9 @@ func isPendingAvailabilityCompatibilityRefinement(approved, proposed CommandMigr
}
func matchCommandMigrationPhase(snapshot Snapshot, migration CommandMigration) commandMigrationPhase {
if migration.Kind == CommandMigrationProductRetirement {
return matchProductRetirementPhase(snapshot, migration)
}
commands := commandIndex(snapshot)
legacy := commandMigrationStateForCommand(commands, migration.Legacy.Command)
if migration.Kind == CommandMigrationAvailability {
@@ -651,6 +748,55 @@ func matchCommandMigrationPhase(snapshot Snapshot, migration CommandMigration) c
return commandMigrationPartial
}
func matchProductRetirementPhase(snapshot Snapshot, migration CommandMigration) commandMigrationPhase {
commands := productRetirementCommands(snapshot, migration)
if len(commands) == 0 {
return commandMigrationAfter
}
root, exists := commandIndex(snapshot)[migration.Legacy.Command]
if !exists || commandMigrationStateForCommand(map[string]Command{root.Path: root}, root.Path) != migration.Legacy.Before {
return commandMigrationPartial
}
approved := make(map[string]bool, len(migration.Commands))
for _, path := range migration.Commands {
approved[path] = true
}
for _, command := range commands {
if !approved[command.Path] {
return commandMigrationPartial
}
}
return commandMigrationBefore
}
func productRetirementCommands(snapshot Snapshot, migration CommandMigration) []Command {
commands := make([]Command, 0, len(migration.Commands))
for _, command := range snapshot.Commands {
if command.Path == migration.Legacy.Command || strings.HasPrefix(command.Path, migration.Legacy.Command+" ") {
commands = append(commands, command)
}
}
sort.Slice(commands, func(i, j int) bool { return commands[i].Path < commands[j].Path })
return commands
}
func validateProductRetirementCommandScope(references map[string]Snapshot, migration CommandMigration) error {
union := make(map[string]bool, len(migration.Commands))
for label, snapshot := range references {
phase := matchProductRetirementPhase(snapshot, migration)
if phase == commandMigrationPartial {
return fmt.Errorf("%s product command surface is outside the reviewed commands", label)
}
for _, command := range productRetirementCommands(snapshot, migration) {
union[command.Path] = true
}
}
if len(union) != len(migration.Commands) {
return fmt.Errorf("reviewed commands do not exactly match the historical product command union")
}
return nil
}
func commandMigrationReplacementConstantsMatch(command Command, migration CommandMigration) bool {
expected := make(map[string]bool)
for _, parameter := range migration.Schema.Parameters {
@@ -738,6 +884,11 @@ func commandMigrationAuthorizesChange(current, reference Snapshot, change Change
if change.Kind == "flag_became_hidden" && change.Flag == migration.LegacyFlag.Name {
return true
}
case CommandMigrationProductRetirement:
if (change.Kind == "command_removed" || change.Kind == "command_alias_removed") &&
change.Flag == "" && canonicalPath == migration.Legacy.Command {
return true
}
}
}
return false
@@ -651,6 +651,239 @@ func TestCrossPlatformCoverageCommandMigrationLifecycleAndExactFiltering(t *test
}
}
func TestCrossPlatformCoverageProductRetirementValidation(t *testing.T) {
valid := productRetirementCommandMigration(CommandMigrationPending)
if err := valid.validate(); err != nil {
t.Fatalf("valid product retirement: %v", err)
}
for _, test := range []struct {
name string
mutate func(*CommandMigration)
want string
}{
{
name: "root must be below dws",
mutate: func(m *CommandMigration) { m.Legacy.Command = "dws" },
want: "exact product root below dws",
},
{
name: "root must be one product segment",
mutate: func(m *CommandMigration) { m.Legacy.Command = "dws edu-app task" },
want: "exactly match schema product_id",
},
{
name: "root must match product id",
mutate: func(m *CommandMigration) { m.Schema.ProductID = "college-contact" },
want: "exactly match schema product_id",
},
{
name: "replacement forbidden",
mutate: func(m *CommandMigration) { m.Replacement.Command = "dws replacement" },
want: "must not declare replacement",
},
{
name: "legacy flag forbidden",
mutate: func(m *CommandMigration) { m.LegacyFlag.Name = "legacy" },
want: "must not declare legacy_flag",
},
{
name: "root must be runnable",
mutate: func(m *CommandMigration) { m.Legacy.Before.Runnable = false },
want: "declared runnable visibility to absent",
},
{
name: "after must be absent",
mutate: func(m *CommandMigration) { m.Legacy.After.Present = true },
want: "declared runnable visibility to absent",
},
{
name: "commands required",
mutate: func(m *CommandMigration) { m.Commands = nil },
want: "commands must be a non-empty array",
},
{
name: "root command required",
mutate: func(m *CommandMigration) { m.Commands = m.Commands[1:] },
want: "must include root",
},
{
name: "command outside product",
mutate: func(m *CommandMigration) { m.Commands[1] = "dws edu-group task" },
want: "exact path in root",
},
{
name: "commands sorted",
mutate: func(m *CommandMigration) {
m.Commands[1], m.Commands[2] = m.Commands[2], m.Commands[1]
},
want: "sorted and unique",
},
{
name: "schema tools required",
mutate: func(m *CommandMigration) { m.Schema.Tools = nil },
want: "tools must be a non-empty array",
},
{
name: "schema tool outside product",
mutate: func(m *CommandMigration) { m.Schema.Tools[0] = "edu-group.query_task" },
want: "canonical path in product",
},
{
name: "schema tool requires leaf",
mutate: func(m *CommandMigration) { m.Schema.Tools[0] = "edu-app." },
want: "canonical path in product",
},
{
name: "schema tools sorted",
mutate: func(m *CommandMigration) {
m.Schema.Tools[0], m.Schema.Tools[1] = m.Schema.Tools[1], m.Schema.Tools[0]
},
want: "sorted and unique",
},
{
name: "source tool forbidden",
mutate: func(m *CommandMigration) { m.Schema.SourceToolID = "edu-app.old" },
want: "only product_id and tools",
},
{
name: "parameter migration forbidden",
mutate: func(m *CommandMigration) {
m.Schema.Parameters = []CommandParameterMigration{}
},
want: "only product_id and tools",
},
} {
t.Run(test.name, func(t *testing.T) {
migration := cloneCommandMigration(valid)
test.mutate(&migration)
if err := migration.validate(); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("validate error=%v, want %q", err, test.want)
}
})
}
move := commandMigrationManifest(CommandMigrationPending).Migrations[0]
move.Commands = []string{"dws chat message old"}
if err := move.validate(); err == nil || !strings.Contains(err.Error(), "must not declare commands") {
t.Fatalf("non-retirement commands error=%v", err)
}
move = commandMigrationManifest(CommandMigrationPending).Migrations[0]
move.Schema.Tools = []string{"chat.old"}
if err := move.validate(); err == nil || !strings.Contains(err.Error(), "must not declare schema tools") {
t.Fatalf("non-retirement tools error=%v", err)
}
invalidSchema := valid.Schema
invalidSchema.ProductID = "bad/id"
if err := invalidSchema.validate(CommandMigrationProductRetirement); err == nil ||
!strings.Contains(err.Error(), "product_id must be an exact identifier") {
t.Fatalf("invalid retirement Schema product error=%v", err)
}
}
func TestCrossPlatformCoverageProductRetirementLifecycleAndExactScope(t *testing.T) {
base := productRetirementSnapshot("full")
stable := productRetirementSnapshot("stable-subset")
after := productRetirementSnapshot("absent")
pending := productRetirementManifest(CommandMigrationPending)
consumed := productRetirementManifest(CommandMigrationConsumed)
empty := CommandMigrationManifest{Version: CommandMigrationManifestVersion, Migrations: []CommandMigration{}}
emptyFlags := FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}}
references := map[string]Snapshot{"merge-base": base, "stable": stable}
if got, err := AuthorizeCommandMigrations(base, references, empty, pending); err != nil || len(got) != 0 {
t.Fatalf("candidate pending retirement plan=%#v, %v", got, err)
}
if _, err := AuthorizeCommandMigrations(after, references, empty, pending); err == nil ||
!strings.Contains(err.Error(), "cannot authorize its own interface change") {
t.Fatalf("candidate retirement self-approval error=%v", err)
}
authorizations, err := AuthorizeCommandMigrations(after, references, pending, consumed)
if err != nil || len(authorizations) != 1 {
t.Fatalf("approved product retirement authorizations=%#v, %v", authorizations, err)
}
ordinary := CompareAll(after, references)
var sawRootRemoval, sawRootAliasRemoval bool
for _, comparison := range ordinary.Comparisons {
if comparison.Reference != "merge-base" {
continue
}
for _, change := range comparison.Blocking {
switch {
case change.Kind == "command_removed" && change.Path == "dws edu-app":
sawRootRemoval = true
case change.Kind == "command_alias_removed" && change.Path == "dws education-app":
sawRootAliasRemoval = true
case strings.HasPrefix(change.Path, "dws edu-app ") || strings.HasPrefix(change.Path, "dws education-app "):
t.Fatalf("ordinary comparison did not collapse nested product removal to its roots: %#v", comparison.Blocking)
}
}
}
if !sawRootRemoval || !sawRootAliasRemoval {
t.Fatalf("ordinary comparison findings root=%v root-alias=%v: %#v", sawRootRemoval, sawRootAliasRemoval, ordinary.Comparisons)
}
report, err := CompareAllWithInterfaceMigrations(
after,
references,
emptyFlags,
emptyFlags,
pending,
consumed,
)
if err != nil {
t.Fatalf("governed product retirement: %v", err)
}
if !report.Compatible {
t.Fatalf("whole product retirement remained blocking: %#v", report.Comparisons)
}
partial := productRetirementSnapshot("partial")
if _, err := AuthorizeCommandMigrations(partial, references, pending, consumed); err == nil ||
!strings.Contains(err.Error(), "changed pending product retirement surface") {
t.Fatalf("partial product retirement error=%v", err)
}
unlisted := productRetirementSnapshot("unlisted-child")
if _, err := AuthorizeCommandMigrations(after, map[string]Snapshot{"merge-base": unlisted, "stable": stable}, pending, consumed); err == nil ||
!strings.Contains(err.Error(), "outside the reviewed commands") {
t.Fatalf("unlisted historical command error=%v", err)
}
tooBroad := productRetirementManifest(CommandMigrationPending)
tooBroad.Migrations[0].Commands = append(tooBroad.Migrations[0].Commands, "dws edu-app task missing")
if _, err := AuthorizeCommandMigrations(base, references, empty, tooBroad); err == nil ||
!strings.Contains(err.Error(), "do not exactly match") {
t.Fatalf("absent reviewed command error=%v", err)
}
mutatedCurrent := productRetirementSnapshot("mutated")
if _, err := AuthorizeCommandMigrations(mutatedCurrent, references, empty, pending); err == nil ||
!strings.Contains(err.Error(), "changed the product surface in its approval PR") {
t.Fatalf("candidate pending surface mutation error=%v", err)
}
migration := pending.Migrations[0]
missingRoot := testSnapshot(
testCommand("dws"),
testCommand("dws edu-app task"),
testCommand("dws edu-app task list"),
)
if phase := matchProductRetirementPhase(missingRoot, migration); phase != commandMigrationPartial {
t.Fatalf("missing retirement root phase=%s, want %s", phase, commandMigrationPartial)
}
wrongRootState := productRetirementSnapshot("full")
for index := range wrongRootState.Commands {
if wrongRootState.Commands[index].Path == migration.Legacy.Command {
wrongRootState.Commands[index].Hidden = false
}
}
if phase := matchProductRetirementPhase(wrongRootState, migration); phase != commandMigrationPartial {
t.Fatalf("wrong retirement root state phase=%s, want %s", phase, commandMigrationPartial)
}
}
func TestCrossPlatformCoverageFlagExtractionRequiresReplacementConstantEvidence(t *testing.T) {
before := commandMigrationSnapshot(false, false)
after := commandMigrationSnapshot(true, false)
@@ -834,6 +1067,8 @@ func flagExtractionCommandMigrationManifest(state string) CommandMigrationManife
func cloneCommandMigration(source CommandMigration) CommandMigration {
cloned := source
cloned.Commands = append([]string(nil), source.Commands...)
cloned.Schema.Tools = append([]string(nil), source.Schema.Tools...)
if source.Schema.Parameters != nil {
cloned.Schema.Parameters = make([]CommandParameterMigration, len(source.Schema.Parameters))
copy(cloned.Schema.Parameters, source.Schema.Parameters)
@@ -848,6 +1083,65 @@ func cloneCommandMigration(source CommandMigration) CommandMigration {
return cloned
}
func productRetirementManifest(state string) CommandMigrationManifest {
return CommandMigrationManifest{
Version: CommandMigrationManifestVersion,
Migrations: []CommandMigration{productRetirementCommandMigration(state)},
}
}
func productRetirementCommandMigration(state string) CommandMigration {
return CommandMigration{
Kind: CommandMigrationProductRetirement,
Legacy: CommandMigrationSide{
Command: "dws edu-app",
Before: CommandMigrationState{Present: true, Runnable: true, Hidden: true},
},
Commands: []string{
"dws edu-app",
"dws edu-app task",
"dws edu-app task list",
},
Schema: CommandMigrationSchema{
ProductID: "edu-app",
Tools: []string{
"edu-app.query_all_task",
"edu-app.query_publish_task",
},
},
State: state,
Reason: "Retire the reviewed education application product surface.",
}
}
func productRetirementSnapshot(variant string) Snapshot {
root := testCommandWithAliases("dws edu-app", []string{"education-app"})
root.Hidden = true
commands := []Command{
testCommand("dws"),
root,
testCommandWithAliases("dws edu-app task", []string{"job"}),
testCommandWithAliases("dws edu-app task list", []string{"ls"}, Flag{Name: "status", Type: "string"}),
testCommand("dws other list"),
}
switch variant {
case "full":
case "stable-subset":
commands = append(commands[:3], commands[4:]...)
case "absent":
commands = []Command{commands[0], commands[4]}
case "partial":
commands = []Command{commands[0], root, commands[4]}
case "unlisted-child":
commands = append(commands, testCommand("dws edu-app task secret"))
case "mutated":
commands[3].LocalFlags[0].Required = true
default:
panic("unknown product retirement snapshot variant: " + variant)
}
return testSnapshot(commands...)
}
func singleCommandMigrationManifest(state string) CommandMigrationManifest {
manifest := commandMigrationManifest(state)
manifest.Migrations = manifest.Migrations[:1]
@@ -207,6 +207,17 @@ has_any_schema_command_migration_governance_artifact() {
return 1
}
base_supports_product_retirement() {
grep -Fq \
'productRetirementMigrationCapability = "dws.command-migration.product-retirement.v1"' \
"$BASE_WORKTREE/$COMMAND_MIGRATIONS_REL"
}
candidate_declares_product_retirement() {
grep -Fq '"product_retirement"' \
"$CANDIDATE_WORKTREE/$COMMAND_MIGRATION_MANIFEST_REL"
}
check_candidate_const_params_source_policy() {
source_policy_failed=false
for token in attachInterfaceBoolConstParams InterfaceBoolConstParams interfaceBoolConstParamsRegistry; do
@@ -363,6 +374,11 @@ if has_complete_schema_command_migration_governance "$BASE_COMMIT"; then
APPROVED_COMMAND_MANIFEST="$BASE_WORKTREE/$COMMAND_MIGRATION_MANIFEST_REL"
CANDIDATE_COMMAND_MANIFEST="$CANDIDATE_WORKTREE/$COMMAND_MIGRATION_MANIFEST_REL"
require_complete_candidate_schema_command_governance
if candidate_declares_product_retirement && ! base_supports_product_retirement; then
printf '%s\n' \
'merge-base command migration authority does not support product_retirement; land capability, pending approval, and consumption in three separate PRs' >&2
exit 2
fi
require_base_identical_command_migration_bridges
elif has_any_schema_command_migration_governance_artifact "$BASE_COMMIT"; then
printf 'error: merge-base contains an incomplete Schema command migration governance artifact set: %s\n' \
@@ -407,6 +423,10 @@ CHECKER_SUPPORTS_MIGRATION_BASE_SCHEMA=false
if "$CHECKER" --help 2>&1 | grep -Fq -- 'migration-base-schema'; then
CHECKER_SUPPORTS_MIGRATION_BASE_SCHEMA=true
fi
CHECKER_SUPPORTS_MIGRATION_STABLE_SCHEMA=false
if "$CHECKER" --help 2>&1 | grep -Fq -- 'migration-stable-schema'; then
CHECKER_SUPPORTS_MIGRATION_STABLE_SCHEMA=true
fi
mkdir -p "$TMP_ROOT/base-home" "$TMP_ROOT/stable-home" "$TMP_ROOT/candidate-home"
HOME="$TMP_ROOT/base-home" DWS_LANG=zh \
@@ -488,6 +508,10 @@ check_with_migrations() {
[ "$CHECKER_SUPPORTS_MIGRATION_BASE_SCHEMA" = true ]; then
set -- "$@" --migration-base-schema "$BASELINE"
fi
if [ "$USE_COMMAND_MIGRATION_GOVERNANCE" = true ] &&
[ "$CHECKER_SUPPORTS_MIGRATION_STABLE_SCHEMA" = true ]; then
set -- "$@" --migration-stable-schema "$STABLE_BASELINE"
fi
check_schema_contract "$historical_kind" "$historical_ref" "$historical_baseline" "$@"
}
@@ -200,6 +200,17 @@ has_any_command_migration_governance_artifact() {
return 1
}
base_supports_product_retirement() {
grep -Fq \
'productRetirementMigrationCapability = "dws.command-migration.product-retirement.v1"' \
"$BASE_WORKTREE/$COMMAND_MIGRATIONS_REL"
}
candidate_declares_product_retirement() {
grep -Fq '"product_retirement"' \
"$CANDIDATE_WORKTREE/$COMMAND_MIGRATION_MANIFEST_REL"
}
check_candidate_const_params_source_policy() {
source_policy_failed=false
for token in attachInterfaceBoolConstParams InterfaceBoolConstParams interfaceBoolConstParamsRegistry; do
@@ -336,6 +347,11 @@ if has_complete_command_migration_governance "$BASE_COMMIT"; then
APPROVED_COMMAND_MANIFEST="$BASE_WORKTREE/$COMMAND_MIGRATION_MANIFEST_REL"
CANDIDATE_COMMAND_MANIFEST="$CANDIDATE_WORKTREE/$COMMAND_MIGRATION_MANIFEST_REL"
require_complete_candidate_command_governance
if candidate_declares_product_retirement && ! base_supports_product_retirement; then
printf '%s\n' \
'merge-base command migration authority does not support product_retirement; land capability, pending approval, and consumption in three separate PRs' >&2
exit 2
fi
require_base_identical_command_migration_bridges
elif has_any_command_migration_governance_artifact "$BASE_COMMIT"; then
printf 'merge-base contains an incomplete command migration governance artifact set: %s\n' \
@@ -20,6 +20,7 @@ import (
"flag"
"fmt"
"os"
"strings"
"example.com/dws-command-compat-authority-test/internal/interfacesnapshot"
)
@@ -52,7 +53,7 @@ func main() {
stable := flags.String("stable", "", "stable snapshot")
_ = flags.String("approved-flag-migrations", "", "approved manifest")
_ = flags.String("approved-command-migrations", "", "approved command manifest")
_ = flags.String("candidate-command-migrations", "", "candidate command manifest")
candidateCommandManifest := flags.String("candidate-command-migrations", "", "candidate command manifest")
_ = flags.Parse(os.Args[2:])
for _, path := range []string{*current, *base, *stable} {
content, err := os.ReadFile(path)
@@ -77,6 +78,17 @@ func main() {
return
}
}
if *candidateCommandManifest != "" {
content, err := os.ReadFile(*candidateCommandManifest)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if strings.Contains(string(content), "product_retirement") {
fmt.Fprintln(os.Stderr, "LEGACY_COMMAND_PARSER_REACHED_PRODUCT_RETIREMENT")
os.Exit(29)
}
}
fmt.Fprintln(os.Stderr, "BASE_COMPARATOR_ENFORCED")
os.Exit(23)
default:
@@ -202,13 +214,15 @@ func main() {
`
type authorityScenario struct {
name string
baseGovernance string
commandGovernance string
commandManifestChanged bool
candidateMutation string
want string
wantAuthorityDecision bool
name string
baseGovernance string
commandGovernance string
commandManifestChanged bool
productRetirementCapability bool
productRetirementCandidate bool
candidateMutation string
want string
wantAuthorityDecision bool
}
func TestCrossPlatformCoverageCommandCompatibilityUsesBaseOwnedAuthority(t *testing.T) {
@@ -295,6 +309,21 @@ func TestCrossPlatformCoverageCommandCompatibilityUsesBaseOwnedAuthority(t *test
commandManifestChanged: true,
wantAuthorityDecision: true,
},
{
name: "product retirement mechanism can land with an empty ledger",
baseGovernance: "complete",
commandGovernance: "complete",
productRetirementCapability: true,
wantAuthorityDecision: true,
},
{
name: "first product retirement kind requires three separate PRs",
baseGovernance: "complete",
commandGovernance: "complete",
productRetirementCapability: true,
productRetirementCandidate: true,
want: "land capability, pending approval, and consumption in three separate PRs",
},
{
name: "unchanged command manifest allows independent corecmd change",
baseGovernance: "complete",
@@ -468,7 +497,11 @@ func runBaseOwnedAuthorityCase(t *testing.T, test authorityScenario) {
)
}
if test.commandGovernance == "complete" || test.commandGovernance == "bootstrap" {
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "command_migrations.go"), "package interfacesnapshot\n", 0o644)
commandMigrationsSource := "package interfacesnapshot\n"
if test.productRetirementCapability {
commandMigrationsSource += "\nconst productRetirementMigrationCapability = \"dws.command-migration.product-retirement.v1\"\n"
}
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "command_migrations.go"), commandMigrationsSource, 0o644)
if test.commandGovernance == "bootstrap" {
writeFile(t, filepath.Join(fixtureRoot, "internal", "corecmd", "interface_const_params.go"), constParamsRegistrySource("CANDIDATE"), 0o644)
}
@@ -476,6 +509,9 @@ func runBaseOwnedAuthorityCase(t *testing.T, test authorityScenario) {
if test.commandManifestChanged {
commandManifest = "{\"version\":1,\"migrations\":[{\"candidate\":\"PENDING\"}]}\n"
}
if test.productRetirementCandidate {
commandManifest = "{\"version\":1,\"migrations\":[{\"kind\":\"product_retirement\"}]}\n"
}
writeFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-command-migrations-v1.json"), commandManifest, 0o644)
}
switch test.candidateMutation {
@@ -560,6 +596,9 @@ func runBaseOwnedAuthorityCase(t *testing.T, test authorityScenario) {
if strings.Contains(got, "UNCOMMITTED_MANIFEST_USED") {
t.Fatalf("compatibility script mixed the live manifest with the committed candidate surface; output:\n%s", got)
}
if strings.Contains(got, "LEGACY_COMMAND_PARSER_REACHED_PRODUCT_RETIREMENT") {
t.Fatalf("legacy command parser received a first-use product retirement record; output:\n%s", got)
}
if test.wantAuthorityDecision {
if count := strings.Count(got, "BASE_HELPER_GENERATE=BASE"); count != 3 {
t.Fatalf("compatibility script generated %d/3 snapshots with the base-owned internal helper; output:\n%s", count, got)
@@ -5,8 +5,10 @@ package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
@@ -384,6 +386,262 @@ func TestCrossPlatformCoverageSchemaCommandMigrationNormalizationEdges(t *testin
}
}
func TestCrossPlatformCoverageSchemaProductRetirementExactDualBaselineScope(t *testing.T) {
base := schemaProductRetirementContract(
"edu-app.query_all_task",
"edu-app.query_publish_task",
)
stable := schemaProductRetirementContract("edu-app.query_all_task")
current := cloneContract(base)
delete(current.Products, "edu-app")
migration := schemaProductRetirementMigration(interfacesnapshot.CommandMigrationPending)
authority := interfacesnapshot.CommandMigrationManifest{
Version: interfacesnapshot.CommandMigrationManifestVersion,
Migrations: []interfacesnapshot.CommandMigration{migration},
}
candidate := authority
candidate.Migrations = append([]interfacesnapshot.CommandMigration(nil), authority.Migrations...)
candidate.Migrations[0].State = interfacesnapshot.CommandMigrationConsumed
if err := validateSchemaProductRetirementScopes(base, stable, current, authority, candidate); err != nil {
t.Fatalf("exact base/stable product retirement scope: %v", err)
}
normalized, err := normalizeSchemaCommandMigrations(base, current, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatalf("normalize base product retirement: %v", err)
}
if _, exists := normalized.Products["edu-app"]; exists {
t.Fatal("normalized base retained retired product")
}
if !reflect.DeepEqual(normalized.Products["doc"], base.Products["doc"]) {
t.Fatal("product retirement changed an unrelated Schema product")
}
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
t.Fatalf("retired product remained incompatible: %v", failures)
}
stableAbsent := schemaProductRetirementContract()
if err := validateSchemaProductRetirementScopes(base, stableAbsent, current, authority, candidate); err != nil {
t.Fatalf("stable baseline without product should be allowed: %v", err)
}
unlisted := cloneContract(stable)
product := unlisted.Products["edu-app"]
product.Tools["edu-app.unreviewed"] = product.Tools["edu-app.query_all_task"]
unlisted.Products["edu-app"] = product
if err := validateSchemaProductRetirementScopes(base, unlisted, current, authority, candidate); err == nil ||
!strings.Contains(err.Error(), "unreviewed tool") {
t.Fatalf("unlisted stable tool error=%v", err)
}
tooBroad := migration
tooBroad.Schema.Tools = append(append([]string(nil), migration.Schema.Tools...), "edu-app.unpublished")
tooBroadAuthority := authority
tooBroadAuthority.Migrations = []interfacesnapshot.CommandMigration{tooBroad}
if err := validateSchemaProductRetirementScopes(base, stable, current, tooBroadAuthority, candidate); err == nil ||
!strings.Contains(err.Error(), "do not exactly match") {
t.Fatalf("absent reviewed Schema tool error=%v", err)
}
pendingCurrent := cloneContract(base)
product = pendingCurrent.Products["edu-app"]
tool := product.Tools["edu-app.query_all_task"]
tool.Risk = "high"
product.Tools["edu-app.query_all_task"] = tool
pendingCurrent.Products["edu-app"] = product
emptyAuthority := interfacesnapshot.CommandMigrationManifest{
Version: interfacesnapshot.CommandMigrationManifestVersion,
Migrations: []interfacesnapshot.CommandMigration{},
}
if err := validateSchemaProductRetirementScopes(base, stable, pendingCurrent, emptyAuthority, authority); err == nil ||
!strings.Contains(err.Error(), "changed the Schema product in its approval PR") {
t.Fatalf("candidate Schema self-approval error=%v", err)
}
if _, err := normalizeSchemaCommandMigrations(base, base, []interfacesnapshot.CommandMigration{migration}); err == nil ||
!strings.Contains(err.Error(), "still publishes the product") {
t.Fatalf("still-published product normalization error=%v", err)
}
}
func TestCrossPlatformCoverageSchemaProductRetirementScopeEdges(t *testing.T) {
base := schemaProductRetirementContract(
"edu-app.query_all_task",
"edu-app.query_publish_task",
)
stable := schemaProductRetirementContract("edu-app.query_all_task")
absent := schemaProductRetirementContract()
pending := schemaProductRetirementMigration(interfacesnapshot.CommandMigrationPending)
consumed := schemaProductRetirementMigration(interfacesnapshot.CommandMigrationConsumed)
nonRetirement := interfacesnapshot.CommandMigration{Kind: interfacesnapshot.CommandMigrationMove}
empty := interfacesnapshot.CommandMigrationManifest{
Version: interfacesnapshot.CommandMigrationManifestVersion,
Migrations: []interfacesnapshot.CommandMigration{},
}
if err := validateSchemaProductRetirementScopes(
base,
stable,
absent,
interfacesnapshot.CommandMigrationManifest{Version: interfacesnapshot.CommandMigrationManifestVersion, Migrations: []interfacesnapshot.CommandMigration{nonRetirement}},
interfacesnapshot.CommandMigrationManifest{Version: interfacesnapshot.CommandMigrationManifestVersion, Migrations: []interfacesnapshot.CommandMigration{nonRetirement}},
); err != nil {
t.Fatalf("non-retirement migrations must be ignored: %v", err)
}
consumedAuthority := interfacesnapshot.CommandMigrationManifest{
Version: interfacesnapshot.CommandMigrationManifestVersion,
Migrations: []interfacesnapshot.CommandMigration{consumed},
}
if err := validateSchemaProductRetirementScopes(base, stable, absent, consumedAuthority, empty); err != nil {
t.Fatalf("consumed retirement with reviewed historical subsets: %v", err)
}
if err := validateSchemaProductRetirementScopes(base, absent, absent, consumedAuthority, empty); err != nil {
t.Fatalf("consumed retirement with absent stable product: %v", err)
}
unreviewed := cloneContract(stable)
product := unreviewed.Products["edu-app"]
product.Tools["edu-app.unreviewed"] = product.Tools["edu-app.query_all_task"]
unreviewed.Products["edu-app"] = product
if err := validateSchemaProductRetirementScopes(base, unreviewed, absent, consumedAuthority, empty); err == nil ||
!strings.Contains(err.Error(), "contains unreviewed tool") {
t.Fatalf("consumed retirement unreviewed historical tool error=%v", err)
}
if err := validateSchemaProductRetirementScopes(base, stable, base, consumedAuthority, empty); err == nil ||
!strings.Contains(err.Error(), "current Schema still publishes the product") {
t.Fatalf("consumed authority still-published product error=%v", err)
}
tooBroad := pending
tooBroad.Schema.Tools = append(append([]string(nil), pending.Schema.Tools...), "edu-app.unpublished")
tooBroadCandidate := interfacesnapshot.CommandMigrationManifest{
Version: interfacesnapshot.CommandMigrationManifestVersion,
Migrations: []interfacesnapshot.CommandMigration{tooBroad},
}
if err := validateSchemaProductRetirementScopes(base, stable, base, empty, tooBroadCandidate); err == nil ||
!strings.Contains(err.Error(), "pending product retirement") {
t.Fatalf("pending retirement invalid tool union error=%v", err)
}
consumedCandidate := interfacesnapshot.CommandMigrationManifest{
Version: interfacesnapshot.CommandMigrationManifestVersion,
Migrations: []interfacesnapshot.CommandMigration{consumed},
}
if err := validateSchemaProductRetirementScopes(base, stable, base, empty, consumedCandidate); err == nil ||
!strings.Contains(err.Error(), "current Schema still publishes the product") {
t.Fatalf("consumed candidate still-published product error=%v", err)
}
if normalized, err := normalizeSchemaCommandMigrations(absent, absent, []interfacesnapshot.CommandMigration{consumed}); err != nil ||
!reflect.DeepEqual(normalized, absent) {
t.Fatalf("retirement absent from historical baseline normalized=%#v error=%v", normalized, err)
}
narrow := consumed
narrow.Schema.Tools = []string{"edu-app.query_all_task"}
if _, err := normalizeSchemaCommandMigrations(base, absent, []interfacesnapshot.CommandMigration{narrow}); err == nil ||
!strings.Contains(err.Error(), "historical Schema contains unreviewed tool") {
t.Fatalf("normalization unreviewed historical tool error=%v", err)
}
}
func TestCrossPlatformCoverageSchemaProductRetirementRunRequiresBothHistoricalContracts(t *testing.T) {
directory := t.TempDir()
baselinePath := filepath.Join(directory, "stable-schema.json")
baseSchemaPath := filepath.Join(directory, "base-schema.json")
stableSchemaPath := filepath.Join(directory, "stable-lineage-schema.json")
currentPath := filepath.Join(directory, "current-schema.json")
approvedPath := filepath.Join(directory, "approved-commands.json")
candidatePath := filepath.Join(directory, "candidate-commands.json")
currentSnapshotPath := filepath.Join(directory, "current-snapshot.json")
baseSnapshotPath := filepath.Join(directory, "base-snapshot.json")
stableSnapshotPath := filepath.Join(directory, "stable-snapshot.json")
base := schemaProductRetirementContract(
"edu-app.query_all_task",
"edu-app.query_publish_task",
)
stable := schemaProductRetirementContract("edu-app.query_all_task")
current := cloneContract(base)
delete(current.Products, "edu-app")
writeSchemaContractFile(t, baselinePath, stable)
writeSchemaContractFile(t, baseSchemaPath, base)
writeSchemaContractFile(t, stableSchemaPath, stable)
writeRawSchemaContractFile(t, currentPath, current)
writeSchemaProductRetirementManifestFile(t, approvedPath, interfacesnapshot.CommandMigrationPending)
writeSchemaProductRetirementManifestFile(t, candidatePath, interfacesnapshot.CommandMigrationConsumed)
writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaProductRetirementSnapshot("absent"))
writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaProductRetirementSnapshot("full"))
writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaProductRetirementSnapshot("stable-subset"))
args := []string{
"--check", baselinePath,
"--current", currentPath,
"--migration-base-schema", baseSchemaPath,
"--migration-stable-schema", stableSchemaPath,
"--approved-command-migrations", approvedPath,
"--candidate-command-migrations", candidatePath,
"--migration-current-snapshot", currentSnapshotPath,
"--migration-base-snapshot", baseSnapshotPath,
"--migration-stable-snapshot", stableSnapshotPath,
}
var stdout, stderr strings.Builder
if code := run(args, &stdout, &stderr); code != 0 {
t.Fatalf("product retirement run code=%d stderr=%s", code, stderr.String())
}
withoutBaseSchema := append([]string(nil), args[:4]...)
withoutBaseSchema = append(withoutBaseSchema, args[6:]...)
stderr.Reset()
if code := run(withoutBaseSchema, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "--migration-stable-schema requires") {
t.Fatalf("stable Schema without base code=%d stderr=%q", code, stderr.String())
}
stderr.Reset()
if code := run([]string{
"--check", baselinePath,
"--current", currentPath,
"--migration-stable-schema", stableSchemaPath,
}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "--migration-stable-schema requires") {
t.Fatalf("stable Schema without command manifests code=%d stderr=%q", code, stderr.String())
}
missingBaseArgs := append([]string(nil), args...)
missingBaseArgs[5] = filepath.Join(directory, "missing-base-schema.json")
stderr.Reset()
if code := run(missingBaseArgs, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "read product retirement merge-base Schema contract") {
t.Fatalf("missing retirement base Schema code=%d stderr=%q", code, stderr.String())
}
missingStableArgs := append([]string(nil), args...)
missingStableArgs[7] = filepath.Join(directory, "missing-stable-schema.json")
stderr.Reset()
if code := run(missingStableArgs, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "read product retirement stable Schema contract") {
t.Fatalf("missing retirement stable Schema code=%d stderr=%q", code, stderr.String())
}
stillPublishedPath := filepath.Join(directory, "still-published-current-schema.json")
writeRawSchemaContractFile(t, stillPublishedPath, base)
invalidScopeArgs := append([]string(nil), args...)
invalidScopeArgs[3] = stillPublishedPath
stderr.Reset()
if code := run(invalidScopeArgs, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "validate Schema product retirement scope") {
t.Fatalf("invalid retirement scope code=%d stderr=%q", code, stderr.String())
}
withoutStableSchema := append([]string(nil), args[:6]...)
withoutStableSchema = append(withoutStableSchema, args[8:]...)
stderr.Reset()
if code := run(withoutStableSchema, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "requires merge-base and stable Schema contracts") {
t.Fatalf("missing stable Schema lineage code=%d stderr=%q", code, stderr.String())
}
}
func TestCrossPlatformCoverageSchemaFlagExtractionRequiresCompleteReplacementProjection(t *testing.T) {
t.Run("missing historical mapping", func(t *testing.T) {
baseline := schemaCommandMigrationContract(false)
@@ -717,15 +975,38 @@ func TestCrossPlatformCoverageSchemaCommandMigrationLifecycleAndRun(t *testing.T
t.Fatalf("command migration run code=%d stderr=%s", code, stderr.String())
}
paths := []string{approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath}
approved, err := readCommandMigrationManifestFile(approvedPath)
if err != nil {
t.Fatal(err)
}
candidate, err := readCommandMigrationManifestFile(candidatePath)
if err != nil {
t.Fatal(err)
}
paths := []string{currentSnapshotPath, baseSnapshotPath, stableSnapshotPath}
for index := range paths {
invalid := append([]string(nil), paths...)
invalid[index] = filepath.Join(directory, "missing.json")
if _, err := authorizeSchemaCommandMigrations(invalid[0], invalid[1], invalid[2], invalid[3], invalid[4]); err == nil {
if _, err := authorizeSchemaCommandMigrations(approved, candidate, invalid[0], invalid[1], invalid[2]); err == nil {
t.Fatalf("missing command migration input %d was accepted", index)
}
}
missingApprovedArgs := append([]string(nil), args...)
missingApprovedArgs[5] = filepath.Join(directory, "missing-approved.json")
stderr.Reset()
if code := run(missingApprovedArgs, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "read approved Schema command migrations") {
t.Fatalf("missing approved command manifest code=%d stderr=%q", code, stderr.String())
}
missingCandidateArgs := append([]string(nil), args...)
missingCandidateArgs[7] = filepath.Join(directory, "missing-candidate.json")
stderr.Reset()
if code := run(missingCandidateArgs, &stdout, &stderr); code != 2 ||
!strings.Contains(stderr.String(), "read candidate Schema command migrations") {
t.Fatalf("missing candidate command manifest code=%d stderr=%q", code, stderr.String())
}
stderr.Reset()
if code := run([]string{"--check", baselinePath, "--current", currentPath, "--approved-command-migrations", approvedPath}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "both command manifests") {
t.Fatalf("partial command pair code=%d stderr=%q", code, stderr.String())
@@ -745,7 +1026,7 @@ func TestCrossPlatformCoverageSchemaCommandMigrationLifecycleAndRun(t *testing.T
}
badArgs := append([]string(nil), args...)
badArgs[5] = filepath.Join(directory, "missing-approved.json")
badArgs[9] = filepath.Join(directory, "missing-current-snapshot.json")
stderr.Reset()
if code := run(badArgs, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "authorize Schema command migrations") {
t.Fatalf("command authorization error code=%d stderr=%q", code, stderr.String())
@@ -831,7 +1112,7 @@ func TestCrossPlatformCoverageSchemaCommandMigrationComposesHistoricalFlagLineag
"--check", baselinePath,
"--current", currentPath,
"--migration-base-schema", baseSchemaPath,
}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "requires both flag and command") {
}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "requires the command migration manifest pair") {
t.Fatalf("orphan migration base Schema code=%d stderr=%q", code, stderr.String())
}
missingBaseSchema := append([]string(nil), args...)
@@ -1659,3 +1940,113 @@ func writeCommandMigrationManifestFile(t *testing.T, path string, manifest inter
t.Fatal(err)
}
}
func schemaProductRetirementMigration(state string) interfacesnapshot.CommandMigration {
return interfacesnapshot.CommandMigration{
Kind: interfacesnapshot.CommandMigrationProductRetirement,
Legacy: interfacesnapshot.CommandMigrationSide{
Command: "dws edu-app",
Before: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true, Hidden: true},
},
Commands: []string{
"dws edu-app",
"dws edu-app task",
"dws edu-app task list",
},
Schema: interfacesnapshot.CommandMigrationSchema{
ProductID: "edu-app",
Tools: []string{
"edu-app.query_all_task",
"edu-app.query_publish_task",
},
},
State: state,
Reason: "Retire the reviewed education application product surface.",
}
}
func schemaProductRetirementContract(toolIDs ...string) schemaContract {
contract := cloneContract(baselineContract())
if len(toolIDs) == 0 {
return contract
}
tools := make(map[string]toolSchema, len(toolIDs))
for _, toolID := range toolIDs {
tools[toolID] = toolSchema{
PrimaryCLIPath: "edu-app task list",
InterfaceMode: "mcp",
Availability: "available",
Parameters: map[string]parameterSchema{},
Effect: "read",
Risk: "low",
Confirmation: "not_required",
Idempotency: "idempotent",
}
}
contract.Products["edu-app"] = productSchema{Tools: tools}
return contract
}
func schemaProductRetirementSnapshot(variant string) interfacesnapshot.Snapshot {
command := func(path string) interfacesnapshot.Command {
return interfacesnapshot.Command{
Path: path,
Runnable: true,
Aliases: []string{},
LocalFlags: []interfacesnapshot.Flag{},
InheritedFlags: []interfacesnapshot.Flag{},
}
}
root := command("dws edu-app")
root.Hidden = true
commands := []interfacesnapshot.Command{
command("dws"),
root,
command("dws edu-app task"),
command("dws edu-app task list"),
command("dws other"),
}
switch variant {
case "full":
case "stable-subset":
commands = append(commands[:3], commands[4:]...)
case "absent":
commands = []interfacesnapshot.Command{commands[0], commands[4]}
default:
panic("unknown Schema product retirement snapshot variant: " + variant)
}
return interfacesnapshot.Snapshot{
SchemaVersion: interfacesnapshot.SchemaVersion,
Rules: interfacesnapshot.Rules{
ExcludedCommandSubtrees: []string{"dws __complete", "dws __completeNoDesc", "dws completion", "dws help"},
ExcludedFlags: []string{"help"},
},
Commands: commands,
}
}
func writeSchemaProductRetirementManifestFile(t *testing.T, path, state string) {
t.Helper()
body := fmt.Sprintf(`{
"version": 1,
"migrations": [{
"kind": "product_retirement",
"legacy": {
"command": "dws edu-app",
"before": {"present": true, "runnable": true, "hidden": true},
"after": {"present": false}
},
"commands": ["dws edu-app", "dws edu-app task", "dws edu-app task list"],
"schema": {
"product_id": "edu-app",
"tools": ["edu-app.query_all_task", "edu-app.query_publish_task"]
},
"state": %q,
"reason": "Retire the reviewed education application product surface."
}]
}
`, state)
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
+194 -15
View File
@@ -38,6 +38,11 @@ type schemaContract struct {
Products map[string]productSchema `json:"products"`
}
type schemaContractReference struct {
label string
contract schemaContract
}
type productSchema struct {
Tools map[string]toolSchema `json:"tools"`
}
@@ -119,7 +124,7 @@ func run(args []string, stdout, stderr io.Writer) int {
var normalizePath, checkPath, mergePath, currentPath string
var approvedFlagMigrationsPath, candidateFlagMigrationsPath string
var approvedCommandMigrationsPath, candidateCommandMigrationsPath string
var migrationBaseSchemaPath string
var migrationBaseSchemaPath, migrationStableSchemaPath string
var migrationCurrentSnapshotPath, migrationBaseSnapshotPath, migrationStableSnapshotPath string
flags := flag.NewFlagSet("schema-compat", flag.ContinueOnError)
flags.SetOutput(stderr)
@@ -132,6 +137,7 @@ func run(args []string, stdout, stderr io.Writer) int {
flags.StringVar(&approvedCommandMigrationsPath, "approved-command-migrations", "", "base-owned approved command migration manifest")
flags.StringVar(&candidateCommandMigrationsPath, "candidate-command-migrations", "", "detached candidate command migration manifest")
flags.StringVar(&migrationBaseSchemaPath, "migration-base-schema", "", "normalized merge-base Schema contract used to verify cross-migration lineage")
flags.StringVar(&migrationStableSchemaPath, "migration-stable-schema", "", "normalized stable Schema contract used to verify product-retirement scope")
flags.StringVar(&migrationCurrentSnapshotPath, "migration-current-snapshot", "", "current interface snapshot used for migration authorization")
flags.StringVar(&migrationBaseSnapshotPath, "migration-base-snapshot", "", "merge-base interface snapshot used for migration authorization")
flags.StringVar(&migrationStableSnapshotPath, "migration-stable-snapshot", "", "stable interface snapshot used for migration authorization")
@@ -187,8 +193,12 @@ func run(args []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, "combined Schema flag and command migration authorization requires --migration-base-schema")
return 2
}
if migrationBaseSchemaPath != "" && (!flagMigrationPair || !commandMigrationPair) {
fmt.Fprintln(stderr, "--migration-base-schema requires both flag and command migration manifest pairs")
if migrationBaseSchemaPath != "" && !commandMigrationPair {
fmt.Fprintln(stderr, "--migration-base-schema requires the command migration manifest pair")
return 2
}
if migrationStableSchemaPath != "" && (!commandMigrationPair || migrationBaseSchemaPath == "") {
fmt.Fprintln(stderr, "--migration-stable-schema requires the command migration manifest pair and --migration-base-schema")
return 2
}
@@ -232,9 +242,19 @@ func run(args []string, stdout, stderr io.Writer) int {
}
}
if commandMigrationPair {
approvedCommandManifest, readErr := readCommandMigrationManifestFile(approvedCommandMigrationsPath)
if readErr != nil {
fmt.Fprintf(stderr, "read approved Schema command migrations: %v\n", readErr)
return 2
}
candidateCommandManifest, readErr := readCommandMigrationManifestFile(candidateCommandMigrationsPath)
if readErr != nil {
fmt.Fprintf(stderr, "read candidate Schema command migrations: %v\n", readErr)
return 2
}
commandMigrations, err := authorizeSchemaCommandMigrations(
approvedCommandMigrationsPath,
candidateCommandMigrationsPath,
approvedCommandManifest,
candidateCommandManifest,
migrationCurrentSnapshotPath,
migrationBaseSnapshotPath,
migrationStableSnapshotPath,
@@ -243,6 +263,32 @@ func run(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "authorize Schema command migrations: %v\n", err)
return 2
}
if commandMigrationManifestsContainProductRetirement(approvedCommandManifest, candidateCommandManifest) {
if migrationBaseSchemaPath == "" || migrationStableSchemaPath == "" {
fmt.Fprintln(stderr, "Schema product retirement authorization requires merge-base and stable Schema contracts")
return 2
}
migrationBase, readErr := readContract(migrationBaseSchemaPath)
if readErr != nil {
fmt.Fprintf(stderr, "read product retirement merge-base Schema contract: %v\n", readErr)
return 2
}
migrationStable, readErr := readContract(migrationStableSchemaPath)
if readErr != nil {
fmt.Fprintf(stderr, "read product retirement stable Schema contract: %v\n", readErr)
return 2
}
if err := validateSchemaProductRetirementScopes(
migrationBase,
migrationStable,
current,
approvedCommandManifest,
candidateCommandManifest,
); err != nil {
fmt.Fprintf(stderr, "validate Schema product retirement scope: %v\n", err)
return 2
}
}
if flagMigrationPair {
migrationBase, readErr := readContract(migrationBaseSchemaPath)
if readErr != nil {
@@ -345,20 +391,12 @@ func readFlagMigrationManifestFile(path string) (interfacesnapshot.FlagMigration
}
func authorizeSchemaCommandMigrations(
approvedManifestPath string,
candidateManifestPath string,
approved interfacesnapshot.CommandMigrationManifest,
candidate interfacesnapshot.CommandMigrationManifest,
currentSnapshotPath string,
baseSnapshotPath string,
stableSnapshotPath string,
) ([]interfacesnapshot.CommandMigration, error) {
approved, err := readCommandMigrationManifestFile(approvedManifestPath)
if err != nil {
return nil, fmt.Errorf("read approved command migrations: %w", err)
}
candidate, err := readCommandMigrationManifestFile(candidateManifestPath)
if err != nil {
return nil, fmt.Errorf("read candidate command migrations: %w", err)
}
current, err := readInterfaceSnapshotFile(currentSnapshotPath)
if err != nil {
return nil, fmt.Errorf("read migration current snapshot: %w", err)
@@ -387,6 +425,120 @@ func readCommandMigrationManifestFile(path string) (interfacesnapshot.CommandMig
return interfacesnapshot.ReadCommandMigrationManifest(bytes.NewReader(data))
}
func commandMigrationManifestsContainProductRetirement(manifests ...interfacesnapshot.CommandMigrationManifest) bool {
for _, manifest := range manifests {
for _, migration := range manifest.Migrations {
if migration.Kind == interfacesnapshot.CommandMigrationProductRetirement {
return true
}
}
}
return false
}
func validateSchemaProductRetirementScopes(
mergeBase schemaContract,
stable schemaContract,
current schemaContract,
authority interfacesnapshot.CommandMigrationManifest,
candidate interfacesnapshot.CommandMigrationManifest,
) error {
references := []schemaContractReference{
{label: "merge-base", contract: mergeBase},
{label: "stable", contract: stable},
}
for _, migration := range authority.Migrations {
if migration.Kind != interfacesnapshot.CommandMigrationProductRetirement {
continue
}
if migration.State == interfacesnapshot.CommandMigrationPending {
if err := validateExactSchemaProductRetirementToolUnion(references, migration); err != nil {
return fmt.Errorf("approved product retirement %q: %w", migration.Schema.ProductID, err)
}
continue
}
for _, reference := range references {
if err := validateSchemaProductRetirementToolSubset(reference.label, reference.contract, migration); err != nil {
return fmt.Errorf("approved product retirement %q: %w", migration.Schema.ProductID, err)
}
}
if _, exists := current.Products[migration.Schema.ProductID]; exists {
return fmt.Errorf("consumed product retirement %q current Schema still publishes the product", migration.Schema.ProductID)
}
}
for _, migration := range candidate.Migrations {
if migration.Kind != interfacesnapshot.CommandMigrationProductRetirement {
continue
}
switch migration.State {
case interfacesnapshot.CommandMigrationPending:
if err := validateExactSchemaProductRetirementToolUnion(references, migration); err != nil {
return fmt.Errorf("pending product retirement %q: %w", migration.Schema.ProductID, err)
}
baseProduct, baseExists := mergeBase.Products[migration.Schema.ProductID]
currentProduct, currentExists := current.Products[migration.Schema.ProductID]
if baseExists != currentExists || (baseExists && !reflect.DeepEqual(baseProduct, currentProduct)) {
return fmt.Errorf("pending product retirement %q changed the Schema product in its approval PR", migration.Schema.ProductID)
}
case interfacesnapshot.CommandMigrationConsumed:
if _, exists := current.Products[migration.Schema.ProductID]; exists {
return fmt.Errorf("consumed product retirement %q current Schema still publishes the product", migration.Schema.ProductID)
}
}
}
return nil
}
func validateExactSchemaProductRetirementToolUnion(
references []schemaContractReference,
migration interfacesnapshot.CommandMigration,
) error {
declared := make(map[string]bool, len(migration.Schema.Tools))
for _, tool := range migration.Schema.Tools {
declared[tool] = true
}
union := make(map[string]bool, len(declared))
for _, reference := range references {
product, exists := reference.contract.Products[migration.Schema.ProductID]
if !exists {
continue
}
for tool := range product.Tools {
if !declared[tool] {
return fmt.Errorf("%s Schema product contains unreviewed tool %q", reference.label, tool)
}
union[tool] = true
}
}
if len(union) != len(declared) {
return fmt.Errorf("reviewed tools do not exactly match the merge-base and stable Schema tool union")
}
return nil
}
func validateSchemaProductRetirementToolSubset(
label string,
contract schemaContract,
migration interfacesnapshot.CommandMigration,
) error {
product, exists := contract.Products[migration.Schema.ProductID]
if !exists {
return nil
}
declared := make(map[string]bool, len(migration.Schema.Tools))
for _, tool := range migration.Schema.Tools {
declared[tool] = true
}
for tool := range product.Tools {
if !declared[tool] {
return fmt.Errorf("%s Schema product contains unreviewed tool %q", label, tool)
}
}
return nil
}
func readInterfaceSnapshotFile(path string) (interfacesnapshot.Snapshot, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -1843,6 +1995,33 @@ func normalizeSchemaCommandMigrations(
) (schemaContract, error) {
normalized := cloneContract(baseline)
for _, migration := range migrations {
if migration.Kind == interfacesnapshot.CommandMigrationProductRetirement {
oldProduct, productExists := baseline.Products[migration.Schema.ProductID]
if !productExists {
continue
}
if _, stillPublished := current.Products[migration.Schema.ProductID]; stillPublished {
return schemaContract{}, fmt.Errorf(
"approved product retirement %q current Schema still publishes the product",
migration.Schema.ProductID,
)
}
declared := make(map[string]bool, len(migration.Schema.Tools))
for _, tool := range migration.Schema.Tools {
declared[tool] = true
}
for tool := range oldProduct.Tools {
if !declared[tool] {
return schemaContract{}, fmt.Errorf(
"approved product retirement %q historical Schema contains unreviewed tool %q",
migration.Schema.ProductID,
tool,
)
}
}
delete(normalized.Products, migration.Schema.ProductID)
continue
}
oldProduct, productExists := baseline.Products[migration.Schema.ProductID]
oldTool, toolExists := oldProduct.Tools[migration.Schema.SourceToolID]
if !productExists || !toolExists {
@@ -174,6 +174,7 @@ func main() {
baseSnapshot := flag.String("migration-base-snapshot", "", "base interface snapshot")
stableSnapshot := flag.String("migration-stable-snapshot", "", "stable interface snapshot")
migrationBaseSchema := flag.String("migration-base-schema", "", "merge-base Schema contract")
migrationStableSchema := flag.String("migration-stable-schema", "", "stable Schema contract")
approvedCommand := flag.String("approved-command-migrations", "", "base command ledger")
candidateCommand := flag.String("candidate-command-migrations", "", "candidate command ledger")
flag.Parse()
@@ -221,6 +222,15 @@ func main() {
os.Exit(2)
}
if commandPair {
candidateCommandData, err := os.ReadFile(*candidateCommand)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if strings.Contains(string(candidateCommandData), "product_retirement") {
fmt.Fprintln(os.Stderr, "LEGACY_SCHEMA_PARSER_REACHED_PRODUCT_RETIREMENT")
os.Exit(29)
}
data, err := os.ReadFile(*migrationBaseSchema)
if err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -231,8 +241,18 @@ func main() {
os.Exit(2)
}
fmt.Fprintln(os.Stdout, "BASE_SCHEMA_LINEAGE=BASE_AUTHORITY")
stableData, err := os.ReadFile(*migrationStableSchema)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(stableData), "STABLE_RELEASE") {
fmt.Fprintf(os.Stderr, "migration stable Schema is not stable-owned: %s\n", stableData)
os.Exit(2)
}
fmt.Fprintln(os.Stdout, "STABLE_SCHEMA_LINEAGE=STABLE_RELEASE")
} else {
if *migrationBaseSchema != "" {
if *migrationBaseSchema != "" || *migrationStableSchema != "" {
fmt.Fprintln(os.Stderr, "flag-only Schema check received command migration lineage")
os.Exit(2)
}
@@ -424,14 +444,16 @@ func main() {
}
type authorityScenario struct {
name string
baseGovernance string
commandGovernance string
commandManifestChanged bool
candidateMutation string
checkerMode string
want string
authorityMarker string
name string
baseGovernance string
commandGovernance string
commandManifestChanged bool
productRetirementCapability bool
productRetirementCandidate bool
candidateMutation string
checkerMode string
want string
authorityMarker string
}
func TestCrossPlatformCoverageSchemaCompatibilityUsesBaseOwnedAuthority(t *testing.T) {
@@ -531,6 +553,22 @@ func TestCrossPlatformCoverageSchemaCompatibilityUsesBaseOwnedAuthority(t *testi
commandManifestChanged: true,
authorityMarker: "BASE_SCHEMA_CHECKER_ENFORCED",
},
{
name: "Schema product retirement mechanism can land with an empty ledger",
baseGovernance: "complete",
commandGovernance: "complete",
productRetirementCapability: true,
authorityMarker: "BASE_SCHEMA_CHECKER_ENFORCED",
},
{
name: "first Schema product retirement kind requires three separate PRs",
baseGovernance: "complete",
commandGovernance: "complete",
checkerMode: "stable-schema-guard",
productRetirementCapability: true,
productRetirementCandidate: true,
want: "land capability, pending approval, and consumption in three separate PRs",
},
{
name: "unchanged command manifest allows independent corecmd change",
baseGovernance: "complete",
@@ -707,7 +745,11 @@ func runSchemaAuthorityCase(t *testing.T, test authorityScenario) {
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"), candidateLedger, 0o644)
if test.commandGovernance == "complete" || test.commandGovernance == "bootstrap" {
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "command_migrations.go"), "package interfacesnapshot\n", 0o644)
commandMigrationsSource := "package interfacesnapshot\n"
if test.productRetirementCapability {
commandMigrationsSource += "\nconst productRetirementMigrationCapability = \"dws.command-migration.product-retirement.v1\"\n"
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "command_migrations.go"), commandMigrationsSource, 0o644)
if test.commandGovernance == "bootstrap" {
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "corecmd", "interface_const_params.go"), schemaConstParamsRegistrySource("CANDIDATE"), 0o644)
}
@@ -715,6 +757,9 @@ func runSchemaAuthorityCase(t *testing.T, test authorityScenario) {
if test.commandManifestChanged {
commandManifest = "{\"version\":1,\"migrations\":[{\"candidate\":\"PENDING\"}]}\n"
}
if test.productRetirementCandidate {
commandManifest = "{\"version\":1,\"migrations\":[{\"kind\":\"product_retirement\"}]}\n"
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-command-migrations-v1.json"), commandManifest, 0o644)
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "SNAPSHOT_MARKER"), "COMMITTED_CANDIDATE\n", 0o644)
@@ -813,7 +858,7 @@ func runSchemaAuthorityCase(t *testing.T, test authorityScenario) {
if runErr == nil {
t.Fatalf("Schema compatibility script unexpectedly succeeded; output:\n%s", got)
}
for _, forbidden := range []string{"CANDIDATE_NOOP", "LIVE_WORKTREE_BINARY"} {
for _, forbidden := range []string{"CANDIDATE_NOOP", "LIVE_WORKTREE_BINARY", "LEGACY_SCHEMA_PARSER_REACHED_PRODUCT_RETIREMENT"} {
if strings.Contains(got, forbidden) {
t.Fatalf("candidate-controlled authority %q took effect; output:\n%s", forbidden, got)
}
@@ -828,6 +873,9 @@ func runSchemaAuthorityCase(t *testing.T, test authorityScenario) {
if test.checkerMode == "stable-schema-guard" && strings.Count(got, "BASE_SCHEMA_LINEAGE=BASE_AUTHORITY") != 2 {
t.Fatalf("governed Schema check did not pass the base-owned Schema lineage to both historical checks; output:\n%s", got)
}
if test.checkerMode == "stable-schema-guard" && strings.Count(got, "STABLE_SCHEMA_LINEAGE=STABLE_RELEASE") != 2 {
t.Fatalf("governed Schema check did not pass the stable Schema lineage to both historical checks; output:\n%s", got)
}
if test.checkerMode == "flag-only-schema-guard" && strings.Count(got, "FLAG_ONLY_SCHEMA_LINEAGE_OMITTED") != 2 {
t.Fatalf("flag-only Schema check received command migration lineage; output:\n%s", got)
}