Skip to main content

🛡️ Sentinel — Deployment Risk Analyzer

One question, one answer: is this change safe to deploy?

Sentinel answers from your repository's own bug history — not a language model's guess.

PyPI version Python 3.11+ License: MIT Tests MCP

┌─────────── Sentinel — deployment risk ────────────────────────┐
│   Score: 67/100   ██████████████████░░░░░   HIGH RISK         │
│   3 file(s) changed — since origin/main                       │
│   Scored by: trained model (4,854 commits)                    │
│                                                               │
│   Top reasons:                                                │
│    • auth/session.py — fixed 14 times in the last 2 years     │
│    • Author has never touched this file before                │
│    • Change spans 6 folders — high blast radius               │
│                                                               │
│   Blast radius: 12 files directly affected, 38 transitively   │
│                                                               │
│   → Hold this, or ship it behind a flag with a rollback ready.│
└───────────────────────────────────────────────────────────────┘

📑 Table of Contents


🔍 Why Sentinel Exists

Every team has files they are quietly afraid of — the ones that broke production twice last year, that only one person truly understands, that nothing has tests for. You can feel that instinct during code review, but you can't put a number on it.

Sentinel puts a number on it.

It reads your repository's full git history, finds which past commits later had to be fixed, traces the bug-inducing commits with a line-level blame algorithm (SZZ), and builds a lightweight gradient-boosted model trained entirely on your own data. It then scores any incoming change on a 0–100 risk scale — using that model, or a transparent rule engine when no model exists yet.

The score is always deterministic. A language model can optionally narrate the findings in plain English, but it structurally cannot affect the number. Remove the API key and the score is identical — only the paragraph disappears.


💡 What Makes It Different

Tool How it decides risk
Linters Style rules — know nothing about history
Coverage tools Tell you what's tested — not what has historically broken
PR-review bots Usually an LLM is the judge — opaque and unrepeatable
Sentinel Trains on your repo's own bug record. The score is a probability from your history, not an opinion.

Design principles baked into the code:

  • No lookahead in training. A historical commit's features are built only from data available before that commit. The running history tally is applied after featurising, never before.
  • Relative thresholds. Eight past bug fixes is remarkable in a young service and unremarkable in a fifteen-year-old library. Sentinel uses your repo's own percentile distribution as thresholds, not hardcoded absolutes.
  • Blast radius is separate from the score. The dependency graph tells you what else can break, but it is deliberately not fed to the model (doing so honestly would require re-parsing the whole tree at every historical commit).
  • The LLM is additive only. The AI layer can read the score. It cannot write to it — and that's enforced by the type system, not a prompt.

📦 Install

Requirements: Python 3.11+, and git on your PATH.

pip install (recommended)

pip install sentinel-risk

Installs everything in one step:

  • The sentinel CLI — sentinel scan, sentinel diff, sentinel train, sentinel evaluate, sentinel explain
  • The sentinel-mcp binary — starts the MCP server for Claude Desktop, Cursor, VS Code, and any other MCP client
  • All runtime dependencies (lightgbm, pydriller, GitPython, lizard, networkx, shap, rich, fastmcp, …)

No Node.js or npm required.

💡 Zero-Path Alternative (Runs Everywhere)

If your terminal does not have Python's scripts folder on your PATH environment variable, you can run them directly as Python modules:

  • For the CLI: python -m sentinel <command> (e.g. python -m sentinel scan)
  • For the MCP server: python -m mcp_server

📦 sentinel-risk on PyPI


🚀 Quickstart (60 seconds)

# 1. Install
pip install sentinel-risk

# 2. Go to any git repo you work in
cd /path/to/your/project

# 3. Score committed changes vs the default branch
sentinel scan

# 4. Or score what you haven't committed yet
sentinel diff

That's it — no configuration needed for the core score. Want plain-English explanations too? Run sentinel configure once, then add --explain.


🧠 Mental Model: Two Phases

Sentinel runs in two clearly separated phases. Training is optional — the rule engine works out of the box; training simply makes the score sharper for your codebase.

