From 3022277167d99e31b6db77b55600dc2435ec18c0 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 17:08:06 -0700 Subject: [PATCH] Stop the deploy tier from reverting live theme state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from verifying the #41 merge against the live box with `deploy.sh --check`. One commit because they share the deploy tier, its documentation, and the first test file that tier ever had. **1. Generated palette output was tracked, and sync would have restored it over a live theme.** `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` (`b3d3df4`, 2026-10-18) sat byte-identical to the live file — harmless while the palette held, and `--check` was already proposing `ADOPT … noctalia.kdl`. The moment the theme changed, the next sync would have quarantined the live file and reinstated 2026-10-18's colours. `deploy.sh` now refuses the class outright (`SKIP … (generated theme output)`), `.gitignore` carries the matching patterns so it cannot be re-added, and the file is untracked. **2. A `zz` override older than the file it overrides.** `dotfiles/zz-blankpad/niri/…/ layout.kdl` held focus ring `width 2` / `#e01b24` (`7b7dbf5`, 2026-10-17) while base absorbed `b3d3df4` (2026-10-18): `width 4`, `#ff8c00`. Stow order makes `zz-*` win, and a cross-package owner change relinks *silently* — no quarantine, no warning. Live was linked to base, the newer intent, so `--check` printed `RELINK … layout.kdl` every run and one unattended `deploy.sh` would have reverted the amber focus ring. The duplicate is deleted; base owns the file. **3. Two topologies I had asserted and not measured.** The media law and the `.gitignore` comment both said `~/.local/share/noctalia/wallpapers` symlinks into the repo, so deleting the tracked copies would blank the desktop. Measured: it is a real directory of real files that Noctalia owns; the repo held a duplicate, and the #41 merge deleted that duplicate without the desktop flickering. The correct general law replaces the false one — untracking a path deletes the file in every other checkout, `git rm --cached` on the branch notwithstanding — and the restore procedure now points at the app's directory, which `deploy.sh` does not manage. The guard is `tests/deploy.bats`, black-box over exit status, stdout, the symlink shape under `$HOME`, and the quarantine directory. Before the change: test 1 and test 2 fail, test 2 because the live palette really is quarantined and reverted. After: 4/4, including the control that an ordinary identical config is still adopted — an exclusion nobody can turn off is just a broken tool. Verification: `bats tests/` 66/66, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0 issues, and live `deploy.sh --check` on blankpad reports `SKIP … (generated theme output)` with `actions=0` instead of `ADOPT … noctalia.kdl`. The real `deploy.sh` run that clears the remaining `RELINK` lines is deliberately left until this is in main — running it from a worktree would repoint live symlinks into a checkout the heartbeat reaps. Refs #42 --- .gitignore | 23 ++++-- .../niri/.config/niri/cfg/layout.kdl | 29 ------- .../niri/.config/niri/noctalia.kdl | 32 -------- omp-docs/inventory/home-config-census.md | 4 +- omp-docs/tools/dotfiles-deploy.md | 49 ++++++++++-- scripts/deploy.sh | 8 ++ tests/deploy.bats | 76 +++++++++++++++++++ 7 files changed, 145 insertions(+), 76 deletions(-) delete mode 100644 dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl delete mode 100644 dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl create mode 100755 tests/deploy.bats diff --git a/.gitignore b/.gitignore index bc12bf6..3264ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -80,10 +80,21 @@ wt/ agent/memories/--home-blank-.omp-wt-* # --- theme media: content the pipeline consumes, not configuration --------- -# 23 wallpapers, 80 MiB. Tracked until 2026-10-20; every clone of a config repo -# was carrying a library no diff can review. The files stay on disk because -# ~/.local/share/noctalia/wallpapers symlinks into here, and every filename -# carries its wallhaven ID, so a fresh box restores them with -# `curl -L -o https://wallhaven.cc/w/` + deploy.sh. -# Law: omp-docs/tools/dotfiles-deploy.md +# 23 wallpapers, 80 MiB. Tracked until 2026-10-20: every clone of a config repo was +# carrying a library no diff can review. The live library at +# ~/.local/share/noctalia/wallpapers is a real directory of real files (measured +# 2026-10-20), so this path in the repo was a duplicate of it, not its source — and +# note that untracking deletes the copy in every other checkout regardless of +# `git rm --cached` on the branch. A fresh box starts with an empty library; every +# filename carries its wallhaven ID, so `curl -L -o https://wallhaven.cc/w/` +# puts it back. Law: omp-docs/tools/dotfiles-deploy.md dotfiles/noctalia/.local/share/noctalia/wallpapers/ + +# --- generated theme output: machine-written, never reviewable, never deployed --- +# noctalia/matugen rewrite these in place; a tracked copy is a stale snapshot that +# deploy.sh would push back over a live palette. Patterns match the exclusion list in +# scripts/deploy.sh. Law: omp-docs/inventory/home-config-census.md, Refs #42 +dotfiles/**/themes/noctalia* +dotfiles/**/colors/noctalia* +dotfiles/**/noctalia.* +dotfiles/**/noctalia-* diff --git a/dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl b/dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl deleted file mode 100644 index 696ef68..0000000 --- a/dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl +++ /dev/null @@ -1,29 +0,0 @@ - layout { - gaps 16 // Gap between windows - center-focused-column "never" // Don’t auto-center focused column - - background-color "transparent" // <- needed for noctalia-shell to set wallpaper - - preset-column-widths { - proportion 0.25 // 1/4 - proportion 0.3333 // 1/3 - proportion 0.5 // 1/2 - } - - preset-window-heights { - proportion 0.25 // 1/4 - proportion 0.5 // 1/2 - } - - struts { - } - shadow - focus-ring { - width 2 - - active-color "#e01b24" - } - border { - off - } -} diff --git a/dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl b/dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl deleted file mode 100644 index d648619..0000000 --- a/dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl +++ /dev/null @@ -1,32 +0,0 @@ -layout { - - focus-ring { - active-color "#ff8c00" - inactive-color "#181818" - urgent-color "#ff8c00" - } - - border { - active-color "#f43841" - inactive-color "#181818" - urgent-color "#f43841" - } - - - tab-indicator { - active-color "#f43841" - inactive-color "#74050b" - urgent-color "#f43841" - } - - insert-hint { - color "#f4384180" - } -} - -recent-windows { - highlight { - active-color "#f43841" - urgent-color "#f43841" - } -} diff --git a/omp-docs/inventory/home-config-census.md b/omp-docs/inventory/home-config-census.md index 5f1fbd4..5ae59e6 100644 --- a/omp-docs/inventory/home-config-census.md +++ b/omp-docs/inventory/home-config-census.md @@ -83,8 +83,8 @@ by category, not file-by-file. | micro | 0 | 154 | adopt: `settings.json`, `bindings.json`; generated: `colorschemes/noctalia.micro`; bundled/state: `syntax/*` (146), `buffers/history` | | mpv | 0 | 0 | — (empty) | | nautilus | 0 | 0 | — (empty) | -| niri | 9 | 1 | 8 live KDL includes (5 from `dotfiles/niri`, 3 overridden by the `zz-blankpad` tier, whose own 9 niri files are counted in that tier) + 1 tracked relic, see *Never track*. Box-only: `noctalia.kdl`, a generated include — [tools/dotfiles-deploy](/tools/dotfiles-deploy.md) | -| noctalia | 3 | 2672 | Tracked and live-linked: `config.toml`, `palettes/Gruber Red.json`, `state/noctalia/settings.toml`. Everything else is app state plus noctalia's downloaded catalog: `community-palettes/` 66, `community-templates/` 80, `wallhaven/thumbs`, `clipboard/`, `plugins/`, `state.toml`, `usage_counts.json`. The 23 wallpapers #41 untracks stay on disk — the theme reads them | +| niri | 8 | 2 | 8 live KDL includes (5 from `dotfiles/niri`, 3 overridden by the `zz-blankpad` tier, whose own 7 niri files are counted in that tier — the stale `layout.kdl` duplicate and generated `noctalia.kdl` were removed by #43) + see *Never track* for the relic. Box-only: `noctalia.kdl` (generated include, refused by `deploy.sh`) and the retired copy | +| noctalia | 3 | 2672 | Tracked and live-linked: `config.toml`, `palettes/Gruber Red.json`, `state/noctalia/settings.toml`. Everything else is app state plus noctalia's downloaded catalog: `community-palettes/` 66, `community-templates/` 80, `wallhaven/thumbs`, `clipboard/`, `plugins/`, `state.toml`, `usage_counts.json`. The app's wallpaper library is `~/.local/share/noctalia/wallpapers` — real files it owns; the repo's duplicate copy of 23 JPEGs was untracked by #41 and deleted from this checkout by the merge, which is what untracking does in every checkout | | nvim | 0 | 0 | — (empty) | | pywalfox | 0 | 1 | `config.json` — program absent, prune | | qt5ct | 0 | 1 | `colors/noctalia.conf` (generated) — program absent, prune | diff --git a/omp-docs/tools/dotfiles-deploy.md b/omp-docs/tools/dotfiles-deploy.md index b3f92b8..87d0fff 100644 --- a/omp-docs/tools/dotfiles-deploy.md +++ b/omp-docs/tools/dotfiles-deploy.md @@ -42,14 +42,18 @@ Wallpapers live at `dotfiles/noctalia/.local/share/noctalia/wallpapers/` and are gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every clone of a *configuration* repo carried a media library no diff can review, and the payload only ever grows because deleting a tracked blob from the tree does not delete it -from history. [Noctalia](noctalia-shell.md) reads -`~/.local/share/noctalia/wallpapers/`, which is a set of symlinks into that directory, so -the files stay on disk and untracking them broke nothing on this box. +from history. The topology is not what one would guess: [Noctalia](noctalia-shell.md) +keeps its own library at `~/.local/share/noctalia/wallpapers/` as a **real directory of +real files**, so the tracked tree was a duplicate of it and never its source — measured +2026-10-20, when the merge of #41 deleted the repo copies and the desktop did not +flicker. Two consequences to know before untracking media: the copy that survives belongs +to the application, not to the repo; and untracking deletes the file in every other +checkout, `git rm --cached` on the branch notwithstanding. -Restore on a fresh box: every filename is `wallhaven_.` and the ID is the whole -address — `curl -L -o "https://wallhaven.cc/w/"` puts the library back, then -`deploy.sh` relinks it. Losing the set is an acceptable outcome; 80 MB per clone forever -is not. +Restore on a fresh box: the library lives in `~/.local/share/noctalia/wallpapers/`, a path +`deploy.sh` does not manage. Every filename is `wallhaven_.` and the ID is the +whole address — `curl -L -o "https://wallhaven.cc/w/"` into that directory puts +the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not. ## Law: never `systemctl --user enable` a unit from a worktree @@ -90,3 +94,34 @@ tracked in `dotfiles/zz-blankpad/`; the pointer is not, because which desk the b is not a fact about the repo. A fresh box therefore starts with no output configuration — `config.kdl` includes `display.kdl` with `optional=true`, so niri comes up usable but with default monitors until `desk.sh` is run once. + +## Law: generated theme output is never deployed + +noctalia rewrites palette output in place — `noctalia.*`, `themes/noctalia*`, +`colors/noctalia*`, `noctalia-*` (the classes the [census](/inventory/home-config-census.md) +proves by same-millisecond mtime). A tracked copy of one is a stale snapshot of a colour +scheme, and the failure is quiet: `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` +(commit `b3d3df4`, 2026-10-18) sat byte-identical to the live file for two days, harmless, +while `--check` cheerfully proposed `ADOPT … noctalia.kdl`. The moment the palette changed, +the next sync would have quarantined the live file and restored 2026-10-18's colours — +a tool silently reverting a user-visible theme choice. + +`deploy.sh` now refuses the class (`SKIP … (generated theme output)`), `.gitignore` carries +the matching patterns so it cannot be re-added, and `tests/deploy.bats` pins both +directions: generated output is never adopted, and an ordinary identical config still is. +Untracked in #43, Refs #42. + +## Law: a `zz` override older than the file it overrides is a time bomb + +Stow order means `zz-$HOST` wins, and when both packages carry the same path, sync relinks +it *silently* — the script's own comment: "managed file changing owners between packages: +silent relink, nothing to quarantine". `dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl` +held focus ring `width 2` / `active-color "#e01b24"` from `7b7dbf5` (2026-10-17) while the +base package absorbed `b3d3df4` (2026-10-18): `width 4`, `"#ff8c00"`. Live was linked to +base, the newer intent, so `--check` printed `RELINK .config/niri/cfg/layout.kdl` on every +run and one unattended `deploy.sh` would have reverted the amber focus ring with no +quarantine and no warning. The duplicate is deleted in #43. + +Rule: a `zz` file exists only to differ. When base absorbs the change, delete the `zz` copy +in the same commit — and never read `RELINK` lines from `--check` as cosmetic noise; they +are the sound of a stale override waiting for someone to run the tool for real. diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 34c2a68..2dfa535 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -44,6 +44,14 @@ actions=0; quarantined=0; skipped=0 deploy_file() { # $1 = repo file path, $2 = home-relative target path local src="$1" tgt="$HOME/$2" + # Generated theme output is never deployment state: noctalia rewrites these files in + # place, so a tracked copy is a stale snapshot and syncing one reverts a live palette to + # the colours of the day it was committed. Classes are those the census measures by + # mtime — see omp-docs/inventory/home-config-census.md, "Generated palette expansion". + case "$2" in + */themes/noctalia*|*/colors/noctalia*|*/noctalia.*|*/noctalia-*|*/Noctalia*) + echo "SKIP $2 (generated theme output)"; skipped=$((skipped+1)); return 0 ;; + esac if [ -L "$tgt" ]; then if [ "$(realpath "$tgt")" = "$(realpath "$src")" ]; then return 0; fi # already deployed case "$MODE" in diff --git a/tests/deploy.bats b/tests/deploy.bats new file mode 100755 index 0000000..d47fdd8 --- /dev/null +++ b/tests/deploy.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats +# tests/deploy.bats — black-box tests for scripts/deploy.sh (Refs #42) +# +# Contract (see script header): the repo checkout IS the deployment; sync adopts +# identicals, links new files, quarantines-and-replaces divergents, and stows the +# zz-$(hostname) package last. One thing it must NEVER do is deploy generated theme +# output: noctalia rewrites `noctalia.*` / `themes/noctalia*` in place, so a tracked +# copy is a stale snapshot, and a sync would revert a live palette to whatever colour +# scheme was current on the day it was committed. +# +# Nothing is observed except exit status, stdout, the file/symlink shape under $HOME, +# and the quarantine directory. + +setup() { + TEST_TMP="$(mktemp -d)" + REPO="$TEST_TMP/repo" + DEPLOY="$BATS_TEST_DIRNAME/../scripts/deploy.sh" + mkdir -p "$REPO/scripts" "$REPO/dotfiles" + # A git repo, because the deployment checkout IS one and deploy.sh arms hooksPath. + git init -q -b main "$REPO" + cp "$DEPLOY" "$REPO/scripts/deploy.sh" + chmod +x "$REPO/scripts/deploy.sh" + export HOME="$TEST_TMP/home" + mkdir -p "$HOME" + export DEPLOY_REPO="$REPO" DEPLOY_HOST=box +} + +teardown() { rm -rf "$TEST_TMP"; } + +# repo_file +repo_file() { mkdir -p "$(dirname "$REPO/dotfiles/$1")"; printf '%s\n' "$2" > "$REPO/dotfiles/$1"; } +# live_file +live_file() { mkdir -p "$(dirname "$1")"; printf '%s\n' "$2" > "$1"; } + +@test "generated theme output is never adopted into a symlink" { + repo_file "ghostty/.config/ghostty/themes/noctalia" 'background = "#f43841"' + live_file "$HOME/.config/ghostty/themes/noctalia" 'background = "#f43841"' + run "$REPO/scripts/deploy.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"SKIP"*".config/ghostty/themes/noctalia"* ]] + [ -f "$HOME/.config/ghostty/themes/noctalia" ] + [ ! -L "$HOME/.config/ghostty/themes/noctalia" ] +} + +@test "a diverged live palette is never quarantined and reverted to a stale copy" { + # The dangerous shape: the tracked file matches today's palette, the theme changes + # tomorrow, and sync would quarantine the live file and restore the stale one. + repo_file "niri/.config/niri/noctalia.kdl" 'active-color "#e01b24"' + live_file "$HOME/.config/niri/noctalia.kdl" 'active-color "#ff8c00"' + run "$REPO/scripts/deploy.sh" + [ "$status" -eq 0 ] + grep -q 'active-color "#ff8c00"' "$HOME/.config/niri/noctalia.kdl" + [ ! -L "$HOME/.config/niri/noctalia.kdl" ] + [ ! -d "$REPO/.quarantine" ] +} + +@test "an ordinary identical config is still adopted" { + # Control against an exclusion so broad the tool stops deploying anything. + repo_file "ghostty/.config/ghostty/config.ghostty" 'theme = "gruber"' + live_file "$HOME/.config/ghostty/config.ghostty" 'theme = "gruber"' + run "$REPO/scripts/deploy.sh" + [ "$status" -eq 0 ] + [ -L "$HOME/.config/ghostty/config.ghostty" ] + [ "$(realpath "$HOME/.config/ghostty/config.ghostty")" = "$(realpath "$REPO/dotfiles/ghostty/.config/ghostty/config.ghostty")" ] +} + +@test "--check reports and writes nothing" { + repo_file "ghostty/.config/ghostty/config.ghostty" 'theme = "gruber"' + live_file "$HOME/.config/ghostty/config.ghostty" 'theme = "tokyo-night"' + run "$REPO/scripts/deploy.sh" --check + [ "$status" -eq 0 ] + [[ "$output" == *"DRIFT"* || "$output" == *"QUAR"* ]] + [ ! -L "$HOME/.config/ghostty/config.ghostty" ] + grep -q 'theme = "tokyo-night"' "$HOME/.config/ghostty/config.ghostty" + [ ! -d "$REPO/.quarantine" ] +} -- 2.54.0