@windyroad/risk-scorer 0.16.3 → 0.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -34,7 +34,10 @@ fi
|
|
|
34
34
|
SESSION_ID=$(_get_session_id)
|
|
35
35
|
[ -n "$SESSION_ID" ] || exit 0
|
|
36
36
|
|
|
37
|
-
# RISK-POLICY.md must exist and not be stale
|
|
37
|
+
# RISK-POLICY.md must exist and not be stale by its own stated review
|
|
38
|
+
# cadence (`> Reviewed <cadence> ...` line; ADR-091 machine-read
|
|
39
|
+
# contract). Fallback threshold 14 days when the cadence line is absent
|
|
40
|
+
# or the word unrecognised. P408.
|
|
38
41
|
if [ ! -f "RISK-POLICY.md" ] || [ ! -s "RISK-POLICY.md" ]; then
|
|
39
42
|
risk_gate_deny "Commit blocked: RISK-POLICY.md is missing. Run /risk-policy to create it before committing."
|
|
40
43
|
exit 0
|
|
@@ -42,19 +45,32 @@ fi
|
|
|
42
45
|
POLICY_STALE=$(python3 -c "
|
|
43
46
|
from datetime import date
|
|
44
47
|
import re
|
|
48
|
+
CADENCE_DAYS = {'weekly': 7, 'fortnightly': 14, 'biweekly': 14,
|
|
49
|
+
'monthly': 30, 'quarterly': 90, 'annually': 365, 'yearly': 365}
|
|
45
50
|
try:
|
|
46
51
|
text = open('RISK-POLICY.md').read()
|
|
47
52
|
m = re.search(r'Last reviewed:\*{0,2}\s*(\d{4}-\d{2}-\d{2})', text)
|
|
53
|
+
# Case-sensitive capital-R match so the cadence line is never
|
|
54
|
+
# confused with the lowercase 'Last reviewed: <date>' line (ADR-091).
|
|
55
|
+
c = re.search(r'(?m)^>?\s*Reviewed\s+([A-Za-z]+)', text)
|
|
56
|
+
cadence = c.group(1) if c else ''
|
|
57
|
+
threshold = CADENCE_DAYS.get(cadence, 14)
|
|
58
|
+
if cadence not in CADENCE_DAYS:
|
|
59
|
+
cadence = ''
|
|
48
60
|
if m:
|
|
49
61
|
reviewed = date.fromisoformat(m.group(1))
|
|
50
|
-
|
|
62
|
+
if (date.today() - reviewed).days > threshold:
|
|
63
|
+
reason = ('per the policy\'s stated %s cadence' % cadence) if cadence else '(default threshold; no stated cadence)'
|
|
64
|
+
print('over %d days ago %s' % (threshold, reason))
|
|
65
|
+
else:
|
|
66
|
+
print('no')
|
|
51
67
|
else:
|
|
52
68
|
print('no')
|
|
53
69
|
except:
|
|
54
70
|
print('no')
|
|
55
71
|
" 2>/dev/null || echo "no")
|
|
56
|
-
if [ "$POLICY_STALE"
|
|
57
|
-
risk_gate_deny "Commit blocked: RISK-POLICY.md is stale (last reviewed
|
|
72
|
+
if [ "$POLICY_STALE" != "no" ]; then
|
|
73
|
+
risk_gate_deny "Commit blocked: RISK-POLICY.md is stale (last reviewed ${POLICY_STALE}). Run /risk-policy to update it before committing."
|
|
58
74
|
exit 0
|
|
59
75
|
fi
|
|
60
76
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# P408 / ADR-091 / RFC-043 / STORY-037: the POLICY_STALE branch of
|
|
4
|
+
# risk-score-commit-gate.sh derives its staleness threshold from the
|
|
5
|
+
# policy's stated review cadence line (`> Reviewed <cadence> ...`)
|
|
6
|
+
# instead of a hardcoded 14 days. Vocabulary: weekly=7,
|
|
7
|
+
# fortnightly/biweekly=14, monthly=30, quarterly=90, annually/yearly=365;
|
|
8
|
+
# fallback 14 when the line is absent or the word unrecognised. The
|
|
9
|
+
# cadence regex is case-sensitive and must bind the capital-R
|
|
10
|
+
# `Reviewed <word>` line, never the lowercase `Last reviewed: <date>`
|
|
11
|
+
# line (ADR-091 machine-read contract).
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
15
|
+
GATE_SCRIPT="$HOOKS_DIR/risk-score-commit-gate.sh"
|
|
16
|
+
TEST_SESSION="bats-test-$$-${BATS_TEST_NUMBER}"
|
|
17
|
+
TMP_REPO="$(mktemp -d)"
|
|
18
|
+
cd "$TMP_REPO"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
teardown() {
|
|
22
|
+
rm -rf "$TMP_REPO"
|
|
23
|
+
rm -rf "${TMPDIR:-/tmp}/claude-risk-${TEST_SESSION}"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Helper: date N days ago, ISO format.
|
|
27
|
+
days_ago() {
|
|
28
|
+
python3 -c "from datetime import date, timedelta; print(date.today() - timedelta(days=int('$1')))"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Helper: write a RISK-POLICY.md reviewed N days ago with an optional
|
|
32
|
+
# cadence line supplied verbatim as the second argument.
|
|
33
|
+
write_policy() {
|
|
34
|
+
local reviewed_days_ago="$1"
|
|
35
|
+
local cadence_line="${2-}"
|
|
36
|
+
{
|
|
37
|
+
echo "# Risk Policy"
|
|
38
|
+
echo ""
|
|
39
|
+
echo "> Last reviewed: $(days_ago "$reviewed_days_ago")"
|
|
40
|
+
if [ -n "$cadence_line" ]; then
|
|
41
|
+
echo ""
|
|
42
|
+
echo "$cadence_line"
|
|
43
|
+
fi
|
|
44
|
+
} > RISK-POLICY.md
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Helper: invoke the gate with a git-commit Bash payload.
|
|
48
|
+
invoke_gate() {
|
|
49
|
+
local input
|
|
50
|
+
input=$(python3 -c "
|
|
51
|
+
import json, sys
|
|
52
|
+
print(json.dumps({
|
|
53
|
+
'tool_name': 'Bash',
|
|
54
|
+
'tool_input': {'command': 'git commit -m \"feat: change\"'},
|
|
55
|
+
'session_id': sys.argv[1]
|
|
56
|
+
}))
|
|
57
|
+
" "$TEST_SESSION")
|
|
58
|
+
echo "$input" | bash "$GATE_SCRIPT"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "P408: monthly cadence, reviewed 16 days ago — NOT stale (witnessed case; deny is the score gate, not staleness)" {
|
|
62
|
+
write_policy 16 "> Reviewed monthly and after any significant change to distribution channels."
|
|
63
|
+
run invoke_gate
|
|
64
|
+
# The gate still denies (no risk score for this session) but the reason
|
|
65
|
+
# must NOT be policy staleness.
|
|
66
|
+
[[ "$output" == *"deny"* ]]
|
|
67
|
+
[[ "$output" != *"stale"* ]]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@test "P408: no cadence line, reviewed 16 days ago — 14-day fallback still flags stale (behaviour preserved)" {
|
|
71
|
+
write_policy 16
|
|
72
|
+
run invoke_gate
|
|
73
|
+
[[ "$output" == *"stale"* ]]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@test "P408: unrecognised cadence word falls back to 14 days" {
|
|
77
|
+
write_policy 16 "> Reviewed sporadically whenever convenient."
|
|
78
|
+
run invoke_gate
|
|
79
|
+
[[ "$output" == *"stale"* ]]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "P408: regex binds the capital-R cadence line, never the lowercase 'Last reviewed:' date line" {
|
|
83
|
+
# Quarterly cadence, reviewed 20 days ago. If the regex wrongly bound
|
|
84
|
+
# the 'Last reviewed:' line the capture would fail and the 14-day
|
|
85
|
+
# fallback would flag stale at 20 days; correct capital-R binding
|
|
86
|
+
# derives 90 days and passes.
|
|
87
|
+
write_policy 20 "> Reviewed quarterly."
|
|
88
|
+
run invoke_gate
|
|
89
|
+
[[ "$output" == *"deny"* ]]
|
|
90
|
+
[[ "$output" != *"stale"* ]]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@test "P408: elapsed monthly cadence denies and the message names the derived threshold and cadence word" {
|
|
94
|
+
write_policy 45 "> Reviewed monthly."
|
|
95
|
+
run invoke_gate
|
|
96
|
+
[[ "$output" == *"stale"* ]]
|
|
97
|
+
[[ "$output" == *"30"* ]]
|
|
98
|
+
[[ "$output" == *"monthly"* ]]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@test "P408: weekly cadence tightens the threshold below the default (10 days is stale under weekly)" {
|
|
102
|
+
write_policy 10 "> Reviewed weekly."
|
|
103
|
+
run invoke_gate
|
|
104
|
+
[[ "$output" == *"stale"* ]]
|
|
105
|
+
}
|