Compare commits

...

2 Commits

Author SHA1 Message Date
Brett Lawson b239f8d43e plan: use a dash, not a dot, for extra records in one PR (WR-1255-2)
Headings end the id with a period, so the dotted form was ambiguous. The
check, its tests and the site parser now take <SERIES>-<N>(-<k>)?, and the
README says to prefer one record per PR.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEtgaA54W2nr8dK1EYp
2026-11-08 16:36:23 -05:00
Brett Lawson 4822201364 plan: number new upstream-issue records by the PR that adds them
A new record takes the number of its PR (WR-1255); further records of the
same series in that PR are WR-1255.2, WR-1255.3. The check, its tests and
the site heading parser accept the dotted form. Existing ids are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEtgaA54W2nr8dK1EYp
2026-11-08 16:36:23 -05:00
4 changed files with 52 additions and 18 deletions
+22 -10
View File
@@ -4,10 +4,23 @@ One file per upstream C++ defect, so parallel PRs never conflict. Never append t
shared file. The rules, fields, severity order and numbering are below (from the former
archive). The site (`site/build.py`) renders every file here, in name order.
- **Name:** `<ID>--<short-slug>.md`, e.g. `WR-262--extrusion-drop-point-fast.md`. The ID
takes the next free number in its series (`UB-`, `HANG-`, `ND-`, `WR-`, `DOC-`) across
this directory. If two PRs pick the same number, whoever merges second renumbers theirs
(the site flags duplicates).
- **Name:** `<ID>--<short-slug>.md`, e.g. `WR-1255--extrusion-drop-point-fast.md`. The
series is `UB-`, `HANG-`, `ND-`, `WR-` or `DOC-`. **The number is the number of the PR
that adds the record.** A second record of the same series in the same PR is
`WR-1255-2--other-slug.md`, a third `WR-1255-3--...`, and so on (the first has no
suffix). Prefer one record per PR; use `-2`, `-3` only when records genuinely belong
together. Open the draft PR first (the workflow does), then name the file.
- *Why:* "the next free number" made parallel PRs pick the same one, and the later PR
had to renumber the record and every citation of it (code comments, kernel field
docs, known mismatches, findings). A PR number is unique and known before the record
is written, so two PRs can never collide.
- *Citing:* cite the full id (`WR-1255`, `WR-1255-2`) exactly as in the file name and
the `#### ` heading, which reads `#### WR-1255-2. One-line title`.
- *Existing records* keep their ids and are never renamed, so numbers up to about
`WR-469` come from the old scheme and gaps are normal. The legacy duplicate ids
(section below) stay as they are.
- `scripts/ci/check_record_ids.py` (CI job `ci-scripts`) fails on a repeated id and on
an addendum that names no record.
- **Content:**
```markdown
@@ -76,10 +89,10 @@ Severity order: crash/UB, hang, nondeterminism, wrong result, docs. Entries are
by area within each severity, so IDs are in order within each area but not across a
whole severity.
**Numbering.** A new entry takes the next free number in its series (`UB-`, `HANG-`,
`ND-`, `WR-`, `DOC-`) at the moment its PR merges. If another PR took that number in the
meantime, whoever merges renumbers the new entry and every reference to it (here, in
`plan/findings.md`, `plan/modules.toml` and code comments). Numbers are not reused.
**Numbering.** Superseded: new entries are numbered by the PR that adds them (see
`Name` above). The text below is the archive's original rule: a new entry took the next
free number in its series (`UB-`, `HANG-`, `ND-`, `WR-`, `DOC-`) when its PR merged, and
whoever merged second renumbered. Numbers are not reused.
<!-- archive:intro end -->
<!-- archive:note-duplicate-ids begin (generated by scripts/split_archives.py) -->
@@ -121,6 +134,5 @@ title (other references in the repo in brackets):
`SmFilletEdge::CalcBlendingRail` builds its Hermite fallback as a degree-2 curve
[`blend/fillet_geom/p6.rs`].
New entries take the next free number after the highest in their series, as the
Numbering paragraph at the top says.
New entries are numbered by their PR, as `Name` at the top says.
<!-- archive:note-duplicate-ids end -->
+6 -4
View File
@@ -2,7 +2,9 @@
"""Fails if two plan/upstream-issues/ records share an ID, or an addendum names no record.
A record is `<SERIES>-<N>--<slug>.md` (e.g. `WR-262--extrusion-drop-point-fast.md`); its ID is
`<SERIES>-<N>`. An addendum is `<SERIES>-<N>--addendum-<YYYY-MM-DD>-<slug>.md` and must name
`<SERIES>-<N>`. New records are numbered by the PR that adds them, and the k-th record of a
series in one PR (k >= 2) is `<SERIES>-<N>-<k>--<slug>.md` with ID `<SERIES>-<N>-<k>`.
An addendum is `<SERIES>-<N>[-<k>]--addendum-<YYYY-MM-DD>-<slug>.md` and must name
the ID of an existing record. Names without a numeric ID (`UB-new--`, `UB-unnumbered--`,
`HANG-unnumbered--`, `UNNUMBERED--`) and README.md are not checked.
@@ -19,8 +21,8 @@ from collections import defaultdict
from pathlib import Path
ISSUES = Path("plan/upstream-issues")
RECORD = re.compile(r"^([A-Z]+-[0-9]+)--(.+)\.md$")
ADDENDUM = re.compile(r"^([A-Z]+-[0-9]+)--addendum-[0-9]{4}-[0-9]{2}-[0-9]{2}-.+\.md$")
RECORD = re.compile(r"^([A-Z]+-[0-9]+(?:-[2-9]|-[1-9][0-9]+)?)--(.+)\.md$")
ADDENDUM = re.compile(r"^([A-Z]+-[0-9]+(?:-[2-9]|-[1-9][0-9]+)?)--addendum-[0-9]{4}-[0-9]{2}-[0-9]{2}-.+\.md$")
ADDENDUM_LIKE = re.compile(r"--addendum")
# Records sharing an ID when this check was added (2026-10-26). The `--dupN` names come from
@@ -92,7 +94,7 @@ def check(names, legacy=None):
new = [f for f in files if f not in legacy.get(rid, set())]
errors.append(
f"{rid} is used by {len(files)} records: {', '.join(files)}. "
f"Give {', '.join(new)} the next free ID in its series; to update a record, "
f"Give {', '.join(new)} an ID that is not in use (the number of the PR that adds it, `-2`, `-3`, ... for more records in that PR); to update a record, "
"add an addendum file instead."
)
for name, rid in addenda:
+21 -1
View File
@@ -45,7 +45,27 @@ class CheckRecordIds(unittest.TestCase):
self.assertEqual(c.check(["UB-7--a.md", "UB-7--b--dup2.md"], legacy=legacy), [])
errors = c.check(["UB-7--a.md", "UB-7--b--dup2.md", "UB-7--c.md"], legacy=legacy)
self.assertEqual(len(errors), 1)
self.assertIn("UB-7--c.md the next free ID", errors[0])
self.assertIn("UB-7--c.md an ID that is not in use", errors[0])
def test_dashed_ids_are_distinct_records(self):
names = ["WR-1250--a.md", "WR-1250-2--b.md", "WR-1250-3--c.md", "WR-12--d.md"]
self.assertEqual(c.check(names, legacy={}), [])
def test_shared_dashed_id_fails(self):
errors = c.check(["WR-1250-2--a.md", "WR-1250-2--b.md"], legacy={})
self.assertEqual(len(errors), 1)
self.assertIn("WR-1250-2 is used by 2 records", errors[0])
def test_dash_one_is_not_an_id(self):
# The first record of a PR has no suffix; `-1` is not a valid form, so it is skipped.
self.assertEqual(c.check(["WR-1250--a.md", "WR-1250-1--b.md"], legacy={}), [])
def test_addendum_names_dashed_record(self):
names = ["WR-1250-2--a.md", "WR-1250-2--addendum-2026-10-27-more.md"]
self.assertEqual(c.check(names, legacy={}), [])
errors = c.check(["WR-1250--a.md", "WR-1250-2--addendum-2026-10-27-more.md"], legacy={})
self.assertEqual(len(errors), 1)
self.assertIn("WR-1250-2, but no record", errors[0])
def test_main_reads_directory(self):
with tempfile.TemporaryDirectory() as tmp:
+3 -3
View File
@@ -596,7 +596,7 @@ def parse_issue_files():
if head is None:
continue
title = lines[head][5:].strip()
m = re.match(r"([A-Z]+)-(\d+)\.\s*(.*)$", title)
m = re.match(r"([A-Z]+)-(\d+)(-[2-9]|-[1-9]\d+)?\.\s*(.*)$", title)
header, i = {}, head + 1
while i < len(lines):
f = re.match(r"^- \*\*(Severity|Area):\*\*\s*(.*)$", lines[i])
@@ -614,8 +614,8 @@ def parse_issue_files():
conf = fields.get("Confirmed", "").strip().lower()
entries.append({
"series": m.group(1) if m else "OTHER", "num": int(m.group(2)) if m else 0,
"id": f"{m.group(1)}-{m.group(2)}" if m else title,
"title": m.group(3) if m else title,
"id": f"{m.group(1)}-{m.group(2)}{m.group(3) or ''}" if m else title,
"title": m.group(4) if m else title,
"severity": header.get("Severity", ""), "area": header.get("Area", ""),
"body": body,
"confirmed": "yes" if conf.startswith("yes") else ("no" if conf.startswith("no") else "unknown"),