Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/issue.go
T
jschoubben 3aaccba4e2
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh-issues: read merged from the pull request, not the state, so a closed one is withdrawn (hq issue 483)
mesh-delivery says stopped for a pull request closed unmerged too; read as
merged, it was said stuck to the operator for good. Cite ADR 0284 for the
worker rules, which the comments gave as 0283.
2026-10-11 12:24:45 +02:00

222 lines
9.3 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// What the tracker keeps (novox/hq to-be 53 §3, §4): one issue per key of the state `issues`, keyed by
// its number, and one counter per sequence in the state `counters`. Both are declared in the manifest and
// made by the controller; this module never makes a bucket (ADR 0201).
const (
IssuesState = "issues"
CountersState = "counters"
// IssueCounter is the key of the issue numbers' counter in `counters`.
IssueCounter = "issue"
)
// The six statuses of an issue, as the development cycle names them (hq playbook 03, ADR 0275, ADR 0284).
// Fixing is the status an agent or person moves an issue to when they start its fix; resolved is reached
// only from it (ADR 0284 §5).
const (
StatusOpen = "open"
StatusDiagnosing = "diagnosing"
StatusLocated = "located"
StatusFixing = "fixing"
StatusResolved = "resolved"
StatusWontfix = "wontfix"
)
// Statuses is every status an issue may have, in the order the cycle walks them.
var Statuses = []string{StatusOpen, StatusDiagnosing, StatusLocated, StatusFixing, StatusResolved, StatusWontfix}
// Severities say how much an issue costs (ADR 0279): critical — something the operator relies on is broken, or
// data is at risk; major — a rule of the mesh does not hold, or people work around it; minor — a contained fix,
// and nothing the operator sees changes.
var Severities = []string{"critical", "major", "minor"}
// NeedsKinds say what an issue needs before an agent can fix it (ADR 0279): none; decision — a decision record
// first; operator — the operator's answer or feedback.
var NeedsKinds = []string{"none", "decision", "operator"}
func oneOf(field, v string, allowed []string) (string, error) {
v = strings.TrimSpace(v)
for _, a := range allowed {
if v == a {
return v, nil
}
}
return "", fmt.Errorf("%s %q is not one of %s", field, v, strings.Join(allowed, ", "))
}
// Unproven is who acted, as recorded, where the runtime proves no caller: a call on the subject that names none,
// a runtime from before hq issue 365, or a public method called at the terminal. Otherwise who acted is the bus
// user the runtime took from the subject the call arrived on (stdio.CallerOf). The caller is never taken from an
// argument, so neither is a name anybody could type (novox/hq ADR 0275 §5). Mesh-sdk says the same word
// (stdio.Unproven).
const Unproven = stdio.Unproven
// Issue is one issue as the tracker keeps it.
type Issue struct {
Number uint64 `json:"number"`
Title string `json:"title"`
// Status is one of the six. It was empty for an issue opened with a folder in hq before the import of
// slice 3, whose folder kept its status; the import fills it (to-be 53 §8).
Status string `json:"status,omitempty"`
Report string `json:"report"`
Opened time.Time `json:"opened"`
OpenedBy string `json:"opened-by"`
Closed *time.Time `json:"closed,omitempty"`
// LocatedIn names registered owners, each optionally followed by where in it.
LocatedIn []string `json:"located-in,omitempty"`
// LocatedInUnverified is the entries of LocatedIn that named no registered owner when the import took
// them from hq as written; a move that gives located-in anew clears it, as a move checks every entry.
LocatedInUnverified []string `json:"located-in-unverified,omitempty"`
FixedBy []string `json:"fixed-by,omitempty"`
AmendedDesign []string `json:"amended-design,omitempty"`
Replay string `json:"replay,omitempty"`
ReplayNone string `json:"replay-none,omitempty"`
// Reason is why it will not be fixed: what a wontfix needs.
Reason string `json:"reason,omitempty"`
// Record is the issue's folder in hq, when it has one.
Record string `json:"record,omitempty"`
Notes []Note `json:"notes,omitempty"`
Links []Link `json:"links,omitempty"`
// Severity, Needs and SuggestedOwner are given at open, set by a person, or estimated by the triage where
// nobody gave them (ADR 0279); empty until then.
Severity string `json:"severity,omitempty"`
Needs string `json:"needs,omitempty"`
SuggestedOwner string `json:"suggested-owner,omitempty"`
// Delivered is the pull requests of FixedBy the delivery said are delivered.
Delivered []string `json:"delivered,omitempty"`
// Fixes is each pull request of FixedBy as the delivery last said it (ADR 0281): measured, never set by a
// verb.
Fixes []Fix `json:"fixes,omitempty"`
// VerifyFor is the pull requests it was last said ready to verify for, so it is said once.
VerifyFor []string `json:"verify-for,omitempty"`
// StuckOpen is the warning on the operator's channel said for a stuck fix and not yet cleared there.
StuckOpen bool `json:"stuck-open,omitempty"`
// NotResolvedOpen is the warning on the operator's channel that it is fixed and delivered and not resolved,
// said and not yet cleared there (novox/hq issue 483).
NotResolvedOpen bool `json:"not-resolved-open,omitempty"`
// ReadyFor is the issues it depended on when it was last said ready (ADR 0279 §3), so one spell of
// readiness is said once however many dependencies let it go at one delivery.
ReadyFor []uint64 `json:"ready-for,omitempty"`
// Triage is the triage's judgement as it was made, kept before it is applied.
Triage *Verdict `json:"triage,omitempty"`
// Worker is who works on it, while it is diagnosing or fixing (ADR 0284); nil at every other status.
Worker *Worker `json:"worker,omitempty"`
// Opening is the id of the call that opened it, so a write whose answer was lost is known as its own.
Opening string `json:"opening"`
// Imported says that the issue, or its status, was taken from its folder in hq by the import of
// slice 3: when, by whom, at which commit of hq, from which document. An issue the import wrote has
// OpenedBy ImportedBy, and its Opened is the date its report gave.
Imported *Import `json:"imported,omitempty"`
}
// ImportedBy is who opened an issue the import wrote: not a caller of `open`, and not a name anybody typed.
const ImportedBy = "import"
// Import is how an issue, or its status, came from hq.
type Import struct {
At time.Time `json:"at"`
// By is who ran the import, as the runtime proves it (Unproven where it proves none).
By string `json:"by"`
Commit string `json:"commit"`
// From is the document read, empty for a status filled without one.
From string `json:"from,omitempty"`
}
// Note is one note on an issue: appended, never edited.
type Note struct {
By string `json:"by"`
At time.Time `json:"at"`
Text string `json:"text"`
}
// Link is one typed link from an issue.
type Link struct {
Kind string `json:"kind"`
Target string `json:"target"`
By string `json:"by"`
At time.Time `json:"at"`
}
// MostIssueBytes is the most one issue may be as kept: a value the bus holds (to-be 53 §4). The largest
// issue folder in hq was 25 KiB on 2026-10-10.
const MostIssueBytes = 256 << 10
// Entry is one key's value and the revision it was read at.
type Entry struct {
Value json.RawMessage
Revision uint64
}
// Store is the module's state as the runtime offers it (novox/hq ADR 0201, ADR 0259): a write that must win
// once is a compare-and-set, refused with stdio.ErrChanged when another writer was first.
type Store interface {
Get(state, key string) (*Entry, error)
Create(state, key string, value any) (uint64, error)
Update(state, key string, value any, revision uint64) (uint64, error)
Keys(state string) ([]string, error)
Increment(state, key string, attempts int) (uint64, error)
}
// runtimeStore is Store through the runtime that launched this bundle.
type runtimeStore struct{}
func (runtimeStore) Get(state, key string) (*Entry, error) {
e, err := stdio.State(state).Get(key)
if err != nil || e == nil {
return nil, err
}
return &Entry{Value: e.Value, Revision: e.Revision}, nil
}
func (runtimeStore) Create(state, key string, value any) (uint64, error) {
return stdio.State(state).Create(key, value)
}
func (runtimeStore) Update(state, key string, value any, revision uint64) (uint64, error) {
return stdio.State(state).Update(key, value, revision)
}
func (runtimeStore) Keys(state string) ([]string, error) { return stdio.State(state).Keys() }
func (runtimeStore) Increment(state, key string, attempts int) (uint64, error) {
return stdio.State(state).Increment(key, attempts)
}
// keyOf is an issue's key: its number in decimal.
func keyOf(n uint64) string { return strconv.FormatUint(n, 10) }
// numberOfKey is the number a key of `issues` holds, or false for a key that is not one.
func numberOfKey(k string) (uint64, bool) {
n, err := strconv.ParseUint(k, 10, 64)
if err != nil || n == 0 || keyOf(n) != k {
return 0, false
}
return n, true
}
// ErrNoIssue is a number the tracker holds no issue under.
var ErrNoIssue = errors.New("no such issue")
// read is one issue and the revision it was read at.
func read(s Store, n uint64) (*Issue, uint64, error) {
e, err := s.Get(IssuesState, keyOf(n))
if err != nil {
return nil, 0, fmt.Errorf("issue %d could not be read: %v", n, err)
}
if e == nil {
return nil, 0, fmt.Errorf("there is no issue %d: %w", n, ErrNoIssue)
}
var is Issue
if err := json.Unmarshal(e.Value, &is); err != nil {
return nil, 0, fmt.Errorf("issue %d is kept in a form this module cannot read: %v", n, err)
}
return &is, e.Revision, nil
}