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
This commit is contained in:
2026-11-07 04:36:37 +01:00
parent 4a07e7a0ce
commit ce70589c84
2 changed files with 104 additions and 4 deletions
@@ -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)
}
}
+85 -2
View File
@@ -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/<name>"}`) 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}