Skip to content

Commit 0ab0fd1

Browse files
feat(rsr): detect rule-table drift against the estate (#536)
The RSR rule table has rotted twice, and there is no deployed rhodibot to catch it. Adds an offline-first drift detector, a census builder, a pre-commit hook, and two Justfile recipes. Shell only — Python is a banned language in this estate. See the commit message for the parser-defensiveness and sample-census rationale.
1 parent 8e741dc commit 0ab0fd1

4 files changed

Lines changed: 645 additions & 0 deletions

File tree

‎Justfile‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ check-scm:
104104
done
105105
@echo "PASS: No SCM files in root"
106106

107+
# Check the RSR rule table against the estate (drift: rules nobody can satisfy)
108+
validate-rsr-drift:
109+
@bash bots/rhodibot/hooks/validate-rsr-drift.sh
110+
111+
# Refresh the cached estate census that validate-rsr-drift measures against.
112+
# ~1 API request per repository, so it is refreshed on demand, not per commit.
113+
refresh-rsr-census:
114+
@bash scripts/rsr-census.sh --refresh
115+
107116
# Clean all build artifacts
108117
clean:
109118
cd robot-repo-automaton && cargo clean
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
# Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
#
5+
# Pre-commit hook: a change to the RSR rule table is checked against the estate.
6+
#
7+
# The rule table in src/rsr.rs is the source of truth for RSR compliance, and it
8+
# has rotted twice before: once by demanding LICENSE.txt (0 of 269 repositories
9+
# carried it) and once by holding the .well-known/security.txt checks advisory
10+
# after the migration they were waiting on had finished.
11+
#
12+
# This hook runs scripts/detect-rsr-drift.sh whenever src/rsr.rs is part of the
13+
# change, so that a rule-table edit cannot land without being measured against
14+
# the repositories it will judge.
15+
#
16+
# The estate census is cached, not fetched here: a 269-repository crawl has no
17+
# business inside a commit. Refresh it first with:
18+
#
19+
# just refresh-rsr-census
20+
#
21+
# If no census is present the hook still validates the table itself (it parses,
22+
# its severity tuples are well-formed, its migration posture is coherent) and
23+
# says loudly that the estate comparison was skipped. It does not fail the
24+
# commit for a missing census; it does fail it for actual drift.
25+
26+
set -euo pipefail
27+
28+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
29+
DETECTOR="$REPO_ROOT/scripts/detect-rsr-drift.sh"
30+
RULE_TABLE="bots/rhodibot/src/rsr.rs"
31+
CENSUS="${RSR_CENSUS_DIR:-$REPO_ROOT/.rsr-census}"
32+
33+
cd "$REPO_ROOT"
34+
35+
if [ ! -x "$DETECTOR" ]; then
36+
echo "ERROR: detector not found or not executable: $DETECTOR" >&2
37+
exit 1
38+
fi
39+
40+
# Only run when the rule table is actually part of this change.
41+
staged=$(git diff --cached --name-only 2>/dev/null || true)
42+
if ! printf '%s\n' "$staged" | grep -qxF "$RULE_TABLE"; then
43+
# Also run when the detector itself is being changed, since its verdicts are
44+
# only as good as its parser.
45+
if ! printf '%s\n' "$staged" | grep -qxF "scripts/detect-rsr-drift.sh"; then
46+
exit 0
47+
fi
48+
fi
49+
50+
echo "=== RSR rule-table drift check ==="
51+
52+
if [ -d "$CENSUS" ] && [ -n "$(ls -A "$CENSUS" 2>/dev/null)" ]; then
53+
repos=$(ls -1 "$CENSUS" | wc -l | tr -d ' ')
54+
echo "using cached census: $CENSUS ($repos repositories)"
55+
echo " (age: $(find "$CENSUS" -maxdepth 1 -type f -printf '%TY-%Tm-%Td\n' 2>/dev/null | sort | head -1))"
56+
set +e
57+
"$DETECTOR" --source "$RULE_TABLE" --trees-dir "$CENSUS"
58+
rc=$?
59+
set -e
60+
else
61+
echo "no census at $CENSUS — validating the table only"
62+
echo "the estate comparison is SKIPPED; run: just refresh-rsr-census"
63+
set +e
64+
"$DETECTOR" --source "$RULE_TABLE"
65+
rc=$?
66+
set -e
67+
# Exit 1 from a source-only run means the table itself is broken, which
68+
# must block. Exit 2 means indeterminate.
69+
fi
70+
71+
case "$rc" in
72+
0) echo "PASS: no rule-table drift" ;;
73+
1) echo "FAIL: rule-table drift found — see the report above" >&2; exit 1 ;;
74+
2) echo "WARN: drift check indeterminate (some inputs unavailable)" >&2; exit 0 ;;
75+
*) echo "FAIL: drift check errored (rc=$rc)" >&2; exit 1 ;;
76+
esac
77+
78+
exit 0

0 commit comments

Comments
 (0)