New Intent-oriented and Incremental contexts. (#57)
( Closes #56, #55 ) -- (Pending #58)
This PR introduces a fundamental refactoring of the aspect dependency and context management system. The primary goal is to provide a more robust, explicit, and powerful way to handle how different parts of the configuration (hosts, users, home-manager) interact with each other. This leads to clearer and more predictable evaluations, but also introduces significant breaking changes from previous unified context `userToHost` and `hostToUser`.
> [!IMPORTANT]
> The previous context objects `userToHost` and `hostToUser` prove to be harder to use and have been **completely removed**.
Now contexts are incremental and context now are used for **configuration intent**. For example, a NixOS configuration starts with context `{ OS }` but later expands to `{ OS, user, host }` as soon as the OS has information of which users and hosts it is being applied to. See the examples on aspects.nix for how this replaces `userToHost` and `hostToUser`.
> [!IMPORTANT]
> Since contexts are now _incremental_, they are also no longer _exact_ by default.
What this means: In previous releases, a function `{ host }: { }` context had to match exactly. For example if applied to `{ host = ...; foo = ... }` it would not apply since `foo` is unexpected. This was as to avoid `function called with unexpectd "foo"` errors. However now contexts can be incremental.
A function `{ host, ... }: { }` provides anytime a `host` is in context.
A function `{ host, user, ... }: { }` provides when at least both are in context.
For example, you can use `{ user, ... }: ...` to provide configurations based on user data anytime it is in context.
**OS/HM Intent Contexts**
OS and HM configurations now start with a single `{ OS }` and `{ HM }` contexts. They are higher-level contexts used to describe the **Intent** of the configuration being produced. This, in hand with incremental contexts, allows to get rid of `hostToUser` and `userToHost`.
For example a context `{ OS, user, host }` is explicit about it being a configuration depending on host/user data but also that it is **being used** for providing NixOS/Darwin configuration specifically.
`OS`/`HM` are not used in our provided batteries at all. Instead the batteries rely only on `host`/`user`/`home` data being in context, fixing the complexity that was introduced by previous `hostToUser` and `userToHost`.
`OS`/`HM` are useful for opt-in bi-directional dependencies, this is how we avoid duplicating settings now. Since a function `{ user, host, ...}: { }` can be installed in `den.default`, and `den.default` is included for any User and Host. That function would produce duplicate configurations - this is correct since the function is **explicitly set in den.default** which is included twice: once for the user and once for the host. To avoid this, either install the function on a specific `den.aspect.<host/user>.includes` or use a higher **intent context** like `OS`/`HM`. See how our aspects.nix examples were updated for this.
## refactor: Dependency Resolution Engine
The core dependency resolution logic in `modules/aspects/dependencies.nix` has been rewritten to be more explicit and prevent recursion.
- It now uses `take.exactly` with the new `OS` and `HM` contexts to ensure that dependency-gathering functions run only once at the appropriate level, preventing evaluation loops.
- The logic for how users contribute to a host's configuration (`osIncludesFromUsers`) and how a host contributes to a user's home-manager configuration (`hmIncludesFromHost`) is now clearly defined and separated.
- The new engine correctly passes the intent `OS` and `HM` contexts down to the aspects being resolved.
## refactor: `provides` Modules Updated for New Context
All built-in `provides` modules have been updated to use the new intent-based context system.
- `provides/define-user`: Now uses generic data-specific `{ host, user, ... }` and `{ home, ... }` contexts directly, removing the need for the intermediate `userToHostContext`.
- `provides/home-manager`: Now passes the `{ HM }` **intent** context when resolving a user's `homeManager` aspect.
- `provides/primary-user`: Logic simplified to expect `{ user, host, ... }` instead of `userToHost`.
- `provides/user-shell`: The logic has been unified. A single `userShell` function now correctly applies shell configuration to `nixos`, `darwin`, and `homeManager` attributes based on the context it receives (`{ user, ... }` or `{ home, ... }`).
## test: Enhanced `canTake` Tests
The test suite for `function_can_take.nix` has been expanded to include assertions for the new `takes.exactly` functionality, ensuring that it correctly identifies functions that take an exact set of arguments.
## feat: Granular Context Control with `take` and `parametric`
A new set of library functions, `den.lib.take` and a redesigned `den.lib.parametric`, have been introduced to give aspect authors fine-grained control over how and when their functions are applied.
- **`den.lib.take`**: This is a new functor that allows a function to declare precisely what context it expects.
- `take.atLeast`: The function is applied if the context provides *at least* the required arguments. This is the default behavior for most parametric functions.
- `take.exactly`: The function is applied only if the context provides the *exact* set of required arguments, with no extras. This is crucial for preventing infinite recursion in dependency resolution.
- `take.unused`: A utility to signal which arguments from the context are "consumed" by the function, allowing for clearer composition.
- **`den.lib.parametric`**: The `parametric` functor has been completely overhauled to leverage `take`.
- It now offers `atLeast` and `exactly` variants to create aspects that apply their includes based on these context matching rules.
- The syntax `parametric true` is now an alias for `parametric.atLeast`.
This new system replaces the older, less precise `canTake` logic, providing a more declarative and safer way to write parametric aspects.
## BREAKING CHANGE: Intent Context (`OS` and `HM`)
The context passed to parametric aspects is no longer a flat attribute set. It has been restructured into namespaced contexts to clearly separate concerns between operating system configuration and home-manager configuration.
- **`OS` Context**: When evaluating for a host's operating system configuration (NixOS or `nix-darwin`), aspects now receive an `OS` attribute in their context. The original `host` object is available as `OS.host`.
- **`HM` Context**: When evaluating for a user's `home-manager` configuration, aspects receive an `HM` attribute. This context contains both the `user` and the `host` they belong to (`HM.user` and `HM.host`).
The previous context objects `userToHost` and `hostToUser` proven to be harder to use and have been **completely removed**. And now contexts are incremental. For example, a NixOS configuration starts with context `{ OS }` but later expands to `{ OS, user, host }` as soon as the OS has information of which users and hosts it is being applied to. See the examples on aspects.nix for how this replaces `userToHost` and `hostToUser`.
### Migration Guide
All parametric aspects must be updated to expect the new context structure.
**Before:**
```nix
my-aspect = { userToHost, ... }: {
${userToHost.host.class}.services.foo.enable = true;
};
my-other-aspect = { hostToUser, ... }: {
homeManager.programs.bar.enable = true;
};
```
**After:**
```nix
# OS context is needed only if my-aspect is installed at den.default
my-aspect = { OS, user, host, ... }: {
${host.class}.services.foo.enable = true;
};
# HM intent context is needed only if installed into den.default.
my-other-aspect = { HM, user, host, ... }: {
homeManager.programs.bar.enable = true;
};
```
authored by
oeiuwq.com
and committed by