ADR-072: The PR Comment Is a Projection, Not a Second Report¶
Date: 2026-09-15
Status: Accepted — implemented. Design record for the reporting-fidelity
decisions behind abicheck/report/change_summary.py,
abicheck/report/evidence_summary.py, abicheck/pr_comment*.py,
abicheck/cli_pr_comment.py, action.yml and action/run.sh. Extends
ADR-061 (responsibility packages), ADR-067 D3 (the not_evaluated detector
state) and ADR-070 (the Action layer does not encode CLI semantics). It
changes no compatibility verdict, no gate, and no exit code.
Context¶
The sticky PR comment is the surface most maintainers actually read. It was
built as an independent adapter over the JSON report, and had drifted into a
second, lossier report: an audit of one production comparison found that
confidence, evidence_tier/evidence_tiers, coverage_warnings and
every per-finding one-sided value present in the JSON reached no comment at
any detail level, full included — while 151 focused comment tests passed,
because each asserted a rendering it already produced rather than a fact it
was required to preserve.
The same audit found the default pr-comment-on: changes produced no
comment at all for a run whose only outcome was a recorded analysis
limitation, and, in sticky mode, deleted the previous comment when it did.
Decisions¶
D1. Renderers project; they never recompute. A comment renderer reads
compatibility, policy decisions, confidence, gate contributions and exit
codes off the completed report. It does not re-derive them, does not scrape
another format, and does not re-run extraction or comparison
(tests/test_pr_comment_reporting.py::test_comment_generation_runs_no_
extraction_or_comparison states this as an executable contract).
D2. A shared presentation fact gets a report/-owned module. The
entity-by-operation rollup (report/change_summary.py) and the evidence
projection (report/evidence_summary.py) live in the report package, not in
the comment adapter, so a second renderer that wants either reads one answer
instead of growing its own. Both read canonical classifications
(ChangeKindMeta via report/change_operation.py); neither classifies by
kind-name prefix or suffix.
D3. Counting units are carried, not implied. ChangeSummary.unit states
that a row counts findings — not declarations, not unique symbols — and
ChangeSummary.exact states whether the list it was computed from was
complete. A caller holding a truncated list (a release report's capped
per-library sample) must pass exact=False with a reason, and the renderer
discloses it rather than presenting a floor as a total. Totals are computed
before grouping and before any display cap.
D4. Absence is never defaulted to reassurance. A report that states no
confidence yields no confidence line. This is the one failure a reporting
layer must not have: an invented "high" is indistinguishable from a real
one.
D5. Detector applicability is three states, not one. "Did not run"
(not_evaluated, ADR-067 D3), "not applicable to these artifacts"
(enabled: false — the PE/Mach-O/kABI/SYCL detectors on every ELF run), and
"ran with partial coverage" are reported distinctly, using the existing
status machinery rather than a new vocabulary. Missing PE metadata on an ELF
comparison is never presented as missing required coverage.
D6. Routine inapplicability is split from material limitation
structurally, never by parsing prose. A real ELF-vs-ELF comparison emits
eight coverage_warnings, all of them detector-disablement notes. They are
subtracted by reconstructing each note from the report's own detectors[]
ledger through confidence.detector_disablement_warning — the single
function that formats it — so producer and consumer share one format string
and no consumer reads the sentence to recover its meaning. A warning the
split cannot account for stays material: the failure direction is "shown
unnecessarily", never "silently dropped".
D7. pr-comment-on: changes posts for a material analysis limitation.
A clean run whose analysis was narrowed is a reportable outcome, and it is
the one a reviewer is least able to infer from silence. Routine detector
inapplicability (D6) is excluded, so this cannot post noise on every clean
run. This changes posting eligibility only; never remains
authoritative, and no verdict, gate or exit code moves. Documented in
docs/use/github-action.md § "What the default comments on".
D8. Shortening is section-aware. The per-section row budget is tightened
before the detail level is downgraded, so a large report keeps per-symbol
rows rather than collapsing to grouped ones. Every shortened body preserves
the headline and gate meaning, head/baseline identity, exact authoritative
counts, evidence and scope limitations, the disposition summary, exact
omitted-row counts, and working navigation. A hard truncation cuts on a line
boundary and closes every open <details>.
D9. A grouped row is a summary, never a dead end. Every aggregated family carries a complete member block (itself bounded by the same row budget, with an exact omitted count), and a family whose findings all concern one symbol is not aggregated at all — there is nothing to summarise, and the rollup dropped both findings' values.
D10. An artifact link is a promise. "View workflow run" and "Download
full report" are distinct footer links. The Action uploads nothing itself,
so it cannot derive the second; a caller passes its own upload step's
artifact-url output via pr-comment-report-artifact-url. A link is
therefore never rendered for an upload that failed.
D11. The compatibility percentage is not propagated. report_summary's
binary_compatibility_pct divides a finding count by an exported symbol
count. It is not added to the comment in any form, and specifically not as a
confidence score or as "N% of symbols are compatible". Its numerator,
denominator and limitations are documented on CompatibilityMetrics itself;
redesigning it belongs in that shared semantic owner, with consistent
cross-format behaviour, never as a comment-only formula. Recorded in
docs/contribute/known-gaps.md.
Consequences¶
The comment is now information-comparable to the HTML report for the facts a
reviewer acts on, at 2–3 KB for a typical comparison and ≤ 14 KB for a
thousand findings, rendering in well under 30 ms warm. What remains open is
recorded in known-gaps.md: a release/bundle report carries no
authoritative entity-by-operation counts, so its rollup is an explicitly
inexact floor until cli_compare_release.py emits one.