diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 1928421c..d4f603bf 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -5557,20 +5557,40 @@ func StateProblems(m catalogue.Manifest) []string { // which names no module (issue 774, the operator 2026-11-03). var streamsKeptUntilMoved = map[string][]string{ "brother": {"device-asks"}, - "mesh-build-go": {"toolchains-go"}, - "mesh-build-node": {"toolchains-node"}, "mesh-mail": {"mail", "mail-sends"}, "only-office": {"convert-asks"}, "mesh-delivery-checker": {"checks"}, "mesh-delivery-forge-bridge": {"forge"}, } -// PredefinedFormProblems refuses each stream a module declares that streamsKeptUntilMoved does not name for it. +// seatStreamsKeptUntilMoved are the streams kept the same way by the seat their module claims, not by its name: a build +// seat's holder keeps its toolchain reports (`toolchains-go`, `toolchains-node`) whichever module holds the seat, since no +// part fixes a module's name (novox/hq ADR 0666 §5; design 47 *A module's registration*, "No part fixes a name"). +var seatStreamsKeptUntilMoved = map[string][]string{ + "node-build-go": {"toolchains-go"}, + "node-build-node": {"toolchains-node"}, +} + +// keptUntilMoved is whether a stream of a manifest is one kept until its module moves it: by the module's name, or by a +// seat it claims. +func keptUntilMoved(m catalogue.Manifest, stream string) bool { + if slices.Contains(streamsKeptUntilMoved[m.Module], stream) { + return true + } + for _, c := range m.Claims { + if slices.Contains(seatStreamsKeptUntilMoved[c.Name], stream) { + return true + } + } + return false +} + +// PredefinedFormProblems refuses each stream a module declares that is not kept until it moves (keptUntilMoved). func PredefinedFormProblems(m catalogue.Manifest) []string { var out []string for _, s := range m.Streams { // A stream that keeps its history is one of the bus's predefined forms (novox/hq ADR 0592 §6). - if !s.KeepsHistory() && !slices.Contains(streamsKeptUntilMoved[m.Module], s.Name) { + if !s.KeepsHistory() && !keptUntilMoved(m, s.Name) { out = append(out, fmt.Sprintf("%s keeps the stream %q, outside the bus's predefined forms: keep it as a state, "+ "a log or an object store, which the runtime reads for the module's tools and others read by `reads`", m.Module, s.Name)) } diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 7d9da3e6..2ef9b609 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -5111,6 +5111,16 @@ func caseTwoModulesCannotDeclareOneStream(t *testing.T) { if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-mail", Streams: a.Streams}); len(got) != 0 { t.Fatalf("a stream kept until it moves was refused: %v", got) } + // A build seat's toolchain stream is kept by the seat its module claims, whatever the module's name (ADR 0666 §5): the + // holder of node-build-go keeps toolchains-go; a module of the old holder's name claiming no build seat does not. + toolchains := []catalogue.StreamDeclaration{{Name: "toolchains-go", Events: []string{"toolchain.**"}}} + if got := PredefinedFormProblems(catalogue.Manifest{Module: "a-go-builder", Streams: toolchains, + Claims: []catalogue.Claim{{Name: "node-build-go", Scope: catalogue.ScopeNode}}}); len(got) != 0 { + t.Fatalf("the build seat's holder's toolchain stream was refused: %v", got) + } + if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-build-go", Streams: toolchains}); len(got) != 1 { + t.Fatalf("a toolchain stream of a module claiming no build seat was kept by its name: %v", got) + } } // A seat name meaning two protocols is the failure nobody could diagnose afterwards.