mesh/merge-gate fail: builds mesh-issues → novox; no bus step; a manifest the change touches fails the module check: modules/mesh-issues/module.json: this …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
Every write appends one operation at the issue's last sequence before the bucket is written; a refused append writes nothing. The start carries what the bucket holds, refolds what is behind and indexes every issue in memory, so list answers from memory with a ranked search and a before cursor.
225 lines
9.5 KiB
Go
225 lines
9.5 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"`
|
|
// Folded is the sequence in the log `changes` of the last operation this issue folds (novox/hq ADR 0300
|
|
// §3); 0 for an issue kept before the switch and not carried into the log yet.
|
|
Folded uint64 `json:"folded,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
|
|
}
|