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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user