From 5cb0c150fda329a641248f33503097e9fd870c8d Mon Sep 17 00:00:00 2001 From: Broomva Date: Mon, 8 Jun 2026 18:41:43 -0500 Subject: [PATCH] =?UTF-8?q?fix(l3-gate):=20count=20mutations=20not=20creat?= =?UTF-8?q?ions=20=E2=80=94=20unblock=20the=20initial=20bootstrap=20commit?= =?UTF-8?q?=20(BRO-1435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found via P11 dogfooding of the v0.27.0 autonomous-loop demo: `bstack bootstrap` activates the L3 rate gate, then the user's first `git commit` of the freshly- *created* governance is blocked — the gate counted 5 newly-created L3 files as 5 mutations (5/1 EXCEEDED). Creation is not mutation: no prior governance state to destabilize. - l3-rate-gate.sh staged count: a staged L3 path counts only if it exists at HEAD (`git cat-file -e HEAD:`); new files + first-ever commit are exempt. - l3-rate-gate.sh committed count: `git log --diff-filter=M` (additions don't consume the budget) + `grep -c .` (fixes a latent off-by-one vs `wc -l` on a format: stream). - tests/l3-rate-gate.test.sh: 4 hermetic cases (creation exempt, 1 mod OK, 2nd mod in window blocked, non-gov ignored). The pre-commit template calls the script, so this fixes both bootstrap Day-1 UX and every deployed pre-commit. Gate purpose preserved: governance modifications still rate-limited to 1/window (case C). Validation: 7 tests green (incl. new + template_lockstep count 20); shellcheck + bash -n clean. P20: independent review PASS 9/10, no blockers. VERSION 0.27.0 -> 0.27.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 21 ++++++++++++++ VERSION | 2 +- scripts/l3-rate-gate.sh | 25 ++++++++++++----- tests/l3-rate-gate.test.sh | 57 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 8 deletions(-) create mode 100755 tests/l3-rate-gate.test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 2565359..6f208bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.27.1 — 2026-06-08 + +### fix: L3 rate gate counts mutations, not creations — unblocks the initial bootstrap commit (BRO-1435) + +Found via live dogfooding (P11) of the v0.27.0 autonomous-loop demo: `bstack bootstrap` activates the L3 rate gate, then the user's very first `git commit` of the freshly-*scaffolded* governance is blocked — the gate counted the 5 newly-**created** L3 files as 5 L3 mutations (`5/1 EXCEEDED`). Creation is not mutation: there is no prior governance state to destabilize. + +### Fixed + +- **`scripts/l3-rate-gate.sh` — staged count:** a staged L3 path counts as a mutation only if it already exists at `HEAD` (`git cat-file -e HEAD:`). Newly-created L3 files (and the first-ever commit, where `HEAD` is absent) are exempt. +- **`scripts/l3-rate-gate.sh` — committed count:** uses `git log --diff-filter=M` so commits that *added* L3 files (e.g. the bootstrap scaffold) don't consume the per-window budget; only commits that *modified* them do. Also switched the counter from `wc -l` on a `format:` stream to `grep -c .`, fixing a latent off-by-one (a single committed L3 mutation previously counted as 0). +- The pre-commit template calls the script (no duplicated logic), so this fixes both `bstack bootstrap` Day-1 UX and every deployed `.githooks/pre-commit`. + +### Added + +- **`tests/l3-rate-gate.test.sh`** — 4 hermetic cases: (A) creating 5 L3 files is exempt, (B) 1 modification is within budget, (C) a 2nd modification in the same window is blocked, (D) non-governance changes are ignored. 4/4 pass. + +### Notes + +- The gate's actual purpose is preserved: governance *modifications* are still rate-limited to one per `τ_a₃` window (verified by case C). Only *creation* is exempt. +- Primitive count unchanged (**20**). `VERSION` 0.27.0 → 0.27.1. + ## 0.27.0 — 2026-06-08 ### fix: ship + deploy the dangling P1/P2/P6/P7 hook scripts; scaffold METALAYER + schemas; document the two-flow bootstrap (BRO-1431) diff --git a/VERSION b/VERSION index 1b58cc1..83b4730 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.27.0 +0.27.1 diff --git a/scripts/l3-rate-gate.sh b/scripts/l3-rate-gate.sh index 8092113..3aab4c2 100755 --- a/scripts/l3-rate-gate.sh +++ b/scripts/l3-rate-gate.sh @@ -132,18 +132,29 @@ fi NOW=$(date +%s) CUTOFF=$((NOW - TAU_A_L3_INT)) -# Count L3-class commits in the window -# Build a pathspec list for `git log -- ` — git accepts multiple paths -COUNT_COMMITTED=$(git log --since="@$CUTOFF" --pretty=format:%H -- "${L3_PATHS[@]}" 2>/dev/null | wc -l | tr -d '[:space:]') +# Count L3-class commits in the window that MODIFIED an L3 file (--diff-filter=M). +# Additions (creation — e.g. the initial `bstack bootstrap` scaffold) are not +# mutations: there is no prior governance state to destabilize, so they do not +# consume the rate budget (BRO-1435). `grep -c .` counts robustly regardless of +# a trailing newline (fixes a latent off-by-one in the prior `wc -l` form). +COUNT_COMMITTED=$(git log --diff-filter=M --since="@$CUTOFF" --format='%H' -- "${L3_PATHS[@]}" 2>/dev/null | grep -c '.' || true) +COUNT_COMMITTED=${COUNT_COMMITTED:-0} COUNT_STAGED=0 STAGED_FILES="" if [ "$INCLUDE_STAGED" = "1" ]; then - # Check if any staged files match L3 paths + # Count a staged L3 file as a mutation ONLY if it already exists at HEAD. + # Newly-created L3 files (e.g. the initial `bstack bootstrap` scaffold) are + # creation, not mutation — there is no prior governance state to destabilize, + # so they are exempt from the rate budget (BRO-1435). If HEAD does not exist + # yet (first commit ever), every path is a creation → exempt. + staged_now="$(git diff --cached --name-only 2>/dev/null)" for path in "${L3_PATHS[@]}"; do - if git diff --cached --name-only 2>/dev/null | grep -qFx "$path"; then - COUNT_STAGED=$((COUNT_STAGED + 1)) - STAGED_FILES="$STAGED_FILES $path" + if printf '%s\n' "$staged_now" | grep -qFx "$path"; then + if git cat-file -e "HEAD:$path" 2>/dev/null; then + COUNT_STAGED=$((COUNT_STAGED + 1)) + STAGED_FILES="$STAGED_FILES $path" + fi fi done fi diff --git a/tests/l3-rate-gate.test.sh b/tests/l3-rate-gate.test.sh new file mode 100755 index 0000000..deaa62c --- /dev/null +++ b/tests/l3-rate-gate.test.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# tests/l3-rate-gate.test.sh — L3 rate gate counts MUTATIONS, not creations (BRO-1435). +# +# Regression guard for the Day-1 bug where `bstack bootstrap`'s initial commit +# (which CREATES the governance files) was blocked by the rate gate. The gate +# must: +# A. exempt newly-CREATED L3 files (creation is not mutation), +# B. allow 1 L3 MODIFICATION per window, +# C. block the 2nd L3 modification in the same window, +# D. ignore non-governance changes. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +GATE="$SCRIPT_DIR/scripts/l3-rate-gate.sh" + +pass=0; fail=0 +check() { # name want_exit got_exit + if [ "$3" = "$2" ]; then echo " [pass] $1"; pass=$((pass + 1)) + else echo " [FAIL] $1 — want exit $2, got $3"; fail=$((fail + 1)); fi +} + +WS="$(mktemp -d)"; trap 'rm -rf "$WS"' EXIT +cd "$WS" || exit 2 +git init -q; git config user.email t@t; git config user.name t +printf 'x\n' > app.py; git add app.py; git commit -q -m seed + +mkdir -p .control +cp "$SCRIPT_DIR/assets/templates/rcs-parameters.toml.template" .control/rcs-parameters.toml +printf '# gov\n' > CLAUDE.md; printf '# gov\n' > AGENTS.md; printf '# gov\n' > METALAYER.md +printf 'version: "1.0"\n' > .control/policy.yaml + +echo "L3 rate gate — creation vs mutation (BRO-1435)" + +# A — create governance files (the bstack bootstrap scenario): EXEMPT +git add -A +BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "A: creation of 5 L3 files is exempt (exit 0)" 0 $? +git commit -q -m "create governance" + +# B — modify ONE existing governance file: within budget +printf '# tweak\n' >> CLAUDE.md; git add CLAUDE.md +BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "B: 1 modification within budget (exit 0)" 0 $? +git commit -q -m "modify governance #1" + +# C — modify again in the same window: EXCEEDED +printf '# tweak2\n' >> CLAUDE.md; git add CLAUDE.md +BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "C: 2nd modification in window blocked (exit 1)" 1 $? + +# D — non-governance change only: ignored +git restore --staged . 2>/dev/null || git reset -q +git checkout -q -- CLAUDE.md 2>/dev/null || true +printf 'y\n' >> app.py; git add app.py +BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "D: non-governance change ignored (exit 0)" 0 $? + +echo "─────────────────────────────────────" +echo "Passed: $pass Failed: $fail" +[ "$fail" -eq 0 ] && echo "All tests passed." || exit 1