CAT-K5: a section is a part with a plain title, never a file; a reference has one form
A new rule, section-form (31 now), refuses from the day the records move a section whose title is a file name, a number prefix, a slash or over 200 bytes, a title twice in a record, and a text that holds a local path, a file and line, or a link to a file. Only a section that is new or changed is judged, so a record imported with file-named sections keeps them until its form-only version; write refuses at once. Issue 1966. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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:"):
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user