Graduate research 042 into ADR 0275 and to-be 53, so issue numbers stop colliding
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

Numbers taken by reading a repository many branches write at once collide;
one counter in a module of the mesh's own, advanced by compare-and-set,
ends that at the source. The operator delegated the five open questions
on 2026-10-10; each is decided as the research recommended. The glossary
gains the issue tracker, retires "ticketing system" and lists ticket as a
homonym, since ADR 0258 already uses it for an answer's one-time token.
This commit is contained in:
jochen
2026-10-10 12:31:32 +02:00
parent 835d7acc1a
commit 015ff1adfb
6 changed files with 356 additions and 7 deletions
@@ -0,0 +1,156 @@
---
topic: how we work
status: accepted
date: 2026-10-10
deciders: jochen
reconstructed: false
---
# 275. The mesh numbers and tracks its issues in a module of its own, and hq keeps only the narrative of a design or governance issue
## Context
[Research 042](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md) measured, on 2026-10-10, how this
repository keeps its issues, after the operator asked that day for an issue tracker on the bus that every current issue
is consolidated into, started small and extended along the way.
**Numbers collide.** An author takes the next free number by reading `main` and every open pull request
([playbook 03](../00-META/process/03-issues.md)), and many branches write at once. 31 commits on `main` mention a
renumbering; [issue 155](../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md) records a
collision that reached `main` with every check passing; three decision numbers collided on the evening of 2026-10-07,
and on 2026-10-10 an open pull request still carried a branch named for an issue number another branch had taken
first. `cycle.py` refuses two records sharing a number, which catches a collision after it happens and prevents none.
The operator said on 2026-10-07 that numbering belongs to the mesh's own issue tracker.
**Status is hard to see.** Of 357 issue folders on `main`, 73 were not closed (27 `open`, 45 `located`, 1
`diagnosing`), and finding them means reading frontmatter across the repository. The frontmatter is a small, regular
data set: six keys over 1.9 MiB of markdown, well inside the limits
[ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
§6 sets for a bucket. Nothing connects a condition the controller raises to the issue that would fix it: on 2026-10-10
a condition was silenced with the words "hq issue to follow", and nothing records whether one followed.
**hits**, the headless issue tracker the operator named as inspiration, was studied at release v0.7.0 (findings §6). Its
model is this repository's frontmatter almost key for key, and it numbers an issue by compare-and-set on a counter only
after the issue is validated. Its licence (Sustainable Use License 1.0) limits distribution, its services create their
own bus resources on start, and its words (*item*, *project*, *release*) clash with the glossary.
The research left five questions for the operator, each with a recommendation. **The operator delegated the choice on
2026-10-10**: the recommendations are taken, in the spirit of the original ask, "start small and extend the module
along the way".
### Against GENESIS
- **The mesh owns the truth** ([mission](../00-META/mission.md)). An issue's number and status live in the mesh, in one
place, written through one module, instead of in a file every branch may edit.
- **The mesh remembers** ([effect](../00-META/effect.md)). The public narrative of a design or governance issue stays
here, reviewed in pull requests, so the symptom-to-component memory is kept.
- **Failure must be loud.** The cycle's issue rules refuse a wrong write at the moment it is made, instead of a check
reporting it at merge.
## Considered Options
### Where the truth lives (open question 1)
1. **Split: the tracker holds every issue's number and status; hq keeps a folder only for a design or governance issue,
pointing at the tracker by number.** Adopted.
2. **Everything moves into the tracker.** Rejected: the public record of design questions, which hq exists to be
([ADR 0019](0019-how-this-repository-works.md)), would be lost, and with it the memory playbook 03 says is never
deleted.
3. **The tracker mirrors hq.** Rejected: hq would still hand out numbers, so the collisions stay, and a second copy of
every status can drift from the first.
### Whose code (open question 2)
1. **Run hits as it is.** Rejected: its services create their own stream and bucket, which ADR 0201 §3 forbids a module
to do; its subjects assume one tracker per bus account; its actor is named by the caller rather than proven by the
bus credential; its words clash with the glossary.
2. **Vendor or fork hits into the catalogue.** Rejected: its licence is not an open-source licence, and vendoring binds
the catalogue to it for as long as the code is there; changing the points above means owning a fork of a young,
single-author project.
3. **Build our own module in Go on hits's model.** Adopted. Its statuses, fields, append-only notes, typed links,
compare-and-set numbering and its auditor are copied as ideas, and the module's README says so.
### Which numbers (open question 3)
1. **Issue numbers only.** Rejected: decision numbers collided three times in one evening, and the same counter serves
any sequence.
2. **Issue, decision and research numbers, all from the tracker.** Adopted, with decision and research numbers in
slice 2.
### The word (open question 4)
1. **Adopt the operator's word *ticket*.** Rejected: *issue* is already the glossary's word for this thing, and one word
names one thing ([ADR 0244](0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)). *Ticket*
also already names something else: an answer's one-time token
([ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)).
2. **Adopt hits's word *item*.** Rejected for the same reason.
3. **Keep *issue*; call the module that holds them the *issue tracker*, named `issues`.** Adopted.
### Conditions (open question 5)
1. **The controller opens an issue by itself for every `healer-wanted` condition, from the start.** Rejected for now:
an automatic opener with no week of linked data behind it can flood the tracker with issues nobody reads.
2. **The controller only links issues to conditions until slice 4 has run for a week; then it opens one issue per
`healer-wanted` condition key by itself.** Adopted.
## Decision
1. **The issue tracker is a Go module, `issues`, with one instance on the control-node.** It declares and claims one
seat held once for the mesh, `issue-tracker`
([ADR 0126](0126-a-module-declares-its-own-seats.md)). It is built on hits's model and carries none of its code.
2. **The tracker is the truth for every issue's number, status and links.** Every issue is opened in the tracker and
takes its number from it, including one that needs no document here. A design or governance issue also gets a
folder `04-ISSUES/NNN-slug/` under the tracker's number, whose narrative is reviewed in pull requests as today.
Status lives in one place.
3. **A number comes from one counter, advanced by compare-and-set, and only after the issue is valid.** A refused
open spends no number, and two callers opening at the same moment never get the same one. The runtime's state
verbs gain a write that is refused when the key changed since it was read; the counter does not rely on there
being one instance. The counter starts above the highest number on `main` and on every open branch at the moment of
the switch. A number never moves again, and numbers already skipped stay unused.
4. **State is in buckets the module declares** (ADR 0201): the controller creates them, the module never does.
**Every change is an event the module declares** ([ADR 0041](0041-events-are-a-relationship.md)), carrying what
changed and who changed it, so the events are the audit trail.
5. **The caller is the bus credential, never an argument.** The module records who acted from the identity the
runtime gives it with each call.
6. **The cycle's issue rules are enforced at the write.** A status outside the five is refused; `located` and
`resolved` need `located-in`; `resolved` needs `fixed-by`; a core issue resolves with `replay` or `replay-none`
([ADR 0237](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md));
`wontfix` needs a reason. `located-in` names an entry of a registered list of repositories and modules.
7. **The word is *issue*.** The module is the *issue tracker*, named `issues`; *ticketing system* is retired in the
glossary, and the bare word *ticket* is a homonym that never means an issue.
8. **It is built in slices, each ending at something the operator sees.** Slice 1 is the module with `open`, `get`,
`list`, `move`, `note` and `link`, and playbook 03 takes an issue's number from it. Slice 2 hands out decision and research numbers too. Slice 3 imports the issue
folders and consolidates them, after which one pull request removes the status keys from every report and changes
playbook 03 and `cycle.py` together. Slice 4 links conditions and pull requests. Slice 5 is a read-only view onto
the tracker's bucket. The design is [to-be 53](../03-DESIGN/01-to-be/53-the-issue-tracker.md); once slice 1 runs,
each slice after it is an issue in the tracker itself.
9. **Conditions are linked, not opened, until slice 4 has run for a week.** A condition is never closed by closing an
issue: a condition is cleared only by observation
([ADR 0231](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)).
## Consequences
- Collisions end at the source: a number is taken from one counter, not read from a repository many branches write.
- Until the slice 3 import has run and been checked against it, an issue that has a folder here keeps its status in
that folder's frontmatter, and the tracker holds its number, title and folder only. An issue without a folder is
whole in the tracker from the start. No status is written in two places for the same issue at any time.
- [ADR 0019](0019-how-this-repository-works.md)'s rule that statuses live only in frontmatter stops holding for issues
on the day slice 3's pull request lands, and part of [ADR 0080](0080-the-development-cycle-is-checked.md)'s check
moves from `cycle.py` into the tracker's `move`. That pull request adds a dated note to each of them saying where
the mechanism now lives; until then both stand unchanged.
- An agent opening an issue needs the mesh to be reachable. A node that cannot reach the control-node cannot take a
number, and says so, rather than guessing one.
- The SDK's state interface gains a compare-and-set write, which every module keeping a counter can use; it is a
contract change under [ADR 0039](0039-what-the-sdk-holds-and-refuses.md), and every module should be rebuilt
against it.
- What got harder: one more module the mesh's own process depends on. hq's checks today run from a checkout alone;
from slice 2 on, writing a decision or an issue here needs the tracker.
## References
- [Research 042](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md) and its
[findings](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md).
- [To-be 53 — The issue tracker](../03-DESIGN/01-to-be/53-the-issue-tracker.md).
- hits, `github.com/impire-io/hits`, release v0.7.0: the model studied, not the code used.
- [Issue 155](../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md): the collision that
reached `main`.