Files
mesh-host/internal/compose/resolution/seat_contributions.go
T
jschoubben d68e241724 Take the catalogue's composition rules into the node-engine
ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk
carries no composition rule. The catalogue functions only the
node-engine's composition reaches (Resolve, Resolution's Compose,
Declaration, DataItems, LeftOut, Rules and the rest, the settings,
placeholder and template refusals, the rendering helpers) are copied
with their tests into internal/compose/resolution; a method of a
mesh-sdk shape becomes a function taking it first. The composition
calls its own copies, and the captured catalogue its tests read lives
here now, shared by mesh-compose's tests.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:15:55 +01:00

201 lines
7.2 KiB
Go

package resolution
import (
"fmt"
"regexp"
"strings"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
// ofContribution is where a holder places a kind: ${contribution:<seat>:<kind>}. Loose inside the
// braces, so a misspelt seat or kind is found and refused rather than written out as text.
var ofContribution = regexp.MustCompile(`\$\{contribution:([^}]*)\}`)
// receivable is what a seat receives of a kind, with the seat's canonical name; false when the seat
// is unknown or does not receive it.
func receivable(seat, kind string) (mcat.Seat, mcat.Receivable, bool) {
s, known := mcat.SeatNamed(seat)
if !known {
return mcat.Seat{}, mcat.Receivable{}, false
}
for _, r := range s.Receives {
if r.Kind == kind {
return s, r, true
}
}
return s, mcat.Receivable{}, false
}
// kindsOf names a seat's receivable kinds for a refusal.
func kindsOf(s mcat.Seat) string {
if len(s.Receives) == 0 {
return "it receives no contributions"
}
var kinds []string
for _, r := range s.Receives {
kinds = append(kinds, r.Kind)
}
return "it receives " + strings.Join(kinds, ", ")
}
// seatPlaceholderProblems is what is wrong with one resource's ${contribution:…}: placed only in a
// file's content, naming a seat and a kind it receives, and only by a module that claims that seat
// — another would be a second writer of a file there is one of (novox/hq ADR 0212 §3).
func seatPlaceholderProblems(m mcat.Manifest, r map[string]any) []string {
var problems []string
for _, field := range sortedKeys(r) {
v, ok := r[field].(string)
if !ok {
continue
}
found := ofContribution.FindAllStringSubmatch(v, -1)
if len(found) == 0 {
continue
}
if field != "content" {
problems = append(problems, fmt.Sprintf(
"%s's resource %v names %s in its %s; contributions are placed only in a file's content",
m.Module, r["id"], found[0][0], field))
continue
}
for _, f := range found {
seat, rest, two := strings.Cut(f[1], ":")
// A kind received as data may be narrowed by its shape's placing fields (novox/hq ADR 0255).
kind, where, _ := strings.Cut(rest, ":")
s, rcv, ok := receivable(seat, kind)
if two && ok {
if _, err := placeholderWhere(rcv, where); err != nil {
problems = append(problems, fmt.Sprintf("%s's resource %v names %s: %v", m.Module, r["id"], f[0], err))
}
}
if !two || !ok {
detail := "the mesh defines no seat " + fmt.Sprintf("%q", seat)
if s.Name != "" {
detail = s.Name + ": " + kindsOf(s)
}
problems = append(problems, fmt.Sprintf(
"%s's resource %v names %s; a contribution is ${contribution:<seat>:<kind>} (%s)",
m.Module, r["id"], f[0], detail))
continue
}
if !m.ClaimsSeat(s.Name) {
problems = append(problems, fmt.Sprintf(
"%s's resource %v names %s and %s does not claim %s; every module's contributions to a "+
"seat are placed by its holder alone (novox/hq ADR 0212)",
m.Module, r["id"], f[0], m.Module, s.Name))
}
}
}
return problems
}
// seatContributions is every module's contribution of one kind to one seat (novox/hq ADR 0212 §3):
// in module order, each module's pieces in the order it declared them, each module's preceded by a
// comment line naming it in the tool's grammar, and empty when nothing is contributed. A kind that
// takes directories has each contributor's `${dir:<id>}` filled with where that module's directory
// is on this machine (novox/hq to-be 43).
//
// A directory placed in a person's home is named `${machine:account-home}/…` (novox/hq to-be 29); in a
// kind that takes directories that is filled from the machine's facts as well, so the holder reads a
// path, never a placeholder (novox/hq ADR 0233: an item in the operator's home is backed up too).
//
// A kind received as data is rendered through the holder's template instead, narrowed by where
// (novox/hq ADR 0255); and a contribution naming a capability this machine did not report is left
// out, of either form.
//
// What it could not render is answered as unplaced, each naming its module and why, and left out: one
// piece the holder's template does not know is that piece's fault, not the machine's.
func seatContributions(modules []mcat.Manifest, holder mcat.Manifest, placeholder string, with mcat.Rendering, facts map[string]string, caps map[string]bool) (string, []string, error) {
seat, rest, _ := strings.Cut(placeholder, ":")
kind, whereText, _ := strings.Cut(rest, ":")
s, r, ok := receivable(seat, kind)
if !ok {
return "", nil, nil
}
if r.Shape != nil {
where, err := placeholderWhere(r, whereText)
if err != nil {
return "", nil, err
}
return shapedContributions(modules, holder, facts["name"], facts, s, r, where, caps)
}
var failed error
var unplacedLines []string
var blocks []textBlock
for _, m := range inModuleOrder(modules) {
var old strings.Builder
// A left-out module's lines are best effort (novox/hq ADR 0262): what cannot be placed is said,
// and the machine is declared without it. A placement setting that does not read is not
// replaced by the definition's own path, which may not be where the data is.
if m.BestEffort && r.Dirs {
if _, err := mcat.Places(m, with.Settings[m.Module]); err != nil {
unplacedLines = append(unplacedLines, fmt.Sprintf("%s's %s for %s, kept while it is left out, "+
"is not placed: its placement setting does not read (%v)", m.Module, kind, s.Name, err))
continue
}
}
for _, c := range manifestAllContributions(m) {
if c.Kind != kind || !capable(c, caps) {
continue
}
if cs, known := mcat.SeatNamed(c.Seat); !known || cs.Name != s.Name {
continue
}
content := c.Content
var lineFailed error
if r.Dirs {
filled, err := dirFill(content, dirsFor(m, with), m.Module)
if err != nil && lineFailed == nil {
lineFailed = err
}
// An operator's path the module was given, as an item of data on it (novox/hq ADR 0233).
if accessRef.MatchString(filled) {
_, byID, err := accessesFor(m, with.Settings[m.Module])
if err == nil {
filled, err = accessFill(filled, byID, m.Module)
}
if err != nil && lineFailed == nil {
lineFailed = err
}
}
for _, key := range machineUsed(filled) {
value, has := facts[key]
if !has {
if lineFailed == nil {
lineFailed = fmt.Errorf("%s's %s for %s says ${machine:%s}, and this machine says %s",
m.Module, kind, s.Name, key, orNothing(namesOfFacts(facts)))
}
continue
}
filled = strings.ReplaceAll(filled, "${machine:"+key+"}", value)
}
content = filled
}
if lineFailed != nil {
if m.BestEffort {
unplacedLines = append(unplacedLines, fmt.Sprintf("%s's %s for %s, kept while it is left "+
"out, is not placed: %v", m.Module, kind, s.Name, lineFailed))
continue
}
if failed == nil {
failed = lineFailed
}
}
if old.Len() == 0 {
fmt.Fprintf(&old, "%s %s\n", r.Comment, m.Module)
}
old.WriteString(content)
if !strings.HasSuffix(content, "\n") {
old.WriteString("\n")
}
}
var held []heldItem
if !r.Dirs {
held = heldItems(m, "contribution:"+s.Name+":"+kind, nil, facts, caps)
}
blocks = append(blocks, moduleBlocks(m, r.Comment, old.String(), held)...)
}
return joinBlocks(blocks), unplacedLines, failed
}