alissa-tools-github-revloop 0.31.16__tar.gz → 0.31.18__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.31.16/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.18}/PKG-INFO +13 -1
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/README.md +12 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/config.py +25 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/ghclient.py +58 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/loop.py +197 -34
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/parking.py +3 -1
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/state.py +6 -3
- alissa_tools_github_revloop-0.31.18/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/server.py +3 -1
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +13 -1
- alissa_tools_github_revloop-0.31.16/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/fleet_vitals.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/janitor.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/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.
|
|
3
|
+
Version: 0.31.18
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-REVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -46,6 +46,18 @@ The rule: a distribution owns a leaf package and declares only that subtree
|
|
|
46
46
|
`__init__.py` at any namespace level would claim it for one distribution and
|
|
47
47
|
shadow the others.
|
|
48
48
|
|
|
49
|
+
## Discovery PRs are not reviewed
|
|
50
|
+
|
|
51
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
52
|
+
body carries `<!-- alissa-discovery v1 -->` on a line of its own and whose
|
|
53
|
+
every changed path is under `knowledge-base/` gets **no review round** — no
|
|
54
|
+
reviewer session, no review task — and one activity-comment line per head
|
|
55
|
+
saying why. A marked PR that touches anything else (or whose file list cannot
|
|
56
|
+
be read in full) is reviewed under the code directive, unchanged. The fleet
|
|
57
|
+
flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
58
|
+
the skip off: every discovery PR is then reviewed like any other. See
|
|
59
|
+
*Discovery PRs* in the repository README.
|
|
60
|
+
|
|
49
61
|
## Install
|
|
50
62
|
|
|
51
63
|
```sh
|
|
@@ -24,6 +24,18 @@ The rule: a distribution owns a leaf package and declares only that subtree
|
|
|
24
24
|
`__init__.py` at any namespace level would claim it for one distribution and
|
|
25
25
|
shadow the others.
|
|
26
26
|
|
|
27
|
+
## Discovery PRs are not reviewed
|
|
28
|
+
|
|
29
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
30
|
+
body carries `<!-- alissa-discovery v1 -->` on a line of its own and whose
|
|
31
|
+
every changed path is under `knowledge-base/` gets **no review round** — no
|
|
32
|
+
reviewer session, no review task — and one activity-comment line per head
|
|
33
|
+
saying why. A marked PR that touches anything else (or whose file list cannot
|
|
34
|
+
be read in full) is reviewed under the code directive, unchanged. The fleet
|
|
35
|
+
flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
36
|
+
the skip off: every discovery PR is then reviewed like any other. See
|
|
37
|
+
*Discovery PRs* in the repository README.
|
|
38
|
+
|
|
27
39
|
## Install
|
|
28
40
|
|
|
29
41
|
```sh
|
|
@@ -183,6 +183,7 @@ CONFIG_KEYS = (
|
|
|
183
183
|
"authors",
|
|
184
184
|
"plan_repos",
|
|
185
185
|
"plan_authors",
|
|
186
|
+
"discovery_review",
|
|
186
187
|
"operators",
|
|
187
188
|
"agent_profile",
|
|
188
189
|
"reviewer_login",
|
|
@@ -268,6 +269,15 @@ JANITOR_WARN_PCT_ENV = "ALISSA_REVIEW_JANITOR_WARN_PCT"
|
|
|
268
269
|
PLAN_REPOS_ENV = "ALISSA_REVIEW_PLAN_REPOS"
|
|
269
270
|
PLAN_AUTHORS_ENV = "ALISSA_REVIEW_PLAN_AUTHORS"
|
|
270
271
|
PLAN_REPO_OWNER_PLACEHOLDER = "<owner>"
|
|
272
|
+
|
|
273
|
+
# The discovery fleet flag (issue #171; build-discovery design §3.3, DS-7).
|
|
274
|
+
# OFF by default: a DISCOVERY PR -- a plans repository, the discovery marker,
|
|
275
|
+
# every changed path under `knowledge-base/` (loop.discovery_skip_reason) --
|
|
276
|
+
# gets no review round at all, because its defence is the knowledge-base lint
|
|
277
|
+
# of record, staleness, the plan refuter and the user's acceptance. ON, every
|
|
278
|
+
# discovery PR is reviewed under the code directive like any other PR. Same
|
|
279
|
+
# env > file > default precedence and boolean spellings as the janitor's rail.
|
|
280
|
+
DISCOVERY_REVIEW_ENV = "ALISSA_REVIEW_DISCOVERY_REVIEW"
|
|
271
281
|
DEFAULT_PLAN_REPO_NAME = "alissa-dark-factory-plans"
|
|
272
282
|
DEFAULT_PLAN_REPOS: "tuple[str, ...]" = (
|
|
273
283
|
f"{PLAN_REPO_OWNER_PLACEHOLDER}/{DEFAULT_PLAN_REPO_NAME}",
|
|
@@ -896,6 +906,10 @@ class Config:
|
|
|
896
906
|
plan_repos: tuple[str, ...] = DEFAULT_PLAN_REPOS
|
|
897
907
|
plan_authors: tuple[str, ...] = ()
|
|
898
908
|
|
|
909
|
+
# Review discovery PRs after all (issue #171); see DISCOVERY_REVIEW_ENV.
|
|
910
|
+
# False -- the default -- skips them.
|
|
911
|
+
discovery_review: bool = False
|
|
912
|
+
|
|
899
913
|
# GitHub logins whose re-entry ack may raise a capped PR's effective cap
|
|
900
914
|
# (loop.parse_reentry_ack). Empty -- the default -- means NO ack is ever
|
|
901
915
|
# honoured: the lever fails closed, because anyone who can comment on a PR
|
|
@@ -1267,6 +1281,16 @@ class Config:
|
|
|
1267
1281
|
env_plan_authors = env_plan_list(PLAN_AUTHORS_ENV, environ)
|
|
1268
1282
|
if env_plan_authors is not None:
|
|
1269
1283
|
raw["plan_authors"] = env_plan_authors
|
|
1284
|
+
# The discovery flag (issue #171), same layer, same reason.
|
|
1285
|
+
env_discovery = _env_bool(DISCOVERY_REVIEW_ENV, environ)
|
|
1286
|
+
if env_discovery is not None:
|
|
1287
|
+
raw["discovery_review"] = env_discovery
|
|
1288
|
+
discovery_review = raw.get("discovery_review", cls.discovery_review)
|
|
1289
|
+
if not isinstance(discovery_review, bool):
|
|
1290
|
+
# A JSON string "false" is truthy; refused rather than read as on.
|
|
1291
|
+
raise ValueError(
|
|
1292
|
+
f"discovery_review must be true or false, got {discovery_review!r}"
|
|
1293
|
+
)
|
|
1270
1294
|
|
|
1271
1295
|
source = raw.get("repos_source", cls.repos_source)
|
|
1272
1296
|
if source not in _REPOS_SOURCES:
|
|
@@ -1552,6 +1576,7 @@ class Config:
|
|
|
1552
1576
|
authors=authors,
|
|
1553
1577
|
plan_repos=plan_repos,
|
|
1554
1578
|
plan_authors=plan_authors,
|
|
1579
|
+
discovery_review=discovery_review,
|
|
1555
1580
|
operators=operators,
|
|
1556
1581
|
agent_profile=raw.get("agent_profile", "claude"),
|
|
1557
1582
|
reviewer_login=raw.get("reviewer_login"),
|
|
@@ -93,6 +93,12 @@ COMPARE_COMMITS_PER_PAGE = 1
|
|
|
93
93
|
# gone?" probe would answer yes on every large PR and abandon real verdicts.
|
|
94
94
|
PR_COMMIT_CAP = 250
|
|
95
95
|
|
|
96
|
+
# GitHub's OWN cap on the pull-request files endpoint: it "lists a maximum of
|
|
97
|
+
# 3000 files" (30 pages of 100). Past it the listing is silently short, so a
|
|
98
|
+
# PR at the cap cannot prove that EVERY changed path is under some prefix --
|
|
99
|
+
# which is the one question `pull_request_files` is asked (issue #171).
|
|
100
|
+
PR_FILES_CAP = 3000
|
|
101
|
+
|
|
96
102
|
# Environment variables `gh` reads a token from. Both are cleared before an
|
|
97
103
|
# explicitly-routed call, so an inherited implementer credential cannot win by
|
|
98
104
|
# being the one variable we did not think to overwrite.
|
|
@@ -783,6 +789,58 @@ class GitHub:
|
|
|
783
789
|
return out
|
|
784
790
|
page += 1
|
|
785
791
|
|
|
792
|
+
def pull_request_files(self, owner: str, repo: str, number: int) -> list[str]:
|
|
793
|
+
"""Every path the PR changes, paged to the end.
|
|
794
|
+
|
|
795
|
+
Read on ONE path: the discovery skip (issue #171), which asks whether
|
|
796
|
+
EVERY changed path is under `knowledge-base/`. That is an absence
|
|
797
|
+
question again -- "nothing outside the prefix" -- so a listing at
|
|
798
|
+
GitHub's PR_FILES_CAP is REFUSED (`TruncatedListing`) rather than
|
|
799
|
+
answered short, and the caller reviews the PR instead of skipping it.
|
|
800
|
+
|
|
801
|
+
A RENAME contributes BOTH names, for the reason `compare_files` gives:
|
|
802
|
+
`src/x.py` moved to `knowledge-base/x.py` changed a path outside the
|
|
803
|
+
prefix, and reading only the destination would make it look confined.
|
|
804
|
+
"""
|
|
805
|
+
out: list[str] = []
|
|
806
|
+
seen: set[str] = set()
|
|
807
|
+
listed = 0
|
|
808
|
+
page = 1
|
|
809
|
+
while True:
|
|
810
|
+
data = (
|
|
811
|
+
self._api(
|
|
812
|
+
"-X",
|
|
813
|
+
"GET",
|
|
814
|
+
f"repos/{owner}/{repo}/pulls/{number}/files",
|
|
815
|
+
"-f",
|
|
816
|
+
f"per_page={PER_PAGE}",
|
|
817
|
+
"-f",
|
|
818
|
+
f"page={page}",
|
|
819
|
+
)
|
|
820
|
+
or []
|
|
821
|
+
)
|
|
822
|
+
listed += len(data)
|
|
823
|
+
for entry in data:
|
|
824
|
+
for key in ("filename", "previous_filename"):
|
|
825
|
+
name = str(entry.get(key) or "")
|
|
826
|
+
if name and name not in seen:
|
|
827
|
+
seen.add(name)
|
|
828
|
+
out.append(name)
|
|
829
|
+
if listed >= PR_FILES_CAP:
|
|
830
|
+
log.warning(
|
|
831
|
+
"%s/%s#%d reached GitHub's %d-file listing cap — the file "
|
|
832
|
+
"list is not complete, so 'nothing outside the prefix' "
|
|
833
|
+
"cannot be read from it",
|
|
834
|
+
owner, repo, number, PR_FILES_CAP,
|
|
835
|
+
)
|
|
836
|
+
raise TruncatedListing(
|
|
837
|
+
f"{owner}/{repo}#{number} reached GitHub's {PR_FILES_CAP}-file "
|
|
838
|
+
f"listing cap; absence cannot be proven from it"
|
|
839
|
+
)
|
|
840
|
+
if len(data) < PER_PAGE:
|
|
841
|
+
return out
|
|
842
|
+
page += 1
|
|
843
|
+
|
|
786
844
|
# -- CI check rollup ---------------------------------------------------
|
|
787
845
|
|
|
788
846
|
def check_rollup(self, owner: str, repo: str, sha: str) -> CheckRollup:
|
|
@@ -1246,6 +1246,51 @@ def plan_id_of(pr: PullRequest) -> "str | None":
|
|
|
1246
1246
|
return match.group(1) if match else None
|
|
1247
1247
|
|
|
1248
1248
|
|
|
1249
|
+
# -- the discovery skip (issue #171; build-discovery design §3.2, §3.3, DS-7) --
|
|
1250
|
+
#
|
|
1251
|
+
# A DISCOVERY PR is a discovery task's output: findings written into the
|
|
1252
|
+
# plans repository's `knowledge-base/`. Its defence is the knowledge-base lint
|
|
1253
|
+
# of record, staleness, the plan refuter and the user's acceptance (§3.3), so a
|
|
1254
|
+
# judgment review per discovery task would only double the subscription cost
|
|
1255
|
+
# -- revloop starts NO round on it, which is what leaves it without a review
|
|
1256
|
+
# task (the reviewer session is what creates one, CR2).
|
|
1257
|
+
#
|
|
1258
|
+
# All THREE signals, or it is not a discovery PR: the repository is in
|
|
1259
|
+
# `plan_repos`, the body carries the discovery marker on a line of its own,
|
|
1260
|
+
# and EVERY changed path is under `knowledge-base/` (the PR's files, paged).
|
|
1261
|
+
# A marked PR that touches anything else -- or whose files cannot be read in
|
|
1262
|
+
# full -- is reviewed under the CODE directive, unchanged: the safe reading.
|
|
1263
|
+
# The fleet flag `discovery_review` (config.DISCOVERY_REVIEW_ENV) turns the
|
|
1264
|
+
# skip off, and every discovery PR is then reviewed like any other.
|
|
1265
|
+
DISCOVERY_MARKER = "<!-- alissa-discovery v1 -->"
|
|
1266
|
+
DISCOVERY_PREFIX = "knowledge-base/"
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def discovery_marked(config: Config, pr: PullRequest) -> bool:
|
|
1270
|
+
"""The two signals readable without a GitHub call: the plans repository
|
|
1271
|
+
and the marker on a line of its own (the plan marker's rule)."""
|
|
1272
|
+
if not config.is_plan_repo(pr.full_name):
|
|
1273
|
+
return False
|
|
1274
|
+
body = pr.body.replace("\r\n", "\n")
|
|
1275
|
+
return any(line.strip() == DISCOVERY_MARKER for line in body.split("\n"))
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def outside_discovery_prefix(paths: "list[str]") -> "list[str]":
|
|
1279
|
+
"""The changed paths NOT under `knowledge-base/`. An empty diff answers
|
|
1280
|
+
with a placeholder rather than nothing: "every path is confined" must not
|
|
1281
|
+
hold vacuously for a PR that changes no path at all."""
|
|
1282
|
+
if not paths:
|
|
1283
|
+
return ["(no changed files)"]
|
|
1284
|
+
return [p for p in paths if not p.startswith(DISCOVERY_PREFIX)]
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def discovery_skip_kind(head_sha: str) -> str:
|
|
1288
|
+
"""The ping-ledger kind that dedupes the skip's activity line: one line
|
|
1289
|
+
per head, since the review request stands and the skip is re-decided on
|
|
1290
|
+
every poll."""
|
|
1291
|
+
return f"activity-discovery-skip:{head_sha}"
|
|
1292
|
+
|
|
1293
|
+
|
|
1249
1294
|
# The ORIGIN task a reviewer spawn is stamped with (`--task`, issue #153; the
|
|
1250
1295
|
# Studio cost meter's lane L4). First from the review task's CR2 title --
|
|
1251
1296
|
# `Review PR <org>/<repo>#<n> (TASK-<origin>)` -- then from the PR body, the
|
|
@@ -2640,6 +2685,12 @@ class ReviewWatcher:
|
|
|
2640
2685
|
# hold a place forever.
|
|
2641
2686
|
self._waiting: dict[tuple[str, int], Waiting] = {}
|
|
2642
2687
|
self._wait_seq = 0
|
|
2688
|
+
# The discovery skip's file reads (issue #171), keyed by (repo, PR,
|
|
2689
|
+
# head): the changed paths outside `knowledge-base/` at that head. A
|
|
2690
|
+
# skipped PR keeps its review request, so it is re-decided every poll,
|
|
2691
|
+
# and a head's file list never changes -- one paged read per head, not
|
|
2692
|
+
# one per poll. Only successful reads are kept; a failed one retries.
|
|
2693
|
+
self._discovery_outside: "dict[tuple[str, int, str], list[str]]" = {}
|
|
2643
2694
|
# Consecutive passes that deferred at least one round -- the summary
|
|
2644
2695
|
# line's streak limiter, and ONLY that. It never escalates: a queue
|
|
2645
2696
|
# that keeps draining is the gate working, and a wave permanently
|
|
@@ -3110,6 +3161,17 @@ class ReviewWatcher:
|
|
|
3110
3161
|
f"PR author {pr.author} is not in the authors allowlist",
|
|
3111
3162
|
)
|
|
3112
3163
|
|
|
3164
|
+
# THE DISCOVERY SKIP (issue #171) gates starting a round exactly as the
|
|
3165
|
+
# authors filter does, and sits beside it for the same reasons: below
|
|
3166
|
+
# every branch that FINISHES a round already started (a discovery PR
|
|
3167
|
+
# reviewed while `discovery_review` was on still gets its verdict of
|
|
3168
|
+
# record), above every one that numbers, records or starts one. Below
|
|
3169
|
+
# the authors filter because it is the only one of the two that can
|
|
3170
|
+
# cost a GitHub call.
|
|
3171
|
+
skipped = self._discovery_skip(pr)
|
|
3172
|
+
if skipped is not None:
|
|
3173
|
+
return skipped
|
|
3174
|
+
|
|
3113
3175
|
# The effective cap is the configured one plus every re-entry an
|
|
3114
3176
|
# operator has explicitly acked on THIS PR (issue #42). The sum is a
|
|
3115
3177
|
# local read, so it costs nothing on the common path; the GitHub scan
|
|
@@ -3254,6 +3316,74 @@ class ReviewWatcher:
|
|
|
3254
3316
|
stability=stability.notice,
|
|
3255
3317
|
)
|
|
3256
3318
|
|
|
3319
|
+
# -- the discovery skip (issue #171) -----------------------------------
|
|
3320
|
+
|
|
3321
|
+
def _discovery_skip(self, pr: PullRequest) -> Decision | None:
|
|
3322
|
+
"""A SKIPPED decision for a discovery PR, or None to review it.
|
|
3323
|
+
|
|
3324
|
+
None -- review under the code directive, the safe reading -- whenever
|
|
3325
|
+
any of the three signals fails, the files cannot be read in full, or
|
|
3326
|
+
the fleet flag `discovery_review` is on. The two cheap signals are read
|
|
3327
|
+
first, so a PR that is not marked never costs a files read.
|
|
3328
|
+
|
|
3329
|
+
A skip appends ONE activity line per head (ledger-deduped, recorded
|
|
3330
|
+
only once the append lands, so a dry run records nothing) and logs at
|
|
3331
|
+
INFO only then; the re-decisions on later polls are DEBUG.
|
|
3332
|
+
"""
|
|
3333
|
+
if not discovery_marked(self.config, pr):
|
|
3334
|
+
return None
|
|
3335
|
+
if self.config.discovery_review:
|
|
3336
|
+
log.debug(
|
|
3337
|
+
"%s carries the discovery marker but discovery_review is on — "
|
|
3338
|
+
"reviewing under the code directive",
|
|
3339
|
+
pr.slug,
|
|
3340
|
+
)
|
|
3341
|
+
return None
|
|
3342
|
+
|
|
3343
|
+
key = (pr.full_name, pr.number, pr.head_sha)
|
|
3344
|
+
outside = self._discovery_outside.get(key)
|
|
3345
|
+
if outside is None:
|
|
3346
|
+
try:
|
|
3347
|
+
paths = self.github.pull_request_files(pr.owner, pr.repo, pr.number)
|
|
3348
|
+
except RateLimited:
|
|
3349
|
+
# Throttling is not a fact about the diff (the stability
|
|
3350
|
+
# guard's rule): raised on, so the poll backs off.
|
|
3351
|
+
raise
|
|
3352
|
+
except (CommandError, TruncatedListing) as exc:
|
|
3353
|
+
log.warning(
|
|
3354
|
+
"%s carries the discovery marker but its changed files "
|
|
3355
|
+
"cannot be read in full — reviewing under the code "
|
|
3356
|
+
"directive: %s",
|
|
3357
|
+
pr.slug, exc,
|
|
3358
|
+
)
|
|
3359
|
+
return None
|
|
3360
|
+
outside = outside_discovery_prefix(paths)
|
|
3361
|
+
self._discovery_outside[key] = outside
|
|
3362
|
+
|
|
3363
|
+
if outside:
|
|
3364
|
+
log.info(
|
|
3365
|
+
"%s carries the discovery marker but changes %d path(s) outside "
|
|
3366
|
+
"%s (first: %s) — reviewing under the code directive",
|
|
3367
|
+
pr.slug, len(outside), DISCOVERY_PREFIX, outside[0],
|
|
3368
|
+
)
|
|
3369
|
+
return None
|
|
3370
|
+
|
|
3371
|
+
reason = (
|
|
3372
|
+
f"discovery PR — plans repository, `{DISCOVERY_MARKER}` marker, "
|
|
3373
|
+
f"every changed path under `{DISCOVERY_PREFIX}`; no review round "
|
|
3374
|
+
f"(discovery_review is off)"
|
|
3375
|
+
)
|
|
3376
|
+
kind = discovery_skip_kind(pr.head_sha)
|
|
3377
|
+
if not self.state.pinged(pr.full_name, pr.number, kind):
|
|
3378
|
+
log.info("%s at %s: skipping review — %s", pr.slug, pr.head_sha[:8], reason)
|
|
3379
|
+
if self._append_activity(
|
|
3380
|
+
pr, f"- {_now()} — skipped at `{pr.head_sha[:8]}` — {reason}"
|
|
3381
|
+
):
|
|
3382
|
+
self.state.record_ping(pr.full_name, pr.number, kind)
|
|
3383
|
+
else:
|
|
3384
|
+
log.debug("%s: %s", pr.slug, reason)
|
|
3385
|
+
return Decision(Action.SKIPPED, reason)
|
|
3386
|
+
|
|
3257
3387
|
# -- the spawn gate ----------------------------------------------------
|
|
3258
3388
|
|
|
3259
3389
|
def _gate_spawn(self, pr: PullRequest, round_: int) -> Decision | None:
|
|
@@ -6172,20 +6302,49 @@ class ReviewWatcher:
|
|
|
6172
6302
|
#157): claim the spawn of this round's session against the drain
|
|
6173
6303
|
flag, run `spawn` under the claim, and drop the claim however it ends.
|
|
6174
6304
|
|
|
6175
|
-
The gate sits ABOVE every side effect of the spawn -- the
|
|
6176
|
-
|
|
6177
|
-
activity line --
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6305
|
+
The gate sits ABOVE every side effect of the spawn but one -- the
|
|
6306
|
+
`alissa tmux queue add`, the census bump, the ledger row and the
|
|
6307
|
+
activity line all come after it -- and a round it stands down stays
|
|
6308
|
+
owed: a QUEUED `drained` decision writes no spawn row, burns no round
|
|
6309
|
+
number and spends no attempt, exactly like the slot gate. The claim
|
|
6310
|
+
and the flag are read and written in one immediate transaction on
|
|
6311
|
+
each side (`State.claim_spawn` / `State.set_drain`): a drain that
|
|
6182
6312
|
lands while this spawn is between its gate and its ledger write sees
|
|
6183
|
-
the claim and refuses itself
|
|
6184
|
-
|
|
6185
|
-
|
|
6313
|
+
the claim and refuses itself. A ledger that cannot answer stands the
|
|
6314
|
+
spawn down too -- a spawn that cannot prove the seat is not drained
|
|
6315
|
+
does not happen.
|
|
6316
|
+
|
|
6317
|
+
The claim only counts for `parking.SPAWN_CLAIM_TTL`, and the hub add
|
|
6318
|
+
(`_ensure_hub`, a clone on a PR's first round) can outlast it. So
|
|
6319
|
+
`_spawn_claimed` re-takes the claim (`_claim_spawn` again, issue
|
|
6320
|
+
#173) between the hub add and the enqueue, which refreshes it and
|
|
6321
|
+
re-reads the flag: a drain that held while the hub was provisioned
|
|
6322
|
+
stands the round down there, the same QUEUED `drained` way. That late
|
|
6323
|
+
re-check is the one stand-down that happens AFTER a side effect --
|
|
6324
|
+
the hub is provisioned and stays -- which is harmless, because the
|
|
6325
|
+
hub is a reusable cache the round's next spawn finds in place.
|
|
6326
|
+
Together the two claims mean a park can never race a spawn, whatever
|
|
6327
|
+
the drain's freshness rule allows.
|
|
6186
6328
|
|
|
6187
6329
|
Dry-run READS the flag and claims nothing: it shares the production
|
|
6188
6330
|
state path, and a dry pass must write no durable row."""
|
|
6331
|
+
refused = self._claim_spawn(pr, round_, name)
|
|
6332
|
+
if refused is not None:
|
|
6333
|
+
return refused
|
|
6334
|
+
try:
|
|
6335
|
+
return spawn()
|
|
6336
|
+
finally:
|
|
6337
|
+
if not self.config.dry_run:
|
|
6338
|
+
self.state.release_spawn(name)
|
|
6339
|
+
|
|
6340
|
+
def _claim_spawn(
|
|
6341
|
+
self, pr: PullRequest, round_: int, name: str, *, late: bool = False,
|
|
6342
|
+
) -> "Decision | None":
|
|
6343
|
+
"""Claim (or, `late`, re-claim) the spawn of `name` against the drain
|
|
6344
|
+
flag: None when the spawn may proceed, else the QUEUED `drained`
|
|
6345
|
+
decision that stands the round down. Dry-run reads the flag and
|
|
6346
|
+
claims nothing. See `_drain_gated`."""
|
|
6347
|
+
where = " after its hub was provisioned" if late else ""
|
|
6189
6348
|
try:
|
|
6190
6349
|
if self.config.dry_run:
|
|
6191
6350
|
drain = self.state.drain()
|
|
@@ -6193,36 +6352,32 @@ class ReviewWatcher:
|
|
|
6193
6352
|
drain = self.state.claim_spawn(name)
|
|
6194
6353
|
except sqlite3.Error as exc:
|
|
6195
6354
|
log.warning(
|
|
6196
|
-
"%s round %d: could not claim the spawn against the drain flag "
|
|
6197
|
-
"(%s) — standing it down this pass", pr.slug, round_, exc,
|
|
6198
|
-
)
|
|
6199
|
-
return Decision(
|
|
6200
|
-
Action.QUEUED,
|
|
6201
|
-
f"round {round_} stood down — the state ledger could not prove "
|
|
6202
|
-
f"the seat is not drained ({exc}); retrying next poll",
|
|
6203
|
-
round_,
|
|
6204
|
-
drained=True,
|
|
6205
|
-
)
|
|
6206
|
-
if drain is not None:
|
|
6207
|
-
since = parking_mod.iso(drain["drained_at"])
|
|
6208
|
-
until = parking_mod.iso(drain["expires_at"])
|
|
6209
|
-
log.info(
|
|
6210
|
-
"%s round %d: not spawning — the seat is drained for parking "
|
|
6211
|
-
"since %s (until %s)", pr.slug, round_, since, until,
|
|
6355
|
+
"%s round %d: could not claim the spawn against the drain flag%s "
|
|
6356
|
+
"(%s) — standing it down this pass", pr.slug, round_, where, exc,
|
|
6212
6357
|
)
|
|
6213
6358
|
return Decision(
|
|
6214
6359
|
Action.QUEUED,
|
|
6215
|
-
f"round {round_}
|
|
6216
|
-
f"
|
|
6217
|
-
f"retrying once it is undrained",
|
|
6360
|
+
f"round {round_} stood down{where} — the state ledger could not "
|
|
6361
|
+
f"prove the seat is not drained ({exc}); retrying next poll",
|
|
6218
6362
|
round_,
|
|
6219
6363
|
drained=True,
|
|
6220
6364
|
)
|
|
6221
|
-
|
|
6222
|
-
return
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6365
|
+
if drain is None:
|
|
6366
|
+
return None
|
|
6367
|
+
since = parking_mod.iso(drain["drained_at"])
|
|
6368
|
+
until = parking_mod.iso(drain["expires_at"])
|
|
6369
|
+
log.info(
|
|
6370
|
+
"%s round %d: not spawning%s — the seat is drained for parking "
|
|
6371
|
+
"since %s (until %s)", pr.slug, round_, where, since, until,
|
|
6372
|
+
)
|
|
6373
|
+
return Decision(
|
|
6374
|
+
Action.QUEUED,
|
|
6375
|
+
f"round {round_} held{where} — the seat is drained for parking since "
|
|
6376
|
+
f"{since} (until {until}: {drain['reason'] or 'no reason given'}); "
|
|
6377
|
+
f"retrying once it is undrained",
|
|
6378
|
+
round_,
|
|
6379
|
+
drained=True,
|
|
6380
|
+
)
|
|
6226
6381
|
|
|
6227
6382
|
def _spawn_claimed(
|
|
6228
6383
|
self,
|
|
@@ -6297,6 +6452,14 @@ class ReviewWatcher:
|
|
|
6297
6452
|
# provisioned -- unknown, never settled.
|
|
6298
6453
|
return Decision(Action.SKIPPED, problem, round_, owed=True)
|
|
6299
6454
|
|
|
6455
|
+
# The hub add can outlast the gate's claim (a clone on a PR's first
|
|
6456
|
+
# round): re-take it, refreshing it and re-reading the drain flag, so
|
|
6457
|
+
# a drain that landed meanwhile stands the round down here (issue
|
|
6458
|
+
# #173). The claim is still released in `_drain_gated`'s finally.
|
|
6459
|
+
refused = self._claim_spawn(pr, round_, name, late=True)
|
|
6460
|
+
if refused is not None:
|
|
6461
|
+
return refused
|
|
6462
|
+
|
|
6300
6463
|
self.alissa.enqueue_reviewer(
|
|
6301
6464
|
session=name,
|
|
6302
6465
|
directive=directive,
|
|
@@ -26,7 +26,9 @@ handshake):
|
|
|
26
26
|
reads every null as a refusal.
|
|
27
27
|
* **The drain.** A flag with a TTL in `state.db` (`State.set_drain`), cleared
|
|
28
28
|
when the daemon boots. While it holds, the round spawn stands down before
|
|
29
|
-
its first side effect (`ReviewWatcher._drain_gated`)
|
|
29
|
+
its first side effect (`ReviewWatcher._drain_gated`), or, when the drain
|
|
30
|
+
lands while the hub is provisioned, after the hub add and before the
|
|
31
|
+
enqueue (the post-hub re-claim).
|
|
30
32
|
"""
|
|
31
33
|
|
|
32
34
|
from __future__ import annotations
|
|
@@ -39,6 +39,8 @@ from collections.abc import Callable, Iterable
|
|
|
39
39
|
from dataclasses import dataclass
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
|
+
from .parking import SPAWN_CLAIM_TTL
|
|
43
|
+
|
|
42
44
|
log = logging.getLogger(__name__)
|
|
43
45
|
|
|
44
46
|
# Poll-snapshot retention: the newest N rows are kept, older ones pruned on
|
|
@@ -2067,11 +2069,12 @@ class State:
|
|
|
2067
2069
|
|
|
2068
2070
|
def set_drain(
|
|
2069
2071
|
self, ttl: int, reason: str, now: "float | None" = None,
|
|
2070
|
-
claim_ttl: int =
|
|
2072
|
+
claim_ttl: int = SPAWN_CLAIM_TTL,
|
|
2071
2073
|
) -> "tuple[dict, list[dict]]":
|
|
2072
2074
|
"""Raise the drain flag for `ttl` seconds and return it with the spawn
|
|
2073
|
-
claims standing at that instant (younger than `claim_ttl
|
|
2074
|
-
one is a spawn whose process
|
|
2075
|
+
claims standing at that instant (younger than `claim_ttl`, by default
|
|
2076
|
+
`parking.SPAWN_CLAIM_TTL` -- an older one is a spawn whose process
|
|
2077
|
+
died mid-way, cleared at the next boot).
|
|
2075
2078
|
ONE immediate transaction, so a spawn either claimed before this (and
|
|
2076
2079
|
is in the list) or claims after it (and sees the flag). A drain that
|
|
2077
2080
|
is already up is replaced: a re-drain restarts the TTL. Raises
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.18
|
|
@@ -332,7 +332,9 @@ class App:
|
|
|
332
332
|
cleared = self.sources.clear_drain()
|
|
333
333
|
except sqlite3.Error as exc:
|
|
334
334
|
self._audit("undrain", {"ok": False, "error": str(exc)})
|
|
335
|
-
|
|
335
|
+
# The flag's state is unknown here, and unknown is never a value
|
|
336
|
+
# (D2): the 503 already says the undrain did not land.
|
|
337
|
+
return 503, {"drained": None, "error": "state_unavailable"}
|
|
336
338
|
self._audit("undrain", {"ok": True, "cleared": cleared})
|
|
337
339
|
return 200, {"drained": False, "cleared": cleared}
|
|
338
340
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alissa-tools-github-revloop
|
|
3
|
-
Version: 0.31.
|
|
3
|
+
Version: 0.31.18
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-REVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -46,6 +46,18 @@ The rule: a distribution owns a leaf package and declares only that subtree
|
|
|
46
46
|
`__init__.py` at any namespace level would claim it for one distribution and
|
|
47
47
|
shadow the others.
|
|
48
48
|
|
|
49
|
+
## Discovery PRs are not reviewed
|
|
50
|
+
|
|
51
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
52
|
+
body carries `<!-- alissa-discovery v1 -->` on a line of its own and whose
|
|
53
|
+
every changed path is under `knowledge-base/` gets **no review round** — no
|
|
54
|
+
reviewer session, no review task — and one activity-comment line per head
|
|
55
|
+
saying why. A marked PR that touches anything else (or whose file list cannot
|
|
56
|
+
be read in full) is reviewed under the code directive, unchanged. The fleet
|
|
57
|
+
flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
58
|
+
the skip off: every discovery PR is then reviewed like any other. See
|
|
59
|
+
*Discovery PRs* in the repository README.
|
|
60
|
+
|
|
49
61
|
## Install
|
|
50
62
|
|
|
51
63
|
```sh
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.16
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.16 → alissa_tools_github_revloop-0.31.18}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|