Skip to content

Commit 5c0bf3a

Browse files
committed
feat: add TestNoFlagBindOutsideFlagbind, migrate 82 flag sites
Ensures all cobra flag registration (.Flags().StringVar, etc.) goes through internal/flagbind/. No direct cobra flag calls allowed outside that package. Added 7 new flagbind helpers: IntFlag, DurationFlag, PersistentBoolFlag, PersistentStringFlag, BoolFlagShort, StringFlagShort, BoolFlagDefault, StringFlagDefault, BoolFlagNoPtr. Migrated 82 flag registration calls across 38 cmd.go files. Blog post: "Code Structure as an Agent Interface." Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent 8ddc5d7 commit 5c0bf3a

41 files changed

Lines changed: 577 additions & 231 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

internal/audit/flagbind_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package audit
8+
9+
import (
10+
"go/ast"
11+
"strings"
12+
"testing"
13+
)
14+
15+
// flagMethods lists cobra flag registration methods
16+
// that must go through internal/flagbind/.
17+
var flagMethods = map[string]bool{
18+
"StringVar": true,
19+
"StringVarP": true,
20+
"BoolVar": true,
21+
"BoolVarP": true,
22+
"IntVar": true,
23+
"IntVarP": true,
24+
"IntP": true,
25+
"BoolP": true,
26+
"StringP": true,
27+
"DurationVar": true,
28+
"DurationVarP": true,
29+
"StringSliceVar": true,
30+
}
31+
32+
// TestNoFlagBindOutsideFlagbind ensures direct cobra
33+
// flag registration (.Flags().StringVar, etc.) only
34+
// appears in internal/flagbind/. All other packages
35+
// must use the flagbind helpers.
36+
//
37+
// Test files are exempt.
38+
//
39+
// See specs/ast-audit-tests.md for rationale.
40+
func TestNoFlagBindOutsideFlagbind(t *testing.T) {
41+
pkgs := loadPackages(t)
42+
var violations []string
43+
44+
for _, pkg := range pkgs {
45+
if strings.Contains(pkg.PkgPath, "flagbind") {
46+
continue
47+
}
48+
49+
for _, file := range pkg.Syntax {
50+
fpath := pkg.Fset.Position(file.Pos()).Filename
51+
if isTestFile(fpath) {
52+
continue
53+
}
54+
55+
ast.Inspect(file, func(n ast.Node) bool {
56+
call, ok := n.(*ast.CallExpr)
57+
if !ok {
58+
return true
59+
}
60+
61+
// Match: x.Flags().Method() or
62+
// x.PersistentFlags().Method()
63+
sel, ok := call.Fun.(*ast.SelectorExpr)
64+
if !ok {
65+
return true
66+
}
67+
68+
if !flagMethods[sel.Sel.Name] {
69+
return true
70+
}
71+
72+
// The receiver should be a call to
73+
// Flags() or PersistentFlags().
74+
innerCall, ok := sel.X.(*ast.CallExpr)
75+
if !ok {
76+
return true
77+
}
78+
79+
innerSel, ok := innerCall.Fun.(*ast.SelectorExpr)
80+
if !ok {
81+
return true
82+
}
83+
84+
method := innerSel.Sel.Name
85+
if method != "Flags" &&
86+
method != "PersistentFlags" {
87+
return true
88+
}
89+
90+
violations = append(violations,
91+
posString(pkg.Fset, call.Pos())+
92+
": ."+method+"()."+
93+
sel.Sel.Name+
94+
"() must use flagbind",
95+
)
96+
97+
return true
98+
})
99+
}
100+
}
101+
102+
if len(violations) > 0 {
103+
t.Errorf(
104+
"%d direct flag registrations:",
105+
len(violations),
106+
)
107+
}
108+
limit := 20
109+
if len(violations) < limit {
110+
limit = len(violations)
111+
}
112+
for _, v := range violations[:limit] {
113+
t.Error(v)
114+
}
115+
if len(violations) > 20 {
116+
t.Errorf(
117+
"... and %d more",
118+
len(violations)-20,
119+
)
120+
}
121+
}

internal/bootstrap/cmd.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
ctxContext "github.com/ActiveMemory/ctx/internal/context/validate"
2222
"github.com/ActiveMemory/ctx/internal/err/fs"
2323
errInit "github.com/ActiveMemory/ctx/internal/err/initialize"
24+
"github.com/ActiveMemory/ctx/internal/flagbind"
2425
"github.com/ActiveMemory/ctx/internal/rc"
2526
"github.com/ActiveMemory/ctx/internal/validate"
2627
writeBootstrap "github.com/ActiveMemory/ctx/internal/write/bootstrap"
@@ -112,17 +113,14 @@ func RootCmd() *cobra.Command {
112113
})
113114

