A local-first CLI that answers: if I change this file, what am I likely to affect?
Serval analyzes a Git repository's dependency graph, Git history, and CI configuration to estimate the blast radius of a code change, with a deterministic, explainable risk score, offline, with no account and no cloud backend.
Understanding downstream impact of a change usually means either tribal knowledge or grepping for imports by hand. Serval automates the mechanical part of that question using evidence already in the repository (imports, exports, history, CI) instead of guesswork or an LLM.
Implemented:
-
serval inspect [path]: direct/indirect dependents, Git history, relevant CI workflows, and an explainable risk score for a file. Defaults to.(the current directory) when no path is given; for a directory, every module inside it is analyzed and reported as a risk-sorted summary. -
serval diff [<ref>]: the same analysis for every file changed against<ref>(defaultHEAD, i.e. uncommitted changes) -
serval graph <path>: one-level dependency/dependent graph for a file -
serval history [path]: Git churn and co-change frequency, defaults to.when no path is given -
serval <path>: convenience alias forserval inspect <path> -
serval doctor: environment/repository checks, including whether a local Ollama daemon is reachable -
serval version, shell completion (serval completion bash|zsh|fish), generated man pages (man serval) -
--jsonon every analysis command;--output-format sarif(oninspect/diff) for a SARIF 2.1.0 report, e.g. forgithub/codeql-action/upload-sarif;--output <file>/-oto write a report to disk instead of stdout;--fail-on <level>(exit code 2) oninspect/difffor CI gating -
--explain(oninspectanddiff): asks an AI provider to explain each result's risk score in natural language. Off by default, makes no network call or subprocess spawn unless passed explicitly, and can never alter the deterministic score, only explain it.--explain-providerpicks the backend:ollama(default, a local daemon),claude/codex/gemini(a local CLI you already have installed and signed in — reuses your existing subscription, no API key needed), oranthropic/openai/gemini-api(a direct API call with your own key, viaANTHROPIC_API_KEY/OPENAI_API_KEY/GEMINI_API_KEY, no CLI required). On a directory or multi-file diff, this is one call per file, sequentially — can be slow. -
TTY-aware colored output, respecting
NO_COLOR -
Language support: JavaScript/TypeScript (relative ESM/CommonJS imports,
tsconfig.jsonpaths/baseUrl), Go (imports resolved againstgo.mod, standard library and external modules recorded as external), Python (plain and from-imports, including relative imports; repository root treated as the solesys.pathentry, standard library/third-party imports recorded as external), Java (plain, type-wildcard, and static imports; each file's ownpackagedeclaration derives its source root, standard/Maven-Gradle dependency imports recorded as external), and C (quoted#includeonly, resolved relative to the including file;.c/.h, not C++). -
.serval.yml: optional per-repository overrides for the critical-path keyword list and the Git history window (see docs/usage.md). -
CI relevance: GitHub Actions (
.github/workflows/*.yml), GitLab CI (.gitlab-ci.yml), Azure DevOps Pipelines (azure-pipelines.yml), Jenkins declarative pipelines (Jenkinsfile), CircleCI (.circleci/config.yml), and Bitbucket Pipelines (bitbucket-pipelines.yml). This completes the originally planned v0.1 CI provider set, plus CircleCI and Bitbucket Pipelines.
See docs/architecture.md for what's scaffolded versus what has real logic.
brew install AlbertoBarrago/tap/servalPublished automatically on tagged releases via GoReleaser: see .goreleaser.yml and AlbertoBarrago/homebrew-tap.
git clone https://github.com/AlbertoBarrago/serval
cd serval
go build -o serval .Requires Go 1.22+.
# zsh
echo 'source <(serval completion zsh)' >> ~/.zshrc
# bash
echo 'source <(serval completion bash)' >> ~/.bashrc
# fish
serval completion fish > ~/.config/fish/completions/serval.fishserval inspect src/auth/token.tsTarget
src/auth/token.ts
Direct impact
src/auth/middleware.ts
Indirect impact
src/api/client.ts
CI
integration-auth.yml
Git history
7 significant changes (last 90 days)
3 frequently co-changed modules
Risk
HIGH: 82/100
+28 14 downstream modules
+20 critical path (matched "auth" in src/auth/token.ts)
+14 high historical churn (7 changes)
+12 3 frequently co-changed modules
+8 1 CI workflow(s) affected
Machine-readable output:
serval inspect src/auth/token.ts --jsonCI gating:
serval diff --fail-on high # exits 2 if any changed file scores HIGHAI explanation (requires ollama serve running locally):
serval inspect src/auth/token.ts --explainSee docs/usage.md for the full command reference, resolution scope, and known limitations.
| Command | Description |
|---|---|
serval inspect [path] |
Full analysis (impact, history, CI, risk) for a file, or a risk-sorted summary for a directory. Defaults to . |
serval diff [<ref>] |
Full analysis for every file changed against <ref> |
serval graph <path> |
One-level dependency/dependent graph for a file |
serval history [path] |
Git churn and co-change frequency. Defaults to . |
serval <path> |
Alias for serval inspect <path> |
serval doctor |
Check environment and repository compatibility |
serval version |
Print version |
CLI -> Target Resolver -> Repository Scanner -> Language Analyzer
-> Dependency Graph -> Impact Engine -> Risk Engine -> Output
Full write-up, including why the JS/TS analyzer is regex-based rather than a full parser, the exact module-resolution scope, and the risk scoring model: see docs/architecture.md.
go build ./...
go vet ./...
go test ./...
gofmt -l . # should print nothing
make man # regenerate docs/*.1 after changing command help textFixture repositories used by tests live under testdata/fixtures/.
This repository is colocated with Git but developed using
Jujutsu (jj) as the primary VCS.
Plain git commands still work for read-only inspection (git log,
git diff), but the day-to-day flow uses jj.
Work happens directly on main — no feature branches, no pull
requests. A typical change looks like this:
# Fetch first: main may have moved from another machine/session since
# you last looked (this repo is worked from more than one).
jj git fetch
# Start a new change on top of the latest main
jj new main
# ... edit files ...
# Describe the change once it's ready
jj describe -m "fix: describe what actually changed"
# Advance main to this change, then push it
jj bookmark set main -r @
jj git push --bookmark mainWorking copy changes in jj are always an anonymous commit (@);
bookmarks (jj's equivalent of branches) are separate, movable pointers
that only advance when you explicitly jj bookmark set them, so
there's no "detached HEAD" state to worry about.
If jj git push refuses with "stale info" (remote main moved since
your last fetch), don't force it: jj git fetch, inspect the new
commit(s) (jj show <rev> --stat), and rebase your change on top
(jj rebase -d main) instead of overwriting.
MIT, see LICENSE.