A modules value with no verdict yet holds only its module's users, and one module's value that does not read is a gap of that module, never of the whole list (ADR 0614 §8, issue 774)

The indexer writes a changed manifest's value before it judges it; the part now reads the indexer's verdicts.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-03 23:43:26 +01:00
parent ffdeb9771a
commit 9d0b3cafc5
5 changed files with 64 additions and 7 deletions
@@ -93,6 +93,8 @@ func TestTheGrantsPartWhole(t *testing.T) {
put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw})
put(grants.ReadModules, module, delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), Manifest: digest,
Repository: "novox/x", Commit: "c"})
verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: module, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()})
put(grants.ReadVerdicts, digest, json.RawMessage(verdict))
}
manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"}}`)
manifest("mod-c", `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["nats.done"]}`)
+20
View File
@@ -154,6 +154,12 @@ func (b *fakeBus) put(state, key string, v any) {
b.states[state] = map[string][]byte{}
}
b.states[state][key] = raw
// A modules value that carries no verdict is accepted by the indexer's verdict on its digest, as the indexer writes it;
// a row that wants the window before the judge deletes that key.
if m, ok := v.(delivery.Module); ok && state == grants.ReadModules && m.Verdict == "" && m.Manifest != "" {
verdict, _ := delivery.EncodeManifestVerdict(delivery.ManifestVerdict{Module: key, State: delivery.VerdictAccepted, Judge: "j", At: time.Now()})
b.put(grants.ReadVerdicts, m.Manifest, json.RawMessage(verdict))
}
}
func (b *fakeBus) Keys(state string) ([]string, error) {
@@ -2841,6 +2847,20 @@ func TestGrants(t *testing.T) {
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "the manifest of mod-r is unreadable") {
t.Errorf("the unreadable manifest was not said: %+v", last)
}
// The window before the judge: the indexer wrote a changed manifest's value and no verdict on its digest yet. Only that
// module's users stay (named unreadable); the others move.
b.manifestOf("mod-r", ctl)
delete(b.states[grants.ReadVerdicts], delivery.ManifestDigest([]byte(ctl)))
b.put(grants.ReadAccounts, "a.mod-x", map[string]any{"user": "a.mod-x", "hash": "$2a$10$gen11", "generation": 11, "fingerprint": "sha256:11"})
wasList, _, _ = p.Sealed()
res = p.Compose()
nowList, _, _ = p.Sealed()
if nowList.Digest == wasList.Digest || strings.Contains(fmt.Sprint(res.Sealed), "kept while a read") {
t.Errorf("a manifest with no verdict yet held the whole list: %+v", res.Sealed)
}
if last := said[len(said)-1]; last.State != asks.StateRaised || !strings.Contains(last.Summary, "no verdict on this manifest yet") {
t.Errorf("a manifest with no verdict yet was not said: %+v", last)
}
// Accepted again: the users compose from the manifest and the condition clears.
b.manifestOf("mod-r", ctl)
if p.Compose(); said[len(said)-1].State != asks.StateCleared {
@@ -21,7 +21,7 @@ type Watcher interface {
//
// And the seat mesh-enrolment's keys (ADR 0587 §4, §5): a key at live composes enrol.<node>, and its end, by the holder or by
// the bus's expiry, is a change of the key that takes the user out; no event is needed beside the watch.
var Followed = []string{ReadModules, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments,
var Followed = []string{ReadModules, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments,
ReadNodeLogins}
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
+40 -6
View File
@@ -25,10 +25,13 @@ import (
const (
ReadModules = "mesh-delivery-indexer.modules"
ReadManifests = "mesh-delivery-indexer.manifests"
ReadHave = "node-engine.have"
ReadResolved = "mesh-config.resolved"
ReadWants = "mesh-delivery-assigner.wants"
ReadBuilds = "mesh-delivery-builder.builds"
// ReadVerdicts is the indexer's verdicts, one key per manifest digest: a manifest it has not judged yet has none, and is
// held like a refused one (ADR 0614 §8).
ReadVerdicts = "mesh-delivery-indexer.verdicts"
ReadHave = "node-engine.have"
ReadResolved = "mesh-config.resolved"
ReadWants = "mesh-delivery-assigner.wants"
ReadBuilds = "mesh-delivery-builder.builds"
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
ReadAccounts = "mesh-vault.bus-accounts"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
@@ -44,7 +47,7 @@ const (
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
var Reads = []string{ReadModules, ReadManifests, ReadResolved, ReadHave, ReadWants, ReadBuilds,
var Reads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadBuilds,
ReadAccounts, ReadPlacement, ReadEnrolments, ReadNodeLogins}
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
@@ -336,7 +339,10 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
for _, module := range indexed {
raw, found, err := r.Get(ReadModules, module)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadModules + " " + module, Why: err.Error(), Failed: true})
// One module's value that does not read holds that module alone, as a refused manifest does (ADR 0614 §8).
g := Gap{Input: "manifest of " + module, Why: "its value in " + ReadModules + " does not read: " + err.Error()}
in.Refused = append(in.Refused, g)
in.Gaps = append(in.Gaps, g)
continue
}
if !found {
@@ -360,6 +366,10 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
// The indexer's refusal, or its wait for a judge that knows a field, carried in its value (ADR 0578 §3): held and named as a refused manifest.
if verdict := v.VerdictOf(module); verdict != nil && verdict.Held() {
err = errors.New(verdict.Why())
} else if verdict == nil {
// The indexer writes a changed manifest's value before it judges it, and a new digest has no verdict: until
// the verdict is written the manifest is held (ADR 0614 §8).
err = noVerdictYet(r, v.Manifest)
}
}
if err == nil {
@@ -651,3 +661,27 @@ func (in Inputs) declared() map[string]catalogue.Manifest {
}
return out
}
// noVerdictYet is nil when the indexer's verdicts state holds an accepted verdict on the manifest, and the reason it is held
// when it holds none yet, cannot be read, or refuses it.
func noVerdictYet(r Reader, digest string) error {
key, err := delivery.VerdictsKey(digest)
if err != nil {
return nil
}
raw, found, err := r.Get(ReadVerdicts, key)
if err != nil {
return fmt.Errorf("its verdict cannot be read: %w", err)
}
if !found {
return errors.New("the indexer has written no verdict on this manifest yet")
}
v, err := delivery.DecodeManifestVerdict(raw)
if err != nil {
return fmt.Errorf("its verdict does not read: %w", err)
}
if v.Held() {
return errors.New(v.Why())
}
return nil
}
+1
View File
@@ -8,6 +8,7 @@
"mesh-delivery-builder.builds",
"mesh-delivery-indexer.manifests",
"mesh-delivery-indexer.modules",
"mesh-delivery-indexer.verdicts",
"mesh-delivery-assigner.placement",
"node-engine.have",
"mesh-vault.bus-accounts",