mesh-issues keeps each design's milestone in the state milestones (history 5), says milestone-set and milestone-moved, and serves milestones, milestone-set and milestone-move. An issue's milestone is its own link of kind milestone, so at most one milestone per issue is checked inside the issue's own write; kind commit links a fixing commit. The triage links no milestone, pull request or commit. import with milestones=N seeds a design's table once. mesh-knowledge's progress reads issue-tracker.milestones and its table parse is gone. Squashed from four commits so the branch history does not carry a compiled bundle that was committed by mistake (6 MB); the path is now in .gitignore.
167 lines
4.7 KiB
Go
167 lines
4.7 KiB
Go
package main
|
|
|
|
// A design as the progress verb reads it (novox/hq issue 1947): its title, its state as its frontmatter says it and
|
|
// the decisions it names, read through the records module until ADR 0423 makes designs records of the knowledge base
|
|
// (issue 428). Its milestones are the issue tracker's records (ADR 0493 §1, issue 1781), read in milestones.go: the
|
|
// table parse that guessed them from prose moved to the tracker's seed and is gone from here.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// DesignsFolder is where the designs being built are, in hq.
|
|
const DesignsFolder = "03-DESIGN/01-to-be"
|
|
|
|
// Design is one design as read.
|
|
type Design struct {
|
|
Number int
|
|
Path string
|
|
Title string
|
|
Status string
|
|
Decisions []string
|
|
Milestones []Milestone
|
|
}
|
|
|
|
// Milestone is one milestone of a design, as the issue tracker keeps it.
|
|
type Milestone struct {
|
|
ID string
|
|
Outcome string
|
|
Issues []int
|
|
// Status is open or closed, with its Reason; a closed milestone holding no issue is done.
|
|
Status string
|
|
Reason string
|
|
}
|
|
|
|
var (
|
|
designFile = regexp.MustCompile(`^([0-9]{2,3})-[a-z0-9-]+\.md$`)
|
|
titleLine = regexp.MustCompile(`^#\s+[0-9]+\s+[—-]\s+(.+)$`)
|
|
decisionItem = regexp.MustCompile(`ADR ([0-9]{4})`)
|
|
)
|
|
|
|
// parseDesign reads a design document.
|
|
func parseDesign(path, content string) *Design {
|
|
d := &Design{Path: path}
|
|
if m := designFile.FindStringSubmatch(lastPart(path)); m != nil {
|
|
d.Number, _ = strconv.Atoi(m[1])
|
|
}
|
|
lines := strings.Split(content, "\n")
|
|
body := 0
|
|
if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" {
|
|
for i := 1; i < len(lines); i++ {
|
|
l := lines[i]
|
|
if strings.TrimSpace(l) == "---" {
|
|
body = i + 1
|
|
break
|
|
}
|
|
if v, ok := strings.CutPrefix(l, "status:"); ok {
|
|
d.Status = strings.TrimSpace(v)
|
|
}
|
|
for _, m := range decisionItem.FindAllStringSubmatch(l, -1) {
|
|
d.Decisions = append(d.Decisions, "ADR "+m[1])
|
|
}
|
|
}
|
|
}
|
|
for i := body; i < len(lines) && d.Title == ""; i++ {
|
|
if m := titleLine.FindStringSubmatch(strings.TrimSpace(lines[i])); m != nil {
|
|
d.Title = strings.TrimSpace(m[1])
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
func lastPart(p string) string {
|
|
if i := strings.LastIndex(p, "/"); i >= 0 {
|
|
return p[i+1:]
|
|
}
|
|
return p
|
|
}
|
|
|
|
// milestone finds a milestone by its id, without regard to case, or to spaces against hyphens.
|
|
func (d *Design) milestone(id string) *Milestone {
|
|
// The tracker keeps an id's words joined by hyphens (3 fix is 3-fix); either is taken.
|
|
want := strings.ToLower(strings.Join(strings.Fields(id), "-"))
|
|
for i := range d.Milestones {
|
|
if strings.ToLower(strings.Join(strings.Fields(d.Milestones[i].ID), "-")) == want {
|
|
return &d.Milestones[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// issues is every issue its milestones name, each once, in order.
|
|
func (d *Design) issues() []int {
|
|
var out []int
|
|
have := map[int]bool{}
|
|
for _, m := range d.Milestones {
|
|
for _, n := range m.Issues {
|
|
if !have[n] {
|
|
have[n] = true
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// designsCache is the designs read at one commit of hq: read again only when the records module's checkout moves.
|
|
type designsCache struct {
|
|
commit string
|
|
paths []string
|
|
byPath map[string]*Design
|
|
}
|
|
|
|
// designs answers the designs being built, read at the commit the records module stands at, from the cache while
|
|
// that commit stands (novox/hq issue 1947, 1947/AC6).
|
|
func (k *KB) designs(all bool, number int) ([]*Design, string, error) {
|
|
if k.Records == nil {
|
|
return nil, "", fmt.Errorf("the knowledge base reads no records here")
|
|
}
|
|
commit, err := k.Records.Standing()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the records module does not say where hq stands: %v", err)
|
|
}
|
|
k.progMu.Lock()
|
|
defer k.progMu.Unlock()
|
|
c := k.progCache
|
|
if c == nil || c.commit != commit {
|
|
c = &designsCache{commit: commit, byPath: map[string]*Design{}}
|
|
_, docs, err := k.Records.List(DesignsFolder)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the records module does not list %s: %v", DesignsFolder, err)
|
|
}
|
|
for _, doc := range docs {
|
|
if designFile.MatchString(doc) {
|
|
c.paths = append(c.paths, DesignsFolder+"/"+doc)
|
|
}
|
|
}
|
|
sort.Strings(c.paths)
|
|
k.progCache = c
|
|
}
|
|
var out []*Design
|
|
for _, p := range c.paths {
|
|
n, _ := strconv.Atoi(designFile.FindStringSubmatch(lastPart(p))[1])
|
|
if !all && n != number {
|
|
continue
|
|
}
|
|
d := c.byPath[p]
|
|
if d == nil {
|
|
content, _, err := k.Records.Read(p)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the records module does not read %s: %v", p, err)
|
|
}
|
|
d = parseDesign(p, content)
|
|
c.byPath[p] = d
|
|
k.progReads++
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
if !all && len(out) == 0 {
|
|
return nil, "", fmt.Errorf("no design %d is in %s at hq %s", number, DesignsFolder, short(commit))
|
|
}
|
|
return out, commit, nil
|
|
}
|