The second reader release: the reader reads G's new words (design 47 G §621, §697) #188
@@ -587,6 +587,11 @@ type Manifest struct {
|
||||
// person's account (design 25 §7) already had the same shape.
|
||||
Invokes []string `json:"invokes,omitempty"`
|
||||
|
||||
// InvokeFields maps a verb of Invokes to the fields it names, read from an entry of `invokes` in its long form
|
||||
// {verb, fields}, which may stand beside `*` (design 47 G §617): Invokes keeps the verb, so none of its readers
|
||||
// changes. A long-form entry naming no field is a verb of Invokes alone.
|
||||
InvokeFields map[string][]string `json:"-"`
|
||||
|
||||
// Waits are the event families a session may wait on through the mesh MCP server's `mesh_wait`, each
|
||||
// `<emitter>.<event>` as `consumes` names one, a seat's or a module's (novox/hq ADR 0367 §2). Only the tool
|
||||
// runner's module declares them, one by one and never by a wildcard. **A grant apart from `consumes`**: the
|
||||
@@ -1582,6 +1587,10 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
if err := consumesEntryProblem(keys["consumes"]); err != nil {
|
||||
return err
|
||||
}
|
||||
invokeFields, err := readInvokes(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plain := map[string]string{}
|
||||
many := map[string]map[string]string{}
|
||||
if secrets, ok := keys["secrets"]; ok && string(secrets) != "null" {
|
||||
@@ -1736,6 +1745,7 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
m.forms = forms
|
||||
m.OptionalRequires = optionalRequires
|
||||
m.RequireShares = requireShares
|
||||
m.InvokeFields = invokeFields
|
||||
if len(plain) > 0 {
|
||||
m.Secrets = plain
|
||||
}
|
||||
@@ -1754,6 +1764,61 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// invokeLongForm is an entry of `invokes` in its long form: the verb, and the fields of its answer the caller relies on
|
||||
// (design 47 G §617).
|
||||
type invokeLongForm struct {
|
||||
Verb string `json:"verb"`
|
||||
Fields []string `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// readInvokes reads `invokes` whose entries are verbs or, in their long form, {verb, fields}: it puts the verbs back in
|
||||
// keys as plain names and answers the fields each long-form entry names.
|
||||
func readInvokes(keys map[string]json.RawMessage) (map[string][]string, error) {
|
||||
raw, ok := keys["invokes"]
|
||||
if !ok || string(raw) == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
var entries []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &entries); err != nil {
|
||||
return nil, nil // not a list: the field's own decoder says so
|
||||
}
|
||||
var verbs []string
|
||||
var fields map[string][]string
|
||||
for i, e := range entries {
|
||||
trimmed := bytes.TrimSpace(e)
|
||||
if len(trimmed) == 0 || trimmed[0] != '{' {
|
||||
var verb string
|
||||
if err := json.Unmarshal(trimmed, &verb); err != nil {
|
||||
return nil, fmt.Errorf("invokes[%d]: a verb, or {verb, fields}: %w", i, err)
|
||||
}
|
||||
verbs = append(verbs, verb)
|
||||
continue
|
||||
}
|
||||
var long invokeLongForm
|
||||
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&long); err != nil {
|
||||
return nil, fmt.Errorf("invokes[%d]: a verb, or {verb, fields}: %w", i, err)
|
||||
}
|
||||
if long.Verb == "" {
|
||||
return nil, fmt.Errorf("invokes[%d]: a long-form entry names its verb", i)
|
||||
}
|
||||
verbs = append(verbs, long.Verb)
|
||||
if len(long.Fields) > 0 {
|
||||
if fields == nil {
|
||||
fields = map[string][]string{}
|
||||
}
|
||||
fields[long.Verb] = long.Fields
|
||||
}
|
||||
}
|
||||
plain, err := json.Marshal(verbs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys["invokes"] = plain
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// checkSharesNotContributed refuses a module whose own `contributes` to s3-bucket names `shares` (ADR 0468 §8).
|
||||
func (m Manifest) checkSharesNotContributed() error {
|
||||
if len(m.RequireShares[BucketRequirement]) == 0 {
|
||||
@@ -1807,13 +1872,30 @@ func (m Manifest) marshalFields() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(m.SecretsMany) == 0 && len(m.ContributesMany) == 0 && len(m.OptionalRequires) == 0 && len(m.RequireShares) == 0 {
|
||||
if len(m.SecretsMany) == 0 && len(m.ContributesMany) == 0 && len(m.OptionalRequires) == 0 && len(m.RequireShares) == 0 &&
|
||||
len(m.InvokeFields) == 0 {
|
||||
return raw, nil
|
||||
}
|
||||
var keys map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(m.InvokeFields) > 0 {
|
||||
// Each verb that names fields is written back in its long form (design 47 G §617).
|
||||
entries := make([]any, 0, len(m.Invokes))
|
||||
for _, verb := range m.Invokes {
|
||||
if fields, named := m.InvokeFields[verb]; named {
|
||||
entries = append(entries, invokeLongForm{Verb: verb, Fields: fields})
|
||||
continue
|
||||
}
|
||||
entries = append(entries, verb)
|
||||
}
|
||||
listed, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys["invokes"] = listed
|
||||
}
|
||||
if optional := optionalRequiresList(m); len(optional) > 0 {
|
||||
// Beside `requires`, never inside it: an older controller reads `requires` as plain names, and one
|
||||
// manifest it cannot read stops the whole catalogue (OptionalRequiresKey).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -43,6 +45,18 @@ func TestTheSecondReaderReleaseReadsGsManifestWords(t *testing.T) {
|
||||
a := m.Build.Artifacts
|
||||
return len(a) == 2 && len(a[0].Calls) == 1 && a[0].Calls[0] == "put-sessions" && len(a[1].States) == 2
|
||||
}},
|
||||
{name: "* with two long-form invokes (§617)",
|
||||
raw: `{"module":"m","version":"1.0","invokes":["*",{"verb":"seat:mesh-catalog.revision","fields":["none-yet"]},` +
|
||||
`{"verb":"seat:mesh-catalog.entry"}]}`,
|
||||
check: func(m Manifest) bool {
|
||||
out, _ := json.Marshal(m)
|
||||
return slices.Equal(m.Invokes, []string{"*", "seat:mesh-catalog.revision", "seat:mesh-catalog.entry"}) &&
|
||||
len(m.InvokeFields) == 1 && slices.Equal(m.InvokeFields["seat:mesh-catalog.revision"], []string{"none-yet"}) &&
|
||||
strings.Contains(string(out), `"invokes":["*",{"verb":"seat:mesh-catalog.revision","fields":["none-yet"]},"seat:mesh-catalog.entry"]`)
|
||||
}},
|
||||
{name: "a long-form invoke with a word it does not know (§617)",
|
||||
raw: `{"module":"m","version":"1.0","invokes":[{"verb":"seat:mesh-catalog.revision","field":["none-yet"]}]}`,
|
||||
refused: `invokes[0]: a verb, or {verb, fields}`},
|
||||
} {
|
||||
m, err := decodeForTest([]byte(c.raw))
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user