···79798080Unlike `flake.modules.<class>.<aspect>` which is _flat_, aspects can be nested forming a _tree_ by using the `provides` (short alias: `_`) attribute. Each aspect can also specify a list of `includes` of other aspects, forming a _graph_ of dependencies.
81818282-```nix
8383-{
8484- flake.aspects = {
8585- gaming = {
8686- nixos = {};
8787- darwin = {};
8282+---
88838989- _.emulation = { aspect, ... }: {
9090- nixos = {};
8484+## Usage
91859292- _.nes.nixos = {};
9393- _.gba.nixos = {};
8686+### As a `flake-parts` Module
94879595- includes = with aspect._; [ nes gba ];
9696- };
8888+```nix
8989+{ inputs, ... }: {
9090+ imports = [ inputs.flake-aspects.flakeModule ];
9191+ flake.aspects = {
9292+ sliding-desktop = {
9393+ nixos = { }; # Niri on Linux
9494+ darwin = { }; # Paneru on macOS
9795 };
9696+ awesome-cli = {
9797+ nixos = { }; darwin = { }; homeManager = { }; nixvim = { };
9898+ };
9999+ };
100100+ flake.nixosConfigurations.my-host = inputs.nixpkgs.lib.nixosSystem {
101101+ modules = [
102102+ inputs.self.modules.nixos.sliding-desktop # read resolved module
103103+ ];
98104 };
99105}
100106```
101107102102-## Usage
108108+### Without Flakes ([test](checkmate/modules/tests/without_flakes.nix))
103109104104-The library can be used in two ways: as a flakes-independent dependency-free utility or as a `flake-parts` module.
110110+```nix
111111+let
105112106106-### As a Dependency-Free Library (`./nix/default.nix`)
113113+ myModules = (lib.evalModules {
114114+ modules = [
115115+ (new-scope "my") # creates my.aspects and my.modules.
116116+ { my.aspects.laptop.nixos = ...; }
117117+ ];
118118+ }).config.my.modules;
107119108108-The core of this project is the [`transpose`](nix/default.nix) function, which is powerful enough to implement cross-aspect dependencies for any Nix configuration class. It accepts an optional `emit` function that can be used to ignore items, modify them, or generate multiple items from a single input.
120120+in lib.nixosSystem { modules = [ myModules.nixos.laptop ]; };
121121+```
109122110110-```nix
111111-let transpose = import ./nix/default.nix { lib = pkgs.lib; }; in
112112-transpose { a.b.c = 1; } # => { b.a.c = 1; }
113113-```
123123+Useful for libraries that want isolated aspect scopes or flake-parts independence (see [`den`'s scope](https://github.com/vic/den/blob/main/nix/scope.nix)).
114124115115-This `emit` function is utilized by the [`aspects`](nix/aspects.nix) library to manage module dependencies between different aspects of the same class. Both `transpose` and `aspects` are independent of flakes.
125125+---
116126117117-#### Use aspects without flakes.
127127+## API ([nix/lib.nix](nix/lib.nix))
118128119119-It is possible to use the aspects system as a library, [without flakes](https://github.com/vic/flake-aspects/blob/b94d806/checkmate.nix#L76). This can be used, for example, to avoid poluting flake-parts' `flake.modules` or by libraries that want to create own isolated aspects scope. For examples of this, see our own [flake-parts integration](nix/flakeModule.nix), and how [`den`](https://github.com/vic/den) creates its own [`den.aspects` scope](https://github.com/vic/den/blob/main/nix/scope.nix) independent of `flakes.aspects`/`flake.modules`.
129129+| Export | Description |
130130+| --------------------- | ----------------------------------------------------------- |
131131+| `transpose { emit? }` | Generic 2-level transposition |
132132+| `types` | Nix type system for aspects and providers |
133133+| `aspects` | Aspect-aware transposition with resolution |
134134+| `new` | Low-level scope factory (callback-based) |
135135+| `new-scope` | Named scope factory (`${name}.aspects` / `${name}.modules`) |
136136+| `forward` | Cross-class module forwarding |
120137121121-### As a Dendritic Flake-Parts Module (`flake.aspects` option)
138138+### Core: `transpose` ([nix/default.nix](nix/default.nix))
122139123123-When used as a `flake-parts` module, the `flake.aspects` option is automatically transposed into `flake.modules`, making the modules available to consumers of your flake.
140140+Generic 2-level attribute set transposition parameterized by an `emit` function.
124141125142```nix
126126-# The code in this example can (and should) be split into different Dendritic modules.
127127-{ inputs, ... }: {
128128- imports = [ inputs.flake-aspects.flakeModule ];
129129- flake.aspects = {
143143+transpose { a.b.c = 1; } # ⇒ { b.a.c = 1; }
144144+```
130145131131- sliding-desktop = {
132132- description = "Next-generation tiling windowing";
133133- nixos = { }; # Configure Niri on Linux
134134- darwin = { }; # Configure Paneru on macOS
135135- };
146146+`emit` receives `{ child, parent, value }` and returns a list of `{ parent, child, value }` items. Default: `lib.singleton` (identity). This allows users to filter, modify or multiply items being transposed. This is exploited by [nix/aspects.nix](nix/aspects.nix) to intercept each transposition and inject [resolution](nix/resolve.nix).
136147137137- awesome-cli = {
138138- description = "Enhances the environment with the best of CLI and TUI";
139139- nixos = { }; # OS services
140140- darwin = { }; # Apps like ghostty, iTerm2
141141- homeManager = { }; # Fish aliases, TUIs, etc.
142142- nixvim = { }; # Plugins
143143- };
148148+Tests: [transpose_swap](checkmate/modules/tests/transpose_swap.nix), [transpose_common](checkmate/modules/tests/transpose_common.nix), [tranpose_flake_modules](checkmate/modules/tests/tranpose_flake_modules.nix).
144149145145- work-network = {
146146- description = "Work VPN and SSH access";
147147- nixos = {}; # Enable OpenSSH
148148- darwin = {}; # Enable macOS SSH server
149149- terranix = {}; # Provision VPN
150150- hjem = {}; # Home: link .ssh keys and configs
151151- };
150150+### Resolution: `resolve` ([nix/resolve.nix](nix/resolve.nix))
152151153153- };
154154-}
155155-```
152152+Recursive dependency resolver. Given a `class` and an `aspect-chain` (the call stack of aspects that led here -- most recent first), it extracts the class-specific config and recursively resolves all `includes`.
156153157157-#### Declaring Cross-Aspect Dependencies
154154+The `aspect-chain` lets providers know who is including them and make decisions based on call context. Tests: [aspect_chain](checkmate/modules/tests/aspect_chain.nix), [aspect_modules_resolved](checkmate/modules/tests/aspect_modules_resolved.nix).
158155159159-Aspects can declare dependencies on other aspects using the `includes` attribute. This allows you to compose configurations in a modular way.
156156+### Scope Factories ([nix/new.nix](nix/new.nix), [nix/new-scope.nix](nix/new-scope.nix))
160157161161-Dependencies are defined at the aspect level, not within individual modules. When a module from an aspect is evaluated (e.g., `flake.modules.nixos.development-server`), the library resolves all dependencies for the `nixos` class and imports the corresponding modules if they exist.
158158+`new` is a callback-based factory: `new (option: transposed: moduleDefinition) aspectsConfig`. The [flakeModule](nix/flakeModule.nix) uses it to wire `flake.aspects → flake.modules`.
162159163163-In the example below, the `development-server` aspect includes the `alice` and `bob` aspects. This demonstrates how to create a consistent development environment across different operating systems and user configurations.
160160+`new-scope` wraps `new` to create named scopes: `new-scope "foo"` produces `foo.aspects` (input) and `foo.modules` (output). Multiple independent namespaces can coexist. Tests: [without_flakes](checkmate/modules/tests/without_flakes.nix), [aspect_assignment](checkmate/modules/tests/aspect_assignment.nix).
164161165165-```nix
166166-{
167167- flake.aspects = { aspects, ... }: {
168168- development-server = {
169169- # This aspect now includes modules from 'alice' and 'bob'.
170170- includes = with aspects; [ alice bob ];
162162+### Forward ([nix/forward.nix](nix/forward.nix))
171163172172- # Without flake-aspects, you would have to do this manually for each class.
173173- # nixos.imports = [ inputs.self.modules.nixos.alice ];
174174- # darwin.imports = [ inputs.self.modules.darwin.bob ];
175175- };
164164+Cross-class configuration forwarding. Routes resolved modules from one class into a submodule path of another class. Used by [`den`](https://github.com/vic/den) to forward `homeManager` modules into `nixos.home-manager.users.<name>`. Test: [forward](checkmate/modules/tests/forward.nix).
176165177177- alice = {
178178- nixos = {};
179179- homeManager = {};
180180- };
166166+---
181167182182- bob = {
183183- darwin = {};
184184- hjem = {};
185185- };
186186- };
187187-}
188188-```
168168+## Dependency Resolution
189169190190-Creating the final OS configurations is outside the scope of this library—for that, see [`vic/den`](https://github.com/vic/den). However, exposing them would look like this:
170170+### `includes` — Cross-Aspect Dependencies ([test](checkmate/modules/tests/aspect_dependencies.nix))
191171192172```nix
193193-{ inputs, ... }:
194194-{
195195- flake.nixosConfigurations.fooHost = inputs.nixpkgs.lib.nixosSystem {
196196- system = "x86_64-linux";
197197- modules = [ inputs.self.modules.nixos.development-server ];
173173+flake.aspects = { aspects, ... }: {
174174+ server = {
175175+ includes = with aspects; [ networking monitoring ];
176176+ nixos = { };
198177 };
199199-200200- flake.darwinConfigurations.fooHost = inputs.darwin.lib.darwinSystem {
201201- system = "aarch64-darwin";
202202- modules = [ inputs.self.modules.darwin.development-server ];
203203- };
204204-}
178178+ networking.nixos = { };
179179+ monitoring.nixos = { };
180180+};
205181```
206182207207-### Advanced Aspect Dependencies: Providers
208208-209209-Dependencies are managed through a powerful abstraction called **providers**. A provider is a value that returns an aspect object, which can then supply modules to the aspect that includes it.
210210-211211-A provider can be either a static aspect object or a function that dynamically returns one. This mechanism enables sophisticated dependency chains, conditional logic, and parameterization.
183183+When `flake.modules.nixos.server` is evaluated, it resolves to `{ imports = [ server.nixos, networking.nixos, monitoring.nixos ] }`. Only classes that exist on the included aspect are imported.
212184213213-#### Default Provider (`__functor`)
185185+### Providers — `provides` / `_` ([test](checkmate/modules/tests/aspect_provides.nix))
214186215215-Each aspect is itself a provider via its hidden option `__functor` (see `nix/types.nix`). You can include aspects directly.
187187+Aspects can expose sub-aspects as providers. `_` is an alias for `provides`.
216188217189```nix
218218-# A 'foo' aspect that depends on 'bar' and 'baz' aspects.
219190flake.aspects = { aspects, ... }: {
220220- foo.includes = with aspects; [ bar baz ];
221221-}
191191+ gaming = {
192192+ nixos = { };
193193+ _.emulation = {
194194+ nixos = { };
195195+ _.nes.nixos = { };
196196+ };
197197+ };
198198+ my-host.includes = [ aspects.gaming._.emulation._.nes ];
199199+};
222200```
223201224224-#### Custom Providers
202202+Providers receive `{ class, aspect-chain }` and can use them for conditional logic or context-aware configuration. The `aspect-chain` tracks the full inclusion path.
225203226226-You can define custom providers to implement more complex logic. A provider function receives the current `class` (e.g., `"nixos"`) and the `aspect-chain` (the list of aspects that led to the call). This allows a provider to act as a conditional proxy or router for dependencies.
204204+### Fixpoint Semantics ([test](checkmate/modules/tests/aspect_fixpoint.nix))
227205228228-In this example, the `kde-desktop` aspect defines a custom `karousel` provider that only returns a module if certain conditions are met:
206206+The top-level `aspects` argument is a fixpoint: providers at any depth can reference siblings or top-level aspects.
229207230208```nix
231231-flake.aspects.kde-desktop._.karousel = { aspect-chain, class }:
232232- if someCondition aspect-chain && class == "nixos" then { nixos = { ... }; } else { };
209209+flake.aspects = { aspects, ... }: {
210210+ two.provides = { aspects, ... }: {
211211+ sub = { includes = [ aspects.sibling ]; classOne = { }; };
212212+ sibling.classOne = { };
213213+ };
214214+ one.includes = [ aspects.two._.sub ];
215215+};
233216```
234217235235-The `karousel` provider can then be included in another aspect:
218218+### Parametric Providers ([test](checkmate/modules/tests/aspect_parametric.nix))
219219+220220+Curried functions act as parametric providers:
236221237222```nix
238223flake.aspects = { aspects, ... }: {
239239- home-server.includes = [ aspects.kde-desktop._.karousel ];
240240-}
224224+ base._.user = userName: {
225225+ nixos.users.${userName}.isNormalUser = true;
226226+ };
227227+ server.includes = [ (aspects.base._.user "bob") ];
228228+};
241229```
242230243243-This pattern allows an included aspect to determine which configuration its caller should use, enabling a tree of dependencies where each node can be either static or parametric.
231231+### Top-Level Parametric Aspects ([test](checkmate/modules/tests/aspect_toplevel_parametric.nix))
232232+233233+Top-level aspects can also be curried providers:
244234245245-#### Parameterized Providers
235235+```nix
236236+flake.aspects = { aspects, ... }: {
237237+ greeter = { message }: { nixos.greeting = message; };
238238+ host.includes = [ (aspects.greeter { message = "hello"; }) ];
239239+};
240240+```
246241247247-Providers can be implemented as curried functions, allowing you to create parameterized modules. This is useful for creating reusable configurations that can be customized at the inclusion site.
242242+### `__functor` Override ([test](checkmate/modules/tests/aspect_default_provider_functor.nix), [test](checkmate/modules/tests/aspect_default_provider_override.nix))
248243249249-For real-world examples, see how `vic/den` defines [auto-imports](https://github.com/vic/den/blob/main/modules/aspects/batteries/import-tree.nix) and [home-managed](https://github.com/vic/den/blob/main/modules/aspects/batteries/home-managed.nix) parametric aspects.
244244+The default `__functor` just returns the aspect itself. However, you can override the `__functor` to allow an aspect to intercept when it is being included and provide different config depending on who is including it.
250245251246```nix
252247flake.aspects = { aspects, ... }: {
253253- system = {
254254- nixos.system.stateVersion = "25.11";
255255- _.user = userName: {
256256- darwin.system.primaryUser = userName;
257257- nixos.users.${userName}.isNormalUser = true;
258258- };
248248+ foo = {
249249+ nixos = { ... };
250250+ __functor = self:
251251+ { class, aspect-chain }:
252252+ if class == "nixos" then self else { darwin = ...; includes = [ ... ]; };
259253 };
254254+};
255255+```
260256261261- home-server.includes = [
262262- aspects.system
263263- (aspects.system._.user "bob")
264264- ];
257257+### Forward ([nix/forward.nix](nix/forward.nix)) ([test](checkmate/modules/tests/forward.nix))
258258+259259+Route modules from one class into a submodule path of another:
260260+261261+```nix
262262+forward {
263263+ each = host.users;
264264+ fromClass = _user: "homeManager";
265265+ intoClass = _user: "nixos";
266266+ intoPath = user: [ "home-manager" "users" user.name ];
267267+ fromAspect = user: den.aspects.${user.name};
265268}
266269```
267270268268-See the `aspects."test provides"` and `aspects."test provides using fixpoints"` sections in the [checkmate tests](checkmate.nix) for more examples of chained providers.
269269-270270-#### The `_` Alias for `provides`
271271-272272-For convenience, `_` is an alias for `provides`. This allows for more concise chaining of providers. For example, `foo.provides.bar.provides.baz` can be written as `foo._.bar._.baz`.
271271+---
273272274273## Testing
275274