Decode a manifest leniently, so a field a reader does not know never leaves its module out (ADR 0578 §3, §6)

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
This commit is contained in:
2026-11-01 01:57:36 +01:00
parent ff5e00f1f8
commit ccb367e68c
4 changed files with 56 additions and 26 deletions
+31
View File
@@ -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: "
+1 -1
View File
@@ -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, ", ") + ")"
}
+18 -22
View File
@@ -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") {
+6 -3
View File
@@ -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 " +