A-277: the manifest validator reads a state's max-mib and judges an approvals state as §677 declares it

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
This commit is contained in:
2026-11-12 11:42:16 +01:00
parent db3f07d277
commit 8f7fd39e21
2 changed files with 60 additions and 0 deletions
@@ -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.<call>` 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.<call> 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.
@@ -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(