Skip to content

The .abicheck.yml config file

.abicheck.yml is the per-project configuration file. It holds the stable, reviewed-in-a-PR properties of a project's ABI contract — build system, header compile context, severity policy, public-surface scoping, and suppression hygiene — as opposed to per-run invocation flags. See the Config Keys Reference for the exhaustive, generated list of every key/sub-key BuildConfig itself validates, and its exact required type (other recognized top-level keys, parsed by a sibling module, are also listed there but without a type); this page covers effective defaults, precedence, and a worked example.

Every field is optional; an absent, empty, or non-mapping file yields the all-defaults configuration. CLI flags always override the config, which in turn overrides the built-in defaults (CLI > config > default).

  • Loader (build/source blocks): load_build_config() in abicheck/buildsource/inline.py; parsed into the BuildConfig dataclass.
  • Precedence resolver (compare project-contract blocks): resolve_compare_config() in abicheck/cli_helpers_compare.py.

File discovery

Within any one directory, three locations are recognized, checked in this order (first match wins):

  1. .abicheck.yml — the original, project-root spelling.
  2. .github/.abicheck.yml — alongside workflows/CODEOWNERS, for a project that keeps tool configuration out of its own root.
  3. .github/abicheck/.abicheck.yml — a dedicated subdirectory, for a project that wants its abicheck config kept apart from other .github content (or that already groups per-tool config under .github/<tool>/).

Only the file's location changes between these three — its content, schema, and strictness rules are identical regardless of which one is used. A file present at a higher-precedence location always wins over one at a lower-precedence location in the same directory; see abicheck/config_paths.py for the exact, shared candidate list every discovery entry point below draws from.

Every command selects its config with one rule (resolve_project_config() in abicheck/config_paths.py), first match wins:

  1. An explicit --config <path>.
  2. The --sources tree root (all three locations, no parent walk), when --sources is given -- a source tree carries its own contract.
  3. The nearest config at or above the current directory, walking up to the filesystem root and checking all three locations in each directory.

So dump and compare run from inside a project checkout pick up the same .abicheck.yml with or without --config. (dump used to consult only the --sources root and silently ignore a config above the current directory.) The typed Python API does not walk up from the process's working directory: pass build_config explicitly there.

Note: an auto-discovered (untrusted) .abicheck.yml never causes a build command in build.query to run — it is skipped with a diagnostic. A build.query runs only when the config is supplied explicitly with --config (which marks it trusted for subprocess execution). No separate opt-in flag is needed or exists any more (the old --allow-build-query was always a no-op, and has since been removed outright).

Strict loading

Config loading is strict: an unknown top-level key, an unknown sub-key inside a recognized block, a value of the wrong type, or a bad enum value are all hard errors — not warnings. This is a behavior change from earlier abicheck versions, which warned on unknown keys and kept going. A malformed YAML file is also a hard error. On the CLI, any of these surfaces as a usage error (exit 64; see Exit Codes) — the run never proceeds on a config abicheck could not fully validate.

This "hard error" rule applies unconditionally to compare's own project config — severity, scope, policy — whether the file came from an explicit --config or was auto-discovered by walking up from the current directory: a parse failure always raises a usage error (exit 64), never a warn-and-continue. It only reaches as far as BuildConfig.from_dict() itself validates, though: targets:/bundles:/profiles:/baseline: are recognized top-level keys (so an unrecognized sibling key still errors) but their contents are opaque to this loader — compare never inspects them at all, so e.g. an invalid targets.foo.kind passes silently here. Only abicheck project validate/abicheck project plan (project_targets.py) actually validate that block's contents, deeply and independently of this loader.

A separate load of the same .abicheck.yml — the compile: block shared by compare/dump's L2 compile context (compiler, includes, sysroot, …), resolved by merge_compile_config() in cli_options.py — does distinguish explicit from auto-discovered: an explicit --config that fails to parse still fails loudly, but an auto-discovered file that fails only prints a warning and continues with the CLI's own compile context, since a config the user never pointed at explicitly could otherwise silently break an unrelated invocation just by existing somewhere upward of the working directory. See Build-context capture for that path in detail. Don't assume this leniency extends to the rest of the file's blocks — it's specific to that one loader, and only when it's the only one invoked: a dump/compare run with --sources/--build-info also goes through cli_buildsource.embed_build_source()'s own, separate config resolution (L3/L4/L5 evidence collection, not the L2 compile context), and that one raises a hard click.UsageError on a parse failure unconditionally — explicit --config or auto-discovered alike, same as the project-config rule above. Passing --sources therefore loses the auto-discovered warn-and-continue leniency even for a run that would otherwise get it through merge_compile_config() alone.

