<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Software-Design on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/software-design/</link><description>Recent content in Software-Design on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Tue, 16 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/software-design/index.xml" rel="self" type="application/rss+xml"/><item><title>When you hand the same key to every call</title><link>https://blog-570662.gitlab.io/when-you-hand-the-same-key-to-every-call/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/when-you-hand-the-same-key-to-every-call/</guid><description>&lt;img src="https://blog-570662.gitlab.io/when-you-hand-the-same-key-to-every-call/cover-when-you-hand-the-same-key-to-every-call.png" alt="Featured image of post When you hand the same key to every call" /&gt;&lt;p&gt;I was building a tutorial, the kind where the whole point is that the reader runs
every command and it just works. So I generated a fresh project with go-tool-base,
added a command, then added a command &lt;em&gt;underneath&lt;/em&gt; that command, and hit build. It
didn&amp;rsquo;t.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg/cmd/hello/cmd.go: props.ChildCmd undefined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; (type *props.Props has no field or method ChildCmd)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My own generator, in my own framework, had just written code that referenced a
thing that didn&amp;rsquo;t exist. That is a special kind of embarrassing.&lt;/p&gt;
&lt;h2 id="a-bug-with-a-two-month-alibi"&gt;A bug with a two-month alibi
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;git blame&lt;/code&gt; walked me straight to the commit that
&lt;a class="link" href="https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/" &gt;introduced the command middleware system&lt;/a&gt;
back in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/commit/8974154" target="_blank" rel="noopener"
 &gt;March&lt;/a&gt;.
Middleware here is the web-style idea of wrapping a command&amp;rsquo;s run function with
cross-cutting behaviour, timing, auth, recovery, that sort of thing. To wire it
in, &lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;the generator&lt;/a&gt;
started emitting this for a nested command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCommandWithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewCmdChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChildCmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;where the line before had simply been:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewCmdChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The catch is that third argument. &lt;code&gt;props.ChildCmd&lt;/code&gt; is meant to be one of a set of
constants, but those constants are hand-declared for the framework&amp;rsquo;s &lt;em&gt;built-in&lt;/em&gt;
commands only (&lt;code&gt;UpdateCmd&lt;/code&gt;, &lt;code&gt;DocsCmd&lt;/code&gt;, and friends). The generator never declares
one for a user&amp;rsquo;s &lt;code&gt;child&lt;/code&gt; command, so the generated parent referenced a name that
nothing had ever declared. Undefined. Won&amp;rsquo;t compile.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part that should worry you more than the bug. It shipped in March and
nobody noticed until late May. Partly because it only bites &lt;em&gt;nested&lt;/em&gt; commands, a
command under another user command; top-level commands register by a different
path and were fine. But mostly because the generator&amp;rsquo;s tests checked the generated
code as &lt;em&gt;text&lt;/em&gt;, asserting that it contained the right strings, and never once ran
&lt;code&gt;go build&lt;/code&gt; on the result. CI was green for two months on code that could not
compile. We were grading the essay without ever reading it aloud.&lt;/p&gt;
&lt;h2 id="what-the-key-actually-was"&gt;What the key actually was
&lt;/h2&gt;&lt;p&gt;Once I stopped staring at the missing name, the real problem came into focus,
and it wasn&amp;rsquo;t the missing constant.&lt;/p&gt;
&lt;p&gt;That third argument is a middleware &lt;em&gt;lookup key&lt;/em&gt;. The framework keeps a table of
middleware registered against each key, and the key tells it which to apply. It is
not an on/off switch and it is not optional, so the generator had to supply one at
&lt;em&gt;every&lt;/em&gt; registration site. It was being asked to guess, on every call, a value it
had no reliable way to produce for a user command.&lt;/p&gt;
&lt;p&gt;And the tell was sitting right there in the same generator: everywhere else, the
idiom was &lt;code&gt;props.FeatureCmd(&amp;quot;name&amp;quot;)&lt;/code&gt;, a function that derives a key from a string.
The nested-registration path was the one place that assumed a hand-declared
constant instead. One call site out of step with all the others.&lt;/p&gt;
&lt;p&gt;That is the actual lesson, and it has nothing to do with cobra or codegen. When you
find yourself threading the same derived value through every single call site, and
getting it wrong, the value is in the wrong place. The feature key was never the
caller&amp;rsquo;s business. It belonged to the command.&lt;/p&gt;
&lt;h2 id="changing-my-mind-about-cobra"&gt;Changing my mind about cobra
&lt;/h2&gt;&lt;p&gt;This is where I had to eat a helping of my own opinion.&lt;/p&gt;
&lt;p&gt;go-tool-base is built on &lt;a class="link" href="https://github.com/spf13/cobra" target="_blank" rel="noopener"
 &gt;cobra&lt;/a&gt;, the de-facto Go
