Files
mesh-sdk/go/delivery/compose_candidate.go
T
jschoubben 42ac1a9a0a mesh-sdk's node-engine names are the seat's generated contract, kept deprecated for their importers, with their checks
The seat is defined whole in mesh-contracts (ADR 0613 §3), so mesh-sdk takes its names from there (ADR 0613 §5):
EngineSeat and EngineEvents are nodeengine's, EngineComposeCandidateVerb is nodeengine.VerbComposeCandidate, and
ComposeCandidateAsk, ComposeCandidateAnswer and ModuleComposed are aliases of nodeengine's ComposeCandidateInput,
ComposeCandidateAnswer and Module. The encode and decode checks stay here over those types until the seat has a client
package (ADR 0625 §4). For importers: a module's fingerprint is Fp, and the answer's Input is a JSON object
(map[string]any), not raw bytes. compose-candidate's input is the boolean the engine and the checker read; the seat
set's test takes that correction, and the whole seat's added verbs as da653ce reads them. EngineSeat leaves
testdata/seat-names.txt.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 12:40:03 +01:00

196 lines
9.6 KiB
Go

package delivery
import (
"bytes"
"encoding/json"
"fmt"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
)
// A composition that does not exist yet is asked of the node-engine (novox/hq ADR 0593 §7): the seat node-engine's verb
// compose-candidate, served by each node's engine alone, composes a check's candidate in shadow with the node's own
// sealed values and the indexer's manifest verdicts, applies and writes nothing, and answers within the asker's bound.
// The checker asks every placed node in parallel and makes the check's composition verdict from the answers; a node
// that does not answer within the bound is named "not composed on that node: no answer" and the check passes with that
// warning (§9). This is the contract only: the composing is the node-engine's own.
const (
// EngineComposeCandidateVerb is the verb's name on the seat node-engine (catalogue.EngineSeat).
//
// Deprecated: the seat's contract names it, nodeengine.VerbComposeCandidate; kept for its importers.
EngineComposeCandidateVerb = nodeengine.VerbComposeCandidate
// ComposeAnswerBound is the default of the checker's setting compose-answer-bound: how long it waits for a node's
// answer (ADR 0593 §7, §9).
ComposeAnswerBound = 60 * time.Second
// ComposeNoAnswer is the words a check's verdict names a node with that did not answer within the bound (ADR 0593 §9).
ComposeNoAnswer = "not composed on that node: no answer"
// ComposeCandidateComposed and ComposeCandidateRefused are an answer's verdict: no module and not the node's own composition is
// refused by the candidate, or at least one is (or the node could not compose with it at all, Refused).
ComposeCandidateComposed = "composed"
ComposeCandidateRefused = "refused"
// ComposeCandidateUnready is the verdict of a node that cannot compose yet (its shadow not ready, its agent
// starting): no module is named, Refused says why in words, and the checker takes it as a node that did not answer
// (ComposeNoAnswer, ADR 0593 §9), deciding by this verdict and never by the words.
ComposeCandidateUnready = "unready"
// ComposeCandidateNotJudged is the verdict for a node the checker's sandbox composed and could not judge (an older
// node-engine's input that does not say how it composes, novox/hq ADR 0593 §9): no module is named, Refused says why in
// words, and the checker names the node with them and passes the check with that warning, never silent and never a
// failure of the change. The checker decides by this verdict, never by the words (ADR 0601 §4's kind: a contract is a
// field, not a phrase).
ComposeCandidateNotJudged = "not-judged"
// The states of one module's composition with the candidate: composed (Digest set), refused by the candidate
// (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict.
ModuleComposedState = "composed"
ModuleRefusedState = "refused"
ModuleHeldState = "held"
)
// The verb's ask and answer are the seat's contract, generated in mesh-contracts (seats/node-engine): mesh-sdk keeps
// these names for their importers and the checks below over them, until the seat has a client package (ADR 0625 §4).
type (
// ComposeCandidateAsk is the verb's input: Candidate is the key of the indexer's candidate (CompositionKey,
// `<escaped group>.<heads hash>`); the engine reads that candidate itself and lays its modules over its placement.
// Input asks the engine to answer too the composition input it composed the candidate from (its own node's scope, the
// catalogue left out as composer.Request.Input says): the checker puts it into the request a build module's composer
// runs over, for a group that changes the node-engine's composition code (ADR 0593 §7).
//
// Deprecated: the seat's contract is nodeengine.ComposeCandidateInput; kept for its importers.
ComposeCandidateAsk = nodeengine.ComposeCandidateInput
// ComposeCandidateAnswer is the verb's answer: the node, the candidate, the verdict, every module placed on the node
// sorted by name, the node's own composition (Own, Module "node"), why the node could not compose with the candidate
// at all (Refused: Modules is then empty and Verdict refused, or unready when it cannot compose yet), how long the
// engine took in milliseconds, and, when asked (ComposeCandidateAsk.Input), the composition input it composed from.
//
// Deprecated: the seat's contract is nodeengine.ComposeCandidateAnswer; kept for its importers.
ComposeCandidateAnswer = nodeengine.ComposeCandidateAnswer
// ModuleComposed is one module's composition with the candidate: the fingerprint it was composed at (the
// candidate's FP2 for a module the candidate touches, else its newest done build as composed-<node> holds it), its
// state, the composition's digest when composed (ComposedDigestOf), whether the candidate changes it from the one
// composed without it (the checker's "what the change reaches on this node"), and why, for a refused or held one.
//
// Deprecated: the seat's contract is nodeengine.Module; kept for its importers.
ModuleComposed = nodeengine.Module
)
func checkComposeAsk(a ComposeCandidateAsk) error {
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
return fmt.Errorf("delivery: a compose-candidate ask names no candidate: %w", err)
}
return nil
}
func checkComposedModule(m ModuleComposed, node string) error {
if m.Module == "" {
return fmt.Errorf("delivery: a module's composition on %s names no module", node)
}
switch m.State {
case ModuleComposedState:
if m.Digest == "" {
return fmt.Errorf("delivery: %s is composed on %s and names no digest", m.Module, node)
}
case ModuleRefusedState, ModuleHeldState:
if m.Words == "" {
return fmt.Errorf("delivery: %s is %s on %s and says no why", m.Module, m.State, node)
}
default:
return fmt.Errorf("delivery: %s on %s is %q, neither %s, %s nor %s", m.Module, node, m.State,
ModuleComposedState, ModuleRefusedState, ModuleHeldState)
}
return nil
}
func checkComposeAnswer(a ComposeCandidateAnswer) error {
if err := checkName("node", a.Node); err != nil {
return err
}
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
return err
}
if a.Took < 0 {
return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took)
}
if (a.Verdict == ComposeCandidateUnready || a.Verdict == ComposeCandidateNotJudged) && a.Refused == "" {
return fmt.Errorf("delivery: %s's answer is %s and says no why", a.Node, a.Verdict)
}
if a.Refused != "" {
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready && a.Verdict != ComposeCandidateNotJudged {
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s, %s nor %s",
a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready, ComposeCandidateNotJudged)
}
return nil
}
refused := false
for i, m := range append([]ModuleComposed{a.Own}, a.Modules...) {
if err := checkComposedModule(m, a.Node); err != nil {
return err
}
if i > 1 && a.Modules[i-2].Module >= m.Module {
return fmt.Errorf("delivery: the modules of %s's answer are not sorted and each once (%s after %s)", a.Node, m.Module, a.Modules[i-2].Module)
}
refused = refused || m.State == ModuleRefusedState
}
switch {
case a.Own.Module != nodeWord:
return fmt.Errorf("delivery: the node's own composition in %s's answer is named %q, not %q", a.Node, a.Own.Module, nodeWord)
case refused && a.Verdict != ComposeCandidateRefused:
return fmt.Errorf("delivery: %s's answer refuses a module and its verdict is %q", a.Node, a.Verdict)
case !refused && a.Verdict != ComposeCandidateComposed:
return fmt.Errorf("delivery: %s's answer refuses no module and its verdict is %q", a.Node, a.Verdict)
}
return nil
}
// strictDecode reads data into v, refusing an unknown key.
func strictDecode(data []byte, v any, what string) error {
d := json.NewDecoder(bytes.NewReader(data))
d.DisallowUnknownFields()
if err := d.Decode(v); err != nil {
return fmt.Errorf("delivery: %s does not fit its shape: %w", what, err)
}
return nil
}
// EncodeComposeCandidateAsk is a as JSON; it refuses an ask naming no candidate key.
func EncodeComposeCandidateAsk(a ComposeCandidateAsk) ([]byte, error) {
if err := checkComposeAsk(a); err != nil {
return nil, err
}
return json.Marshal(a)
}
// DecodeComposeCandidateAsk reads an ask strictly; it refuses an unknown key and what EncodeComposeCandidateAsk refuses.
func DecodeComposeCandidateAsk(data []byte) (ComposeCandidateAsk, error) {
var a ComposeCandidateAsk
if err := strictDecode(data, &a, "a compose-candidate ask"); err != nil {
return ComposeCandidateAsk{}, err
}
return a, checkComposeAsk(a)
}
// EncodeComposeCandidateAnswer is a as JSON at Version; it refuses an unknown state, a composed module without a digest,
// a refused or held one without words, a refusal of the whole naming modules, and a verdict the modules do not give.
func EncodeComposeCandidateAnswer(a ComposeCandidateAnswer) ([]byte, error) {
if err := checkComposeAnswer(a); err != nil {
return nil, err
}
a.V = Version
return json.Marshal(a)
}
// DecodeComposeCandidateAnswer reads an answer strictly; it refuses an unknown version (ErrUnknownVersion), an unknown
// key and what EncodeComposeCandidateAnswer refuses.
func DecodeComposeCandidateAnswer(data []byte) (ComposeCandidateAnswer, error) {
if err := checkVersion(data); err != nil {
return ComposeCandidateAnswer{}, err
}
var a ComposeCandidateAnswer
if err := strictDecode(data, &a, "a compose-candidate answer"); err != nil {
return ComposeCandidateAnswer{}, err
}
return a, checkComposeAnswer(a)
}