Skip to content

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.PresentationModel and everything in internal/templating are pure, I/O-free: they're unit-tested directly with arbitrary constructed values and no real filesystem, and are consumed identically by both cmd/init.go and the Godog acceptance suite. filepath.Abs calls os.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 TargetPath itself to become absolute.

Considered Options

  1. Resolve filepath.Abs in cmd/init.go, right before printing, passed as an explicit resolvedTargetPath parameter to templating.BuildPostGenerationSummary.
  2. Make ProjectDescriptor.TargetPath always absolute from the moment CLI flags are parsed, before any generation code runs.
  3. Resolve inside model.NewPresentationModelForTemplatingGenerator, storing the absolute path on PresentationModel alongside 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.PresentationModel and internal/templating stay pure and OS-independent — existing unit tests that construct a PresentationModel directly, 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 reads ProjectDescriptor.TargetPath (e.g. TargetFolder() and friends) changes behavior.

Negative Consequences / Risks

  • filepath.Abs is 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 onto TargetPath (TargetFolder(), TargetAvatarFolder(), etc.) and every test that constructs a ProjectDescriptor with 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 relative TargetPath.
  • 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 a PresentationModel via this constructor to become sensitive to the process's actual working directory.
  • internal/templating/post_generation_instructions.goBuildPostGenerationSummary's resolvedTargetPath parameter.
  • cmd/init.go — the three call sites (initFromCLIArgs, initFromRepoConfig, initFromMonoRepoConfig) where filepath.Abs is called.