The machine now carries its own manual for AI hands
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
+184
-13
@@ -5,16 +5,17 @@
|
||||
# Author: Gabriel Brown
|
||||
#
|
||||
# Commands:
|
||||
# update Bring this machine up to date: pull, then install --upgrade
|
||||
# sync Review, commit & push local changes to this repo
|
||||
# edit Open the Panama repo in Neovim
|
||||
# doctor Report what is actually running on this machine
|
||||
# test Run every contract under tests/
|
||||
# upgrade Re-run the installer from anywhere, interview included
|
||||
# migrate Apply repairs this machine has not had yet
|
||||
# apps Choose applications to install, by category
|
||||
# app Build and install an application that no repository packages
|
||||
# help Show this help
|
||||
# update Bring this machine up to date: pull, then install --upgrade
|
||||
# sync Review, commit & push local changes to this repo
|
||||
# edit Open the Panama repo in Neovim
|
||||
# doctor Report what is actually running on this machine
|
||||
# test Run every contract under tests/ (--safe skips the hijacking ones)
|
||||
# contracts Name the contracts that mention a given file
|
||||
# upgrade Re-run the installer from anywhere, interview included
|
||||
# migrate Apply repairs this machine has not had yet
|
||||
# apps Choose applications to install, by category
|
||||
# app Build and install an application that no repository packages
|
||||
# help Show this help
|
||||
#
|
||||
# update and sync are deliberately separate verbs. One acts on the machine, the
|
||||
# other on the repository, and a single command that guessed between them by
|
||||
@@ -82,6 +83,11 @@ ${BOLD}Commands:${RESET}
|
||||
than what was installed. Takes --summary for one line per check.
|
||||
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
|
||||
a subset: 'panama test dock' runs the ones matching 'dock'.
|
||||
--safe skips the ones that take over the live desktop; what
|
||||
they are and why is tests/desktop-hijacking.
|
||||
${GREEN}contracts${RESET} Name the contracts that mention a given file, each marked
|
||||
safe or desktop. A heuristic over the text of tests/, so it
|
||||
answers "what should I run" rather than "what covers this".
|
||||
${GREEN}upgrade${RESET} Re-run ./install from anywhere, interview and all. For a new
|
||||
machine, or to change an answer you gave. Routine updates are
|
||||
'$PROGRAM update', which asks nothing.
|
||||
@@ -106,6 +112,8 @@ ${BOLD}Examples:${RESET}
|
||||
$PROGRAM edit
|
||||
$PROGRAM doctor --summary
|
||||
$PROGRAM test dock
|
||||
$PROGRAM test --safe
|
||||
$PROGRAM contracts config/dot/quickshell/services/Displays.qml
|
||||
$PROGRAM upgrade
|
||||
$PROGRAM apps
|
||||
$PROGRAM app
|
||||
@@ -340,6 +348,32 @@ cmd_doctor() {
|
||||
exec "$doctor" "$@"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# The desktop-hijacking ledger
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# tests/desktop-hijacking lists, one repo-relative path per line with a '#'
|
||||
# comment saying what it does to the live session, the contracts that drive the
|
||||
# real shell, compositor or machine rather than a harness. Read by `test --safe`
|
||||
# to decide what to skip, and by `contracts` to mark each hit.
|
||||
#
|
||||
# Prints the paths, comments and blank lines stripped. A missing ledger prints
|
||||
# nothing: no ledger means nothing is known to hijack, which is the honest
|
||||
# reading of an absent file and keeps `--safe` from failing on a fresh checkout.
|
||||
DESKTOP_HIJACKING_LEDGER="tests/desktop-hijacking"
|
||||
|
||||
hijacking_entries() {
|
||||
local ledger="$PANAMA_DIR/$DESKTOP_HIJACKING_LEDGER" line
|
||||
[[ -r "$ledger" ]] || return 0
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%%#*}"
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[[ -n "$line" ]] && printf '%s\n' "$line"
|
||||
done < "$ledger"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: test
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -351,9 +385,34 @@ cmd_doctor() {
|
||||
# Each runs in its own process and a failure does not stop the rest, because the
|
||||
# useful output is the whole list of what is broken rather than the first thing
|
||||
# that broke. The exit code is what a caller can act on.
|
||||
#
|
||||
# --safe exists because a fair number of these contracts ARE the desktop: they
|
||||
# open overlays, restart the shell, move your windows. Running the suite while
|
||||
# sitting in front of the machine used to mean losing the session for a few
|
||||
# minutes, so the honest options were "run everything" or "run nothing". --safe
|
||||
# is the third: skip exactly what tests/desktop-hijacking names, and say how
|
||||
# many were skipped, so the gap is stated rather than implied.
|
||||
cmd_test() {
|
||||
local pattern="${1:-}"
|
||||
local pattern="" safe=0 arg
|
||||
# Position-independent, because 'panama test --safe dock' and
|
||||
# 'panama test dock --safe' are the same intent and nobody should have to
|
||||
# remember which one this accepts.
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--safe) safe=1 ;;
|
||||
*) pattern="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local -a suite=()
|
||||
local -A hijacking=()
|
||||
local skipped=0 entry rel
|
||||
|
||||
if (( safe )); then
|
||||
while IFS= read -r entry; do
|
||||
hijacking["$entry"]=1
|
||||
done < <(hijacking_entries)
|
||||
fi
|
||||
|
||||
# Executables, plus the Python suites. Those are unittest files rather than
|
||||
# executables, and collecting only what has the executable bit would skip them
|
||||
@@ -361,11 +420,25 @@ cmd_test() {
|
||||
# A runner with a blind spot is worse than no runner, because it reports PASS.
|
||||
while IFS= read -r path; do
|
||||
[[ -x "$path" || "$path" == *_test.py ]] || continue
|
||||
[[ -z "$pattern" || "$path" == *"$pattern"* ]] && suite+=("$path")
|
||||
[[ -z "$pattern" || "$path" == *"$pattern"* ]] || continue
|
||||
rel="tests/${path#"$PANAMA_DIR"/tests/}"
|
||||
if (( safe )) && [[ -n "${hijacking[$rel]:-}" ]]; then
|
||||
(( ++skipped ))
|
||||
continue
|
||||
fi
|
||||
suite+=("$path")
|
||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||
|
||||
if (( ${#suite[@]} == 0 )); then
|
||||
err "No contracts match '${pattern}'"
|
||||
# "Nothing matched" and "everything that matched was skipped" are different
|
||||
# answers, and reporting the first for the second is how --safe would come
|
||||
# to look like a broken pattern.
|
||||
if (( skipped > 0 )); then
|
||||
err "Every contract matching '${pattern}' is desktop-hijacking; --safe skipped all ${skipped}."
|
||||
printf ' What they do to the session: %s/%s\n' "$PANAMA_DIR" "$DESKTOP_HIJACKING_LEDGER" >&2
|
||||
else
|
||||
err "No contracts match '${pattern}'"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -391,14 +464,111 @@ cmd_test() {
|
||||
header "Result"
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
ok "${#suite[@]} contract(s) passed"
|
||||
if (( safe )); then
|
||||
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
err "${#failed[@]} of ${#suite[@]} failed:"
|
||||
printf ' %s\n' "${failed[@]}" >&2
|
||||
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}"
|
||||
if (( safe )); then
|
||||
printf 'Skipped %d desktop-hijacking contract(s) (%s).\n' "$skipped" "$DESKTOP_HIJACKING_LEDGER"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: contracts
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# "I changed this file -- what should I run?" There are 177 contracts and no
|
||||
# index, so the honest answers were "all of them" (minutes, and half of them
|
||||
# take the desktop away) or "the ones whose name sounds related" (which is how
|
||||
# a covering contract gets skipped).
|
||||
#
|
||||
# This is a grep, and says so. A contract that names the file, or a
|
||||
# parent-trimmed suffix of it, or just its basename, is a contract worth
|
||||
# running; one that reaches the file through a harness or a generated artifact
|
||||
# is not found, which is why the empty answer says "coverage may be indirect"
|
||||
# rather than "nothing covers this". Naming a file the suite does not mention is
|
||||
# a real answer -- exit 1 so a script can tell the difference -- but it is a
|
||||
# statement about this search, not about the file.
|
||||
#
|
||||
# Each hit is marked from tests/desktop-hijacking, so the output also answers
|
||||
# "and can I run them right now".
|
||||
cmd_contracts() {
|
||||
local target="${1:-}"
|
||||
if [[ -z "$target" ]]; then
|
||||
err "Which file? Usage: ${BOLD}$PROGRAM contracts <file>${RESET}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Absolute, relative to where you are standing, or repo-relative -- all three
|
||||
# are how somebody refers to a file in this tree, and readlink resolves the
|
||||
# symlinked dotfile in ~/.config back into the checkout it points at.
|
||||
local absolute=""
|
||||
if [[ -e "$target" ]]; then
|
||||
absolute="$(readlink -f "$target")"
|
||||
elif [[ -e "$PANAMA_DIR/$target" ]]; then
|
||||
absolute="$(readlink -f "$PANAMA_DIR/$target")"
|
||||
else
|
||||
err "No such file: '$target'"
|
||||
printf 'Give a path, absolute or relative to here or to %s.\n' "$PANAMA_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local path
|
||||
case "$absolute" in
|
||||
"$PANAMA_DIR"/*) path="${absolute#"$PANAMA_DIR"/}" ;;
|
||||
*)
|
||||
err "'$target' is outside the Panama repo (${PANAMA_DIR})."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# The repo-relative path, then each parent trimmed off in turn, ending at the
|
||||
# basename. Contracts refer to their subject every one of these ways: by the
|
||||
# full path from the repo root, by the path from the shell directory, and by
|
||||
# name alone.
|
||||
local -a patterns=()
|
||||
local suffix="$path"
|
||||
while :; do
|
||||
patterns+=(-e "$suffix")
|
||||
[[ "$suffix" == */* ]] || break
|
||||
suffix="${suffix#*/}"
|
||||
done
|
||||
|
||||
local -A hijacking=()
|
||||
local entry
|
||||
while IFS= read -r entry; do
|
||||
hijacking["$entry"]=1
|
||||
done < <(hijacking_entries)
|
||||
|
||||
# The same collection `test` runs, so anything named here is something the
|
||||
# runner would actually execute.
|
||||
local -a hits=()
|
||||
local candidate rel
|
||||
while IFS= read -r candidate; do
|
||||
[[ -x "$candidate" || "$candidate" == *_test.py ]] || continue
|
||||
grep -qF "${patterns[@]}" "$candidate" 2>/dev/null || continue
|
||||
hits+=("tests/${candidate#"$PANAMA_DIR"/tests/}")
|
||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||
|
||||
if (( ${#hits[@]} == 0 )); then
|
||||
printf 'No contract mentions %s — coverage may be indirect (a harness or a generated artifact); nothing verified.\n' "$path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for rel in "${hits[@]}"; do
|
||||
if [[ -n "${hijacking[$rel]:-}" ]]; then
|
||||
printf '%s [desktop]\n' "$rel"
|
||||
else
|
||||
printf '%s [safe]\n' "$rel"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: upgrade
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -636,6 +806,7 @@ main() {
|
||||
edit) shift; cmd_edit "$@" ;;
|
||||
doctor) shift; cmd_doctor "$@" ;;
|
||||
test) shift; cmd_test "$@" ;;
|
||||
contracts) shift; cmd_contracts "$@" ;;
|
||||
upgrade) shift; cmd_upgrade "$@" ;;
|
||||
migrate) shift; cmd_migrate "$@" ;;
|
||||
app) shift; cmd_app "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user