There is no longer an init/config scaffolding or diagnostic command (abicheck init, config validate, config show-effective are all gone) — write .abicheck.yml by hand, using this page as the schema/key reference. Since unknown keys are now a hard error rather than a silent warning, a typo or a key from a newer abicheck release will fail loudly instead of being ignored — set the top-level version: if you need to signal a schema generation to tooling, though it does not by itself suppress an unknown-key error.


Top-level keys

build:, sources:, severity:, scope:, suppression:, source:, compile:, debug:, bundle:, python:, gate:, release:, assurance:, deployment:, resource_limits:, performance:, policy:, version:, risk_rules:, crosschecks:, targets:, bundles:, profiles:, baseline:, bundle_variants:, and contract: are the recognized top-level keys. See the Config Keys Reference for the exhaustive, generated key/type list (BuildConfig's own schema); the sections below cover what each block does, its effective defaults, and behavior that isn't visible from the type alone.


build:

Drives inline build/source collection: an advisory build-system hint (system:, default auto), a build-query command (query:) to produce a compile DB, and/or an explicit compile_db: path or glob. query runs only when the config is passed explicitly with --config (trusted) — never from an auto-discovered config; no separate opt-in flag exists. See Producing source facts and Build & source data.


sources:

Public-header roots/globs (public_headers:, default []) defining the public surface, paths/globs excluded from source collection (exclude:, default []), and the L5 source-graph detail cap (graph:, summary (default, a cheap changed-scope CI graph) or full, a full replay scope).


severity:

Per-category severity map consumed by compare: a baseline preset (default/strict/info-only) plus per-category overrides (abi_breaking/potential_breaking/quality_issues/addition, each error/warning/info) — per-category levels override the preset. When any severity value is in effect, compare uses the severity-aware exit-code path. See Severity and Exit codes.


scope:

Public-surface scoping — the main false-positive control. public: (default effectively true) restricts analysis to the public exported surface. It has no CLI flag; for one run, compare --contract all turns it off and --contract public names it; collapse_versioned_symbols: (default false) collapses symbol-versioned duplicates before diffing; show_redundant: (default false) disables redundancy filtering. public_symbols: is an explicit public-symbol overlay, the only spelling for it — the per-run CLI duplicates that used to shadow this key were removed, so a project states it once, here — entries match exactly (the raw symbol, or a qualified name's trailing :: segment, so foo also matches ns::foo); globs/wildcards are not supported (mylib_* matches nothing), list each symbol. See API-surface intelligence.

exclude_headers: (default []) is the config spelling of --exclude-header: fnmatch-style patterns naming headers to drop from the parsed surface, so a header directory containing two headers that cannot be parsed in one translation unit stays usable as a -H operand (the reported case is Intel MKL's include/, which ships FFTW2 and FFTW3 headers declaring conflicting typedefs). A matching header is also scoped out when it is only reached through another header's #include, exactly like a toolchain header: anything only it declares is not observed (reported as reduced evidence rather than as a removal), while a type the library's own public API uses is still checked. This needs dependency scoping, so it does not apply under --include-system-declarations. Both sides of a comparison are narrowed by the same rules, and a pair whose two sides were narrowed differently is refused rather than compared.

This is not sources.exclude, and the distinction is load-bearing. sources.exclude narrows source collection — which files the L3–L5 build/source evidence layers gather. scope.exclude_headers narrows header extraction — which declarations the L2 header-AST parse ever sees. They act on different inputs at different layers, and neither implies the other: a project that wants a vendored header out of the parse is not asking for it to disappear from source collection, and the reverse.

A --exclude-header on the command line takes the whole decision: a run that states the flag at all uses exactly the rules it stated and ignores this key, rather than unioning the two. The rule set is part of the run's scope identity (it feeds effective_config_fields["surface.exclude_headers"] and therefore the configuration digest), so silently widening a stated set would make the run narrower than the command line says it is. A rule that matches no header warns once for the whole run — including a directory/package comparison, where the rules are release-wide and are stated once rather than repeated per library.

public_header_dirs: (default []) names directories (project-root-relative or absolute) whose headers are this project's public/internal boundary for the boundary-dependent cross-source checks (exported_not_public, public_not_exported, rtti_for_internal_type, public_to_internal_dependency). It is unrelated to public: above despite the name: public: decides whether findings are scoped to the public surface, public_header_dirs: says which headers draw that surface's boundary. Entries are folded in beside any -H directory (never a -H file). With neither, every declaration's header origin is unknown and those four checks report NOT_EVALUATED rather than a finding.

Ownership keys (dependencies:, private_headers:, private_namespaces:, dependency_evidence:) say who owns each declaration a header parse sees: the target (this library), a named dependency, or the toolchain. They classify, they do not filter: every header dump records each declaration's owner and contract, and the rules it was classified under, in the snapshot's extraction_scope . No declaration is kept or dropped because of them. A declaration whose contract is private or external owes no export, so it produces no public_not_exported finding. abicheck dump … --dry-run previews the rules and the owner of every -H header. The task guide is Target ownership; the plan behind the keys is Target ownership and extraction scope.

scope:
  public_header_dirs: [include/svs/]      # the target's roots
  dependencies:                           # named dependency roots
    - name: fmt
      header_roots: [include/svs/third-party/fmt/include/]
  private_headers: [include/svs/*/detail/**]   # owned, not promised
  private_namespaces: [svs::detail]
  dependency_evidence: full               # the only accepted value today

Roots are relative to the project root: the directory holding the config file, or the repository root for .github/.abicheck.yml. A -H directory is also a target root; a -H file is not. dependency_evidence accepts only full (keep every dependency declaration, today's behaviour); referenced is rejected until retention by reference lands. The rules are applied in this order:

  1. An explicit root beats the system-path heuristic: a target installed under /usr/include/svs/ is still the target's.
  2. The most specific root wins: a dependency vendored inside a target root belongs to the dependency. A root claimed by two owners is an error.
  3. A -I (include) directory is compile context. It never makes anything target-owned.
  4. private_headers (fnmatch patterns, matched against the path relative to the project root and the absolute path) and private_namespaces narrow only target-owned declarations to contract=private. svs::detail covers what is declared inside it (svs::detail::X), not svs::detailed.
  5. A file no root claims, outside the system directories, is owner=unresolved.
  6. A compiler builtin that castxml declares implicitly (__atomic_*, __builtin_*, __sync_*) belongs to the toolchain, whichever file castxml attributes it to.
  7. A declaration whose namespace disagrees with its file (a target file declaring fmt::formatter<svs::…>) keeps the file's owner and gets a diagnostic.

There is deliberately no namespace-based ownership key: a namespace filter lost owned declarations on every real target measured (the plan's M2).

Both sides of a compare are classified under the one project config, so their rules agree. A stored baseline keeps the rules it was dumped under; if they differ from the candidate's, the pair is still compared (presence is unchanged under full) and the report names the declarations that moved owner or contract. A baseline written before snapshot schema v52 has no recorded rules; it is compared with a note saying so.

on_incomplete: (warn, the default, or block) is Phase 7d's (one-comparison-product.md §4.1) CONFIG-only replacement for the former compare --on-incomplete-scope on the directory/package release fan-out — no CLI spelling exists any more. Governs what an incompletely checked comparison scope does to the exit code: warn reports every unchecked member and contributes 0; block contributes 1, folded with max() like the contract-coverage axis. A run that completed no comparison at all contributes 1 under either setting. See Multi-binary § Comparison scope and completeness.


contract:

Contract overlays: concrete documents that decide part of the public contract domain beside the headers. overlays: maps an overlay kind to a document path; post_manifest is the only kind today — a POST Python export manifest whose committed pp_*/ufunc-loop surface scopes the comparison (private __pp_* kernel churn is demoted to the filtered ledger, which every run discloses). See POST Python extensions.

contract:
  overlays:
    post_manifest: python/abi/post_manifest.json

A relative path resolves against the project root, like compile.include_dirs. This key is the overlay's only spelling (the former per-run flag was removed).

It applies only from a config named with --config. The overlay narrows what gates, and an auto-discovered .abicheck.yml is one a pull request can edit in the very checkout it is judged on, so a discovered value is noted on stderr and not applied. This is the same trust boundary build.query and compile.compiler sit behind. The composite Action likewise drops it from a discovered config; set its build-config input to a reviewed config to opt in. The overlay applies to a single-pair compare only. A directory/package comparison and a --no-baseline audit do not apply it and say so on stderr (an unapplied narrowing overlay can only add findings, never hide one). A stored-bundle-facts baseline rejects the contract: block like the other blocks it cannot honour.

acknowledgment:

Acknowledgment records and the additions-review gate. file: names the records document (a relative path resolves against the project root) and unacknowledged_additions: is allow (default), warn or block.

acknowledgment:
  file: abi/acknowledgments.yml
  unacknowledged_additions: block

file applies only from a config named with --config; a discovered value is noted and not loaded. unacknowledged_additions applies from any config. A --policy document stating its own acknowledgment: block wins over this one. See Change acknowledgment.

suppression:

Suppression hygiene policy (a project rule, distinct from the suppression rules file — see Related files): strict: (default false) treats suppression-file problems strictly; require_justification: (default false) requires a justification on every suppression entry. See Suppressions.


source:

method: pins the precise S-axis (evidence method, s0..s6) for power users.

Use a concrete s0..s6, not auto. When compare reads source.method from the config (i.e. no --depth on the command line), the value must resolve to a concrete method — compare rejects auto with a usage error. Pin a specific level here, or leave the key unset and let --depth (binary/headers/build/source — --max and the old full depth no longer exist) drive the collection depth per run.

See Evidence depth and the --depth dial. (graph is not a valid source: sub-key — a config with source: {graph: ...} now fails with an unknown-key error. The L5 graph-detail knob is sources.graph, in the plural sources: block above.)


compile:

The L2 header compile context. This is Phase 7's CONFIG surface (one-comparison-product.md §4.1/§4.2) — every field below has no CLI spelling at all, on any command, with no escape hatch and no per-side spelling. See Upgrading to 0.6 §C1 for the flag→key mapping.

  • frontend: — AST frontend (auto/castxml/clang/hybrid, case-insensitive — hybrid runs castxml and clang together and merges them). Was --ast-frontend on compare/dump.
  • std: — C/C++ standard, e.g. c++17.
  • include_dirs:/defines: — lists. defines: is the stable half of a pair: dump/compare also accept a per-invocation -D/--define NAME[=VALUE] , which merges with this list by macro name — the CLI value wins for the macro it names and every other entry here stays in force. Prefer this key for CI and baseline generation; use -D for one-off runs and experiments. It is one of two compile: fields with a per-run CLI counterpart — the other is include_dirs, whose -I/--include roots are searched before the configured ones rather than merged by name.
  • sysroot: — was --sysroot on compare/dump.
  • nostdinc: — boolean; was --nostdinc/--no-nostdinc on compare/dump.
  • compiler: — path to the compiler binary, or a cross-toolchain prefix (e.g. aarch64-linux-gnu-) when the value ends in -. Merges the former --compiler/--compiler-prefix pair into one spelling ("not one-for-one" — --compiler-prefix does not become its own compiler_prefix: key).
  • options: — a list of raw compiler flags passed through verbatim, each a single whitespace-free atom like std:/defines: below. Was the repeatable --compiler-option on compare/dump.
  • ast_frontend_fallback: — boolean; was --allow-ast-frontend-fallback (itself always a pure ABICHECK_ALLOW_AST_FALLBACK env-var toggle, so a config true has the identical effect).
  • allow_unsupported_castxml: — boolean; was --allow-unsupported-castxml (same env-var-toggle shape as ast_frontend_fallback: above, via ABICHECK_ALLOW_UNSUPPORTED_CASTXML).
  • frontend_context: — host/device; was --frontend-context.
  • lang: — c++/c; was --lang on compare/dump. Defaults to c++ when unset (no header/source-content language inference exists in this codebase to do better than that fixed default).

Values in compile.std/compile.defines/compile.options must each be a single whitespace-free compiler-option atom (a config scalar cannot expand into multiple compiler arguments).

A relative compile.include_dirs entry resolves against the project root — the directory containing the discovered config, or the directory containing .github/ when the config was found under .github/ or .github/abicheck/ (see File discovery) — never against .github/ itself.


debug:

Separate-debug-file resolution for ELF. On compare/dump this is Phase 7's CONFIG surface (one-comparison-product.md §4.1/§4.2) — every field below has no CLI spelling at all on those two commands (0.5.0 first demoted them to a config-key-with-override; Phase 7 removed the override itself, since this repo runs no deprecation window). The coarse per-run --debug-info stays a visible CLI flag on both commands — it is a per-run evidence input, not a stable project property. (It was spelled --debug-root until plan Phase 7n merged it into --debug-info, which now carries the whole role: a directory to search, a detached debug file, or — on compare — a debug package.)

format: (auto/dwarf/btf/ctf, case-insensitive, default auto-pick) forces the ELF debug format for both sides (was --debug-format); dwarf_only: (default false) uses DWARF as the primary source even when headers are available (was --dwarf-only); debuginfod: (default false) enables debuginfod network resolution (was --debuginfod); debuginfod_url: overrides DEBUGINFOD_URLS (was --debuginfod-url); pdb_path: — explicit path to a Windows PE PDB file, overriding automatic PDB discovery (was dump --pdb-path). This key is the only PDB input that survives, on any command, and it is a single value — not side-aware, so a two-sided PE comparison cannot point old and new at different PDBs.


bundle:

Cross-library bundle-analysis topology (CLI cleanup phase two, PR J) — the sole source for both settings now, replacing the removed --bundle-system-providers/--bundle-cohort CLI flags: system_providers: (a list of extra sonames to treat as system-provided, extending the built-in libc/libstdc++/libgcc/libtbb allow-list) and cohorts: (a list of co-versioned library name prefixes enabling the BUNDLE_SONAME_SKEW check). Entries are stripped of surrounding whitespace and empty entries dropped at parse time. system_providers: and cohorts: (the SONAME-skew check) both apply to compare's directory/package fan-out. (system_providers: also reached scan --artifact-set until 0.6 retired that mode.) Distinct from the plural bundles: block below, which serves a different, unrelated purpose (the project command family's target declarations). See Multi-binary § The bundle-analysis flags.


python:

CPython extension-module properties. One key today: abi3_floor: — the Py_LIMITED_API (stable ABI) version this project promises, e.g. abi3_floor: "3.9". Quote it: a bare 3.9 is a YAML float and is rejected rather than coerced.

Which floor a project targets is a stable, reviewed-in-a-PR property, not a per-run decision, so it lives here — and compare --abi3 VERSION is the per-run override on top of it. With either in effect, compare audits the candidate (NEW) side's imported CPython C-API against that floor and reports python_stable_abi_violation findings, marked as candidate-side enrichment, in the same report as the comparison itself. The findings are advisory (RISK): gate them through --policy / policy.overrides. A candidate that is not a recognisable CPython extension module is an evidence-contract error (exit 7), since the audit that was asked for cannot be performed at all. scan --abi3 accepts the identical version spelling.


gate:

CI gate policy demoted off the CLI (Phase 7d, one-comparison-product.md §4.1). One key today: fail_on_removed_library: (default false) — the former compare --fail-on-removed-library/--no-fail-on-removed-library, no CLI spelling any more. Exits 8 when a library present in OLD is proven removed in NEW on compare's directory/package fan-out — NEW's inventory must be proven complete; an unmatched library under an unproven inventory is reported as an incomplete scope instead. Whether a release enforces this gate is a stable project policy, not a per-run choice. See Multi-binary § Comparison scope and completeness.


release:

Directory/package release topology demoted off the CLI (Phase 7d, one-comparison-product.md §4.1), applying only to compare's directory/package fan-out — no CLI spelling exists for either key any more:

  • dso_only: (default false) — the former compare --dso-only: only compare shared objects, skip executables.
  • include_private_dso: (default false) — the former compare --include-private-dso: include private (non-public) shared objects from non-standard paths.

assurance:

P0.4's orthogonal analysis-assurance exit floor, demoted off the CLI (rulings.py deferred-option followup) — no CLI spelling any more. One key today: require_complete: (default false) — the former compare --require-complete-analysis: fail the step when analysis_assurance.status is not complete, independent of the compatibility verdict. Contributes exit 1, folded with max the same way --contract's coverage axis is : it raises a clean 0 to 1 and never lowers a 2/4. Applies to a single-pair compare and to a directory/package (release) fan-out alike (the release operand used to reject it). A release has one analysis_assurance per compared member, not one for the run, so its contribution is max over the members': over a one-member package that is the identity (it gates exactly as the scalar path does for the same pair), and any member whose analysis fell short floors the release no matter how many complete siblings it has. The release JSON then carries an analysis_assurance block naming the short members and why, plus a per-libraries[] analysis_assurance_status; a stored BundleFacts operand folds identically. See Exit codes § Analysis-assurance contribution.

assurance:
  require_complete: true

deployment:

The project's declared deployment constraints, demoted off the CLI — no CLI spelling exists any more: the former compare --env-matrix FILE is now this key, embedding EnvironmentMatrix's own YAML shape inline instead of a side file. When runtime_floors is set, a new symbol-version requirement is judged against the declared floor: at or below it → COMPATIBLE, above it → BREAKING, instead of the default COMPATIBLE_WITH_RISK verdict. Unlike release:'s two keys above, this one does apply to compare's directory/package fan-out — a project-wide property, not a per-invocation one, so it reaches every library the fan-out compares.

deployment:
  target_os: linux
  target_arch: x86_64
  compilers: [gcc-13, clang-17]
  runtime_floors:
    GLIBC: "2.28"        # we ship to RHEL 8 / Ubuntu 20.04
    GLIBCXX: "3.4.28"
  sycl:
    implementation: dpcpp
    backends: [level_zero, opencl]

Comparing wheels. When NEW is a .whl and this block declares no runtime_floors, the wheel's own claims become them: its platform tag's floor (GLIBC from a manylinux_X_Y tag, MUSLLINUX, MACOS_DEPLOYMENT_TARGET), the single architecture it names (WHEEL_ARCH), WHEEL_CONTEXT, and the numpy requirement from its METADATA (NUMPY_REQUIREMENT). Each library in the wheel is then checked against what the wheel promises: a binary needing a newer glibc than the tag allows is platform_baseline_floor_raised, and a NumPy C-API target above the declared numpy floor is numpy_metadata_understates_required_version (plus numpy_abi_major_incompatible when the target needs NumPy 2 and the declaration still admits 1.x). A declared runtime_floors wins whole; the tag does not fill its gaps. NUMPY_REQUIREMENT can also be declared by hand as a PEP 440 specifier set ("" for no floor).

See Environment & Toolchain Drift for the full worked example, including CI/GitHub Action usage.


bundle_variants:

Declares the build variants of one project — each a target triple, a compiler family, and a set of feature toggles — for abicheck project capture-variants, which captures every declared variant into one ProjectSnapshot package, one VariantRef per variant.

bundle_variants:
  linux-x86_64-gcc:              # variant name: [A-Za-z0-9._-], 1-100 chars
    target_triple: x86_64-linux-gnu   # required, non-empty string
    compiler_family: gcc              # required, non-empty string
    feature_toggles:                  # optional mapping
      simd: avx2                      #   value: string, boolean or integer
      threads: true                   #   (stored as "true"/"false"/"2")
    required: true                    # optional boolean, default true
  linux-aarch64-clang:
    target_triple: aarch64-linux-gnu
    compiler_family: clang
    required: false
Key Type Default Meaning
target_triple string — (required) the variant's target
compiler_family string — (required) the variant's compiler family
feature_toggles mapping of string → string/bool/int {} build toggles that define the variant; may not reuse target_triple/compiler_family as a key
required boolean true whether the capture must produce this variant

Validation is strict on every .abicheck.yml load, not only in capture-variants: an unknown key, a wrong type, a missing coordinate, an unsafe or case-colliding variant name is a usage error (exit 64) naming every finding at once.

What the block drives:

  • declared — each variant's VariantRef.declared map is {target_triple, compiler_family, **feature_toggles}, known from this block alone.
  • captured — filled from what the capture actually observed (the DWARF DW_AT_producer compiler family/version/producer string, target architecture, binary format, ELF machine/class, header-parse standard and frontend). The two maps are stored side by side and never merged: if the config says clang and the binary says GCC 13.2.0, the package records both.
  • required — a required variant with no --variant input, a missing path, nothing capturable, or a failed capture is an error before anything is written (no partial package). An optional variant in any of those states is skipped and reported; the package carries no VariantRef for it (never an empty placeholder).

Comparing two such packages (abicheck compare OLD NEW --variant old=ID) adds comparison_scope.variant_pairing to the JSON report: both packages' variants paired by id, a change to a variant's declared coordinates reported as a variant-boundary change (distinct from captured drift such as a compiler-version bump), and a variant present on only one side reported as unmatched — never as a removal — with its recorded required flag.


policy:

The documented project-config policy override mechanism. One key today: overrides:, a ChangeKind slug -> severity mapping (break/warn/risk/ignore) — the identical vocabulary and validation --policy <file>'s own overrides: block uses (an unknown slug or severity spelling is a hard load error, not a silently-skipped entry). Folds into the run's effective policy at the project_config precedence tier: an explicit --policy <file> that also states an override for the same kind always wins; a kind only the project config states is otherwise applied as-is. A project with no --policy <file> given at all still gets its policy.overrides applied.

policy:
  overrides:
    exported_not_public: ignore
    func_removed: warn

performance:

The memory/speed trade-off a run executes under. One key today:

Key Values Default
profile balanced, low-memory balanced
performance:
  profile: low-memory
  • balanced — fastest. Both sides of a comparison may be resolved concurrently in one process (the typed API does; the compare CLI resolves sides one after the other), and a directory/package comparison sizes its worker pool to the host.
  • low-memory — lowest peak memory. The two sides are resolved one at a time, each in its own short-lived process on Linux, so the memory a side's header parse used goes back to the system before the next side starts. A directory/package comparison handles its libraries one at a time. On macOS and Windows the sides run one at a time in-process. Measured on a oneDAL comparison: peak 1.34 → 0.89 GiB for about 17% more wall time on that small input (see Memory).

A profile changes how a run executes, never what it reports: every profile produces the same snapshots, findings, verdict and exit code.

Precedence: compare --performance-profile › .abicheck.yml's performance.profile › balanced. In the Python API, set CompareRequest.performance_profile; left unset, it follows the ambient profile, which is balanced unless the caller entered abicheck.model.performance.performance_profile_scope(...).

An unknown value is a load-time error naming the valid ones (exit 64 from the CLI). The block is where further memory/speed settings will go; each maps onto the profile rather than exposing a separate mechanism switch.


resource_limits:

The calibrated JSON decode resource limit, demoted off the CLI (Phase 7g, one-comparison-product.md §4.1/§3 #21) — no CLI spelling any more. One key today: max_bundle_facts_decode_nodes: (default unset, applying bundle_facts.DEFAULT_MAX_JSON_OBJECT_NODES, 1,000,000 — deliberately kept conservative rather than raised to cover a real large-blob need, since raising the default would widen every unconfigured/untrusted run's own decode-bomb ceiling) — the former compare --max-json-object-nodes: overrides the JSON container/scalar-token budget when decoding a stored BundleFacts document (OLD_INPUT, from a prior compare --bundle-facts-out). A real per-library facts blob for a large, template-heavy library (e.g. SYCL/DPC++, or a release sized like oneDAL's own ~20k-25k-function public header surface) can legitimately need well over the default to decode. Only an explicitly-supplied --config may raise this budget past the default — an auto-discovered .abicheck.yml (found by walking up from the current directory) can come from the same checkout supplying the content being decoded, so honoring a raise from it would let that checkout pair a raised budget with a compact decode-bomb payload. An auto-discovered config may still lower the budget below the default, since narrowing a ceiling is never a decode-bomb risk (Codex review, PR

1174, second round).

Deliberately node-based, not a memory size — see bundle_facts.DEFAULT_MAX_JSON_OBJECT_NODES's own docstring for the real calibration measurement and why a memory-labelled dial would understate the actual container-count defense this budget provides.

Through the composite GitHub Action, "an explicitly-supplied --config" means the build-config input: a config named there is the operator's own reviewed document and its raise is honored, while a config the Action discovered on its own is folded into a synthesized overlay that caps this one key back to the default (with a ::warning:: saying so). So a large-toolkit stored-facts comparison in CI needs build-config pointing at the .abicheck.yml carrying the raised value — not extra-args, and not a bare .abicheck.yml left for autodiscovery. The decode failure itself names this key and that requirement, so a run that hits the ceiling says how to fix it rather than reading as an unfixable refusal.


Exit-code scheme (no config key — fully automatic)

There is no exit_code_scheme: key (CLI cleanup phase two PR G2 removed it, along with the --exit-code-scheme CLI flag): the exit-code scheme is severity when a severity map is in effect (this file's severity: block, a --severity-preset, or a kind: gate pack's gate.severity.<category>), otherwise legacy. There is no way to force one scheme regardless of whether a severity setting is present.

See Exit codes.


version:

Top-level integer. Default 0 (unset). Declares the config schema version for forward compatibility.


risk_rules: and crosschecks:

Both are recognized top-level keys (so they do not trigger the unknown-key error), but they are handled outside the compare config merge:

  • risk_rules: — formerly a mapping of rule-name → { paths: [...], weight: <int> } path-glob risk profile, parsed by RiskRules.from_dict in buildsource/risk.py. Nothing loads it any more, and there is no replacement: scan --risk-rules, the one option that ever read a risk_rules: block, was retired ahead of the command itself, and buildsource/risk.py — including the risk scorer, not only the auto-depth-escalation half — was deleted outright along with the rest of scan in 0.6 (no alias, no deprecation window). There is now no risk score of any kind, reported or otherwise; compare's --depth is always an explicit pin. The key stays recognized (so an existing file does not trigger the unknown-key error), but is inert. See Evidence depth.
  • crosschecks: — reserved. It has never actually been read: cross-checks (buildsource/cross_source_checks.py) run unconditionally as part of every compare invocation, with no per-check tuning surface — the scan command's repeatable --crosscheck KEY=LEVEL flag that once tuned them was deleted along with scan itself.

targets:, bundles:, profiles:, and baseline:

Recognized top-level keys (so they do not trigger the unknown-key error), but — like risk_rules:/crosschecks: above — not parsed by BuildConfig itself. dump/compare never read this block; it exists solely for G30's GitHub Actions CI-integration primitives: abicheck project validate validates it, and abicheck project plan consumes it to generate run-plan.json. Parsed and validated by buildsource/project_targets.py; see the Project Targets Schema reference for the full field-by-field schema, the checks: list, and both abicheck project subcommands.


Some settings often discussed alongside the config live in separate YAML files, not in .abicheck.yml:

Concept File / flag Top-level schema Docs
Policy profile --policy <file> (PolicyFile.load, policy_file.py) — note --policy only takes the built-in names strict_abi/sdk_vendor/plugin_abi base_policy, overrides, reclassify, frozen_namespaces, evidence_policy Policies
Suppression rules --suppress <file> (suppression.py) Suppression rule entries (YAML or ABICC format) Suppressions

The evidence_policy block is part of the policy file, not .abicheck.yml.


Complete example

A .abicheck.yml using only verified keys:

# Config schema version (forward-compat marker)
version: 1

# Build-system hint + where the compile DB lands
build:
  system: cmake
  compile_db: build/compile_commands.json

# Public surface definition for source collection
sources:
  public_headers:
    - include/**
  exclude:
    - include/**/detail/**
  graph: summary

# Stable L2 header compile context
compile:
  frontend: castxml
  std: c++17
  include_dirs:
    - include
  defines:
    - MYLIB_STATIC=0
  nostdinc: false

# Separate-debug-file resolution (coarse --debug-info stays a CLI flag)
debug:
  format: auto
  dwarf_only: false
  debuginfod: false

# Severity policy consumed by `compare`
severity:
  preset: default
  abi_breaking: error
  potential_breaking: warning
  addition: info

# Public-surface scoping (false-positive control)
scope:
  public: true
  collapse_versioned_symbols: false
  show_redundant: false
  public_symbols:
    - mylib_foo
    - mylib_bar

# Suppression hygiene
suppression:
  strict: true
  require_justification: true

# Precise evidence method (optional; a concrete s0..s6, never `auto`)
source:
  method: s6