|
| 1 | +# review — PR Diff Analyzer |
| 2 | + |
| 3 | +Analyzes a pull request diff and outputs two things: |
| 4 | + |
| 5 | +1. **Complexity Score** — a weighted composite that estimates how long the PR will take to review, derived from lines changed, files touched, and cyclomatic complexity of new code (detected via tree-sitter AST queries on JS/TS/Python/Go). |
| 6 | +2. **New-code coverage gaps** — which newly added lines are NOT covered by the provided LCOV or Cobertura report, with a pass/fail gate against a configurable floor. |
| 7 | + |
| 8 | +Both outputs are available on every commit, not just in CI — making this practical as a local pre-push check or a `greengate run` pipeline step. |
| 9 | + |
| 10 | +## Usage |
| 11 | + |
| 12 | +``` |
| 13 | +greengate review [OPTIONS] |
| 14 | +
|
| 15 | +Options: |
| 16 | + --base <REF> Diff base: commit, branch, or tag [default: HEAD~1] |
| 17 | + --staged Diff staged changes instead of committed diff |
| 18 | + --coverage-file <FILE> LCOV or Cobertura XML coverage file to cross-reference |
| 19 | + --min-coverage <PCT> Minimum % coverage required for newly added lines [default: 80] |
| 20 | + --complexity-budget <N> Fail if Complexity Score exceeds this; 0 = warn only [default: 0] |
| 21 | + --format <FMT> Output format: text | json | sarif [default: text] |
| 22 | + --annotate Post results as a GitHub Check Run + PR comment |
| 23 | + -h, --help Print help |
| 24 | +``` |
| 25 | + |
| 26 | +## Complexity Score |
| 27 | + |
| 28 | +The score is computed on the **added lines only** (not deleted or context lines): |
| 29 | + |
| 30 | +``` |
| 31 | +raw = (lines_added × 0.3) |
| 32 | + + (files_changed × 5.0) |
| 33 | + + (cyclomatic_nodes × 2.0) |
| 34 | + + (lines_removed × 0.1) |
| 35 | +
|
| 36 | +score = round(raw) |
| 37 | +estimated_review_minutes = min(raw × 0.5, 120) |
| 38 | +``` |
| 39 | + |
| 40 | +**Tier labels:** |
| 41 | + |
| 42 | +| Score | Label | Estimated review time | |
| 43 | +|---|---|---| |
| 44 | +| 0 – 20 | Quick Review | < 10 min | |
| 45 | +| 21 – 50 | Normal Review | 10 – 25 min | |
| 46 | +| 51 – 100 | Complex Review | 25 – 50 min | |
| 47 | +| 101+ | Large PR — consider splitting | > 50 min | |
| 48 | + |
| 49 | +**Cyclomatic complexity** is counted by running tree-sitter queries over the added code: |
| 50 | +- JS/TS/TSX/JSX: `if`, ternary, `switch case`, `for`, `while`, `do`, `catch` |
| 51 | +- Python: `if`, `elif`, `for`, `while`, `except`, boolean operators, conditional expressions |
| 52 | +- Go: `if`, `for`, `switch`, `select`, `binary_expression` |
| 53 | +- Other file types: 0 (not counted, but lines/files still contribute to the score) |
| 54 | + |
| 55 | +## New-code coverage gaps |
| 56 | + |
| 57 | +When `--coverage-file` is provided, greengate cross-references the diff with the coverage report line by line: |
| 58 | + |
| 59 | +| Line state | Outcome | |
| 60 | +|---|---| |
| 61 | +| In coverage report with `hits > 0` | **covered** — counts toward % | |
| 62 | +| In coverage report with `hits == 0` | **uncovered** — gap, counts against % | |
| 63 | +| Not in coverage report | **unmeasured** — excluded from % calculation (no penalty) | |
| 64 | + |
| 65 | +Per-file and overall `new_code_coverage_pct` are computed from covered + uncovered lines only. The gate fails if `overall_pct < min_coverage`. |
| 66 | + |
| 67 | +## Examples |
| 68 | + |
| 69 | +```bash |
| 70 | +# Complexity score only (no coverage gate) |
| 71 | +greengate review --base main |
| 72 | + |
| 73 | +# With coverage gate — fails if new-code coverage < 80% |
| 74 | +greengate review --base main --coverage-file coverage/lcov.info --min-coverage 80 |
| 75 | + |
| 76 | +# Staged changes (before committing) |
| 77 | +greengate review --staged --coverage-file coverage/lcov.info |
| 78 | + |
| 79 | +# JSON output (machine-readable) |
| 80 | +greengate review --base HEAD~1 --coverage-file coverage/lcov.info --format json |
| 81 | + |
| 82 | +# SARIF output — upload uncovered lines to GitHub Security tab |
| 83 | +greengate review --base HEAD~1 --coverage-file coverage/lcov.info --format sarif > review.sarif |
| 84 | + |
| 85 | +# Set a hard complexity budget (fail if score > 100) |
| 86 | +greengate review --base main --complexity-budget 100 |
| 87 | + |
| 88 | +# GitHub Actions — annotate PR with uncovered lines + score |
| 89 | +greengate review \ |
| 90 | + --base "${{ github.event.pull_request.base.sha }}" \ |
| 91 | + --coverage-file coverage/lcov.info \ |
| 92 | + --min-coverage 80 \ |
| 93 | + --annotate |
| 94 | +``` |
| 95 | + |
| 96 | +## Sample output (text) |
| 97 | + |
| 98 | +``` |
| 99 | +╔══ PR Review ═══════════════════════════════════════╗ |
| 100 | + Complexity Score : 47 (Normal Review, ~23 min) |
| 101 | + Files changed : 5 |
| 102 | + Lines added/del : +120 / -34 |
| 103 | + Cyclomatic nodes : 18 |
| 104 | +╚════════════════════════════════════════════════════╝ |
| 105 | +
|
| 106 | +New-Code Coverage: 73.3% ✗ (target: 80.0%) |
| 107 | +
|
| 108 | + src/engine.rs 12/15 added lines covered (80.0%) ✓ |
| 109 | + src/scanner.rs 6/11 added lines covered (54.5%) ✗ |
| 110 | + Uncovered lines: 88, 89, 92, 95, 101 |
| 111 | +
|
| 112 | +⚠️ Review gate FAILED. |
| 113 | +Error: greengate review: quality gate failed. |
| 114 | +``` |
| 115 | + |
| 116 | +## JSON output schema |
| 117 | + |
| 118 | +```json |
| 119 | +{ |
| 120 | + "complexity": { |
| 121 | + "score": 47, |
| 122 | + "tier": "Normal Review", |
| 123 | + "estimated_review_minutes": 23, |
| 124 | + "files_changed": 5, |
| 125 | + "lines_added": 120, |
| 126 | + "lines_removed": 34, |
| 127 | + "cyclomatic_nodes": 18 |
| 128 | + }, |
| 129 | + "coverage": { |
| 130 | + "overall_pct": 73.3, |
| 131 | + "min_required": 80.0, |
| 132 | + "passed": false, |
| 133 | + "files": [ |
| 134 | + { |
| 135 | + "file": "src/scanner.rs", |
| 136 | + "added_lines": 11, |
| 137 | + "covered": 6, |
| 138 | + "uncovered": 5, |
| 139 | + "unmeasured": 0, |
| 140 | + "coverage_pct": 54.5, |
| 141 | + "uncovered_lines": [88, 89, 92, 95, 101] |
| 142 | + } |
| 143 | + ] |
| 144 | + }, |
| 145 | + "passed": false |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +## Configuration |
| 150 | + |
| 151 | +Configure defaults in `.greengate.toml`: |
| 152 | + |
| 153 | +```toml |
| 154 | +[review] |
| 155 | +min_new_code_coverage = 80 # minimum % for newly added lines (default: 80) |
| 156 | +complexity_budget = 0 # 0 = warn only; > 0 = fail threshold (default: 0) |
| 157 | +``` |
| 158 | + |
| 159 | +CLI flags always override config values. |
| 160 | + |
| 161 | +## GitHub integration (`--annotate`) |
| 162 | + |
| 163 | +When run with `--annotate` and the `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_SHA` environment variables present, `review` will: |
| 164 | + |
| 165 | +1. Create a **GitHub Check Run** named `greengate review` with the Complexity Score as the title |
| 166 | +2. Post a **per-line annotation** (warning level) for each uncovered added line |
| 167 | +3. Post a **PR comment** with a markdown summary table |
| 168 | + |
| 169 | +If any of the required environment variables are absent, `--annotate` is a no-op. |
| 170 | + |
| 171 | +## Pipeline usage |
| 172 | + |
| 173 | +```toml |
| 174 | +[pipeline] |
| 175 | +steps = [ |
| 176 | + "scan", |
| 177 | + "review --base main --coverage-file coverage/lcov.info --min-coverage 80", |
| 178 | + "coverage --min 80", |
| 179 | + "audit", |
| 180 | +] |
| 181 | +``` |
| 182 | + |
| 183 | +## GitHub Actions |
| 184 | + |
| 185 | +```yaml |
| 186 | +- name: PR review (complexity + coverage gaps) |
| 187 | + if: github.event_name == 'pull_request' |
| 188 | + env: |
| 189 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 190 | + GITHUB_REPOSITORY: ${{ github.repository }} |
| 191 | + GITHUB_SHA: ${{ github.sha }} |
| 192 | + run: | |
| 193 | + greengate review \ |
| 194 | + --base "${{ github.event.pull_request.base.sha }}" \ |
| 195 | + --coverage-file coverage/lcov.info \ |
| 196 | + --min-coverage 80 \ |
| 197 | + --annotate |
| 198 | +``` |
| 199 | +
|
| 200 | +Required permissions: |
| 201 | +```yaml |
| 202 | +permissions: |
| 203 | + checks: write # create Check Runs |
| 204 | + pull-requests: write # post PR comments |
| 205 | +``` |
| 206 | +
|
| 207 | +## Exit codes |
| 208 | +
|
| 209 | +| Code | Meaning | |
| 210 | +|---|---| |
| 211 | +| `0` | All gates passed (or no coverage file specified) | |
| 212 | +| `1` | Coverage below threshold, complexity budget exceeded, or a runtime error | |
| 213 | + |
| 214 | +## Notes |
| 215 | + |
| 216 | +- Coverage gaps are computed only for **measurable** lines — lines not present in the coverage report at all (e.g. blank lines, comments, or untested new files) are treated as **unmeasured** and excluded from the percentage calculation rather than counted as failures |
| 217 | +- The complexity scoring runs tree-sitter on **added lines only** (not the full file). This means the score reflects only the new code being introduced, not background complexity in the file |
| 218 | +- For non-git directories or when the diff is empty, the command exits 0 with a brief message |
| 219 | +- SARIF output from `review` uses rule ID `GG/NewCodeUncovered` and severity `warning`; it can be uploaded to GitHub Advanced Security alongside the `scan` SARIF output |
0 commit comments