Discover help routes through intermediate aliases

This commit is contained in:
2026-10-20 01:03:54 +00:00
parent 4ddf960836
commit bafd365115
5 changed files with 121 additions and 59 deletions

View File

@@ -1,6 +1,6 @@
# Alias interaction audit remediation # Alias interaction audit remediation
Status: open; three confirmed findings. Status: in progress; completed findings are marked individually below.
## Review scope and evidence ## Review scope and evidence
@@ -38,7 +38,7 @@ messages as contracts.
## R-1: Include intermediate alias prefixes in help discovery ## R-1: Include intermediate alias prefixes in help discovery
Status: open. Priority: P2. Category: alias-chain composition. Status: complete. Priority: P2. Category: alias-chain composition.
**Evidence:** [resolveEnteredRoute](../../route.go#L25) resolves the entered path **Evidence:** [resolveEnteredRoute](../../route.go#L25) resolves the entered path
to its final form, then looks for discovery contexts using only that final path to its final form, then looks for discovery contexts using only that final path
@@ -86,6 +86,16 @@ Cover an overriding mapping and applicable global help aliases while preserving
descendant flag exclusion and metadata-only help. Execution and help must agree descendant flag exclusion and metadata-only help. Execution and help must agree
on the displayed destinations. on the displayed destinations.
**Completed:** Discovery selection now checks intermediate rewrite paths when
the entered and final paths do not supply a context. Canonical and discovery
help share alias listing across the resolution chain and resolve each displayed
suffix through actual execution precedence. New public-boundary regressions
failed before the correction and now cover group/discovery help, expanded
chains, entered overrides, both help forms, global help aliases, descendant
flag exclusion, metadata-only help, and matching execution destinations. The
complete offline root suite passed on Go 1.26.8. No canonical nodes or public
invocation metadata were added.
## R-2: Retain discovery-prefix context in usage diagnostics ## R-2: Retain discovery-prefix context in usage diagnostics
Status: open. Priority: P3. Category: presentation consistency. Status: open. Priority: P3. Category: presentation consistency.

View File

@@ -16,13 +16,12 @@ func (a *App) helpView(route routeResult, snapshot *routingSnapshot) commandmeta
if snapshot == nil { if snapshot == nil {
return view return view
} }
view.CommandAliases = routeHelpAliases(snapshot, route)
if view.Discovery { if view.Discovery {
view.CommandAliases = discoveryHelpAliases(snapshot, route)
view.FlagAliases = snapshot.discoveryFlags[route.discoveryPath.Key()].neutralRecords() view.FlagAliases = snapshot.discoveryFlags[route.discoveryPath.Key()].neutralRecords()
return view return view
} }
view.FlagAliases = snapshot.flagAliases[route.canonicalPath.Key()].neutralRecords() view.FlagAliases = snapshot.flagAliases[route.canonicalPath.Key()].neutralRecords()
view.CommandAliases = routeHelpAliases(snapshot, route)
return view return view
} }
@@ -30,9 +29,12 @@ func clonePath(path commandpath.Path) []string {
return append([]string(nil), path...) return append([]string(nil), path...)
} }
func discoveryHelpAliases(snapshot *routingSnapshot, route routeResult) []commandmeta.CommandAlias { func routeHelpAliases(snapshot *routingSnapshot, route routeResult) []commandmeta.CommandAlias {
if route.kind != routeDiscovery && len(route.canonicalPath) == 0 {
return append([]commandmeta.CommandAlias(nil), snapshot.commandAliases...)
}
bySource := make(map[string]commandmeta.CommandAlias) bySource := make(map[string]commandmeta.CommandAlias)
for _, prefix := range []commandpath.Path{route.enteredPath, route.discoveryPath} { for _, prefix := range snapshot.aliases.ResolutionPaths(route.enteredPath) {
for _, alias := range snapshot.commandAliases { for _, alias := range snapshot.commandAliases {
source := commandpath.FromTokens(alias.Source) source := commandpath.FromTokens(alias.Source)
if len(source) <= len(prefix) || !source.HasPrefix(prefix) { if len(source) <= len(prefix) || !source.HasPrefix(prefix) {
@@ -58,55 +60,3 @@ func discoveryHelpAliases(snapshot *routingSnapshot, route routeResult) []comman
} }
return result return result
} }
func routeHelpAliases(snapshot *routingSnapshot, route routeResult) []commandmeta.CommandAlias {
if len(route.canonicalPath) == 0 {
return append([]commandmeta.CommandAlias(nil), snapshot.commandAliases...)
}
node := traceCanonicalPath(snapshot.canonical, route.canonicalPath).exact
if node == nil {
return nil
}
entered := route.enteredPath
canonical := route.canonicalPath
bySource := make(map[string]commandmeta.CommandAlias)
add := func(base commandpath.Path, includeOnlyWhenReached bool) {
for _, alias := range snapshot.commandAliases {
source := commandpath.FromTokens(alias.Source)
if len(source) <= len(base) || !source.HasPrefix(base) {
continue
}
suffix := source[len(base):]
if includeOnlyWhenReached {
enteredSource := append(entered.Clone(), suffix...)
if !snapshot.aliases.Resolve(enteredSource).Equal(commandpath.FromTokens(alias.Target)) {
continue
}
}
key := commandpath.FromTokens(suffix).Key()
if _, exists := bySource[key]; exists {
continue
}
bySource[key] = commandmeta.CommandAlias{
Source: clonePath(suffix),
Target: append([]string(nil), alias.Target...),
}
}
}
add(entered, false)
if !entered.Equal(canonical) {
add(canonical, true)
}
keys := make([]string, 0, len(bySource))
for key := range bySource {
keys = append(keys, key)
}
sort.Strings(keys)
result := make([]commandmeta.CommandAlias, 0, len(keys))
for _, key := range keys {
result = append(result, bySource[key])
}
return result
}

View File

@@ -133,6 +133,20 @@ func (resolver *Resolver) Resolve(path commandpath.Path) commandpath.Path {
} }
} }
// ResolutionPaths returns the entered path and each path produced by a
// longest-prefix rewrite. It supports metadata discovery along alias chains.
// Installation validates termination before this method is used.
func (resolver *Resolver) ResolutionPaths(path commandpath.Path) []commandpath.Path {
paths := []commandpath.Path{path.Clone()}
for {
next, _, matched := resolver.ResolveOnce(paths[len(paths)-1])
if !matched {
return paths
}
paths = append(paths, next)
}
}
// ResolveWithProvenance resolves a command path and carries the input token // ResolveWithProvenance resolves a command path and carries the input token
// indexes that contributed to each output component. Rewritten target // indexes that contributed to each output component. Rewritten target
// components inherit the origins of the matched source prefix. // components inherit the origins of the matched source prefix.

View File

@@ -40,7 +40,11 @@ func resolveEnteredRoute(args []string, snapshot *routingSnapshot) routeResult {
case len(path) == 0: case len(path) == 0:
result.kind = routeCanonical result.kind = routeCanonical
default: default:
if discovery, ok := findDiscoveryPath(snapshot.discoveryFlags, resolved, path); ok { discovery, ok := findDiscoveryPath(snapshot.discoveryFlags, resolved, path)
if !ok && len(snapshot.discoveryFlags) > 0 {
discovery, ok = findDiscoveryPath(snapshot.discoveryFlags, snapshot.aliases.ResolutionPaths(path)...)
}
if ok {
result.kind = routeDiscovery result.kind = routeDiscovery
result.discoveryPath = discovery result.discoveryPath = discovery
result.enteredPath = path.Clone() result.enteredPath = path.Clone()

View File

@@ -239,6 +239,90 @@ func TestDiscoveryHelpComposesGroupShortcutsAndOverrides(t *testing.T) {
} }
} }
func TestHelpDiscoversRoutesThroughIntermediateAliases(t *testing.T) {
for _, test := range []struct {
name string
nested bool
override bool
expanded bool
}{
{name: "group"},
{name: "discovery", nested: true},
{name: "discovery override", nested: true, override: true},
{name: "expanded chain", nested: true, expanded: true},
} {
t.Run(test.name, func(t *testing.T) {
built, runs := 0, 0
var canonical, entered string
makeCommand := func(name string) Command {
return NewCommand(CommandSpec[struct{}]{
Name: name, NewInput: func() struct{} { built++; return struct{}{} },
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
Run: func(inv Invocation, _ struct{}) Outcome {
runs++
entered, canonical = inv.EnteredPath(), inv.CanonicalPath()
return Success()
},
})
}
group, target := Group("box", "Box"), "box"
if test.expanded {
group, target = Group("outer", "Outer", group), "outer box"
}
prefix, middle := "p", "q"
if test.nested {
prefix, middle = "p admin", "q admin"
}
declarations := []Declaration{group, makeCommand("inspect"), makeCommand("alternate"),
Alias("p", "q"), Alias("q", target), Alias(middle+" pick", "inspect"), Alias(middle+" other", "inspect"),
FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--format")}
wantTarget := "inspect"
if test.override {
declarations = append(declarations, Alias(prefix+" pick", "alternate"))
wantTarget = "alternate"
}
app, err := New(Identity{Name: "Demo"}, declarations...)
if err != nil {
t.Fatal(err)
}
for _, args := range [][]string{
append(strings.Fields(prefix), "--help"),
append([]string{"help"}, strings.Fields(prefix)...),
append(strings.Fields(prefix), "--hlep"),
} {
var output, diagnostics bytes.Buffer
outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics})
if outcome.Kind != OutcomeSuccess {
t.Errorf("Execute(%q) = %v, diagnostics %q", args, outcome.Kind, diagnostics.String())
}
for _, fragment := range []string{"Demo " + prefix, "pick → " + wantTarget, "other → inspect", "--hlep → --help"} {
if !strings.Contains(output.String(), fragment) {
t.Errorf("help %q omits %q", output.String(), fragment)
}
}
if strings.Contains(output.String(), "--fmt") || strings.Contains(output.String(), "--format") ||
(test.override && strings.Contains(output.String(), "pick → inspect")) {
t.Errorf("help exposes a descendant flag or overridden destination: %q", output.String())
}
}
if built != 0 || runs != 0 {
t.Fatalf("help built %d inputs and ran %d handlers", built, runs)
}
for _, suffix := range []string{"pick", "other"} {
outcome := app.Execute(context.Background(), strings.Fields(prefix+" "+suffix), Streams{Out: io.Discard, Err: io.Discard})
want := "inspect"
if suffix == "pick" {
want = wantTarget
}
if outcome.Kind != OutcomeSuccess || canonical != want || entered != prefix+" "+suffix {
t.Errorf("route %s %s: outcome %v, entered %q, canonical %q", prefix, suffix, outcome.Kind, entered, canonical)
}
}
})
}
}
func TestDiscoveryHelpThroughExpandedGroupPath(t *testing.T) { func TestDiscoveryHelpThroughExpandedGroupPath(t *testing.T) {
app, err := New(Identity{Name: "Demo"}, Group("outer", "Outer", Group("box", "Box")), app, err := New(Identity{Name: "Demo"}, Group("outer", "Outer", Group("box", "Box")),
simpleRunnable("inspect"), Alias("pk", "outer box"), Alias("outer box admin pick", "inspect")) simpleRunnable("inspect"), Alias("pk", "outer box"), Alias("outer box admin pick", "inspect"))