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 // Moved is who last moved it and when, as the tracker says it; nil before its first move. Moved *stamp } 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 }