diff options
| -rw-r--r-- | docs/superpowers/specs/2026-06-16-suggest-flag-design.md | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-06-16-suggest-flag-design.md b/docs/superpowers/specs/2026-06-16-suggest-flag-design.md new file mode 100644 index 0000000..5bcd5d6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-suggest-flag-design.md @@ -0,0 +1,180 @@ +# Design: `--suggest` flag + +**Date:** 2026-06-16 +**Status:** Approved (pending implementation) + +## Goal + +Add a `-s|--suggest` flag to `dot-backup.sh` that surfaces config +directories and files created since the last backup that are not yet +tracked in `DOTFILES`, and prints a paste-ready block for `files.list`. + +The flag is read-only: it never copies, commits, pushes, or mutates any +config. It scans, prints, and exits. + +## Behavior + +### Flag + +- New flag `-s` / `--suggest`, registered in the arg loop and `usage()`. +- When set, run suggest logic, then **exit 0**. No backup/restore/copy/commit. +- Mutually exclusive with `--restore` in practice (suggest exits first if both + given; suggest takes precedence). + +### Scan sources + +Three sources, all scanned one level deep only: + +1. `~/.config/*/` — direct child directories of `~/.config`. + Emitted as `.config/<name>`. +2. `~/.*/` — top-level hidden directories in `$HOME`. + Emitted as `.<name>`. +3. `~/.*` (files) — top-level hidden regular files in `$HOME`. + Emitted as `.<name>`. + +Always skip the special entries `.` and `..`. + +### Candidate filter + +A scanned path is suggested only if **all** of the following hold: + +1. **Newer than last backup** — its mtime is greater than the last-backup + timestamp (see "lastupdate change" below). If the timestamp file is + missing (never run, or an older backup that predates this feature), treat + every path as new. +2. **Not already covered** by a `DOTFILES` entry (see "Dedup matching"). +3. **Not ignored** — its basename is not in `SUGGEST_IGNORE`. + +### Dedup matching + +The emitted path of a candidate is `c` (e.g. `.config/foobar`, `.newtool`). +For each existing `DOTFILES` entry `e`, `c` is considered **covered** if any +of: + +- `c == e` (exact match), OR +- `c` is under `e/` (e is an ancestor — e.g. `e=.config`, `c=.config/foobar`), OR +- `e` is under `c/` (c is an ancestor — e.g. `c=.config`, `e=.config/nvim`). + +If covered by any entry, the candidate is dropped. This prevents suggesting a +path already listed, a path whose parent is listed, or a parent whose child is +already listed. + +Note: `DOTFILES` entries are compared as-emitted (relative home paths and +absolute system paths). System entries (starting `/`) never match home-relative +candidates, which is correct — suggest only scans `$HOME`. + +### SUGGEST_IGNORE + +A bash array of basenames to skip. If unset (not defined in the config file), +the script falls back to a baked-in default: + +```bash +SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv \ + .mozilla .thunderbird .npm .cargo .rustup .java \ + .dbus .config) +``` + +`.config` is in the list to exclude it from the **home-dir** scan only (its +children are scanned separately as source 1). The home-dir dir/file scans check +basenames against this list; the `~/.config/*` scan does **not** apply the +`.config` skip to its children (only to deeper basenames if present). + +The user may override by defining `SUGGEST_IGNORE` in +`~/.config/dot-backup/config`. A commented example is added to `config.example`. + +Detection of "unset": use `${SUGGEST_IGNORE+set}` test so an explicitly empty +array from the user is respected and not overwritten by the default. + +## lastupdate change (required) + +The current `lastupdate()` writes a human string: + +``` +Last update: <date> +``` + +This is not machine-parseable for mtime comparison. Change `lastupdate()` to +**also** write epoch seconds to a sibling file: + +- `${DEFAULT_OUTPUT_DIR}/lastupdate` — unchanged human-readable file (kept for + display and backward compatibility). +- `${DEFAULT_OUTPUT_DIR}/lastupdate.epoch` — new file containing a single + integer: `date +%s`. + +The suggest logic reads `lastupdate.epoch`. If the file is absent or +unparseable, the last-backup timestamp is treated as `0` (everything is new). + +This is the only change to existing backup behavior. + +## Output + +When candidates exist: + +``` +New since last backup (<human date of last backup>): + .config/foobar + .newtool + .somerc + +Paste into files.list: +.config/foobar +.newtool +.somerc +``` + +- The indented top section is for human reading (colored like other output). +- The bottom block is unindented, plain, ready to paste/append to `files.list`. +- Candidates sorted alphabetically for stable output. + +When no candidates: + +``` +No new config dirs/files since last backup. +``` + +When `lastupdate.epoch` missing: + +- Still works; header reads `New since last backup (never):` and all unlisted, + unignored paths are shown. + +## Interaction with `--quiet` + +`--suggest` output goes to stdout like normal. If combined with `--quiet`, it +follows the existing redirect (writes to log). No special handling — suggest is +a read-only report, quiet just redirects it. Document this is allowed but +unusual. + +## Scope cuts (YAGNI) + +- No interactive prompt, no `y/n`, no appending to `files.list`. Print only. +- No recursion deeper than one level in any scan source. +- No mtime tracking per-path beyond the single last-backup epoch. +- No new dependencies (pure bash + coreutils `date`, `stat`). + +## Files touched + +- `dot-backup.sh`: + - Add `SUGGEST=false` flag var + `-s|--suggest` case + usage line. + - Modify `lastupdate()` to also write `lastupdate.epoch`. + - Add `SUGGEST_IGNORE` default (guarded by unset test). + - Add `do_suggest()` function (scan, filter, dedup, print). + - Early dispatch: if `SUGGEST == true`, call `do_suggest` and exit before + the backup/restore flow. +- `config.example`: add commented `SUGGEST_IGNORE` example. +- `README.md`: document the `--suggest` flag in the options/usage section. +- `CLAUDE.md`: note the flag and the `lastupdate.epoch` file. + +## Testing (manual) + +No test harness in repo. Manual verification steps: + +1. `./dot-backup.sh --suggest` with no `lastupdate.epoch` → lists all unlisted, + unignored hidden dirs/files and `.config` children. +2. Run a real backup, then `--suggest` → list shrinks to only paths newer than + the backup. +3. `mkdir ~/.config/zzztest`, then `--suggest` → `zzztest` appears. +4. Add `.config/zzztest` to `files.list`, re-run `--suggest` → it disappears + (dedup). +5. Add `zzztest` basename to `SUGGEST_IGNORE` in config (after removing from + list) → disappears (ignore). +6. `--dry-run --suggest` and `--suggest --restore` → suggest wins, exits clean. |
