Take novox/hq in any form and a declared hardware word as no finding in the particulars check (issue 1838)

novox/hq is the name of the software's own record, whatever follows it; asus-g14 is the laptop's name in
its vendor trigger file, declared in particulars as hardware asus-g14 (1838/AC2).
This commit is contained in:
jochen
2026-10-22 22:31:05 +02:00
parent 212c8e4f52
commit fab57411c5
3 changed files with 105 additions and 19 deletions
+50 -13
View File
@@ -7,8 +7,8 @@
// is covered from its first commit. A name is matched whole and in any case; a hyphen, an underscore or a
// CamelCase boundary parts one, so a longer word holding a name is another word.
// Some text holds a node name and is not one, so it is blanked before a line is read, each form exact: the
// software's module path (git.novox.be/novox/<repo>), a record citation (novox/hq followed by ADR, issue,
// design, to-be or research), the hardware model (asus-zephyrus-g14, ROG Zephyrus G14), and the software's own
// software's module path (git.novox.be/novox/<repo>), the name of the software's record (novox/hq, followed by
// anything), the hardware model (asus-zephyrus-g14, ROG Zephyrus G14), and the software's own
// name and npm scope (Novox Mesh, novox-mesh, @novox/mesh-sdk with nothing before the @). Any other novox/<x>,
// the forge's host alone and a node after a word's @ (database@novox/postgres) are findings.
//
@@ -16,6 +16,8 @@
// one of the names, is then no finding in that module's directory and still one in every other
// (asus-zephyrus-g14's adopted paths such as /etc/modprobe.d/g14-nvidia-power.conf). A declaration naming no
// module or no listed name makes the list unreadable.
// A hardware's own name holding a name is declared as a line `hardware <word>` (asus-g14): that exact word, in
// any case and not part of a longer hyphened word, is no finding in any module.
//
// go run . <catalogue root>
//
@@ -35,9 +37,10 @@ import (
"strings"
)
// forge is the software's module path on its forge and the exact citation of its record, blanked before a line is
// read; any other novox/<x> (a repository in a fixture, testdata/novox/x, novox/hq review 2) is not blanked.
var forge = regexp.MustCompile(`(?i:git\.novox\.be/novox/[\w.-]+)|novox/hq (?:ADR|issue|design|to-be|research)\b`)
// forge is the software's module path on its forge and the name of its record (novox/hq, in any form: a citation,
// a review, a URL path), blanked before a line is read; any other novox/<x> (a repository in a fixture,
// testdata/novox/x) is not blanked, nor novox/hqx.
var forge = regexp.MustCompile(`(?i:git\.novox\.be/novox/[\w.-]+)|novox/hq(?:$|[^\w-])`)
// ownNames are the hardware model's and the software's own names, blanked before a line is read: the model
// in any separator and case (asus-zephyrus-g14, ASUS_ZEPHYRUS_G14, ROG Zephyrus G14), the software as two
@@ -57,7 +60,7 @@ func main() {
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
func check(root string) int {
entries, err := readList(filepath.Join(root, "particulars"))
names, models := splitModels(entries)
names, models, hardware := splitModels(entries)
if err == nil && len(names) == 0 {
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
}
@@ -71,11 +74,15 @@ func check(root string) int {
if err == nil {
err = checkModels(models, names, modules)
}
var blanks *regexp.Regexp
if err == nil {
blanks, err = hardwareWords(hardware)
}
if err != nil {
fmt.Fprintln(os.Stderr, "particulars:", err)
return 2
}
findings, err := run(root, names, modules, models)
findings, err := run(root, names, modules, models, blanks)
if err != nil {
fmt.Fprintln(os.Stderr, "particulars:", err)
return 2
@@ -110,11 +117,16 @@ func readList(path string) ([]string, error) {
return out, sc.Err()
}
// splitModels parts the list's entries into the names and the models' words by module (`model <module> <word>`).
func splitModels(entries []string) (names []string, models map[string][]string) {
// splitModels parts the list's entries into the names, the models' words by module (`model <module> <word>`)
// and the hardware's words (`hardware <word>`, kept whole to be checked).
func splitModels(entries []string) (names []string, models map[string][]string, hardware []string) {
models = map[string][]string{}
for _, e := range entries {
f := strings.Fields(e)
if len(f) > 0 && f[0] == "hardware" {
hardware = append(hardware, e)
continue
}
if len(f) > 0 && f[0] == "model" {
if len(f) != 3 {
models[""] = append(models[""], e)
@@ -125,7 +137,32 @@ func splitModels(entries []string) (names []string, models map[string][]string)
}
names = append(names, e)
}
return names, models
return names, models, hardware
}
// hardwareWords is the matcher of the declared hardware words, nil when none is declared; a line that is not
// `hardware <word>` makes the list unreadable.
func hardwareWords(lines []string) (*regexp.Regexp, error) {
var words []string
for _, l := range lines {
f := strings.Fields(l)
if len(f) != 2 {
return nil, fmt.Errorf("particulars: %q is not `hardware <word>`", l)
}
words = append(words, regexp.QuoteMeta(f[1]))
}
if len(words) == 0 {
return nil, nil
}
return regexp.MustCompile(`(?i)(?:^|[^A-Za-z0-9-])(?:` + strings.Join(words, "|") + `)(?:$|[^A-Za-z0-9-])`), nil
}
// blankWith blanks what rx matches in s, keeping every other byte where it was.
func blankWith(rx *regexp.Regexp, s string) string {
if rx == nil {
return s
}
return rx.ReplaceAllStringFunc(s, func(m string) string { return strings.Repeat(" ", len(m)) })
}
// checkModels refuses a model declaration that is malformed, names no module or names a word that is no name.
@@ -174,7 +211,7 @@ func moduleDirs(root string) ([]string, error) {
// run reads every text file of each module named and returns the findings, sorted; a module's model words are
// not read as names in it.
func run(root string, all, modules []string, models map[string][]string) ([]string, error) {
func run(root string, all, modules []string, models map[string][]string, hardware *regexp.Regexp) ([]string, error) {
var out []string
for _, m := range modules {
names := without(all, models[m])
@@ -192,7 +229,7 @@ func run(root string, all, modules []string, models map[string][]string) ([]stri
}
rel, _ := filepath.Rel(root, path)
// A path is read too: a testdata directory named after a node is a finding.
for _, n := range hits(filepath.ToSlash(rel), names) {
for _, n := range hits(blankWith(hardware, filepath.ToSlash(rel)), names) {
out = append(out, fmt.Sprintf("%s:0: node name %q in the path", filepath.ToSlash(rel), n))
}
if d.Name() == "go.sum" {
@@ -205,7 +242,7 @@ func run(root string, all, modules []string, models map[string][]string) ([]stri
if bytes.IndexByte(b, 0) >= 0 {
return nil
}
out = append(out, scan(filepath.ToSlash(rel), string(b), names)...)
out = append(out, scan(filepath.ToSlash(rel), blankWith(hardware, string(b)), names)...)
return nil
})
if err != nil {
+51 -6
View File
@@ -67,7 +67,7 @@ func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
p := filepath.Join(root, "modules/kernel/testdata/ace/x")
os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte("clean\n"), 0o644)
got, err := run(root, names, []string{"kernel"}, nil)
got, err := run(root, names, []string{"kernel"}, nil, nil)
if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") {
t.Errorf("got %v, %v", got, err)
}
@@ -104,7 +104,7 @@ func TestRunReadsOnlyTheModulesItIsGiven(t *testing.T) {
}
write("modules/kernel/files/a", "on novox\n")
write("modules/other/files/a", "on novox\n")
got, err := run(root, names, []string{"kernel"}, nil)
got, err := run(root, names, []string{"kernel"}, nil, nil)
if err != nil {
t.Fatal(err)
}
@@ -253,11 +253,7 @@ func TestOnlyTheModulePathThePackageAndTheCitationAreNotNodeNames(t *testing.T)
for _, s := range []string{
`handedWith("node-b", "gitea", "postgres-database@novox/postgres+pair")`,
"delivery.novox/mesh-controller",
"git.anchor.internal:222/novox/hq.git",
"see novox/mesh-catalog #441",
"novox/hq review round 2",
"novox/hq issues 3 and 4",
"repository: novox/hq",
"git.novox.be",
} {
if got := scan("f", s, names); len(got) != 1 {
@@ -265,3 +261,52 @@ func TestOnlyTheModulePathThePackageAndTheCitationAreNotNodeNames(t *testing.T)
}
}
}
// 1838/AC2: novox/hq is the name of the software's own record, no finding in any form; another novox/<repo> is.
func TestTheRecordsNameIsNoFindingInAnyForm(t *testing.T) {
for _, s := range []string{
"novox/hq review round 2",
"novox/hq issues 1665 and 1666",
"repository: novox/hq",
"[the record](https://forge.example.org/novox/hq/src/branch/main/00-META/glossary.md)",
"novox/hq#7 at aaa",
"git.anchor.internal:222/novox/hq.git",
} {
if got := scan("f", s, names); len(got) != 0 {
t.Errorf("%q refused: %v", s, got)
}
}
for _, s := range []string{`repo := "novox/mesh-catalog"`, "novox/hqx", "novox/hq-mirror"} {
if got := scan("f", s, names); len(got) != 1 {
t.Errorf("%q gave %v, want one finding", s, got)
}
}
}
// 1838/AC2: a hardware word declared in particulars (hardware <word>) is no finding anywhere; a name standing
// alone still is.
func TestAHardwareWordIsNoFindingAnywhere(t *testing.T) {
root := t.TempDir()
write := func(p, s string) {
p = filepath.Join(root, p)
os.MkdirAll(filepath.Dir(p), 0o755)
os.WriteFile(p, []byte(s), 0o644)
}
write("particulars", "g14\nhardware asus-g14\n")
write("modules/triggerhappy/a", "/etc/triggerhappy/triggers.d/asus-g14.conf and ASUS-G14\n")
if got := check(root); got != 0 {
t.Errorf("the hardware word = %d, want 0", got)
}
write("modules/triggerhappy/a", "asus-g14.conf on g14\n")
if got := check(root); got != 1 {
t.Errorf("g14 standing alone beside it = %d, want 1", got)
}
write("modules/triggerhappy/a", "my-asus-g14-x\n")
if got := check(root); got != 1 {
t.Errorf("a longer word holding it = %d, want 1", got)
}
write("particulars", "g14\nhardware\n")
if got := check(root); got != 2 {
t.Errorf("a hardware line with no word = %d, want 2", got)
}
}
+4
View File
@@ -7,3 +7,7 @@ g14
# A module's hardware model's word, no finding in that module only (checks/particulars): model <module> <word>.
# asus-zephyrus-g14: g14 is the model (ROG Zephyrus G14), in adopted paths such as g14-nvidia-power.conf.
model asus-zephyrus-g14 g14
# A hardware's own name that holds a node name, no finding in any module (checks/particulars): hardware <word>.
# asus-g14 is the laptop's name in its vendor trigger file, /etc/triggerhappy/triggers.d/asus-g14.conf.
hardware asus-g14