From 592d33296889e51ada0c7b13405b1a95ae27d56e Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 21:39:32 +0200 Subject: [PATCH] docs(spec): Plan 4b force-reapply design --- .../specs/2026-04-14-force-reapply-design.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-14-force-reapply-design.md diff --git a/docs/superpowers/specs/2026-04-14-force-reapply-design.md b/docs/superpowers/specs/2026-04-14-force-reapply-design.md new file mode 100644 index 0000000..7a66663 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-force-reapply-design.md @@ -0,0 +1,161 @@ +# Force Reapply Design (Plan 4b) + +**Date:** 2026-04-14 +**Scope:** Plan 4b of the Archduino Go rewrite — add `archduino install --force` that re-runs every selected module's Apply unconditionally. + +## Goal + +Let the user bypass the four-phase contract's idempotency guard when they explicitly want to reapply everything — typically after a manual edit, corruption, or "I'm not sure what's broken, redo it" scenario. Scope is narrow: force a full reapply of currently-selected modules. No uninstall, no revert, no selective force-by-category. + +## Non-goals + +- No uninstall / reset / revert behavior. Modules have no `Revert` method today; adding one is a separate, much larger plan. +- No selective force (`--force-configs`, `--force-packages`). The user picked "full re-everything" during brainstorming; single flag only. +- No new subcommand (`archduino reinstall`). A flag on `install` is enough. +- No changes to `Verify`; force affects only what `Apply` does, not how we confirm it. + +## Interface change + +Add one method to `module.Module`: + +```go +// PlanForce returns a plan that reapplies the module unconditionally, +// ignoring current detected state. Used by `archduino install --force`. +// Implementations should produce a plan equivalent to "install from +// scratch" — reinstall all packages (without pacman's --needed guard) +// and rewrite all managed config files. +PlanForce(ctx context.Context) (Plan, error) +``` + +Every existing module implements this. Two categories: + +### Config-writing modules +`zsh`, `neovim`, `themes`, `starship`, `shellconfig`, `zellij`, `yazi` (config bits), `arduinocli` (sketch dirs, indexes). + +`PlanForce` produces a plan that: +1. Reinstalls packages via `platform.Pacman.Reinstall` / `platform.Yay.Reinstall` (no `--needed`). +2. Rewrites every managed config file, overwriting existing content. +3. Re-runs any post-install steps (e.g., arduino-cli core update, zsh plugin clone). + +### Package-only modules +`packages`, `gittools`, `fonts`, `renode`, and the package-only portions of `starship` / `yazi`. + +`PlanForce` produces a plan that calls `platform.Pacman.Reinstall(ctx, pkgs)` — identical to the install step but without `--needed`, so pacman physically re-fetches and reinstalls the package even if already present. + +## Platform layer additions + +`internal/platform/pacman.go`: +```go +// Reinstall runs `pacman -S --noconfirm pkg...` without the --needed flag +// so pacman unconditionally reinstalls each package. +func (p Pacman) Reinstall(ctx context.Context, pkgs []string) error +``` + +`internal/platform/aur.go`: +```go +// Reinstall runs `yay -S --noconfirm pkg...` without the --needed flag. +func (y Yay) Reinstall(ctx context.Context, pkgs []string) error +``` + +Both are straight copies of the existing `Install` methods with `--needed` omitted from the command-line args. + +## Engine changes + +`internal/engine/engine.go`: + +- `RunOptions` gains a field: + ```go + Force bool + ``` +- `Run` per-module path becomes: + ```go + var plan module.Plan + if opts.Force { + plan, err = mod.PlanForce(ctx) + } else { + cur, derr := mod.Detect(ctx) + if derr != nil { /* existing error handling */ } + plan, err = mod.Plan(ctx, cur) + } + ``` + When `Force` is true, `Detect` is not called. + +The Apply and Verify phases are unchanged. Force does not affect DAG ordering, reporter output, or halt-on-error behavior. + +## CLI changes + +`cmd/archduino/main.go::newInstallCmd()`: + +```go +var force bool +cmd.Flags().BoolVar(&force, "force", false, + "Reapply every selected module, ignoring current state") +``` + +In `RunE`, propagate to both engine and wizard: +```go +runOpts.Force = force +initialState.Force = force +``` + +## Wizard / TUI integration + +`internal/tui/state.go` — `WizardState` gains: +```go +Force bool +``` + +`internal/tui/wizard/wizard.go` — the `planCache` path branches: +- When `state.Force` is true, call `PlanForce(ctx)` instead of `Detect` + `Plan`. +- When a user toggles a module's selection, the cache key includes `force` so caches from non-force runs are not reused. +- Header gains a persistent banner: `⚠ Force mode: all selected modules will be reapplied`. + +## Data flow + +``` +user: archduino install --force + → main.go: force=true → RunOptions.Force = true, WizardState.Force = true + → wizard.Run: per-module preview uses PlanForce (skips Detect) + → user confirms + → engine.Run: per-module PlanForce → Apply → Verify +``` + +## Error handling + +No new failure modes. `PlanForce` errors are handled identically to `Plan` errors: reported via the reporter, counted in the engine summary, respected by `--halt-on-error`. + +## Testing + +**Per-module tests** — each existing `*_test.go` gains a `TestPlanForce` case. The test arranges a "satisfied" system state (mocked platform says everything is installed, configs exist), calls `PlanForce(ctx)`, and asserts the returned plan is non-empty and contains the expected reinstall/rewrite actions. This is the core behavioral contract — without this test, a module could silently "forget" to implement force. + +**Engine test** — `engine_test.go` gets a case using a fake module that records whether `Detect`, `Plan`, and `PlanForce` were called. Runs with `RunOptions.Force: true` and asserts `Detect` was NOT called and `PlanForce` WAS. + +**Platform tests** — `pacman_test.go` and `aur_test.go` each gain a `TestReinstall` that verifies the command-line args include `-S` and `--noconfirm` but do NOT include `--needed`. + +**No TUI test for the banner** — visual-only, covered by the existing wizard tests for preview-cache behavior which will get a new "force preview" case. + +## Interaction with existing features + +- `--dry-run --force` prints the force plan and exits. Existing dry-run code path is unchanged. +- `--only`/`--skip` respect force: force applies only to the selected set. +- `--halt-on-error` respects force identically. +- Theme application at end of install (Plan 4a feature) runs unchanged; force doesn't skip it. + +## File touch list + +**Modified:** +- `internal/module/module.go` — add `PlanForce` to `Module` interface +- `internal/engine/engine.go` — add `RunOptions.Force`, branch in `Run` +- `internal/engine/engine_test.go` — new force test +- `internal/tui/state.go` — add `WizardState.Force` +- `internal/tui/wizard/wizard.go` — force-aware preview cache + banner +- `internal/tui/wizard/wizard_test.go` — new preview test with force +- `internal/platform/pacman.go` — add `Reinstall` method +- `internal/platform/pacman_test.go` — new test +- `internal/platform/aur.go` — add `Reinstall` method +- `internal/platform/aur_test.go` — new test +- `cmd/archduino/main.go` — add `--force` flag, wire through +- 13 module files (`internal/modules/*/*.go`) — implement `PlanForce` +- 13 module test files — `TestPlanForce` each + +**New:** none.