The baseline gate lets CodeCharter fail a build only on new findings while tolerating a known set of pre-existing ones. It is the natural fit for adopting CodeCharter on an existing codebase: accept today's findings as a baseline, then keep new ones from sneaking in.
How it differs from diff mode
- Diff mode (
--diff/--git-ref) scopes to the lines a change touched — "new in this PR, by line". - Baseline mode (
--baseline) scopes to findings not in an accepted snapshot — "new versus the baseline, by identity". It catches a new finding even on a line the PR did not touch.
The two compose: with both set, a finding must be on a changed line and not in the baseline to be reported.
Fingerprints
Each finding gets a stable fingerprint derived from its identity (rule, entity
kind, entity name, category), its file path (repository-relative when
--workspace-root is set), and — for a finding on a method — the method's
parameter types, so two overloads that share a name (for example Process(string)
and Process(int)) get distinct fingerprints instead of being told apart only by
their position in the file. An occurrence counter keeps any remaining repeated
findings of the same kind in one file distinguishable. The fingerprint is
independent of line numbers, so it survives line shifts and reformatting. Moving
the file or renaming the entity changes the fingerprint, so the finding then
looks new — regenerate the baseline to re-accept it. The fingerprint is also
emitted in JSON output and as SARIF partialFingerprints["codecharter/v1"]; the
declaration identity below is emitted alongside it as codecharter/v2 when the
finding has one.
Declaration identity (move-stable matching)
For a finding on a type, method, property, field, event, or namespace, the
baseline also records a declaration identity derived from the .NET compiler
(Roslyn's DocumentationCommentId) — a stable id like M:MyApp.Service.Do(System.Int32)
that does not depend on the file path at all. When that finding's declaring file
moves, this identity still matches even though the fingerprint (which encodes the
path) no longer does, so the finding is not reported as new just because the file
moved. A finding is suppressed when either its identity or its fingerprint is
in the baseline — a baseline written before this existed keeps working unchanged,
matched on fingerprints alone.
This only covers those six entity kinds. An occurrence-level finding — a catch
clause, a literal, a comment, and similar — has no declaration to anchor an
identity to, so it is always matched by fingerprint alone, exactly as before.
Renaming the entity is still not covered: the identity is built from the
declaration's current name, so a rename still makes the finding look new (see the
overload example below — a declaration identity also tells overloaded methods
apart, the same disambiguation the fingerprint's parameter types provide).
Rename migration (baseline migrate)
Renaming still is not covered by the declaration identity itself (see above) —
but codecharter baseline migrate closes that gap for the common case: a
symbol renamed alongside its containing file. It never guesses purely from name
similarity; it only proposes a mapping for a stale identity when git's own
rename detection (git diff -M20% --name-status) says the old and new files
are "the same file, moved". The 20% threshold is deliberately permissive — a
rename that also rewrote most of the file's content still counts as evidence
here, because every candidate it produces is re-validated below against the
actual symbols at the new path, so a low-similarity false positive from git
simply yields zero or ambiguous candidates rather than a wrong mapping.
codecharter baseline migrate --baseline .codecharter/baseline.json --git-ref main..HEAD
- Runs
git diff -M20% --name-statusover--git-refto find renamed/moved files, then checks every baseline identity against the current compilation: an identity that still resolves needs nothing; one that no longer resolves (NotFound) is a migration candidate. - For each candidate, it looks for symbols of the same kind (type, method,
property, field, or event) in the renamed file's new location. Exactly one
unclaimed match proposes a mapping —
Highconfidence when only the container moved (the simple name is unchanged),Renamedconfidence when the symbol's own name also changed. Zero matches, more than one match, or a match already claimed by another mapping in the same run (a split or merge) are reported as unresolved, with every candidate listed, never silently guessed. - Prints every proposed mapping and every unresolved identity, then asks for
confirmation before writing anything. Pass
--yesto apply without a prompt (required in CI or any non-interactive context — the command refuses to prompt against a non-interactive stdin and exits without changes). - Applying a migration rewrites the stale identities in the baseline and
appends an entry to an append-only migration journal recorded in the same
file (when it ran, the
--git-refrange it was evidenced from, and every mapping applied) — the one place this otherwise timestamp-free file deliberately keeps one, so a rename mapping is never a silent, untraceable rewrite. - Exit codes:
0clean migration (every stale identity resolved, or nothing needed migrating),1unresolved/ambiguous identities remain (or the run was cancelled — nothing was written),64usage error (missing baseline file, unresolvable analysis target, or a failed git/analysis step).
Workflow
# 1. Accept the current findings as the baseline and commit it.
codecharter analyze MySolution.sln --write-baseline .codecharter/baseline.json
git add .codecharter/baseline.json && git commit -m "chore: codecharter baseline"
# 2. In CI, gate only on new findings.
codecharter analyze MySolution.sln --baseline .codecharter/baseline.json --fail-on error
- Generate the baseline with the same options you gate with (rules,
--severity), so the recorded set matches what is later evaluated. - Pass
--workspace-root <repo root>(or run from the repo root) when generating and gating, so fingerprints use repository-relative paths and the baseline is portable across machines and CI. The GitHub Action sets this for you. - The baseline file is deterministic (sorted, no timestamps), so it diffs cleanly in version control.
- To accept newly introduced findings (or prune resolved ones), regenerate the
file with
--write-baselineand commit it.
Behavior notes
- A missing
--baselinefile analyzes everything and prints a warning — it does not silently pass. (Useful before the first baseline is committed.) - A corrupt baseline file, or one written with a schema version the CLI
does not know, is a hard error (exit
2). --write-baselineonly records and exits0; it never reports findings or trips--fail-on.--write-baselinealways records the full (severity-filtered) analysis result: combined with--diff/--git-ref, the written baseline is not diff-scoped.