Skip to content

feat(governance): exemption ledgers may shrink, never grow silently - #588

Merged
hyperpolymath merged 5 commits into
mainfrom
feat/exemption-ratchet
Aug 7, 2026
Merged

feat(governance): exemption ledgers may shrink, never grow silently#588
hyperpolymath merged 5 commits into
mainfrom
feat/exemption-ratchet

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Every large CI failure diagnosed in this estate shares one shape: an exemption was added quietly and then never removed.

The 2026-08-06 audit across 424 repositories found a gitleaks gate that had run with no allowlist for months; 13,206 banned-language files, most declared once and forgotten; a .hypatia-baseline.json that absorbed 255 findings in a single commit, turning a library's central guarantee into accepted debt; and exemption files written in the wrong format, which therefore suppressed nothing — unnoticed for months, because a suppression that doesn't work is silent in exactly the same way as one that does.

A gate that checks only the current state cannot catch any of that — each individual state is "valid". What matters is the direction of travel.

This compares a PR against its base and enforces three things:

  1. No silent growth. A ledger may lose entries freely. Gaining them requires Ratchet-exception: <why> in a commit message — so adding debt stays possible, attributable and reviewed. A gate nobody can satisfy gets deleted; a gate that costs one honest sentence gets obeyed.
  2. No anonymous entries. Every baseline entry must carry a note or tracking_issue. This is the rule that would have caught the 255-entry commit.
  3. No wildcards in the migration ledger. A ** in .hypatia-ignore absorbs files added later, so the ledger grows while appearing to hold steady — precisely the failure this exists to prevent.

Covers .hypatia-baseline.json, .hypatia-ignore, .gitleaks.toml, .machine_readable/root-allow.txt.

⚠ The ratchet is itself tested, and the test asserts it CAN FAIL

This estate's recurring defect is the gate that cannot fail — just proof-check exiting 0 with no prover, continue-on-error on the primary secret scanner, a linter that never parsed its input. A ratchet that always passed would be worse than none, because it would license the silent growth it claims to prevent.

scripts/tests/exemption-ratchet-test.sh exercises all six branches in both directions:

 ok unchanged ledgers pass
ok silent growth fails
ok declared growth passes
ok anonymous entry fails even when growth is declared
ok wildcard in the migration ledger fails
ok shrinking passes

🤖 Generated with Claude Code

Every large CI failure diagnosed in this estate shares one shape: an exemption
was added quietly and then never removed. The 2026-08-06 audit across 424
repositories found a gitleaks gate that had run with NO allowlist for months;
13,206 banned-language files, most declared once and forgotten; a
.hypatia-baseline.json that absorbed 255 findings in a single commit, turning a
library's central guarantee into accepted debt; and exemption files written in
the WRONG FORMAT, which therefore suppressed nothing — unnoticed for months,
because a suppression that does not work is silent in exactly the same way as
one that does.
A gate that checks only the CURRENT state cannot catch any of that: each
individual state is "valid". What matters is the direction of travel. This
compares a pull request against its base and enforces three things:
1. NO SILENT GROWTH. A ledger may lose entries freely. Gaining them requires
`Ratchet-exception: <why>` in a commit message — so adding debt stays
possible, attributable and reviewed. A gate nobody can satisfy gets
deleted; a gate that costs one honest sentence gets obeyed.
2. NO ANONYMOUS ENTRIES. Every .hypatia-baseline.json entry must carry a
`note` or a `tracking_issue`. "What is this?" must be answerable without
archaeology. This is the rule that would have caught the 255-entry commit.
3. NO WILDCARDS IN THE MIGRATION LEDGER. A `**` in .hypatia-ignore absorbs
files added later, so the ledger grows while appearing to hold steady —
precisely the failure this check exists to prevent. Architectural
exemptions belong in .hypatia-baseline.json, where a note explains them.
Covers .hypatia-baseline.json, .hypatia-ignore, .gitleaks.toml and
.machine_readable/root-allow.txt.
⚠ THE RATCHET IS ITSELF TESTED, and the test asserts it CAN FAIL. This estate's
recurring defect is the gate that cannot fail — `just proof-check` recipes
exiting 0 with no prover installed, `continue-on-error` on the primary secret
scanner, a linter that never parsed its input. A ratchet that always passed
would be worse than none, because it would license the silent growth it claims
to prevent. scripts/tests/exemption-ratchet-test.sh exercises all six branches
in both directions, including the subtle one: an anonymous entry fails even
when growth IS declared.
Runs on pull_request only — the check needs a base to compare against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
Comment on lines +76 to +79
*)
# comments and blank lines are not exemptions
printf '%s' "$blob" | grep -vE '^\s*(#|$)' | wc -l | tr -d ' ' ;;
esac

