Semantic diff for Anchor/Solana programs. Semora parses two versions of a program, normalizes each into a Semantic Model, diffs them, and classifies the result as compatible / warning / breaking plus surfaces security regressions (e.g. a dropped signer check).
frontend → model::Program → diff::SemDiff → analysis → report
# from source (not yet on crates.io)
cargo install --git https://github.com/narsibhati-dev/semora --locked# terminal report (default)
semora diff ./path/to/v1 ./path/to/v2
# machine-readable, for CI
semora diff ./v1 ./v2 --format json
# SARIF for GitHub Code Scanning (findings anchored to source lines)
semora diff ./v1 ./v2 --format sarif --output semora.sarif
# markdown for a PR comment, or an LLM review prompt
semora diff ./v1 ./v2 --format markdown
semora diff ./v1 ./v2 --format prompt | llm
# inspect the rule catalog
semora rules
semora explain SIGNER_REMOVEDSample output:
instructions
~ withdraw
~ account authority signer: true -> false
verdict: COMPATIBLE
security findings
HIGH SIGNER_REMOVED instruction withdraw / account authority
signer check removed
Each side is a source directory (scanned for .rs) or a single file.
Each side is auto-detected: a .json file is parsed as an Anchor IDL,
anything else (a directory or .rs file) as Anchor source. A directory that
is an Anchor workspace (has Anchor.toml) is expanded to its [workspace] members / programs/* crates automatically. IDL and source inputs can be mixed
on the two sides.
semora diff ./target/idl/prog.json ./programs/prog # old IDL vs new sourceCI gate --fail-on <off|low|medium|high|critical> (default high) sets the
exit code: 1 on a breaking change or a security finding at/above the
threshold, 0 otherwise, 2 on error. --fail-on off disables gating.
Config an optional semora.toml (next to <NEW>, in the cwd, or via
--config) sets the gate and per-rule overrides. CLI flags win. See
semora.example.toml:
[gate]
fail_on = "high"
[diff]
rename_threshold = 0.5 # rename-detection sensitivity, 0.0..=1.0
[rules.INIT_IF_NEEDED_ADDED]
enabled = false # drop this rule's findings
[rules.ADDRESS_REMOVED]
severity = "high" # override severityRenames are detected when a removed and an added item share enough member names
(--rename-threshold, default 0.5); a lower value pairs more aggressively, a
higher value only pairs near-identical bodies.
Baseline --baseline accepted.json suppresses already-accepted findings so
CI only fails on new regressions:
{ "findings": [ { "rule": "SIGNER_REMOVED", "target": "instruction withdraw / account authority" } ] }For PR gating with results in the GitHub Security tab, copy
examples/github-actions/semora.yml into a consumer Anchor project it diffs
the PR base against HEAD and uploads the SARIF.
| module | role |
|---|---|
model/ |
normalized Program the spine everything else speaks |
frontend/ |
input → Program (source = Anchor .rs via syn; idl stubbed) |
diff/ |
Program × Program → SemDiff (serde-serializable core object) |
analysis/ |
classify the diff: breaking (compatibility) + security (findings) |
report/ |
render only human (terminal) and json (CI) |
tests/diff_cases.rs runs one case per fixture pair under tests/fixtures/,
with golden SemDiff JSON checked in per case. Regenerate goldens after an
intentional shape change:
BLESS=1 cargo test