From 32ac8c9b3c2c89d1591c20fcd8fcd950f8427289 Mon Sep 17 00:00:00 2001 From: Vinta Chen Date: Fri, 16 Oct 2026 19:18:59 +0800 Subject: [PATCH] fix: reopen parked projects in incremental star scans Parking a project during a full review left its previous screen-out or rejection active, so incremental scans hid it instead of revisiting it. Co-Authored-By: Codex --- .claude/skills/scan-github-stars/SKILL.md | 6 +- .../scripts/scan_github_stars.py | 13 +- website/tests/test_scan_github_stars.py | 118 ++++++++++++++++++ 3 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 website/tests/test_scan_github_stars.py diff --git a/.claude/skills/scan-github-stars/SKILL.md b/.claude/skills/scan-github-stars/SKILL.md index e2622f18..94ce9adf 100644 --- a/.claude/skills/scan-github-stars/SKILL.md +++ b/.claude/skills/scan-github-stars/SKILL.md @@ -7,7 +7,7 @@ disable-model-invocation: true # Scan GitHub 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. +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`, rejected under `docs/audit-logs.md` "Rejected Additions", or parked for another review. Judgment belongs to `audit-the-list` and the rules to CONTRIBUTING.md; this skill only sources candidates and records outcomes. ## 1. Scan @@ -30,7 +30,7 @@ Run `preview-verdicts` over all groups: survivors with kind `candidate` and verd ## 5. Record - **Added**: follow `audit-the-list` step 5 for the README change, one commit per entry. When the entry links a different repo than the star (a Python binding or client of a starred engine), also append a ledger row `already listed as ()` for the star, since the script only recognizes a star whose own repo is linked. -- **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 row the maintainer parks gets no record, so it resurfaces next run. +- **Rejected**: a candidate with verdict `reject` gets one line under "Rejected Additions": `- owner/repo -- yyyy-mm-dd, /month (): .`, replacing its earlier line. This exact rejection takes precedence over a parked ledger row. +- **Parked**: leave listed entries in README.md. An unlisted repo with no prior decision needs no record. To reopen one screened out or rejected before, remove its exact "Rejected Additions" line and append `owner/repoyyyy-mm-ddparked` to the ledger. Its earlier screen-out rows stay as history. The marker also exempts this repo from an `owner/*` rejection; leave that organization line in place for its other repos. A later screen-out or exact rejection excludes the repo again. A new organization-wide rejection must explicitly resolve any parked exceptions it is intended to cover. 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-github-stars/scripts/scan_github_stars.py b/.claude/skills/scan-github-stars/scripts/scan_github_stars.py index c0b5fc4e..23e09941 100644 --- a/.claude/skills/scan-github-stars/scripts/scan_github_stars.py +++ b/.claude/skills/scan-github-stars/scripts/scan_github_stars.py @@ -60,13 +60,16 @@ def listed_repos() -> set[str]: 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 = {} +def prior_verdicts() -> dict[str, str | None]: + """The last ledger row wins; `parked` reopens a repo. An exact registered rejection still wins.""" + prior: dict[str, str | None] = {} 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}" + if reason == "parked": + prior[repo.lower()] = None + else: + prior[repo.lower()] = f"screened out {date}: {reason}" register = REGISTER.read_text().splitlines() start = register.index("## Rejected Additions") for line in register[start + 1 :]: @@ -92,7 +95,7 @@ def main() -> None: print("repo\tlanguage\tstars\tpushed\ttopics\tdescription\tprior") for star in stars: repo = star["full_name"].lower() - verdict = prior.get(repo) or prior.get(f"{star['owner']['login'].lower()}/*") + verdict = prior[repo] if repo in prior else prior.get(f"{star['owner']['login'].lower()}/*") if repo in listed: dropped["already listed"] += 1 elif star["owner"]["login"].lower() == login: diff --git a/website/tests/test_scan_github_stars.py b/website/tests/test_scan_github_stars.py new file mode 100644 index 00000000..a3b5b35c --- /dev/null +++ b/website/tests/test_scan_github_stars.py @@ -0,0 +1,118 @@ +import importlib.util +import json +from datetime import UTC, datetime, timedelta +from pathlib import Path + +import pytest + + +@pytest.fixture +def scan(tmp_path, monkeypatch, capsys): + script = Path(__file__).resolve().parents[2] / ".claude/skills/scan-github-stars/scripts/scan_github_stars.py" + spec = importlib.util.spec_from_file_location("scan_github_stars", script) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + monkeypatch.setattr(module, "LEDGER", tmp_path / "starred-scan.tsv") + monkeypatch.setattr(module, "REGISTER", tmp_path / "audit-logs.md") + monkeypatch.setattr(module, "listed_repos", lambda: {"example/listed"}) + module.LEDGER.write_text("repo\tdate\treason\n") + module.REGISTER.write_text("## Rejected Additions\n\n## Last Audited\n") + now = datetime.now(UTC) + star = { + "full_name": "Example/Project", + "owner": {"login": "Example"}, + "archived": False, + "pushed_at": now.isoformat(), + "created_at": (now - timedelta(days=100)).isoformat(), + "language": "Python", + "stargazers_count": 100, + "topics": [], + "description": "A candidate", + } + + def run(*args, stars=None): + monkeypatch.setattr(module.sys, "argv", [str(script), *args]) + monkeypatch.setattr(module, "gh", lambda *args: "maintainer" if args[1] == "user" else "\n".join(json.dumps(s) for s in (stars or [star]))) + module.main() + return capsys.readouterr() + + return module, star, run + + +@pytest.mark.parametrize("rejected", [False, True]) +def test_full_review_can_park_a_previous_verdict(scan, rejected): + module, _, run = scan + history = "repo\tdate\treason\nExample/Project\t2026-10-16\tno matching use case\n" + module.LEDGER.write_text(history) + if rejected: + module.REGISTER.write_text("## Rejected Additions\n- example/project -- 2026-10-16: no matching use case\n") + + assert "Example/Project" not in run().out + review = run("--full").out + assert "Example/Project" in review + assert ("rejected" if rejected else "screened out") in review + + module.LEDGER.write_text(history + "example/project\t2026-10-16\tparked\n") + if rejected: + module.REGISTER.write_text("## Rejected Additions\n") + + assert "Example/Project" in run().out + assert module.LEDGER.read_text().startswith(history) + + +@pytest.mark.parametrize("decision", ["screen", "reject"]) +def test_later_decision_on_same_day_excludes_parked_repo(scan, decision): + module, _, run = scan + history = "repo\tdate\treason\nexample/project\t2026-10-16\tparked\n" + module.LEDGER.write_text(history) + assert "Example/Project" in run().out + + if decision == "screen": + module.LEDGER.write_text(history + "EXAMPLE/PROJECT\t2026-10-16\tstill no use case\n") + else: + module.REGISTER.write_text("## Rejected Additions\n- EXAMPLE/PROJECT -- 2026-10-16: still no use case\n") + + assert "Example/Project" not in run().out + assert "still no use case" in run("--full").out + + +def test_parking_does_not_override_exact_registered_rejection(scan): + module, _, run = scan + module.LEDGER.write_text("repo\tdate\treason\nexample/project\t2026-10-16\tparked\n") + module.REGISTER.write_text("## Rejected Additions\n- Example/Project -- 2026-10-16: maintainer rejection\n") + + assert "Example/Project" not in run().out + assert "maintainer rejection" in run("--full").out + + +def test_parking_reopens_one_repo_from_rejected_organization(scan): + module, star, run = scan + module.REGISTER.write_text("## Rejected Additions\n- EXAMPLE/* -- 2026-10-16: maintainer rejection\n") + peer = star | {"full_name": "Example/Other"} + assert "Example/Project" not in run().out + + module.LEDGER.write_text("repo\tdate\treason\nexample/project\t2026-10-16\tparked\n") + result = run(stars=[star, peer]).out + assert "Example/Project" in result + assert "Example/Other" not in result + + module.REGISTER.write_text(module.REGISTER.read_text() + "- example/project -- 2026-10-16: rejected again\n") + assert "Example/Project" not in run().out + assert "rejected again" in run("--full").out + + +def test_parking_keeps_other_exclusions(scan): + module, star, run = scan + now = datetime.now(UTC) + changes = [ + {"full_name": "example/listed"}, + {"owner": {"login": "maintainer"}}, + {"archived": True}, + {"pushed_at": (now - timedelta(days=400)).isoformat()}, + {"created_at": now.isoformat()}, + ] + for change in changes: + candidate = star | change + module.LEDGER.write_text(f"repo\tdate\treason\n{candidate['full_name']}\t2026-10-16\tparked\n") + assert candidate["full_name"] not in run(stars=[candidate]).out