From 9582e01eef5f329882ed94b806cf135b64eabd47 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 21:19:56 +0100 Subject: [PATCH] A kept data's form below 0 is refused where it is read, once for every kept kind Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/catalogue/busstate.go | 3 +++ go/catalogue/kept_form.go | 11 +++++++++++ go/catalogue/kept_form_test.go | 17 +++++++++++++++++ go/catalogue/logs.go | 3 +++ go/catalogue/objects.go | 3 +++ go/catalogue/state.go | 3 +++ go/catalogue/streams.go | 3 +++ 7 files changed, 43 insertions(+) diff --git a/go/catalogue/busstate.go b/go/catalogue/busstate.go index 4b96cbb..89274d9 100644 --- a/go/catalogue/busstate.go +++ b/go/catalogue/busstate.go @@ -179,6 +179,9 @@ func readBusStateEntry(raw json.RawMessage) (BusStateEntry, []string, error) { var e BusStateEntry err := strict(&e) if err == nil { + if err := e.KeptLayout.refuse(fmt.Sprintf("requires entry %q", e.Name)); err != nil { + return BusStateEntry{}, nil, err + } return e, aside, nil } unknown := asUnknownField(err) diff --git a/go/catalogue/kept_form.go b/go/catalogue/kept_form.go index 15a918a..a07f315 100644 --- a/go/catalogue/kept_form.go +++ b/go/catalogue/kept_form.go @@ -1,6 +1,7 @@ package catalogue import ( + "fmt" "sort" ) @@ -21,6 +22,16 @@ type KeptLayout struct { CatchUp string `json:"catch-up,omitempty"` } +// refuse is the one reading of a layout's form every kept kind's decoder calls: a form is a whole number from 0, and one +// below it is refused here, naming the declaration (`what` is its kind and name, such as `state "seen"`). +func (k KeptLayout) refuse(what string) error { + if k.Form < 0 { + return fmt.Errorf("%s: its form is %d; a form is a whole number from 0, which its holder raises when it changes how "+ + "it writes it", what, k.Form) + } + return nil +} + // The kinds of kept data, by the template each is on (ADR 0595 ยง2): the five a restore point is taken of. const ( KeptKindState = "state" diff --git a/go/catalogue/kept_form_test.go b/go/catalogue/kept_form_test.go index bbf0afa..88a2960 100644 --- a/go/catalogue/kept_form_test.go +++ b/go/catalogue/kept_form_test.go @@ -2,6 +2,7 @@ package catalogue import ( "reflect" + "strings" "testing" ) @@ -70,3 +71,19 @@ func TestAKeptDataSaysItsFormAndItsCatchUp(t *testing.T) { t.Errorf("a manifest applied over itself changes %+v", got) } } + +// A form below 0 is refused where it is read, in every kept kind's declaration, naming it. +func TestAKeptDataFormBelowZeroIsRefused(t *testing.T) { + for _, c := range []struct{ name, raw, says string }{ + {"state", `"state":[{"name":"seen","form":-3}]`, `state "seen": its form is -3`}, + {"log", `"logs":[{"name":"acts","form":-1}]`, `log "acts": its form is -1`}, + {"stream", `"streams":[{"name":"forge","events":["merged"],"form":-2}]`, `stream "forge": its form is -2`}, + {"objects", `"objects":[{"name":"revisions","max-mib":64,"form":-1}]`, `object bucket "revisions": its form is -1`}, + {"requires", `"requires":[{"name":"holds","storage":"key-value","template":"state","form":-1}]`, `requires entry "holds": its form is -1`}, + } { + _, err := decodeForTest([]byte(`{"module":"x","version":"1",` + c.raw + `}`)) + if err == nil || !strings.Contains(err.Error(), c.says) { + t.Errorf("%s: read as %v, want a refusal holding %q", c.name, err, c.says) + } + } +} diff --git a/go/catalogue/logs.go b/go/catalogue/logs.go index b949549..82b1d50 100644 --- a/go/catalogue/logs.go +++ b/go/catalogue/logs.go @@ -55,6 +55,9 @@ func (l *LogDeclaration) UnmarshalJSON(raw []byte) error { if err := dec.Decode(&full); err != nil { return fmt.Errorf("a log is either a name or {name, max-mib, max-age-days, form, catch-up}: %w", typedUnknown(err)) } + if err := full.KeptLayout.refuse(fmt.Sprintf("log %q", full.Name)); err != nil { + return err + } l.Name = full.Name l.KeptLayout = full.KeptLayout l.MaxMiB = 0 diff --git a/go/catalogue/objects.go b/go/catalogue/objects.go index fc50d00..8373121 100644 --- a/go/catalogue/objects.go +++ b/go/catalogue/objects.go @@ -49,6 +49,9 @@ func (o *ObjectsDeclaration) UnmarshalJSON(raw []byte) error { if err := dec.Decode(&p); err != nil { return fmt.Errorf("an object bucket is {name, max-mib, grant, form, catch-up}: %w", typedUnknown(err)) } + if err := p.KeptLayout.refuse(fmt.Sprintf("object bucket %q", p.Name)); err != nil { + return err + } *o = ObjectsDeclaration(p) return nil } diff --git a/go/catalogue/state.go b/go/catalogue/state.go index bbaaf0e..2bca54e 100644 --- a/go/catalogue/state.go +++ b/go/catalogue/state.go @@ -52,6 +52,9 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { if err := dec.Decode(&full); err != nil { return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}: %w", typedUnknown(err)) } + if err := full.KeptLayout.refuse(fmt.Sprintf("state %q", full.Name)); err != nil { + return err + } *s = StateDeclaration(full) return nil } diff --git a/go/catalogue/streams.go b/go/catalogue/streams.go index 286bfdc..b9f9565 100644 --- a/go/catalogue/streams.go +++ b/go/catalogue/streams.go @@ -88,6 +88,9 @@ func (s *StreamDeclaration) UnmarshalJSON(raw []byte) error { return fmt.Errorf("a stream is {name, events, max-age-days, max-mib, grant, retention, max-deliveries, "+ "duplicates, form, catch-up}: %w", typedUnknown(err)) } + if err := p.KeptLayout.refuse(fmt.Sprintf("stream %q", p.Name)); err != nil { + return err + } *s = StreamDeclaration(p) return nil }