Ask an acknowledgement apart from an approval, change every kept ask by compare-and-set, and rehearse rather than drill (hq ADR 0259, review M1/L2/L3/L7)
- M1: a condition offering both kinds of answer is asked twice: its authorising answers about the condition, its acknowledging ones (Silence) apart, so an answer from a channel that only acknowledges never ends an approval. - L2: the asked store creates once and changes only over the revision it read, deciding again on what it reads; a stale cancel no longer writes over an act. - L3: every ask is kept before it is published, one whose publishing failed is marked unsent and asked again, and a cancel is kept before it is said. The terminal's test question is now `rehearse`, so it is not called what the glossary calls a drill; its two answers are both approve-level. - L7: two deliveries of one warrant to two controllers at once act exactly once, on a real bus. - Re-vendored onto mesh-sdk 76902998 (canonical digests): an option binds an asks.Act with each argument as arg.<name>. - The lab's bus fixture composes verified-sender only where the lab says its machine is root-free (MESH_LAB_ASKS_ROOT_FREE=true).
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
// The rehearsal of the operator's answers — not a drill, which in the glossary is something broken on purpose (novox/hq ADR 0259, the live acceptance after rollout): an ask the
|
||||
// operator starts at the controller's terminal, answered on the phone, whose approval changes nothing and is
|
||||
// recorded as a person's decision like any other.
|
||||
//
|
||||
// mesh-controller rehearse [--for 15m]
|
||||
//
|
||||
// It asks with two answers, Approve and Decline, each bound to the rehearsal's own act and **both at the level
|
||||
// approve** (the review of 2026-10-09, M1: an acknowledgement never shares an ask with an approval), so only a
|
||||
// channel that proves who answered carries either — the rehearsal is of exactly that. The serving controller acts on the warrant
|
||||
// as on any other: it claims the ask once, checks the act is the one bound, performs nothing, and records the
|
||||
// hand-act `warrant` with who answered, through which channel, and the proofs. `hand-acts` then shows it.
|
||||
//
|
||||
// **The terminal's alone**: a command a verb runs (MESH_VERB set) is refused, so no agent starts a rehearsal — a
|
||||
// rehearsal is a question the operator expects, and one an agent could start would teach them to approve what they
|
||||
// did not ask for.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
)
|
||||
|
||||
// rehearsalVerb is the act a rehearsal's answers bind: nothing is called.
|
||||
const rehearsalVerb = "rehearsal"
|
||||
|
||||
// rehearsalActions are the rehearsal's two answers.
|
||||
func rehearsalActions() []conditions.Action {
|
||||
return []conditions.Action{
|
||||
{Label: "Approve", Verb: rehearsalVerb, Level: conditions.LevelApprove, Arguments: map[string]string{"rehearsal": "approve"}},
|
||||
{Label: "Decline", Verb: rehearsalVerb, Level: conditions.LevelApprove, Arguments: map[string]string{"rehearsal": "decline"}},
|
||||
}
|
||||
}
|
||||
|
||||
// rehearsalAsk is the rehearsal's ask, as the router is sent it.
|
||||
func rehearsalAsk(id string, now time.Time, lasts time.Duration) (asks.Ask, map[string]int) {
|
||||
q := asks.Ask{ID: id, Headline: "Rehearsal: approve this test question?", Who: asks.Operator,
|
||||
Explanation: "Needs you: approve or decline. You started this rehearsal at the controller's terminal. Approving " +
|
||||
"changes nothing on the mesh; it is recorded as your decision, so you can check the record.",
|
||||
OnExpiry: "nothing is done", Expires: now.Add(lasts), About: "rehearsal." + id}
|
||||
options := map[string]int{}
|
||||
for i, act := range rehearsalActions() {
|
||||
binds, _ := asks.ActDigest(boundAct(act))
|
||||
oid := optionID(act.Label)
|
||||
options[oid] = i
|
||||
q.Options = append(q.Options, asks.Option{ID: oid, Label: act.Label, Does: doesRehearsal(act), Level: asks.Level(act.Level),
|
||||
Binds: binds})
|
||||
}
|
||||
return q, options
|
||||
}
|
||||
|
||||
func doesRehearsal(act conditions.Action) string {
|
||||
if act.Arguments["rehearsal"] == "approve" {
|
||||
return "nothing changes; your approval is recorded"
|
||||
}
|
||||
return "nothing changes; your answer is recorded"
|
||||
}
|
||||
|
||||
func rehearseCommand(ctx context.Context, args []string) error {
|
||||
if os.Getenv(verbVar) != "" {
|
||||
return errors.New("rehearse is the controller's terminal's alone: a verb may not start one, so no agent asks " +
|
||||
"the operator a question they did not start (novox/hq ADR 0259)")
|
||||
}
|
||||
set := flag.NewFlagSet("rehearse", flag.ContinueOnError)
|
||||
lasts := set.Duration("for", 15*time.Minute, "how long the question waits for an answer")
|
||||
if err := set.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *lasts < time.Minute || *lasts > askApproveFor {
|
||||
return fmt.Errorf("a rehearsal waits between a minute and %s", askApproveFor)
|
||||
}
|
||||
js, err := aBus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer js.Close()
|
||||
now := time.Now()
|
||||
id := newAskID()
|
||||
q, options := rehearsalAsk(id, now, *lasts)
|
||||
if err := q.Check(now); err != nil {
|
||||
return err
|
||||
}
|
||||
store := busAsked{conn: js.Conn()}
|
||||
// Kept before it is published, as the asker keeps every ask, so a warrant always finds it.
|
||||
if err := store.Create(ctx, asked{ID: id, Condition: q.About, Ask: q, Actions: rehearsalActions(), Options: options,
|
||||
State: askOpen, Opened: now, Rehearsal: true}); err != nil {
|
||||
return fmt.Errorf("the rehearsal could not be kept in the controller's asks: %w", err)
|
||||
}
|
||||
body, err := json.Marshal(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := js.Context().Publish(asks.AskSubject(askerName), body, nats.MsgId("ask."+id), nats.Context(ctx)); err != nil {
|
||||
return fmt.Errorf("the rehearsal could not be asked: %w", err)
|
||||
}
|
||||
fmt.Printf("rehearsal %s asked: answer it on your phone before %s. Then `mesh-controller hand-acts` shows the "+
|
||||
"answer as a warrant, with who answered, through which channel and the proofs; nothing else changes.\n",
|
||||
id, q.Expires.Local().Format("15:04"))
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user