From e7e23c57ccb99017392ae8b1fe29dab3d994bf87 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 18:16:50 +0200 Subject: [PATCH] =?UTF-8?q?Read=20a=20module=20whose=20every=20artifact=20?= =?UTF-8?q?names=20its=20paths=20from=20those=20paths=20and=20its=20manife?= =?UTF-8?q?st=20alone,=20so=20a=20change=20elsewhere=20in=20its=20director?= =?UTF-8?q?y=20moves=20nothing=20(issue=201758,=20B1,=20ADR=200499=20?= =?UTF-8?q?=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mesh-controller/key_takein.go | 40 ++++++++++++++++++++++-- cmd/mesh-controller/paths_source_test.go | 40 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 cmd/mesh-controller/paths_source_test.go diff --git a/cmd/mesh-controller/key_takein.go b/cmd/mesh-controller/key_takein.go index b418c049..67423633 100644 --- a/cmd/mesh-controller/key_takein.go +++ b/cmd/mesh-controller/key_takein.go @@ -289,8 +289,14 @@ func buildOfKey(ctx context.Context, keys keyReads, t keyTake, kc keyContext) ( Commit: t.Trunk.Commit, On: k.Build.By, Path: keyPath(t.Trunk), Manifest: raw, SourceFingerprint: k.Fingerprint, At: time.Now().UTC()} // **Read from the manifest, fresh at every key** (the same rule build-agent says, internal/buildreads). Sources are - // left unsaid: the planner then reads the module's whole directory, over-planning and never under-planning, and a - // fingerprint that did not change moves nothing (R4e A9). + // left unsaid but for a module whose every artifact names its paths: the planner then reads the module's whole + // directory, over-planning and never under-planning, and a fingerprint that did not change moves nothing (R4e A9). + // **Its build source where its every artifact names its paths** (novox/hq ADR 0499 §1, §5; issue 1758, B1): the + // planner reads a merge's changed files against those paths and the manifest alone, so a change elsewhere in the + // module's directory touches nothing it builds from. + if own := pathsBuildSource(manifest, keyPath(t.Trunk)); own != nil { + kept.Sources = []inventory.BuildSource{{Paths: own}} + } for _, c := range buildreads.Of(manifest) { kept.Read = append(kept.Read, inventory.ReadRepository{Repository: c.Repository, Ref: c.Ref}) } @@ -422,3 +428,33 @@ func openBuilderKeys() (builderkeys.Reader, func(), error) { } return builderkeys.Reader{JS: api}, js.Close, nil } + +// pathsBuildSource is the build source of a module whose every artifact names the paths it is built from (ADR 0499 +// §1): its manifest and each path, relative to its repository's root, a path read as a file or everything below a +// directory of that name (`/**` holds both). Nil when an artifact names none: the module reads its directory. +func pathsBuildSource(m catalogue.Manifest, dir string) []string { + if m.Build == nil || len(m.Build.Artifacts) == 0 { + return nil + } + in := func(p string) string { + if dir == "" { + return p + } + return dir + "/" + p + } + seen := map[string]bool{} + out := []string{in("module.json")} + for _, a := range m.Build.Artifacts { + if len(a.Paths) == 0 { + return nil + } + for _, p := range a.Paths { + entry := in(strings.Trim(path.Clean(p), "/")) + "/**" + if !seen[entry] { + seen[entry] = true + out = append(out, entry) + } + } + } + return out +} diff --git a/cmd/mesh-controller/paths_source_test.go b/cmd/mesh-controller/paths_source_test.go new file mode 100644 index 00000000..9f667375 --- /dev/null +++ b/cmd/mesh-controller/paths_source_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "github.com/novox/mesh-controller/internal/inventory" +) + +// B1 (ADR 0499 §1, §5; issue 1758): a module whose every artifact names the paths it is built from is read from those +// paths and its manifest alone. mesh-sdk's TypeScript package at its repository's root: a change to its go/ touches no +// module, one to its src/ or its package.json touches it. A module with an artifact naming no paths reads its directory. +func TestAModuleNamingItsPathsIsReadFromThemAlone(t *testing.T) { + m, err := catalogue.ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"lib", + "kind":"package","language":"typescript","paths":["src","package.json","package-lock.json","tsconfig.json"]}]}, + "resources":[]}`)) + if err != nil { + t.Fatal(err) + } + own := pathsBuildSource(m, "") + read := inventory.WithBuildSources(nil, []inventory.BuildSource{{Paths: own}}) + e := inventory.Entry{Manifest: m, Source: inventory.Source{Repository: "novox/mesh-sdk", Path: ""}} + for file, reads := range map[string]bool{ + "go/delivery/builder.go": false, "go/go.mod": false, "README.md": false, + "src/index.ts": true, "src/deep/x.ts": true, "package.json": true, "module.json": true, + } { + if got := readsFile(e, read, file); got != reads { + t.Errorf("%s: read %v, want %v", file, got, reads) + } + } + // In a module's directory, the paths are under it. + if got := pathsBuildSource(m, "modules/sdk"); got[0] != "modules/sdk/module.json" || got[1] != "modules/sdk/src/**" { + t.Errorf("paths under a directory: %v", got) + } + // An artifact naming no paths: the whole directory, as before. + m.Build.Artifacts = append(m.Build.Artifacts, catalogue.Artifact{Name: "files", Kind: "archive", From: "files"}) + if got := pathsBuildSource(m, ""); got != nil { + t.Errorf("a module with an artifact naming no paths is narrowed to %v", got) + } +}