From 104909817b7014e57dd4162e4fc4471fe6b0a3f1 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 13:15:00 +0100 Subject: [PATCH] indexer: say manifest-accepted for a merged manifest a pull request's judgement already accepted (issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pull request's candidate is judged before its merge and its verdict written, said nowhere. At the merge the digest becomes the module's trunk manifest, but the merge's judging pass finds the same verdict, writes nothing and so said nothing: the bus module never heard the new manifest, and the grants part, which follows the verdicts on manifest-accepted, held the module as having no verdict and wrote no grant for its new fingerprint (claude-licence-manager, three times on 2026-11-12). The merge's pass now says a fresh trunk manifest's accepted verdict from the verdict held, with its revision. Builds: design 25 bus-state §1, §12.2 (the indexer says manifest-accepted for every newly accepted manifest; the grants part follows the verdicts on it) Builds: design 25 bus-state L1 (hq ff30ac16, the lead's answer: a merged candidate's manifest is newly accepted). --- .../internal/index/verdicts.go | 26 +++++++++++++++++++ .../internal/index/verdicts_test.go | 16 ++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index 24ce8159..ccb67181 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -245,6 +245,12 @@ func (ix *Indexer) judgeOverHeld(ctx context.Context, fresh map[string]bool, ove a := acceptedOf(digests[name], v, revision, raws[name]) _ = ix.sayAccepted(ctx, a, acceptedEventID(a)) } + } else if over == nil && fresh[digests[name]] && !meshcatalogclient.VerdictHeld(v) { + // A manifest a merge just made its module's trunk manifest is newly accepted there, even when a pull request's + // judgement wrote its verdict before the merge and said it nowhere (a candidate's verdict is not said): it is + // said now from the verdict held, with its revision, so the bus module and the grants part hear it (design 25 + // bus-state §1; issue 1966: claude-licence-manager's new builds waited for a grant nothing wrote). + ix.sayHeld(ctx, digests[name], raws[name]) } // The reader capabilities the manifest's own words, forms and seat names need, named with the verdict (ADR 0614 §4). needs, _ := catalogue.ManifestNeeds(raws[name]) @@ -262,6 +268,26 @@ func (ix *Indexer) judgeOverHeld(ctx context.Context, fresh map[string]bool, ove return out, nil } +// sayHeld says manifest-accepted for the trunk's manifest of digest from the accepted verdict the verdicts state holds on +// it, with that verdict's revision; a verdict that cannot be read, or that refuses or waits, says nothing. +func (ix *Indexer) sayHeld(ctx context.Context, digest string, raw []byte) { + key, err := meshcatalogclient.VerdictsKey(digest) + if err != nil { + return + } + value, revision, found, err := ix.Verdicts.Get(ctx, key) + if err != nil || !found { + ix.logf("judge: the verdict on %s cannot be read, so its manifest-accepted is not said: %v", digest, err) + return + } + v, err := meshcatalogclient.DecodeVerdict(value) + if err != nil || meshcatalogclient.VerdictHeld(v) { + return + } + a := acceptedOf(digest, v, revision, raw) + _ = ix.sayAccepted(ctx, a, acceptedEventID(a)) +} + // JudgeAtStart is JudgeAll of every manifest held, none fresh, as the indexer starts (a judge that changed judges every // one again); one that fails is judged again at the next message or source revision heard (kickJudge). Then every other // digest of the verdicts state that another judge refused or held waiting (one of a candidate, or one back on the trunk diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 09d6cf52..80be7d72 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -185,8 +185,9 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { moved := hold("moved", `{"module":"moved","version":"1","requires":[`+ `{"name":"cache","storage":"key-value","template":"state","options":{"history":8}},`+ `{"name":"audit","storage":"stream","template":"log","options":{"max-mib":64}}]}`) - for pass := 0; pass < 2; pass++ { - if _, err := ix.JudgeAll(ctx, map[string]bool{moved: true}); err != nil { + // The merge's pass, then a later one that knows nothing of the merge. + for _, fresh := range []map[string]bool{{moved: true}, nil} { + if _, err := ix.JudgeAll(ctx, fresh); err != nil { t.Fatal(err) } } @@ -235,6 +236,17 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if _, err := ix.judgeOver(ctx, nil, "bydigest", candidate); err != nil || accepted[len(accepted)-1].Digest != byDigest { t.Fatalf("a candidate's verdict was said: %v %+v", err, accepted[len(accepted)-1]) } + // Issue 1966 (design 25 bus-state §1): the candidate merged, its verdict unchanged, is newly accepted on the trunk and + // said at the merge's pass, from the verdict held and with its revision, though the pass writes no verdict. + if hold("bydigest", `{"module":"bydigest","version":"2"}`) != candidate { + t.Fatal("the merged manifest is not the candidate's") + } + if j, err := ix.JudgeAll(ctx, map[string]bool{candidate: true}); err != nil || j.Written != 0 { + t.Fatalf("the merge's pass: %+v %v", j, err) + } + if _, rev, _, _ := ix.Verdicts.Get(ctx, candidate); accepted[len(accepted)-1].Digest != candidate || accepted[len(accepted)-1].Revision != int64(rev) { + t.Fatalf("the merged candidate's manifest was not said accepted with its verdict's revision %d: last said %+v", rev, accepted[len(accepted)-1]) + } // DLV-I9b: a failed publish is published again; one failing twice writes the finding naming the module, which the next // judging pass says again and clears. ix.Findings = newMem()