The indexer's judge names, with each verdict, the reader capabilities a manifest's own words, forms and seat names need, in its module value (ADR 0614 §4, issue 774)

Module.Needs is catalogue.ManifestNeeds of the manifest held: written by the merge that takes a changed manifest in, so
no reader takes it as needing nothing before the judge's pass, named again by the judge with the verdict, and carried
over by a write that leaves the digest, as the verdict is. The module value is the one the assigner and the checker
already read.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 23:22:08 +01:00
parent 007c982ae6
commit 477b3bfea6
5 changed files with 37 additions and 6 deletions
@@ -7,6 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/fingerprint"
@@ -95,6 +96,9 @@ func (ix *Indexer) applyMoving(ctx context.Context, f delivery.Forge, drop bool,
}
dirOf[name] = dir
next := delivery.Module{Repository: f.Repository, Commit: f.Merge, Src: ix.srcOf(f.Repository, f.Merge), Manifest: digests[dir], Path: pathOf(dir)}
// The reader capabilities a changed manifest needs are written with it, before the judge's pass names them with its
// verdict, so no reader takes a new manifest as needing nothing (ADR 0614 §4).
next.Needs, _ = catalogue.ManifestNeeds([]byte(raw))
if next.Manifest == "" {
next.None2 = "the manifest cannot be read: it cannot be kept as a value"
} else {
@@ -10,6 +10,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -413,6 +414,11 @@ func TestAMergeAddingAModuleDirectoryWritesItsValueWithNoBuildRecordAnywhere(t *
if m.FP2 == "" && m.None2 == "" {
t.Errorf("module = %+v, want a new-form fingerprint or its reason", m)
}
// ADR 0614 §4: the merge writes the reader capabilities its manifest needs, so no reader takes it as needing nothing
// before the judge's pass.
if want, _ := catalogue.ManifestNeeds([]byte(avahi)); len(m.Needs) == 0 || !slices.Equal(m.Needs, want) {
t.Errorf("module needs %v, want %v", m.Needs, want)
}
}
func TestAMergeWritesOnlyTheModulesItsManifestsName(t *testing.T) { // I1
@@ -365,8 +365,9 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
held := found && err == nil
if held && cur.Manifest == next.Manifest {
// The judge's verdict stands while the digest does (ADR 0578 §3, ADR 0579 §1): a recompute never writes a
// refused or outgrown module as accepted; only the judge changes it.
// refused or outgrown module as accepted; only the judge changes it. So do the reader capabilities it named.
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
next.Needs = cur.Needs
}
// The source revision at which FP2 last changed (ADR 0601 §1): this merge's when it moved it (0 until the
// source revision naming the merge is heard, which stamps it), else the one held.
@@ -401,6 +402,7 @@ func sameButWhere(cur, next delivery.Module) bool {
next = withV(next)
next.Commit, next.Src, next.FPSrc = cur.Commit, cur.Src, cur.FPSrc
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
next.Needs = cur.Needs
return reflect.DeepEqual(cur, next)
}
@@ -160,7 +160,9 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged,
}
}
}
if err := ix.carryVerdict(ctx, name, digests[name], v); err != nil {
// 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])
if err := ix.carryVerdict(ctx, name, digests[name], v, needs); err != nil {
ix.logf("judge: modules.%s does not carry its verdict: %v", name, err)
}
}
@@ -257,9 +259,10 @@ func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool {
}
// carryVerdict puts a refusal into the module's value, and takes one out once the manifest is accepted, while the value
// still names the digest judged; an accepted module's value carries nothing, so judging writes no value of a module the
// still names the digest judged, and the reader capabilities the manifest needs beside it (needs, ADR 0614 §4); an
// accepted module's value carries no verdict, so judging writes no value of a module the
// judge accepts (a reader takes a value without a verdict as accepted, delivery.TakeManifestVerdict).
func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v delivery.ManifestVerdict) error {
func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v delivery.ManifestVerdict, needs []string) error {
key, err := delivery.ModulesKey(name)
if err != nil {
return err
@@ -280,10 +283,11 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli
case v.Refused():
state = delivery.VerdictRefused
}
if cur.Verdict == state && slices.Equal(cur.Problems, problems) && slices.Equal(cur.Unknown, v.Unknown) && slices.Equal(cur.Warnings, v.Warnings) {
if cur.Verdict == state && slices.Equal(cur.Problems, problems) && slices.Equal(cur.Unknown, v.Unknown) && slices.Equal(cur.Warnings, v.Warnings) &&
slices.Equal(cur.Needs, needs) {
return nil, false, nil
}
cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings = state, problems, v.Unknown, v.Warnings
cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings, cur.Needs = state, problems, v.Unknown, v.Warnings, needs
if state == "" {
cur.Problems = nil
}
@@ -4,10 +4,12 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -113,6 +115,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// A merge's write of a module whose digest it leaves as it is carries the verdict over, and nothing is fresh; a changed
// digest is fresh, judged in full (review R1, R2).
w := &mergeWrite{}
oldNeeds := module("old").Needs
same := delivery.Module{FP2: "src2:" + strings.Repeat("c", 64), Manifest: changed, Repository: "novox/mesh-catalog", Commit: "c2"}
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil {
t.Fatal(err)
@@ -120,6 +123,9 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if m := module("old"); m.Verdict != delivery.VerdictRefused || len(m.Problems) == 0 || len(w.fresh) != 0 {
t.Fatalf("a merge rewrote a refused module as %+v, fresh %v", m, w.fresh)
}
if got := module("old").Needs; len(oldNeeds) == 0 || !slices.Equal(got, oldNeeds) {
t.Fatalf("a merge leaving the digest dropped the reader capabilities the judge named: %v, held %v", got, oldNeeds)
}
other := strings.Repeat("d", 64)
same.Manifest = other
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil || !w.fresh[other] {
@@ -137,6 +143,15 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatal(err)
}
}
// ADR 0614 §4: the judge names, with the verdict, the reader capabilities the manifest's own words need: the bus-state
// words, which an engine stating nothing (the baseline) lacks; a manifest of the baseline's words lacks none.
if needs := module("moved").Needs; !slices.Contains(needs, "word:bus-state.storage") ||
!slices.Contains(catalogue.MissingManifestReads(needs, nil), "word:bus-state") {
t.Fatalf("modules.moved needs %v", needs)
}
if needs := module("good").Needs; !slices.Contains(needs, "word:module") || len(catalogue.MissingManifestReads(needs, nil)) != 0 {
t.Fatalf("modules.good needs %v", needs)
}
if len(accepted) != 1 || accepted[0].Module != "moved" || accepted[0].Digest != moved || accepted[0].Judge != "judge-2" ||
len(accepted[0].BusState) != 2 || accepted[0].BusState[0].Name != "cache" || accepted[0].BusState[1].Template != "log" ||
string(accepted[0].BusState[0].Options["history"]) != "8" {