diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 9650ee12..5db980e7 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -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"}]}`) diff --git a/modules/mesh-delivery-indexer/judge/g_words_test.go b/modules/mesh-delivery-indexer/judge/g_words_test.go index 37d8267e..59e225db 100644 --- a/modules/mesh-delivery-indexer/judge/g_words_test.go +++ b/modules/mesh-delivery-indexer/judge/g_words_test.go @@ -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) } diff --git a/modules/mesh-delivery-indexer/judge/gwords.go b/modules/mesh-delivery-indexer/judge/gwords.go index 12088b74..75991d62 100644 --- a/modules/mesh-delivery-indexer/judge/gwords.go +++ b/modules/mesh-delivery-indexer/judge/gwords.go @@ -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 `-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 +} diff --git a/modules/mesh-delivery-indexer/judge/oldsections.go b/modules/mesh-delivery-indexer/judge/oldsections.go index a6da40ea..f9157365 100644 --- a/modules/mesh-delivery-indexer/judge/oldsections.go +++ b/modules/mesh-delivery-indexer/judge/oldsections.go @@ -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.