Commit 14d1c9a

HPCesia <me@hpcesia.com>
2026-10-20 02:18:48
den: rework mk-settings by upstream docs
See https://den.denful.dev/guides/aspect-settings/ Assisted-by: crush:deepseek-v4.1-flash
1 parent 4316033
Changed files (1)
modules/mk-settings.nix
@@ -1,22 +1,27 @@
-# Settings surface shared by hosts and users. Aspects declare
-# `settings.{host,user}` as an option set or as a function of the entity
-# instance; entities supply values and aspects read them back.
+# Settings surface shared by hosts and users: aspects declare
+# `settings.{host,user}`, entities supply values, aspects read them back.
 {
   config,
+  den,
   lib,
   ...
 }: let
-  # Pipeline-special keys; skip when walking for settings declarations.
-  structuralKeys = [
-    "excludes"
-    "includes"
-    "meta"
-    "name"
-    "provides"
-    "themes" # See modules/users/theme.nix
-  ];
+  inherit (den.lib.aspects.fx.keyClassification) isStructuralKey;
+
+  skipKey = name:
+    isStructuralKey name
+    || (den.classes or {}) ? ${name}
+    || (den.quirks or {}) ? ${name};
+
+  # Root names are aspect names, so a reserved one such as `themes` stays
+  # walkable there, and only keys inside an aspect are classified.
+  isChildKey = prefix: name: name != "settings" && (prefix == [] || !(skipKey name));
+
+  reshape = raw:
+    if raw ? options || raw ? config || raw ? imports
+    then raw
+    else {options = raw;};
 
-  # Collect { prefix, host, user } for aspect nodes declaring settings.
   # Bare-function aspects cannot carry a settings attr and are skipped.
   collect = prefix: node:
     if !(builtins.isAttrs node)
@@ -43,46 +48,43 @@
           else {};
       };
       children = lib.concatMap (name: collect (prefix ++ [name]) node.${name}) (
-        builtins.filter (name:
-          !(builtins.elem name (
-            if prefix == []
-            then []
-            else structuralKeys
-          ))
-          && name != "settings") (
-          builtins.attrNames node
-        )
+        builtins.filter (isChildKey prefix) (builtins.attrNames node)
       );
     in
       mine ++ children;
 
   collected = collect [] config.den.aspects;
 
-  # Build nested option declarations, grouping shared path prefixes. `ctx` is the
-  # entity instance, and forcing it here would recurse: the fixpoint is still
-  # being built (gen-schema ADR-0033), so only lazy reads such as defaults may touch it.
+  # `ctx` is the entity instance, and forcing it here would recurse (gen-schema ADR-0033),
+  # so only lazy reads such as defaults may touch it.
   mkOptions = ctx: nodes: let
     grouped = lib.groupBy (n: builtins.head n.path) nodes;
   in
     lib.mapAttrs (
-      name: group:
-        if lib.all (n: builtins.length n.path == 1) group
-        then let
-          decls = (builtins.head group).decls;
-        in
-          lib.mkOption {
-            type = lib.types.submodule {
-              options =
-                if builtins.isFunction decls
-                then decls ctx
-                else decls;
-            };
-            default = {};
-            description = "Settings for aspect `${name}'.";
-          }
-        else if !(lib.all (n: builtins.length n.path > 1) group)
-        then throw "aspect settings: aspect `${name}' is both a settings leaf and a container"
-        else mkOptions ctx (map (n: n // {path = lib.drop 1 n.path;}) group)
+      name: group: let
+        own = lib.filter (n: builtins.length n.path == 1) group;
+        children = lib.filter (n: builtins.length n.path > 1) group;
+        ownModule =
+          if own == []
+          then {}
+          else let
+            raw = (builtins.head own).decls;
+          in
+            reshape (
+              if builtins.isFunction raw
+              then raw ctx
+              else raw
+            );
+        childOptions =
+          if children == []
+          then {}
+          else mkOptions ctx (map (n: n // {path = lib.drop 1 n.path;}) children);
+      in
+        lib.mkOption {
+          type = lib.types.submodule (ownModule // {options = (ownModule.options or {}) // childOptions;});
+          default = {};
+          description = "Settings for aspect `${name}'.";
+        }
     )
     grouped;
 
@@ -98,12 +100,12 @@
 
   userSettingsType = ctx:
     lib.types.submodule {
-      # The shared registry and hosts also carry raw user-system values
+      # The shared registry may carry keys outside `settings`.
       freeformType = lib.types.attrsOf lib.types.anything;
       options.settings = mkOptions ctx userNodes;
     };
 
-  # Merge the shared `den.users.<username>` values as lowest-priority defs.
+  # `den.users.<username>` values merge as the lowest-priority definition.
   registryAware = inner:
     lib.mkOptionType {
       name = "den-shared-user-system-settings";
@@ -124,8 +126,7 @@ in {
   config = {
     den.reservedKeys = ["settings"];
 
-    # Through imports, not on the kind: the option type must be built inside the
-    # instance for function declarations to receive it.
+    # Built per instance through imports so function declarations receive entity.
     den.schema.host.imports = [
       ({host, ...}: {
         options.settings = lib.mkOption {