From ccb367e68c3c9565f50a2dd560f332a90c4db8f9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 01:57:36 +0100 Subject: [PATCH] =?UTF-8?q?Decode=20a=20manifest=20leniently,=20so=20a=20f?= =?UTF-8?q?ield=20a=20reader=20does=20not=20know=20never=20leaves=20its=20?= =?UTF-8?q?module=20out=20(ADR=200578=20=C2=A73,=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1585, S1: catalogue.Decode sets an unknown field aside and names it (SetAside); only a manifest not read past the field is still left out. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/catalogue/decode.go | 31 +++++++++++++++++++++ go/catalogue/manifest.go | 2 +- go/catalogue/setting_defaults_test.go | 40 ++++++++++++--------------- go/catalogue/unknown_field.go | 9 ++++-- 4 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 go/catalogue/decode.go diff --git a/go/catalogue/decode.go b/go/catalogue/decode.go new file mode 100644 index 0000000..09008b6 --- /dev/null +++ b/go/catalogue/decode.go @@ -0,0 +1,31 @@ +package catalogue + +import ( + "encoding/json" + "strings" +) + +// Decode reads a manifest leniently, as every part but the judge reads one (novox/hq ADR 0578 §3): every field the +// contract knows as it declares it, and a field it does not know set aside and named (SetAside), never refused. It +// judges nothing: no name, seat, bus or catalogue rule is applied, since the indexer judged the manifest once and its +// verdict is in the indexer's state (delivery.VerdictsBucket); a reader takes the verdict beside it +// (delivery.TakeVerdict). An error is only bytes that are no manifest: not JSON, or a field of the wrong shape. +func Decode(raw []byte) (Manifest, error) { + var m Manifest + if err := json.Unmarshal(raw, &m); err != nil { + return Manifest{}, err + } + return m, nil +} + +// SetAside names what Decode set aside of this manifest: each field it does not know, as the decoder words the first +// and with the path of every one it read without, or "" when it knew every field. A reader says it in its own words +// (its journal, its finding) and reads the rest; the part that needs the field takes the newer package (ADR 0578 §6). +func (m Manifest) SetAside() string { return m.unknown } + +// ReadNoFurther is whether Decode could not read past a field it does not know, so the manifest holds its name and +// version alone: a reader holds such a module, as it holds a refused one, and names why (SetAside). +func (m Manifest) ReadNoFurther() bool { return strings.Contains(m.unknown, readNoFurther) } + +// readNoFurther marks, in what a manifest set aside, that it was not read past it. +const readNoFurther = "(read no further: " diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index 5db0e07..c475a59 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -1680,7 +1680,7 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error { fields = manifestFields{} _ = json.Unmarshal(keys["module"], &fields.Module) _ = json.Unmarshal(keys["version"], &fields.Version) - unknown += " (read no further: " + perr.Error() + ")" + unknown += " " + readNoFurther + perr.Error() + ")" } else { unknown += " (read without " + strings.Join(pruned, ", ") + ")" } diff --git a/go/catalogue/setting_defaults_test.go b/go/catalogue/setting_defaults_test.go index d5e50da..1bdf7a9 100644 --- a/go/catalogue/setting_defaults_test.go +++ b/go/catalogue/setting_defaults_test.go @@ -298,10 +298,10 @@ func TestADefaultFillsAContributionAndAServedFactButNotALiteral(t *testing.T) { } } -// A stored manifest with a key this controller does not know, at the top or inside any block, is read -// and its module is left out of every declaration by name; the rest of the catalogue is read; the module -// check refuses it. One case per block whose decoder wraps the decoder's words in its own. -func TestAStoredManifestWithAnUnknownKeyIsLeftOutAndRegistrationRefusesIt(t *testing.T) { +// A stored manifest with a key this reader does not know, at the top or inside any block, is decoded with the key set +// aside and named, and its module is composed, never left out (novox/hq ADR 0578 §6, which changes ADR 0262); the judge +// refuses it. One case per block whose decoder wraps the decoder's words in its own. +func TestAStoredManifestWithAnUnknownKeyIsSetAsideAndTheJudgeRefusesIt(t *testing.T) { for where, raw := range map[string]string{ "the top": `{"module": "later", "version": "2", "a-field-from-later": {"x": 1}}`, "a state": `{"module": "later", "version": "2", "state": [{"name": "s", "a-field-from-later": 1}]}`, @@ -311,12 +311,12 @@ func TestAStoredManifestWithAnUnknownKeyIsLeftOutAndRegistrationRefusesIt(t *tes "an own secret": `{"module": "later", "version": "2", "own-secrets": {"s": {"path": "/x", "a-field-from-later": 1}}}`, "a seat's verb": `{"module": "later", "version": "2", "seats": [{"name": "later-seat", "serves": [{"name": "v", "a-field-from-later": 1}]}]}`, } { - var m Manifest - if err := json.Unmarshal([]byte(raw), &m); err != nil { + m, err := Decode([]byte(raw)) + if err != nil { t.Errorf("%s: the stored manifest was not read: %v", where, err) continue } - if m.Module != "later" || m.Version != "2" || !strings.Contains(m.UnknownField(), `"a-field-from-later"`) { + if m.Module != "later" || m.Version != "2" || !strings.Contains(m.SetAside(), `"a-field-from-later"`) || m.ReadNoFurther() { t.Errorf("%s: read as %q %q, unknown %q", where, m.Module, m.Version, m.UnknownField()) continue } @@ -324,12 +324,8 @@ func TestAStoredManifestWithAnUnknownKeyIsLeftOutAndRegistrationRefusesIt(t *tes t.Errorf("%s: the block's decoder did not say it: %q", where, m.UnknownField()) } left := Resolution{Node: "laptop", Modules: []Manifest{m, notifier()}}.LeftOut(nil, false) - if why := left["later"]; !strings.Contains(why, "uses a field this controller does not know") || - !strings.Contains(why, "a-field-from-later") { - t.Errorf("%s: not left out by name: %v", where, left) - } - if _, notifierLeft := left["notifier"]; notifierLeft { - t.Errorf("%s: another module was left out with it: %v", where, left) + if len(left) != 0 { + t.Errorf("%s: a module was left out for a field set aside: %v", where, left) } if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "a-field-from-later") { t.Errorf("%s: the module check took it: %v", where, err) @@ -373,14 +369,14 @@ func TestALeftOutModuleStillProvidesWhatItProvides(t *testing.T) { t.Errorf("unknown %q does not say it read without %s", m.UnknownField(), want) } } - if !strings.Contains(UnknownFieldReason(m), "left out") { - t.Fatal(UnknownFieldReason(m)) + if why := UnknownFieldReason(m); why != "" { + t.Fatal(why) } } -// A left-out module's data is still copied: the backup holder on its machine keeps its lines while every -// other thing of it is left out. -func TestALeftOutModulesDataIsStillBackedUp(t *testing.T) { +// A module with a field set aside is composed whole: its files are declared and its data backed up (novox/hq ADR 0578 +// §6; before, ADR 0262 left it out and backed up its data alone). +func TestAModuleWithAFieldSetAsideIsComposedWhole(t *testing.T) { var later Manifest if err := json.Unmarshal([]byte(`{"module": "later", "version": "2", "a-field-from-later": 1, "resources": [{"id": "d", "type": "directory", "mode": "0700"}, {"id": "rc", "type": "file", "path": "/etc/later.conf", "content": "x\n"}], @@ -397,12 +393,12 @@ func TestALeftOutModulesDataIsStillBackedUp(t *testing.T) { if err != nil { t.Fatal(err) } - if _, left := composed.LeftOut["later"]; !left { - t.Fatalf("not left out: %v", composed.LeftOut) + if _, left := composed.LeftOut["later"]; left { + t.Fatalf("left out: %v", composed.LeftOut) } got := byID(composed.Resources) - if _, declared := got["later.rc"]; declared { - t.Fatal("the left-out module's file is still declared") + if _, declared := got["later.rc"]; !declared { + t.Fatal("the module's file is not declared") } list, _ := got["backups.list"]["content"].(string) if !strings.Contains(list, "# later") || !strings.Contains(list, "path /var/lib/later/d") { diff --git a/go/catalogue/unknown_field.go b/go/catalogue/unknown_field.go index dc41465..6ce7f3d 100644 --- a/go/catalogue/unknown_field.go +++ b/go/catalogue/unknown_field.go @@ -50,10 +50,13 @@ func asUnknownField(err error) *UnknownFieldError { return nil } -// UnknownFieldReason is why a module whose stored manifest has a key this controller does not know is -// left out of a machine's declaration, or "" when it has none. +// UnknownFieldReason is why a module whose stored manifest has a key this reader does not know is left out of a +// machine's declaration, or "" when it is not. **A field set aside no longer leaves the module out** (novox/hq ADR 0578 +// §6, which changes ADR 0262 here): the lenient decoder reads the rest, the reader names the field (SetAside), and the +// part that needs the field takes the newer package. Only a manifest not read past the field, which holds its name and +// version alone (ReadNoFurther), is still left out, since nothing of it could be composed. func UnknownFieldReason(m Manifest) string { - if m.unknown == "" { + if !m.ReadNoFurther() { return "" } return m.Module + " uses a field this controller does not know (" + m.unknown + "); it is left out " +