alissa-tools-github-revloop 0.16.11__tar.gz → 0.16.12__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.
- {alissa_tools_github_revloop-0.16.11/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.16.12}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/alissa.py +70 -16
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/loop.py +113 -15
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/state.py +10 -2
- alissa_tools_github_revloop-0.16.12/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.16.11/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/README.md +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/setup.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import logging
|
|
7
7
|
import re
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
from .proc import CommandError, run, run_json
|
|
@@ -190,17 +191,25 @@ def _task_from_row(row: object) -> "Task | None":
|
|
|
190
191
|
|
|
191
192
|
@dataclass(frozen=True)
|
|
192
193
|
class TaskDetail:
|
|
193
|
-
"""One task as `alissa task get` sees it: the task itself, plus
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
path needs
|
|
198
|
-
has it recorded?
|
|
199
|
-
|
|
194
|
+
"""One task as `alissa task get` sees it: the task itself, plus everything
|
|
195
|
+
the decide path reads off its CR6 verdict evidence.
|
|
196
|
+
|
|
197
|
+
All three travel together because they come out of ONE payload. The decide
|
|
198
|
+
path needs every one of them (is this still the PR's open review task? how
|
|
199
|
+
many rounds has it recorded? what did the newest round decide?), and
|
|
200
|
+
`_count_verdicts` and `_newest_verdict` are deliberately written as mirrors
|
|
201
|
+
over the same evidence array -- so reading them apart would mean fetching
|
|
202
|
+
and re-parsing the same task two and three times per PR per poll, which is
|
|
203
|
+
the exact cost this whole path exists to stop paying.
|
|
204
|
+
|
|
205
|
+
`verdict` is None when no envelope on the task parses -- the normal round-1
|
|
206
|
+
case, indistinguishable here from "no verdict of record yet", which is what
|
|
207
|
+
the caller wants it to mean anyway.
|
|
200
208
|
"""
|
|
201
209
|
|
|
202
210
|
task: Task
|
|
203
211
|
verdicts: int
|
|
212
|
+
verdict: "str | None"
|
|
204
213
|
|
|
205
214
|
|
|
206
215
|
class Alissa:
|
|
@@ -251,7 +260,11 @@ class Alissa:
|
|
|
251
260
|
task = _task_from_row(data)
|
|
252
261
|
if task is None:
|
|
253
262
|
return None
|
|
254
|
-
return TaskDetail(
|
|
263
|
+
return TaskDetail(
|
|
264
|
+
task=task,
|
|
265
|
+
verdicts=self._count_verdicts(data),
|
|
266
|
+
verdict=self._newest_verdict(data),
|
|
267
|
+
)
|
|
255
268
|
except Exception: # pragma: no cover - defence in depth
|
|
256
269
|
log.exception("could not parse task payload for %s", ref)
|
|
257
270
|
return None
|
|
@@ -316,6 +329,44 @@ class Alissa:
|
|
|
316
329
|
log.exception("could not parse verdict evidence for %s", task_ref)
|
|
317
330
|
return None
|
|
318
331
|
|
|
332
|
+
@staticmethod
|
|
333
|
+
def _created_key(value: object) -> tuple[int, float]:
|
|
334
|
+
"""One evidence item's `createdAt`, as a sortable stamp.
|
|
335
|
+
|
|
336
|
+
`alissa task get --json` dates evidence with epoch MILLISECONDS as an
|
|
337
|
+
int; the API's other surfaces (and hand-written fixtures) use an ISO-8601
|
|
338
|
+
string. Both are normalised here to one float, because the previous key
|
|
339
|
+
-- `created if isinstance(created, str) else ""` -- collapsed every real
|
|
340
|
+
item to the empty string, leaving `max` to keep the FIRST element of an
|
|
341
|
+
all-equal set. Evidence comes back oldest-first, so on live data the
|
|
342
|
+
OLDEST verdict won: a PR whose round 1 was request_changes and round 2
|
|
343
|
+
approve never converged through the envelope branch (TASK-194837655).
|
|
344
|
+
|
|
345
|
+
Normalising rather than widening the isinstance is deliberate: a task
|
|
346
|
+
carrying both shapes would produce `(str, ...)` and `(int, ...)` keys
|
|
347
|
+
that raise TypeError the moment sorting compared them.
|
|
348
|
+
|
|
349
|
+
Returns `(has_stamp, seconds)`. An absent or unparseable stamp is
|
|
350
|
+
`(0, 0.0)` and sorts FIRST, preserving the old rule that a dated
|
|
351
|
+
envelope always beats one that lost its timestamp.
|
|
352
|
+
"""
|
|
353
|
+
if isinstance(value, bool): # bool is an int; never a timestamp
|
|
354
|
+
return (0, 0.0)
|
|
355
|
+
if isinstance(value, (int, float)):
|
|
356
|
+
seconds = float(value)
|
|
357
|
+
# Milliseconds, by magnitude: 1e11 seconds is the year 5138, while
|
|
358
|
+
# 1e11 milliseconds is 1973 -- so anything above it is ms, and a
|
|
359
|
+
# task holding both units still orders correctly.
|
|
360
|
+
if abs(seconds) > 1e11:
|
|
361
|
+
seconds /= 1000.0
|
|
362
|
+
return (1, seconds)
|
|
363
|
+
if isinstance(value, str) and value:
|
|
364
|
+
try:
|
|
365
|
+
return (1, datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp())
|
|
366
|
+
except ValueError:
|
|
367
|
+
return (0, 0.0)
|
|
368
|
+
return (0, 0.0)
|
|
369
|
+
|
|
319
370
|
@staticmethod
|
|
320
371
|
def _newest_verdict(payload: object) -> str | None:
|
|
321
372
|
"""Pick the newest parseable verdict out of a task's evidence array.
|
|
@@ -329,8 +380,8 @@ class Alissa:
|
|
|
329
380
|
if not isinstance(evidence, list):
|
|
330
381
|
return None
|
|
331
382
|
|
|
332
|
-
found: list[tuple[
|
|
333
|
-
for item in evidence:
|
|
383
|
+
found: list[tuple[tuple[int, float], int, str]] = []
|
|
384
|
+
for index, item in enumerate(evidence):
|
|
334
385
|
if not isinstance(item, dict):
|
|
335
386
|
continue
|
|
336
387
|
title = item.get("title")
|
|
@@ -340,18 +391,21 @@ class Alissa:
|
|
|
340
391
|
continue
|
|
341
392
|
match = _VERDICT_RE.search(blob)
|
|
342
393
|
if match:
|
|
343
|
-
created = item.get("createdAt")
|
|
344
394
|
found.append(
|
|
345
|
-
(
|
|
395
|
+
(Alissa._created_key(item.get("createdAt")),
|
|
396
|
+
index,
|
|
397
|
+
match.group(1).lower())
|
|
346
398
|
)
|
|
347
399
|
break
|
|
348
400
|
|
|
349
401
|
if not found:
|
|
350
402
|
return None
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
|
|
403
|
+
# Newest stamp wins; undated evidence sorts first, so a dated envelope
|
|
404
|
+
# always beats one that lost its timestamp (see _created_key). The
|
|
405
|
+
# append INDEX breaks ties: evidence comes back oldest-first, so two
|
|
406
|
+
# envelopes sharing a stamp resolve to the later-recorded one rather
|
|
407
|
+
# than to whichever `max` happened to reach first.
|
|
408
|
+
return max(found, key=lambda item: (item[0], item[1]))[2]
|
|
355
409
|
|
|
356
410
|
def count_verdicts(self, task_ref: str) -> int:
|
|
357
411
|
"""How many CR6 verdict envelopes are on the review task.
|
|
@@ -27,6 +27,7 @@ from .alissa import (
|
|
|
27
27
|
ManagedSession,
|
|
28
28
|
SessionRef,
|
|
29
29
|
Task,
|
|
30
|
+
TaskDetail,
|
|
30
31
|
is_review_task_for,
|
|
31
32
|
session_repo_slug,
|
|
32
33
|
)
|
|
@@ -872,6 +873,56 @@ class Decision:
|
|
|
872
873
|
reenqueued: bool = False
|
|
873
874
|
|
|
874
875
|
|
|
876
|
+
@dataclass(frozen=True)
|
|
877
|
+
class ResolvedTask:
|
|
878
|
+
"""One PR's review task as the decide path resolved it THIS pass.
|
|
879
|
+
|
|
880
|
+
Carries what the resolution happened to learn, and -- the part that matters
|
|
881
|
+
-- whether it learned it. The two resolution paths read different things:
|
|
882
|
+
|
|
883
|
+
* the CACHED path fetches the task by ref to check the mapping still holds,
|
|
884
|
+
so the whole evidence array comes with it and both the round count and the
|
|
885
|
+
newest verdict are free;
|
|
886
|
+
* the SEARCH path matches TITLES out of the task corpus, so it reads the
|
|
887
|
+
matched task afterwards to learn both -- and only when THAT read fails
|
|
888
|
+
does it fall back to a bare `count_verdicts` and know no verdict.
|
|
889
|
+
|
|
890
|
+
Hence `verdict_read`, rather than letting `verdict=None` stand for both "no
|
|
891
|
+
envelope parses" and "nobody looked". Convergence treats a None verdict as
|
|
892
|
+
"not approved"; conflating the two would silently refuse to close a round
|
|
893
|
+
that had in fact approved, every time the mapping was resolved by search.
|
|
894
|
+
"""
|
|
895
|
+
|
|
896
|
+
task: "Task | None"
|
|
897
|
+
verdicts: int = 0
|
|
898
|
+
verdict: "str | None" = None
|
|
899
|
+
verdict_read: bool = False
|
|
900
|
+
|
|
901
|
+
@classmethod
|
|
902
|
+
def from_detail(cls, detail: TaskDetail) -> "ResolvedTask":
|
|
903
|
+
"""One `alissa task get` payload answered all three questions.
|
|
904
|
+
|
|
905
|
+
The `verdict_read=True` invariant lives here rather than at the call
|
|
906
|
+
sites: it is a property of having read a task detail, and every path
|
|
907
|
+
that reads one gets it the same way.
|
|
908
|
+
"""
|
|
909
|
+
return cls(
|
|
910
|
+
task=detail.task,
|
|
911
|
+
verdicts=detail.verdicts,
|
|
912
|
+
verdict=detail.verdict,
|
|
913
|
+
verdict_read=True,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
def newest_verdict(self, alissa: Alissa) -> "str | None":
|
|
917
|
+
"""The newest CR6 verdict envelope, fetching it only if this resolution
|
|
918
|
+
did not already have it in hand."""
|
|
919
|
+
if self.task is None:
|
|
920
|
+
return None
|
|
921
|
+
if self.verdict_read:
|
|
922
|
+
return self.verdict
|
|
923
|
+
return alissa.latest_verdict(self.task.ref)
|
|
924
|
+
|
|
925
|
+
|
|
875
926
|
@dataclass(frozen=True)
|
|
876
927
|
class ChecksGate:
|
|
877
928
|
"""What the head's CI rollup does to a round's APPROVE verdict.
|
|
@@ -994,8 +1045,8 @@ class ReviewWatcher:
|
|
|
994
1045
|
self._pass_tasks = self.alissa.list_tasks()
|
|
995
1046
|
return self._pass_tasks
|
|
996
1047
|
|
|
997
|
-
def _review_task(self, pr: PullRequest) ->
|
|
998
|
-
"""This PR's open CR2 review task and its
|
|
1048
|
+
def _review_task(self, pr: PullRequest) -> "ResolvedTask":
|
|
1049
|
+
"""This PR's open CR2 review task and what its evidence says, cheaply.
|
|
999
1050
|
|
|
1000
1051
|
The decide path needs the review task on every pass of every open
|
|
1001
1052
|
round, and the only way to FIND one is to search the actor's whole
|
|
@@ -1019,13 +1070,31 @@ class ReviewWatcher:
|
|
|
1019
1070
|
Fail-open is the whole contract: every degradation here lands on "do
|
|
1020
1071
|
what the daemon did before the cache existed", and none of them can
|
|
1021
1072
|
answer "no review task" unless a successful search actually said so.
|
|
1073
|
+
|
|
1074
|
+
TWO consequences of resolving from cache, both accepted rather than
|
|
1075
|
+
overlooked (PR #68 round 1):
|
|
1076
|
+
|
|
1077
|
+
* A cached hit does NOT re-check CR2 uniqueness. The duplicate-task
|
|
1078
|
+
alarm lives in `find_review_task`, which only runs on a miss, so a
|
|
1079
|
+
second open review task created for this PR AFTER the mapping was
|
|
1080
|
+
cached goes unreported and the daemon keeps counting envelopes on the
|
|
1081
|
+
one it pinned. Detecting it needs the corpus, and not fetching the
|
|
1082
|
+
corpus is the point -- relocating the check is TASK-317167904.
|
|
1083
|
+
* VALIDATING the review task drops the mapping, because
|
|
1084
|
+
`is_review_task_for` requires an open task and the search cannot find
|
|
1085
|
+
a terminal one either, so `evaluate` falls back to the GitHub review
|
|
1086
|
+
count -- the fallback `_completed_rounds` deliberately avoids. That is
|
|
1087
|
+
pre-existing and unchanged, but this path now HAS a durable ref that
|
|
1088
|
+
could survive validation; it is not used because a terminal task can
|
|
1089
|
+
never become open again, so a retained ref would have no disproof and
|
|
1090
|
+
its row would be immortal. Settling that is TASK-1897198077.
|
|
1022
1091
|
"""
|
|
1023
1092
|
cached = self.state.review_task(pr.full_name, pr.number)
|
|
1024
1093
|
if cached is not None:
|
|
1025
1094
|
detail = self.alissa.get_task(cached)
|
|
1026
1095
|
if detail is not None:
|
|
1027
1096
|
if is_review_task_for(pr.owner, pr.repo, pr.number, detail.task):
|
|
1028
|
-
return
|
|
1097
|
+
return ResolvedTask.from_detail(detail)
|
|
1029
1098
|
log.info(
|
|
1030
1099
|
"%s: cached review task %s no longer matches (title=%r "
|
|
1031
1100
|
"status=%s) — re-resolving",
|
|
@@ -1048,10 +1117,24 @@ class ReviewWatcher:
|
|
|
1048
1117
|
# no open review task for this PR, whatever the cache said.
|
|
1049
1118
|
if cached is not None:
|
|
1050
1119
|
self.state.forget_review_task(pr.full_name, pr.number)
|
|
1051
|
-
return None
|
|
1120
|
+
return ResolvedTask(task=None)
|
|
1052
1121
|
|
|
1053
1122
|
self.state.record_review_task(pr.full_name, pr.number, task.ref)
|
|
1054
|
-
|
|
1123
|
+
# The search matched a TITLE out of the corpus; the round count and the
|
|
1124
|
+
# verdict both live in the task's evidence, and ONE read returns both.
|
|
1125
|
+
# Reading it here rather than paying `count_verdicts` now and
|
|
1126
|
+
# `latest_verdict` again from convergence is the same saving the cached
|
|
1127
|
+
# path makes -- and it matters most in the degraded state this whole
|
|
1128
|
+
# change serves: an unreadable ledger sends EVERY PR down this path on
|
|
1129
|
+
# EVERY pass (PR #69 round 1).
|
|
1130
|
+
detail = self.alissa.get_task(task.ref)
|
|
1131
|
+
if detail is not None:
|
|
1132
|
+
return ResolvedTask.from_detail(detail)
|
|
1133
|
+
# That read failed, so the verdict is genuinely unknown -- left UNREAD
|
|
1134
|
+
# rather than defaulted to None, which convergence would take as "no
|
|
1135
|
+
# approve" and lose a closed round. Falls back to exactly the two-read
|
|
1136
|
+
# behaviour this path had before.
|
|
1137
|
+
return ResolvedTask(task=task, verdicts=self.alissa.count_verdicts(task.ref))
|
|
1055
1138
|
|
|
1056
1139
|
# -- per-PR decision ---------------------------------------------------
|
|
1057
1140
|
|
|
@@ -1082,9 +1165,10 @@ class ReviewWatcher:
|
|
|
1082
1165
|
# Fall back to the substantive-review count only before the review task
|
|
1083
1166
|
# exists (round 1). Looked up here (not in _spawn) because both the count
|
|
1084
1167
|
# and convergence need it.
|
|
1085
|
-
|
|
1168
|
+
resolved = self._review_task(pr)
|
|
1169
|
+
task = resolved.task
|
|
1086
1170
|
native = countable_rounds(my_reviews)
|
|
1087
|
-
completed = verdicts if task is not None else native
|
|
1171
|
+
completed = resolved.verdicts if task is not None else native
|
|
1088
1172
|
|
|
1089
1173
|
# A round is not over until its verdict exists as a native review by
|
|
1090
1174
|
# the reviewer identity (issue #51). An envelope ahead of the native
|
|
@@ -1114,7 +1198,7 @@ class ReviewWatcher:
|
|
|
1114
1198
|
# anything else the round is still open and nothing may follow it.
|
|
1115
1199
|
return self._close_round_natively(pr, task, round_=completed)
|
|
1116
1200
|
|
|
1117
|
-
converged = self._convergence_reason(my_reviews,
|
|
1201
|
+
converged = self._convergence_reason(my_reviews, resolved, pr.head_sha)
|
|
1118
1202
|
if converged is not None:
|
|
1119
1203
|
# THE terminal branch: a verdict of record exists at the current
|
|
1120
1204
|
# head, so no round k+1 can be owed from here -- every path that
|
|
@@ -1972,7 +2056,7 @@ class ReviewWatcher:
|
|
|
1972
2056
|
)
|
|
1973
2057
|
|
|
1974
2058
|
def _convergence_reason(
|
|
1975
|
-
self, my_reviews: list[Review],
|
|
2059
|
+
self, my_reviews: list[Review], resolved: "ResolvedTask", head_sha: str
|
|
1976
2060
|
) -> str | None:
|
|
1977
2061
|
"""Why the loop is done, or None if it is not.
|
|
1978
2062
|
|
|
@@ -2033,9 +2117,14 @@ class ReviewWatcher:
|
|
|
2033
2117
|
return None
|
|
2034
2118
|
|
|
2035
2119
|
# Only checkable once a review task exists; before that there is
|
|
2036
|
-
# nowhere for a verdict to have been recorded.
|
|
2037
|
-
|
|
2038
|
-
|
|
2120
|
+
# nowhere for a verdict to have been recorded. On the cached path the
|
|
2121
|
+
# envelope was already read back with the task, so this costs nothing;
|
|
2122
|
+
# on the search path it is the one fetch that still has to happen.
|
|
2123
|
+
if (
|
|
2124
|
+
resolved.task is not None
|
|
2125
|
+
and resolved.newest_verdict(self.alissa) == VERDICT_APPROVE
|
|
2126
|
+
):
|
|
2127
|
+
return f"newest verdict envelope on {resolved.task.ref} reads approve"
|
|
2039
2128
|
|
|
2040
2129
|
return None
|
|
2041
2130
|
|
|
@@ -3043,9 +3132,18 @@ class ReviewWatcher:
|
|
|
3043
3132
|
# DRY-RUN IS EXEMPT, and vacuously so: it already suppresses every side
|
|
3044
3133
|
# effect AND every correctness write (`_spawn` skips record_spawn, the
|
|
3045
3134
|
# reaper logs instead of killing, the drift/cap-out/deferral paths
|
|
3046
|
-
# return before both their comment and their record).
|
|
3047
|
-
#
|
|
3048
|
-
#
|
|
3135
|
+
# return before both their comment and their record). The ledger writes
|
|
3136
|
+
# it may still take are the snapshot and the review-task cache
|
|
3137
|
+
# (`_review_task` runs in dry-run and both records and forgets
|
|
3138
|
+
# mappings) -- both classified by this module as best-effort telemetry,
|
|
3139
|
+
# both absorbed by _write_telemetry, and neither a decision the daemon
|
|
3140
|
+
# has to remember. Writing the cache in dry-run is deliberate: a
|
|
3141
|
+
# dry-run pass that learns a mapping hands it to the next production
|
|
3142
|
+
# pass, and suppressing it would make the two disagree about ledger
|
|
3143
|
+
# contents for no correctness reason. The cost on a read-only volume is
|
|
3144
|
+
# a reconnect attempt on the first failure of the streak plus
|
|
3145
|
+
# streak-limited warnings, which is the same best-effort behaviour the
|
|
3146
|
+
# snapshot has always had there.
|
|
3049
3147
|
# So the gate would protect nothing there and cost the operator the one
|
|
3050
3148
|
# tool that answers "what would you do right now" -- asked, precisely,
|
|
3051
3149
|
# during the substrate incident this whole change is about.
|
|
@@ -157,6 +157,12 @@ CREATE TABLE IF NOT EXISTS verdict_posts (
|
|
|
157
157
|
-- losing the whole table only costs one title search per PR (see
|
|
158
158
|
-- loop._review_task). That is why this is best-effort like the snapshots
|
|
159
159
|
-- above, not a correctness write.
|
|
160
|
+
--
|
|
161
|
+
-- Rows are never pruned, unlike poll_snapshots and like grants: one row per PR
|
|
162
|
+
-- ever sighted (a few hundred, for this daemon's repo set, forever), and a
|
|
163
|
+
-- stale row costs exactly one disproved read the first time that PR is seen
|
|
164
|
+
-- again. `resolved_at` is retained for inspection -- when was this mapping last
|
|
165
|
+
-- confirmed by a search -- and nothing in the daemon reads it.
|
|
160
166
|
CREATE TABLE IF NOT EXISTS review_tasks (
|
|
161
167
|
repo TEXT NOT NULL,
|
|
162
168
|
number INTEGER NOT NULL,
|
|
@@ -905,8 +911,10 @@ class State:
|
|
|
905
911
|
"""Remember which review task a PR resolved to. Best-effort.
|
|
906
912
|
|
|
907
913
|
REPLACE, not IGNORE: re-resolving is how a mapping is corrected, so the
|
|
908
|
-
newest answer has to win. `resolved_at`
|
|
909
|
-
confirmed by a search
|
|
914
|
+
newest answer has to win. `resolved_at` records when the mapping was
|
|
915
|
+
last confirmed by a search; it is kept for inspection (the age of a
|
|
916
|
+
mapping is the first thing worth knowing if one is ever suspected) and
|
|
917
|
+
no query, log line or console view reads it.
|
|
910
918
|
|
|
911
919
|
A database error here is absorbed exactly like a snapshot's: the cache
|
|
912
920
|
is an optimization, and a pass that cannot persist it still decides the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.16.12
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.16.11
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.16.11 → alissa_tools_github_revloop-0.16.12}/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|