Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/design.go
T
jschoubben 76e4ec4dc6
mesh/delivery merged: source.main moved to novox/mesh-catalog@5a4ac4991395
mesh/delivery-group group feat/1781-milestone-records merged: every member is merged
mesh/check checking 76e4ec4dc680 on 26908a89506f | novox/mesh-catalog#506 in group feat/1781-milestone-records
mesh-issues and mesh-knowledge: milestones are records of the issue tracker, and progress reads them (ADR 0493 §1 to §4, issue 1781)
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.
2026-10-26 18:42:51 +01:00

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
}