@nurix/etna 0.18.4 → 0.18.6

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.
@@ -0,0 +1,399 @@
1
+ # Testing & Resilience — Extraction Plan
2
+
3
+ - **Sub-criterion:** 1.2 (AI-only)
4
+ - **Weight:** 12.5 pts (4 Code AI × 12.5 = 50 pts half)
5
+ - **Contenders:** Semgrep + filesystem + GHA + LLM (joint load-bearing) + CodeRabbit resilience reuse (**calibrating modifier**) + **multi-protocol route-auth scan** (HTTP + WebSocket + gRPC + tRPC — feeds the `AUTH` gate).
6
+ - **Source quote:** _"Unit/integration tests, error handling, input validation. It should survive more than the happy path."_
7
+
8
+ Bands are **absolute** — thresholds in the _Banding_ section below are fixed numeric cutoffs. Gates this recipe owns: **`UNTESTED`** (hard, `testing ≤ 1` when `test_file_count == 0`), **`TESTS_TRIVIAL`** (soft, `testing ≤ 2` when `assertion_density < 1.0` OR `trivial_body_ratio > 0.8`), and **`AUTH`** (fires `testing ≤ 2` when network-routed handlers exist and ≥ 1 mutating endpoint isn't behind an auth middleware/dependency/decorator, detected across HTTP [FastAPI/Express/Flask/Gin/Django POST/PUT/PATCH/DELETE], WebSocket [ws/Socket.io/SignalR], gRPC [grpc/grpcio unary+streaming], tRPC [@trpc/server mutations]). Raw dumps: `raw/testing-auth-scan-{http,ws,grpc,trpc}.json`. Data fields: `routed_mutating_endpoints_total`, `routed_mutating_endpoints_unguarded`, `auth_by_protocol: {http: N, ws: N, grpc: N, trpc: N}`. AUTH gate is skipped if no routed handlers are detected.
9
+
10
+ > Four load-bearing signals + one calibrating modifier (CR), integrated by the scorer. No weighted-math internal split — the LLM emits a single 0–5 band and cites the most load-bearing signal.
11
+
12
+ ---
13
+
14
+ ## Preflight
15
+
16
+ Before running this recipe, execute [`../../preflight.md`](../../preflight.md). Required:
17
+
18
+ - `semgrep` (polyglot static analysis — resilience rule-set)
19
+ - `gh`, `jq` (CI workflow introspection + CR evidence scrape)
20
+
21
+ If `semgrep` cannot be installed, emit `semgrep_resilience_findings: null, semgrep_NOT_RUN: true, reason: "..."` and flag loudly in `§6.1`. Do not substitute grep for it — the patterns Semgrep catches (`bare-except`, `empty-catch`, `unhandled-promise`) are structural, not lexical.
22
+
23
+ ---
24
+
25
+ ## Extraction recipe
26
+
27
+ ### Signal 1 — Filesystem (tests exist, framework configured, tests meaningful)
28
+
29
+ ```bash
30
+ # Test file count
31
+ find <repo> -type f \
32
+ \( -name 'test_*.py' -o -name '*_test.py' \
33
+ -o -name '*.test.ts' -o -name '*.test.js' \
34
+ -o -name '*.spec.ts' -o -name '*.spec.js' \
35
+ -o -name '*_test.go' \) \
36
+ -not -path '*/node_modules/*' -not -path '*/.venv/*' \
37
+ | wc -l
38
+
39
+ # Source file count (for ratio) — language-aware; see "Per-language thresholds" below
40
+ find <repo> -type f \
41
+ \( -name '*.py' -o -name '*.ts' -o -name '*.tsx' \
42
+ -o -name '*.js' -o -name '*.jsx' -o -name '*.go' \) \
43
+ -not -path '*/node_modules/*' -not -path '*/.venv/*' | wc -l
44
+
45
+ # Framework configs
46
+ ls <repo>/{pytest.ini,jest.config.*,vitest.config.*} 2>/dev/null
47
+ grep -lE 'pytest|jest|vitest|\[tool\.pytest\]' \
48
+ <repo>/{pyproject.toml,package.json} 2>/dev/null
49
+
50
+ # Test meaningfulness — assertion density + trivial-body ratio
51
+ TEST_FILES=$(find <repo> -type f \
52
+ \( -name 'test_*.py' -o -name '*_test.py' \
53
+ -o -name '*.test.ts' -o -name '*.test.js' \
54
+ -o -name '*.spec.ts' -o -name '*.spec.js' \
55
+ -o -name '*_test.go' \) \
56
+ -not -path '*/node_modules/*' -not -path '*/.venv/*')
57
+
58
+ # Assertions across all test files
59
+ echo "$TEST_FILES" | xargs grep -cE \
60
+ 'assert |expect\(|\.to(Equal|Be|Throw|Have|Contain|Match)|require\.|assertEqual|assertTrue' \
61
+ 2>/dev/null | awk -F: '{sum+=$2} END {print sum}'
62
+
63
+ # Trivial bodies (assert True / pass / expect(true))
64
+ echo "$TEST_FILES" | xargs grep -cE \
65
+ '^\s*(pass|assert True|expect\(true\)|return)\s*$' 2>/dev/null \
66
+ | awk -F: '{sum+=$2} END {print sum}'
67
+ ```
68
+
69
+ Capture: `test_file_count`, `src_file_count`, `test_ratio`, `framework_configured`, `assertion_density` (assertions per test file), `trivial_body_ratio` (trivial bodies ÷ total test bodies).
70
+
71
+ **Primary language detection** (for thresholds — see Aggregation):
72
+
73
+ ```bash
74
+ find <repo> -type f \
75
+ \( -name '*.py' -o -name '*.ts' -o -name '*.tsx' \
76
+ -o -name '*.js' -o -name '*.jsx' -o -name '*.go' \) \
77
+ -not -path '*/node_modules/*' -not -path '*/.venv/*' \
78
+ | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -1
79
+ ```
80
+
81
+ Capture: `primary_language` (py / go / ts / js / tsx / jsx).
82
+
83
+ ### Signal 2 — GitHub Actions (did CI test and pass?)
84
+
85
+ ```bash
86
+ # Recent runs
87
+ gh api "repos/<org>/<repo>/actions/runs?per_page=30" \
88
+ --jq '[.workflow_runs[] | {name, conclusion, event, head_branch}]'
89
+
90
+ # Workflow files → find test invocations
91
+ gh api repos/<org>/<repo>/contents/.github/workflows --jq '.[].path' | while read wf; do
92
+ gh api "repos/<org>/<repo>/contents/$wf" --jq '.content' | base64 -d \
93
+ | grep -E 'pytest|jest|vitest|go test|cargo test|npm test|yarn test'
94
+ done
95
+ ```
96
+
97
+ Capture: `ci_has_test_job` (bool), `latest_test_conclusion` (SUCCESS/FAILURE/NONE), `ci_test_invocation`.
98
+
99
+ ### Signal 3 — CodeRabbit reuse (**calibrating modifier**)
100
+
101
+ Reuse the CR scrape from 1.1 (see [code-quality.md](code-quality.md#signal-6--coderabbit-scrape-calibrating-modifier) Signal 6). Filter to resilience-relevant categories `[bug, warning]` — these are CR's signal for missing error handling, swallowed exceptions, or validation gaps on the PRs it reviewed.
102
+
103
+ ```yaml
104
+ categories_in_scope: [bug, warning]
105
+ ```
106
+
107
+ Capture:
108
+
109
+ - `cr_resilience_flags = by_category.bug + by_category.warning`
110
+ - `cr_resilience_per_kloc = cr_resilience_flags ÷ (scc_code_loc ÷ 1000)` (lower-is-better; `null` when `cr_pr_count == 0` or `cr_resilience_flags == 0` on a zero-actionable walkthrough)
111
+ - `cr_semgrep_overlap` — count of `file:line` flagged by both CR and Semgrep Signal 4. Prefer CR's context in evidence when they overlap; surface the overlap count for transparency.
112
+
113
+ **Banding rule:** CR is a calibrating modifier — it pulls the band down when `cr_resilience_per_kloc` is high and (when non-null) participates in the aggregation table below alongside `semgrep_resilience_per_kloc`. When CR data is empty, `cr_resilience_per_kloc` is `null` and the modifier is a no-op. Full rationale + plan-upgrade escape hatch in [`SKILL.md → CodeRabbit — calibrating modifier with null-data semantics`](../SKILL.md#coderabbit--calibrating-modifier-with-null-data-semantics).
114
+
115
+ **Semgrep remains the broadest resilience signal** (full-tree scan, not just reviewed diffs). Signal 4 below.
116
+
117
+ ### Signal 4 — Semgrep scan
118
+
119
+ ```bash
120
+ semgrep --config p/python --config p/javascript --config p/typescript \
121
+ --config p/go \
122
+ --json <repo> > /tmp/semgrep.json
123
+ ```
124
+
125
+ **Filter to resilience-relevant rules only.** Tight pattern (catches the actual phenomena, drops noise):
126
+
127
+ ```bash
128
+ jq '[.results[] | select(
129
+ .check_id | test(
130
+ "(swallow|bare-except|broad-except|empty-catch|unhandled-(rejection|promise|exception)|missing-(validation|null-check|type-check|input-validation)|no-(validation|check)|unchecked-(await|promise))"
131
+ )
132
+ )]' /tmp/semgrep.json
133
+ ```
134
+
135
+ **What was dropped from the earlier regex and why:**
136
+
137
+ - `error|exception` — matched every rule name containing those words (most security rules do), inflating findings ~10×.
138
+ - `injection|deserial|unsafe` — security territory; belongs in 1.4 Maintainability / SECURITY gate to avoid double-counting.
139
+ - `await|promise` (bare) — tightened to `unchecked-await` / `unhandled-promise` so async-correctness rules aren't indiscriminately matched.
140
+
141
+ Also dropped `p/security-audit` config — was producing security-skewed noise. Language packs alone carry the resilience rules we want.
142
+
143
+ Capture:
144
+
145
+ - `semgrep_total_findings`
146
+ - `semgrep_resilience_findings` (after filter)
147
+ - `semgrep_resilience_by_severity` (ERROR / WARNING / INFO)
148
+ - `semgrep_resilience_per_kloc`
149
+
150
+ **Note:** Semgrep ERROR severity does **not** act as a hard score cap — signal only.
151
+
152
+ ### Signal 5 — LLM sample (3 handler-or-resilience-dense files)
153
+
154
+ Tiered file selection — walk tiers until 3 files are picked:
155
+
156
+ **Tier 1 — request handlers (preferred):**
157
+
158
+ - Matches `*route*`, `*handler*`, `*controller*`, `*api*`, `*server*`, `*endpoint*`, `*view*`
159
+ - Under `src/`, `app/`, `internal/`, `pkg/` or root
160
+
161
+ **Tier 2 — resilience-dense files (for non-API repos: CLIs, agents, batch jobs):** Rank source files by `(try|catch|raise|throw|except|panic|recover) count ÷ LOC`; pick top 3. These are where resilience _matters_, regardless of repo shape.
162
+
163
+ ```bash
164
+ for f in $(find <repo>/src <repo>/app <repo>/internal \
165
+ -type f \( -name '*.py' -o -name '*.ts' -o -name '*.go' -o -name '*.js' \) 2>/dev/null); do
166
+ loc=$(wc -l < "$f")
167
+ hits=$(grep -cE '\b(try|catch|raise|throw|except|panic|recover)\b' "$f" 2>/dev/null)
168
+ [ "$loc" -gt 20 ] && echo "$(awk -v h="$hits" -v l="$loc" 'BEGIN{print h/l}') $f"
169
+ done | sort -rn | head -3
170
+ ```
171
+
172
+ **Tier 3 — fallback** (if Tier 1 & 2 yield nothing): 3 largest files under `src/` / `app/`.
173
+
174
+ Score each on:
175
+
176
+ - Error handling shape (bare except / granular try / explicit raise?)
177
+ - Input validation at boundaries (Pydantic / Zod / class-validator / manual / none?)
178
+ - Resilience patterns (retry decorators, timeout wrappers, circuit breakers?)
179
+
180
+ Capture: `llm_resilience_score` (0–5), `llm_notes` (one sentence), `llm_files_sampled` (list), `llm_sample_tier` (1/2/3).
181
+
182
+ ### Signal 6 — Multi-protocol AUTH route-scan (AUTH gate trigger)
183
+
184
+ Enterprise backends ship mutating endpoints. A POST/PUT/PATCH/DELETE (HTTP) — or a WebSocket message handler, gRPC unary/streaming method, or tRPC mutation — that doesn't run behind an auth middleware / dependency / decorator is an enterprise-grade resilience failure: anyone can mutate server state. This signal scans for routed mutating handlers across four protocols and checks each one for an auth guard. **Skipped (gate does not fire) if no routed handlers are detected** — a CLI or batch job isn't expected to carry auth.
185
+
186
+ ```bash
187
+ # HTTP — FastAPI / Flask / Django / Express / Gin / Echo
188
+ # Mutating verbs: POST / PUT / PATCH / DELETE. GET is informational, not gated.
189
+ semgrep --config p/python --config p/javascript --config p/typescript --config p/go \
190
+ --timeout 30 --metrics=off --disable-version-check --json \
191
+ --pattern-either \
192
+ --pattern '@app.post(...)' --pattern '@app.put(...)' \
193
+ --pattern '@app.patch(...)' --pattern '@app.delete(...)' \
194
+ --pattern '@router.post(...)' --pattern '@router.put(...)' \
195
+ --pattern '@router.patch(...)' --pattern '@router.delete(...)' \
196
+ --pattern 'app.post(...)' --pattern 'app.put(...)' \
197
+ --pattern 'app.patch(...)' --pattern 'app.delete(...)' \
198
+ --pattern 'router.post(...)' --pattern 'router.put(...)' \
199
+ --pattern 'router.patch(...)' --pattern 'router.delete(...)' \
200
+ --pattern 'r.POST(...)' --pattern 'r.PUT(...)' \
201
+ --pattern 'r.PATCH(...)' --pattern 'r.DELETE(...)' \
202
+ <repo> > /tmp/auth-http.json 2>/dev/null || true
203
+
204
+ # WebSocket — ws / Socket.io / SignalR / Django Channels / FastAPI WebSocket
205
+ # Mutating WS handlers are identified by event registration patterns.
206
+ semgrep --config=auto --timeout 30 --metrics=off --disable-version-check --json \
207
+ --pattern-either \
208
+ --pattern 'socket.on("$EVENT", $HANDLER)' \
209
+ --pattern 'io.on("connection", ...)' \
210
+ --pattern '@websocket.event' \
211
+ --pattern 'WebSocketHandler' \
212
+ --pattern '@sio.on(...)' \
213
+ <repo> > /tmp/auth-ws.json 2>/dev/null || true
214
+
215
+ # gRPC — grpc / grpcio unary + streaming server methods
216
+ # Service implementations typically subclass *Servicer; methods are RPC endpoints.
217
+ semgrep --config=auto --timeout 30 --metrics=off --disable-version-check --json \
218
+ --pattern-either \
219
+ --pattern 'class $S($B.Servicer): ...' \
220
+ --pattern 'def $METHOD(self, request, context): ...' \
221
+ --pattern 'grpc.UnaryUnaryClientInterceptor' \
222
+ --pattern 'grpc.ServerInterceptor' \
223
+ <repo> > /tmp/auth-grpc.json 2>/dev/null || true
224
+
225
+ # tRPC — @trpc/server mutations (not queries — queries are reads, not mutations)
226
+ semgrep --config=auto --timeout 30 --metrics=off --disable-version-check --json \
227
+ --pattern-either \
228
+ --pattern '$R.mutation(...)' \
229
+ --pattern 'router({ ...mutation: ... })' \
230
+ --pattern 't.procedure.mutation(...)' \
231
+ --pattern 't.procedure.input(...).mutation(...)' \
232
+ <repo> > /tmp/auth-trpc.json 2>/dev/null || true
233
+ ```
234
+
235
+ Parse each tool's output for the number of mutating handlers and, critically, whether each one is adjacent to or wrapped by an auth guard. A handler is "guarded" when any of these are present in the same scope:
236
+
237
+ - **Python/FastAPI**: `Depends(...)` call on the function signature that resolves to an auth dep, `@require_auth`/`@login_required` decorator, or a router prefix/dependency declaring auth
238
+ - **Python/Django**: `@login_required` / `@permission_required` / DRF `permission_classes`
239
+ - **Python/Flask**: `@login_required` / `@jwt_required` / flask-login / flask-security decorator
240
+ - **JS/TS/Express**: middleware ordering — auth middleware registered on the app or router before the route, or a `passport.authenticate(...)` / `requireAuth` / `verifyJWT` call in the handler chain
241
+ - **JS/TS/tRPC**: `t.procedure.use(authMiddleware).mutation(...)` or a `protectedProcedure` base
242
+ - **Go/Gin/Echo/chi**: auth middleware chain (`r.Use(AuthMiddleware)` / `router.Group("/x", AuthMiddleware)`)
243
+ - **WebSocket**: handshake-time auth in `connect` handler OR per-message token validation
244
+ - **gRPC**: `ServerInterceptor` registered at `grpc.Server(interceptors=[...])` OR explicit `context.authenticate(...)` in the method
245
+
246
+ The scan runs with a 30 s per-rule cap + 120 s wall-clock ceiling per protocol; tools that time out emit `<protocol>_NOT_RUN: true, reason: "semgrep timeout"` with no effect on the gate.
247
+
248
+ Aggregate per protocol, then across all protocols:
249
+
250
+ ```bash
251
+ # Count mutating handlers and unguarded ones, per protocol.
252
+ # The guard detection is scorer-side — LLM reads the semgrep output + surrounding
253
+ # source for each mutating handler and emits {path, line, protocol, guarded: bool, guard_mechanism: string|null}.
254
+ ```
255
+
256
+ Capture:
257
+
258
+ - `routed_mutating_endpoints_total` — int; sum across HTTP + WS + gRPC + tRPC
259
+ - `routed_mutating_endpoints_unguarded` — int; unguarded count, also summed
260
+ - `auth_by_protocol` — `{http: {total: N, unguarded: N}, ws: {total: N, unguarded: N}, grpc: {total: N, unguarded: N}, trpc: {total: N, unguarded: N}}`
261
+ - `auth_unguarded_examples` — list of `{path, line, protocol}` for the first 5 unguarded handlers (for evidence citation)
262
+ - `auth_NOT_RUN` — bool; true when no routed handlers were detected at all (CLI / batch / library repo)
263
+ - `auth_reason` — string; populated when `_NOT_RUN` or when a protocol scan timed out
264
+
265
+ **AUTH gate trigger:** `routed_mutating_endpoints_total ≥ 1` AND `routed_mutating_endpoints_unguarded ≥ 1`. Caps `testing ≤ 2`. Skipped (gate does not fire) when `auth_NOT_RUN == true`.
266
+
267
+ ---
268
+
269
+ ## Raw dumps (flat — files under `raw/` named `testing-*`)
270
+
271
+ Each signal writes its raw output to the flat `raw/` directory at the workspace root. Absent files = not extracted. Aggregates go to `scorecard.yaml` under `data.`.
272
+
273
+ | File | Source signal | Shape / note |
274
+ | --- | --- | --- |
275
+ | `raw/testing-tests-inventory.jsonl` | Signal 1 | One line per test file: `{path, assertions, trivial_body, loc}` |
276
+ | `raw/testing-llm-sample.jsonl` | Signal 5 | 3 lines, one per resilience-dense sampled file: `{path, tier, error_handling_shape, validation_shape, resilience_patterns, one_sentence}` |
277
+ | `raw/testing-semgrep-resilience.json` | Signal 4 | Post-filter semgrep output (regex narrowed to swallow / bare-except / unhandled-promise / missing-validation); security rules stripped |
278
+ | `raw/testing-gha-runs.json` | Signal 2 | Recent workflow runs (conclusion, event, branch) + workflow files with test invocations |
279
+ | `raw/testing-auth-scan-http.json` | Signal 6 | Semgrep output for HTTP mutating handlers (FastAPI/Express/Flask/Gin/Django POST/PUT/PATCH/DELETE). Absent when no HTTP server detected. |
280
+ | `raw/testing-auth-scan-ws.json` | Signal 6 | Semgrep output for WebSocket event handlers (ws/Socket.io/SignalR/Channels). Absent when no WS detected. |
281
+ | `raw/testing-auth-scan-grpc.json` | Signal 6 | Semgrep output for gRPC service implementations. Absent when no `grpc`/`grpcio` imports detected. |
282
+ | `raw/testing-auth-scan-trpc.json` | Signal 6 | Semgrep output for tRPC mutation procedures. Absent when no `@trpc/server` imports detected. |
283
+
284
+ CodeRabbit comments are dumped once by 1.1 under `raw/code-quality-cr-comments.jsonl`; this recipe reuses that file (see Signal 3) rather than re-scraping.
285
+
286
+ ---
287
+
288
+ ## Aggregation → 0–5 band
289
+
290
+ Scorer integrates the 5 signals. Rough directionality:
291
+
292
+ | Signal | Pulls score UP | Pulls score DOWN |
293
+ | --- | --- | --- |
294
+ | `test_ratio` (per-language — see table) | Above language threshold | 0 (triggers `UNTESTED` gate) |
295
+ | `assertion_density` + `trivial_body_ratio` | ≥ 2 assertions/file, < 10% trivial | < 1 assertion/file OR > 80% trivial → triggers `TESTS_TRIVIAL` gate |
296
+ | `ci_has_test_job` + `latest_test_conclusion` | Both + SUCCESS | Missing or FAILURE |
297
+ | `semgrep_resilience_per_kloc` | < 1 | > 5 |
298
+ | `llm_resilience_score` | 4–5 | 0–2 |
299
+ | `cr_resilience_per_kloc` _(modifier; participates only when non-null)_ | 0 on a reviewed repo (clean on reviewer context) | > 1 |
300
+
301
+ ### Per-language test ratio thresholds
302
+
303
+ Go colocates `foo_test.go` with `foo.go`; Python typically uses a separate `tests/` tree; TS varies. Fixed thresholds distort across stacks — use language-aware floors:
304
+
305
+ | Primary language | Top-band ratio | Floor (pulls band down) |
306
+ | ----------------------- | -------------- | ----------------------- |
307
+ | Go (`go`) | ≥ 0.50 | < 0.10 |
308
+ | Python (`py`) | ≥ 0.30 | < 0.05 |
309
+ | TypeScript / JavaScript | ≥ 0.20 | < 0.03 |
310
+ | Mixed / unknown | ≥ 0.25 | < 0.05 |
311
+
312
+ **Bands (absolute):**
313
+
314
+ - **5** — All banded signals top-tier (CI green, ratio above language threshold, Semgrep < 1/kLoC, LLM ≥ 4, assertion density healthy)
315
+ - **4** — Strong but one weaker signal
316
+ - **3** — Mixed: tests exist but resilience signals middling
317
+ - **2** — Tests present but resilience poor OR vice versa; or `TESTS_TRIVIAL` fires
318
+ - **1** — Near-zero tests (`UNTESTED` gate enforces this), many resilience hits
319
+ - **0** — Suppressed (gates raise to 1)
320
+
321
+ ---
322
+
323
+ ## Gates
324
+
325
+ | Gate | Trigger | Cap |
326
+ | --- | --- | --- |
327
+ | `UNTESTED` (hard) | `test_file_count == 0` | `testing` ≤ 1 |
328
+ | `TESTS_TRIVIAL` (soft) | `test_file_count > 0` AND (`assertion_density < 1.0` per test file OR `trivial_body_ratio > 0.8`) | `testing` ≤ 2 |
329
+ | `AUTH` | `routed_mutating_endpoints_total ≥ 1` AND `routed_mutating_endpoints_unguarded ≥ 1`. Covers HTTP (POST/PUT/PATCH/DELETE), WebSocket event handlers, gRPC unary+streaming, tRPC mutations. Skipped when `auth_NOT_RUN == true` (no routed handlers detected — CLI/batch/library). | `testing` ≤ 2 |
330
+
331
+ All three can fire independently and stack. `AUTH` is the "enterprise-grade mutation without authentication" case — a POST endpoint that lets any unauthenticated caller mutate server state is a testing failure regardless of how clean the happy-path tests are. `TESTS_TRIVIAL` is the "tests exist but are meaningless" case (a single `def test(): pass` file passes `UNTESTED` but should not reach band 3+).
332
+
333
+ ---
334
+
335
+ ## Fallback — signals missing
336
+
337
+ - `test_file_count == 0` → `UNTESTED` gate fires regardless; cap at 1.
338
+ - No CI workflows → drop signal 2, rely on 1, 4, 5.
339
+ - `cr_pr_count == 0` OR CR empty (Free-plan walkthrough-only) → `cr_resilience_per_kloc: null`, Signal 3 modifier is a no-op; rely on 1, 2, 4, 5 for the band.
340
+ - Semgrep doesn't support a repo's primary language → drop signal 4, rely on 1, 2, 5.
341
+ - Semgrep not installed (preflight failed) → emit `semgrep_NOT_RUN: true`, rely on 1, 2, 5, flag in §6.1.
342
+ - LLM tier 1 & 2 yield nothing → use tier 3, annotate `llm_sample_tier: 3` (confidence is lower).
343
+
344
+ Annotate evidence with `"Signals used: [...]"` so the audit trail is explicit.
345
+
346
+ ---
347
+
348
+ ## Output (what the scorer emits)
349
+
350
+ ```yaml
351
+ sub_criterion: testing
352
+ score: 3
353
+ evidence: "Tests present (test_ratio 0.40); 2.1 avg assertions/test; CI test job passes; Semgrep 3.1 resilience hits/kLoC (swallowed exceptions dominate); LLM (tier 2, resilience-dense) sees inconsistent try/except in sampled handlers."
354
+ data:
355
+ test_file_count: 23
356
+ src_file_count: 57
357
+ test_ratio: 0.40
358
+ primary_language: py
359
+ framework_configured: true
360
+ assertion_density: 2.1
361
+ trivial_body_ratio: 0.04
362
+ ci_has_test_job: true
363
+ latest_test_conclusion: SUCCESS
364
+ cr_resilience_flags: 8
365
+ cr_resilience_per_kloc: 0.8
366
+ cr_semgrep_overlap: 2
367
+ semgrep_total_findings: 142
368
+ semgrep_resilience_findings: 31
369
+ semgrep_resilience_by_severity: { ERROR: 4, WARNING: 20, INFO: 7 }
370
+ semgrep_resilience_per_kloc: 3.1
371
+ llm_resilience_score: 3
372
+ llm_sample_tier: 2
373
+ llm_files_sampled: [src/orchestrator.py, src/retry.py, src/handlers/inbox.py]
374
+ signals_used: [filesystem, gha, semgrep, llm, coderabbit]
375
+ gates_triggered: []
376
+ ```
377
+
378
+ When CR data is empty (`cr_pr_count == 0` or all walkthroughs actionable-free):
379
+
380
+ ```yaml
381
+ cr_resilience_flags: 0
382
+ cr_resilience_per_kloc: null
383
+ cr_semgrep_overlap: 0
384
+ signals_used: [filesystem, gha, semgrep, llm, coderabbit_empty]
385
+ ```
386
+
387
+ `null` signals the scorer to skip the CR modifier for this project.
388
+
389
+ ---
390
+
391
+ ## Caveats
392
+
393
+ - Semgrep pre-built packs skew toward security. Tightened regex keeps resilience focus; worth 3–5 custom rules if scoring reveals gaps.
394
+ - CodeRabbit reuse is a calibrating modifier, not a primary axis; `null` on Free-plan walkthrough-only repos means no-op. Full rationale + plan-upgrade escape hatch in [`SKILL.md → CodeRabbit — calibrating modifier with null-data semantics`](../SKILL.md#coderabbit--calibrating-modifier-with-null-data-semantics).
395
+ - `AI_AUTHORED_SUSPECTED` flag (from 1.1) remains informational — LLM reviewer should note AI-generated resilience patterns (e.g. copy-pasted try/except blocks with identical logging).
396
+ - LLM sample size is 3. Diverse codebases may score unevenly. Evidence must name the files.
397
+ - `trivial_body_ratio` heuristic may false-positive on pytest `parametrize` stubs where the real body lives in fixtures. Acceptable — over-triggering a soft gate is safer than under-triggering.
398
+ - Per-language `test_ratio` thresholds are **absolute**. Each language's floor/top band is fixed in the table above.
399
+ - AUTH scan's guard-detection is LLM-adjudicated — Semgrep surfaces the mutating handler; the scorer reads surrounding context to decide whether a guard is present. This is a deliberate trade-off: pattern-matching every auth idiom across 4+ protocols and 10+ frameworks is combinatorial, and an LLM with the source in front of it is more accurate than a brittle megapattern. When the LLM's confidence on guard-detection is low (unusual framework, ambiguous middleware chain), emit `auth_reason: "guard detection uncertain"` and let the gate fire conservatively — false-positive AUTH is cheaper than false-negative.