Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/sectionform.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

102 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// A section is a part with a title, never a file (novox/hq to-be 55 §2.2, ADR 0592 §29, §30): its name is a plain title,
// at most 200 bytes, unique in its record; its text holds no local path, no reference to a file and line, and no link
// whose target is a relative path or a file. A reference has one form: the record, a section's title, then a row or
// clause, such as "research 081 › Direction › D3" or "ADR 0290 §7".
import (
"fmt"
"regexp"
"strings"
)
// MostTitleBytes is the longest a section's title is.
const MostTitleBytes = 200
var (
// A name that looks like a file: it has an extension, a number prefix such as 00-, or a slash.
fileExtension = regexp.MustCompile(`\.[A-Za-z][A-Za-z0-9]{0,4}$`)
numberPrefix = regexp.MustCompile(`^\d{1,3}[-_.]`)
// A local path: in a person's home folder or the temporary folder.
localPath = regexp.MustCompile(`(?:^|[\s("'` + "`" + `])(?:/home/[^\s/)]+/|/Users/[^\s/)]+/|/tmp/|/var/tmp/|~/|[A-Za-z]:\\)`)
// A reference to a file and line: a name with a known extension, a colon and a number.
fileAndLine = regexp.MustCompile(`\b[\w./-]*\w\.(?:go|md|json|ts|tsx|js|py|sh|html|css|yaml|yml|toml|txt|sql|rs|c|h):\d+\b`)
// A markdown link, and its target.
sectionLink = regexp.MustCompile(`\]\(([^)\s]+)(?:\s+"[^"]*")?\)`)
)
// linkToFile says whether a link's target is a relative path or a file, not an address or an anchor.
func linkToFile(target string) bool {
switch {
case strings.HasPrefix(target, "#"), strings.HasPrefix(target, "mailto:"), strings.HasPrefix(target, "ref:"):
return false
case regexp.MustCompile(`^[A-Za-z][A-Za-z0-9+.-]*://`).MatchString(target):
return false
}
return true
}
// sectionNameProblems are the ways a section's name is not a plain title.
func sectionNameProblems(name string) []string {
var out []string
switch {
case strings.TrimSpace(name) == "":
return []string{"a section has no title"}
case len(name) > MostTitleBytes:
return []string{fmt.Sprintf("the section %.40q… has a title of %d bytes; a title is at most %d", name, len(name), MostTitleBytes)}
case strings.ContainsAny(name, "\n\r"):
return []string{fmt.Sprintf("the section %.40q has a title of more than one line", name)}
}
switch {
case strings.Contains(name, "/"):
out = append(out, fmt.Sprintf("the section %q has a slash in its title: a title is plain words, never a path", name))
case numberPrefix.MatchString(name):
out = append(out, fmt.Sprintf("the section %q starts with a number prefix such as 00-: a title is plain words, never a file's name", name))
case fileExtension.MatchString(name):
out = append(out, fmt.Sprintf("the section %q ends in a file extension: a title is plain words, never a file's name", name))
}
return out
}
// sectionTextProblems are the ways a section's text holds a file, not a reference of the one form.
func sectionTextProblems(name, text string) []string {
var out []string
if m := localPath.FindString(text); m != "" {
out = append(out, fmt.Sprintf("the section %q holds a local path (%s…): say what it is, not where it was on a machine", name, strings.TrimSpace(m)))
}
if m := fileAndLine.FindString(text); m != "" {
out = append(out, fmt.Sprintf("the section %q refers to a file and line (%s): name the function or the record's section instead", name, m))
}
for _, l := range sectionLink.FindAllStringSubmatch(text, -1) {
if linkToFile(l[1]) {
out = append(out, fmt.Sprintf("the section %q links to a file (%s): refer to a record as \"ADR 0290 §7\" or \"research 081 › Direction\"", name, l[1]))
break
}
}
return out
}
// sectionFormProblems are all of them, for one section.
func sectionFormProblems(name, text string) []string {
return append(sectionNameProblems(name), sectionTextProblems(name, text)...)
}
// unchangedSection says whether the record already holds a section of this name and text: a record imported with
// file-named sections keeps them until its form-only version, and a section left alone is not judged again.
func unchangedSection(held *Entry, s Section) bool {
if held == nil {
return false
}
for _, r := range []*Record{held.Current, held.Pending} {
if r != nil {
for _, h := range r.Sections {
if h.Name == s.Name && h.Text == s.Text {
return true
}
}
}
}
return false
}