From 77f2eb9f113212bffb5d098ce257e25f7e8eafc5 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 23 Aug 2026 19:41:11 +0200 Subject: [PATCH] Give the artifact cap a --prune escape hatch A gate that blocks a commit with no tooling to clear it just moves the problem to whoever hits it. --prune stages the removal of the oldest artifacts, keeping the newest 20 per directory plus the template, and leaves the commit to the operator. Running it caught a bug that reading it would not: sorting on %cs (date only) ties every artifact committed the same day, so sort fell back to the path and the prune deleted alphabetically - it picked one of today's newest files instead of the oldest. Sorts on %ct with -rn now. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp --- .../artifact-prune-mode-2026-08-23.json | 36 ++++++++++++ meta/scripts/check_repo_size.sh | 56 ++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/evidence/artifact-prune-mode-2026-08-23.json diff --git a/docs/superpowers/evidence/artifact-prune-mode-2026-08-23.json b/docs/superpowers/evidence/artifact-prune-mode-2026-08-23.json new file mode 100644 index 00000000..d29087e2 --- /dev/null +++ b/docs/superpowers/evidence/artifact-prune-mode-2026-08-23.json @@ -0,0 +1,36 @@ +{ + "intent": "Give the artifact cap a one-command fix, so the gate can never block a commit with no way out except reconstructing today's selection logic by hand.", + "scope": [ + "meta/scripts/check_repo_size.sh - adds --prune", + "Non-goal: pruning anything now. The tree is at 21/21, well under the 60 cap." + ], + "changes": [ + "Added --prune: stages the removal of the oldest artifacts, keeping the newest KEEP_ARTIFACTS=20 per directory plus template.json, and leaves the commit to the operator.", + "Fixed a same-day ordering bug found by running it: the first version sorted on %cs (date only), so every artifact committed on the same day tied, sort fell back to comparing the path, and the prune deleted alphabetically. It removed cap-artifact-growth-2026-08-23.json - one of the newest files - instead of the oldest. Now sorts on %ct (unix seconds) with sort -rn." + ], + "verification": [ + { + "command": "./meta/scripts/check_repo_size.sh --prune (in a detached worktree)", + "result": "pass", + "evidence": "First run staged 2 deletions but they were WRONG: cap-artifact-growth-2026-08-23.json and flatten-linux-configuration-2026-08-23.json, both same-day files chosen alphabetically. This is what exposed the %cs tie." + }, + { + "command": "./meta/scripts/check_repo_size.sh --prune after the %ct fix (fresh worktree)", + "result": "pass", + "evidence": "Now stages quarantine-arch-sysctl-cases-2026-08-22.json and thorium-repairs-tests-2026-08-23.json - the genuinely oldest. Templates survive; contracts correctly reports 'nothing to prune' at 20 + template." + }, + { + "command": "./meta/scripts/check_repo_size.sh; pre-commit run --files meta/scripts/check_repo_size.sh", + "result": "pass", + "evidence": "'1050 source files (ceiling 1200)' EXIT=0; shfmt and shellcheck clean. Tested in a throwaway worktree so the real tree was never modified." + } + ], + "risks": [ + "--prune stages deletions rather than committing them, so an operator who runs it and forgets to review still has git diff --cached as the checkpoint.", + "KEEP_ARTIFACTS=20 is below MAX_ARTIFACTS_PER_DIR=60 on purpose; a prune therefore buys headroom instead of landing back on the limit." + ], + "rollback": [ + "git revert the commit. --prune is additive and off by default; the enforcing path is unchanged.", + "After rollback, confirm ./meta/scripts/check_repo_size.sh still exits 0 and prints the count." + ] +} diff --git a/meta/scripts/check_repo_size.sh b/meta/scripts/check_repo_size.sh index 8da1626f..cc856bdf 100755 --- a/meta/scripts/check_repo_size.sh +++ b/meta/scripts/check_repo_size.sh @@ -31,6 +31,11 @@ # # check_repo_size.sh # count and enforce # check_repo_size.sh --count # print the number only, always exit 0 +# check_repo_size.sh --prune # stage the removal of the oldest artifacts +# +# --prune is the fix when the artifact cap fails: it stages the deletions and +# leaves committing to you, so the gate never blocks a commit without a +# one-command way out. # # Exits 0 when at or under every ceiling, 1 when over any of them. # ============================================================================ @@ -60,18 +65,58 @@ readonly ARTIFACT_DIRS=( ) readonly MAX_ARTIFACTS_PER_DIR=60 +# What --prune keeps per directory. Below the cap on purpose, so a prune +# buys real headroom instead of landing back on the limit. +readonly KEEP_ARTIFACTS=20 + COUNT_ONLY=0 +PRUNE=0 usage() { - echo "Usage: $SCRIPT_NAME [--count]" + echo "Usage: $SCRIPT_NAME [--count | --prune]" echo "Options:" echo " --count Print the source-file count and exit 0" + echo " --prune git rm the oldest artifacts, keeping the newest" + echo " $KEEP_ARTIFACTS per directory plus template.json" echo " -h, --help Show this help" echo echo "Fails when tracked source files exceed $MAX_SOURCE_FILES." exit 0 } +# Remove the oldest artifacts, keeping the newest KEEP_ARTIFACTS per +# directory plus template.json. Ordered by git commit date rather than by +# filename: the corpus mixes -2026-05, -20260604 and undated slugs, so a +# filename sort silently mis-orders a chunk of it. Deleted files remain +# available in git history. +# +# The sort key is %ct (unix seconds), NOT %cs (date only). With %cs every +# artifact committed on the same day ties, sort falls back to comparing the +# path, and the prune deletes alphabetically - which cost the newest file in +# a same-day batch when this was first written. +prune_artifacts() { + local dir removed=0 + for dir in "${ARTIFACT_DIRS[@]}"; do + local -a doomed=() + mapfile -t doomed < <( + git ls-files "$dir" | + grep -v '/template\.json$' | + while read -r f; do + printf '%s\t%s\n' "$(git log -1 --format=%ct -- "$f")" "$f" + done | + sort -rn | tail -n "+$((KEEP_ARTIFACTS + 1))" | cut -f2 + ) + if ((${#doomed[@]} == 0)); then + echo "$dir: nothing to prune" + continue + fi + git rm -q -- "${doomed[@]}" + echo "$dir: pruned ${#doomed[@]}, kept $KEEP_ARTIFACTS + template" + removed=$((removed + ${#doomed[@]})) + done + echo "Pruned $removed file(s). Review with 'git diff --cached --stat', then commit." +} + # Tracked files minus the generated and vendored trees. Printed one per line. source_files() { local -a filter=() @@ -102,6 +147,11 @@ check_artifact_dirs() { } main() { + if ((PRUNE)); then + prune_artifacts + return 0 + fi + local count count="$(source_files | wc -l)" @@ -135,6 +185,10 @@ while [[ $# -gt 0 ]]; do COUNT_ONLY=1 shift ;; + --prune) + PRUNE=1 + shift + ;; *) echo "Unknown option: $1" >&2 exit 1