This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new bc2f163 feat(pr-management-stats): trends-over-time, CODEOWNERS
panel, waiting-bucket split (#230)
bc2f163 is described below
commit bc2f1630222bced5ee560ca1c3b71d0527314211
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed May 20 02:33:41 2026 +0100
feat(pr-management-stats): trends-over-time, CODEOWNERS panel,
waiting-bucket split (#230)
* feat(pr-management-stats): trends-over-time, CODEOWNERS panel,
waiting-bucket split
Three related additions to the pr-management-stats spec, all derived from
running the skill on apache/airflow (484 open PRs, 6-week cutoff) and
finding
gaps in what the existing dashboard surfaces.
## 1. Trends over time (panel 3b — 5 sub-panels)
Adds a "Trends over time" section between "What needs attention" and
"Closure velocity" with 5 inline-SVG line-chart sub-panels:
- **Open backlog over time** — end-of-week snapshot of total open count,
showing whether the queue is growing/shrinking week-over-week
(createdAt ≤ window.end AND (still open OR closedAt > window.end)).
- **PRs opened by author class** — per-week `opened` count split by author
association (FIRST_TIME / CONTRIBUTOR / MAINTAINER). Surfaces shifts in
inflow composition.
- **Triage velocity** — count of PRs whose first QC marker comment fell in
each week, split by AI-drafted vs manually-typed QC. Surfaces triage
throughput.
- **Triage coverage rate by week opened** — for PRs opened in each week,
% that ever received `is_engaged` maintainer engagement. A declining
trend is an early-warning indicator for backlog growth.
- **Ready-for-review queue size (cumulative)** — total ready-queue size
end-of-week, single line, all areas combined. (Per-area version is the
existing panel 6.)
Each chart pairs with a per-week table for precise numbers. The
classify-and-aggregate path needs no new fetches — all computed from the
already-fetched open + closed datasets + the ready-label timeline.
Two of the five panels (triage velocity, triage coverage rate) have a
documented data caveat: the closed-PR fetch caps `comments(last:25)` per
PR, so older outstanding triage markers on chatty PRs are missed. Surface
this in the panel.
## 2. CODEOWNERS panel (8b)
New panel: "Ready-for-review queue by CODEOWNER". For each entry in the
project's `.github/CODEOWNERS`, count how many currently-ready PRs that
owner is responsible for (file-pattern match, GitHub's last-match-wins).
A PR with multiple owners contributes once per owner.
Two extras in the panel:
- **Waiting-for-author column** — count of the owner's ready PRs where
that owner has personally posted an unresponded comment. Surfaces who
the queue is *waiting on the author to respond to* per maintainer.
- **"No CODEOWNERS match" row** — currently-ready PRs whose changed files
match no rule. Surfaces paths where the CODEOWNERS file could be
extended.
Needs one new fetch — `pullRequest.files(first:100)` aliased per ready
PR — ~8 GraphQL calls for ~150 ready PRs. Cache per `(pr_number,
head_sha)`. Skip the panel entirely when `.github/CODEOWNERS` is absent.
## 3. Triage funnel split (panel 9 — 4 → 5 cards)
Splits the existing "Waiting for author" hero card into two mutually-
exclusive buckets:
- **Waiting: AI-triage comment only** — author hasn't responded AND the
most recent unresponded maintainer comment is AI-drafted, AND no
manual maintainer comment is unresponded.
- **Waiting: author response to maintainer** — at least one *manual*
(non-AI) maintainer comment is unresponded. Higher priority — this is
the real "author owes a maintainer a reply" signal.
Implementation uses two new classify-time predicates
(`waiting_for_ai_only` / `waiting_for_manual_response`) defined in terms
of the most-recent author activity timestamp + the AI-attribution footer
substring already used by `is_ai_triaged`.
The funnel grid grows 4 → 5 cards; the existing 4 funnel cards (Ready /
Responded / Stalest / Velocity) are reorganised in the spec text as
well — the new layout has Ready / Responded / AI-only / Manual /
Untriaged in precedence order.
## Why upstream now
All three improvements proved useful on apache/airflow's queue during
maintenance this week. They're project-agnostic (CODEOWNERS is a GitHub
feature, AI-attribution footer is the framework's own substring, trends
use the same weekly window pattern the existing panels already use), so
upstreaming benefits any adopter.
The three changes are independent — a reviewer who wants to take only a
subset can drop the relevant commit / file edits. They were bundled here
because they all touch the same skill and were developed together.
* fix(pr-management-stats): correct fetch.md anchor and close fences cleanly
- Two cross-file links pointed at fetch.md#pr-changed-files; the
actual heading slug is pr-changed-files-codeowners-panel.
- Pre-existing ```text...```text fence pairs in aggregate.md
were tolerated until this PR added properly-closed (```)
fences. The first bare close swallowed an earlier leniently-open
fence into one giant code block, hiding the
Opened-vs-closed-weekly-buckets and Ready-for-review-trend
headings from MD051's same-file anchor lookup. Closing the older
fences with bare ``` matches the dominant convention across
the skills and restores heading visibility.
Generated-by: Claude Code (Opus 4.7)
---
.claude/skills/pr-management-stats/SKILL.md | 47 +++++-
.claude/skills/pr-management-stats/aggregate.md | 202 +++++++++++++++++++++++-
.claude/skills/pr-management-stats/classify.md | 53 +++++++
.claude/skills/pr-management-stats/fetch.md | 40 +++++
.claude/skills/pr-management-stats/render.md | 75 +++++++--
5 files changed, 399 insertions(+), 18 deletions(-)
diff --git a/.claude/skills/pr-management-stats/SKILL.md
b/.claude/skills/pr-management-stats/SKILL.md
index a922096..fa09a82 100644
--- a/.claude/skills/pr-management-stats/SKILL.md
+++ b/.claude/skills/pr-management-stats/SKILL.md
@@ -269,6 +269,42 @@ Sort areas by score descending; render the top 8
(filtering areas with < 3 contr
---
+## Step 5g — Compute trend snapshots (backlog / inflow / triage velocity /
coverage)
+
+Pure function of the union of open + closed-since-cutoff PR sets. No
additional network beyond what Steps 1, 3, and 5d already fetched.
+
+For each of the same six weekly windows, compute (see
[`aggregate.md`](aggregate.md) for each spec):
+
+- **Open backlog** — count of PRs that were *open at end-of-week-`w`*
(createdAt ≤ window.end AND (currently open OR closedAt > window.end)).
+- **PRs opened by author class** — partition the `opened` per-week count by
`authorAssociation` (FIRST_TIME / CONTRIBUTOR / MAINTAINER).
+- **Triage velocity** — count of PRs whose *first* QC-marker comment fell in
the window, split by AI-drafted vs manual.
+- **Triage coverage rate** — for PRs opened in the window, percentage where
`is_engaged` is true.
+- **Ready-queue size cumulative** — count of currently-ready PRs whose
`labeled_at` ≤ window.end (single line, all areas combined; the per-area
version is from Step 5d).
+
+These five series feed the dashboard's "Trends over time" section (panel 3b).
+
+⚠ Triage velocity and triage coverage rate are limited by the
`comments(last:N)` cap on the closed-PR fetch (N=25): older outstanding triage
markers on chatty PRs are missed. Annotate the panels with the caveat.
+
+---
+
+## Step 5h — Compute CODEOWNERS responsibility (optional)
+
+Skip if `.github/CODEOWNERS` (and the fallback locations described in
+[`fetch.md#reading-githubcodeowners`](fetch.md#reading-githubcodeowners)) are
absent.
+
+Otherwise:
+
+1. Parse the file into `(pattern, [owners])` rules in declaration order. Owner
tokens are stripped of leading `@`.
+2. For each currently-ready PR, fetch its changed file paths (
+
[`fetch.md#pr-changed-files-codeowners-panel`](fetch.md#pr-changed-files-codeowners-panel))
— one extra GraphQL pass, ~8 calls for ~150 ready PRs.
+3. For each file, apply the rules and take the **last** matching rule's
owners. Union per PR.
+4. Per owner, count distinct PRs in their union.
+5. **Waiting subcount**: for each (owner, PR) pair, check whether the owner
has posted any comment on the PR (from the comments fetched in Step 1) such
that the author has not commented or pushed since. Count distinct PRs per owner.
+
+Feeds the dashboard's "Ready-for-review queue by CODEOWNER" panel (8b). See
[`aggregate.md#ready-for-review-queue-by-codeowner`](aggregate.md#ready-for-review-queue-by-codeowner).
+
+---
+
## Step 6 — Render dashboard
Render the maintainer dashboard per the layout in
[`render.md#dashboard-layout`](render.md#dashboard-layout):
@@ -276,12 +312,15 @@ Render the maintainer dashboard per the layout in
[`render.md#dashboard-layout`]
1. **Context line** — repo, open count, cutoff, viewer, timestamp.
2. **Hero cards (4)** — health rating, total open, ready count,
untriaged-non-draft count.
3. **What needs attention** — recommendation list from Step 5a.
-4. **Closure velocity** — weekly bar chart from Step 5b.
+3b. **Trends over time** — 5 inline-SVG line charts (open backlog, PRs opened
by author class, ready-queue cumulative, triage velocity, triage coverage
rate). Each chart sits above a precise per-week table.
+4. **Closure velocity** — weekly line chart + stacked-bar table from Step 5b.
5. **Opened vs closed momentum** — line chart from Step 5c.
-6. **Ready-for-review trend** — multi-line chart from Step 5d (top areas).
-7. **Closed by triage reason** — stacked-bar chart from Step 5e.
+6. **Ready-for-review trend by top areas** — multi-line chart from Step 5d.
+7. **Closed by triage reason** — line chart + stacked-bar table from Step 5e.
8. **Pressure by area** — top areas from Step 5f.
-9. **Triage funnel** — coverage %, response rate %, stalest bucket, this-week
velocity.
+8b. **Ready-for-review queue by CODEOWNER** — per-owner Ready +
Waiting-for-author table (skip if `.github/CODEOWNERS` absent). See
[`aggregate.md#ready-for-review-queue-by-codeowner`](aggregate.md#ready-for-review-queue-by-codeowner).
+9. **Triage funnel** — 5-column hero grid: Ready / Responded / Waiting
(AI-only) / Waiting (manual maintainer response) / Not yet triaged. The
"Waiting" cards are mutually exclusive — see
[`classify.md#waiting-sub-states--ai-only-vs-maintainer-response`](classify.md#waiting-sub-states--ai-only-vs-maintainer-response).
+9b. **Triager activity** — per-maintainer per-week PR-engagement counts.
10. **Detailed tables** (collapsed by default):
1. **Triaged PRs — Final State since `<cutoff>`** — one row per area where
`Triaged Total > 0`.
2. **Triaged PRs — Still Open** — one row per area where `Total > 0`, plus
the `TOTAL` row.
diff --git a/.claude/skills/pr-management-stats/aggregate.md
b/.claude/skills/pr-management-stats/aggregate.md
index c5b3cb9..25764aa 100644
--- a/.claude/skills/pr-management-stats/aggregate.md
+++ b/.claude/skills/pr-management-stats/aggregate.md
@@ -160,7 +160,7 @@ For each `w` in `0..5`:
```text
window_end = now - w * 7 days
window_start = window_end - 7 days
-```text
+```
`w == 0` is the current week (oldest = `<now> - 7d`, newest = `<now>`). `w ==
5` is the oldest week in the chart.
@@ -203,7 +203,7 @@ Below the chart, print two summary lines computed from
these buckets:
```text
Net delta this week: ±<N> PRs (<opened> opened - <closed_total> closed)
6-week net: ±<N> PRs (<sum_opened> opened - <sum_closed> closed) — backlog
<growing|shrinking|stable>
-```text
+```
`stable` is used when `|6-week net| < 10`. Anything bigger reads as a real
direction.
@@ -225,7 +225,7 @@ For each top area `a` and each weekly bucket `w` in `0..5`:
```text
ready_count[a][w] = count of currently-ready PRs in area a where labeled_at <=
w.end
-```text
+```
This is a **cumulative** count, not a per-week delta — by construction it's
monotonically non-decreasing because PRs that lose the label drop out of the
*currently-ready* set entirely (they're not in this dataset).
@@ -237,7 +237,7 @@ Below the chart, print a one-line per-area summary:
providers: 46 ready (+8 in last 7d)
task-sdk: 40 ready (+5 in last 7d)
…
-```text
+```
The "+N in last 7d" is the count of PRs labeled within the last week —
surfaces whether the queue is growing faster than it can be reviewed.
@@ -273,7 +273,7 @@ Below the bars, print three summary numbers:
```text
6-week breakdown: <merged_total> merged · <closed_after_responded>
engaged-then-closed · <closed_no_response> sweep-closed · <closed_no_triage>
no-triage
-```text
+```
This panel makes the *quality* of closures visible — the velocity panel says
"how many", this panel says "of what type".
@@ -363,6 +363,198 @@ zero engagement in the window are excluded from the panel.
---
+## Backlog over time (per-week snapshots)
+
+The dashboard's "Open backlog over time" panel shows how the total open-PR
count
+moved at the end of each of the last 6 weeks.
+
+For each weekly bucket `w` in `0..5`, count PRs that were *open at
end-of-week-`w`*:
+
+```text
+open_count[w] = count of non-bot PRs P where:
+ P.createdAt <= w.end
+ AND (P is currently open
+ OR (P is closed AND P.closedAt > w.end))
+```
+
+PRs closed *before* the cutoff are absent from both datasets, so for `w` near
the
+cutoff edge the snapshot is a slight under-count. Note the caveat in the
+dashboard.
+
+Below the chart, print:
+
+```text
+6-week trend: <delta> open PRs (start: <open_count[0]>, end: <open_count[5]>)
+```
+
+Colour the delta red when growth is >10, green when shrinkage is >10, grey
+otherwise.
+
+---
+
+## PRs opened by author class (per-week)
+
+The "PRs opened by author class" panel splits the `opened` per-week count from
+[`#opened-vs-closed-weekly-buckets`](#opened-vs-closed-weekly-buckets) by the
PR
+author's GitHub association:
+
+| Bucket | `authorAssociation` |
+|---|---|
+| `first_time` | `FIRST_TIME_CONTRIBUTOR`, `FIRST_TIMER` |
+| `contributor` | `CONTRIBUTOR`, `NONE`, anything else not below |
+| `collab` (maintainer) | `OWNER`, `MEMBER`, `COLLABORATOR` |
+
+Bot-authored PRs are excluded
+(per [`classify.md#is_bot`](classify.md#is_bot--author-is-a-recognised-bot)).
+
+For each `w` in `0..5`, count PRs whose `createdAt` falls in the window,
+bucketed by association. Render as a multi-line chart with three lines (or
+stacked-bar if multi-line is too crowded).
+
+Surfaces shifts in inflow composition — a sudden rise in `first_time` is signal
+for triage-attention demand; a drop in `contributor` may indicate community
+drift.
+
+---
+
+## Triage velocity (per-week)
+
+The "Triage velocity" panel counts PRs whose **first** Quality-Criteria marker
+comment fell in each week. Distinct from the closure-by-reason chart (which
+counts when PRs *closed*) — this counts when triage *happened*.
+
+For each PR (open OR closed-since-cutoff), `triage_comment_ts` is the timestamp
+of its *earliest* maintainer comment containing the `Pull Request quality
+criteria` marker. Bucket by week of `triage_comment_ts` (skip PRs that were
+never triaged).
+
+Split each week's count by AI vs manual:
+
+| Series | Definition |
+|---|---|
+| `ai` | The triage-comment also contains the
[`is_ai_triaged`](classify.md#is_ai_triaged--ai-assisted-triage) footer
substring |
+| `non_ai_qc` | Triage comment present but no AI footer (a maintainer pasted
the QC link manually) |
+
+⚠ **Data caveat**: the closed-PR fetch caps comments at `last:25` per PR. PRs
+whose first triage marker is older than the 25 most recent comments are
+missed — so older weeks systematically under-count. Note this in the dashboard.
+
+---
+
+## Triage coverage rate by week opened
+
+The "Triage coverage rate" panel shows the percentage of PRs opened in each
+week that ever received any maintainer engagement (the broader
+[`is_engaged`](classify.md#is_engaged--de-facto-triaged) predicate — includes
+label-adds + comments + reviews).
+
+For each `w` in `0..5`:
+
+```text
+opened[w] = count of PRs created in window w (open or closed)
+engaged[w] = subset of opened[w] where is_engaged(p) is true
+pct[w] = round(100 * engaged[w] / opened[w])
+```
+
+Render as a single-line chart with y-axis capped at 100. Colour the line green
+when ≥ 50%, amber for 25-49%, red below 25% (or colour each data point
+individually).
+
+A declining trend means triage capacity is falling behind inflow — usually a
+precursor to backlog growth.
+
+⚠ Same data caveat as triage velocity — `is_engaged` depends on the comment
+fetch, so older weeks under-count.
+
+---
+
+## Ready-for-review queue size (cumulative over time)
+
+A second view on the ready queue — the **total** queue size cumulatively at end
+of each week, single line for all areas combined. (The per-area version is the
+chart from
[`#ready-for-review-trend-by-top-areas`](#ready-for-review-trend-by-top-areas).)
+
+For each currently-`ready for maintainer review` PR, use its most recent
+`LabeledEvent` timestamp (from the
+[`ready-label-timeline`](fetch.md#ready-label-timeline) fetch). For each `w`
+in `0..5`:
+
+```text
+ready_count[w] = count of currently-ready PRs whose labeled_at <= w.end
+```
+
+The line is monotonically non-decreasing by construction (PRs that lost the
+label are not in this dataset).
+
+Below the chart, print the per-week delta and a 6-week growth summary.
+Surfaces whether code-review capacity is keeping up with mark-ready
+throughput.
+
+---
+
+## Ready-for-review queue by CODEOWNER
+
+The "Ready by CODEOWNER" panel surfaces how the maintainer-review queue is
+distributed across the project's `.github/CODEOWNERS` entries.
+
+### Computation
+
+1. Parse `.github/CODEOWNERS` into a list of `(pattern, [owners])` rules in
+ declaration order. Patterns follow GitHub's CODEOWNERS glob syntax:
+ - Leading `/` anchors to repo root
+ - Trailing `/` matches directory contents
+ - `*` matches any character except `/`
+ - `**` matches any character including `/`
+ - No leading `/` matches anywhere in the tree
+2. For each currently-ready PR, fetch its changed file paths (use
+ `pullRequest.files(first:100)` — see
+
[`fetch.md#pr-changed-files-codeowners-panel`](fetch.md#pr-changed-files-codeowners-panel)).
+3. For each file, find the **LAST** matching rule (GitHub's last-match-wins
+ semantics). The owners of that rule are responsible for that file.
+4. Per ready PR, collect the union of owners across all its files.
+5. Per owner, count the *distinct* PRs they own at least one file of.
+ **A PR with N owners contributes to N owner rows** — counts can sum to
+ more than the total ready-PR count.
+
+### Waiting-for-author column
+
+For each (owner X, ready PR Y) where X is among Y's codeowners:
+
+```text
+waiting[X][Y] := X has posted at least one comment on Y AND
+ the author has neither commented nor pushed a commit
+ since X's most recent comment
+```
+
+Per owner, count the distinct PRs in `waiting[X]`. This is the subset where
+the author *currently owes that specific owner a response*.
+
+⚠ The open-PR fetch caps issue-level comments at `last:10` per PR; older
+outstanding comments are systematically missed. Treat the waiting count as a
+lower bound.
+
+### "No CODEOWNERS match" row
+
+Render an additional row at the bottom of the table for ready PRs where **no
+file** matches any CODEOWNERS rule (the union of file-owner sets is empty).
+These PRs are not routed to any maintainer by the CODEOWNERS file — a signal
+that the project might need to extend `.github/CODEOWNERS` to cover those
+paths.
+
+### Zero-count owners
+
+Surface owners listed in `.github/CODEOWNERS` who have 0 ready PRs in the
+queue as a collapsible footnote — useful for sanity-checking that no critical
+owner has been accidentally underutilised.
+
+### Adopters without `.github/CODEOWNERS`
+
+If the file is absent, skip this panel entirely. The skill should detect its
+absence at fetch time and degrade gracefully (no panel rendered, no warning
+beyond the skipped step).
+
+---
+
## Health rating
Top-of-dashboard hero card. Computed as a count of fired threshold conditions:
diff --git a/.claude/skills/pr-management-stats/classify.md
b/.claude/skills/pr-management-stats/classify.md
index 47be4ac..b1e7976 100644
--- a/.claude/skills/pr-management-stats/classify.md
+++ b/.claude/skills/pr-management-stats/classify.md
@@ -207,6 +207,59 @@ A PR pushed a new commit after the triage counts as
"responded" too — treat a
---
+## Waiting sub-states — AI-only vs maintainer-response
+
+The dashboard's "Triage funnel" panel splits the broader notion of "waiting on
the author" into two **mutually exclusive** buckets, because the priority
differs: an unresponded *manual* maintainer comment is a higher-priority "the
author owes a maintainer a reply" signal than an unresponded *AI-drafted*
comment.
+
+The split is computed over the same comment scan that produces
`triaged_waiting` / `triaged_responded`, but extended to cover ALL maintainer
comments (not just ones containing the QC marker) and partitioned by whether
the comment contains the AI-attribution footer.
+
+Define `last_author_activity` as the latest of:
+
+- PR's last commit `committedDate`
+- The most recent comment by `pr.author.login`
+
+Then for each *maintainer* (`OWNER`/`MEMBER`/`COLLABORATOR`, not-`is_bot`)
comment with `createdAt > last_author_activity`, classify the comment as AI vs
manual:
+
+- **AI-drafted** — body contains the
[`is_ai_triaged`](#is_ai_triaged--ai-assisted-triage) footer substring
+- **Manual** — body does not contain the footer
+
+### `waiting_for_manual_response`
+
+```text
+waiting_for_manual_response := EXISTS comment c WHERE
+ c is a maintainer comment AND
+ c.createdAt > last_author_activity AND
+ c is NOT AI-drafted
+```
+
+### `waiting_for_ai_only`
+
+```text
+waiting_for_ai_only := (NOT waiting_for_manual_response)
+ AND EXISTS comment c WHERE
+ c is a maintainer comment AND
+ c.createdAt > last_author_activity AND
+ c IS AI-drafted
+```
+
+The `NOT waiting_for_manual_response` clause makes these two predicates a
clean partition over contributor non-draft PRs — a PR with both an AI-drafted
and a manual unresponded comment counts only in the manual bucket (higher
priority).
+
+### Why this split matters
+
+The `triaged_waiting` count alone conflates two very different states:
+
+- **Author owes the maintainer a reply** (manual review feedback unanswered) —
high priority, real review work blocked.
+- **Author hasn't responded to an AI-drafted triage comment** (CI complaints,
generic violations note) — lower priority, may not even merit a reply if the
author is mid-fix.
+
+Splitting them lets the maintainer focus stale-sweep / ping efforts on the
high-priority subset.
+
+### Caveats
+
+- Both predicates rely on `pr.comments(last:10)` — older outstanding comments
on chatty PRs may be missed. Lower-bound numbers.
+- The footer-substring match (`AI-assisted triage tool`) is configurable per
[`<project-config>/pr-management-config.md`](../../../projects/_template/pr-management-config.md)'s
`ai_attribution_substring`. The default works as long as the project doesn't
customise the footer text.
+
+---
+
## Drafted by triager
A PR is *drafted by triager* when the viewer (or any maintainer) converted the
PR to draft *after* having posted the triage comment. Two ways to detect this:
diff --git a/.claude/skills/pr-management-stats/fetch.md
b/.claude/skills/pr-management-stats/fetch.md
index bc727aa..5933b23 100644
--- a/.claude/skills/pr-management-stats/fetch.md
+++ b/.claude/skills/pr-management-stats/fetch.md
@@ -319,6 +319,46 @@ Cache the per-PR `ready_at` in
`/tmp/pr-management-stats-cache-<repo-slug>.json`
---
+## PR changed files (CODEOWNERS panel)
+
+For the
[`#ready-for-review-queue-by-codeowner`](aggregate.md#ready-for-review-queue-by-codeowner)
panel, fetch each currently-ready PR's changed file paths. Aliased GraphQL,
**20 PRs per call** (the `files` connection on each PR returns up to 100 paths
— `files(first:100)`).
+
+```graphql
+query {
+ repository(owner:"<owner>",name:"<name>") {
+ pr12345: pullRequest(number:12345) {
+ number
+ files(first:100) {
+ nodes { path }
+ pageInfo { hasNextPage }
+ }
+ }
+ # … repeated for the other 19 PRs in the batch
+ }
+}
+```
+
+Per-PR processing: collect the `path` list. If `pageInfo.hasNextPage == true`
the PR has > 100 changed files — record it in the truncation log and proceed
with the first 100 (overwhelmingly enough for CODEOWNERS matching). For most
projects 100 is far above the median changed-file count.
+
+Cache per `(pr_number, head_sha)` — file lists rarely change without a new
commit so the cache hit rate is high on re-runs.
+
+Budget: ~8 GraphQL calls for ~150 currently-ready PRs on a busy project. Skip
the panel entirely if `.github/CODEOWNERS` is absent (no fetch necessary).
+
+### Reading `.github/CODEOWNERS`
+
+The CODEOWNERS file is read from the local checkout — same `<adopter-repo>`
the skill is running in. Look at:
+
+1. `.github/CODEOWNERS` (most common — GitHub's default location)
+2. `CODEOWNERS` (repo root — also recognised by GitHub)
+3. `docs/CODEOWNERS` (last GitHub-recognised location)
+
+The first existing path wins. If none exist, skip the panel.
+
+Parse line-by-line: a non-comment, non-blank line is `<pattern> <owner1>
<owner2> ...`. Strip inline `#`-comments before splitting. Owners start with
`@` (individual logins) or `@<org>/<team>` (team handles). Rules apply in
declaration order; matching is **last-match-wins** per GitHub semantics. See
+[`aggregate.md#ready-for-review-queue-by-codeowner`](aggregate.md#ready-for-review-queue-by-codeowner)
for the glob-to-regex translation.
+
+---
+
## Why no `statusCheckRollup` / `mergeable` / `reviewThreads`
`pr-management-triage` needs all three for classification;
`pr-management-stats` does not. Dropping them keeps the query complexity well
below GitHub's per-page ceiling, which is how we can safely run `batchSize=50`
here versus `20` in `pr-management-triage`. If a future stats column ever needs
one of those fields, raise only that query's complexity — don't pull them into
the default shape "just in case".
diff --git a/.claude/skills/pr-management-stats/render.md
b/.claude/skills/pr-management-stats/render.md
index d58aeb4..02cb785 100644
--- a/.claude/skills/pr-management-stats/render.md
+++ b/.claude/skills/pr-management-stats/render.md
@@ -95,6 +95,26 @@ If zero rules fire, render a single low-priority card with a
`✨` icon and the
The order inside the panel is: high-priority first (sorted by count
descending), then medium (same), then low. Within a tier the rule firing order
from [`#recommendation-rules`](#recommendation-rules) breaks ties.
+### 3b. Trends over time (line charts, 5 sub-panels)
+
+A "Trends over time" section between "What needs attention" and "Closure
velocity" with 5 sub-panels, each rendered as an inline SVG line chart followed
by a precise per-week table for readers who want exact numbers. Charts use the
same 6-week window the rest of the dashboard uses.
+
+| Sub-panel | Data source | Line(s) | Caveat |
+|---|---|---|---|
+| **Open backlog over time** |
[`aggregate.md#backlog-over-time-per-week-snapshots`](aggregate.md#backlog-over-time-per-week-snapshots)
| single line (open count at end of each week) | — |
+| **PRs opened by author class** |
[`aggregate.md#prs-opened-by-author-class-per-week`](aggregate.md#prs-opened-by-author-class-per-week)
| 3 lines: FIRST_TIME, CONTRIBUTOR, MAINTAINER | — |
+| **Ready-for-review queue size** |
[`aggregate.md#ready-for-review-queue-size-cumulative-over-time`](aggregate.md#ready-for-review-queue-size-cumulative-over-time)
| single line, cumulative end-of-week | — |
+| **Triage velocity** |
[`aggregate.md#triage-velocity-per-week`](aggregate.md#triage-velocity-per-week)
| 2 lines: AI-drafted, manual QC | `comments(last:25)` cap under-counts older
weeks; note in panel |
+| **Triage coverage rate by week opened** |
[`aggregate.md#triage-coverage-rate-by-week-opened`](aggregate.md#triage-coverage-rate-by-week-opened)
| single line (% engaged), y-axis 0-100 | same comment-cap caveat |
+
+### Inline SVG line-chart helper
+
+The recommended renderer is a small inline-SVG helper rather than an external
chart library — avoids JS dependencies and keeps the dashboard self-contained
for archival / gist publication. Typical shape: 640×140 viewBox, left margin
for Y-axis labels, right margin for series legend, 5 horizontal gridlines,
coloured polylines with circular markers + inline value labels.
+
+Multi-series charts get a per-series colour key in the right margin.
Single-series charts use the panel's primary colour (e.g. blue for backlog,
green for ready-queue, red for the "untriaged" colour palette).
+
+The same helper is used by panels 4, 5, 6, 7, and the 8b CODEOWNERS panel
below — any weekly time-series should render as a line chart for visual rhythm.
Bar charts are kept only where the data is genuinely categorical (e.g. stacked
velocity), and even there the line-chart version sits *above* the table for
trend visibility.
+
### 4. Closure velocity (per-week bar chart)
Title: **Closures per week (oldest → newest)**
@@ -188,18 +208,55 @@ Up to 8 rows, sorted by pressure score descending
(filtering areas with < 3 cont
This panel answers "if I have 30 minutes, which area moves the most needles?".
Top row is always the highest-leverage focus.
-### 9. Triage funnel (4-column hero grid)
+### 8b. Ready-for-review queue by CODEOWNER
-A second hero grid, same layout as the top one, showing the funnel-health
summary numbers:
+Data from
[`aggregate.md#ready-for-review-queue-by-codeowner`](aggregate.md#ready-for-review-queue-by-codeowner).
Rendered as a 4-column table:
-| Card | Big number | Sub-label | Colour rule |
-|---|---|---|---|
-| **Triage Coverage** | `<pct>%` of contributor PRs that have been seen by a
maintainer (triaged + ready + draft / contributors) | `<seen> of <total>
contributor PRs have been seen by a maintainer` | green ≥ 50, amber 20–49, red
< 20 |
-| **Author Response Rate** | `<pct>%` of triaged PRs where the author replied
| `<responded> of <triaged> triaged PRs got an author reply` | same colour rule
|
-| **Stalest Bucket** | count of contributor PRs in the `>4w` age bucket |
"contributor PRs untouched >4 weeks" | red if > 50, amber if > 20, green
otherwise |
-| **This Week's Velocity** | this week's `merged + closed` total | `<merged>
merged · <closed> closed (avg <N>/wk)` | default (informational) |
+| Column | Content |
+|---|---|
+| **CODEOWNER** | `@login` (or `@<org>/<team>` for team handles) |
+| **Ready PRs** | Count of currently-ready PRs the owner is responsible for,
plus `(N% of queue)` ratio. Severity colour: red ≥ 50, amber ≥ 20, green ≥ 10,
grey below. |
+| **Waiting for author response** | Count of those PRs where this owner has
personally left a comment that the author hasn't replied to or pushed past. Red
when > 0; grey "0" when none. Sub-text: `(N% of theirs)`. |
+| **Bar (overlay)** | Horizontal bar where the full-width green/amber/red
segment shows total Ready PRs, with a red segment overlaid (same x-origin,
narrower) showing the Waiting subset. |
+
+Rows are ordered by Ready-PR count descending.
+
+Below the per-owner rows, append a **highlighted total-style row** for ready
PRs with **no CODEOWNERS match** — paths not covered by any rule. This row uses
a "⚠ (no CODEOWNERS match)" label, displays the count and `(N% of queue)`,
leaves the Waiting column as `—`, and renders a single-tone bar (no overlay).
+
+After the table, two collapsible details:
+
+- **`<N> ready PRs with no CODEOWNERS match`** — opens to show the PR numbers
as clickable links, useful for "which paths does CODEOWNERS not cover"
exploration.
+- **`<N> CODEOWNERS with 0 ready PRs in the queue`** — opens to list owners
present in `.github/CODEOWNERS` but with no responsibility in the current
queue. Sanity check for accidentally-empty owners.
+
+Caveat note above the table:
+
+```text
+For each owner in .github/CODEOWNERS, count of currently-ready PRs that touch
at least one file they own. A PR with multiple owners counts once per owner
(rows can sum to more than the total ready-PR count). The Waiting column counts
the subset where THAT owner has personally left a comment that the PR author
hasn't replied or pushed past. Caveat: comment fetch capped at last:10 per PR;
older outstanding comments on chatty PRs may be missed.
+```
+
+Skip the entire panel if `.github/CODEOWNERS` (and fallback locations — see
[`fetch.md#reading-githubcodeowners`](fetch.md)) are absent.
+
+### 9. Triage funnel (5-column hero grid)
+
+A second hero grid, same layout as the top one. The contents are
precedence-based — a single contributor non-draft PR lands in exactly one card.
The cards in order of precedence:
+
+| Card | Definition | Colour |
+|---|---|---|
+| **Ready for review** | `is_ready` (label present) | green |
+| **Responded (post-QC)** | `triaged_responded` (QC marker + author activity
since) | bright cyan |
+| **Waiting: AI-triage comment only** |
[`waiting_for_ai_only`](classify.md#waiting-sub-states--ai-only-vs-maintainer-response)
(unresponded AI comment, NO unresponded manual comment) | purple |
+| **Waiting: author response to maintainer** |
[`waiting_for_manual_response`](classify.md#waiting-sub-states--ai-only-vs-maintainer-response)
(any unresponded MANUAL maintainer comment — higher priority than AI-only) |
red |
+| **Not yet triaged** |
[`is_untriaged`](classify.md#is_untriaged--broad-untriaged) and non-draft |
blue |
+
+The two "Waiting" cards are **mutually exclusive** by construction (the
`waiting_for_ai_only` predicate explicitly excludes PRs that also have manual
unresponded comments). This split surfaces the high-priority backlog of PRs
where the author owes a maintainer a reply, separate from the lower-priority
backlog of PRs that have only an AI-drafted comment pending.
+
+Print a one-line caveat below the grid:
+
+```text
+The two waiting cards are mutually exclusive — a PR with both unresponded
AI-drafted and manual maintainer comments counts only in the "author response
to maintainer" bucket.
+```
-This grid completes the dashboard: hero cards at the top (queue size +
immediate red flags), recommendations next (what to do), velocity +
opened-vs-closed (momentum), pressure by area (where), and triage funnel
(process health).
+This grid completes the dashboard's status section: hero cards at the top
(queue size + red flags), recommendations next (what to do), trends + velocity
+ opened-vs-closed (momentum), pressure by area (where), CODEOWNERS
distribution (who), and triage funnel (process health).
### 9b. Triager activity panel