RD-2: the validator accepts one module claiming the seat node-engine

G's node-engine claims its seat as one module; the trunk refused every
claim of a seat the engine holds. One claim is accepted, a second refused
naming both.

Builds: design 47 G §508, §494, §621

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 03:11:42 +01:00
parent 4d1e4e6e6e
commit cf2db47e57
3 changed files with 53 additions and 7 deletions
@@ -167,6 +167,7 @@ func Between(shelf catalogue.Shelf, longestMachine int, known KnownPackages) []s
between = append(between, PackagesCatalogueProblems(shelf, known)...)
between = append(between, IdentityProblems(shelf, longestMachine)...)
between = append(between, DataProblems(shelf)...)
between = append(between, nodeEngineClaimProblems(shelf)...)
sort.Strings(between)
return slices.Compact(between)
}
+19 -7
View File
@@ -4651,13 +4651,6 @@ func claimProblems(m catalogue.Manifest) []string {
}
for _, c := range m.Claims {
if s, known := catalogue.SeatNamed(c.Name); known && s.HeldByTheEngine {
// The node's engine holds it on every node, and no module may (novox/hq ADR 0563 §1): a module answering
// root-free or a hand-over would be an agent answering it.
problems = append(problems, fmt.Sprintf("%s claims %s, which every node's engine holds itself and no "+
"module may (novox/hq ADR 0563 §1)", m.Module, c.Name))
continue
}
if _, known := catalogue.SeatNamed(c.Name); known {
// **A seat's scope and what it delivers are not judged here** (novox/hq ADR 0122).
// This function runs wherever a manifest is parsed, and one of those places is the
@@ -7918,3 +7911,22 @@ func ObjectsCatalogueProblems(shelf catalogue.Shelf) []string {
}
return problems
}
// NodeEngineSeat is the seat the node-engine's module claims as one module (design 47 G §508).
const NodeEngineSeat = "node-engine"
// nodeEngineClaimProblems refuses a revision in which more than one module claims the seat node-engine, naming them: one
// module claims it, the node-engine's own (design 47 G §508), in place of refusing every claim.
func nodeEngineClaimProblems(shelf catalogue.Shelf) []string {
var holders []string
for module, m := range shelf {
if slices.ContainsFunc(m.Claims, func(c catalogue.Claim) bool { return c.Name == NodeEngineSeat }) {
holders = append(holders, module)
}
}
if len(holders) < 2 {
return nil
}
slices.Sort(holders)
return []string{fmt.Sprintf("%s claim the seat %s, which one module claims (design 47 G §508)", strings.Join(holders, " and "), NodeEngineSeat)}
}
@@ -6380,6 +6380,7 @@ func TestRulesOther(t *testing.T) {
t.Run("TakesSettingsIsRefusedWhereItCannotMeanAnything", caseTakesSettingsIsRefusedWhereItCannotMeanAnything)
t.Run("TheInstallationCheckReadsADefault", caseTheInstallationCheckReadsADefault)
t.Run("ASeatNamedViewIsJudgedAgainstTheSeatsViews", caseASeatNamedViewIsJudgedAgainstTheSeatsViews)
t.Run("OneModuleClaimsTheNodeEnginesSeat", caseOneModuleClaimsTheNodeEnginesSeat)
}
// A consumed view, `seat:<seat>.views.<state>`, is accepted where the seat offers the state as a view and refused, by name,
@@ -6401,3 +6402,35 @@ func caseASeatNamedViewIsJudgedAgainstTheSeatsViews(t *testing.T) {
}
}
}
// One module of a revision claims the seat node-engine and is accepted; a second is refused naming both (design 47 G §508).
func caseOneModuleClaimsTheNodeEnginesSeat(t *testing.T) {
was := catalogue.Seats()
t.Cleanup(func() { catalogue.UseSeats(was) })
catalogue.UseSeats([]catalogue.Seat{{Name: NodeEngineSeat, Scope: catalogue.ScopeNode, Decision: "test", HeldByTheEngine: true}})
engine := func(module string) []byte {
return []byte(`{"module":"` + module + `","version":"1","claims":[{"name":"node-engine","scope":"node"}]}`)
}
for _, row := range []struct {
name string
raws map[string][]byte
wants string
}{
{"one claim", map[string][]byte{"mesh-host": engine("mesh-host")}, ""},
{"two claims", map[string][]byte{"mesh-host": engine("mesh-host"), "other": engine("other")},
"mesh-host and other claim the seat node-engine, which one module claims"},
} {
shelf := catalogue.Shelf{}
for name, raw := range row.raws {
m, problems := Manifest(raw, nil)
if len(problems) > 0 {
t.Fatalf("%s: %s refused alone: %v", row.name, name, problems)
}
shelf[name] = m
}
got := strings.Join(Between(shelf, 0, nil), "\n")
if row.wants == "" && strings.Contains(got, "node-engine") || row.wants != "" && !strings.Contains(got, row.wants) {
t.Errorf("%s: %s", row.name, got)
}
}
}