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
201 lines
7.2 KiB
Go
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
|
|
}
|