From 8f9625f13f0ccc88b869371a4829ab0856eb77ba Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 17 Jun 2026 19:40:16 +1000 Subject: [PATCH 01/24] QEP-2: Standard GitHub label set and labelling policy Re-records the label-set decision from QuantEcon/meta#324 as a worked Standard-type QEP, per QEP-1's rollout. Defines the standard label set (18 core + 2 lecture), the labelling policy, and the one behavioural change (retire status labels in favour of native GitHub PR lifecycle). Co-Authored-By: Claude Opus 4.8 --- README.md | 1 + qeps/qep-0002-standard-github-labels.md | 241 ++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 qeps/qep-0002-standard-github-labels.md diff --git a/README.md b/README.md index 130d749..e7e5a23 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ need a QEP. | QEP | Title | Status | |-----|-------|--------| | [QEP-1](qeps/qep-0001-purpose-and-process.md) | QEP Purpose and Process | Accepted | +| [QEP-2](qeps/qep-0002-standard-github-labels.md) | Standard GitHub Label Set and Labelling Policy | Draft | ## Proposing a QEP diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md new file mode 100644 index 0000000..3835356 --- /dev/null +++ b/qeps/qep-0002-standard-github-labels.md @@ -0,0 +1,241 @@ +--- +qep: 2 +title: Standard GitHub Label Set and Labelling Policy +author: "@mmcky" +status: Draft +type: Standard +created: 2026-06-17 +discussion: https://github.com/QuantEcon/meta/issues/324 +--- + +# QEP-2: Standard GitHub Label Set and Labelling Policy + +| | | +| ------------ | ------------------------------------------------------------------ | +| **QEP** | 2 | +| **Title** | Standard GitHub Label Set and Labelling Policy | +| **Author** | @mmcky | +| **Status** | Draft | +| **Type** | Standard | +| **Created** | 2026-06-17 | +| **Discussion** | [QuantEcon/meta#324](https://github.com/QuantEcon/meta/issues/324) | + +## Summary + +This QEP defines a single standard set of GitHub labels for QuantEcon +repositories, the meaning of each label, and the policy for how labels are +applied. It standardises label **names, colours, and descriptions** so they +mean the same thing everywhere, and it commits to **one behavioural change**: +the pull-request lifecycle (`in-work` / `ready` / `review`) moves off labels +and onto native GitHub features (Draft PRs, "Ready for review", review +requests). The set is defined as a universal **core** plus a small **lecture +extension**, applied additively so existing issue and PR history is preserved. + +## Motivation + +QuantEcon's repositories have drifted into inconsistent, overlapping label +vocabularies. The same idea appears under different names (`improve` / +`content` / `teaching` vs `enhancement`; `linkchecker` vs a link-checker +label), priority is expressed three different ways, and bot-generated noise is +hard to tell from human triage. This is the fourth attempt to unify them, and +the cost of *not* settling it has grown beyond tidiness: + +- **Triage is ambiguous.** A contributor cannot tell from the label list what + "kind" of work an issue is, or whether it has been triaged at all. +- **Automation reads labels and the names don't match.** The org activity + reports and the quantecon.org/news drafts key off labels to rank and surface + work; while wiring these up we found they were boosting on names that **do + not exist anywhere** (`priority: high`, `feature`, `chore`, `docs`) while the + real labels (`high-priority`, `enhancement`, `documentation`) went unread. A + standard set gives the reporting a stable contract. +- **Cross-repo work has no shared shape.** Editorial sign-off, newcomer + discovery, and "do not merge yet" all need to mean the same thing across + every lecture repo. + +A standard set is a cross-repository, team-wide decision, which is exactly what +a [QEP](qep-0001-purpose-and-process.md) is for. + +## Proposal + +### The label set + +Labels are grouped by purpose. **Colour carries meaning where it helps:** +priority is a heat scale (hot red β†’ cool green), the automation/CI family +shares a quiet grey ("a machine made this"), and type/community keep +conventional GitHub colours. A label with no profile is **core** β€” meaningful +on any repository. Two labels form a **lecture extension** applied only to +lecture repos. + +**Type β€” what kind of work is this? (one per issue, set at triage)** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `bug` | πŸŸ₯ `#d73a4a` | Something is wrong or broken in a lecture or build | The content is *incorrect*: wrong maths, erroring code, broken rendering | +| `enhancement` | 🟦 `#a2eeef` | Improvement to existing material | Better exposition, a new exercise, improved figures | +| `documentation` | 🟫 `#b08968` | Repo docs and contributor meta (not lecture content) | READMEs, CONTRIBUTING β€” *about the repo*; lecture content is never `documentation` | +| `infrastructure` | 🟦 `#006b75` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped. Use *instead of* `maintenance` | +| `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn a reader never sees; "rendered lectures look identical after" | +| `question` | πŸŸͺ `#d876e3` | Someone needs an answer or clarification | Terminal state: *answered*. Swap for a work type once it becomes agreed work | +| `discuss` | πŸ’— `#f904a0` | Open-ended team deliberation or a decision to be made | No single right answer. Reuses meta's existing `discuss` colour | + +**Priority β€” a heat scale (unlabelled = normal priority)** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `high-priority` | 🟧 `#d93f0b` | Address soon | Published content visibly broken; build blockers. Label only the outliers | +| `low-priority` | 🟩 `#c2e0c6` | Nice to have, no time pressure | Agreed work that's fine to sit; "someday" ideas | + +There is deliberately **no `medium-priority`** β€” the unlabelled default *is* the +middle of the scale. + +**Community (GitHub-canonical names β€” spaces are deliberate, see Alternatives)** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `good first issue` | πŸŸͺ `#7057ff` | Self-contained and friendly to newcomers | Only when genuinely self-contained with clear acceptance criteria | +| `help wanted` | 🟩 `#008672` | Maintainers would welcome outside help | Including domain (econ/math) expertise β€” say what's needed in a comment | + +**Workflow** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `do-not-merge` | πŸŸ₯ `#b60205` | Approved-looking but must not be merged yet | A *voluntary* hold on a mergeable PR: pins, experiments, cross-repo timing | + +**Automation β€” applied by bots, not humans (quiet grey)** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `automated` | ⬜ `#ededed` | Opened by a bot or scheduled workflow | Every bot issue carries this **plus exactly one** diagnostic below | +| `broken-links` | ⬜ `#dddddd` | Link checker found dead links | Applied by the link-checker action | +| `build-failure` | ⬜ `#cccccc` | Notebook execution, build, or warnings failure | Applied by build / warnings checks | +| `dependencies` | ⬜ `#bdbdbd` | Dependency or environment update (pip, conda, actions) | The single Dependabot label (replaces `github_actions` / `conda`) | + +**Meta β€” closing outcomes** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `duplicate` | ⬜ `#cfd3d7` | Already tracked elsewhere | On close; link the survivor | +| `wontfix` | ⬜ `#ffffff` | Decided not to act | On close, with one sentence why | + +**Lecture extension β€” lecture repos only** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `new-lecture` | 🟦 `#0537E9` | A new lecture (the marquee outcome) | Brand-new lecture β€” proposed, in progress, or shipped. Use *instead of* `enhancement` | +| `editor` | 🟩 `#0e8a16` | Requires editor review β€” final sign-off stage | Apply at handoff *after* team review; editor's queue = `org:QuantEcon label:editor`. Remove on sign-off | + +This is **18 core labels + 2 lecture labels = 20**. The org-level default set +for new repositories is the **core 18**. + +### Labelling policy + +The labels only help if they are applied consistently. The rules are +deliberately few: + +- **Unlabelled means "needs triage, normal priority."** Nothing *must* be + labelled to exist. Note that **"no labels at all" and "no priority label" are + different states**: a `bug` with no priority label has been triaged and is + normal priority, whereas a completely unlabelled issue still needs triage. +- **Exactly one Type label per issue,** chosen at triage. The Type labels are + mutually exclusive on purpose so the boundary stays clean β€” `new-lecture` + *instead of* `enhancement`, `infrastructure` *instead of* `maintenance`. +- **Priority labels mark only the outliers.** Most work sits at the unlabelled + default; reach for `high-priority` / `low-priority` only when an item is + genuinely off-centre. +- **`question` vs `discuss`.** `question` seeks a single answer and is done when + answered; `discuss` is open-ended deliberation with no single right answer. + **A `discuss` thread should not drift open indefinitely** β€” once it reaches a + conclusion, summarise the decision and close, spawn a concrete follow-up + issue, or escalate to a QEP if it crosses repos or changes team workflow. +- **Automation labels are bot-applied.** A bot-opened issue carries `automated` + plus exactly one diagnostic (`broken-links`, `build-failure`, or + `dependencies`); humans do not hand-apply these. +- **`do-not-merge` is a voluntary hold,** distinct from being *blocked*: see the + status-label change below. + +### The one behavioural change: retire status labels + +Pull-request lifecycle state moves entirely onto **native GitHub features** and +off labels: + +| Old label | Replaced by | +|---|---| +| `in-work` | a **Draft** pull request | +| `ready` | clicking **"Ready for review"** | +| `review` | a **review request** | +| `blocked` / `on-hold` | native **issue dependencies** ("Blocked by #N") for issues; a Draft PR with a "Blocked by #N" note for PRs | + +Only **`do-not-merge`** survives as a workflow label, for the genuine case of a +PR that *looks* mergeable (even approved) but must be held. + +### What we deliberately don't label + +`medium-priority` β†’ no label (unlabelled is the middle) Β· project / grouping +labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels +(`colab`, …) β†’ `build-failure` Β· PR lifecycle β†’ native GitHub, as above. + +### Scope + +- **Lecture repos get all 20** (core 18 + the lecture extension). +- **Software / tooling repos get the core 18,** which is also the **org-level + default** for new repositories. +- **Not touched:** `meta` (keeps its own `project` / `education` labels), + translation forks (`translate:*`), and `*.notebooks` build repos. +- **The sync is additive.** It *guarantees* the standard set on a repo and + renames known variants in place, but it does **not** blanket-prune bespoke + local labels that some repos legitimately rely on (`jax-conversion`, `colab`, + `site-refresh`, `reading-group-*`). Renaming in place (e.g. + `linkchecker` β†’ `broken-links`) preserves every existing issue and PR tag. + +## Alternatives considered + +- **Fold `infrastructure` into `maintenance` (one Type label, not two).** The + simpler option is a single "work a reader never sees" bucket. We keep them + **separate** because the team treats them differently: `infrastructure` is + substantial engineering a teammate wants to know shipped (a CI migration, a + build-system change, new automation), whereas `maintenance` is routine, + invisible churn (style, env bumps). The activity reports already surface them + differently β€” folding them together sinks "we re-architected CI across the + lecture repos" into the same bucket as "bumped a pin." *(This is the one item + still open at draft β€” see the PR description.)* +- **Add a `blocked` label.** Rejected in favour of native GitHub **issue + dependencies** (generally available since Aug 2025), which record *what + blocks what* rather than just *that* something is blocked, and avoid a second + red label clashing with `do-not-merge`. PRs use a Draft + "Blocked by #N" + note. +- **Hyphenate `good first issue` / `help wanted` for consistency.** Rejected: + the spaced names are load-bearing. GitHub's algorithm uses the exact spaced + label `good first issue` to populate a repo's **Contribute page** and surface + approachable issues; hyphenating would lose newcomer discovery for no + functional gain. The hyphenated `good-first-issue` is a repo *topic*, a + different namespace. Every label QuantEcon *owns* stays hyphenated; these two + belong to GitHub. +- **Keep status labels (`in-work` / `ready` / `review`).** Rejected: native + Draft / Ready-for-review / review-request state already expresses PR + lifecycle, keeps it in sync automatically, and removes labels that were + routinely stale. Retiring them has no impact on the activity reports, which + never read them. +- **Recolour `documentation` to stay in the blue family.** Rejected: blue is + reserved for "new content to publish" (`enhancement`, `new-lecture`); + `documentation` is a tier-2, about-the-repo concern, so it moves to tan + `#b08968` to read as distinct at a glance. + +## Rollout + +1. **Accept this QEP** to fix the names, colours, descriptions, and policy as + the QuantEcon standard. +2. **Record the schema** as the single source of truth in + [`QuantEcon/cli`](https://github.com/QuantEcon/cli) + (`src/qe_cli/data/labels.yml`), with the per-label rationale and the + labelling policy mirrored in the labels guide. +3. **Apply it with `qe gh labels sync`** β€” additively, renaming known variants + in place so issue and PR history is preserved. +4. **First-wave repos.** Confirmed: `lecture-python-intro`, + `lecture-python-programming`, `lecture-python.myst`, + `lecture-python-advanced.myst`, `lecture-dp`, `lecture-jax`. Candidates + added in the same wave: `lecture-julia.myst`, `lecture-datascience.myst`, + `lecture-stats`, `continuous_time_mcs`, `lecture-wasm`. +5. **Set the org-level default labels** for new repositories to the core 18 + (a manual settings change β€” there is no public API for org defaults). +6. **Point contributors at the guide** so triage follows the policy above, and + close the earlier unification attempts (#178, #290) against this outcome. From b84605bd795d8e8f425f876246a3828465783094 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 17 Jun 2026 19:44:43 +1000 Subject: [PATCH 02/24] QEP-2: fold in the #324 decisions Apply the final decisions from QuantEcon/meta#324: - infrastructure split approved (no longer flagged open) - Q2 rollout is a staged pilot on lecture-python-programming, then wider - add the separate `qe gh labels prune` step (one-by-one review) Co-Authored-By: Claude Opus 4.8 --- qeps/qep-0002-standard-github-labels.md | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 3835356..4bca8c8 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -181,11 +181,14 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels default** for new repositories. - **Not touched:** `meta` (keeps its own `project` / `education` labels), translation forks (`translate:*`), and `*.notebooks` build repos. -- **The sync is additive.** It *guarantees* the standard set on a repo and - renames known variants in place, but it does **not** blanket-prune bespoke - local labels that some repos legitimately rely on (`jax-conversion`, `colab`, - `site-refresh`, `reading-group-*`). Renaming in place (e.g. - `linkchecker` β†’ `broken-links`) preserves every existing issue and PR tag. +- **Applying the set is additive; pruning is separate and deliberate.** + `qe gh labels sync` *guarantees* the standard set on a repo and renames known + variants in place (e.g. `linkchecker` β†’ `broken-links`, preserving every + existing issue and PR tag), but it **never blanket-prunes** bespoke local + labels that some repos legitimately rely on (`jax-conversion`, `colab`, + `site-refresh`, `reading-group-*`). Removing non-standard labels is a + **separate** `qe gh labels prune` step that reviews each one **one by one**, + keeping the ones that still make sense locally. ## Alternatives considered @@ -196,8 +199,8 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels build-system change, new automation), whereas `maintenance` is routine, invisible churn (style, env bumps). The activity reports already surface them differently β€” folding them together sinks "we re-architected CI across the - lecture repos" into the same bucket as "bumped a pin." *(This is the one item - still open at draft β€” see the PR description.)* + lecture repos" into the same bucket as "bumped a pin." The split was + **approved** in [#324](https://github.com/QuantEcon/meta/issues/324). - **Add a `blocked` label.** Rejected in favour of native GitHub **issue dependencies** (generally available since Aug 2025), which record *what blocks what* rather than just *that* something is blocked, and avoid a second @@ -229,12 +232,16 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels (`src/qe_cli/data/labels.yml`), with the per-label rationale and the labelling policy mirrored in the labels guide. 3. **Apply it with `qe gh labels sync`** β€” additively, renaming known variants - in place so issue and PR history is preserved. -4. **First-wave repos.** Confirmed: `lecture-python-intro`, - `lecture-python-programming`, `lecture-python.myst`, - `lecture-python-advanced.myst`, `lecture-dp`, `lecture-jax`. Candidates - added in the same wave: `lecture-julia.myst`, `lecture-datascience.myst`, - `lecture-stats`, `continuous_time_mcs`, `lecture-wasm`. + in place so issue and PR history is preserved. Removing any non-standard + labels is a separate, deliberate `qe gh labels prune` pass, reviewed one by + one. +4. **Pilot on one repo first, then widen.** The first rollout target is + **`lecture-python-programming`**; once it's validated, roll out more widely + to the rest of the confirmed lecture repos (`lecture-python-intro`, + `lecture-python.myst`, `lecture-python-advanced.myst`, `lecture-dp`, + `lecture-jax`) and the candidates (`lecture-julia.myst`, + `lecture-datascience.myst`, `lecture-stats`, `continuous_time_mcs`, + `lecture-wasm`), then the software / tooling repos. 5. **Set the org-level default labels** for new repositories to the core 18 (a manual settings change β€” there is no public API for org defaults). 6. **Point contributors at the guide** so triage follows the policy above, and From d61987268291747a89002d6b97e37f808c468640 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 11:20:42 +1000 Subject: [PATCH 03/24] =?UTF-8?q?QEP-2:=20field-report=20revision=20?= =?UTF-8?q?=E2=80=94=20security=20modifier,=20yml=20appendix=20as=20source?= =?UTF-8?q?=20of=20truth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the QuantEcon/actions field report and follow-up discussion: - Add `security` as a cross-cutting non-Type modifier (like priority): one Type label per issue, plus optionally security. 19 core + 2 lecture = 21. Recorded the Type-label / repo-local alternatives. - Settle the `infrastructure` colour: #1d3c78 (the #006b75 placeholder clashed with the `help wanted` teal). - Sharpen the infrastructure/maintenance boundary with a repo-agnostic test: would it appear in a release note? - Add `testing` to the deliberately-unlabelled list; add the `high priority` -> `high-priority` rename example. - Ship the normative schema as a co-located machine-readable appendix (qep-0002-labels.yml) β€” the source of truth tooling consumes, versioned with the QEP per QEP-1's substantive-amendment rule. The yml carries only the standard itself; rename/prune knowledge stays in the CLI. - CI: check-labels.mjs keeps the tables and the yml in lockstep and requires a version bump when an Accepted QEP's appendix changes. Co-Authored-By: Claude Fable 5 --- .github/scripts/check-labels.mjs | 142 ++++++++++++++++++++++++ .github/workflows/qep-checks.yml | 4 + qeps/qep-0002-labels.yml | 140 +++++++++++++++++++++++ qeps/qep-0002-standard-github-labels.md | 68 ++++++++++-- 4 files changed, 342 insertions(+), 12 deletions(-) create mode 100644 .github/scripts/check-labels.mjs create mode 100644 qeps/qep-0002-labels.yml diff --git a/.github/scripts/check-labels.mjs b/.github/scripts/check-labels.mjs new file mode 100644 index 0000000..ad0dc0a --- /dev/null +++ b/.github/scripts/check-labels.mjs @@ -0,0 +1,142 @@ +// Machine-readable appendix checks (see QEP-2 Β§ Machine-readable appendix): +// 1. Every qeps/qep-NNNN-*.yml companion stays in lockstep with the label +// tables in its owning QEP: same label names, and per label the same +// colour, description, and scope (core vs lecture extension). +// 2. Once the owning QEP is Accepted, a change to its companion yml is a +// substantive amendment and must bump the QEP's `version` in the same PR. +// Run by .github/workflows/qep-checks.yml. Exits non-zero on any failure. +import { readFileSync, readdirSync } from 'node:fs'; +import { execSync } from 'node:child_process'; +import { FRONTMATTER, QEP_DIR, parseQep, qepFiles } from './qeps.mjs'; + +const base = process.env.BASE_REF || 'main'; +const errors = []; + +function stripQuotes(s) { + return s.replace(/^["']|["']$/g, ''); +} + +// Parse a companion yml: a `labels:` list whose items are flat scalar maps. +// Deliberately minimal β€” the appendix format is constrained by construction. +function parseLabelsYml(path) { + const labels = []; + let current; + for (const raw of readFileSync(path, 'utf8').split('\n')) { + const line = raw.replace(/\s+$/, ''); + if (/^\s*(#|$)/.test(line)) continue; + let m = line.match(/^\s*-\s+name:[ \t]*(.*)$/); + if (m) { + current = { name: stripQuotes(m[1]) }; + labels.push(current); + continue; + } + m = line.match(/^\s+([\w-]+):[ \t]*(.*)$/); + if (m && current) current[m[1]] = stripQuotes(m[2]); + } + return labels; +} + +// Parse the label tables out of a QEP body: any table row whose first cell is +// a backticked name and whose second cell carries a `#hex` colour. Scope comes +// from the nearest preceding bold group heading ("Lecture extension β€” ..."). +function parseLabelTables(path) { + const labels = []; + let scope = 'core'; + for (const line of readFileSync(path, 'utf8').split('\n')) { + const heading = line.match(/^\*\*(.+?)\*\*$/); + if (heading) { + scope = /^Lecture extension/i.test(heading[1]) ? 'lecture' : 'core'; + continue; + } + const m = line.match(/^\|\s*`([^`]+)`\s*\|[^|`]*`#([0-9a-fA-F]{6})`\s*\|([^|]*)\|/); + if (m) labels.push({ name: m[1], color: m[2].toLowerCase(), description: m[3].trim(), scope }); + } + return labels; +} + +// The QEP file a companion belongs to, matched on the qep-NNNN- prefix. +function owningQep(ymlPath) { + const num = ymlPath.match(/qep-(\d{4})-/)?.[1]; + return qepFiles().find((p) => p.includes(`/qep-${num}-`)); +} + +// Frontmatter `status`/`version` of a file on the base branch (undefined if new). +function baseFrontmatter(path) { + let text; + try { + text = execSync(`git show "origin/${base}:${path}"`, { + stdio: ['pipe', 'pipe', 'ignore'], + }).toString(); + } catch { + return undefined; + } + const block = text.match(FRONTMATTER)?.[1] ?? ''; + return { + status: block.match(/^status:[ \t]*(.*?)[ \t]*$/m)?.[1], + version: block.match(/^version:[ \t]*(\d+)/m)?.[1], + }; +} + +function baseFile(path) { + try { + return execSync(`git show "origin/${base}:${path}"`, { + stdio: ['pipe', 'pipe', 'ignore'], + }).toString(); + } catch { + return undefined; + } +} + +const companions = readdirSync(QEP_DIR) + .filter((f) => /^qep-\d{4}-.*\.yml$/.test(f)) + .map((f) => `${QEP_DIR}/${f}`); + +for (const ymlPath of companions) { + const qepPath = owningQep(ymlPath); + if (!qepPath) { + errors.push(`${ymlPath}: no owning QEP file found for this companion`); + continue; + } + + // 1. The yml and the QEP's tables describe the same label set. + const yml = new Map(parseLabelsYml(ymlPath).map((l) => [l.name, l])); + const tables = new Map(parseLabelTables(qepPath).map((l) => [l.name, l])); + for (const name of yml.keys()) { + if (!tables.has(name)) errors.push(`${qepPath}: tables are missing \`${name}\` (present in ${ymlPath})`); + } + for (const [name, t] of tables) { + const y = yml.get(name); + if (!y) { + errors.push(`${ymlPath}: missing \`${name}\` (present in ${qepPath} tables)`); + continue; + } + if ((y.color ?? '').toLowerCase() !== t.color) { + errors.push(`\`${name}\`: colour "${y.color}" (${ymlPath}) != "#${t.color}" (${qepPath})`); + } + if (y.description !== t.description) { + errors.push(`\`${name}\`: description differs between ${ymlPath} and ${qepPath}`); + } + if ((y.scope ?? 'core') !== t.scope) { + errors.push(`\`${name}\`: scope "${y.scope}" (${ymlPath}) != "${t.scope}" (${qepPath})`); + } + } + + // 2. Post-acceptance, a yml change is substantive: `version` must move with it. + const before = baseFile(ymlPath); + const fm = baseFrontmatter(qepPath); + if (before !== undefined && before !== readFileSync(ymlPath, 'utf8') && fm?.status === 'Accepted') { + const { version } = parseQep(qepPath); + if (String(version ?? '') === (fm.version ?? '')) { + errors.push( + `${ymlPath}: changed on an Accepted QEP without bumping ${qepPath} version ` + + `(a machine-readable appendix change is substantive under QEP-1)`, + ); + } + } +} + +if (errors.length) { + console.error('Label appendix checks failed:\n' + errors.map((e) => ` - ${e}`).join('\n')); + process.exit(1); +} +console.log(`Label appendix checks passed (${companions.length} companion file(s)).`); diff --git a/.github/workflows/qep-checks.yml b/.github/workflows/qep-checks.yml index 451850f..de91fd0 100644 --- a/.github/workflows/qep-checks.yml +++ b/.github/workflows/qep-checks.yml @@ -27,3 +27,7 @@ jobs: run: node .github/scripts/check.mjs env: BASE_REF: ${{ github.base_ref }} + - name: Run label appendix checks + run: node .github/scripts/check-labels.mjs + env: + BASE_REF: ${{ github.base_ref }} diff --git a/qeps/qep-0002-labels.yml b/qeps/qep-0002-labels.yml new file mode 100644 index 0000000..085b84a --- /dev/null +++ b/qeps/qep-0002-labels.yml @@ -0,0 +1,140 @@ +# Machine-readable appendix to QEP-2 β€” the normative label schema. +# +# This file is the single source of truth for tooling (`qe gh labels sync`). +# Every field is normative and versioned with the QEP: changing this file is a +# substantive amendment under QEP-1 and bumps QEP-2's `version`. CI checks that +# it stays in lockstep with the tables in qep-0002-standard-github-labels.md. +# +# Operational mappings (rename variants, retired-label handling, pruning) are +# deliberately NOT recorded here β€” they belong to the CLI implementing the QEP. +# +# Fields: name Β· color (hex, no '#') Β· description (shown on GitHub) Β· +# group (type | priority | cross-cutting | community | workflow | +# automation | meta | lecture) Β· scope (core | lecture) Β· +# applied-by: bot (automation labels only; default is human) + +labels: + # Type β€” what kind of work is this? (one per issue, set at triage) + - name: bug + color: d73a4a + description: Something is wrong or broken in a lecture or build + group: type + scope: core + - name: enhancement + color: a2eeef + description: Improvement to existing material + group: type + scope: core + - name: documentation + color: b08968 + description: Repo docs and contributor meta (not lecture content) + group: type + scope: core + - name: infrastructure + color: 1d3c78 + description: Substantial CI / build / deploy / tooling / automation work + group: type + scope: core + - name: maintenance + color: fbca04 + description: "Routine housekeeping: style, formatting, env & dependency upkeep" + group: type + scope: core + - name: question + color: d876e3 + description: Someone needs an answer or clarification + group: type + scope: core + - name: discuss + color: f904a0 + description: Open-ended team deliberation or a decision to be made + group: type + scope: core + + # Priority β€” a heat scale (unlabelled = normal priority) + - name: high-priority + color: d93f0b + description: Address soon + group: priority + scope: core + - name: low-priority + color: c2e0c6 + description: Nice to have, no time pressure + group: priority + scope: core + + # Cross-cutting β€” combine with any Type (like priority) + - name: security + color: ee0701 + description: Security implications β€” needs a security-aware review bar + group: cross-cutting + scope: core + + # Community (GitHub-canonical names β€” spaces are deliberate) + - name: good first issue + color: 7057ff + description: Self-contained and friendly to newcomers + group: community + scope: core + - name: help wanted + color: "008672" + description: Maintainers would welcome outside help + group: community + scope: core + + # Workflow + - name: do-not-merge + color: b60205 + description: Approved-looking but must not be merged yet + group: workflow + scope: core + + # Automation β€” applied by bots, not humans + - name: automated + color: ededed + description: Opened by a bot or scheduled workflow + group: automation + scope: core + applied-by: bot + - name: broken-links + color: dddddd + description: Link checker found dead links + group: automation + scope: core + applied-by: bot + - name: build-failure + color: cccccc + description: Notebook execution, build, or warnings failure + group: automation + scope: core + applied-by: bot + - name: dependencies + color: bdbdbd + description: Dependency or environment update (pip, conda, actions) + group: automation + scope: core + applied-by: bot + + # Meta β€” closing outcomes + - name: duplicate + color: cfd3d7 + description: Already tracked elsewhere + group: meta + scope: core + - name: wontfix + color: ffffff + description: Decided not to act + group: meta + scope: core + + # Lecture extension β€” lecture repos only + - name: new-lecture + color: 0537e9 + description: A new lecture (the marquee outcome) + group: lecture + scope: lecture + - name: editor + color: 0e8a16 + description: Requires editor review β€” final sign-off stage + group: lecture + scope: lecture diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 413bed4..352d90b 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -73,8 +73,8 @@ lecture repos. | `bug` | πŸŸ₯ `#d73a4a` | Something is wrong or broken in a lecture or build | The content is *incorrect*: wrong maths, erroring code, broken rendering | | `enhancement` | 🟦 `#a2eeef` | Improvement to existing material | Better exposition, a new exercise, improved figures | | `documentation` | 🟫 `#b08968` | Repo docs and contributor meta (not lecture content) | READMEs, CONTRIBUTING β€” *about the repo*; lecture content is never `documentation` | -| `infrastructure` | 🟦 `#006b75` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped. Use *instead of* `maintenance` | -| `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn a reader never sees; "rendered lectures look identical after" | +| `infrastructure` | 🟦 `#1d3c78` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped β€” it would appear in a release note. Use *instead of* `maintenance` | +| `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn that would never appear in a release note ("rendered lectures look identical after") | | `question` | πŸŸͺ `#d876e3` | Someone needs an answer or clarification | Terminal state: *answered*. Swap for a work type once it becomes agreed work | | `discuss` | πŸ’— `#f904a0` | Open-ended team deliberation or a decision to be made | No single right answer. Reuses meta's existing `discuss` colour | @@ -88,6 +88,12 @@ lecture repos. There is deliberately **no `medium-priority`** β€” the unlabelled default *is* the middle of the scale. +**Cross-cutting β€” combine with any Type (like priority)** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `security` | πŸŸ₯ `#ee0701` | Security implications β€” needs a security-aware review bar | Credentials / tokens, supply-chain surface, workflow permissions. Applied *alongside* the Type label (e.g. `bug` + `security`); makes `org:QuantEcon label:security` a standing query | + **Community (GitHub-canonical names β€” spaces are deliberate, see Alternatives)** | Label | Colour | Description | When to use | @@ -124,8 +130,8 @@ middle of the scale. | `new-lecture` | 🟦 `#0537E9` | A new lecture (the marquee outcome) | Brand-new lecture β€” proposed, in progress, or shipped. Use *instead of* `enhancement` | | `editor` | 🟩 `#0e8a16` | Requires editor review β€” final sign-off stage | Apply at handoff *after* team review; editor's queue = `org:QuantEcon label:editor`. Remove on sign-off | -This is **18 core labels + 2 lecture labels = 20**. The org-level default set -for new repositories is the **core 18**. +This is **19 core labels + 2 lecture labels = 21**. The org-level default set +for new repositories is the **core 19**. ### Labelling policy @@ -139,6 +145,12 @@ deliberately few: - **Exactly one Type label per issue,** chosen at triage. The Type labels are mutually exclusive on purpose so the boundary stays clean β€” `new-lecture` *instead of* `enhancement`, `infrastructure` *instead of* `maintenance`. + Cross-cutting labels (priority, `security`) sit *alongside* the Type label + and do not count against this rule. +- **`security` is a cross-cutting modifier, not a Type.** Add it alongside the + Type label when an issue has security implications (e.g. `bug` + `security` + + `high-priority`) β€” it signals a different review bar and makes + `org:QuantEcon label:security` work as a standing query. - **Priority labels mark only the outliers.** Most work sits at the unlabelled default; reach for `high-priority` / `low-priority` only when an item is genuinely off-centre. @@ -172,24 +184,47 @@ PR that *looks* mergeable (even approved) but must be held. `medium-priority` β†’ no label (unlabelled is the middle) Β· project / grouping labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels -(`colab`, …) β†’ `build-failure` Β· PR lifecycle β†’ native GitHub, as above. +(`colab`, …) β†’ `build-failure` Β· `testing` β†’ `infrastructure` or `maintenance` +(test work is not its own Type) Β· PR lifecycle β†’ native GitHub, as above. ### Scope -- **Lecture repos get all 20** (core 18 + the lecture extension). -- **Software / tooling repos get the core 18,** which is also the **org-level +- **Lecture repos get all 21** (core 19 + the lecture extension). +- **Software / tooling repos get the core 19,** which is also the **org-level default** for new repositories. - **Not touched:** `meta` (keeps its own `project` / `education` labels), translation forks (`translate:*`), and `*.notebooks` build repos. - **Applying the set is additive; pruning is separate and deliberate.** `qe gh labels sync` *guarantees* the standard set on a repo and renames known - variants in place (e.g. `linkchecker` β†’ `broken-links`, preserving every - existing issue and PR tag), but it **never blanket-prunes** bespoke local + variants in place (e.g. `linkchecker` β†’ `broken-links`, `high priority` β†’ + `high-priority`, preserving every existing issue and PR tag), but it **never + blanket-prunes** bespoke local labels that some repos legitimately rely on (`jax-conversion`, `colab`, `site-refresh`, `reading-group-*`). Removing non-standard labels is a **separate** `qe gh labels prune` step that reviews each one **one by one**, keeping the ones that still make sense locally. +### Machine-readable appendix + +The normative schema ships with this QEP as a companion file, +[`qep-0002-labels.yml`](qep-0002-labels.yml), co-located so that the standard +and the file tooling consumes are versioned together: under +[QEP-1's versioning rule](qep-0001-purpose-and-process.md), a change to a +machine-readable appendix is a **substantive amendment** that bumps this QEP's +`version`, and `version-hash` (a commit hash) pins both files at once. + +- **The yml is canonical for the machine-consumed fields** β€” name, colour, + description, group, scope, and who applies it. The tables above are the human + spec: rationale, colour semantics, and when-to-use guidance. +- **CI keeps the two in lockstep.** The `qep-checks` workflow fails any pull + request where the tables and the yml disagree, so drift is a red βœ—, not a + judgement call. +- **The yml carries only the standard itself.** Rename maps for historical + variants, retired-label handling, and pruning knowledge are operational + concerns of the tool implementing this QEP (`qe gh labels sync` / `prune` in + [`QuantEcon/cli`](https://github.com/QuantEcon/cli)), which reads the yml + from this repository rather than carrying its own copy. + ## Alternatives considered - **Fold `infrastructure` into `maintenance` (one Type label, not two).** The @@ -201,6 +236,14 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels differently β€” folding them together sinks "we re-architected CI across the lecture repos" into the same bucket as "bumped a pin." The split was **approved** in [#324](https://github.com/QuantEcon/meta/issues/324). +- **Make `security` a Type label, or leave it repo-local.** Field-tested on + `QuantEcon/actions`: security findings are orthogonal to Type β€” a + pwn-request pattern recommended in a README reads as `documentation`/`bug`, + root execution of external notebook code as `infrastructure` β€” so a + Type-level `security` would either break the one-Type rule or force a wrong + Type choice. Keeping it repo-local forfeits the org-wide standing query that + is most of its value. It joins the priority labels as a **cross-cutting + modifier** instead: exactly one Type label, plus optionally `security`. - **Add a `blocked` label.** Rejected in favour of native GitHub **issue dependencies** (generally available since Aug 2025), which record *what blocks what* rather than just *that* something is blocked, and avoid a second @@ -227,9 +270,10 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels 1. **Accept this QEP** to fix the names, colours, descriptions, and policy as the QuantEcon standard. -2. **Record the schema** as the single source of truth in - [`QuantEcon/cli`](https://github.com/QuantEcon/cli) - (`src/qe_cli/data/labels.yml`), with the per-label rationale and the +2. **Point tooling at the appendix.** The schema's single source of truth is + the co-located [`qep-0002-labels.yml`](qep-0002-labels.yml) (see + *Machine-readable appendix*); [`QuantEcon/cli`](https://github.com/QuantEcon/cli) + reads it from this repository rather than carrying its own copy, with the labelling policy mirrored in the labels guide. 3. **Apply it with `qe gh labels sync`** β€” additively, renaming known variants in place so issue and PR history is preserved. Removing any non-standard From 988f47ef5b330b1097b541cdb59543fe52fd6e02 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 11:39:13 +1000 Subject: [PATCH 04/24] QEP-2: Summary covers the full status-label retirement (Copilot review) The Summary described the behavioural change as moving only in-work/ready/review off labels; align it with the retire-table, which also covers blocked/on-hold via native issue dependencies. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 352d90b..9937585 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -26,9 +26,9 @@ This QEP defines a single standard set of GitHub labels for QuantEcon repositories, the meaning of each label, and the policy for how labels are applied. It standardises label **names, colours, and descriptions** so they mean the same thing everywhere, and it commits to **one behavioural change**: -the pull-request lifecycle (`in-work` / `ready` / `review`) moves off labels -and onto native GitHub features (Draft PRs, "Ready for review", review -requests). The set is defined as a universal **core** plus a small **lecture +the status labels (`in-work` / `ready` / `review` / `blocked`) are retired in +favour of native GitHub features (Draft PRs, "Ready for review", review +requests, issue dependencies). The set is defined as a universal **core** plus a small **lecture extension**, applied additively so existing issue and PR history is preserved. ## Motivation From 392b2792e2e7b6d54bcba47c3b0f7c07d58060c2 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 12:44:03 +1000 Subject: [PATCH 05/24] =?UTF-8?q?QEP-2:=20automation=20policy=20=E2=80=94?= =?UTF-8?q?=20at-most-one=20diagnostic,=20origin/task=20labels=20defer=20t?= =?UTF-8?q?o=20QEP-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the reports-activity field report and the namespace design discussion on the PR: the diagnostic quantifier becomes at most one (routine scheduled output diagnoses nothing); registered automations carry a durable origin label and may use transient /-namespaced task labels, legislated in QEP-4 rather than enumerated here; the grey colour claim is amended (grey = low-salience state, automation vs meta distinguished by text) with a band allocation rule; prune skips registered zones by rule. Records the retired closed-vocabulary rule in Alternatives. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 43 ++++++++++++++++++++----- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 9937585..c5a23a2 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -60,9 +60,10 @@ a [QEP](qep-0001-purpose-and-process.md) is for. ### The label set Labels are grouped by purpose. **Colour carries meaning where it helps:** -priority is a heat scale (hot red β†’ cool green), the automation/CI family -shares a quiet grey ("a machine made this"), and type/community keep -conventional GitHub colours. A label with no profile is **core** β€” meaningful +priority is a heat scale (hot red β†’ cool green), grey marks **low-salience +state** β€” machine output or a closing outcome, rather than triage signal β€” +with the automation and meta families distinguished by text, not hue, and +type/community keep conventional GitHub colours. A label with no profile is **core** β€” meaningful on any repository. Two labels form a **lecture extension** applied only to lecture repos. @@ -111,11 +112,16 @@ middle of the scale. | Label | Colour | Description | When to use | |---|---|---|---| -| `automated` | ⬜ `#ededed` | Opened by a bot or scheduled workflow | Every bot issue carries this **plus exactly one** diagnostic below | +| `automated` | ⬜ `#ededed` | Opened by a bot or scheduled workflow | Every bot issue or PR carries this, **plus at most one** diagnostic below | | `broken-links` | ⬜ `#dddddd` | Link checker found dead links | Applied by the link-checker action | | `build-failure` | ⬜ `#cccccc` | Notebook execution, build, or warnings failure | Applied by build / warnings checks | | `dependencies` | ⬜ `#bdbdbd` | Dependency or environment update (pip, conda, actions) | The single Dependabot label (replaces `github_actions` / `conda`) | +Automation labels take a grey in the band `#ededed`–`#bdbdbd`; a new origin +label or task-label family registered in +[QEP-4](qep-0004-automation-registry-and-label-coordination.md) takes the next +unused value in the band. + **Meta β€” closing outcomes** | Label | Colour | Description | When to use | @@ -159,9 +165,17 @@ deliberately few: **A `discuss` thread should not drift open indefinitely** β€” once it reaches a conclusion, summarise the decision and close, spawn a concrete follow-up issue, or escalate to a QEP if it crosses repos or changes team workflow. -- **Automation labels are bot-applied.** A bot-opened issue carries `automated` - plus exactly one diagnostic (`broken-links`, `build-failure`, or - `dependencies`); humans do not hand-apply these. +- **Automation labels are machine vocabulary.** Every bot-opened issue or PR + carries `automated`, plus **at most one** diagnostic (`broken-links`, + `build-failure`, `dependencies`) describing what the automation found β€” + routine scheduled output diagnoses nothing and carries no diagnostic. An + artifact created by a *registered* automation also carries that automation's + durable **origin label** (e.g. `action-translation`), and may carry transient + **task labels** under its registered `/`-namespace (e.g. `translate/review`). + Origin labels, task namespaces, and the coordination policy behind them are + defined in [QEP-4](qep-0004-automation-registry-and-label-coordination.md); + humans never hand-apply diagnostics or origin labels, and touch task labels + only as the owning automation's contract allows. - **`do-not-merge` is a voluntary hold,** distinct from being *blocked*: see the status-label change below. @@ -202,7 +216,11 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels labels that some repos legitimately rely on (`jax-conversion`, `colab`, `site-refresh`, `reading-group-*`). Removing non-standard labels is a **separate** `qe gh labels prune` step that reviews each one **one by one**, - keeping the ones that still make sense locally. + keeping the ones that still make sense locally. Origin labels and task + namespaces registered in + [QEP-4](qep-0004-automation-registry-and-label-coordination.md) are **skipped + by rule** β€” they are load-bearing routing keys with documented consumers, not + bespoke leftovers β€” and `sync` never touches a registered namespace. ### Machine-readable appendix @@ -236,6 +254,15 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's differently β€” folding them together sinks "we re-architected CI across the lecture repos" into the same bucket as "bumped a pin." The split was **approved** in [#324](https://github.com/QuantEcon/meta/issues/324). +- **A closed bot vocabulary (`automated` + exactly one diagnostic).** The first + draft's rule. Field use broke it twice: routine scheduled output (the weekly + status reports) diagnoses nothing, so "exactly one" would force a false + `build-failure` and poison it as a standing query; and automations that + coordinate through labels (`action-translation`, `status-report`) were + non-conformant by construction. The quantifier is now **at most one**, and + coordination labels are legislated in + [QEP-4](qep-0004-automation-registry-and-label-coordination.md) rather than + enumerated here. - **Make `security` a Type label, or leave it repo-local.** Field-tested on `QuantEcon/actions`: security findings are orthogonal to Type β€” a pwn-request pattern recommended in a README reads as `documentation`/`bug`, From c9c572b237ccd091dbd7b546f9b446c291db9eb0 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 12:59:30 +1000 Subject: [PATCH 06/24] =?UTF-8?q?QEP-2:=20Related=20cross-reference=20?= =?UTF-8?q?=E2=80=94=20header-table=20row,=20frontmatter=20mirror,=20CI=20?= =?UTF-8?q?parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header table's Related row is the primary human record (with the why); related: [N] in frontmatter is its machine mirror. check.mjs enforces lockstep (one without the other, differing sets, or a self-reference fails); a related QEP missing from the branch only warns, since in-flight drafts reference each other across branches. Editing related is editorial. Inline links remain the normative mechanism; no MyST admonitions, which don't render on github.com. Co-Authored-By: Claude Fable 5 --- .github/scripts/check.mjs | 37 +++++++++++++++++++++++++ .github/scripts/qeps.mjs | 12 ++++++++ qeps/qep-0002-standard-github-labels.md | 2 ++ 3 files changed, 51 insertions(+) diff --git a/.github/scripts/check.mjs b/.github/scripts/check.mjs index e94f3e4..d83c2e3 100644 --- a/.github/scripts/check.mjs +++ b/.github/scripts/check.mjs @@ -6,6 +6,7 @@ // 3. The README Type/Status/Version columns match each QEP's frontmatter. // Run by .github/workflows/qep-checks.yml. Exits non-zero on any failure. import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; import { FRONTMATTER, parseQep, qepFiles, readIndex, versionCell } from './qeps.mjs'; const base = process.env.BASE_REF || 'main'; @@ -103,6 +104,42 @@ for (const path of qepFiles()) { } } +// 4. `related` frontmatter stays in lockstep with the header table's +// **Related** row (the primary human record). Both absent is fine; one +// without the other, a self-reference, or differing QEP sets is an error. +// A related QEP whose file is not on this branch only warns β€” in-flight +// drafts may reference each other across branches; once both merge the +// warning disappears. +for (const path of qepFiles()) { + const q = parseQep(path); + const row = readFileSync(path, 'utf8').match(/^\|\s*\*\*Related\*\*\s*\|(.*)\|\s*$/m); + const inTable = row ? [...row[1].matchAll(/qep-(\d{4})/g)].map((m) => Number(m[1])) : []; + const inFm = q.related ?? []; + if (!row && inFm.length === 0) continue; + if (!row) { + errors.push(`${path}: has related: [...] frontmatter but no **Related** row in the header table`); + continue; + } + if (inFm.length === 0) { + errors.push(`${path}: has a **Related** header-table row but no related: [...] frontmatter`); + continue; + } + const fmSet = [...new Set(inFm)].sort((a, b) => a - b); + const tableSet = [...new Set(inTable)].sort((a, b) => a - b); + if (JSON.stringify(fmSet) !== JSON.stringify(tableSet)) { + errors.push( + `${path}: related frontmatter [${fmSet.join(', ')}] != header-table Related row [${tableSet.join(', ')}]`, + ); + } + for (const n of fmSet) { + if (n === q.qep) { + errors.push(`${path}: related lists itself (QEP-${n})`); + } else if (!qepFiles().some((p) => p.includes(`/qep-${String(n).padStart(4, '0')}-`))) { + console.warn(`WARN ${path}: related QEP-${n} not on this branch (in-flight draft?)`); + } + } +} + if (errors.length) { console.error('QEP checks failed:\n' + errors.map((e) => ` - ${e}`).join('\n')); process.exit(1); diff --git a/.github/scripts/qeps.mjs b/.github/scripts/qeps.mjs index 7cf12e4..7c4dfa0 100644 --- a/.github/scripts/qeps.mjs +++ b/.github/scripts/qeps.mjs @@ -44,6 +44,17 @@ export function parseQep(path) { version = Number(v[1]); } + // Optional `related: [N, ...]` β€” QEPs this one is paired with. The header + // table's **Related** row is the primary human record; check.mjs keeps the + // two in lockstep. Editing `related` is editorial (no normative content). + const rawRelated = field(block, 'related'); + let related; + if (rawRelated !== undefined && rawRelated !== '') { + const r = rawRelated.match(/^\[([0-9,\s]*)\]$/); + if (!r) throw new Error(`${path}: malformed "related: ${rawRelated}" (expected e.g. [2, 4])`); + related = r[1].split(',').map((s) => s.trim()).filter(Boolean).map(Number); + } + // `version-hash` may carry a trailing "# stamped by CI" signpost comment. const rawHash = field(block, 'version-hash'); let hash; @@ -60,6 +71,7 @@ export function parseQep(path) { title: stripQuotes(field(block, 'title')), status: stripQuotes(field(block, 'status')), type: stripQuotes(field(block, 'type')), + related, version, hash, }; diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index c5a23a2..eb2c3f8 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -6,6 +6,7 @@ status: Draft type: standard created: 2026-06-17 discussion: https://github.com/QuantEcon/meta/issues/324 +related: [4] --- # QEP-2: Standard GitHub Label Set and Labelling Policy @@ -19,6 +20,7 @@ discussion: https://github.com/QuantEcon/meta/issues/324 | **Type** | standard | | **Created** | 2026-06-17 | | **Discussion** | [QuantEcon/meta#324](https://github.com/QuantEcon/meta/issues/324) | +| **Related** | [QEP-4](qep-0004-automation-registry-and-label-coordination.md) β€” the automation registry and coordination policy this standard defers to | ## Summary From 5e7c8c18409b030db4e578fc1576e7d5f2ed7338 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 13:22:01 +1000 Subject: [PATCH 07/24] =?UTF-8?q?QEP-2:=20fix=20stale=20core-label=20count?= =?UTF-8?q?=20in=20Rollout=20(18=20=E2=86=92=2019);=20close=20grey-band=20?= =?UTF-8?q?allocation=20ambiguity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rollout step 5 predated the security modifier's addition to the core set. The band rule now says 'unused by any label', since duplicate's #cfd3d7 sits numerically inside #ededed–#bdbdbd. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index eb2c3f8..a9b957a 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -122,7 +122,8 @@ middle of the scale. Automation labels take a grey in the band `#ededed`–`#bdbdbd`; a new origin label or task-label family registered in [QEP-4](qep-0004-automation-registry-and-label-coordination.md) takes the next -unused value in the band. +value in the band not already used by *any* label β€” the meta grey `duplicate` +`#cfd3d7` also sits inside it. **Meta β€” closing outcomes** @@ -315,7 +316,7 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's `lecture-jax`) and the candidates (`lecture-julia.myst`, `lecture-datascience.myst`, `lecture-stats`, `continuous_time_mcs`, `lecture-wasm`), then the software / tooling repos. -5. **Set the org-level default labels** for new repositories to the core 18 +5. **Set the org-level default labels** for new repositories to the core 19 (a manual settings change β€” there is no public API for org defaults). 6. **Point contributors at the guide** so triage follows the policy above, and close the earlier unification attempts (#178, #290) against this outcome. From dea698c8e09a4af84043d5141ec697ec7f011104 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 13:35:26 +1000 Subject: [PATCH 08/24] QEP-2: tier-neutral core descriptions; build-failure scope clarified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The description field is machine-consumed β€” qe gh labels sync writes it to every repo adopting the core set, including tooling repos and the org default β€” so bug / enhancement / documentation lose their lecture wording and the lecture guidance moves to the human-only When-to-use column. build-failure drops 'Notebook' from the description, and its When-to-use now states the boundary: scheduled checks label the issues they open; a failing check on a PR carries no label (the red βœ— is the signal). Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-labels.yml | 8 ++++---- qeps/qep-0002-standard-github-labels.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qeps/qep-0002-labels.yml b/qeps/qep-0002-labels.yml index 085b84a..1baad1e 100644 --- a/qeps/qep-0002-labels.yml +++ b/qeps/qep-0002-labels.yml @@ -17,17 +17,17 @@ labels: # Type β€” what kind of work is this? (one per issue, set at triage) - name: bug color: d73a4a - description: Something is wrong or broken in a lecture or build + description: Something is wrong or broken group: type scope: core - name: enhancement color: a2eeef - description: Improvement to existing material + description: Improvement to existing content or functionality group: type scope: core - name: documentation color: b08968 - description: Repo docs and contributor meta (not lecture content) + description: Repo docs and contributor meta group: type scope: core - name: infrastructure @@ -104,7 +104,7 @@ labels: applied-by: bot - name: build-failure color: cccccc - description: Notebook execution, build, or warnings failure + description: Execution, build, or warnings failure group: automation scope: core applied-by: bot diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index a9b957a..ab095d7 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -73,9 +73,9 @@ lecture repos. | Label | Colour | Description | When to use | |---|---|---|---| -| `bug` | πŸŸ₯ `#d73a4a` | Something is wrong or broken in a lecture or build | The content is *incorrect*: wrong maths, erroring code, broken rendering | -| `enhancement` | 🟦 `#a2eeef` | Improvement to existing material | Better exposition, a new exercise, improved figures | -| `documentation` | 🟫 `#b08968` | Repo docs and contributor meta (not lecture content) | READMEs, CONTRIBUTING β€” *about the repo*; lecture content is never `documentation` | +| `bug` | πŸŸ₯ `#d73a4a` | Something is wrong or broken | The content is *incorrect*: wrong maths, erroring code, broken rendering β€” in a lecture or a build | +| `enhancement` | 🟦 `#a2eeef` | Improvement to existing content or functionality | Better exposition, a new exercise, improved figures | +| `documentation` | 🟫 `#b08968` | Repo docs and contributor meta | READMEs, CONTRIBUTING β€” *about the repo*; lecture content is never `documentation` | | `infrastructure` | 🟦 `#1d3c78` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped β€” it would appear in a release note. Use *instead of* `maintenance` | | `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn that would never appear in a release note ("rendered lectures look identical after") | | `question` | πŸŸͺ `#d876e3` | Someone needs an answer or clarification | Terminal state: *answered*. Swap for a work type once it becomes agreed work | @@ -116,7 +116,7 @@ middle of the scale. |---|---|---|---| | `automated` | ⬜ `#ededed` | Opened by a bot or scheduled workflow | Every bot issue or PR carries this, **plus at most one** diagnostic below | | `broken-links` | ⬜ `#dddddd` | Link checker found dead links | Applied by the link-checker action | -| `build-failure` | ⬜ `#cccccc` | Notebook execution, build, or warnings failure | Applied by build / warnings checks | +| `build-failure` | ⬜ `#cccccc` | Execution, build, or warnings failure | Applied by scheduled build / warnings checks to the issues they open. A failing check on a PR needs no label β€” the red βœ— is the signal | | `dependencies` | ⬜ `#bdbdbd` | Dependency or environment update (pip, conda, actions) | The single Dependabot label (replaces `github_actions` / `conda`) | Automation labels take a grey in the band `#ededed`–`#bdbdbd`; a new origin From 091eeea536cd690c7b3814780c1eb28c1b188a11 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 14:13:15 +1000 Subject: [PATCH 09/24] =?UTF-8?q?QEP-2:=20self-contained=20=E2=80=94=20QEP?= =?UTF-8?q?-4=20hooks=20move=20to=20QEP-4's=20acceptance=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-QEP process pivot: the QEP that creates a dependency ships the amendments to the standards it touches, in its own PR (QEP-1's amend-in-place model). QEP-2 therefore carries no forward references to the in-flight automation-registry QEP: the Related pairing, the origin/task-label sentences, the prune skip-rule, and the grey-band registration clause come back as amendments in that QEP's landing PR, in whatever form its review settles. Until then main never holds a dangling link, and this standard asserts nothing still under review elsewhere. The removed wording is preserved verbatim on the qep-0004 branch. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 32 +++++++------------------ 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index ab095d7..22e0b1a 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -6,7 +6,6 @@ status: Draft type: standard created: 2026-06-17 discussion: https://github.com/QuantEcon/meta/issues/324 -related: [4] --- # QEP-2: Standard GitHub Label Set and Labelling Policy @@ -20,7 +19,6 @@ related: [4] | **Type** | standard | | **Created** | 2026-06-17 | | **Discussion** | [QuantEcon/meta#324](https://github.com/QuantEcon/meta/issues/324) | -| **Related** | [QEP-4](qep-0004-automation-registry-and-label-coordination.md) β€” the automation registry and coordination policy this standard defers to | ## Summary @@ -119,11 +117,9 @@ middle of the scale. | `build-failure` | ⬜ `#cccccc` | Execution, build, or warnings failure | Applied by scheduled build / warnings checks to the issues they open. A failing check on a PR needs no label β€” the red βœ— is the signal | | `dependencies` | ⬜ `#bdbdbd` | Dependency or environment update (pip, conda, actions) | The single Dependabot label (replaces `github_actions` / `conda`) | -Automation labels take a grey in the band `#ededed`–`#bdbdbd`; a new origin -label or task-label family registered in -[QEP-4](qep-0004-automation-registry-and-label-coordination.md) takes the next -value in the band not already used by *any* label β€” the meta grey `duplicate` -`#cfd3d7` also sits inside it. +Automation labels take a grey in the band `#ededed`–`#bdbdbd`; a new automation +label takes the next value in the band not already used by *any* label β€” the +meta grey `duplicate` `#cfd3d7` also sits inside it. **Meta β€” closing outcomes** @@ -171,14 +167,8 @@ deliberately few: - **Automation labels are machine vocabulary.** Every bot-opened issue or PR carries `automated`, plus **at most one** diagnostic (`broken-links`, `build-failure`, `dependencies`) describing what the automation found β€” - routine scheduled output diagnoses nothing and carries no diagnostic. An - artifact created by a *registered* automation also carries that automation's - durable **origin label** (e.g. `action-translation`), and may carry transient - **task labels** under its registered `/`-namespace (e.g. `translate/review`). - Origin labels, task namespaces, and the coordination policy behind them are - defined in [QEP-4](qep-0004-automation-registry-and-label-coordination.md); - humans never hand-apply diagnostics or origin labels, and touch task labels - only as the owning automation's contract allows. + routine scheduled output diagnoses nothing and carries no diagnostic. Humans + never hand-apply automation labels. - **`do-not-merge` is a voluntary hold,** distinct from being *blocked*: see the status-label change below. @@ -219,11 +209,7 @@ labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels labels that some repos legitimately rely on (`jax-conversion`, `colab`, `site-refresh`, `reading-group-*`). Removing non-standard labels is a **separate** `qe gh labels prune` step that reviews each one **one by one**, - keeping the ones that still make sense locally. Origin labels and task - namespaces registered in - [QEP-4](qep-0004-automation-registry-and-label-coordination.md) are **skipped - by rule** β€” they are load-bearing routing keys with documented consumers, not - bespoke leftovers β€” and `sync` never touches a registered namespace. + keeping the ones that still make sense locally. ### Machine-readable appendix @@ -263,9 +249,9 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's `build-failure` and poison it as a standing query; and automations that coordinate through labels (`action-translation`, `status-report`) were non-conformant by construction. The quantifier is now **at most one**, and - coordination labels are legislated in - [QEP-4](qep-0004-automation-registry-and-label-coordination.md) rather than - enumerated here. + coordination labels (automation origin labels and task `/`-namespaces) are + spun out to a dedicated automation-registry QEP rather than enumerated here; + when that QEP is accepted, its PR amends this one with the registry hooks. - **Make `security` a Type label, or leave it repo-local.** Field-tested on `QuantEcon/actions`: security findings are orthogonal to Type β€” a pwn-request pattern recommended in a README reads as `documentation`/`bug`, From 27f1a84c28c409589830da7dde3522fb494925e2 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 15:23:00 +1000 Subject: [PATCH 10/24] QEP-2: fold in the triage-pass field-report outcomes Three policy-prose clarifications from applying the draft to a real 92-finding triage pass (see the field report on #2); no yml changes, set stays 19 core + 2 lecture: - infrastructure vs maintenance: state the axis explicitly - the boundary is effort, not subject (release-note test decides). - documentation: generalise the lecture rule - product content is never documentation, even where the product is docs; defects and gaps take bug / maintenance / enhancement. - sub-issue parents: structure, not work - exempt from the one-Type rule and not "needs triage" when unlabelled. No tracking label: has:sub-issue works in the advanced issue search org-wide, so discovery is native; legacy REST search caveat noted for tooling. security stays a single broad cross-cutting modifier (no escalation carve-out - incident practice is out of scope for a label spec). Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 22e0b1a..d4be4da 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -73,7 +73,7 @@ lecture repos. |---|---|---|---| | `bug` | πŸŸ₯ `#d73a4a` | Something is wrong or broken | The content is *incorrect*: wrong maths, erroring code, broken rendering β€” in a lecture or a build | | `enhancement` | 🟦 `#a2eeef` | Improvement to existing content or functionality | Better exposition, a new exercise, improved figures | -| `documentation` | 🟫 `#b08968` | Repo docs and contributor meta | READMEs, CONTRIBUTING β€” *about the repo*; lecture content is never `documentation` | +| `documentation` | 🟫 `#b08968` | Repo docs and contributor meta | READMEs, CONTRIBUTING β€” *about the repo*. Product content is never `documentation`, even where the product *is* docs (lectures, the manual): wrong content is `bug`, routine sweeps are `maintenance`, gaps and improvements are `enhancement` | | `infrastructure` | 🟦 `#1d3c78` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped β€” it would appear in a release note. Use *instead of* `maintenance` | | `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn that would never appear in a release note ("rendered lectures look identical after") | | `question` | πŸŸͺ `#d876e3` | Someone needs an answer or clarification | Terminal state: *answered*. Swap for a work type once it becomes agreed work | @@ -152,6 +152,17 @@ deliberately few: *instead of* `enhancement`, `infrastructure` *instead of* `maintenance`. Cross-cutting labels (priority, `security`) sit *alongside* the Type label and do not count against this rule. +- **The `infrastructure` / `maintenance` boundary is effort, not subject.** + Routine upkeep of CI is `maintenance` (an action version bump); substantial + CI work is `infrastructure` (rewriting a deploy job) β€” the release-note test + decides, not which files were touched. +- **A sub-issue parent is structure, not work.** An umbrella issue that groups + work via native sub-issues is exempt from the one-Type rule, and unlabelled + does not mean "needs triage" β€” the sub-issue relationship is the + machine-checkable signal distinguishing a tracker from an untriaged issue. + Discovery is native too: `has:sub-issue` in the advanced issue search, which + works org-wide (tooling note: legacy REST search silently ignores the + qualifier β€” programmatic discovery must use the advanced search type). - **`security` is a cross-cutting modifier, not a Type.** Add it alongside the Type label when an issue has security implications (e.g. `bug` + `security` + `high-priority`) β€” it signals a different review bar and makes @@ -190,7 +201,9 @@ PR that *looks* mergeable (even approved) but must be held. ### What we deliberately don't label `medium-priority` β†’ no label (unlabelled is the middle) Β· project / grouping -labels (`reading-group-*`) β†’ **Milestones** Β· per-tool diagnostic labels +labels (`reading-group-*`) β†’ **Milestones** Β· umbrella / tracking issues β†’ +native **sub-issues** (the parent carries no Type, see the policy above) Β· +per-tool diagnostic labels (`colab`, …) β†’ `build-failure` Β· `testing` β†’ `infrastructure` or `maintenance` (test work is not its own Type) Β· PR lifecycle β†’ native GitHub, as above. From 16559e452f68801faa330e75f45c51e3525d90d2 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 15:35:07 +1000 Subject: [PATCH 11/24] QEP-2: Rollout states mechanism and guarantees; execution moves to a tracking issue The sequenced checklist (CLI integration, pilot target, widening order, org defaults, closing meta#178 / meta#290) goes stale as steps complete and would force no-op amendments of an accepted standard. It moves to a tracking issue opened at acceptance - a sub-issue parent, dogfooding the new umbrella-issue policy. Rollout keeps the durable content: additive history-preserving sync from the co-located appendix, one-by-one prune, pilot-first ordering, core-19 org default. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 40 +++++++++++-------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index d4be4da..4f01e8c 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -297,25 +297,21 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's ## Rollout -1. **Accept this QEP** to fix the names, colours, descriptions, and policy as - the QuantEcon standard. -2. **Point tooling at the appendix.** The schema's single source of truth is - the co-located [`qep-0002-labels.yml`](qep-0002-labels.yml) (see - *Machine-readable appendix*); [`QuantEcon/cli`](https://github.com/QuantEcon/cli) - reads it from this repository rather than carrying its own copy, with the - labelling policy mirrored in the labels guide. -3. **Apply it with `qe gh labels sync`** β€” additively, renaming known variants - in place so issue and PR history is preserved. Removing any non-standard - labels is a separate, deliberate `qe gh labels prune` pass, reviewed one by - one. -4. **Pilot on one repo first, then widen.** The first rollout target is - **`lecture-python-programming`**; once it's validated, roll out more widely - to the rest of the confirmed lecture repos (`lecture-python-intro`, - `lecture-python.myst`, `lecture-python-advanced.myst`, `lecture-dp`, - `lecture-jax`) and the candidates (`lecture-julia.myst`, - `lecture-datascience.myst`, `lecture-stats`, `continuous_time_mcs`, - `lecture-wasm`), then the software / tooling repos. -5. **Set the org-level default labels** for new repositories to the core 19 - (a manual settings change β€” there is no public API for org defaults). -6. **Point contributors at the guide** so triage follows the policy above, and - close the earlier unification attempts (#178, #290) against this outcome. +Acceptance fixes the names, colours, descriptions, and policy above as the +QuantEcon standard. Implementation is `qe gh labels sync` in +[`QuantEcon/cli`](https://github.com/QuantEcon/cli), reading the co-located +[`qep-0002-labels.yml`](qep-0002-labels.yml) from this repository (see +*Machine-readable appendix*) and applying it **additively** β€” known variants +are renamed in place so issue and PR history is preserved. Removing +non-standard labels is the separate, deliberate `qe gh labels prune` pass, +reviewed one label at a time. Rollout is **pilot-first**: validate on a single +lecture repo before widening to the remaining lecture repos and then the +software / tooling repos. The org-level default label set for new repositories +is the core 19 (a manual settings change β€” there is no public API for org +defaults). + +The sequenced execution checklist β€” the CLI integration, the pilot target, +widening order, org defaults, closing the earlier unification attempts +(meta#178, meta#290) β€” lives in a **tracking issue opened at acceptance** (a +sub-issue parent, per the labelling policy above), not in this document, so +completing a step never requires amending the standard. From 7bdd1045abe7544557b29dcbe6c7fb69462513a9 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 15:45:24 +1000 Subject: [PATCH 12/24] QEP-2: Rollout in normative mood - obligations, not predictions Two phrases read as claims about future events (that the CLI tool is the implementation; that a tracking issue is opened at acceptance). Recast as conditional obligations so nothing in the section can be falsified by work not happening: applying the set reads the appendix from this repo and is additive; the execution checklist belongs in a tracking issue rather than this document. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 4f01e8c..9f8ac63 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -298,11 +298,12 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's ## Rollout Acceptance fixes the names, colours, descriptions, and policy above as the -QuantEcon standard. Implementation is `qe gh labels sync` in -[`QuantEcon/cli`](https://github.com/QuantEcon/cli), reading the co-located +QuantEcon standard. Applying the set to a repo is +done with tooling that reads the co-located [`qep-0002-labels.yml`](qep-0002-labels.yml) from this repository (see -*Machine-readable appendix*) and applying it **additively** β€” known variants -are renamed in place so issue and PR history is preserved. Removing +*Machine-readable appendix*) β€” `qe gh labels sync` in +[`QuantEcon/cli`](https://github.com/QuantEcon/cli) β€” and is **additive**: +known variants are renamed in place so issue and PR history is preserved. Removing non-standard labels is the separate, deliberate `qe gh labels prune` pass, reviewed one label at a time. Rollout is **pilot-first**: validate on a single lecture repo before widening to the remaining lecture repos and then the @@ -312,6 +313,6 @@ defaults). The sequenced execution checklist β€” the CLI integration, the pilot target, widening order, org defaults, closing the earlier unification attempts -(meta#178, meta#290) β€” lives in a **tracking issue opened at acceptance** (a -sub-issue parent, per the labelling policy above), not in this document, so -completing a step never requires amending the standard. +(meta#178, meta#290) β€” belongs in a **tracking issue** (a sub-issue parent, per the +labelling policy above), not in this document β€” so completing, reordering, or +dropping a step never requires amending the standard. From e8728ff77f6c48690b1d84cb3fa7b9c79595399a Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 15:51:12 +1000 Subject: [PATCH 13/24] QEP-1 v2 (carried by QEP-2): rename the Rollout section to Adoption Adoption names what accepting a QEP requires - mechanism, tooling, and the guarantees any implementation must honour - as obligations rather than a plan, so nothing in the section can be falsified by work not happening. Sequenced execution (who does what, when) belongs in a tracking issue, per the convention QEP-2 established. Renames the heading in template.md (with matching guidance text), QEP-1's format rule and its own section (adding the (v2) entry to its adoption record), and QEP-2; bumps QEP-1 to version 2 and syncs the README index. version-hash restamps at merge. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- qeps/qep-0001-purpose-and-process.md | 16 +++++++++++----- qeps/qep-0002-standard-github-labels.md | 4 ++-- qeps/template.md | 7 ++++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a5b3f27..fcf8f84 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ need a QEP. | QEP | Title | Type | Status | Version | |-----|-------|------|--------|---------| -| [QEP-1](qeps/qep-0001-purpose-and-process.md) | QEP Purpose and Process | process | Accepted | v1 | +| [QEP-1](qeps/qep-0001-purpose-and-process.md) | QEP Purpose and Process | process | Accepted | v2 | | [QEP-2](qeps/qep-0002-standard-github-labels.md) | Standard GitHub Label Set and Labelling Policy | standard | Draft | – | QEPs that set an ongoing rule are **maintained in place**: a substantive amendment bumps diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index 95e7d3a..6756931 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -4,7 +4,7 @@ title: QEP Purpose and Process author: "@mmcky" status: Accepted type: process -version: 1 +version: 2 version-hash: 0bff77a # stamped by CI; do not edit created: 2026-06-16 discussion: https://github.com/QuantEcon/meta/issues/325 @@ -19,7 +19,7 @@ discussion: https://github.com/QuantEcon/meta/issues/325 | **Author** | @mmcky | | **Status** | Accepted | | **Type** | process | -| **Version** | 1 | +| **Version** | 2 | | **Created** | 2026-06-16 | | **Discussion** | [QuantEcon/meta#325](https://github.com/QuantEcon/meta/issues/325) | @@ -155,7 +155,7 @@ From `v1` onward a sibling `version-hash` field carries the short commit hash th anchors the revision to git history: ```yaml -version: 1 +version: 2 version-hash: a1b2c3d # stamped by CI; do not edit ``` @@ -240,7 +240,7 @@ Each QEP is a Markdown file with YAML frontmatter (`qep`, `title`, `author`, `st `type`, `created`, `discussion` β€” plus `version` and its CI-stamped `version-hash`, which sit just after `type` once the QEP is first amended) followed by the sections in [`qeps/template.md`](../qeps/template.md): **Summary, Motivation, Proposal, Alternatives -considered, Rollout**. The `type` field describes the **kind of content** the QEP +considered, Adoption**. The `type` field describes the **kind of content** the QEP carries: - **`standard`** β€” a normative spec or rule you conform to (a label schema, a style @@ -279,7 +279,7 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or instead, surfaced on the site by the theme's history feature and on GitHub by history/blame. -## Rollout +## Adoption 1. **(v0) Establish the process.** Merge this QEP to set the process; re-record the label-set decision @@ -296,3 +296,9 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or author-side steps in `AGENTS.md`; and set the repository to squash-merge only. Adopting this mechanism is QEP-1's own first substantive amendment, so QEP-1 becomes **v1**. +3. **(v2) Rename this section: `Rollout` becomes `Adoption`.** The section states + what adopting a QEP requires β€” mechanism, tooling, and the guarantees any + implementation must honour β€” as obligations rather than a plan, so nothing in it + can be falsified by work not happening; sequenced execution (who does what, when) + belongs in a tracking issue. Applied first by QEP-2, whose acceptance PR carries + this amendment. diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 9f8ac63..bb950e5 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -295,7 +295,7 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's `documentation` is a tier-2, about-the-repo concern, so it moves to tan `#b08968` to read as distinct at a glance. -## Rollout +## Adoption Acceptance fixes the names, colours, descriptions, and policy above as the QuantEcon standard. Applying the set to a repo is @@ -305,7 +305,7 @@ done with tooling that reads the co-located [`QuantEcon/cli`](https://github.com/QuantEcon/cli) β€” and is **additive**: known variants are renamed in place so issue and PR history is preserved. Removing non-standard labels is the separate, deliberate `qe gh labels prune` pass, -reviewed one label at a time. Rollout is **pilot-first**: validate on a single +reviewed one label at a time. Adoption is **pilot-first**: validate on a single lecture repo before widening to the remaining lecture repos and then the software / tooling repos. The org-level default label set for new repositories is the core 19 (a manual settings change β€” there is no public API for org diff --git a/qeps/template.md b/qeps/template.md index ecdccb2..b63c5e4 100644 --- a/qeps/template.md +++ b/qeps/template.md @@ -44,7 +44,8 @@ that becomes binding once the QEP is Accepted. The main options you weighed and why they were not chosen. This section is where much of a QEP's long-term value lives. -## Rollout +## Adoption -How the decision gets implemented: migration steps, tooling, who does what, and any -timeline. Omit if not applicable. +What adopting the decision requires: the mechanism, tooling, and the guarantees any +implementation must honour β€” written as obligations, not a plan. Sequenced execution +(who does what, when) belongs in a tracking issue, not the QEP. Omit if not applicable. From 2a3d0203a011dd4695d201d96c89e33aedd48bae Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 15:56:45 +1000 Subject: [PATCH 14/24] QEP-2: Motivation states the case, not the history Drop the fourth-attempt narrative, the discovered-mismatch anecdote, and the closing paragraph justifying use of the QEP process itself - the QEP process is assumed. Motivation now states the drift and the three things one standard set fixes: consistent triage, a stable label contract for automation, and shared cross-repo workflow. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 34 ++++++++++--------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index bb950e5..734bb0a 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -34,26 +34,20 @@ extension**, applied additively so existing issue and PR history is preserved. ## Motivation QuantEcon's repositories have drifted into inconsistent, overlapping label -vocabularies. The same idea appears under different names (`improve` / -`content` / `teaching` vs `enhancement`; `linkchecker` vs a link-checker -label), priority is expressed three different ways, and bot-generated noise is -hard to tell from human triage. This is the fourth attempt to unify them, and -the cost of *not* settling it has grown beyond tidiness: - -- **Triage is ambiguous.** A contributor cannot tell from the label list what - "kind" of work an issue is, or whether it has been triaged at all. -- **Automation reads labels and the names don't match.** The org activity - reports and the quantecon.org/news drafts key off labels to rank and surface - work; while wiring these up we found they were boosting on names that **do - not exist anywhere** (`priority: high`, `feature`, `chore`, `docs`) while the - real labels (`high-priority`, `enhancement`, `documentation`) went unread. A - standard set gives the reporting a stable contract. -- **Cross-repo work has no shared shape.** Editorial sign-off, newcomer - discovery, and "do not merge yet" all need to mean the same thing across - every lecture repo. - -A standard set is a cross-repository, team-wide decision, which is exactly what -a [QEP](qep-0001-purpose-and-process.md) is for. +vocabularies: the same idea appears under different names (`improve` / +`content` / `teaching` vs `enhancement`), priority is expressed several +different ways, and bot-generated output is hard to tell from human triage. +One standard set, defined once for the whole organisation, fixes three +things: + +- **Triage.** A contributor can tell from the label list what kind of work an + issue is and whether it has been triaged β€” and the answer reads the same in + every repository. +- **Automation.** The org activity reports and the quantecon.org/news drafts + rank and surface work by label; a standard set gives them a stable contract + of names to read. +- **Cross-repo workflow.** Editorial sign-off, newcomer discovery, and "do not + merge yet" mean the same thing across every repository. ## Proposal From ea3b943e39b975360f1c0c360de363dce0aca83e Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 15:59:20 +1000 Subject: [PATCH 15/24] QEP-2: Motivation leads with common meaning Promote the core point to its own dot point - one label, one meaning, org-wide - absorbing the cross-repo workflow examples; Triage and Automation stay single-purpose. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 734bb0a..a9a9111 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -40,14 +40,14 @@ different ways, and bot-generated output is hard to tell from human triage. One standard set, defined once for the whole organisation, fixes three things: +- **Common meaning.** A label carries the same name, colour, and meaning in + every repository β€” editorial sign-off, newcomer discovery, and "do not + merge yet" read identically wherever they appear. - **Triage.** A contributor can tell from the label list what kind of work an - issue is and whether it has been triaged β€” and the answer reads the same in - every repository. + issue is and whether it has been triaged. - **Automation.** The org activity reports and the quantecon.org/news drafts rank and surface work by label; a standard set gives them a stable contract of names to read. -- **Cross-repo workflow.** Editorial sign-off, newcomer discovery, and "do not - merge yet" mean the same thing across every repository. ## Proposal From 0d855bb1f5d037a53b19c8db5973328bc7c9f1c0 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 16:06:47 +1000 Subject: [PATCH 16/24] =?UTF-8?q?QEP-2:=20discuss=20swatch=20=F0=9F=92=97?= =?UTF-8?q?=20=E2=86=92=20=F0=9F=9F=AA=20=E2=80=94=20swatches=20mark=20col?= =?UTF-8?q?our=20family,=20not=20sentiment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same square-sharing precedent as the four automation greys under ⬜; the hex is the definition, the emoji only the family. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index a9a9111..dd0afcf 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -71,7 +71,7 @@ lecture repos. | `infrastructure` | 🟦 `#1d3c78` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped β€” it would appear in a release note. Use *instead of* `maintenance` | | `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn that would never appear in a release note ("rendered lectures look identical after") | | `question` | πŸŸͺ `#d876e3` | Someone needs an answer or clarification | Terminal state: *answered*. Swap for a work type once it becomes agreed work | -| `discuss` | πŸ’— `#f904a0` | Open-ended team deliberation or a decision to be made | No single right answer. Reuses meta's existing `discuss` colour | +| `discuss` | πŸŸͺ `#f904a0` | Open-ended team deliberation or a decision to be made | No single right answer. Reuses meta's existing `discuss` colour | **Priority β€” a heat scale (unlabelled = normal priority)** From 2bf6f0f8af92ba59549a967b0200a6ce2a616d5b Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 16:12:31 +1000 Subject: [PATCH 17/24] QEP-2: meta joins the core-19 scope, keeping its unique local labels Previously fully carved out under Not touched, which left a hole in the org-wide standing queries (security, discuss, question) exactly where their traffic is heaviest. The additive sync plus the bespoke prune allowance already make inclusion safe: project / education survive like jax-conversion and reading-group-* do elsewhere. A meta-specific extension was considered and rejected - a profile of one repo standardises single-repo vocabulary at QEP amendment cost. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index dd0afcf..ddac612 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -205,9 +205,11 @@ per-tool diagnostic labels - **Lecture repos get all 21** (core 19 + the lecture extension). - **Software / tooling repos get the core 19,** which is also the **org-level - default** for new repositories. -- **Not touched:** `meta` (keeps its own `project` / `education` labels), - translation forks (`translate:*`), and `*.notebooks` build repos. + default** for new repositories. This includes `meta`, which may keep unique + local labels (`project`, `education`) to triage its own kinds of work + alongside the standard set. +- **Not touched:** translation forks (`translate:*`) and `*.notebooks` build + repos. - **Applying the set is additive; pruning is separate and deliberate.** `qe gh labels sync` *guarantees* the standard set on a repo and renames known variants in place (e.g. `linkchecker` β†’ `broken-links`, `high priority` β†’ From 29b5518771306f281a504582f52bd451988b7f1a Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 16:14:48 +1000 Subject: [PATCH 18/24] QEP-2: Alternatives keep the merits, drop the archaeology Each entry now states only why the rejected option loses; approval citations, draft-evolution narrative, and field-test attributions move to git history and the PR thread where they already live. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index ddac612..c8a8c6f 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -250,19 +250,15 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's build-system change, new automation), whereas `maintenance` is routine, invisible churn (style, env bumps). The activity reports already surface them differently β€” folding them together sinks "we re-architected CI across the - lecture repos" into the same bucket as "bumped a pin." The split was - **approved** in [#324](https://github.com/QuantEcon/meta/issues/324). -- **A closed bot vocabulary (`automated` + exactly one diagnostic).** The first - draft's rule. Field use broke it twice: routine scheduled output (the weekly - status reports) diagnoses nothing, so "exactly one" would force a false - `build-failure` and poison it as a standing query; and automations that - coordinate through labels (`action-translation`, `status-report`) were - non-conformant by construction. The quantifier is now **at most one**, and - coordination labels (automation origin labels and task `/`-namespaces) are - spun out to a dedicated automation-registry QEP rather than enumerated here; - when that QEP is accepted, its PR amends this one with the registry hooks. -- **Make `security` a Type label, or leave it repo-local.** Field-tested on - `QuantEcon/actions`: security findings are orthogonal to Type β€” a + lecture repos" into the same bucket as "bumped a pin." +- **A closed bot vocabulary (`automated` + exactly one diagnostic).** Rejected: + routine scheduled output diagnoses nothing, so a mandatory diagnostic forces + a false `build-failure` and poisons it as a standing query β€” the quantifier + is **at most one**. Labels automations use to *coordinate* (origin labels, + task namespaces) evolve with their tooling and belong to a dedicated + automation-registry QEP, not to enumeration here. +- **Make `security` a Type label, or leave it repo-local.** Rejected: security + findings are orthogonal to Type β€” a pwn-request pattern recommended in a README reads as `documentation`/`bug`, root execution of external notebook code as `infrastructure` β€” so a Type-level `security` would either break the one-Type rule or force a wrong From 0d3f0ffcc406cf90744a8a519a4bbf1ecb8e4112 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 23 Jul 2026 16:16:40 +1000 Subject: [PATCH 19/24] QEP-2: final pass - Summary states the shape, wording and wrap cleanups Summary drops the adoption-mechanics clause (Scope and Adoption carry the additive/history guarantee in full); the colour intro loses the CLI-speak 'profile' and a mid-list grammar wobble; rewrap debris from successive edits tidied in the don't-label list, the security alternative, and the Adoption paragraphs. Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 38 ++++++++++++------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index c8a8c6f..468ea66 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -28,8 +28,8 @@ applied. It standardises label **names, colours, and descriptions** so they mean the same thing everywhere, and it commits to **one behavioural change**: the status labels (`in-work` / `ready` / `review` / `blocked`) are retired in favour of native GitHub features (Draft PRs, "Ready for review", review -requests, issue dependencies). The set is defined as a universal **core** plus a small **lecture -extension**, applied additively so existing issue and PR history is preserved. +requests, issue dependencies). The set is defined as a universal **core** plus +a small **lecture extension** for lecture repos. ## Motivation @@ -57,9 +57,9 @@ Labels are grouped by purpose. **Colour carries meaning where it helps:** priority is a heat scale (hot red β†’ cool green), grey marks **low-salience state** β€” machine output or a closing outcome, rather than triage signal β€” with the automation and meta families distinguished by text, not hue, and -type/community keep conventional GitHub colours. A label with no profile is **core** β€” meaningful -on any repository. Two labels form a **lecture extension** applied only to -lecture repos. +type and community labels keep conventional GitHub colours. Every label is +**core** β€” meaningful on any repository β€” except the two that form the +**lecture extension**, applied only to lecture repos. **Type β€” what kind of work is this? (one per issue, set at triage)** @@ -197,8 +197,8 @@ PR that *looks* mergeable (even approved) but must be held. `medium-priority` β†’ no label (unlabelled is the middle) Β· project / grouping labels (`reading-group-*`) β†’ **Milestones** Β· umbrella / tracking issues β†’ native **sub-issues** (the parent carries no Type, see the policy above) Β· -per-tool diagnostic labels -(`colab`, …) β†’ `build-failure` Β· `testing` β†’ `infrastructure` or `maintenance` +per-tool diagnostic labels (`colab`, …) β†’ `build-failure` Β· `testing` β†’ +`infrastructure` or `maintenance` (test work is not its own Type) Β· PR lifecycle β†’ native GitHub, as above. ### Scope @@ -214,8 +214,7 @@ per-tool diagnostic labels `qe gh labels sync` *guarantees* the standard set on a repo and renames known variants in place (e.g. `linkchecker` β†’ `broken-links`, `high priority` β†’ `high-priority`, preserving every existing issue and PR tag), but it **never - blanket-prunes** bespoke local - labels that some repos legitimately rely on (`jax-conversion`, `colab`, + blanket-prunes** bespoke local labels that some repos legitimately rely on (`jax-conversion`, `colab`, `site-refresh`, `reading-group-*`). Removing non-standard labels is a **separate** `qe gh labels prune` step that reviews each one **one by one**, keeping the ones that still make sense locally. @@ -258,8 +257,8 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's task namespaces) evolve with their tooling and belong to a dedicated automation-registry QEP, not to enumeration here. - **Make `security` a Type label, or leave it repo-local.** Rejected: security - findings are orthogonal to Type β€” a - pwn-request pattern recommended in a README reads as `documentation`/`bug`, + findings are orthogonal to Type β€” a pwn-request pattern recommended in a + README reads as `documentation`/`bug`, root execution of external notebook code as `infrastructure` β€” so a Type-level `security` would either break the one-Type rule or force a wrong Type choice. Keeping it repo-local forfeits the org-wide standing query that @@ -290,13 +289,12 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's ## Adoption Acceptance fixes the names, colours, descriptions, and policy above as the -QuantEcon standard. Applying the set to a repo is -done with tooling that reads the co-located -[`qep-0002-labels.yml`](qep-0002-labels.yml) from this repository (see -*Machine-readable appendix*) β€” `qe gh labels sync` in +QuantEcon standard. Applying the set to a repo is done with tooling that reads +the co-located [`qep-0002-labels.yml`](qep-0002-labels.yml) from this +repository (see *Machine-readable appendix*) β€” `qe gh labels sync` in [`QuantEcon/cli`](https://github.com/QuantEcon/cli) β€” and is **additive**: -known variants are renamed in place so issue and PR history is preserved. Removing -non-standard labels is the separate, deliberate `qe gh labels prune` pass, +known variants are renamed in place so issue and PR history is preserved. +Removing non-standard labels is the separate, deliberate `qe gh labels prune` pass, reviewed one label at a time. Adoption is **pilot-first**: validate on a single lecture repo before widening to the remaining lecture repos and then the software / tooling repos. The org-level default label set for new repositories @@ -305,6 +303,6 @@ defaults). The sequenced execution checklist β€” the CLI integration, the pilot target, widening order, org defaults, closing the earlier unification attempts -(meta#178, meta#290) β€” belongs in a **tracking issue** (a sub-issue parent, per the -labelling policy above), not in this document β€” so completing, reordering, or -dropping a step never requires amending the standard. +(meta#178, meta#290) β€” belongs in a **tracking issue** (a sub-issue parent, +per the labelling policy above), not in this document, so completing, +reordering, or dropping a step never requires amending the standard. From f865f260bba64acd985c5ac9c01cf42a69bcda49 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 24 Jul 2026 12:18:11 +1000 Subject: [PATCH 20/24] QEP-2: software extension adds refactor; infrastructure owns behaviour-preserving restructuring Co-Authored-By: Claude Fable 5 --- .github/scripts/check-labels.mjs | 8 +++-- qeps/qep-0002-labels.yml | 12 +++++-- qeps/qep-0002-standard-github-labels.md | 46 ++++++++++++++++++------- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/.github/scripts/check-labels.mjs b/.github/scripts/check-labels.mjs index ad0dc0a..391b0a0 100644 --- a/.github/scripts/check-labels.mjs +++ b/.github/scripts/check-labels.mjs @@ -1,7 +1,7 @@ // Machine-readable appendix checks (see QEP-2 Β§ Machine-readable appendix): // 1. Every qeps/qep-NNNN-*.yml companion stays in lockstep with the label // tables in its owning QEP: same label names, and per label the same -// colour, description, and scope (core vs lecture extension). +// colour, description, and scope (core vs the lecture / software extensions). // 2. Once the owning QEP is Accepted, a change to its companion yml is a // substantive amendment and must bump the QEP's `version` in the same PR. // Run by .github/workflows/qep-checks.yml. Exits non-zero on any failure. @@ -45,7 +45,11 @@ function parseLabelTables(path) { for (const line of readFileSync(path, 'utf8').split('\n')) { const heading = line.match(/^\*\*(.+?)\*\*$/); if (heading) { - scope = /^Lecture extension/i.test(heading[1]) ? 'lecture' : 'core'; + scope = /^Lecture extension/i.test(heading[1]) + ? 'lecture' + : /^Software extension/i.test(heading[1]) + ? 'software' + : 'core'; continue; } const m = line.match(/^\|\s*`([^`]+)`\s*\|[^|`]*`#([0-9a-fA-F]{6})`\s*\|([^|]*)\|/); diff --git a/qeps/qep-0002-labels.yml b/qeps/qep-0002-labels.yml index 1baad1e..161cd74 100644 --- a/qeps/qep-0002-labels.yml +++ b/qeps/qep-0002-labels.yml @@ -10,7 +10,8 @@ # # Fields: name Β· color (hex, no '#') Β· description (shown on GitHub) Β· # group (type | priority | cross-cutting | community | workflow | -# automation | meta | lecture) Β· scope (core | lecture) Β· +# automation | meta | lecture | software) Β· +# scope (core | lecture | software) Β· # applied-by: bot (automation labels only; default is human) labels: @@ -32,7 +33,7 @@ labels: scope: core - name: infrastructure color: 1d3c78 - description: Substantial CI / build / deploy / tooling / automation work + description: Substantial CI / build / deploy / tooling / automation work, or behaviour-preserving restructuring group: type scope: core - name: maintenance @@ -138,3 +139,10 @@ labels: description: Requires editor review β€” final sign-off stage group: lecture scope: lecture + + # Software extension β€” software / tooling repos only + - name: refactor + color: 2c5aa0 + description: Behaviour-preserving restructuring of product source + group: software + scope: software diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 468ea66..f3f36e8 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -28,8 +28,9 @@ applied. It standardises label **names, colours, and descriptions** so they mean the same thing everywhere, and it commits to **one behavioural change**: the status labels (`in-work` / `ready` / `review` / `blocked`) are retired in favour of native GitHub features (Draft PRs, "Ready for review", review -requests, issue dependencies). The set is defined as a universal **core** plus -a small **lecture extension** for lecture repos. +requests, issue dependencies). The set is defined as a universal **core** plus two +small domain extensions: a **lecture extension** for lecture repos and a +one-label **software extension** for software / tooling repos. ## Motivation @@ -59,7 +60,8 @@ state** β€” machine output or a closing outcome, rather than triage signal β€” with the automation and meta families distinguished by text, not hue, and type and community labels keep conventional GitHub colours. Every label is **core** β€” meaningful on any repository β€” except the two that form the -**lecture extension**, applied only to lecture repos. +**lecture extension**, applied only to lecture repos, and the one that forms +the **software extension**, applied only to software / tooling repos. **Type β€” what kind of work is this? (one per issue, set at triage)** @@ -68,7 +70,7 @@ type and community labels keep conventional GitHub colours. Every label is | `bug` | πŸŸ₯ `#d73a4a` | Something is wrong or broken | The content is *incorrect*: wrong maths, erroring code, broken rendering β€” in a lecture or a build | | `enhancement` | 🟦 `#a2eeef` | Improvement to existing content or functionality | Better exposition, a new exercise, improved figures | | `documentation` | 🟫 `#b08968` | Repo docs and contributor meta | READMEs, CONTRIBUTING β€” *about the repo*. Product content is never `documentation`, even where the product *is* docs (lectures, the manual): wrong content is `bug`, routine sweeps are `maintenance`, gaps and improvements are `enhancement` | -| `infrastructure` | 🟦 `#1d3c78` | Substantial CI / build / deploy / tooling / automation work | Engineering a teammate should know shipped β€” it would appear in a release note. Use *instead of* `maintenance` | +| `infrastructure` | 🟦 `#1d3c78` | Substantial CI / build / deploy / tooling / automation work, or behaviour-preserving restructuring | Engineering a teammate should know shipped β€” it would appear in a release note; includes restructuring that changes no behaviour (software repos sharpen this to `refactor`). Use *instead of* `maintenance` | | `maintenance` | 🟨 `#fbca04` | Routine housekeeping: style, formatting, env & dependency upkeep | Invisible churn that would never appear in a release note ("rendered lectures look identical after") | | `question` | πŸŸͺ `#d876e3` | Someone needs an answer or clarification | Terminal state: *answered*. Swap for a work type once it becomes agreed work | | `discuss` | πŸŸͺ `#f904a0` | Open-ended team deliberation or a decision to be made | No single right answer. Reuses meta's existing `discuss` colour | @@ -129,8 +131,14 @@ meta grey `duplicate` `#cfd3d7` also sits inside it. | `new-lecture` | 🟦 `#0537E9` | A new lecture (the marquee outcome) | Brand-new lecture β€” proposed, in progress, or shipped. Use *instead of* `enhancement` | | `editor` | 🟩 `#0e8a16` | Requires editor review β€” final sign-off stage | Apply at handoff *after* team review; editor's queue = `org:QuantEcon label:editor`. Remove on sign-off | -This is **19 core labels + 2 lecture labels = 21**. The org-level default set -for new repositories is the **core 19**. +**Software extension β€” software / tooling repos only** + +| Label | Colour | Description | When to use | +|---|---|---|---| +| `refactor` | 🟦 `#2c5aa0` | Behaviour-preserving restructuring of product source | Extract a seam, consolidate duplication, introduce a choke point β€” code moves, behaviour doesn't. Use *instead of* `infrastructure`; the repo's own CI / build / tooling stays `infrastructure` | + +This is **19 core labels + 2 lecture labels + 1 software label = 22**. The +org-level default set for new repositories is the **core 19**. ### Labelling policy @@ -143,13 +151,16 @@ deliberately few: normal priority, whereas a completely unlabelled issue still needs triage. - **Exactly one Type label per issue,** chosen at triage. The Type labels are mutually exclusive on purpose so the boundary stays clean β€” `new-lecture` - *instead of* `enhancement`, `infrastructure` *instead of* `maintenance`. + *instead of* `enhancement`, `infrastructure` *instead of* `maintenance`, + `refactor` *instead of* `infrastructure`. Cross-cutting labels (priority, `security`) sit *alongside* the Type label and do not count against this rule. - **The `infrastructure` / `maintenance` boundary is effort, not subject.** Routine upkeep of CI is `maintenance` (an action version bump); substantial CI work is `infrastructure` (rewriting a deploy job) β€” the release-note test - decides, not which files were touched. + decides, not which files were touched. Substantial restructuring that changes + no behaviour passes the same test: `infrastructure`, not `maintenance`, even + in product source β€” sharpened to `refactor` on software repos. - **A sub-issue parent is structure, not work.** An umbrella issue that groups work via native sub-issues is exempt from the one-Type rule, and unlabelled does not mean "needs triage" β€” the sub-issue relationship is the @@ -203,11 +214,11 @@ per-tool diagnostic labels (`colab`, …) β†’ `build-failure` Β· `testing` β†’ ### Scope -- **Lecture repos get all 21** (core 19 + the lecture extension). -- **Software / tooling repos get the core 19,** which is also the **org-level - default** for new repositories. This includes `meta`, which may keep unique - local labels (`project`, `education`) to triage its own kinds of work - alongside the standard set. +- **Lecture repos get 21** (core 19 + the lecture extension). +- **Software / tooling repos get 20** (core 19 + the software extension). +- **The org-level default for new repositories is the core 19.** `meta` takes + the default, and may keep unique local labels (`project`, `education`) to + triage its own kinds of work alongside the standard set. - **Not touched:** translation forks (`translate:*`) and `*.notebooks` build repos. - **Applying the set is additive; pruning is separate and deliberate.** @@ -250,6 +261,15 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's invisible churn (style, env bumps). The activity reports already surface them differently β€” folding them together sinks "we re-architected CI across the lecture repos" into the same bucket as "bumped a pin." +- **Cover behaviour-preserving refactors with `enhancement`, or add `refactor` + to the core.** Rejected: nothing user-visible changes, so `enhancement` would + leak internal restructuring into reports that read it as user-facing + improvement β€” and in the core, `refactor` is dead weight on lecture repos. + Instead `infrastructure` owns behaviour-preserving restructuring (the + release-note test judges effort, not subject), and software repos β€” where + refactoring is a standing work-type, a quarter of a typical technical-debt + milestone β€” sharpen it to the extension label `refactor`, mirroring + `new-lecture` for lectures. - **A closed bot vocabulary (`automated` + exactly one diagnostic).** Rejected: routine scheduled output diagnoses nothing, so a mandatory diagnostic forces a false `build-failure` and poisons it as a standing query β€” the quantifier From 5758c91c4923e7268cd4e4a72358676b1b3c411e Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 24 Jul 2026 12:18:22 +1000 Subject: [PATCH 21/24] =?UTF-8?q?QEP-2:=20priority=20is=20judged=20repo-wi?= =?UTF-8?q?de=20=E2=80=94=20a=20milestone=20doesn't=20re-centre=20the=20sc?= =?UTF-8?q?ale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index f3f36e8..6580136 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -174,7 +174,8 @@ deliberately few: `org:QuantEcon label:security` work as a standing query. - **Priority labels mark only the outliers.** Most work sits at the unlabelled default; reach for `high-priority` / `low-priority` only when an item is - genuinely off-centre. + genuinely off-centre *for the repo* β€” a milestone doesn't re-centre the + scale. - **`question` vs `discuss`.** `question` seeks a single answer and is done when answered; `discuss` is open-ended deliberation with no single right answer. **A `discuss` thread should not drift open indefinitely** β€” once it reaches a From 16754a5660c71f8b9bbbd89f976a4959f264d592 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 24 Jul 2026 12:18:33 +1000 Subject: [PATCH 22/24] QEP-2: grouping examples span recurring programs and one-shot campaigns Co-Authored-By: Claude Fable 5 --- qeps/qep-0002-standard-github-labels.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 6580136..f383cc8 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -207,7 +207,8 @@ PR that *looks* mergeable (even approved) but must be held. ### What we deliberately don't label `medium-priority` β†’ no label (unlabelled is the middle) Β· project / grouping -labels (`reading-group-*`) β†’ **Milestones** Β· umbrella / tracking issues β†’ +labels (a recurring program like `reading-group-*`, a one-shot campaign like a +tech-debt audit) β†’ **Milestones** Β· umbrella / tracking issues β†’ native **sub-issues** (the parent carries no Type, see the policy above) Β· per-tool diagnostic labels (`colab`, …) β†’ `build-failure` Β· `testing` β†’ `infrastructure` or `maintenance` From adcb0db34575135af9e5fcdfb3cf5a4c57cd556f Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Tue, 28 Jul 2026 17:13:22 +1000 Subject: [PATCH 23/24] QEP-2: dependencies are for hard blocks; non-issue blockers go in the body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retiring `blocked` moved issue-to-issue blocks onto native dependencies, but left two cases with nowhere to go, both found triaging a whole 52-issue tracker: a blocker that is not an issue (a pending decision, a named person's availability), and soft ordering that a hard dependency overstates in the UI. Neither wants the label back β€” its staleness failure mode is real. State the two rules the retirement implies: dependencies for hard blocks only, and a "Blocked by: …" first line in the body for everything else, so a deliberately parked issue reads as parked rather than neglected. --- qeps/qep-0002-standard-github-labels.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index f383cc8..4b3ca87 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -201,6 +201,13 @@ off labels: | `review` | a **review request** | | `blocked` / `on-hold` | native **issue dependencies** ("Blocked by #N") for issues; a Draft PR with a "Blocked by #N" note for PRs | +Dependencies are for **hard blocks only** β€” the UI renders one as a blocker, +which overstates "should ideally follow"; soft ordering belongs to sub-issue +order or the parent's body. A blocker that **is not an issue** β€” a pending +decision, a named person's availability β€” has no native representation: record +it as a **"Blocked by: …"** first line in the issue body, so a deliberately +parked issue reads as parked rather than neglected, and ages visibly. + Only **`do-not-merge`** survives as a workflow label, for the genuine case of a PR that *looks* mergeable (even approved) but must be held. From ef9e847a561fd553d868089e940718a69ed9d708 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Tue, 28 Jul 2026 17:14:39 +1000 Subject: [PATCH 24/24] QEP-2: adoption converges the corpus; migration reports are the evidence base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three items from the website-dynamic field report, plus the posture change the first of them exposed. Boundary tests. The set had tests for infrastructure/maintenance and refactor/infrastructure but not for the commonest ambiguity, bug vs enhancement β€” the field case was output that misrepresents its own title, where the fix is additive but the current behaviour is wrong. The test is the promise, not the size of the fix, and the example is mirrored so the same work lands on either side depending only on what the text already claimed. Separately, question/discuss now covers the investigate-then-decide shape: an issue that opens factual and terminates in a decision is `discuss`. Convergence. The report asked for unused GitHub stock labels to be exempt from the one-by-one prune. The larger point behind it is that additive sync leaves every repo carrying the standard set *plus* its legacy vocabulary, which is the drift this QEP exists to end. Conformance is now the standard set and nothing else, and adoption is a migration rather than an addition β€” history-preserving, because rename carries every issue and PR tag and deletion silently strips them: a local label with usages is renamed, or its issues re-tagged before it is removed. The tool never removes a label that carries tags. Feedback loop. A label the standard does not cover is removed if unused and quarantined if it carries tags β€” recorded as unmapped, and re-surfaced on every subsequent report so "kept" cannot become permanent by default. Recurrence across repos is the bar for growing the set, which makes the reports the evidence base for amending this QEP rather than a one-off audit artefact. Tooling produces the evidence; proposing the amendment stays human. `meta`'s `project` / `education` lose their standing carve-out and become the first rows of a migration report, which is the loop's own first test case. --- qeps/qep-0002-standard-github-labels.md | 64 ++++++++++++++++++------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/qeps/qep-0002-standard-github-labels.md b/qeps/qep-0002-standard-github-labels.md index 4b3ca87..7d9f673 100644 --- a/qeps/qep-0002-standard-github-labels.md +++ b/qeps/qep-0002-standard-github-labels.md @@ -161,6 +161,15 @@ deliberately few: decides, not which files were touched. Substantial restructuring that changes no behaviour passes the same test: `infrastructure`, not `maintenance`, even in product source β€” sharpened to `refactor` on software repos. +- **The `bug` / `enhancement` boundary is the promise, not the size of the + fix.** An artefact's own words β€” a heading, a title, a docstring β€” are its + specification. A lecture section headed *Existence and uniqueness* that proves + only existence is a `bug`; adding a uniqueness proof to a section headed + *Existence* is an `enhancement`. The work is identical; what the text already + claimed decides the label. Same on the software side: a `--dry-run` flag + documented as making no changes that still writes a cache file is a `bug`, + whereas adding `--dry-run` to a command that never offered one is an + `enhancement`. - **A sub-issue parent is structure, not work.** An umbrella issue that groups work via native sub-issues is exempt from the one-Type rule, and unlabelled does not mean "needs triage" β€” the sub-issue relationship is the @@ -178,6 +187,8 @@ deliberately few: scale. - **`question` vs `discuss`.** `question` seeks a single answer and is done when answered; `discuss` is open-ended deliberation with no single right answer. + An issue that opens with a factual question but **terminates in a decision** + is `discuss`. **A `discuss` thread should not drift open indefinitely** β€” once it reaches a conclusion, summarise the decision and close, spawn a concrete follow-up issue, or escalate to a QEP if it crosses repos or changes team workflow. @@ -226,18 +237,21 @@ per-tool diagnostic labels (`colab`, …) β†’ `build-failure` Β· `testing` β†’ - **Lecture repos get 21** (core 19 + the lecture extension). - **Software / tooling repos get 20** (core 19 + the software extension). - **The org-level default for new repositories is the core 19.** `meta` takes - the default, and may keep unique local labels (`project`, `education`) to - triage its own kinds of work alongside the standard set. + the default; its local labels (`project`, `education`) are resolved by the + migration like any other non-standard label, not held as a standing + exception. - **Not touched:** translation forks (`translate:*`) and `*.notebooks` build repos. -- **Applying the set is additive; pruning is separate and deliberate.** - `qe gh labels sync` *guarantees* the standard set on a repo and renames known - variants in place (e.g. `linkchecker` β†’ `broken-links`, `high priority` β†’ - `high-priority`, preserving every existing issue and PR tag), but it **never - blanket-prunes** bespoke local labels that some repos legitimately rely on (`jax-conversion`, `colab`, - `site-refresh`, `reading-group-*`). Removing non-standard labels is a - **separate** `qe gh labels prune` step that reviews each one **one by one**, - keeping the ones that still make sense locally. +- **Conformance is the standard set and nothing else.** Legacy labels left in + place alongside the standard ones reproduce the drift this QEP exists to end, + so adoption is a **migration**, not an addition. The migration is + history-preserving: a local label that maps to a standard one is **renamed in + place** (`linkchecker` β†’ `broken-links`, `high priority` β†’ `high-priority`), + which carries every existing issue and PR tag; where a rename is blocked + because both labels already exist, the issues are **re-tagged before** the + local label is removed. Nothing is deleted out from under an issue that + carries it. A local label the standard does not cover is **kept and recorded + as unmapped** rather than forced into a wrong Type β€” see *Adoption*. ### Machine-readable appendix @@ -255,8 +269,8 @@ machine-readable appendix is a **substantive amendment** that bumps this QEP's request where the tables and the yml disagree, so drift is a red βœ—, not a judgement call. - **The yml carries only the standard itself.** Rename maps for historical - variants, retired-label handling, and pruning knowledge are operational - concerns of the tool implementing this QEP (`qe gh labels sync` / `prune` in + variants, retired-label handling, and the migration report are operational + concerns of the tool implementing this QEP (`qe gh labels sync` in [`QuantEcon/cli`](https://github.com/QuantEcon/cli)), which reads the yml from this repository rather than carrying its own copy. @@ -321,15 +335,33 @@ Acceptance fixes the names, colours, descriptions, and policy above as the QuantEcon standard. Applying the set to a repo is done with tooling that reads the co-located [`qep-0002-labels.yml`](qep-0002-labels.yml) from this repository (see *Machine-readable appendix*) β€” `qe gh labels sync` in -[`QuantEcon/cli`](https://github.com/QuantEcon/cli) β€” and is **additive**: -known variants are renamed in place so issue and PR history is preserved. -Removing non-standard labels is the separate, deliberate `qe gh labels prune` pass, -reviewed one label at a time. Adoption is **pilot-first**: validate on a single +[`QuantEcon/cli`](https://github.com/QuantEcon/cli) β€” and is a **migration**, +not an addition: known variants are renamed in place so issue and PR history is +preserved (see *Scope*). Adoption is **pilot-first**: validate on a single lecture repo before widening to the remaining lecture repos and then the software / tooling repos. The org-level default label set for new repositories is the core 19 (a manual settings change β€” there is no public API for org defaults). +Syncing a repo emits a **migration report**: one row per non-standard label, its +usage count, and its disposition β€” *renamed to `X`* Β· *re-tagged (12 β†’ +`infrastructure`, 9 β†’ `maintenance`), removed* Β· *replaced by a milestone* Β· +*removed (unused)* Β· *kept, unmapped*. The report is the repo's audit trail and +the standard's evidence base. A label the standard does not cover is **removed +if unused** and **kept if it carries tags** β€” removing it would strip triage +signal from live issues with nothing to replace it. A kept-unmapped label is a +**quarantine, not an exception**: it reappears on every subsequent report until +it is resolved, by an amendment that covers it, by re-tagging its issues to a +standard label, or by a decision that the tags are not worth keeping. The tool +never removes a label that carries tags; only a human decision does. + +A label recorded as **unmapped across several repos** is a gap in the set, and +closing it is a normal in-place amendment under +[QEP-1](qep-0001-purpose-and-process.md), which bumps this QEP's `version`. The +bar is **recurrence** β€” one repo's oddity is a local exception, the same label +unmapped in four repos is a missing label. Tooling produces the evidence; +proposing the amendment stays a human judgement. + The sequenced execution checklist β€” the CLI integration, the pilot target, widening order, org defaults, closing the earlier unification attempts (meta#178, meta#290) β€” belongs in a **tracking issue** (a sub-issue parent,