docs(spec): Plan 4b force-reapply design

This commit is contained in:
MDietermann
2026-04-14 21:39:32 +02:00
parent f6b39fbdbc
commit 592d332968

View File

@@ -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.