package main // A form-only version (novox/hq to-be 55 §2.2, ADR 0592 §31): the records imported with file-named sections get plain titles // in one version per record, written after the move by the module's tool alone. It changes only the sections' names and the // targets of links to files, each becoming a reference of the one form. It is marked `form`, lists the names it changed, // and is accepted without a judgement because a check proves only form changed: each section's text with every link replaced // by its label is the same before and after, and each new link target names the record the old one named. `get` takes the // old section names as aliases, recorded in the version. A part's review carries over to the part renamed. import ( "context" "fmt" "net/url" "regexp" "sort" "strings" "unicode" ) // FormTool is the module's tool that lists, dry-runs and runs the form-only versions. const FormTool = "knowledge_form_only" var ( fileLink = regexp.MustCompile(`\[([^\]]*)\]\(([^)\s]+)\)`) issueTarget = regexp.MustCompile(`04-ISSUES/(\d{3,5})-`) ) // refTarget is the target a reference is written as in a link: the scheme ref: and the reference of the one form. func refTarget(ref string) string { return "ref:" + url.PathEscape(ref) } // targetKey is the record a link's target names, in the old form (a file) or the new (a ref:). func targetRef(target string) (string, bool) { if rest, ok := strings.CutPrefix(target, "ref:"); ok { s, err := url.PathUnescape(rest) return s, err == nil } switch { case researchFolder.MatchString(target): return fmt.Sprintf("research %03d", atoi(researchFolder.FindStringSubmatch(target)[1])), true case issueTarget.MatchString(target): return fmt.Sprintf("issue %d", atoi(issueTarget.FindStringSubmatch(target)[1])), true case designPath.MatchString(target): return "design " + designPath.FindString(target), true case adrFile.MatchString(target): return fmt.Sprintf("ADR %04d", atoi(adrFile.FindStringSubmatch(target)[1])), true } return "", false } // titleOf is a plain title for a file-named section: "00-overview.md" is "Overview", "01-open-questions.md" "Open questions". func titleOf(name string) string { t := name if i := strings.LastIndexByte(t, '/'); i >= 0 { t = t[i+1:] } t = fileExtension.ReplaceAllString(t, "") t = numberPrefix.ReplaceAllString(t, "") t = strings.NewReplacer("-", " ", "_", " ").Replace(strings.TrimSpace(t)) if t == "" { return name } r := []rune(t) r[0] = unicode.ToUpper(r[0]) return string(r) } // FormChange is what a form-only version changes. type FormChange struct { Renamed map[string]string `json:"renamed,omitempty"` Relinked int `json:"relinked"` } // formOf is the record with plain titles and links of the one form, and what it changed; none when it is already so. func formOf(r *Record) (*Record, *FormChange) { n := r.Clone() ch := &FormChange{Renamed: map[string]string{}} taken := map[string]bool{} for _, s := range n.Sections { if len(sectionNameProblems(s.Name)) == 0 { taken[s.Name] = true } } for i, s := range n.Sections { if len(sectionNameProblems(s.Name)) > 0 { t := titleOf(s.Name) for base, k := t, 2; taken[t]; k++ { t = fmt.Sprintf("%s %d", base, k) } taken[t] = true ch.Renamed[s.Name] = t n.Sections[i].Name = t } n.Sections[i].Text = fileLink.ReplaceAllStringFunc(n.Sections[i].Text, func(link string) string { m := fileLink.FindStringSubmatch(link) if strings.HasPrefix(m[2], "ref:") || !linkToFile(m[2]) { return link } ref, ok := targetRef(m[2]) if !ok { return link } ch.Relinked++ return "[" + m[1] + "](" + refTarget(ref) + ")" }) } if len(ch.Renamed) == 0 && ch.Relinked == 0 { return r, nil } if len(ch.Renamed) > 0 { if n.Aliases == nil { n.Aliases = map[string]string{} } for old, t := range ch.Renamed { n.Aliases[old] = t } } return n, ch } // checkForm proves that only form changed between two versions: the fields and clauses and links are the same, each // section's text with every link replaced by its label is the same, and each link's new target names what the old one did. func checkForm(old, nw *Record, ch *FormChange) error { if len(old.Sections) != len(nw.Sections) { return fmt.Errorf("the number of sections changed") } strip := func(r *Record) string { c := r.Clone() c.Sections, c.Aliases, c.Parts, c.FormOnly = nil, nil, nil, false return Render(c) } if strip(old) != strip(nw) { return fmt.Errorf("a field, a clause or a link changed") } for i := range old.Sections { label := func(s string) string { return fileLink.ReplaceAllString(s, "$1") } if label(old.Sections[i].Text) != label(nw.Sections[i].Text) { return fmt.Errorf("the text of %q changed beyond its links", old.Sections[i].Name) } o, n := fileLink.FindAllStringSubmatch(old.Sections[i].Text, -1), fileLink.FindAllStringSubmatch(nw.Sections[i].Text, -1) for j := range o { ro, ok1 := targetRef(o[j][2]) rn, ok2 := targetRef(n[j][2]) if ok1 && o[j][2] != n[j][2] && (!ok2 || ro != rn) { return fmt.Errorf("the link %q of %q names %s, and its new target %s", o[j][1], old.Sections[i].Name, ro, rn) } } } if len(ch.Renamed) == 0 && ch.Relinked == 0 { return fmt.Errorf("nothing changed") } return nil } // FormedRecord is one record a form-only version was written (or would be) for. type FormedRecord struct { Record string `json:"record"` Renamed map[string]string `json:"renamed,omitempty"` Relinked int `json:"relinked,omitempty"` Skipped string `json:"skipped,omitempty"` } // FormOnly lists (dry), or writes (commit), the form-only version of one record or of every record that needs one. A // version is written only when the check proves form alone changed; a record with a version waiting for its review is // skipped, saying so. It answers what it did, record by record. func (k *KB) FormOnly(caller string, only *Key, commit bool) ([]FormedRecord, error) { if _, err := k.ledger(); err != nil { return nil, err } if commit && !proven(caller) { return nil, fmt.Errorf("a form-only version needs a caller the runtime proved; this call is by %s", caller) } k.mu.Lock() defer k.mu.Unlock() ctx := k.ctxFor(caller) all, err := k.Store.All(ctx) if err != nil { return nil, err } var out []FormedRecord for _, e := range all { key := e.Current.Key() if only != nil && *only != key { continue } next, ch := formOf(e.Current) if ch == nil { continue } fr := FormedRecord{Record: key.String(), Renamed: ch.Renamed, Relinked: ch.Relinked} switch { case e.Pending != nil: fr.Skipped = "a version waits for its review: it takes the form-only version once that is decided" case e.Current.Status == StatusReserved || e.Current.Status == StatusWithdrawn: fr.Skipped = "it is " + e.Current.Status default: if err := checkForm(e.Current, next, ch); err != nil { fr.Skipped = "the check refused it: " + err.Error() } else if commit { k.carryParts(next, ch) next.FormOnly = true names := make([]string, 0, len(ch.Renamed)) for old, t := range ch.Renamed { names = append(names, old+" → "+t) } sort.Strings(names) why := fmt.Sprintf("form only: %d section name(s), %d link(s) written in the one form", len(ch.Renamed), ch.Relinked) if len(names) > 0 { why += " (" + strings.Join(names, "; ") + ")" } if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: why, Record: next}, true, false, e.Revision); err != nil { fr.Skipped = "it could not be written: " + err.Error() } else { next.FormOnly = false k.emit(EventFormed, next, caller, why, e.Versions+1) } } } out = append(out, fr) } return out, nil } // carryParts moves a record's part verdicts to the parts renamed, with the digest of the new part, so the review carries // over to a version whose meaning is unchanged. func (k *KB) carryParts(r *Record, ch *FormChange) { if len(r.Parts) == 0 { return } linked := k.linkedOf(context.Background(), r) fresh := map[string]string{} for _, p := range partsOf(r, linked) { fresh[p.name] = p.digest } var out []PartReview for _, p := range r.Parts { if t, ok := ch.Renamed[p.Part]; ok { p.Part = t } if d, ok := fresh[p.Part]; ok { p.Digest = d out = append(out, p) } } r.Parts = out } // linkedOf are the records r links, as the review reads them. func (k *KB) linkedOf(ctx context.Context, r *Record) []*Record { var linked []*Record seen := map[Key]bool{r.Key(): true} for _, l := range r.Links { to, ok := l.To.Record() if !ok || seen[to] { continue } seen[to] = true if t, err := k.Store.Get(ctx, to); err == nil && t != nil { linked = append(linked, t.Current) } } return linked }