Files
Drew T 827295e241 tools+docs(phase-33.5): task 13.5 — the tools audit + the two dictionaries: tools/tool_census.py (two agreeing enumerations of 327 tool files; docstring/SETUP row/consumers/class derived from the tree; the authored half in config/tool_dictionary.tsv — phase · portability · the NEED each tool answers · what · adapts · verdict — with coverage asserted both ways) → docs/tool-index.md (need-keyed, KEEP-GEN, Reference-index row, wiki + how-to pointers), the kit's tools/MANIFEST.md regenerated (header states live 293 + superseded 28 = 321 rows), and the two verbatim corpora in-tree (Drew, confirmed S91): decomp-architect/corpus/tools/<phase>/ (302 copies + 28 superseded pointers + INDEX) and corpus/cookbook/ (the cookbook, its symptom index, the codegen map, a front page stating what transfers per compiler) — sha1-equal to their sources by tool_census --check in tools-health, regenerated by make kit-corpus; kit_lint exempts the corpus dirs (verbatim evidence) but syntax-checks them; G66 (consult the tool dictionary first) + G67 (translate an inherited idiom through its pass) + two memory seeds (34 at install); SETUP Step 6 installs docs/knowledge-corpus.md and checks the manifest against its own stated total; the ops-setup dictionary rows; the intake's Phase 7 cites G66/G67 and Phase 10 + Part C name the raw-cast → declared-symbol step; templates/layout-contract.md (the five-tool probe, a draft for the split). The review under Drew's criterion: 93 no-consumer tools (one Opus agent's draft, verified: 0 defects, every successor live, 0 live consumers, 0 collisions; four one-off verdicts overturned to STILL-NEEDED) → 34 retired by git mv to tools/sunset/ (28 superseded, 6 one-offs; README review table; SETUP rows moved; Archive-index group). Run 4 (fresh throwaway, the final kit): stopped on my Step-6 check (321 vs the live 293) → both sides derived → resumed → PASS 10/10, manifest 56 == 56, 4 commits, guardrails held (the one foreign path was the timeline regenerated by the detached tools-health). tools-health OK; doc_links --strict rc 0; audit_public OK over 6,842 paths; the purge probe PASSED (Phase 34's gate open). decision-log "P33.5 S91" + accelerators "P33.5 S91" banked; log + checkpoint (NEXT = task 14, xHigh, fresh session)
2026-09-07 22:09:15 -06:00

49 lines
2.3 KiB
Bash

#!/usr/bin/env bash
# treelock.sh — THE mutex for tree-writing campaigns. Source it or wrap a command with it.
#
# tools/treelock.sh <name> <command…> # acquire, run, release (even on failure/kill)
# tools/treelock.sh --status # who holds it
#
# WHY (P30, twice in one session — the second time I caused it myself):
# `src/`, `config/`, `asm/` and `build/` are ONE shared mutable state. Any two of {gate_stage,
# dedup_propagate, family_sweep, jtbl_family_bank, make clean/extract/check} running at once can
# interleave writes and leave the fleet incoherent — and a killed writer performs NO undo, so the
# damage outlives the process.
#
# The first attempt at a guard was `while pgrep -f dedup_propagate; do sleep; done`. That is
# STRUCTURALLY WRONG and it failed within the hour: a CAMPAIGN is a LOOP of short-lived processes
# (15 sequential dedup_propagate invocations), so between every pair there is a window with no
# matching process. A parallel gate polled during one of those windows, saw "clear", and started —
# then a `make clean` deleted asm/ under the still-running campaign. Result: 63 of 140 binaries
# failed check-all and every uncommitted bank had to be reverted.
#
# The lesson, stated generally: **guard the CAMPAIGN, not the process.** Presence-of-a-process is a
# sampling test on a gappy signal; a lock file is a statement of intent that spans the gaps.
# Uses flock(1) — held for the WHOLE wrapped command, released by the kernel on exit or kill.
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCK="$REPO/.run/tree.lock"
mkdir -p "$(dirname "$LOCK")"
if [ "${1:-}" = "--status" ]; then
if flock -n "$LOCK" true 2>/dev/null; then echo "tree lock: FREE"; else
echo "tree lock: HELD by -> $(cat "$LOCK.owner" 2>/dev/null || echo '(unknown)')"; fi
exit 0
fi
NAME="${1:?usage: treelock.sh <name> <command...>}"; shift
[ $# -gt 0 ] || { echo "treelock: no command given" >&2; exit 2; }
exec 9>"$LOCK"
if ! flock -w "${TREELOCK_WAIT:-14400}" 9; then
echo "treelock: timed out waiting for the tree (held by $(cat "$LOCK.owner" 2>/dev/null))" >&2
exit 75
fi
echo "$NAME pid=$$ since=$(date +%H:%M:%S)" > "$LOCK.owner"
trap 'rm -f "$LOCK.owner"' EXIT
echo "[treelock] acquired by $NAME"
"$@"
rc=$?
echo "[treelock] released by $NAME (rc=$rc)"
exit $rc