|
| 1 | +# Structural PR Review: BlockerKind Classification + Contamination Gate |
| 2 | + |
| 3 | +**PRs**: Ref #2465, Ref #2409 |
| 4 | +**Author**: AI Agent |
| 5 | +**Reviewed**: 2026-06-29 |
| 6 | +**Commit range**: 218d9e2a3..4e007d737 |
| 7 | +**Files**: 6 changed, +395/-52 lines |
| 8 | + |
| 9 | +## Summary |
| 10 | + |
| 11 | +This PR adds two independently valuable features to the merge coordinator: |
| 12 | +1. **BlockerKind classification**: queries Gitea's CI status API to distinguish CI failures (`ci_failed`, `ci_pending`) from policy/confidence holds (`not_mergeable`), so operators can triage blocked PRs at a glance. |
| 13 | +2. **Contamination gate**: scans PR file lists for artefact contamination (`.sessions/`, `.review_tmp/`, `.handoff/`, `.beads/`) before mergeability evaluation, preventing ADF agent noise from entering main. |
| 14 | + |
| 15 | +The implementation is clean and well-structured. The `PrEvaluation` struct gains a backward-compatible `Option<BlockerKind>` field. `evaluate_one` is now async to support CI status lookups, with an `Option<&GiteaClient>` parameter for testability. The contamination check runs first (blocking artefact PRs immediately), followed by CI-status-based blocker classification. |
| 16 | + |
| 17 | +**What was done well**: |
| 18 | +- Clean separation of concerns: `classify_blocker`, `check_contamination`, and `evaluate_one` each have single responsibilities. |
| 19 | +- Testability: `evaluate_one` accepts `Option<&GiteaClient>` so unit tests can pass `None` and skip network calls. |
| 20 | +- Pagination: `list_pr_files` correctly uses `X-Total-Count` header with a page loop and guards against infinite loops via `page_len == 0`. |
| 21 | +- Test coverage: 35 tests (34 existing + 1 new pattern-match test) all pass. |
| 22 | + |
| 23 | +**What remains problematic**: |
| 24 | +- One P1 finding: `list_pr_files` retry-with-backoff on non-retryable 404/4xx responses may waste time. |
| 25 | +- One P2 finding: `check_contamination` pattern matching uses `contains` which could produce false positives on filenames that coincidentally contain pattern strings. |
| 26 | + |
| 27 | +## Confidence Score: 4/5 |
| 28 | + |
| 29 | +Safe to merge with awareness of one P1 retry-noisiness issue. The retry-on-404 behaviour pre-exists in the `send_with_retry` helper (not introduced here), so it is consistent with the rest of the codebase. The contamination pattern false-positive risk is theoretical (no known collision in practice). Tests pass, clippy clean, fmt clean. No security or data-loss risks. |
| 30 | + |
| 31 | +## Important Files Changed |
| 32 | + |
| 33 | +| Filename | Overview | |
| 34 | +|----------|----------| |
| 35 | +| `types.rs` | Added `BlockerKind` enum with `Display` + `Serialize`, 2 new tests. Clean. | |
| 36 | +| `gitea.rs` | Added `head_sha` to PrSummary, `CommitCombinedStatus` struct, paginated `list_pr_files`, `get_commit_status`. One P1 finding (retry on 404). | |
| 37 | +| `evaluator.rs` | Core changes: `evaluate_one` async, `check_contamination`, `classify_blocker`. Updated all 5 existing tests + 1 new test. `#[allow(clippy::collapsible_match)]` on `evaluate_one`. | |
| 38 | +| `lib.rs` | Extended `extract_fixes` to match `closes/close/fixes/fix/resolves/resolve` (was only `fixes`). Deduplication via `BTreeSet`. | |
| 39 | +| `main.rs` | Minor doc/import sync. | |
| 40 | +| `pid_lock.rs` | Minor doc sync. | |
| 41 | + |
| 42 | +## Diagram |
| 43 | + |
| 44 | +```mermaid |
| 45 | +%%{init: {'theme': 'neutral'}}%% |
| 46 | +flowchart TD |
| 47 | + A[evaluate_all: fetch open PRs] --> B[evaluate_one: per PR] |
| 48 | + B --> C{head_sha + GiteaClient?} |
| 49 | + C -->|Yes| D[check_contamination] |
| 50 | + C -->|No / test mode| G[Skip contamination] |
| 51 | + D --> E{file list has artefacts?} |
| 52 | + E -->|Yes: .sessions/ etc| F[Hold: contaminated] |
| 53 | + E -->|No| G |
| 54 | + G --> H{pr.mergeable?} |
| 55 | + H -->|No| I[classify_blocker] |
| 56 | + H -->|Yes| J[Merge] |
| 57 | + I --> K[GET /commits/{sha}/status] |
| 58 | + K --> L{CI state?} |
| 59 | + L -->|failure/error| M[Hold: ci_failed] |
| 60 | + L -->|pending| N[Hold: ci_pending] |
| 61 | + L -->|no data| O[Hold: ci_no_status] |
| 62 | + L -->|success| P[Hold: not_mergeable] |
| 63 | + |
| 64 | + style D fill:#d4edda,stroke:#28a745 |
| 65 | + style I fill:#d4edda,stroke:#28a745 |
| 66 | + style F fill:#fff3cd,stroke:#ffc107 |
| 67 | + style M fill:#fff3cd,stroke:#ffc107 |
| 68 | + style N fill:#fff3cd,stroke:#ffc107 |
| 69 | + style O fill:#fff3cd,stroke:#ffc107 |
| 70 | + style P fill:#fff3cd,stroke:#ffc107 |
| 71 | +``` |
| 72 | + |
| 73 | +## Inline Findings |
| 74 | + |
| 75 | +**P1 `gitea.rs`, line 175**: **`list_pr_files` retries on 4xx/non-retryable errors** |
| 76 | + |
| 77 | +The `get_with_retry` helper (called by `list_pr_files` to fetch each page) retries with backoff on ANY non-success status, including 404 (PR deleted), 403 (token expired), and 422 (malformed). A 404 on page 1 will retry 4 times (1s+2s+4s = 7s wasted) before giving up. A 404 on page N (after successful page 1..N-1) will also retry. This behaviour is inherited from `send_with_retry` (pre-existing in the codebase), so it is consistent, but it still adds latency to failure paths. |
| 78 | + |
| 79 | +**Suggestion**: Consider adding a `non_retryable` status check to `send_with_retry` for 400-499 range, or wrap the page-loop in code that handles 404 specially (break early, return accumulated files). Alternatively, accept as-is since merge coordinator runs are infrequent and 7s latency on failure is negligible. |
| 80 | + |
| 81 | +**P2 `evaluator.rs`, line 77**: **`check_contamination` uses `contains` which risks false positives** |
| 82 | + |
| 83 | +The pattern match uses `file.starts_with(pattern) || file.contains(pattern)`. The `contains` check could match a path like `src/sessions_parser.rs` against `.sessions/` (though the `/` in the pattern makes this extremely unlikely). A more precise check would use `file.starts_with(pattern) || file.contains(&format!("/{}", pattern.trim_end_matches('/')))`. |
| 84 | + |
| 85 | +**Impact**: Theoretical only. No known collision in practice. The check is a hold, not a permanent block — a human can override it. Acceptable as-is for initial implementation. |
| 86 | + |
| 87 | +## Comments Outside Diff |
| 88 | + |
| 89 | +*No findings on unchanged code.* |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +*Last reviewed commit: 4e007d737 | Reviews (1)* |
0 commit comments