From 11f5e2ceaba049874d1cfb9c9caef5f561f0d6ef Mon Sep 17 00:00:00 2001 From: Vinta Chen Date: Fri, 16 Oct 2026 14:12:14 +0800 Subject: [PATCH] feat: add scan-stars skill and Rejected Additions register Co-Authored-By: Claude --- .claude/skills/audit-the-list/SKILL.md | 2 +- .claude/skills/review-prs/SKILL.md | 2 +- .claude/skills/scan-stars/SKILL.md | 36 ++++++ .../skills/scan-stars/scripts/scan_stars.py | 117 ++++++++++++++++++ docs/audit-logs.md | 4 + 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 .claude/skills/scan-stars/SKILL.md create mode 100644 .claude/skills/scan-stars/scripts/scan_stars.py diff --git a/.claude/skills/audit-the-list/SKILL.md b/.claude/skills/audit-the-list/SKILL.md index 66118c1f..b7b397ea 100644 --- a/.claude/skills/audit-the-list/SKILL.md +++ b/.claude/skills/audit-the-list/SKILL.md @@ -27,7 +27,7 @@ Done when every entry in scope has downloads (or a stated no-signal reason), rep ## 3. Verdicts -Draft a verdict and reason for every entry, restructure before cap: decide Splits, mints, and re-homes first — cutting before restructuring destroys entries — then apply the cap tier by tier, including promotions and demotions between obvious choice and challenger. Ground every reason in the fetched evidence; label anything unverifiable as a judgment call. Where fresh evidence contradicts the standing verdict, say so in the reason instead of silently keeping the seed. +Draft a verdict and reason for every entry, restructure before cap: decide Splits, mints, and re-homes first — cutting before restructuring destroys entries — then apply the cap tier by tier, including promotions and demotions between obvious choice and challenger. Ground every reason in the fetched evidence; label anything unverifiable as a judgment call. Where fresh evidence contradicts the standing verdict, say so in the reason instead of silently keeping the seed. A proposed addition under `docs/audit-logs.md` "Rejected Additions" seeds as drop from that line. ## 4. Review and go diff --git a/.claude/skills/review-prs/SKILL.md b/.claude/skills/review-prs/SKILL.md index f5c3c890..70bb8ba3 100644 --- a/.claude/skills/review-prs/SKILL.md +++ b/.claude/skills/review-prs/SKILL.md @@ -16,7 +16,7 @@ Sort the batch: entry additions continue; anything else (typo fixes, website cha ## 2. Screen -Apply the Automatic Rejection rules that the diff and PR metadata answer without judgment. The recently-closed-duplicate rule needs a lookup: `gh pr list --repo vinta/awesome-python --state closed --search "" --limit 10`. A screened-out PR goes straight to step 4 as a close, with the rule it broke as its reason. +Apply the Automatic Rejection rules that the diff and PR metadata answer without judgment. The recently-closed-duplicate rule needs a lookup: `gh pr list --repo vinta/awesome-python --state closed --search "" --limit 10`; a project under `docs/audit-logs.md` "Rejected Additions" also counts as previously rejected unless the PR brings evidence newer than its line. A screened-out PR goes straight to step 4 as a close, with the rule it broke as its reason. Resolve each survivor's target use case against the current README — a diff's context lines show the base the PR was written on, which may have changed since. A merge conflict rides to the Merge arm, which absorbs it. Done when every surviving PR names its target use case. diff --git a/.claude/skills/scan-stars/SKILL.md b/.claude/skills/scan-stars/SKILL.md new file mode 100644 index 00000000..41b9e0e7 --- /dev/null +++ b/.claude/skills/scan-stars/SKILL.md @@ -0,0 +1,36 @@ +--- +name: scan-stars +description: Scan the maintainer's GitHub stars for README.md addition candidates; incremental by default, `full` rejudges stars judged before. +argument-hint: [full] +disable-model-invocation: true +--- + +# Scan stars + +A star scan turns the maintainer's GitHub stars into proposed additions. Every star the scan sees ends in one place: listed in README.md, screened out in `local/starred-scan.tsv`, or rejected under `docs/audit-logs.md` "Rejected Additions". Judgment belongs to `audit-the-list` and the rules to CONTRIBUTING.md; this skill only sources candidates and records outcomes. + +## 1. Scan + +Run `uv run python .claude/skills/scan-stars/scripts/scan_stars.py`, adding `--full` when the argument is `full`. With no ledger yet, a plain run already covers every star. Print the drop counts from stderr. Done when the candidate TSV is in hand. + +## 2. Screen + +Judge each candidate from its name, description, topics, and language against CONTRIBUTING Quality Requirement 1 (Serves Python Developers), and name the current use case it would compete in. The screen is loose: drop only what plainly fails, and pass anything uncertain on, since `audit-the-list` is the real judge. In a full scan, read the `prior` column as context and overturn a rejection only on new evidence. + +Append each screen-out to `local/starred-scan.tsv` as it is decided, one `repoyyyy-mm-ddreason` row (create the file with header `repo date reason`), so an interrupted run keeps its work. The file is append-only; the script reads the last row per repo. A survivor no current use case fits is a structure question: carry it to review with its evidence. Done when every candidate is a ledger row, holds a target use case, or is a structure question. + +## 3. Judge + +Group survivors by target use case. Per group, invoke `audit-the-list` with: "Judge proposed entries for the
use case. Evidence and Verdicts steps only; report the verdicts back. No preview page, no README changes, no commits." Done when every survivor holds an add or drop verdict with a reason, and each add at cap names the entry it displaces. + +## 4. Review + +Run `preview-verdicts` over all groups: survivors as `add` or `drop` rows, displaced entries as `drop`, structure questions noted on their group, and any `prior` verdict quoted in the row's reason. Done when the maintainer's feedback is processed and they gave the go. + +## 5. Record + +- **Added**: follow `audit-the-list` step 5 for the README change, one commit per entry. +- **Rejected**: a candidate that reached review and was not added gets one line under "Rejected Additions": `- owner/repo -- yyyy-mm-dd, /month (): .`, replacing its earlier line. +- **Parked**: a structure question the maintainer leaves open gets no record, so it resurfaces next run. + +Done when every candidate from step 1 is listed in README.md, a ledger row, a Rejected Additions line, or parked by the maintainer. diff --git a/.claude/skills/scan-stars/scripts/scan_stars.py b/.claude/skills/scan-stars/scripts/scan_stars.py new file mode 100644 index 00000000..f6c10c7a --- /dev/null +++ b/.claude/skills/scan-stars/scripts/scan_stars.py @@ -0,0 +1,117 @@ +"""List the maintainer's GitHub stars that could be README.md candidates. + +Drops stars that fail a check needing no judgment: already listed, own repo, +archived, no push in 12 months, or created under a month ago. These drops are +recomputed every run, so a repo that becomes active again resurfaces. An +incremental run also drops stars already judged: model screen-outs in +local/starred-scan.tsv and maintainer rejections under docs/audit-logs.md +"Rejected Additions". `--full` keeps them and shows the earlier verdict in the +`prior` column. Prints survivors as TSV on stdout, drop counts on stderr. +""" + +import argparse +import json +import re +import subprocess +import sys +from collections import Counter +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import TypedDict + +ROOT = Path(__file__).resolve().parents[4] +sys.path.insert(0, str(ROOT / "website")) + +from readme_parser import parse_readme + +LEDGER = ROOT / "local" / "starred-scan.tsv" +REGISTER = ROOT / "docs" / "audit-logs.md" +GITHUB_REPO_RE = re.compile(r"^https://github\.com/([^/#?]+/[^/#?]+)") +REJECTED_LINE_RE = re.compile(r"^- (\S+) -- (.+)$") + + +class Owner(TypedDict): + login: str + + +class Star(TypedDict): + full_name: str + owner: Owner + archived: bool + pushed_at: str + created_at: str + language: str | None + stargazers_count: int + topics: list[str] + description: str | None + + +def gh(*args: str) -> str: + return subprocess.run(["gh", *args], capture_output=True, text=True, check=True).stdout + + +def listed_repos() -> set[str]: + repos = set() + for group in parse_readme((ROOT / "README.md").read_text()): + for category in group["categories"]: + for link in [*category["entries"], *(sub for entry in category["entries"] for sub in entry["also_see"])]: + if match := GITHUB_REPO_RE.match(link["url"]): + repos.add(match.group(1).removesuffix(".git").lower()) + return repos + + +def prior_verdicts() -> dict[str, str]: + """Repo -> earlier verdict. The ledger is append-only, so its last row per repo wins; a maintainer rejection outranks both.""" + prior = {} + if LEDGER.exists(): + for line in LEDGER.read_text().splitlines()[1:]: + repo, date, reason = line.split("\t", 2) + prior[repo.lower()] = f"screened out {date}: {reason}" + register = REGISTER.read_text().splitlines() + start = register.index("## Rejected Additions") + for line in register[start + 1 :]: + if line.startswith("## "): + break + if match := REJECTED_LINE_RE.match(line): + prior[match.group(1).lower()] = f"rejected {match.group(2)}" + return prior + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--full", action="store_true", help="rejudge stars already screened out or rejected") + args = parser.parse_args() + + now = datetime.now(UTC) + login = gh("api", "user", "--jq", ".login").strip().lower() + listed = listed_repos() + prior = prior_verdicts() + + stars: list[Star] = [json.loads(line) for line in gh("api", "--paginate", "user/starred?per_page=100", "--jq", ".[]").splitlines()] + dropped = Counter() + print("repo\tlanguage\tstars\tpushed\ttopics\tdescription\tprior") + for star in stars: + repo = star["full_name"].lower() + if repo in listed: + dropped["already listed"] += 1 + elif star["owner"]["login"].lower() == login: + dropped["own repo"] += 1 + elif star["archived"]: + dropped["archived"] += 1 + elif datetime.fromisoformat(star["pushed_at"]) < now - timedelta(days=365): + dropped["no push in 12 months"] += 1 + elif datetime.fromisoformat(star["created_at"]) > now - timedelta(days=30): + dropped["created under a month ago"] += 1 + elif repo in prior and not args.full: + dropped["judged before"] += 1 + else: + description = " ".join((star["description"] or "").split()) + print("\t".join([star["full_name"], star["language"] or "-", str(star["stargazers_count"]), star["pushed_at"][:10], ",".join(star["topics"]), description, prior.get(repo, "")])) + + print(f"{len(stars)} stars; {len(stars) - dropped.total()} candidates", file=sys.stderr) + for reason, count in dropped.most_common(): + print(f" dropped {count}: {reason}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/docs/audit-logs.md b/docs/audit-logs.md index ae95952e..ad525f29 100644 --- a/docs/audit-logs.md +++ b/docs/audit-logs.md @@ -67,6 +67,10 @@ These entries sit past the 12-month activity requirement without an override. Ea - schedule -- kept 2026-08-16, recorded 2026-10-02. Last commit 2024-05-25. - sortedcontainers +## Rejected Additions + +A proposed entry the maintainer turned down leaves no commit, so it is recorded here with the evidence at decision time, keyed by its GitHub `owner/repo`. Rejudge one only on new evidence. + ## Last Audited An audit that changes nothing in a section leaves no commit, so its date is recorded here, one line per section, replaced on each re-audit.