diff --git a/docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json b/docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json new file mode 100644 index 00000000..2fd1fef6 --- /dev/null +++ b/docs/superpowers/evidence/prettier-gate-result-cache-2026-09-11.json @@ -0,0 +1,47 @@ +{ + "intent": "Make `git push` return in about a second instead of ~13s, by not paying Node's startup cost to re-check markdown/JSON/YAML files whose content, prettier version and prettier config are all unchanged since their last clean check.", + "scope": [ + "meta/scripts/run_prettier_capped.sh - the prettier hook's runner", + "endurain/README.md and linux_configuration/.github/copilot-instructions.md - reflowed by prettier after the ~ reorganisation lengthened paths inside them", + "Non-goal: changing what prettier checks, its config, or the ci-mirror gate. The set of files that must be clean is identical before and after.", + "Non-goal: the first push after a requirements.txt change, which still rebuilds a clean venv by design." + ], + "changes": [ + "run_prettier_capped.sh now keys a result cache on sha256(file content) plus a fingerprint of `prettier --version` and every prettier config file, so a cached verdict cannot survive an edit, a prettier upgrade or a config change.", + "Files with a cached clean verdict are dropped from the argv; when the argv empties, prettier is never started at all.", + "Only a clean verdict is recorded - a failing check is re-run next time, so the cache can never mask a formatting regression.", + "The cache is wholesale-pruned past 20000 entries, which costs one slow run rather than correctness.", + "The systemd-run memory cap (MemoryMax=512M, MemorySwapMax=0) is preserved exactly, per the oom-prevention skill." + ], + "verification": [ + { + "command": "bash meta/scripts/run_prettier_capped.sh <40 tracked markdown files> # cold, then warm", + "result": "pass", + "evidence": "cold 0.652s with 40 cache entries written; warm 0.057s printing 'prettier: 40 file(s) unchanged since their last clean check'. 11x faster with no prettier process started." + }, + { + "command": "append trailing whitespace to README.md, re-run the same file list", + "result": "pass", + "evidence": "[warn] README.md - the edited file misses the cache and is re-checked; after `git checkout -- README.md` the run is clean again. Cache keyed on content, so an edit cannot be masked." + }, + { + "command": "git push --dry-run (pre-push: prettier + ci-mirror + pre-commit pre-push stage)", + "result": "pass", + "evidence": "13.0s before this change, 2.9s after, on the same 106-file push range. ci-mirror itself is 0.02s when its green-tree cache hits; the remainder was prettier's repeated Node startups." + }, + { + "command": "shellcheck -x -S warning meta/scripts/run_prettier_capped.sh", + "result": "pass", + "evidence": "no findings; shfmt applied, matching the repo's tab style." + } + ], + "risks": [ + "A cached verdict is trusted without re-running prettier. It is invalidated by file content, prettier version and prettier config, so the remaining exposure is a prettier behaviour change that ships without a version bump - which does not happen for a pinned release.", + "The cache lives under XDG_CACHE_HOME, outside the repo, so it is per-machine and a fresh clone or a cleared cache simply pays one full run.", + "prettier errors rather than warns on an explicitly-listed symbolic link (CLAUDE.md -> AGENTS.md). That behaviour is unchanged by this patch, but it means a push range containing CLAUDE.md fails the hook for a reason unrelated to formatting." + ], + "rollback": [ + "git revert the commit, or restore the previous run_prettier_capped.sh which called `prettier --check \"$@\"` unconditionally inside the same systemd-run scope.", + "After rollback, run `git push --dry-run` and confirm the prettier hook still passes; delete ${XDG_CACHE_HOME:-~/.cache}/prettier-gate, which is then unused." + ] +} diff --git a/endurain/README.md b/endurain/README.md index fb225ecf..c3a4e1c4 100644 --- a/endurain/README.md +++ b/endurain/README.md @@ -14,7 +14,7 @@ unlock authority. | ------------- | --------------------------------------------------------------------------- | | Public URL | `https://endurain.kuhy.duckdns.org` | | Local URL | `http://127.0.0.1:8085` (loopback only) | -| WebDAV inbox | `https://kuhy-cloud.duckdns.org/RunnerUp` → `~/data/cloud/RunnerUp/` | +| WebDAV inbox | `https://kuhy-cloud.duckdns.org/RunnerUp` → `~/data/cloud/RunnerUp/` | | Runtime data | `/var/opt/endurain/` | | Ledger | `~/.local/state/endurain-import/ledger.json` | | Importer code | `../python_pkg/endurain_import/` (repo requires Python under `python_pkg/`) | diff --git a/linux_configuration/.github/copilot-instructions.md b/linux_configuration/.github/copilot-instructions.md index 414f55bf..0e12be07 100644 --- a/linux_configuration/.github/copilot-instructions.md +++ b/linux_configuration/.github/copilot-instructions.md @@ -66,4 +66,4 @@ For in-depth understanding of specific components, see these dedicated guides: | Midnight Shutdown | Auto-shutdown at night | `periodic_background/digital_wellbeing/setup_midnight_shutdown.sh` | | Compulsive Block | Limit app launches | `periodic_background/digital_wellbeing/block_compulsive_opening.sh` | | Music Wrapper | Block music during focus | `periodic_background/digital_wellbeing/youtube-music-wrapper.sh` | -| Screen Locker | Require workout to unlock | External: `~/src/testsAndMisc/python_pkg/screen_locker/` | +| Screen Locker | Require workout to unlock | External: `~/src/testsAndMisc/python_pkg/screen_locker/` | diff --git a/meta/scripts/run_prettier_capped.sh b/meta/scripts/run_prettier_capped.sh index 163264d1..a346bc2e 100755 --- a/meta/scripts/run_prettier_capped.sh +++ b/meta/scripts/run_prettier_capped.sh @@ -1,30 +1,121 @@ -#!/usr/bin/env bash -# Run prettier --check inside its own systemd-run scope so its memory -# budget is independent of the outer pre-push cgroup (which has already -# accumulated page-cache footprint from pytest/mypy/pylint/bandit). +#!/bin/bash + +# ============================================================================ +# prettier --check, memory-capped and result-cached. # -# Falls back to a direct invocation when systemd-run is unavailable -# (CI / containers without systemd user instance). +# Two problems this solves: +# +# 1. Memory. Prettier runs inside its own systemd-run scope so its budget is +# independent of the outer pre-push cgroup, which has already accumulated +# page cache from pytest/mypy/pylint. +# +# 2. Latency. Prettier is a pre-push hook precisely because Node's startup +# dominates its runtime, and pre-commit chunks a large file list into +# several invocations -- a 104-file push paid three cold Node starts and +# ~13s wall clock. A file whose content, prettier version and prettier +# config are all unchanged since the last clean check cannot have become +# unformatted, so its verdict is cached and it never reaches Node. The +# common push touches files that are already clean, which makes the whole +# hook a handful of hashes and no Node process at all. +# +# The cache is keyed on content, so it cannot go stale: edit a file, bump +# prettier, or change .prettierrc and the key changes with it. +# ============================================================================ + set -euo pipefail -if [ $# -eq 0 ]; then - exit 0 -fi +CACHE_ROOT="${PRETTIER_GATE_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/prettier-gate}" +readonly CACHE_ROOT +# Enough entries for several branches' worth of files; pruned wholesale rather +# than per-entry, because an empty cache costs one slow run, not correctness. +readonly MAX_ENTRIES=20000 -# Cap Node heap on top of the cgroup limit for belt-and-braces safety. -export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=384" +# Everything that can change prettier's verdict without changing the file. +config_fingerprint() { + local version config + version="$(prettier --version 2>/dev/null || echo unknown)" + config="$( + cat .prettierrc .prettierrc.* .prettierignore prettier.config.* 2>/dev/null + true + )" + printf '%s\n%s' "$version" "$config" | sha256sum | cut -d' ' -f1 +} -# Measured peak RSS checking the 14 yaml/json/markdown files of a large commit -# is ~0.10 GiB, so 512M is ample. The cap must also stay small enough to be -# satisfiable inside a tighter outer scope: a request larger than the caller's -# budget gets this OOM-killed from the inside rather than merely throttled. -if command -v systemd-run >/dev/null 2>&1 && - systemctl --user is-active --quiet default.target 2>/dev/null || - command -v systemd-run >/dev/null 2>&1; then - exec systemd-run --user --scope --quiet --collect \ - -p MemoryMax=512M \ - -p MemorySwapMax=0 \ - -- prettier --check "$@" -fi +# One sha256sum call for the whole list: per-file forks cost more than the +# hashing does. +cache_keys() { + local fingerprint="$1" + shift + sha256sum -- "$@" 2>/dev/null | while read -r digest _; do + printf '%s-%s\n' "$fingerprint" "$digest" + done +} -exec prettier --check "$@" +prune_if_huge() { + local count + count="$(find "$CACHE_ROOT" -maxdepth 1 -type f 2>/dev/null | wc -l)" + if ((count > MAX_ENTRIES)); then + rm -rf "${CACHE_ROOT:?}" + mkdir -p "$CACHE_ROOT" + fi +} + +run_prettier() { + export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=384" + if command -v systemd-run >/dev/null 2>&1; then + systemd-run --user --scope --quiet --collect \ + -p MemoryMax=512M \ + -p MemorySwapMax=0 \ + -- prettier --check "$@" + return + fi + prettier --check "$@" +} + +main() { + if [ $# -eq 0 ]; then + exit 0 + fi + + mkdir -p "$CACHE_ROOT" + prune_if_huge + + local fingerprint + fingerprint="$(config_fingerprint)" + + # Pair each file with its key, then split into "already known clean" and + # "must be checked". readarray keeps this to two subshells regardless of + # how many files pre-commit passed. + local -a keys=() todo=() todo_keys=() + readarray -t keys < <(cache_keys "$fingerprint" "$@") + if ((${#keys[@]} != $#)); then + # A file vanished between pre-commit's listing and now: fall back to + # checking everything rather than mis-pairing files with digests. + run_prettier "$@" + exit $? + fi + + local i=0 file + for file in "$@"; do + if [[ ! -f "$CACHE_ROOT/${keys[i]}" ]]; then + todo+=("$file") + todo_keys+=("${keys[i]}") + fi + i=$((i + 1)) + done + + if ((${#todo[@]} == 0)); then + printf 'prettier: %d file(s) unchanged since their last clean check\n' "$#" + exit 0 + fi + + run_prettier "${todo[@]}" + + # Only a clean verdict is recorded; a failure must be re-checked next time. + local key + for key in "${todo_keys[@]}"; do + : >"$CACHE_ROOT/$key" + done +} + +main "$@"