GATE A-18: the gate's record type drops the remove operation, and table, recheck and close go with their handlers and tools

Builds: design 47 G §587, §305, A-18 (G-20)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:48:19 +01:00
parent 846291069b
commit 11d1be3ef8
10 changed files with 13 additions and 149 deletions
@@ -87,9 +87,7 @@ func (h *Holder) askAgainForConfig(id, why string) {
h.mu.Unlock()
switch {
case refused == nil:
if _, err := h.Recheck(id, why, byOwner); err != nil {
h.Logf("[mesh-delivery] %s: its check could not be asked again for a config value: %v", id, err)
}
h.Logf("[mesh-delivery] %s: its check could not be asked again for a config value: %v", id, checkerRefusal(id))
case proposed:
h.askProposedAgain(id, why)
}
@@ -329,15 +329,12 @@ func (f *fakeLinks) Linked(repository string, number int) ([]int, error) {
return out, nil
}
// Stalled: a delivery past its state's bound is listed with what H2 may do; close takes only that. Only a proposed
// Stalled: a delivery past its state's bound is listed with what H2 may do. Only a proposed
// delivery has a bound now: its check was asked and not answered.
func TestStalledAndCloseWorkFromTheTable(t *testing.T) {
func TestStalledWorksFromTheTable(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
if _, err := w.h.Close(id, "too early"); err == nil {
t.Fatal("closed within its bound")
}
w.later(3 * time.Hour)
stalled := w.h.Stalled()
if len(stalled) != 1 || stalled[0].ID != id || stalled[0].State != Proposed {
@@ -352,16 +349,10 @@ func TestStalledAndCloseWorkFromTheTable(t *testing.T) {
t.Fatalf("a merged delivery is stalled: %+v", s)
}
}
if _, err := w.h.Close(id, "x"); err == nil {
t.Fatal("H2 closed a merged delivery")
}
// A merge without a passing check is held: a person's, with no transition for H2.
w.h.PullUpdated(pr("novox/app", 8, "ffffffffffff0000", "feat/q"))
w.h.PullMerged(merged("novox/app", 8, "ffffffffffff0000", "eeeeeeeeeeee0000"))
w.later(48 * time.Hour)
if _, err := w.h.Close(IDOf("novox/app", "ffffffffffff0000"), "x"); err == nil {
t.Fatal("H2 closed a held delivery")
}
}
// novox/hq issue 867, ADR 0382 §1 (contract A): each member of a group in `groups` says its pull request number, full
@@ -298,9 +298,6 @@ func boolArg(a map[string]any, k string) bool {
return false
}
// aPerson is who an act through the seat is by: the seat does not name its caller.
const aPerson = "a person, through the mesh-delivery seat"
func need(a map[string]any, keys ...string) error {
for _, k := range keys {
if strArg(a, k) == "" {
@@ -367,36 +364,10 @@ func tools(h *Holder, l *listening) []stdio.Tool {
}
return h.Checks(strArg(a, "repository"), number, strArg(a, "commit"))
}},
{Name: seat + "table",
Description: "The state table every delivery runs by: each transition with its guard, each state's bound " +
"and what healer H2 may do once it has passed; ",
Run: func(map[string]any) (any, error) { return TableText(), nil }},
{Name: seat + "stalled",
Description: "Every delivery held past its state's bound, with the transition the table lets healer H2 take. " +
"Also a merge refused or not made past its bound, a group half merged, and a head with no merge check.",
Run: func(a map[string]any) (any, error) { return h.Stalled(), nil }},
{Name: seat + "recheck",
Description: "Refused for every delivery: the checker checks a head again on every move of the head, its trunk or its " +
"group, and nothing is asked of anyone here (novox/hq ADR 0447, issue 1318). Kept so a caller is told where to read.",
Input: map[string]any{"type": "object", "properties": map[string]any{"id": str("the delivery's id"),
"why": str("why, kept with the transition")}, "required": []string{"id", "why"}},
Run: func(a map[string]any) (any, error) {
if err := need(a, "id", "why"); err != nil {
return nil, err
}
return h.Recheck(strArg(a, "id"), strArg(a, "why"), aPerson)
}},
{Name: seat + "close",
Description: "Healer H2's verb: for a proposed delivery past its bound, take the transition the table " +
"names for that state. Refused for any other.",
Input: map[string]any{"type": "object", "properties": map[string]any{"id": str("the delivery's id"),
"why": str("the condition it answers")}, "required": []string{"id", "why"}},
Run: func(a map[string]any) (any, error) {
if err := need(a, "id", "why"); err != nil {
return nil, err
}
return h.Close(strArg(a, "id"), strArg(a, "why"))
}},
{Name: "delivery_merge_again",
Description: "On the operator's word, ask again at once the merge of an approved delivery the forge refused, or of an approved " +
"group held or half merged, at the heads the Approve named (novox/hq issue 1275): every member is read first and " +
@@ -77,7 +77,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
}
// The verbs the seat keeps: times, what-if, release, stop and retire-history went with the plans (ADR 0539 §17), and
// settle with the merge states (design 47 G §431).
if want := []string{"deliveries", "show", "groups", "source", "checks", "table", "stalled", "recheck", "close"}; !reflect.DeepEqual(m.Claims[0].Serves, want) {
if want := []string{"deliveries", "show", "groups", "source", "checks", "stalled"}; !reflect.DeepEqual(m.Claims[0].Serves, want) {
t.Fatalf("serves %v, wanted %v", m.Claims[0].Serves, want)
}
// The forge messages' layout is the bridge's published package forgemsg, taken by its series (ADR 0634 §3, design 32).
@@ -293,7 +293,7 @@ func TestTheGateHoldsTheSeatAsTheSDKsTableOfRelease6HasIt(t *testing.T) {
t.Errorf("the manifest serves %s, which G removed with the merge states", removed)
}
}
for _, verb := range []string{"deliveries", "show", "groups", "checks", "table", "stalled", "recheck", "close"} {
for _, verb := range []string{"deliveries", "show", "groups", "checks", "stalled"} {
if !served[verb] {
t.Errorf("the gate does not serve %s, which the seat keeps", verb)
}
@@ -177,21 +177,6 @@ func TestAFailingMeshCheckRejects(t *testing.T) {
}
}
// 1302 (lead's (2)): the recheck verb refuses a delivery on a mesh/check base, saying the checker checks again on every
// move and where to read it.
func TestRecheckRefusesAMeshCheckDelivery(t *testing.T) {
w := newWorld(t)
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "failure", oneTrunk))
w.settleAll()
_, err := w.h.Recheck(IDOf(oneRepo, oneHead), "try again", "tester")
if err == nil || !strings.Contains(err.Error(), "checker_pull") || !strings.Contains(err.Error(), "checker") {
t.Fatalf("recheck answered %v, want a refusal naming the checker and checker_pull", err)
}
}
// 1318 (the gate's part): a pull request closed unmerged asks the controller to forget nothing: forget-checks is not called.
func TestAClosedPullRequestAsksNoForgetChecks(t *testing.T) {
w := newWorld(t)
@@ -55,13 +55,12 @@ const (
OpFree = "free" // remove from pinned, by revision
OpEnd = "end" // set-fields: ended
OpAct = "act" // set-entry into acts, by id: an act owed the forge with its outcome
OpRemove = "remove" // remove the instance, at its retention
OpHold = "hold" // a month's list: set-entry into records, by instance
OpDrop = "drop" // a month's list: remove from records, by instance
)
// RecordOperations are a record's operations, in the order the manifest's type allows them.
var RecordOperations = []string{OpOpen, OpGroup, OpTrace, OpEntry, OpOutcome, OpAway, OpBack, OpPin, OpFree, OpAct, OpEnd, OpRemove}
var RecordOperations = []string{OpOpen, OpGroup, OpTrace, OpEntry, OpOutcome, OpAway, OpBack, OpPin, OpFree, OpAct, OpEnd}
// HeldOperations are the month's list's operations.
var HeldOperations = []string{OpHold, OpDrop}
@@ -146,8 +146,10 @@ func (f *ledgerFake) Apply(instance, operation string, body any) (uint64, error)
}
func (f *ledgerFake) Remove(instance string) error {
_, err := f.Apply(instance, OpRemove, map[string]any{})
return err
f.seq[instance]++
f.states[instance] = json.RawMessage("null")
f.history = append(f.history, instance+" remove")
return nil
}
func (f *ledgerFake) Get(instance string) (json.RawMessage, uint64, error) {
@@ -381,33 +381,3 @@ func pastBound(d *Delivery, now time.Time) bool {
b, bounded := boundOf(d)
return bounded && !d.State.Final() && (b.For == 0 || now.Sub(boundSince(d)) > b.For)
}
// TableText is the table as the `table` verb answers it.
func TableText() map[string]any {
var rows []map[string]any
for _, r := range Table {
from := make([]string, 0, len(r.From))
for _, s := range r.From {
if s == None {
from = append(from, "(none)")
continue
}
from = append(from, string(s))
}
kind := "observed"
if r.Act {
kind = "asked"
}
rows = append(rows, map[string]any{"from": strings.Join(from, ", "), "event": r.Event, "to": r.To,
"guard": r.Guard, "taken": kind})
}
bounds := map[string]any{}
for s, b := range Bounds {
var h2 []string
for _, e := range b.H2 {
h2 = append(h2, string(e))
}
bounds[string(s)] = map[string]any{"bound": b.For.String(), "h2": h2, "says": b.Says}
}
return map[string]any{"transitions": rows, "bounds": bounds}
}
@@ -252,51 +252,6 @@ func (h *Holder) Stalled() []StalledLine {
return out
}
// Recheck is the `recheck` verb. The checker judges every head and checks it again on every move of its trunk (ADR 0447 §4), and
// the controller judges none (ADR 0524 §5), so there is no ask to make: a delivery that exists is refused, naming the checker
// (the lead's decision for issue 1302), and an unknown id is refused as before.
func (h *Holder) Recheck(id, why, by string) (string, error) {
h.mu.Lock()
defer h.mu.Unlock()
if h.deliveries[id] == nil {
return "", fmt.Errorf("no delivery %q", id)
}
return "", checkerRefusal(id)
}
// Close is healer H2's verb: for a proposed delivery past its bound, its check is asked once more; only a transition
// the table names for that state counts, any other answer is refused.
func (h *Holder) Close(id, why string) (string, error) {
h.mu.Lock()
d := h.deliveries[id]
if d == nil {
h.mu.Unlock()
return "", fmt.Errorf("no delivery %q", id)
}
b, bounded := boundOf(d)
if !bounded || len(b.H2) == 0 {
h.mu.Unlock()
return "", fmt.Errorf("%s is %s: H2 has no transition there — the state is the operator's", id, d.State)
}
if !pastBound(d, h.Now()) {
h.mu.Unlock()
return "", fmt.Errorf("%s has been %s for %s, within its bound of %s: nothing to close", id, d.State,
h.Now().Sub(boundSince(d)).Round(time.Second), b.For)
}
if d.State == Proposed {
return h.reask(d, why) // unlocks
}
h.mu.Unlock()
return "", fmt.Errorf("%s is %s: H2 has no transition there", id, d.State)
}
// reask is H2's close of a proposed delivery past its bound. A head's check is the checker's, which asks nothing of anyone
// and checks again on its own (ADR 0447 §4), so there is no check to ask once more. Called under the lock; it unlocks.
func (h *Holder) reask(d *Delivery, why string) (string, error) {
h.mu.Unlock()
return "", checkerRefusal(d.ID)
}
// Status is the module's own `delivery_status`.
func (h *Holder) Status(listening string) map[string]any {
h.mu.Lock()
+3 -10
View File
@@ -12,10 +12,7 @@
"groups",
"source",
"checks",
"table",
"stalled",
"recheck",
"close"
"stalled"
]
}
],
@@ -248,10 +245,6 @@
"ended"
],
"sequence": true
},
{
"name": "remove",
"effect": "remove"
}
]
},
@@ -343,7 +336,7 @@
"config": {
"refusal-stall-floor-minutes": {
"type": "number",
"description": "minutes, at least, before a merge refused because its trunk moved, whose check the gate asked again, is said as stalled; twice the repository's last check time when that is longer (ADR 0505 §4).",
"description": "minutes, at least, before a merge refused because its trunk moved, whose check the gate asked again, is said as stalled; twice the repository's last check time when that is longer (ADR 0505 \u00a74).",
"minimum": 1,
"maximum": 1440,
"trusted": false,
@@ -351,7 +344,7 @@
},
"author-new-head-minutes": {
"type": "number",
"description": "minutes a delivery that needs its author's new head (a conflict, a failed check, a rejected group) waits for one before the operator hears of it (ADR 0505 §5).",
"description": "minutes a delivery that needs its author's new head (a conflict, a failed check, a rejected group) waits for one before the operator hears of it (ADR 0505 \u00a75).",
"minimum": 1,
"maximum": 10080,
"trusted": false,