From 007dd1791091a03fd4503c73569044b954e7a9b4 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 22:00:05 +0200 Subject: [PATCH] Let a module declare its hardware model's word, and blank only the module path and the exact record citation (issue 1838) asus-zephyrus-g14's g14 is the model in adopted paths, so the module declares it in particulars and it is no finding there only. The forge rule hid a node after a word's @ (database@novox/postgres) and any novox/; now only git.novox.be/novox/, novox/hq ADR|issue|design|to-be|research and the npm scope are blanked (1838/AC2). merge-check.sh's comment no longer names the removed covered list. --- checks/particulars/main.go | 81 +++++++++++++++++++++++++++++---- checks/particulars/main_test.go | 73 +++++++++++++++++++++++++++-- merge-check.sh | 4 +- particulars | 4 ++ 4 files changed, 147 insertions(+), 15 deletions(-) diff --git a/checks/particulars/main.go b/checks/particulars/main.go index 6f0c745b..e42004a4 100644 --- a/checks/particulars/main.go +++ b/checks/particulars/main.go @@ -6,9 +6,16 @@ // they may be written. Every directory under modules/ is read, with no list of covered modules, so a new module // 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. -// Three names hold a node name and are not one, so they are blanked before a line is read: the forge's host -// (git.novox.be) and organisation path (novox/), 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). +// 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 +// 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. +// +// A module may declare its hardware model's word in `particulars`, as a line `model `: the word, +// 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. // // go run . // @@ -23,13 +30,14 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" ) -// forge is the forge's host and organisation path, blanked before a line is read; a path segment novox/ elsewhere -// (testdata/novox/x, /srv/novox/data) is not blanked. -var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\w.-])novox/[\w.-]+`) +// 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`) // 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 @@ -48,7 +56,8 @@ 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 { - names, err := readList(filepath.Join(root, "particulars")) + entries, err := readList(filepath.Join(root, "particulars")) + names, models := splitModels(entries) if err == nil && len(names) == 0 { err = fmt.Errorf("particulars lists no name — the check would pass on anything") } @@ -59,11 +68,14 @@ func check(root string) int { err = fmt.Errorf("modules/ holds no module — the check would pass on anything") } } + if err == nil { + err = checkModels(models, names, modules) + } if err != nil { fmt.Fprintln(os.Stderr, "particulars:", err) return 2 } - findings, err := run(root, names, modules) + findings, err := run(root, names, modules, models) if err != nil { fmt.Fprintln(os.Stderr, "particulars:", err) return 2 @@ -98,6 +110,53 @@ 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) { + models = map[string][]string{} + for _, e := range entries { + f := strings.Fields(e) + if len(f) > 0 && f[0] == "model" { + if len(f) != 3 { + models[""] = append(models[""], e) + continue + } + models[f[1]] = append(models[f[1]], f[2]) + continue + } + names = append(names, e) + } + return names, models +} + +// checkModels refuses a model declaration that is malformed, names no module or names a word that is no name. +func checkModels(models map[string][]string, names, modules []string) error { + for m, words := range models { + if m == "" { + return fmt.Errorf("particulars: %q is not `model `", words[0]) + } + if !slices.Contains(modules, m) { + return fmt.Errorf("particulars: model %s %s names no module under modules/", m, words[0]) + } + for _, w := range words { + if !slices.ContainsFunc(names, func(n string) bool { return strings.EqualFold(n, w) }) { + return fmt.Errorf("particulars: model %s %s: %q is not a listed name", m, w, w) + } + } + } + return nil +} + +// without returns the names but the words, in any case. +func without(names, words []string) []string { + var out []string + for _, n := range names { + if !slices.ContainsFunc(words, func(w string) bool { return strings.EqualFold(n, w) }) { + out = append(out, n) + } + } + return out +} + // moduleDirs returns every directory under the catalogue's modules/, sorted. func moduleDirs(root string) ([]string, error) { entries, err := os.ReadDir(filepath.Join(root, "modules")) @@ -113,10 +172,12 @@ func moduleDirs(root string) ([]string, error) { return out, nil } -// run reads every text file of each module named and returns the findings, sorted. -func run(root string, names, modules []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) { var out []string for _, m := range modules { + names := without(all, models[m]) dir := filepath.Join(root, "modules", m) err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { diff --git a/checks/particulars/main_test.go b/checks/particulars/main_test.go index faf8914d..1f6c4963 100644 --- a/checks/particulars/main_test.go +++ b/checks/particulars/main_test.go @@ -35,7 +35,6 @@ func TestTheForgeIsNotANodeName(t *testing.T) { "see novox/hq issue 5", "module git.novox.be/novox/mesh-sdk/go v1.2.3", "https://git.novox.be/novox/mesh-catalog", - "git.novox.be", "space trace surface grace g14x", } { if got := scan("f", s, names); len(got) != 0 { @@ -68,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"}) + got, err := run(root, names, []string{"kernel"}, nil) if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") { t.Errorf("got %v, %v", got, err) } @@ -105,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"}) + got, err := run(root, names, []string{"kernel"}, nil) if err != nil { t.Fatal(err) } @@ -198,3 +197,71 @@ func TestALineIsReadWithoutCompilingAMatcher(t *testing.T) { t.Errorf("a line costs %.0f allocations, want at most 20", n) } } + +// 1838/AC2: a module may declare its hardware model's word in `particulars` (model ); the word is +// then no finding in that module, and still one in every other. +func TestAModelsWordIsAllowedInItsModuleOnly(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", "ace\ng14\nmodel asus-zephyrus-g14 g14\n") + write("modules/asus-zephyrus-g14/README.md", "the adopted /etc/modprobe.d/g14-nvidia-power.conf; a second G14\n") + write("modules/kernel/a", "clean\n") + if got := check(root); got != 0 { + t.Errorf("the model's word in its own module = %d, want 0", got) + } + write("modules/asus-zephyrus-g14/a", "on ace\n") + if got := check(root); got != 1 { + t.Errorf("another node name in the model's module = %d, want 1", got) + } + write("modules/asus-zephyrus-g14/a", "clean\n") + write("modules/kernel/a", "i3's 20-g14.conf\n") + if got := check(root); got != 1 { + t.Errorf("the model's word in another module = %d, want 1", got) + } + write("modules/kernel/a", "clean\n") + write("particulars", "ace\ng14\nmodel no-such-module g14\n") + if got := check(root); got != 2 { + t.Errorf("a model of a module that does not exist = %d, want 2", got) + } + write("particulars", "ace\ng14\nmodel asus-zephyrus-g14 shanks\n") + if got := check(root); got != 2 { + t.Errorf("a model's word that is no listed name = %d, want 2", got) + } +} + +// 1838/AC2: only the module path git.novox.be/novox/, a package name or scope with nothing before its @, and +// the exact record citation novox/hq ADR|issue|design|to-be|research are the forge's or the software's; any +// other novox/, the forge's host alone, and a node after a word's @ are findings. +func TestOnlyTheModulePathThePackageAndTheCitationAreNotNodeNames(t *testing.T) { + for _, s := range []string{ + "module git.novox.be/novox/mesh-sdk/go v1.2.3", + "https://git.novox.be/novox/mesh-catalog", + `import { registerModuleTools } from "@novox/mesh-sdk/tools";`, + `"dependencies": { "@novox/ui": "^2.0.0" }`, + "// novox/hq ADR 0507 §9", + "(novox/hq issue 1838)", + "novox/hq design 47 and novox/hq to-be 53, novox/hq research 12", + } { + if got := scan("f", s, names); len(got) != 0 { + t.Errorf("%q refused: %v", s, got) + } + } + 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 { + t.Errorf("%q gave %v, want one finding", s, got) + } + } +} diff --git a/merge-check.sh b/merge-check.sh index 3fe55bd1..72a073e9 100755 --- a/merge-check.sh +++ b/merge-check.sh @@ -55,8 +55,8 @@ set -eu (cd checks/words && go test -count=1 . && go run . ../..) -# Node names (novox/hq ADR 0507 §9, issue 1838): no node name of this mesh in a covered module; the names are in -# `particulars`, the covered modules in checks/particulars/covered. +# Node names (novox/hq ADR 0507 §9, issue 1838): no node name of this mesh in any module (every directory under +# modules/ is covered, with no list); the names, and a module's hardware model word, are in `particulars`. (cd checks/particulars && go test -count=1 . && go run . ../..) # The toolchains (novox/hq ADR 0477 clause 4): the package go only in modules/go, nodejs and npm only in modules/nodejs. (cd checks/toolchains && go test -count=1 . && go run . ../..) diff --git a/particulars b/particulars index b2ab98b1..92686284 100644 --- a/particulars +++ b/particulars @@ -3,3 +3,7 @@ novox ace shanks 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