Catalogue: a module's needs include its templates' text (ADR 0632 §2; issue 1966)
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
This commit is contained in:
@@ -163,12 +163,8 @@ func ManifestNeeds(raw []byte) ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
used := map[string]bool{}
|
used := map[string]bool{}
|
||||||
for _, f := range referenceForms {
|
textNeeds(raw, used)
|
||||||
if bytes.Contains(raw, []byte("${"+f+":")) {
|
if claimsAtMesh(top, ContainerImageRegistrySeat) {
|
||||||
used[ReadForm+f] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ofSeatReach.Match(raw) || claimsAtMesh(top, ContainerImageRegistrySeat) {
|
|
||||||
used[ReadForm+ReferenceFormSeatReach] = true
|
used[ReadForm+ReferenceFormSeatReach] = true
|
||||||
}
|
}
|
||||||
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
|
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
|
||||||
@@ -429,6 +425,39 @@ func asList(v any) []any {
|
|||||||
return list
|
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 {
|
func sortedSet(set map[string]bool) []string {
|
||||||
out := make([]string, 0, len(set))
|
out := make([]string, 0, len(set))
|
||||||
for k := range set {
|
for k := range set {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -87,12 +88,26 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
|
|||||||
{"the store's holder", `{"module":"distribution","version":"1","claims":[{"name":"container-image-registry",` +
|
{"the store's holder", `{"module":"distribution","version":"1","claims":[{"name":"container-image-registry",` +
|
||||||
`"scope":"mesh","serves":["list"]}],"listens":[{"name":"registry","port":5000,"from":"mesh"}]}`,
|
`"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"}},
|
[]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},
|
{"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 {
|
if err != nil {
|
||||||
t.Fatalf("%s: %v", row.why, err)
|
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 {
|
for _, n := range row.needs {
|
||||||
if !slices.Contains(needs, n) {
|
if !slices.Contains(needs, n) {
|
||||||
t.Errorf("%s: needs %v, not %s", row.why, needs, n)
|
t.Errorf("%s: needs %v, not %s", row.why, needs, n)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
@@ -184,6 +185,25 @@ func fillItemTemplates(m Manifest, items []Item, text func(string) (string, bool
|
|||||||
return out, nil
|
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,
|
// 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
|
// 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
|
// (TemplatesDir) the build's packing refuses — that directory itself, or any directory or file under it, that is a
|
||||||
|
|||||||
@@ -594,6 +594,10 @@ type Forge struct {
|
|||||||
// relative to it (novox/hq ADR 0465 clause 1). Older writers send neither.
|
// relative to it (novox/hq ADR 0465 clause 1). Older writers send neither.
|
||||||
Manifests map[string]string `json:"manifests,omitempty"`
|
Manifests map[string]string `json:"manifests,omitempty"`
|
||||||
Locks map[string]map[string]string `json:"locks,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
|
// 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
|
// 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.
|
// indexer names the paths; a message without them gives such a module none. Older writers send none.
|
||||||
|
|||||||
Reference in New Issue
Block a user