library for building command trees, and I like it a great deal. I had &lt;em&gt;deliberately&lt;/em&gt;
not wrapped it. Every abstraction over a good library is a tax the reader pays, so
my standing rule was: use cobra directly, don&amp;rsquo;t hide it behind something of mine.&lt;/p&gt;
&lt;p&gt;The trouble is the middleware pattern kept growing, and the bigger it got the more
plainly &amp;ldquo;don&amp;rsquo;t abstract cobra&amp;rdquo; was a position I was holding past its evidence. The
very thing I&amp;rsquo;d refused to build was the thing the design had come to need. It
helped that, having recently
&lt;a class="link" href="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/" &gt;moved the project to GitLab&lt;/a&gt;,
the version had reset to a &lt;code&gt;0.x&lt;/code&gt; prerelease, which makes a breaking change cheap.
The window to stop patching and do it properly was open, and not for long.&lt;/p&gt;
&lt;p&gt;Go&amp;rsquo;s composition model made it almost painless. You can embed a pointer to one
struct inside another and the outer type inherits all the inner one&amp;rsquo;s methods for
free, which is about as close to monkey-patching as a statically typed language
gets. So a &lt;code&gt;setup.Command&lt;/code&gt;
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/61844e6/pkg/setup/command.go#L22-L60" target="_blank" rel="noopener"
 &gt;became&lt;/a&gt;
a cobra command &lt;em&gt;plus&lt;/em&gt; the feature it belongs to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FeatureCmd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FeatureCmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...*&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RunE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RunE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RunE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because &lt;code&gt;*cobra.Command&lt;/code&gt; is embedded, a &lt;code&gt;setup.Command&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; a cobra command for
every method cobra offers; the one place you need the raw pointer, you reach for
&lt;code&gt;.Command&lt;/code&gt;. The generated command now carries its own identity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;child&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RunE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FeatureCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;child&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the parent registers it with nothing threaded through the call:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewCmdChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The feature key now lives on the command, derived from the command&amp;rsquo;s own name,
which is the one place the generator can &lt;em&gt;always&lt;/em&gt; produce it correctly. The
bug isn&amp;rsquo;t so much fixed as made unsayable: there&amp;rsquo;s no call site
left to write the wrong thing into. The wiring got cleaner on the way past, too,
each command&amp;rsquo;s run is wrapped exactly once with its own feature, instead of the old
recursive pass that re-applied the &lt;em&gt;parent&amp;rsquo;s&lt;/em&gt; feature down the whole subtree. And
the old free function stays on as a deprecated shim that just calls &lt;code&gt;Register&lt;/code&gt;, so
nothing downstream breaks before v1.0.&lt;/p&gt;
&lt;h2 id="changing-my-mind-about-the-tests"&gt;Changing my mind about the tests
&lt;/h2&gt;&lt;p&gt;The redesign was the satisfying fix. The test was the important one.&lt;/p&gt;
&lt;p&gt;The reason a non-compiling generator sailed through CI for two months is that its
tests read the generated source as text. A generator is a program that writes a
program, and we were checking that it wrote the expected words without ever asking
whether the words formed a working program. So the redesign shipped with
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/21a90e4/internal/generator/compile_integration_test.go" target="_blank" rel="noopener"
 &gt;a different kind of test&lt;/a&gt;,
one that scaffolds a real project, adds a nested command, and actually builds it.
Its own comment says the quiet part out loud:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The previous test suite asserted file-content shapes but never tried to &lt;code&gt;go build&lt;/code&gt;
the generated module, so the nested-command path that referenced undefined
&lt;code&gt;props.&amp;lt;Name&amp;gt;Cmd&lt;/code&gt; symbols compiled cleanly in tests and broke only when downstream
users built their tools.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s gated behind an integration flag, because it shells out to the Go toolchain
and that&amp;rsquo;s too heavy for every unit run, but it closes the exact gap that hid the
bug. The only &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/" &gt;real test of a code generator&lt;/a&gt;
is whether its output compiles.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;Three times in one bug I had to change my mind. I&amp;rsquo;d decided cobra shouldn&amp;rsquo;t be
abstracted; the evidence said abstract it. I&amp;rsquo;d reached for the one-line patch; the
evidence said redesign. I&amp;rsquo;d trusted tests that read the generated code; the evidence
said build it. None of those were comfortable, and the version reset is the only
reason the timing was kind.&lt;/p&gt;
&lt;p&gt;Both technical lessons are worth keeping. When the same derived value is threaded
through every call, the abstraction is in the wrong place. And the only proof that
something which writes code actually works is to compile what it writes. But the one underneath
both, the one I apparently have to keep relearning, is simpler than either: don&amp;rsquo;t
get so attached to an implementation that you can&amp;rsquo;t change your mind when the
evidence says it doesn&amp;rsquo;t fit. The framework is better for it, and it now has a
composition seam to hang the features cobra doesn&amp;rsquo;t give us natively. A nested
command that wouldn&amp;rsquo;t build was just the thing that finally made me look.&lt;/p&gt;</description></item></channel></rss>