diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 9565735..b93a366 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -76,6 +76,8 @@ rendering. A session owns the mutable backend tree for one invocation; parsing and presentation reuse that session. The adapter returns raw values and neutral syntax failures to core. It does not own the registry, typed input preparation, integration declarations, provider acquisition, or native report rendering. +Parser options carry flag-alias selection; invocation context and streams stay +with core, while backend output destinations discard and help uses a local buffer. Cobra and pflag imports remain inside this package. ## Optional modules diff --git a/docs/roadmap/audit/catalog.md b/docs/roadmap/audit/catalog.md index 2fc32c6..23b8db5 100644 --- a/docs/roadmap/audit/catalog.md +++ b/docs/roadmap/audit/catalog.md @@ -1,6 +1,6 @@ # Consolidated audit catalog -Status: **14 distinct open findings: 14 P3**, consolidated from 30 +Status: **13 distinct open findings: 13 P3**, consolidated from 30 numbered entries in seven audit documents. Reviewed audit state: `db732e3a1de55c844f77870e1c6db683c2685d5d`. @@ -257,6 +257,10 @@ consumer compatibility or migration work is needed. ## F12 — P3: Parser sessions carry unused execution resources +**Complete.** Unused execution and parser resource plumbing was removed; the +backend retains discard destinations and buffered help. Focused routing, help, +context-forwarding and session-isolation tests passed. + Core and the Cobra adapter duplicate stream normalization and retain context and streams through parser options and backend nodes, although the adapter parses and renders buffered help rather than executing callbacks or reading application diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 6bb3b23..644c61e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Audit remediation implementation plan -Status: in progress; Stages 1–12 complete. +Status: in progress; Stages 1–13 complete. Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, intervening changes recorded decisions and retired the completed older @@ -442,6 +442,12 @@ Complete F21 when both callers share one adapter and preserve their context. ## Stage 13 — Remove unused parser resource plumbing +**Complete.** Parser options carry only alias selection, backend output and +error destinations explicitly discard, and core retains invocation resources. +The test-only App.parse wrapper was removed. Focused routed parsing, help, +context, backend parsing and session-isolation tests passed in the offline aligned +Go 1.27.0 workspace with `-mod=readonly`. F24 remains open for binding wrappers. + Findings: F12 and parsing portion of F24. Sources: `execute.go`, `parser.go`, `internal/commandmeta/metadata.go`, and `internal/cobra/parse.go`. diff --git a/execute.go b/execute.go index 6f7d2c6..051f55b 100644 --- a/execute.go +++ b/execute.go @@ -80,10 +80,8 @@ func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outco type execution struct { app *App ctx context.Context - streams Streams reporter *Reporter debugEvents []string - options commandmeta.ParseOptions snapshot *routingSnapshot session *internalcobra.Session route routeResult @@ -116,13 +114,10 @@ func (current commandExecution) renderHelpTargets(targets []string) Outcome { } func newExecution(app *App, ctx context.Context, streams Streams) *execution { - streams = normalizeStreams(streams) current := &execution{ app: app, ctx: ctx, - streams: streams, reporter: newReporter(streams), - options: commandmeta.ParseOptions{Context: ctx, Out: streams.Out, Err: streams.Err}, } if app != nil { current.snapshot = app.routeSnapshot() @@ -147,7 +142,7 @@ func (current *execution) prepareSession() { if current.snapshot == nil { current.snapshot = current.app.routeSnapshot() } - options := current.options + options := commandmeta.ParseOptions{} if current.snapshot != nil { switch current.route.kind { case routeCanonical: @@ -201,11 +196,11 @@ func (current *execution) renderHelpTargets(targets []string) Outcome { // Explicit help targets are paths, not command invocations with operands. // Discovery routes have no canonical path and retain only trailing options. if route.kind == routeUnknown || len(route.args) != len(route.canonicalPath) { - session := internalcobra.Prepare(snapshot.metadata, current.options) + session := internalcobra.Prepare(snapshot.metadata, commandmeta.ParseOptions{}) view := commandmeta.HelpView{EnteredPath: targets, CanonicalPath: targets} return current.app.renderHelpViewOutcomeWithSession(view, current.reporter, session) } - options := current.options + options := commandmeta.ParseOptions{} switch route.kind { case routeCanonical: options.FlagAliases = snapshot.flagAliases[route.canonicalPath.Key()].neutralRecords() diff --git a/internal/cobra/parse.go b/internal/cobra/parse.go index 81489fa..c82618d 100644 --- a/internal/cobra/parse.go +++ b/internal/cobra/parse.go @@ -4,7 +4,6 @@ package cobra import ( "bytes" - "context" "fmt" "io" "strconv" @@ -35,8 +34,7 @@ func Prepare(root *commandmeta.Command, options commandmeta.ParseOptions) *Sessi session.metadata = root session.flagAliases = append([]commandmeta.FlagAlias(nil), options.FlagAliases...) session.flagAliasesForPath = options.FlagAliasesForPath - options = normalizeOptions(options) - session.root, session.declarations = buildTree(root, options) + session.root, session.declarations = buildTree(root) return session } @@ -289,23 +287,7 @@ func Parse(root *commandmeta.Command, argv []string, options commandmeta.ParseOp return Prepare(root, options).Parse(argv) } -func normalizeOptions(options commandmeta.ParseOptions) commandmeta.ParseOptions { - if options.Context == nil { - options.Context = context.Background() - } - if options.In == nil { - options.In = strings.NewReader("") - } - if options.Out == nil { - options.Out = io.Discard - } - if options.Err == nil { - options.Err = io.Discard - } - return options -} - -func buildTree(root *commandmeta.Command, options commandmeta.ParseOptions) (*spfcobra.Command, map[*spfcobra.Command]*commandmeta.Command) { +func buildTree(root *commandmeta.Command) (*spfcobra.Command, map[*spfcobra.Command]*commandmeta.Command) { nodes := make(map[*spfcobra.Command]*commandmeta.Command) var build func(*commandmeta.Command) *spfcobra.Command build = func(declaration *commandmeta.Command) *spfcobra.Command { @@ -321,10 +303,8 @@ func buildTree(root *commandmeta.Command, options commandmeta.ParseOptions) (*sp if declaration.Runnable { backend.Run = func(*spfcobra.Command, []string) {} } - backend.SetContext(options.Context) - backend.SetIn(options.In) - backend.SetOut(options.Out) - backend.SetErr(options.Err) + backend.SetOut(io.Discard) + backend.SetErr(io.Discard) backend.SetHelpFunc(func(*spfcobra.Command, []string) {}) backend.SetFlagErrorFunc(func(_ *spfcobra.Command, err error) error { return err }) backend.SetUsageTemplate(usageTemplate) diff --git a/internal/cobra/parse_test.go b/internal/cobra/parse_test.go index 3b58f4d..dcac96e 100644 --- a/internal/cobra/parse_test.go +++ b/internal/cobra/parse_test.go @@ -1,8 +1,6 @@ package cobra import ( - "bytes" - "context" "fmt" "io" "reflect" @@ -182,7 +180,7 @@ func TestParseReturnsNeutralFailuresAndFreshState(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{ {Name: "run", Runnable: true, Flags: []Flag{{Name: "value"}}}, }} - options := ParseOptions{Context: context.Background(), In: strings.NewReader("unused"), Out: io.Discard, Err: io.Discard} + options := ParseOptions{} first := Parse(tree, []string{"run", "--value=old"}, options) second := Parse(tree, []string{"run"}, options) if first.Failure != nil || second.Failure != nil { @@ -247,8 +245,7 @@ func TestSelectedPathsResolveThroughBackendParents(t *testing.T) { func TestSessionReusesTreeForParseHelpAndUsageWithFreshState(t *testing.T) { tree := &Command{Name: "app", Children: []*Command{{Name: "run", Runnable: true, Flags: []Flag{{Name: "value"}}}}} - var output, errors bytes.Buffer - session := Prepare(tree, ParseOptions{Context: context.Background(), Out: &output, Err: &errors}) + session := Prepare(tree, ParseOptions{}) first := session.Parse([]string{"run", "--value=old"}) if first.Failure != nil || !reflect.DeepEqual(first.FlagValues, [][]string{{"old"}}) { t.Fatalf("first session parse = %#v", first) @@ -261,19 +258,15 @@ func TestSessionReusesTreeForParseHelpAndUsageWithFreshState(t *testing.T) { if failure != nil || !strings.Contains(line, "run") { t.Fatalf("session usage = %q, %v", line, failure) } - if output.Len() != 0 || errors.Len() != 0 { - t.Fatalf("help escaped session delivery control: out=%q err=%q", output.String(), errors.String()) + if session.root.OutOrStdout() != io.Discard || session.root.ErrOrStderr() != io.Discard { + t.Fatal("backend destinations are not discarded") } - var secondOutput, secondErrors bytes.Buffer - second := Prepare(tree, ParseOptions{Context: context.Background(), Out: &secondOutput, Err: &secondErrors}) + second := Prepare(tree, ParseOptions{}) secondResult := second.Parse([]string{"run"}) if secondResult.Failure != nil || !reflect.DeepEqual(secondResult.FlagValues, [][]string{nil}) { t.Fatalf("fresh session state = %#v", secondResult) } - if output.Len() != 0 || errors.Len() != 0 || secondOutput.Len() != 0 || secondErrors.Len() != 0 { - t.Fatal("prepared session streams were unexpectedly used") - } } func TestMissingSessionTreeReturnsNeutralFailures(t *testing.T) { diff --git a/internal/cobra/performance_test.go b/internal/cobra/performance_test.go index 8ff709c..4074c96 100644 --- a/internal/cobra/performance_test.go +++ b/internal/cobra/performance_test.go @@ -1,9 +1,7 @@ package cobra import ( - "context" "fmt" - "io" "testing" ) @@ -11,7 +9,7 @@ func BenchmarkPerformanceParse(b *testing.B) { wide := performanceFlatTree(128) wideArgs := []string{"wide127"} deep, deepArgs := performanceNestedTree(24) - options := ParseOptions{Context: context.Background(), Out: io.Discard, Err: io.Discard} + options := ParseOptions{} for _, benchmark := range []struct { name string @@ -41,7 +39,7 @@ func BenchmarkPerformanceParse(b *testing.B) { func BenchmarkPerformanceSessionParseAndHelp(b *testing.B) { root := performanceFlatTree(8) args := []string{"wide007"} - session := Prepare(root, ParseOptions{Context: context.Background(), Out: io.Discard, Err: io.Discard}) + session := Prepare(root, ParseOptions{}) result := session.Parse(args) if result.Failure != nil { b.Fatalf("fixture parse failed: %s", result.Failure.Message) diff --git a/internal/commandmeta/metadata.go b/internal/commandmeta/metadata.go index 7e088bf..8104c0e 100644 --- a/internal/commandmeta/metadata.go +++ b/internal/commandmeta/metadata.go @@ -2,11 +2,6 @@ // its command-line backend. package commandmeta -import ( - "context" - "io" -) - // Key identifies one command inside one routing snapshot. type Key uint64 @@ -181,14 +176,10 @@ type Result struct { Failure *Failure } -// ParseOptions makes parser context and streams explicit. +// ParseOptions selects flag aliases for parser contexts. type ParseOptions struct { // FlagAliasesForPath selects aliases for the actual parser context when // routing has not already selected one. It overrides FlagAliases when set. FlagAliasesForPath func([]string) []FlagAlias - Context context.Context - In io.Reader - Out io.Writer - Err io.Writer FlagAliases []FlagAlias } diff --git a/parser.go b/parser.go index be99fe1..1fc71a5 100644 --- a/parser.go +++ b/parser.go @@ -5,19 +5,10 @@ import ( "sort" "strconv" - internalcobra "go.maximumdirect.net/commandkit/internal/cobra" commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta" "go.maximumdirect.net/commandkit/internal/commandpath" ) -func (a *App) parse(args []string, options commandmeta.ParseOptions) commandmeta.Result { - if a == nil { - return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}} - } - result := internalcobra.Parse(a.routeSnapshot().metadata, args, options) - return reduceReportingOccurrences(result) -} - func reduceReportingOccurrences(result commandmeta.Result) commandmeta.Result { selection := commandmeta.ReportingSelection{} for index, occurrence := range result.FlagOccurrences { diff --git a/parser_test.go b/parser_test.go index 455ad75..e947b08 100644 --- a/parser_test.go +++ b/parser_test.go @@ -34,15 +34,14 @@ func TestAppParseUsesRegisteredBindingsWithTheAdapter(t *testing.T) { t.Fatal(err) } - options := commandmeta.ParseOptions{Context: context.Background()} - result := app.parse([]string{"config", "show", "one", "--scope", "app"}, options) + result := parseReportingArgs(app, []string{"config", "show", "one", "--scope", "app"}) if result.Failure != nil { t.Fatalf("parse failed: %s", result.Failure.Message) } if !reflect.DeepEqual(result.Path, []string{"config", "show"}) || !reflect.DeepEqual(result.FlagValues, [][]string{{"app"}}) || !reflect.DeepEqual(result.Operands, []string{"one"}) { t.Fatalf("parse result = %#v", result) } - result = app.parse([]string{"config", "--scope", "app", "show"}, options) + result = parseReportingArgs(app, []string{"config", "--scope", "app", "show"}) if result.Failure == nil { t.Fatal("accepted a child flag before the complete command path") } @@ -55,7 +54,7 @@ func TestRoutingSnapshotPairsParsedKeyWithItsRunner(t *testing.T) { t.Fatal(err) } snapshot := app.routeSnapshot() - session := internalcobra.Prepare(snapshot.metadata, commandmeta.ParseOptions{Context: context.Background()}) + session := internalcobra.Prepare(snapshot.metadata, commandmeta.ParseOptions{}) parsed := session.Parse([]string{"run"}) if parsed.Failure != nil { t.Fatalf("parse failed: %s", parsed.Failure.Message)