FB4a: the forge message forms of an owed act's outcome (owed.ended)

The gate's developer needs OwedEnded and its outcomes; fields are the ones §164 and §168 name (id, outcome, words, the seat that said the act, time).

Builds: design 47 G §164, §168

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 11:45:19 +01:00
parent bcd0888928
commit d813ce7e39
2 changed files with 110 additions and 0 deletions
@@ -5,6 +5,7 @@ import (
"errors"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-sdk/go/keys"
@@ -191,4 +192,37 @@ func TestForge(t *testing.T) {
}
}
})
// Design 47 G §164: an owed act's outcome names its seat, its id, one outcome and words for every outcome but
// performed; an act the bridge cannot decode may name no id.
t.Run("owed ended", func(t *testing.T) {
at := time.Date(2026, 11, 9, 20, 0, 0, 0, time.UTC)
ok := OwedEnded{Of: "mesh-delivery", ID: "g1.view.1", Outcome: OwedPerformed, At: at}
raw, err := EncodeOwedEnded(ok)
if back, derr := DecodeOwedEnded(raw); err != nil || derr != nil || back.V != Version || back.ID != ok.ID {
t.Fatalf("round trip: %s %v %v", raw, err, derr)
}
if _, err := DecodeOwedEnded([]byte(`{"v":99,"of":"x","id":"a","outcome":"performed","at":"2026-11-09T20:00:00Z"}`)); err == nil {
t.Errorf("an unknown version was read")
}
for _, c := range []struct {
name string
edit func(*OwedEnded)
want string
}{
{"no seat", func(e *OwedEnded) { e.Of = "" }, "no seat"},
{"an outcome of no kind", func(e *OwedEnded) { e.Outcome = "done" }, "none of"},
{"no id", func(e *OwedEnded) { e.ID = "" }, "no id"},
{"a refusal with no words", func(e *OwedEnded) { e.Outcome = OwedRefused }, "no words"},
{"dropped with no reason", func(e *OwedEnded) { e.Outcome = OwedDropped }, "no words"},
{"no time", func(e *OwedEnded) { e.At = time.Time{} }, "no time"},
{"undecodable with no id is said", func(e *OwedEnded) { e.ID, e.Outcome, e.Words = "", OwedUndecodable, "not json" }, ""},
} {
e := ok
c.edit(&e)
_, err := EncodeOwedEnded(e)
if (c.want == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.want)) {
t.Errorf("%s: %v, want %q", c.name, err, c.want)
}
}
})
}
@@ -0,0 +1,76 @@
package forgemsg
import (
"encoding/json"
"fmt"
"slices"
"time"
)
// OwedEndedEvent is the name of an owed act's outcome on the forge bridge's stream `forge` (design 47 G §164): the
// bridge says it for each act the gate or the checker said as the event `owed`. Its two parts are no repository's
// forge event (`forge.<owner>.<name>.<kind>`).
const OwedEndedEvent = "owed.ended"
// The outcomes of an owed act (design 47 G §164, §168): performed, refused (the forge's own words), could not be
// performed (the forge did not answer within the call's bound, with the error), undecodable (an act the bridge cannot
// read, with why) and, from the tool `bridge_owed_drop`, dropped.
const (
OwedPerformed = "performed"
OwedRefused = "refused"
OwedCouldNotBePerformed = "could not be performed"
OwedUndecodable = "undecodable"
OwedDropped = "dropped"
)
// OwedOutcomes are the outcomes an owed.ended says, one of them.
var OwedOutcomes = []string{OwedPerformed, OwedRefused, OwedCouldNotBePerformed, OwedUndecodable, OwedDropped}
// OwedEnded is an owed act's outcome (design 47 G §164): the act's id and one outcome, with the words of any outcome
// but performed. Of is the seat that said the act (mesh-delivery for the gate's, mesh-delivery-checker for the
// checker's), so each hears its own ids (§164). An act the bridge cannot decode may name no id.
type OwedEnded struct {
V int `json:"v"`
Of string `json:"of"`
ID string `json:"id,omitempty"`
Outcome string `json:"outcome"`
Words string `json:"words,omitempty"`
At time.Time `json:"at"`
}
func (e OwedEnded) check() error {
switch {
case e.Of == "":
return fmt.Errorf("an owed act's outcome names no seat that said it")
case !slices.Contains(OwedOutcomes, e.Outcome):
return fmt.Errorf("the outcome %q of an owed act is none of %v", e.Outcome, OwedOutcomes)
case e.ID == "" && e.Outcome != OwedUndecodable:
return fmt.Errorf("the outcome %s of an owed act names no id", e.Outcome)
case e.Outcome != OwedPerformed && e.Words == "":
return fmt.Errorf("the outcome %s of the owed act %s says no words", e.Outcome, e.ID)
case e.At.IsZero():
return fmt.Errorf("the outcome of the owed act %s says no time", e.ID)
}
return nil
}
// EncodeOwedEnded is e as JSON at Version; it refuses what check refuses.
func EncodeOwedEnded(e OwedEnded) ([]byte, error) {
if err := e.check(); err != nil {
return nil, err
}
e.V = Version
return json.Marshal(e)
}
// DecodeOwedEnded reads an owed act's outcome; it refuses an unknown version and what EncodeOwedEnded refuses.
func DecodeOwedEnded(data []byte) (OwedEnded, error) {
var e OwedEnded
if err := decode(data, &e); err != nil {
return OwedEnded{}, err
}
if err := e.check(); err != nil {
return OwedEnded{}, err
}
return e, nil
}