Provider-interface adoption¶
- Authors
- Matt Cockayne, Claude (claude-opus-4-8) (AI drafting assistant)
- Date
- 2026-06-15
- Status
- IMPLEMENTED (open questions resolved in review 2026-06-15)
Summary¶
pkg/props defines nine narrow provider interfaces so a leaf package or function can declare only the capabilities it actually needs (the interface-segregation principle), with *props.Props satisfying every one of them via compile-time checks. The documented intent β see the Props doc comment ("consider whether a narrow interface β¦ would suffice") and the architecture section of CLAUDE.md β is that consumers narrow their dependency surface. In practice nothing internal does: every non-generated signature in the codebase takes the concrete *props.Props. The provider interfaces are dead API, and the codebase does not eat its own dogfood.
This spec quantifies the adoption impact across the codebase and proposes how to close the drift: either (A) adopt the interfaces in leaf-package signatures, phased package-by-package behind the advisory apidiff job, or (B) keep *Props everywhere and re-document the interfaces as a downstream-convenience surface only. The recommendation, the threshold rules, and the genuine open decisions are laid out for maintainer sign-off.
Finding addressed (from docs/development/reports/codebase-audit-2026-06-12.md, tabled in Β§3.5 Improvements, detailed in Β§3.4 Idiomatic Go):
props-provider-interfaces-unusedβ Medium, idiomatic-go / documented-architecture drift
Motivation¶
GTB sells downstream authors on the narrow-interface pattern: a function that only logs should take a LoggerProvider, not the whole container, so its dependency contract is honest and it is trivially testable with a one-method fake. The framework defines the interfaces, wires the compile-time satisfaction checks, and documents the convention β then ignores it everywhere. That is a credibility gap: the pattern we recommend to downstreams is one we do not follow ourselves.
Adopting the interfaces internally would (1) make each function's dependency contract self-documenting (the signature states exactly what it touches), (2) improve unit-test ergonomics (narrow fakes instead of a fully-wired Props), and (3) turn the dead interfaces into living, exercised API that downstreams can trust. The cost is a wide-but-mechanical signature churn and a long list of advisory apidiff entries β which this spec scopes precisely so the maintainer can weigh it.
Impact analysis¶
The interfaces that exist today¶
pkg/props/interfaces.go defines nine interfaces β seven single-capability and two composites β each backed by a getter method on *Props and a compile-time satisfaction check:
| Interface | Method(s) | Backing Props field |
|---|---|---|
LoggerProvider |
GetLogger() logger.Logger |
Logger |
ConfigProvider |
GetConfig() config.Containable |
Config |
FileSystemProvider |
GetFS() afero.Fs |
FS |
AssetProvider |
GetAssets() Assets |
Assets |
VersionProvider |
GetVersion() version.Version |
Version |
ErrorHandlerProvider |
GetErrorHandler() errorhandling.ErrorHandler |
ErrorHandler |
ToolMetadataProvider |
GetTool() Tool |
Tool |
LoggingConfigProvider |
LoggerProvider + ConfigProvider |
(composite) |
CoreProvider |
LoggerProvider + ConfigProvider + FileSystemProvider |
(composite) |
Two observations from the source:
- The getters exist but the fields are exported. Internal code reaches the dependency directly as
p.Logger,p.Config,p.FS, β¦ β never throughp.GetLogger()etc. Adopting a narrow-interface parameter therefore also forces those call sites to switch from field access to the getter method (the interface exposes only the method, not the field). This is part of the churn, not a free rename. - There is no provider for
Collector.Propshas eight fields (Tool,Logger,Config,Assets,FS,Version,ErrorHandler,Collector), but only seven have a getter/provider βTelemetryCollectorhas neither. Any site that needs only the collector (we found two) cannot narrow today without first adding aTelemetryProviderinterface +GetCollector()getter.
How many sites take *props.Props¶
Counting non-test, non-mock references:
- 179 total
props.Propstoken occurrences across the tree (grep -rn "props\.Props" --include=*.go | grep -v _test.go | grep -v /mocks/). - ~139 of those are in
funcsignatures (parameters or constructors). - Generated/template noise: a large slice lives in
internal/generator/templates/andinternal/generator/stubs.goas string literals that emit*props.Propsinto scaffolded downstream code β these are not real signatures and are out of scope for narrowing (changing them would alter what we scaffold for users; see Out of scope).
Excluding tests, mocks, and the generator's emitted-code string literals leaves roughly 45 real source files with a live *props.Props parameter or field. Distribution by package (files referencing props.Props, non-test, non-mock):
| Package | Files |
|---|---|
pkg/chat |
5 |
internal/cmd/generate |
5 |
pkg/setup (+ ai/github/bitbucket/telemetry subpkgs) |
~9 |
internal/generator (+ verifier/templates) |
~6 |
internal/cmd/keys |
4 |
internal/cmd/regenerate |
3 |
pkg/cmd/docs |
3 |
pkg/telemetry (+ pkg/cmd/telemetry) |
4 |
pkg/vcs/repo |
2 |
internal/cmd/{enable,remove,sign,disable,root} |
~7 |
cmd/e2e |
2 |
others (pkg/docs, pkg/cmd/changelog, internal/cmd/resolve.go) |
3 |
Which sites actually use a narrow slice¶
For every non-generated file with a *props.Props parameter, we enumerated the distinct Props fields actually accessed (p.Logger, p.Config, p.FS, β¦). The result categorises cleanly:
Category (a) β single-provider candidates (uses exactly one field). Strong candidates for a one-method interface:
| Site(s) | Field used | Narrow type |
|---|---|---|
pkg/telemetry/datadir.go (ResolveDataDir, configDataDir) |
Config |
ConfigProvider |
pkg/docs/ask.go (ResolveProvider) |
Config |
ConfigProvider |
cmd/e2e/config_probe.go |
Config |
ConfigProvider |
pkg/chat/claude_local.go (newClaudeLocal) |
Logger |
LoggerProvider |
internal/cmd/disable/disable.go |
Logger |
LoggerProvider |
internal/cmd/generate/command.go |
Logger |
LoggerProvider |
internal/cmd/keys/{generate,mint,wkd}.go |
Logger |
LoggerProvider |
internal/cmd/sign/sign.go |
Logger |
LoggerProvider |
pkg/setup/telemetry/telemetry.go |
Tool |
ToolMetadataProvider |
pkg/setup/middleware_builtin.go |
Collector |
(needs new TelemetryProvider) |
β ~12 files, several with multiple functions, are single-field. The Logger-only and Config-only clusters dominate.
Category (b) β two/three-provider candidates. Map cleanly onto the existing composites or a small multi-param signature:
| Site(s) | Fields | Fits |
|---|---|---|
pkg/chat/{claude,client,gemini,openai}.go, internal/cmd/enable/signing.go |
Config+Logger |
LoggingConfigProvider |
internal/cmd/resolve.go, internal/cmd/generate/flag.go, internal/generator/ast_extract.go, internal/generator/verifier/{agent,legacy}.go |
FS+Logger |
two params, or a CoreProvider minus config |
pkg/cmd/changelog/changelog.go |
Assets+Logger |
two params |
pkg/cmd/docs/docs.go, pkg/props/propstest |
Assets+Tool |
two params |
internal/cmd/root/root.go |
ErrorHandler+Tool |
two params |
pkg/setup/init.go |
Assets+FS+Logger |
three params, or CoreProvider-ish |
internal/generator/commands.go |
Config+FS+Logger |
CoreProvider |
pkg/cmd/telemetry/checks.go |
Collector+Config+Tool |
needs new TelemetryProvider |
β ~14 files. The Config+Logger cluster is the single biggest win and maps onto the already-defined LoggingConfigProvider.
Category © β genuinely need most of Props (leave as *Props). Four-or-more fields, or constructors that fan dependencies into many sub-components:
| Site | Fields |
|---|---|
pkg/vcs/repo/repo.go (NewRepo, auth helpers) |
Config+FS+Logger+Tool |
pkg/setup/update.go |
Config+FS+Logger+Tool+Version |
pkg/telemetry/observability.go (Setup) |
Config+Logger+Tool+Version |
pkg/cmd/telemetry/telemetry.go |
Collector+Config+FS+Logger+Tool |
pkg/setup/ai/ai.go |
Assets+Config+FS+Logger+Tool |
pkg/setup/github/{github,ssh}.go, pkg/setup/bitbucket/bitbucket.go |
FS+Logger+Tool (Β±Assets) |
internal/generator/generator.go (New) |
FS+Logger+Version |
β ~9 files stay on *Props (or, at most, a 3-param signature where it reads cleanly).
Headline split: of the ~35 narrowing-eligible source files, roughly 12 are single-provider, 14 are two/three-provider, and 9 genuinely need *Props. Roughly three-quarters of eligible sites could narrow β a large but bounded surface.
Backward-compatibility and apidiff assessment¶
This is the load-bearing risk, and it must be stated honestly:
- Source-compatible for
*Propscallers. Because*props.Propssatisfies every provider interface, changing a parameter from*props.Propstoprops.LoggerProvider(etc.) keeps compiling for every caller that passes a*Propsβ which is all internal callers and the overwhelming majority of downstream ones. No caller edits are required at the pass-the-argument site. - It is still an exported-signature change.
apidiffwill report each narrowed exported function/constructor as an incompatible signature change, because the parameter's static type changed. With ~35 files and many of them exposing multiple exported symbols, this is dozens of advisoryapidiffentries. Per the pre-1.0 policy (CLAUDE.mdβ API Stability), theapidiffCI job is advisory /allow_failure: true, so this does not block β but it produces a large, noisy diff a reviewer must scan and confirm as intentional. Concentrating the churn per-package per-MR keeps each diff reviewable. - A genuine break exists for non-
*Propscallers. Any downstream that passed something other than a*Props(e.g. their own concrete type, or β for a return-value/struct-field change β code that relied on the concrete type) would break. For parameters this is unlikely (downstreams pass the Props they were handed). For struct fields and return types the risk is higher and the benefit lower β narrowing a stored field changes the struct's exported shape and can ripple. Recommendation: narrow parameters of free functions and methods first; leave exported struct fields and return types as*Propsunless there is a specific reason. - Pre-1.0 lever. Even in the worst case this ships as a minor bump, not a major β the project explicitly permits API churn pre-1.0. So the back-compat question is really "is the advisory-diff noise and the fieldβgetter churn worth the dogfooding payoff?", not "can we afford a break?".
Test impact¶
Net positive, and de-risked by recent work. pkg/props/propstest.New now returns a fully-wired *props.Props that satisfies every provider interface, so existing tests that build a Props and call a narrowed function keep working unchanged (they just pass their *Props where a LoggerProvider is now expected). New or refactored tests for narrowed functions gain the option of a one-method fake instead of a full Props β better isolation, no over-wiring. There is no test-rewrite tax for adoption; the upside is opt-in.
Design decisions¶
These are proposals for review, not yet ratified. Each is paired with an open question where the maintainer's call is genuinely needed.
D1 β Adopt in leaf packages; recommend Option A over Option B¶
Prefer adopting the interfaces (Option A) over re-documenting them as downstream-only (Option B). The interfaces already exist with compile-time checks and doc-comment intent; deleting that intent (Option B) is the lower-effort path but cements the dogfooding gap. Adoption is mechanical and pre-1.0-cheap. (See O1.)
D2 β A β€2-field threshold for narrowing¶
Narrow a signature to a provider interface (or to β€2 narrow params) only when the function body touches β€2 of the Props fields. At 3 fields, narrow only if a defined composite (CoreProvider) fits exactly; otherwise keep *Props. At 4+ fields, always keep *Props. This keeps signatures short and avoids the "five narrow params" anti-pattern that is strictly worse than the container. (See O2.)
D3 β Composite vs multiple params for the 2-field case¶
For the dominant Config+Logger cluster, use the existing LoggingConfigProvider composite (one named type reads better than two params and is already defined). For other 2-field combinations with no matching composite (e.g. FS+Logger, Assets+Tool), prefer two narrow params over minting a new composite per combination β proliferating composites is its own smell. (See O2, O4.)
D4 β Free functions and methods first; constructors and struct fields last¶
Narrow free functions and methods first (lowest blast radius β parameters only). Treat constructors as category-©-leaning: a constructor that fans a Props into several sub-components legitimately needs many fields and should usually keep *Props; narrow a constructor only if it genuinely uses β€2 fields. Do not narrow exported struct fields or return types in this effort. (See O3.)
D5 β Add a TelemetryProvider if (and only if) we narrow the collector-only sites¶
Two sites use only Collector. To narrow them we add a TelemetryProvider interface (GetCollector() TelemetryCollector), a GetCollector getter on *Props, and a satisfaction check β a small, additive, non-breaking change. Resolved: this is in scope (O5) β the collector-only sites are narrowed and the missing provider is added for consistency with the other seven fields. (See O5.)
D6 β Phased, package-by-package, behind the advisory apidiff job¶
Roll out one package per MR, lowest-risk first, so each apidiff advisory diff is small and reviewable. Do not do a single tree-wide MR. (See Phased rollout plan.)
Phased rollout plan¶
Each phase is an independent MR; lint + tests + the advisory apidiff job run per MR. Ordered lowest-risk β highest, fully abortable after any phase.
- Phase 0 β groundwork. Add
TelemetryProvider+GetCollector()+ satisfaction check (D5, resolved in scope per O5). Pure addition, zero diff to existing signatures. - Phase 1 β
Logger-only cluster (internal/cmd/{disable,generate,keys,sign},pkg/chat/claude_local.go). All single-fieldLoggerProvider. Lowest risk, biggest count, establishes the fieldβgetter pattern. - Phase 2 β
Config-only cluster (pkg/telemetry/datadir.go,pkg/docs/ask.go). Single-fieldConfigProvider. - Phase 3 β
Config+Loggercluster (pkg/chat/{claude,client,gemini,openai}.go,internal/cmd/enable/signing.go) viaLoggingConfigProvider. The single biggest readability win. - Phase 4 β
FS+Loggerand other 2-field clusters (internal/cmd/resolve.go,internal/generator/{ast_extract,verifier},internal/cmd/generate/flag.go,pkg/cmd/changelog,pkg/cmd/docs/docs.go,internal/cmd/root/root.go). - Phase 5 β 3-field composite fits (
internal/generator/commands.goviaCoreProvider;pkg/setup/init.go,pkg/cmd/telemetry/checks.goif a composite reads cleanly). - Phase 6 β docs. Update
docs/concepts//docs/components/to show the now-live convention. The generator templates keep emitting*props.Propsfor new scaffolded commands (O8 resolved β Out of scope; see below).
Category-© packages (pkg/vcs/repo, pkg/setup/update.go, pkg/telemetry/observability.go, pkg/setup/ai, generator New) are explicitly not in the plan; they keep *Props.
Open questions¶
- O1 β Adopt or document-only? Option A (adopt the interfaces in leaf signatures β recommended) vs Option B (keep
*Propseverywhere and re-document the interfaces as a downstream-convenience surface only, closing the finding by amending the docs). Drafting recommendation: A. This is the one decision that gates the entire spec. Resolved (2026-06-15): Option A β ADOPT the provider interfaces internally (dogfood them). Not document-only. - O2 β The narrowing threshold. Is "narrow only when the body uses β€2 fields" the right line? Or stricter (single-field only) / looser (β€3 with a composite)? (D2) Resolved (2026-06-15): narrow a function/method to provider-interface params only when it uses β€2 Props fields; if it needs >2, keep
*props.Props. - O3 β Constructors. Should constructors ever narrow, or is
*Propsthe standing rule for anyNew*? And confirm exported struct fields / return types stay*Props(recommended). (D4) Resolved (2026-06-15): constructors stay on*props.Props(they build many-field state); narrow only free functions and methods. Exported struct fields and return types stay*Props. - O4 β Composites policy. Use the two existing composites only, or are we willing to add more (and where's the line before composite-proliferation is worse than two params)? (D3) Resolved (2026-06-15): reuse the existing composites (
LoggingConfigProvider,CoreProvider) only; do not mint a new composite per combination β anything needing >2 providers stays*Props(consistent with O2). - O5 β
TelemetryProvider. Add the missing collector provider (and getter) so collector-only sites can narrow, or leave them on*Props? (D5) Resolved (2026-06-15): ADD a newTelemetryProviderinterface (a getter for theCollector) so collector-only sites can narrow too β closing the one field that lacks a provider, for consistency. Add aGetCollector()-equivalent getter toPropsif one does not already exist, as part of implementation. - O6 β Field access vs getters. Adoption forces narrowed sites from
p.Loggertop.GetLogger(). Are we comfortable with the mixed style during rollout (some files getter-based, some field-based), or do we want a convention note so it doesn't read as accidental inconsistency? Resolved (2026-06-15): narrowed code accesses dependencies via the getters (p.GetLogger(), etc.), not the exported fields, since the interfaces expose getters. - O7 β How far? Full rollout through Phase 5, or stop after the high-value Logger/Config phases (1β3) and leave the rest as "narrow opportunistically when touched"? The advisory-diff noise scales with how far we push. Resolved (2026-06-15): be comprehensive but pragmatic β adopt across all eligible sites (not just a Logger/Config first phase then stop), bounded by the O2 β€2-field threshold. The phased per-package rollout stays for safety, but the end-state target is full adoption of every eligible site.
- O8 β Generator templates. Should newly scaffolded commands be generated with narrowed signatures (teaching downstreams the pattern by example), or keep emitting
*props.Propsfor simplicity? (Currently Out of scope.) Resolved (2026-06-15): generated/scaffolded command output stays on*props.Props(keeps downstream-author ergonomics simple); the narrowing is an internal-GTB dogfooding exercise, not pushed into the generator templates.
Verification plan¶
- Compiles unchanged for callers β after each phase, the whole tree builds with no caller-site edits at the pass-the-argument points (proving the
*Props-satisfies-interface back-compat claim). - Tests green, unchanged β existing tests using
propstest.New(returning*Props) pass against the narrowed signatures without modification. apidiffadvisory reviewed β each MR'sapidiffjob output is read and confirmed as only the intended parameter-type narrowings, no unintended breaks (e.g. an accidental return-type or struct-field change).- Narrow-fake demonstration β at least one narrowed function gains a unit test driven by a one-method fake (not a full Props) to prove the testability payoff is real.
- Lint β
golangci-lintclean; the now-consumed interfaces remove any dead-code/deadcodesignal on the provider types. - Docs accuracy β
docs/concepts//docs/components/examples match real adopted signatures (cross-referenced to code per the After-Implementation rule).
Out of scope¶
- Generator template / scaffold output. The
*props.Propsstrings emitted byinternal/generator/templates/andstubs.gointo downstream scaffolds stay as-is (O8 resolved: keep emitting*props.Props); changing them alters what every new tool is generated with. - Category-© packages.
pkg/vcs/repo,pkg/setup/update.go,pkg/telemetry/observability.go,pkg/setup/ai, and the generatorNewconstructor keep*Props. - Narrowing exported struct fields and return types. Parameters only (D4).
- Removing the exported
Propsfields in favour of getters. The fields stay public; this spec only adds getter usage at narrowed sites. - Any
RepoLikeinterface split (repolike-kitchen-sink-weak-typing) β that is a separate Phase 14 spec.
Related¶
- 2026-06-12 codebase audit Β§3.5 Improvements (finding tabled) / Β§3.4 Idiomatic Go (finding detail) β
props-provider-interfaces-unused - Plan 3 β improvements Phase 15 β
pkg/propsprovider-interface adoption pkg/props/interfaces.go,pkg/props/props.go,pkg/props/propstestCLAUDE.mdβ API Stability (pre-1.0) β the advisory, non-blockingapidiffjobdocs/about/api-stability.md