Stop the deploy tier from reverting live theme state #43

Merged
dank merged 1 commits from fix/42-deploy-generated into main 2026-10-20 17:10:03 -07:00
7 changed files with 145 additions and 76 deletions
+17 -6
View File
@@ -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 <name> https://wallhaven.cc/w/<id>` + 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 <name> https://wallhaven.cc/w/<id>`
# 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-*
@@ -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
}
}
@@ -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"
}
}
+2 -2
View File
@@ -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 |
+42 -7
View File
@@ -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_<id>.<ext>` and the ID is the whole
address — `curl -L -o <name> "https://wallhaven.cc/w/<id>"` 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_<id>.<ext>` and the ID is the
whole address — `curl -L -o <name> "https://wallhaven.cc/w/<id>"` 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.
+8
View File
@@ -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
+76
View File
@@ -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 <pkg-relative path> <content>
repo_file() { mkdir -p "$(dirname "$REPO/dotfiles/$1")"; printf '%s\n' "$2" > "$REPO/dotfiles/$1"; }
# live_file <absolute path under $HOME> <content>
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" ]
}