diff --git a/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/contract.go b/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/contract.go new file mode 100644 index 00000000..9368a0ba --- /dev/null +++ b/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/contract.go @@ -0,0 +1,364 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshdeliveryview is the contract of the seat "mesh-delivery-view", defined whole (seats/mesh-delivery-view/seat.json): The status view of deliveries (ADR 0540): it serves no verb and says no seat event; its tools and its events delivered, moving, settled and stuck stay its holder's own, and it keeps the person's answers given in the web view. +package meshdeliveryview + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-delivery-view" + +const ( + StateAnswers = "answers" +) + +// AnswersByHoldModuleFingerprintViewKey is the key of one value of the state answers under the form "hold...view". +func AnswersByHoldModuleFingerprintViewKey(module, fingerprint string) string { + return "hold." + module + "." + fingerprint + ".view" +} + +// ParseAnswersByHoldModuleFingerprintViewKey reads the parts of a key of the form "hold...view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByHoldModuleFingerprintViewKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "hold.") { + return "", "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + v3, after3, found3 := strings.Cut(rest, ".view") + if !found3 || v3 == "" { + return "", "", false + } + rest = after3 + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeAnswersByHoldModuleFingerprintViewValue reads one value of the state answers under the form "hold...view", refusing a field AnswersByHoldModuleFingerprintViewValue does not know and any text after the value. +func DecodeAnswersByHoldModuleFingerprintViewValue(raw []byte) (AnswersByHoldModuleFingerprintViewValue, error) { + var v AnswersByHoldModuleFingerprintViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AnswersByRestoreNodeModuleFingerprintViewKey is the key of one value of the state answers under the form "restore....view". +func AnswersByRestoreNodeModuleFingerprintViewKey(node, module, fingerprint string) string { + return "restore." + node + "." + module + "." + fingerprint + ".view" +} + +// ParseAnswersByRestoreNodeModuleFingerprintViewKey reads the parts of a key of the form "restore....view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByRestoreNodeModuleFingerprintViewKey(key string) (string, string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "restore.") { + return "", "", "", false + } + rest = rest[8:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", "", false + } + rest = after1 + v3, after3, found3 := strings.Cut(rest, ".") + if !found3 || v3 == "" { + return "", "", "", false + } + rest = after3 + v5, after5, found5 := strings.Cut(rest, ".view") + if !found5 || v5 == "" { + return "", "", "", false + } + rest = after5 + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAnswersByRestoreNodeModuleFingerprintViewValue reads one value of the state answers under the form "restore....view", refusing a field AnswersByRestoreNodeModuleFingerprintViewValue does not know and any text after the value. +func DecodeAnswersByRestoreNodeModuleFingerprintViewValue(raw []byte) (AnswersByRestoreNodeModuleFingerprintViewValue, error) { + var v AnswersByRestoreNodeModuleFingerprintViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AnswersByBackupNodeModuleFingerprintViewKey is the key of one value of the state answers under the form "backup....view". +func AnswersByBackupNodeModuleFingerprintViewKey(node, module, fingerprint string) string { + return "backup." + node + "." + module + "." + fingerprint + ".view" +} + +// ParseAnswersByBackupNodeModuleFingerprintViewKey reads the parts of a key of the form "backup....view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByBackupNodeModuleFingerprintViewKey(key string) (string, string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "backup.") { + return "", "", "", false + } + rest = rest[7:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", "", false + } + rest = after1 + v3, after3, found3 := strings.Cut(rest, ".") + if !found3 || v3 == "" { + return "", "", "", false + } + rest = after3 + v5, after5, found5 := strings.Cut(rest, ".view") + if !found5 || v5 == "" { + return "", "", "", false + } + rest = after5 + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAnswersByBackupNodeModuleFingerprintViewValue reads one value of the state answers under the form "backup....view", refusing a field AnswersByBackupNodeModuleFingerprintViewValue does not know and any text after the value. +func DecodeAnswersByBackupNodeModuleFingerprintViewValue(raw []byte) (AnswersByBackupNodeModuleFingerprintViewValue, error) { + var v AnswersByBackupNodeModuleFingerprintViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AnswersByRecordNodeModuleDigestViewKey is the key of one value of the state answers under the form "record....view". +func AnswersByRecordNodeModuleDigestViewKey(node, module, digest string) string { + return "record." + node + "." + module + "." + digest + ".view" +} + +// ParseAnswersByRecordNodeModuleDigestViewKey reads the parts of a key of the form "record....view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByRecordNodeModuleDigestViewKey(key string) (string, string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "record.") { + return "", "", "", false + } + rest = rest[7:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", "", false + } + rest = after1 + v3, after3, found3 := strings.Cut(rest, ".") + if !found3 || v3 == "" { + return "", "", "", false + } + rest = after3 + v5, after5, found5 := strings.Cut(rest, ".view") + if !found5 || v5 == "" { + return "", "", "", false + } + rest = after5 + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAnswersByRecordNodeModuleDigestViewValue reads one value of the state answers under the form "record....view", refusing a field AnswersByRecordNodeModuleDigestViewValue does not know and any text after the value. +func DecodeAnswersByRecordNodeModuleDigestViewValue(raw []byte) (AnswersByRecordNodeModuleDigestViewValue, error) { + var v AnswersByRecordNodeModuleDigestViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AnswersByOutsideNodeModuleFingerprintViewKey is the key of one value of the state answers under the form "outside....view". +func AnswersByOutsideNodeModuleFingerprintViewKey(node, module, fingerprint string) string { + return "outside." + node + "." + module + "." + fingerprint + ".view" +} + +// ParseAnswersByOutsideNodeModuleFingerprintViewKey reads the parts of a key of the form "outside....view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByOutsideNodeModuleFingerprintViewKey(key string) (string, string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "outside.") { + return "", "", "", false + } + rest = rest[8:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", "", false + } + rest = after1 + v3, after3, found3 := strings.Cut(rest, ".") + if !found3 || v3 == "" { + return "", "", "", false + } + rest = after3 + v5, after5, found5 := strings.Cut(rest, ".view") + if !found5 || v5 == "" { + return "", "", "", false + } + rest = after5 + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAnswersByOutsideNodeModuleFingerprintViewValue reads one value of the state answers under the form "outside....view", refusing a field AnswersByOutsideNodeModuleFingerprintViewValue does not know and any text after the value. +func DecodeAnswersByOutsideNodeModuleFingerprintViewValue(raw []byte) (AnswersByOutsideNodeModuleFingerprintViewValue, error) { + var v AnswersByOutsideNodeModuleFingerprintViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AnswersByRevertNodeModuleFingerprintViewKey is the key of one value of the state answers under the form "revert....view". +func AnswersByRevertNodeModuleFingerprintViewKey(node, module, fingerprint string) string { + return "revert." + node + "." + module + "." + fingerprint + ".view" +} + +// ParseAnswersByRevertNodeModuleFingerprintViewKey reads the parts of a key of the form "revert....view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByRevertNodeModuleFingerprintViewKey(key string) (string, string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "revert.") { + return "", "", "", false + } + rest = rest[7:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", "", false + } + rest = after1 + v3, after3, found3 := strings.Cut(rest, ".") + if !found3 || v3 == "" { + return "", "", "", false + } + rest = after3 + v5, after5, found5 := strings.Cut(rest, ".view") + if !found5 || v5 == "" { + return "", "", "", false + } + rest = after5 + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAnswersByRevertNodeModuleFingerprintViewValue reads one value of the state answers under the form "revert....view", refusing a field AnswersByRevertNodeModuleFingerprintViewValue does not know and any text after the value. +func DecodeAnswersByRevertNodeModuleFingerprintViewValue(raw []byte) (AnswersByRevertNodeModuleFingerprintViewValue, error) { + var v AnswersByRevertNodeModuleFingerprintViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// AnswersByAssignmentsProposalViewKey is the key of one value of the state answers under the form "assignments..view". +func AnswersByAssignmentsProposalViewKey(proposal string) string { + return "assignments." + proposal + ".view" +} + +// ParseAnswersByAssignmentsProposalViewKey reads the parts of a key of the form "assignments..view", in the order of the form; the last result is false for a key of another form. +func ParseAnswersByAssignmentsProposalViewKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "assignments.") { + return "", false + } + rest = rest[12:] + v1, after1, found1 := strings.Cut(rest, ".view") + if !found1 || v1 == "" { + return "", false + } + rest = after1 + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeAnswersByAssignmentsProposalViewValue reads one value of the state answers under the form "assignments..view", refusing a field AnswersByAssignmentsProposalViewValue does not know and any text after the value. +func DecodeAnswersByAssignmentsProposalViewValue(raw []byte) (AnswersByAssignmentsProposalViewValue, error) { + var v AnswersByAssignmentsProposalViewValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Answer is the status view's answer to an ask. +type Answer struct { + // the asking it answers + Asking string `json:"asking"` + // the option the person chose + Option string `json:"option"` + V int64 `json:"v,omitempty"` + // the person's words with it + Words string `json:"words,omitempty"` +} + +// AnswersByHoldModuleFingerprintViewValue is one value of the state answers under the key hold...view. +type AnswersByHoldModuleFingerprintViewValue = Answer + +// AnswersByRestoreNodeModuleFingerprintViewValue is one value of the state answers under the key restore....view. +type AnswersByRestoreNodeModuleFingerprintViewValue = Answer + +// AnswersByBackupNodeModuleFingerprintViewValue is one value of the state answers under the key backup....view. +type AnswersByBackupNodeModuleFingerprintViewValue = Answer + +// AnswersByRecordNodeModuleDigestViewValue is one value of the state answers under the key record....view. +type AnswersByRecordNodeModuleDigestViewValue = Answer + +// AnswersByOutsideNodeModuleFingerprintViewValue is one value of the state answers under the key outside....view. +type AnswersByOutsideNodeModuleFingerprintViewValue = Answer + +// AnswersByRevertNodeModuleFingerprintViewValue is one value of the state answers under the key revert....view. +type AnswersByRevertNodeModuleFingerprintViewValue = Answer + +// AnswersByAssignmentsProposalViewValue is one value of the state answers under the key assignments..view. +type AnswersByAssignmentsProposalViewValue = Answer diff --git a/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/contract_test.go b/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/contract_test.go new file mode 100644 index 00000000..21f78b34 --- /dev/null +++ b/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/contract_test.go @@ -0,0 +1,52 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshdeliveryview + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-delivery-view/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "an answer to a hold ask, given in the web view", subject: "", given: "{\"asking\":\"hold.web.0123456789ab.2\",\"option\":\"release\",\"v\":1,\"words\":\"released from the web view\"}", wire: "{\"asking\":\"hold.web.0123456789ab.2\",\"option\":\"release\",\"v\":1,\"words\":\"released from the web view\"}", + decode: func(b []byte) (any, error) { + var v AnswersByHoldModuleFingerprintViewValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/go.mod b/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/go.mod new file mode 100644 index 00000000..ca9153f4 --- /dev/null +++ b/modules/mesh-delivery-contracts/go/seats/meshdeliveryview/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdeliveryview + +go 1.26.0 diff --git a/modules/mesh-delivery-contracts/module.json b/modules/mesh-delivery-contracts/module.json index 1fda11a3..a42e77e2 100644 --- a/modules/mesh-delivery-contracts/module.json +++ b/modules/mesh-delivery-contracts/module.json @@ -306,6 +306,26 @@ "series": "0.1", "generated-from": "." }, + { + "name": "go-meshdeliveryview", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshdeliveryview" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshdeliveryview", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshdeliveryview" + ], + "series": "0.1", + "generated-from": "." + }, { "name": "go-meshwantsclient", "kind": "package", diff --git a/modules/mesh-delivery-contracts/seats/mesh-delivery-view/fixtures/states.json b/modules/mesh-delivery-contracts/seats/mesh-delivery-view/fixtures/states.json new file mode 100644 index 00000000..d3e12bf0 --- /dev/null +++ b/modules/mesh-delivery-contracts/seats/mesh-delivery-view/fixtures/states.json @@ -0,0 +1,12 @@ +[ + { + "name": "an answer to a hold ask, given in the web view", + "of": "state:answers/hold...view", + "given": { + "v": 1, + "asking": "hold.web.0123456789ab.2", + "option": "release", + "words": "released from the web view" + } + } +] diff --git a/modules/mesh-delivery-contracts/seats/mesh-delivery-view/seat.json b/modules/mesh-delivery-contracts/seats/mesh-delivery-view/seat.json new file mode 100644 index 00000000..9b78f1f0 --- /dev/null +++ b/modules/mesh-delivery-contracts/seats/mesh-delivery-view/seat.json @@ -0,0 +1,96 @@ +{ + "format": 1, + "seat": "mesh-delivery-view", + "scope": "mesh", + "grain": "whole", + "domain": "Delivery", + "decision": "novox/hq ADR 0540", + "description": "The status view of deliveries (ADR 0540): it serves no verb and says no seat event; its tools and its events delivered, moving, settled and stuck stay its holder's own, and it keeps the person's answers given in the web view.", + "states": [ + { + "name": "answers", + "template": "state", + "view": true, + "history": 5, + "description": "the person's answer in the web view to an ask of the seat mesh-wants' state asks of a kind that needs no proof of who answered; only the holder writes a key, and only the holder deletes it, once the ask's key has left asks (design 47 G section 717)", + "key-forms": [ + { + "key": "hold...view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under hold.. (design 47 G A-299)" + }, + { + "key": "restore....view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under restore... (design 47 G A-299)" + }, + { + "key": "backup....view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under backup... (design 47 G A-299)" + }, + { + "key": "record....view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under record... (design 47 G A-299)" + }, + { + "key": "outside....view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under outside... (design 47 G A-299)" + }, + { + "key": "revert....view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under revert... (design 47 G A-299)" + }, + { + "key": "assignments..view", + "value": { + "$ref": "#/defs/Answer" + }, + "description": "the status view's answer to the ask of the seat mesh-wants' state asks under assignments. (design 47 G A-299)" + } + ] + } + ], + "defs": { + "Answer": { + "type": "object", + "description": "the status view's answer to an ask", + "properties": { + "v": { + "type": "integer" + }, + "asking": { + "type": "string", + "description": "the asking it answers" + }, + "option": { + "type": "string", + "description": "the option the person chose" + }, + "words": { + "type": "string", + "description": "the person's words with it" + } + }, + "required": [ + "asking", + "option" + ] + } + } +} diff --git a/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/index.d.ts b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/index.d.ts new file mode 100644 index 00000000..7065c3a8 --- /dev/null +++ b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/index.d.ts @@ -0,0 +1,84 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-delivery-view", defined whole (seats/mesh-delivery-view/seat.json): The status view of deliveries (ADR 0540): it serves no verb and says no seat event; its tools and its events delivered, moving, settled and stuck stay its holder's own, and it keeps the person's answers given in the web view. + +export declare const seat: "mesh-delivery-view"; + +export declare const states: { + readonly answers: "answers"; +}; + +/** The key of one value of the state answers under the form "hold...view". */ +export declare function answersByHoldModuleFingerprintViewKey(module: string, fingerprint: string): string; + +/** The parts of a key of the form "hold...view", or undefined for a key of another form. */ +export declare function parseAnswersByHoldModuleFingerprintViewKey(_key: string): { module: string; fingerprint: string } | undefined; + +/** The key of one value of the state answers under the form "restore....view". */ +export declare function answersByRestoreNodeModuleFingerprintViewKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "restore....view", or undefined for a key of another form. */ +export declare function parseAnswersByRestoreNodeModuleFingerprintViewKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state answers under the form "backup....view". */ +export declare function answersByBackupNodeModuleFingerprintViewKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "backup....view", or undefined for a key of another form. */ +export declare function parseAnswersByBackupNodeModuleFingerprintViewKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state answers under the form "record....view". */ +export declare function answersByRecordNodeModuleDigestViewKey(node: string, module: string, digest: string): string; + +/** The parts of a key of the form "record....view", or undefined for a key of another form. */ +export declare function parseAnswersByRecordNodeModuleDigestViewKey(_key: string): { node: string; module: string; digest: string } | undefined; + +/** The key of one value of the state answers under the form "outside....view". */ +export declare function answersByOutsideNodeModuleFingerprintViewKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "outside....view", or undefined for a key of another form. */ +export declare function parseAnswersByOutsideNodeModuleFingerprintViewKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state answers under the form "revert....view". */ +export declare function answersByRevertNodeModuleFingerprintViewKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "revert....view", or undefined for a key of another form. */ +export declare function parseAnswersByRevertNodeModuleFingerprintViewKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state answers under the form "assignments..view". */ +export declare function answersByAssignmentsProposalViewKey(proposal: string): string; + +/** The parts of a key of the form "assignments..view", or undefined for a key of another form. */ +export declare function parseAnswersByAssignmentsProposalViewKey(_key: string): { proposal: string } | undefined; + +/** the status view's answer to an ask */ +export interface Answer { + /** the asking it answers */ + "asking": string; + /** the option the person chose */ + "option": string; + "v"?: number; + /** the person's words with it */ + "words"?: string; +} + +/** is one value of the state answers under the key hold...view. */ +export type AnswersByHoldModuleFingerprintViewValue = Answer; + +/** is one value of the state answers under the key restore....view. */ +export type AnswersByRestoreNodeModuleFingerprintViewValue = Answer; + +/** is one value of the state answers under the key backup....view. */ +export type AnswersByBackupNodeModuleFingerprintViewValue = Answer; + +/** is one value of the state answers under the key record....view. */ +export type AnswersByRecordNodeModuleDigestViewValue = Answer; + +/** is one value of the state answers under the key outside....view. */ +export type AnswersByOutsideNodeModuleFingerprintViewValue = Answer; + +/** is one value of the state answers under the key revert....view. */ +export type AnswersByRevertNodeModuleFingerprintViewValue = Answer; + +/** is one value of the state answers under the key assignments..view. */ +export type AnswersByAssignmentsProposalViewValue = Answer; + diff --git a/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/index.js b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/index.js new file mode 100644 index 00000000..7666fe1b --- /dev/null +++ b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/index.js @@ -0,0 +1,180 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-delivery-view", defined whole (seats/mesh-delivery-view/seat.json): The status view of deliveries (ADR 0540): it serves no verb and says no seat event; its tools and its events delivered, moving, settled and stuck stay its holder's own, and it keeps the person's answers given in the web view. + +export const seat = "mesh-delivery-view"; + +export const states = { + answers: "answers", +}; + +/** The key of one value of the state answers under the form "hold...view". */ +export function answersByHoldModuleFingerprintViewKey(module, fingerprint) { + return "hold." + module + "." + fingerprint + ".view"; +} + +/** The parts of a key of the form "hold...view", or undefined for a key of another form. */ +export function parseAnswersByHoldModuleFingerprintViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("hold.")) return undefined; + _rest = _rest.slice(5); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const module = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + const _at3 = _rest.indexOf(".view"); + if (_at3 <= 0) return undefined; + const fingerprint = _rest.slice(0, _at3); + _rest = _rest.slice(_at3 + 5); + if (_rest !== "") return undefined; + return { module, fingerprint }; +} + +/** The key of one value of the state answers under the form "restore....view". */ +export function answersByRestoreNodeModuleFingerprintViewKey(node, module, fingerprint) { + return "restore." + node + "." + module + "." + fingerprint + ".view"; +} + +/** The parts of a key of the form "restore....view", or undefined for a key of another form. */ +export function parseAnswersByRestoreNodeModuleFingerprintViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("restore.")) return undefined; + _rest = _rest.slice(8); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + const _at3 = _rest.indexOf("."); + if (_at3 <= 0) return undefined; + const module = _rest.slice(0, _at3); + _rest = _rest.slice(_at3 + 1); + const _at5 = _rest.indexOf(".view"); + if (_at5 <= 0) return undefined; + const fingerprint = _rest.slice(0, _at5); + _rest = _rest.slice(_at5 + 5); + if (_rest !== "") return undefined; + return { node, module, fingerprint }; +} + +/** The key of one value of the state answers under the form "backup....view". */ +export function answersByBackupNodeModuleFingerprintViewKey(node, module, fingerprint) { + return "backup." + node + "." + module + "." + fingerprint + ".view"; +} + +/** The parts of a key of the form "backup....view", or undefined for a key of another form. */ +export function parseAnswersByBackupNodeModuleFingerprintViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("backup.")) return undefined; + _rest = _rest.slice(7); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + const _at3 = _rest.indexOf("."); + if (_at3 <= 0) return undefined; + const module = _rest.slice(0, _at3); + _rest = _rest.slice(_at3 + 1); + const _at5 = _rest.indexOf(".view"); + if (_at5 <= 0) return undefined; + const fingerprint = _rest.slice(0, _at5); + _rest = _rest.slice(_at5 + 5); + if (_rest !== "") return undefined; + return { node, module, fingerprint }; +} + +/** The key of one value of the state answers under the form "record....view". */ +export function answersByRecordNodeModuleDigestViewKey(node, module, digest) { + return "record." + node + "." + module + "." + digest + ".view"; +} + +/** The parts of a key of the form "record....view", or undefined for a key of another form. */ +export function parseAnswersByRecordNodeModuleDigestViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("record.")) return undefined; + _rest = _rest.slice(7); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + const _at3 = _rest.indexOf("."); + if (_at3 <= 0) return undefined; + const module = _rest.slice(0, _at3); + _rest = _rest.slice(_at3 + 1); + const _at5 = _rest.indexOf(".view"); + if (_at5 <= 0) return undefined; + const digest = _rest.slice(0, _at5); + _rest = _rest.slice(_at5 + 5); + if (_rest !== "") return undefined; + return { node, module, digest }; +} + +/** The key of one value of the state answers under the form "outside....view". */ +export function answersByOutsideNodeModuleFingerprintViewKey(node, module, fingerprint) { + return "outside." + node + "." + module + "." + fingerprint + ".view"; +} + +/** The parts of a key of the form "outside....view", or undefined for a key of another form. */ +export function parseAnswersByOutsideNodeModuleFingerprintViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("outside.")) return undefined; + _rest = _rest.slice(8); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + const _at3 = _rest.indexOf("."); + if (_at3 <= 0) return undefined; + const module = _rest.slice(0, _at3); + _rest = _rest.slice(_at3 + 1); + const _at5 = _rest.indexOf(".view"); + if (_at5 <= 0) return undefined; + const fingerprint = _rest.slice(0, _at5); + _rest = _rest.slice(_at5 + 5); + if (_rest !== "") return undefined; + return { node, module, fingerprint }; +} + +/** The key of one value of the state answers under the form "revert....view". */ +export function answersByRevertNodeModuleFingerprintViewKey(node, module, fingerprint) { + return "revert." + node + "." + module + "." + fingerprint + ".view"; +} + +/** The parts of a key of the form "revert....view", or undefined for a key of another form. */ +export function parseAnswersByRevertNodeModuleFingerprintViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("revert.")) return undefined; + _rest = _rest.slice(7); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + const _at3 = _rest.indexOf("."); + if (_at3 <= 0) return undefined; + const module = _rest.slice(0, _at3); + _rest = _rest.slice(_at3 + 1); + const _at5 = _rest.indexOf(".view"); + if (_at5 <= 0) return undefined; + const fingerprint = _rest.slice(0, _at5); + _rest = _rest.slice(_at5 + 5); + if (_rest !== "") return undefined; + return { node, module, fingerprint }; +} + +/** The key of one value of the state answers under the form "assignments..view". */ +export function answersByAssignmentsProposalViewKey(proposal) { + return "assignments." + proposal + ".view"; +} + +/** The parts of a key of the form "assignments..view", or undefined for a key of another form. */ +export function parseAnswersByAssignmentsProposalViewKey(_key) { + let _rest = _key; + if (!_rest.startsWith("assignments.")) return undefined; + _rest = _rest.slice(12); + const _at1 = _rest.indexOf(".view"); + if (_at1 <= 0) return undefined; + const proposal = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 5); + if (_rest !== "") return undefined; + return { proposal }; +} + diff --git a/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/package-lock.json b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/package-lock.json new file mode 100644 index 00000000..562df240 --- /dev/null +++ b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-delivery-contracts-mesh-delivery-view", + "packages": { + "": { + "name": "@novox/mesh-delivery-contracts-mesh-delivery-view", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/package.json b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/package.json new file mode 100644 index 00000000..cd53f26f --- /dev/null +++ b/modules/mesh-delivery-contracts/ts/seats/meshdeliveryview/package.json @@ -0,0 +1,18 @@ +{ + "description": "The contract of the seat mesh-delivery-view, generated from its definition.", + "exports": { + ".": { + "default": "./index.js", + "types": "./index.d.ts" + } + }, + "files": [ + "index.js", + "index.d.ts", + "parts" + ], + "name": "@novox/mesh-delivery-contracts-mesh-delivery-view", + "type": "module", + "types": "./index.d.ts", + "version": "0.1.0" +} diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go index ccf227e1..59281c66 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go @@ -109,7 +109,7 @@ func fullConf(dir string) string { teller = append(teller, keepsOf(bucketsOf(tellerModule).marks)...) // The assigner's asks and the view's answers, each by its read tool and its views family (ADR 0615, DLV-M3; A-175), and the // teller's own first-views and findings; the buckets themselves are no longer the teller's to read. - teller = append(append(append(teller, viewstest.Grants(assignerModule, assignerModule+"."+assignerAsksState, tellerConsumer)...), viewstest.Grants(viewModule, viewModule+"."+viewstate.ViewAnswersReadTool, tellerConsumer)...), keepsOf(bucketsOf(tellerModule).firstViews)...) + teller = append(append(append(teller, viewstest.Grants(assignerModule, assignerModule+"."+assignerAsksState, tellerConsumer)...), viewstest.Grants(viewModule, viewModule+"."+viewstate.ViewAnswersTool, tellerConsumer)...), keepsOf(bucketsOf(tellerModule).firstViews)...) teller = append(teller, keepsOf(bucketsOf(tellerModule).findings)...) view := append(append(readOf(tellerAnswers), readOf(assignerAsks)...), keepsOf(viewAnswers)...) assigner := keepsOf(assignerAsks) @@ -194,7 +194,7 @@ func startFull(t *testing.T) *fullBed { // The assigner as the owner of its asks: its read tool and its views family (ADR 0615). viewstest.Serve(t, ctx, js, ctl, assignerModule, "asks", assignerModule+"."+assignerAsksState, tellerConsumer) // The view as the owner of its answers, as the teller follows them (A-175, A-178). - viewstest.Serve(t, ctx, js, ctl, viewModule, viewstate.ViewAnswersState, viewModule+"."+viewstate.ViewAnswersReadTool, tellerConsumer) + viewstest.Serve(t, ctx, js, ctl, viewModule, viewstate.ViewAnswersState, viewModule+"."+viewstate.ViewAnswersTool, tellerConsumer) return &fullBed{srv: srv, ctl: ctl, js: js, ctx: ctx} } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go index b1032c5e..59ee7db3 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go @@ -128,7 +128,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } assignerKV := jetstream.KeyValue(asks) // The view's answers to the assigner's asks (issue 1297 B10, A-175): followed as the assigner's asks are, never its bucket. - answered, err := reader.Follow(viewModule, viewstate.ViewAnswersState, viewModule+"."+viewstate.ViewAnswersReadTool) + answered, err := reader.Follow(viewModule, viewstate.ViewAnswersState, viewModule+"."+viewstate.ViewAnswersTool) if err != nil { return err } diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/names_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/names_test.go index 9d9de430..a0517f61 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/names_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/names_test.go @@ -41,11 +41,9 @@ func TestTheNamesAreTheManifests(t *testing.T) { if want := "seat:mesh-wants.views." + assignerAsksState; !slices.Contains(m.Consumes, want) || slices.ContainsFunc(m.Invokes, func(e string) bool { return strings.HasPrefix(e, assignerModule+".") }) { t.Errorf("the teller follows %s and invokes nothing of the assigner's, its manifest's consumes are %v and invokes %v", want, m.Consumes, m.Invokes) } - if want := viewModule + "." + viewstate.ViewAnswersReadTool; !slices.Contains(m.Invokes, want) { - t.Errorf("the teller follows the view's answers by %s, its manifest's invokes are %v", want, m.Invokes) - } - if want := viewModule + ".views." + viewstate.ViewAnswersState; !slices.Contains(m.Consumes, want) { - t.Errorf("the teller follows %s, its manifest's consumes are %v", want, m.Consumes) + // Design 47 G §717: the view's answers are the seat mesh-delivery-view's view, followed in seat form; no invoke names its tool. + if want := "seat:mesh-delivery-view.views." + viewstate.ViewAnswersState; !slices.Contains(m.Consumes, want) || slices.ContainsFunc(m.Invokes, func(e string) bool { return strings.HasPrefix(e, viewModule+".") }) { + t.Errorf("the teller follows %s and invokes nothing of the view's, its manifest's consumes are %v and invokes %v", want, m.Consumes, m.Invokes) } if want := "views." + answersState; !slices.Contains(m.Emits, want) { t.Errorf("the teller says %s, its manifest's emits are %v", want, m.Emits) diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go index 747820dd..16cff681 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go @@ -14,7 +14,7 @@ import ( // asks of the assigner that need no proof of who answered (the hold of a fingerprint and the restore of a data module set back, // mesh-sdk go/delivery AskNeedsProof); the view writes its answer under `.view` in its state answers, and the teller, // which alone fills an ask's answer, takes it from the picture it follows of that state (the view's views family, first view -// by view_read_answers; design 47 G A-175) as it takes a warrant from the phone: under the ask's own key, once, by +// by its first-view tool answers; design 47 G A-175, §717) as it takes a warrant from the phone: under the ask's own key, once, by // compare-and-set. // OnViewAnswer handles a key put in the followed picture of the view's answers. A key that is no view answer key, or is the answer to an ask that diff --git a/modules/mesh-delivery-teller/manifest_test.go b/modules/mesh-delivery-teller/manifest_test.go index ad15fe63..990e78e2 100644 --- a/modules/mesh-delivery-teller/manifest_test.go +++ b/modules/mesh-delivery-teller/manifest_test.go @@ -104,14 +104,14 @@ func TestManifestDeclarations(t *testing.T) { // 1240/AC1 } // Its own states answers and teller-marks are said as its views family views.answers, read by teller_read_answers (A-175, A-178). // The join: its first view asked again by hand is a hand act, recorded through the seat (views.StatusTools). - if want := []string{"mesh-delivery-view.view_read_answers", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.registrations", "seat:mesh-broker.show", "seat:operator-channel.notify", + if want := []string{ "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.registrations", "seat:mesh-broker.show", "seat:operator-channel.notify", // A-126: who holds the seat operator-channel, whose state asks holds the router's record of its asks (operatorask.Bucket). "seat:mesh-module-assigner.assignments"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // design 47 G §335 (TL1): the widening of a pipeline end is told through the seat operator-channel and kept in widens-said. // design 47 G §387: the keeper's ask for the findings is heard, and the finding stream is declared (§383). - if want := []string{"mesh-conditions.findings-asked", "seat:mesh-wants.views.asks", "mesh-delivery-view.views.answers", "mesh-catalog.registration-changed", "mesh-pipeline-runner.pipeline-ended", "mesh-delivery.recovery-merged", "mesh-delivery.record-opened", "mesh-broker.recreated", "mesh-broker.dropped"}; !reflect.DeepEqual(m.Consumes, want) { + if want := []string{"mesh-conditions.findings-asked", "seat:mesh-wants.views.asks", "seat:mesh-delivery-view.views.answers", "mesh-catalog.registration-changed", "mesh-pipeline-runner.pipeline-ended", "mesh-delivery.recovery-merged", "mesh-delivery.record-opened", "mesh-broker.recreated", "mesh-broker.dropped"}; !reflect.DeepEqual(m.Consumes, want) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } if want := []string{"operator-channel"}; !reflect.DeepEqual(sorted(m.Uses), want) { diff --git a/modules/mesh-delivery-teller/module.json b/modules/mesh-delivery-teller/module.json index 81f5b12b..935475a6 100644 --- a/modules/mesh-delivery-teller/module.json +++ b/modules/mesh-delivery-teller/module.json @@ -11,7 +11,6 @@ "teller_widens_said" ], "invokes": [ - "mesh-delivery-view.view_read_answers", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.registrations", "seat:mesh-broker.show", @@ -21,7 +20,7 @@ "consumes": [ "mesh-conditions.findings-asked", "seat:mesh-wants.views.asks", - "mesh-delivery-view.views.answers", + "seat:mesh-delivery-view.views.answers", "mesh-catalog.registration-changed", "mesh-pipeline-runner.pipeline-ended", "mesh-delivery.recovery-merged", diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go index 28b17f79..19d3de88 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main_test.go @@ -186,6 +186,8 @@ type putter struct{ puts map[string][]byte } func (p *putter) Put(key string, value []byte) error { p.puts[key] = value; return nil } +func (p *putter) Delete(key string) error { delete(p.puts, key); return nil } + func serveWith(t *testing.T, m *view.Model) string { t.Helper() dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "c") diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index c0a6f8da..7e454ad7 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -466,7 +466,7 @@ func main() { // view_first_views and view_first_view_again: where the view's process stands with its first views of the indexer's and // the assigner's states (ADR 0615 §6), and a re-read by hand. tools := append(Tools(r, u), gate.Tools()...) - tools = append(tools, AnswersReadTool(func(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }, time.Now)) + tools = append(tools, AnswersTool(views.ParseCallers(os.Getenv(views.CallersEnv(viewstate.ViewAnswersTool))), func(name string) views.Source { return views.KeptSource{State: stdio.State(name)} }, time.Now)) tools = append(tools, views.StatusTools(viewstate.ViewModule, "view_first_views", "view_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) if err := stdio.Serve("", tools); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err) @@ -474,12 +474,13 @@ func main() { } } -// AnswersReadTool is view_read_answers, the view's ordinary read tool of its own state answers (mesh-sdk views.ReadTool): the -// teller's first view of the answers the view says on views.answers (design 47 G A-175, A-178). -func AnswersReadTool(state func(string) views.Source, now func() time.Time) stdio.Tool { - return views.ReadTool(viewstate.ViewModule, viewstate.ViewAnswersReadTool, viewstate.ViewAnswersState, - "the operator's answers given in the view to the assigner's asks, one key .view each, which the teller fills the ask from.", - state(viewstate.ViewAnswersState), state(views.MarksState), now) +// AnswersTool is answers, the first-view tool of the seat mesh-delivery-view's state answers (mesh-sdk views.FirstViewTool), +// answered to the callers its list names (the teller): its first view of the answers the view says on views.answers +// (design 47 G A-175, A-178, §717, A-301). +func AnswersTool(callers stdio.Callers, state func(string) views.Source, now func() time.Time) stdio.Tool { + return views.FirstViewTool(viewstate.ViewModule, viewstate.ViewAnswersTool, viewstate.ViewAnswersState, + "the operator's answers given in the view to the asks of the seat mesh-wants, one key .view each, which the teller fills the ask from.", + callers, state(viewstate.ViewAnswersState), state(views.MarksState), now) } // pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5). diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go index 4ac60f21..b41eb316 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/owners_test.go @@ -256,24 +256,24 @@ var catalogAnswer = meshcatalog.RevisionAnswer{ // §506, §578), never one computed again from the manifest whole. const manifestDigestA = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" -// view_read_answers answers the view's own answers as every owner's read tool answers its state (design 47 G A-178): one -// key's value, and every key when none is named. +// answers is the first-view tool of the seat mesh-delivery-view's state answers (design 47 G A-178, §717, A-301): a page of every +// key, answered to the teller its callers list names and refused to any other caller. func TestTheViewReadsItsAnswersAsAnOwner(t *testing.T) { answers := source{"hold.plex.ab.view": `{"asking":"a1","option":"release"}`} - tool := AnswersReadTool(func(name string) views.Source { + tool := AnswersTool(views.ParseCallers("mesh-delivery-teller"), func(name string) views.Source { if name == viewstate.ViewAnswersState { return answers } return source{} }, time.Now) - if tool.Name != "view_read_answers" { + if tool.Name != "answers" { t.Fatalf("the tool is %s", tool.Name) } - for args, want := range map[string]map[string]any{"release": {"key": "hold.plex.ab.view"}, "hold.plex.ab.view": {}} { - out, err := tool.Run(want) - raw, _ := json.Marshal(out) - if err != nil || !strings.Contains(string(raw), args) { - t.Errorf("%v: %s, %v", args, raw, err) - } + // Answered only to the callers its list names: the teller, never a call no runtime proved. + if !strings.Contains(tool.Description, "mesh-delivery-teller") { + t.Errorf("the tool's callers are not the teller: %s", tool.Description) + } + if out, err := tool.Run(map[string]any{}); err == nil { + t.Errorf("an unproven caller is answered: %v", out) } } diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go index c85c073c..b4ee947b 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view/main.go @@ -205,7 +205,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // sayAnswers says each change of the view's own state answers on its views family (`views.answers`), from the revision its // marks state view-marks says it said up to, as every owner says its states (design 47 G A-175, A-178): the teller follows it -// by view_read_answers and that family, never by reading the state. A watch that ends is sent on errs, and the program ends +// by its first-view tool answers and that family, never by reading the state. A watch that ends is sent on errs, and the program ends // with it, loudly. func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscriber, cred process.Account, writer views.WireWriter, errs chan<- error, logf func(string, ...any)) { marks := cred.Module + "_" + views.MarksState @@ -213,7 +213,7 @@ func sayAnswers(ctx context.Context, js jetstream.JetStream, nc process.Subscrib em := views.Emitter{Owner: viewstate.ViewModule, Subject: cred.EventSubject, Publish: views.JetStreamPublish(js), Marks: mk, Findings: writer, Logf: logf} process.AwaitBucket(ctx, js, nc, viewstate.BucketsOf(cred.Module).Answers, "the view's answers are not said on its views family", logf).Ready(func(kv jetstream.KeyValue) { if err := em.Start(ctx, viewstate.ViewAnswersState, kv, errs); err != nil { - logf("%v: the teller asks view_read_answers at each change until the view starts again", err) + logf("%v: the teller asks the first-view tool answers at each change until the view starts again", err) } }) }) @@ -236,6 +236,16 @@ func (w kvWriter) Put(key string, value []byte) error { return err } +// Delete removes a key of the view's own answers: the answer to an ask that left asks, which only the view deletes (design 47 G +// §717). +func (w kvWriter) Delete(key string) error { + kv, err := w.js.KeyValue(w.ctx, w.bucket) + if err != nil { + return fmt.Errorf("the bucket %s: %w", w.bucket, err) + } + return kv.Delete(w.ctx, key) +} + // putReach writes a reach value under its key in the view's own state reach (ADR 0556 clause 2), its only writer. func putReach(ctx context.Context, js jetstream.JetStream, bucket string, w view.ReachWrite) (uint64, error) { kv, err := js.KeyValue(ctx, bucket) diff --git a/modules/mesh-delivery-view/internal/view/answers_test.go b/modules/mesh-delivery-view/internal/view/answers_test.go index 98406ab2..49a34f12 100644 --- a/modules/mesh-delivery-view/internal/view/answers_test.go +++ b/modules/mesh-delivery-view/internal/view/answers_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "slices" "strings" "testing" "time" @@ -19,8 +20,9 @@ import ( // memWriter is the view's bucket answers in memory. type memWriter struct { - puts map[string][]byte - fail error + puts map[string][]byte + deleted []string + fail error } func (w *memWriter) Put(key string, value []byte) error { @@ -34,6 +36,15 @@ func (w *memWriter) Put(key string, value []byte) error { return nil } +func (w *memWriter) Delete(key string) error { + if w.fail != nil { + return w.fail + } + w.deleted = append(w.deleted, key) + delete(w.puts, key) + return nil +} + // holdAsk is a key of the assigner's asks: the ask about a held fingerprint, as a fixture (in release 5 the assigner writes // none yet; the value is `{"v":1,"ask":}`, as the gate's minus group and heads). func holdAsk(t *testing.T) (Entry, oc.Ask) { @@ -93,6 +104,14 @@ func TestTheViewWritesItsAnswerUnderTheAsksViewKey(t *testing.T) { // 1297/B9a A if len(got) != 1 || got[0].Chosen != "release" { t.Errorf("asks = %+v, want the ask with chosen release", got) } + // Design 47 G §717: only the holder deletes its answer, once the ask's key has left asks, so no answer outlives its ask. + if len(w.deleted) != 0 { + t.Fatalf("deleted %v while the ask stands", w.deleted) + } + fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Deleted: true, At: t0}) + if !slices.Equal(w.deleted, []string{e.Key + ".view"}) { + t.Errorf("deleted %v once the ask left asks, want %s.view", w.deleted, e.Key) + } } func TestTheViewRefusesWhatItMayNotAnswer(t *testing.T) { // 1297/B9a AC diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index ea977abe..0f56a9ce 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -237,6 +237,7 @@ type UnreadableRow struct { type Model struct { mu sync.Mutex own viewstate.Buckets // its own buckets, named from its credential's module (A-296) + left []string // the asks heard leaving asks whose answer the view still keeps (§717) placed *meshmoduleassignerclient.Placed // the seat's assignments (the assigner's state, ADR 0567, ADR 0549 §3) builds map[string]buildEntry // by .build. groups map[string]groupEntry // the indexer's candidates, by group @@ -391,7 +392,10 @@ func (m *Model) Apply(e Entry) { writes, reached := m.reachNow() say, put := m.deliveredEvent, m.reachPut sayMotion := m.motionEvent + left, w := m.left, m.writer + m.left = nil m.mu.Unlock() + m.deleteAnswers(w, left) for _, event := range events { if say != nil && say(event) != nil { m.unsay(event) @@ -401,6 +405,22 @@ func (m *Model) Apply(e Entry) { m.sayMotion(sayMotion, motion) } +// deleteAnswers deletes the view's answers to the asks that left asks, after the model's lock is released; one not deleted is +// tried again at the next change the view hears. +func (m *Model) deleteAnswers(w Writer, left []string) { + for _, askKey := range left { + key, err := viewstate.ViewAnswerKey(askKey) + if err != nil { + continue + } + if w == nil || w.Delete(key) != nil { + m.mu.Lock() + m.left = append(m.left, askKey) + m.mu.Unlock() + } + } +} + // sayMotion says the motion events after the model's lock is released; one not said is forgotten as told, so the next change of // its want, have or stuck key says it again. func (m *Model) sayMotion(say func(MotionEvent) error, events []MotionEvent) { @@ -579,6 +599,10 @@ func (m *Model) apply(e Entry) { case AssignerAsksBucket: if e.Deleted { delete(m.aasks, e.Key) + // The view's answer outlives no ask: once the ask has left asks, the view deletes its answer (design 47 G §717). + if _, ok := m.chosen[e.Key]; ok { + m.left = append(m.left, e.Key) + } return } a, err := decodeAssignerAsk(e.Key, e.Value) @@ -989,6 +1013,8 @@ func (m *Model) chosenOf(key string, ask oc.Ask) string { // Writer puts a key of the view's bucket answers, the only state the view writes. type Writer interface { Put(key string, value []byte) error + // Delete removes a key of the view's answers: the answer to an ask that left asks (design 47 G §717). + Delete(key string) error } // SetWriter gives the model the bucket its answers are written to. diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 6237f0d2..7113348e 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -18,6 +18,7 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate" "git.novox.be/novox/mesh-sdk/go/keys" + "git.novox.be/novox/mesh-sdk/go/views" ) // The controller's manifest reader is mesh-controller internal/catalogue (ParseManifest), an internal package of another @@ -36,9 +37,11 @@ type artifact struct { From string `json:"from"` Binary string `json:"binary"` Loads []string `json:"loads"` - Paths []string `json:"paths"` - Series string `json:"series"` - Context *struct { + // Env is the bundle's environment: the tools program's callers list of its first-view tool answers (A-301). + Env map[string]string `json:"env"` + Paths []string `json:"paths"` + Series string `json:"series"` + Context *struct { Repository string `json:"repository"` } `json:"context"` } @@ -60,7 +63,15 @@ type stream struct { MaxMiB int `json:"max-mib"` } +// claim is a seat the manifest claims (design 47 G §717: the status view claims mesh-delivery-view and serves none of its own). +type claim struct { + Name string `json:"name"` + Scope string `json:"scope"` + Serves []string `json:"serves"` +} + type manifest struct { + Claims []claim `json:"claims"` State []state `json:"state"` Module string `json:"module"` Moved bool `json:"moved"` @@ -130,6 +141,15 @@ func load(t *testing.T) (manifest, map[string]json.RawMessage) { func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 m, top := load(t) + for _, a := range m.Build.Artifacts { + if a.Name == "tools" && a.Env[views.CallersEnv("answers")] != "mesh-delivery-teller" { + t.Errorf("the tools' env = %v, want the callers of answers mesh-delivery-teller", a.Env) + } + } + // Design 47 G §717: it claims the seat mesh-delivery-view, scope mesh, serving no verb, and its first-view tool answers. + if want := []claim{{Name: "mesh-delivery-view", Scope: "mesh", Serves: []string{}}}; !reflect.DeepEqual(m.Claims, want) { + t.Errorf("claims = %+v, want %+v", m.Claims, want) + } if m.Module != "mesh-delivery-view" { t.Errorf("module = %q", m.Module) } @@ -139,7 +159,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if !reflect.DeepEqual(got, want) { t.Errorf("reads = %v, want exactly %v", got, want) } - for _, k := range []string{"provides", "claims"} { + for _, k := range []string{"provides"} { // its one claim, a seat serving no verb, is checked above (§717) if _, ok := top[k]; ok { t.Errorf("the manifest declares %q: the view writes only its answers", k) } @@ -203,7 +223,7 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 } // The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer // itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads. - if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads", "view_seat_list", "view_seat_list_again", "view_read_answers"}; !reflect.DeepEqual(m.Tools, want) { + if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads", "view_seat_list", "view_seat_list_again", "answers"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } // D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index 5df396dc..68878c5f 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -30,7 +30,7 @@ "node_manifest_reads", "view_seat_list", "view_seat_list_again", - "view_read_answers" + "answers" ], "emits": [ "delivered", @@ -40,6 +40,13 @@ "stuck", "views.answers" ], + "claims": [ + { + "name": "mesh-delivery-view", + "scope": "mesh", + "serves": [] + } + ], "state": [ { "name": "answers", @@ -226,7 +233,10 @@ "binary": "mesh-delivery-view-tools", "loads": [ "mesh-delivery-view-tools" - ] + ], + "env": { + "MESH_VIEW_CALLERS_ANSWERS": "mesh-delivery-teller" + } }, { "name": "files", diff --git a/modules/mesh-delivery-view/viewstate/reach.go b/modules/mesh-delivery-view/viewstate/reach.go index 031c3937..96e81e65 100644 --- a/modules/mesh-delivery-view/viewstate/reach.go +++ b/modules/mesh-delivery-view/viewstate/reach.go @@ -146,9 +146,10 @@ func SameReach(a, b Reach) bool { const ( ViewAnswersState = "answers" ViewAnswersHistory = 5 - // ViewAnswersReadTool is the view's read tool of its state answers (mesh-sdk views.ReadTool), whose changes it says on its - // views family as views.answers; its marks are its state view-marks (views.MarksState) (design 47 G A-178). - ViewAnswersReadTool = "view_read_answers" + // ViewAnswersTool is the first-view tool of the seat mesh-delivery-view's state answers (mesh-sdk views.FirstViewTool), named + // for the state, whose changes the view says on its views family as views.answers; its marks are its state view-marks + // (views.MarksState) (design 47 G A-178, §717, A-301). + ViewAnswersTool = ViewAnswersState ) // ViewAnswer is the value of the view's answer to an ask of the assigner: the asking it answers, the option chosen and the words