feat: add scan-stars skill and Rejected Additions register

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen
2026-10-16 14:12:14 +08:00
co-authored by Claude
parent 2ef2de18f0
commit 11f5e2ceab
5 changed files with 159 additions and 2 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 "<project name>" --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 "<project name>" --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.
+36
View File
@@ -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 `repo<TAB>yyyy-mm-dd<TAB>reason` 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 <names, each with its GitHub repo> for the <section — subcategory> 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, <downloads>/month (<source>): <reason>.`, 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.
@@ -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()
+4
View File
@@ -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.