Files
mesh-sdk/go/views/tools.go
T
jschoubben fe83d66b62 J4 (A2j finding 1): an owner's read tool answers a key's kept history, so a reader sees what changed before its first view
The view's module history, heard messages and a want's since asked the owners' read tools for a key's history and
were refused: views.ReadTool takes key with history and answers {history: [{revision, at, op, value}]} from the
state's kept values (KeptSource through mesh/state.history; viewstest.Source and Serve from the bucket).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:48:21 +01:00

221 lines
9.0 KiB
Go

package views
import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// KeptSource is a state as a tools bundle reads it through its runtime (stdio.KeptState), as a Source.
type KeptSource struct{ State stdio.KeptState }
// Keys is every key with a value.
func (k KeptSource) Keys() ([]string, error) { return k.State.Keys() }
// Get is one key's value and revision.
func (k KeptSource) Get(key string) (*Entry, bool, error) {
e, err := k.State.Get(key)
if err != nil || e == nil {
return nil, false, err
}
return &Entry{Value: e.Value, Revision: e.Revision, Written: e.At}, true, nil
}
// History is every value the state still keeps for key, oldest first (stdio.KeptState.History): as many as the
// state declares it keeps, deletes included.
func (k KeptSource) History(key string) ([]Revision, error) {
kept, err := k.State.History(key)
if err != nil {
return nil, err
}
out := make([]Revision, 0, len(kept))
for _, r := range kept {
out = append(out, Revision{Revision: r.Revision, At: r.At, Op: r.Op, Value: r.Value})
}
return out, nil
}
// ParseCallers is a callers list as a manifest writes it into an env value: comma-separated, each a module (on any
// machine), <machine>.<module> or person.<name> (stdio.Callers).
func ParseCallers(list string) stdio.Callers {
var out stdio.Callers
for _, c := range strings.Split(list, ",") {
if c = strings.TrimSpace(c); c != "" {
out = append(out, c)
}
}
return out
}
// CallersEnv is the env value of a first-view tool's callers list, in the owner's tools bundle: MESH_VIEW_CALLERS_<TOOL>,
// the tool's name in capitals, a hyphen an underscore.
func CallersEnv(tool string) string {
return "MESH_VIEW_CALLERS_" + strings.ToUpper(strings.ReplaceAll(tool, "-", "_"))
}
// Refusal is why caller may not ask tool, or nil: the caller's proof, never an argument, against the tool's callers
// list holding exactly its readers (ADR 0621 §2, ADR 0622 §1); the owner itself is on no list, so it is refused too.
func Refusal(tool string, callers stdio.Callers, caller string) error {
if callers.Allows(caller) {
return nil
}
return fmt.Errorf("%s is refused to %q: the tool answers only %s (novox/hq ADR 0621 §2)", tool, caller, strings.Join(callers, ", "))
}
// FirstViewTool is an owner's first-view tool of one of its states (ADR 0621 §1): each call answers one page of what the
// state holds now, at the revision its mark says, to the callers its list names and nobody else. It reads only the
// owner's own state, so it answers as soon as the runtime is connected, before the owner has asked anyone anything
// (ADR 0621 §3).
func FirstViewTool(owner, name, state, words string, callers stdio.Callers, values, marks Source, now func() time.Time) stdio.Tool {
return stdio.Tool{
Name: name,
Description: fmt.Sprintf("The first view of %s's state %s (novox/hq ADR 0615, ADR 0621): %s Each value whole by key, at the revision "+
"the owner said its views family up to, so a reader then follows the events on that family and asks again only on a gap in "+
"revisions. One page at a time: next is the key to ask after for the rest. Answered only to %s; any other caller is refused.",
owner, state, words, strings.Join(callers, ", ")),
Input: map[string]any{"type": "object", "properties": map[string]any{
"after": map[string]any{"type": "string", "description": "the key the page before ended at (its next); none starts at the first"}}},
Run: func(args map[string]any) (any, error) {
if err := Refusal(name, callers, stdio.CallerOf(args)); err != nil {
return nil, err
}
after, _ := args["after"].(string)
return FirstView(owner, state, values, marks, strings.TrimSpace(after), now())
},
}
}
// ReadTool is an owner's ordinary read tool of one of its states (design 25 § 12.2): the keys, one key's value and
// revision, one key's kept history (values a Historian), or a page of values in the first view's form, and the state's
// fill against its cap as its mark last said it. Granted, as any tool is, to every reader that invokes it, and callable
// by a session.
func ReadTool(owner, name, state, words string, values, marks Source, now func() time.Time) stdio.Tool {
return stdio.Tool{
Name: name,
Description: fmt.Sprintf("Read %s's state %s: %s With no argument, its keys and how full it is against its cap (as the "+
"owner last marked it); with key, that key's value and revision; with key and history, every value the state still "+
"keeps for that key, oldest first, each with its revision, time and op; with values, a page of values whole by key, "+
"at the revision the owner said its views family up to, continued with after (the page's next).", owner, state, words),
Input: map[string]any{"type": "object", "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "one key: its value and revision"},
"history": map[string]any{"type": "boolean", "description": "with key: every value the state keeps for it, oldest first"},
"values": map[string]any{"type": "boolean", "description": "a page of values whole by key"},
"after": map[string]any{"type": "string", "description": "with values: the key the page before ended at"}}},
Run: func(args map[string]any) (any, error) {
key, _ := args["key"].(string)
after, _ := args["after"].(string)
all, _ := args["values"].(bool)
if history, _ := args["history"].(bool); history {
return ReadHistory(owner, state, values, strings.TrimSpace(key))
}
return Read(owner, state, values, marks, strings.TrimSpace(key), all, strings.TrimSpace(after), now())
},
}
}
// ReadAnswer is a read tool's answer without values: the state's keys and fill.
type ReadAnswer struct {
Owner string `json:"owner"`
State string `json:"state"`
Keys []string `json:"keys"`
Fill *Fill `json:"fill,omitempty"`
Revision uint64 `json:"revision"`
Marked time.Time `json:"marked,omitzero"`
// Words say what the fill means where the owner has not marked the state yet.
Words string `json:"words,omitempty"`
}
// KeyAnswer is one key of a read tool's answer.
type KeyAnswer struct {
Owner string `json:"owner"`
State string `json:"state"`
Key string `json:"key"`
Found bool `json:"found"`
Value json.RawMessage `json:"value,omitempty"`
Revision uint64 `json:"revision,omitempty"`
}
// Revision is one value a state keeps for a key: its revision, when the bus wrote it, its op (put, or delete) and
// its value, null for a delete.
type Revision struct {
Revision uint64 `json:"revision"`
At time.Time `json:"at"`
Op string `json:"op"`
Value json.RawMessage `json:"value"`
}
// Historian is a Source that also keeps each key's history (KeptSource: the state's own kept values).
type Historian interface {
History(key string) ([]Revision, error)
}
// HistoryAnswer is a read tool's answer of one key's history: every value kept, oldest first; none kept is empty.
type HistoryAnswer struct {
Owner string `json:"owner"`
State string `json:"state"`
Key string `json:"key"`
History []Revision `json:"history"`
}
// ReadHistory is a read tool's answer of key's history from values, refused when no key is named or values keeps none.
func ReadHistory(owner, state string, values Source, key string) (HistoryAnswer, error) {
if key == "" {
return HistoryAnswer{}, fmt.Errorf("a history of %s is of one key: name it", state)
}
h, ok := values.(Historian)
if !ok {
return HistoryAnswer{}, fmt.Errorf("%s's state %s keeps no history of its keys here", owner, state)
}
kept, err := h.History(key)
if err != nil {
return HistoryAnswer{}, fmt.Errorf("the history of %s.%s cannot be read: %w", state, key, err)
}
if kept == nil {
kept = []Revision{}
}
return HistoryAnswer{Owner: owner, State: state, Key: key, History: kept}, nil
}
// Read is a read tool's answer: one key, a page of values, or the keys and fill.
func Read(owner, state string, values, marks Source, key string, all bool, after string, now time.Time) (any, error) {
switch {
case key != "":
e, ok, err := values.Get(key)
if err != nil {
return nil, fmt.Errorf("%s.%s cannot be read: %w", state, key, err)
}
a := KeyAnswer{Owner: owner, State: state, Key: key, Found: ok}
if ok {
a.Value, a.Revision = e.Value, e.Revision
}
return a, nil
case all:
return FirstView(owner, state, values, marks, after, now)
}
keys, err := values.Keys()
if err != nil {
return nil, fmt.Errorf("the state %s cannot be listed: %w", state, err)
}
slices.Sort(keys)
a := ReadAnswer{Owner: owner, State: state, Keys: keys}
if marks != nil {
if e, ok, err := marks.Get(state); err == nil && ok {
if m, err := DecodeMark(e.Value); err == nil {
fill := m.Fill
a.Fill, a.Revision, a.Marked = &fill, m.Revision, m.At
}
}
}
if a.Fill == nil {
a.Words = "the owner has not marked this state yet: its fill is said once its views family says a change of it"
}
if a.Keys == nil {
a.Keys = []string{}
}
return a, nil
}