Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/form.go
T
jschoubben 47b3ae7528 CAT-K11: form-only versions: plain titles and links of the one form, proven to change form alone
A record imported with file-named sections gets plain titles, and its links to files become references of the one form, in one
version written after the move by the tool knowledge_form_only: marked form, listing the names changed, accepted without a
judgement because a check proves each section's text with its links replaced by their labels is the same and each new link names
the record the old one named. The old names stay as aliases get takes; the parts' reviews carry over; a record waiting for its
review is skipped.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:20:19 +01:00

263 lines
8.7 KiB
Go

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
}