feat: require pagination proof for reconciliation inventory claims (Closes #308)
Add assess_reconciliation_inventory_proof to review_proofs: already-landed reconciliation reports may not claim a complete queue scan or that all already-landed PRs were found unless the report carries the pagination proof fields (Requested page size, Pages fetched, Returned PR count per page, Final page proof) and the per-page gitea_list_prs pagination metadata proves the final page was reached — inventory_complete, is_final_page with has_more=false, or a returned count below the requested page size. Declared page counts and per-page counts must match the evidence. Reports without a completeness claim pass without proof. Fails closed on missing evidence or malformed page metadata. Tests cover first page below limit, first page exactly at the page limit, multi-page traversal to the final page, missing pagination metadata, missing report fields, page-count and per-page-count mismatches, and the no-claim case. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -1052,6 +1052,113 @@ def _prs_from_list_response(list_prs_response: list | dict | None) -> list | Non
|
||||
return None
|
||||
|
||||
|
||||
# ── Reconciliation inventory pagination proof (Issue #308) ───────────────────
|
||||
#
|
||||
# Already-landed reconciliation may not claim a complete queue scan (or
|
||||
# that every already-landed PR was found) unless the report carries the
|
||||
# pagination proof fields and the per-page gitea_list_prs metadata
|
||||
# proves the final page was reached or the page limit was not hit.
|
||||
|
||||
_RECONCILE_COMPLETE_CLAIM_RE = re.compile(
|
||||
r"complete queue scan|all already-landed|inventory complete|"
|
||||
r"all open prs (?:were )?(?:inspected|scanned|listed)|found all",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
RECONCILIATION_INVENTORY_PROOF_FIELDS = (
|
||||
"requested page size",
|
||||
"pages fetched",
|
||||
"returned pr count per page",
|
||||
"final page proof",
|
||||
)
|
||||
|
||||
|
||||
def assess_reconciliation_inventory_proof(report_text, *,
|
||||
pagination_evidence=None):
|
||||
"""#308: reconciliation completeness claims need pagination proof.
|
||||
|
||||
*pagination_evidence* is the list of per-page ``pagination`` dicts
|
||||
from the session's ``gitea_list_prs`` calls, in fetch order. Reports
|
||||
that do not claim inventory completeness pass without proof; any
|
||||
completeness claim requires the report proof fields and evidence
|
||||
that the final page was reached (``inventory_complete``,
|
||||
``is_final_page`` with ``has_more=false``, or a returned count below
|
||||
the requested page size).
|
||||
|
||||
Returns {'complete', 'downgraded', 'reasons'}; fails closed.
|
||||
"""
|
||||
reasons = []
|
||||
if not _RECONCILE_COMPLETE_CLAIM_RE.search(report_text or ""):
|
||||
return {"complete": True, "downgraded": False, "reasons": []}
|
||||
|
||||
fields = _report_labeled_fields(report_text)
|
||||
for required in RECONCILIATION_INVENTORY_PROOF_FIELDS:
|
||||
if required not in fields:
|
||||
reasons.append(
|
||||
"reconciliation completeness claimed but report missing "
|
||||
f"pagination proof field '{required}'"
|
||||
)
|
||||
|
||||
if not pagination_evidence:
|
||||
reasons.append(
|
||||
"reconciliation completeness claimed without pagination "
|
||||
"evidence from gitea_list_prs"
|
||||
)
|
||||
return {"complete": False, "downgraded": True, "reasons": reasons}
|
||||
|
||||
evidence_valid = True
|
||||
for page in pagination_evidence:
|
||||
if not isinstance(page, dict) or not all(
|
||||
key in page for key in ("per_page", "returned_count")
|
||||
):
|
||||
reasons.append(
|
||||
"pagination evidence page missing 'per_page'/"
|
||||
"'returned_count' metadata"
|
||||
)
|
||||
evidence_valid = False
|
||||
|
||||
if evidence_valid:
|
||||
last = pagination_evidence[-1]
|
||||
final_proven = (
|
||||
last.get("inventory_complete") is True
|
||||
or (last.get("is_final_page") is True
|
||||
and last.get("has_more") is False)
|
||||
or last["returned_count"] < last["per_page"]
|
||||
)
|
||||
if not final_proven:
|
||||
reasons.append(
|
||||
"pagination evidence does not prove final page (need "
|
||||
"inventory_complete, is_final_page with has_more=false, "
|
||||
"or a returned count below the requested page size)"
|
||||
)
|
||||
|
||||
pages_fetched = fields.get("pages fetched")
|
||||
if pages_fetched:
|
||||
declared = re.findall(r"\d+", pages_fetched)
|
||||
if declared and int(declared[0]) != len(pagination_evidence):
|
||||
reasons.append(
|
||||
f"report 'Pages fetched' ({declared[0]}) does not match "
|
||||
f"pagination evidence ({len(pagination_evidence)} pages)"
|
||||
)
|
||||
|
||||
counts_field = fields.get("returned pr count per page")
|
||||
if counts_field:
|
||||
declared_counts = [int(n) for n in re.findall(r"\d+", counts_field)]
|
||||
actual_counts = [p["returned_count"] for p in pagination_evidence]
|
||||
if declared_counts != actual_counts:
|
||||
reasons.append(
|
||||
"report 'Returned PR count per page' "
|
||||
f"({declared_counts}) does not match pagination "
|
||||
f"evidence {actual_counts}"
|
||||
)
|
||||
|
||||
return {
|
||||
"complete": not reasons,
|
||||
"downgraded": bool(reasons),
|
||||
"reasons": reasons,
|
||||
}
|
||||
|
||||
|
||||
# ── Linked-issue consistency (Issue #314) ────────────────────────────────────
|
||||
#
|
||||
# Stale linked-issue text from a previous PR must not leak into a final
|
||||
|
||||
Reference in New Issue
Block a user