Files
dingtalk-workspace-cli/internal/cli/param_aliases.go

434 lines
14 KiB
Go

// 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 (
"fmt"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// ParamAliasEntry is the reduced parameter-alias table for one runnable Cobra
// leaf. It is the typed value the build-time generator serializes into
// param_aliases_generated.go and that the runtime normalizer (P2) consumes.
//
// Aliases maps an already-morphed emitted name to the command's canonical real
// flag; the runtime looks up Morph(emitted) here to resolve a synonym. Blocked
// lists morphed names that must never be reduced (they route to did-you-mean),
// and Ambiguous lists morphed names that a reviewed co-occurrence guard leaves
// unresolved on purpose.
type ParamAliasEntry struct {
CLIPath string `json:"cli_path"`
Aliases map[string]string `json:"aliases,omitempty"`
Blocked []string `json:"blocked,omitempty"`
Ambiguous []string `json:"ambiguous,omitempty"`
}
// ReduceParamAliases resolves the reviewed concept dictionary against every
// runnable leaf's real flags and returns the per-command alias table. It is the
// single source of the reduction algorithm, shared by the generator and tests
// so the build-time (intersection) and generated views can never disagree.
//
// The reduction is deliberately mechanical (no NLU): for each concept it morphs
// the concept members (plus any command-bound generic flag) and intersects them
// with the command's morphed real flags. An intersection of exactly one real
// flag yields aliases onto it; two or more real flags is a co-occurrence that
// must be an explicitly reviewed `ambiguous` entry or generation fails. Command
// scoped aliases override, blocks are removed and recorded, and every override
// path and target is validated against the live tree.
func ReduceParamAliases(root *cobra.Command) ([]ParamAliasEntry, error) {
concepts, err := LoadParamConcepts()
if err != nil {
return nil, fmt.Errorf("load reviewed parameter concepts: %w", err)
}
if root == nil {
return nil, fmt.Errorf("parameter alias source root is nil")
}
overrideByPath := make(map[string]CommandOverride, len(concepts.Overrides))
for _, ov := range concepts.Overrides {
overrideByPath[ov.CommandPath] = ov
}
usedOverride := make(map[string]bool, len(overrideByPath))
conceptsByPath := make(map[string][]Concept)
usedConceptScope := make(map[string]bool)
for _, concept := range concepts.Concepts {
for _, path := range concept.Commands {
conceptsByPath[path] = append(conceptsByPath[path], concept)
}
}
var problems []string
var entries []ParamAliasEntry
walkRunnableParamCommands(root, func(leaf *cobra.Command) {
path := normalizeSchemaCLIPath(leaf.CommandPath())
realByMorph := realFlagsByMorph(leaf)
ov, hasOverride := overrideByPath[path]
if hasOverride {
usedOverride[path] = true
}
scopedConcepts := conceptsByPath[path]
for _, concept := range scopedConcepts {
usedConceptScope[concept.ID+"\x00"+path] = true
if !conceptHasRealFlag(concept, ov, realByMorph) {
problems = append(problems, fmt.Sprintf("concept %q reviewed command %q has no matching real flag or reviewed bind", concept.ID, path))
}
}
entry, entryProblems := reduceLeafParamAliases(path, realByMorph, scopedConcepts, ov)
problems = append(problems, entryProblems...)
if entry != nil {
entries = append(entries, *entry)
}
})
for path := range overrideByPath {
if !usedOverride[path] {
problems = append(problems, fmt.Sprintf("command_override %q does not match any runnable Cobra leaf", path))
}
}
for _, concept := range concepts.Concepts {
for _, path := range concept.Commands {
if !usedConceptScope[concept.ID+"\x00"+path] {
problems = append(problems, fmt.Sprintf("concept %q command scope %q does not match any runnable Cobra command", concept.ID, path))
}
}
}
if len(problems) > 0 {
sort.Strings(problems)
return nil, fmt.Errorf("parameter alias reduction failed:\n - %s", strings.Join(problems, "\n - "))
}
sort.Slice(entries, func(i, j int) bool { return entries[i].CLIPath < entries[j].CLIPath })
return entries, nil
}
// walkRunnableParamCommands invokes fn for every runnable command in the tree,
// including runnable parents such as `chat group members` that expose their own
// flags while also owning subcommands. Parameter aliasing applies to any command
// that accepts flags, which is broader than the schema's leaf-only traversal.
func walkRunnableParamCommands(root *cobra.Command, fn func(*cobra.Command)) {
if root == nil {
return
}
var walk func(*cobra.Command)
walk = func(cmd *cobra.Command) {
if cmd.Runnable() {
fn(cmd)
}
for _, sub := range cmd.Commands() {
if sub.Name() == "help" {
continue
}
if !sub.IsAvailableCommand() && !hasRuntimeSchemaCommand(sub) {
continue
}
walk(sub)
}
}
walk(root)
}
// realFlag is one of a leaf's real flags, remembering whether it is hidden so
// the reduction can treat a hidden legacy alias flag (for example a hand-written
// --base living next to the visible --base-id) as an absorbable synonym rather
// than a genuine co-occurrence.
type realFlag struct {
name string
hidden bool
}
// realFlagsByMorph maps each of a leaf's real flags (local + inherited) by its
// morphed name to the real flags that share that morph.
func realFlagsByMorph(leaf *cobra.Command) map[string][]realFlag {
byMorph := make(map[string][]realFlag)
visitAgentExampleCommandFlags(leaf, func(flag *pflag.Flag) {
if flag == nil || flag.Name == "help" {
return
}
key := cmdutil.Morph(flag.Name)
byMorph[key] = appendRealFlag(byMorph[key], realFlag{name: flag.Name, hidden: flag.Hidden})
})
return byMorph
}
// reduceLeafParamAliases computes one leaf's alias entry and returns any
// contract problems. A nil entry means the leaf produced no aliases, blocks, or
// ambiguous guards.
func reduceLeafParamAliases(path string, realByMorph map[string][]realFlag, concepts []Concept, ov CommandOverride) (*ParamAliasEntry, []string) {
var problems []string
aliasMap := make(map[string]string)
blockedSet := make(map[string]bool)
excludedSet := make(map[string]bool)
claimedRealSet := make(map[string]bool)
pendingReview := ov.Confirm || ov.Investigate
for boundFlag, conceptID := range ov.Bind {
if _, ok := realByMorph[cmdutil.Morph(boundFlag)]; !ok {
problems = append(problems, fmt.Sprintf("command_override %q binds %q to concept %q but %q is not a real flag", path, boundFlag, conceptID, boundFlag))
}
}
// (a) Concept auto-reduction. The caller has already admitted only the
// concepts whose reviewed command scope contains this exact leaf.
for _, concept := range concepts {
eff := make(map[string]bool, len(concept.Members)+2)
for _, member := range concept.Members {
eff[cmdutil.Morph(member)] = true
}
for boundFlag, conceptID := range ov.Bind {
if conceptID == concept.ID {
if pendingReview {
for _, member := range concept.Members {
morphed := cmdutil.Morph(member)
if _, isReal := realByMorph[morphed]; !isReal {
blockedSet[morphed] = true
}
}
} else {
eff[cmdutil.Morph(boundFlag)] = true
}
}
}
// Gather the concept's candidate real flags on this command, then
// choose a canonical. A single visible real flag wins and absorbs the
// rest (including hidden legacy alias flags). Two or more visible real
// flags is a genuine co-occurrence that must be reviewed.
var candidates []realFlag
for key := range eff {
candidates = append(candidates, realByMorph[key]...)
}
if len(candidates) == 0 {
continue
}
for m := range eff {
if _, isReal := realByMorph[m]; isReal {
claimedRealSet[m] = true
}
}
for _, exclude := range concept.Excludes {
excludedSet[cmdutil.Morph(exclude)] = true
}
visible := distinctRealNames(candidates, true)
var canon string
switch len(visible) {
case 1:
canon = visible[0]
case 0:
names := distinctRealNames(candidates, false)
if len(names) != 1 {
continue
}
canon = names[0]
default:
// Genuine co-occurrence: this concept intersects two or more
// visible real flags, so it cannot be auto-reduced. Require that
// every emittable synonym of THIS concept (a concept member that is
// not itself a real flag on the command) is acknowledged in the
// reviewed ambiguous whitelist. Checking only that the command has
// some ambiguous entry would let one concept's whitelist silently
// vouch for a different concept's unreviewed co-occurrence.
ambiguousSet := make(map[string]bool, len(ov.Ambiguous))
for _, a := range ov.Ambiguous {
ambiguousSet[cmdutil.Morph(a)] = true
}
var unreviewed []string
for m := range eff {
if _, isReal := realByMorph[m]; isReal {
continue
}
if !ambiguousSet[m] {
unreviewed = append(unreviewed, m)
}
}
if len(unreviewed) > 0 {
sort.Strings(visible)
sort.Strings(unreviewed)
problems = append(problems, fmt.Sprintf("command %q concept %q intersects visible real flags %s; unreviewed emittable members %s must be listed in the ambiguous whitelist", path, concept.ID, strings.Join(visible, ","), strings.Join(unreviewed, ",")))
}
continue
}
for m := range eff {
if _, isReal := realByMorph[m]; isReal {
continue
}
if prev, ok := aliasMap[m]; ok && prev != canon {
problems = append(problems, fmt.Sprintf("command %q emitted %q reduces to both %q and %q", path, m, prev, canon))
continue
}
aliasMap[m] = canon
}
}
for excluded := range excludedSet {
if _, isAlias := aliasMap[excluded]; isAlias {
continue
}
if claimedRealSet[excluded] {
continue
}
if _, isReal := realByMorph[excluded]; isReal {
continue
}
blockedSet[excluded] = true
}
// (b) Command scoped aliases override concept reductions.
for emitted, target := range ov.ScopedAliases {
morphedEmitted := cmdutil.Morph(emitted)
reals, ok := realByMorph[cmdutil.Morph(target)]
if !ok {
problems = append(problems, fmt.Sprintf("command_override %q scoped alias %q->%q targets %q which is not a real flag", path, emitted, target, target))
continue
}
if _, sourceIsReal := realByMorph[morphedEmitted]; sourceIsReal {
problems = append(problems, fmt.Sprintf("command_override %q scoped alias source %q is already a real flag; keep its native compatibility path or remove it before enabling semantic rewrite", path, emitted))
continue
}
if pendingReview {
delete(aliasMap, morphedEmitted)
blockedSet[morphedEmitted] = true
continue
}
delete(blockedSet, morphedEmitted)
aliasMap[morphedEmitted] = canonicalRealName(reals)
}
// (c) Blocks are removed from the alias map and recorded for did-you-mean.
for _, b := range ov.Block {
mb := cmdutil.Morph(b)
if _, isReal := realByMorph[mb]; isReal {
problems = append(problems, fmt.Sprintf("command_override %q blocks %q but it is already a real flag; blocking must not disable a canonical/native parameter", path, b))
continue
}
delete(aliasMap, mb)
blockedSet[mb] = true
}
ambiguous := make([]string, 0, len(ov.Ambiguous))
for _, a := range ov.Ambiguous {
ma := cmdutil.Morph(a)
if _, isReal := realByMorph[ma]; isReal {
problems = append(problems, fmt.Sprintf("command_override %q marks %q ambiguous but it is already a real flag", path, a))
continue
}
if canon, ok := aliasMap[ma]; ok {
problems = append(problems, fmt.Sprintf("command %q name %q is both auto-reduced to %q and marked ambiguous; a name cannot be aliased and ambiguous at once", path, a, canon))
}
delete(aliasMap, ma)
delete(blockedSet, ma)
ambiguous = append(ambiguous, ma)
}
blocked := make([]string, 0, len(blockedSet))
for b := range blockedSet {
blocked = append(blocked, b)
}
if len(aliasMap) == 0 && len(blocked) == 0 && len(ambiguous) == 0 {
return nil, problems
}
return &ParamAliasEntry{
CLIPath: path,
Aliases: aliasMap,
Blocked: sortedUnique(blocked),
Ambiguous: sortedUnique(ambiguous),
}, problems
}
func conceptHasRealFlag(concept Concept, ov CommandOverride, realByMorph map[string][]realFlag) bool {
for _, member := range concept.Members {
if _, ok := realByMorph[cmdutil.Morph(member)]; ok {
return true
}
}
for boundFlag, conceptID := range ov.Bind {
if conceptID == concept.ID {
if _, ok := realByMorph[cmdutil.Morph(boundFlag)]; ok {
return true
}
}
}
return false
}
func appendRealFlag(list []realFlag, value realFlag) []realFlag {
for i, existing := range list {
if existing.name == value.name {
// Prefer the visible record if any registration is visible.
if existing.hidden && !value.hidden {
list[i] = value
}
return list
}
}
list = append(list, value)
sort.Slice(list, func(i, j int) bool { return list[i].name < list[j].name })
return list
}
// distinctRealNames returns the sorted unique flag names among candidates,
// optionally restricted to visible (non-hidden) flags.
func distinctRealNames(candidates []realFlag, visibleOnly bool) []string {
seen := make(map[string]bool, len(candidates))
out := make([]string, 0, len(candidates))
for _, c := range candidates {
if visibleOnly && c.hidden {
continue
}
if seen[c.name] {
continue
}
seen[c.name] = true
out = append(out, c.name)
}
sort.Strings(out)
return out
}
// canonicalRealName chooses the canonical target among real flags sharing a
// morph key, preferring a visible flag over a hidden legacy alias.
func canonicalRealName(reals []realFlag) string {
for _, r := range reals {
if !r.hidden {
return r.name
}
}
if len(reals) > 0 {
return reals[0].name
}
return ""
}
func sortedUnique(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]bool, len(values))
out := make([]string, 0, len(values))
for _, v := range values {
if seen[v] {
continue
}
seen[v] = true
out = append(out, v)
}
sort.Strings(out)
return out
}