@gitar-botgitar-botBotAug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️Bug: Comments-only text ledger aborts ratchet via set -e

In count_at, the text branch grep -vE '^\s*(#|$)' | wc -l returns exit 1 when a non-empty ledger contains only comment/blank lines (grep -v matches nothing). Combined with set -euo pipefail, the failing pipe propagates out of the command substitution after="$(count_at ...)" and aborts the whole script with exit 1 mid-loop — before the anonymous-baseline and wildcard checks run, and with no diagnostic. This turns the intended 'debt fully paid down, header comment retained' case (which should SHRINK/pass) into an opaque failure, contradicting the tool's purpose. Verified reproducible on Linux/GNU grep. Fix by neutralizing grep's no-match exit, e.g. printf '%s' "$blob" | { grep -vE '^\s*(#|$)' || true; } | wc -l | tr -d ' '.

Prevent grep's no-match exit (1) from aborting the script under set -e/pipefail.:

*)
# comments and blank lines are not exemptions
printf '%s' "$blob" | { grep -vE '^\s*(#|$)' || true; } | wc -l | tr -d ' ' ;;

Was this helpful? React with 👍 / 👎

Comment threadscripts/check-exemption-ratchet.sh Outdated
Comment on lines +82 to +86
# Does the pull request explicitly declare that a ledger must grow?
EXCEPTION=0
if git log --format=%B "${BASE_REF}..HEAD" 2>/dev/null | grep -qiE '^Ratchet-exception:[[:space:]]*\S'; then
EXCEPTION=1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: One Ratchet-exception licenses growth in all four ledgers

EXCEPTION is a single global flag set when any commit in base..HEAD contains a Ratchet-exception: line, and it then permits unlimited growth across every ledger in the loop. A PR that legitimately declares growth for one ledger (e.g. a vendored corpus in .hypatia-baseline.json) silently also permits unrelated growth in .gitleaks.toml or root-allow.txt within the same PR. Consider scoping the declaration to a named ledger (e.g. Ratchet-exception: <path>: <why>) so the escape hatch stays targeted.

Was this helpful? React with 👍 / 👎

Comment on lines +73 to +79
case "$path" in
*.json)
printf '%s' "$blob" | jq 'if type=="array" then length else 0 end' 2>/dev/null || echo 0 ;;
*)
# comments and blank lines are not exemptions
printf '%s' "$blob" | grep -vE '^\s*(#|$)' | wc -l | tr -d ' ' ;;
esac

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: Line-count proxy is unreliable for .gitleaks.toml

count_at treats every non-comment, non-blank line as one exemption for text ledgers, but .gitleaks.toml is structured TOML: allowlist entries span multiple lines (tables, regexes = [ ... ] arrays), and headers/keys count too. Reformatting an existing allowlist can inflate the count (false GREW) while genuinely adding a suppression inside an existing array line may not change it (silent growth). The ratchet's guarantee is therefore weak for the gitleaks ledger specifically; consider parsing it as TOML or counting allowlist array elements rather than lines.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-botBot commented Aug 6, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review⚠️ Changes requested0 resolved / 3 findings

Adds an exemption ratchet to prevent silent debt growth across compliance ledgers, but the implementation contains critical issues: comments-only text ledgers abort the ratchet via set -e, a single commit flag licenses growth globally across all four ledgers, and line-count proxies are unreliable for TOML files.

⚠️Bug: Comments-only text ledger aborts ratchet via set -e

📄 scripts/check-exemption-ratchet.sh:76-79📄 scripts/check-exemption-ratchet.sh:97-98

In count_at, the text branch grep -vE '^\s*(#|$)' | wc -l returns exit 1 when a non-empty ledger contains only comment/blank lines (grep -v matches nothing). Combined with set -euo pipefail, the failing pipe propagates out of the command substitution after="$(count_at ...)" and aborts the whole script with exit 1 mid-loop — before the anonymous-baseline and wildcard checks run, and with no diagnostic. This turns the intended 'debt fully paid down, header comment retained' case (which should SHRINK/pass) into an opaque failure, contradicting the tool's purpose. Verified reproducible on Linux/GNU grep. Fix by neutralizing grep's no-match exit, e.g. printf '%s' "$blob" | { grep -vE '^\s*(#|$)' || true; } | wc -l | tr -d ' '.

