A collision is two places, not two spellings

checkResources compared paths as written, so ${dir:state}/server.env —
the same characters in every module, a different directory in each —
refused the first two placed modules that met. Paths are placed before
they are compared, under the default root, which keeps every real
collision: distinct modules' places are distinct under any one root,
and a module stating another's placed root is caught because a pathless
directory now owns its placed path in the comparison too.
This commit is contained in:
2026-09-26 18:25:28 +02:00
parent 8ea80f9584
commit fda47558d5
2 changed files with 42 additions and 1 deletions
+20 -1
View File
@@ -705,11 +705,30 @@ func checkResources(modules []Manifest) []string {
ownedPath := map[string]string{} // path → owning module, for the access check below
for _, m := range modules {
// Compared placed, not as written (novox/hq ADR 0112): ${dir:state} is the same six
// characters in every module and a different directory in each — two modules' templates
// being spelled alike is not two modules owning one path. The default root serves the
// comparison: a collision is within one node, and any one root keeps distinct modules'
// places distinct. A reference that cannot be placed is left as written — naming what
// does not exist is the manifest's own problem, refused where it was made.
dirs := dirsFor(m, Rendering{})
for _, r := range m.Resources {
for _, field := range []string{"path", "unit", "name", "package"} {
value, ok := r[field].(string)
if !ok || value == "" {
continue
if field == "path" && fmt.Sprint(r["type"]) == "directory" {
// A pathless directory owns its placed path — a module stating that
// very path is exactly the collision this exists to catch.
value = dirs[fmt.Sprint(r["id"])]
}
if value == "" {
continue
}
}
if field == "path" {
if placed, err := dirFill(value, dirs, m.Module); err == nil {
value = placed
}
}
key := field + " " + value
if other, taken := owner[key]; taken && other != m.Module {