From 8f7fd39e21b792115236fe3da003816610047e0c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 11:42:16 +0100 Subject: [PATCH] =?UTF-8?q?A-277:=20the=20manifest=20validator=20reads=20a?= =?UTF-8?q?=20state's=20max-mib=20and=20judges=20an=20approvals=20state=20?= =?UTF-8?q?as=20=C2=A7677=20declares=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template state takes max-mib (1 to 8192, as the old state section reads it). A state named approvals is refused without key-ttl true, with ttl-seconds other than 172800 (approval.KeptFor, 48 hours), or with max-mib above 64; with none declared its bucket takes the default 64 MiB. Builds: design 47 G A-277, A-270, §677 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../mesh-delivery-indexer/judge/busstate.go | 52 +++++++++++++++++++ .../judge/busstate_test.go | 8 +++ 2 files changed, 60 insertions(+) diff --git a/modules/mesh-delivery-indexer/judge/busstate.go b/modules/mesh-delivery-indexer/judge/busstate.go index 8bdfc158..edde8726 100644 --- a/modules/mesh-delivery-indexer/judge/busstate.go +++ b/modules/mesh-delivery-indexer/judge/busstate.go @@ -8,6 +8,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-sdk/go/approval" "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -49,6 +50,8 @@ var busTemplates = map[string]busTemplate{ catalogue.OptionTimeToLive: {kind: optionInt, least: 1, most: 315360000}, catalogue.OptionLifetimePerKey: {kind: optionFlag}, catalogue.OptionPerMachine: {kind: optionFlag}, + // A-277: a state's cap is an option of its template, read as the old state section reads it. + catalogue.OptionCapMiB: {kind: optionInt, least: catalogue.ObjectsLeastMiB, most: catalogue.ObjectsMostMiB}, }, }, catalogue.TemplateEvents: { @@ -236,9 +239,58 @@ func busEntryProblems(at string, e catalogue.BusStateEntry, terminal []string) [ if e.Template == catalogue.TemplateWorkQueue { problems = append(problems, workQueueProblems(at, e)...) } + if e.Template == catalogue.TemplateState && e.Name == approval.StateName { + problems = append(problems, approvalsStateProblems(at, e)...) + } return problems } +// approvalsMostMiB is the most an approvals state holds (design 47 G §677's 64 MiB); with none declared its bucket takes +// the default 64 MiB. +const approvalsMostMiB = 64 + +// approvalsStateProblems judges a holder's approvals state as §677 declares it (lead answers A-270, A-277): with key-ttl, +// since its bound keys `unanswered.` need it, a lifetime of 48 hours and at most 64 MiB. +func approvalsStateProblems(at string, e catalogue.BusStateEntry) []string { + var problems []string + var keyTTL bool + if json.Unmarshal(e.Options[catalogue.OptionLifetimePerKey], &keyTTL) != nil || !keyTTL { + problems = append(problems, fmt.Sprintf("%s: an approvals state is declared with %s true, since its bound keys "+ + "unanswered. need it (novox/hq design 47 G §677, A-270)", at, catalogue.OptionLifetimePerKey)) + } + want := int(approval.KeptFor.Seconds()) + if ttl, ok := intOption(e.Options, catalogue.OptionTimeToLive); !ok || ttl != want { + problems = append(problems, fmt.Sprintf("%s: an approvals state keeps a value %d seconds (48 hours), so its %s is "+ + "%d, not %s (novox/hq design 47 G §677, A-277)", at, want, catalogue.OptionTimeToLive, want, givenOr(e.Options, catalogue.OptionTimeToLive))) + } + if mib, ok := intOption(e.Options, catalogue.OptionCapMiB); ok && mib > approvalsMostMiB { + problems = append(problems, fmt.Sprintf("%s: an approvals state holds at most %d MiB, so its %s is at most %d, not %d "+ + "(novox/hq design 47 G §677, A-277)", at, approvalsMostMiB, catalogue.OptionCapMiB, approvalsMostMiB, mib)) + } + return problems +} + +// intOption is an option's whole number, false where it is not given or no number. +func intOption(options map[string]json.RawMessage, name string) (int, bool) { + raw, given := options[name] + if !given { + return 0, false + } + var n int + if json.Unmarshal(raw, &n) != nil { + return 0, false + } + return n, true +} + +// givenOr is an option's value as given, or "missing". +func givenOr(options map[string]json.RawMessage, name string) string { + if raw, given := options[name]; given { + return string(raw) + } + return "missing" +} + // settingsProblems judges the given settings against the rules: an unknown one is refused (a fixed setting among them), // a number outside its bound, a kind mismatch, and a required one missing. A bare stream's when-full and age are read // here too. diff --git a/modules/mesh-delivery-indexer/judge/busstate_test.go b/modules/mesh-delivery-indexer/judge/busstate_test.go index 8fa1d166..8616e425 100644 --- a/modules/mesh-delivery-indexer/judge/busstate_test.go +++ b/modules/mesh-delivery-indexer/judge/busstate_test.go @@ -62,6 +62,14 @@ func TestBusStateIsJudgedOnce(t *testing.T) { {"log age 3651", manifest(`{"name":"x","storage":"stream","template":"log","options":{"max-age-days":3651}}`), []string{"\"x\"", "\"max-age-days\" is 3651"}}, {"objects without a cap", manifest(`{"name":"x","storage":"object-store","template":"objects"}`), []string{"\"x\"", "\"max-mib\" is missing"}}, {"when full given as an option", manifest(`{"name":"x","storage":"stream","template":"log","options":{"when-full":"drop-oldest"}}`), []string{"\"x\"", "\"when-full\"", "fixed"}}, + // A-277: a state's max-mib is an option of its template; an approvals state is §677's (key-ttl, 48 hours, at most 64 MiB). + {"a state capped at 256 MiB", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"max-mib":256}}`), nil}, + {"a state cap over 8192", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"max-mib":8193}}`), []string{"\"x\"", "\"max-mib\" is 8193"}}, + {"an approvals state as §677 declares it", manifest(`{"name":"approvals","storage":"key-value","template":"state","options":{"key-ttl":true,"ttl-seconds":172800,"max-mib":64}}`), nil}, + {"an approvals state with the default cap", manifest(`{"name":"approvals","storage":"key-value","template":"state","options":{"key-ttl":true,"ttl-seconds":172800}}`), nil}, + {"an approvals state lacking key-ttl", manifest(`{"name":"approvals","storage":"key-value","template":"state","options":{"ttl-seconds":172800}}`), []string{"\"approvals\"", "key-ttl true"}}, + {"an approvals state kept a day", manifest(`{"name":"approvals","storage":"key-value","template":"state","options":{"key-ttl":true,"ttl-seconds":86400}}`), []string{"\"approvals\"", "ttl-seconds is 172800, not 86400"}}, + {"an approvals state over 64 MiB", manifest(`{"name":"approvals","storage":"key-value","template":"state","options":{"key-ttl":true,"ttl-seconds":172800,"max-mib":128}}`), []string{"\"approvals\"", "max-mib is at most 64, not 128"}}, {"another fixed setting given", manifest(`{"name":"x","storage":"key-value","template":"state","options":{"max-value-kib":1024}}`), []string{"\"x\"", "\"max-value-kib\" is not a setting of the template state"}}, {"a configuration beside a template", manifest(`{"name":"x","storage":"key-value","template":"state","configuration":{"max-mib":1}}`), []string{"\"x\"", "configuration"}}, {"a name used twice across storage types", manifest(