diff --git a/.claude/skills/scan-github-stars/SKILL.md b/.claude/skills/scan-github-stars/SKILL.md index 5b45b059..c84b6fe9 100644 --- a/.claude/skills/scan-github-stars/SKILL.md +++ b/.claude/skills/scan-github-stars/SKILL.md @@ -10,13 +10,13 @@ A star scan turns the maintainer's GitHub stars into proposed additions. Every s ## 1. Scan -Run `uv run python .claude/skills/scan-github-stars/scripts/scan_github_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. +Run `uv run python .claude/skills/scan-github-stars/scripts/scan_github_stars.py`, adding `--full` when the argument is `full`. With no ledger yet, a plain run already covers every star. Print the drop and prune 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. +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. Only append rows: the script reads the last row per repo, and it prunes rows for repos the maintainer no longer stars. 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 diff --git a/.claude/skills/scan-github-stars/scripts/scan_github_stars.py b/.claude/skills/scan-github-stars/scripts/scan_github_stars.py index 23e09941..faf025cc 100644 --- a/.claude/skills/scan-github-stars/scripts/scan_github_stars.py +++ b/.claude/skills/scan-github-stars/scripts/scan_github_stars.py @@ -6,7 +6,8 @@ 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. +`prior` column. Ledger rows for repos no longer starred are pruned each run. +Prints survivors as TSV on stdout, drop and prune counts on stderr. """ import argparse @@ -80,6 +81,16 @@ def prior_verdicts() -> dict[str, str | None]: return prior +def prune_ledger(starred: set[str]) -> int: + if not LEDGER.exists(): + return 0 + header, *rows = LEDGER.read_text().splitlines() + kept = [row for row in rows if row.split("\t", 1)[0].lower() in starred] + if len(kept) < len(rows): + LEDGER.write_text("\n".join([header, *kept]) + "\n") + return len(rows) - len(kept) + + 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") @@ -91,6 +102,7 @@ def main() -> None: prior = prior_verdicts() stars: list[Star] = [json.loads(line) for line in gh("api", "--paginate", "user/starred?per_page=100", "--jq", ".[]").splitlines()] + pruned = prune_ledger({star["full_name"].lower() for star in stars}) dropped = Counter() print("repo\tlanguage\tstars\tpushed\ttopics\tdescription\tprior") for star in stars: @@ -115,6 +127,8 @@ def main() -> None: 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 pruned: + print(f" pruned {pruned} ledger rows for repos no longer starred", file=sys.stderr) if __name__ == "__main__": diff --git a/website/tests/test_scan_github_stars.py b/website/tests/test_scan_github_stars.py index a3b5b35c..15b97534 100644 --- a/website/tests/test_scan_github_stars.py +++ b/website/tests/test_scan_github_stars.py @@ -102,6 +102,15 @@ def test_parking_reopens_one_repo_from_rejected_organization(scan): assert "rejected again" in run("--full").out +def test_unstarred_repos_are_pruned_from_ledger(scan): + module, _, run = scan + kept = "repo\tdate\treason\nexample/project\t2026-10-16\tno use case\n" + module.LEDGER.write_text(kept + "gone/repo\t2026-10-16\tno use case\ngone/repo\t2026-10-17\tparked\n") + + assert "pruned 2" in run().err + assert module.LEDGER.read_text() == kept + + def test_parking_keeps_other_exclusions(scan): module, star, run = scan now = datetime.now(UTC)