Files
mesh-controller/internal/catalogue/seat_data.go
T
jochen 8984c3437f
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
Leave out a block its holder cannot render, and keep if-capability to known names on offered kinds (hq ADR 0255)
A piece whose shows the holder's template does not know rendered as nothing and failed
the whole machine's declaration; it is now left out and named, for push, plan and the
merge gate. quote escapes DEL, which TOML refuses bare. An if-capability nothing
detects, or on a kind a holder depends on, would drop a piece silently, so both are
refused.
2026-10-08 15:20:59 +02:00

403 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package catalogue
import (
"bytes"
"encoding/json"
"fmt"
"math"
"regexp"
"sort"
"strings"
"text/template"
)
// A kind a seat receives as data, rendered by its holder (novox/hq ADR 0255).
//
// ADR 0212 made a contribution text in the tool's own grammar, which the controller places and never
// reads. That is right where the tool is the seat's for good — a hotkey daemon's trigger lines — and
// wrong where the seat's whole point is that its tool can be replaced: a module adding a battery to the
// bar would write i3status-rust's TOML, and a second bar would have to read it. So a seat may define a
// kind as data instead. The seat says the data's shape; a contributor gives data in that shape; the
// holder gives a template that renders one piece of it into its tool's grammar, as a module's facts
// template renders the roster (`facts`): the data is the mesh's, the format is the holder's.
//
// What the holder places is narrowed by the shape's placing fields — the bar's `bar` and `place` — in
// the placeholder, `${contribution:<seat>:<kind>:<field>=<value>,…}`, and every combination of them is
// placed by the holder exactly once, so nothing a module contributes is dropped by a holder that forgot
// a corner of the shape.
// Shape is what a kind received as data looks like.
type Shape struct {
// Places are the fields a holder narrows its placeholders by, each with every value it takes.
Places []PlaceField
// Order is the whole-number field that orders the pieces within one placeholder (lowest first,
// then module order); absent in a piece means OrderDefault.
Order string
// Check is what else is wrong with one piece's data, beyond its placing fields and its order.
Check func(data map[string]any) []string `json:"-"`
// Examples are pieces every holder's template must render, one for each variant the shape has,
// so a holder that cannot render one is refused at registration and not found on a machine.
Examples []map[string]any
}
// PlaceField is one field a holder places by, and the values it takes.
type PlaceField struct {
Field string
Values []string
}
// OrderDefault is a piece's order when it gives none: the middle of 0–99.
const OrderDefault = 50
// capabilityName is the shape of a capability's name, as a node's profile reports it.
var capabilityName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
// KnownCapabilities is every capability the node-engine detects (mesh-host internal/profile), the
// names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its
// contribution out on every machine, silently, so it is refused. A detector added there is added here.
var KnownCapabilities = []string{
"battery", "container-runtime", "firewall", "graphical-session", "overlay", "package-manager",
"power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager",
"uplink-systemd-networkd", "virtualisation",
}
// renderFuncs are the functions a holder's template may call. `quote` writes a value as a JSON
// string with DEL escaped as well — JSON leaves it bare, and TOML refuses it — which makes it a valid
// basic string in TOML and in most tools' grammars, so a contributed command with quotes or control
// characters in it cannot break out of the holder's line.
var renderFuncs = template.FuncMap{"quote": quote}
func quote(v any) (string, error) {
b, err := json.Marshal(fmt.Sprint(v))
return strings.ReplaceAll(string(b), "\x7f", `\u007f`), err
}
// shapedProblems is what is wrong with one contribution of a kind received as data.
func shapedProblems(module string, i int, r Receivable, c SeatContribution) []string {
var problems []string
at := fmt.Sprintf("%s's contribution %d to %s (%s)", module, i+1, c.Seat, c.Kind)
if strings.TrimSpace(c.Content) != "" {
problems = append(problems, at+" has content; this kind is data, given as `data` (novox/hq ADR 0255)")
}
if len(c.Data) == 0 {
return append(problems, at+" has no data")
}
for _, p := range r.Shape.Places {
v, _ := c.Data[p.Field].(string)
if !oneOf(p.Values, v) {
problems = append(problems, fmt.Sprintf("%s says %s %q; it is one of %s",
at, p.Field, fmt.Sprint(c.Data[p.Field]), strings.Join(p.Values, ", ")))
}
}
if r.Shape.Order != "" {
if v, has := c.Data[r.Shape.Order]; has {
if _, ok := wholeIn(v, 0, 99); !ok {
problems = append(problems, fmt.Sprintf("%s says %s %v; it is a whole number from 0 to 99",
at, r.Shape.Order, v))
}
}
}
if r.Shape.Check != nil {
for _, p := range r.Shape.Check(c.Data) {
problems = append(problems, at+": "+p)
}
}
return problems
}
// wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float.
func wholeIn(v any, lo, hi int) (int, bool) {
f, ok := v.(float64)
if !ok {
if i, isInt := v.(int); isInt {
f, ok = float64(i), true
}
}
if !ok || f != math.Trunc(f) || f < float64(lo) || f > float64(hi) {
return 0, false
}
return int(f), true
}
// placeholderWhere parses the narrowing part of a placeholder, `bar=bottom,place=status`, against the
// kind's shape. Empty narrows nothing.
func placeholderWhere(r Receivable, where string) (map[string]string, error) {
out := map[string]string{}
if where == "" {
return out, nil
}
if r.Shape == nil {
return nil, fmt.Errorf("%s is text, which a placeholder does not narrow", r.Kind)
}
for _, pair := range strings.Split(where, ",") {
field, value, ok := strings.Cut(pair, "=")
var p *PlaceField
for i := range r.Shape.Places {
if r.Shape.Places[i].Field == field {
p = &r.Shape.Places[i]
}
}
switch {
case !ok || p == nil:
return nil, fmt.Errorf("%q narrows by %s, and %s is placed by %s", where, field, r.Kind, placeFields(r))
case !oneOf(p.Values, value):
return nil, fmt.Errorf("%q says %s=%s; %s is one of %s", where, field, value, field, strings.Join(p.Values, ", "))
case out[field] != "":
return nil, fmt.Errorf("%q names %s twice", where, field)
}
out[field] = value
}
return out, nil
}
func placeFields(r Receivable) string {
var names []string
for _, p := range r.Shape.Places {
names = append(names, p.Field)
}
return strings.Join(names, ", ")
}
// holderTemplate is the holder's template for a kind, parsed.
func holderTemplate(holder Manifest, seat Seat, kind string) (*template.Template, error) {
for _, c := range holder.Claims {
if s, known := SeatNamed(c.Name); !known || s.Name != seat.Name {
continue
}
text, has := c.Renders[kind]
if !has || strings.TrimSpace(text) == "" {
break
}
t, err := template.New(seat.Name + ":" + kind).Funcs(renderFuncs).Parse(text)
if err != nil {
return nil, fmt.Errorf("%s's template for %s:%s does not parse: %v", holder.Module, seat.Name, kind, err)
}
return t, nil
}
return nil, fmt.Errorf("%s places %s:%s, which is data, and its claim gives no template for it in `renders` "+
"(novox/hq ADR 0255)", holder.Module, seat.Name, kind)
}
// renderPiece is one piece of data in the holder's grammar. The template sees the piece's fields and
// `module`, the contributor. A value the piece does not have reads as nothing in a `with` or an `if`;
// printed bare it would write `<no value>` into the tool's file, which is refused here instead.
func renderPiece(t *template.Template, module string, data map[string]any) (string, error) {
view := map[string]any{"module": module}
for k, v := range data {
view[k] = v
}
var b bytes.Buffer
if err := t.Execute(&b, view); err != nil {
return "", fmt.Errorf("rendering %s's piece: %v", module, err)
}
out := b.String()
if strings.Contains(out, "<no value>") {
return "", fmt.Errorf("rendering %s's piece printed a value it does not have: %q", module, out)
}
return out, nil
}
// placedPiece is one contributed piece on its way into a holder's file.
type placedPiece struct {
module string
data map[string]any
order int
}
// shapedContributions is every module's data of one kind to one seat, narrowed by where and by the
// machine's capabilities, ordered, and rendered through the holder's template — each piece under a
// comment line naming its module.
//
// A piece the template renders to nothing — a `shows` the holder does not know yet — or fails on is
// left out and answered as unplaced, naming its module: the other pieces and the rest of the machine's
// declaration go on (novox/hq ADR 0255).
func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivable, where map[string]string, caps map[string]bool) (string, []string, error) {
var pieces []placedPiece
for _, m := range inModuleOrder(modules) {
for _, c := range m.allContributions() {
if c.Kind != r.Kind || !capable(c, caps) {
continue
}
if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name {
continue
}
match := true
for field, value := range where {
if v, _ := c.Data[field].(string); v != value {
match = false
}
}
if !match {
continue
}
order := OrderDefault
if v, ok := wholeIn(c.Data[r.Shape.Order], 0, 99); ok {
order = v
}
pieces = append(pieces, placedPiece{module: m.Module, data: c.Data, order: order})
}
}
if len(pieces) == 0 {
return "", nil, nil
}
t, err := holderTemplate(holder, s, r.Kind)
if err != nil {
return "", nil, err
}
sort.SliceStable(pieces, func(a, b int) bool { return pieces[a].order < pieces[b].order })
var b strings.Builder
var unplaced []string
for _, p := range pieces {
out, err := renderPiece(t, p.module, p.data)
if err == nil && strings.TrimSpace(out) == "" {
err = fmt.Errorf("%s's template renders nothing for it", holder.Module)
}
if err != nil {
unplaced = append(unplaced, fmt.Sprintf("%s's %s:%s %s is not placed by %s: %v",
p.module, s.Name, r.Kind, describePiece(p.data), holder.Module, err))
continue
}
fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module)
b.WriteString(out)
if !strings.HasSuffix(out, "\n") {
b.WriteString("\n")
}
}
return b.String(), unplaced, nil
}
// describePiece is a piece in one line, its fields in order, for a person reading why it was left out.
func describePiece(data map[string]any) string {
var parts []string
for _, k := range sortedKeys(data) {
if _, nested := data[k].(map[string]any); nested {
continue
}
parts = append(parts, fmt.Sprintf("%s=%v", k, data[k]))
}
return "(" + strings.Join(parts, " ") + ")"
}
// capable is whether a contribution applies on a machine with these capabilities: always, unless it
// names one with `if-capability` the machine did not report (novox/hq ADR 0255). A machine whose
// capabilities are not known has none, so a conditional piece is left out rather than guessed in.
func capable(c SeatContribution, caps map[string]bool) bool {
return c.IfCapability == "" || caps[c.IfCapability]
}
// placementProblems is what is wrong with how a holder places the kinds its seats receive as data:
// once it places one at all, it needs a template that renders every example of the shape, and every combination of the placing
// fields placed exactly once across its files — or something contributed would be dropped, or written
// twice (novox/hq ADR 0255).
func (m Manifest) placementProblems() []string {
var problems []string
for _, c := range m.Claims {
s, known := SeatNamed(c.Name)
if !known {
continue
}
for _, r := range s.Receives {
// A holder that places none of the kind is older than it (novox/hq ADR 0255): refusing it
// would refuse every bar registered before the kind existed, and the controller that
// defines the kind ships first. Its contributions wait, unplaced, until it places them.
if r.Shape == nil || !placesKind(m, s, r.Kind) {
continue
}
t, err := holderTemplate(m, s, r.Kind)
if err != nil {
problems = append(problems, err.Error())
continue
}
for _, ex := range r.Shape.Examples {
if out, err := renderPiece(t, "example", ex); err != nil || strings.TrimSpace(out) == "" {
problems = append(problems, fmt.Sprintf("%s's template for %s:%s renders nothing for %v: %v",
m.Module, s.Name, r.Kind, ex, err))
}
}
placed := map[string]int{}
for _, res := range m.Resources {
content, _ := res["content"].(string)
for _, f := range ofContribution.FindAllStringSubmatch(content, -1) {
seat, rest, _ := strings.Cut(f[1], ":")
kind, whereText, _ := strings.Cut(rest, ":")
if cs, ok := SeatNamed(seat); !ok || cs.Name != s.Name || kind != r.Kind {
continue
}
where, err := placeholderWhere(r, whereText)
if err != nil {
continue // said by seatPlaceholderProblems
}
for _, combo := range combinations(r.Shape.Places) {
if covers(where, combo) {
placed[comboKey(combo)]++
}
}
}
}
for _, combo := range combinations(r.Shape.Places) {
switch n := placed[comboKey(combo)]; {
case n == 0:
problems = append(problems, fmt.Sprintf("%s never places %s:%s for %s; every combination of %s is "+
"placed once, or a contribution to it is lost (novox/hq ADR 0255)",
m.Module, s.Name, r.Kind, comboKey(combo), placeFields(r)))
case n > 1:
problems = append(problems, fmt.Sprintf("%s places %s:%s for %s %d times; once, or a contribution "+
"is written twice (novox/hq ADR 0255)", m.Module, s.Name, r.Kind, comboKey(combo), n))
}
}
}
}
return problems
}
// placesKind is whether any of a holder's files names the kind's placeholder.
func placesKind(m Manifest, s Seat, kind string) bool {
for _, res := range m.Resources {
content, _ := res["content"].(string)
for _, f := range ofContribution.FindAllStringSubmatch(content, -1) {
seat, rest, _ := strings.Cut(f[1], ":")
k, _, _ := strings.Cut(rest, ":")
if cs, ok := SeatNamed(seat); ok && cs.Name == s.Name && k == kind {
return true
}
}
}
return false
}
// combinations is every assignment of a value to each placing field.
func combinations(places []PlaceField) []map[string]string {
out := []map[string]string{{}}
for _, p := range places {
var next []map[string]string
for _, partial := range out {
for _, v := range p.Values {
c := map[string]string{p.Field: v}
for k, w := range partial {
c[k] = w
}
next = append(next, c)
}
}
out = next
}
return out
}
func covers(where, combo map[string]string) bool {
for field, value := range where {
if combo[field] != value {
return false
}
}
return true
}
func comboKey(combo map[string]string) string {
var parts []string
for _, k := range sortedKeys(combo) {
parts = append(parts, k+"="+combo[k])
}
return strings.Join(parts, ",")
}