IX30 (part 1): the validator's foundation list gains the forge and image registry modules, and a part's findings are judged in its part's form

Builds: design 47 G §529, §577, §584 (IX30)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 15:49:32 +01:00
parent fdc482d512
commit 268d2be034
4 changed files with 96 additions and 22 deletions
@@ -371,8 +371,8 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
}
if !slices.Equal(judge.Foundation, []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline"}) {
t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619 and the pipeline runner (ADR 0666 §4)", judge.Foundation)
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline", "gitea", "distribution"}) {
t.Fatalf("the foundation is %v, want exactly the nine of ADR 0619, the pipeline runner (ADR 0666 §4) and the forge and image registry modules (design 47 G §584)", judge.Foundation)
}
late := hold("late", `{"module":"late","version":"1.0","logs":[{"name":"l"}]}`)
grants := hold("mesh-grants", `{"module":"mesh-grants","version":"2.0","logs":[{"name":"l"},{"name":"m"}]}`)
@@ -2,6 +2,7 @@ package judge
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"regexp"
"strings"
"testing"
)
@@ -16,16 +17,32 @@ func TestTheJudgeReadsMilestoneGsWords(t *testing.T) {
{name: "MAJOR.MINOR", raw: `{"module":"a","version":"1.4"}`},
{name: "a pre-release suffix", raw: `{"module":"a","version":"1.4-beta"}`, want: "version"},
{name: "three numbers", raw: `{"module":"a","version":"1.4.2"}`, want: "version"},
{name: "a findings state without its stream", raw: `{"module":"a","version":"1.0","state":["findings"]}`, want: "findings"},
{name: "a findings state with its stream and ask", raw: `{"module":"a","version":"1.0","state":["findings"],` +
`"streams":[{"name":"a-findings","events":["finding-written"],"retention":"history"}],"consumes":["mesh-conditions.findings-asked"]}`},
{name: "a foundation part's findings without its stream", raw: `{"module":"gitea","version":"1.0","state":["findings"]}`, want: "findings"},
{name: "a foundation part's findings with its stream and ask (§529, §584)", raw: `{"module":"gitea","version":"1.0","state":["findings"],` +
`"streams":[{"name":"gitea-findings","events":["finding-written"],"retention":"history"}],"consumes":["mesh-conditions.findings-asked"]}`},
{name: "the image registry is a foundation part too (§584)", raw: `{"module":"distribution","version":"1.0","state":["findings"],` +
`"streams":[{"name":"distribution-findings","events":["finding-written"],"retention":"history"}],"consumes":["mesh-conditions.findings-asked"]}`},
{name: "another part's old findings and stream (§529, §577)", raw: `{"module":"a","version":"1.0","state":["findings"],` +
`"streams":[{"name":"a-findings","events":["finding-written"],"retention":"history"}],"consumes":["mesh-conditions.findings-asked"]}`, want: "old sections"},
{name: "another part's findings in its requires (§529)", raw: `{"module":"a","version":"1.0","consumes":["mesh-conditions.findings-asked"],"requires":[` +
`{"name":"findings","storage":"key-value","template":"state"},` +
`{"name":"finding-events","storage":"stream","template":"events","options":{"events":["finding-written.*","finding-cleared.*","findings-stated.*"],"max-age-days":7,"max-mib":16}}]}`},
{name: "another part's findings without the finding events (§529)", raw: `{"module":"a","version":"1.0","consumes":["mesh-conditions.findings-asked"],"requires":[` +
`{"name":"findings","storage":"key-value","template":"state"}]}`, want: "finding-events"},
{name: "another part's finding events not kept 7 days (§529)", raw: `{"module":"a","version":"1.0","consumes":["mesh-conditions.findings-asked"],"requires":[` +
`{"name":"findings","storage":"key-value","template":"state"},` +
`{"name":"finding-events","storage":"stream","template":"events","options":{"events":["finding-written.*","finding-cleared.*","findings-stated.*"],"max-age-days":30,"max-mib":16}}]}`, want: "max-age-days"},
} {
raws := map[string][]byte{"a": []byte(row.raw)}
module := "a"
if m := regexp.MustCompile(`"module":"([a-z-]+)"`).FindStringSubmatch(row.raw); m != nil {
module = m[1]
}
raws := map[string][]byte{module: []byte(row.raw)}
var refused map[string][]string
WithSeats([]catalogue.Seat{{Name: "thing", Scope: catalogue.ScopeMesh, Emits: []string{"finding-written"}}}, func() {
refused, _ = Shelf(raws, nil, 0)
})
got := strings.Join(refused["a"], "\n")
got := strings.Join(refused[module], "\n")
if (row.want == "") != (got == "") || !strings.Contains(got, row.want) {
t.Errorf("%s: refused with %q, want %q", row.name, got, row.want)
}
+68 -12
View File
@@ -1,9 +1,11 @@
package judge
import (
"encoding/json"
"fmt"
"regexp"
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
@@ -30,25 +32,79 @@ func versionProblems(m catalogue.Manifest) []string {
"and no pre-release or custom suffix exists (design 47 G §368)", m.Module, m.Version)}
}
// gWordsProblems are the problems of a manifest's words milestone G adds: the finding stream and
// ask of a module that declares a state `findings` (§387). The `*` emitter of `consumes` (§386) is judged with the
// other consumed patterns (consumePattern).
// FindingEventsEntry is the local name of the stream a part that is no foundation part asks for its findings, by the template
// `events` (design 47 G §529).
const FindingEventsEntry = "finding-events"
// findingEvents are the three events a finding stream lists (design 47 G §383, §529).
var findingEvents = []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}
// gWordsProblems are the problems of a manifest's words milestone G adds: the finding state and stream and the ask of a
// module that declares a state `findings` (§387), in the form of its part (§529, §577): a foundation part (Foundation)
// declares the state in its old `state` section and the stream `<module>-findings` in its old `streams` section; every other
// part asks both in its `requires`, the state `findings` by the template `state` and the stream `finding-events` by the
// template `events`, and the old stream is refused. The `*` emitter of `consumes` (§386) is judged with the other consumed
// patterns (consumePattern).
func gWordsProblems(m catalogue.Manifest) []string {
var problems []string
for _, s := range m.State {
if s.Name != "findings" {
continue
oldState := slices.ContainsFunc(m.State, func(s catalogue.StateDeclaration) bool { return s.Name == "findings" })
stream := FindingStreamOf(m.Module)
oldStream := slices.ContainsFunc(m.Streams, func(d catalogue.StreamDeclaration) bool { return d.Name == stream })
var asked, askedStream *catalogue.BusStateEntry
for i, e := range m.BusState {
switch {
case e.Name == "findings" && e.Template == catalogue.TemplateState:
asked = &m.BusState[i]
case e.Name == FindingEventsEntry && e.Template == catalogue.TemplateEvents:
askedStream = &m.BusState[i]
}
stream := FindingStreamOf(m.Module)
if !slices.ContainsFunc(m.Streams, func(d catalogue.StreamDeclaration) bool { return d.Name == stream }) {
}
if !oldState && asked == nil {
return nil
}
if slices.Contains(Foundation, m.Module) {
if !oldStream {
problems = append(problems, fmt.Sprintf("%s declares the state findings without the stream %q its findings are said on "+
"(design 47 G §387)", m.Module, stream))
}
if !slices.Contains(m.Consumes, FindingsAsked) {
problems = append(problems, fmt.Sprintf("%s declares the state findings without consuming %q, with which the "+
"keeper asks its findings again (design 47 G §387)", m.Module, FindingsAsked))
} else {
if oldState || oldStream {
problems = append(problems, fmt.Sprintf("%s declares its findings in its old sections, which only a foundation part does; "+
"it asks the state `findings` by the template `state` and the stream %q by the template `events` in `requires` "+
"(design 47 G §529, §577)", m.Module, FindingEventsEntry))
}
break
if askedStream == nil {
problems = append(problems, fmt.Sprintf("%s declares the state findings without the stream %q its findings are said on, "+
"asked by the template `events` in `requires` (design 47 G §529)", m.Module, FindingEventsEntry))
} else {
problems = append(problems, findingEventsProblems(m.Module, *askedStream)...)
}
}
if !slices.Contains(m.Consumes, FindingsAsked) {
problems = append(problems, fmt.Sprintf("%s declares the state findings without consuming %q, with which the "+
"keeper asks its findings again (design 47 G §387)", m.Module, FindingsAsked))
}
return problems
}
// findingEventsProblems judges the entry `finding-events` of a part that is no foundation part: it lists the three finding
// events in its option `events` and keeps them 7 days and 16 MiB (design 47 G §529).
func findingEventsProblems(module string, e catalogue.BusStateEntry) []string {
var problems []string
var listed []string
if raw, ok := e.Options["events"]; ok {
_ = json.Unmarshal(raw, &listed)
}
for _, want := range findingEvents {
if !slices.Contains(listed, want) {
problems = append(problems, fmt.Sprintf("%s's %s does not list the event %q in its `events` (design 47 G §529)", module, FindingEventsEntry, want))
}
}
for option, want := range map[string]string{"max-age-days": "7", "max-mib": "16"} {
if got := strings.TrimSpace(string(e.Options[option])); got != want {
problems = append(problems, fmt.Sprintf("%s's %s sets %s to %q, not %s (design 47 G §529)", module, FindingEventsEntry, option, got, want))
}
}
slices.Sort(problems)
return problems
}
@@ -16,13 +16,14 @@ import (
// OldSections are the sections of a manifest that keep bus state in the old forms.
var OldSections = []string{"state", "logs", "streams", "objects"}
// Foundation are the modules that keep using the old sections for good: exactly the ten parts the bus module's own
// Foundation are the modules that keep using the old sections for good: exactly the parts the bus module's own
// composition, assignments, build or delivery needs before it can answer (novox/hq ADR 0619 §1, §3, as ADR 0666 §4 changes
// them): the bus module, the grants part, the indexer, the assigner, the tool runner, the builder, the checker, mesh-config,
// the vault and the pipeline runner, which runs the bus module's own pipeline. Their new state goes on their old sections
// the vault, the pipeline runner, which runs the bus module's own pipeline, and the forge module and the image registry
// module, which its build calls (design 47 G §584). Their new state goes on their old sections
// too, so a foundation manifest asking bus state in the new form is refused.
var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline"}
"mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault", "mesh-delivery-pipeline", "gitea", "distribution"}
// ViewOwners are the two owners whose state every reader reads through their first views and read tools, never by a
// `reads` entry (novox/hq ADR 0615 §1): CountOwnerReads counts what is left of those entries.