From 9e61f75d3b46703998b255bc24c0ce9b9ad41d31 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 20:36:25 +0100 Subject: [PATCH] W1.1: the seat-definition reader reads a seat's acting rules A definition carrying acts was refused as an unknown field by every reader (design 47, What ADR 0656 leaves to this design). seatdef reads acts at either grain: each rule's step one of the seven, on every or a language, a bound as a duration, one step on one language once; ActsOn and Wait give a rule's match and its bound, the step's default when it names none. W1.2: the node-engine copy follows mesh-contracts a6c5eaa6 (its compose rule). Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/seatdef/seatdef.go | 101 +++++++++++++++++++++++++++++++++++++ go/seatdef/seatdef_test.go | 40 +++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index 40de632..e77fbbb 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -20,6 +20,7 @@ import ( "slices" "sort" "strings" + "time" ) // MaxSize is the largest definition, or part, the mesh keeps (design 26: at most 256 KiB a definition). @@ -92,6 +93,105 @@ type Definition struct { Paths []Path `json:"paths,omitempty"` // Defs are named types a schema refers to as `{"$ref": "#/defs/"}`. Defs map[string]map[string]any `json:"defs,omitempty"` + // Acts are the seat's acting rules (novox/hq ADR 0656 ยง1; design 47 *What ADR 0656 leaves to this design*): each step + // of a delivery the seat's holder acts in, on which modules, and how long an entry waits there for the holder's new + // version. At either grain: a seat-list definition names its holder's step too. + Acts []ActingRule `json:"acts,omitempty"` +} + +// ActingRule is one acting rule of a seat: the step its holder acts in, the modules it acts on (every module, or those +// whose manifest says this language), and how long an entry waits in that step for the holder's new version, a +// duration such as "30m"; none is the step's default (DefaultBound). +type ActingRule struct { + Step string `json:"step"` + On string `json:"on,omitempty"` + Bound string `json:"bound,omitempty"` +} + +// The steps of a delivery a seat's holder acts in (design 47): what the indexer, the checker, the build modules, the +// node-engine, the grants part and the assigner each do for a change. +const ( + StepTakeIn = "take-in" + StepValidate = "validate" + StepCheck = "check" + StepBuild = "build" + StepCompose = "compose" + StepGrant = "grant" + StepPlace = "place" +) + +// Steps are the steps an acting rule names, in a delivery's order. +var Steps = []string{StepTakeIn, StepValidate, StepCheck, StepBuild, StepCompose, StepGrant, StepPlace} + +// OnEvery is an acting rule's `on` for every module; an absent `on` reads as it. +const OnEvery = "every" + +// DefaultBound is how long an entry waits in step for its acting part's new version when the rule names no bound +// (design 47 *An acting part's new version acts first*): 60 minutes to build, 30 to compose, 20 to check (the first +// tier's), 10 to grant and to place; the indexer's take-in and validate have no wait, so zero. +func DefaultBound(step string) time.Duration { + switch step { + case StepBuild: + return 60 * time.Minute + case StepCompose: + return 30 * time.Minute + case StepCheck: + return 20 * time.Minute + case StepGrant, StepPlace: + return 10 * time.Minute + } + return 0 +} + +// Acting is the modules the rule acts on: OnEvery, or a language. +func (r ActingRule) Acting() string { + if r.On == "" { + return OnEvery + } + return r.On +} + +// ActsOn says whether the rule acts on a module whose manifest says language. +func (r ActingRule) ActsOn(language string) bool { + on := r.Acting() + return on == OnEvery || strings.EqualFold(on, strings.TrimSpace(language)) +} + +// Wait is how long an entry waits in the rule's step for its acting part's new version: its bound, or the step's +// default. A bound the reader refused never reaches here; one that does not parse reads as the default. +func (r ActingRule) Wait() time.Duration { + if d, err := time.ParseDuration(r.Bound); err == nil && d > 0 { + return d + } + return DefaultBound(r.Step) +} + +func checkActs(rs []ActingRule) Problems { + var ps Problems + seen := map[string]bool{} + for i, r := range rs { + where := fmt.Sprintf("acts[%d]", i) + switch { + case r.Step == "": + ps = append(ps, Problem{where + ".step", "missing"}) + case !slices.Contains(Steps, r.Step): + ps = append(ps, Problem{where + ".step", fmt.Sprintf("%q is not a step (%s)", r.Step, strings.Join(Steps, ", "))}) + } + if r.On != "" && r.On != OnEvery && !wordName.MatchString(r.On) { + ps = append(ps, Problem{where + ".on", fmt.Sprintf("%q is neither every nor a language's lower-case name", r.On)}) + } + if r.Bound != "" { + if d, err := time.ParseDuration(r.Bound); err != nil || d <= 0 { + ps = append(ps, Problem{where + ".bound", fmt.Sprintf("%q is not a duration such as 30m", r.Bound)}) + } + } + if k := r.Step + " " + r.Acting(); r.Step != "" && seen[k] { + ps = append(ps, Problem{where, fmt.Sprintf("the step %s on %s twice", r.Step, r.Acting())}) + } else { + seen[k] = true + } + } + return ps } // Verb is one verb the seat serves: its input and, written whole, its answer, each a JSON Schema. @@ -554,6 +654,7 @@ func (d Definition) check() Problems { } } ps = append(ps, checkStates(d.States)...) + ps = append(ps, checkActs(d.Acts)...) for i, p := range d.Provisions { where := fmt.Sprintf("provisions[%d]", i) if p.Name == "" { diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index 2cca1b5..548207e 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -99,6 +99,21 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { "key-forms":[{"key":"","value":{}}]}`), "states[0].history"}, {"a state twice", withState(withState(whole, `{"name":"asks","template":"state","key-forms":[{"key":"","value":{}}]}`), `{"name":"asks","template":"log","key-forms":[{"key":"","value":{}}]}`), `"asks" twice`}, + // W1.1: a seat's acting rules (design 47 *What ADR 0656 leaves to this design*), at either grain. + {"acting rules", strings.Replace(whole, `"family":`, `"acts":[{"step":"take-in"},{"step":"validate","on":"every"},`+ + `{"step":"build","on":"go","bound":"60m"}],"family":`, 1), ""}, + {"an acting rule at the seat list's grain", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"compose","bound":"30m"}],"verbs":`, 1), ""}, + {"an acting rule with no step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"on":"go"}],"verbs":`, 1), "acts[0].step: missing"}, + {"an acting rule of an unknown step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"deploy"}],"verbs":`, 1), + `acts[0].step: "deploy" is not a step (take-in, validate, check, build, compose, grant, place)`}, + {"an acting rule on no language", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"Go Lang"}],"verbs":`, 1), + "acts[0].on"}, + {"an acting rule's bound that is no duration", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","bound":"an hour"}],"verbs":`, 1), + `acts[0].bound: "an hour" is not a duration`}, + {"an acting rule's field the format does not know", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","when":"now"}],"verbs":`, 1), + "when: a field the format does not know"}, + {"one step on one language twice", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"grant"},{"step":"grant","on":"every"}],"verbs":`, 1), + "acts[1]: the step grant on every twice"}, {"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`, ErrTooLarge.Error()}, } @@ -199,3 +214,28 @@ func TestAnEventsSubjectNamesItsParts(t *testing.T) { } } } + +// W1.1: what an acting rule reads as where it says less (design 47 *An acting part's new version acts first*). +func TestAnActingRuleReadsItsStepsDefaults(t *testing.T) { + for _, c := range []struct { + rule ActingRule + language string + acts bool + wait string + }{ + {ActingRule{Step: StepBuild, On: "go"}, "go", true, "1h0m0s"}, + {ActingRule{Step: StepBuild, On: "go"}, "typescript", false, "1h0m0s"}, + {ActingRule{Step: StepCompose}, "anything", true, "30m0s"}, + {ActingRule{Step: StepCheck, On: OnEvery}, "", true, "20m0s"}, + {ActingRule{Step: StepGrant, Bound: "5m"}, "go", true, "5m0s"}, + {ActingRule{Step: StepPlace}, "go", true, "10m0s"}, + {ActingRule{Step: StepTakeIn}, "go", true, "0s"}, + } { + if got := c.rule.ActsOn(c.language); got != c.acts { + t.Errorf("%+v acts on %q: %v, want %v", c.rule, c.language, got, c.acts) + } + if got := c.rule.Wait().String(); got != c.wait { + t.Errorf("%+v waits %s, want %s", c.rule, got, c.wait) + } + } +}