Artifact d96474c3be564164781b0b010b21cbde3da0ba5a16d26c27aef76b299434f9dc:
- File
internal/evt/evt.go
— part of check-in
[f1a6be9a77]
at
2026-08-24 19:59:53
on branch praise
— Praise: vocabulary, storage, ops
Any signed-in account may praise any repository it can SEE — no capability letter, because the read gate has already decided whether the repository exists for this caller and past that gate praising is not a privilege anyone needs granting.
Per account, not per contributor: someone who has never authored an artifact can still say a repository is worth attention, which is why this cannot hang off the contributor pages.
praise.list filters by the VIEWER, never the owner. Praise is public; the existence of a private repository is not, and this is where the two meet.
UI deferred: the button belongs in the repo stats strip, which is on the unlanded clone-counts branch. (user: dmestas, size: 7559) [annotate] [blame] [check-ins using] [more...]
// Package evt is quarry's event vocabulary (RFC 0005): the names a // pipeline, a webhook subscription or a notification rule may ask for. // // It is a leaf on purpose. The vocabulary is a PUBLISHED INTERFACE — a // pipeline definition names an event in its `on:` clause, and that definition // is a versioned artifact in someone else's store — so the names must be // nameable from anywhere without dragging a transport in. This package // imports nothing but the standard library, and nothing here knows that // events travel over NATS. package evt import "strings" // Subject is where this event publishes: quarry.evt.<repo>.<noun>.<action>. // // The subject IS the subscription interface (RFC 0005 §2.3.1). dagnats // triggers take a NATS subject filter, so a wildcard does the selecting and // quarry adds no syntax of its own: `zit.evt.*.landing.opened` is every // landing opened anywhere, `zit.evt.quarry.>` is one repository entire. // // Which is the second reason noun-and-action was right, and one the RFC's // motivation did not know: the vocabulary is a subject HIERARCHY. Gitea's // issue_assign naming would flatten it into a string a wildcard cannot take // apart. It also makes a rule of what was an accident — a noun and an action // must be single tokens, because a dot is the subject separator. // // The root is `quarry.`, not `zit.`, for two reasons. The blunt one: the // ingest pipeline's fold consumer already subscribes to `zit.evt.<repo>.>`, // so an event published under that root would be handed to the folder and // parsed as an artifact. The better one: `zit.` is zit's DAG transport, and // half this vocabulary — ci, mirror, account, clone — names things zit has // never heard of. These are the FORGE's events. func (k Kind) Subject(repo string) string { return "quarry.evt." + repo + "." + k.Noun() + "." + k.Action() } // Kind is one event: a noun and an action, `ticket.closed`. // // Noun-and-action rather than one name per pair (RFC 0005 §Motivation). // Gitea mints a permanent name for every verb — issue_assign, issue_label, // issue_milestone — so a subscriber wanting "any issue activity" must list // them all and remember the next one. Here a subscription can name the noun // and get every action on it, and adding an action later costs no name. type Kind string // The vocabulary. An action marked "reserved" below is not raised by any code // path yet and exists so that raising it later is not a vocabulary change — // which for a published interface is the expensive kind. const ( CheckinCreated Kind = "checkin.created" BranchCreated Kind = "branch.created" BranchMoved Kind = "branch.moved" BranchClosed Kind = "branch.closed" // reserved TagCreated Kind = "tag.created" TagRemoved Kind = "tag.removed" // reserved TicketOpened Kind = "ticket.opened" TicketChanged Kind = "ticket.changed" TicketClosed Kind = "ticket.closed" TicketReopened Kind = "ticket.reopened" WikiCreated Kind = "wiki.created" WikiEdited Kind = "wiki.edited" WikiDeleted Kind = "wiki.deleted" TechnoteCreated Kind = "technote.created" TechnoteEdited Kind = "technote.edited" ForumPosted Kind = "forum.posted" ForumReplied Kind = "forum.replied" ForumEdited Kind = "forum.edited" // reserved ForumModerated Kind = "forum.moderated" LandingOpened Kind = "landing.opened" LandingCommented Kind = "landing.commented" LandingAccepted Kind = "landing.accepted" LandingRejected Kind = "landing.rejected" CIStarted Kind = "ci.started" CICompleted Kind = "ci.completed" MirrorPulled Kind = "mirror.pulled" MirrorFailed Kind = "mirror.failed" RepoCreated Kind = "repo.created" RepoRemoved Kind = "repo.removed" RepoSettings Kind = "repo.settings" AccountAdded Kind = "account.added" AccountCaps Kind = "account.caps" AccountRemoved Kind = "account.removed" PraiseGiven Kind = "praise.given" PraiseWithdrawn Kind = "praise.withdrawn" CloneCloned Kind = "clone.cloned" CloneFetched Kind = "clone.fetched" ) // Derived reports whether this event is a reading of artifacts the store // holds, and so can be re-derived by walking the index after a missed // delivery (RFC 0005 §2.1). // // The distinction is load-bearing rather than tidy. The bus is a ONE-HOUR // window: a consumer outside it does not replay, it rebuilds. A derived event // missed costs latency; an operational one missed is a fact that is simply // gone — which is the difference between a mirror.failed an operator can page // on and one that silently did not happen. Operational events are therefore // recorded in OLTP as they are raised, and the bus carries notice of a row // that already exists. func (k Kind) Derived() bool { switch k.Noun() { case "checkin", "branch", "tag", "ticket", "wiki", "technote", "forum", "landing": return true } return false } // Noun is the thing the event is about. func (k Kind) Noun() string { noun, _, _ := strings.Cut(string(k), ".") return noun } // Action is what happened to it, empty when k names only a noun. func (k Kind) Action() string { _, action, _ := strings.Cut(string(k), ".") return action } // Valid reports whether k is in the vocabulary. An unknown name is refused at // the moment a subscription is written rather than silently never firing, // because a pipeline that never runs and a pipeline that runs and fails look // nothing alike to the person waiting for it. func (k Kind) Valid() bool { for _, known := range All { if k == known { return true } } return false } // Matches reports whether a subscription pattern selects this event. // // A pattern is either `*` for the whole vocabulary, a full kind // (`ticket.closed`), or a bare noun (`ticket`), which selects every // action on it — including actions added after the subscription was // written. That is the whole reason for the noun-and-action shape: a // subscriber says what it cares about at the level it cares, and a // vocabulary that grows does not silently narrow what it already asked // for. func (k Kind) Matches(pattern string) bool { if pattern == "" { return false } if pattern == "*" || pattern == string(k) { return true } return !strings.Contains(pattern, ".") && pattern == k.Noun() } // All is the vocabulary, in the order RFC 0005 §1 lists it. Iterated by // Valid, and by the API doc so the set is discoverable rather than folklore. // MatchesAny reports whether any pattern in the list selects this event. // An empty list selects nothing — a subscription that asked for no // events gets none, rather than silently getting all of them. func (k Kind) MatchesAny(patterns []string) bool { for _, p := range patterns { if k.Matches(p) { return true } } return false } var All = []Kind{ CheckinCreated, BranchCreated, BranchMoved, BranchClosed, TagCreated, TagRemoved, TicketOpened, TicketChanged, TicketClosed, TicketReopened, WikiCreated, WikiEdited, WikiDeleted, TechnoteCreated, TechnoteEdited, ForumPosted, ForumReplied, ForumEdited, ForumModerated, LandingOpened, LandingCommented, LandingAccepted, LandingRejected, CIStarted, CICompleted, MirrorPulled, MirrorFailed, RepoCreated, RepoRemoved, RepoSettings, AccountAdded, AccountCaps, AccountRemoved, PraiseGiven, PraiseWithdrawn, CloneCloned, CloneFetched, } // Nouns lists each noun once, in All's order. func Nouns() []string { var out []string seen := map[string]bool{} for _, k := range All { if n := k.Noun(); !seen[n] { seen[n] = true out = append(out, n) } } return out }