From 0d3f7299ccd20dcd87dc2a8f98717656dc962b6d Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 7 Nov 2026 15:54:36 +0000 Subject: [PATCH] Memoize flag alias resolution by context --- docs/roadmap/audit/catalog.md | 6 ++- docs/roadmap/implementation.md | 8 +++- flag_aliases.go | 73 ++++++++++++++++++++-------------- flag_aliases_test.go | 18 +++++++++ 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/docs/roadmap/audit/catalog.md b/docs/roadmap/audit/catalog.md index 3dce289..ac76ab5 100644 --- a/docs/roadmap/audit/catalog.md +++ b/docs/roadmap/audit/catalog.md @@ -1,6 +1,6 @@ # Consolidated audit catalog -Status: **12 distinct open findings: 12 P3**, consolidated from 30 +Status: **11 distinct open findings: 11 P3**, consolidated from 30 numbered entries in seven audit documents. Reviewed audit state: `db732e3a1de55c844f77870e1c6db683c2685d5d`. @@ -274,6 +274,10 @@ ownership; it is separate from the closed-reporting-handle retention in F07. ## F13 — P3: Flag-alias compilation repeatedly walks shared chain suffixes +**Complete.** Each effective context memoizes its target resolutions, including +inactive endpoints, while retaining canonical precedence and cycle diagnostics. +Focused alias and discovery tests pass, including shared suffixes and registration. + For A chained effective flag mappings across C contexts, independent target walks can perform Θ(CA²) target examinations, including inactive chains. Each source repeats work already performed for another source's suffix. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 7fe6d5a..886eca3 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Audit remediation implementation plan -Status: in progress; Stages 1–14 complete. +Status: in progress; Stages 1–15 complete. Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, intervening changes recorded decisions and retired the completed older @@ -500,6 +500,12 @@ clear, with selected syntax and provenance preserved. ## Stage 15 — Memoize each context's flag-alias graph +**Complete.** Context-local traversal memoizes canonical and unavailable +endpoints, with an active stack for cycle diagnostics. Source review confirms +resolved suffixes are reused within each context. Focused alias, registration, +cycle and discovery tests passed offline with Go 1.27.0 and `-mod=readonly`, +including shared suffixes and unavailable destinations. + Findings: F13. Source: `flag_aliases.go`. 1. Replace independent suffix walks with context-local visitation and memoized diff --git a/flag_aliases.go b/flag_aliases.go index c0bd803..921175f 100644 --- a/flag_aliases.go +++ b/flag_aliases.go @@ -230,14 +230,54 @@ func resolveFlagAliases(context flagAliasContext, mappings map[string]flagAliasM sources = append(sources, source) } sort.Strings(sources) + type terminal struct { + spelling capturedFlagSpelling + active bool + } + resolved := make(map[string]terminal) + visiting := make(map[string]int) + var stack []capturedFlagSpelling + var resolve func(capturedFlagSpelling) (terminal, error) + resolve = func(current capturedFlagSpelling) (terminal, error) { + key := flagSpellingKey(current) + if endpoint, exists := context.canonical[key]; exists { + return terminal{spelling: endpoint, active: true}, nil + } + if result, exists := resolved[key]; exists { + return result, nil + } + if start, exists := visiting[key]; exists { + cycle := append(append([]capturedFlagSpelling(nil), stack[start:]...), current) + spellings := make([]string, 0, len(cycle)) + for _, spelling := range cycle { + spellings = append(spellings, spelling.token()) + } + return terminal{}, fmt.Errorf("flag alias cycle: %s", strings.Join(spellings, " → ")) + } + mapping, exists := mappings[key] + if !exists { + resolved[key] = terminal{} + return terminal{}, nil + } + visiting[key] = len(stack) + stack = append(stack, current) + result, err := resolve(mapping.target) + stack = stack[:len(stack)-1] + delete(visiting, key) + if err != nil { + return terminal{}, err + } + resolved[key] = result + return result, nil + } for _, sourceKey := range sources { mapping := mappings[sourceKey] source := spellingFromKey(sourceKey) - target, active, err := resolveFlagTarget(source, mapping.target, mappings, context.canonical) + terminal, err := resolve(mapping.target) if err != nil { return resolvedFlagAliases{}, fmt.Errorf("%s: %w", flagContextDescription(context), err) } - if !active { + if !terminal.active { if mapping.scoped { return resolvedFlagAliases{}, fmt.Errorf("scoped flag alias %s has no canonical destination in %s", source.token(), flagContextDescription(context)) } @@ -246,38 +286,11 @@ func resolveFlagAliases(context flagAliasContext, mappings map[string]flagAliasM if _, collision := context.canonical[sourceKey]; collision { return resolvedFlagAliases{}, fmt.Errorf("flag alias %s conflicts with a canonical spelling in %s", source.token(), flagContextDescription(context)) } - result.bySpelling[sourceKey] = resolvedFlagAlias{source: source, target: target} + result.bySpelling[sourceKey] = resolvedFlagAlias{source: source, target: terminal.spelling} } return result, nil } -func resolveFlagTarget(source, target capturedFlagSpelling, mappings map[string]flagAliasMapping, canonical map[string]capturedFlagSpelling) (capturedFlagSpelling, bool, error) { - visited := map[string]int{flagSpellingKey(source): 0} - chain := []capturedFlagSpelling{source} - current := target - for { - key := flagSpellingKey(current) - if endpoint, exists := canonical[key]; exists { - return endpoint, true, nil - } - if start, exists := visited[key]; exists { - cycle := append(append([]capturedFlagSpelling(nil), chain[start:]...), current) - spellings := make([]string, 0, len(cycle)) - for _, spelling := range cycle { - spellings = append(spellings, spelling.token()) - } - return capturedFlagSpelling{}, false, fmt.Errorf("flag alias cycle: %s", strings.Join(spellings, " → ")) - } - visited[key] = len(chain) - chain = append(chain, current) - next, exists := mappings[key] - if !exists { - return capturedFlagSpelling{}, false, nil - } - current = next.target - } -} - func spellingFromKey(key string) capturedFlagSpelling { if strings.HasPrefix(key, "short:") { return capturedFlagSpelling{name: strings.TrimPrefix(key, "short:"), short: true} diff --git a/flag_aliases_test.go b/flag_aliases_test.go index fddf918..853e9d3 100644 --- a/flag_aliases_test.go +++ b/flag_aliases_test.go @@ -295,3 +295,21 @@ func capturedSpelling(token string) capturedFlagSpelling { issues := []error(nil) return captureFlagSpelling("test", token, &issues) } + +func TestFlagAliasSharedSuffixAndUnavailableDestination(t *testing.T) { + app, err := New(Identity{Name: "Example"}, flagAliasCommand("active", stringFlag("color", "")), flagAliasCommand("inactive"), + FlagAlias("--first", "--common"), FlagAlias("--second", "--common"), FlagAlias("--common", "--color")) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"first", "second", "common"} { + key := flagSpellingKey(capturedFlagSpelling{name: name}) + active := app.flagAliases[commandpath.Path{"active"}.Key()].bySpelling + if alias, ok := active[key]; !ok || alias.target.token() != "--color" { + t.Fatalf("active %s = %#v", name, alias) + } + if _, ok := app.flagAliases[commandpath.Path{"inactive"}.Key()].bySpelling[key]; ok { + t.Fatalf("inactive %s was installed", name) + } + } +}