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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user