Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ den.oeiuwq.com
configurations den dendritic nix aspect oriented
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: preserve aspect identity and adapter through functor evaluation (#398)

Functor-producing functions in parametric.nix (applyIncludes,
deepRecurse, withOwn) carry name, __provider, and eta.adapter from self
into their return values. ctxApply also carries name and __provider on
its result.

Currently, when parametric functors evaluate, they return plain {
includes = [...]; } attrsets — losing the aspect's identity. This means
any code inspecting the resolved aspect (such as a custom adapter via
resolve.withAdapter) sees incorrect defaults instead of the original
aspect's name.

This lays groundwork for context-level adapter resolution, where
adapters declared on aspects or contexts need to propagate through the
parametric pipeline to reach forward.nix and outputs.nix. Child includes
are tagged with the parent's meta.adapter so adapters compose downward
through the include tree.

Upcoming related PRs:
- #397
- Structural provider provenance via __provider paths
- Context-level adapter on ctxApply result
- Forward/output consumption of meta.adapter

authored by

Jason Bowman and committed by
GitHub
f09f211b a2e7241a

+163 -29
+15 -7
checkmate/modules/aspect-functor.nix
··· 69 69 ]; 70 70 }; 71 71 72 + identity = { 73 + meta = { 74 + adapter = null; 75 + provider = [ ]; 76 + }; 77 + name = "<anon>"; 78 + }; 79 + 72 80 flake.tests."test functor applied with empty attrs" = { 73 81 expr = (aspect-example { }); 74 - expected = { 82 + expected = identity // { 75 83 includes = [ 76 84 { nixos.any = 10; } 77 85 ]; ··· 84 92 host = 2; 85 93 } 86 94 ); 87 - expected = { 95 + expected = identity // { 88 96 includes = [ 89 97 { nixos.host = 2; } # host 90 98 { nixos.any = 10; } ··· 98 106 home = 2; 99 107 } 100 108 ); 101 - expected = { 109 + expected = identity // { 102 110 includes = [ 103 111 { nixos.home = 2; } # home 104 112 { nixos.any = 10; } ··· 113 121 unknown = 1; 114 122 } 115 123 ); 116 - expected = { 124 + expected = identity // { 117 125 includes = [ 118 126 { nixos.home = 2; } 119 127 { nixos.any = 10; } ··· 127 135 user = 2; 128 136 } 129 137 ); 130 - expected = { 138 + expected = identity // { 131 139 includes = [ 132 140 { nixos.user = 2; } # user 133 141 { nixos.user-only = 2; } # user-only ··· 143 151 host = 1; 144 152 } 145 153 ); 146 - expected = { 154 + expected = identity // { 147 155 includes = [ 148 156 { nixos.host = 1; } 149 157 { ··· 167 175 host = 1; 168 176 } 169 177 ); 170 - expected = { 178 + expected = identity // { 171 179 includes = [ 172 180 { nixos.host = 1; } 173 181 {
+10 -8
nix/lib/ctx-apply.nix
··· 105 105 result = [ ]; 106 106 } items).result; 107 107 108 - ctxApply = self: ctx: { 109 - includes = assembleIncludes (traverse { 110 - prev = null; 111 - prevCtx = null; 112 - key = self.name; 113 - inherit self ctx; 114 - }); 115 - }; 108 + ctxApply = 109 + self: ctx: 110 + parametric.withIdentity self { 111 + includes = assembleIncludes (traverse { 112 + prev = null; 113 + prevCtx = null; 114 + key = self.name; 115 + inherit self ctx; 116 + }); 117 + }; 116 118 117 119 in 118 120 ctxApply
+37 -14
nix/lib/parametric.nix
··· 3 3 inherit (den.lib) take; 4 4 inherit (den.lib.statics) owned statics isCtxStatic; 5 5 6 + # Preserve aspect identity through functor evaluation. 7 + # Carries name and typed meta options so adapters and provenance 8 + # survive, without leaking freeform user meta to child results. 9 + withIdentity = 10 + self: extra: 11 + let 12 + meta = self.meta or { }; 13 + in 14 + { 15 + name = self.name or "<anon>"; 16 + meta = { 17 + adapter = meta.adapter or null; 18 + provider = meta.provider or [ ]; 19 + }; 20 + } 21 + // extra; 22 + 6 23 parametric.applyIncludes = 7 24 takeFn: aspect: 8 25 aspect 9 26 // { 10 - __functor = self: ctx: { 11 - includes = (builtins.filter (x: x != { })) (map (fn: takeFn fn ctx) (self.includes or [ ])); 12 - }; 27 + __functor = 28 + self: ctx: 29 + withIdentity self { 30 + includes = builtins.filter (x: x != { }) (map (fn: takeFn fn ctx) (self.includes or [ ])); 31 + }; 13 32 }; 14 33 15 34 mapIncludes = ··· 35 54 __functor = 36 55 self: 37 56 { class, aspect-chain }: 38 - { 57 + withIdentity self { 39 58 includes = [ 40 59 (include self { inherit class aspect-chain; }) 41 60 (mapIncludes (deepRecurse include branch leaf) leaf (branch aspect)) ··· 64 83 functor: aspect: 65 84 aspect 66 85 // { 67 - __functor = self: ctx: { 68 - includes = 69 - if isCtxStatic ctx then 70 - [ 71 - (owned self) 72 - (statics self ctx) 73 - ] 74 - else 75 - [ (functor self ctx) ]; 76 - }; 86 + __functor = 87 + self: ctx: 88 + withIdentity self { 89 + includes = 90 + if isCtxStatic ctx then 91 + [ 92 + (owned self) 93 + (statics self ctx) 94 + ] 95 + else 96 + [ (functor self ctx) ]; 97 + }; 77 98 }; 99 + 100 + parametric.withIdentity = withIdentity; 78 101 79 102 parametric.__functor = _: parametric.withOwn parametric.atLeast; 80 103 in
+101
templates/ci/modules/features/identity-preservation.nix
··· 1 + { denTest, lib, ... }: 2 + { 3 + flake.tests.identity-preservation = 4 + let 5 + getName = 6 + { aspect, recurse, ... }: 7 + { 8 + name = aspect.name; 9 + adapter = aspect.meta.adapter or null; 10 + children = map (i: (recurse i)) (aspect.includes or [ ]); 11 + }; 12 + in 13 + { 14 + 15 + test-parametric-aspect-preserves-name = denTest ( 16 + { den, ... }: 17 + { 18 + den.aspects.igloo.includes = [ den.aspects.foo ]; 19 + 20 + den.aspects.foo = 21 + { host }: 22 + { 23 + nixos.environment.sessionVariables.WHO = "foo"; 24 + }; 25 + 26 + expr = (den.lib.aspects.resolve.withAdapter getName "nixos" den.aspects.igloo).name; 27 + expected = "igloo"; 28 + } 29 + ); 30 + 31 + test-parametric-child-preserves-name = denTest ( 32 + { den, ... }: 33 + { 34 + den.aspects.foo.includes = [ den.aspects.bar ]; 35 + den.aspects.bar = 36 + { host }: 37 + { 38 + nixos = { }; 39 + }; 40 + 41 + expr = 42 + let 43 + result = den.lib.aspects.resolve.withAdapter getName "nixos" den.aspects.foo; 44 + in 45 + (lib.head result.children).name; 46 + expected = "bar"; 47 + } 48 + ); 49 + 50 + test-meta-preserved-through-functor = denTest ( 51 + { den, ... }: 52 + { 53 + den.aspects.foo.nixos = { }; 54 + 55 + expr = (den.lib.aspects.resolve.withAdapter getName "nixos" den.aspects.foo).adapter; 56 + expected = null; 57 + } 58 + ); 59 + 60 + # meta.loc, meta.name, meta.file are set by aspectMeta at merge 61 + # time and aren't available during functor evaluation. They don't 62 + # need explicit preservation — aspectType.merge provides defaults 63 + # for curried results, and non-curried results pass through as-is. 64 + test-den-meta-not-available-at-functor-eval = denTest ( 65 + { den, ... }: 66 + let 67 + getMeta = 68 + { aspect, recurse, ... }: 69 + { 70 + metaName = aspect.meta.name or null; 71 + children = map (i: recurse i) (aspect.includes or [ ]); 72 + }; 73 + in 74 + { 75 + den.aspects.foo.includes = [ den.aspects.bar ]; 76 + den.aspects.bar = 77 + { host }: 78 + { 79 + nixos = { }; 80 + }; 81 + 82 + expr = 83 + let 84 + result = den.lib.aspects.resolve.withAdapter getMeta "nixos" den.aspects.foo; 85 + child = lib.head result.children; 86 + in 87 + { 88 + # Both get "" because aspectMeta's mkForce hasn't 89 + # resolved when the functor/merge runs. 90 + parentMetaName = result.metaName; 91 + childMetaName = child.metaName; 92 + }; 93 + expected = { 94 + parentMetaName = ""; 95 + childMetaName = ""; 96 + }; 97 + } 98 + ); 99 + 100 + }; 101 + }