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 makes no model call, so it costs nothing, it reads the same on every re-run of the same head, and 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 …
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 -->, and
with pr_labels on the PR gets a risk/<level> label. Locally, lgtmaybe
review prints the same summary line.
How the Level Is Decided¶
Four factors each raise 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 infrastructure, security, data migrations, or backup and recovery paths | high |
| Touching 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 |
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.
Blast Radius¶
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, and 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, or 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 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: |
if gh pr view "$PR" --json reviews --jq '.reviews[].body' \
| grep -q 'lgtmaybe-risk:critical'; then
gh pr edit "$PR" --add-reviewer my-org/platform-leads
fi
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.