@windyroad/risk-scorer 0.14.0 → 0.14.1-preview.774

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.14.0"
313
+ "version": "0.14.1"
314
314
  }
@@ -442,4 +442,4 @@ They outrank the technical assessment.
442
442
  | 4 Significant | 4 | 8 | 12 | 16 | 20 |
443
443
  | 5 Severe | 5 | 10 | 15 | 20 | 25 |
444
444
 
445
- Label Bands: 1-2 Very Low, 3-4 Low, 5-9 Medium, 10-16 High, 17-25 Very High.
445
+ Label Bands (ADR-086, supersedes ADR-065): 1-2 Very Low, 3-5 Low, 6-9 Medium, 10-16 High, 17-25 Very High. Only `score = 5` differs from the prior bands — now Low (was Medium), restoring feasibility for severe-but-rare residuals.
@@ -101,23 +101,26 @@ check_risk_gate() {
101
101
  fi
102
102
 
103
103
  # 5. Threshold check — block when the score EXCEEDS the project's
104
- # RISK-POLICY.md risk appetite (P007 / ADR-065). The threshold is the
105
- # adopter's documented appetite, not a code constant: a project whose
106
- # policy sets a higher appetite (e.g. "exceeds 9") must not have its
107
- # within-appetite changes gate-rejected.
104
+ # RISK-POLICY.md risk appetite (P007 / ADR-086 supersedes ADR-065). The
105
+ # threshold is the adopter's documented appetite, not a code constant: a
106
+ # project whose policy sets a higher appetite (e.g. "exceeds 9") must not
107
+ # have its within-appetite changes gate-rejected.
108
108
  # Precedence: RISK_APPETITE env override > RISK-POLICY.md § Risk Appetite
109
- # parse > default 4. Default 4 reproduces the prior hardcoded `score >= 5`
110
- # behaviour exactly for integer scores (5 blocks, 4 passes) when the
111
- # policy is absent or unparseable. The parse is tolerant of the phrasings
112
- # "Threshold: N", "exceeds N", and "N/Low appetite", scoped to the
113
- # "## Risk Appetite" section. Cost ~3-8ms/invocation (ADR-065 § Consequences).
109
+ # parse > default 5 (ADR-086). Default 5 tracks the new Low ceiling so
110
+ # the no-policy adopter's default fallback admits residual=5 (the
111
+ # Impact=5/Likelihood=1 floor for severe-but-rare risks). The normal
112
+ # path is /wr-risk-scorer:update-policy interviewing the user to build
113
+ # the policy; the default fallback is defensive.
114
+ # The parse is tolerant of the phrasings "Threshold: N", "exceeds N",
115
+ # and "N/Low appetite", scoped to the "## Risk Appetite" section.
116
+ # Cost ~3-8ms/invocation (ADR-065 § Consequences carries forward).
114
117
  local DECISION
115
118
  DECISION=$(RISK_SCORE_VAL="$SCORE" RISK_APPETITE_ENV="${RISK_APPETITE:-}" python3 -c "
116
119
  import os, re, sys
117
120
  try:
118
121
  score = float(os.environ['RISK_SCORE_VAL'])
119
122
  except Exception:
120
- print('no 4'); sys.exit(0)
123
+ print('no 5'); sys.exit(0)
121
124
  N = None
122
125
  override = os.environ.get('RISK_APPETITE_ENV', '').strip()
123
126
  if override.isdigit():
@@ -137,9 +140,9 @@ else:
137
140
  if m:
138
141
  N = int(m.group(1)); break
139
142
  if N is None:
140
- N = 4
143
+ N = 5
141
144
  print(('yes' if score > N else 'no') + ' ' + str(N))
142
- " 2>/dev/null || echo "no 4")
145
+ " 2>/dev/null || echo "no 5")
143
146
  local DENIED="${DECISION%% *}"
144
147
  local APPETITE="${DECISION##* }"
145
148
 
@@ -237,10 +237,12 @@ _write_matching_hash() {
237
237
 
238
238
  # ---------------------------------------------------------------------------
239
239
  # P007 / ADR-065 — the block threshold is the project's RISK-POLICY.md risk
240
- # appetite (block when score > N), not a hardcoded 5. Default N=4 when the
241
- # policy is absent or unparseable, which reproduces the prior `score >= 5`
242
- # behaviour exactly for integer scores. Precedence: RISK_APPETITE env >
243
- # RISK-POLICY.md parse > default 4. (Behavioural fixtures per ADR-052.)
240
+ # appetite (block when score > N), not a hardcoded 5. Default N=5 per ADR-086
241
+ # (was 4 under superseded ADR-065) when the policy is absent or unparseable,
242
+ # which tracks the new Low ceiling so the default fallback admits residual=5
243
+ # (the Impact=5/Likelihood=1 floor for severe-but-rare risks). Precedence:
244
+ # RISK_APPETITE env > RISK-POLICY.md parse > default 5. (Behavioural fixtures
245
+ # per ADR-052.)
244
246
  # ---------------------------------------------------------------------------
245
247
 
246
248
  @test "appetite 9 (exceeds 9): score 7 within the 5-9 band PASSES" {
@@ -275,29 +277,38 @@ _write_matching_hash() {
275
277
  assert_gate_denies "$TEST_SESSION" "commit" "appetite of 4/25"
276
278
  }
277
279
 
278
- @test "absent RISK-POLICY.md: defaults to appetite 4 (4 PASSES, 5 FAILS)" {
280
+ @test "absent RISK-POLICY.md: defaults to appetite 5 per ADR-086 (5 PASSES, 6 FAILS)" {
279
281
  _use_policy '' # no RISK-POLICY.md in the temp dir
280
282
  rm -f "$HASH_FILE"
281
- printf '4' > "$SCORE_FILE"
282
- assert_gate_allows "$TEST_SESSION" "commit"
283
283
  printf '5' > "$SCORE_FILE"
284
- assert_gate_denies "$TEST_SESSION" "commit" "appetite of 4/25"
284
+ assert_gate_allows "$TEST_SESSION" "commit"
285
+ printf '6' > "$SCORE_FILE"
286
+ assert_gate_denies "$TEST_SESSION" "commit" "appetite of 5/25"
285
287
  }
286
288
 
287
- @test "unparseable RISK-POLICY.md (no appetite integer): defaults to appetite 4" {
289
+ @test "unparseable RISK-POLICY.md (no appetite integer): defaults to appetite 5 per ADR-086" {
288
290
  _use_policy 'We are conservative about risk but state no number here.'
289
291
  rm -f "$HASH_FILE"
290
- printf '4' > "$SCORE_FILE"
291
- assert_gate_allows "$TEST_SESSION" "commit"
292
292
  printf '5' > "$SCORE_FILE"
293
- assert_gate_denies "$TEST_SESSION" "commit" "appetite of 4/25"
293
+ assert_gate_allows "$TEST_SESSION" "commit"
294
+ printf '6' > "$SCORE_FILE"
295
+ assert_gate_denies "$TEST_SESSION" "commit" "appetite of 5/25"
294
296
  }
295
297
 
296
- @test "fractional score 4.5 FAILS under default appetite 4 (4.5 > 4)" {
298
+ @test "fractional score 5.5 FAILS under default appetite 5 (5.5 > 5) per ADR-086" {
297
299
  _use_policy ''
298
300
  rm -f "$HASH_FILE"
299
- printf '4.5' > "$SCORE_FILE"
300
- assert_gate_denies "$TEST_SESSION" "commit" "4.5/25"
301
+ printf '5.5' > "$SCORE_FILE"
302
+ assert_gate_denies "$TEST_SESSION" "commit" "5.5/25"
303
+ }
304
+
305
+ @test "appetite via 'Threshold: 5' phrasing (ADR-086 new Low ceiling): score 5 PASSES, score 6 FAILS" {
306
+ _use_policy '**Threshold: 5 (Low)**'
307
+ rm -f "$HASH_FILE"
308
+ printf '5' > "$SCORE_FILE"
309
+ assert_gate_allows "$TEST_SESSION" "commit"
310
+ printf '6' > "$SCORE_FILE"
311
+ assert_gate_denies "$TEST_SESSION" "commit" "appetite of 5/25"
301
312
  }
302
313
 
303
314
  @test "RISK_APPETITE env override takes precedence over RISK-POLICY.md parse" {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.14.0",
3
+ "version": "0.14.1-preview.774",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"
@@ -113,7 +113,7 @@ Use the Risk Matrix from `RISK-POLICY.md`:
113
113
 
114
114
  - **Inherent Score** = Impact × Likelihood
115
115
  - **Residual Score** = Impact × Likelihood (after controls)
116
- - **Band** (for each) per the Label Bands table: 1-2 Very Low · 3-4 Low · 5-9 Medium · 10-16 High · 17-25 Very High
116
+ - **Band** (for each) per the Label Bands table (ADR-086): 1-2 Very Low · 3-5 Low · 6-9 Medium · 10-16 High · 17-25 Very High
117
117
  - **Within appetite?** = residual score ≤ `RISK-POLICY.md`'s appetite threshold (read the threshold at runtime; do not hardcode)
118
118
 
119
119
  ### 5. Write the risk file
@@ -108,20 +108,23 @@ Reference specific product features, user workflows, and infrastructure by name.
108
108
  - **Likely (4)**: High complexity, many code paths, or limited controls. Expected to occur without intervention.
109
109
  - **Almost certain (5)**: Known gap, no controls in place, or previously observed failure mode.
110
110
 
111
- **Risk matrix**: Include the Impact × Likelihood multiplication table (5×5 = scores 1-25) and the label bands:
111
+ **Risk matrix**: Include the Impact × Likelihood multiplication table (5×5 = scores 1-25) and the label bands per ADR-086 (supersedes ADR-065):
112
112
 
113
113
  | Score Range | Label |
114
114
  |-------------|-------|
115
- | 1-4 | Low |
116
- | 5-9 | Medium |
117
- | 10-14 | High |
118
- | 15-25 | Critical |
115
+ | 1-2 | Very Low |
116
+ | 3-5 | Low |
117
+ | 6-9 | Medium |
118
+ | 10-16 | High |
119
+ | 17-25 | Very High |
120
+
121
+ The bands match the risk-scorer agent's authoritative source at `packages/risk-scorer/agents/pipeline.md` and the create-risk SKILL — single source of truth across the plugin. The Low ceiling at 5 admits residual=5 (Impact=5×Likelihood=1, the floor for severe-but-rare risks) within Low so an appetite of 5 is reachable for that class.
119
122
 
120
123
  The risk matrix is used by both the **risk-scorer agent** (pipeline risk assessment) and the **problem management process** (problem severity via `/problem` skill).
121
124
 
122
125
  ### 6. Confirm with the user
123
126
 
124
- You MUST use the AskUserQuestion tool (not plain text output) to collect user confirmation. Do not proceed to step 7 until you have received answers via AskUserQuestion.
127
+ You MUST use the AskUserQuestion tool (not plain text output) to collect user confirmation. Do not proceed to step 6a or step 7 until you have received answers via AskUserQuestion.
125
128
 
126
129
  Call AskUserQuestion with a single message that presents:
127
130
 
@@ -129,6 +132,28 @@ Call AskUserQuestion with a single message that presents:
129
132
  2. The risk appetite threshold -- present the label bands from the agent contract (step 1) and recommend a threshold based on project maturity. Ask the user to confirm or adjust. A prototype with no real users may tolerate higher risk than a production system with paying users or compliance requirements
130
133
  3. Whether any business context is missing (e.g., compliance requirements, SLAs, user base size)
131
134
 
135
+ ### 6a. Tight-appetite warning when threshold < 5 (ADR-086)
136
+
137
+ If the user picked an appetite threshold below 5 in step 6, fire a second `AskUserQuestion` confirm-with-warning before proceeding. The Low band's ceiling under ADR-086 is 5; an appetite below 5 means a class of risks (those with Impact=5/Severe and no impact-reducing control available) can never be within appetite — the policy is mathematically infeasible for that class. The user can still set the tighter threshold (some domains genuinely want to prohibit severe-impact activities), but the consequence must be a conscious choice, not a quiet trap.
138
+
139
+ **Build the warning's example list** (cite concrete activity-classes the user is about to prohibit):
140
+
141
+ 1. Glob `docs/risks/R*.active.md`. For each file, read the `## Inherent Risk` and `## Residual Risk` sections and extract the numeric `Impact` value. Collect entries whose Inherent Impact = 5 OR Residual Impact = 5.
142
+ 2. If 1+ entries match: pick up to three. Format each as a kebab-case activity-class derived from the entry's filename slug (the part after `R<NNN>-` and before `.active.md`), followed by its R-ID as the audit pointer. Brief-before-ID per P350 — activity-class FIRST, R-ID in parentheses, never the R-ID alone as the carrier of meaning.
143
+ 3. If 0 entries match (empty register OR no Impact=5 entries): fall back to citing the policy's own Impact=5 row from the Impact Levels table just drafted — the user has it in working memory and it's the right surface for "what kind of thing would be prohibited."
144
+
145
+ **The warning question**:
146
+
147
+ > *"You've chosen appetite N. Under the rebalanced label bands (ADR-086) the Low ceiling is 5, so an appetite of N means residual-5 risks like `<activity-class>` ({R-ID}) and `<activity-class>` ({R-ID}) can never be within appetite under this policy — those activities become effectively prohibited (no amount of control work brings their residual below 5 because Impact=5 is fixed). Continue with appetite N, or revise?"*
148
+
149
+ (Substitute the register-derived example list or the policy-row fallback as appropriate.)
150
+
151
+ **Options**:
152
+ - **Confirm: appetite N, with severe-impact activities prohibited** — the user explicitly accepts the trade-off. Proceed to step 7.
153
+ - **Revise the appetite** — return to step 6 with the appetite question only.
154
+
155
+ **Non-interactive (AFK) fallback** per ADR-013 Rule 6: if `AskUserQuestion` is unavailable, do NOT proceed — the warning is load-bearing (prohibits an entire risk class) and silently consuming it would normalise the prohibition. Halt with a clear "appetite < 5 selected interactively required" message for the orchestrator to drain later.
156
+
132
157
  ### 7. Validate draft with risk-scorer agent
133
158
 
134
159
  Before writing the policy file, invoke the risk-scorer agent to validate the draft. This is the gate -- the enforce hook will only allow writes to RISK-POLICY.md after the scorer returns PASS.