Zum Inhalt springen

Baseline gate: fail only on new CodeCharter findings

Fail only on new findings by accepting a baseline of existing ones.

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-status over --git-ref to 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 — High confidence when only the container moved (the simple name is unchanged), Renamed confidence 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 --yes to 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-ref range 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: 0 clean migration (every stale identity resolved, or nothing needed migrating), 1 unresolved/ambiguous identities remain (or the run was cancelled — nothing was written), 64 usage 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-baseline and commit it.

Behavior notes

  • A missing --baseline file 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-baseline only records and exits 0; it never reports findings or trips --fail-on.
  • --write-baseline always records the full (severity-filtered) analysis result: combined with --diff/--git-ref, the written baseline is not diff-scoped.