From d813ce7e3976b973f9d752d6d94da83db2bf1ef9 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 11:45:19 +0100 Subject: [PATCH] FB4a: the forge message forms of an owed act's outcome (owed.ended) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../forgemsg/forge_test.go | 34 +++++++++ .../forgemsg/owed.go | 76 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 modules/mesh-delivery-forge-bridge/forgemsg/owed.go diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go b/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go index a94f4779..18c75b4c 100644 --- a/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go +++ b/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go @@ -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) + } + } + }) } diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/owed.go b/modules/mesh-delivery-forge-bridge/forgemsg/owed.go new file mode 100644 index 00000000..b8400a36 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/forgemsg/owed.go @@ -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...`). +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 +}