From 028f93575f28cd23091431caa1534ce53160968b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 03:04:05 +0100 Subject: [PATCH 1/5] RR2-1: the seat-definition reader reads by-caller and chunks on a verb, view on a state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A definition G writes marks a verb by-caller (mesh-recovery, mesh-broker, the gated verbs) or chunks, and a state as a view; the running reader refused each as a field the format does not know, so every such definition. Builds: design 47 G §621, §697, §681, §699 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/seatdef/seatdef.go | 8 ++++++++ go/seatdef/seatdef_test.go | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index d283899..92537e5 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -221,6 +221,12 @@ type Verb struct { // NeedsApproval marks the verb for every holder: the operator approves a call of it first (design 46 *The approval // mark* §2). NeedsApproval bool `json:"needs-approval,omitempty"` + // ByCaller says the holder's process answers the verb itself and tells callers apart by the account the bus proved + // (design 47 G §554, §621, §697). + ByCaller bool `json:"by-caller,omitempty"` + // Chunks says the holder's process sends the answer's file in chunks, each acknowledged by the caller, on the + // subjects mesh.seat..chunk... and mesh.seat..ack... (design 47 G §681). + Chunks bool `json:"chunks,omitempty"` } // The streams an accepted kind may ride (design 47 G §415). @@ -402,6 +408,8 @@ type State struct { History int `json:"history,omitempty"` MaxAgeDays int `json:"max-age-days,omitempty"` Description string `json:"description,omitempty"` + // View is true when the seat offers the state as a view for a module to follow by `consumes` (design 47 G §699). + View bool `json:"view,omitempty"` } // KeyForm is one form a state's key takes: a pattern of named parts (`.`, a part between angle brackets, diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index abaa972..2678f76 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -84,6 +84,8 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { `"domain":"Core","decision":"ADR 1","records":[{"name":"asks"}],`, 1), "records: a field the format does not know"}, {"a state with a key form and a value", withState(whole, `{"name":"asks","template":"state","writer":"caller", "history":3,"key-forms":[{"key":".","value":{"type":"object"}}]}`), ""}, + {"a state offered as a view", withState(whole, `{"name":"asks","template":"state","view":true, + "key-forms":[{"key":"","value":{"type":"object"}}]}`), ""}, {"a stream state with an age", withState(whole, `{"name":"placement","template":"events","max-age-days":7, "key-forms":[{"key":"changed.","value":{"type":"object"}}]}`), ""}, {"a state of an unknown template", withState(whole, `{"name":"asks","template":"bucket", @@ -142,6 +144,8 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { {"an acting rule on another kind", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"kind:Image"}],"verbs":`, 1), "acts[0].on"}, {"a verb marked for approval", strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":true,`, 1), ""}, {"a verb marked with a word that is no boolean", strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":"yes",`, 1), "not a seat definition's JSON"}, + {"a verb answered by its caller's account, in chunks", strings.Replace(whole, `"name":"list",`, `"name":"list","by-caller":true,"chunks":true,`, 1), ""}, + {"a verb's by-caller that is no boolean", strings.Replace(whole, `"name":"list",`, `"name":"list","by-caller":"yes",`, 1), "not a seat definition's JSON"}, {"a bodies section", strings.Replace(whole, `"family":`, `"bodies":{"Finding":{"type":"object"}},"family":`, 1), ""}, {"one kind on two streams", strings.Replace(whole, `"events":`, `"accepts":[{"name":"step","body":{},"stream":"check"},{"name":"step","body":{},"stream":"delivery"}],"events":`, 1), ""}, From c06482f0d4485b00f7376cad3b31d015031d721f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 03:05:33 +0100 Subject: [PATCH 2/5] RR2-2: the manifest reader reads max-mib on an old state section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G's indexer, builder and runner cap their buckets; the running reader called the word an unknown field and refused the whole manifest. A value out of the objects section's bounds is refused naming them. Builds: design 47 G §494, §469, §621 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/catalogue/reader_release_test.go | 29 ++++++++++++++++++++++++++++- go/catalogue/state.go | 16 +++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go index 43cdf35..93d0912 100644 --- a/go/catalogue/reader_release_test.go +++ b/go/catalogue/reader_release_test.go @@ -1,6 +1,9 @@ package catalogue -import "testing" +import ( + "strings" + "testing" +) // A manifest carrying the words design 47 G writes is read whole by the reader release (design 47 G §340, §409): // `version` as MAJOR.MINOR (§368), a `*` emitter in `consumes` (§386), an observer's finding stream (§387) and an @@ -21,3 +24,27 @@ func TestTheReaderReleaseReadsTheManifestWordsGWrites(t *testing.T) { t.Fatalf("read as %+v", m) } } + +// The second reader release reads the words G's manifests write beyond the first's (design 47 G §494, §621): each row a +// manifest read whole, or refused naming what is wrong, and what the reader gives of it. +func TestTheSecondReaderReleaseReadsGsManifestWords(t *testing.T) { + for _, c := range []struct { + name, raw, refused string + check func(Manifest) bool + }{ + {name: "a state's cap (§469)", raw: `{"module":"m","version":"1.0","state":["plain",{"name":"pipelines","max-mib":256}]}`, + check: func(m Manifest) bool { return m.State[0].MaxMiB == 0 && m.State[1].MaxMiB == 256 }}, + {name: "a state's cap out of the objects section's bounds (§469)", + raw: `{"module":"m","version":"1.0","state":[{"name":"pipelines","max-mib":0}]}`, refused: "max-mib is between 1 and 8192"}, + } { + m, err := decodeForTest([]byte(c.raw)) + switch { + case c.refused != "" && (err == nil || !strings.Contains(err.Error(), c.refused)): + t.Errorf("%s: read as %v, want refused naming %q", c.name, err, c.refused) + case c.refused == "" && err != nil: + t.Errorf("%s: refused: %v", c.name, err) + case c.refused == "" && (len(m.UnknownField()) != 0 || !c.check(m)): + t.Errorf("%s: read as %+v (set aside %v)", c.name, m, m.UnknownField()) + } + } +} diff --git a/go/catalogue/state.go b/go/catalogue/state.go index 2bca54e..fc6a289 100644 --- a/go/catalogue/state.go +++ b/go/catalogue/state.go @@ -35,11 +35,14 @@ type StateDeclaration struct { // the tool runner's account, read it and never write it (the lead's ruling, 2026-11-04: nats' warrant spend record // `spent`). The judge's rule and the grants follow from it. ReadOnly bool `json:"read-only,omitempty"` + // MaxMiB is the bucket's cap in MiB, between ObjectsLeastMiB and ObjectsMostMiB, as an old objects section's + // is (design 47 G §469, §494); zero is no cap, as before. + MaxMiB int `json:"max-mib,omitempty"` // KeptLayout is the layout its holder says it writes: its form and its catch-up tool (design 47). KeptLayout } -// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}. +// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}. func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { trimmed := bytes.TrimSpace(raw) if len(trimmed) > 0 && trimmed[0] == '"' { @@ -47,21 +50,28 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { } type plain StateDeclaration var full plain + var said struct { + MaxMiB *int `json:"max-mib"` + } + _ = json.Unmarshal(trimmed, &said) dec := json.NewDecoder(bytes.NewReader(trimmed)) dec.DisallowUnknownFields() if err := dec.Decode(&full); err != nil { - return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, form, catch-up}: %w", typedUnknown(err)) + return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only, max-mib, form, catch-up}: %w", typedUnknown(err)) } if err := full.KeptLayout.refuse(fmt.Sprintf("state %q", full.Name)); err != nil { return err } + if said.MaxMiB != nil && (*said.MaxMiB < ObjectsLeastMiB || *said.MaxMiB > ObjectsMostMiB) { + return fmt.Errorf("state %q: max-mib is between %d and %d, not %d", full.Name, ObjectsLeastMiB, ObjectsMostMiB, *said.MaxMiB) + } *s = StateDeclaration(full) return nil } // MarshalJSON writes back the short form when there is nothing else to say. func (s StateDeclaration) MarshalJSON() ([]byte, error) { - if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.KeptLayout == (KeptLayout{}) { + if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly && s.MaxMiB == 0 && s.KeptLayout == (KeptLayout{}) { return json.Marshal(s.Name) } type plain StateDeclaration From 3747b5f78c74a63b20b2efcc9a90f7629e87f5c2 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 03:06:59 +0100 Subject: [PATCH 3/5] RR2-3: the manifest reader reads an artifact's calls and states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-sdk's own manifest names the capability words its packages call, and the tool runner's artifact the words it states; the running reader refused both manifests on the unknown word. Builds: design 47 G §621, §605, §545 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/catalogue/manifest.go | 10 ++++++++++ go/catalogue/reader_release_test.go | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 5d501f9..ec6f765 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -1246,6 +1246,16 @@ type Artifact struct { // release keeps a manifest that names them whole (design 47 G §340, §409); the manifest validator checks their form. Needs []string `json:"needs,omitempty"` + // Calls are the capability words (design 47 G §488, §605) a package's code calls of the tool runner: the delivery plan + // derives, for each module whose build resolved a version of such a package, an ordering edge to the module whose + // artifact States the word. Read here so the second reader release keeps a manifest that names them (§621). + Calls []string `json:"calls,omitempty"` + + // States are the capability words (design 47 G §488) the tool runner's artifact states, the edge of a package's Calls + // running to the module that states each (§545). Read here so the second reader release keeps a manifest that names + // them (§621). + States []string `json:"states,omitempty"` + // GeneratedFrom marks a package of a contracts module (novox/hq ADR 0608 §5, design 26 *Contracts modules*): the // directory, relative to the module's, of the seat definitions every file of the package is generated from. A // module carrying it holds contracts only and claims no seat, which the judge refuses naming the seat; the build diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go index 93d0912..a52ba46 100644 --- a/go/catalogue/reader_release_test.go +++ b/go/catalogue/reader_release_test.go @@ -36,6 +36,13 @@ func TestTheSecondReaderReleaseReadsGsManifestWords(t *testing.T) { check: func(m Manifest) bool { return m.State[0].MaxMiB == 0 && m.State[1].MaxMiB == 256 }}, {name: "a state's cap out of the objects section's bounds (§469)", raw: `{"module":"m","version":"1.0","state":[{"name":"pipelines","max-mib":0}]}`, refused: "max-mib is between 1 and 8192"}, + {name: "a package's calls and the tool runner's states (§605, §545)", + raw: `{"module":"m","version":"1.0","build":{"artifacts":[{"name":"go","kind":"package","language":"go","calls":["put-sessions"]},` + + `{"name":"runtime","kind":"bundle","language":"go","states":["put-sessions","finding-calls"]}]}}`, + check: func(m Manifest) bool { + a := m.Build.Artifacts + return len(a) == 2 && len(a[0].Calls) == 1 && a[0].Calls[0] == "put-sessions" && len(a[1].States) == 2 + }}, } { m, err := decodeForTest([]byte(c.raw)) switch { From 98f22ffe72ef95cdcb5e0f243f0a53effcbe8ae9 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 03:08:25 +0100 Subject: [PATCH 4/5] RR2-4: the manifest reader reads the long form of an invokes entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool runner's invokes hold * beside {verb, fields} entries; Invokes keeps every verb, so none of its readers changes, and InvokeFields maps a verb to the fields it names, written back in the long form. Builds: design 47 G §621, §617 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/catalogue/manifest.go | 84 ++++++++++++++++++++++++++++- go/catalogue/reader_release_test.go | 14 +++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index ec6f765..ef1f49c 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -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 // `.` 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). diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go index a52ba46..0a4f4e5 100644 --- a/go/catalogue/reader_release_test.go +++ b/go/catalogue/reader_release_test.go @@ -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 { From 09d6070ebd865a8bdefb74f525c4940de8819dae Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 03:09:33 +0100 Subject: [PATCH 5/5] RR2-5: the reader reads a seat-named view in consumes and a seat's views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G's builder and checker follow the assigner's views by the seat, seat:mesh-module-assigner.views.assignments; SeatViewConsume splits that form, and a seat compiled from its definition names the states it offers as views, which the validator checks the form against. Builds: design 47 G §621, §616, §699 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/catalogue/reader_release_test.go | 30 ++++++++++++++++++++++++++++ go/catalogue/seat_from_definition.go | 5 +++++ go/catalogue/seats.go | 3 +++ go/catalogue/state.go | 19 ++++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/go/catalogue/reader_release_test.go b/go/catalogue/reader_release_test.go index 0a4f4e5..4413b13 100644 --- a/go/catalogue/reader_release_test.go +++ b/go/catalogue/reader_release_test.go @@ -5,6 +5,8 @@ import ( "slices" "strings" "testing" + + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // A manifest carrying the words design 47 G writes is read whole by the reader release (design 47 G §340, §409): @@ -69,3 +71,31 @@ func TestTheSecondReaderReleaseReadsGsManifestWords(t *testing.T) { } } } + +// A consumed view of a seat, `seat:.views.`, is read apart from a consumed event, and a seat's compiled form +// names the states its definition offers as views (design 47 G §616, §699, §621). +func TestTheSecondReaderReleaseReadsASeatNamedView(t *testing.T) { + for _, row := range []struct { + consume, seat, state string + ok, bad bool + }{ + {"seat:mesh-module-assigner.views.assignments", "mesh-module-assigner", "assignments", true, false}, + {"mesh-delivery-assigner.views.assignments", "", "", false, false}, + {"seat:mesh-module-assigner.assignments", "", "", true, true}, + {"seat:.views.assignments", "", "", true, true}, + } { + seat, state, ok, err := SeatViewConsume(row.consume) + if seat != row.seat || state != row.state || ok != row.ok || (err != nil) != row.bad { + t.Errorf("SeatViewConsume(%q) = %q, %q, %v, %v", row.consume, seat, state, ok, err) + } + } + m, err := decodeForTest([]byte(`{"module":"m","version":"1.0","consumes":["seat:mesh-module-assigner.views.assignments"]}`)) + if err != nil || len(m.UnknownField()) != 0 || m.Consumes[0] != "seat:mesh-module-assigner.views.assignments" { + t.Errorf("a seat-named view in consumes read as %v, %v", m.Consumes, err) + } + s := SeatFromDefinition(seatdef.Definition{Seat: "mesh-module-assigner", Scope: "mesh", + States: []seatdef.State{{Name: "assignments", View: true}, {Name: "placement"}}}) + if !slices.Equal(s.Views, []string{"assignments"}) { + t.Errorf("the seat's views = %v", s.Views) + } +} diff --git a/go/catalogue/seat_from_definition.go b/go/catalogue/seat_from_definition.go index 1acc52c..5e59183 100644 --- a/go/catalogue/seat_from_definition.go +++ b/go/catalogue/seat_from_definition.go @@ -25,6 +25,11 @@ func SeatFromDefinition(d seatdef.Definition) Seat { Input: seatdef.ResolveRefs(v.Input, d.Defs), Output: seatdef.ResolveRefs(v.Answer, d.Defs), Optional: v.Optional, Replaces: v.Replaces}) } + for _, st := range d.States { + if st.View { + s.Views = append(s.Views, st.Name) + } + } for _, r := range d.Receives { rv := Receivable{Kind: r.Kind, Comment: r.Comment, Dirs: r.Dirs, Offered: r.Offered} if r.Shape != nil { diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 17a21e1..9c0c11c 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -45,6 +45,9 @@ type Seat struct { // Serves carries each verb in full — name, description, schema — because a role's tools are the // mesh's to define and an agent's to call (novox/hq ADR 0132, design 33 §2). Serves []Verb + // Views are the names of the states the seat offers as views, which a module follows by `consumes` + // `seat:.views.` (design 47 G §616, §699). Compiled from the definition's states marked `view`. + Views []string // Receives is what other modules may contribute to the seat's holder, by kind (novox/hq ADR // 0212): each kind is text in the tool's own grammar, placed by the holder with // ${contribution::}. Compiled, never stored: like the protocol, it is the mesh's diff --git a/go/catalogue/state.go b/go/catalogue/state.go index fc6a289..6e53af5 100644 --- a/go/catalogue/state.go +++ b/go/catalogue/state.go @@ -78,6 +78,25 @@ func (s StateDeclaration) MarshalJSON() ([]byte, error) { return json.Marshal(plain(s)) } +// SeatViewPrefix opens a consumed view of a seat, `seat:.views.` (design 47 G §616). +const SeatViewPrefix = "seat:" + +// SeatViewConsume splits a consumed view of a seat, `seat:.views.`, into the seat and the state; ok is false for a +// consumed entry that is not of this kind (an event, `.`), and err says why a `seat:` entry is malformed. The +// holder is not in it: it is resolved where the view is followed, so a new holder is followed with no change of the +// manifest (design 47 G §616, §621). +func SeatViewConsume(consume string) (seat, state string, ok bool, err error) { + rest, isSeat := strings.CutPrefix(consume, SeatViewPrefix) + if !isSeat { + return "", "", false, nil + } + seat, view, named := strings.Cut(rest, ".views.") + if !named || !stateName.MatchString(seat) || !stateName.MatchString(view) { + return "", "", true, fmt.Errorf("%q does not name a seat and its view: a consumed view of a seat is seat:.views.", consume) + } + return seat, view, true, nil +} + // ReadState splits a read into the owning module and the local name, or says why it is not one. func ReadState(read string) (module, local string, err error) { at := strings.LastIndex(read, ".")