Validator (A-337): the trunk case counts and declares by every manifest that decodes
mesh/delivery-group group feat/47-G-join11 checking: 15 of 17 member(s) ready
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 9e98b2884440 on 7c0492b06c37 | novox/mesh-delivery#209 in group feat/47-G-join11

A change of migrated manifests was refused against an unmigrated trunk: the
old sections were counted and the seats declared only by the trunk manifests
the new rules accept, so every count was 0 and every declarer gone. The
trunk case now counts and declares by each manifest that decodes, and one
that does not decode is named in the trunk case's verdict.

Builds: design 47 G §712, A-337

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-13 02:53:57 +01:00
parent bf852f27aa
commit 9e98b28844
3 changed files with 114 additions and 25 deletions
+79 -25
View File
@@ -95,6 +95,12 @@ func ShelfCounting(raws map[string][]byte, predefined catalogue.PredefinedList,
// (novox/hq ADR 0578 §1, amended). The indexer judges so; the checker, which runs the judge built at the head of the change,
// judges with Shelf.
func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts, known KnownPackages) (refused, waits map[string][]string, unnamed []string) {
return shelfWaiting(raws, predefined, longestMachine, counts, known, nil)
}
// shelfWaiting is ShelfWaiting where the seats the trunk case declares (A-337) are declared too, each where no module of the
// shelf is its declarer's: a module the shelf holds declares by its own manifest.
func shelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, longestMachine int, counts OldSectionCounts, known KnownPackages, trunk trunkSeats) (refused, waits map[string][]string, unnamed []string) {
refused, waits = map[string][]string{}, map[string][]string{}
shelf := catalogue.Shelf{}
for name, raw := range raws {
@@ -117,20 +123,26 @@ func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, l
}
shelf[name] = m
}
between := Between(shelf, longestMachine, known)
also := trunkSeats{}
for seat, d := range trunk {
if _, held := shelf[d.module]; !held {
also[seat] = d
}
}
problems := between(shelf, longestMachine, known, also)
without := map[string]map[string]bool{} // by module: the problems between the rest, once asked
ends := func(module, problem string) bool {
if without[module] == nil {
rest := maps.Clone(shelf)
delete(rest, module)
without[module] = map[string]bool{}
for _, p := range Between(rest, longestMachine, known) {
for _, p := range between(rest, longestMachine, known, also) {
without[module][p] = true
}
}
return !without[module][problem]
}
for _, p := range between {
for _, p := range problems {
// The modules it names are asked first; only when none of them ends it is every module asked.
var of []string
for _, name := range Named(p, shelf) {
@@ -163,10 +175,15 @@ func ShelfWaiting(raws map[string][]byte, predefined catalogue.PredefinedList, l
// the longest machine name (catalogue.DefaultLongestMachine when not above 0), DataProblems, and
// PackagesCatalogueProblems against the known packages (nil judges no name).
func Between(shelf catalogue.Shelf, longestMachine int, known KnownPackages) []string {
return between(shelf, longestMachine, known, nil)
}
// between is Between with the seats the trunk case declares outside the shelf declared too (A-337).
func between(shelf catalogue.Shelf, longestMachine int, known KnownPackages, trunk trunkSeats) []string {
if longestMachine <= 0 {
longestMachine = catalogue.DefaultLongestMachine
}
between := CatalogueProblems(shelf)
between := catalogueProblems(shelf, trunk)
between = append(between, PackagesCatalogueProblems(shelf, known)...)
between = append(between, IdentityProblems(shelf, longestMachine)...)
between = append(between, DataProblems(shelf)...)
@@ -228,7 +245,7 @@ func Request(r composer.Request) (composer.Verdict, error) {
}
v := composer.Verdict{V: composer.Version, SDK: Identity()}
seen := map[string]bool{}
counts := trunkOldSections(r)
trunkCase := readTrunk(r)
known := knownOf(r.Packages)
for _, c := range r.Cases {
if seen[c.Name] {
@@ -242,7 +259,15 @@ func Request(r composer.Request) (composer.Verdict, error) {
}
raws[name] = m.Raw
}
refused, unnamed := ShelfCounting(raws, r.Predefined, 0, counts, known)
refused, waits, unnamed := shelfWaiting(raws, r.Predefined, 0, trunkCase.counts, known, trunkCase.declared)
for name := range waits {
_, refused[name] = Manifest(raws[name], r.Predefined)
}
if c.Name == TrunkCase {
for name, why := range trunkCase.undecoded {
refused[name] = append(refused[name], undecodedWords(name, why))
}
}
cv := composer.CaseVerdict{Name: c.Name, Manifests: map[string]composer.ManifestRead{}}
for name := range raws {
if problems := refused[name]; len(problems) > 0 {
@@ -270,34 +295,63 @@ func UseSeatList(defs []seatdef.Definition) {
// TrunkCase is the case of a request holding the catalogue as the trunk holds it (the checker's JudgeTrunk).
const TrunkCase = "trunk"
// trunkOldSections counts the old sections the modules accepted at the trunk use, as the indexer counts them from its
// verdicts (issue 1966 D4, ADR 0595 §11): a change is refused at the merge check exactly as at take-in (judged once, ADR
// 0578). Nil where the request holds no trunk case, which refuses no use of a section.
func trunkOldSections(r composer.Request) OldSectionCounts {
// trunkRead is what the trunk case as it stands gives every case of a request (design 47 G A-337, §712, ADR 0579 §2): every
// trunk-case manifest that decodes counts its old sections and declares its seats, accepted by this judge's rules or not, so
// a change is refused only for what it causes and not for a trunk the new rules refuse; each that does not decode is named,
// with why, for the trunk case's verdict.
type trunkRead struct {
counts OldSectionCounts
declared trunkSeats
undecoded map[string]string
}
// trunkSeats are the seats the trunk case's manifests declare, by seat: the first declaring module in name order and its
// declaration.
type trunkSeats map[string]trunkSeat
type trunkSeat struct {
module string
declaration catalogue.SeatDeclaration
}
// readTrunk reads the request's trunk case (TrunkCase): the old sections its manifests use, as the indexer counts them from
// its verdicts (issue 1966 D4, ADR 0595 §11), so a change is refused at the merge check exactly as at take-in (judged once,
// ADR 0578), and the seats they declare. No count where the request holds no trunk case, which refuses no use of a section.
func readTrunk(r composer.Request) trunkRead {
for _, c := range r.Cases {
if c.Name != TrunkCase {
continue
}
raws := map[string][]byte{}
for name, m := range c.Manifests {
if len(m.Raw) > 0 {
raws[name] = m.Raw
}
}
refused, _ := Shelf(raws, r.Predefined, 0)
var accepted []catalogue.Manifest
for name, raw := range raws {
if len(refused[name]) > 0 {
read := trunkRead{declared: trunkSeats{}, undecoded: map[string]string{}}
var decoded []catalogue.Manifest
for _, name := range slices.Sorted(maps.Keys(c.Manifests)) {
raw := c.Manifests[name].Raw
if len(raw) == 0 {
continue
}
if m, err := catalogue.Decode(raw); err == nil {
m.Module = name
accepted = append(accepted, m)
m, err := catalogue.Decode(raw)
if err != nil {
read.undecoded[name] = err.Error()
continue
}
m.Module = name
decoded = append(decoded, m)
for _, s := range m.DefinesSeats {
if _, taken := read.declared[s.Name]; s.Name != "" && !taken {
read.declared[s.Name] = trunkSeat{module: name, declaration: s}
}
}
}
return CountOldSections(accepted)
read.counts = CountOldSections(decoded)
return read
}
return nil
return trunkRead{}
}
// undecodedWords names a trunk-case manifest that does not decode, which counts no old section and declares no seat.
func undecodedWords(module, why string) string {
return fmt.Sprintf("%s does not decode, so it counts no old section and declares no seat for the change (design 47 G "+
"A-337): %s", module, why)
}
var (
@@ -132,6 +132,29 @@ func TestTheJudge(t *testing.T) {
!got["newer"].Accepted || !got["user"].Accepted || !got["mesh-grants"].Accepted {
t.Errorf("the change's old sections: %+v", got)
}
// A-337 (§712, ADR 0579 §2): the trunk case as it stands counts and declares, every manifest of it that decodes, accepted
// by the new rules or not: a change of migrated manifests against an unmigrated trunk (version "1") refuses nothing for
// a count or a declarer, and a trunk manifest that does not decode is named in the trunk case's verdict.
unmigrated := composer.Request{V: composer.Version, Cases: []composer.Case{
{Name: "change", Manifests: map[string]composer.Manifest{
"user": {Raw: json.RawMessage(`{"module":"user","version":"1.0","state":["x"],"seats":[{"name":"channel","scope":"mesh"}]}`)},
"caller": {Raw: json.RawMessage(`{"module":"caller","version":"1.0","uses":["channel","kept"]}`)},
"declarer": {Raw: json.RawMessage(`{"module":"declarer","version":"1","seats":[{"name":"kept","scope":"mesh"}]}`)}}},
{Name: "trunk", Manifests: map[string]composer.Manifest{
"user": {Raw: json.RawMessage(`{"module":"user","version":"1","state":["x"],"seats":[{"name":"channel","scope":"mesh"}]}`)},
"caller": {Raw: json.RawMessage(`{"module":"caller","version":"1"}`)},
"declarer": {Raw: json.RawMessage(`{"module":"declarer","version":"1","seats":[{"name":"kept","scope":"mesh"}]}`)},
"broken": {Raw: json.RawMessage(`{"module":"broken","version":[`)}}}}}
uv, err := Request(unmigrated)
if err != nil {
t.Fatal(err)
}
if got := uv.Cases[0].Manifests; !got["user"].Accepted || !got["caller"].Accepted || got["declarer"].Accepted {
t.Errorf("a change of migrated manifests against an unmigrated trunk: %+v", got)
}
if got := uv.Cases[1].Manifests["broken"]; got.Accepted || !strings.Contains(got.Refused, "broken does not decode") {
t.Errorf("an undecodable trunk manifest is not named: %+v", got)
}
r.Cases[0].Manifests["c"] = composer.Manifest{Digest: strings.Repeat("a", 64)}
if _, err := Request(r); err == nil {
t.Fatal("a manifest left as a digest was judged")
@@ -4806,6 +4806,12 @@ const meshSeatPrefix = "mesh-"
const retiredLoginShell = "login-shell"
func CatalogueProblems(shelf catalogue.Shelf) []string {
return catalogueProblems(shelf, nil)
}
// catalogueProblems is CatalogueProblems with the seats the trunk case declares (A-337) declared too, each a seat no module
// of the shelf declares: a module the trunk holds and the new rules refuse still declares.
func catalogueProblems(shelf catalogue.Shelf, trunk trunkSeats) []string {
var problems []string
// A stream name has one owner (novox/hq ADR 0363).
problems = append(problems, StreamNameProblems(shelf)...)
@@ -4847,6 +4853,12 @@ func CatalogueProblems(shelf catalogue.Shelf) []string {
declared[s.Name] = s
}
}
for seat, d := range trunk {
if _, taken := declaredBy[seat]; !taken {
declaredBy[seat] = d.module
declared[seat] = d.declaration
}
}
exists := func(seat string) bool {
if _, isMesh := catalogue.SeatNamed(seat); isMesh {