alissa-tools-github-revloop 0.12.0__tar.gz → 0.14.0__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 (31) hide show
  1. {alissa_tools_github_revloop-0.12.0/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.14.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/setup.py +1 -0
  3. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/loop.py +114 -3
  4. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/state.py +488 -0
  5. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/version +1 -0
  6. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/__init__.py +36 -0
  7. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/__main__.py +130 -0
  8. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/auth.py +200 -0
  9. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/page.py +588 -0
  10. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/server.py +355 -0
  11. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/sources.py +615 -0
  12. alissa_tools_github_revloop-0.14.0/src/main/alissa/tools/github/revloop/webui/sysinfo.py +174 -0
  13. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
  14. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +7 -0
  15. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +1 -0
  16. alissa_tools_github_revloop-0.12.0/src/main/alissa/tools/github/revloop/state.py +0 -218
  17. alissa_tools_github_revloop-0.12.0/src/main/alissa/tools/github/revloop/version +0 -1
  18. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/MANIFEST.in +0 -0
  19. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/README.md +0 -0
  20. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/requirements.txt +0 -0
  21. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/setup.cfg +0 -0
  22. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  23. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
  24. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
  25. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/config.py +0 -0
  26. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  27. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  28. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  29. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa/tools/github/revloop/version.py +0 -0
  30. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
  31. {alissa_tools_github_revloop-0.12.0 → alissa_tools_github_revloop-0.14.0}/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.12.0
3
+ Version: 0.14.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -50,6 +50,7 @@ setup(
50
50
  "console_scripts": [
51
51
  "alissa-revloop=alissa.tools.github.revloop.__main__:main",
52
52
  "alissa-pr-review=alissa.tools.github.revloop.prreview:main",
53
+ "alissa-revloop-ui=alissa.tools.github.revloop.webui.__main__:main",
53
54
  ]
54
55
  },
55
56
  install_requires=requirements,
@@ -14,6 +14,7 @@ import logging
14
14
  import re
15
15
  import secrets
16
16
  import time
17
+ from collections import Counter
17
18
  from dataclasses import dataclass
18
19
  from enum import Enum
19
20
  from pathlib import Path
@@ -185,6 +186,19 @@ class Decision:
185
186
  action: Action
186
187
  reason: str = ""
187
188
  round: int | None = None
189
+ # Descriptive metadata for the poll-snapshot exhaust (see
190
+ # ReviewWatcher._stage_record). Populated where a decision names a concrete
191
+ # reviewer -- the SPAWNED path and the liveness-deferral IN_FLIGHT paths --
192
+ # and left at its default elsewhere. Purely observational: nothing in the
193
+ # decision logic reads these, so they never change which branch is taken.
194
+ # `deferred` marks an IN_FLIGHT that is a liveness deferral (a live session
195
+ # holding the respawn back) rather than a freshly-enqueued round;
196
+ # `reenqueued` marks a SPAWNED that respawned a round whose prior session
197
+ # was presumed dead (the "stale-re-enqueued" summary bucket).
198
+ session: str | None = None
199
+ task_ref: str | None = None
200
+ deferred: bool = False
201
+ reenqueued: bool = False
188
202
 
189
203
 
190
204
  def session_name(pr: PullRequest, round_: int) -> str:
@@ -323,6 +337,8 @@ class ReviewWatcher:
323
337
  Action.IN_FLIGHT,
324
338
  f"round {round_} is stale but liveness is unprobeable — deferring",
325
339
  round_,
340
+ session=session,
341
+ deferred=True,
326
342
  )
327
343
 
328
344
  ses = live.get(session)
@@ -356,6 +372,8 @@ class ReviewWatcher:
356
372
  f"{session} is still {life} — not "
357
373
  f"respawning over a live reviewer",
358
374
  round_,
375
+ session=session,
376
+ deferred=True,
359
377
  )
360
378
 
361
379
  def _convergence_reason(
@@ -405,9 +423,14 @@ class ReviewWatcher:
405
423
 
406
424
  # -- reap sweep --------------------------------------------------------
407
425
 
408
- def sweep_sessions(self) -> None:
426
+ def sweep_sessions(self) -> int:
409
427
  """Kill the managed session of every finished round. Runs every poll.
410
428
 
429
+ Returns the number of sessions actually reaped this pass (0 in
430
+ `--dry-run`, where the sweep only logs) -- the poll-snapshot exhaust
431
+ records it, and it is the one count the snapshot cannot derive from
432
+ the per-PR Decision list.
433
+
411
434
  The predecessor of this sweep ran inside evaluate(), which is fed by
412
435
  the review-requested:@me search -- and submitting a review CLEARS the
413
436
  request, so a finished round's PR vanished from the search at exactly
@@ -435,7 +458,7 @@ class ReviewWatcher:
435
458
  sessions = self.alissa.list_review_sessions()
436
459
  except CommandError as exc:
437
460
  log.warning("reap sweep skipped: could not list sessions: %s", exc)
438
- return
461
+ return 0
439
462
 
440
463
  # Per-sweep memos. The PR fetch is keyed per distinct PR; the round
441
464
  # count additionally keys on the task ref, because two spawns of one
@@ -443,6 +466,7 @@ class ReviewWatcher:
443
466
  # task existed carries None). None = undecidable this pass.
444
467
  prs: dict[tuple[str, int], PullRequest | None] = {}
445
468
  completed_cache: dict[tuple[str, int, str | None], float | None] = {}
469
+ reaped = 0
446
470
 
447
471
  for ses in sessions:
448
472
  if not ses.is_idle:
@@ -486,10 +510,12 @@ class ReviewWatcher:
486
510
  # The live list is the authority; gating on the reaps table would
487
511
  # spare any session killed behind the ledger's back.
488
512
  self.state.record_reap(ses.name)
513
+ reaped += 1
489
514
  log.info(
490
515
  "reaped finished reviewer session %s (round %d done)",
491
516
  ses.name, row["round"],
492
517
  )
518
+ return reaped
493
519
 
494
520
  def _sweep_pr(self, repo_slug: str, number: int) -> PullRequest | None:
495
521
  """One PR fetch for the sweep; the caller memoizes per distinct PR.
@@ -596,6 +622,9 @@ class ReviewWatcher:
596
622
  Action.SPAWNED,
597
623
  f"session {name} → {task.ref if task else 'no task'}",
598
624
  round_,
625
+ session=name,
626
+ task_ref=task.ref if task else None,
627
+ reenqueued=reenqueued,
599
628
  )
