From 3271c18b77d2953a8ea9db959e577ad3b65c5e17 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 8 Nov 2026 15:00:34 +0000 Subject: [PATCH] Accept shared debug bindings --- command.go | 2 +- docs/roadmap/implementation.md | 7 +++++- flag_aliases_test.go | 18 ++++++++++++++++ internal/commandmeta/metadata.go | 31 +++++++++++++++++++++++++++ internal/commandmeta/metadata_test.go | 17 ++++++++++++++- 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 5467d46..eb82911 100644 --- a/command.go +++ b/command.go @@ -498,7 +498,7 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error { if binding.meta.Name == "help" { return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name) } - if commandmeta.IsReportingFlag(binding.meta.Name) { + if commandmeta.IsReportingFlag(binding.meta.Name) && !(binding.meta.Name == "debug" && binding.boolean) { return fmt.Errorf("flag name %q is reserved for reporting", binding.meta.Name) } if _, exists := flagNames[binding.meta.Name]; exists { diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 372666e..3924ac0 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Shared debug binding implementation plan -Status: in progress; Stage 1 complete. +Status: in progress; Stages 1–2 complete. Baseline: `6727d22ce6e1ac668b56a3e6d8fa5f7a06e54d88`. Execution audience: Luna coding agent. @@ -106,6 +106,11 @@ and concurrent use and retained invocations preserve the selected result. ## Stage 2 — Recognize shared boolean bindings and compose metadata +**Complete.** Declaration validation accepts only boolean debug bindings and +retains other reservation and wiring checks. Effective metadata combines the +shared option without removing consumer entries. Focused validation and metadata +tests passed offline with Go 1.27.0; backend adoption follows in Stage 3. + Primary files: [command.go](../../command.go), [binding.go](../../binding.go), [parser.go](../../parser.go), and [neutral metadata](../../internal/commandmeta/metadata.go). diff --git a/flag_aliases_test.go b/flag_aliases_test.go index 853e9d3..7c5e8c3 100644 --- a/flag_aliases_test.go +++ b/flag_aliases_test.go @@ -1,6 +1,7 @@ package commandkit import ( + "strconv" "strings" "testing" @@ -313,3 +314,20 @@ func TestFlagAliasSharedSuffixAndUnavailableDestination(t *testing.T) { } } } + +func TestSharedDebugDeclarationValidation(t *testing.T) { + valid := BoolFlag(FlagMeta{Name: "debug", Short: "d"}, true, func(*struct{}, Supplied[bool]) {}) + if _, err := New(Identity{Name: "Example"}, flagAliasCommand("run", valid)); err != nil { + t.Fatalf("valid debug: %v", err) + } + for _, flags := range [][]Flag[struct{}]{ + {valid, valid}, + {BoolFlag[struct{}](FlagMeta{Name: "debug"}, false, nil)}, + {BoolFlag(FlagMeta{Name: "debug", Short: "h"}, false, func(*struct{}, Supplied[bool]) {})}, + {ValueFlag(FlagMeta{Name: "debug"}, (*bool)(nil), strconv.ParseBool, func(*struct{}, Supplied[bool]) {})}, + } { + if _, err := New(Identity{Name: "Example"}, flagAliasCommand("bad", flags...)); err == nil { + t.Fatalf("invalid binding accepted: %#v", flags) + } + } +} diff --git a/internal/commandmeta/metadata.go b/internal/commandmeta/metadata.go index 3b5452c..0daab59 100644 --- a/internal/commandmeta/metadata.go +++ b/internal/commandmeta/metadata.go @@ -2,6 +2,8 @@ // its command-line backend. package commandmeta +import "strings" + // Key identifies one command inside one routing snapshot. type Key uint64 @@ -100,6 +102,35 @@ func ReportingFlags() []Flag { return flags } +// EffectiveFlags combines the shared debug option while preserving consumer order. +func EffectiveFlags(flags, core []Flag) []Flag { + result := append([]Flag(nil), flags...) + shared := -1 + for i, flag := range result { + if flag.Name == "debug" && flag.Boolean { + shared = i + break + } + } + for _, flag := range core { + if flag.Name == "debug" && shared >= 0 { + app := result[shared] + usage := flag.Usage + if app.Usage != "" { + usage += "; " + app.Usage + } + if strings.EqualFold(app.Default, "true") { + usage += "; application input defaults to true (reporting defaults to ordinary)" + } + flag.Usage, flag.Short, flag.Required, flag.Completion = usage, app.Short, app.Required, app.Completion + result[shared] = flag + continue + } + result = append(result, flag) + } + return result +} + // IsReportingFlag reports whether name is reserved for a shared reporting flag. func IsReportingFlag(name string) bool { for _, flag := range reportingFlags { diff --git a/internal/commandmeta/metadata_test.go b/internal/commandmeta/metadata_test.go index 9704dd8..aa798be 100644 --- a/internal/commandmeta/metadata_test.go +++ b/internal/commandmeta/metadata_test.go @@ -1,6 +1,9 @@ package commandmeta -import "testing" +import ( + "strings" + "testing" +) func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) { flags := ReportingFlags() @@ -33,3 +36,15 @@ func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) { } } } + +func TestEffectiveFlagsCombinesDebugMetadata(t *testing.T) { + flags := []Flag{{Name: "before"}, {Name: "debug", Short: "d", Boolean: true, Default: "true", Required: true, Usage: "application diagnostics", Completion: CompletionHint{Kind: CompletionFiles}}, {Name: "after"}} + core := ReportingFlags() + got := EffectiveFlags(flags, core) + if len(got) != len(flags)+len(core)-1 || got[1].Name != "debug" || got[1].Default != "false" || got[1].Short != "d" || !got[1].Required || got[1].Completion.Kind != CompletionFiles || !strings.Contains(got[1].Usage, "application diagnostics") || !strings.Contains(got[1].Usage, "application input defaults to true") { + t.Fatalf("effective debug = %#v", got) + } + if flags[1].Default != "true" || core[2].Short != "" { + t.Fatal("input metadata changed") + } +}