From a98b6ba77e89f14c53c3d9552e89a2d92df770f5 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 13 Nov 2026 20:48:04 +0100 Subject: [PATCH] S6: the bus keeper treats a none-yet catalog half as not yet viewed PlanCatalog plans nothing, deletes no plans key and orphans no entry while the catalog half lists no module before any was seen; the first revision with entries plans as before. Builds: design 47 G A-367 (G-22-lead-answers-2.md), W22/Q4 of start-waits.md Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/nats/internal/keeper/keeper.go | 12 +++++- modules/nats/internal/keeper/keeper_test.go | 42 +++++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/modules/nats/internal/keeper/keeper.go b/modules/nats/internal/keeper/keeper.go index 3b1af6d94..68788bed4 100644 --- a/modules/nats/internal/keeper/keeper.go +++ b/modules/nats/internal/keeper/keeper.go @@ -456,7 +456,8 @@ func (k *Keeper) statusWritten(s Status, err error) { // watched branch's newest revision is applied (Heard), a module of the new form that sends gets its sent store, a drift // between branches is said once, and a module in no watched branch's newest revision has its plans key deleted, its // entries then orphans. Before every watched branch has its first view it plans nothing, and says what it waits for in -// its status; the first plan checks what is held. +// its status, and an answer listing no module before one has listed any is the same (none-yet); the first plan checks what +// is held. func (k *Keeper) PlanCatalog() error { at := k.now() view, err := k.Catalog() @@ -466,6 +467,15 @@ func (k *Keeper) PlanCatalog() error { } k.mu.Lock() first := !k.viewed + // A catalog half with no module in it before any has been seen is a trunk with no revision yet (`none-yet`, design 47 + // G A-367): not yet viewed, so nothing is planned, no plan deleted and no entry made an orphan for want of knowledge. + if first && len(view.Modules) == 0 { + k.mu.Unlock() + k.setStatus(func(s *Status) { + s.FirstView.Asked, s.FirstView.Error = at, "the catalog half lists no module yet (none-yet): nothing is planned or orphaned until a revision lists one" + }) + return nil + } k.viewed = true k.mu.Unlock() var revision uint64 diff --git a/modules/nats/internal/keeper/keeper_test.go b/modules/nats/internal/keeper/keeper_test.go index b7c2aa305..500eb3d04 100644 --- a/modules/nats/internal/keeper/keeper_test.go +++ b/modules/nats/internal/keeper/keeper_test.go @@ -450,6 +450,9 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) { if len(bus.created) != 0 { t.Fatalf("created %v before the first view", bus.created) } + k.Catalog = func() (CatalogView, error) { + return CatalogView{Modules: []CatalogModule{{Accepted: Accepted{Module: "m", Digest: "d1", Revision: 3, BusState: []catalogue.BusStateEntry{drafts}}}}}, nil + } if err := k.PlanCatalog(); err != nil { t.Fatal(err) } @@ -541,6 +544,37 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) { t.Errorf("old sections %d: 8 MiB counted", k.Status().OldBytes) } }}, + // A-367: a trunk with no revision yet (none-yet) is not yet viewed: nothing is planned, deleted or orphaned. + {"a none-yet catalog half plans nothing and orphans nothing until a module is listed", func(t *testing.T, k *Keeper, bus *fakeBus, out *said) { + k.Heard(Accepted{Module: "m", Revision: 9, BusState: []catalogue.BusStateEntry{drafts}}) + k.Catalog = func() (CatalogView, error) { + return CatalogView{Modules: []CatalogModule{{Accepted: Accepted{Module: "m", Revision: 9, BusState: []catalogue.BusStateEntry{drafts}}}}}, nil + } + _ = k.PlanCatalog() + k.mu.Lock() + k.viewed = false + k.mu.Unlock() + k.Catalog = func() (CatalogView, error) { return CatalogView{}, nil } + if err := k.PlanCatalog(); err != nil { + t.Fatal(err) + } + if _, ok := k.planned("m"); !ok { + t.Error("m's plan is deleted by a none-yet view") + } + if a := k.Held()["m"]; a.Made["drafts"].Orphaned { + t.Errorf("held %+v: drafts orphaned by a none-yet view", a) + } + if k.Viewed() || !strings.Contains(k.Status().FirstView.Error, "none-yet") { + t.Errorf("viewed %v, status %+v: none-yet is not a first view and says so", k.Viewed(), k.Status().FirstView) + } + k.Catalog = func() (CatalogView, error) { + return CatalogView{Modules: []CatalogModule{{Accepted: Accepted{Module: "m", Revision: 10, BusState: []catalogue.BusStateEntry{drafts}}}}}, nil + } + _ = k.PlanCatalog() + if !k.Viewed() || k.Status().FirstView.Error != "" { + t.Error("the first revision with entries is viewed and plans as before") + } + }}, // ยง538: a module in no watched branch's newest revision has its plans key deleted, its entries then orphans. {"a module no watched branch lists loses its plan, its entries orphans", func(t *testing.T, k *Keeper, bus *fakeBus, out *said) { k.Catalog = func() (CatalogView, error) { @@ -1240,9 +1274,11 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) { } { t.Run(row.name, func(t *testing.T) { k, bus, _, out := newKeeper(CatalogView{}) - if err := k.PlanCatalog(); err != nil { - t.Fatal(err) - } + // The rows start after the first view: an empty answer is none-yet (A-367), so the view is marked answered here. + k.mu.Lock() + k.viewed = true + k.mu.Unlock() + k.setStatus(func(s *Status) { s.FirstView.Asked, s.FirstView.Answered = k.now(), k.now() }) row.run(t, k, bus, out) }) }