From 25a2cecd989cc45cf84590d6d144951dadf855c9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 02:33:19 +0100 Subject: [PATCH] =?UTF-8?q?Let=20the=20tree=20readers=20decode,=20give=20t?= =?UTF-8?q?he=20judge=20the=20manifest's=20data=20it=20reads,=20and=20stop?= =?UTF-8?q?=20judging=20outside=20the=20catalogue=20(ADR=200578=20=C2=A73,?= =?UTF-8?q?=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1585, S6-S8: ReadTreeManifestWritten and WithTemplatesFrom decode (the judge in mesh-delivery judges what they answer); Manifest.Predefined, SetPredefined and FormsWritten expose what the moved judge reads; buscompose names its removals without the catalogue's allow-list; tests outside the catalogue decode, two tree-judging tests moved. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/announce/announce.go | 2 +- go/buildstep/trees_named_test.go | 13 ++++------- go/buscompose/health_grants.go | 5 ++-- go/buscompose/modstreams.go | 2 +- go/buscompose/users.go | 2 +- go/catalogue/decode.go | 11 +++++++++ go/catalogue/handed_consumers_test.go | 31 ------------------------- go/catalogue/predefined.go | 13 +++++++---- go/catalogue/setting_in_owner_test.go | 32 -------------------------- go/catalogue/template.go | 4 +++- go/cmd/mesh-compose/main_test.go | 2 +- go/compose/frombus/answer_rows_test.go | 2 +- go/composer/composer_test.go | 2 +- go/overlay/seat_test.go | 5 ++-- go/provisions/provisions_test.go | 4 ---- 15 files changed, 40 insertions(+), 90 deletions(-) diff --git a/go/announce/announce.go b/go/announce/announce.go index c4cbd2b..8cf8867 100644 --- a/go/announce/announce.go +++ b/go/announce/announce.go @@ -33,7 +33,7 @@ const KeptBucket = "mesh-controller_states-announced" var name = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // Key is module's key in KeptBucket: the module itself, which must be one plain name, since a module's name is part of -// its buckets' names (catalogue.StateProblems). +// its buckets' names (the indexer's judge, novox/hq ADR 0578 §1). func Key(module string) (string, error) { if !name.MatchString(module) { return "", fmt.Errorf("announce: %q is not a module that keeps state: lower-case letters, digits and hyphens", module) diff --git a/go/buildstep/trees_named_test.go b/go/buildstep/trees_named_test.go index 3fdf0ea..d9438e4 100644 --- a/go/buildstep/trees_named_test.go +++ b/go/buildstep/trees_named_test.go @@ -75,20 +75,17 @@ func TestModuleTreesFailsNamingANamedPathAbsentAtTheCommit(t *testing.T) { } } -// A manifest whose config value falls back to a predefined key is narrowed here as the indexer narrows it: both read it -// with catalogue.ParseManifestForInputs (issue 1751), so a build module holding no published list computes the same -// tree (ADR 0499 clause 2). +// A manifest whose config value falls back to a predefined key is narrowed here: the merge preview decodes it and judges +// nothing (novox/hq ADR 0578 §3), so a build module holding no published list computes the tree the indexer does (ADR +// 0499 clause 2; issue 1751). func TestModuleTreesNarrowsAManifestFallingBackToAPredefinedKey(t *testing.T) { was := catalogue.ReplacePublished(nil) // the build modules' case: no published list defer catalogue.ReplacePublished(was) root := t.TempDir() raw := `{"module":"xterm","version":"1","config":{"size":{"type":"number","description":"The text size.","predefined":"font-size","default":12,"runtime":true}},` + `"build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript","paths":["src"]}]}}` - if _, err := catalogue.ParseManifest([]byte(raw)); err == nil { - t.Fatal("ParseManifest reads the manifest with no list; the test proves nothing") - } - if _, err := catalogue.ParseManifestForInputs([]byte(raw)); err != nil { - t.Fatalf("the indexer's read refuses the manifest: %v", err) + if _, err := catalogue.Decode([]byte(raw)); err != nil { + t.Fatalf("the preview's read refuses the manifest: %v", err) } writeTreeFile(t, root, "module.json", raw) run := func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { diff --git a/go/buscompose/health_grants.go b/go/buscompose/health_grants.go index f997e60..7225b6e 100644 --- a/go/buscompose/health_grants.go +++ b/go/buscompose/health_grants.go @@ -42,7 +42,7 @@ var ( // write's step, clause 5); the node-engine's acts bucket read (STREAM.INFO with a subject filter lists what a removal // would take, so the tools read before they remove) and its keys removed, which nothing else may empty when it fills; the ASSIGNMENTS stream read and deleted whole (the lease // write, research 072 row 33). A -// removal outside the set is refused, as the catalogue refuses it (catalogue.BusRemovals). +// removal outside the set is refused, as the indexer's judge refuses it in a manifest (novox/hq ADR 0578 §1). func BusRemovalGrants(removes []string) ([]string, error) { var out []string for _, r := range removes { @@ -56,7 +56,8 @@ func BusRemovalGrants(removes []string) ([]string, error) { out = append(out, "$JS.API.STREAM.INFO."+AssignmentsStream, "$JS.API.STREAM.DELETE."+AssignmentsStream) default: return nil, fmt.Errorf("a bus account removes %q, which is not one of %s (novox/hq ADR 0564 Limits 4)", r, - strings.Join(catalogue.BusRemovals, ", ")) + strings.Join([]string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, + catalogue.RemovesAssignmentsStream}, ", ")) } } return out, nil diff --git a/go/buscompose/modstreams.go b/go/buscompose/modstreams.go index d270738..956d2ed 100644 --- a/go/buscompose/modstreams.go +++ b/go/buscompose/modstreams.go @@ -108,7 +108,7 @@ func (s ModuleStream) Stream() Stream { // QueueOwner is the one process of a module that is the durable consumer of one of its own work-queue // streams (novox/hq ADR 0436 §1, §2, §6): named in that process's bus.queues, and checked at registration -// to be the only one (catalogue.WorkQueueStreamProblems). +// to be the only one (the indexer's judge, novox/hq ADR 0578 §1). type QueueOwner struct { // Process is the process resource's id — its bus account's name, with the node and module (novox/hq ADR // 0436 §2). diff --git a/go/buscompose/users.go b/go/buscompose/users.go index aaf966e..31fdfec 100644 --- a/go/buscompose/users.go +++ b/go/buscompose/users.go @@ -108,7 +108,7 @@ type ProcessPrincipal struct { Emits []string // Queues are the module's own work-queue streams this process is the one consumer of. Queues []QueueOwner - // Removes are what its account may remove from the bus, as its bus declares them (catalogue.BusRemovals; novox/hq + // Removes are what its account may remove from the bus, as its bus declares them (the indexer's judge refuses any other; novox/hq // ADR 0564 Limits 4): a tools bundle's account, whose whole authority is then BusRemovalGrants. Removes []string // State is the module's own state, by local name, this process reads and writes. diff --git a/go/catalogue/decode.go b/go/catalogue/decode.go index 09008b6..b362814 100644 --- a/go/catalogue/decode.go +++ b/go/catalogue/decode.go @@ -29,3 +29,14 @@ func (m Manifest) ReadNoFurther() bool { return strings.Contains(m.unknown, read // readNoFurther marks, in what a manifest set aside, that it was not read past it. const readNoFurther = "(read no further: " + +// Predefined is the list of predefined keys the manifest was read with (ReadTreeManifestWritten, or the judge's +// SetPredefined), nil when none: data of the contract a judge reads, never a rule. +func (m Manifest) Predefined() PredefinedList { return m.predefined } + +// SetPredefined keeps the list of predefined keys a manifest is read with, for what reads it after (the judge). +func (m *Manifest) SetPredefined(list PredefinedList) { m.predefined = list } + +// FormsWritten is the fields the manifest's config was written with, in the config form (`config`, `takes-config`, +// `${config:…}`) and in the settings form (novox/hq ADR 0348 §1): data a judge reads to say a manifest mixes them. +func (m Manifest) FormsWritten() (config, settings []string) { return m.forms.config, m.forms.settings } diff --git a/go/catalogue/handed_consumers_test.go b/go/catalogue/handed_consumers_test.go index a93f6ea..abef0b9 100644 --- a/go/catalogue/handed_consumers_test.go +++ b/go/catalogue/handed_consumers_test.go @@ -1,8 +1,6 @@ package catalogue import ( - "os" - "path/filepath" "strings" "testing" ) @@ -103,32 +101,3 @@ func TestAProvidersConsumersFileIsLeftOutWhereItIsNotHanded(t *testing.T) { t.Fatalf("handed: said %q, filled %v", c.HandedOnly, c.Filled) } } - -// What `module check` and the merge gate judge: the provider read from its tree, its text inline. Judged by the same -// rules, a provider naming a provision it does not provide is refused there too, and one that does composes. -func TestAProviderReadFromItsTreeIsJudgedByTheSameRules(t *testing.T) { - read := func(text string) (Manifest, error) { - dir := t.TempDir() - if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(dir, "files", "consumers.json"), []byte(text), 0o644); err != nil { - t.Fatal(err) - } - m, err := ParseManifest([]byte(handedProvider)) - if err != nil { - t.Fatal(err) - } - return WithTemplatesFrom(m, dir) - } - filled, err := read(consumersText) - if err != nil { - t.Fatalf("a provider read from its tree is refused: %v", err) - } - if _, err := notifierOn(t, filled).Compose(handingTo("store:5000")); err != nil { - t.Fatalf("a provider read from its tree does not compose where it would be handed: %v", err) - } - if _, err := read("${consumers:cache}"); err == nil || !strings.Contains(err.Error(), "does not provide") { - t.Fatalf("a provider read from its tree naming a provision it does not provide: %v", err) - } -} diff --git a/go/catalogue/predefined.go b/go/catalogue/predefined.go index ce0f264..2f0e7d0 100644 --- a/go/catalogue/predefined.go +++ b/go/catalogue/predefined.go @@ -236,9 +236,9 @@ func ReplacePublished(list PredefinedList) (was PredefinedList) { } // ReadTreeManifestWritten is the manifest in a module's directory of a repository tree as written, still naming its -// templates, judged against the list of predefined keys at the tree's root as the merge gate judges it -// (cmd/mesh-controller). What a build resolves, given the text it reads back from the archive it packs the templates -// into (Built.Texts, ReadTemplates). +// templates, decoded and judged by nothing (novox/hq ADR 0578 §3: the indexer's judge judges manifests), the tree's +// list of predefined keys read so a tree whose list does not read is still refused. What a build resolves, given the +// text it reads back from the archive it packs the templates into (Built.Texts, ReadTemplates). func ReadTreeManifestWritten(root, dir string) (Manifest, error) { list, err := ReadPredefined(root) if err != nil { @@ -248,7 +248,12 @@ func ReadTreeManifestWritten(root, dir string) (Manifest, error) { if err != nil { return Manifest{}, err } - return ParseManifestWith(raw, list) + m, err := Decode(raw) + if err != nil { + return Manifest{}, err + } + m.predefined = list + return m, nil } // ReadTreeManifest is ReadTreeManifestWritten with each template's text read from the module's directory as the diff --git a/go/catalogue/setting_in_owner_test.go b/go/catalogue/setting_in_owner_test.go index 6e8f359..00affaa 100644 --- a/go/catalogue/setting_in_owner_test.go +++ b/go/catalogue/setting_in_owner_test.go @@ -1,8 +1,6 @@ package catalogue import ( - "os" - "path/filepath" "strings" "testing" ) @@ -181,33 +179,3 @@ func TestAnOwnersKeyIsTheTerminals(t *testing.T) { } } } - -// A key asked only in a template is not refused while the manifest still names its templates (issue 817), and -// is judged once they are filled: WithTemplatesFrom over a tree whose template does not ask for it refuses it. -func TestAnOwnerSettingAskedOnlyInATemplateIsJudgedOnceFilled(t *testing.T) { - raw := `{"module":"sonarr","version":"1", - "build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}, - "resources":[{"id":"recycle-bin","type":"directory","path":"/srv/sonarr/recycle","mode":"0755","owner":"${setting:puid}:${setting:pgid}"}, - {"id":"identity","type":"file","path":"/srv/sonarr/identity.env","mode":"0644","template":"files/identity.env"}]}` - m, err := ParseManifest([]byte(raw)) - if err != nil { - t.Fatalf("a templated manifest was refused: %v", err) - } - write := func(text string) string { - dir := t.TempDir() - if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(dir, "files", "identity.env"), []byte(text), 0o644); err != nil { - t.Fatal(err) - } - return dir - } - if _, err := WithTemplatesFrom(m, write("PUID=${setting:puid}\nPGID=${setting:pgid}\n")); err != nil { - t.Errorf("a template asking for both keys was refused: %v", err) - } - _, err = WithTemplatesFrom(m, write("NOTHING=1\n")) - if err == nil || !strings.Contains(err.Error(), "asks for \"puid\" nowhere else") { - t.Errorf("a template not asking for puid was not refused: %v", err) - } -} diff --git a/go/catalogue/template.go b/go/catalogue/template.go index 0759b7a..455afd8 100644 --- a/go/catalogue/template.go +++ b/go/catalogue/template.go @@ -422,10 +422,12 @@ func WithTemplatesFrom(m Manifest, dir string) (Manifest, error) { if err != nil { return Manifest{}, err } - again, err := parseManifest(raw, m.predefined) + // Decoded again, never judged (novox/hq ADR 0578 §3): the judge judges what this answers where it judges. + again, err := Decode(raw) if err != nil { return Manifest{}, err } + again.predefined = m.predefined again.Moved = m.Moved return again, nil } diff --git a/go/cmd/mesh-compose/main_test.go b/go/cmd/mesh-compose/main_test.go index df60e35..f2d909b 100644 --- a/go/cmd/mesh-compose/main_test.go +++ b/go/cmd/mesh-compose/main_test.go @@ -123,7 +123,7 @@ func TestTheCommandAnswersWhatTheLinkedCompositionAnswers(t *testing.T) { in.Catalogue = map[string]catalogue.Manifest{} for name, m := range r.Cases[0].Manifests { // A request without a list of predefined keys reads a fallback as the indexer does (Request.Predefined). - parsed, err := catalogue.ParseManifestForInputs(m.Raw) + parsed, err := catalogue.Decode(m.Raw) got := v.Cases[0].Manifests[name] if (err == nil) != got.Accepted || (err != nil && err.Error() != got.Refused) { t.Errorf("%s: the manifest of %s: linked %v, command %+v", depth, name, err, got) diff --git a/go/compose/frombus/answer_rows_test.go b/go/compose/frombus/answer_rows_test.go index 45fbe64..167bb27 100644 --- a/go/compose/frombus/answer_rows_test.go +++ b/go/compose/frombus/answer_rows_test.go @@ -282,7 +282,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) { if err != nil { t.Fatal(err) } - f1, err := catalogue.ParseManifest(f1raw) + f1, err := catalogue.Decode(f1raw) if err != nil { t.Fatal(err) } diff --git a/go/composer/composer_test.go b/go/composer/composer_test.go index 9570894..e1ec9ea 100644 --- a/go/composer/composer_test.go +++ b/go/composer/composer_test.go @@ -79,7 +79,7 @@ func TestTheInputTravelsAsJSON(t *testing.T) { // The seats a node's assigned modules claim are held there when the request says the holdings come from the claims. func TestHoldingsComeFromTheClaims(t *testing.T) { - m, err := catalogue.ParseManifest([]byte(demo)) + m, err := catalogue.Decode([]byte(demo)) if err != nil { t.Fatal(err) } diff --git a/go/overlay/seat_test.go b/go/overlay/seat_test.go index d005546..8d472e6 100644 --- a/go/overlay/seat_test.go +++ b/go/overlay/seat_test.go @@ -18,8 +18,9 @@ func TestThePrivateNetworksClaimIsASeatTheMeshDefines(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := catalogue.ParseManifest(raw); err != nil { - t.Errorf("%s, composed by the control plane, is refused: %v", composed["module"], err) + // Decoded here; the indexer's judge judges it (mesh-delivery's judge tests hold it to every rule). + if _, err := catalogue.Decode(raw); err != nil { + t.Errorf("%s, composed by the control plane, does not decode: %v", composed["module"], err) } } seat, defined := catalogue.SeatNamed(TheNetwork) diff --git a/go/provisions/provisions_test.go b/go/provisions/provisions_test.go index 37a9796..a5c49ad 100644 --- a/go/provisions/provisions_test.go +++ b/go/provisions/provisions_test.go @@ -109,10 +109,6 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { if got := DependsOn(keycloak, vaultEntry, nil); len(got) != 0 { t.Errorf("the vault's secret depends on %v", got) } - if _, err := catalogue.ParseManifest([]byte(`{"module":"x","version":"1","provides":[{"name":"secret-db","scope":"mesh"}]}`)); err == nil || - !strings.Contains(err.Error(), "secret-") { - t.Errorf("a provision named secret-*: %v", err) - } if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" { t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead) }