alissa-tools-github-revloop 0.31.13__tar.gz → 0.31.14__tar.gz

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.
Files changed (39) hide show
  1. {alissa_tools_github_revloop-0.31.13/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.14}/PKG-INFO +1 -1
  2. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/loop.py +5 -0
  3. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/loop_events.py +79 -0
  4. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/state.py +24 -5
  5. alissa_tools_github_revloop-0.31.14/src/main/alissa/tools/github/revloop/version +1 -0
  6. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
  7. alissa_tools_github_revloop-0.31.13/src/main/alissa/tools/github/revloop/version +0 -1
  8. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/LICENSE +0 -0
  9. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/MANIFEST.in +0 -0
  10. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/NOTICE +0 -0
  11. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/README.md +0 -0
  12. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/requirements.txt +0 -0
  13. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/setup.cfg +0 -0
  14. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/setup.py +0 -0
  15. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  16. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
  17. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
  18. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
  19. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/bows.py +0 -0
  20. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/config.py +0 -0
  21. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
  22. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  23. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/parking.py +0 -0
  24. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  25. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
  26. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  27. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/trust.py +0 -0
  28. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/version.py +0 -0
  29. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
  30. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
  31. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
  32. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
  33. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
  34. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
  35. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
  36. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
  37. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
  38. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
  39. {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.14}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.13
3
+ Version: 0.31.14
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -102,10 +102,12 @@ from .loop_events import (
102
102
  auth_rejected_prefix,
103
103
  build_emitter,
104
104
  credential_identity,
105
+ pr_closing_issue,
105
106
  recorded_round_verdicts,
106
107
  round_verdict_kind,
107
108
  round_verdict_prefix,
108
109
  ROUND_VERDICT_PREFIX,
110
+ task_number_of,
109
111
  )
110
112
  from .proc import CommandError
111
113
  from .state import State
@@ -6319,6 +6321,9 @@ class ReviewWatcher:
6319
6321
  head_sha=pr.head_sha,
6320
6322
  session=name,
6321
6323
  task_ref=task.ref if task else None,
6324
+ # What the PR serves (issue #165), for the round events.
6325
+ issue_number=pr_closing_issue(pr.body, pr.full_name),
6326
+ task_number=task_number_of(pr_origin_task_ref(pr.body)),
6322
6327
  )
6323
6328
  # Recorded only once the round is actually enqueued, and beside
6324
6329
  # the spawn row for the same reason: a notice the reviewer never
@@ -91,6 +91,17 @@ the envelope's `Contract: v<n>` and the review task's ref. Every
91
91
  the ledger does not know one, and is keyed on `(repo, pr, round, head)`, so a
92
92
  native post's `verdict_posts` row and its ping derive ONE event (the row's,
93
93
  with the ping folded in), never two.
