actions-queue-status: count a PMC's active repos against its estate

A PMC row said how many of its repos had jobs, which is only half a fact: four
busy repos read very differently for a PMC of four than for a PMC of
forty-seven, and the row gave no way to tell those apart.

The Repos column now reads active / total, counting the repos the sweep
actually covered -- so a --repos-file run measures against that file rather
than against an organisation it never looked at. group_by_pmc() takes that
population as an argument and falls back to the active repos when it has none,
which is all a caller without a wider list can honestly claim. The TOTAL row
carries the same ratio, 60 / 1258 for a full org sweep, and the CSV gains a
repos_total column beside repos.

Generated-by: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01LefMKj1fh7D2RxwNC6A4L5
diff --git a/README.md b/README.md
index 0048d49..ab0c10f 100644
--- a/README.md
+++ b/README.md
@@ -615,7 +615,7 @@
 | `--suites N` | Check suites read per commit (default: 5). |
 | `--workers N` | Batched queries in flight (default: 3). |
 | `--top N` | Rows shown per table (default: 25). |
-| `--by-pmc` | Group rows by PMC — the repository name's prefix before the first hyphen. |
+| `--by-pmc` | Group rows by PMC — the repository name's prefix before the first hyphen. The `Repos` column reads active / total. |
 | `--include-archived` | Include archived repositories. |
 | `--repos-file PATH` | Skip discovery and read repository names from a file; `#` comment lines are ignored. |
 | `--save-repos PATH` | Write the discovered repository list to a file, sorted and with a header. |
@@ -696,16 +696,23 @@
 
 ```text
 Sorted by RUNNING jobs
-┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
-┃ PMC     ┃ Queued ┃ Running ┃ Repos ┃ Repositories                   ┃
-┡━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
-│ airflow │     13 │     133 │     2 │ airflow, airflow-client-python │
-│ spark   │      1 │      83 │     1 │ spark                          │
-├─────────┼────────┼─────────┼───────┼────────────────────────────────┤
-│ TOTAL   │     14 │     216 │     3 │ 2 PMCs                         │
-└─────────┴────────┴─────────┴───────┴────────────────────────────────┘
+┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+┃ PMC        ┃ Queued ┃ Running ┃     Repos ┃ Repositories                   ┃
+┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
+│ airflow    │     13 │     133 │    2 / 10 │ airflow, airflow-client-python │
+│ fineract   │      0 │      96 │     1 / 8 │ fineract                       │
+│ datafusion │     21 │      49 │    2 / 12 │ datafusion, datafusion-comet   │
+├────────────┼────────┼─────────┼───────────┼────────────────────────────────┤
+│ TOTAL      │    330 │     881 │ 60 / 1258 │ 49 PMCs                        │
+└────────────┴────────┴─────────┴───────────┴────────────────────────────────┘
 ```
 
+The `Repos` column reads *active / total*: how many of the PMC's repositories have jobs right
+now, out of every repository of theirs the sweep covered. Four busy repositories mean something
+different for a PMC of four than for a PMC of forty-seven, and the denominator is what says
+which one you are looking at. It counts the repositories the sweep actually ran over, so a
+`--repos-file` run measures against that file rather than the whole organisation.
+
 A repository's PMC is the text before the first hyphen in its name, and the whole name when there
 is no hyphen — so `spark`, `spark-connect-go` and `spark-docker` group under `spark`. That is the
 same rule [`--pmc` uses in `actions-audit.py`](#how-pmc-filtering-works), so the two scripts agree
diff --git a/utils/actions-queue-status.py b/utils/actions-queue-status.py
index 4dd1d8a..ce8988c 100644
--- a/utils/actions-queue-status.py
+++ b/utils/actions-queue-status.py
@@ -170,6 +170,7 @@
     "queued_jobs",
     "running_jobs",
     "repos",
+    "repos_total",
     "open_prs",
     "runs_awaiting_approval",
     "source",
@@ -769,8 +770,15 @@
     return repo.split("/")[-1].split("-", 1)[0]
 
 
-def group_by_pmc(rows: list[dict]) -> list[dict]:
-    """Aggregate per-repo summaries into one row per PMC."""
+def group_by_pmc(rows: list[dict], population: list[str] | None = None) -> list[dict]:
+    """Aggregate per-repo summaries into one row per PMC.
+
+    `population` is every repo the sweep covered, active or not. It is what makes the
+    active count mean something: four busy repos is a different picture for a PMC of
+    four than for a PMC of forty-seven. Without it a PMC's total is just its active
+    repos, which is what a caller with no wider list can honestly say.
+    """
+    totals: dict[str, int] = collections.Counter(pmc_of(name) for name in population or [])
     groups: dict[str, dict] = {}
     for row in rows:
         name = row["repo"].split("/")[-1]
@@ -794,6 +802,10 @@
         group["sources"].add(row.get("source", "graphql"))
     for group in groups.values():
         group["repos"].sort()
+        # A repo with jobs is by definition part of its PMC's estate, so the count can
+        # never be smaller than what was found active -- even if the population somehow
+        # did not list it.
+        group["repos_total"] = max(totals.get(group["pmc"], 0), len(group["repos"]))
         # A PMC counted partly each way is neither: say so rather than pick a winner.
         group["source"] = group["sources"].pop() if len(group["sources"]) == 1 else "mixed"
         del group["sources"]
@@ -827,6 +839,7 @@
         row["queued_jobs"],
         row["running_jobs"],
         len(row["repos"]),
+        row["repos_total"],
         row["open_prs"],
         row["runs_awaiting_approval"],
         row["source"],
@@ -891,7 +904,14 @@
             writer.writerow(pmc_csv_row(row) if by_pmc else csv_row(row))
         total_row = ["TOTAL", totals["queued_jobs"], totals["running_jobs"]]
         if by_pmc:
-            total_row += [totals["repos_active"], "", "", "", f"pmcs={totals['pmcs_active']}"]
+            total_row += [
+                totals["repos_active"],
+                totals["repos_with_actions"],
+                "",
+                "",
+                "",
+                f"pmcs={totals['pmcs_active']}",
+            ]
         else:
             total_row += ["", "", "", f"repos={totals['repos_active']}"]
         writer.writerow(total_row)
@@ -923,7 +943,7 @@
                 row["pmc"],
                 str(row["queued_jobs"]),
                 str(row["running_jobs"]),
-                str(len(row["repos"])),
+                f"{len(row['repos'])} / {row['repos_total']}",
                 escape(", ".join(row["repos"])[:50]),
             )
             continue
@@ -955,7 +975,7 @@
         "[bold]TOTAL[/]",
         f"[bold]{totals['queued_jobs']}[/]",
         f"[bold]{totals['running_jobs']}[/]",
-        f"[bold]{totals['repos_active']}[/]" if by_pmc else "",
+        f"[bold]{totals['repos_active']} / {totals['repos_with_actions']}[/]" if by_pmc else "",
         f"[dim]{active} {unit}{'' if active == 1 else 's'}[/]",
     )
     if len(rows) > top:
@@ -1119,7 +1139,7 @@
         reporter.log("  No REST re-count needed — the GraphQL sample covered every repo", "green")
 
     active = [row for row in results if row["queued_jobs"] or row["running_jobs"]]
-    grouped = group_by_pmc(active)
+    grouped = group_by_pmc(active, repos)
     if args.by_pmc:
         key = "pmc"
         rows = grouped