Phase 1 — Train (learn from history) · optional, run once per repo

1. Read git log                  → all commits in the window (default: 1,500)
2. Detect bug-fix commits        → keyword match on subjects ("fix", "hotfix", …)
3. SZZ blame                     → for each fix, blame the deleted lines against the
                                    parent to find the commits that introduced the bug
4. Label commits                 → blame-identified = 1 (risky), everything else = 0
5. Build feature vectors         → 20 features per commit, from info available *at commit time*
6. Train LightGBM + early stop   → ≤ 300 rounds, stops when held-out AP stops improving
7. Save model + metadata         → .sentinel/model.txt + .sentinel/model.meta.json

Phase 2 — Score (assess a change) · every scan / diff / MCP call

1. Identify changed files        → git diff vs base (scan) or working tree (diff)
2. Read file histories           → bug-fix counts, churn, per-author ownership
3. Static analysis               → cyclomatic complexity via lizard
4. Build feature vector          → the SAME 20-feature function used in training
5. Score                         → trained model if present; rule engine otherwise
6. Blast radius                  → networkx dependency graph → direct + transitive impact
7. Report                        → rich terminal table, or JSON for CI
8. Explain (optional)            → one LLM call for a plain-English narrative

Phase 2 never touches the network. Steps 1–7 are fully offline. Only step 8 — opt-in, via --explain — makes an API call.


🖥️ The CLI

Command What it does
sentinel scan Score committed changes vs the default branch (or --since <ref>)
sentinel diff Score uncommitted work in the working tree (staged + unstaged)
sentinel explain Score and narrate in plain English (shorthand for scan + --explain)
sentinel train Mine bug history (SZZ) and train the LightGBM model into .sentinel/
sentinel evaluate Time-based train/test split; compare the model to a baseline
sentinel configure Interactive wizard to set up an LLM provider and write .env
sentinel version Print the installed version

sentinel scan — committed changes

sentinel scan                        # vs auto-detected default branch
sentinel scan --since origin/main    # vs a specific ref
sentinel scan --since HEAD~10        # last 10 commits
sentinel scan --all                  # rank every tracked file by inherent risk
sentinel scan --explain              # also request the LLM narrative
sentinel scan --json                 # machine-readable JSON
sentinel scan --repo /other/project  # analyze a different repository

sentinel diff — uncommitted work

sentinel diff
sentinel diff --explain
sentinel diff --json

sentinel explain — score and explain

sentinel explain                     # explains committed changes
sentinel explain --diff              # explains uncommitted changes
sentinel explain --since HEAD~5
sentinel explain --json

sentinel train — build a model on your history

sentinel train                       # mine up to 1,500 commits (default)
sentinel train --max-commits 5000    # deeper history (slower — SZZ blame is the slow half)
sentinel train --repo /other/project

After training, every scan / diff automatically uses the model. Re-run it periodically (e.g. on a schedule in CI) as history grows.

sentinel evaluate — is the model actually good?

sentinel evaluate
sentinel evaluate --max-commits 3000

Example output:

Training rows:   3,621   Positives: 312 (8.6%)
Test rows:       1,207   Positives: 104 (8.6%)

Rule / baseline    PR-AUC: 0.271
LightGBM model     PR-AUC: 0.438   ✓ beats baseline

The split is always by time — newest commits in the test set, never a random shuffle. See Honest, time-based evaluation.


📐 How the Score Is Computed

The rule engine (default)

With no trained model, Sentinel uses a transparent, points-based engine. Every rule is a small pure function that either stays silent or contributes points with its own evidence string. The report isn't a separate rendering of the score — it is the list of rules that fired.

File-level rules (evaluated per changed file):

Rule Trigger (default) Points
Hot file (many past fixes) ≥ 8 bug-fix commits +35
Hot file ≥ 3 bug-fix commits +20
Large edit ≥ 300 lines changed +25
Sizeable edit ≥ 80 lines changed +12
Missing tests no test file found +20
Stale tests tests exist but weren't touched +10
Very high complexity peak cyclomatic complexity ≥ 20 +22
High complexity peak CCN ≥ 10 +12
Author new to file 0 prior commits by this author +15
Low ownership author made < 25% of past commits +8
Churn ≥ 25 commits ever touched it +10

