From 17be61ce81b534426e8f1bb9bef96c908e934b45 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 27 Aug 2026 13:27:59 +1000 Subject: [PATCH 1/7] QEP-1 v3: stamp version and version-hash from v0; one-week window Every merged QEP carries version and version-hash from the moment it lands: the post-merge stamp writes version: 0 and the hash at first merge, uniformly across Accepted, Rejected and Withdrawn outcomes, and the hash moves on every later change, editorial included. Replaces the implicit v0 (absent version, anchored only from v1), which pushed an absent-means-v0 special case into every consumer and left QEP-2's machine-readable appendix with no recorded revision to cite (#22). Also shortens the default comment window from one-to-two weeks to one week (small team; the author may extend it for a larger change), states plainly that version-hash is a historical anchor rather than a file checksum, and records the dropped implicit-v0 design under Alternatives. Supporting machinery changes (stamp action, checks, backfill, README note, AGENTS.md) follow after acceptance per adoption entry v3. Co-Authored-By: Claude Fable 5 --- README.md | 5 +- qeps/qep-0001-purpose-and-process.md | 99 +++++++++++++++++++--------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 17c03fb..1d3525a 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 | v2 | +| [QEP-1](qeps/qep-0001-purpose-and-process.md) | QEP Purpose and Process | process | Accepted | v3 | | [QEP-2](qeps/qep-0002-standard-github-labels.md) | Standard GitHub Label Set and Labelling Policy | standard | Accepted | – | QEPs that set an ongoing rule are **maintained in place**: a substantive amendment bumps @@ -29,7 +29,8 @@ and each QEP's `version-hash` is stamped into its frontmatter at merge; `Version socialise it and confirm it needs a QEP. 2. **Draft it.** Copy [`qeps/template.md`](qeps/template.md) to `qeps/qep-XXXX-short-slug.md`, fill it in with **Status: Draft**, and open a PR. -3. **Set a deadline.** Announce the PR and give a comment window (1–2 weeks). +3. **Set a deadline.** Announce the PR and give a comment window (normally one + week; extend it for a larger change). 4. **Decide.** At the deadline the Core Maintainers decide by lazy consensus; the QEP is merged recording the outcome (Accepted / Rejected / Withdrawn). diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index 2854d5e..96836ef 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -4,8 +4,7 @@ title: QEP Purpose and Process author: "@mmcky" status: Accepted type: process -version: 2 -version-hash: 4ee318d # stamped by CI; do not edit +version: 3 created: 2026-06-16 discussion: https://github.com/QuantEcon/meta/issues/325 --- @@ -19,7 +18,7 @@ discussion: https://github.com/QuantEcon/meta/issues/325 | **Author** | @mmcky | | **Status** | Accepted | | **Type** | process | -| **Version** | 2 | +| **Version** | 3 | | **Created** | 2026-06-16 | | **Discussion** | [QuantEcon/meta#325](https://github.com/QuantEcon/meta/issues/325) | @@ -109,14 +108,16 @@ standard. 2. **Draft.** Open a PR adding `qeps/qep-XXXX-slug.md` from the template (with **Status: Draft** and a discussion link) and a matching row in the README index. 3. **Set a deadline.** The author announces the PR and sets a comment window — - normally **one to two weeks** — recording the **decision deadline** in the PR + normally **one week**; the team is small, and the author may extend it for a + larger or more contested change — recording the **decision deadline** in the PR description. 4. **Decide.** At the deadline, the **Core Maintainers** decide by **lazy consensus**: objections are raised as PR comments, and no sustained objection means the QEP is Accepted. If there is no consensus, the lead (@jstac) decides or defers. 5. **Record.** On acceptance, set **Status: Accepted** — in the frontmatter, the header - table, and the README index row — confirm the number, and merge. A newly accepted - QEP carries no `version`: it is implicitly **v0** until first amended. + table, and the README index row — confirm the number, and merge. The PR itself + carries no `version`: CI stamps **`version: 0`** and its `version-hash` anchor at + merge (see *Versioning*). ### Amending an accepted QEP @@ -144,36 +145,45 @@ substantive-milestone marker. ### Versioning: `version` and its git anchor -A QEP gains a `version` the first time it is **substantively** changed after acceptance: +Every merged QEP carries a `version` from the moment it lands: | `version` | Meaning | | ----------- | --------------------------------------------------------------------- | -| *absent* | Implicitly **v0** — as originally accepted, never substantively changed. Many QEPs (a one-off decision) stay here forever. | -| `1`, `2`, … | The current substantive revision. The first substantive amendment introduces `version: 1`; each later substantive change climbs to `2`, `3`, … | +| `0` | As originally merged, never substantively changed. Many QEPs (a one-off decision) stay here forever. | +| `1`, `2`, … | The current substantive revision. The first substantive amendment climbs to `version: 1`; each later substantive change to `2`, `3`, … | -From `v1` onward a sibling `version-hash` field carries the short commit hash that -anchors the revision to git history: +A sibling `version-hash` field carries the short commit hash that anchors the revision +to git history: ```yaml -version: 2 +version: 0 version-hash: a1b2c3d # stamped by CI; do not edit ``` +Both fields are machine-written at birth: a Draft carries neither, and the post-merge +step (see *Automation*) stamps `version: 0` and the hash when the QEP first merges — a +commit cannot contain its own hash, so neither field is ever hand-written to start. +From then on the author bumps `version` on substantive amendments, and CI re-stamps the +hash on every merged change, editorial included. The stamp is uniform across merged +outcomes — Accepted, Rejected, and Withdrawn QEPs all carry it — so every durable +record is machine-referenceable. + `version` is a plain number; the commit hash lives in the separate `version-hash` field — -a real key, so any YAML parser keeps it. The hash is stamped -**automatically at merge** — a commit cannot contain its own hash, so a post-merge step -(see *Automation*) writes it; never hand-write it. Tooling that pins a standard (for -example a labels-sync command) reads `version` and verifies against `version-hash`. A -per-QEP `version` is the right anchor because a git *tag* tags the whole repository, not -one QEP's revision. +a real key, so any YAML parser keeps it. Tooling that pins a standard (for example a +labels-sync command) reads `version` and cross-checks `version-hash` against the +revision it fetched. **`version-hash` is a historical anchor, not a file checksum**: +the stamp commit post-dates the hash it writes, so the field names the revision that +last changed the QEP — it does not hash the file's bytes. A per-QEP `version` is the +right anchor because a git *tag* tags the whole repository, not one QEP's revision. **Substantive vs editorial** decides whether the number moves: - **Substantive** — any change to normative content (a rule, a value, a table row, a machine-readable appendix) → **bump `version`** by one; the hash moves too. - **Editorial** — no change to normative content (a typo, wording, formatting, a link) - → **`version` unchanged**; only the hash moves (at v0, the change is simply a git - commit). + → **`version` unchanged**; only the hash moves — at v0 exactly as at v1+, so a + consumer of a machine-readable appendix sees that something changed without diffing + git. One-line rule: *editorial = no change to normative content; substantive = any change to normative content.* This keeps version numbers meaningful — not inflated by typos — @@ -194,21 +204,24 @@ hand-maintained changelog (which would drift and clutter the document): Type and version are surfaced two ways: -- the **README index** carries `Type` and `Version` columns, with `Version` showing `–` - at v0 and `v{N}` thereafter — repo-controlled, so it renders on any theme; -- under the **QuantEcon theme** (once adopted), a coloured **`type` pill** always and a - **`version` pill** once a QEP reaches `v1` — e.g. `standard` · `v2`; a v0 QEP shows only - the type pill. +- the **README index** carries `Type` and `Version` columns, with `Version` showing + `v{N}` from `v0` up (`–` only while a Draft's PR is open) — repo-controlled, so it + renders on any theme; +- under the **QuantEcon theme** (once adopted), a coloured **`type` pill** and a + **`version` pill** on every merged QEP — e.g. `standard` · `v2`; `v0` is shown rather + than hidden, since it names an anchored revision. ### Automation Two mechanical steps are enforced by CI rather than left to memory: - a **post-merge action** (`.github/workflows/stamp-version.yml`) reads the merged short - hash, writes it into the `version-hash` field, and keeps the README `Type`/`Version` - columns in sync with each QEP's frontmatter; + hash, stamps `version: 0` alongside it into any newly merged QEP that carries no + `version`, writes the hash into every changed QEP's `version-hash` field, and keeps + the README `Type`/`Version` columns in sync with each QEP's frontmatter; - a **pull-request check** (`.github/workflows/qep-checks.yml`) confirms that `version` - moves legally — a new QEP starts unversioned, a versioned QEP stays versioned, and the + moves legally — a new QEP arrives unversioned in its PR (`version: 0` is stamped at + merge), a stamped QEP stays versioned, and the number stays the same (editorial) or increases by exactly one (substantive) — that `type` and `status` are known values, and that the README `Type`/`Status`/`Version` columns match each QEP's frontmatter. @@ -237,8 +250,8 @@ light as the decisions it records. ### Format Each QEP is a Markdown file with YAML frontmatter (`qep`, `title`, `author`, `status`, -`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 +`type`, `created`, `discussion` — plus the CI-stamped `version` and `version-hash`, which +sit just after `type` from the QEP's first merge) followed by the sections in [`qeps/template.md`](../qeps/template.md): **Summary, Motivation, Proposal, Alternatives considered, Adoption**. The `type` field describes the **kind of content** the QEP carries: @@ -278,6 +291,14 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or QEP would duplicate git, drift from it, and clutter the document; we point at git instead, surfaced on the site by the theme's history feature and on GitHub by history/blame. +- **An implicit v0 (absent `version`), anchored only from v1.** The v1–v2 design: + absence itself said "never substantively changed", and one-off QEPs carried no stamp. + Dropped in v3 because the asymmetry pushed a special case into every consumer + ("absent means v0 — choose your own anchor") and left the machine-readable appendix + of an accepted-but-unamended standard with no recorded revision at all: QEP-2 shipped + normative tooling input with nothing to cite ([#22](https://github.com/QuantEcon/qeps/issues/22)). + Uniform stamping from v0 costs a pill and a bot commit; the implicit v0 cost + correctness, in prose that described pinning which did not yet exist. ## Adoption @@ -302,3 +323,21 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or 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. +4. **(v3) Stamp `version` from v0; default comment window one week.** The same + amendment shortens the normal comment window from one-to-two weeks to **one + week** — the team is small enough that a fortnight is drift, not diligence, and + the author can still extend the window for a larger change. On the stamping + change: every merged QEP carries `version` and + `version-hash` from the moment it lands, so tooling reads one uniform contract + instead of treating an absent `version` as an implicit v0 with no anchor — the + asymmetry surfaced by QEP-2's machine-readable appendix + ([#22](https://github.com/QuantEcon/qeps/issues/22)). Stamping's supporting + changes, landing + as a follow-up once this amendment merges: the post-merge stamp action adds + `version: 0` where missing; already-merged v0 QEPs are backfilled, each stamped + with the most recent commit that touched it — mechanical, and consistent with the + rule that editorial changes move the hash; the pull-request check's new-QEP rule + becomes "unversioned in the PR, `v0` at merge"; the README index and the theme's + version pill show `v0` rather than `–`; `AGENTS.md` and the README's index note + follow. `qeps/template.md` is unchanged — `version` is machine-written, never + hand-written. From e8b5114239caf02c349e62d87bf99ee53324ea00 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 4 Sep 2026 10:13:01 +1000 Subject: [PATCH 2/7] QEP-1 v3: stamp at the outcome merge, not the first merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QEP-3 sits on main with Status: Draft — field-tested in place, per #14 — carrying neither `version` nor `version-hash`. That falsified two claims in the v3 draft on their own terms, not merely ahead of the automation: - "stamps `version: 0` and the hash when the QEP first merges" would, read literally, stamp QEP-3 today, contradicting the same paragraph's "a Draft carries neither" and the outcome list two sentences later, which names Accepted, Rejected and Withdrawn and pointedly omits Draft; - "`–` only while a Draft's PR is open" would stay wrong after the backfill lands, since QEP-3's PR is merged and its README row is still `–`. Tie the stamp to the merge that records the QEP's outcome instead. The rule then covers a merged Draft with no exception clause, and needs no revisiting when the stamping automation follows. The same wording lands in Automation, Format, the surfacing bullets, and the Adoption item's backfill scope. Raised by Copilot's review of #23, which read all three as automation lag. The normative prose is deliberately unhedged — Adoption item 4 carries the transition — but the merged-Draft case was a real defect underneath it. Co-Authored-By: Claude Opus 5 (1M context) --- qeps/qep-0001-purpose-and-process.md | 56 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index 96836ef..3cd8a18 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -161,12 +161,12 @@ version-hash: a1b2c3d # stamped by CI; do not edit ``` Both fields are machine-written at birth: a Draft carries neither, and the post-merge -step (see *Automation*) stamps `version: 0` and the hash when the QEP first merges — a -commit cannot contain its own hash, so neither field is ever hand-written to start. -From then on the author bumps `version` on substantive amendments, and CI re-stamps the -hash on every merged change, editorial included. The stamp is uniform across merged -outcomes — Accepted, Rejected, and Withdrawn QEPs all carry it — so every durable -record is machine-referenceable. +step (see *Automation*) stamps `version: 0` and the hash on the merge that records the +QEP's outcome — a commit cannot contain its own hash, so neither field is ever +hand-written to start. From then on the author bumps `version` on substantive +amendments, and CI re-stamps the hash on every merged change, editorial included. The +stamp is uniform across merged outcomes — Accepted, Rejected, and Withdrawn QEPs all +carry it — so every durable record is machine-referenceable. `version` is a plain number; the commit hash lives in the separate `version-hash` field — a real key, so any YAML parser keeps it. Tooling that pins a standard (for example a @@ -205,18 +205,18 @@ hand-maintained changelog (which would drift and clutter the document): Type and version are surfaced two ways: - the **README index** carries `Type` and `Version` columns, with `Version` showing - `v{N}` from `v0` up (`–` only while a Draft's PR is open) — repo-controlled, so it + `v{N}` from `v0` up (`–` only while a QEP is Draft) — repo-controlled, so it renders on any theme; -- under the **QuantEcon theme** (once adopted), a coloured **`type` pill** and a - **`version` pill** on every merged QEP — e.g. `standard` · `v2`; `v0` is shown rather - than hidden, since it names an anchored revision. +- under the **QuantEcon theme** (once adopted), a coloured **`type` pill** always and a + **`version` pill** once the QEP leaves Draft — e.g. `standard` · `v2`; `v0` is shown + rather than hidden, since it names an anchored revision. ### Automation Two mechanical steps are enforced by CI rather than left to memory: - a **post-merge action** (`.github/workflows/stamp-version.yml`) reads the merged short - hash, stamps `version: 0` alongside it into any newly merged QEP that carries no + hash, stamps `version: 0` alongside it into any QEP that has left Draft and carries no `version`, writes the hash into every changed QEP's `version-hash` field, and keeps the README `Type`/`Version` columns in sync with each QEP's frontmatter; - a **pull-request check** (`.github/workflows/qep-checks.yml`) confirms that `version` @@ -251,10 +251,10 @@ light as the decisions it records. Each QEP is a Markdown file with YAML frontmatter (`qep`, `title`, `author`, `status`, `type`, `created`, `discussion` — plus the CI-stamped `version` and `version-hash`, which -sit just after `type` from the QEP's first merge) followed by the sections in -[`qeps/template.md`](../qeps/template.md): **Summary, Motivation, Proposal, Alternatives -considered, Adoption**. The `type` field describes the **kind of content** the QEP -carries: +sit just after `type` from the merge that records the QEP's outcome) followed by the +sections in [`qeps/template.md`](../qeps/template.md): **Summary, Motivation, Proposal, +Alternatives 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 guide, editorial or metadata conventions, a licensing choice). @@ -327,17 +327,15 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or amendment shortens the normal comment window from one-to-two weeks to **one week** — the team is small enough that a fortnight is drift, not diligence, and the author can still extend the window for a larger change. On the stamping - change: every merged QEP carries `version` and - `version-hash` from the moment it lands, so tooling reads one uniform contract - instead of treating an absent `version` as an implicit v0 with no anchor — the - asymmetry surfaced by QEP-2's machine-readable appendix - ([#22](https://github.com/QuantEcon/qeps/issues/22)). Stamping's supporting - changes, landing - as a follow-up once this amendment merges: the post-merge stamp action adds - `version: 0` where missing; already-merged v0 QEPs are backfilled, each stamped - with the most recent commit that touched it — mechanical, and consistent with the - rule that editorial changes move the hash; the pull-request check's new-QEP rule - becomes "unversioned in the PR, `v0` at merge"; the README index and the theme's - version pill show `v0` rather than `–`; `AGENTS.md` and the README's index note - follow. `qeps/template.md` is unchanged — `version` is machine-written, never - hand-written. + change: every QEP that records an outcome carries `version` and `version-hash` from + the moment it lands, so tooling reads one uniform contract instead of treating an + absent `version` as an implicit v0 with no anchor — the asymmetry surfaced by QEP-2's + machine-readable appendix ([#22](https://github.com/QuantEcon/qeps/issues/22)). + Stamping's supporting changes, landing as a follow-up once this amendment merges: the + post-merge stamp action adds `version: 0` where missing; already-merged QEPs past + Draft are backfilled, each stamped with the most recent commit that touched it — + mechanical, and consistent with the rule that editorial changes move the hash; the + pull-request check's new-QEP rule becomes "unversioned in the PR, `v0` at merge"; the + README index and the theme's version pill show `v0` rather than `–`; `AGENTS.md` and + the README's index note follow. `qeps/template.md` is unchanged — `version` is + machine-written, never hand-written. From f375a1c35d87958db7a63ce8bf2b4bf77eecae3b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 10:31:54 +0000 Subject: [PATCH 3/7] QEP-1 v3: drop the decision deadline, generate the index, carry downstream amendments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds three ruled changes into this amendment and drops one it originally proposed, so v3 lands as one coherent round instead of a v3 that a v4 would immediately partly delete. Ruled 2026-09-08; discussion on #26, #9 and #22. DROPPED FROM THIS AMENDMENT: the one-week default comment window. The ruling went further than shortening it. THE DECISION DEADLINE GOES. Step 3 of "How a QEP is decided" is struck and the acceptance trigger becomes "no objection is outstanding" rather than a date — lazy consensus with no clock, so a QEP nobody objects to still lands without needing the lead, and nothing is accepted by the mere passage of time. Roles, the Summary's "clear deadline" promise, and the README's Proposing steps all follow. The evidence is recorded under Alternatives considered: on 2026-09-08 none of the four open QEP pull requests had a live deadline — #8 had never set one in 47 days, #13's and #23's had passed, #18's was deliberately deferred — while nothing in CI ever read a PR description. The field was required by this document and checked by nothing. Enforcing it in CI was considered and declined: it would have turned three open PRs red for a rule they predated. THE README INDEX IS GENERATED, post-merge from frontmatter and ordered by number, so a PR carries no row of its own and two QEP PRs cannot collide on one line of one table. The mechanism shipped ahead of this amendment in #30, together with the ordered-list check; Automation now names both, and a stale index is a warning rather than a failure. Numbering states what already happened informally: a number is reserved when its draft PR opens, released if that PR closes unmerged, so index gaps are normal while drafts are open. A DOWNSTREAM QEP CARRIES ITS UPSTREAM AMENDMENTS — #9's sentence, widened by the case it did not anticipate. Where the superseded standard is not itself a QEP, the Adoption section must name the contract, the obligation and a date by which the handover completes: QEP-6 asserts precedence over an external tracker contract with no date on it, and the qe skills depend on that sentence at runtime. Closes #9. Also brings the branch up to date with main, which had moved on by QEP-3's merge (this branch predated it, so it carried neither the file nor its index row). Verified green under both the pre-#30 checks on this branch and #30's new ones, including the ordered-list rule over the rewritten Adoption entry. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Hwm5shrGXmaq4r3Lp9kS33 --- README.md | 8 +- qeps/qep-0001-purpose-and-process.md | 113 ++++++++++++++++++++------- 2 files changed, 89 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 2822eec..8452c1d 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ and each QEP's `version-hash` is stamped into its frontmatter at merge; `Version socialise it and confirm it needs a QEP. 2. **Draft it.** Copy [`qeps/template.md`](qeps/template.md) to `qeps/qep-XXXX-short-slug.md`, fill it in with **Status: Draft**, and open a PR. -3. **Set a deadline.** Announce the PR and give a comment window (normally one - week; extend it for a larger change). -4. **Decide.** At the deadline the Core Maintainers decide by lazy consensus; the - QEP is merged recording the outcome (Accepted / Rejected / Withdrawn). + The index row above is generated at merge — do not add one. +3. **Decide.** Any Core Maintainer may accept it once it has been announced and no + objection is outstanding; the QEP is merged recording the outcome (Accepted / + Rejected / Withdrawn). The process itself is defined in **QEP-1**. diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index 3cd8a18..c089a25 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -28,8 +28,8 @@ A **QuantEcon Enhancement Proposal (QEP)** is a short, durable document that rec decision affecting **more than one QuantEcon repository**, or that **changes how the team works**. This QEP defines what a QEP is, when one is needed, where QEPs live, how a proposal moves from draft to decision, and how an accepted QEP is **maintained over -time**. It is deliberately lightweight: the aim is a ten-minute read, a clear deadline, -and a clean close — not governance for its own sake. As the first proposal, this +time**. It is deliberately lightweight: the aim is a ten-minute read and a clean +close — not governance for its own sake. As the first proposal, this document is also a worked example of the template and the in-place versioning it describes. @@ -105,19 +105,18 @@ standard. 1. **(Optional) Float the idea.** Open a *QEP discussion* issue to socialise it and confirm it warrants a QEP. -2. **Draft.** Open a PR adding `qeps/qep-XXXX-slug.md` from the template (with - **Status: Draft** and a discussion link) and a matching row in the README index. -3. **Set a deadline.** The author announces the PR and sets a comment window — - normally **one week**; the team is small, and the author may extend it for a - larger or more contested change — recording the **decision deadline** in the PR - description. -4. **Decide.** At the deadline, the **Core Maintainers** decide by **lazy consensus**: - objections are raised as PR comments, and no sustained objection means the QEP is - Accepted. If there is no consensus, the lead (@jstac) decides or defers. -5. **Record.** On acceptance, set **Status: Accepted** — in the frontmatter, the header - table, and the README index row — confirm the number, and merge. The PR itself - carries no `version`: CI stamps **`version: 0`** and its `version-hash` anchor at - merge (see *Versioning*). +2. **Draft.** Open a PR adding `qeps/qep-XXXX-slug.md` from the template, with + **Status: Draft** and a discussion link. The README index row is generated at + merge — do not add one. +3. **Decide.** Any **Core Maintainer** may accept a QEP once it has been announced and + **no objection is outstanding** — lazy consensus, with no clock. Objections are + raised as PR comments, and an outstanding one blocks acceptance until it is resolved + or withdrawn. If there is no consensus, the lead (@jstac) decides or defers. There is + deliberately **no decision deadline**; see *Alternatives considered* for why the v1–v2 + design had one and why it was dropped. +4. **Record.** On acceptance, set **Status: Accepted** in the frontmatter and the header + table, confirm the number, and merge. The PR itself carries no `version`: CI stamps + **`version: 0`** and its `version-hash` anchor at merge (see *Versioning*). ### Amending an accepted QEP @@ -134,6 +133,16 @@ point, and it evolves in small, frequent steps. Two paths keep that change order that marks the old one `Superseded` (link it). Superseding is reserved for a genuine rethink, not routine maintenance. +**A QEP that touches another standard carries the amendment itself.** A QEP that +depends on, extends or supersedes an existing standard is *not* forward-referenced by +that standard while in development; instead the new QEP's acceptance PR carries the +amendments to the standards it touches, version-bumped per the rule above. Reference and +target then land atomically, `main` never holds a dangling cross-QEP link, and reviewers +see the whole blast radius as one diff. Where the superseded standard is **not itself a +QEP** — an external contract in another repository — the Adoption section names the +contract, the obligation, and a **date by which the handover completes**: an undated +precedence sentence is a handover that never happens. + **Squash-merge only.** Each amendment lands as a single commit, so a QEP's history reads as one line per change. This is a repository setting, not a convention to remember. @@ -217,14 +226,18 @@ Two mechanical steps are enforced by CI rather than left to memory: - a **post-merge action** (`.github/workflows/stamp-version.yml`) reads the merged short hash, stamps `version: 0` alongside it into any QEP that has left Draft and carries no - `version`, writes the hash into every changed QEP's `version-hash` field, and keeps - the README `Type`/`Version` columns in sync with each QEP's frontmatter; + `version`, writes the hash into every changed QEP's `version-hash` field, and + **regenerates the README index** from each QEP's frontmatter, ordered by number; - a **pull-request check** (`.github/workflows/qep-checks.yml`) confirms that `version` moves legally — a new QEP arrives unversioned in its PR (`version: 0` is stamped at merge), a stamped QEP stays versioned, and the number stays the same (editorial) or increases by exactly one (substantive) — that - `type` and `status` are known values, and that the README `Type`/`Status`/`Version` - columns match each QEP's frontmatter. + `type` and `status` are known values, that `related:` and the header table's + **Related** row agree, and that **ordered-list markers ascend in source**. That last + rule exists because Markdown *renumbers* an ordered list on render: a source list + reading `1., 2., 2., 3., 4.` displays as 1–5 while every external "clause N" citation + silently shifts by one. A stale README index is a **warning, not a failure** — the + index is generated, so a PR carries no row of its own to be wrong. The author-side judgement — substantive vs editorial, bumping `version`, the commit subject — is documented in `AGENTS.md`; CI enforces the mechanical steps that a @@ -233,15 +246,19 @@ maintainer merging through the GitHub UI would otherwise have to remember. ### Numbering Numbers are assigned sequentially. An author may propose the next free number when -opening the PR; it is confirmed (and adjusted if two proposals collide) at merge. +opening the PR; it is confirmed (and adjusted if two proposals collide) at merge. A +number is **reserved when the draft PR opens** and released if that PR is closed without +merging. The README index lists only merged QEPs, so **gaps are normal while drafts are +open**, and a QEP that merges out of numeric order takes its numeric position +automatically. Numbers are written **unpadded** in text (QEP-1, QEP-2, …); only the filename zero-pads them to four digits (`qep-0001-…`). This QEP is QEP-1. ### Roles -- **Author** — anyone may write a QEP; typically a maintainer. The author drives - discussion and sets the deadline. -- **Core Maintainers** — decide by lazy consensus at the deadline. +- **Author** — anyone may write a QEP; typically a maintainer. The author announces + the PR and drives the discussion. +- **Core Maintainers** — decide by lazy consensus, once no objection is outstanding. - **Lead** (@jstac) — breaks ties and may defer a decision. No sponsor, delegate, or editor role is introduced; the process is intended to stay as @@ -300,6 +317,20 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or Uniform stamping from v0 costs a pill and a bot commit; the implicit v0 cost correctness, in prose that described pinning which did not yet exist. +- **A recorded decision deadline, with lazy consensus resolving at it.** The v1–v2 + design: the author set a comment window and recorded a decision date in the PR + description, and the Core Maintainers decided at it. Dropped in v3 because it + described something the team did not do. On 2026-09-08 **none of the four open QEP + pull requests had a live deadline** — one had never set one in 47 days, two had + passed, and one was deliberately deferred pending evidence — while nothing in CI ever + read a PR description, so the field was required by this document and checked by + nothing. Enforcing it was considered and declined: a check would have turned three + open PRs red for a rule they predated, and the deadline's real function — a moment at + which lazy consensus resolves — is served as well by *no objection outstanding*, which + a reviewer can establish by reading the thread. The cost of dropping it is that + nothing is accepted by the mere passage of time; a QEP nobody is looking at stays open + until someone looks. + ## Adoption 1. **(v0) Establish the process.** Merge this QEP to set the process; re-record the @@ -323,11 +354,37 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or 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. -4. **(v3) Stamp `version` from v0; default comment window one week.** The same - amendment shortens the normal comment window from one-to-two weeks to **one - week** — the team is small enough that a fortnight is drift, not diligence, and - the author can still extend the window for a larger change. On the stamping - change: every QEP that records an outcome carries `version` and `version-hash` from +4. **(v3) Stamp `version` from v0; drop the decision deadline; generate the README + index; carry downstream amendments upstream.** Four changes in one round, each + removing a place where this document described machinery that did not exist or + practice the team did not follow. + + **The decision deadline goes** and step 3 of *How a QEP is decided* with it; the + acceptance trigger becomes *no objection outstanding* rather than a date. The + evidence and the declined alternative (enforcing it in CI) are under *Alternatives + considered*. `Roles` follows, and nothing else in the process depends on a date. + + **The README index is generated** post-merge from each QEP's frontmatter, ordered by + number, so a PR no longer carries its own row and two QEP PRs can no longer collide + on one line of one table — the add/add conflict that made + [#18](https://github.com/QuantEcon/qeps/pull/18) unmergeable against QEP-3's row. A + branch that still carries a row conflicts textually, but a *mis-resolved* index + conflict is self-healing, because the regeneration restores the table from + frontmatter whatever the resolution did. Shipped ahead of this amendment in + [#30](https://github.com/QuantEcon/qeps/pull/30), together with the ordered-list + check the *Automation* section now names; `AGENTS.md` follows there. + + **Numbering** states what already happened informally: a number is reserved when its + draft PR opens and released if that PR closes unmerged, so index gaps are normal. + + **A downstream QEP carries its upstream amendments**, and where the superseded + standard is not a QEP the Adoption section must name a handover date — closing + [#9](https://github.com/QuantEcon/qeps/issues/9), whose sentence this is, widened by + the case it did not anticipate: QEP-6's precedence clause over an external tracker + contract, with no date on it. + + On the stamping change: every QEP that records an outcome carries `version` and + `version-hash` from the moment it lands, so tooling reads one uniform contract instead of treating an absent `version` as an implicit v0 with no anchor — the asymmetry surfaced by QEP-2's machine-readable appendix ([#22](https://github.com/QuantEcon/qeps/issues/22)). From c06cffdda5333d611cc1d441a03ad69d6d04e81c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 11:42:40 +0000 Subject: [PATCH 4/7] QEP-1 v3: acceptance is one other Core Maintainer's approval; a handover names the item that discharges it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two amendments to the v3 text before it lands, both from the adversarial review of the 2026-09-08 rulings. Step 3 said any Core Maintainer may accept a QEP, which let an author accept their own — and the first Accepted-status PR after the rule was drafted would have been exactly that. Acceptance is now an approving review from a Core Maintainer other than the author, still by lazy consensus and still without a clock: one other reader is the whole check, and it is the external check the deadline never supplied. Roles, step 4, the Alternatives entry, Adoption entry 4 and the README follow. The rule for a QEP that supersedes an external contract asked its Adoption section for "a date by which the handover completes". A date is the thing this same amendment has just removed from the decision process for describing what the team does not do; what a handover needs is an owner and a state, which is a tracking issue in the contract's own repository. The Adoption section now names the item that discharges the obligation. QEP-6 applies this first, naming QuantEcon/status-projects#114. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Hwm5shrGXmaq4r3Lp9kS33 --- README.md | 6 ++--- qeps/qep-0001-purpose-and-process.md | 40 +++++++++++++++++----------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 8452c1d..3655ee6 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ and each QEP's `version-hash` is stamped into its frontmatter at merge; `Version 2. **Draft it.** Copy [`qeps/template.md`](qeps/template.md) to `qeps/qep-XXXX-short-slug.md`, fill it in with **Status: Draft**, and open a PR. The index row above is generated at merge — do not add one. -3. **Decide.** Any Core Maintainer may accept it once it has been announced and no - objection is outstanding; the QEP is merged recording the outcome (Accepted / - Rejected / Withdrawn). +3. **Decide.** A Core Maintainer other than the author approves it once it has been + announced and no objection is outstanding — nobody accepts their own QEP; the QEP is + merged recording the outcome (Accepted / Rejected / Withdrawn). The process itself is defined in **QEP-1**. diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index c089a25..1d830a9 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -108,14 +108,17 @@ standard. 2. **Draft.** Open a PR adding `qeps/qep-XXXX-slug.md` from the template, with **Status: Draft** and a discussion link. The README index row is generated at merge — do not add one. -3. **Decide.** Any **Core Maintainer** may accept a QEP once it has been announced and - **no objection is outstanding** — lazy consensus, with no clock. Objections are - raised as PR comments, and an outstanding one blocks acceptance until it is resolved - or withdrawn. If there is no consensus, the lead (@jstac) decides or defers. There is - deliberately **no decision deadline**; see *Alternatives considered* for why the v1–v2 - design had one and why it was dropped. +3. **Decide.** A QEP is accepted when a **Core Maintainer other than its author** + approves it — an approving review on the PR — once it has been announced and + **no objection is outstanding**: lazy consensus, with no clock. The author does not + accept their own QEP; one other reader is the whole check, and a team this size has + no use for a larger one. Objections are raised as PR comments, and an outstanding one + blocks acceptance until it is resolved or withdrawn. If there is no consensus, the + lead (@jstac) decides or defers. There is deliberately **no decision deadline**; see + *Alternatives considered* for why the v1–v2 design had one and why it was dropped. 4. **Record.** On acceptance, set **Status: Accepted** in the frontmatter and the header - table, confirm the number, and merge. The PR itself carries no `version`: CI stamps + table, confirm the number, and merge — the approval is the acceptance and the merge + records it, so either party may merge. The PR itself carries no `version`: CI stamps **`version: 0`** and its `version-hash` anchor at merge (see *Versioning*). ### Amending an accepted QEP @@ -140,8 +143,9 @@ amendments to the standards it touches, version-bumped per the rule above. Refer target then land atomically, `main` never holds a dangling cross-QEP link, and reviewers see the whole blast radius as one diff. Where the superseded standard is **not itself a QEP** — an external contract in another repository — the Adoption section names the -contract, the obligation, and a **date by which the handover completes**: an undated -precedence sentence is a handover that never happens. +contract, the obligation, and **the item that discharges it**: a tracking issue in the +contract's own repository, so the handover has an owner and a state rather than a date. +A precedence sentence with nothing to discharge it is a handover that never happens. **Squash-merge only.** Each amendment lands as a single commit, so a QEP's history reads as one line per change. This is a repository setting, not a convention to @@ -258,7 +262,8 @@ them to four digits (`qep-0001-…`). This QEP is QEP-1. - **Author** — anyone may write a QEP; typically a maintainer. The author announces the PR and drives the discussion. -- **Core Maintainers** — decide by lazy consensus, once no objection is outstanding. +- **Core Maintainers** — decide by lazy consensus, once no objection is outstanding; + acceptance is an approving review from one of them who is not the author. - **Lead** (@jstac) — breaks ties and may defer a decision. No sponsor, delegate, or editor role is introduced; the process is intended to stay as @@ -329,7 +334,9 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or which lazy consensus resolves — is served as well by *no objection outstanding*, which a reviewer can establish by reading the thread. The cost of dropping it is that nothing is accepted by the mere passage of time; a QEP nobody is looking at stays open - until someone looks. + until someone looks. The external check the deadline never supplied is now step 3's: + an author cannot accept their own QEP, so at least one other Core Maintainer has read + it before it lands. ## Adoption @@ -360,8 +367,10 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or practice the team did not follow. **The decision deadline goes** and step 3 of *How a QEP is decided* with it; the - acceptance trigger becomes *no objection outstanding* rather than a date. The - evidence and the declined alternative (enforcing it in CI) are under *Alternatives + acceptance trigger becomes *no objection outstanding* rather than a date, and the + accepting party becomes **a Core Maintainer other than the author** — an author does + not accept their own QEP, which a deadline never prevented. The evidence and the + declined alternative (enforcing the deadline in CI) are under *Alternatives considered*. `Roles` follows, and nothing else in the process depends on a date. **The README index is generated** post-merge from each QEP's frontmatter, ordered by @@ -378,10 +387,11 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or draft PR opens and released if that PR closes unmerged, so index gaps are normal. **A downstream QEP carries its upstream amendments**, and where the superseded - standard is not a QEP the Adoption section must name a handover date — closing + standard is not a QEP the Adoption section must name the item that discharges the + handover — closing [#9](https://github.com/QuantEcon/qeps/issues/9), whose sentence this is, widened by the case it did not anticipate: QEP-6's precedence clause over an external tracker - contract, with no date on it. + contract, with nothing to discharge it. On the stamping change: every QEP that records an outcome carries `version` and `version-hash` from From 3a670fbc169d3e277c841de4b3a77d08611f0208 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 9 Sep 2026 08:57:17 +1000 Subject: [PATCH 5/7] QEP-1 v3: drop the cross-standard amendment rule; v3 is three changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the "A QEP that touches another standard carries the amendment itself" paragraph and its Adoption entry. QEPs are the source of truth and are independent of every repository: a downstream consumer adapts to a QEP, and the QEP owes it nothing in return. Under that model the external half of the rule has no subject — the status-projects tracker contract is a consumer that must conform, not a peer standard being superseded — so a QEP has no cause to assert precedence over it or to name a tracking issue in another repository. Naming one would also make the Adoption section a status pointer into a repo this one does not own. The intra-repo half (carry the upstream edit in the downstream QEP's own PR; do not forward-reference a draft from an accepted standard) stays practice rather than QEP-1 text. QuantEcon/qeps#9 stays open recording that, and the explanation is on the issue. Co-Authored-By: Claude Opus 5 (1M context) --- qeps/qep-0001-purpose-and-process.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index 1d830a9..273f796 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -136,17 +136,6 @@ point, and it evolves in small, frequent steps. Two paths keep that change order that marks the old one `Superseded` (link it). Superseding is reserved for a genuine rethink, not routine maintenance. -**A QEP that touches another standard carries the amendment itself.** A QEP that -depends on, extends or supersedes an existing standard is *not* forward-referenced by -that standard while in development; instead the new QEP's acceptance PR carries the -amendments to the standards it touches, version-bumped per the rule above. Reference and -target then land atomically, `main` never holds a dangling cross-QEP link, and reviewers -see the whole blast radius as one diff. Where the superseded standard is **not itself a -QEP** — an external contract in another repository — the Adoption section names the -contract, the obligation, and **the item that discharges it**: a tracking issue in the -contract's own repository, so the handover has an owner and a state rather than a date. -A precedence sentence with nothing to discharge it is a handover that never happens. - **Squash-merge only.** Each amendment lands as a single commit, so a QEP's history reads as one line per change. This is a repository setting, not a convention to remember. @@ -362,9 +351,8 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or belongs in a tracking issue. Applied first by QEP-2, whose acceptance PR carries this amendment. 4. **(v3) Stamp `version` from v0; drop the decision deadline; generate the README - index; carry downstream amendments upstream.** Four changes in one round, each - removing a place where this document described machinery that did not exist or - practice the team did not follow. + index.** Three changes in one round, each removing a place where this document + described machinery that did not exist or practice the team did not follow. **The decision deadline goes** and step 3 of *How a QEP is decided* with it; the acceptance trigger becomes *no objection outstanding* rather than a date, and the @@ -386,13 +374,6 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or **Numbering** states what already happened informally: a number is reserved when its draft PR opens and released if that PR closes unmerged, so index gaps are normal. - **A downstream QEP carries its upstream amendments**, and where the superseded - standard is not a QEP the Adoption section must name the item that discharges the - handover — closing - [#9](https://github.com/QuantEcon/qeps/issues/9), whose sentence this is, widened by - the case it did not anticipate: QEP-6's precedence clause over an external tracker - contract, with nothing to discharge it. - On the stamping change: every QEP that records an outcome carries `version` and `version-hash` from the moment it lands, so tooling reads one uniform contract instead of treating an From f9deb5fe382ca3a833814ec9cd55d27e8b77b091 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Wed, 9 Sep 2026 11:48:47 +1000 Subject: [PATCH 6/7] README: the index note matches QEP-1 v3's version rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The note said `Version` reads `–` until a QEP is first amended — the v1–v2 rule this amendment replaces. Under v3 every merged QEP carries a version from the moment it lands, so the column shows `v{N}` from `v0` up and a `–` means only that the stamp has not reached that QEP yet, which QEP-1's Adoption section commits to closing. Stated as what the column means rather than hedged with when the automation arrives: the pointer to Adoption carries the gap, as it does for the rest of this amendment. The note's other stale half — "the Type/Version columns are kept in sync by CI", which whole-table regeneration replaces — is fixed in #30, the PR that makes it false. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3655ee6..ff8e5aa 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ need a QEP. QEPs that set an ongoing rule are **maintained in place**: a substantive amendment bumps the QEP's `version` (shown above) under the same review process, rather than superseding the whole document — see **QEP-1**. The `Type`/`Version` columns are kept in sync by CI, -and each QEP's `version-hash` is stamped into its frontmatter at merge; `Version` reads -`–` until a QEP is first amended. +and each QEP's `version-hash` is stamped into its frontmatter at merge. +`Version` shows `v{N}` from `v0` up; a `–` marks a QEP the stamp has not reached yet +(see QEP-1's *Adoption*). ## Proposing a QEP From ff2fda9eaafed6f7c5cf4ede065d55ab3e8ec724 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Thu, 10 Sep 2026 16:37:50 +1000 Subject: [PATCH 7/7] QEP-1 v3: state the second-reader rule, define Core Maintainers, trim narration Step 3 states the rule only; the rationale lives under Alternatives considered. Substantive amendments are accepted the same way as a new QEP; editorial changes need no second reader. Roles defines Core Maintainers by admin rights on this repository. The deadline entry under Alternatives keeps the principle and links #26 for the evidence; Adoption item 4 is reordered to match its title, drops the PR narration, and points the follow-up at #22. The unreachable Draft clause on the README Version column is removed. qeps/template.md and AGENTS.md follow the v3 version rule so neither describes the implicit-v0 semantics after this merges. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 30 +++++--- qeps/qep-0001-purpose-and-process.md | 108 ++++++++++----------------- qeps/template.md | 8 +- 3 files changed, 62 insertions(+), 84 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c2863a6..f6fafe6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,9 +39,9 @@ table from frontmatter whatever the resolution did to it. ## Accepting a QEP -When a QEP reaches a decision (see QEP-1 for the lazy-consensus rule), apply the outcome -in a **single PR**. The status lives in **two places in the document** — keep them in -sync: +When a QEP reaches a decision (QEP-1: no objection outstanding, and an approving review +from a Core Maintainer other than the author), apply the outcome in a **single PR**. The +status lives in **two places in the document** — keep them in sync: 1. the YAML frontmatter `status:` field, and 2. the **Status** row in the in-document header table. @@ -64,8 +64,9 @@ that state. QEP-3 did, for two months. Copy [`qeps/template.md`](qeps/template.md) to `qeps/qep-XXXX-slug.md`, fill it in with **Status: Draft** and a discussion link, and open a PR. **Do not add a README index -row** — it is generated from the frontmatter when the PR merges. A new QEP is unversioned -(implicitly v0): omit the `version` field. See QEP-1 for the full process. +row** — it is generated from the frontmatter when the PR merges. Omit the `version` +field: CI stamps `version: 0` and `version-hash` at the merge that records the outcome +(QEP-1 v3; the stamp change is tracked in #22). See QEP-1 for the full process. ## Amending an accepted QEP @@ -97,8 +98,11 @@ it to consumers you have never heard of. - **Substantive** (any change to normative content — a rule, a value, a table row, a machine-readable appendix): increment `version` by one in **both** the frontmatter and - the header table. The first amendment introduces `version: 1` and adds a **Version** - row to the header table; the README `Version` column moves from `–` to `v{N}`. + the header table. The first amendment moves the stamped `version: 0` to `1` and adds a + **Version** row to the header table — a v0 QEP has no such row, because the stamp + writes frontmatter only. The README `Version` column is generated. A substantive + amendment is accepted the same way as a new QEP (a second reader who is not the + author); an editorial change needs no second reader. - **Editorial** (no change to normative content — typo, wording, formatting, link): leave `version` unchanged. @@ -118,13 +122,15 @@ GitHub UI choose **Squash and merge**. ## What CI does (don't do these by hand) -- **Post-merge** — [`stamp-version.yml`](.github/workflows/stamp-version.yml) stamps the - merged short hash into the `version-hash` field and **regenerates the whole README +- **Post-merge** — [`stamp-version.yml`](.github/workflows/stamp-version.yml) stamps + `version: 0` into any QEP that has left Draft and carries no `version`, stamps the + merged short hash into the `version-hash` field, and **regenerates the whole README index** from each QEP's frontmatter, ordered by number. - **On every PR** — [`qep-checks.yml`](.github/workflows/qep-checks.yml) checks that - `version` moves legally (a new QEP starts unversioned; a versioned QEP stays versioned; - the number stays the same or increases by exactly one), that `type` and `status` are - known values, that `related:` and the header table's **Related** row agree, and that + `version` moves legally (a new QEP arrives unversioned in its PR and is stamped `v0` + at merge; a versioned QEP stays versioned; the number stays the same or increases by + exactly one), that `type` and `status` are known values, that `related:` and the + header table's **Related** row agree, and that **ordered-list markers ascend in source** (see below). A stale README index is a warning, not a failure. diff --git a/qeps/qep-0001-purpose-and-process.md b/qeps/qep-0001-purpose-and-process.md index 273f796..843d2a0 100644 --- a/qeps/qep-0001-purpose-and-process.md +++ b/qeps/qep-0001-purpose-and-process.md @@ -108,14 +108,12 @@ standard. 2. **Draft.** Open a PR adding `qeps/qep-XXXX-slug.md` from the template, with **Status: Draft** and a discussion link. The README index row is generated at merge — do not add one. -3. **Decide.** A QEP is accepted when a **Core Maintainer other than its author** - approves it — an approving review on the PR — once it has been announced and - **no objection is outstanding**: lazy consensus, with no clock. The author does not - accept their own QEP; one other reader is the whole check, and a team this size has - no use for a larger one. Objections are raised as PR comments, and an outstanding one - blocks acceptance until it is resolved or withdrawn. If there is no consensus, the - lead (@jstac) decides or defers. There is deliberately **no decision deadline**; see - *Alternatives considered* for why the v1–v2 design had one and why it was dropped. +3. **Decide.** Objections are raised as PR comments; an outstanding objection blocks + acceptance until it is resolved or withdrawn. Once the PR has been announced and + **no objection is outstanding**, a **Core Maintainer other than the author** accepts + the QEP with an approving review; the author does not accept their own QEP. There + is no decision deadline. If there is no consensus, the lead (@jstac) decides or + defers. 4. **Record.** On acceptance, set **Status: Accepted** in the frontmatter and the header table, confirm the number, and merge — the approval is the acceptance and the merge records it, so either party may merge. The PR itself carries no `version`: CI stamps @@ -128,10 +126,11 @@ guide, editorial conventions — is a **living standard**: its *current state* i point, and it evolves in small, frequent steps. Two paths keep that change orderly: - **Amend in place.** A substantive evolution of the *same* standard — tweak a value, - add a label, clarify a rule — is a normal PR against the QEP, reviewed under the same - lazy-consensus rule, that bumps the QEP's `version` (below). The document stays - `Accepted`, and a reader always sees one current standard instead of chasing a chain - of superseding documents. + add a label, clarify a rule — is a normal PR against the QEP that bumps the QEP's + `version` (below), accepted the same way as a new QEP: no objection outstanding, and + an approving review from a Core Maintainer other than the author. An editorial change + needs no second reader. The document stays `Accepted`, and a reader always sees one + current standard instead of chasing a chain of superseding documents. - **Supersede.** A *different* decision that replaces the QEP wholesale is a **new** QEP that marks the old one `Superseded` (link it). Superseding is reserved for a genuine rethink, not routine maintenance. @@ -207,8 +206,7 @@ hand-maintained changelog (which would drift and clutter the document): Type and version are surfaced two ways: - the **README index** carries `Type` and `Version` columns, with `Version` showing - `v{N}` from `v0` up (`–` only while a QEP is Draft) — repo-controlled, so it - renders on any theme; + `v{N}` from `v0` up — repo-controlled, so it renders on any theme; - under the **QuantEcon theme** (once adopted), a coloured **`type` pill** always and a **`version` pill** once the QEP leaves Draft — e.g. `standard` · `v2`; `v0` is shown rather than hidden, since it names an anchored revision. @@ -243,16 +241,16 @@ opening the PR; it is confirmed (and adjusted if two proposals collide) at merge number is **reserved when the draft PR opens** and released if that PR is closed without merging. The README index lists only merged QEPs, so **gaps are normal while drafts are open**, and a QEP that merges out of numeric order takes its numeric position -automatically. -Numbers are written **unpadded** in text (QEP-1, QEP-2, …); only the filename zero-pads -them to four digits (`qep-0001-…`). This QEP is QEP-1. +automatically. Numbers are written **unpadded** in text (QEP-1, QEP-2, …); only the +filename zero-pads them to four digits (`qep-0001-…`). This QEP is QEP-1. ### Roles - **Author** — anyone may write a QEP; typically a maintainer. The author announces the PR and drives the discussion. -- **Core Maintainers** — decide by lazy consensus, once no objection is outstanding; - acceptance is an approving review from one of them who is not the author. +- **Core Maintainers** — the maintainers with admin rights on `QuantEcon/qeps`. They + decide by lazy consensus, once no objection is outstanding; acceptance is an + approving review from one of them who is not the author. - **Lead** (@jstac) — breaks ties and may defer a decision. No sponsor, delegate, or editor role is introduced; the process is intended to stay as @@ -310,22 +308,16 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or normative tooling input with nothing to cite ([#22](https://github.com/QuantEcon/qeps/issues/22)). Uniform stamping from v0 costs a pill and a bot commit; the implicit v0 cost correctness, in prose that described pinning which did not yet exist. - - **A recorded decision deadline, with lazy consensus resolving at it.** The v1–v2 design: the author set a comment window and recorded a decision date in the PR - description, and the Core Maintainers decided at it. Dropped in v3 because it - described something the team did not do. On 2026-09-08 **none of the four open QEP - pull requests had a live deadline** — one had never set one in 47 days, two had - passed, and one was deliberately deferred pending evidence — while nothing in CI ever - read a PR description, so the field was required by this document and checked by - nothing. Enforcing it was considered and declined: a check would have turned three - open PRs red for a rule they predated, and the deadline's real function — a moment at - which lazy consensus resolves — is served as well by *no objection outstanding*, which - a reviewer can establish by reading the thread. The cost of dropping it is that - nothing is accepted by the mere passage of time; a QEP nobody is looking at stays open - until someone looks. The external check the deadline never supplied is now step 3's: - an author cannot accept their own QEP, so at least one other Core Maintainer has read - it before it lands. + description, and the Core Maintainers decided at it. Dropped in v3 because nothing + enforced it and the team did not keep it + ([#26](https://github.com/QuantEcon/qeps/issues/26)). Enforcing it in CI was + considered and declined: the deadline's function — a moment at which lazy consensus + resolves — is served as well by *no objection outstanding*, which a reviewer + establishes by reading the thread. The cost is that nothing is accepted by the passage + of time; a QEP nobody looks at stays open until someone looks. The check the deadline + never supplied is the second reader in step 3. ## Adoption @@ -351,39 +343,19 @@ type; a one-off *decision* is a `standard` if it sets an ongoing rule, or belongs in a tracking issue. Applied first by QEP-2, whose acceptance PR carries this amendment. 4. **(v3) Stamp `version` from v0; drop the decision deadline; generate the README - index.** Three changes in one round, each removing a place where this document - described machinery that did not exist or practice the team did not follow. - - **The decision deadline goes** and step 3 of *How a QEP is decided* with it; the - acceptance trigger becomes *no objection outstanding* rather than a date, and the - accepting party becomes **a Core Maintainer other than the author** — an author does - not accept their own QEP, which a deadline never prevented. The evidence and the - declined alternative (enforcing the deadline in CI) are under *Alternatives - considered*. `Roles` follows, and nothing else in the process depends on a date. - - **The README index is generated** post-merge from each QEP's frontmatter, ordered by - number, so a PR no longer carries its own row and two QEP PRs can no longer collide - on one line of one table — the add/add conflict that made - [#18](https://github.com/QuantEcon/qeps/pull/18) unmergeable against QEP-3's row. A - branch that still carries a row conflicts textually, but a *mis-resolved* index - conflict is self-healing, because the regeneration restores the table from - frontmatter whatever the resolution did. Shipped ahead of this amendment in - [#30](https://github.com/QuantEcon/qeps/pull/30), together with the ordered-list - check the *Automation* section now names; `AGENTS.md` follows there. - - **Numbering** states what already happened informally: a number is reserved when its - draft PR opens and released if that PR closes unmerged, so index gaps are normal. - - On the stamping change: every QEP that records an outcome carries `version` and - `version-hash` from - the moment it lands, so tooling reads one uniform contract instead of treating an + index.** Every QEP that records an outcome carries `version` and `version-hash` from + the merge that records it, so tooling reads one contract instead of treating an absent `version` as an implicit v0 with no anchor — the asymmetry surfaced by QEP-2's - machine-readable appendix ([#22](https://github.com/QuantEcon/qeps/issues/22)). - Stamping's supporting changes, landing as a follow-up once this amendment merges: the - post-merge stamp action adds `version: 0` where missing; already-merged QEPs past - Draft are backfilled, each stamped with the most recent commit that touched it — - mechanical, and consistent with the rule that editorial changes move the hash; the - pull-request check's new-QEP rule becomes "unversioned in the PR, `v0` at merge"; the - README index and the theme's version pill show `v0` rather than `–`; `AGENTS.md` and - the README's index note follow. `qeps/template.md` is unchanged — `version` is - machine-written, never hand-written. + machine-readable appendix. Supporting changes: the post-merge stamp action writes + `version: 0` where missing; already-merged QEPs past Draft are backfilled, each + stamped with the most recent commit that touched it; the pull-request check's + new-QEP rule becomes "unversioned in the PR, `v0` at merge"; the README index and + the theme's version pill show `v0` rather than `–` + ([#22](https://github.com/QuantEcon/qeps/issues/22) tracks them). The decision + deadline goes: acceptance is *no objection outstanding* plus an approving review from + a Core Maintainer other than the author, and `Roles` follows. The README index is + generated post-merge from each QEP's frontmatter, ordered by number, so a PR carries + no row of its own and two QEP PRs cannot collide on one line of one table; + *Numbering* states the reservation rule that makes index gaps normal, and the + pull-request check requires ordered-list markers to ascend in source. + `qeps/template.md` and `AGENTS.md` follow in the same round. diff --git a/qeps/template.md b/qeps/template.md index b63c5e4..1195d69 100644 --- a/qeps/template.md +++ b/qeps/template.md @@ -9,10 +9,10 @@ discussion: --- - + # QEP-N: