From 1bac48e84e9da0cf331b4158e0e4de49898bdeda Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 19:29:57 +0100 Subject: [PATCH] =?UTF-8?q?=C2=A7717=20(2):=20the=20assigner's=20asks=20an?= =?UTF-8?q?d=20wants=20are=20views=20of=20the=20seat=20mesh-wants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seat mesh-wants' definition gains the state asks (template state, history 5, view), its key forms the seven kinds' keys as the assigner's asks package writes them (hold, restore, backup, record, outside, revert, assignments) and its value the ask as the assigner composes it; its wants gains view. The assigner's first-view tool asks replaces assigner_read_asks, its callers the teller and the status view; the status view joins the callers of wants and assignments. The teller follows the asks by seat:mesh-wants.views.asks and invokes nothing of the assigner's. The status view's part waits on a question (its tools ask assigner_read_wants). Builds: design 47 G §717, A-299 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../mesh-delivery-assigner/askagain_tool.go | 6 +- .../mesh-delivery-assigner/asksform_test.go | 74 ++++ .../cmd/mesh-delivery-assigner/views.go | 3 +- .../mesh-delivery-assigner/manifest_test.go | 11 +- modules/mesh-delivery-assigner/module.json | 7 +- .../go/seats/meshwants/contract.go | 343 ++++++++++++++++++ .../go/seats/meshwants/contract_test.go | 6 + .../seats/mesh-wants/fixtures/states.json | 12 + .../seats/mesh-wants/seat.json | 79 ++++ .../ts/seats/meshwants/index.d.ts | 73 ++++ .../ts/seats/meshwants/index.js | 164 +++++++++ .../mesh-delivery-teller/integration_test.go | 4 +- .../cmd/mesh-delivery-teller/main.go | 4 +- .../cmd/mesh-delivery-teller/names_test.go | 5 + modules/mesh-delivery-teller/manifest_test.go | 4 +- modules/mesh-delivery-teller/module.json | 3 +- 16 files changed, 779 insertions(+), 19 deletions(-) create mode 100644 modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go index 93529ea5..dcc18206 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/askagain_tool.go @@ -36,7 +36,7 @@ const ( func askAgainByHand(st store, ask func(string, any) (json.RawMessage, error), key, why, by string, now time.Time) (any, error) { switch { case key == "": - return nil, errors.New("name the ask's key, as assigner_read_asks lists it") + return nil, errors.New("name the ask's key, as the tool asks lists it") case why == "": return nil, errors.New("a hand ask again says why") case by == "" || by == stdio.Unproven: @@ -47,7 +47,7 @@ func askAgainByHand(st store, ask func(string, any) (json.RawMessage, error), ke return nil, err } if !found { - return nil, fmt.Errorf("the assigner holds no ask under %s: assigner_read_asks lists them", key) + return nil, fmt.Errorf("the assigner holds no ask under %s: the tool asks lists them", key) } _, routerAsk, err := assign.DecodeAsk(key, current) if err != nil { @@ -90,7 +90,7 @@ func askAgainTools(st store, ask func(string, any) (json.RawMessage, error), now "under key is written again under a new asking, which the teller asks on the operator channel, as when the channel says it " + "ended without an answer. Refused for an asking the operator answered, a proposal's authorising ask (the seat's verb again " + "asks it) and a record ask a put asked. Recorded as a hand act.", - Input: schema(map[string]string{"key": "the ask's key, as assigner_read_asks lists it", "why": "why it is asked again"}, + Input: schema(map[string]string{"key": "the ask's key, as the tool asks lists it", "why": "why it is asked again"}, []string{"key", "why"}), Run: func(a map[string]any) (any, error) { key, _ := a["key"].(string) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go new file mode 100644 index 00000000..67549841 --- /dev/null +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/asksform_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "encoding/json" + "testing" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" +) + +// Design 47 G §717: the assigner's asks are the seat mesh-wants' state asks, so every key its asks package writes is one of +// that state's key forms, read back to the same parts, and each value is the seat's Ask. +func TestTheAssignersAsksAreTheSeatMeshWantsAsks(t *testing.T) { + const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + const fp, digest = "src2:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + must := func(k string, err error) string { + t.Helper() + if err != nil { + t.Fatal(err) + } + return k + } + three := func(parse func(string) (string, string, string, bool)) func(string) []string { + return func(k string) []string { + if a, b, c, ok := parse(k); ok { + return []string{a, b, c} + } + return nil + } + } + for _, row := range []struct { + key string + parse func(string) []string + want []string + }{ + {must(asks.HoldAskKey("web", fp)), func(k string) []string { + if a, b, ok := meshwants.ParseAsksByHoldModuleFingerprintKey(k); ok { + return []string{a, b} + } + return nil + }, []string{"web", hex}}, + {must(asks.RestoreAskKey("ace", "web", fp)), three(meshwants.ParseAsksByRestoreNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.BackupAskKey("ace", "web", fp)), three(meshwants.ParseAsksByBackupNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.OutsideAskKey("ace", "web", fp)), three(meshwants.ParseAsksByOutsideNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.RevertAskKey("ace", "web", fp)), three(meshwants.ParseAsksByRevertNodeModuleFingerprintKey), []string{"ace", "web", hex}}, + {must(asks.RecordAskKey("ace", "web", digest)), three(meshwants.ParseAsksByRecordNodeModuleDigestKey), []string{"ace", "web", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}, + {must(asks.AssignmentsAskKey("p-20261028t183000-1a2b3c")), func(k string) []string { + if a, ok := meshwants.ParseAsksByAssignmentsProposalKey(k); ok { + return []string{a} + } + return nil + }, []string{"p-20261028t183000-1a2b3c"}}, + } { + got := row.parse(row.key) + if len(got) != len(row.want) { + t.Errorf("%s is no key form of mesh-wants' asks: read as %v", row.key, got) + continue + } + for i := range got { + if got[i] != row.want[i] { + t.Errorf("%s reads as %v, want %v", row.key, got, row.want) + break + } + } + } + key := must(asks.HoldAskKey("web", fp)) + raw, err := asks.EncodeAsk(key, asks.Ask{Ask: json.RawMessage(`{"id":"x"}`), Asking: 2}) + if err != nil { + t.Fatal(err) + } + if v, err := meshwants.DecodeAsksByHoldModuleFingerprintValue(raw); err != nil || v.Asking != 2 { + t.Errorf("the ask %s is not the seat's Ask: %+v, %v", raw, v, err) + } +} diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/views.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/views.go index 4eb76b26..d45cba08 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/views.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/views.go @@ -40,6 +40,8 @@ var firstViewTools = []viewedTool{ {"assignments", mma.StateAssignments, "where each module is placed: the seat mesh-module-assigner's assignments, by module."}, {"seat-holders", mma.StateSeatHolders, "who holds which seat on which node."}, {"wants", "wants", "what each node should run: want.., its fingerprint and the rule that gave it."}, + // The seat mesh-wants' asks (design 47 G §717): the asks the assigner put to a person about what a node runs. + {"asks", "asks", "the asks the assigner put to a person about what a node runs, and their answers."}, } // readTools are the assigner's ordinary read tools, one per state its readers read (design 25 § 12.2). @@ -48,7 +50,6 @@ var readTools = []viewedTool{ {"assigner_read_seat_holders", mma.StateSeatHolders, "who holds which seat on which node."}, {"assigner_read_wants", "wants", "what each node should run: want..."}, {"assigner_read_findings", assign.FindingsState, "the assigner's findings, which the condition keeper raises and clears."}, - {"assigner_read_asks", "asks", "the asks the assigner put to a person, and their answers."}, {"assigner_read_differences", "differences", "every difference between a want and what a node runs."}, {"assigner_read_copies", copiesState, "the copies the assigner made of a retired state: when the copy into assignments ended and how many keys it copied."}, } diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index a3e01885..141bce97 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -100,11 +100,11 @@ func TestManifest(t *testing.T) { t.Errorf("objects = %+v, want assignment-revisions, 512 MiB, form 1", m.Objects) } // Whether the seats' definitions let it hold them is the manifest validator's (the judge, at the seat list). - // ADR 0621 §1: the first-view tools assignments, seat-holders and wants and design 25 § 12.2's six read tools, of the + // ADR 0621 §1: the first-view tools assignments, seat-holders, wants and asks (§717) and design 25 § 12.2's six read tools, of the // program's bundle, which runs in a runtime of its own on the assigner's account; ADR 0618 §3: and assigner_engine_reads, // which lists and asks again what each node's engine reads. - if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "assigner_waits", "assigner_inputs", "assignments", "seat-holders", "wants", - "assigner_read_assignments", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences", "assigner_read_copies", + if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "assigner_waits", "assigner_inputs", "assignments", "seat-holders", "wants", "asks", + "assigner_read_assignments", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_differences", "assigner_read_copies", "assigner_first_views", "assigner_first_view_again", "assigner_engine_reads", "assigner_seat_list", "assigner_seat_list_again", "assigner_ask_again", "assigner_written", "assigner_pins", "assigner_unpin", "assigner_delivered_again"}) { t.Errorf("the assigner serves tools %v, want its three put tools, three first views, six read tools, assigner_engine_reads, the seat list's two (ADR 0636 §7) and assigner_ask_again (ADR 0668 §5)", m.Tools) } @@ -112,7 +112,10 @@ func TestManifest(t *testing.T) { t.Errorf("runs-as = %q: the bundle runs on the assigner's own account (ADR 0621 §1)", m.RunsAs) } // ADR 0621 §2: each first-view tool answers exactly its readers, never the assigner. - callers := map[string]string{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker", "seat-holders": "node-tools,mesh-delivery-builder,mesh-delivery-view", "wants": "mesh-grants"} + // Design 47 G §717: the teller and the status view take their first views of the seat mesh-wants' asks and wants, and of + // the assignments, from the tools named for the states. + callers := map[string]string{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker,mesh-delivery-view", + "seat-holders": "node-tools,mesh-delivery-builder,mesh-delivery-view", "wants": "mesh-grants,mesh-delivery-view", "asks": "mesh-delivery-teller,mesh-delivery-view"} env := m.Build.Artifacts[0].Env for tool, list := range callers { if env[views.CallersEnv(tool)] != list || views.ParseCallers(env[views.CallersEnv(tool)]).Allows("n1."+m.Module) { diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 5efc0f95..969b18c8 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -115,11 +115,11 @@ "assignments", "seat-holders", "wants", + "asks", "assigner_read_assignments", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", - "assigner_read_asks", "assigner_read_differences", "assigner_read_copies", "assigner_first_views", @@ -247,9 +247,10 @@ "mesh-delivery-assigner" ], "env": { - "MESH_VIEW_CALLERS_ASSIGNMENTS": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker", + "MESH_VIEW_CALLERS_ASKS": "mesh-delivery-teller,mesh-delivery-view", + "MESH_VIEW_CALLERS_ASSIGNMENTS": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker,mesh-delivery-view", "MESH_VIEW_CALLERS_SEAT_HOLDERS": "node-tools,mesh-delivery-builder,mesh-delivery-view", - "MESH_VIEW_CALLERS_WANTS": "mesh-grants" + "MESH_VIEW_CALLERS_WANTS": "mesh-grants,mesh-delivery-view" } } ] diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go index f8dd9332..4f4eae93 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go @@ -121,6 +121,7 @@ const AcceptSubjectStep = "mesh.seat.mesh-wants.accept.step" const ( StateWants = "wants" + StateAsks = "asks" StateDifferences = "differences" StateCompositions = "compositions" StateAssignmentRevisions = "assignment-revisions" @@ -244,6 +245,318 @@ func DecodeWantsByHeldModuleFingerprintValue(raw []byte) (WantsByHeldModuleFinge return v, nil } +// AsksByHoldModuleFingerprintKey is the key of one value of the state asks under the form "hold..". +func AsksByHoldModuleFingerprintKey(module, fingerprint string) string { + return "hold." + module + "." + fingerprint +} + +// ParseAsksByHoldModuleFingerprintKey reads the parts of a key of the form "hold..", in the order of the form; the last result is false for a key of another form. +func ParseAsksByHoldModuleFingerprintKey(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 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeAsksByHoldModuleFingerprintValue reads one value of the state asks under the form "hold..", refusing a field AsksByHoldModuleFingerprintValue does not know and any text after the value. +func DecodeAsksByHoldModuleFingerprintValue(raw []byte) (AsksByHoldModuleFingerprintValue, error) { + var v AsksByHoldModuleFingerprintValue + 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 +} + +// AsksByRestoreNodeModuleFingerprintKey is the key of one value of the state asks under the form "restore...". +func AsksByRestoreNodeModuleFingerprintKey(node, module, fingerprint string) string { + return "restore." + node + "." + module + "." + fingerprint +} + +// ParseAsksByRestoreNodeModuleFingerprintKey reads the parts of a key of the form "restore...", in the order of the form; the last result is false for a key of another form. +func ParseAsksByRestoreNodeModuleFingerprintKey(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 + if rest == "" { + return "", "", "", false + } + v5 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAsksByRestoreNodeModuleFingerprintValue reads one value of the state asks under the form "restore...", refusing a field AsksByRestoreNodeModuleFingerprintValue does not know and any text after the value. +func DecodeAsksByRestoreNodeModuleFingerprintValue(raw []byte) (AsksByRestoreNodeModuleFingerprintValue, error) { + var v AsksByRestoreNodeModuleFingerprintValue + 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 +} + +// AsksByBackupNodeModuleFingerprintKey is the key of one value of the state asks under the form "backup...". +func AsksByBackupNodeModuleFingerprintKey(node, module, fingerprint string) string { + return "backup." + node + "." + module + "." + fingerprint +} + +// ParseAsksByBackupNodeModuleFingerprintKey reads the parts of a key of the form "backup...", in the order of the form; the last result is false for a key of another form. +func ParseAsksByBackupNodeModuleFingerprintKey(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 + if rest == "" { + return "", "", "", false + } + v5 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAsksByBackupNodeModuleFingerprintValue reads one value of the state asks under the form "backup...", refusing a field AsksByBackupNodeModuleFingerprintValue does not know and any text after the value. +func DecodeAsksByBackupNodeModuleFingerprintValue(raw []byte) (AsksByBackupNodeModuleFingerprintValue, error) { + var v AsksByBackupNodeModuleFingerprintValue + 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 +} + +// AsksByRecordNodeModuleDigestKey is the key of one value of the state asks under the form "record...". +func AsksByRecordNodeModuleDigestKey(node, module, digest string) string { + return "record." + node + "." + module + "." + digest +} + +// ParseAsksByRecordNodeModuleDigestKey reads the parts of a key of the form "record...", in the order of the form; the last result is false for a key of another form. +func ParseAsksByRecordNodeModuleDigestKey(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 + if rest == "" { + return "", "", "", false + } + v5 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAsksByRecordNodeModuleDigestValue reads one value of the state asks under the form "record...", refusing a field AsksByRecordNodeModuleDigestValue does not know and any text after the value. +func DecodeAsksByRecordNodeModuleDigestValue(raw []byte) (AsksByRecordNodeModuleDigestValue, error) { + var v AsksByRecordNodeModuleDigestValue + 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 +} + +// AsksByOutsideNodeModuleFingerprintKey is the key of one value of the state asks under the form "outside...". +func AsksByOutsideNodeModuleFingerprintKey(node, module, fingerprint string) string { + return "outside." + node + "." + module + "." + fingerprint +} + +// ParseAsksByOutsideNodeModuleFingerprintKey reads the parts of a key of the form "outside...", in the order of the form; the last result is false for a key of another form. +func ParseAsksByOutsideNodeModuleFingerprintKey(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 + if rest == "" { + return "", "", "", false + } + v5 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAsksByOutsideNodeModuleFingerprintValue reads one value of the state asks under the form "outside...", refusing a field AsksByOutsideNodeModuleFingerprintValue does not know and any text after the value. +func DecodeAsksByOutsideNodeModuleFingerprintValue(raw []byte) (AsksByOutsideNodeModuleFingerprintValue, error) { + var v AsksByOutsideNodeModuleFingerprintValue + 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 +} + +// AsksByRevertNodeModuleFingerprintKey is the key of one value of the state asks under the form "revert...". +func AsksByRevertNodeModuleFingerprintKey(node, module, fingerprint string) string { + return "revert." + node + "." + module + "." + fingerprint +} + +// ParseAsksByRevertNodeModuleFingerprintKey reads the parts of a key of the form "revert...", in the order of the form; the last result is false for a key of another form. +func ParseAsksByRevertNodeModuleFingerprintKey(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 + if rest == "" { + return "", "", "", false + } + v5 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v1, v3, v5, true +} + +// DecodeAsksByRevertNodeModuleFingerprintValue reads one value of the state asks under the form "revert...", refusing a field AsksByRevertNodeModuleFingerprintValue does not know and any text after the value. +func DecodeAsksByRevertNodeModuleFingerprintValue(raw []byte) (AsksByRevertNodeModuleFingerprintValue, error) { + var v AsksByRevertNodeModuleFingerprintValue + 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 +} + +// AsksByAssignmentsProposalKey is the key of one value of the state asks under the form "assignments.". +func AsksByAssignmentsProposalKey(proposal string) string { return "assignments." + proposal } + +// ParseAsksByAssignmentsProposalKey reads the parts of a key of the form "assignments.", in the order of the form; the last result is false for a key of another form. +func ParseAsksByAssignmentsProposalKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "assignments.") { + return "", false + } + rest = rest[12:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeAsksByAssignmentsProposalValue reads one value of the state asks under the form "assignments.", refusing a field AsksByAssignmentsProposalValue does not know and any text after the value. +func DecodeAsksByAssignmentsProposalValue(raw []byte) (AsksByAssignmentsProposalValue, error) { + var v AsksByAssignmentsProposalValue + 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 +} + // DifferencesByNodeModuleKey is the key of one value of the state differences under the form ".". func DifferencesByNodeModuleKey(node, module string) string { return node + "." + module } @@ -464,6 +777,15 @@ func DecodeInputsByTimeDigestValue(raw []byte) (InputsByTimeDigestValue, error) return v, nil } +// Ask is an ask the assigner puts to a person: the router's ask whole, as the assigner composes it, and how many times it was written at its key. +type Ask struct { + // the seat operator-channel's ask, whole + Ask map[string]any `json:"ask"` + // the times the assigner wrote the ask at this key: 0 or 1 for the first, n for the n-th + Asking int64 `json:"asking,omitempty"` + V int64 `json:"v,omitempty"` +} + // AssignmentEntry is one module version on a node of an assignment graph revision, stored once by its digest (ADR 0656). type AssignmentEntry struct { // the watched branch its placement names, whose catalog graph the entry comes from (design 47 lines 523-525) @@ -975,6 +1297,27 @@ type WantsByHeldModuleFingerprintValue struct { Words string `json:"words,omitempty"` } +// AsksByHoldModuleFingerprintValue is one value of the state asks under the key hold... +type AsksByHoldModuleFingerprintValue = Ask + +// AsksByRestoreNodeModuleFingerprintValue is one value of the state asks under the key restore.... +type AsksByRestoreNodeModuleFingerprintValue = Ask + +// AsksByBackupNodeModuleFingerprintValue is one value of the state asks under the key backup.... +type AsksByBackupNodeModuleFingerprintValue = Ask + +// AsksByRecordNodeModuleDigestValue is one value of the state asks under the key record.... +type AsksByRecordNodeModuleDigestValue = Ask + +// AsksByOutsideNodeModuleFingerprintValue is one value of the state asks under the key outside.... +type AsksByOutsideNodeModuleFingerprintValue = Ask + +// AsksByRevertNodeModuleFingerprintValue is one value of the state asks under the key revert.... +type AsksByRevertNodeModuleFingerprintValue = Ask + +// AsksByAssignmentsProposalValue is one value of the state asks under the key assignments.. +type AsksByAssignmentsProposalValue = Ask + // DifferencesByNodeModuleValue is one value of the state differences under the key .. type DifferencesByNodeModuleValue struct { Rule int64 `json:"rule,omitempty"` diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go index 95ad5671..3daf1981 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract_test.go @@ -113,6 +113,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "an ask about a held fingerprint, asked a second time", subject: "", given: "{\"ask\":{\"headline\":\"web 0123456789ab is held\",\"id\":\"hold.web.0123456789ab.2\"},\"asking\":2,\"v\":1}", wire: "{\"ask\":{\"headline\":\"web 0123456789ab is held\",\"id\":\"hold.web.0123456789ab.2\"},\"asking\":2,\"v\":1}", + decode: func(b []byte) (any, error) { + var v AsksByHoldModuleFingerprintValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "revision asks the trunk's newest", subject: "", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v RevisionInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "revision asks a candidate's", subject: "", given: "{\"group\":\"feat/47-G\",\"heads\":\"0123456789ab\"}", wire: "{\"group\":\"feat/47-G\",\"heads\":\"0123456789ab\"}", diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/states.json b/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/states.json index 85e5d74c..e49128ca 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/states.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/fixtures/states.json @@ -151,5 +151,17 @@ } ] } + }, + { + "name": "an ask about a held fingerprint, asked a second time", + "of": "state:asks/hold..", + "given": { + "v": 1, + "ask": { + "id": "hold.web.0123456789ab.2", + "headline": "web 0123456789ab is held" + }, + "asking": 2 + } } ] diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json index 9a0f5535..b3c1c761 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json @@ -489,6 +489,7 @@ { "name": "wants", "template": "state", + "view": true, "key-forms": [ { "key": "want..", @@ -618,6 +619,64 @@ "history": 20, "description": "what each node should run, and the fingerprints held back" }, + { + "name": "asks", + "template": "state", + "view": true, + "history": 5, + "description": "the asks the assigner puts to a person about what a node runs, one key per ask, each the ask as the assigner composes it (design 47 G section 717)", + "key-forms": [ + { + "key": "hold..", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the ask about a module's held fingerprint whose health check failed: release it or keep it held" + }, + { + "key": "restore...", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the ask about a data module set back on a node: restore and lose newer writes, or leave it stopped (ADR 0429 section 6)" + }, + { + "key": "backup...", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the ask about a backup that failed before a node applies a fingerprint: back up again, or go on without one (ADR 0539 section 16)" + }, + { + "key": "record...", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the ask about a move a person must allow for a module of policy record, naming the composition's digest (ADR 0539 section 14)" + }, + { + "key": "outside...", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the ask about a moved module whose failing health check waited past its bound on an outside cause (design 47 G section 706)" + }, + { + "key": "revert...", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the ask about a held revert with no restore point, the reverted build's fingerprint (design 47 G section 710)" + }, + { + "key": "assignments.", + "value": { + "$ref": "#/defs/Ask" + }, + "description": "the authorising ask about a proposal of assignments (ADR 0567 section 5)" + } + ] + }, { "name": "differences", "template": "state", @@ -1409,6 +1468,26 @@ "branch", "revision" ] + }, + "Ask": { + "type": "object", + "description": "an ask the assigner puts to a person: the router's ask whole, as the assigner composes it, and how many times it was written at its key", + "properties": { + "v": { + "type": "integer" + }, + "ask": { + "type": "object", + "description": "the seat operator-channel's ask, whole" + }, + "asking": { + "type": "integer", + "description": "the times the assigner wrote the ask at this key: 0 or 1 for the first, n for the n-th" + } + }, + "required": [ + "ask" + ] } }, "acts": [ diff --git a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts index 1380f032..e037cb56 100644 --- a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts +++ b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts @@ -74,6 +74,7 @@ export declare const stepSubject: "mesh.seat.mesh-wants.accept.step"; export declare const states: { readonly wants: "wants"; + readonly asks: "asks"; readonly differences: "differences"; readonly compositions: "compositions"; readonly assignmentRevisions: "assignment-revisions"; @@ -98,6 +99,48 @@ export declare function wantsByHeldModuleFingerprintKey(module: string, fingerpr /** The parts of a key of the form "held..", or undefined for a key of another form. */ export declare function parseWantsByHeldModuleFingerprintKey(_key: string): { module: string; fingerprint: string } | undefined; +/** The key of one value of the state asks under the form "hold..". */ +export declare function asksByHoldModuleFingerprintKey(module: string, fingerprint: string): string; + +/** The parts of a key of the form "hold..", or undefined for a key of another form. */ +export declare function parseAsksByHoldModuleFingerprintKey(_key: string): { module: string; fingerprint: string } | undefined; + +/** The key of one value of the state asks under the form "restore...". */ +export declare function asksByRestoreNodeModuleFingerprintKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "restore...", or undefined for a key of another form. */ +export declare function parseAsksByRestoreNodeModuleFingerprintKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state asks under the form "backup...". */ +export declare function asksByBackupNodeModuleFingerprintKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "backup...", or undefined for a key of another form. */ +export declare function parseAsksByBackupNodeModuleFingerprintKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state asks under the form "record...". */ +export declare function asksByRecordNodeModuleDigestKey(node: string, module: string, digest: string): string; + +/** The parts of a key of the form "record...", or undefined for a key of another form. */ +export declare function parseAsksByRecordNodeModuleDigestKey(_key: string): { node: string; module: string; digest: string } | undefined; + +/** The key of one value of the state asks under the form "outside...". */ +export declare function asksByOutsideNodeModuleFingerprintKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "outside...", or undefined for a key of another form. */ +export declare function parseAsksByOutsideNodeModuleFingerprintKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state asks under the form "revert...". */ +export declare function asksByRevertNodeModuleFingerprintKey(node: string, module: string, fingerprint: string): string; + +/** The parts of a key of the form "revert...", or undefined for a key of another form. */ +export declare function parseAsksByRevertNodeModuleFingerprintKey(_key: string): { node: string; module: string; fingerprint: string } | undefined; + +/** The key of one value of the state asks under the form "assignments.". */ +export declare function asksByAssignmentsProposalKey(proposal: string): string; + +/** The parts of a key of the form "assignments.", or undefined for a key of another form. */ +export declare function parseAsksByAssignmentsProposalKey(_key: string): { proposal: string } | undefined; + /** The key of one value of the state differences under the form ".". */ export declare function differencesByNodeModuleKey(node: string, module: string): string; @@ -134,6 +177,15 @@ export declare function inputsByTimeDigestKey(time: string, digest: string): str /** The parts of a key of the form "