A directory/package compare cannot render sarif/review; bundles: cannot state scope policy¶
Index: Known gaps
Status (2026-11-01): CLOSED. The format set closed on 2026-10-23: sarif and review render for a directory/package compare (abicheck/report/render_release_sarif.py; RELEASE_FORMATS in frontends/cli/commands/compare_routing.py). Part 2 (bundle-level scope policy) closed on 2026-11-01; see "How part 2 was closed" at the end. Archived in closed known gaps.
Two halves of one-comparison-product.md's bundle-parity item that this
pass did not close, recorded with what each actually needs. The rest of
that item did land (assurance.require_complete accepted, --write
repeatable, the machine document no longer implicitly truncated) — see the
changelog entry and tests/test_one_comparison_product_parity.py.
1. The format set. compare renders
json/markdown/sarif/html/junit/review/oneline; a directory or
package operand renders json/markdown/junit/oneline/html and rejects the
rest
(frontends/cli/commands/compare_routing.py's _RELEASE_FORMATS). The rejection is
loud rather than silent, and it is not arbitrary — the missing formats are
the ones whose renderers take a single DiffResult:
sarifis closed: the fan-out keeps each member'sDiffResultonly for the pair formats (junit,sarif;frontends/cli/release_summary._PAIR_FORMATS), and the SARIF log is one run per member, each run the single-pairto_sarifprojection, with every result stampedproperties.releaseMember. Each run keeps its own rule catalog, soruleIndexvalues stay valid.reviewis closed as a release-level digest: the verdict, exit code, per-member counts and each member's breaking findings, rendered from the release JSON document. The per-memberreview_digest_documentneeds a per-memberReportEnvelope(old and new snapshots) that the fan-out does not retain, so it is not reused per member. The aggregate shape question is answered by that digest, not by a per-library PR comment.onelinewas the fourth of that list and is closed: it never needed aDiffResultat all (it is a count summary, and the release summary already carries every count), soreport/release_oneline.pyfolds the per-library counts through the sameformat_stat_linea single-paircomparerenders and the release path accepts-o oneline=.../-o oneline=...like any other.htmlis closed the same way:report/render_release_html.pyrenders the release JSON document itself (headline, per-member table, comparison scope, release coherence findings and the recorded dependency graph), so it needs no memberDiffResulteither. It is a release page, not the single-pair HTML report; per-member finding detail stays in the JSON.
Library count no longer changes which formats a release can render:
sarif and review are available for one library and for N.
2. Scope policy at bundle level. ADR-065's scope policy is fully
expressible for a directory/package compare — --select,
--select-required and .abicheck.yml's scope.on_incomplete all apply to
a release operand, and --select-required naming an absent member really
does floor the exit code (verified live). What has no expression is the
project-config bundle surface: buildsource/project_targets.py's
BundleSpec accepts only targets: and checks:, so a bundles: entry
cannot state required members beyond its own membership list, nor its own
permitted-incompleteness policy, nor a selection narrower than the whole
bundle. A project that wants "these four of the six must be present, the
other two may be missing" has to say it per invocation rather than in the
config the Action reads. That is a BundleSpec schema addition plus a
check-target input, in the ADR-047 project-targets layer — not a
compare change, which is why it is listed separately from the format set
above.
How part 2 was closed (2026-11-01)¶
The maintainer chose option A: the scope policy lives on the bundles:
entry, not on checks: and not as a new check-target-only knob.
bundles.<id>.required— a non-empty, duplicate-free subset of the bundle'stargets:; omitted means every member is required.bundles.<id>.on_incomplete—failorwarn; omitted leaves the project'sscope.on_incompletein charge.failis the config spelling of the CLI'sblock(one mapping,model/release_selection.BUNDLE_ON_INCOMPLETE_TO_SCOPE).- Owner:
buildsource/project_bundles.py(BundleSpec). It was moved out ofproject_targets.py, a no-growth file, together withCheckSpecand the strict field readers (now the leafproject_check_spec.py), so the new owner imports nothing back.run_plan.py's gate-block parser moved torun_plan_gate.pyfor the same reason; both files' debt baselines went down. - Run plan: the bundle cell carries
bundle_required(the effective required members, so a consumer never has to re-derive the default) andbundle_on_incomplete, and the plan is stampedabicheck.run-plan/v4. - Action:
check-target'sbundle-required/bundle-on-incomplete. The one non-obvious step is the key space:--select-requiredmatches the canonical release key of a member's file (libfoo.soforlibfoo.so.1.2), never the project target id, so passing target ids through unchanged would have marked every present required member expected-not-produced.check-targetnames each member by its baseline binary (resolve-baseline'sbinary-paths, complete for every resolved bundle) and selects every member — required ones with--select-required, the rest with--select— because a member left out of a declared selection is out of scope and would not be compared at all. compare --set scope.on_incomplete=…is now applied (it was refused).check-project.ymlstages the members it finds for a bundle that declares a policy and leaves a missing one to the scope gate; without a policy every member must still be staged.
Not changed: a bundle baseline still needs every member
(baseline_set.resolve_bundle), so required: governs which candidate
members may be absent.