Files
mesh-delivery/modules/mesh-delivery-sdk/buildstep/stepoutput.go
T
jschoubben 386095bd53 A-290 B1: a step answer over the steps state's bound is kept as the object its key names
A step's key keeps its answer only within what the steps state holds per value (kvlimit.ModuleStateValueBytes less
the headers' room). Over it, finish keeps the answer with the holder's KeepOutput as the object output.<ask> in its
outputs store and the key names it in output-object: a check's whole answer, its result emptied; a run's output, the
key keeping the run answer without it. An answer that cannot be kept ends the key failed, saying why. The Puller says
step-ended from the end finish wrote, so the key and the event say one outcome: a run whose program exited other than
0 ends its key failed as well, its answer kept; step-ended names the object finish kept instead of putting it twice.
Step.Whole reads a key's object back through the seat's verb output on the step's node. The part build-step's Step
gains output-object, the verb output says it answers a step's answer; fixtures and the generated contracts follow.

Builds: design 47 G A-290, A-254, §467

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 16:44:07 +01:00

101 lines
4.0 KiB
Go

package buildstep
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
)
// OutputVerb is the verb of a build seat that answers a step's answer kept as an object (design 47 G A-254, A-290).
const OutputVerb = "output"
// AskVerb asks a seat verb as the runtime calls it (stdio.Ask): the key `seat:<seat>.<verb>[@<node>]` and the body in, the
// verb's answer out. A verb marked chunks is asked by the plain call, and the runtime hands the whole file in `file` (A-249).
type AskVerb func(key string, body any) (json.RawMessage, error)
// ReadOutput is the answer a build seat's step-ended carries (design 47 G A-254): its inline `output`, else the object
// `output-object` names, read through the verb `output` of the seat of language on the node that ran the ask. Every asker of a
// run reads the answer this way, whichever build seat ran it; none reads the seat's step state.
func (e StepEnded) ReadOutput(language string, ask AskVerb) ([]byte, error) {
if e.OutputObject == "" {
return []byte(e.Output), nil
}
return readObject(language, e.Node, e.Ask, e.OutputObject, ask)
}
// Whole is the step with its answer whole (design 47 G A-290): a step whose key names its answer as the object
// `output-object`, its value being over what the steps state holds, has it read through the verb `output` of the seat of
// language on the step's node, and put back where the key holds the answer itself: a run's (run true) as its run answer's
// output, any other's as its result. A step whose key holds its answer is answered as it is. Every reader of a step's answer
// reads it so, the checker and the builder alike (no step reads passed in one place and failed in another).
func (s Step) Whole(language string, run bool, ask AskVerb) (Step, error) {
if s.OutputObject == "" {
return s, nil
}
body, err := readObject(language, s.Node, s.ID, s.OutputObject, ask)
if err != nil {
return s, err
}
if !run {
if !json.Valid(body) {
return s, fmt.Errorf("buildstep: the answer %s of the ask %s is not JSON", s.OutputObject, s.ID)
}
s.Result, s.OutputObject = json.RawMessage(body), ""
return s, nil
}
var answer RunAnswer
if len(s.Result) > 0 {
if err := json.Unmarshal(s.Result, &answer); err != nil {
return s, fmt.Errorf("buildstep: the run answer of the ask %s is not one: %w", s.ID, err)
}
}
answer.Output = json.RawMessage(body)
raw, err := json.Marshal(answer)
if err != nil {
return s, fmt.Errorf("buildstep: the run answer of the ask %s with its output %s: %w", s.ID, s.OutputObject, err)
}
s.Result, s.OutputObject = raw, ""
return s, nil
}
// readObject reads the object a build seat's holder on node keeps of the ask askID through the seat's verb `output`.
func readObject(language, node, askID, object string, ask AskVerb) ([]byte, error) {
seat, err := BuildSeat(language)
if err != nil {
return nil, err
}
if ask == nil {
return nil, errors.New("buildstep: the answer is in the output object " + object + ", which this reader has no verb to read")
}
var call [16]byte
if _, err := rand.Read(call[:]); err != nil {
return nil, err
}
raw, err := ask("seat:"+seat+"."+OutputVerb+"@"+node, map[string]any{"ask": askID, "call": hex.EncodeToString(call[:])})
if err != nil {
return nil, fmt.Errorf("buildstep: asking %s for the answer %s of the ask %s: %w", seat, object, askID, err)
}
var got struct {
Ask string `json:"ask"`
File string `json:"file"`
Chunks int `json:"chunks"`
}
if err := json.Unmarshal(raw, &got); err != nil {
return nil, fmt.Errorf("buildstep: the verb output of %s answered no answer for %s: %w", seat, askID, err)
}
if got.Ask != askID {
return nil, fmt.Errorf("buildstep: the verb output of %s answered the ask %q, asked %s", seat, got.Ask, askID)
}
if got.File == "" {
return nil, fmt.Errorf("buildstep: %s on %s keeps no answer of the ask %s", seat, node, askID)
}
body, err := base64.StdEncoding.DecodeString(got.File)
if err != nil {
return nil, fmt.Errorf("buildstep: the answer of %s in no base64: %w", askID, err)
}
return body, nil
}