Reuse alias tree for discovery and help

This commit is contained in:
2026-11-07 15:55:43 +00:00
parent 0d3f7299cc
commit 83386217db
6 changed files with 110 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
# Consolidated audit catalog # Consolidated audit catalog
Status: **11 distinct open findings: 11 P3**, consolidated from 30 Status: **9 distinct open findings: 9 P3**, consolidated from 30
numbered entries in seven audit documents. Reviewed audit state: numbered entries in seven audit documents. Reviewed audit state:
`db732e3a1de55c844f77870e1c6db683c2685d5d`. `db732e3a1de55c844f77870e1c6db683c2685d5d`.
@@ -296,6 +296,10 @@ selected routing and discovery semantics.
## F14 — P3: Discovery-context preparation repeatedly rebuilds shared prefixes ## F14 — P3: Discovery-context preparation repeatedly rebuilds shared prefixes
**Complete.** Distinct trie prefixes are enumerated and classified once;
focused discovery and resolver tests pass, and traversal was reviewed for
elimination of repeated per-declaration prefix construction.
R alias sources sharing a depth-D proper prefix cause Θ(RD²) copying and key R alias sources sharing a depth-D proper prefix cause Θ(RD²) copying and key
construction before result deduplication, with repeated classification as well. construction before result deduplication, with repeated classification as well.
The source declarations contain only Θ(RD) tokens. The source declarations contain only Θ(RD) tokens.
@@ -313,12 +317,16 @@ Reuse the tree's routing-snapshot lifetime; do not introduce a parallel index or
a cross-invocation result cache. The repeated preparation and help-time scan a cross-invocation result cache. The repeated preparation and help-time scan
remain distinct findings. The [joint analysis](remediation-analysis.md#f14f15--joint-source-prefix-design) remain distinct findings. The [joint analysis](remediation-analysis.md#f14f15--joint-source-prefix-design)
records the accepted design. The performance-testing decision under F13 applies records the accepted design. The performance-testing decision under F13 applies
to both; implementation remains pending. to both.
**Original:** [02-efficiency E3](02-efficiency.md#e3--p3-discovery-context-preparation-rebuilds-shared-prefixes-repeatedly). **Original:** [02-efficiency E3](02-efficiency.md#e3--p3-discovery-context-preparation-rebuilds-shared-prefixes-repeatedly).
## F15 — P3: Alias help scans the entire source catalog for each intermediate route ## F15 — P3: Alias help scans the entire source catalog for each intermediate route
**Complete.** Help queries strict descendants from each resolution path instead
of scanning the complete catalog. Focused alias/help tests pass, and the tree
traversal was reviewed for the removal of unrelated source scans.
Help discovery examines all A alias sources at each of P rewrite paths, doing PA Help discovery examines all A alias sources at each of P rewrite paths, doing PA
examinations and repeated copies even when there are no descendants. A valid examinations and repeated copies even when there are no descendants. A valid
single-token alias chain produces Θ(A²) work to discover an empty alias section single-token alias chain produces Θ(A²) work to discover an empty alias section

View File

@@ -1,6 +1,6 @@
# Audit remediation implementation plan # Audit remediation implementation plan
Status: in progress; Stages 1–15 complete. Status: in progress; Stages 1–16 complete.
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
intervening changes recorded decisions and retired the completed older intervening changes recorded decisions and retired the completed older
@@ -527,6 +527,12 @@ Complete when repeated suffix traversal is removed without routing changes.
## Stage 16 — Reuse the resolver tree for prefix discovery and help ## Stage 16 — Reuse the resolver tree for prefix discovery and help
**Complete.** Discovery classifies each eligible trie prefix once (F14), and
help visits only strict descendants for each resolution path (F15). Source
review confirms both changes use the existing resolver tree; focused alias,
discovery, help and resolver tests pass offline with Go 1.27.0 and
`-mod=readonly`, including returned-path ownership and exact aliases with children.
Findings: F14 and F15, one coordinated change. Sources: Findings: F14 and F15, one coordinated change. Sources:
`internal/alias/resolver.go`, `flag_aliases.go`, and `help_view.go`. `internal/alias/resolver.go`, `flag_aliases.go`, and `help_view.go`.

View File

@@ -66,6 +66,7 @@ func (spelling capturedFlagSpelling) token() string {
func compileFlagAliases(rules []RoutingRule, roots map[string]*canonicalNode, aliases interface { func compileFlagAliases(rules []RoutingRule, roots map[string]*canonicalNode, aliases interface {
Resolve(commandpath.Path) commandpath.Path Resolve(commandpath.Path) commandpath.Path
ProperSourcePrefixes() []commandpath.Path
}) (map[string]resolvedFlagAliases, map[string]resolvedFlagAliases, error) { }) (map[string]resolvedFlagAliases, map[string]resolvedFlagAliases, error) {
canonicalNodes := indexCanonicalNodes(roots) canonicalNodes := indexCanonicalNodes(roots)
global := make(map[string]flagAliasMapping) global := make(map[string]flagAliasMapping)
@@ -170,6 +171,7 @@ func canonicalFlagSpellings(command Command) map[string]capturedFlagSpelling {
func discoverFlagContexts(rules []RoutingRule, roots map[string]*canonicalNode, aliases interface { func discoverFlagContexts(rules []RoutingRule, roots map[string]*canonicalNode, aliases interface {
Resolve(commandpath.Path) commandpath.Path Resolve(commandpath.Path) commandpath.Path
ProperSourcePrefixes() []commandpath.Path
}, canonicalNodes map[string]*canonicalNode) map[string]flagAliasContext { }, canonicalNodes map[string]*canonicalNode) map[string]flagAliasContext {
commandSources := make(map[string]struct{}) commandSources := make(map[string]struct{})
prefixes := make(map[string]commandpath.Path) prefixes := make(map[string]commandpath.Path)
@@ -178,12 +180,7 @@ func discoverFlagContexts(rules []RoutingRule, roots map[string]*canonicalNode,
commandSources[rule.record.sourcePath.Key()] = struct{}{} commandSources[rule.record.sourcePath.Key()] = struct{}{}
} }
} }
for _, rule := range rules { for _, prefix := range aliases.ProperSourcePrefixes() {
if rule.record.kind != commandAliasRule {
continue
}
for length := 1; length < len(rule.record.sourcePath); length++ {
prefix := rule.record.sourcePath[:length].Clone()
key := prefix.Key() key := prefix.Key()
if canonicalNodes[key] != nil { if canonicalNodes[key] != nil {
continue continue
@@ -198,7 +195,6 @@ func discoverFlagContexts(rules []RoutingRule, roots map[string]*canonicalNode,
} }
prefixes[key] = prefix prefixes[key] = prefix
} }
}
contexts := make(map[string]flagAliasContext, len(prefixes)) contexts := make(map[string]flagAliasContext, len(prefixes))
for key, path := range prefixes { for key, path := range prefixes {
contexts[key] = flagAliasContext{path: path, canonical: helpFlagSpellings(), discovery: true} contexts[key] = flagAliasContext{path: path, canonical: helpFlagSpellings(), discovery: true}

View File

@@ -36,11 +36,8 @@ func routeHelpAliases(snapshot *routingSnapshot, route routeResult) []commandmet
} }
bySource := make(map[string]commandmeta.CommandAlias) bySource := make(map[string]commandmeta.CommandAlias)
for _, prefix := range snapshot.aliases.ResolutionPaths(route.enteredPath) { for _, prefix := range snapshot.aliases.ResolutionPaths(route.enteredPath) {
for _, alias := range snapshot.commandAliases { for _, alias := range snapshot.aliases.Descendants(prefix) {
source := commandpath.FromTokens(alias.Source) source := alias.Source
if len(source) <= len(prefix) || !source.HasPrefix(prefix) {
continue
}
suffix := source[len(prefix):] suffix := source[len(prefix):]
entered := append(route.enteredPath.Clone(), suffix...) entered := append(route.enteredPath.Clone(), suffix...)
target := snapshot.aliases.Resolve(entered) target := snapshot.aliases.Resolve(entered)

View File

@@ -182,3 +182,50 @@ func (resolver *Resolver) ResolveWithProvenance(path commandpath.Path) (commandp
origins = nextOrigins origins = nextOrigins
} }
} }
// ProperSourcePrefixes returns distinct nonempty proper prefixes of source paths.
// Returned paths are owned by the caller.
func (resolver *Resolver) ProperSourcePrefixes() []commandpath.Path {
if resolver == nil {
return nil
}
var paths []commandpath.Path
for index := 1; index < len(resolver.trie); index++ {
node := &resolver.trie[index]
if len(node.children) != 0 {
paths = append(paths, node.path.Clone())
}
}
return paths
}
// Descendants returns owned mappings whose sources are strictly below prefix.
func (resolver *Resolver) Descendants(prefix commandpath.Path) []Mapping {
if resolver == nil {
return nil
}
index := 0
for _, component := range prefix {
next, ok := resolver.trie[index].children[component]
if !ok {
return nil
}
index = next
}
var mappings []Mapping
var visit func(int)
visit = func(index int) {
node := &resolver.trie[index]
if node.mapping >= 0 {
mapping := resolver.mappings[node.mapping]
mappings = append(mappings, Mapping{Source: mapping.Source.Clone(), Target: mapping.Target.Clone()})
}
for _, child := range node.children {
visit(child)
}
}
for _, child := range resolver.trie[index].children {
visit(child)
}
return mappings
}

View File

@@ -314,3 +314,34 @@ func (machine *Machine) classify(token string) symbol {
} }
return symbol{kind: otherSymbol} return symbol{kind: otherSymbol}
} }
func TestResolverSourcePrefixesAndDescendantsAreOwned(t *testing.T) {
resolver, err := NewResolver([]Mapping{
{Source: commandpath.Path{"a"}, Target: commandpath.Path{"root"}},
{Source: commandpath.Path{"a", "b", "one"}, Target: commandpath.Path{"root", "one"}},
{Source: commandpath.Path{"a", "b", "two"}, Target: commandpath.Path{"root", "two"}},
{Source: commandpath.Path{"other"}, Target: commandpath.Path{"root"}},
})
if err != nil {
t.Fatal(err)
}
prefixes := resolver.ProperSourcePrefixes()
if len(prefixes) != 2 || !prefixes[0].Equal(commandpath.Path{"a"}) || !prefixes[1].Equal(commandpath.Path{"a", "b"}) {
t.Fatalf("prefixes = %#v", prefixes)
}
prefixes[0][0] = "mutated"
descendants := resolver.Descendants(commandpath.Path{"a"})
if len(descendants) != 2 {
t.Fatalf("descendants = %#v", descendants)
}
for _, mapping := range descendants {
if len(mapping.Source) != 3 || mapping.Source[0] != "a" {
t.Fatalf("mapping = %#v", mapping)
}
}
descendants[0].Source[0] = "mutated"
descendants[0].Target[0] = "mutated"
if !resolver.Resolve(commandpath.Path{"a", "b", "one"}).Equal(commandpath.Path{"root", "one"}) || len(resolver.Descendants(commandpath.Path{"missing"})) != 0 {
t.Fatal("resolver was mutated or missing branch returned mappings")
}
}