Why this is its own module¶
Project-root detection is about a hundred lines of code. It began life inside
go-tool-base, the CLI framework, and was
cut out into a module of its own. That looks like overhead — another repository,
another version number, another entry in your go.mod — so it is worth stating what it
buys, because the reasoning also explains why some obvious features will never be added
here.
The cost of asking a framework where your project root is¶
In Go, a dependency is transitive and a module is the unit of it. Importing one package
from a framework brings the framework's module requirements with it, and go.sum,
govulncheck, your SBOM and your organisation's supply-chain review all see the whole
graph — not the one function you called.
For a CLI framework that graph is large by design: a command library, a config loader, a
terminal-rendering stack, telemetry. None of it is wrong. All of it is irrelevant to
answering "which directory holds go.mod". Split out, the answer to that question costs
afero, cockroachdb/errors and the standard library,
and nothing else.
That is the trade: one more line in go.mod, in exchange for a dependency graph you can
read in one screen.
Why the boundary is enforced by a test rather than by intent¶
A stated intention to stay small survives until the first convenient import. So the
constraint is executable. depfootprint_test.go shells out to go list -deps ./... and
fails the build if the graph ever contains go-tool-base, go-git, Viper, pflag, Cobra, the
Charm stack, OpenTelemetry, or the AWS, Google Cloud and Azure SDKs.
The list is not a security policy — nothing there is dangerous. It is the shape of the module written down where CI can check it. If a change needs one of those, the change does not belong in this module; the test says so before review does.
What that boundary rules out¶
Reading the constraint forwards tells you what to expect from this package, and what to stop waiting for:
- No logging. A logger would mean a logging dependency or an interface for callers to
satisfy. The walk returns a result or
ErrNotFound; anything worth saying about it, the caller says. - No configuration. No config file, no environment variable, no marker list read from disk. That would need a config loader, and a config loader is a framework's job. Every input is an argument to the call.
- No git integration.
.gitis a marker becauseStatcan see it, not because anything here understands repositories. Branch names, remotes and submodule boundaries are out of reach by construction. - No CLI. The module ships a library and the pipeline builds no binary — which is
also why there is no release signing here, and why
go.sumplus the Go checksum database is the whole integrity story.
The full list of what the package does not do, including the parts that are about the algorithm rather than the dependency graph, is in what workspace does not do.
When you would not use it¶
If your tool already depends on a framework that answers this question, use that answer.
The value of a leaf module is entirely in the graph you avoid, and there is none to avoid
if you have already paid for it. workspace is for the tool that wants project-root
detection without buying anything else — a linter, a small utility, a library that must
stay cheap to depend on.