diff --git a/checks/words/lines.go b/checks/words/lines.go new file mode 100644 index 00000000..87c68496 --- /dev/null +++ b/checks/words/lines.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "regexp" + "sort" + "strings" +) + +// The daemons' lines (novox/hq ADR 0272, its insight of 2026-10-09): the operator types one command, `nox` +// (`mesh-cli`). The node-engine (`nox-mesh-host`) and the controller (`mesh-controller`) are daemons, and a +// person never types their names — so no text a module can show names a line of theirs. The list is the +// copy in retired-lines at the catalogue's root, which novox/hq's words.py prints (`--list lines`) and +// compares; one line each, ` `, and a trailing ` …` for a command that is a line only with +// an argument after it (`mesh-controller module check` is a line; "the mesh-controller module" is prose). + +// retiredLine is one daemon's line: the daemon, its command, and whether an argument must follow. +type retiredLine struct { + daemon, command string + withArgument bool +} + +// readLines reads retired-lines: one line a line; blank lines and lines starting with # are not lines. +func readLines(path string) ([]retiredLine, error) { + entries, err := readList(path) + if err != nil { + return nil, err + } + var out []retiredLine + for _, e := range entries { + withArgument := strings.HasSuffix(e, " …") + e = strings.TrimSuffix(e, " …") + parts := strings.Fields(e) + if len(parts) != 2 { + return nil, fmt.Errorf("retired-lines: %q is not ` `", e) + } + out = append(out, retiredLine{daemon: parts[0], command: parts[1], withArgument: withArgument}) + } + return out, nil +} + +// linePattern matches the line whole: the daemon, white space, the command. A command that is a line only +// with an argument is matched as a line is written — at the start of a line or a code span, a prompt allowed, +// with an argument after it — so prose that names the daemon beside the word ("the mesh-controller module is +// assigned") is not one. +func linePattern(l retiredLine) *regexp.Regexp { + daemon, command := regexp.QuoteMeta(l.daemon), regexp.QuoteMeta(l.command) + if l.withArgument { + tick := "`" + return regexp.MustCompile(`(?m)(?:^|` + tick + `)[ \t]*(?:\$[ \t]*)?(` + daemon + `\s+` + command + `\s+[^\s` + tick + `])`) + } + return regexp.MustCompile(`(?:^|[^\w-])(` + daemon + `\s+` + command + `)(?:$|[^\w-])`) +} + +// checkLines finds every daemon's line in the texts. +func checkLines(lines []retiredLine, texts []text) []string { + var out []string + for _, l := range lines { + rx := linePattern(l) + for _, t := range texts { + if m := rx.FindStringSubmatchIndex(t.s); m != nil { + line := t.line + if line > 0 { + line += strings.Count(t.s[:m[2]], "\n") + } + out = append(out, fmt.Sprintf("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (novox/hq "+ + "ADR 0272) — in %q", t.file, line, l.daemon, l.command, excerpt(t.s, m[2], m[3]))) + } + } + } + sort.Strings(out) + return out +} diff --git a/checks/words/main.go b/checks/words/main.go index 345e86bc..2ef0d32a 100644 --- a/checks/words/main.go +++ b/checks/words/main.go @@ -2,7 +2,9 @@ // 0244): no word the glossary retired for the tools' descriptions — the copy in retired-words at the // catalogue's root — in any text a module's tools can show. That text is every string literal in a module's // own code that is not a test (a tool's description, the notes and errors it answers with) and every -// description in its manifest. Comments are not read: an agent never sees them. +// description in its manifest. Comments are not read: an agent never sees them. And no daemon's line +// (novox/hq ADR 0272, lines.go): the copy in retired-lines, the lines a person would otherwise be told to +// type of the node-engine's or the controller's binary, where the operator's command is `nox`. // // A word is matched whole and in any case, a space in it matching any run of white space, and its plural // (`s` or `es` on its last part) is matched as the word. A vendor word is @@ -97,7 +99,26 @@ func main() { fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings)) os.Exit(1) } - fmt.Printf("words: %d retired words, none in the %d strings of %d files\n", len(words), len(texts), files) + // And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line. + lines, err := readLines(filepath.Join(root, "retired-lines")) + if err != nil { + fmt.Fprintln(os.Stderr, "words:", err) + os.Exit(2) + } + if len(lines) == 0 { + fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything") + os.Exit(2) + } + if findings := checkLines(lines, texts); len(findings) > 0 { + for _, f := range findings { + fmt.Println(f) + } + fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n", + len(findings)) + os.Exit(1) + } + fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines), + len(texts), files) } // readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words. diff --git a/checks/words/main_test.go b/checks/words/main_test.go index 949d7460..84388d14 100644 --- a/checks/words/main_test.go +++ b/checks/words/main_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "os" + "testing" +) // A retired word's plural is the word; anything else joined on is another word. func TestPatternMatchesTheWordAndItsPlural(t *testing.T) { @@ -24,3 +27,63 @@ func TestPatternMatchesTheWordAndItsPlural(t *testing.T) { } } } + +// A daemon's line is found whole — the daemon, its command, and an argument where the command is a line only with +// one — and prose that merely names the daemon beside a word is not one (novox/hq ADR 0272). +func TestADaemonsLineIsFoundWhole(t *testing.T) { + push := retiredLine{daemon: "mesh-controller", command: "push"} + module := retiredLine{daemon: "mesh-controller", command: "module", withArgument: true} + handOver := retiredLine{daemon: "nox-mesh-host", command: "hand-over"} + cases := []struct { + line retiredLine + text string + want bool + }{ + {push, "run `mesh-controller push laptop` after", true}, + {push, "mesh-controller push", true}, + {push, "mesh-controller\n push", true}, + {push, "mesh-controller pushed it", false}, + {push, "the mesh-controller pushes", false}, + {push, "mesh-controller-push", false}, + {module, "`mesh-controller module check modules/*/module.json`", true}, + {module, "run\n $ mesh-controller module check x", true}, + {module, "the mesh-controller module is assigned", false}, + {module, "mesh-controller module", false}, + {module, "`mesh-controller module`", false}, + {handOver, "`nox-mesh-host hand-over /srv/x` at the machine", true}, + {handOver, "`nox node hand-over laptop /srv/x`", false}, + {retiredLine{daemon: "mesh-host", command: "hand-over"}, "nox-mesh-host hand-over", false}, + } + for _, c := range cases { + if got := linePattern(c.line).MatchString(c.text); got != c.want { + t.Errorf("%s %s on %q = %v, want %v", c.line.daemon, c.line.command, c.text, got, c.want) + } + } + found := checkLines([]retiredLine{push, module}, []text{{file: "m/a.go", line: 3, s: "say\n`mesh-controller push`"}, + {file: "m/b.go", line: 1, s: "the mesh-controller module"}}) + if len(found) != 1 || found[0] != "m/a.go:4: `mesh-controller push` is a daemon's line — the operator types `nox …` (novox/hq ADR 0272) — in \"say `mesh-controller push`\"" { + t.Fatalf("found %q", found) + } +} + +// The list's lines read as ` `, a trailing ` …` meaning an argument must follow; anything else is +// refused rather than read as some line. +func TestRetiredLinesAreReadAsDaemonAndCommand(t *testing.T) { + dir := t.TempDir() + path := dir + "/retired-lines" + if err := os.WriteFile(path, []byte("# the list\nmesh-controller push\nmesh-controller module …\n\nnox-mesh-host hand-over\n"), 0o644); err != nil { + t.Fatal(err) + } + lines, err := readLines(path) + if err != nil || len(lines) != 3 || lines[0] != (retiredLine{daemon: "mesh-controller", command: "push"}) || + lines[1] != (retiredLine{daemon: "mesh-controller", command: "module", withArgument: true}) || + lines[2] != (retiredLine{daemon: "nox-mesh-host", command: "hand-over"}) { + t.Fatalf("read %+v, %v", lines, err) + } + if err := os.WriteFile(path, []byte("mesh-controller\n"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := readLines(path); err == nil { + t.Fatal("a line with no command was read") + } +} diff --git a/modules/mesh-vault/tools/index.ts b/modules/mesh-vault/tools/index.ts index c6c533bc..a6437e60 100644 --- a/modules/mesh-vault/tools/index.ts +++ b/modules/mesh-vault/tools/index.ts @@ -25,7 +25,7 @@ export function getVaultTools(ledger: Ledger, receives: string | undefined, root "The mesh's root secrets as this vault keeps them: every secret a module holds for itself, " + "sealed to the operator's key (novox/hq ADR 0085, amended). Ciphertext — nothing here can " + "open a line of it; the operator, holding the private key off the mesh, recovers one with " + - "`mesh-controller secret recover --from-export`. Also lists what is NOT recoverable: secrets " + + "`nox secret recover --from-export` on the control-node. Also lists what is NOT recoverable: secrets " + "made before the mesh had an operator key.", input: { sealed: { diff --git a/retired-lines b/retired-lines new file mode 100644 index 00000000..2eb26ab9 --- /dev/null +++ b/retired-lines @@ -0,0 +1,75 @@ +# The daemons' lines a text for the operator never names (novox/hq ADR 0272, its insight of 2026-10-09): the +# operator types one command, nox (mesh-cli); the node-engine and the controller are daemons. One line each, +# ` `; a trailing ` …` marks a command that is a line only with an argument after it. A copy, +# so a catalogue merge needs nothing else; novox/hq's words.py compares it. Regenerate with: +# python3 00-META/checks/words.py --list lines +mesh-controller adopt +mesh-controller artifacts … +mesh-controller ask … +mesh-controller assign +mesh-controller broker +mesh-controller builds … +mesh-controller bus … +mesh-controller cancel +mesh-controller cleanup +mesh-controller collect +mesh-controller collection … +mesh-controller conditions +mesh-controller converge +mesh-controller data … +mesh-controller delivery … +mesh-controller doctor +mesh-controller durations +mesh-controller hand-act +mesh-controller hand-acts +mesh-controller healers +mesh-controller identity +mesh-controller images … +mesh-controller kill +mesh-controller licence +mesh-controller migrate +mesh-controller mirrors … +mesh-controller module … +mesh-controller node … +mesh-controller operator +mesh-controller overlay … +mesh-controller pause +mesh-controller pin +mesh-controller plan +mesh-controller plans … +mesh-controller prepare +mesh-controller push +mesh-controller queue … +mesh-controller rebuild +mesh-controller rehearse +mesh-controller replay +mesh-controller resume +mesh-controller retire +mesh-controller rollout +mesh-controller rotate +mesh-controller seat … +mesh-controller seats … +mesh-controller secret +mesh-controller settings +mesh-controller status +mesh-controller take … +mesh-controller token +mesh-controller unassign +mesh-controller unpin +mesh-controller upgrade +mesh-controller version … +mesh-host apply +mesh-host bundle +mesh-host enrol +mesh-host enroll +mesh-host hand-over +mesh-host help +mesh-host inventory +mesh-host key +mesh-host overlay +mesh-host owned +mesh-host profile +mesh-host reconcile +mesh-host run +mesh-host version +nox-mesh-host hand-over