From 273da7535bd30018ffe17864d749fad0d2412f15 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:07:57 +0100 Subject: [PATCH] DLV-R1: a verdict below the picture's revision asks the verdicts' first view again, unless the picture holds it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0615 §5: a gap in revisions asks the first view once more. The indexer's verdict events carry no mark of when its verdicts were made, so verdicts made again show only as revisions starting low, which the grants part passed over as already held. A redelivery of a verdict the picture holds for its manifest is still passed over, so a consumer's backlog after a restart asks nothing. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- modules/mesh-grants/grants_test.go | 10 ++++++++++ .../mesh-grants/internal/grants/firstviews.go | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 716be76e..7b0aa40d 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -2444,6 +2444,16 @@ func TestGrants(t *testing.T) { if v := verdictOf(); !v.Refused() || !slices.Equal(v.Problems, []string{"p"}) || verdicts.Status().Asks != asked { t.Errorf("manifest-refused, then a revision already held: %+v, asks %d (was %d)", v, verdicts.Status().Asks, asked) } + // A revision below the picture's (ADR 0615 §5; the verdicts made again restart low): the same verdict again is a + // redelivery, passed over; another verdict asks the first view once more. + heard(delivery.ManifestRefusedEvent, delivery.ManifestRefused{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Problems: []string{"p"}, Revision: 3}) + if verdicts.Status().Asks != asked { + t.Errorf("a held verdict redelivered at a lower revision asked: asks %d (was %d)", verdicts.Status().Asks, asked) + } + heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j", Revision: 2}) + waitFor(t, func() bool { return verdicts.Status().Asks > asked }) + waitFor(t, func() bool { s := verdicts.Status(); return !s.Answered.Before(s.Asked) }) + asked = verdicts.Status().Asks heard(delivery.ManifestAcceptedEvent, delivery.ManifestAccepted{V: 1, Module: "mod-z", Digest: digest, Judge: "j"}) waitFor(t, func() bool { return verdicts.Status().Asks > asked }) asked = verdicts.Status().Asks diff --git a/modules/mesh-grants/internal/grants/firstviews.go b/modules/mesh-grants/internal/grants/firstviews.go index 4a6d017c..e7240e04 100644 --- a/modules/mesh-grants/internal/grants/firstviews.go +++ b/modules/mesh-grants/internal/grants/firstviews.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strings" "sync" @@ -259,6 +260,18 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf } return gap(fmt.Sprintf("the indexer's %s of %s at revision %d names no manifest", event, verdict.Module, revision)) } + // A revision below the picture's is a gap (ADR 0615 §5): the verdict events carry no mark of when the indexer's verdicts + // were made, so one made again shows only as revisions starting low. A redelivery of a verdict the picture already + // holds for its manifest is passed over, so a consumer's backlog after a restart asks nothing. + if revision < held.Revision { + if raw, found := f.Get(key); found { + if was, err := delivery.DecodeManifestVerdict(raw); err == nil && sameVerdict(was, verdict) { + return nil + } + } + return gap(fmt.Sprintf("the indexer's %s of %s at revision %d, below the picture's %d: its verdicts were made again, or this verdict was missed", + event, verdict.Module, revision, held.Revision)) + } value, err := json.Marshal(verdict) if err != nil { return err @@ -270,6 +283,11 @@ func (v *Views) HearVerdict(ctx context.Context, event string, data []byte, logf return nil } +// sameVerdict says two verdicts of one manifest say the same: its module, accepted or refused, the judge and the problems. +func sameVerdict(a, b delivery.ManifestVerdict) bool { + return a.Module == b.Module && a.State == b.State && a.Judge == b.Judge && slices.Equal(a.Problems, b.Problems) +} + // unansweredCondition is FirstViewUnanswered for one first view, raised or cleared. func unansweredCondition(fv FirstView, s views.Status, raised bool) asks.Condition { c := asks.Condition{Key: FirstViewUnanswered + "." + fv.Owner + "." + fv.State, Subject: asks.Scope{Scope: "module", ID: Module}}