Change-level rules (evaluated once for the whole change; skipped for --all):

Rule Trigger (default) Points
Broad change (files) ≥ 10 files touched +10
Broad change (lines) ≥ 600 lines total +10
Weekend deploy committed Sat/Sun +10
Outside working hours committed ≥ 22:00 or < 06:00 +12

Every threshold and point value lives in one place (config.py → RiskWeights) and is overridable via SENTINEL_RULES — so you can re-tune policy without editing scoring logic.

Relative thresholds (how Sentinel adapts to your repo)

Absolute counts don't travel between repositories. When your repo has enough history (≥ 20 files with history), Sentinel raises the hot-file and churn thresholds to your repo's own percentiles:

Signal Percentile used
"Very hot" file (bug fixes) 90th
"Hot" file (bug fixes) 75th
High churn 90th

The configured absolutes act as floors (max(config, percentile)), so a young repo whose 90th percentile is a single fix never hands out the top tier for one fix, and a threshold can never get looser than your written policy. Disable with SENTINEL_DISTRIBUTION='{"enabled": false}'.

Score bands

Band Score Meaning
🟢 low 0 – 34 Safe to deploy with a normal review.
🟡 medium 35 – 64 Deployable, but review properly and watch it after release.
🔴 high 65 – 100 Hold it, or ship behind a flag with a rollback ready.

How the headline number is assembled

The headline is the riskiest file's score, plus change-level context penalties — not a sum across all files. Summing would make any large refactor hit 100 and say nothing. A change is dangerous mainly because of the worst thing in it:

score = min(100,  riskiest_file_score  +  Σ change-level penalties)

scan --all is different: there's no "change", so it ranks every tracked file by inherent riskiness and reports the worst — change-level and timing rules are switched off, because "it's Saturday" describes the clock, not a deploy.


🧬 Learning From History: SZZ + the Model

This is what turns Sentinel from a linter into a predictor. The only honest source of "what a risky change looks like" is your repo's own record of what it later had to fix.

The SZZ pipeline:

  1. Find commits whose subject reads like a bug fix.
  2. For each, blame the lines that the fix deleted against the parent commit — the commits that last touched those lines are the ones that introduced the bug.
  3. Those commits are labelled bug-inducing (1); everything else is clean (0).

PyDriller's get_commits_last_modified_lines is exactly this line-level blame, already written and tested — which is why it earns its place in the stack alongside the faster git log --numstat walk used for aggregate counts.

Bug-fix detection matches these keywords on word boundaries (so fix never matches prefix):

fix · fixes · fixed · fixing · bug · bugs · bugfix ·
patch · hotfix · defect · regression · crash · broken

