Serve node-engine.compose-candidate from the node's shadow (H4)

ADR 0593 §7: a check's composition is the checker's, made from each
placed node's answer, since only a node can compose with its own sealed
values. The node composes the indexer's candidate in shadow on a copy of
its shadow's inputs, the candidate's modules laid over the indexer's
with their manifest verdicts, and answers per module composed (changed
or not), refused or held, and its own composition's verdict, within the
answer bound; it applies, writes and hands nothing. With input it also
answers the composition input, for the sandbox path.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 18:33:58 +01:00
parent 4f51393b72
commit 3fa41dfe1a
7 changed files with 468 additions and 0 deletions
+15
View File
@@ -45,6 +45,8 @@ const (
// ActPackages reads the packages this node's compositions declare, and with write writes its key again (novox/hq
// ADR 0565 §6, §15).
ActPackages = "packages"
// ActComposeCandidate composes a check's candidate on this node in shadow, read-only (novox/hq ADR 0593 §7).
ActComposeCandidate = "compose-candidate"
)
// The states of an act.
@@ -143,6 +145,10 @@ type HandActs struct {
// Packages answers the node's key of the engine's state packages beside the controller's mirror's key and their
// difference, and with write writes the key again from what the engine composed (the verb packages, ADR 0565 §6, §15).
Packages func(write bool) (any, error)
// ComposeCandidate composes a check's candidate (the indexer's candidate key) on this node in shadow and answers per
// module, applying and writing nothing; with input, also the composition input it composed from (the verb
// compose-candidate, ADR 0593 §7).
ComposeCandidate func(candidate string, input bool) (any, error)
// Say says one of the seat's events, asked, done or refused; nil says nothing.
Say func(event string, body any)
Now func() time.Time
@@ -215,6 +221,15 @@ func (h *HandActs) Call(c EngineCall) (any, error) {
write = b
}
return h.Packages(write)
case ActComposeCandidate:
if h.ComposeCandidate == nil {
return nil, errors.New("this engine composes no candidate yet: its node agent has not started")
}
input := arg("input") == "true"
if b, ok := c.Args["input"].(bool); ok {
input = b
}
return h.ComposeCandidate(arg("candidate"), input)
case "acts":
if id := arg("id"); id != "" {
return h.Act(id)