Prevent grep's no-match exit (1) from aborting the script under set -e/pipefail.
*)
# comments and blank lines are not exemptions
printf '%s' "$blob" | { grep -vE '^\s*(#|$)' || true; } | wc -l | tr -d ' ' ;;
💡 Bug: One Ratchet-exception licenses growth in all four ledgers

📄 scripts/check-exemption-ratchet.sh:82-86📄 scripts/check-exemption-ratchet.sh:100-106

EXCEPTION is a single global flag set when any commit in base..HEAD contains a Ratchet-exception: line, and it then permits unlimited growth across every ledger in the loop. A PR that legitimately declares growth for one ledger (e.g. a vendored corpus in .hypatia-baseline.json) silently also permits unrelated growth in .gitleaks.toml or root-allow.txt within the same PR. Consider scoping the declaration to a named ledger (e.g. Ratchet-exception: <path>: <why>) so the escape hatch stays targeted.

💡 Bug: Line-count proxy is unreliable for .gitleaks.toml

📄 scripts/check-exemption-ratchet.sh:73-79📄 scripts/check-exemption-ratchet.sh:88-93

count_at treats every non-comment, non-blank line as one exemption for text ledgers, but .gitleaks.toml is structured TOML: allowlist entries span multiple lines (tables, regexes = [ ... ] arrays), and headers/keys count too. Reformatting an existing allowlist can inflate the count (false GREW) while genuinely adding a suppression inside an existing array line may not change it (silent growth). The ratchet's guarantee is therefore weak for the gitleaks ledger specifically; consider parsing it as TOML or counting allowlist array elements rather than lines.

🤖 Prompt for agents
Code Review: Adds an exemption ratchet to prevent silent debt growth across compliance ledgers, but the implementation contains critical issues: comments-only text ledgers abort the ratchet via set -e, a single commit flag licenses growth globally across all four ledgers, and line-count proxies are unreliable for TOML files.
1. ⚠️ Bug: Comments-only text ledger aborts ratchet via set -e
Files: scripts/check-exemption-ratchet.sh:76-79, scripts/check-exemption-ratchet.sh:97-98
In count_at, the text branch `grep -vE '^\s*(#|$)' | wc -l` returns exit 1 when a non-empty ledger contains only comment/blank lines (grep -v matches nothing). Combined with `set -euo pipefail`, the failing pipe propagates out of the command substitution `after="$(count_at ...)"` and aborts the whole script with exit 1 mid-loop — before the anonymous-baseline and wildcard checks run, and with no diagnostic. This turns the intended 'debt fully paid down, header comment retained' case (which should SHRINK/pass) into an opaque failure, contradicting the tool's purpose. Verified reproducible on Linux/GNU grep. Fix by neutralizing grep's no-match exit, e.g. `printf '%s' "$blob" | { grep -vE '^\s*(#|$)' || true; } | wc -l | tr -d ' '`.
Fix (Prevent grep's no-match exit (1) from aborting the script under set -e/pipefail.):
*)
# comments and blank lines are not exemptions
printf '%s' "$blob" | { grep -vE '^\s*(#|$)' || true; } | wc -l | tr -d ' ' ;;
2. 💡 Bug: One Ratchet-exception licenses growth in all four ledgers
Files: scripts/check-exemption-ratchet.sh:82-86, scripts/check-exemption-ratchet.sh:100-106
EXCEPTION is a single global flag set when any commit in base..HEAD contains a `Ratchet-exception:` line, and it then permits unlimited growth across every ledger in the loop. A PR that legitimately declares growth for one ledger (e.g. a vendored corpus in .hypatia-baseline.json) silently also permits unrelated growth in .gitleaks.toml or root-allow.txt within the same PR. Consider scoping the declaration to a named ledger (e.g. `Ratchet-exception: <path>: <why>`) so the escape hatch stays targeted.
3. 💡 Bug: Line-count proxy is unreliable for .gitleaks.toml
Files: scripts/check-exemption-ratchet.sh:73-79, scripts/check-exemption-ratchet.sh:88-93
count_at treats every non-comment, non-blank line as one exemption for text ledgers, but .gitleaks.toml is structured TOML: allowlist entries span multiple lines (tables, `regexes = [ ... ]` arrays), and headers/keys count too. Reformatting an existing allowlist can inflate the count (false GREW) while genuinely adding a suppression inside an existing array line may not change it (silent growth). The ratchet's guarantee is therefore weak for the gitleaks ledger specifically; consider parsing it as TOML or counting allowlist array elements rather than lines.
Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose 

