Give the indexer's verdicts on manifests a contract readers take (ADR 0578 §1, §3)

Issue 1585, S3: state mesh-delivery-indexer_verdicts keyed by manifest digest, event manifest-refused, a modules
value carrying its verdict, and TakeManifestVerdict reading a missing verdict as accepted. Contract only, no rules.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 01:57:36 +01:00
parent ccb367e68c
commit a343557554
3 changed files with 238 additions and 0 deletions
+21
View File
@@ -415,6 +415,19 @@ type Module struct {
// so the checker tells whether a change touches an artifact's build source without reading a manifest (the lead's
// decision L8 of R4e). Absent for an artifact that names none, and in a value written before.
ArtifactPaths map[string][]string `json:"paths,omitempty"`
// Verdict is the indexer's verdict on Manifest, VerdictAccepted or VerdictRefused, and Problems each problem of a
// refusal, as the verdicts state holds them (novox/hq ADR 0578 §1): a reader of modules takes it without a read of
// that state. Empty in a value written before the indexer judged, which a reader takes as accepted (TakeManifestVerdict).
Verdict string `json:"verdict,omitempty"`
Problems []string `json:"problems,omitempty"`
}
// VerdictOf is the verdict a modules value carries, or nil when it carries none (taken as accepted, TakeManifestVerdict).
func (m Module) VerdictOf(module string) *ManifestVerdict {
if m.Verdict == "" {
return nil
}
return &ManifestVerdict{Module: module, State: m.Verdict, Problems: m.Problems}
}
// RootPath is Module.Path of a module whose manifest is its repository's root.
@@ -503,6 +516,14 @@ func (m Module) check() error {
return err
}
}
switch {
case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused:
return fmt.Errorf("delivery: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused)
case m.Verdict != "" && m.Manifest == "":
return fmt.Errorf("delivery: a module carries a verdict and no manifest digest")
case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0):
return fmt.Errorf("delivery: a module's problems are named only with a refusal, and a refusal names them")
}
return checkNewForm(m.FP2, m.Manifest)
}
+152
View File
@@ -0,0 +1,152 @@
package delivery
import (
"encoding/json"
"fmt"
"time"
)
// The verdicts on manifests (novox/hq ADR 0578 §1, §3): the indexer judges every manifest it takes in once, with its own
// judge, and writes the verdict in its own state; every other part decodes a manifest leniently (catalogue.Decode) and
// takes the verdict from here. This is the contract only: what a verdict holds, its key and its event. The rules that
// make a verdict are the judge's, a package of mesh-delivery the indexer and the checker use, and no reader runs them.
const (
// VerdictsBucket is the indexer's state of verdicts, one key per manifest digest (VerdictsKey).
VerdictsBucket = IndexerModule + "_verdicts"
// VerdictsHistory is the revisions the verdicts state keeps per key: a verdict is written again only when the judge
// that made it changed and the verdict with it.
VerdictsHistory = 5
// VerdictAccepted is the state of a manifest the judge found no problem in.
VerdictAccepted = "accepted"
// VerdictRefused is the state of a manifest the judge found a problem in; Problems says each.
VerdictRefused = "refused"
// ManifestRefusedEvent is the indexer's event saying a manifest was refused (ManifestRefused), so the watchman
// raises it as a condition beside the indexer's finding.
ManifestRefusedEvent = "manifest-refused"
)
// ManifestVerdict is the value of a verdicts key: the module whose manifest it is, accepted or refused, every problem the judge
// found (none when accepted), the judge that made it (the indexer's build fingerprint) and when.
type ManifestVerdict struct {
V int `json:"v"`
Module string `json:"module"`
State string `json:"state"`
Problems []string `json:"problems,omitempty"`
Judge string `json:"judge"`
At time.Time `json:"at"`
}
// Refused is whether v refuses its manifest.
func (v ManifestVerdict) Refused() bool { return v.State == VerdictRefused }
// Same is whether v and o say the same of a manifest: the module, the state and the problems, whoever judged and when.
// The indexer writes a verdict only when it is not the same as the one it holds.
func (v ManifestVerdict) Same(o ManifestVerdict) bool {
if v.Module != o.Module || v.State != o.State || len(v.Problems) != len(o.Problems) {
return false
}
for i := range v.Problems {
if v.Problems[i] != o.Problems[i] {
return false
}
}
return true
}
func (v ManifestVerdict) check() error {
if err := checkName("module", v.Module); err != nil {
return err
}
switch v.State {
case VerdictAccepted:
if len(v.Problems) > 0 {
return fmt.Errorf("delivery: the verdict on %s accepts its manifest and names problems", v.Module)
}
case VerdictRefused:
if len(v.Problems) == 0 {
return fmt.Errorf("delivery: the verdict on %s refuses its manifest and names no problem", v.Module)
}
default:
return fmt.Errorf("delivery: the verdict on %s is %q, neither %s nor %s", v.Module, v.State, VerdictAccepted, VerdictRefused)
}
if v.Judge == "" {
return fmt.Errorf("delivery: the verdict on %s names no judge", v.Module)
}
return nil
}
// VerdictsKey is the key, in VerdictsBucket, of the verdict on a manifest: its digest (ManifestDigest), as in the
// manifests bucket.
func VerdictsKey(digest string) (string, error) {
if !isHex64(digest) {
return "", fmt.Errorf("delivery: %q is not a manifest digest, 64 lowercase hex digits", digest)
}
return digest, nil
}
// EncodeManifestVerdict is v as JSON at Version; it refuses a verdict without a module or a judge, a state other than accepted
// or refused, a refusal naming no problem and an acceptance naming one.
func EncodeManifestVerdict(v ManifestVerdict) ([]byte, error) {
if err := v.check(); err != nil {
return nil, err
}
v.V = Version
return json.Marshal(v)
}
// DecodeManifestVerdict reads a verdict; it refuses an unknown version (ErrUnknownVersion) and what EncodeManifestVerdict refuses.
func DecodeManifestVerdict(data []byte) (ManifestVerdict, error) {
var v ManifestVerdict
if err := decode(data, &v); err != nil {
return ManifestVerdict{}, err
}
if err := v.check(); err != nil {
return ManifestVerdict{}, err
}
return v, nil
}
// TakeManifestVerdict is how a reader takes the verdict on a manifest (ADR 0578 §3): refused, with its problems, only when the
// indexer wrote a refusal. A missing verdict (nil) is taken as accepted, because every manifest the indexer holds before
// it has judged it was taken in under the judges that ran until then; that lasts until the indexer's first pass after its
// delivery has written every verdict. A reader holds a refused module and names it with Why, and reads the rest.
func TakeManifestVerdict(v *ManifestVerdict) (refused bool, problems []string) {
if v == nil || !v.Refused() {
return false, nil
}
return true, v.Problems
}
// Why is a refusal in a reader's words: the module, that the indexer's judge refused its manifest, and each problem.
func (v ManifestVerdict) Why() string {
if !v.Refused() {
return ""
}
judge := "the indexer's judge"
if v.Judge != "" {
judge += " (" + v.Judge + ")"
}
why := fmt.Sprintf("%s refused the manifest of %s:", judge, v.Module)
for _, p := range v.Problems {
why += " " + p + ";"
}
return why[:len(why)-1]
}
// ManifestRefused is the body of the indexer's event ManifestRefusedEvent: the module, the digest of its manifest, the
// judge and each problem, as its verdict holds them.
type ManifestRefused struct {
V int `json:"v"`
Module string `json:"module"`
Digest string `json:"digest"`
Judge string `json:"judge"`
Problems []string `json:"problems"`
}
// RefusedOf is the event body saying v, the verdict on the manifest of digest.
func RefusedOf(digest string, v ManifestVerdict) ManifestRefused {
return ManifestRefused{V: Version, Module: v.Module, Digest: digest, Judge: v.Judge, Problems: v.Problems}
}
+65
View File
@@ -0,0 +1,65 @@
package delivery
import (
"strings"
"testing"
"time"
)
// A verdict on a manifest round-trips, refuses what it cannot be, and a reader takes a missing one as accepted (novox/hq
// ADR 0578 §1, §3); a modules value carries it.
func TestAManifestVerdictIsTheContractReadersTake(t *testing.T) {
at := time.Date(2026, 11, 1, 2, 0, 0, 0, time.UTC)
refused := ManifestVerdict{Module: "nats", State: VerdictRefused, Problems: []string{"a", "b"}, Judge: "src2:x", At: at}
data, err := EncodeManifestVerdict(refused)
if err != nil {
t.Fatal(err)
}
back, err := DecodeManifestVerdict(data)
if err != nil || !back.Same(refused) || back.Judge != "src2:x" || !back.At.Equal(at) {
t.Fatalf("round trip: %+v %v", back, err)
}
if !strings.Contains(back.Why(), "refused the manifest of nats: a; b") {
t.Fatalf("why: %q", back.Why())
}
for name, bad := range map[string]ManifestVerdict{
"no module": {State: VerdictAccepted, Judge: "j"},
"no judge": {Module: "m", State: VerdictAccepted},
"another state": {Module: "m", State: "maybe", Judge: "j"},
"refused, no problem": {Module: "m", State: VerdictRefused, Judge: "j"},
"accepted, a problem": {Module: "m", State: VerdictAccepted, Problems: []string{"x"}, Judge: "j"},
} {
if _, err := EncodeManifestVerdict(bad); err == nil {
t.Errorf("%s: encoded", name)
}
}
if refusedTaken, _ := TakeManifestVerdict(nil); refusedTaken {
t.Fatal("a missing verdict was taken as a refusal")
}
if r, p := TakeManifestVerdict(&refused); !r || len(p) != 2 {
t.Fatalf("a refusal was not taken: %v %v", r, p)
}
if k, err := VerdictsKey(strings.Repeat("a", 64)); err != nil || k != strings.Repeat("a", 64) {
t.Fatalf("key: %q %v", k, err)
}
if _, err := VerdictsKey("nats"); err == nil {
t.Fatal("a key that is no digest")
}
m := Module{FP2: "src2:" + strings.Repeat("b", 64), Manifest: strings.Repeat("c", 64), Repository: "novox/x",
Commit: "c", Verdict: VerdictRefused, Problems: []string{"p"}}
data, err = EncodeModule(m)
if err != nil {
t.Fatal(err)
}
got, err := DecodeModule(data)
if err != nil {
t.Fatal(err)
}
if r, p := TakeManifestVerdict(got.VerdictOf("x")); !r || p[0] != "p" {
t.Fatalf("the module's verdict: %+v", got)
}
m.Problems = nil
if _, err := EncodeModule(m); err == nil {
t.Fatal("a module refused naming no problem was encoded")
}
}