From ce70589c849c642e2acd20e0c4699cac5bbcbe48 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 7 Nov 2026 04:36:37 +0100 Subject: [PATCH] A checker that hands no seat digests gets the heads' definitions without their verbs' and messages' schemas (issue 1921, milestone 47/G, the group check of 616c42434ac2) The build seat's step state keeps the checking program's answer and refuses a value with a field named like a credential; schema property names such as mesh-vault's verb input 'credentials' tripped it. Neither the manifest validator nor the composer reads those schemas: the verdicts of the head's and the trunk's validator are byte-identical over the group, and the answer is 99,784 bytes (was 171,088). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../cmd/mesh-checking-program/main_test.go | 21 ++++- modules/mesh-delivery-checker/rules/seats.go | 87 ++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go b/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go index e1ca2d3e..5b34dfe1 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-checking-program/main_test.go @@ -60,7 +60,7 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { } heads := lay("heads", map[string]string{ "novox/x/x-contracts/module.json": contracts("x-contracts"), - "novox/x/x-contracts/seats/node-build-x/seat.json": `{"format":1,"seat":"node-build-x","scope":"node","grain":"whole","domain":"Delivery","decision":"ADR 1","description":"kept","includes":["build"],"verbs":[{"name":"own","input":{"type":"object"},"answer":{"type":"object"}}]}`, + "novox/x/x-contracts/seats/node-build-x/seat.json": `{"format":1,"seat":"node-build-x","scope":"node","grain":"whole","domain":"Delivery","decision":"ADR 1","description":"kept","includes":["build"],"verbs":[{"name":"own","input":{"type":"object","properties":{"credentials":{"$ref":"#/defs/Sealed"}}},"answer":{"type":"object"}}],"defs":{"Sealed":{"type":"object"}}}`, "novox/x/x-contracts/parts/build.json": `{"format":1,"part":"build","verbs":[{"name":"current","input":{"type":"object"},"answer":{"type":"object"}}],"events":[{"name":"ended","body":{"type":"object"}}]}`, "novox/x/contracts/module.json": contracts("mesh-contracts"), "novox/x/contracts/seats/mesh-catalog/seat.json": `{"format":1,"seat":"mesh-catalog","scope":"mesh","grain":"whole","domain":"Delivery","decision":"ADR 1"}`, @@ -130,6 +130,12 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { if d.Seat != "node-build-x" || len(d.Verbs) != 2 || len(sa.Left) != 1 || !strings.Contains(sa.Left[0].Why, "whose seats mesh-delivery-contracts defines") { t.Errorf("%s: seats %s, left %+v; want node-build-x with its part's verb, and mesh-catalog left out", name, sa.Seats, sa.Left) } + // The group check of 616c42434ac2: a checker from before G (no digests in these facts) writes the answer into the + // build seat's step state, whose writer refuses a value with a field named like a credential, here a schema's + // property name; the answer carries no verb's or message's schema and no type only they referred to. + if len(sa.Seats) == 1 && (bytes.Contains(sa.Seats[0], []byte("credentials")) || bytes.Contains(sa.Seats[0], []byte("Sealed"))) { + t.Errorf("%s: %s carries a verb's schema to a checker that hands no digests", name, sa.Seats[0]) + } continue } var a rules.Answer @@ -185,7 +191,18 @@ func TestTheCheckingProgramAnswersTheVerdictOfItsFacts(t *testing.T) { for _, m := range append(d.Verbs, d.Events...) { inner += strings.Count(string(m), `"description":"`) } - if _, perr := seatdef.Parse(raw); inner > 0 || d.Description != own.Description || perr != nil { + parsed, perr := seatdef.Parse(raw) + for _, v := range parsed.Verbs { + if len(v.Input) != 0 || len(v.Answer) != 0 { + t.Errorf("B0-shaped facts answer %s's verb %s with its schemas; want none", parsed.Seat, v.Name) + } + } + for name := range parsed.Defs { + if !bytes.Contains(raw, []byte(`"#/defs/`+name+`"`)) { + t.Errorf("B0-shaped facts answer %s with the type %s, which nothing it keeps refers to", parsed.Seat, name) + } + } + if inner > 0 || d.Description != own.Description || perr != nil { t.Errorf("B0-shaped facts answer %s with %d inner descriptions, its own %q: %v; want only its own", raw[:min(len(raw), 80)], inner, d.Description, perr) } } diff --git a/modules/mesh-delivery-checker/rules/seats.go b/modules/mesh-delivery-checker/rules/seats.go index 6c28b0b0..576240bc 100644 --- a/modules/mesh-delivery-checker/rules/seats.go +++ b/modules/mesh-delivery-checker/rules/seats.go @@ -140,8 +140,11 @@ func SeatsOf(heads fs.FS, f Facts) (SeatsAnswer, error) { if f.SeatDigests == nil { // A checker from before G hands no digests, so every definition is answered: whole they are over a run answer's // bound, so every description but the seat's own is left out, of its verbs, events and their schemas, which - // decides no verdict (neither the manifest validator nor the composer reads one); review round 2 of G. - raw = withoutInnerDescriptions(raw) + // decides no verdict (neither the manifest validator nor the composer reads one); review round 2 of G. Nor do they + // read a verb's or a message's schema, and the build seat's step state, which keeps this answer, refuses a value + // carrying a field named like a credential, as a schema's property may be named (the group check of + // 616c42434ac2: mesh-vault's verb input property `credentials`): those schemas are left out too. + raw = withoutMessageSchemas(withoutInnerDescriptions(raw)) } out.Seats = append(out.Seats, raw) } @@ -213,6 +216,86 @@ func localForge(head fs.FS) seatdefs.Forge { } } +// withoutMessageSchemas is a definition whose verbs' input and answer, and whose accepted messages', events' and holder +// events' bodies, are each the empty schema, and whose types (`defs`) are only those something it keeps still refers to +// (a state's key forms, a provision's form, another kept type). Every other field is kept as written; bytes that are not +// a JSON object are given back as they are. +func withoutMessageSchemas(raw json.RawMessage) json.RawMessage { + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + var def map[string]any + if d.Decode(&def) != nil { + return raw + } + empty := func(list any, fields ...string) { + items, _ := list.([]any) + for _, it := range items { + m, ok := it.(map[string]any) + if !ok { + continue + } + for _, f := range fields { + if _, has := m[f]; has { + m[f] = map[string]any{} + } + } + } + } + empty(def["verbs"], "input", "answer") + for _, field := range []string{"accepts", "events", "holder-events"} { + empty(def[field], "body") + } + if defs, ok := def["defs"].(map[string]any); ok { + rest := maps.Clone(def) + delete(rest, "defs") + kept := map[string]bool{} + todo := typesReferred(rest) + for len(todo) > 0 { + name := todo[len(todo)-1] + todo = todo[:len(todo)-1] + if kept[name] || defs[name] == nil { + continue + } + kept[name] = true + todo = append(todo, typesReferred(defs[name])...) + } + maps.DeleteFunc(defs, func(name string, _ any) bool { return !kept[name] }) + if len(defs) == 0 { + delete(def, "defs") + } + } + out, err := json.Marshal(def) + if err != nil { + return raw + } + return out +} + +// typesReferred are the names of the definition's types (`{"$ref": "#/defs/"}`) v refers to, anywhere in it. +func typesReferred(v any) []string { + var out []string + var walk func(any) + walk = func(v any) { + switch t := v.(type) { + case map[string]any: + if ref, ok := t["$ref"].(string); ok { + if name, found := strings.CutPrefix(ref, "#/defs/"); found { + out = append(out, name) + } + } + for _, x := range t { + walk(x) + } + case []any: + for _, x := range t { + walk(x) + } + } + } + walk(v) + return out +} + // schemaData are the keywords of a schema whose value is data, never a schema: what they hold is kept as written. var schemaData = map[string]bool{"examples": true, "enum": true, "const": true, "default": true}