From fab57411c57a2a1f4cdbfce5cb5cc167a3278380 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 22:31:05 +0200 Subject: [PATCH] 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). --- checks/particulars/main.go | 63 ++++++++++++++++++++++++++------- checks/particulars/main_test.go | 57 +++++++++++++++++++++++++---- particulars | 4 +++ 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/checks/particulars/main.go b/checks/particulars/main.go index e42004a4..44f995aa 100644 --- a/checks/particulars/main.go +++ b/checks/particulars/main.go @@ -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/), 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/), 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/, // 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 ` (asus-g14): that exact word, in +// any case and not part of a longer hyphened word, is no finding in any module. // // go run . // @@ -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/ (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/ (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 `). -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 `) +// and the hardware's words (`hardware `, 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 ` 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 `", 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 { diff --git a/checks/particulars/main_test.go b/checks/particulars/main_test.go index 1f6c4963..d863aa44 100644 --- a/checks/particulars/main_test.go +++ b/checks/particulars/main_test.go @@ -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/ 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 ) 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) + } +} diff --git a/particulars b/particulars index 92686284..40476cc0 100644 --- a/particulars +++ b/particulars @@ -7,3 +7,7 @@ g14 # A module's hardware model's word, no finding in that module only (checks/particulars): model . # 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 . +# asus-g14 is the laptop's name in its vendor trigger file, /etc/triggerhappy/triggers.d/asus-g14.conf. +hardware asus-g14