From 72b7f4571c612d92eb025b63b48957adcd63b0a6 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Fri, 10 Apr 2026 00:35:29 -0700 Subject: [PATCH] fix: applyDeep skips static subs that don't consume parametric ctx PR #419 introduced applyDeep to propagate parametric context into bare-result sub-includes. But it called takeFn unconditionally on every sub, including full static aspects whose default functor (withOwn parametric.atLeast) discards owned class configs in a non-static branch. Result: `den.aspects.role._.sub.nixos.x = true` was silently dropped when role was a parametric parent that included its sub-aspect. Gate the inner re-application on canTake.upTo: a static aspect's default functor has empty functionArgs, so upTo is false and the sub is left alone for the static resolve pass. User-provided provider fns (e.g. `{ host, ... }: { nixos = ...; }`) have host in functionArgs, so upTo fires and their config is materialized. This preserves the Fixes #423. --- nix/lib/parametric.nix | 16 ++++---- ...23-static-sub-aspect-parametric-parent.nix | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 templates/ci/modules/features/deadbugs/issue-423-static-sub-aspect-parametric-parent.nix diff --git a/nix/lib/parametric.nix b/nix/lib/parametric.nix index 4cf8cc3f..12f738bb 100644 --- a/nix/lib/parametric.nix +++ b/nix/lib/parametric.nix @@ -1,6 +1,6 @@ { lib, den, ... }: let - inherit (den.lib) take; + inherit (den.lib) take canTake; inherit (den.lib.statics) owned statics isCtxStatic; # Preserve aspect identity through functor evaluation. @@ -40,13 +40,13 @@ let else if isBareResult then r // { - includes = map ( - sub: - let - sr = takeFn sub ctx; - in - if sr != { } then sr else sub - ) r.includes; + # Only recurse into subs whose functor actually consumes this ctx. + # A static aspect's default functor takes a bare `ctx` (functionArgs + # = {}), so canTake.upTo is false and we leave it alone — its owned + # class configs must be picked up later by the static resolve pass. + # A user-provided provider fn (e.g. { host, ... }: { nixos = ...; }) + # has host in functionArgs; canTake.upTo fires and we materialize it. + includes = map (sub: if canTake.upTo ctx sub then take.upTo sub ctx else sub) r.includes; } else r; diff --git a/templates/ci/modules/features/deadbugs/issue-423-static-sub-aspect-parametric-parent.nix b/templates/ci/modules/features/deadbugs/issue-423-static-sub-aspect-parametric-parent.nix new file mode 100644 index 00000000..3c3d1dd8 --- /dev/null +++ b/templates/ci/modules/features/deadbugs/issue-423-static-sub-aspect-parametric-parent.nix @@ -0,0 +1,38 @@ +# Static sub-aspect (attrset with owned class config) included from a +# parametric parent aspect. Regression from the applyDeep fix in #419: +# re-applying takeFn to the sub invoked its default functor in a non-static +# context, which dropped owned class configs. +# https://github.com/vic/den/pull/423 +{ denTest, ... }: +{ + flake.tests.deadbugs-issue-423 = { + test-static-sub-aspect-from-parametric-parent = denTest ( + { den, igloo, ... }: + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + + # Split across modules so the parametric parent and the static sub + # don't clash on the same `den.aspects.role` attribute definition. + imports = [ + { + den.aspects.role = + { host, ... }: + { + includes = [ den.aspects.role._.sub ]; + }; + } + { + den.aspects.role._.sub.nixos.networking.networkmanager.enable = true; + } + { + den.aspects.igloo.includes = [ den.aspects.role ]; + } + ]; + + expr = igloo.networking.networkmanager.enable; + expected = true; + } + ); + + }; +}