From 2e4eb0be90a577b4ea2191b92a8ecafe5f0c1477 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 21:32:04 +0100 Subject: [PATCH] =?UTF-8?q?Catalogue:=20a=20module's=20needs=20include=20i?= =?UTF-8?q?ts=20templates'=20text=20(ADR=200632=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker's daemon file and artifact-store-tools' store address move to the image seat's reach in their templates while their manifests stay the same, so needs read from the manifest alone let an engine before the reading lose the file. ModuleNeeds reads the templates TemplatePaths names; the merge message carries the result per directory for the indexer. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- go/catalogue/manifest_reads.go | 41 ++++++++++++++++++++++++++----- go/catalogue/own_manifest_test.go | 17 ++++++++++++- go/catalogue/template.go | 20 +++++++++++++++ go/delivery/source.go | 4 +++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/go/catalogue/manifest_reads.go b/go/catalogue/manifest_reads.go index ac69427..98503a1 100644 --- a/go/catalogue/manifest_reads.go +++ b/go/catalogue/manifest_reads.go @@ -163,12 +163,8 @@ func ManifestNeeds(raw []byte) ([]string, error) { return nil, err } used := map[string]bool{} - for _, f := range referenceForms { - if bytes.Contains(raw, []byte("${"+f+":")) { - used[ReadForm+f] = true - } - } - if ofSeatReach.Match(raw) || claimsAtMesh(top, ContainerImageRegistrySeat) { + textNeeds(raw, used) + if claimsAtMesh(top, ContainerImageRegistrySeat) { used[ReadForm+ReferenceFormSeatReach] = true } if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) { @@ -429,6 +425,39 @@ func asList(v any) []any { return list } +// textNeeds adds the reference forms a manifest's or a template's text writes to used: a form the engine fills in a +// file's text is read there whether the text is inline or in a template file. +func textNeeds(text []byte, used map[string]bool) { + for _, f := range referenceForms { + if bytes.Contains(text, []byte("${"+f+":")) { + used[ReadForm+f] = true + } + } + if ofSeatReach.Match(text) { + used[ReadForm+ReferenceFormSeatReach] = true + } +} + +// ModuleNeeds is ManifestNeeds of a module whose files take their text from templates (template.go): what the manifest +// needs and what each template's text writes, by template path (TemplatePaths), sorted. A module's needs are both: an +// engine renders the template's text as it renders inline content, so a template naming ${seat:x:reach} needs the +// reading as the same content inline does, though the manifest itself is unchanged (novox/hq ADR 0632 §2). A template +// named and not given is no need; the reader of the files says it could not read one. +func ModuleNeeds(raw []byte, templates map[string]string) ([]string, error) { + needs, err := ManifestNeeds(raw) + if err != nil || len(templates) == 0 { + return needs, err + } + used := map[string]bool{} + for _, n := range needs { + used[n] = true + } + for _, text := range templates { + textNeeds([]byte(text), used) + } + return sortedSet(used), nil +} + func sortedSet(set map[string]bool) []string { out := make([]string, 0, len(set)) for k := range set { diff --git a/go/catalogue/own_manifest_test.go b/go/catalogue/own_manifest_test.go index ce279b0..21ae5f8 100644 --- a/go/catalogue/own_manifest_test.go +++ b/go/catalogue/own_manifest_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "maps" "os" "slices" "testing" @@ -87,12 +88,26 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) { {"the store's holder", `{"module":"distribution","version":"1","claims":[{"name":"container-image-registry",` + `"scope":"mesh","serves":["list"]}],"listens":[{"name":"registry","port":5000,"from":"mesh"}]}`, []string{"form:seat-reach", "seat:container-image-registry"}, []string{"form:seat-reach", "seat:container-image-registry"}}, + // docker's daemon file as ADR 0632 leaves it: the manifest unchanged, its template naming the image seat's reach; + // TemplatePaths names the template a reader of the files reads. + {"a template naming a seat's reach", `{"module":"d","version":"1","resources":[{"id":"daemon","type":"file",` + + `"path":"/etc/d.json","template":"files/daemon.json"}]}`, []string{"form:seat", "form:seat-reach"}, + []string{"form:seat-reach"}}, {"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil}, } { - needs, err := ManifestNeeds([]byte(row.raw)) + templates := map[string]map[string]string{ // the templates' text a row's module names + "a template naming a seat's reach": {"files/daemon.json": `{"x":"${seat:container-image-registry:reach}"}`}, + }[row.why] + needs, err := ModuleNeeds([]byte(row.raw), templates) if err != nil { t.Fatalf("%s: %v", row.why, err) } + if templates != nil { + m, err := Decode([]byte(row.raw)) + if paths := TemplatePaths(m); err != nil || !slices.Equal(paths, slices.Sorted(maps.Keys(templates))) { + t.Errorf("%s: TemplatePaths = %v (%v), want %v", row.why, paths, err, slices.Sorted(maps.Keys(templates))) + } + } for _, n := range row.needs { if !slices.Contains(needs, n) { t.Errorf("%s: needs %v, not %s", row.why, needs, n) diff --git a/go/catalogue/template.go b/go/catalogue/template.go index d5d8945..53d849f 100644 --- a/go/catalogue/template.go +++ b/go/catalogue/template.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "unicode/utf8" ) @@ -184,6 +185,25 @@ func fillItemTemplates(m Manifest, items []Item, text func(string) (string, bool return out, nil } +// TemplatePaths is every template the manifest's files and items name, by its path in the module's directory, sorted +// and once each: what a reader of the module's files reads for ModuleNeeds. A template whose name is wrong is left out; +// the judge refuses it by name. +func TemplatePaths(m Manifest) []string { + var out []string + for _, r := range m.Resources { + if name, has, wrong := templateOf(m.Module, m.TemplatesDir(), r); has && wrong == "" { + out = append(out, name) + } + } + for _, it := range m.Items { + if it.Template != "" && itemTemplateProblem(m, it.Template) == "" { + out = append(out, it.Template) + } + } + slices.Sort(out) + return slices.Compact(out) +} + // ReadTemplates is the text of each template the manifest names, read from the module's directory in a repository tree, // by template path. Refused by name: a template the directory lacks, and anything under its templates directory // (TemplatesDir) the build's packing refuses — that directory itself, or any directory or file under it, that is a diff --git a/go/delivery/source.go b/go/delivery/source.go index 6eebdfb..500dfd8 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -594,6 +594,10 @@ type Forge struct { // relative to it (novox/hq ADR 0465 clause 1). Older writers send neither. Manifests map[string]string `json:"manifests,omitempty"` Locks map[string]map[string]string `json:"locks,omitempty"` + // Needs are, of each such directory whose manifest names templates, the reader capabilities of the module whole: + // its manifest's and its templates' text's at the same commit (catalogue.ModuleNeeds; novox/hq ADR 0632 §2). The + // indexer takes them over the manifest's own; a directory absent, or an older writer, has the manifest's alone. + Needs map[string][]string `json:"needs,omitempty"` // Paths are, of each module directory whose manifest names paths on every artifact, each named path's git object // id at the same commit as its manifest, by path relative to the directory (novox/hq ADR 0499 clause 3). The // indexer names the paths; a message without them gives such a module none. Older writers send none.