Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/source-control/fix-stale-summary-count-2af1363f.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Fixed

- The merge gate's security count reads each earlier review's `critical-findings:` line from the reviewed-head marker's edit history, so an IMPORTANT summary finding an earlier review reported no longer holds the pull request once a clean incremental review covers the head; a history that is missing, truncated, deleted, edited by anyone but the lane, or carries no single line still counts every finding.
Original file line number Diff line number Diff line change
Expand Up @@ -3205,16 +3205,19 @@ def human(actor: Any) -> bool:
_LANE_AUTHORS = review_evidence.LANE_AUTHORS
_EDITS_QUERY = (
"query($ids:[ID!]!){nodes(ids:$ids){... on IssueComment"
"{id lastEditedAt editor{__typename login}}}}"
"{id lastEditedAt editor{__typename login} userContentEdits(first:100)"
"{totalCount nodes{editedAt deletedAt editor{__typename login} diff}}}}}"
)


def _attach_edits(comments: Any, label: str) -> None:
"""Give each lane-author comment the `edit` record `bind()` reads
(babysit_review_evidence._edited_by_another): unedited when REST's
`updated_at` equals `created_at`, else GraphQL's `lastEditedAt` and
`editor`, read 100 node ids at a time. A comment whose record cannot be
read gets none, so `bind()` holds it unproven."""
`editor`, with its `userContentEdits` as `history`
(babysit_review_evidence._marker_revisions), read 100 node ids at a time.
A comment whose record cannot be read gets none, so `bind()` holds it
unproven."""
if not is_json_array(comments):
return
pending: dict[str, dict[str, Any]] = {}
Expand Down Expand Up @@ -3250,6 +3253,7 @@ def _attach_edits(comments: Any, label: str) -> None:
pending[node["id"]]["edit"] = {
"lastEditedAt": node["lastEditedAt"],
"editor": node.get("editor"),
"history": node.get("userContentEdits"),
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
names its head's tree, which finding closure keys on (`_open_findings`).
- `comments`: the pull request's issue comments (REST), each by
github-actions[bot] or the Claude App carrying an `edit` record: GraphQL's
`lastEditedAt` and `editor{__typename login}` for it (`_edited_by_another`).
`lastEditedAt` and `editor{__typename login}` for it (`_edited_by_another`),
and for an edited comment its `userContentEdits` as `history`
(`_marker_revisions`).
- `threads`: `babysit_gh.fetch_review_threads(..., include_resolved=True)`,
whose comments carry `lastEditedAt` and `editor`, or None when they could
not be read.
Expand Down Expand Up @@ -552,6 +554,7 @@ def _bind_lane(
chain,
head_file,
[(None, marker), *_summary_comments(lane, comments)],
_marker_revisions(lane, comments),
)
except Unbound as exc:
return _unbound(
Expand Down Expand Up @@ -620,6 +623,7 @@ def _carried_blocking(
chain,
head_file,
[(None, marker), *_summary_comments(SEVERITY_LANE, comments)],
_marker_revisions(SEVERITY_LANE, comments),
)
except Exception: # noqa: BLE001 - this head does not bind; try older
continue
Expand Down Expand Up @@ -1101,6 +1105,62 @@ def _summary_comments(
return found


def _marker_revisions(
lane: str, comments: Sequence[Mapping[str, Any]]
) -> list[tuple[datetime, str]]:
"""Every body the lane's reviewed-head marker comments have held, with
when it was written: the lane producer edits one marker in place per head
(ci-workflows publish.cjs `writeMarker`), so an earlier review's summary
survives only in the comment's edit history (GraphQL `userContentEdits`,
each node's `diff` the whole body after that edit). An unedited marker's
one body counts at its creation. A comment whose history is missing,
incomplete, has a deleted revision or one written by anyone but its
author yields nothing, so the summaries it held stay unread."""
found: list[tuple[datetime, str]] = []
for comment in comments:
user = comment.get("user")
body = str(comment.get("body") or "")
match = MARKER_RE.match(body.split("\n", 1)[0].rstrip("\r"))
if (
not isinstance(user, Mapping)
or user.get("login") != MARKER_AUTHOR
or not match
or match[1] != lane
):
continue
edit = comment.get("edit")
if not isinstance(edit, Mapping) or "lastEditedAt" not in edit:
continue
if edit["lastEditedAt"] is None:
created = _time(comment.get("created_at"))
if created is not None:
found.append((created, body))
continue
history = edit.get("history")
nodes = history.get("nodes") if isinstance(history, Mapping) else None
if not isinstance(nodes, list | tuple) or history.get("totalCount") != len(
nodes
):
continue
revisions = []
for node in nodes:
at = _time(node.get("editedAt")) if isinstance(node, Mapping) else None
if (
at is None
or node.get("deletedAt") is not None
or not isinstance(node.get("diff"), str)
or _edited_by_another(
user,
{"lastEditedAt": node["editedAt"], "editor": node.get("editor")},
)
):
break
revisions.append((at, node["diff"]))
else:
found.extend(revisions)
return found


def _require_marked_runs(
lane: str,
lane_runs: list[Mapping[str, Any]],
Expand Down Expand Up @@ -1129,6 +1189,7 @@ def _open_findings(
chain: Sequence[_Record],
head_file: Callable[[str, str], str | None] | None = None,
summaries: Sequence[tuple[datetime | None, str]] = (),
revisions: Sequence[tuple[datetime, str]] = (),
) -> tuple[int, bool, int]:
"""Summary findings of the review chain covering the pinned head
(`chain`, from `_resolve`), plus inline threads still open, plus inline
Expand Down Expand Up @@ -1171,12 +1232,16 @@ def _open_findings(
`reviewed`'s tree reported a finding, and none of them was a whole
review; and how many open findings are CRITICAL (`SEVERITY_RE`): open
threads whose comment is not marked IMPORTANT or SUGGESTION, every inline
finding whose thread is gone, and the summary findings, of which only the
covering review's own may count fewer, when no other review of its tree
reported a summary finding: its one `critical-findings:` line, read from
`summaries` (the reviewed-head marker, which carries the summary, and
each Claude App summary comment posted inside that review's job). An
earlier review's summary is not read, so all its findings count."""
finding whose thread is gone, and the summary findings. A chain tree's
summary findings count fewer only when one review of that tree alone
reported any: by its one `critical-findings:` line. For the covering
review that line is read from `summaries` (the reviewed-head marker,
which carries the summary, and each Claude App summary comment posted
inside that review's job). For an earlier link's tree it is read from the
marker bodies in `revisions` (`_marker_revisions`) that name the
reporting review's head and were written inside its job, and from the
summary comments posted inside it; with no such line, or more than one,
all its findings count."""
if threads is None:
raise Unbound("threads-unread")
# The covering review's job window, to place its summary comments.
Expand All @@ -1195,8 +1260,10 @@ def _open_findings(
# review of it, whole or incremental, reported.
head_max: dict[str, int] = {}
summary_max: dict[str, int] = {}
# Per tree, how many reviews of it reported a summary finding.
# Per tree, how many reviews of it reported a summary finding, and the
# (head sha, job started, job completed, summary findings) of the last.
summary_reviews: dict[str, int] = {}
reporter: dict[str, tuple[str, datetime | None, datetime | None, int]] = {}
# The trees any review of which was whole or reported a finding.
spent: set[str] = set()
for item in lane_runs:
Expand Down Expand Up @@ -1224,9 +1291,15 @@ def _open_findings(
summary_max.get(tree, 0),
rec.findings - rec.inline, # type: ignore[operator]
)
started = _time(job.get("started_at"))
if rec.findings > rec.inline: # type: ignore[operator]
summary_reviews[tree] = summary_reviews.get(tree, 0) + 1
started = _time(job.get("started_at"))
reporter[tree] = (
rec.sha,
started,
rec.completed,
rec.findings - rec.inline, # type: ignore[operator]
)
if rec == chain[0]:
span = (started, rec.completed)
windows.append(
Expand Down Expand Up @@ -1315,6 +1388,28 @@ def _open_findings(
and len(lines) == 1
):
summary = rest + min(int(lines[0]), own)
for tree in chain_trees[1:]:
if summary_reviews.get(tree, 0) != 1:
continue
sha, start, end, reported = reporter[tree]
if not (start and end):
continue
named = [
body
for at, body in revisions
if start <= at <= end
and (mark := MARKER_RE.match(body.split("\n", 1)[0].rstrip("\r")))
and mark[1] == lane
and mark[2] == sha
]
posted = [body for at, body in summaries if at and start <= at <= end]
lines = [
line
for body in named + posted
for line in SUMMARY_CRITICAL_RE.findall(body)
]
if len(lines) == 1:
summary -= reported - min(int(lines[0]), reported)
return (
count,
count > 0 and not unresolved and not head_spent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4462,13 +4462,15 @@ def test_unedited_comments_need_no_read_and_edited_ones_get_graphql(self) -> Non
edited = self.comment("github-actions[bot]", "2026-10-22T03:00:00Z", "IC_b")
other = self.comment("someone", "2026-10-22T03:00:00Z", "IC_c")
editor = {"__typename": "User", "login": "pusher"}
history = {"totalCount": 1, "nodes": [{"editedAt": "2026-10-22T03:00:00Z"}]}
reply = {
"data": {
"nodes": [
{
"id": "IC_b",
"lastEditedAt": "2026-10-22T03:00:00Z",
"editor": editor,
"userContentEdits": history,
}
]
}
Expand All @@ -4477,7 +4479,12 @@ def test_unedited_comments_need_no_read_and_edited_ones_get_graphql(self) -> Non
merge._attach_edits([same, edited, other], "#1")
self.assertEqual(same["edit"], {"lastEditedAt": None})
self.assertEqual(
edited["edit"], {"lastEditedAt": "2026-10-22T03:00:00Z", "editor": editor}
edited["edit"],
{
"lastEditedAt": "2026-10-22T03:00:00Z",
"editor": editor,
"history": history,
},
)
self.assertNotIn("edit", other)
args = read.call_args.args[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3077,6 +3077,128 @@ def test_a_thread_less_inline_finding_counts_as_critical(self) -> None:
]
self.assertEqual(result.blocking_findings, 1)

# #10817: a whole review at S0 reported one IMPORTANT summary finding with
# `critical-findings: 0`, then an incremental review at S found none. The
# lane edits one marker in place, so S0's summary lives in its history.
ACTIONS_BOT = {"__typename": "Bot", "login": "github-actions"}

def earlier_link(self) -> list[dict[str, Any]]:
early = attempt(
self.SEC, S0, 1, [record(self.SEC, S0, "findings=1 inline=0 scope=whole")]
)
late = attempt(
self.SEC,
S,
1,
[record(self.SEC, S, f"findings=0 inline=0 scope=incremental since={S0}")],
)
late["jobs"][0]["started_at"] = "2026-01-01T02:00:00Z"
late["jobs"][0]["completed_at"] = "2026-01-01T03:00:00Z"
return [
whole("claude-review", S),
run(self.SEC, S0, attempts=[early]),
run(self.SEC, S, attempts=[late], created="2026-01-01T02:00:00Z"),
]

def edited_marker(
self, earlier: str | None, history: Any = None
) -> list[dict[str, Any]]:
"""Both markers, the security one edited from S0's summary (`earlier`)
to S's clean one; `history` replaces the edit history whole."""
current = (
f"<!-- claude-lane-reviewed-head lane={self.SEC} sha={S} -->\n"
"reviewed: 0 finding(s).\n\ncritical-findings: 0"
)
old = (
f"<!-- claude-lane-reviewed-head lane={self.SEC} sha={S0} -->\n"
f"reviewed: 1 finding(s).\n\n{earlier}"
)
if history is None:
history = {
"totalCount": 2,
"nodes": [
{"editedAt": "2026-01-01T02:30:00Z", "deletedAt": None,
"editor": self.ACTIONS_BOT, "diff": current},
{"editedAt": "2026-01-01T00:30:00Z", "deletedAt": None,
"editor": self.ACTIONS_BOT, "diff": old},
],
} # fmt: skip
found = both_markers()
for item in found:
if f"lane={self.SEC}" in item["body"]:
item["body"] = current
item["edit"] = {
"lastEditedAt": "2026-01-01T02:30:00Z",
"editor": self.ACTIONS_BOT,
"history": history,
}
return found

def test_an_earlier_links_summary_blocks_only_when_critical_or_unreadable(
self,
) -> None:
for earlier, blocking in (
("critical-findings: 0", 0),
("critical-findings: 1", 1),
("critical-findings: 7", 1),
("no line", 1),
("critical-findings: 0\ncritical-findings: 0", 1),
):
with self.subTest(earlier=earlier):
comments = self.edited_marker(earlier)
result = self.bind(self.earlier_link(), comments=comments)[self.SEC]
self.assertTrue(result.bound)
self.assertEqual(result.open_findings, 1)
self.assertEqual(result.blocking_findings, blocking)

def test_an_unreadable_marker_history_counts_the_earlier_summary_in_full(
self,
) -> None:
good = self.edited_marker("critical-findings: 0")[1]["edit"]["history"]
nodes = good["nodes"]
for name, history in (
("absent", "missing"),
("truncated", {"totalCount": 3, "nodes": nodes}),
("deleted", {"totalCount": 2, "nodes": [nodes[0], {**nodes[1], "deletedAt": "2026-01-01T04:00:00Z"}]}),
("no diff", {"totalCount": 2, "nodes": [nodes[0], {**nodes[1], "diff": None}]}),
("another editor", {"totalCount": 2, "nodes": [nodes[0], {**nodes[1], "editor": MAINTAINER}]}),
("outside the job", {"totalCount": 2, "nodes": [nodes[0], {**nodes[1], "editedAt": "2026-01-01T01:30:00Z"}]}),
): # fmt: skip
with self.subTest(name=name):
comments = self.edited_marker("critical-findings: 0", history)
if name == "absent":
del comments[1]["edit"]["history"]
result = self.bind(self.earlier_link(), comments=comments)[self.SEC]
self.assertTrue(result.bound)
self.assertEqual(result.blocking_findings, 1)

def test_a_revision_for_another_head_does_not_read_for_the_earlier_link(
self,
) -> None:
# S's own revision falls in S0's window: it names S, not S0.
comments = self.edited_marker("no line")
history = comments[1]["edit"]["history"]
history["nodes"][0]["editedAt"] = "2026-01-01T00:40:00Z"
result = self.bind(self.earlier_link(), comments=comments)[self.SEC]
self.assertEqual(result.blocking_findings, 1)

def test_the_covering_reviews_line_still_reads_beside_the_history(self) -> None:
# The newest revision is the current marker body: it must not count
# as a second line for the covering review.
runs = [whole("claude-review", S), whole(self.SEC, S, n=2, k=0)]
comments = self.summary_markers("critical-findings: 0")
sec = comments[1]
sec["edit"] = {
"lastEditedAt": "2026-01-01T00:30:00Z",
"editor": self.ACTIONS_BOT,
"history": {"totalCount": 1, "nodes": [
{"editedAt": "2026-01-01T00:30:00Z", "deletedAt": None,
"editor": self.ACTIONS_BOT, "diff": sec["body"]},
]},
} # fmt: skip
result = self.bind(runs, comments=comments)[self.SEC]
self.assertEqual(result.blocking_findings, 0)

def test_claude_review_findings_all_block(self) -> None:
runs = [whole("claude-review", S, n=1, k=0), whole(self.SEC, S)]
result = self.bind(runs)["claude-review"]
Expand Down
Loading