Discover help routes through intermediate aliases
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
62
help_view.go
62
help_view.go
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
6
route.go
6
route.go
@@ -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()
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user