@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.
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.16.3"
313
+ "version": "0.16.4"
314
314
  }
@@ -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 (>14 days)
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
- print('yes' if (date.today() - reviewed).days > 14 else 'no')
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" = "yes" ]; then
57
- risk_gate_deny "Commit blocked: RISK-POLICY.md is stale (last reviewed over 2 weeks ago). Run /risk-policy to update it before committing."
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"