From 57b1be1bd56cc44812fce78909bac8c1fbd75828 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 10:38:11 +0100 Subject: [PATCH] D-3: a kept event's stream is SEAT__, bounded by its keep object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keep is an object (max-age-days, max-mib, last-per-subject; 90 days and 16 MiB by default, 0 no age limit); the string form and the message-level bounds go; an event with a keep has no dot in its name. D-2 (step pipeline, 30 min default; on kind:image) was built in the reader release. Builds: design 47 G §260, §331, §396, §397, §203, §266, §324 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/seatdef/seatdef.go | 60 ++++++++++++++++++-------------------- go/seatdef/seatdef_test.go | 37 +++++++++++++++++------ 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index d2838992..143657a5 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -244,29 +244,22 @@ type Message struct { // the grants part grants a node's account alone (ADR 0363 §2). Subject string `json:"subject,omitempty"` // Keep says how an event is kept when not as the family keeps the seat's others, in a stream of its own named - // KeptStream: KeepLastPerSubject, the last message of each subject and no age limit (design 47: the node-engine's - // running.., so the last build of every module on every node stays on the bus however long ago it - // was said, and a durable consumer starting from the last message per subject holds them all); or KeepStream, - // every message within MaxAgeDays and MaxMiB. + // KeptStream (design 47 G §396): its age, its cap and whether it keeps the last message of each subject only (the + // node-engine's running.., so the last build of every module on every node stays on the bus however + // long ago it was said). An event with a Keep has no dot in its name. Keep *Keep `json:"keep,omitempty"` // Callers says who may send an accepted kind, as a verb's (design 47 G §338). Callers *Callers `json:"callers,omitempty"` // Stream is the stream an accepted kind rides, StreamCheck or StreamDelivery; its subject is `.`, and // the same kind name may ride more than one stream (design 47 G §415). Stream string `json:"stream,omitempty"` - // MaxAgeDays and MaxMiB bound an event kept KeepStream: how long a message is kept and how much the stream holds - // (design 47 *What the gate owes the forge*: the stream `owed`, 7 days and at most 64 MiB). Given with KeepStream - // alone. - MaxAgeDays int `json:"max-age-days,omitempty"` - MaxMiB int `json:"max-mib,omitempty"` } -// The ways an event is kept in a stream of its own, KeptStream, instead of its seat's family. +// What an event's keep says where it says less (design 47 G §396, §331). const ( - // KeepLastPerSubject keeps the last message of each subject, with no age limit. - KeepLastPerSubject = "last-per-subject" - // KeepStream keeps every message for MaxAgeDays, at most MaxMiB in all. - KeepStream = "stream" + // KeepDefaultAgeDays and KeepDefaultMiB are an event's age and cap when its keep names none. + KeepDefaultAgeDays = 90 + KeepDefaultMiB = 16 ) // KeptStream is the stream an event with Keep is kept in: `SEAT__` (design 47 G §260, §396). @@ -275,14 +268,26 @@ func KeptStream(seat string, m Message) string { return KeptStreamPrefix + seat // KeptStreamPrefix begins the name of every kept event's stream. const KeptStreamPrefix = "SEAT_" -// legacyMode is the string form of Keep, empty for none or for the object form. -func (m Message) legacyMode() string { - if m.Keep == nil { - return "" +// KeptAgeDays is how long a kept event's stream keeps a message: the keep's age, 90 days when it names none, and 0 +// for no age limit. +func (m Message) KeptAgeDays() int { + if m.Keep == nil || m.Keep.MaxAgeDays == nil { + return KeepDefaultAgeDays } - return m.Keep.Mode + return *m.Keep.MaxAgeDays } +// KeptMiB is the most a kept event's stream holds: the keep's cap, 16 MiB when it names none. +func (m Message) KeptMiB() int { + if m.Keep == nil || m.Keep.MaxMiB == nil { + return KeepDefaultMiB + } + return *m.Keep.MaxMiB +} + +// KeptLastPerSubject says the stream keeps only the last message of each subject. +func (m Message) KeptLastPerSubject() bool { return m.Keep != nil && m.Keep.LastPerSubject } + // SubjectPattern is the event's subject after the seat's event prefix with its parts between angle brackets: the // name itself when the definition gives no subject. func (m Message) SubjectPattern() string { @@ -833,26 +838,19 @@ func checkMessages(field string, ms []Message, whole bool) Problems { case m.Stream != "" && m.Stream != StreamCheck && m.Stream != StreamDelivery: ps = append(ps, Problem{where + ".stream", fmt.Sprintf("%q is not %s or %s", m.Stream, StreamCheck, StreamDelivery)}) } - mode := m.legacyMode() switch { case m.Keep != nil && field != "events": ps = append(ps, Problem{where + ".keep", "only an event is kept"}) - case m.Keep != nil && m.Keep.Object: + case m.Keep != nil && !m.Keep.Object: + ps = append(ps, Problem{where + ".keep", fmt.Sprintf("%q is the old form: keep is an object of max-age-days, max-mib and last-per-subject", m.Keep.Mode)}) + case m.Keep != nil: ps = append(ps, checkKeepObject(where+".keep", *m.Keep)...) - if m.MaxAgeDays != 0 || m.MaxMiB != 0 { - ps = append(ps, Problem{where + ".max-age-days", "an event whose keep is an object gives its max-age-days and max-mib there"}) + if strings.Contains(m.Name, ".") { + ps = append(ps, Problem{where + ".name", "an event with a keep has no dot in its name: its stream is named from it"}) } if m.Keep.LastPerSubject && len(m.Parts()) == 0 && !m.ByCaller { ps = append(ps, Problem{where + ".keep", "an event of one subject kept last per subject is one message: give it a subject with parts"}) } - case mode != "" && mode != KeepLastPerSubject && mode != KeepStream: - ps = append(ps, Problem{where + ".keep", fmt.Sprintf("%q is not %s or %s", mode, KeepLastPerSubject, KeepStream)}) - case mode == KeepLastPerSubject && len(m.Parts()) == 0 && !m.ByCaller: - ps = append(ps, Problem{where + ".keep", "an event of one subject kept last per subject is one message: give it a subject with parts"}) - case mode == KeepStream && (m.MaxAgeDays <= 0 || m.MaxMiB <= 0): - ps = append(ps, Problem{where + ".keep", "an event kept in a stream of its own gives its max-age-days and max-mib"}) - case mode != KeepStream && (m.MaxAgeDays != 0 || m.MaxMiB != 0): - ps = append(ps, Problem{where + ".max-age-days", "only an event kept as a stream of its own gives max-age-days and max-mib"}) } if m.Callers != nil && field != "accepts" { ps = append(ps, Problem{where + ".callers", "only an accepted kind carries callers"}) diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index 206db19f..e68e2969 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -2,6 +2,7 @@ package seatdef import ( "errors" + "fmt" "strings" "testing" ) @@ -64,17 +65,18 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { {"a subject on an event named by its caller", strings.Replace(whole, `"name":"created"`, `"name":"owed","by-caller":true,"subject":"owed."`, 1), "carries no subject of its own"}, {"an event kept last per subject", strings.Replace(whole, `"name":"created"`, - `"name":"running","subject":"running..","keep":"last-per-subject"`, 1), ""}, - {"an event kept otherwise", strings.Replace(whole, `"name":"created"`, - `"name":"running","subject":"running.","keep":"forever"`, 1), `events[0].keep: "forever" is not last-per-subject or stream`}, + `"name":"running","subject":"running..","keep":{"last-per-subject":true,"max-age-days":0}`, 1), ""}, + {"an event kept in the old form", strings.Replace(whole, `"name":"created"`, + `"name":"running","subject":"running.","keep":"last-per-subject"`, 1), `events[0].keep: "last-per-subject" is the old form`}, {"an event kept in a stream of its own, bounded", strings.Replace(whole, `"name":"created"`, - `"name":"owed","subject":"owed..","keep":"stream","max-age-days":7,"max-mib":64`, 1), ""}, - {"an event kept in a stream of its own, unbounded", strings.Replace(whole, `"name":"created"`, - `"name":"owed","subject":"owed..","keep":"stream","max-age-days":7`, 1), "gives its max-age-days and max-mib"}, - {"a bound on an event kept by its family", strings.Replace(whole, `"name":"created"`, - `"name":"owed","max-mib":64`, 1), "events[0].max-age-days: only an event kept as a stream"}, + `"name":"owed","subject":"owed..","keep":{"max-age-days":7,"max-mib":64}`, 1), ""}, + {"an event kept with a dot in its name", strings.Replace(whole, `"name":"created"`, + `"name":"pull.updated","keep":{}`, 1), "events[0].name: an event with a keep has no dot"}, + {"an event kept by its family may have a dot", strings.Replace(whole, `"name":"created"`, `"name":"pull.updated"`, 1), ""}, + {"a bound beside no keep", strings.Replace(whole, `"name":"created"`, + `"name":"owed","max-mib":64`, 1), "max-mib: a field the format does not know"}, {"an event of one subject kept last per subject", strings.Replace(whole, `"name":"created"`, - `"name":"running","keep":"last-per-subject"`, 1), "give it a subject with parts"}, + `"name":"running","keep":{"last-per-subject":true}`, 1), "give it a subject with parts"}, {"an accept with dotted words", strings.Replace(whole, `"events":`, `"accepts":[{"name":"run.now","body":{}}],"events":`, 1), "accepts[0].name"}, {"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`}, @@ -300,5 +302,22 @@ func TestAVerbWithoutCallersTakesItsClassDefault(t *testing.T) { if got := strings.Join(c.verb.EffectiveCallers(), " "); got != c.want { t.Errorf("%+v is open to %q, want %q", c.verb, got, c.want) } + } } + +// Design 47 G §260, §396, §331: a kept event's stream is named from the seat and the event, 90 days and 16 MiB unless +// the keep says otherwise, and 0 is no age limit. +func TestAKeptEventsStreamIsNamedAndBoundedByItsKeep(t *testing.T) { + d, err := Parse([]byte(`{"format":1,"seat":"mesh-delivery","scope":"mesh","grain":"seat-list","domain":"Delivery","decision":"x", + "events":[{"name":"owed","keep":{"max-age-days":7,"max-mib":64}},{"name":"toolchain-agreed","keep":{}}, + {"name":"versions-filled","keep":{"max-age-days":0,"max-mib":1}}]}`)) + if err != nil { + t.Fatal(err) + } + for i, want := range []string{"SEAT_mesh-delivery_owed 7 64", "SEAT_mesh-delivery_toolchain-agreed 90 16", "SEAT_mesh-delivery_versions-filled 0 1"} { + m := d.Events[i] + if got := fmt.Sprint(KeptStream(d.Seat, m), " ", m.KeptAgeDays(), " ", m.KeptMiB()); got != want { + t.Errorf("%s: %s, want %s", m.Name, got, want) + } + } }