Merge branch 'feat/47-G-maxmib6' into feat/47-G-join8

This commit is contained in:
2026-11-12 00:11:47 +01:00
4 changed files with 43 additions and 3 deletions
+4
View File
@@ -29,6 +29,10 @@ func TestTheBusNamesAgree(t *testing.T) {
if got := (Principal{Kind: KindLedger, Node: "a", Module: "m"}).Username(); got != "ledger.a.m" {
t.Fatalf("a ledger account is named %q", got)
}
// G §469: a state's bucket is capped at its max-mib; naming none is unbounded.
if (Bucket{}).MaxBytes() != -1 || (Bucket{MaxMiB: 2}).MaxBytes() != 2<<20 {
t.Fatal("a bucket's cap is not its max-mib in bytes")
}
// A seat's family is the shared events stream filtered to its subjects: a definition says the stream's age.
for _, st := range MeshStreams() {
if st.Name == EventsStream && int64(st.MaxAge) != seatdef.SharedEventsMaxAgeDays*24*60*60 {
+10
View File
@@ -24,6 +24,16 @@ type Bucket struct {
// KeyTTL says the bucket allows a time to live per key (novox/hq ADR 0433 §1): its owner writes each key with its
// own, and the server writes a delete marker, kept KeyTTLMarkerTTL, when it ends.
KeyTTL bool
// MaxMiB is the bucket's cap in MiB (design 47 G §469); zero is no cap.
MaxMiB int
}
// MaxBytes is the bucket's cap in bytes as the bus's maximum size: -1, unbounded, where it names none.
func (b Bucket) MaxBytes() int64 {
if b.MaxMiB <= 0 {
return -1
}
return int64(b.MaxMiB) * 1024 * 1024
}
// BucketName is the bucket a module's state lives in: the module and the local name joined by an
+13 -3
View File
@@ -35,11 +35,14 @@ type StateDeclaration struct {
// the tool runner's account, read it and never write it (the lead's ruling, 2026-11-04: nats' warrant spend record
// `spent`). The judge's rule and the grants follow from it.
ReadOnly bool `json:"read-only,omitempty"`
// MaxMiB is the bucket's cap in MiB, between ObjectsLeastMiB and ObjectsMostMiB, as an old objects section's
// is (design 47 G §469, §695); zero is no cap, as on main.
MaxMiB int `json:"max-mib,omitempty"`
// KeptLayout is the layout its holder says it writes: its form and its catch-up tool (design 47).
KeptLayout
}
// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}.
// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}.
func (s *StateDeclaration) UnmarshalJSON(raw []byte) error {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) > 0 && trimmed[0] == '"' {
@@ -47,21 +50,28 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error {
}
type plain StateDeclaration
var full plain
var said struct {
MaxMiB *int `json:"max-mib"`
}
_ = json.Unmarshal(trimmed, &said)
dec := json.NewDecoder(bytes.NewReader(trimmed))
dec.DisallowUnknownFields()
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))
return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}: %w", typedUnknown(err))
}
if err := full.KeptLayout.refuse(fmt.Sprintf("state %q", full.Name)); err != nil {
return err
}
if said.MaxMiB != nil && (*said.MaxMiB < ObjectsLeastMiB || *said.MaxMiB > ObjectsMostMiB) {
return fmt.Errorf("state %q: max-mib is between %d and %d, not %d", full.Name, ObjectsLeastMiB, ObjectsMostMiB, *said.MaxMiB)
}
*s = StateDeclaration(full)
return nil
}
// MarshalJSON writes back the short form when there is nothing else to say.
func (s StateDeclaration) MarshalJSON() ([]byte, error) {
if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.KeptLayout == (KeptLayout{}) {
if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.MaxMiB == 0 && s.KeptLayout == (KeptLayout{}) {
return json.Marshal(s.Name)
}
type plain StateDeclaration
+16
View File
@@ -46,6 +46,22 @@ func TestAStateMayHaveKeysThatExpireOneByOne(t *testing.T) {
if string(out) != `[{"name":"pulse","key-ttl":true},"stuck",{"name":"seen","history":5},{"name":"spent","read-only":true}]` {
t.Fatalf("written back as %s", out)
}
// G §469, §695: an old state entry takes max-mib, written back, bounded as an objects section's.
capped, err := decodeForTest([]byte(`{"module":"x","version":"1","state":[{"name":"a","max-mib":256},"b"]}`))
if err != nil || capped.State[0].MaxMiB != 256 || capped.State[1].MaxMiB != 0 {
t.Fatalf("max-mib read as %+v, %v", capped.State, err)
}
if out, _ := json.Marshal(capped.State); string(out) != `[{"name":"a","max-mib":256},"b"]` {
t.Fatalf("max-mib written back as %s", out)
}
for _, bad := range []string{"0", "-1", "8193"} {
if _, err := decodeForTest([]byte(`{"module":"x","version":"1","state":[{"name":"a","max-mib":` + bad + `}]}`)); err == nil {
t.Errorf("max-mib %s was read", bad)
}
}
if _, err := decodeForTest([]byte(`{"module":"x","version":"1","state":[{"name":"a","max-mib":8192}]}`)); err != nil {
t.Errorf("max-mib 8192 refused: %v", err)
}
if _, err := decodeForTest([]byte(`{"module":"nats","version":"1","state":[{"name":"spent","read-only":true,"read-only":false}]}`)); err == nil ||
!strings.Contains(err.Error(), "the field state[0].read-only is given twice") {
t.Fatalf("read-only given twice: %v", err)