Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 2: an assignee is a person you can click

67dd8854450865e9c5d2a8643b99c9ef7f71c284 · Keenan Nemetz · 17d ago · parent 307cbc2a

Phase 2: an assignee is a person you can click

`assignees:` was in the frontmatter, written by the tool and read by nothing:
not validated, not set by any command, not in the index, not on a page. It now
goes the whole way — the field is checked at the push boundary, the index holds
it in an `issue_assignees` table mirroring `issue_labels`, and the web renders it
as a filter link on both the list and the detail page.

The field is held to the *roster's* account-name rule rather than the label
rule. It names a person, so the label rule (which allows dots) would accept a
value no account could ever have; a capital or a `@` prefix is a typo that would
render as a link to nobody. It is deliberately NOT resolved against the roster at
push time: a clone may be offline, an assignment may name someone who has not
been created yet, and a name in an issue file is a claim on the same footing as
`author` — the hook's job is the format, not the org chart. Stored as written
rather than as an account id, for the same reason `author` is: the file stays
readable in a plain clone, and because names are never reissued an assignment
keeps meaning the same person after a rename.

The index needed a schema version to go with the new table, because
`EnsureIssues` only re-reads when the ref moves: an existing instance would have
served rows derived from the old shape until someone happened to push. A bump
now empties the derived issue rows and leaves every repo's `issues_sha` empty, so
the next read rebuilds from git. The index is disposable; a stale row is worse
than a rebuild.

`vole issue new -a`, `assign`, `unassign`, `list --assignee` and `%(assignees)`
are the CLI half. An assignment that changes nothing is not a commit — `Update`
reports `ErrNoChange` and the branch does not move — so a script that reruns
stays idempotent without growing a linear log.

Verified: TestAssigneeRuleMatchesAccountNames compares this package's regex with
identity.ValidateName over the edge cases (a name the roster accepts and the
format rejects would only ever show up as a rejected push);
TestAssigneesAreAccountNames drives valid, capitalised, dotted, `@`-prefixed,
too-short, too-long and space-bearing values through the reader;
TestAssigneeRoundTrip proves the writer emits a sorted one-per-line list that the
reader reads back; TestSchemaVersionDropsRowsFromAnOlderShape writes rows an
older build would have left, reopens, and checks they are gone, `issues_sha` is
empty and a second open does not re-wipe; and
TestIssueAssigneesRenderedFilteredAndValidated plus the CLI end-to-end test push
an assignment over SSH, click it, filter by it and get a push rejected for
`Wilbur` with the path and value named and the branch unmoved. scripts/ci.sh
green (gofmt, vet, build, markdown links, go test -race ./...).

docs/roadmap.md +20−4

@@ -170,17 +170,30 @@ Shipped:
170 170 - **Filtering and search**: state, label, and a LIKE search over title and body,
171 171 backed by an `issue_labels` table and an indexed `body` column.
172 172
173 Three follow-ups, none of them blocking:
173 Follow-ups:
174 174
175 175 - **Notifications are deliberately not built.** There is no user identity on a
176 176 read-only web, so "in-app" has nobody to notify, and email needs subscriptions
177 177 that would themselves need a store. The natural fit for this architecture is a
178 178 per-repo Atom feed of issue and commit activity, which needs no auth — worth
179 179 doing when someone asks, instead of building a notification system nobody can
180 log into.
181 - Assignees are in the frontmatter but not rendered or filterable
180 log into. The shape it should take when it is wanted: one feed per repo over
181 tags *and* issues/comments, because the audience with no alternative is the
182 person consuming artifacts, not the person who already has a clone.
183 - ✅ **Assignees are first-class.** `assignees:` in the frontmatter was parsed and
184 written but went nowhere; it is now validated, filterable and clickable. An
185 assignee is held to the roster's account-name rule rather than the label rule —
186 it names a person, and a value that could never be an account is a typo, not a
187 new kind of label — but it is **not** roster-checked at push time: the hook has
188 no business failing a push because an assignment names somebody who has not been
189 created yet, and a name in frontmatter is a claim in the same way `author` is.
190 It is stored as written rather than resolved to an account id, so the file stays
191 readable in a plain clone and an assignment keeps meaning the same person after
192 a rename, since names are never reissued. `vole issue new -a`, `assign`,
193 `unassign`, `list --assignee`, `%(assignees)`, the index's `issue_assignees`
194 table and the web's filter links all read that one field.
182 195 - Comment editing and deletion (today: append another comment, or rewrite history
183 and force-push)
196 and force-push).
184 197
185 198 Exit criteria: a user with an editor and a key files, comments on and closes an
186 199 issue without installing Vole; an invalid tree is rejected with the offending path
@@ -744,6 +757,9 @@ Filled in as phases land. Append; don't rewrite.
744 757 | 2026-10-23 | The roster is `data/users.db`; clients read it with the `vole-roster` SSH verb | Accounts are rows, not commits: SQLite enforces "a name is issued once" and "a key belongs to one account" as table keys, and a backup is one file. The client still needs an id/name map, so it asks over the key it already pushes with instead of cloning a repository that held nothing else. Supersedes both roster rows above |
745 758 | 2026-10-23 | Metadata on `refs/heads/vole` is frontmatter markdown, not JSON | [Spike](spikes/issue-frontmatter/README.md): the same six edits cost 25 changed lines as `meta.json` and 10 as frontmatter. A JSON array rewrites its neighbour to add a comma; one label per line is one added line, and one file per entity means one path to diff, blame and log. Always quoting free text turns the YAML edge cases into a writer concern instead of a title-restriction list, and the validator only has to refuse an unquoted title the parser read differently. The `updated` field was dropped: it was 11 of the 21 frontmatter lines, and the commit already carries the time |
746 759 | 2026-10-22 | A published release is immutable, admin-overridable and audited | A link whose bytes can change is worse than no link; but refusing outright leaves a mistyped tag unrepairable, so the override follows the force-push precedent: admin only, always audited |
760 | 2026-10-23 | An assignee is a name held to the account-name rule, not roster-checked at push | It names a person, so the label rule (which allows dots) would accept a value no account can have — but resolving it against the roster at the gate would fail a push written offline, or for someone added after the assignment. A name in frontmatter is a claim on the same footing as `author`; the web shows it and the filter links by it. Stored as written, so a rename never rewrites history (names are never reissued) and the file stays readable in a clone |
761 | 2026-10-23 | The index carries a schema version; a bump empties the derived rows | A new table and column would leave existing instances serving rows derived from the old shape until the next push touched each repo. The index is disposable, so the honest migration is "delete and let the next read rebuild from git" — a stale row is worse than a rebuild, and the version stops a half-applied schema from being read as if it were complete |
762 | 2026-10-23 | An edit that changes nothing is not a commit | `vole issue assign` on an already-assigned person would otherwise add an empty commit to a linear branch, and a script that reruns has to be idempotent without growing the log. `Update` reports `ErrNoChange` and the branch does not move |
747 763
748 764 ## Open questions
749 765