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"` // FixingFrom is when it was first moved to fixing: where its fix time starts (novox/hq ADR 0313 §1). A // take-over or a later move back to fixing keeps it. FixingFrom *time.Time `json:"fixing-from,omitempty"` // FixTime is its fix time, phase by phase, once the last of its fixes is delivered (ADR 0313 §4): measured, // never set by a verb. FixTime *FixTime `json:"fix-time,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: 252 KiB, so the operation holding it whole (an open, an // import, a carry) fits in one entry of the log, which takes at most 256 KiB (novox/hq ADR 0297, ADR 0300). The // largest issue folder in hq was 25 KiB on 2026-10-10. const MostIssueBytes = 252 << 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) // History is every value a key still keeps, oldest first, puts and deletes (novox/hq ADR 0307): what the // carry into the log reads (ADR 0300 §6). History(state, key string) ([]stdio.StateRevision, 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() } // History is every value the bucket still keeps of a key, oldest first (novox/hq ADR 0307). func (runtimeStore) History(state, key string) ([]stdio.StateRevision, error) { return stdio.State(state).History(key) } 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 }