Accept shared debug bindings

This commit is contained in:
2026-11-08 15:00:34 +00:00
parent 7dd30f7df3
commit 3271c18b77
5 changed files with 72 additions and 3 deletions

View File

@@ -498,7 +498,7 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
if binding.meta.Name == "help" { if binding.meta.Name == "help" {
return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name) 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) return fmt.Errorf("flag name %q is reserved for reporting", binding.meta.Name)
} }
if _, exists := flagNames[binding.meta.Name]; exists { if _, exists := flagNames[binding.meta.Name]; exists {

View File

@@ -1,6 +1,6 @@
# Shared debug binding implementation plan # Shared debug binding implementation plan
Status: in progress; Stage 1 complete. Status: in progress; Stages 1–2 complete.
Baseline: `6727d22ce6e1ac668b56a3e6d8fa5f7a06e54d88`. Baseline: `6727d22ce6e1ac668b56a3e6d8fa5f7a06e54d88`.
Execution audience: Luna coding agent. 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 ## 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), Primary files: [command.go](../../command.go), [binding.go](../../binding.go),
[parser.go](../../parser.go), and [parser.go](../../parser.go), and
[neutral metadata](../../internal/commandmeta/metadata.go). [neutral metadata](../../internal/commandmeta/metadata.go).

View File

@@ -1,6 +1,7 @@
package commandkit package commandkit
import ( import (
"strconv"
"strings" "strings"
"testing" "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)
}
}
}

View File

@@ -2,6 +2,8 @@
// its command-line backend. // its command-line backend.
package commandmeta package commandmeta
import "strings"
// Key identifies one command inside one routing snapshot. // Key identifies one command inside one routing snapshot.
type Key uint64 type Key uint64
@@ -100,6 +102,35 @@ func ReportingFlags() []Flag {
return flags 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. // IsReportingFlag reports whether name is reserved for a shared reporting flag.
func IsReportingFlag(name string) bool { func IsReportingFlag(name string) bool {
for _, flag := range reportingFlags { for _, flag := range reportingFlags {

View File

@@ -1,6 +1,9 @@
package commandmeta package commandmeta
import "testing" import (
"strings"
"testing"
)
func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) { func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
flags := ReportingFlags() 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")
}
}