What you get
Twelve features spanning the full loop: catch NFR gaps before merge, route them to the right reviewer, and keep the whole platform observable.
Static analysis
Rule-based checks for logging, metrics, tracing, and reliability NFRs on every changed Python, Java, or Go file.
LLM semantic assessment
Ticket text and diff context judged against the rubric for criteria no static rule can catch on its own.
Confidence-based routing
High-confidence failures on medium/high/critical rules auto-block; everything else is flagged for human review — nothing is silently dropped.
CI gate comments
One continuously-updated PR/MR comment with collapsible sections and direct links to each rule's docs page.
Overrides API + audit trail
Every human override of a routed decision is recorded and queryable, via CLI or HTTP.
Findings dashboard
Coverage trend, most-flagged criteria, and AI-vs-human agreement rate, backed by the findings store.
Slack / Jira notifications
Blocking failures and tier0 human-review escalations pushed straight to your team's tools.
IDE integration
A real language server (nfrgate-lsp) plus a VS Code extension surface the same diagnostics inline, before a PR ever opens.
Public rule reference
Every rule code has its own page: what it checks, why, and exactly how, per language.
Self-observability
Structured JSON logs, OpenTelemetry traces/metrics, and a /healthz liveness probe — nfrgate observes itself the same way it grades other services.
Multi-arch Docker images
linux/amd64 and linux/arm64, published to GHCR on every release.
Versioned DB migrations
Alembic-managed schema for the findings store — never hand-run SQL against a live database.
Installation
Every install path ships the same static analyzers and LLM assessment core; extras add the optional API/dashboard, IDE, and telemetry surfaces.
pip install nfrgate # static analysis + LLM assessment pip install "nfrgate[api]" # + the overrides API and findings dashboard (FastAPI/uvicorn) pip install "nfrgate[lsp]" # + the nfrgate-lsp language server for IDE integration pip install "nfrgate[otel]" # + OpenTelemetry trace/metric export pip install "nfrgate[dev,api,lsp]" # local development (test suite + all extras)
Or run the packaged API + dashboard via Docker (multi-arch: linux/amd64, linux/arm64):
docker pull ghcr.io/parab-rohit/rubric:latest docker run --rm -p 8000:8000 -e NFR_GATE_API_KEY=<a-real-secret> ghcr.io/parab-rohit/rubric:latest
Pin a specific released version instead of latest by tag, e.g. ghcr.io/parab-rohit/rubric:0.2.0 — see GitHub Releases for what's available.
Configuration
Every piece of deployment-specific config is an environment variable — no source edits needed to point at different infrastructure.
| Env var | Default | Purpose |
|---|---|---|
NFR_GATE_DATABASE_URL | postgresql://nfr_gate:nfr_gate_dev@localhost:5432/nfr_gate | Postgres connection for the findings store |
NFR_GATE_SERVICE_TIERS_PATH | config/service_tiers.yaml | Service-criticality tier registry; missing file defaults every service to the most conservative tier (tier0) |
NFR_GATE_LLM_BACKEND | ollama | ollama (local model, no API key) or anthropic (hosted, needs ANTHROPIC_API_KEY) |
NFR_GATE_LLM_MODEL | backend's own default | Override the model name for either backend |
NFR_GATE_OLLAMA_BASE_URL | http://localhost:11434 | Where to reach Ollama |
NFR_GATE_API_KEY | (none — required to start the API) | Shared bearer-token secret the overrides API and dashboard endpoints check, and the CLI sends |
NFR_GATE_API_URL | http://localhost:8000 | Where the nfrgate-override CLI reaches the API |
NFR_GATE_API_PORT | 8000 | Port nfrgate-api binds to |
NFR_GATE_SLACK_WEBHOOK_URL | (unset — disabled) | Slack incoming-webhook URL for blocking-failure/escalation notifications |
NFR_GATE_JIRA_BASE_URL / _EMAIL / _API_TOKEN / _PROJECT_KEY | (unset — disabled) | All four required together to enable Jira ticket creation on the same triggers as Slack |
OTEL_EXPORTER_OTLP_ENDPOINT | (unset — tracing/metrics are no-ops) | Where nfrgate exports its own traces/metrics; needs pip install "nfrgate[otel]" |
NFR_GATE_LOG_LEVEL | INFO | Level for nfrgate's own structured JSON logs (to stderr) |
Using the CI gate
Analyzes the diff on a PR/MR, posts one continuously-updated comment, and fails the job on high-confidence blocking violations. Both examples below are complete, runnable job definitions — not just the invocation line.
GitHub Actions
name: NFRGate
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write # needed to post/update the PR comment
jobs:
nfrgate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history -- pr_gate.py diffs base...head, a shallow clone can't resolve an arbitrary base SHA
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install nfrgate
- name: Run NFRGate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python -m nfr_gate.ci.pr_gate
Reads GITHUB_TOKEN, GITHUB_REPOSITORY, and GITHUB_EVENT_PATH from the standard Actions environment — the three env vars above and GitHub's own automatic ones are everything it needs, no further wiring. This exact shape (installing from a repo checkout instead of PyPI) is what .github/workflows/nfrgate-ci.yml in this repo runs for real, dogfooding it on this project's own PRs.
GitLab CI
nfrgate:
stage: test
image: python:3.12-slim
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
GIT_DEPTH: 0 # full history, same reason as the GitHub Actions example
script:
- pip install nfrgate
- python -m nfr_gate.ci.mr_gate
Reads GitLab's built-in CI_* predefined variables (CI_PROJECT_DIR, CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_DIFF_BASE_SHA, CI_PROJECT_PATH, CI_API_V4_URL, CI_PROJECT_ID, etc.) automatically — nothing to map by hand. It still needs a token with permission to post MR notes: add a GITLAB_TOKEN CI/CD variable (Settings → CI/CD → Variables, masked + protected — a project or group access token with api scope works) if the job's own CI_JOB_TOKEN isn't allowed to post notes on your instance; GitLab injects CI/CD variables into the job automatically, so nothing in the YAML above needs to reference it explicitly.
Both examples only need the static-analysis path to work out of the box. The LLM-assessment half defaults to a local Ollama model (NFR_GATE_OLLAMA_BASE_URL, http://localhost:11434 by default) — unreachable from a hosted runner unless you also set NFR_GATE_LLM_BACKEND=anthropic and ANTHROPIC_API_KEY, or run on a self-hosted runner with Ollama on it. Static analysis, routing, and blocking all work correctly either way; only the LLM-only/LLM-supplement findings are skipped without one of those two.
Overrides API & CLI
Every routed decision a human overrides is recorded in an auditable log.
nfrgate-api # or: uvicorn nfr_gate.api.app:app nfrgate-override list --source-ref <ref> nfrgate-override apply --source-ref <ref> --rule-code T1 --reviewer-id you@example.com --reason "..."
Requires NFR_GATE_API_KEY to be set on the server; the CLI sends it via NFR_GATE_API_URL. GET /healthz is an unauthenticated liveness probe independent of database reachability — point a load balancer's health check there. The API serves plain HTTP; put a TLS-terminating reverse proxy in front of it for any non-localhost deployment.
Findings dashboard
Coverage trend, most-flagged criteria, and AI-vs-human agreement rate — on the same running API, once it's deployed:
GET /dashboard
Open it in a browser, paste in your NFR_GATE_API_KEY, and click Connect — the key is kept in that browser's local storage only, never sent anywhere but your own API.
Notifications
Best-effort Slack and/or Jira notifications fire on blocking failures and on tier0 (always-human-review) failing results — never on a routine pass. Configure one, both, or neither; an unconfigured backend is silently skipped rather than an error. See the NFR_GATE_SLACK_WEBHOOK_URL / NFR_GATE_JIRA_* rows above.
IDE integration
The exact same rule engine that gates PRs in CI, surfaced as inline diagnostics before a PR ever opens.
pip install "nfrgate[lsp]" nfrgate-lsp # stdio transport — point any generic LSP client at it (Neovim, Emacs' eglot, ...)
A minimal VS Code extension lives in editors/vscode/ in the repo — see its README for setup (npm install, then F5 to launch an Extension Development Host).