What
Issues and change requests get a shared, gap-free sequence — [#1](/vole/n/1), [#42](/vole/n/42) — that
people can read, remember and type. The ULID stays the identity.
The number is derived, not stored: it is the 1-based rank of the entity’s
creation commit in the linear vole branch history, counted over
issues/*/issue.md and requests/*/request.md together. Nothing is added to a
file, no counter or ledger exists, and a repository needs no backfill: this one
numbers 1–44 from history alone, and the issue this change closes is [#45](/vole/n/45).
Consequences of deriving it:
- Nothing is written when an entity is created, so there is no second serialized commit on the one linear branch and no window where an entity exists without a number.
- The same history gives the same numbers on every clone and every instance, so a number is not instance-local.
- A deleted entity keeps its number, because its creation commit stays in history. A path deleted and re-added keeps its first number.
- Editing, labelling or closing an entity never moves its number.
vole admin index rebuildreproduces the mapping from git.
The trade-off, documented rather than hidden: the number comes from history, so an
admin redaction force-push that rewrites vole renumbers everything after the
rewritten commit. That is why the docs keep calling the ULID the durable reference
and [#42](/vole/n/42) a convenience, and why closes: still stores ULIDs.
Where it lives
One implementation, two callers, so the CLI and the web cannot disagree:
metadata/number.go—LoadNumbers(onegit log --reverse --diff-filter=Awalk, via the newgit.AddedPathsPerCommit) andRankAdditions, the pure rule: commit order across the history, ULID order within one commit, first number wins for a re-added path.forge/index— anentity_numberstable, refreshed in the same transaction as the issue rows and dropped by the same schema-version mechanism, so a page render never walks history.Issues()joins it so the list shows[#42](/vole/n/42)without a second query.client.Branch.Numbers,metadata.FindRequest— the CLI path, which walks the local branch and therefore works offline and with--no-push.
Surfaces:
vole issue list --format '%(number) ...', and numbers in the default list,show,issue newandchange newconfirmations,change list(text andjson),change show.- Every id-taking command accepts
42or[#42](/vole/n/42); asking an issue command for a change request’s number says so instead of guessing. --closes 42on the CLI and[#42](/vole/n/42)in the browser’s Closes field resolve to the ULID before the link is written, sorequest.mdstill stores ids.[#42](/vole/n/42)in a body links through the kind-agnosticGET /r/{name}/n/{num}, which redirects to whichever of the two kinds holds the number.#<ulid>still links by id.- Issue and request pages and lists show
[#42](/vole/n/42)where they used to show the 8-character ULID slice.%(shortid)remains for compatibility.
Review notes
- The numbering rule is the thing to check:
metadata/number_test.gocovers the shared sequence, ULID ordering inside a bulk commit, comment/approval/run paths being ignored, re-added paths keeping their number, and an edit or a deletion not moving it.TestLoadNumbersFollowsHistorydoes the same against a real repository. - The integration test now proves the loop end to end:
issue listshows[#1](/vole/n/1),issue comment ... '[#1](/vole/n/1)'resolves it, the issue page renders[#1](/vole/n/1), and/r/<repo>/n/1lands on that issue. schemaVersiongoes to 3 so existing instances rebuild their derived rows once instead of serving an empty numbering.
Checks
vole workflow check— ok, 2 workflows.vole workflow run verify— ok, 1 job (formatting, vet, build, packaging, link checks, race tests).
Closes #45