ADR-020: Resolve the Target Path at the CLI Boundary, Not in the Domain/Model Layer¶
| Field | Value |
|---|---|
| Date | 2026-08-06 |
| Status | Accepted |
| Deciders | Holger Zahnleiter |
| Supersedes | — |
| Superseded by | — |
Context and Problem Statement¶
repo init's SUMMARY output needs to confirm where a project actually got created. --targetpath
accepts ., a relative path, or an absolute path, but no filepath.Abs call exists anywhere in
the codebase — internal.ProjectDescriptor.TargetPath carries whatever raw string was passed on
the command line, unresolved, all the way through generation. Printing that raw string back (e.g.
.) tells the user nothing they didn't already type.
Where should the raw-to-absolute resolution happen?
Decision Drivers¶
model.PresentationModeland everything ininternal/templatingare pure, I/O-free: they're unit-tested directly with arbitrary constructed values and no real filesystem, and are consumed identically by bothcmd/init.goand the Godog acceptance suite.filepath.Abscallsos.Getwd()when its argument isn't already absolute — that's an OS interaction, which the domain/model layer must not perform (general_coding.md: "Domain layer: technology-agnostic, no I/O").cmd/is this codebase's established I/O boundary — flag parsing, filesystem checks (os.Stat), and error surfacing already live there for exactly this reason.- The only current consumer of the resolved path is display (the SUMMARY headline); nothing
downstream in generation needs
TargetPathitself to become absolute.
Considered Options¶
- Resolve
filepath.Absincmd/init.go, right before printing, passed as an explicitresolvedTargetPathparameter totemplating.BuildPostGenerationSummary. - Make
ProjectDescriptor.TargetPathalways absolute from the moment CLI flags are parsed, before any generation code runs. - Resolve inside
model.NewPresentationModelForTemplatingGenerator, storing the absolute path onPresentationModelalongside the other print-ready fields.
Decision Outcome¶
Option 1.
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolve target path %q: %w", path, err)
}
fprintPostGeneration(cmd.OutOrStdout(), templating.BuildPostGenerationSummary(m, absPath))
The resolution happens exactly once, at the one place the resolved value is actually needed (display), in the layer that already owns OS interaction for this command.
Positive Consequences¶
model.PresentationModelandinternal/templatingstay pure and OS-independent — existing unit tests that construct aPresentationModeldirectly, with no real filesystem or working directory, are unaffected.- The change is fully localized to
cmd/init.go's three post-generation print sites; no other code path that readsProjectDescriptor.TargetPath(e.g.TargetFolder()and friends) changes behavior.
Negative Consequences / Risks¶
filepath.Absis now called three times (once per print site) instead of once — acceptable given each call is a cheap, local string operation, and the alternative (threading a pre-resolved value through more of the call graph) would touch more code for no behavioral gain.
Rejected alternatives¶
- Option 2 (always-absolute
TargetPath) has a much larger blast radius: every existing helper that joins ontoTargetPath(TargetFolder(),TargetAvatarFolder(), etc.) and every test that constructs aProjectDescriptorwith a relative or.path would need re-auditing, for a need that is purely cosmetic (SUMMARY display). Nothing about generation itself is currently broken by a relativeTargetPath. - Option 3 (resolve inside the model constructor) would couple a function that is otherwise a
pure struct-literal builder to
os.Getwd(), breaking the "no I/O in domain/model layer" rule this codebase holds elsewhere, and would force every existing/future unit test that constructs aPresentationModelvia this constructor to become sensitive to the process's actual working directory.
Links¶
internal/templating/post_generation_instructions.go—BuildPostGenerationSummary'sresolvedTargetPathparameter.cmd/init.go— the three call sites (initFromCLIArgs,initFromRepoConfig,initFromMonoRepoConfig) wherefilepath.Absis called.