Files
mesh-delivery/sdk/composer/composer.go
T
jschoubben c3d47e4733 A composition at a node-engine head without its seat list fails (issue 1966)
When the checker's seat list is not read, or a head's definitions are not
taken, the composer at a node-engine member's head was handed no list,
judged no machine, and the composition passed with warnings. It now
fails, naming why (ADR 0635 §3), and no composer is asked. The check's
readers of the members' heads are its manifest validator and that
composer (ADR 0636 §4 insight 1). The composer's Request.Seats contract
says both the judge and the composer read the list, and a composer
handed none judges no machine (ADR 0636 §1).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 12:10:36 +01:00

214 lines
10 KiB
Go

// Package composer is the shape of a composition over a plain request (novox/hq ADR 0482 clause 4, issue 1621, as ADR
// 0593 §7 changes it): the request the command mesh-compose reads and the verdict it answers. The command and the
// composer that runs it are the node-engine's (mesh-host cmd/mesh-compose), built from its repository at a group's
// head, so a change to its composition code is judged by that code.
//
// **It reads its request and nothing else.** No bus, store, network, file or environment: the manifests come inside the
// request (raw, as their repository holds them), the rest of the mesh is a compose.Input, and the answer is a value.
// A manifest named only by its digest in the delivery indexer's bucket is filled before the run by Fill, by whoever
// holds the bus (the Go build module's compose job); Run refuses a digest left unfilled.
package composer
import (
"encoding/json"
"errors"
"fmt"
"slices"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Version is the version of a Request and of a Verdict; a reader refuses another.
const Version = 1
// Package is the import path of the command, which a compose job builds at a commit of the node-engine's repository
// (novox/hq ADR 0593 §7).
const Package = "github.com/novox/mesh-host/cmd/mesh-compose"
// The depths of a composition.
const (
// DepthResolve resolves each machine's set over the rest of the mesh (compose.Resolve): what the assigner judges
// while the facts snapshot is not on the bus (issue 1461).
DepthResolve = "resolve"
// DepthCompose composes each machine's whole declaration and runs the node-engine's validator over it
// (compose.Compose).
DepthCompose = "compose"
)
// Request is everything one run composes: the rest of the mesh, one or more cases of manifests composed over it, and
// the machines composed in each case.
type Request struct {
V int `json:"v"`
// Input is the composition's input without its catalogue (and without each node's kept manifests): the cases give
// those. A Catalogue or Kept it holds is ignored. It is the node-engine's composition input as JSON, which only the
// composer decodes (novox/hq ADR 0593 §7), so this shape imports no composition.
Input json.RawMessage `json:"input"`
// Cases are composed one after the other over Input, each with its own manifests; their names are unique.
Cases []Case `json:"cases"`
// Machines are the nodes composed in every case; none means the manifests are only read.
Machines []string `json:"machines,omitempty"`
// Depth is DepthResolve or DepthCompose; empty is DepthCompose.
Depth string `json:"depth,omitempty"`
// HoldingsFromClaims adds to Input's holdings every seat an assigned module claims, held on its node (the
// assigner's input, which has no holdings of record).
HoldingsFromClaims bool `json:"holdings_from_claims,omitempty"`
// Declarations asks each composed machine's declared paths (DepthCompose only).
Declarations bool `json:"declarations,omitempty"`
// Predefined is the list of predefined keys at the catalogue's root (predefined.json, novox/hq ADR 0348 §7). No
// longer read: the composer judges no manifest (novox/hq ADR 0578 §5), the indexer's judge does, and its verdicts
// come in each case's Verdicts. Kept so an asker that still sends it is read.
Predefined catalogue.PredefinedList `json:"predefined,omitempty"`
// Packages are the known packages a manifest's `packages` may name, each by its name in its own ecosystem (design 32:
// a Go package's go.mod module path, a TypeScript package's package.json name), for the indexer's judge (novox/hq ADR
// 0578 §5, ADR 0606 §4); the composer does not read them. Nil is no list at hand, and the judge judges no name; an
// empty list is a list, and every name is unknown to it.
Packages []string `json:"packages,omitzero"`
// Seats are the seat definitions the judge judges and the composer composes against, as the asker's seat list reader holds
// them (the definitions of the seat mesh-catalog, resolved), raw so that this package names no seat (ADR 0635 §4, ADR 0636
// §1). The judge and the composer read them. None is no list at hand: a composer handed none judges no machine (ADR 0636 §1).
Seats []json.RawMessage `json:"seats,omitempty"`
}
// Case is one catalogue composed: every module's manifest by name, and per node the manifests kept at the build it runs.
//
// Assignments are, by node, the assignments the case composes that node with in place of what Input says of it: a change's
// own assignments files, so a check composes a catalogue pull request's assignments (issue 1126, frombus.Placed); a null
// assignments leaves nothing placed on the node. A case without them composes Input's.
//
// Verdicts are, by module, as raw JSON (this package is seat-neutral, novox/hq ADR 0635 §4; the node-engine's composer decodes the
// mesh-catalog seat's manifest verdict, and Assignments the mesh-module-assigner seat's assignments), the indexer's judge's verdict on its manifest in Manifests (novox/hq ADR 0578 §5), put in by
// the assigner or the checker that runs the composer: a refused one is held, composed as its stub so what it offers
// still shapes the rest, and named in the case's verdict. A module without one is accepted (the mesh-catalog seat's ManifestVerdict, taken by its readers).
type Case struct {
Name string `json:"name"`
Manifests map[string]Manifest `json:"manifests"`
Verdicts map[string]json.RawMessage `json:"verdicts,omitempty"`
Kept map[string]map[string]Manifest `json:"kept,omitempty"`
Assignments map[string]json.RawMessage `json:"assignments,omitempty"`
}
// Manifest is a module's manifest as its repository holds it (Raw, the bytes), or the digest the delivery indexer keeps
// it under (Digest), which Fill replaces by the bytes. On the wire it is the manifest itself, or {"digest": "..."}.
type Manifest struct {
Raw json.RawMessage
Digest string
}
// MarshalJSON writes the raw manifest, or {"digest": ...} when only the digest is known.
func (m Manifest) MarshalJSON() ([]byte, error) {
if len(m.Raw) > 0 {
return m.Raw, nil
}
return json.Marshal(map[string]string{"digest": m.Digest})
}
// UnmarshalJSON reads {"digest": "..."} (an object with that one key) as a digest, and any other value as the raw
// manifest.
func (m *Manifest) UnmarshalJSON(raw []byte) error {
var probe map[string]json.RawMessage
if json.Unmarshal(raw, &probe) == nil && len(probe) == 1 {
if d, ok := probe["digest"]; ok {
var digest string
if err := json.Unmarshal(d, &digest); err != nil {
return fmt.Errorf("a manifest's digest is not a string: %w", err)
}
*m = Manifest{Digest: digest}
return nil
}
}
*m = Manifest{Raw: slices.Clone(raw)}
return nil
}
// Verdict is what one run answers: the mesh-sdk the command was built with, and each case's verdict in the order asked.
type Verdict struct {
V int `json:"v"`
SDK string `json:"sdk"`
Cases []CaseVerdict `json:"cases"`
}
// CaseVerdict is one case's: every manifest read or refused, every machine composed or not.
type CaseVerdict struct {
Name string `json:"name"`
Manifests map[string]ManifestRead `json:"manifests"`
Machines map[string]Machine `json:"machines,omitempty"`
}
// ManifestRead is one manifest as this mesh-sdk reads it: accepted, or refused with why: the indexer's verdict
// (Case.Verdicts) or bytes that do not decode. A field the decoder does not know is set aside, named in SetAside, and
// the manifest still accepted (novox/hq ADR 0578 §6).
type ManifestRead struct {
Accepted bool `json:"accepted"`
Refused string `json:"refused,omitempty"`
SetAside string `json:"set_aside,omitempty"`
}
// Machine is one machine composed: whether it composes (resolves, at DepthResolve), why not, the node-engine
// validator's problems and the composition's refusals (DepthCompose), and its declared paths when asked. NotJudged says,
// in words, why the composer did not judge the machine (an older node-engine's input that does not say how it composes,
// novox/hq ADR 0593 §9): the machine is then neither composed nor refused, and a reader decides by this field alone,
// never by the words of Reason.
type Machine struct {
Composes bool `json:"composes"`
NotJudged string `json:"not_judged,omitempty"`
Reason string `json:"reason,omitempty"`
Problems []string `json:"problems,omitempty"`
Refusals []string `json:"refusals,omitempty"`
Declared []Path `json:"declared,omitempty"`
}
// Path is one path a declaration puts on the machine, with its kind (the resource's type), owner, group and mode as
// declared, and the module it is of.
type Path struct {
Path string `json:"path"`
Kind string `json:"kind"`
Owner string `json:"owner,omitempty"`
Group string `json:"group,omitempty"`
Mode string `json:"mode,omitempty"`
Module string `json:"module,omitempty"`
}
// ErrUnfilled is a manifest named by a digest that nobody replaced by its bytes.
var ErrUnfilled = errors.New("composer: a manifest is named by its digest and was not filled")
// Decode reads a request; it refuses one that is not JSON or of another version.
func Decode(data []byte) (Request, error) {
var r Request
if err := json.Unmarshal(data, &r); err != nil {
return Request{}, fmt.Errorf("composer: the request is not JSON of its shape: %w", err)
}
if r.V != Version {
return Request{}, fmt.Errorf("composer: a request of version %d, this composer reads %d", r.V, Version)
}
return r, nil
}
// Fill replaces every manifest named by its digest by the bytes get answers for that digest; a digest get cannot answer
// stops it, named.
func Fill(r *Request, get func(digest string) ([]byte, error)) error {
fill := func(m map[string]Manifest) error {
for name, man := range m {
if len(man.Raw) > 0 {
continue
}
raw, err := get(man.Digest)
if err != nil {
return fmt.Errorf("composer: the manifest of %s (digest %s): %w", name, man.Digest, err)
}
m[name] = Manifest{Raw: raw}
}
return nil
}
for _, c := range r.Cases {
if err := fill(c.Manifests); err != nil {
return err
}
for _, kept := range c.Kept {
if err := fill(kept); err != nil {
return err
}
}
}
return nil
}