Important

Your trial ends in 4 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@gitar-botgitar-botBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️This PR is blocked due to unresolved code review findings.

Configure merge blocking · Maintainers can dismiss this review.

hyperpolymathand others added 2 commits August 6, 2026 22:58
Found by gitar-bot review on the ratchet's own pull request.
`count_at` counts entries with `grep -vE '^\s*(#|$)'`. grep exits 1 when
nothing matches — which is exactly what a ledger containing only its
explanatory header produces, and that is the state a repository reaches when it
has finished paying its debt down. Under `set -euo pipefail` that exit
propagated through the command substitution and killed the script mid-report.
The failure was worse than an abort. It printed the first ledger's line, then
exited 1 — and exit 1 from this script means "ratchet FAILED". So a repository
that had cleared its ledger entirely would be reported as violating the
ratchet, with no reason printed and nothing to fix. That is the same class of
defect the ratchet exists to prevent: a gate failing for a reason unrelated to
what it measures.
Reproduced before fixing, and two regression cases added — a comments-only
ledger and a completely empty one. The original suite missed both because every
case it exercised left at least one live entry behind. 8/8 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
Two further findings from gitar-bot review on this pull request.
1. ONE EXCEPTION LICENSED ALL FOUR LEDGERS. `Ratchet-exception:` was a single
global flag, so a pull request that legitimately needed to add one gitleaks
path also silently gained permission to grow the Hypatia baseline, the
migration ledger and the root allowlist. The whole point of the check is
that each addition is SEEN; a blanket permit defeats it. The declaration now
names its ledger, and one naming a different ledger does not license this
one. A declaration naming no known ledger is rejected rather than treated as
blanket permission — "unparseable" must never mean "allowed".
2. LINE COUNTING IS WRONG FOR TOML, IN BOTH DIRECTIONS. A non-comment line
count for .gitleaks.toml counts `paths = [`, the closing `]` and every
structural line. Reformatting one array across several lines read as growth,
while adding an entry to an existing single-line array read as NO CHANGE —
and the second is the dangerous one, because it let an exemption be added
invisibly. Now counts quoted entries inside paths/regexes arrays. Verified
both ways: a reformat that triples the line count reports `unchanged`, and a
genuine +1 on an identical line count reports GREW.
⚠ The counter is a SIBLING SCRIPT, not an inline `python3 -c`. Inlining it
mangled the quote escaping into invalid Python — the regex needs both ''' and "
for TOML's string forms — and a `|| echo 0` fallback swallowed the SyntaxError,
so the count silently became 0 on both sides and the ledger was skipped
entirely. A check that reported OK while measuring nothing. There is now no
fallback: a counter that cannot run fails the check, because a count of 0 is
indistinguishable from an empty ledger, and empty is the state that passes.
The workflow stages both files together.
Suite extended to 10 cases, including an exception naming the wrong ledger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
Comment threadscripts/count-ledger-entries.py Fixed
…ver ran
Three defects, one of them live.
**The counter was returning 0 on the real ledger.** `count-ledger-entries.py`
matched an array with a non-greedy `\[(.*?)\]`, so a `]` inside a string ended
the array early. The estate's own `.gitleaks.toml` contains exactly that — regex
entries with character classes — so the counter reported 0 entries for a ledger
holding 16. The ratchet then compared 0 against 0 and passed while measuring
nothing: precisely the failure the counter's own header warns about.
Verified independently: a line-grep for quoted entries in `.gitleaks.toml`
returns 16, matching the port.
**Ported to shell.** Estate policy bans Python outright, and the JS runtime has
already moved once (Deno to Bun); a counter the ratchet depends on must not be
invalidated by the next runtime change. awk has no non-greedy matching, so the
port walks the array explicitly — which is why a `]` inside a string, a quote
inside a comment, and a commented-out array all behave correctly where the
regex did not. Eleven tests, including one named for the bug above so a future
rewrite cannot reintroduce it.
The ratchet's own fixtures moved from Python to jq for the same reason.
**16 test suites had never been run by anything.** `self-test.yml` discovered
only `tests/*.sh`, while `scripts/tests/` held sixteen suites — including the
ratchet's — that no workflow executed. It also ran only on push to main, so it
reported after a change had already landed. Now it runs on pull_request too and
discovers both roots.
⚠ Enabling discovery makes four pre-existing failures visible, and this job will
be red until they are repaired:
scripts/tests/governance-gates-505-test.sh
scripts/tests/wave0-false-green-test.sh
scripts/tests/wave3-scorecards-test.sh
scripts/tests/wave5-language-guides-test.sh
They are not quarantined and not skipped. A red self-test naming four broken
suites is an accurate report; a green one that never ran them was not.
@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit 528cfb0 into mainAug 7, 2026
33 of 36 checks passed
@hyperpolymath
hyperpolymath deleted the feat/exemption-ratchet branch August 7, 2026 09:46
hyperpolymath added a commit that referenced this pull request Aug 7, 2026
…ck fighting the lockfile (#597)
Three structural corrections — each removes a *class* of estate failure
rather than another instance.
### 1. The baseline never reached the security tab
```
scan ──┬─> findings.json ─> apply-baseline.sh ─> the GATE (filtered)
└─> hypatia.sarif ────────────────────> code scanning (NOT filtered)
```
The `code_scanning` ruleset rule blocks on those alerts and knows
nothing of the baseline, so **acknowledging a finding silenced the gate
while the same finding still blocked the merge** — every required check
green, nothing to point at.
Measured 2026-08-07: **147 open PRs across 91 repos**, held by **1,777
alerts, zero introduced by the PR they blocked**.
`scripts/filter-sarif-by-baseline.sh` filters the SARIF before upload.
It contains **no matcher of its own** — it runs the caller's
`apply-baseline.sh` (the same copy the gate runs) and uses its
`findings_suppressed` list, so there is exactly one matcher by
construction. It **fails open** in every error path; 4 of its 9 tests
assert that.
### 2. The SPDX check fought `gh actions-lock`
The check was `head -1 | grep`, but `gh actions-lock` inserts its
managed-by comment at line 1 on every mint — so the check re-failed on
files whose licence sat on line 2. It reported 40 workflows estate-wide
as missing a header they had, and prepending a default **mis-licensed 3
files** (PMPL shadowed by MPL) before it was caught.
Now reads the leading comment block, where REUSE puts the identifier.
**Validated against 984 real workflow files: 983 accepted**, the single
rejection genuinely has no declaration.
### 3. The pin check named the wrong thing
`ERROR: Found unpinned actions:` comes only from the no-lockfile branch
but reads generically — indistinguishable from the stale-lockfile branch
or an SPDX failure in the same job. It now says which tree lacked a
lockfile and states the mutual exclusion, because inline-pinning to
"fix" it *removes* actions from the lockfile (hypatia: 14
`startup_failure`s from one commit).
**Tests**: 9 + 9, both in `scripts/tests/` so self-test.yml discovers
them as of #588.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
hyperpolymath added a commit that referenced this pull request Aug 7, 2026
`Debtfile.adoc` — what the 2026-08-07 sweep found and did **not** fix,
with the measurement behind each number and, for the two big items, the
decision each awaits. Matching entries in
`.machine_readable/agent_instructions/debt.a2ml`, which is explicitly
the next session's Phase 0 input.
Most of what remains is blocked on a **ruling, not effort**:
**D-1 — 545 Scorecard alerts cannot be closed by any code change.**
`scorecard-reusable.yml` runs `results_format: json` with no
`upload-sarif` step, so code scanning never gets a fresh run. Of 143
sampled alerts only **7 were updated within 31 days**; 8 date to
**2025-12-18**. Proven: minting `actions.lock` and re-running Scorecard
left `PinnedDependenciesID` at 5 → 5, `updated_at` frozen at 2026-05-17.
`code_scanning` gates **106 of 171** repos on this.
**D-2 — 15 repos pin the reusables to `@81dbf2d`**, predating the
lockfile-aware pin check, so they report "Found unpinned actions" on
repos holding a complete lockfile. A caller pinned to an old reusable
**cannot be reached by any fix made in the reusable** — repointing is
unavoidable, not tidy.
Both were attempted and refused by the permission classifier —
correctly, since each changes security gating across ~100 repos.
Also recorded: 1,629 genuine findings by rule (with an explicit *do not
sweep* warning); the 85-of-91 baseline gap; 4 test suites that fail and
had never run before #588; Deno CI still running post-Bun; and the 6
repos the sweep guards deliberately left alone.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hyperpolymath@github-advanced-security