@windyroad/risk-scorer 0.12.4 → 0.12.5

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.12.4"
313
+ "version": "0.12.5"
314
314
  }
@@ -39,9 +39,18 @@ You receive structured pipeline state context with these sections:
39
39
 
40
40
  - **UNCOMMITTED CHANGES**: Diff stat, untracked files, and categories
41
41
  - **UNPUSHED CHANGES**: Commits and cumulative diff between remote and HEAD
42
- - **UNRELEASED CHANGES**: Changeset count and cumulative diff
42
+ - **UNRELEASED CHANGES**: Partitioned changeset counts (Pending vs Queued — see Layer 1 contract below) and cumulative diff
43
43
  - **STALE FILES**: Modified files uncommitted for over 24h
44
44
 
45
+ ### Layer 1 changeset partition (P202)
46
+
47
+ The UNRELEASED CHANGES section emits TWO distinct changeset counts:
48
+
49
+ - `Pending changesets (commits unpushed): N` — changesets whose introducing commit is in `origin/<base>..HEAD` (local) OR is untracked. These ARE pending consumer-facing changes at THIS commit's surface and count toward Layer 1 release risk as before.
50
+ - `Queued changesets (commits already on origin): N` — changesets whose introducing commit is already on `origin/<base>`. The underlying code has already been pushed (and in the maintainer pipeline, reviewed); only the release-PR merge to npm is pending. These contribute **zero** release-risk at THIS commit's surface and MUST NOT count as pending consumer-facing changes in Layer 1.
51
+
52
+ When computing Layer 1 release risk, score only the Pending count (plus any unreleased diff content). A Queued count > 0 with Pending = 0 and no other unreleased diff content is a within-appetite state — the queue is awaiting a release-PR merge, not a maintainer decision. Do NOT emit `RISK_REMEDIATIONS:` lines (such as `move-to-holding`) targeting queued-on-origin changesets; their commits have already shipped and `git mv`'ing them into `docs/changesets-holding/` would fragment the release without reducing actual risk.
53
+
45
54
  ## Catalog Consumption Protocol (ADR-059)
46
55
 