600
629
 
601
630
  def _ensure_hub(self, pr: PullRequest) -> tuple[Path, str | None]:
@@ -778,7 +807,8 @@ class ReviewWatcher:
778
807
  # needs the slot a finished session is squatting on. Deliberately not
779
808
  # inside the per-request loop below — the sweep must reach sessions
780
809
  # whose PR no longer appears in the search at all.
781
- self.sweep_sessions()
810
+ started = time.monotonic()
811
+ reaped = self.sweep_sessions()
782
812
 
783
813
  requests = self.github.review_requests(self.config.repos)
784
814
  log.info("%d PR(s) with a review pending from %s", len(requests), self.github.login)
@@ -799,8 +829,89 @@ class ReviewWatcher:
799
829
  level = logging.INFO if decision.action != Action.SKIPPED else logging.DEBUG
800
830
  log.log(level, "%s → %s (%s)", slug, decision.action.value, decision.reason)
801
831
  results.append((slug, decision))
832
+
833
+ # Persist one poll_snapshots row per pass, built entirely from the
834
+ # Decision list already in hand plus the reap count -- no new GitHub
835
+ # calls. Written in dry-run too: a snapshot OBSERVES the pass, it is
836
+ # not a side effect the daemon takes, so a future console sees dry-run
837
+ # passes as well as live ones.
838
+ self._write_snapshot(
839
+ results, reaped, duration_ms=int((time.monotonic() - started) * 1000)
840
+ )
802
841
  return results
803
842
 
843
+ def _stage_record(self, slug: str, decision: Decision) -> dict:
844
+ """One per-item entry of a poll snapshot's compact JSON: the PR
845
+ reference (the slug and the number parsed from it), the current stage
846
+ (the decision's action, refined to name the stale-re-enqueue and
847
+ liveness-deferral buckets the bare action folds together), and the
848
+ round, session name, and origin task ref carried on the Decision.
849
+ `attempt` is carried as a fixed None for schema parity with the
850
+ devloop's per-item record -- the reviewloop is round-based and has no
851
+ attempt dimension -- so one console can read both loops' snapshots."""
852
+ _, _, tail = slug.partition("#")
853
+ stage = decision.action.value
854
+ if decision.reenqueued:
855
+ stage = "stale-re-enqueued"
856
+ elif decision.deferred:
857
+ stage = "deferred"
858
+ return {
859
+ "slug": slug,
860
+ "number": int(tail),
861
+ "round": decision.round,
862
+ "attempt": None,
863
+ "session": decision.session,
864
+ "stage": stage,
865
+ "reason": decision.reason,
866
+ "task_ref": decision.task_ref,
867
+ }
868
+
869
+ def _write_snapshot(
870
+ self,
871
+ results: list[tuple[str, Decision]],
872
+ reaped: int,
873
+ *,
874
+ duration_ms: int,
875
+ ) -> None:
876
+ """Persist one poll_snapshots row from the pass's Decision list and the
877
+ reaper count -- no new GitHub calls. The SPAWNED and IN_FLIGHT buckets
878
+ each split in two off observational Decision flags: a SPAWNED that
879
+ respawned a presumed-dead round is `stale_reenqueued`, and an
880
+ IN_FLIGHT that is a liveness deferral (not a freshly-enqueued round) is
881
+ `deferred`."""
882
+ stages = [self._stage_record(slug, d) for slug, d in results]
883
+ counts = Counter(d.action for _, d in results)
884
+ spawned = sum(
885
+ 1 for _, d in results
886
+ if d.action is Action.SPAWNED and not d.reenqueued
887
+ )
888
+ stale_reenqueued = sum(
889
+ 1 for _, d in results
890
+ if d.action is Action.SPAWNED and d.reenqueued
891
+ )
892
+ in_flight = sum(
893
+ 1 for _, d in results
894
+ if d.action is Action.IN_FLIGHT and not d.deferred
895
+ )
896
+ deferred = sum(
897
+ 1 for _, d in results
898
+ if d.action is Action.IN_FLIGHT and d.deferred
899
+ )
900
+ self.state.record_snapshot(
901
+ duration_ms=duration_ms,
902
+ candidates=len(results),
903
+ spawned=spawned,
904
+ stale_reenqueued=stale_reenqueued,
905
+ in_flight=in_flight,
906
+ deferred=deferred,
907
+ converged=counts[Action.CONVERGED],
908
+ capped=counts[Action.CAPPED],
909
+ escalated=counts[Action.ESCALATED],
910
+ skipped=counts[Action.SKIPPED],
911
+ reaped=reaped,
912
+ stages=stages,
913
+ )
914
+
804
915
  def run_forever(self) -> None:
805
916
  # preflight() is the caller's responsibility -- the CLI runs it once for
806
917
  # every mode, so calling it here too would double every check.