testsAndMisc/docs/shell-split-verification.md
Krzysztof kuhy Rudnicki 23baf9d63f shell splits: add a verbatim-move harness and triage all 29 targets
The five Python splits this session were made safe by a throwaway ast
identity check. There is no ast for bash, so meta/scripts/verify_shell_split.sh
does the same job with shfmt -mn (minify: strips comments and indentation,
keeps logic), hashing each top-level function and naming the one that differs.
shfmt is already a REQUIRED_TOOLS binary, so this adds no dependency.

Validated in all three directions before trusting it: an unchanged copy is
IDENTICAL, a copy with blank lines inserted is IDENTICAL, and a one-character
logic change ($1 -> $2) is caught and named. The first mutation attempt used
sed 's/exit 1/exit 2/' on a file with no 'exit 1' and reported IDENTICAL - the
harness was fine, the test was vacuous. That trap is now documented, because a
normalizer that silently passes everything is worse than none.

Also triaged all 29 over-cap shell scripts for deployed copies, which the
trace harness cannot see. The finding that changes the plan: deploy.sh copies
a hardcoded per-file list into /data/local/tmp/focus_mode and never deploys
lib/, so an entry+lib seam would break focus mode on the phone at its first
source - for 8 of the 29, not the 2 the brief named. Those scripts source only
config.sh, which is on the copy list, so a sibling-file seam works provided
deploy.sh's list grows in the same commit.

16 scripts have no deployed copy and are safe to split conventionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 14:07:20 +02:00

137 lines
7.0 KiB
Markdown

