vps-health-api/scripts/review-levels.py
le king fu 44d58970ad
All checks were successful
CI / verification (pull_request) Successful in 38s
docs: apply three review suggestions, record Max's choices (#63)
- .claude/hooks/** joins the always-high row: a hook script runs on the
  workstation.
- Bas narrows to **/*.md and LICENSE: a script or an image added under
  docs/ or reports/ is no longer low.
- package.json: a package moved between devDependencies and dependencies,
  or any line of the dependencies block, is high; the script now tracks
  the block each package line belongs to.
- Calibration records the choices Max confirmed on 2026-10-19.

Replay unchanged: 20 haut out of 31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:21:29 -04:00

287 lines
12 KiB
Python
Executable file

#!/usr/bin/env python3
"""Replay the review levels of REVIEW.md on the pull requests that landed (#63).
Usage:
python3 scripts/review-levels.py [N ...] (default: every PR of LANDED)
python3 scripts/review-levels.py --check (no network)
The first form reads the Forgejo API (token in $FORGEJO_TOKEN, else
~/.forgejo-token; read-only calls) and prints each PR with its level and the
files that set it, then the distribution. --check compares the patterns of
this script with the first column of the Haut table of REVIEW.md and exits 1
if they differ: a PR that changes the Niveaux section updates this script in
the same diff.
Adapted from simpl-resultat/scripts/review-levels.py. The file list comes from
the PR's .diff, never from /pulls/{n}/files: that route answers 500 on a PR
whose base branch is gone, which is every link of a stack merged by
fast-forward. Three PRs (EMPTY_DIFF) have an empty .diff and are read from
the local clone: run the script from the repository. The content conditions of the Haut table are approximated from
the diff: index.js routing and auth, side effects of the shipped modules
(disk writes, the temp-file sweep, body caps, shell commands, outbound calls,
require.main), guard tests (an existing line changed or removed),
package.json (anything but a version), package-lock.json (a package added or
removed, an origin changed), .gitignore (a line removed).
"""
import itertools
import json
import os
import re
import subprocess
import sys
import urllib.request
API = "https://git.lacompagniemaximus.com/api/v1/repos/maximus/vps-health-api"
REVIEW_MD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "REVIEW.md")
# Every PR whose head reached main, frozen by number. Forgejo flags only part
# of them as merged: a stack merged by fast-forward (#18-#23, #31, #34, #37)
# shows as closed. #32 (a red-CI demo) never landed.
LANDED = [2, 5, 6, 8, 9, 18, 19, 20, 21, 22, 23, 26, 28, 30, 31, 34, 37, 39, 42, 43,
44, 45, 47, 51, 52, 53, 56, 57, 59, 60, 62]
# Three PRs whose .diff comes back empty: once fast-forwarded into main, their
# merge_base equals their head. Each is one commit, read from the local clone.
EMPTY_DIFF = {
18: ("39a55fad93414b8e0049724f351710bfe1eb7a77", "79cb813767cfb53282d5fe6f386af3d740bf7f7e"),
31: ("2b35b78a0beb04dc9ae92c13e42b9ff09fd2d147", "4e7879da6ca5163d37d11b9c5803cf2bed640b59"),
34: ("4e7879da6ca5163d37d11b9c5803cf2bed640b59", "156ca818ca37441a2166d112251529ac32783afc"),
}
# REVIEW.md, table Haut: rows without a condition.
HIGH_ALWAYS = [
"agent/pg-backup-pull/**",
"agent/claude-usage/{limits,push-claude-usage}.js", "agent/push-metrics.js", "agent/**/*.sh",
"Dockerfile",
".forgejo/workflows/**",
"vitest.config.js", "scripts/boot-from-copy.sh",
".claude/settings.json", ".claude/hooks/**", ".mcp.json",
]
SHIPPED = ["{index,metrics,claude-usage,claude-usage-history,backups}.js"]
GUARDS = ["__tests__/{auth,writes}.test.js"]
LOW = ["**/*.md", "LICENSE"]
NEVER_LOW = ["CLAUDE.md", "REVIEW.md", "agent/README.md", ".claude/**"]
AUTH = re.compile(r"\b(ROUTES|TOKEN_KINDS|resolveRoute|checkAuth|tokenMatches|logRejection|"
r"safeLogValue|handler|\w*_TOKEN)\b")
EFFECTS = re.compile(
r"\b(writeFile\w*|writeSync|createWriteStream|appendFile\w*|rename\w*|mkdir\w*|unlink\w*|"
r"rmSync|rmdir\w*|chmod\w*|chown\w*|copyFile\w*|symlink\w*|truncate\w*|writeJsonAtomic|"
r"writeHostSnapshot|sweepStaleTempFiles|TEMP_FILE_RE|readBody|\w*MAX_BODY_BYTES|"
r"child_process|execSync|execFile\w*|spawn\w*|fetch|https?\.(request|get))\b"
r"|require\.main")
def token():
return os.environ.get("FORGEJO_TOKEN") or open(os.path.expanduser("~/.forgejo-token")).read().strip()
def get(path, raw=False):
req = urllib.request.Request(API + path, headers={"Authorization": "token " + token()})
data = urllib.request.urlopen(req).read()
return data if raw else json.loads(data)
def expand_braces(pat):
m = re.search(r"\{([^{}]*)\}", pat)
if not m:
return [pat]
return list(itertools.chain.from_iterable(
expand_braces(pat[:m.start()] + alt + pat[m.end():]) for alt in m.group(1).split(",")))
def glob_re(pat):
# '**/' spans zero or more directories, '*' and '?' never cross '/'
i, out = 0, ""
while i < len(pat):
if pat.startswith("**/", i):
out += "(?:.*/)?"; i += 3; continue
if pat.startswith("**", i):
out += ".*"; i += 2; continue
c = pat[i]
out += "[^/]*" if c == "*" else "[^/]" if c == "?" else re.escape(c)
i += 1
return re.compile("^" + out + "$")
def match(pats, path):
# '{a,b}' alternates, wildcards allowed inside
return any(glob_re(p).match(path) for pat in pats for p in expand_braces(pat))
def parse_diff(diff):
"""[{filename, previous_filename, status, block}] from a git diff."""
files = []
for block in re.split(r"(?m)^diff --git ", diff)[1:]:
head = block.split("\n@@", 1)[0]
m = re.match(r"a/(\S+) b/(\S+)", block)
old, new = m.group(1), m.group(2)
status = "changed"
if re.search(r"(?m)^new file mode", head):
status = "added"
elif re.search(r"(?m)^deleted file mode", head):
status = "deleted"
elif re.search(r"(?m)^rename from", head) or old != new:
status = "renamed"
files.append({"filename": new, "previous_filename": old if old != new else None,
"status": status, "block": block})
return files
def meaningful(text):
t = text.strip()
return t != "" and not t.startswith(("#", "//", "*", "/*"))
def changed(block):
return [l for l in block.split("\n")
if l[:1] in "+-" and not l.startswith(("+++", "---")) and meaningful(l[1:])]
def touches(block, pattern):
# a hunk whose enclosing function (the @@ context) or a changed line matches
for hunk in re.split(r"(?m)^(?=@@)", block)[1:]:
if pattern.search(hunk.split("\n", 1)[0]) or any(pattern.search(l) for l in changed(hunk)):
return True
return False
def package_json_changes(block):
# anything but "version" and a bump of a package listed in the same block;
# any line of "dependencies" counts, since the runtime has none
before, after = {}, {}
for hunk in re.split(r"(?m)^@@", block)[1:]:
section = None
for l in hunk.split("\n")[1:]:
if l.startswith(("+++", "---")) or l[:1] not in " +-":
continue
m = re.match(r'^[ +-]\s*"([^"]+)":\s*\{', l)
if m:
section = m.group(1)
if l[:1] == " " or not meaningful(l[1:]):
if re.match(r"^[ +-]\s*\}", l):
section = None
continue
if section == "dependencies":
return True
m = re.match(r'^([+-])\s*"([^"]+)":\s*(.*?),?\s*$', l)
if not m:
return True
if m.group(3) == "{":
continue
(before if m.group(1) == "-" else after)[(section, m.group(2))] = m.group(3)
if re.match(r"^[+-]\s*\}", l):
section = None
for key in set(before) | set(after):
if key[1] == "version" and key[0] is None:
continue
if key in before and key in after and all(re.match(r'^"[\^~]?\d', v) for v in (before[key], after[key])):
continue
return True
return False
def origin_of(url):
m = re.match(r"^([a-z+]+)://([^/]+)", url)
return m.group(1) + "://" + m.group(2) if m else url
def lock_adds_or_removes(block):
if any(re.match(r'^[+-]\s+"node_modules/[^"]+": \{', l) for l in block.split("\n")):
return True
for hunk in re.split(r"(?m)^@@", block)[1:]:
hl = hunk.split("\n")
before, after = set(), set()
for l in hl:
m = re.match(r'^([+-])\s+"resolved": "([^"]+)"', l)
if m:
(before if m.group(1) == "-" else after).add(origin_of(m.group(2)))
if after - before:
return True
if any(re.match(r'^\+\s+"integrity": ', l) for l in hl) and not any(re.match(r'^\+\s+"version": ', l) for l in hl):
return True
return False
def review_level(files):
triggers = []
for f in files:
b = f["block"]
for path in filter(None, {f["filename"], f["previous_filename"]}):
if match(HIGH_ALWAYS, path):
triggers.append(path)
if path == "index.js" and touches(b, AUTH):
triggers.append(path + " (routing or auth)")
if match(SHIPPED, path) and (f["status"] != "changed" or touches(b, EFFECTS)):
triggers.append(path + " (side effect)")
if match(GUARDS, path) and [l for l in changed(b) if l.startswith("-")]:
triggers.append(path + " (existing line changed)")
if path == "package.json" and package_json_changes(b):
triggers.append(path)
if path == "package-lock.json" and lock_adds_or_removes(b):
triggers.append(path)
if path == ".gitignore" and [l for l in changed(b) if l.startswith("-")]:
triggers.append(path + " (line removed)")
if path == "REVIEW.md":
triggers.append(path + " (check the « Niveaux » section by hand)")
parts = path.split("/")
new_hidden = f["status"] == "added" and any(
x.startswith(".") for x in parts[:-1]) and parts[0] not in (".forgejo", ".claude")
if (f["status"] != "changed" and len(parts) == 1 and not path.endswith(".md")) or new_hidden:
triggers.append(path + " (" + f["status"] + " at top level)")
paths = [f["filename"] for f in files]
if triggers:
return "haut", sorted(set(triggers))
if all(match(LOW, p) and not match(NEVER_LOW, p) for p in paths):
return "bas", []
return "normal", []
CONDITIONAL = {
"index.js", "{index,metrics,claude-usage,claude-usage-history,backups}.js",
"__tests__/{auth,writes}.test.js", "package.json", "package-lock.json", ".gitignore",
"REVIEW.md",
}
def check():
"""Compare this script's patterns with the first column of the Haut table."""
text = open(REVIEW_MD, encoding="utf-8").read()
table = text.split("### Haut", 1)[1].split("\n\n", 2)[1]
documented = set()
for row in table.split("\n"):
if not row.startswith("| ") or row.startswith("| Motif") or row.startswith("| fichier à la racine"):
continue
documented.update(re.findall(r"`([^`]+)`", row.split(" | ")[0]))
coded = set(HIGH_ALWAYS) | CONDITIONAL
for label, extra in (("REVIEW.md only", documented - coded), ("script only", coded - documented)):
for pattern in sorted(extra):
print(f"{label}: {pattern}")
if documented != coded:
return 1
print(f"ok: {len(coded)} patterns, identical to the Haut table of REVIEW.md")
return 0
def main():
if sys.argv[1:] == ["--check"]:
sys.exit(check())
numbers = [int(a) for a in sys.argv[1:]] or LANDED
rows = []
for n in numbers:
if n in EMPTY_DIFF:
diff = subprocess.run(["git", "diff", *EMPTY_DIFF[n]], capture_output=True, text=True, check=True).stdout
else:
diff = get(f"/pulls/{n}.diff", raw=True).decode("utf-8", "replace")
files = parse_diff(diff)
if not files:
sys.exit(f"#{n}: empty diff, the level would read as bas")
level, triggers = review_level(files)
rows.append((n, level))
print(f"#{n}\t{level}\t{'; '.join(triggers)}")
counts = {lvl: sum(1 for r in rows if r[1] == lvl) for lvl in ("bas", "normal", "haut")}
print(f"{len(rows)} PRs: {counts} -> haut {100 * counts['haut'] / len(rows):.1f} %")
if __name__ == "__main__":
main()