Lenscheck documentation
Two packages. Every command, flag, and CI recipe in one place.
Install
| Package | Install | Where it runs |
|---|---|---|
| lenscheck-semantic-reviewer | pip install lenscheck-semantic-reviewer | Your CI (a GitHub Action / CLI). Never imported by your app. |
| lenscheck-contract | pip install lenscheck-contract | Inside your Django app (a real dependency). |
The reviewer needs Python 3.8+, plus git and tar. The contract needs Python 3.10+ and, for the Django features, django>=4.2.
Two vantage points
| lenscheck-semantic-reviewer | lenscheck-contract | |
|---|---|---|
| Sees | the PR diff, statically (AST) | the real app at startup + runtime |
| Output | a comment on the PR | fails CI (diff) + blocks calls (guard) |
| Setup | none — add the Action | export in CI + optional middleware |
| Imported? | no — it's a tool | yes — decorators + middleware |
🔎 lenscheck-semantic-reviewer
A semantic PR reviewer for Python web apps. It reads the diff, tells you what it means and where to look, and can enforce a confirmed invariant corpus — no LLM, no API key. CLI entrypoint: lenscheck.
lenscheck review
Run a semantic review of a PR or commit range. With no repo, it reviews the current git repo's branch vs. its default branch.
lenscheck review <repo|url> --pr 128 --out pr_review.md --json pr_review.json --sarif pr_review.sarif
| Flag | What it does |
|---|---|
repo | Positional. Local path or git URL. Defaults to the current git repo. |
--pr N | Review a GitHub PR by number (needs a GitHub URL repo). |
--commit SHA | Review a single commit (its diff vs its parent). |
--merge SHA | Review a merge commit (base ^1, head ^2). |
--base / --head | Explicit base..head range. |
--out FILE | Markdown review. Default pr_review.md. |
--json FILE | Structured review JSON (drives inline comments + labels). |
--html FILE | Self-contained HTML viewer. |
--sarif FILE | SARIF 2.1.0 for GitHub code scanning. |
--invariants FILE | Confirmed invariant corpus JSON to enforce (see the bridge). |
--fail-on {violation,crit} | Exit non-zero on 🔴 invariant violations (violation) or any 🔴 (crit). |
lenscheck post
Post a review to a PR — a sticky summary comment, optional inline comments, and one triage label.
lenscheck post 128 pr_review.md --json pr_review.json --inline --label
| Arg | What it does |
|---|---|
pr file | Positional: the PR number and the markdown review file. |
--json FILE | The review JSON (required for --inline / --label). |
--inline | Pin each finding as an inline comment on the exact changed line. |
--label | Apply one lenscheck:* triage label for the PR's top severity. |
lenscheck serve
Start the Lenscheck web UI to explore a repo/PR in the browser.
cd your-repo && lenscheck serve --port 8765
| Flag / env | What it does |
|---|---|
--repo · LENSCHECK_DEFAULT_REPO | Repo to open. |
--invariants · LENSCHECK_INVARIANTS | Invariant corpus to enforce in the UI. |
--host · HOST | Bind host. Default 0.0.0.0. |
--port · PORT | Bind port. Default 8765. |
--pr / --commit / --merge / --base / --head | Open this target on load. |
--no-open | Don't open a browser window. |
LENSCHECK_TOKEN · LENSCHECK_ALLOWED_REPOS · LENSCHECK_BASE_PATH | Auth token, repo allowlist, and base path when hosting the UI. |
lenscheck invariants
Discover candidate invariants from a repo's history (ranked by how persistently they held), for a human to confirm into a corpus. the reviewer's own discovery — see also lenscheck-contract invariants
lenscheck invariants <repo> --snapshots 40 --out invariants.discovered.json
repo | Repo to sample. |
--snapshots N | How many commits across history to sample. |
--out FILE | Discovered corpus. Default invariants.discovered.json. |
lenscheck digest
Org-wide leadership roll-up from the labels Lenscheck applies to PRs.
lenscheck digest --org my-org --repos repos.txt --slack $SLACK_WEBHOOK
--org | Required. GitHub org/owner to summarize. |
--repos FILE | Optional file of owner/repo lines → adds an adoption line. |
--slack URL | Slack incoming-webhook to post the digest to. |
GitHub Action
The zero-setup path. Add it to a pull_request workflow.
name: lenscheck
on: pull_request
permissions: { contents: read, pull-requests: write, issues: write }
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: AnkushSinghGandhi/lenscheck-semantic-reviewer@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
fail_on: crit # violation | crit | none
inline: true
label: true
| Input | Default | What it does |
|---|---|---|
github_token | ${{ github.token }} | Token for PR data + posting the comment. |
fail_on | none | violation, crit, or none — when to fail the build. |
invariants | — | Path to a confirmed invariants JSON to enforce. |
comment | true | Post the review as a sticky PR comment. |
inline | true | Pin each finding on its exact changed line. |
label | true | Apply one lenscheck:* triage label (needs issues: write). |
scan_deps | true | Scan bumped dependencies for new capabilities via PyPI. |
upload_sarif | true | Upload SARIF to code scanning (needs GitHub Advanced Security on private repos; set false without it). |
Outputs: review_markdown (pr_review.md) and review_sarif (pr_review.sarif).
🛡️ lenscheck-contract
Your Django backend keeps a list of what it does — and can't lie about it. It records routes, models, and effects at runtime, diffs that contract in CI, and blocks undeclared effects while the app runs. CLI entrypoint: lenscheck-contract.
Decorators
Optional — Level 1 works with zero code. Declare what matters and the guard can enforce it.
from lenscheck_contract import contract
@contract.route("POST /orders", auth="user") # method + path + who may call it
@contract.effects("net:api.stripe.com", "email") # every outside call it's allowed to make
def create_order(request):
...
@contract.job("nightly-invoices", schedule="0 2 * * *")
@contract.effects("net:api.stripe.com")
def nightly(): ...
| Decorator | What it declares |
|---|---|
@contract.route("METHOD /path", auth=...) | A route and its required auth (public, user, …). |
@contract.effects(*patterns) | Allowed effects. Patterns: net:host, net:*.stripe.com, net:*, email. |
@contract.job("name", schedule=...) | A background job and its (optional) cron schedule. |
Guard & middleware
The guard hooks the socket layer once — so requests, httpx, urllib, boto3, smtplib and anything else are all covered. An undeclared call in error mode never leaves the process.
# settings.py
MIDDLEWARE = ["lenscheck_contract.middleware.ContractMiddleware", ...]
# apps.py / conftest.py
from lenscheck_contract import guard
guard.install(mode="error", allow=["net:*.internal"]) # off | record | warn | error
| Mode | Behaviour |
|---|---|
off | Do nothing. |
record | Observe only — collect what actually happens (for suggest). |
warn | Warn on an undeclared effect, but allow it. |
error | Raise UndeclaredEffect — the call is blocked. |
Don't know what to declare? Run tests in record mode, dump guard.suggestions(), and feed it to lenscheck-contract suggest.
lenscheck-contract export
Write the contract as JSON. Reads Django's real router + model registry — routes built in loops and DRF routers included.
lenscheck-contract export --settings myproj.settings -o contract.json
-o, --output FILE | Where to write. Omit to print to stdout. |
--settings MOD | DJANGO_SETTINGS_MODULE. |
--root DIR | Project root to put on sys.path. Default . |
--import-module MOD | Extra modules to import (repeatable) so their decorators run. |
--no-django | Declarations only; skip Django probing. |
lenscheck-contract diff
Compare two contract files (base vs head) and print what a reviewer cares about. This is the CI gate.
lenscheck-contract diff base.json head.json --markdown --fail-on risky
base head | Positional: the two contract JSON files. |
--markdown | Format for a PR comment. |
--fail-on {never,risky,review,any} | Exit 1 at this severity. Default never. risky = auth weakening, new egress, dropped fields, relaxed uniqueness. |
lenscheck-contract invariants
Emit a confirmed-invariant corpus for the reviewer's --invariants. This is the bridge — see below.
lenscheck-contract invariants --settings myproj.settings -o invariants.json
-o, --output FILE | Where to write the corpus. |
--settings / --root / --import-module / --no-django | Same as export. |
lenscheck-contract suggest
Turn a recorded run into ready-to-paste decorators.
lenscheck-contract suggest observed.json # prints @contract.effects(...) per handler
observed | JSON produced by guard.suggestions() after a record-mode run. |
🔗 The bridge: runtime truth → the reviewer
On its own the reviewer guesses your invariants from git history. lenscheck-contract hands it the truth: your app's declared egress destinations become an enforced allowlist, and its auth/PII rules are confirmed. A brand-new domain sneaking into a PR then becomes a 🔴 critical alert — grounded in fact.
# 1. export the real contract as an invariant corpus (runs your app)
lenscheck-contract invariants --settings myproj.settings -o invariants.json
# 2. hand it to the reviewer — it stops guessing, it enforces
lenscheck review <repo> --pr 128 --invariants invariants.json --fail-on violation
external-egress-allowlist (your declared net: destinations), auth-before-write, and pii-egress-authed — all marked confirmed, so the reviewer treats them as hard rules, not hunches.Full CI — both tools on one PR
name: pr-checks
on: pull_request
permissions: { contents: read, pull-requests: write, issues: write }
jobs:
# 1) the reviewer — comments on the PR (soft heads-up)
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: AnkushSinghGandhi/lenscheck-semantic-reviewer@v1
with: { github_token: "${{ secrets.GITHUB_TOKEN }}", fail_on: crit }
# 2) the contract — hard gate on structural change (+ feeds the reviewer)
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: pip install -e . lenscheck-contract
- run: lenscheck-contract export --settings app.settings -o head.json
- run: lenscheck-contract invariants --settings app.settings -o invariants.json
- run: git checkout ${{ github.base_ref }}
- run: lenscheck-contract export --settings app.settings -o base.json
- run: lenscheck-contract diff base.json head.json --markdown --fail-on risky
The third catch — the runtime guard — runs in your test/staging job, because it needs the app actually running (guard.install(mode="error")).