Skip to content

Use-Case Impact

Preview: a compare-native report block, but no finding kind yet. abicheck compare --use-cases MANIFEST (below) folds a manifest into a real comparison and reports, per declared use case, which of that comparison's findings its own resolved entrypoints reach — emitted as the report's use_case_impact block (schema 2.39) and as a section of the text/markdown report. It genuinely answers "was this use case affected", but it is read-only: no Change field, no exit-code contribution, and still no USE_CASE_IMPACT_CONFIRMED finding kind (see "What this does not cover yet" below). abicheck project validate checks a manifest's structure on its own.

An optional impact-use-cases.yaml manifest lets you declare a project's own business/runtime use cases — "the training workflow", "the batch export job" — and which public entry points and tests exercise each one. abicheck promotes the manifest to graph facts and joins them onto the library's own unified impact-assessment graph, the same way --used-by promotes a real consumer binary's requirements.

This is G29 Phase 4 slice 2, amending ADR-057. abicheck.impact.use_cases parses the manifest, builds/joins the graph facts, and (explain_use_case_impact) answers which declared use case(s) reach a given changed symbol. Two CLI surfaces use it: abicheck project validate <manifest> checks the manifest's own structure, and abicheck compare --use-cases <manifest> OLD NEW resolves each use case's entrypoints against the comparison's own snapshots and reports which of its findings each use case reaches (impact/use_case_impact.py). Each finding in the JSON report also carries its own affected_use_cases list (report schema 5.10) — the same attribution read per finding. There is still no USE_CASE_IMPACT_CONFIRMED finding kind — see "What this does not cover yet" below.

Checking a manifest with the CLI

$ abicheck project validate impact-use-cases.yaml
use-case manifest validation: impact-use-cases.yaml
OK — 2 use case(s), structurally well-formed.

$ abicheck compare libtraining-1.0.abi.json libtraining-1.1.abi.json \
    --use-cases impact-use-cases.yaml
...
Use-case impact (impact-use-cases.yaml):
  training workflow: 1 change(s)
    - legacy_helper (func_removed)
    unresolved entrypoints: legacy_train_v1
  2 of 3 change(s) reached by no declared entrypoint (absence of proof, not
  proof of absence).

project validate checks only the manifest's own structure (a non-mapping entry, an unrecognized field, or a missing use_case name is a usage error, exit 64).

compare --use-cases <manifest> does the resolution and the attribution together, against the comparison it is already running. At least one side must carry a source graph — a dump --sources/--build-info snapshot, or any snapshot carrying the always-on header-only graph — and a pair with none on either side is a usage error rather than a silently missing section. Entrypoints are resolved by the same index resolve_use_case_entrypoints reports from, so the report can never disagree with what the comparison itself sees; an unresolved entrypoint is reported, never treated as a failure — the same "absence is not evidence of a wrong answer" discipline the rest of this page documents (see "Declared vs. observed use" below).