94
+
95
+ Every ROUND event (`ROUND_EVENT_KINDS`) also says what the PR serves (issue
96
+ #165), so Studio can link a review to its task: `issueNumber` when the PR
97
+ body CLOSES one issue of the PR's own repo (`pr_closing_issue` -- a mention,
98
+ a cross-repo close or two different closes is no field, never a guess) and
99
+ `taskNumber` when it names its origin task (`Origin task: TASK-<n>`, read by
100
+ loop's `pr_origin_task_ref`). Both are read once, at spawn, onto the round's
101
+ `spawns` row; an event takes them from its own round's row, else from the
102
+ PR's newest spawn row (a pre-spawn `checks.held`, a `round.capped` no spawn
103
+ explains). Each field is omitted when unknown -- never null -- so a PR whose
104
+ body names neither emits the same payload as before, byte for byte.
94
105
  """
95
106
 
96
107
  from __future__ import annotations
@@ -148,6 +159,16 @@ ROUND_EVENT_KINDS = frozenset({
148
159
  "checks.held",
149
160
  })
150
161
 
162
+ # A GitHub closing claim (issue #165): one of the nine keywords, an optional
163
+ # colon, then `#<n>`, `<owner>/<repo>#<n>` or an issue URL. The repo parts
164
+ # are matched so a cross-repo close can be told apart -- and refused.
165
+ _CLOSING_REF_RE = re.compile(
166
+ r"(?<![\w-])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)(?:\s*:\s*|\s+)"
167
+ r"(?:https?://github\.com/(?P<url_repo>[\w.-]+/[\w.-]+)/issues/(?P<url_n>\d+)"
168
+ r"|(?:(?P<repo>[\w.-]+/[\w.-]+))?#(?P<n>\d+))(?![\w-])",
169
+ re.IGNORECASE,
170
+ )
171
+
151
172
  # The verdict-of-record row (issue #155): one per (PR, round),
152
173
  # `round-verdict:<round>@<head>:<verdict>:<readiness>:<class>:<contract>:<taskRef>`.
153
174
  # The round leads so "already recorded for this round" is one prefix read
@@ -340,6 +361,30 @@ def _verdict_data(
340
361
  }
341
362
 
342
363
 
364
+ def pr_closing_issue(body: str, repo: str) -> "int | None":
365
+ """The issue number a PR body CLOSES in the PR's own `repo`, or None.
366
+
367
+ Only a closing claim counts (`Closes #6`, `fixes: owner/repo#6`, a
368
+ closing keyword before the issue's URL); a bare mention (`see #6`) is not
369
+ one. A claim on another repo's issue is dropped, and two DIFFERENT
370
+ same-repo issues are ambiguous -- the event carries one `issueNumber`,
371
+ and picking between them would be a guess -- so they read as None too.
372
+ """
373
+ found: "set[int]" = set()
374
+ for match in _CLOSING_REF_RE.finditer(body or ""):
375
+ target = match.group("url_repo") or match.group("repo")
376
+ if target is not None and target.lower() != repo.lower():
377
+ continue
378
+ found.add(int(match.group("url_n") or match.group("n")))
379
+ return found.pop() if len(found) == 1 else None
380
+
381
+
382
+ def task_number_of(ref: "str | None") -> "int | None":
383
+ """`TASK-<n>` as the integer `taskNumber` the ingest takes, or None."""
384
+ match = re.fullmatch(r"TASK-(\d+)", ref or "", re.IGNORECASE)
385
+ return int(match.group(1)) if match else None
386
+
387
+
343
388
  def _ms(seconds: "int | float") -> int:
344
389
  """A ledger stamp (epoch seconds) as the API's epoch-ms `at`."""
345
390
  return int(seconds) * 1000
@@ -748,6 +793,39 @@ def _mark_plan_rounds(
748
793
  event["data"] = {**event.get("data", {}), "kind": ROUND_KIND_PLAN}
749
794
 
750
795
 
796
+ def _mark_links(
797
+ stamped: "list[tuple[int, dict]]", spawns: "list[dict]"
798
+ ) -> None:
799
+ """Stamp `issueNumber` / `taskNumber` on round events, in place (issue
800
+ #165). The event's own round's spawn row wins; an event whose round has
801
+ no spawn row (or no round at all) reads the PR's newest one. A field the
802
+ row does not know is left off, so a PR with neither keeps its payload."""
803
+ by_round: "dict[tuple[str, int, int], dict]" = {}
804
+ by_pr: "dict[tuple[str, int], dict]" = {}
805
+ # Oldest first, so the newest spawn of a round / PR is the one kept --
806
+ # what the body said LAST, even when that is nothing. `read_spawns` is
807
+ # newest first, so reversing it first keeps a same-second tie in order.
808
+ for spawn in sorted(reversed(spawns), key=lambda r: int(r["spawned_at"])):
809
+ repo, number = spawn["repo"], int(spawn["number"])
810
+ by_round[(repo, number, int(spawn["round"]))] = spawn
811
+ by_pr[(repo, number)] = spawn
812
+ for _, event in stamped:
813
+ if event["kind"] not in ROUND_EVENT_KINDS or "prNumber" not in event:
814
+ continue
815
+ repo, number = str(event.get("repo") or ""), int(event["prNumber"])
816
+ row: "dict | None" = None
817
+ if "round" in event:
818
+ row = by_round.get((repo, number, int(event["round"])))
819
+ if row is None:
820
+ row = by_pr.get((repo, number))
821
+ if row is None:
822
+ continue
823
+ if row.get("issue_number") is not None:
824
+ event["issueNumber"] = int(row["issue_number"])
825
+ if row.get("task_number") is not None:
826
+ event["taskNumber"] = int(row["task_number"])
827
+
828
+
751
829
  def derive_events(state: State, *, since: int = 0) -> "list[dict]":
752
830
  """Every loop event the ledger implies whose stamp is >= `since`,
753
831
  oldest first.
@@ -772,6 +850,7 @@ def derive_events(state: State, *, since: int = 0) -> "list[dict]":
772
850
  stamped += _grant_events(state.read_grants())
773
851
  stamped += _reap_events(state.read_reaps())
774
852
  _mark_plan_rounds(stamped, _plan_rounds(pings))
853
+ _mark_links(stamped, spawns)
775
854
 
776
855
  stamped = [(at, event) for at, event in stamped if at >= since]
777
856
  stamped.sort(key=lambda pair: pair[0])
@@ -64,7 +64,9 @@ CREATE TABLE IF NOT EXISTS spawns (
64
64
  head_sha TEXT NOT NULL,
65
65
  session TEXT NOT NULL PRIMARY KEY,
66
66
  task_ref TEXT,
67
- spawned_at INTEGER NOT NULL
67
+ spawned_at INTEGER NOT NULL,
68
+ issue_number INTEGER,
69
+ task_number INTEGER
68
70
  )"""
69
71
 
70
72
  SCHEMA = f"""
@@ -405,6 +407,14 @@ _ADDED_COLUMNS = {
405
407
  "verdicts": (
406
408
  ("readiness", "TEXT"),
407
409
  ),
410
+ # What the PR serves, read off its body at spawn (issue #165): the
411
+ # same-repo issue it closes and the origin task it names. NULL is "the
412
+ # body named none" -- and is the true value for every historical row,
413
+ # whose PR body was never read for it.
414
+ "spawns": (
415
+ ("issue_number", "INTEGER"),
416
+ ("task_number", "INTEGER"),
417
+ ),
408
418
  }
409
419
 
410
420
 
@@ -755,12 +765,20 @@ class State:
755
765
  head_sha: str,
756
766
  session: str,
757
767
  task_ref: str | None,
768
+ issue_number: int | None = None,
769
+ task_number: int | None = None,
758
770
  ) -> None:
771
+ """`issue_number` / `task_number` are what the PR serves (issue
772
+ #165): the same-repo issue its body closes and the origin task it
773
+ names, None when it names none. The loop-event emitter stamps them
774
+ on every round event of the PR."""
759
775
  self._db.execute(
760
776
  "INSERT OR REPLACE INTO spawns "
761
- "(repo, number, round, head_sha, session, task_ref, spawned_at) "
762
- "VALUES (?,?,?,?,?,?,?)",
763
- (repo, number, round_, head_sha, session, task_ref, int(time.time())),
777
+ "(repo, number, round, head_sha, session, task_ref, spawned_at, "
778
+ "issue_number, task_number) "
779
+ "VALUES (?,?,?,?,?,?,?,?,?)",
780
+ (repo, number, round_, head_sha, session, task_ref, int(time.time()),
781
+ issue_number, task_number),
764
782
  )
765
783
  self._db.commit()
766
784
 
@@ -1937,7 +1955,8 @@ class State:
1937
1955
  at any row age. An empty collection reads nothing.
1938
1956
  """
1939
1957
  sql = (
1940
- "SELECT repo, number, round, head_sha, session, task_ref, spawned_at "
1958
+ "SELECT repo, number, round, head_sha, session, task_ref, spawned_at, "
1959
+ "issue_number, task_number "
1941
1960
  "FROM spawns"
1942
1961
  )
1943
1962
  params: tuple = ()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.31.13
3
+ Version: 0.31.14
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera