diff --git a/modules/mesh-delivery-indexer/judge/busstate.go b/modules/mesh-delivery-indexer/judge/busstate.go index bd7c0cf6..5db56a78 100644 --- a/modules/mesh-delivery-indexer/judge/busstate.go +++ b/modules/mesh-delivery-indexer/judge/busstate.go @@ -49,6 +49,9 @@ var busTemplates = map[string]busTemplate{ catalogue.OptionTimeToLive: {kind: optionInt, least: 1, most: 315360000}, catalogue.OptionLifetimePerKey: {kind: optionFlag}, catalogue.OptionPerMachine: {kind: optionFlag}, + // The cap of the state's own objects store `-objects`, for a state needing more than the cap planned for it + // (design 47 G §513, §547). + catalogue.OptionObjectsCapMiB: {kind: optionInt, least: catalogue.ObjectsLeastMiB, most: catalogue.ObjectsMostMiB}, }, }, catalogue.TemplateEvents: { diff --git a/modules/mesh-delivery-indexer/judge/busstate_test.go b/modules/mesh-delivery-indexer/judge/busstate_test.go index 8f68a4b7..8c97ec71 100644 --- a/modules/mesh-delivery-indexer/judge/busstate_test.go +++ b/modules/mesh-delivery-indexer/judge/busstate_test.go @@ -52,6 +52,10 @@ func TestBusStateIsJudgedOnce(t *testing.T) { {"template on another storage type", manifest(`{"name":"x","storage":"stream","template":"state"}`), []string{"\"x\"", "template state is on the storage type key-value"}}, {"past values 0", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":0}}`), []string{"\"x\"", "options \"history\" is 0", "0 is refused"}}, {"past values 65", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"history":65}}`), []string{"\"x\"", "\"history\" is 65", "1 to 64"}}, + // Design 47 G §513, §547: a state needing more for its objects store names objects-max-mib, within the objects' bounds. + {"an objects cap on a state", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"objects-max-mib":64}}`), nil}, + {"an objects cap of 0", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"objects-max-mib":0}}`), []string{"\"x\"", "\"objects-max-mib\" is 0"}}, + {"an objects cap over 8192", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"objects-max-mib":8193}}`), []string{"\"x\"", "\"objects-max-mib\" is 8193"}}, {"a lifetime that is no number", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"ttl-seconds":"soon"}}`), []string{"\"x\"", "\"ttl-seconds\""}}, {"events age over 30 days", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-age-days":31}}`), []string{"\"x\"", "\"max-age-days\" is 31", "1 to 30"}}, {"events cap over 8192", manifest(`{"name":"x","storage":"stream","template":"events","options":{"events":["seen"],"max-mib":8193}}`), []string{"\"x\"", "\"max-mib\" is 8193"}}, diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 0eaf20e8..93457894 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -116,7 +116,17 @@ func TestTheJudge(t *testing.T) { "user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)}, "late": {Raw: json.RawMessage(`{"module":"late","version":"1","logs":[{"name":"l"}]}`)}, "newer": {Raw: json.RawMessage(`{"module":"newer","version":"1","state":["y"]}`)}, - "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}, + "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}, + // Design 47 G §584, §627, A-345: the parts G puts on the foundation list keep their old sections before it, the + // old objects section, the stream `-findings` and bus state in `requires` among them; another module's + // old objects section, which no trunk module uses, is refused as before. + "gitea": {Raw: json.RawMessage(`{"module":"gitea","version":"1","objects":[{"name":"incoming","max-mib":64}],` + + `"requires":[{"name":"idle","storage":"key-value","template":"state"}]}`)}, + "mesh-host": {Raw: json.RawMessage(`{"module":"mesh-host","version":"1","streams":[` + + `{"name":"mesh-host-findings","events":["finding-written.*"],"max-age-days":7,"max-mib":16}]}`)}, + "mesh-delivery-pipeline": {Raw: json.RawMessage(`{"module":"mesh-delivery-pipeline","version":"1",` + + `"objects":[{"name":"step-outputs","max-mib":64}]}`)}, // and the pipeline runner (§529, A-349) + "keeper": {Raw: json.RawMessage(`{"module":"keeper","version":"1","objects":[{"name":"incoming","max-mib":64}]}`)}}}, {Name: "trunk", Manifests: map[string]composer.Manifest{ "user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"]}`)}, "mesh-grants": {Raw: json.RawMessage(`{"module":"mesh-grants","version":"1","logs":[{"name":"l"}]}`)}}}}} @@ -126,7 +136,8 @@ func TestTheJudge(t *testing.T) { } got := ov.Cases[0].Manifests if got["late"].Accepted || !strings.Contains(got["late"].Refused, "late uses the old section logs") || - !got["newer"].Accepted || !got["user"].Accepted || !got["mesh-grants"].Accepted { + !got["newer"].Accepted || !got["user"].Accepted || !got["mesh-grants"].Accepted || !got["gitea"].Accepted || + !got["mesh-host"].Accepted || !got["mesh-delivery-pipeline"].Accepted || got["keeper"].Accepted || !strings.Contains(got["keeper"].Refused, "keeper uses the old section objects") { t.Errorf("the change's old sections: %+v", got) } r.Cases[0].Manifests["c"] = composer.Manifest{Digest: strings.Repeat("a", 64)} diff --git a/modules/mesh-delivery-indexer/judge/oldsections.go b/modules/mesh-delivery-indexer/judge/oldsections.go index 91a2446d..a7beaa35 100644 --- a/modules/mesh-delivery-indexer/judge/oldsections.go +++ b/modules/mesh-delivery-indexer/judge/oldsections.go @@ -23,6 +23,12 @@ var OldSections = []string{"state", "logs", "streams", "objects"} var Foundation = []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools", "mesh-delivery-builder", "mesh-delivery-checker", "mesh-config", "mesh-vault"} +// foundationAtG are the parts design 47 G puts on the foundation list in its own change: the forge module and the image +// registry module (G §584), the node-engine's module (§627) and the pipeline runner (§529, A-349). Until then their old +// sections are accepted as a foundation part's, its old `objects` section and its stream `-findings` among them, +// and their bus state in `requires` as today (A-343, A-345); they are counted as today, so no other module's verdict moves. +var foundationAtG = []string{"gitea", "distribution", "mesh-host", "mesh-delivery-pipeline"} + // 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. var ViewOwners = []string{"mesh-delivery-indexer", "mesh-delivery-assigner"} @@ -90,7 +96,7 @@ func OldSectionProblems(m catalogue.Manifest, counts OldSectionCounts) []string } return nil } - if counts == nil { + if counts == nil || slices.Contains(foundationAtG, m.Module) { return nil } var problems []string diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 1b69c235..6af40f0a 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -4034,6 +4034,14 @@ func ProcessBusProblems(m catalogue.Manifest) []string { m.Module, id, e, m.Module)) } } + // Design 47 G A-340: a process hears only what its module consumes, as V5 lets it name only what its module asks for. + for _, e := range b.Consumes { + if !slices.Contains(m.Consumes, e) { + problems = append(problems, fmt.Sprintf( + "%s's process %s's bus consumes %q, which %s does not consume (design 47 G A-340)", + m.Module, id, e, m.Module)) + } + } for _, q := range b.Queues { if !streams[q] { problems = append(problems, fmt.Sprintf( @@ -4067,12 +4075,24 @@ func filepathDir(p string) string { } // invokeNameValid says a module or process's invoked entry names a tool: ., a role's -// seat:., or * for every tool on the mesh (novox/hq ADR 0152, ADR 0436 §1). +// seat:. or a kind the seat accepts (seat:., seat:.., design 47 G A-102), +// or * for every tool on the mesh (novox/hq ADR 0152, ADR 0436 §1). func invokeNameValid(t string) bool { if t == "*" { return true } - t = strings.TrimPrefix(t, "seat:") + if rest, isSeat := strings.CutPrefix(t, "seat:"); isSeat { + // A seat's verb, or a kind the seat accepts: ``, or `.` for one that rides a stream (design 47 G + // A-102), an asker's queue `accept.` among them (§601, A-231); seatInvokeProblems judges it against the seat. + seat, after, named := strings.Cut(rest, ".") + if !named || !name.MatchString(seat) { + return false + } + if stream, kind, onStream := strings.Cut(after, "."); onStream { + return toolName.MatchString(stream) && toolName.MatchString(kind) + } + return toolName.MatchString(after) + } first, second, named := strings.Cut(t, ".") return named && name.MatchString(first) && toolName.MatchString(second) } @@ -4586,7 +4606,12 @@ func seatInvokeProblems(shelf catalogue.Shelf, declared map[string]catalogue.Sea problems = append(problems, fmt.Sprintf("%s invokes %s, a verb of the seat %q, "+catalogue.UndeclaredSeat, module, invoke, seat)) continue } - if !slices.ContainsFunc(verbs, func(v catalogue.Verb) bool { return v.Name == verb }) { + // An asker's queue of a build seat, `seat:.accept.` (design 47 G §544, §601, A-231), read by mesh-sdk's + // one reader: judged against the kinds the seat's definition accepts. + if _, queue, isQueue := catalogue.QueueInvoke(invoke); isQueue { + verb = queue + } + if !slices.ContainsFunc(verbs, func(v catalogue.Verb) bool { return v.Name == verb }) && !acceptsKind(seat, verb, declared) { problems = append(problems, fmt.Sprintf("%s invokes %s, and the seat %s serves no verb %q", module, invoke, seat, verb)) continue } @@ -4598,6 +4623,15 @@ func seatInvokeProblems(shelf catalogue.Shelf, declared map[string]catalogue.Sea return problems } +// acceptsKind says the seat accepts the kind an invoke names after its seat (design 47 G A-102): ``, or +// `.` for a kind that rides a stream. A module's own seat names its accepts by kind alone. +func acceptsKind(seat, kind string, declared map[string]catalogue.SeatDeclaration) bool { + if s, isMesh := catalogue.SeatNamed(seat); isMesh { + return slices.Contains(s.Accepts, kind) || slices.Contains(s.AcceptedOn, kind) + } + return slices.Contains(declared[seat].Accepts, kind) +} + // SeatPathProblems is what is wrong with the paths a set of the mesh's own seats names (novox/hq ADR // 0287): only a node seat names one, each absolute and literal; every path has exactly one seat naming it // without stepping back; and a seat steps back only for another node seat naming the same path without @@ -5618,6 +5652,19 @@ func StateProblems(m catalogue.Manifest) []string { } } for _, r := range m.Reads { + if seat, state, isSeat, err := catalogue.SeatRead(r); isSeat { + // A read of a seat holder's state (design 47 G A-116, A-128): the seat declares the state; the holder is resolved + // where it is read. + switch s, known := catalogue.SeatNamed(seat); { + case err != nil: + problems = append(problems, fmt.Sprintf("%s reads %v", m.Module, err)) + case !known: + problems = append(problems, fmt.Sprintf("%s reads %q, a state of the seat %q, "+catalogue.UndeclaredSeat, m.Module, r, seat)) + case !slices.Contains(s.States, state): + problems = append(problems, fmt.Sprintf("%s reads %q, and the seat %s declares no state %q (design 47 G A-128)", m.Module, r, seat, state)) + } + continue + } module, _, err := catalogue.ReadState(r) if err != nil { problems = append(problems, fmt.Sprintf("%s reads %v", m.Module, err)) @@ -5654,8 +5701,9 @@ func PredefinedFormProblems(m catalogue.Manifest) []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). // A foundation part keeps its old sections for good, its streams among them: its findings stream - // `-findings` (design 47 G §529, A-30, A-146). - if !s.KeepsHistory() && !slices.Contains(Foundation, m.Module) && !slices.Contains(streamsKeptUntilMoved[m.Module], s.Name) { + // `-findings` (design 47 G §529, A-30, A-146); so do the parts G puts on the list (A-345). + foundation := slices.Contains(Foundation, m.Module) || slices.Contains(foundationAtG, m.Module) + if !s.KeepsHistory() && !foundation && !slices.Contains(streamsKeptUntilMoved[m.Module], 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 abc533d4..5da4519e 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -2459,7 +2459,7 @@ func caseAProcessBusNamesOnlyTheModulesOwn(t *testing.T) { // its events stream and a state of its key-value state, and a name it does not ask for there is refused as before. asked := func(bus string) string { raw := mailSenderProcessManifest(t, bus) - return strings.Replace(raw, `"resources"`, `"requires":[{"name":"mail-events","storage":"stream","template":"events",`+ + return strings.Replace(raw, `"resources"`, `"consumes":["mesh-conditions.findings-asked"],"requires":[{"name":"mail-events","storage":"stream","template":"events",`+ `"options":{"events":["mail.send.queued"]}},{"name":"mail-kept","storage":"key-value","template":"state"}],"resources"`, 1) } for name, c := range map[string]struct{ bus, says string }{ @@ -2467,6 +2467,9 @@ func caseAProcessBusNamesOnlyTheModulesOwn(t *testing.T) { "a state asked for by template": {`{"state":["mail-kept"]}`, ""}, "an event the events stream does not hold": {`{"emits":["mail.send.lost"]}`, "mail.send.lost"}, "a state the module does not ask for": {`{"state":["mail-other"]}`, "mail-other"}, + // Design 47 G A-340: a process's bus may name, under consumes, an event its module consumes, judged as V5 judges. + "an event the module consumes": {`{"consumes":["mesh-conditions.findings-asked"]}`, ""}, + "an event the module does not consume": {`{"consumes":["mesh-conditions.other"]}`, `consumes "mesh-conditions.other"`}, } { _, err := ParseManifest([]byte(asked(c.bus))) if (c.says == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.says)) { @@ -2947,6 +2950,11 @@ func caseAStateNameIsLocalAndOneToken(t *testing.T) { {`{"module":"a.b","version":"1","state":["s"]}`, `no dot`}, {`{"module":"a","version":"1","reads":["bindings"]}`, `a read is .`}, {`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`}, + // A read of a seat holder's state names the seat, which declares the state (design 47 G A-116, A-128). + {`{"module":"a","version":"1","reads":["seat:mesh-conditions.findings"]}`, ``}, + {`{"module":"a","version":"1","reads":["seat:mesh-conditions.nothing"]}`, `the seat mesh-conditions declares no state "nothing"`}, + {`{"module":"a","version":"1","reads":["seat:mesh-unknown.findings"]}`, `a state of the seat "mesh-unknown"`}, + {`{"module":"a","version":"1","reads":["seat:mesh-conditions"]}`, `does not name a seat and its state`}, {`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}`}, } { _, err := ParseManifest([]byte(c.manifest)) @@ -5218,6 +5226,11 @@ func caseTwoModulesCannotDeclareOneStream(t *testing.T) { if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-delivery-indexer", Streams: findings}); len(got) != 0 { t.Fatalf("a foundation part's findings stream was refused: %v", got) } + // So does a part G puts on the list, before it (design 47 G §627, A-345). + hostFindings := []catalogue.StreamDeclaration{{Name: "mesh-host-findings", Events: []string{"finding-written.*"}}} + if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-host", Streams: hostFindings}); len(got) != 0 { + t.Fatalf("the node-engine's findings stream was refused: %v", got) + } } // A seat name meaning two protocols is the failure nobody could diagnose afterwards. @@ -5240,6 +5253,11 @@ func caseTwoModulesCannotDeclareTheSameSeat(t *testing.T) { func caseUsingASeatNobodyDeclaresIsRefused(t *testing.T) { unclaimed := telegram() unclaimed.Claims = nil + // A mesh seat that accepts a kind riding a stream (design 47 G A-102), beside the seat list's own. + was := catalogue.Seats() + t.Cleanup(func() { catalogue.UseSeats(was) }) + catalogue.UseSeats(append(catalogue.Seats(), catalogue.Seat{Name: "mesh-steps", Scope: catalogue.ScopeMesh, Decision: "test", + Accepts: []string{"step"}, AcceptedOn: []string{"check.step"}})) for _, row := range []struct { name, want string shop catalogue.Manifest @@ -5268,8 +5286,20 @@ func caseUsingASeatNobodyDeclaresIsRefused(t *testing.T) { {"what the engine reads of a manifest, and its event (ADR 0618 §3, §4)", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-engine.manifest-reads"}, Consumes: []string{"node-engine.manifest-reads-changed"}}, telegram(), false}, + // An asker names the queue it asks as `seat:.accept.`, checked against the kinds the seat's definition + // accepts (design 47 G §601, A-231); a kind the seat accepts, alone or on its stream, as `seat:.` or + // `seat:..` (A-102). + {"a queue a build seat accepts", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-build-go.accept.check"}}, telegram(), false}, + {"a queue a build seat does not accept", `the seat node-build-go serves no verb "other"`, + catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-build-go.accept.other"}}, telegram(), false}, + {"a queue a declared seat accepts", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.accept.send"}}, telegram(), false}, + {"a kind a seat accepts", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-steps.step"}}, telegram(), false}, + {"a kind a seat accepts on its stream", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-steps.check.step"}}, telegram(), false}, + {"a kind on a stream the seat does not take it on", `the seat mesh-steps serves no verb "delivery.step"`, + catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-steps.delivery.step"}}, telegram(), false}, } { - got := CatalogueProblems(catalogue.Shelf{"telegram": row.seat, "shop": row.shop}) + // The manifest's own invokes rule (at parse) takes the dotted kind as well; the seat's rule then judges it. + got := append(invokeProblems(row.shop), CatalogueProblems(catalogue.Shelf{"telegram": row.seat, "shop": row.shop})...) joined := strings.Join(got, "; ") if row.want == "" && joined != "" || row.want != "" && (len(got) != 1 || !strings.Contains(joined, row.want) || catalogue.IsUndeclaredSeat(got[0]) != row.note) {