47
56
  Before scoring, READ the standing-risk catalog at `docs/risks/` and filter to risks applicable to THIS action. The catalog is the persistent record of risk classes the project has surfaced; consuming it eliminates the wasted-effort cost of re-deriving risk classes on every assessment AND closes the missed-risk-class hazard (forgetting a class the agent surfaced before because it didn't think of it this time). Per `RISK-POLICY.md` `## Risk Catalog` section.
@@ -248,10 +248,45 @@ fi
248
248
  if [ "$SHOW_UNRELEASED" = true ]; then
249
249
  echo "=== UNRELEASED CHANGES ==="
250
250
 
251
- # Count pending changesets
251
+ # Partition changesets by introducing-commit provenance (P202):
252
+ # - Queued: introducing commit is already on origin/<base>; awaiting
253
+ # only the release-PR merge to npm. Zero release-risk at THIS
254
+ # commit's surface — the underlying code has already shipped to
255
+ # origin and (in the maintainer pipeline) been reviewed.
256
+ # - Pending: introducing commit is NOT on origin/<base> (in
257
+ # origin/<base>..HEAD), OR the file is untracked. Counts as a
258
+ # pending consumer-facing change.
259
+ # Detection: for each .changeset/<name>.md (excluding README.md),
260
+ # run `git log <DEFAULT_BRANCH>..HEAD -- <file>`. Non-empty output OR
261
+ # untracked status => Pending. Empty output AND tracked => Queued.
262
+ # When DEFAULT_BRANCH is unset (no origin/main and no origin/master),
263
+ # nothing is on origin so all changesets are Pending.
252
264
  CHANGESET_COUNT=0
265
+ QUEUED_COUNT=0
266
+ PENDING_COUNT=0
253
267
  if [ -d ".changeset" ]; then
254
- CHANGESET_COUNT=$(find .changeset -name '*.md' -not -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')
268
+ CHANGESET_FILES=$(find .changeset -name '*.md' -not -name 'README.md' 2>/dev/null || true)
269
+ if [ -n "$CHANGESET_FILES" ]; then
270
+ CHANGESET_COUNT=$(echo "$CHANGESET_FILES" | wc -l | tr -d ' ')
271
+ while IFS= read -r CS_FILE; do
272
+ [ -z "$CS_FILE" ] && continue
273
+ CS_TRACKED=$(git ls-files --error-unmatch -- "$CS_FILE" 2>/dev/null || true)
274
+ if [ -z "$CS_TRACKED" ]; then
275
+ PENDING_COUNT=$((PENDING_COUNT + 1))
276
+ continue
277
+ fi
278
+ if [ -z "$DEFAULT_BRANCH" ]; then
279
+ PENDING_COUNT=$((PENDING_COUNT + 1))
280
+ continue
281
+ fi
282
+ CS_UNPUSHED=$(git log "$DEFAULT_BRANCH"..HEAD --oneline -- "$CS_FILE" 2>/dev/null || true)
283
+ if [ -n "$CS_UNPUSHED" ]; then
284
+ PENDING_COUNT=$((PENDING_COUNT + 1))
285
+ else
286
+ QUEUED_COUNT=$((QUEUED_COUNT + 1))
287
+ fi
288
+ done <<< "$CHANGESET_FILES"
289
+ fi
255
290
  fi
256
291
 
257
292
  # Check if origin/publish exists
@@ -269,7 +304,8 @@ if [ "$SHOW_UNRELEASED" = true ]; then
269
304
  echo "No unreleased changes."
270
305
  else
271
306
  if [ "$CHANGESET_COUNT" -gt 0 ]; then
272
- echo "Pending changesets: ${CHANGESET_COUNT}"
307
+ echo "Pending changesets (commits unpushed): ${PENDING_COUNT}"
308
+ echo "Queued changesets (commits already on origin): ${QUEUED_COUNT}"
273
309
  fi
274
310
  if [ -n "$UNRELEASED_STAT" ]; then
275
311
  echo "Accumulated unreleased diff (origin/publish..$DEFAULT_BRANCH):"
@@ -296,7 +332,8 @@ if [ "$SHOW_UNRELEASED" = true ]; then
296
332
  else
297
333
  echo "No publish branch (origin/publish not found)."
298
334
  if [ "$CHANGESET_COUNT" -gt 0 ]; then
299
- echo "Pending changesets: ${CHANGESET_COUNT}"
335
+ echo "Pending changesets (commits unpushed): ${PENDING_COUNT}"
336
+ echo "Queued changesets (commits already on origin): ${QUEUED_COUNT}"
300
337
  fi
301
338
  fi
302
339
  echo ""
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural coverage for pipeline-state.sh --unreleased changeset partition
3
+ # per P202: a changeset whose introducing commit is already on origin/<base>
4
+ # is awaiting only the release-PR merge to npm and must NOT count as a
5
+ # "pending consumer-facing change" at this commit's surface.
6
+ #
7
+ # Per ADR-052 (behavioural tests default) — fixture seeds two changesets
8
+ # with distinct commit provenance and asserts pipeline-state.sh emits
9
+ # distinct counts so the pipeline.md Layer-1 scoring contract can rely
10
+ # on the partition without re-deriving it.
11
+
12
+ setup() {
13
+ HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
14
+ SCRIPT="$HOOKS_DIR/lib/pipeline-state.sh"
15
+
16
+ TEST_DIR="$(mktemp -d)"
17
+ REMOTE_DIR="$(mktemp -d)"
18
+
19
+ (cd "$REMOTE_DIR" && git init --bare --initial-branch=main >/dev/null 2>&1)
20
+
21
+ cd "$TEST_DIR"
22
+ git init --initial-branch=main >/dev/null 2>&1
23
+ git config user.email "test@example.com"
24
+ git config user.name "Test"
25
+ git remote add origin "$REMOTE_DIR"
26
+
27
+ echo "initial" > README.md
28
+ git add README.md
29
+ git commit -m "initial" >/dev/null 2>&1
30
+ git push -u origin main >/dev/null 2>&1
31
+
32
+ mkdir -p .changeset
33
+ }
34
+
35
+ teardown() {
36
+ cd /
37
+ rm -rf "$TEST_DIR" "$REMOTE_DIR"
38
+ }
39
+
40
+ # Helper: seed a changeset, commit it, and push to origin (queued state).
41
+ seed_queued_changeset() {
42
+ local name="$1"
43
+ cat > ".changeset/${name}.md" <<EOF
44
+ ---
45
+ "@windyroad/itil": patch
46
+ ---
47
+
48
+ ${name} changeset body.
49
+ EOF
50
+ git add ".changeset/${name}.md"
51
+ git commit -m "add changeset ${name}" >/dev/null 2>&1
52
+ git push origin main >/dev/null 2>&1
53
+ }
54
+
55
+ # Helper: seed a changeset and commit it locally without pushing (pending state).
56
+ seed_pending_changeset() {
57
+ local name="$1"
58
+ cat > ".changeset/${name}.md" <<EOF
59
+ ---
60
+ "@windyroad/itil": patch
61
+ ---
62
+
63
+ ${name} changeset body.
64
+ EOF
65
+ git add ".changeset/${name}.md"
66
+ git commit -m "add changeset ${name}" >/dev/null 2>&1
67
+ }
68
+
69
+ # Helper: seed a changeset file but leave it untracked (uncommitted-pending state).
70
+ seed_untracked_changeset() {
71
+ local name="$1"
72
+ cat > ".changeset/${name}.md" <<EOF
73
+ ---
74
+ "@windyroad/itil": patch
75
+ ---
76
+
77
+ ${name} changeset body.
78
+ EOF
79
+ }
80
+
81
+ @test "emits queued+pending breakdown when changesets straddle origin" {
82
+ cd "$TEST_DIR"
83
+ seed_queued_changeset queued-one
84
+ seed_pending_changeset pending-one
85
+
86
+ run bash "$SCRIPT" --unreleased
87
+ [ "$status" -eq 0 ]
88
+
89
+ echo "$output" | grep -q "Queued changesets (commits already on origin): 1" || {
90
+ echo "Missing 'Queued changesets (commits already on origin): 1' line."
91
+ echo "Output was:"
92
+ echo "$output"
93
+ return 1
94
+ }
95
+ echo "$output" | grep -q "Pending changesets (commits unpushed): 1" || {
96
+ echo "Missing 'Pending changesets (commits unpushed): 1' line."
97
+ echo "Output was:"
98
+ echo "$output"
99
+ return 1
100
+ }
101
+ }
102
+
103
+ @test "all-on-origin scenario: Queued > 0, Pending = 0" {
104
+ cd "$TEST_DIR"
105
+ seed_queued_changeset on-origin-a
106
+ seed_queued_changeset on-origin-b
107
+
108
+ run bash "$SCRIPT" --unreleased
109
+ [ "$status" -eq 0 ]
110
+
111
+ echo "$output" | grep -q "Queued changesets (commits already on origin): 2" || {
112
+ echo "Missing 'Queued changesets (commits already on origin): 2' line."
113
+ echo "Output was:"
114
+ echo "$output"
115
+ return 1
116
+ }
117
+ echo "$output" | grep -q "Pending changesets (commits unpushed): 0" || {
118
+ echo "Missing 'Pending changesets (commits unpushed): 0' line."
119
+ echo "Output was:"
120
+ echo "$output"
121
+ return 1
122
+ }
123
+ }
124
+
125
+ @test "all-local scenario: Pending > 0, Queued = 0" {
126
+ cd "$TEST_DIR"
127
+ seed_pending_changeset local-a
128
+ seed_pending_changeset local-b
129
+
130
+ run bash "$SCRIPT" --unreleased
131
+ [ "$status" -eq 0 ]
132
+
133
+ echo "$output" | grep -q "Pending changesets (commits unpushed): 2" || {
134
+ echo "Missing 'Pending changesets (commits unpushed): 2' line."
135
+ echo "Output was:"
136
+ echo "$output"
137
+ return 1
138
+ }
139
+ echo "$output" | grep -q "Queued changesets (commits already on origin): 0" || {
140
+ echo "Missing 'Queued changesets (commits already on origin): 0' line."
141
+ echo "Output was:"
142
+ echo "$output"
143
+ return 1
144
+ }
145
+ }
146
+
147
+ @test "untracked changeset counts as pending" {
148
+ cd "$TEST_DIR"
149
+ seed_untracked_changeset draft-one
150
+
151
+ run bash "$SCRIPT" --unreleased
152
+ [ "$status" -eq 0 ]
153
+
154
+ echo "$output" | grep -q "Pending changesets (commits unpushed): 1" || {
155
+ echo "Untracked changeset should count as pending."
156
+ echo "Output was:"
157
+ echo "$output"
158
+ return 1
159
+ }
160
+ }
161
+
162
+ @test "no changesets emits no breakdown lines" {
163
+ cd "$TEST_DIR"
164
+ # .changeset dir exists but is empty (setup creates it).
165
+
166
+ run bash "$SCRIPT" --unreleased
167
+ [ "$status" -eq 0 ]
168
+
169
+ ! echo "$output" | grep -q "Queued changesets" || {
170
+ echo "Should not emit Queued line when no changesets present."
171
+ echo "Output was:"
172
+ echo "$output"
173
+ return 1
174
+ }
175
+ ! echo "$output" | grep -q "Pending changesets" || {
176
+ echo "Should not emit Pending line when no changesets present."
177
+ echo "Output was:"
178
+ echo "$output"
179
+ return 1
180
+ }
181
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"