den.ctx. Den declarative context definitions. (#175)
Related #174
# Declarative Context Definitions
This introduces [`den.ctx`](https://github.com/vic/den/pull/175), a
declarative system for defining how context (data) is transformed and
which aspects are applied at every stage of the configuration pipeline.
## The Problem
Before this release, `den.default` served two purposes:
- A place to define global or generic includes for `host`, `user`, and
`home` entities.
- The backbone of context propagation — moving data from `{ host }` to
`{ host, user }` to `{ host, user }` in the HM pipeline, etc.
As a consequence, `den.default.includes` was abused by many of us,
including Den itself, because it was [where context transformation
happened](https://github.com/vic/den/blob/f8cb8c618e1680e99c165d2bf826600170abb5a2/modules/aspects/dependencies.nix#L28)
[\[2\]](https://github.com/vic/den/blob/f8cb8c618e1680e99c165d2bf826600170abb5a2/modules/aspects/provides/home-manager/hm-dependencies.nix#L31).
This "dependency system" — parametric aspects installed unconditionally
at `den.default.includes` — was hard to reason about, hard to document,
and hard for people to understand.
The symptoms were duplicate configuration values, caused by lax
parametric functions matching too many pipeline stages.
## What den.ctx Provides
- **Keep `den.default` for what it's good at**: global settings. You can
still use `den.default.includes`, but there are better alternatives now.
- **Move the dependency system out of `den.default.includes`**: those
parametric aspects were not individually testable, and you couldn't
change how data flows. They were Den's hardcoded backbone.
- **Declarative data stages**: context transformations are now explicit.
Given a `host`, you declare how to enumerate users, detect HM support,
etc.
- **Named contexts**: previously we identified contexts only by their
`attrNames` — `{ host }`, `{ host, user }`. Now they have names:
`ctx.host`, `ctx.hm-host`. Names allow different contexts with the same
structural shape but different semantic guarantees.
- **Extensible context flows**: one core principle of Den is not getting
in your way. You can create alternative flows, or use Den purely as a
library.
## Named Contexts: Transform, Don't Validate
Named contexts carry semantic meaning beyond their structure. `ctx.host
{ host }` and `ctx.hm-host { host }` hold the same data, but `hm-host`
**guarantees** that home-manager support was validated:
- `inputs.home-manager` exists (or the host has a custom `hm-module`)
- The host has at least one user with `class = "homeManager"`
You cannot obtain an `hm-host` context unless these conditions hold.
This follows the transform-don't-validate principle.
## How a Context Type Works
A context type has four components: `desc`, `conf`, `includes`, and
`into`.
```nix
den.ctx.foo.desc = "The foo context requires { foo } data.";
den.ctx.foo.conf = { foo }: my-aspects.${foo.name};
```
When `ctx.foo` is applied — it works like a function taking `{ foo }` —
it locates the responsible aspect via `conf`. For example, `ctx.foo {
foo.name = "bar"; }` uses `my-aspects.bar`. The aspect's owned config,
static includes, and parametric includes matching `{ foo }` all
contribute to whatever `ctx.foo` is being used to configure.
Context types are independent of NixOS. Den can be used as a library for
network topologies, declarative cloud infrastructure, or anything
describable as data transformations.
## How a NixOS Configuration Is Built
The initial data for `nixosConfigurations.igloo` is the host itself:
```nix
# Nothing NixOS-specific yet — just a graph of dependencies.
aspect = den.ctx.host {
host = den.hosts.x86_64-linux.igloo;
};
```
The result of `ctxApply` is a new aspect that includes
`den.aspects.igloo` plus the entire transformation chain — user
enumeration, HM detection, defaults.
```nix
# This is where things enter the NixOS domain.
nixosModule = aspect.resolve { class = "nixos"; };
nixosConfigurations.igloo = lib.nixosSystem {
modules = [ nixosModule ];
};
```
These two steps can be adapted for any class, for anything
Nix-configurable.
## Context Propagation
Context transformation is declarative. If your data fans out to other
contexts, you specify the transformations using `.into`:
```nix
den.ctx.foo.conf = { foo }: ...;
den.ctx.moo.conf = { moo }: ...;
den.ctx.foo.into.moo = { foo }: lib.singleton { moo = deriveMoo foo; };
```
All `<source>.into.<target>` transformations are taken into account by
`ctxApply`.
### Why Lists?
Transformations have the type `source → [ target ]`. This enables:
- **Fan-out**: one host produces many `{ host, user }` contexts (`map`)
- **Conditional propagation**: zero or one contexts (`lib.optional`)
- **Pass-through**: identity transformation (`lib.singleton`)
For example, HM detection uses conditional propagation:
```nix
den.ctx.host.into.hm-host = { host }:
lib.optional (isHmSupported host) { inherit host; };
```
Same data, but the named context guarantees validation passed.
## Contexts as Aspect Cutting-Points
Contexts are aspect-like themselves. They have owned configs and
`.includes`:
```nix
# Owned config — only for validated HM hosts:
den.ctx.hm-host.nixos.home-manager.useGlobalPkgs = true;
# Scoped includes — only for validated HM hosts:
den.ctx.hm-host.includes = [
({ host, ... }: { nixos.home-manager.backupFileExtension = "bak"; })
];
```
This is like `den.default.includes` **but scoped** — it only activates
for hosts with validated home-manager support.
## Extending the Context Flow
You can add new transformations to any existing context type:
```nix
den.ctx.hm-host.into.foo = { host }: [ { foo = host.name; } ];
den.ctx.foo.conf = { foo }: ...;
den.ctx.foo.includes = [ ({ foo, ... }: ...) ];
```
The module system merges these definitions. You can extend the pipeline
without modifying any built-in file.
## Custom Context Flows
Each host has a `mainModule` option that defaults to:
```nix
(den.ctx.host { host }).resolve { class = "nixos"; }
```
You can override `mainModule` to use a completely alternative context
flow, independent of `ctx.host`. Custom flows can be designed and tested
in isolation — Den's CI uses a `funny.names` class that has nothing to
do with NixOS to verify context mechanics independently.
## What Happened to den.default?
`den.default` stays and is still useful for truly global settings. The
issue was abusing `den.default.includes` as the context propagation
backbone.
### Internal Changes
Previously, all host, user, and home aspects had:
```nix
includes = [ den.default ]
```
Now they **no longer include `den.default` directly**. Including
`den.default` explicitly is discouraged.
### How Defaults Are Applied Now
Each context type transforms into `default`:
```nix
den.ctx.host.into.default = lib.singleton; # passes { host }
den.ctx.user.into.default = lib.singleton; # passes { host, user }
den.ctx.home.into.default = lib.singleton; # passes { home }
```
`den.default` is now an alias for `den.ctx.default`. The data that flows
into `den.default.includes` comes from these declarative
transformations, not from direct aspect inclusion.
### Best Practices
| Instead of | Use |
|-----------|-----|
| `den.default.includes = [ hostFunc ]` | `den.ctx.host.includes = [
hostFunc ]` |
| `den.default.includes = [ hmFunc ]` | `den.ctx.hm-host.includes = [
hmFunc ]` |
| `den.default.nixos.x = 1` | `den.ctx.host.nixos.x = 1` |
`den.default` remains the right place for values that genuinely apply
everywhere — like `stateVersion`. Use context-specific includes for
anything that belongs to a particular pipeline stage.
authored by
oeiuwq.com
and committed by
This is a binary file and will not be displayed.