Files
mesh-sdk/go/delivery/verdicts.go
T
jschoubben 19c204273b Let a verdict warn of what a stricter judge found in a manifest it accepted before (ADR 0579 §1)
Issue 1585: ManifestVerdict and the modules value carry Warnings; the finding manifest-outgrown names it.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:43:10 +01:00

157 lines
6.4 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"slices"
"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"
// ManifestOutgrown is the finding of a manifest the indexer accepted before and a stricter judge finds problems in
// (novox/hq ADR 0579 §1): it stays accepted, its problems are the verdict's Warnings, and the watchman raises it as a
// warning until the manifest is mended.
ManifestOutgrown = "manifest-outgrown"
)
// 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"`
// Warnings are the problems a stricter judge found in a manifest it accepted before, digest unchanged (novox/hq ADR
// 0579 §1): the verdict stays accepted.
Warnings []string `json:"warnings,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 {
return v.Module == o.Module && v.State == o.State && slices.Equal(v.Problems, o.Problems) && slices.Equal(v.Warnings, o.Warnings)
}
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)
}
if len(v.Warnings) > 0 {
return fmt.Errorf("delivery: the verdict on %s refuses its manifest and warns: a refusal names problems", 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}
}