From ccd731be3b255a57127a4d1232dee9f073892a7e Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:42:33 +0100 Subject: [PATCH] feat(fleet): add session-detritus archiver --- scripts/fix-script-registry.json | 6 +- scripts/fix-session-detritus.sh | 157 ++++++++++++++++++++++++++++ tests/fix-session-detritus-smoke.sh | 75 +++++++++++++ 3 files changed, 236 insertions(+), 2 deletions(-) create mode 100755 scripts/fix-session-detritus.sh create mode 100755 tests/fix-session-detritus-smoke.sh diff --git a/scripts/fix-script-registry.json b/scripts/fix-script-registry.json index 0b4ed9cb..7904e327 100644 --- a/scripts/fix-script-registry.json +++ b/scripts/fix-script-registry.json @@ -47,7 +47,8 @@ "MissingVexometerHooks": "fix-missing-vexometer-hooks.sh", "TrackedNpmLockfile": "fix-tracked-package-lock.sh", "LicensePMPLDrift": "fix-pmpl-drift.sh", - "ActionsPolicyTooRestrictive": "fix-actions-policy.sh" + "ActionsPolicyTooRestrictive": "fix-actions-policy.sh", + "SessionDetritus": "fix-session-detritus.sh" }, "by_recipe": { "recipe-actions-allow-all": "fix-actions-policy.sh", @@ -107,7 +108,8 @@ "chapel-chpl-llvm-export": "fix-chapel-chpl-llvm-export.sh", "chapel-replace-chpl-about-with-version": "fix-chapel-replace-chpl-about.sh", "reusable_workflow_sha_bump_needs_propagation": "propagate-sha-bump.sh", - "recipe-fix-pmpl-drift": "fix-pmpl-drift.sh" + "recipe-fix-pmpl-drift": "fix-pmpl-drift.sh", + "recipe-archive-session-detritus": "fix-session-detritus.sh" } } } diff --git a/scripts/fix-session-detritus.sh b/scripts/fix-session-detritus.sh new file mode 100755 index 00000000..99d54a85 --- /dev/null +++ b/scripts/fix-session-detritus.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Archive point-in-time session reports found inside standards REGISTRY homes. +# Dispatch contract: fix-session-detritus.sh +# Operator preview: fix-session-detritus.sh --dry-run + +set -euo pipefail + +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true + shift +fi + +REPO_PATH="${1:?Usage: $0 [--dry-run] [finding-json]}" +FINDING_FILE="${2:-}" +REGISTRY="$REPO_PATH/.machine_readable/REGISTRY.a2ml" +ARCHIVE_ROOT="docs/archive/session-detritus" + +if ! git -C "$REPO_PATH" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "ERROR: not a git worktree: $REPO_PATH" >&2 + exit 2 +fi + +if [[ -n "$FINDING_FILE" ]]; then + if [[ ! -f "$FINDING_FILE" ]] || ! jq -e . "$FINDING_FILE" >/dev/null 2>&1; then + echo "ERROR: finding JSON is missing or invalid: $FINDING_FILE" >&2 + exit 2 + fi + + recipe_id=$(jq -r '.recipe_id // empty' "$FINDING_FILE") + if [[ -n "$recipe_id" && "$recipe_id" != "recipe-archive-session-detritus" ]]; then + echo "ERROR: finding recipe mismatch: $recipe_id" >&2 + exit 2 + fi +fi + +if [[ ! -f "$REGISTRY" ]]; then + echo "SKIP: no standards REGISTRY.a2ml at $REGISTRY" + exit 0 +fi + +is_detritus() { + local name="$1" + case "$name" in + *-COMPLETE*.md|*COMPLETE-*.md|*SESSION_SUMMARY*|DEPLOYMENT-SUCCESS.md|\ + DEPLOYMENT-SESSION_*.md|*SESSION-*.md|SONNET-TASKS.md) + return 0 + ;; + *) + return 1 + ;; + esac +} + +is_archived() { + local path="$1" + [[ "/$path/" == */docs/archive/* ]] +} + +is_exempt_path() { + local path="/$1/" + [[ "$path" == */examples/* || "$path" == */templates/* || \ + "$path" == */test/fixtures/* || "$path" == */tests/fixtures/* ]] +} + +declare -a LOCAL_HOMES=() +while IFS=$'\t' read -r spec_id home; do + [[ -z "$spec_id" || -z "$home" ]] && continue + + if [[ "$home" == /* || "/$home/" == */../* || "$home" == "." ]]; then + echo "ERROR: invalid LOCAL registry home for $spec_id: $home" >&2 + exit 2 + fi + + LOCAL_HOMES+=("${home%/}") +done < <( + awk ' + /^\[\[spec\]\]$/ { + if (in_spec && kind != "external" && id != "" && home != "") { + print id "\t" home + } + in_spec=1; id=""; home=""; kind=""; next + } + in_spec && /^id[[:space:]]*=/ { + value=$0; sub(/^[^=]*=[[:space:]]*"/, "", value); sub(/"[[:space:]]*$/, "", value); id=value; next + } + in_spec && /^home[[:space:]]*=/ { + value=$0; sub(/^[^=]*=[[:space:]]*"/, "", value); sub(/"[[:space:]]*$/, "", value); home=value; next + } + in_spec && /^kind[[:space:]]*=/ { + value=$0; sub(/^[^=]*=[[:space:]]*"/, "", value); sub(/"[[:space:]]*$/, "", value); kind=value; next + } + END { + if (in_spec && kind != "external" && id != "" && home != "") { + print id "\t" home + } + } + ' "$REGISTRY" +) + +if [[ ${#LOCAL_HOMES[@]} -eq 0 ]]; then + echo "ERROR: registry contains no valid LOCAL spec homes" >&2 + exit 2 +fi + +in_local_home() { + local path="$1" + local home + for home in "${LOCAL_HOMES[@]}"; do + if [[ "$path" == "$home/"* ]]; then + return 0 + fi + done + return 1 +} + +declare -a SOURCES=() +declare -a DESTINATIONS=() + +while IFS= read -r -d '' path; do + in_local_home "$path" || continue + is_archived "$path" && continue + is_exempt_path "$path" && continue + is_detritus "$(basename "$path")" || continue + + destination="$ARCHIVE_ROOT/$path" + if [[ -e "$REPO_PATH/$destination" ]]; then + echo "ERROR: archive collision: $destination already exists" >&2 + exit 2 + fi + + SOURCES+=("$path") + DESTINATIONS+=("$destination") +done < <(git -C "$REPO_PATH" ls-files -z) + +if [[ ${#SOURCES[@]} -eq 0 ]]; then + echo "OK: no tracked session detritus in LOCAL spec homes" + exit 0 +fi + +for index in "${!SOURCES[@]}"; do + source_path="${SOURCES[$index]}" + destination="${DESTINATIONS[$index]}" + + if [[ "$DRY_RUN" == "true" ]]; then + echo "WOULD ARCHIVE: $source_path -> $destination" + continue + fi + + mkdir -p "$REPO_PATH/$(dirname "$destination")" + git -C "$REPO_PATH" mv -- "$source_path" "$destination" + echo "ARCHIVED: $source_path -> $destination" +done + +echo "Archived ${#SOURCES[@]} tracked session-detritus file(s); review and commit the staged renames in a PR." diff --git a/tests/fix-session-detritus-smoke.sh b/tests/fix-session-detritus-smoke.sh new file mode 100755 index 00000000..270152f8 --- /dev/null +++ b/tests/fix-session-detritus-smoke.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 + +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +SCRIPT="$ROOT/scripts/fix-session-detritus.sh" +REGISTRY="$ROOT/scripts/fix-script-registry.json" +TMP=$(mktemp -d /tmp/session-detritus-smoke-XXXXXX) +trap 'rm -rf "$TMP"' EXIT + +REPO="$TMP/repo" +mkdir -p "$REPO/.machine_readable" "$REPO/spec/sub" "$REPO/spec/examples" \ + "$REPO/external" "$REPO/docs/archive" +git -C "$REPO" init -q +git -C "$REPO" config user.name "Smoke Test" +git -C "$REPO" config user.email "smoke@example.invalid" + +printf '%s\n' \ + '[[spec]]' \ + 'id = "local"' \ + 'home = "spec/"' \ + '' \ + '[[spec]]' \ + 'id = "external"' \ + 'home = "external/"' \ + 'kind = "external"' > "$REPO/.machine_readable/REGISTRY.a2ml" + +printf 'summary\n' > "$REPO/spec/SESSION_SUMMARY_2026-08-24.md" +printf 'tasks\n' > "$REPO/spec/sub/SONNET-TASKS.md" +printf 'canonical\n' > "$REPO/spec/README.adoc" +printf 'fixture\n' > "$REPO/spec/examples/SESSION_SUMMARY_EXAMPLE.md" +printf 'external\n' > "$REPO/external/SESSION_SUMMARY.md" +printf 'already archived\n' > "$REPO/docs/archive/SESSION_SUMMARY_OLD.md" + +git -C "$REPO" add . +git -C "$REPO" commit -qm "fixture" + +dry_output=$("$SCRIPT" --dry-run "$REPO") +[[ "$dry_output" == *"spec/SESSION_SUMMARY_2026-08-24.md"* ]] +[[ "$dry_output" == *"spec/sub/SONNET-TASKS.md"* ]] +[[ "$dry_output" != *"external/SESSION_SUMMARY.md"* ]] +[[ -f "$REPO/spec/SESSION_SUMMARY_2026-08-24.md" ]] + +"$SCRIPT" "$REPO" + +test -f "$REPO/docs/archive/session-detritus/spec/SESSION_SUMMARY_2026-08-24.md" +test -f "$REPO/docs/archive/session-detritus/spec/sub/SONNET-TASKS.md" +test -f "$REPO/spec/README.adoc" +test -f "$REPO/spec/examples/SESSION_SUMMARY_EXAMPLE.md" +test -f "$REPO/external/SESSION_SUMMARY.md" +test -f "$REPO/docs/archive/SESSION_SUMMARY_OLD.md" + +status=$(git -C "$REPO" status --short) +[[ $(printf '%s\n' "$status" | grep -c '^R ') -eq 2 ]] + +second_output=$("$SCRIPT" "$REPO") +[[ "$second_output" == *"no tracked session detritus"* ]] + +printf 'collision\n' > "$REPO/spec/SESSION_SUMMARY_2026-08-24.md" +git -C "$REPO" add spec/SESSION_SUMMARY_2026-08-24.md +set +e +collision_output=$("$SCRIPT" "$REPO" 2>&1) +collision_rc=$? +set -e +[[ "$collision_rc" -eq 2 ]] +[[ "$collision_output" == *"archive collision"* ]] +test -f "$REPO/spec/SESSION_SUMMARY_2026-08-24.md" + +test "$(jq -r '.registry.by_recipe["recipe-archive-session-detritus"]' "$REGISTRY")" = \ + "fix-session-detritus.sh" +test "$(jq -r '.registry.by_category.SessionDetritus' "$REGISTRY")" = \ + "fix-session-detritus.sh" + +echo "PASS: session-detritus archiver is bounded, collision-safe, and idempotent"