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
18 changes: 17 additions & 1 deletion scripts/epic_body_staleness.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,21 @@ def cited_issues(self) -> set[int]:

@dataclass(frozen=True)
class EpicStaleness:
"""Triage signals for one EPIC."""
"""Triage signals for one EPIC.

``citation_breadth`` is the largest number of examined EPICs that any single
``unrecorded_merged`` PR cites. A PR quoting many issue numbers at once (a
belt dump, a dispatch list) is attributed to every EPIC it names, so a
breadth well above 1 marks a citation-list rather than a delivery: such a
finding is a candidate for human review, never for a body rewrite.
"""

number: int
title: str
unrecorded_merged: tuple[int, ...]
stance_contradicted: bool
stance_pattern: str | None
citation_breadth: int = 0


def is_epic(row: dict) -> bool:
Expand Down Expand Up @@ -215,6 +223,11 @@ def analyze_epics(
for issue_number in pr.cited_issues() & cited_by_epic.keys():
cited_by_epic[issue_number].append(pr)

breadth: dict[int, int] = {}
for citing_epic in cited_by_epic.values():
for pr in citing_epic:
breadth[pr.number] = breadth.get(pr.number, 0) + 1

findings: list[EpicStaleness] = []
for epic in epics:
citing = cited_by_epic[epic.number]
Expand All @@ -236,6 +249,9 @@ def analyze_epics(
unrecorded_merged=unrecorded,
stance_contradicted=stance is not None,
stance_pattern=stance,
citation_breadth=max(
(breadth[number] for number in unrecorded), default=0
),
)
)

Expand Down
59 changes: 59 additions & 0 deletions scripts/tests/test_epic_body_staleness.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,65 @@ def test_up_to_date_epic_has_no_signal():
) == []


def test_citation_list_pr_reports_its_epic_breadth():
"""A PR quoting several EPICs is a citation list, not a delivery."""
epics = [
Epic(10355, "[EPIC] Fallacy detection", "## Objectif\nDetecter."),
Epic(7265, "[EPIC] Heritage", "## Objectif\nHeriter."),
Epic(11044, "[EPIC] Debt", "## Objectif\nPayer."),
]
shared = _pr(19147, "Belt dump: #10355 #7265 #11044", title="fix(picker)")

findings = analyze_epics(epics, [shared])

assert sorted(f.number for f in findings) == [7265, 10355, 11044]
assert all(f.citation_breadth == 3 for f in findings)


def test_single_epic_citation_keeps_breadth_one():
"""The ordinary 1:1 shape stays distinguishable from a citation list."""
epics = [
Epic(10355, "[EPIC] Fallacy detection", "## Objectif\nDetecter."),
Epic(7265, "[EPIC] Heritage", "## Objectif\nHeriter."),
]

findings = analyze_epics(epics, [_pr(19147, "See #10355")])

assert len(findings) == 1
assert findings[0].number == 10355
assert findings[0].citation_breadth == 1


def test_breadth_zero_when_the_citing_pr_is_already_recorded():
"""A stance-only finding carries no PR breadth at all."""
epic = Epic(
7,
"[EPIC] Veille",
"Pas pour maintenant.\n\n## Suivi\n- #80 — deja inscrit",
)

findings = analyze_epics([epic], [_pr(80, "See #7")])

assert len(findings) == 1
assert findings[0].unrecorded_merged == ()
assert findings[0].citation_breadth == 0


def test_payload_exposes_citation_breadth():
"""The additive field survives serialization for downstream triage."""
epics = [
Epic(10355, "[EPIC] Fallacy detection", "## Objectif\nDetecter."),
Epic(7265, "[EPIC] Heritage", "## Objectif\nHeriter."),
]

payload = build_payload(epics, [_pr(19147, "Belt dump: #10355 #7265")])

assert {f["number"]: f["citation_breadth"] for f in payload["findings"]} == {
10355: 2,
7265: 2,
}


def test_quoted_dormant_stance_is_not_live():
epic = Epic(
1210,
Expand Down
Loading