Each side is explained against its own graph and the two are unioned per symbol: a symbol added on the NEW side never existed in OLD's graph at all, so attributing only against OLD would read every addition as unattributed regardless of whether the NEW side's graph proves it reachable. A change whose symbol none of a use case's entrypoints can be shown to reach (directly, or transitively through a consumer-compiled inline/template body) is simply absent from that use case's list, and counted in unattributed_changes. None of this ever moves a verdict or an exit code: zero attributed changes just means none of the changes touch a declared use case's own surface, reported as such rather than silently omitted. Two structural limitations carry over from --used-by's own consumer-impact walk, since both reuse the identical graph primitives: only a function/variable-shaped change (one a SOURCE_DECL_MAPS_TO_SYMBOL edge backs) can ever be named — a type layout change has no such edge to resolve through — and only a consumer-compiled entrypoint (an inline/template body, not an ordinary out-of-line exported function's own internal calls) can walk transitively past its own declaration.

Why a separate manifest from usecase-registry.yaml

docs/contribute/usecase-registry.yaml tracks abicheck's own feature coverage — whether abicheck itself supports header-only analysis, for example. impact-use-cases.yaml tracks your project's business/runtime use cases — whether your training workflow calls train(). These are unrelated concepts that happen to share the English phrase "use case"; conflating them into one schema would make "abicheck supports X" and "our workflow uses Y" read as the same kind of fact. They are deliberately kept as two separate, unrelated files.

Manifest format

# impact-use-cases.yaml
- use_case: training-workflow
  entrypoints:
    - train
    - _ZN6detail4evalEv
  tests:
    - test_train_end_to_end

- use_case: batch-export
  entrypoints:
    - export_batch
  tests: []

Three fields per entry, deliberately minimal — matching exactly what the plan sketches, nothing more:

  • use_case (required, non-empty string) — the use case's name. Becomes the label of a use_case graph node.
  • entrypoints (optional list of strings) — public-entry symbol or declaration names/labels this use case exercises. Each name is matched against the library's own graph: an exported binary_symbol node, or a source_decl node whose own declared visibility is public. Deliberately restricted to these two kinds — a public type (record_type/enum_type/ typedef) is never a valid entrypoint target, since it has no outgoing call-graph edge for a consumer-impact walk to follow. A name can be spelled either as the graph's internal node id (binary_symbol://_ZN6detail4evalEv) or as the node's plain label (_ZN6detail4evalEv, train). An exact node id always resolves and always takes precedence over a label. A plain label resolves only when exactly one public node in the graph carries that label — a label two or more public nodes share (a common shape for overloaded C++ entries) is genuinely ambiguous and is treated the same as an unresolvable name (below), never guessed at.
  • tests (optional list of strings) — free-form test identifiers that cover this use case. Recorded as test_case nodes with no resolution step, since there is no graph node kind an external test identifier could fail to resolve against.

An entrypoint name the library graph cannot resolve is silently skipped — no node, no edge, no error, and no signal that the entrypoint is somehow wrong. This is the same "absence, never a wrong answer" discipline abicheck.impact.consumer_graph (ADR-057 slice 1) already follows for an unresolvable required symbol: a library graph that's incomplete (header-only, partial --sources coverage, or simply missing that one declaration) is a far more likely explanation than a genuinely broken manifest entry, and treating the two the same way would make an ordinary coverage gap look like a manifest bug.

The document's own shape is validated as a hard error, raising abicheck.errors.UseCaseManifestError rather than silently dropping or misreading the bad entry — silently accepting a malformed manifest could make a use case's declared coverage quietly disappear or misresolve from every future run with no indication why. Rejected:

  • invalid YAML syntax, or a mapping that repeats a key (e.g. two entrypoints: lines pasted into one entry — YAML's own default keeps only the last value) or uses an unhashable key (a YAML sequence used as a mapping key);
  • a top-level document that isn't a YAML list, or a list entry that isn't a mapping;
  • an entry with an unrecognized field — only use_case/entrypoints/ tests are accepted, so a misspelling (entrypoint instead of entrypoints) fails loudly instead of silently contributing nothing;
  • a missing or blank use_case name;
  • an entrypoints/tests value that isn't a YAML list of strings.

An empty document (no file content at all) is a valid, empty manifest — no use cases declared, not an error.

Entrypoint mapping and test association

from abicheck.impact.use_cases import (
    load_use_case_manifest,
    resolve_use_case_entrypoints,
)

definitions = load_use_case_manifest("impact-use-cases.yaml")
for resolution in resolve_use_case_entrypoints(definitions, library_graph):
    print(resolution.use_case, resolution.unresolved_entrypoints)

resolve_use_case_entrypoints resolves every entrypoints name against library_graph — the library's own L5 source graph or header-only graph (see Build Info & Sources for how that graph gets built in the first place) — by node id or by a label that names exactly one public entry, and reports which names resolved and which did not. tests are recorded as given: there is no graph node kind for an external test identifier to resolve against. The library graph is only read, never modified, so it stays safe to share with every other analysis of the same snapshot (internal-leak walks, the source-graph diff, a --used-by consumer join).

Declared vs. observed use — and what "no trace" does not mean

This slice only ever adds declared evidence: a human wrote impact-use-cases.yaml and asserted that a use case exercises certain entry points. There is no observed counterpart yet — no runtime trace confirms or contradicts a declaration. Two edge kinds are already reserved in the graph schema for that future work, TRACE_OBSERVED_ENTRY and TRACE_OBSERVED_EDGE, but nothing populates them today.

This distinction matters for how you read the graph: the absence of a use_case node naming some entry point is not evidence that no use case depends on it — it only means nobody has written a manifest entry for it yet (or the manifest doesn't exist at all). Likewise, once trace ingestion exists, the absence of an observed trace edge will not mean "this use case doesn't really use this entry" — a trace only ever positively confirms what it happened to observe during one run; it can never prove a codepath is unused. Runtime-trace ingestion is explicitly deferred (see ADR-057's "Deliberately not implemented this slice") precisely because getting this distinction wrong — reading a missing trace as "not used" — is a real correctness risk with no data yet to validate the right semantics against.

Full-library vs. consumer/use-case-scoped verdict semantics

Nothing in this slice changes any verdict, finding set, or exit code. The same rule that governs --used-by scoping applies here in advance of any consumer of this graph existing: a full-library compare verdict reflects every change to every declared symbol, regardless of whether any use case (declared or observed) reaches it. A use-case-scoped verdict — not implemented yet, tracked as G29 Phase 6's USE_CASE_IMPACT_CONFIRMED report-level overlay — would answer a narrower question: does this declared use case's own entry points and their call-graph closure reach the change at all. The two are not in tension and neither ever silently replaces the other, the same additive-evidence principle every other optional L3–L5 layer in abicheck already follows (see Build Info & Sources's "one rule that governs everything").

What this does not cover yet

  • Per-finding field is a report projection, not a Change field. compare --use-cases emits the attribution as one report-level use_case_impact block (schema 2.39), a text/markdown section, and — since schema 5.10 — an affected_use_cases list on each JSON changes entry, rendered as an "Affects use cases" note on Markdown change rows and in the review digest's review groups and impacted-symbol list. The per-finding list is derived from use_case_impact.by_use_case by joining on finding_id (UseCaseImpact.use_cases_by_finding), never by a second attribution, so the two are exact inverses. It is not stored on the Change object itself and not inside impact_assessment; SARIF, JUnit and the HTML changes table carry nothing yet. The use-case graph (and now this read-only explain step) exist as evidence a future Change-level field could be built from, the same position the consumer graph was in before ADR-057's D5/D8 wiring enriched CONSUMER_REQUIRED_SYMBOL_REMOVED and shared FUNC_REMOVED/SYMBOL_REMOVED findings with it.
  • No new ChangeKind or verdict. USE_CASE_IMPACT_CONFIRMED (G29 Phase 6) is a planned report-level overlay, not a raw break — matching how CONSUMER_IMPACT_PATH_CONFIRMED is scoped for the consumer side.
  • Runtime-trace ingestion. As above — TRACE_OBSERVED_ENTRY/ TRACE_OBSERVED_EDGE are reserved vocabulary with no producer.
  • Multiple use-case manifests, or a manifest embedded in .abicheck.yml. Only a single, standalone YAML file loaded explicitly via load_use_case_manifest is supported.