Skip to content

Risk of Change

Every review carries a risk of change verdict: low, medium, high or critical, with the reasons that produced it. It answers a different question from the findings. A finding says "this line is wrong". The verdict says "this change can reach a long way, so read it carefully", even when every line is right.

The verdict is deterministic: it comes from fixed rules. It makes no model call, so it costs nothing. It reads the same on every re-run of the same head commit, so a workflow can rely on it.

Where You See It

The review summary line names the level and its top reason:

3 findings · risk high (`src/core/models.py` is imported by 84 files, +2 more) · provider … · model … · lgtmaybe …

The change overview lists every reason in a Risk of Change section above High Impact Areas. The summary carries the verdict even when the overview is turned off or skipped, so every review has it.

The summary also carries a hidden marker, <!-- lgtmaybe-risk:high -->. With pr_labels on, the PR gets a risk/<level> label. Locally, lgtmaybe review prints the same summary line.

How the Level Is Decided

Each factor raises the level, and the verdict takes the highest:

Factor Raises to
A changed file imported by 5 or more files in the repository medium
A changed file imported by 20 or more files, or matching a risk.core_paths glob high
Touching an infrastructure, security, data migration, or backup and recovery path high
Touching a path for any other High Impact area (dependencies, compatibility, cost, …) medium
500 or more changed lines medium
50 or more changed lines of code with no test file changed medium
A regulated project stage medium

The area rows use the same path patterns as the High Impact Areas floor, applied to changed files that are not tests or documentation.

Two distinct high reasons make the change critical: a widely imported module and a Terraform file, say, or an auth change and a database migration. Two files in one area count once. A change touching only documentation and tests is low, whatever the project stage. The regulated floor never goes above medium, so it can't combine with one other high reason to make a change critical.

Blast Radius

The blast radius is how much existing code a change can reach. lgtmaybe measures it by counting the files that import each changed file.

The importer count comes from one ast-grep scan of the checked-out repository for Python, TypeScript and JavaScript imports. It parses files and runs nothing. On pull_request_target the checkout is the trusted base branch, and nothing from the scan is sent to the model.

The count measures the code that already depends on a file:

  • A file the PR adds has no existing importers.
  • Importers the PR itself changes are left out, because they are already in the diff.
  • Test files don't count as importers.

When a blast radius can't be measured, the verdict names the file instead of guessing. This happens:

  • for a language the scan doesn't read;
  • when there is no checkout;
  • when the ast-grep binary is missing or the scan fails;
  • when the scan runs past its 20-second limit.

A low verdict with unmeasured files always says so:

risk low (blast radius not assessed: `lib/billing.rb`)

The import count can't see modules reached by dynamic import, TypeScript path aliases or barrel re-exports. Name those in risk.core_paths (see risk).

Gating on the Verdict

lgtmaybe never approves or blocks a PR on its risk. The marker lets you decide what a level means for your team. For example, a step after the lgtmaybe step can request a second reviewer when the latest review summary carries lgtmaybe-risk:critical:

- name: Require a second reviewer on critical changes
  env:
    GH_TOKEN: ${{ github.token }}
    PR: ${{ github.event.pull_request.number }}
  run: |
    latest=$(gh pr view "$PR" --json reviews \
      --jq '[.reviews[].body // "" | select(contains("lgtmaybe-risk:"))] | last // ""')
    if grep -q 'lgtmaybe-risk:critical' <<<"$latest"; then
      gh pr edit "$PR" --add-reviewer my-org/platform-leads
    fi

Reading only the latest summary matters: each new head commit gets a fresh summary, so an older review can still carry a level the current change no longer has.

If the assessment itself fails, the summary reads risk unavailable and no marker is written, so a gate reads a missing marker as "unknown", never as a level.