alissa-tools-github-devloop 0.8.30__py3-none-any.whl → 0.8.32__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,13 @@ FIRST_PARENT_MAX_PAGES = 3
53
53
  SUBMITTED_STATES = {"APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED"}
54
54
 
55
55
 
56
+ # The states `GitHub.linked_pull_requests` reports a cross-referenced pull request in
57
+ # (issue #144): still open, merged, or closed without a merge.
58
+ PR_OPEN = "open"
59
+ PR_MERGED = "merged"
60
+ PR_CLOSED = "closed"
61
+
62
+
56
63
  def parse_github_timestamp(ts: "str | None") -> "int | None":
57
64
  """A GitHub API timestamp (`2026-07-23T02:13:56Z`) as unix seconds, or
58
65
  None when absent/unparseable. GitHub emits UTC `Z` timestamps
@@ -1325,8 +1332,45 @@ class GitHub:
1325
1332
  is invisible either way, so `exhaustive` means complete UP TO the
1326
1333
  documented ceiling, not unbounded.
1327
1334
  """
1335
+ return [
1336
+ url
1337
+ for url, state in self.linked_pull_requests(
1338
+ owner, repo, number, exhaustive=exhaustive
1339
+ )
1340
+ if state == PR_OPEN
1341
+ ]
1342
+
1343
+ def linked_pull_requests(
1344
+ self,
1345
+ owner: str,
1346
+ repo: str,
1347
+ number: int,
1348
+ *,
1349
+ exhaustive: bool = False,
1350
+ ) -> "list[tuple[str, str]]":
1351
+ """Every same-repo pull request the issue's timeline cross-references,
1352
+ in ANY state, as `(url, state)` pairs -- `state` is `PR_OPEN`,
1353
+ `PR_MERGED` or `PR_CLOSED` (closed without a merge). The one walk
1354
+ behind `linked_open_prs` (which filters it to the open ones) and the
1355
+ issue edge's respawn rule (issue #144, design studio.alissa.app
1356
+ docs/design/loop-operator-surface.md §8.4): a DEAD session's lane
1357
+ with no open PR is only an implement-phase death when no MERGED or
1358
+ closed PR claims the issue either -- 2026-09-28, an operator's hand
1359
+ merge closed issue #1620 two seconds before the daemon read "no PR
1360
+ opened" off the open-only probe and spawned a duplicate lane.
1361
+
1362
+ Merged is read off the cross-reference's own payload
1363
+ (`source.issue.pull_request.merged_at`), so the walk costs nothing
1364
+ more than it did; the caller still fetches a candidate before acting
1365
+ on it, because a cross-reference proves only a MENTION (provenance is
1366
+ the fetched body's closing keyword and author). The walk, its bounds,
1367
+ its same-repo filter and the `exhaustive` switch are exactly
1368
+ `linked_open_prs`'s: the default stops at the first page that yields
1369
+ an OPEN PR (closed ones never stop it), so a lane with no open PR
1370
+ walks the whole timeline -- up to TIMELINE_MAX_PAGES -- and sees
1371
+ every closed one."""
1328
1372
  same_repo_pull = f"https://github.com/{owner}/{repo}/pull/"
1329
- urls: list[str] = []
1373
+ found: "dict[str, str]" = {}
1330
1374
  for page in range(1, TIMELINE_MAX_PAGES + 1):
1331
1375
  events = (
1332
1376
  self._api(
@@ -1352,15 +1396,25 @@ class GitHub:
1352
1396
  src = source.get("issue") if isinstance(source, dict) else None
1353
1397
  if not isinstance(src, dict):
1354
1398
  continue
1355
- if not src.get("pull_request"):
1399
+ pull = src.get("pull_request")
1400
+ if not pull:
1356
1401
  continue # the cross-reference came from a plain issue
1357
- if src.get("state") != "open":
1358
- continue
1359
1402
  url = src.get("html_url") or ""
1360
1403
  if not url.startswith(same_repo_pull):
1361
1404
  continue # a mention from another repo says nothing here
1362
- urls.append(url)
1363
- if urls and not exhaustive:
1405
+ if src.get("state") == "open":
1406
+ state = PR_OPEN
1407
+ elif isinstance(pull, dict) and pull.get("merged_at"):
1408
+ state = PR_MERGED
1409
+ elif src.get("state") == "closed":
1410
+ state = PR_CLOSED
1411
+ else:
1412
+ continue # no readable state: never guess
1413
+ # One PR can cross-reference the issue more than once (body
1414
+ # edit, comment mention); report it once, in its first
1415
+ # position -- the state is the PR's, not the event's.
1416
+ found.setdefault(url, state)
1417
+ if not exhaustive and PR_OPEN in found.values():
1364
1418
  # The DEFAULT caller's question is boolean -- is ANY open
1365
1419
  # same-repo PR linked? -- so once a page yields one, later
1366
1420
  # pages can only add more URLs to a message, never change the
@@ -1370,9 +1424,7 @@ class GitHub:
1370
1424
  break
1371
1425
  if len(events) < TIMELINE_PAGE_SIZE:
1372
1426
  break # short page: the timeline is exhausted
1373
- # One PR can cross-reference the issue more than once (body edit,
1374
- # comment mention); report it once.
1375
- return list(dict.fromkeys(urls))
1427
+ return list(found.items())
1376
1428
 
1377
1429
  def assign_self(self, owner: str, repo: str, number: int) -> None:
1378
1430
  """Assign the token identity -- the server-side in-flight marker.