Issue references only count when a fixing verb is attached (closes #123, fixes ABC-45). A bare #1234 is usually just a squash-merge PR number — treating those as bug evidence mislabelled 51% of commits on the requests repo, versus 16% with the stricter pattern. Over-labelling here doesn't just add noise; it corrupts every downstream training label.

Two honest caveats (both designed around by the time-based evaluation):

  • Keyword-matched fixes are a proxy: a fix that says "tidy up" is missed; a feature commit that says "fix" is a false positive.
  • Recent commits look cleaner than they are — nobody's found their bugs yet. That's exactly why the newest commits belong in the test set, never the training set.

The 20-feature vector

Training and prediction call one vector() function, so the two can never drift apart. Feature order is part of the saved model's contract (append-only, never reordered).

# Feature Reads as
1 lines_added Lines added
2 lines_deleted Lines deleted
3 lines_changed Size of the change
4 files_changed Number of files touched
5 folders_touched Spread across folders
6 max_file_commits Change history of the busiest file
7 mean_file_commits Avg change history of files touched
8 max_file_bugfixes Bug-fix history of the worst file
9 mean_file_bugfixes Avg bug-fix history of files touched
10 new_files Brand-new files
11 min_author_ownership Familiarity with the least-known file
12 mean_author_ownership Familiarity with these files
13 files_author_never_touched Files the author never changed
14 max_complexity Complexity of the most complex file
15 mean_complexity Avg complexity of files touched
16 code_files_changed Source files touched
17 test_files_changed Test files touched
18 hour_of_day Hour of day
19 day_of_week Day of week
20 is_weekend Weekend

Every input is knowable at the moment the change was made — no future commits, no current file contents, no blast radius (see No data leakage).

How the model is trained

Capacity is deliberately small — a repo yields a few hundred labelled commits with a few dozen positives, and a wide model just memorises the training period.

Setting Default Why
num_leaves 7 Small tree — avoids memorising
learning_rate 0.05 Gentle boosting
num_rounds 300 Ceiling, not target — early stopping decides
early_stopping_rounds 30 Stop when held-out score plateaus
early_stopping_metric average_precision ROC-AUC is dominated by the huge true-negative pool
min_data_in_leaf 30 Prevents tiny, overfit leaves
train_fraction 0.75 Oldest 75% train, newest 25% test
holdout_fraction 0.85 Slice used to fit before early stopping
seed 42 Reproducible runs

Bug-inducing commits are a small minority, so the positive class is weighted by negatives / positives. Unweighted, LightGBM maximises accuracy by predicting "clean" for everything — a great-looking, useless model.

Measured example (requests repo): stopping on ROC-AUC halted at 17 rounds → 0.407 held-out PR-AUC; stopping on average precision ran to 43 rounds → 0.438. That's why the metric choice is a first-class setting.

The trained model is a plain text file plus a small JSON sidecar recording the exact feature list. Load a model built against a different feature set and the columns still line up numerically but the predictions are nonsense — so the sidecar lets Sentinel detect that, warn, and fall back to the rule engine instead of silently scoring garbage.

Honest, time-based evaluation

sentinel evaluate enforces two non-negotiable rules:

  • Split by time, never at random. Train on older commits, test on newer. A random split lets the model peek at commits that came after the ones it's tested on — the single easiest way to fake a good result.
  • Report rare-event metrics against a baseline. With positives at a few percent, plain accuracy is ~95% for a model that always says "clean". Sentinel reports ROC-AUC and PR-AUC next to a lines-changed-only baseline — a model that can't beat "big diffs are risky" hasn't earned its complexity.

Both metrics are implemented from scratch (ROC-AUC via the Mann-Whitney rank identity, with tie handling) and unit-tested against hand-computed values — no scikit-learn dependency, and you can explain exactly how your headline number was produced.

No data leakage: the guarantees

  • Historical features are built from data available before the commit; the running history tally is applied after featurising.
  • Blast radius is excluded from the model on purpose. Feeding it in honestly would need the import graph as it stood at each past commit — every source file re-parsed at every commit. Using today's graph for a 2015 commit would leak present-day structure into a past feature — exactly what the time-based split exists to catch.
  • Nothing derived from the future (later commits, current file contents) ever enters a feature.

💥 Blast Radius

Sentinel builds an in-memory import graph (networkx) and walks it backwards from the changed files — an edge points from importer to imported, so a change's dependents are its ancestors in the graph. In memory on purpose: a graph database would be permanent infrastructure for something rebuilt from scratch in under a second.

Three real-world messes are handled deliberately:

  • Cycles (a imports b imports a) terminate via a visited set, and files sitting in a cycle are flagged.
  • Hubs — a settings/types module imported by everything. Reporting "affects all 400 files" is true and useless, so depth is capped, listings are truncated (full counts kept), and hubs are named as hubs (≥ 20 direct dependents).
  • Direct vs transitive dependents are never merged — importing the changed file directly is a different proposition from being five hops away.

Languages parsed for the dependency graph: Python (via ast, so relative/aliased/multi-line imports are handled correctly) and Java. Adding a language means adding one LanguageParser — the graph and traversal code never changes.

Blast setting Default
max_depth (hops followed) 3
max_listed (names per list) 12
hub_dependents (hub cutoff) 20
max_analyzed_files (per-file impact) 40
max_files (graph size guard) 5,000
max_bytes_per_file 400,000

🤖 The AI Layer — Explanation Only

The LLM's job is to translate a finished analysis into human guidance. It is not a judge.

Why it structurally cannot move the score:

  • explain() receives an already-frozen ChangeRisk, computed before it ran.
  • It returns an Explanation object that has no field for a score, band, or reason.
  • The result is rebuilt immutably, so the analysis that was scored is the analysis that's reported.

That's a guarantee from the type system, not a promise in a prompt — because prompts aren't a security boundary. Remove the API key and the score is byte-for-byte identical; only the narrative disappears.

Graceful by design. No key, a timeout, a rate limit, a malformed response — every failure degrades to the deterministic report with a note. A risk tool that can't answer without a third-party API is worse than one with no AI at all.

Provider-agnostic (any OpenAI-compatible endpoint):

# NVIDIA NIM (default)
NVIDIA_API_KEY=nvapi-xxxx
SENTINEL_LLM_MODEL=meta/llama-3.3-70b-instruct
SENTINEL_LLM_BASE_URL=https://integrate.api.nvidia.com/v1

# OpenAI
OPENAI_API_KEY=sk-...
SENTINEL_LLM_BASE_URL=https://api.openai.com/v1
SENTINEL_LLM_MODEL=gpt-4o

# Local Ollama
SENTINEL_LLM_API_KEY=anything
SENTINEL_LLM_BASE_URL=http://localhost:11434/v1
SENTINEL_LLM_MODEL=llama3.2

# DeepSeek
SENTINEL_LLM_API_KEY=sk-...
SENTINEL_LLM_BASE_URL=https://api.deepseek.com/v1
SENTINEL_LLM_MODEL=deepseek-chat

Key resolution order: SENTINEL_LLM_API_KEY → NVIDIA_API_KEY → OPENAI_API_KEY. Set any one with sentinel configure (interactive) or in .env.

The narrative is returned as four grounded sections — summary, rollout, rollback trigger, monitoring — and the model is instructed to use only the facts it's given (real file names and numbers), so it can't invent history.


🔌 MCP Server — Use Sentinel from Claude / Cursor

Sentinel exposes one MCP tool, get_deployment_risk, over stdio (the transport Claude Desktop and Cursor use). Built on FastMCP, so it also speaks SSE/HTTP for remote use.

Tool signature:

get_deployment_risk(
    repo_path: str,           # absolute path to the git repo
    scope: str = "scan",      # "scan" | "diff" | "all"
    since: str | None = None, # e.g. "main", "HEAD~20" (for scope="scan")
    explain: bool = False,    # add the LLM narrative (needs a key)
) -> dict                     # the full risk report as JSON

The score is computed from your repo's history — never by a language model. explain defaults to false, so the tool is offline unless you opt in.

Claude Desktop

Edit claude_desktop_config.json (Windows: %APPDATA%\Claude\claude_desktop_config.json · macOS: ~/Library/Application Support/Claude/claude_desktop_config.json)

{
  "mcpServers": {
    "sentinel": {
      "command": "sentinel-mcp",
      "env": { "NVIDIA_API_KEY": "nvapi-your-key-here" }
    }
  }
}

Alternative (Runs everywhere without PATH issues):

{
  "mcpServers": {
    "sentinel": {
      "command": "python",
      "args": ["-m", "mcp_server"],
      "env": { "NVIDIA_API_KEY": "nvapi-your-key-here" }
    }
  }
}

Tip: GUI apps (Claude Desktop, Cursor, VS Code) may not inherit your shell PATH. If sentinel-mcp isn't found, you can use the python -m mcp_server alternative above, or use the absolute path:

  • Windows: C:\Users\<you>\AppData\Roaming\Python\Python313\Scripts\sentinel-mcp.exe (or wherever pip installed it)
  • macOS/Linux: ~/.local/bin/sentinel-mcp

Cursor

Settings → MCP → Add:

  • Name: sentinel
  • Type: command
  • Command: sentinel-mcp (or python -m mcp_server)

Remote HTTP / SSE (teams)

fastmcp run mcp_server/server.py --transport sse --port 8000
{ "mcpServers": { "sentinel": { "url": "http://localhost:8000/sse" } } }

Test it with the Inspector

# Using the global command:
npx @modelcontextprotocol/inspector sentinel-mcp

# Or via python -m (no PATH setup required):
npx @modelcontextprotocol/inspector python -m mcp_server

Open the printed URL, pick the sentinel server, and call get_deployment_risk interactively.


⚙️ GitHub Action — Score Every PR

A composite action posts (and updates) a single sticky comment on each PR with the score, reasons, and blast radius.

# .github/workflows/deployment-risk.yml
name: Deployment risk
on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write   # needed to post the comment

jobs:
  risk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # REQUIRED — the score comes from history
      - uses: Nishka30/SentinalScan@main
        with:
          threshold: "65"
          fail-over-threshold: "false"   # comment first, gate later
          # explain: "true"
          # nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }}

fetch-depth: 0 is not tidiness — it's required. A shallow clone hides commit history, so every file looks brand new and every score collapses.

Inputs: threshold (default 65), fail-over-threshold (default false), base-ref, comment (default true), explain (default false), nvidia-api-key, llm-model, python-version (default 3.12), github-token. Outputs: score, band, json-path.

Two upgrades worth knowing:

  1. Train a model in CI and cache it — the model beats the rule baseline. Do it on a schedule, not every PR:
    - uses: actions/cache@v4
      with: { path: .sentinel, key: sentinel-model-${{ github.ref_name }} }
    - run: sentinel train --max-commits 5000
      if: steps.cache.outputs.cache-hit != 'true'
    
  2. Gate merges by setting fail-over-threshold: "true" and adding the check to branch protection. Start with a high threshold and lower it as the team trusts it — a gate that fires on everything gets ignored, and an ignored gate is worse than none.

🧾 JSON Output (for scripting & CI)

--json (or the MCP tool) returns a structured payload:

{
  "score": 67,
  "band": "high",
  "recommendation": "Hold this, or ship it behind a flag with a rollback ready. …",
  "scope": { "mode": "scan", "author": "…", "files_analyzed": 3,
             "base_ref": "origin/main", "commits_walked": 4854, "model": { … } },
  "reasons": [ { "rule": "hot_file", "label": "…", "points": 35,
                 "detail": "14 past bug-fix commits", "path": "auth/session.py" } ],
  "files":   [ { "path": "…", "score": 62, "band": "high", "reasons": [ … ] } ],
  "blast":   { "direct_count": 12, "transitive_count": 38, "hubs": [ … ] },
  "explanation": { "available": false }   // populated only with --explain
}

Pipe it straight into a gate:

pip install sentinel-risk
sentinel scan --json > risk.json
SCORE=$(jq '.score' risk.json)
[ "$SCORE" -gt 80 ] && { echo "::error::Risk $SCORE exceeds 80"; exit 1; } || true

🎛️ Configuration Reference

Everything is read from environment variables or a .env file. No module other than config.py touches the environment, so "never hardcode secrets" is checkable by reading one file.

LLM

Variable Description Default
NVIDIA_API_KEY NVIDIA NIM key —
OPENAI_API_KEY OpenAI key —
SENTINEL_LLM_API_KEY Key for any other provider —
SENTINEL_LLM_BASE_URL OpenAI-compatible base URL https://integrate.api.nvidia.com/v1
SENTINEL_LLM_MODEL Model for explanations meta/llama-3.3-70b-instruct
SENTINEL_LLM_TIMEOUT Seconds to wait for prose 60
SENTINEL_LLM_MAX_RETRIES Retries (scan should fail fast) 1
SENTINEL_LLM_MAX_TOKENS Max tokens in the narrative 800
SENTINEL_LLM_TEMPERATURE Sampling temperature 0.2

Scoring, distribution, blast, training

Variable Overrides Example
SENTINEL_RULES any RiskWeights field {"file_large_lines": 150, "file_large_points": 30}
SENTINEL_DISTRIBUTION percentile settings {"enabled": false}
SENTINEL_BLAST blast-radius limits {"max_depth": 2}
SENTINEL_TRAINING any TrainingSettings field {"max_commits": 5000}
SENTINEL_BUGFIX bug-fix keyword/issue patterns —
SENTINEL_REPORT_TOP_FILES how many files the report lists 10

All values are JSON objects, validated by pydantic — a typo fails loudly instead of being silently ignored.


🚦 Exit Codes

Code Meaning
0 Success (a score was produced, or there was nothing in scope)
2 Repository error (not a git repo, bad ref, --all combined with --since)
3 Model error (e.g. no bug-inducing commits found to train on)

Stable codes make Sentinel safe to wire into a pipeline — a crash is never mistaken for a passing check.


🏗️ Architecture

The core library imports neither typer nor rich and never touches the network. Three callers share it — the CLI, the MCP server, and the GitHub Action — and only one has a terminal, so anything that must be said rather than returned comes back as AnalysisResult.warnings.

sentinalScan/
├── sentinel/                  # Core library — no terminal, no network
│   ├── analysis.py            # Orchestrator → AnalysisResult
│   ├── git_reader.py          # Low-level git (GitPython); default-branch + merge-base
│   ├── commit_log.py          # git log --numstat walk (+ rename tracking)
│   ├── history_mining.py      # SZZ labelling (PyDriller blame)
│   ├── features.py            # Data model + the 20-feature vector()
│   ├── risk_rules.py          # Transparent points-based engine
│   ├── model.py               # LightGBM: train / predict / SHAP explain
│   ├── blast_radius.py        # networkx dependency graph + impact walk
│   ├── static_analysis.py     # Cyclomatic complexity (lizard) + test detection
│   ├── explain.py             # LLM narrative (OpenAI-compatible) — read-only
│   ├── evaluation.py          # Time-based split, ROC-AUC / PR-AUC vs baseline
│   ├── report.py              # Rich terminal rendering
│   ├── pr_comment.py          # Renders the GitHub PR comment + threshold gate
│   ├── serialization.py       # JSON for --json and MCP
│   ├── results.py             # AnalysisResult / Scope / Explanation
│   └── config.py              # Every setting + tunable number (only env reader)
├── mcp_server/server.py       # FastMCP wrapper — one tool, delegates to sentinel/
├── github-action/             # Composite action + example workflow
├── tests/                     # One test file per module
└── pyproject.toml             # Package metadata + console-script entry points

Data flow (scan):

git diff → git_reader → FileChange[]
                            ↓
          git log → FileHistory (per file)
                            ↓
          lizard → ComplexityInfo (per file)
                            ↓
                     features.vector()        ← same fn used in training
                            ↓
              ┌── model.predict() ──────────── .sentinel/model.txt exists?
              │                                       yes ↗     no ↘
              │                           LightGBM score       rule engine
              │                                       ↘        ↗
              └────────────── ChangeRisk (score, band, reasons)
                                        ↓
                             blast_radius.compute()   ← networkx walk
                                        ↓
                                  AnalysisResult
                                        ↓
                          report.render() / serialization.to_dict()
                                        ↓ (optional, opt-in)
                          explain.explain() → LLM API → narrative

🚫 Non-Goals (Deliberate Simplicity)

Sentinel is intentionally small. Things it does not use, and why:

Not used Why
Vector database Nothing needs embedding or semantic search
Redis / cache A local scan is fast enough to recompute
Neo4j (graph server) The blast graph is built in memory in < 1s
"AI agents" The pipeline steps are just functions
Many MCP servers Sentinel exposes one server with one clear tool

Each cut removes months of maintenance and zero real value. That restraint is a feature.


❓ FAQ

Does sentinel scan call an LLM? No. scan and diff are fully offline — the score comes from the LightGBM model (if trained) or the rule engine, both pure math. The LLM is only ever contacted with --explain or sentinel explain. Run with no API key set and the score is identical.

Wait — so what's the "model" in sentinel scan? The LightGBM model — a gradient-boosted tree trained on your repo's own bug history. That's deterministic math, not a language model. The only LLM in the project is the optional explanation layer.

Why not just ask an LLM "is this risky"? A language model doesn't know your repo's history, defect patterns, ownership, or dependency graph — and it isn't repeatable. Sentinel measures those signals directly and gives the same answer twice.

What if my repo has almost no history? The rule engine handles it out of the box. Training needs at least some bug-fixing commits; with none, train exits with a clear message (code 3).

Does it support my language? Complexity and test detection cover many languages (Python, Java/Kotlin/Scala/Groovy, JS/TS, Go, Ruby, Rust, C#, PHP, Swift, C/C++, …). The dependency graph (blast radius) currently parses Python and Java; other languages still get history, complexity, and test signals.

Will it slow my CI? Scoring is seconds. Training is minutes (SZZ blame is the slow half) — so train on a schedule and cache .sentinel/ rather than on every PR.

Can the AI ever change the number? No. It receives a frozen result and returns prose with no score field. It's a structural guarantee, not a prompt instruction.


🛠️ Troubleshooting

Symptom Fix
Every file scores as if brand new (in CI) Add fetch-depth: 0 to actions/checkout — a shallow clone hides history
warning: the trained model was built from a different feature set … Sentinel updated its features; re-run sentinel train
error: no bug-inducing commits were found Increase --max-commits, or check that commit messages mention fixes
Claude/Cursor can't find sentinel-mcp GUI apps may not inherit PATH — use the absolute path to the binary
Explanation missing but score present No API key / timeout / rate limit — expected; the score is unaffected
LLM call is slow Swap SENTINEL_LLM_MODEL to a smaller/faster model; don't just raise the timeout

📖 Glossary

  • SZZ — the algorithm that links a bug-fixing commit back to the commit that introduced the bug, via line-level blame.
  • Blast radius — the set of files that (transitively) depend on what you changed.
  • Hot file — a file with an unusually high count of past bug fixes for this repo.
  • Churn — how often a file changes, independent of bugs.
  • Ownership — an author's share of a file's past commits.
  • Cyclomatic complexity (CCN) — the number of independent paths through a function; more paths, more places to hide a bug.
  • PR-AUC — area under the precision-recall curve; the right metric when positives are rare.
  • Band — the low/medium/high bucket a score falls into.

👩‍💻 Author & License

Nishka Shrimali — @Nishka30

Licensed under the MIT License — see LICENSE.

If Sentinel saves you from a bad deploy, leave a ⭐ on GitHub.

Release files for sentinel-risk 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sentinel-risk 0.1.3
File Size Uploaded
sentinel_risk-0.1.3.tar.gz 123.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sentinel-risk 0.1.3
File Interpreter ABI Platform
sentinel_risk-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 205.6 kB

Release files / sentinel_risk-0.1.3.tar.gz

Download URL sentinel_risk-0.1.3.tar.gz
Size 123.6 kB
Tags Source
SHA-256 checksum
How to use checksums
26ac6f865df2d40e80fbd3dfd0903e21fe1926c73ed69a410619810ccd8c6b9a
BLAKE2b-256 checksum
How to use checksums
5aff83ad04f914ba470712f3caba81a184f43099eac6c9685b14db2a5e20ff4a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / sentinel_risk-0.1.3-py3-none-any.whl

Download URL sentinel_risk-0.1.3-py3-none-any.whl
Size 82.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
395ae2554848558b35fc58d8c4811335dac3e6a4f09068a262896f452e574dfe
BLAKE2b-256 checksum
How to use checksums
14dbc71b011844f73cc9ded505623a97d9168689136d26dc5949abfd186e2c17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page