114115
// Global flags available to all subcommands
115-
c.PersistentFlags().StringVar(
116-
&contextDir,
117-
flag.ContextDir,
118-
"",
119-
desc.Flag(embedFlag.DescKeyContextDir),
116+
flagbind.PersistentStringFlag(
117+
c, &contextDir,
118+
flag.ContextDir, embedFlag.DescKeyContextDir,
120119
)
121-
c.PersistentFlags().BoolVar(
122-
&allowOutsideCwd,
120+
flagbind.PersistentBoolFlag(
121+
c, &allowOutsideCwd,
123122
flag.AllowOutsideCwd,
124-
false,
125-
desc.Flag(embedFlag.DescKeyAllowOutsideCwd),
123+
embedFlag.DescKeyAllowOutsideCwd,
126124
)
127125

128126
return c

internal/cli/add/cmd/root/cmd.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,26 @@ func Cmd() *cobra.Command {
7575
},
7676
}
7777

78-
c.Flags().StringVarP(
79-
&priority,
80-
cFlag.Priority, cFlag.ShortPriority, "",
81-
desc.Flag(flag.DescKeyAddPriority),
78+
flagbind.StringFlagP(
79+
c, &priority,
80+
cFlag.Priority, cFlag.ShortPriority,
81+
flag.DescKeyAddPriority,
8282
)
8383
_ = c.RegisterFlagCompletionFunc(
8484
cFlag.Priority, func(_ *cobra.Command, _ []string, _ string) (
8585
[]string, cobra.ShellCompDirective,
8686
) {
8787
return entry.Priorities, cobra.ShellCompDirectiveNoFileComp
8888
})
89-
c.Flags().StringVarP(
90-
&section,
91-
cFlag.Section, cFlag.ShortSection, "",
92-
desc.Flag(flag.DescKeyAddSection),
89+
flagbind.StringFlagP(
90+
c, &section,
91+
cFlag.Section, cFlag.ShortSection,
92+
flag.DescKeyAddSection,
9393
)
94-
c.Flags().StringVarP(
95-
&fromFile,
96-
cFlag.File, cFlag.ShortFile, "",
97-
desc.Flag(flag.DescKeyAddFile),
94+
flagbind.StringFlagP(
95+
c, &fromFile,
96+
cFlag.File, cFlag.ShortFile,
97+
flag.DescKeyAddFile,
9898
)
9999
flagbind.StringFlagP(
100100
c, &context,

internal/cli/agent/cmd/root/cmd.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1818
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
1919
"github.com/ActiveMemory/ctx/internal/config/fmt"
20+
"github.com/ActiveMemory/ctx/internal/flagbind"
2021
"github.com/ActiveMemory/ctx/internal/rc"
2122
)
2223

@@ -56,22 +57,24 @@ func Cmd() *cobra.Command {
5657
},
5758
}
5859

59-
c.Flags().IntVar(
60-
&budget,
60+
flagbind.IntFlag(
61+
c, &budget,
6162
cFlag.Budget, rc.DefaultTokenBudget,
62-
desc.Flag(flag.DescKeyAgentBudget),
63+
flag.DescKeyAgentBudget,
6364
)
64-
c.Flags().StringVar(
65-
&format, cFlag.Format, fmt.FormatMarkdown,
66-
desc.Flag(flag.DescKeyAgentFormat),
65+
flagbind.StringFlagDefault(
66+
c, &format,
67+
cFlag.Format, fmt.FormatMarkdown,
68+
flag.DescKeyAgentFormat,
6769
)
68-
c.Flags().DurationVar(
69-
&cooldown, cFlag.Cooldown, agent.DefaultCooldown,
70-
desc.Flag(flag.DescKeyAgentCooldown),
70+
flagbind.DurationFlag(
71+
c, &cooldown,
72+
cFlag.Cooldown, agent.DefaultCooldown,
73+
flag.DescKeyAgentCooldown,
7174
)
72-
c.Flags().StringVar(
73-
&session, cFlag.Session, "",
74-
desc.Flag(flag.DescKeyAgentSession),
75+
flagbind.StringFlag(
76+
c, &session,
77+
cFlag.Session, flag.DescKeyAgentSession,
7578
)
7679

7780
return c

internal/cli/change/cmd/root/cmd.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/ActiveMemory/ctx/internal/config/embed/cmd"
1414
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1515
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
16+
"github.com/ActiveMemory/ctx/internal/flagbind"
1617
)
1718

1819
// Cmd returns the change command.
@@ -33,9 +34,9 @@ func Cmd() *cobra.Command {
3334
},
3435
}
3536

36-
c.Flags().StringVar(
37-
&since, cFlag.Since, "",
38-
desc.Flag(flag.DescKeyChangesSince),
37+
flagbind.StringFlag(
38+
c, &since,
39+
cFlag.Since, flag.DescKeyChangesSince,
3940
)
4041

4142
return c

internal/cli/compact/cmd/root/cmd.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/ActiveMemory/ctx/internal/config/embed/cmd"
1414
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1515
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
16+
"github.com/ActiveMemory/ctx/internal/flagbind"
1617
)
1718

1819
// Cmd returns the "ctx compact" command for cleaning up context files.
@@ -40,11 +41,9 @@ func Cmd() *cobra.Command {
4041
},
4142
}
4243

43-
c.Flags().BoolVar(
44-
&archive,
45-
cFlag.Archive,
46-
false,
47-
desc.Flag(flag.DescKeyCompactArchive),
44+
flagbind.BoolFlag(
45+
c, &archive,
46+
cFlag.Archive, flag.DescKeyCompactArchive,
4847
)
4948

5049
return c

internal/cli/dep/cmd/root/cmd.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1515
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
1616
"github.com/ActiveMemory/ctx/internal/config/fmt"
17+
"github.com/ActiveMemory/ctx/internal/flagbind"
1718
)
1819

1920
// Cmd returns the dep command.
@@ -42,16 +43,18 @@ func Cmd() *cobra.Command {
4243
},
4344
}
4445

45-
c.Flags().StringVar(
46-
&format,
47-
cFlag.Format, fmt.FormatMermaid, desc.Flag(flag.DescKeyDepsFormat),
46+
flagbind.StringFlagDefault(
47+
c, &format,
48+
cFlag.Format, fmt.FormatMermaid,
49+
flag.DescKeyDepsFormat,
4850
)
49-
c.Flags().BoolVar(
50-
&external,
51-
cFlag.External, false, desc.Flag(flag.DescKeyDepsExternal),
51+
flagbind.BoolFlag(
52+
c, &external,
53+
cFlag.External, flag.DescKeyDepsExternal,
5254
)
53-
c.Flags().StringVar(
54-
&projType, cFlag.Type, "", desc.Flag(flag.DescKeyDepsType),
55+
flagbind.StringFlag(
56+
c, &projType,
57+
cFlag.Type, flag.DescKeyDepsType,
5558
)
5659

5760
return c

internal/cli/doctor/cmd/root/cmd.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ActiveMemory/ctx/internal/config/embed/cmd"
1515
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1616
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
17+
"github.com/ActiveMemory/ctx/internal/flagbind"
1718
)
1819

1920
// Cmd returns the "ctx doctor" command.
@@ -35,9 +36,9 @@ func Cmd() *cobra.Command {
3536
return Run(cmd, jsonOut)
3637
},
3738
}
38-
c.Flags().BoolP(
39-
cFlag.JSON, cFlag.ShortJSON, false,
40-
desc.Flag(flag.DescKeyDoctorJson),
39+
flagbind.BoolFlagShort(
40+
c, cFlag.JSON, cFlag.ShortJSON,
41+
flag.DescKeyDoctorJson,
4142
)
4243
return c
4344
}

internal/cli/drift/cmd/root/cmd.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/ActiveMemory/ctx/internal/config/embed/cmd"
1414
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1515
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
16+
"github.com/ActiveMemory/ctx/internal/flagbind"
1617
)
1718

1819
// Cmd returns the "ctx drift" command for detecting stale context.
@@ -42,11 +43,13 @@ func Cmd() *cobra.Command {
4243
},
4344
}
4445

45-
c.Flags().BoolVar(
46-
&jsonOutput, cFlag.JSON, false, desc.Flag(flag.DescKeyDriftJson),
46+
flagbind.BoolFlag(
47+
c, &jsonOutput,
48+
cFlag.JSON, flag.DescKeyDriftJson,
4749
)
48-
c.Flags().BoolVar(&fix,
49-
cFlag.Fix, false, desc.Flag(flag.DescKeyDriftFix),
50+
flagbind.BoolFlag(
51+
c, &fix,
52+
cFlag.Fix, flag.DescKeyDriftFix,
5053
)
5154

5255
return c

internal/cli/guide/cmd/root/cmd.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ActiveMemory/ctx/internal/config/embed/cmd"
1515
"github.com/ActiveMemory/ctx/internal/config/embed/flag"
1616
cFlag "github.com/ActiveMemory/ctx/internal/config/flag"
17+
"github.com/ActiveMemory/ctx/internal/flagbind"
1718
)
1819

1920
// Cmd returns the "ctx guide" cobra command.
@@ -37,17 +38,13 @@ func Cmd() *cobra.Command {
3738
},
3839
}
3940

40-
c.Flags().BoolVar(
41-
&showSkills,
42-
cFlag.Skills,
43-
false,
44-
desc.Flag(flag.DescKeyGuideSkills),
41+
flagbind.BoolFlag(
42+
c, &showSkills,
43+
cFlag.Skills, flag.DescKeyGuideSkills,
4544
)
46-
c.Flags().BoolVar(
47-
&showCommands,
48-
cFlag.Commands,
49-
false,
50-
desc.Flag(flag.DescKeyGuideCommands),
45+
flagbind.BoolFlag(
46+
c, &showCommands,
47+
cFlag.Commands, flag.DescKeyGuideCommands,
5148
)
5249

5350
return c

0 commit comments

Comments
 (0)