Files
gnoma/internal
vikingowl 70cd530578 feat(config): upgrade-config command + Duration pointer fix
Closes the two follow-up caveats from the 2026-06-04
config-migration follow-up plan:

Caveat 1 — Duration pointer conversion
  SLM.StartupTimeout and SLM.ClassifyTimeout are now *Duration
  (pointer) instead of bare Duration. nil = "use documented
  default" (5s and 0s respectively); *Duration(0) = explicit
  zero. ResolvedSLMSection added to the mirror so consumers
  read resolved time.Duration values instead of the raw
  pointer. cmd/gnoma/main.go, profile_cmd, and the SLM
  startup wiring all move through the mirror. The remaining
  cosmetic encoder issue (startup_timeout = 0 / classify_timeout
  = 0 written even with omitempty) is fixed because the
  BurntSushi encoder now sees a nil pointer when the user
  didn't set the field.

  ResolvedSLMSection's RegisterAsArm mirrors the existing
  nil→true default-substitution semantics from the field's
  doc comment; the if-nil check in main.go is collapsed to
  a direct read of resolved.SLM.RegisterAsArm.

Caveat 2 — `gnoma upgrade-config` (single-file mode)
  New command that cleans a config file in place: drops
  pointer-converted fields whose resolved value matches the
  resolved default, leaves explicit-zero pointer fields
  alone (the "explicit zero preserved" contract from Phase 1),
  and writes the cleaned form atomically with a
  .bak-YYYYMMDD-HHMMSS backup of the original. Idempotent —
  a second run on the cleaned file reports "already clean,
  nothing to do" without creating a second backup.

  Cleaning rules per field type (encoded in internal/config/
  upgrade.go::clean):
    - pointer-converted fields: null iff resolved value
      equals resolved default
    - non-pointer string / map / slice / numeric / bool
      fields: encoder's omitempty already handles them on
      rewrite; the cleaner doesn't touch them

  Diff output uses a simple line-by-line algorithm (added/
  removed/neutral) via splitLines + a forward scan. Adequate
  for the small config files gnoma produces. A proper Myers
  diff could be vendored later — pmezard/go-difflib is
  already a transitive dep in go.sum.

  internal/config/load.go::ProjectConfigPath is now exported
  so the CLI can default the upgrade target to the project
  config when no path is given.

  --dry-run runs the upgrade then restores the file from the
  backup so the operation is truly side-effect-free.

Scope notes
  Single-file mode only. --all-projects is deferred until the
  project registry (Phase 2 of the 2026-05-24 plan) lands —
  the follow-up doc calls this out as the natural next slice
  and it can be added as a follow-up PR without touching
  upgrade-config's core semantics.

  No-op test cases (TestUpgrade_NoChangesOnAlreadyCleanFile,
  TestUpgrade_KeepsExplicitUserValues, TestUpgrade_Keeps-
  ExplicitZeroPointerFields) assert the "resolved view is
  identical before and after" contract.

Test coverage
  internal/config/upgrade_test.go: 10 tests (drops, keeps,
  backup, idempotency, diff, edge cases)
  internal/config/resolve_test.go: +3 tests for ResolvedSLM
  internal/config/write_test.go: +1 test for the Duration
  emission fix
  cmd/gnoma/upgrade_config_cmd_test.go: 3 tests for the CLI

Refs: docs/superpowers/plans/2026-06-04-config-migration-followups.md
2026-06-04 13:18:30 +02:00
..