diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py index aafe85c..77dc613 100644 --- a/00-META/checks/cycle.py +++ b/00-META/checks/cycle.py @@ -14,7 +14,8 @@ What is enforced: its owning code (`code:`) -- no development without a design that says where. issues a known `status:`; once `located`, `located-in:` names the owner; once `resolved`, `fixed-by:` says what fixed it (prose counts -- - "nothing, the capability existed" is an answer). + "nothing, the capability existed" is an answer). And no two records share a + number -- the number is how a record is cited. research a known `status:`; a `graduated` overview says what it `became:`, and every target it names exists. decisions every accepted record is REACHABLE from the cycle: cited by a design doc's @@ -109,6 +110,27 @@ def main(): "without a design that says where" % status) # ---- issues ------------------------------------------------------------------------ + # Two records may not share a number. Numbers are taken as "next free after main", and work + # sits on unmerged branches for days -- so two people reading the same main allocate the same + # number, and nothing said so. It happened twice in one evening between two machines, and the + # second collision landed on main with all three checks passing (issue 155). An issue number is + # how every other record cites this one; two records answering to it means a pointer that + # resolves to whichever the reader happened to open. + seen = {} + for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))): + name = os.path.basename(os.path.normpath(folder)) + number = name.split("-", 1)[0] + if not number.isdigit(): + continue + if number in seen: + bad(os.path.join("04-ISSUES", name), + "is numbered %s, and so is %s -- an issue number is how it is cited, and two " + "records answering to one means a citation that resolves to whichever the reader " + "opened. Take the next free number across main AND every open pull request" + % (number, seen[number])) + else: + seen[number] = name + for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))): front = frontmatter(path) if front is None: diff --git a/04-ISSUES/149-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md b/04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md similarity index 97% rename from 04-ISSUES/149-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md rename to 04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md index c13aa01..f2907ef 100644 --- a/04-ISSUES/149-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md +++ b/04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md @@ -8,7 +8,7 @@ fixed-by: amended-design: --- -# 149 — An adopted machine's data cannot be placed where it is +# 153 — An adopted machine's data cannot be placed where it is ## What was observed diff --git a/04-ISSUES/150-a-machines-own-network-is-not-a-reach/00-report.md b/04-ISSUES/154-a-machines-own-network-is-not-a-reach/00-report.md similarity index 97% rename from 04-ISSUES/150-a-machines-own-network-is-not-a-reach/00-report.md rename to 04-ISSUES/154-a-machines-own-network-is-not-a-reach/00-report.md index c49970f..57a8e33 100644 --- a/04-ISSUES/150-a-machines-own-network-is-not-a-reach/00-report.md +++ b/04-ISSUES/154-a-machines-own-network-is-not-a-reach/00-report.md @@ -8,7 +8,7 @@ fixed-by: amended-design: --- -# 150 — A machine's own network is not a reach +# 154 — A machine's own network is not a reach ## What was observed diff --git a/04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md b/04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md new file mode 100644 index 0000000..ec19f6e --- /dev/null +++ b/04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md @@ -0,0 +1,53 @@ +--- +status: resolved +opened: 2026-09-29 +located-in: [hq 00-META/checks/cycle.py] +fixed-by: hq issue/two-records-share-a-number-and-nothing-says-so +amended-design: +--- + +# 155 — Two records may share a number, and every check passes + +## What was observed + +On 2026-09-29 two machines opened issues against this repository within the same hour. Both read +`main` correctly and both took "the next free number", and they collided twice: + +| | one machine opened | the other had already used | +|---|---|---| +| first | 147, 148 | 147, 148 on an unmerged branch | +| second | 149, 150 | 149 on an unmerged branch, 150 from renumbering the first collision | + +The first collision was reconciled by hand before merging. The second was **merged into `main`**, and +`records.py`, `cycle.py` and `index.py` all reported success over a tree holding +`149-a-declaration-that-shrinks-to-empty` beside `149-an-adopted-machines-data-cannot-be-placed-where-it-is`, +and two folders numbered 150. + +## Why + +The number is allocated as `max(main) + 1`, and `main` lags every open pull request — seven of them +that evening. Two readers of the same `main` therefore compute the same next number, and neither is +doing anything wrong. The existing reconciliation precedent (a second record numbered 127 became 149) +assumed a single writer, which stopped being true when a second machine began filing its own findings. + +## Why it matters + +An issue number is how every other record cites this one — `fixed-by:`, `located-in:`, a decision +record's consequence, a commit message. Two records answering to one number is a citation that +resolves to whichever folder the reader happened to open, and the failure is silent on both sides: +the citer is not wrong, and the cited record exists. + +It is also exactly the class this repository says it does not permit — a rule (`00-META/process/03-issues.md`: +"take the next free number") enforced by nothing. + +## How it was fixed, and how the fix is checked + +`cycle.py` now refuses a tree in which two issue folders share a leading number, and names both. +Proven by adding a duplicate and watching it fail, then removing it and watching it pass. + +The colliding records were renumbered 153 and 154, in the branch that landed last — renumbering a +branch whose author is still pushing only moves the race. + +**The check catches the collision; it does not prevent it.** Allocating a number still needs the open +pull requests read as well as `main`. That is a habit the check now backstops rather than one it +replaces.