Data is queried, not merged
The NixOS module system merges every definition into one value, then reads it. yen does not merge. A read of a path is one query on the files that answer that path.
Here two feature files declare settings at two paths of one field. Neither file knows the other:
# Each feature declares its own setting, at its own path, in its own file.{ yen, lib, ... }:{ yen.kinds.host.fields.settings.zfs.arcMax = yen.lib.maybe yen.lib.int; # (1) yen.kinds.environment.fields.settings.zfs.arcMax = yen.lib.maybe yen.lib.int; # (2)}{ yen, lib, ... }:{ yen.kinds.host.fields.settings.k3s.role = yen.lib.opt yen.lib.str "agent"; # (1)}In zfs.nix, (1) declares the field settings.zfs.arcMax of every host, and (2) declares
the same path on the kind environment. In k3s.nix, (1) declares settings.k3s.role with the default "agent".
A read of host.settings.k3s.role looks at the instance, then at the type default. It never reads zfs.nix.
result.declaredIn answers which files declare a path:
[ "k3s.nix" "zfs.nix"]Cascades
Section titled “Cascades”These hosts take a value from their own settings, or from their environment:
{ yen, ... }:{ yen.kinds.host.fields.environment = yen.kinds.environment; # (1) yen.environment.prod.settings.zfs.arcMax = 8; # (2)
yen.host.nas = { environment = yen.environment.prod; settings.zfs.arcMax = 32; # (3) }; yen.host.node = { environment = yen.environment.prod; settings.k3s.role = "server"; };}At (1), a host has a ref to an environment. At (2), the environment prod sets arcMax. At (3), nas sets its
own. node sets none. This aspect reads the value for each host:
# `cascade path xs`: the first non-null value along a list, in your order.{ yen, ... }:{ yen.aspects.report = { at = yen.kinds.host; report = { host, ... }: { ${host.name} = { arcMax = yen.lib.cascade [ "settings" "zfs" "arcMax" ] [ host host.environment ]; # (1) k3s = yen.helpers.k3sRole host; # (2) }; }; };}A cascade is a list in your order. At (1), yen.lib.cascade path xs gives the first non-null value along xs.
nas has its own arcMax; node falls back to its environment. At (2), the aspect reads a helper (see Helpers below). The report of the fleet:
{ nas = { arcMax = 32; k3s = "agent"; }; node = { arcMax = 8; k3s = "server"; };}Helpers
Section titled “Helpers”yen.helpers holds shared values and functions. It is an open namespace: any file declares a key, and any
module reads it from its yen arg, as report.nix does. The key k3sRole is declared here:
# `yen.helpers`: shared values and functions. Any file declares a key; any module reads it from `yen`.{ yen, ... }:{ yen.helpers.k3sRole = host: host.settings.k3s.role; # (1)}A fleet chooses its own helpers, so yen does not document them and the schema (see Agent surface) does not list them. A helper is a declaration like any other: one answer per key. A second file that gives the same key is two answers, because two functions are never equal:
# A second file gives the same helper. Functions are never equal, so this is two answers, not an override.{ yen, ... }:{ yen.helpers.k3sRole = host: "server";}[ { code = "two-answers"; path = [ "helpers" "k3sRole" ]; }]Two answers are an error
Section titled “Two answers are an error”One leaf with two different values is not an override. It is an error record, and you keep one answer:
# A second, different answer for one leaf. Data never overrides data.{ yen, ... }:{ yen.host.nas.settings.zfs.arcMax = 16;}[ { class = "fixable"; code = "two-answers"; error = "two answers for host.nas.settings.zfs.arcMax"; fix = "keep one answer for host.nas.settings.zfs.arcMax: data never overrides data"; from = "declarations"; got = 2; path = [ "host" "nas" "settings" "zfs" "arcMax" ]; }]