# Shell split verification: how a green split is still broken
Split out of `docs/shell-split-recipes.md` when that file hit the same 250-line
cap it exists to serve. That file covers **how to make** a split; this one
covers **how to find out it is wrong**. Read both before the next
`linux_configuration` or `phone_focus_mode` split.
Every section below cost a failed run, a revert, or — once — a live system
mutation.
### A `--skip-install` flag is not evidence the script is safe to run
`shell_check.sh` gates only `install_if_missing`'s pacman branch on
`SKIP_INSTALL`. The AUR branch inside `install_linters` runs regardless, so a
backgrounded "baseline" run installed four packages before it was killed
mid-`makepkg`. Decision 6 was violated by a flag that looked like permission.
Before running any script to capture a baseline, grep the flag's variable and
confirm it gates **every** mutation path, not just the first one:
```bash
grep -n 'SKIP_INSTALL\|DRY_RUN' <script> # then read each hit
```
`--list-only` was the genuinely safe path here, because it returns before
`install_linters` is reached at all.
### Check whether the target is DEPLOYED as a single file — the trace cannot
Before designing seams, find out whether some other script copies the target
somewhere and runs it from there. An entry+`lib/` shape ships an entry whose
deployed `SCRIPT_DIR` has no `lib/`, so it dies on its first `source` under
`set -e` — while your trace stays green, because the trace runs the repo copy
where `lib/` exists.
```bash
grep -rn '<target-basename>' linux_configuration/ --include='*.sh' \
| grep -v '<the target itself>:'
```
Confirmed blocked on this, not theoretically: `install_leechblock.sh` (the
pacman installer `cp`s it to `/usr/local/share/digital_wellbeing/` and
`pacman_wrapper.sh:831` prefers that copy on **every pacman invocation**) and
`block_compulsive_opening.sh` (`install_all` copies itself to
`/usr/local/bin`). A target invoked as `$CONFIG_DIR/scripts/...` is fine.
### A stubbed run cannot reach a bug in a seam that passes state
`meta/scripts/verify_shell_split.sh` and the hand-rolled `declare -F` probe
both **source the libs and never call the functions**. That is enough to prove
every `source` line resolves, and nothing more. The `analyze_repo.sh` split
passed `bash -n`, `shellcheck`, a function-set diff, a line-set diff and the
stubbed probe — and still shipped a script that aborted immediately after
language detection. Two separate bugs, neither reachable without executing:
- **`set -e` and the function tail.** Eight `((LANG_FILES[x] > 0)) && HAS_Y=true`
lines were top-level statements, where a false `&&` merely leaves `$?`
non-zero. Wrapped into a function they become its _return value_, and the
last one (java, false for most repos) killed the script. Any time you wrap
former top-level code in a function, check what its **last statement** is;
append `|| true` if it is a bare conditional.
- **A self-referencing nameref.** `local -n LANG_CODE_FILES="$1"` pointing at a
global of the same name still moves data on bash 5.3, but warns
`circular name reference` on _every access_ — straight into the script's
output. Name the local something else (`_code_files`).
So: when a seam passes state, run the thing. Stub only the parts that mutate
the system, and diff the generated artifacts against a detached worktree at the
pre-split commit. That comparison is what proved the fix — same file set, every
text artifact byte-identical.
## The harness and the fixtures
Everything about _running_ the check — `trace_shell_split.sh`, the `--prefix`
mode for scripts whose job is placing files, the regression fixtures, the
entry-point and `shfmt` traps — moved to
**`docs/shell-split-harness.md`** when this file hit the same 250-line cap it
exists to serve. This file stays the _why_; that one is the _how_.
### Prove the move was verbatim: `meta/scripts/verify_shell_split.sh`
The Python splits in this repo are verified with a throwaway `ast` identity
check. There is no `ast` for bash, so the shell equivalent normalises through
`shfmt -mn` (minify: strips comments and indentation, keeps logic) and compares
a hash per top-level function:
```bash
bash meta/scripts/verify_shell_split.sh HEAD <old-path> <new-path>...
# IDENTICAL: all N top-level functions moved verbatim
```
It reports which function changed, so a diff points at the seam rather than the
whole file. Run it after every split, and again after `shfmt`/pre-commit
autofixes — reordering an import or reflowing a line is exactly what it is
designed to see through.
**Verify the harness before trusting it.** A normalizer that silently passes
everything is worse than none. The three checks it must survive:
| input | expected |
| ------------------------------------------------ | --------------------------------------- |
| unchanged copy of the file | `IDENTICAL` |
| comments/blank lines/indentation changed | `IDENTICAL` |
| one character of logic changed (`"$1"` → `"$2"`) | `DIFFERENCE FOUND`, naming the function |
The first attempt at that mutation test used `sed 's/exit 1/exit 2/'` on a file
containing no `exit 1`. It reported `IDENTICAL` — not because the harness was
broken, but because the mutation never applied. Confirm the mutation actually
changed the file (`diff` it) before concluding anything about the harness.
### Deployment triage of the over-cap scripts (2026-08-17)
Run before designing any seam, per the section above. `deploy.sh` copies a
**hardcoded per-file list** into `/data/local/tmp/focus_mode` and never deploys
`lib/`:
- **`phone_focus_mode/*` (8 over-cap scripts)** — deployed individually.
A new sibling must be added to `deploy.sh`'s copy list **in the same commit**,
or focus mode dies on the phone at its first `source`. These scripts source
only `config.sh`, which is on that list, so a sibling-file seam is workable —
an entry+`lib/` seam is not.
- **`install_leechblock.sh`, `block_compulsive_opening.sh`** — copied to
`/usr/local/…` and preferred there by `pacman_wrapper.sh:831` on every pacman
invocation. Teach the installer to deploy the directory first, in its own
commit. Highest blast radius; do these last.
- **`hosts/install.sh`, `setup_hosts_guard.sh`, `lib/monitor.sh`** — install
helper scripts to `/usr/local/sbin` or `/usr/local/bin`; check whether the
file being split is itself the deployed one.
- **The other 16** (`fresh-install/main.sh`, `nvidia_troubleshoot.sh`,
`setup_passwordless_system.sh`, `clean_audio.sh`, `enforce_vbox_hosts.sh`,
`migrate_hosts_guard_to_guard_lib.sh`, `libre_translate.sh`,
`diagnose_pacman_hook_stall.sh`, `install_plagiarism_tools.sh`,
`steam_compatibility.sh`, `deploy.sh`, `setup_night_lockdown.sh`,
`pacman_wrapper.sh`, `generate_study_materials.sh`,
`check_and_enable_services.sh`, `setup_midnight_shutdown.sh`) — no deployed
copy found, so an entry+`lib/` shape is safe.