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-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.
222 lines
9.3 KiB
Go
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
|
|
}
|