delivery, catalogue: the node-engine's compose-candidate verb, compositions carrying their plan or refusal, the kept import form and a build's imports on its key, so the engine alone composes and the indexer switches form without a rebuild (ADR 0593 §4, §6, §7)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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).
|
||||
EngineComposeCandidateVerb = "compose-candidate"
|
||||
// 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"
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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).
|
||||
type ComposeCandidateAsk struct {
|
||||
Candidate string `json:"candidate"`
|
||||
Input bool `json:"input,omitempty"`
|
||||
}
|
||||
|
||||
// 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), how long the engine took in milliseconds, and, when asked
|
||||
// (ComposeCandidateAsk.Input), the composition input it composed from.
|
||||
type ComposeCandidateAnswer struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Candidate string `json:"candidate"`
|
||||
Verdict string `json:"verdict"`
|
||||
Modules []ModuleComposed `json:"modules"`
|
||||
Own ModuleComposed `json:"own"`
|
||||
Refused string `json:"refused,omitempty"`
|
||||
Took int64 `json:"took"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type ModuleComposed struct {
|
||||
Module string `json:"module"`
|
||||
FP string `json:"fp,omitempty"`
|
||||
State string `json:"state"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Changed bool `json:"changed,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
func (a ComposeCandidateAsk) check() error {
|
||||
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
|
||||
return fmt.Errorf("delivery: a compose-candidate ask names no candidate: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m ModuleComposed) check(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 (a ComposeCandidateAnswer) check() 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.Refused != "" {
|
||||
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused {
|
||||
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or no refusal", a.Node, a.Refused)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
refused := false
|
||||
for i, m := range append([]ModuleComposed{a.Own}, a.Modules...) {
|
||||
if err := m.check(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 := a.check(); 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, a.check()
|
||||
}
|
||||
|
||||
// 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 := a.check(); 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, a.check()
|
||||
}
|
||||
Reference in New Issue
Block a user