Skip to content

den.ctx. Den declarative context definitions.#175

Merged
vic merged 1 commit intomainfrom
ctx
Feb 20, 2026
Merged

den.ctx. Den declarative context definitions.#175
vic merged 1 commit intomainfrom
ctx

Conversation

@vic
Copy link
Copy Markdown
Member

@vic vic commented Feb 18, 2026

Related #174

Declarative Context Definitions

This introduces den.ctx, 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 [2]. 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.

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:

# 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.

# 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:

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:

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:

# 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:

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:

(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:

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:

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.

@vic vic changed the title WIP: den.ctx WIP: den.ctx. Den declarative context definitions. Feb 18, 2026
Comment thread modules/context/initial.nix Outdated
Comment thread modules/context/types.nix Outdated
@vic vic force-pushed the ctx branch 7 times, most recently from 765a1f6 to bef3f80 Compare February 18, 2026 11:06
Comment thread checkmate/modules/formatter.nix
Comment thread modules/context/types.nix Outdated
Comment thread modules/context/hm.nix Outdated
@vic vic marked this pull request as draft February 19, 2026 00:07
@vic vic force-pushed the ctx branch 9 times, most recently from 2ec0c05 to c73014f Compare February 19, 2026 10:02
@vic vic marked this pull request as ready for review February 19, 2026 10:03
@vic vic force-pushed the ctx branch 5 times, most recently from 98f02d5 to 5a698df Compare February 19, 2026 19:07
@vic vic force-pushed the ctx branch 6 times, most recently from c4c5a84 to 9bf8d2d Compare February 19, 2026 23:53
@vic vic changed the title WIP: den.ctx. Den declarative context definitions. den.ctx. Den declarative context definitions. Feb 20, 2026
@vic vic force-pushed the ctx branch 5 times, most recently from 5e1a391 to 855fa67 Compare February 20, 2026 07:36
docs
@vic vic merged commit def3ecf into main Feb 20, 2026
10 checks passed
@vic vic deleted the ctx branch February 20, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant