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.

refactor: den-fx (#462)

## Summary

Replace den's legacy recursive tree-walking resolution with an
effects-based pipeline. Aspects compile into effectful computations via
`aspectToEffect` — the tree structure emerges from effect composition,
not explicit recursion. All resolution strategy (constraint checking,
dedup, tracing, context dispatch) lives in handlers.

- **`aspectToEffect` compiler** — single function compiles any aspect
into a computation that emits `emit-class`, `register-constraint`,
`emit-include`, and `resolve-complete` effects
- **Handler-owned recursion** — `emit-include` handler checks
constraints and recurses via effectful resume; `into-transition` handler
processes context transitions with `scope.stateful`
- **Constraint system** — `meta.handleWith` / `meta.excludes` replace fx
usage of `meta.adapter`; scoped constraints via includes-chain ancestry;
`exclude`, `substitute`, `filterBy` constructors with subtree/global
variants
- **Includes chain provenance** — `chain-push`/`chain-pop` effects
replace `__parent` string tracking; observable by any handler for
diagrams, scope visualization, composable analysis
- **Module wiring** — `{ lib, den }` only, no `init` function, barrel
`default.nix`; nix-effects accessed as `den.lib.fx`
- **`den.fxPipeline` option** — defaults to `true`; legacy tests using
`resolve.withAdapter` run with `fxPipeline = false`

### Pipeline architecture

```
nix/lib/aspects/fx/
aspect.nix — aspectToEffect compiler
pipeline.nix — mkPipeline, defaultHandlers, fxResolve
identity.nix — path/identity utilities, collectPathsHandler, pathSetHandler
constraints.nix — exclude, substitute, filterBy constructors
includes.nix — includeIf conditional inclusion
trace.nix — structuredTraceHandler, tracingHandler
handlers/
include.nix — emit-include handler (owns recursion)
transition.nix — into-transition handler (scope.stateful)
ctx.nix — constantHandler, ctxSeenHandler
tree.nix — constraintRegistryHandler, chainHandler, classCollectorHandler
```

### Effect protocol

| Effect | Handler responsibility |
|---|---|
| `emit-class` | Accumulate modules by class |
| `emit-include` | Check constraints, recurse via `aspectToEffect` |
| `register-constraint` | Store in registry with scope/ownerChain |
| `check-constraint` | Query registry, first-registered-wins |
| `chain-push` / `chain-pop` | Track includes-path stack |
| `into-transition` | Walk context transitions with scoped handlers |
| `ctx-seen` | Dedup context stages |
| `resolve-complete` | Emit trace entries, accumulate paths |
| `get-path-set` | Return accumulated path set for `includeIf` guards |
| `<arg-name>` | `constantHandler` resumes with context value |

### Compatibility shims

The type system operates at declaration time and cannot be gated on
`config.den.fxPipeline` without circular evaluation:

- `aspect-chain` in `constantHandler` — provider functions from
`providerFnType.merge` create `{ class, aspect-chain }` functors
- `options.nix` uses legacy `ctxApply` — `config.resolved` can't access
`config.den` without circularity
- Legacy adapter tests run with `fxPipeline = false`

### Also in this PR

- `refactor: replace _. alias with provides.` across all
aspect/output/template modules
- `feat: ci-fast recipe` using nix-eval-jobs for parallel test eval
- Test restructuring: `batteries/`, `context/`, `perf/`, `home-manager/`
subdirs flattened to `features/`
- Regression reproductions for issues #413, #423, #426, #437
- Consolidated design spec at `docs/design/fx-pipeline-spec.md`

## Test plan

- [x] `nix develop -c just ci ""` — 488/488 pass
- [x] `nix develop -c just ci-fast` — parallel eval, 488/488 pass
- [x] `nix flake check` — templates, packages, devShells all pass
- [x] Checkmate `system-agnostic` tests — 15/15 pass
- [x] Legacy tests with `fxPipeline = false` verify backward
compatibility

---------

Co-authored-by: Victor Borja <vborja@apache.org>

authored by

Jason Bowman
Victor Borja
and committed by
GitHub
68c26555 927f4d8e

+7420 -594
+3
Justfile
··· 18 18 ci test="" *args: 19 19 just nix-unit ci "{{test}}" {{args}} 20 20 21 + ci-fast test="" *args: 22 + bash ./ci-fast.bash "{{system}}" "{{test}}" {{args}} 23 + 21 24 bogus *args: 22 25 just nix-unit bogus "bogus" {{args}} 23 26
+2
checkmate/modules/aspect-functor.nix
··· 72 72 identity = { 73 73 meta = { 74 74 adapter = null; 75 + excludes = [ ]; 76 + handleWith = null; 75 77 provider = [ ]; 76 78 }; 77 79 name = "<anon>";
+1
checkmate/modules/formatter.nix
··· 5 5 "Justfile" 6 6 "AGENT*.md" 7 7 "*.txt" 8 + "ci-fast.bash" 8 9 ]; 9 10 perSystem.treefmt.programs.deadnix.enable = false; 10 11 perSystem.treefmt.programs.nixf-diagnose.enable = false;
+71
ci-fast.bash
··· 1 + #!/usr/bin/env bash 2 + # 3 + # Uses nix-eval-jobs with $(nproc) workers 4 + # NOTE: Ignores tests with expectedError 5 + # 6 + set -euo pipefail 7 + 8 + system="${1:-"x86_64-linux"}" 9 + shift 10 + 11 + suite="" 12 + preSuite="" 13 + postSuite="" 14 + 15 + if test -n "${1:-}"; then 16 + suite="$1" 17 + preSuite=".${suite}" 18 + postSuite="${suite}." 19 + shift 20 + fi 21 + 22 + args=($@) 23 + 24 + results=$(mktemp -t den-test-XXXXX.json) 25 + 26 + nix-eval-jobs \ 27 + --flake ./templates/ci#tests${preSuite} \ 28 + --override-input den . \ 29 + --workers $(nproc) \ 30 + --force-recurse \ 31 + --select 'tests: let 32 + system="'"${system}"'"; 33 + go = prefix: v: 34 + if v ? expr then 35 + let 36 + hasExpected = v ? expected && !(v.expected ? undefined); 37 + hasExpectedError = v ? expectedError && !(v.expectedError ? undefined); 38 + pass = if hasExpected then v.expr == v.expected 39 + else if hasExpectedError then true # ignored 40 + else true; 41 + name = builtins.replaceStrings ["." "'\''"] ["-" "_"] prefix; 42 + in derivation { 43 + name = if pass then "PASS-${name}" else "FAIL-${name}"; 44 + system = "${system}"; builder = "/bin/sh"; 45 + args = ["-c" "echo > $out"]; 46 + } 47 + else if builtins.isAttrs v then 48 + builtins.mapAttrs (k: go (if prefix == "" then k else "${prefix}.${k}")) v 49 + else derivation { name = "SKIP"; system = "${system}"; builder = "/bin/sh"; args = ["-c" "echo > $out"]; }; 50 + in builtins.mapAttrs (k: go k) tests' \ 51 + "${args[@]}" 2>/dev/null \ 52 + | tee "$results" \ 53 + | jq -r 'if (.name != null and (.name | startswith("PASS-"))) then "✅ '"${postSuite}"'" + .attr else "❌ '"${postSuite}"'" + .attr end' 54 + 55 + total=$(cat "$results" | wc -l) 56 + pass=$(jq -r 'select(.name != null and (.name | startswith("PASS-"))) | "."' "$results" | wc -l) 57 + fail=$(jq -r 'select(.name != null and (.name | startswith("PASS-") | not)) | "."' "$results" | wc -l) 58 + 59 + 60 + if [ "$fail" -eq 0 ]; then 61 + echo "🎉 ${pass}/${total} successful" 62 + rm "$results" || true 63 + else 64 + echo "😢 ${pass}/${total} successful" 65 + echo 66 + echo "💥 FAILURES (${fail}):" 67 + jq -r 'select(.name != null and (.name | startswith("PASS-") | not)) | "❌ '"${postSuite}"'" + .attr' "$results" 68 + rm "$results" || true 69 + exit 1 70 + fi 71 +
+1
docs/astro.config.mjs
··· 51 51 { label: 'Parametric Aspects', slug: 'explanation/parametric' }, 52 52 { label: 'NixOS Context Pipeline', slug: 'explanation/context-pipeline' }, 53 53 { label: 'Library vs Framework', slug: 'explanation/library-vs-framework' }, 54 + { label: 'ABC on Den Effects', slug: 'explanation/effects' }, 54 55 ], 55 56 }, 56 57 {
+457
docs/design/fx-pipeline-spec.md
··· 1 + # FX Pipeline — Unified Effects Architecture 2 + 3 + **Branch:** `feat/fx-resolution` 4 + **Status:** Implemented (enabled by default) 5 + **Depends on:** `nix-effects` with effectful handler support 6 + 7 + ## Overview 8 + 9 + The fx pipeline replaces den's legacy recursive tree-walking resolution with an effects-based architecture. Aspects are compiled into effectful computations — the tree structure emerges from effect composition, not explicit recursion. All resolution strategy (constraint checking, dedup, tracing, context dispatch) lives in handlers. 10 + 11 + ### Design goals 12 + 13 + 1. **Aspects are computations.** An aspect `{ nixos = ...; includes = [...] }` compiles to a computation that emits `emit-class` per class and `emit-include` per child. The compilation function is `aspectToEffect`. Aspects know nothing about constraints, tracing, or chain tracking. 14 + 2. **No internal `fx.handle`.** Resolution stays in the effectful world from root to leaf. `fx.handle` runs once at the pipeline edge. 15 + 3. **All strategy in handlers.** Constraint checking, recursion, dedup, tracing, context dispatch — each is a handler. Handlers compose via `composeHandlers`. Adding new resolution behavior means adding a handler, not modifying the compiler. 16 + 17 + ## Module structure 18 + 19 + ``` 20 + nix/lib/aspects/fx/ 21 + default.nix — barrel: { lib, den } only, no init, no re-exports 22 + aspect.nix — aspectToEffect compiler (compileStatic, compileFunctor) 23 + pipeline.nix — mkPipeline, defaultHandlers, fxResolve, composeHandlers 24 + identity.nix — aspectPath, pathKey, toPathSet, tombstone, collectPathsHandler, pathSetHandler 25 + constraints.nix — exclude, substitute, filterBy constructors 26 + includes.nix — includeIf conditional inclusion 27 + trace.nix — structuredTraceHandler, tracingHandler 28 + handlers/ 29 + default.nix — barrel for handler subdirectory 30 + include.nix — emit-include handler (effectful, owns recursion) 31 + transition.nix — into-transition handler (scope.stateful) 32 + ctx.nix — constantHandler, ctxSeenHandler 33 + tree.nix — constraintRegistryHandler, chainHandler, classCollectorHandler 34 + ``` 35 + 36 + Every module takes `{ lib, den, ... }` as its function args. Dependencies are accessed via fully qualified paths (`den.lib.fx.identity.pathKey`, `den.lib.fx.constraints.exclude`, etc.). The nix-effects library is accessed as `den.lib.fx`. 37 + 38 + There is no `init` function. Modules load lazily through the barrel — pure constructors (`exclude`, `substitute`, `filterBy`, `includeIf`) don't touch nix-effects at all. 39 + 40 + ## The aspect compiler: `aspectToEffect` 41 + 42 + `aspectToEffect` replaces `resolveAspect`, `resolveOne`, `wrapAspect`, `wrapIdentity`, `emitClassConfig`, and `registerHandlers`. One function compiles any aspect into an effectful computation. 43 + 44 + **Input:** An aspect attrset — `{ name, meta, nixos, homeManager, includes, __functor, ... }` 45 + 46 + **Output:** A `Computation` that, when handled, emits effects for everything the aspect declares. 47 + 48 + ### Static aspects 49 + 50 + For a non-functor aspect, `compileStatic` emits: 51 + 52 + 1. `emit-class` for each class key (everything not in the structural key set: `name`, `meta`, `includes`, `provides`, `into`, `__functor`, `__functionArgs`) 53 + 2. `register-constraint` for each entry in `meta.handleWith` / `meta.excludes` 54 + 3. `chain-push` (if the node is meaningful — has a real name, not `<anon>` or `<function body>`) 55 + 4. Self-provide children (`emitSelfProvide`), transition children (`emitTransitions`), and include children (`emitIncludes`) — all within the chain scope 56 + 5. `chain-pop` (if pushed) 57 + 6. `resolve-complete` with the resolved aspect 58 + 59 + The compiler emits effects in this order via `fx.bind` chains. It does not check constraints, trace, or recurse — those are handler responsibilities. 60 + 61 + ### Functor (parametric) aspects 62 + 63 + When an aspect has `__functor`, it's parametric. `compileFunctor` uses `bind.fn`: 64 + 65 + ```nix 66 + fx.bind.fn { 67 + __functionArgs = lib.functionArgs aspect; 68 + __functor = _: args: aspectToEffect (aspect args); 69 + } 70 + ``` 71 + 72 + `bind.fn` sends one effect per declared arg (`host`, `user`, `class`, etc.). Handlers provide the values via `constantHandler`. The result feeds into the functor, producing a new aspect attrset, which is merged with the parent envelope's `meta` (preserving `meta.provider` chain) and recursively compiled. 73 + 74 + ### `wrapChild` normalization 75 + 76 + `wrapChild` (in `handlers/include.nix`) normalizes each child in `includes` before the `emit-include` handler compiles it. This is the most bug-prone area of the pipeline — children arrive in many forms from the type system, user code, and `deepRecurse` wrappers. 77 + 78 + **Three cases:** 79 + 80 + 1. **Attrset (not a function):** Pass through unchanged. Already a well-formed aspect. 81 + 82 + 2. **Functor attrset (isFunction=true, isAttrs=true):** These are attrsets with `__functor` — typically `deepRecurse` wrappers or type-system-merged aspects with `defaultFunctor`. The stale `__functionArgs` on the outer attrset may not reflect the real inner function's args. `wrapChild` extracts `innerFn = child.__functor child`, gets the real args from `innerFn`, and replaces `__functionArgs` so `aspectToEffect` makes the correct parametric/static decision. 83 + 84 + 3. **Bare lambda (isFunction=true, isAttrs=false):** Two sub-cases: 85 + - **Module function** (`{ config, lib, ... }: ...`): Detected via `canTake.upTo { lib; config; options; }`. Normalized via `normalizeModuleFn` (extracted helper that calls `aspectType.merge` to extract class keys — the only handler-layer reference to the type system). 86 + - **Parametric aspect** (`{ host, ... }: { nixos = ...; }`): Wrapped in an aspect envelope with `__functor`, `__functionArgs`, and empty `includes`. 87 + 88 + This normalization is distinct from `fxResolveTree`'s root normalization (see Pipeline entry points below). 89 + 90 + ### ctx-stage tag propagation 91 + 92 + `tagChild` propagates context stage tags (`__ctxStage`, `__ctxKind`, `__ctxAspect`) from parent to children that don't have their own. This preserves the context information through the tree so trace handlers can identify which context stage an aspect belongs to. 93 + 94 + ### Anonymous node handling 95 + 96 + Anonymous nodes (name is `<anon>`, `<function body>`, or starts with `[definition `) are transparent to the includes chain — no `chain-push`/`chain-pop`. Two cases: 97 + 98 + - **Wrapper around a named child** (e.g., `deepRecurse` scaffolding): The named child pushes its own identity when compiled. The wrapper is invisible in the chain. 99 + - **Bare lambda leaf** (e.g., `{ host, ... }: { nixos = ...; }`): At `resolve-complete`, the handler reads `lib.last state.includesChain` as parent — the nearest meaningful ancestor. The trace handler disambiguates the name using ctx stage tags. 100 + 101 + In both cases, the anonymous node doesn't corrupt the chain. 102 + 103 + ### Root `resolve-complete` 104 + 105 + The root aspect has no parent sending `emit-include`, so no handler emits `resolve-complete` for it. Instead, `compileStatic` emits `resolve-complete` at the end of every aspect's compilation (including the root). This means `resolve-complete` is emitted by the compiler rather than the handler — a deviation from the "all strategy in handlers" principle, but it works correctly and covers both root and child cases uniformly. 106 + 107 + ### Identity preservation 108 + 109 + `aspectToEffect` preserves `name` and `meta` from the input aspect. The computation carries the aspect's identity throughout — handlers can inspect it for constraint checking, tracing, etc. 110 + 111 + ## Effect protocol 112 + 113 + ``` 114 + Context: 115 + into-transition { key, transitionFn, ctx, self } — handler walks transitions with scope.stateful 116 + ctx-seen <key> — dedup handler for context stages 117 + 118 + Aspect: 119 + emit-class { class, module, identity } — handler accumulates modules 120 + emit-include { from, include } — handler checks constraints + recurses 121 + register-constraint { type, scope, identity, ... } — handler stores in registry 122 + check-constraint { identity, aspect } — handler queries registry 123 + 124 + Tree: 125 + chain-push { identity } — handler tracks includes chain 126 + chain-pop — handler pops includes chain 127 + resolve-complete <resolved-aspect> — handler emits trace entries 128 + get-path-set — handler returns accumulated paths 129 + 130 + Parametric: 131 + <arg-name> — constantHandler resumes with value 132 + ``` 133 + 134 + ## Handlers 135 + 136 + ### `emit-include` handler (include.nix) — owns recursion 137 + 138 + The central handler. Intercepts `emit-include` effects and returns an effectful resume: 139 + 140 + 1. Wraps the child via `wrapChild` (normalizes bare lambdas into aspect envelopes) 141 + 2. Checks if the child is conditional (`meta.conditional`) 142 + 3. For conditional children: evaluates the guard against the accumulated path set 143 + 4. For regular children: sends `check-constraint` to query the constraint registry 144 + 5. Based on the constraint decision: 145 + - **keep**: recursively calls `aspectToEffect child`, emits `resolve-complete` 146 + - **exclude**: creates a tombstone, emits `resolve-complete` 147 + - **substitute**: creates a tombstone, recursively resolves the replacement 148 + 149 + This is the only place recursion happens. The handler closes over `aspectToEffect` for recursive compilation. 150 + 151 + ### `constantHandler` (ctx.nix) — parametric value provider 152 + 153 + Replaces the former `parametricHandler`, `staticHandler`, and `contextHandlers`. For each key-value pair, resumes with the value when that key appears as an effect name. Provides `class`, `host`, `user`, and any other context values to parametric aspects. 154 + 155 + Includes `aspect-chain = []` as a compatibility shim for type-system-baked provider functions (see Compatibility Shims below). 156 + 157 + ### `ctxSeenHandler` (ctx.nix) — context dedup 158 + 159 + Tracks which context stages have been seen. Returns `{ isFirst }` so the transition handler can skip duplicate processing. 160 + 161 + ### `constraintRegistryHandler` (tree.nix) — constraint storage and lookup 162 + 163 + Handles `register-constraint` and `check-constraint`: 164 + 165 + - **register-constraint**: Accumulates constraints as a list per identity, stamping `ownerChain` from the current `state.includesChain` for scoped constraints. Multiple constraints for the same identity are preserved (not overwritten). 166 + - **check-constraint**: Finds the first in-scope constraint for the identity (first-registered wins), then falls back to filter predicates. Returns `{ action = "keep"|"exclude"|"substitute"; ... }`. Scoped constraints only apply when the owner's chain is a prefix of the current chain. 167 + 168 + ### `chainHandler` (tree.nix) — includes-path tracking 169 + 170 + Handles `chain-push` and `chain-pop`. Maintains `state.includesChain` — the stack of meaningful ancestor identities. Anonymous nodes are transparent (no push/pop). Trace handlers derive `parent` from `lib.last state.includesChain` instead of an explicit `__parent` field. `chain-pop` on an empty chain throws a descriptive error to surface push/pop mismatches immediately. 171 + 172 + ### `classCollectorHandler` (tree.nix) — module accumulation 173 + 174 + Handles `emit-class`. Accumulates modules by class, deduplicates by identity. 175 + 176 + ### `transitionHandler` (transition.nix) — context transitions 177 + 178 + Handles `into-transition`. For each transition key: 179 + 180 + 1. Looks up the target context aspect from `den.ctx` by path. If the path doesn't exist, emits a diagnostic tombstone with `transitionMissing = true` (rather than silently skipping). 181 + 2. Sends `ctx-seen` for dedup (using `/` separator matching `pathKey` convention). 182 + 3. For each new context value, installs scoped handlers via `scope.stateful` with `constantHandler (parentCtx // newCtx)` — explicitly merging parent context so the scoped handler is self-contained rather than relying on `scope.stateful` fallthrough for parent keys. 183 + 184 + Uses `scope.stateful` (not `scope.run`) to preserve handler state across scoped computations. 185 + 186 + ### Trace handlers (trace.nix) — observation 187 + 188 + `structuredTraceHandler` and `tracingHandler` consume `resolve-complete` effects. They read parent from `state.includesChain` (not from the param). Trace entries carry a `handlers` field (the actual handler data from `meta.handleWith`) instead of a boolean `hasAdapter`. 189 + 190 + ### `collectPathsHandler` (identity.nix) — path accumulation 191 + 192 + Handles `resolve-complete`. Accumulates aspect paths into both `state.paths` (list) and `state.pathSet` (attrset) incrementally, skipping excluded aspects (tombstones). 193 + 194 + ### `pathSetHandler` (identity.nix) — path set query 195 + 196 + Handles `get-path-set`. Returns `state.pathSet` directly (O(1)), used by `includeIf` guards to evaluate `hasAspect` queries. 197 + 198 + ### Conditional inclusion via `includeIf` 199 + 200 + `includeIf guardFn aspects` creates a conditional node: `{ name = "<includeIf>"; meta = { guard = guardFn; aspects = aspects; }; }`. 201 + 202 + The `emit-include` handler detects conditional nodes by checking `child.meta ? guard` and evaluates the guard against the accumulated path set. If the guard passes, each guarded aspect is emitted via `emit-include` (hitting the handler for constraint checking and recursion). If the guard fails, each guarded aspect gets a tombstone with `guardFailed = true`. 203 + 204 + The guard function receives `{ hasAspect = ref: ...; }` where `hasAspect` checks the handler's accumulated `pathSet` state. Because resolution is sequential (left-to-right through includes), guards can only see aspects resolved before them in the tree. Reordering includes may change which guards pass. 205 + 206 + ## Constraints 207 + 208 + ### `meta.handleWith` 209 + 210 + The extension point where aspect authors provide handlers governing resolution of their subtree. Accepts a single record, a list of records, or null. 211 + 212 + ```nix 213 + meta.handleWith = exclude foo; 214 + meta.handleWith = [ (exclude foo) (substitute bar baz) (filterBy pred) ]; 215 + ``` 216 + 217 + ### `meta.excludes` (sugar) 218 + 219 + Convenience field that expands into `meta.handleWith`: 220 + 221 + ```nix 222 + meta.excludes = [ foo bar ]; 223 + # Equivalent to: meta.handleWith = [ (exclude foo) (exclude bar) ]; 224 + ``` 225 + 226 + When both are set, `excludes` appends (takes final say). 227 + 228 + ### Constraint constructors 229 + 230 + All live in `constraints.nix`. Each has a default (subtree-scoped) and `.global` variant. `exclude` and `substitute` validate that `ref` is an attrset, throwing a descriptive error on misuse: 231 + 232 + | Constructor | Effect | 233 + |---|---| 234 + | `exclude ref` | Exclude `ref` from this subtree | 235 + | `exclude.global ref` | Exclude `ref` everywhere in the pipeline | 236 + | `substitute ref replacement` | Replace `ref` with `replacement` in this subtree | 237 + | `substitute.global ref replacement` | Replace `ref` globally | 238 + | `filterBy pred` | Exclude children failing `pred` in this subtree | 239 + | `filterBy.global pred` | Filter globally | 240 + 241 + Scoped constraints use includes-chain ancestry (`isAncestor ownerChain currentChain`) to determine applicability. Root-registered scoped constraints are effectively global since the empty prefix matches everything. 242 + 243 + ### `meta.adapter` (legacy) 244 + 245 + Reverted to accept only legacy function adapters (the GOF adapter pattern used by `resolve.withAdapter`). The fx pipeline reads `meta.handleWith`, not `meta.adapter`. Both fields are carried through `wrapIdentity`/`withIdentity` until legacy removal. 246 + 247 + ## Includes chain provenance 248 + 249 + The includes chain replaces `__parent` string tracking. It's an observable effect protocol: 250 + 251 + - **`chain-push { identity }`** — emitted when entering a meaningful node's subtree 252 + - **`chain-pop`** — emitted when leaving 253 + 254 + Anonymous nodes (wrappers from `deepRecurse`, bare lambdas) are transparent — no push/pop. Their children see the parent's chain. This gives correct parent attribution without the information loss of the old single-`__parent` approach. 255 + 256 + ### Diagram observability 257 + 258 + Chain effects are observable by any handler: 259 + - Sequence diagrams: `chain-push`/`chain-pop` map to activation bars 260 + - Scope visualization: adapter registration alongside current chain shows scope boundaries 261 + - Composable analysis: depth tracking, per-subtree counts, cycle detection — all addable as handlers 262 + 263 + ## Context transitions 264 + 265 + Contexts are aspects. They have `name`, `meta`, `includes`, `provides` — like any aspect — plus an `into` attrset defining context transitions. 266 + 267 + `aspectToEffect` handles contexts the same way, with two additions: 268 + - `.into` keys are excluded from class emission (they're transitions, not configs) 269 + - An `into-transition` effect is emitted per transition key 270 + 271 + The `transitionHandler` processes each transition: 272 + 273 + 1. Applies the transition function: `{ host } -> [{ host, user: alice }, { host, user: bob }]` 274 + 2. For each new context value, installs scoped handlers via `scope.stateful` containing a `constantHandler` with the new values 275 + 3. Runs `aspectToEffect targetCtx` inside the scope 276 + 277 + Each context value is local to its scope. `den.ctx.user` running inside a `user = alice` scope sees `alice` via the scoped `constantHandler`. 278 + 279 + Self-provide auto-include: if `aspect.provides.${aspect.name}` exists, it's automatically included — the aspect provides its own sub-aspects. 280 + 281 + ## Pipeline flow 282 + 283 + ``` 284 + aspectToEffect(rootAspect) 285 + -> emit-class for each class key 286 + -> register-constraint for meta.handleWith 287 + -> chain-push (if meaningful) 288 + -> emit-include for each child 289 + -> handler checks constraint 290 + -> handler recurses: aspectToEffect(child) 291 + -> (child may have into transitions) 292 + -> into-transition handler installs scoped handlers 293 + -> aspectToEffect(targetCtx) runs in scope 294 + -> chain-pop (if pushed) 295 + -> resolve-complete 296 + 297 + fx.handle { handlers = defaultHandlers; state = defaultState; } computation 298 + -> { value = resolvedTree; state = { entries, paths, imports, ... }; } 299 + ``` 300 + 301 + `mkPipeline` composes the handler set and runs `fx.handle` once at the pipeline edge. `fxResolve` and `fxResolveTree` are the public entry points. 302 + 303 + ## Pipeline entry points and A/B gating 304 + 305 + ### `den.fxPipeline` option 306 + 307 + Defined in `modules/fxPipeline.nix`. A boolean option (default `true`) that switches the resolution path: 308 + 309 + ```nix 310 + resolve = if fxEnabled then fxResolveTree else legacyResolve; 311 + ``` 312 + 313 + This gate lives in `nix/lib/aspects/default.nix`. When `false`, the entire fx pipeline is bypassed and the legacy recursive `resolve` function handles resolution. Legacy adapter tests (`resolve.withAdapter`) run with `fxPipeline = false`. 314 + 315 + ### `fxResolveTree` — root entry point 316 + 317 + `fxResolveTree` (in `nix/lib/aspects/default.nix`) normalizes the root aspect before entering the pipeline. This is separate from `wrapChild` (which normalizes children inside the pipeline). 318 + 319 + Root normalization handles aspects arriving from `forward.nix`'s `fromAspect`, which may be raw lambdas or `fixedTo`-wrapped functor attrsets: 320 + 321 + 1. **Raw lambda** (`isFunction=true, isAttrs=false`): Wrapped in an aspect envelope with `__functor`, `__functionArgs`, `name`, `meta`, `includes`. 322 + 2. **Functor attrset with named args** (`isAttrs=true, __functor` present, `functionArgs != {}`): Same treatment — extract inner function, wrap with correct `__functionArgs`. 323 + 3. **Functor attrset with no args** (default functor, `functionArgs == {}`): Pass through to `compileStatic` to preserve class keys. 324 + 4. **Plain attrset**: Pass through unchanged. 325 + 326 + After normalization, `fxResolveTree` calls `fx.pipeline.fxResolve { class; self = wrapped; ctx = {}; }`. 327 + 328 + Note: `ctx` is always `{}` at root — context values are provided by handlers during resolution, not passed in from the entry point. 329 + 330 + ### `fxResolve` and `mkPipeline` 331 + 332 + `fxResolve` delegates to `mkPipeline`, which: 333 + 1. Composes `defaultHandlers` (with root-level `aspect-chain = [self]` override) with any `extraHandlers` 334 + 2. Compiles the root aspect via `aspectToEffect self` 335 + 3. Runs `fx.handle` with the composed handlers and `defaultState` 336 + 4. Returns `{ imports = result.state.imports; }` — the accumulated NixOS/HM modules 337 + 338 + ## nix-effects dependency 339 + 340 + The fx pipeline depends on a nix-effects fork with effectful handler support (`sini/nix-effects#feat/effectful-handlers`). Two key extensions: 341 + 342 + ### Effectful handlers (resume with computations) 343 + 344 + Standard nix-effects handlers return `{ resume = value; state; }` where `resume` is a plain value. The fork allows `resume` to be a computation. The trampoline interpreter (`interpret` and `rotateInterpret`) detects computations by checking `resume ? _tag && (resume._tag == "Pure" || resume._tag == "Impure")`: 345 + 346 + - **Plain value resume**: existing behavior — value feeds directly to the continuation 347 + - **`Pure` computation resume**: unwraps the value and feeds it to the continuation 348 + - **`Impure` computation resume**: appends the original continuation queue to the computation's queue (queue splicing). The computation's effects run first under the same handler set, then the original continuation resumes with the result. 349 + 350 + State threading: when a handler returns an effectful resume, the sub-computation runs with the handler's updated state. Effects in the sub-computation that modify state propagate through to the original continuation. This is correct — the sub-computation is part of the same handling scope. 351 + 352 + Backward compatible — plain value resumes are unchanged. 353 + 354 + ### `scope.stateful` 355 + 356 + Preserves handler state across scoped computations. Critical for the transition handler: without it, inner computations would run with `state = null`, losing accumulated constraint registries, path sets, and trace entries. Uses `state.get`/`state.put` internally. 357 + 358 + ### Loading 359 + 360 + - Accessed as `den.lib.fx` (set once, available to all fx modules) 361 + - Loaded from flake input when available, falls back to locked `fetchTarball` 362 + 363 + ## What was removed 364 + 365 + The following modules and functions from the pre-fx architecture no longer exist in the fx pipeline: 366 + 367 + | Removed | Replaced by | 368 + |---|---| 369 + | `go` / `resolveChild` / `resolveChildren` (explicit recursion) | `aspectToEffect` + `emit-include` handler | 370 + | `resolveOne` / `resolveOneStrict` (per-aspect `fx.handle`) | `aspectToEffect` (no internal handle boundary) | 371 + | `wrapAspect` / `wrapIdentity` / `emitClassConfig` | `aspectToEffect` (single compiler) | 372 + | `ctx-apply.nix` / `ctx-stage.nix` | Context transitions through `aspectToEffect` + `into-transition` handler | 373 + | `resolve-deep.nix` | Handler-driven recursion via `emit-include` | 374 + | `resolve-handler.nix` (`resolveIncludeHandler`) | `handlers/include.nix` (effectful handler) | 375 + | `resolve-one.nix` | `aspect.nix` (`aspectToEffect`) | 376 + | `resolve-legacy.nix` | Removed | 377 + | `parametricHandler` / `staticHandler` / `contextHandlers` | `constantHandler` | 378 + | `ctxProviderHandler` / `ctxTraverseHandler` / `ctxTraceHandler` / `ctxEmitHandler` | Removed (dead after unified pipeline) | 379 + | `fx/adapters.nix` (monolithic) | Split into `identity.nix`, `constraints.nix`, `includes.nix`, `trace.nix` | 380 + | `init` function / explicit arg threading | `{ lib, den }` module pattern, lazy barrel | 381 + | `__parent` string field | `chain-push`/`chain-pop` effect protocol | 382 + | `meta.adapter` for fx records | `meta.handleWith` (fx) / `meta.adapter` reverted to legacy functions only | 383 + | `adapterRegistryHandler` / `register-adapter` / `check-exclusion` | `constraintRegistryHandler` / `register-constraint` / `check-constraint` | 384 + | `hasAdapter` boolean in trace entries | `handlers` field (carries actual handler data) | 385 + 386 + ## Compatibility shims 387 + 388 + These exist because the type system operates at declaration time and cannot be gated on `config.den.fxPipeline` without circular evaluation: 389 + 390 + ### `aspect-chain` in `constantHandler` 391 + 392 + `defaultFunctor` (from `parametric.withOwn`) is baked into every aspect at type-declaration time. Provider functions from `providerFnType.merge` create `{ class, aspect-chain }` functors. When `aspectToEffect` encounters these via `bind.fn`, it sends `aspect-chain` as an effect. `constantHandler` provides `aspect-chain = []`. 393 + 394 + At root level, `aspect-chain = [self]` is overridden because downstream consumers of the legacy resolve pipeline (`resolve.nix`, `home-env.nix`) and type-system-baked provider functions (`parametric.nix`) expect `aspect-chain` to contain the resolution chain. Note: the comment in `pipeline.nix:89` attributing this to `forward.nix` is stale — `forward.nix` does not reference `aspect-chain`. 395 + 396 + ### `options.nix` uses legacy `ctxApply` 397 + 398 + `config.resolved` in `options.nix` can't access `config.den` without circularity, so it always goes through the legacy `ctxApply` path. The fx pipeline receives pre-wrapped (parametric) aspects. 399 + 400 + ### Legacy adapter tests on `fxPipeline = false` 401 + 402 + Tests that explicitly call `resolve.withAdapter` (the legacy adapter API) run with `fxPipeline = false`. The fx pipeline uses constraints instead of the `{ aspect, recurse, ... }` adapter protocol. These tests verify legacy behavior and don't need fx equivalents. 403 + 404 + ## Known architectural constraints 405 + 406 + ### Circular evaluation prevents gating on `den.fxPipeline` 407 + 408 + `defaultFunctor` feeds into `typesConf` -> `types` -> aspect option declarations. Accessing `den.fxPipeline` (a config value) during type declaration creates circular evaluation: `config.den` -> aspects -> types -> `defaultFunctor` -> `config.den`. This was verified — it produces `attribute 'den' missing` errors. 409 + 410 + The same applies to `options.nix`: `config.resolved` is an option default and can't access `config.den` without circularity. 411 + 412 + **Implication:** The type system always bakes `defaultFunctor` (`parametric.withOwn`) into every aspect. Provider functions from `providerFnType.merge` always create `{ class, aspect-chain }` functors. The fx pipeline must handle this via the `constantHandler` shim. 413 + 414 + ### Template circular eval with barrel imports 415 + 416 + `fx/default.nix` barrel imports effectful modules (`pipeline.nix`, `handlers/include.nix`, etc.) that access `den.lib.aspects.fx.*` siblings in their `let` blocks. When anything forces `den.lib.aspects.fx` during module system initialization (e.g., `meta.handleWith = den.lib.aspects.fx.exclude ...`), it can trigger circular eval through `config.den`. 417 + 418 + The old `init` pattern avoided this because effectful modules were only loaded when `init(nxFx)` was explicitly called at runtime. The barrel loads them at import time. Pure constructors (`exclude`, `substitute`, `filterBy`) don't trigger this because they don't reference effectful siblings. 419 + 420 + ### `resolve-complete` placement deviation 421 + 422 + `resolve-complete` is emitted inside `compileStatic` (the aspect compiler) for every aspect, including the root. This means the compiler knows about a resolution lifecycle event rather than leaving it entirely to handlers. It works correctly and covers the root case (which has no parent handler) uniformly, but deviates from the "all strategy in handlers" principle. Lower priority — can address when the type system is reworked. 423 + 424 + ## Followup work 425 + 426 + ### forward.nix redesign 427 + 428 + The `aspect-chain` compatibility shim is consumed by type-system-baked provider functions (`parametric.nix`, `home-env.nix`) and the legacy resolve pipeline (`resolve.nix`). The root override (`aspect-chain = [self]`) exists to satisfy these consumers. The target: expose `root` from the fx pipeline result so consumers can read it directly, then remove the `aspect-chain` root override. 429 + 430 + ### Type system rework 431 + 432 + When the legacy pipeline is removed: 433 + 1. Remove `defaultFunctor` / `parametric.withOwn` from the type system 434 + 2. Remove `providerFnType.merge`'s `__functor` wrapping 435 + 3. Aspects become plain attrsets — no functor needed 436 + 4. Remove `aspect-chain` handler from pipeline 437 + 5. Remove `aspect-chain` consumption from `home-env.nix`, `parametric.nix`, `resolve.nix` 438 + 6. `options.nix` can use `aspectToEffect` directly (no `ctxApply`) 439 + 440 + ## Integration points 441 + 442 + | File | Role | 443 + |---|---| 444 + | `nix/lib/aspects/default.nix` | `fxResolveTree`, `defaultFunctor`, resolution gate | 445 + | `modules/options.nix` | `config.resolved` (legacy ctxApply, circular eval constraint) | 446 + | `nix/lib/parametric.nix` | `defaultFunctor` source (type-system-baked) | 447 + | `nix/lib/statics.nix` | `isCtxStatic`, `{ class, aspect-chain }` functors | 448 + | `nix/lib/forward.nix` | builds NixOS/HM modules from resolved aspect trees | 449 + | `nix/lib/home-env.nix` | consumes `{ class, aspect-chain }` in provider functions | 450 + 451 + ## Verification 452 + 453 + ```bash 454 + nix develop -c just ci "" 455 + ``` 456 + 457 + All tests pass. The `fxPipeline` flag defaults to `true`. Legacy tests using `resolve.withAdapter` run with `fxPipeline = false`.
+3
docs/src/content/docs/contributing.md
··· 52 52 ```console 53 53 just fmt 54 54 just ci 55 + 56 + # or if you are in a hurry 57 + just ci-fast 55 58 ``` 56 59 57 60 ### Bug Reports
+260
docs/src/content/docs/explanation/effects.mdx
··· 1 + --- 2 + title: "ABC on Den Effects" 3 + description: "Den's small guide into Algebraic Effects with Handlers" 4 + --- 5 + 6 + import { Aside } from '@astrojs/starlight/components'; 7 + 8 + 9 + This is Den's small guide into Algebraic Effects with Handlers. 10 + 11 + Fear not, this guide does not require any scary math nor learning category theory, we limit fancy words to the previous line and that is only for search engines. 12 + 13 + The purpose of this guide is to be an approachable introduction to effects. Showing that **You already know effects** and that **You already use them in Nix**, even without using Den. 14 + 15 + <Aside> 16 + From a user perspective, Den's declarative syntax isn't changing. This guide is just here to help you understand the magic happening under the hood, which is mostly useful for contributors or people squashing bugs. 17 + </Aside> 18 + 19 + ## You already know effects 20 + 21 + Suppose you are writing a standard Nix package. It usually looks something like this: 22 + 23 + ```nix 24 + myPackage = { stdenv, jq }: stdenv.mkDerivation { 25 + name = "my-awesome-script"; 26 + # ... 27 + }; 28 + ``` 29 + 30 + `myPackage` is a pure function, but it cannot actually *do* its job until `stdenv` and `jq` are given to it from the outside world. 31 + 32 + This is the exact essence of what effects are about: 33 + 34 + <Aside type="tip" title="An Effectful Computation is"> 35 + **A function that asks something from the outside before it can give you a result**. 36 + </Aside> 37 + 38 + No scary math. Just: "I need a compiler and `jq` to give you this derivation." 39 + 40 + Nix itself works exactly like this. When you write that function, you are declaring a dependency. You are describing *what* you need. Somewhere up the chain, `callPackage` acts as the **handler**—it digs through `nixpkgs`, finds the right `stdenv` and the right `jq`, and injects them into your function. 41 + 42 + <Aside type="tip" title="A Handler is"> 43 + **A function interacting with the external world.** 44 + 45 + A handler takes _effect-requests_ from computations and decides how and when to perform operations, potentially _mutating_ state or other _side-effects_. 46 + </Aside> 47 + 48 + Let's look at how Den brings this concept out of `nixpkgs` and directly into your configurations. 49 + 50 + ## Try `den.lib.fx` right now 51 + 52 + All examples from this guide can be run on a Nix REPL. 53 + 54 + For now you have to use our experimental branch until that lands on Den main: 55 + 56 + ```shell 57 + nix repl "github:sini/den/feat/fx-resolution?dir=templates/ci" \ 58 + --override-input den "github:sini/den/feat/fx-resolution" 59 + ``` 60 + 61 + Once at the REPL, you can access the `nix-effects` API via `den.lib.fx`. 62 + 63 + ```nix 64 + # Access lib from nixpkgs 65 + :a import <nixpkgs> {} 66 + 67 + # Add all names from den.lib into scope: 68 + :a den.lib 69 + 70 + # Your first effect depends on nothing: Any constant is pure. 71 + fx.handle { handlers = {}; } (fx.pure 22) 72 + # => { state = null; value = 22; } 73 + ``` 74 + 75 + You just ran your first effect! `fx.pure 22` already has its answer. Pure values do not depend on the external world, so they need no handlers. 76 + 77 + <Aside title="Explore nix-effects"> 78 + If you want to explore all that `nix-effects` has to offer—beware of scary words, but an awesome open world of future-Nix posibilities—take a look at the [docs website](https://docs.kleisli.io/nix-effects). 79 + </Aside> 80 + 81 + ## Computations and _Effect Requests_ 82 + 83 + Things become more interesting than `fx.pure` when our computations actually need to ask something from the outside world. 84 + 85 + Effects can represent *any* external dependency. Let's imagine we are building a system configuration and we need to know the target hostname. 86 + 87 + We describe this dependency using an **Effect Request**: 88 + 89 + ```nix "fx.send" 90 + fx.send "hostName" null 91 + ``` 92 + 93 + Requests are like ordering food. You say what you want, the cook (handler) makes it, you get something delicious to eat. 94 + 95 + ## _Handlers_ answer Effect Requests 96 + 97 + Our previous `send "hostName"` does nothing by its own, it just describes a dependency from the external world. And if we tried to evaluate that effect, it will just crash, because there's no one that knows **how to** answer. 98 + 99 + For things to actually work we need someone (a **Handler**) for the `hostName` kind of requests: 100 + 101 + ```nix 102 + fx.handle { 103 + handlers.hostName = { param, state }: { 104 + resume = "igloo"; # The handler decides the host is 'igloo' 105 + inherit state; 106 + }; 107 + state = {}; 108 + } (fx.send "hostName" null) 109 + # => { value = "igloo"; ... } 110 + ``` 111 + 112 + A handler gets `{ param, state }`, and returns `{ resume, state }`. 113 + 114 + `param` is what you sent as payload of the request, `resume` is the answer payload. 115 + 116 + ## Chaining Effects: The Hard Way 117 + 118 + Effects aren't just for strings; they are heavily used for injecting massive system variables like `pkgs`. If a configuration needs multiple dependencies—like `pkgs` and `hostName` to write a file—`fx.bind` chains computations, so the result of one feeds the next: 119 + 120 + ```nix 121 + fx.handle { 122 + handlers = { 123 + pkgs = { param, state }: { resume = import <nixpkgs> {}; inherit state; }; 124 + hostName = { param, state }: { resume = "igloo"; inherit state; }; 125 + }; 126 + state = {}; 127 + } (fx.bind (fx.send "pkgs" null) 128 + (pkgs: 129 + fx.bind (fx.send "hostName" null) 130 + (hostName: fx.pure (pkgs.writeText "motd" "Welcome to ${hostName}")))) 131 + # => { state = {}; value = «derivation /nix/store/...-motd»; } 132 + ``` 133 + 134 + If you have used Promises or Futures this is starting to look familiar. But writing deeply nested code like this in Nix would be miserable. Hold on a bit on `fx.bind`'s nested nature for now. 135 + 136 + 137 + ## The magic: `fx.bind.fn` 138 + 139 + Remember our initial `myPackage = { stdenv, jq }: ...` function? And how `callPackage` magically figures out what arguments it needs? 140 + 141 + We can turn *any* pure-Nix function into an effectful Computation automatically by using `fx.bind.fn`. 142 + 143 + It [works](https://github.com/kleisli-io/nix-effects/pull/12/changes#diff-017d9d683e5fee10699fd9ba2f084783199442cd6ebc0c67e186e9b8f90ed410R134) like this: 144 + - it takes a plain Nix function 145 + - uses each argument name to generate effect-requests automatically (e.g., `fx.send "hostName"`) 146 + - binds all handler responses into a single attrset 147 + - invokes the original Nix function when all handlers have replied 148 + - and wraps the result in `fx.pure`. 149 + 150 + Let's apply this to a function that generates an actual `/nix/store` derivation for a server banner (`motd`): 151 + 152 + ```nix 153 + myMotd = { pkgs, hostName }: pkgs.writeText "motd" '' 154 + ======================================= 155 + Welcome to the ${hostName} server! 156 + ======================================= 157 + ''; 158 + 159 + fx.handle { 160 + handlers = { 161 + pkgs = { param, state }: { resume = import <nixpkgs> {}; inherit state; }; 162 + hostName = { param, state }: { resume = "igloo"; inherit state; }; 163 + }; 164 + state = {}; 165 + } (fx.bind.fn {} myMotd) 166 + # => { state = {}; value = «derivation /nix/store/y3p...-motd»; } 167 + ``` 168 + 169 + Look at that return value. `bind.fn` inspected `{ pkgs, hostName }`, sent the requests, the handlers injected a real `nixpkgs` instance and the string `"igloo"`, and the pure function materialized an actual derivation in your Nix store. 170 + 171 + **Your plain nix functions are already effectful.** They just need a handler to provide their arguments. 172 + 173 + ## Using the same computation with different handlers 174 + 175 + Separating the request from the handler allows you to reuse the exact same function everywhere without hardcoding paths. This is incredibly powerful for testing configurations without accidentally pulling in your heavy system dependencies. 176 + 177 + Let's test our `myMotd` function, but instead of handing it a massive `nixpkgs` instance, we will give it a mocked `pkgs` object just to verify the string output: 178 + 179 + ```nix 180 + # local unit testing with a mock 'pkgs' 181 + fx.handle { 182 + handlers = { 183 + pkgs = { param, state }: { 184 + resume = { writeText = name: text: "MOCKED DIR: ${name} -> ${text}"; }; 185 + inherit state; 186 + }; 187 + hostName = { param, state }: { resume = "test-runner"; inherit state; }; 188 + }; 189 + state = {}; 190 + } (fx.bind.fn {} myMotd) 191 + # => { value = "MOCKED DIR: motd -> \n=======================================\n Welcome to the test-runner server!\n=======================================\n"; ... } 192 + ``` 193 + 194 + Same computation. Totally different behaviour. We successfully unit-tested a Nix function that normally produces derivations, completely bypassing the Nix store by just swapping the handler. 195 + 196 + ## The Payoff: How Den aspects uses effects 197 + 198 + When you write a Den aspect: 199 + 200 + ```nix 201 + { host, user }: { 202 + nixos.networking.hostName = host; 203 + homeManager.programs.git.userName = user; 204 + } 205 + ``` 206 + 207 + Den uses `bind.fn` under the hood. Each argument automatically becomes an effect request. 208 + 209 + > "Hey, I need a `host` to configure `nixos`" 210 + 211 + and 212 + 213 + > "I need `user` to configure `homeManager`". 214 + 215 + Den's pipeline acts as the handler, responding with the right values for each machine and person. Your aspect is just a pure function; effects wire the dependencies (dependency-injection). 216 + 217 + Besides that, aspects also contribute `nixos` and `homeManager` classes via effect requests: 218 + 219 + > "Hey, I'd like to produce this `nixos` class module, here is it!" 220 + 221 + A centralized handler is responsible for keeping track of what is provided and from where, enabling advanced deduplication, feature detection, aspect replacement, etc. Using effects, Den aspects can even communicate between each other via a higher level message handler. 222 + 223 + And we are just scratching the surface of what is possible using effects in Den. 224 + 225 + --- 226 + 227 + ## Appendix: Under the Hood 228 + 229 + *For contributors and those squashing bugs:* 230 + 231 + Up to `v0.16.0`, Den already ships a half-baked, rigid-context, manually-threaded effects system as its core. 232 + 233 + We are now moving Den to use a proper effects system. The reason is better context control and better dedup detection. Our prototype has already validated this. 234 + 235 + <Aside type="tip" title="Den is about UX"> 236 + **Our goal is not Den machinery but the _experience_ we can provide to Den users.** 237 + </Aside> 238 + 239 + Den will be using `vic/nix-effects`, based on `vic/nfx`, based on `vic/fx-rs` based on `vic/fx.go`. 240 + 241 + ## FAQ 242 + 243 + ### Why not just use `callPackage`? 244 + 245 + **Q**: If `callPackage` is already a dependency injection system, why introduce a dedicated library like `nix-effects`? 246 + 247 + **A**: It’s an insightful question—Nix users are naturally cautious about adding complexity to their evaluation graph. 248 + 249 + ##### 1. Beyond the NixOS Domain 250 + `callPackage` is a brilliant "one-shot" pattern, but it’s purpose-built for the `nixpkgs` ecosystem and finding package derivations. Den is exploring Nix’s capabilities *outside* of standard package builds. We need a system that isn't hardcoded to look for `stdenv` or `lib`, but can handle arbitrary communication between different parts of a system—including advanced error reporting (Common Lisp-style conditions) and complex cross-aspect coordination. 251 + 252 + ##### 2. Footprint and Maintenance 253 + While `nix-effects` is an external dependency, its impact on the project is measured: 254 + * **The "Core" is minimal:** The actual logic driving the effects is remarkably small. The repository appears larger because it includes extensive inline testing and documentation. 255 + * **A Structural Swap:** By moving to a formalized effects library, we are actually **removing** a significant amount of "half-baked" internal code from Den’s core. It allows us to replace home-grown, imperative-style hacks with a professional, ability-based system. 256 + 257 + ##### 3. Provenance and Design 258 + The library’s design is deeply rooted in functional programming research. It was hand-crafted to explore "freer monads" and "ability-based" programming within the Nix language. While the high-level type theory in the documentation can look dense, it’s there to provide a foundation for reliability that standard `lib` patterns don't natively expose. 259 + 260 + **In short:** We use it because it moves the "magic" of Den’s dependency injection into a formal, transparent, and tested system rather than keeping it buried in the framework's internals.
+6 -2
docs/src/content/docs/tutorials/ci.mdx
··· 140 140 From the Den root against your local checkout: 141 141 142 142 ```console 143 - nix flake check --override-input den . ./templates/ci 143 + just ci 144 144 ``` 145 145 146 146 You can also run a single or a subset of tests using: 147 147 148 148 ```console 149 149 # You can use any attr-path bellow flake.tests after system-agnositc to run those specific tests: 150 - nix-unit --override-input den . --flake ./templates/ci#.tests.systems.x86_64-linux.system-agnostic 150 + just ci wsl-class 151 151 ``` 152 + 153 + If you are in a hurry you can also use `just ci-fast`. 154 + NOTE: `ci-fast` cannot show error diffs and ignores tests using nix-unit `expectedError`. 155 + 152 156 153 157 ## Writing New Tests 154 158
+3 -3
modules/aspects/provides/define-user.nix
··· 8 8 ## Usage 9 9 10 10 # for NixOS/Darwin 11 - den.aspects.my-user.includes = [ den._.define-user ] 11 + den.aspects.my-user.includes = [ den.provides.define-user ] 12 12 13 13 # for standalone home-manager 14 - den.aspects.my-home.includes = [ den._.define-user ] 14 + den.aspects.my-home.includes = [ den.provides.define-user ] 15 15 16 16 or globally (automatically applied depending on context): 17 17 18 - den.default.includes = [ den._.define-user ] 18 + den.default.includes = [ den.provides.define-user ] 19 19 ''; 20 20 21 21 homeDir =
+3 -3
modules/aspects/provides/flake-parts/inputs.nix
··· 19 19 **Global (Recommended):** 20 20 Apply to all hosts, users, and homes. 21 21 22 - den.default.includes = [ den._.inputs' ]; 22 + den.default.includes = [ den.provides.inputs' ]; 23 23 24 24 **Specific:** 25 25 Apply only to a specific host, user, or home aspect. 26 26 27 - den.aspects.my-laptop.includes = [ den._.inputs' ]; 28 - den.aspects.alice.includes = [ den._.inputs' ]; 27 + den.aspects.my-laptop.includes = [ den.provides.inputs' ]; 28 + den.aspects.alice.includes = [ den.provides.inputs' ]; 29 29 30 30 **Note:** This aspect is contextual. When included in a `host` aspect, it 31 31 configures `inputs'` for the host's OS. When included in a `user` or `home`
+3 -3
modules/aspects/provides/flake-parts/self.nix
··· 19 19 **Global (Recommended):** 20 20 Apply to all hosts, users, and homes. 21 21 22 - den.default.includes = [ den._.self' ]; 22 + den.default.includes = [ den.provides.self' ]; 23 23 24 24 **Specific:** 25 25 Apply only to a specific host, user, or home aspect. 26 26 27 - den.aspects.my-laptop.includes = [ den._.self' ]; 28 - den.aspects.alice.includes = [ den._.self' ]; 27 + den.aspects.my-laptop.includes = [ den.provides.self' ]; 28 + den.aspects.alice.includes = [ den.provides.self' ]; 29 29 30 30 **Note:** This aspect is contextual. When included in a `host` aspect, it 31 31 configures `self'` for the host's OS. When included in a `user` or `home`
+3 -3
modules/aspects/provides/forward.nix
··· 20 20 This is exactly how `homeManager` class support is implemented in Den. 21 21 See home-manager/hm-integration.nix. 22 22 23 - Den also provides the mentioned `user` class (`den._.os-user`) for setting 23 + Den also provides the mentioned `user` class (`den.provides.os-user`) for setting 24 24 NixOS/Darwin options under `users.users.<userName>` at os-level. 25 25 26 26 Any other user-environments like `nix-maid` or `hjem` or user-custom classes 27 - are easily implemented using `den._.forward`. 27 + are easily implemented using `den.provides.forward`. 28 28 29 - Note: `den._.forward` returns an aspect that needs to be included for 29 + Note: `den.provides.forward` returns an aspect that needs to be included for 30 30 the new class to exist. 31 31 32 32 See templates/ci/modules/guarded-forward.nix, templates/ci/modules/forward-from-custom-class.nix
+1 -1
modules/aspects/provides/hostname.nix
··· 7 7 8 8 ## Usage 9 9 10 - den.defaults.includes = [ den._.hostname ]; 10 + den.defaults.includes = [ den.provides.hostname ]; 11 11 ''; 12 12 13 13 setHostname =
+10 -10
modules/aspects/provides/import-tree.nix
··· 13 13 ``` 14 14 # this is at <repo>/modules/non-dendritic.nix 15 15 den.aspects.my-laptop.includes = [ 16 - (den._.import-tree._.host ../non-dendritic) 16 + (den.provides.import-tree.provides.host ../non-dendritic) 17 17 ] 18 18 ``` 19 19 ··· 43 43 this aspect can be included explicitly on any aspect: 44 44 45 45 # example: will import ./disko/_nixos files automatically. 46 - den.aspects.my-disko.includes = [ (den._.import-tree ./disko/) ]; 46 + den.aspects.my-disko.includes = [ (den.provides.import-tree ./disko/) ]; 47 47 48 48 or it can be default imported per host/user/home: 49 49 50 50 # load from ./hosts/<host>/_nixos 51 - den.ctx.os.includes = [ (den._.import-tree._.host ./hosts) ]; 51 + den.ctx.os.includes = [ (den.provides.import-tree.provides.host ./hosts) ]; 52 52 53 53 # load from ./users/<user>/{_homeManager, _nixos} 54 - den.ctx.hm.includes = [ (den._.import-tree._.user ./users) ]; 54 + den.ctx.hm.includes = [ (den.provides.import-tree.provides.user ./users) ]; 55 55 56 56 # load from ./homes/<home>/_homeManager 57 - den.ctx.home.includes = [ (den._.import-tree._.home ./homes) ]; 57 + den.ctx.home.includes = [ (den.provides.import-tree.provides.home ./homes) ]; 58 58 59 59 you are also free to create your own auto-imports layout following the implementation of these. 60 60 ''; 61 61 62 - den._.import-tree.__functor = 62 + den.provides.import-tree.__functor = 63 63 _: root: 64 64 { class, aspect-chain }: 65 65 let ··· 68 68 in 69 69 if builtins.pathExists path then aspect else { }; 70 70 71 - den._.import-tree.provides = { 72 - host = root: { host, ... }: den._.import-tree "${toString root}/${host.name}"; 73 - home = root: { home, ... }: den._.import-tree "${toString root}/${home.name}"; 74 - user = root: { user, ... }: den._.import-tree "${toString root}/${user.name}"; 71 + den.provides.import-tree.provides = { 72 + host = root: { host, ... }: den.provides.import-tree "${toString root}/${host.name}"; 73 + home = root: { home, ... }: den.provides.import-tree "${toString root}/${home.name}"; 74 + user = root: { user, ... }: den.provides.import-tree "${toString root}/${user.name}"; 75 75 }; 76 76 }
+1 -1
modules/aspects/provides/insecure/insecure-predicate-builder.nix
··· 8 8 This is a private aspect always included in den.default. 9 9 10 10 It adds a module option that gathers all packages defined 11 - in den._.insecure usages and declares a 11 + in den.provides.insecure usages and declares a 12 12 nixpkgs.config.permittedInsecurePackages for each class. 13 13 14 14 '';
+1 -1
modules/aspects/provides/insecure/insecure.nix
··· 6 6 7 7 ## Usage 8 8 9 - den.aspects.my-laptop.includes = [ (den._.insecure [ "example-insecure-package-1.0.0" ]) ]; 9 + den.aspects.my-laptop.includes = [ (den.provides.insecure [ "example-insecure-package-1.0.0" ]) ]; 10 10 11 11 It will dynamically provide a module for each class when accessed. 12 12 '';
+5 -5
modules/aspects/provides/mutual-provider.nix
··· 15 15 ## Usage 16 16 17 17 den.hosts.x86_64-linux.igloo.users.tux = { }; 18 - den.ctx.user.includes = [ den._.mutual-provider ]; 18 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 19 19 20 20 # user aspect provides to specific host or to all where it lives 21 21 den.aspects.tux = { ··· 34 34 }; 35 35 ''; 36 36 37 - find-mutual = from: to: from.aspect._.${to.aspect.name} or { }; 38 - to-hosts = from: from.aspect._.to-hosts or { }; 39 - to-users = from: from.aspect._.to-users or { }; 37 + find-mutual = from: to: from.aspect.provides.${to.aspect.name} or { }; 38 + to-hosts = from: from.aspect.provides.to-hosts or { }; 39 + to-users = from: from.aspect.provides.to-users or { }; 40 40 41 41 mutual-user-user = host: user: { 42 42 includes = map ( ··· 66 66 mutual-standalone-home = 67 67 { home }: 68 68 parametric.fixedTo { inherit home; } ( 69 - if home.hostName == null then { } else home.aspect._.${home.hostName} or { } 69 + if home.hostName == null then { } else home.aspect.provides.${home.hostName} or { } 70 70 ); 71 71 72 72 in
+1 -1
modules/aspects/provides/os-class.nix
··· 16 16 17 17 os-class = 18 18 { class, aspect-chain }: 19 - den._.forward { 19 + den.provides.forward { 20 20 each = [ 21 21 "nixos" 22 22 "darwin"
+1 -1
modules/aspects/provides/os-user.nix
··· 31 31 32 32 fwd = 33 33 { user, host }: 34 - den._.forward { 34 + den.provides.forward { 35 35 each = lib.singleton user; 36 36 fromClass = _: "user"; 37 37 intoClass = _: host.class;
+1 -1
modules/aspects/provides/primary-user.nix
··· 9 9 10 10 ## Usage 11 11 12 - den.aspects.my-user.includes = [ den._.primary-user ]; 12 + den.aspects.my-user.includes = [ den.provides.primary-user ]; 13 13 14 14 ''; 15 15
+1 -1
modules/aspects/provides/tty-autologin.nix
··· 4 4 5 5 This battery must be included in a Host aspect. 6 6 7 - den.aspects.my-laptop.includes = [ (den._.tty-autologin "root") ]; 7 + den.aspects.my-laptop.includes = [ (den.provides.tty-autologin "root") ]; 8 8 ''; 9 9 10 10 # From https://discourse.nixos.org/t/autologin-for-single-tty/49427/2
+1 -1
modules/aspects/provides/unfree/unfree-predicate-builder.nix
··· 9 9 This is a private aspect always included in den.default. 10 10 11 11 It adds a module option that gathers all packages defined 12 - in den._.unfree usages and declares a 12 + in den.provides.unfree usages and declares a 13 13 nixpkgs.config.allowUnfreePredicate for each class. 14 14 15 15 '';
+1 -1
modules/aspects/provides/unfree/unfree.nix
··· 6 6 7 7 ## Usage 8 8 9 - den.aspects.my-laptop.includes = [ (den._.unfree [ "example-unfree-package" ]) ]; 9 + den.aspects.my-laptop.includes = [ (den.provides.unfree [ "example-unfree-package" ]) ]; 10 10 11 11 It will dynamically provide a module for each class when accessed. 12 12 '';
+1 -1
modules/aspects/provides/user-shell.nix
··· 8 8 9 9 den.aspects.vic.includes = [ 10 10 # will always love red snappers. 11 - (den._.user-shell "fish") 11 + (den.provides.user-shell "fish") 12 12 ]; 13 13 ''; 14 14
+1 -1
modules/aspects/provides/wsl.nix
··· 41 41 fwd = 42 42 host: 43 43 { class, aspect-chain }: 44 - den._.forward { 44 + den.provides.forward { 45 45 each = lib.singleton true; 46 46 fromClass = _: "wsl"; 47 47 intoClass = _: host.class;
+11
modules/fxPipeline.nix
··· 1 + { 2 + lib, 3 + ... 4 + }: 5 + { 6 + options.den.fxPipeline = lib.mkOption { 7 + type = lib.types.bool; 8 + default = true; 9 + description = "Use effects-based resolution pipeline (experimental). Optional nix-effects flake input."; 10 + }; 11 + }
+1 -1
modules/outputs/flakeSystemOutputs.nix
··· 16 16 systemOutputFwd = 17 17 { system, output }: 18 18 { class, aspect-chain }: 19 - den._.forward { 19 + den.provides.forward { 20 20 each = lib.optional (class == "flake") output; 21 21 fromClass = _: output; 22 22 intoClass = _: "flake";
+1 -1
modules/outputs/hmConfigurations.nix
··· 8 8 9 9 hmFwd = 10 10 { home }: 11 - den._.forward { 11 + den.provides.forward { 12 12 each = lib.optional (home.intoAttr != [ ]) true; 13 13 fromClass = _: home.class; 14 14 intoClass = _: "flake";
+1 -1
modules/outputs/osConfigurations.nix
··· 7 7 8 8 osFwd = 9 9 { host }: 10 - den._.forward { 10 + den.provides.forward { 11 11 each = lib.optional (host.intoAttr != [ ]) true; 12 12 fromClass = _: host.class; 13 13 intoClass = _: "flake";
+91 -3
nix/lib/aspects/adapters.nix
··· 1 - # Adapters for resolve.withAdapter. Default adapter is module. 1 + # Legacy pipeline only — GOF adapters for resolve.withAdapter. 2 + # The fx pipeline uses meta.handleWith + constraint handlers instead. 3 + # Remove when the legacy pipeline is removed. 2 4 # 3 5 # Adapters determine the return value of resolve. They are called for each 4 6 # resolved aspect and can recurse into includes, filter, or transform them. ··· 45 47 aspectPath = a: (a.meta.provider or [ ]) ++ [ (a.name or "<anon>") ]; 46 48 47 49 # Exclude by aspect reference. Also excludes aspects provided by the 48 - # reference (e.g., excluding monitoring also excludes monitoring._.node-exporter). 50 + # reference (e.g., excluding monitoring also excludes monitoring.provides.node-exporter). 49 51 excludeAspect = 50 52 ref: 51 53 let ··· 105 107 inner: 106 108 args@{ aspect, resolveChild, ... }: 107 109 let 108 - metaAdapter = aspect.meta.adapter or null; 110 + rawAdapter = aspect.meta.adapter or null; 111 + # Only process function adapters (legacy). Resolution handlers 112 + # (meta.handleWith) are handled by the fx pipeline separately. 113 + metaAdapter = if rawAdapter != null && lib.isFunction rawAdapter then rawAdapter else null; 109 114 ownerName = aspect.meta.adapterOwner or (pathKey (aspectPath aspect)); 110 115 in 111 116 if metaAdapter != null && aspect ? includes then ··· 237 242 ); 238 243 }; 239 244 245 + # Structured trace producing flat entry lists with rich metadata. 246 + # Each entry carries name, class, parent path, provider chain, 247 + # exclusion info, parametric detection, and context stage tags. 248 + # Returns { trace = [entries]; paths = [aspectPaths]; }. 249 + structuredTrace = filterIncludes ( 250 + { 251 + aspect, 252 + recurse, 253 + class, 254 + classModule, 255 + aspect-chain, 256 + ... 257 + }: 258 + let 259 + meaningful = 260 + name: name != "<anon>" && name != "<function body>" && !(lib.hasPrefix "[definition " name); 261 + rawName = aspect.meta.originalName or aspect.name or "<anon>"; 262 + depth = builtins.length aspect-chain; 263 + provPath = lib.concatStringsSep "/" (aspect.meta.provider or [ ]); 264 + ctxStages' = builtins.filter (a: builtins.isAttrs a && a ? __ctxStage) aspect-chain; 265 + nearestCtx = if ctxStages' == [ ] then null else lib.last ctxStages'; 266 + ctxKind' = if nearestCtx != null then nearestCtx.__ctxKind or null else null; 267 + ctxStage' = if nearestCtx != null then nearestCtx.__ctxStage or null else null; 268 + ctxAspect' = if nearestCtx != null then nearestCtx.__ctxAspect or null else null; 269 + name = 270 + if rawName == "<anon>" || rawName == "<function body>" || lib.hasPrefix "[definition " rawName then 271 + let 272 + stage = if ctxStage' != null then ctxStage' else "d${toString depth}"; 273 + kind = if ctxKind' != null then ctxKind' else "resolve"; 274 + aspectTag = if ctxAspect' != null then "(${ctxAspect'})" else ""; 275 + provTag = lib.optionalString (provPath != "") ":${provPath}"; 276 + in 277 + "${stage}/${kind}${aspectTag}${provTag}" 278 + else 279 + rawName; 280 + selfFullPath = if provPath != "" then "${provPath}/${name}" else name; 281 + chainBareName = a: a.meta.originalName or a.name or "<anon>"; 282 + chainFullPath = 283 + a: 284 + let 285 + bare = chainBareName a; 286 + prov = a.meta.provider or [ ]; 287 + in 288 + if prov != [ ] then lib.concatStringsSep "/" (prov ++ [ bare ]) else bare; 289 + parentPaths = builtins.filter ( 290 + fp: fp != selfFullPath && meaningful (lib.last (lib.splitString "/" fp)) 291 + ) (builtins.map chainFullPath aspect-chain); 292 + chainLen = builtins.length aspect-chain; 293 + rawProvided = if chainLen >= 2 then builtins.elemAt aspect-chain (chainLen - 2) else null; 294 + rawFnArgs = 295 + if 296 + rawProvided != null 297 + && (builtins.isFunction rawProvided || (builtins.isAttrs rawProvided && rawProvided ? __functor)) 298 + then 299 + lib.functionArgs rawProvided 300 + else 301 + { }; 302 + entry = { 303 + inherit name class; 304 + isParametric = rawFnArgs != { }; 305 + fnArgNames = builtins.attrNames rawFnArgs; 306 + hasClass = classModule != [ ]; 307 + parent = if parentPaths == [ ] then null else lib.last parentPaths; 308 + provider = aspect.meta.provider or [ ]; 309 + excluded = aspect.meta.excluded or false; 310 + excludedFrom = aspect.meta.excludedFrom or null; 311 + replacedBy = aspect.meta.replacedBy or null; 312 + isProvider = (aspect.meta.provider or [ ]) != [ ]; 313 + hasAdapter = aspect.meta.adapter or null != null; 314 + ctxStage = if nearestCtx != null then nearestCtx.__ctxStage else null; 315 + ctxKind = if nearestCtx != null then nearestCtx.__ctxKind else null; 316 + }; 317 + childResults = builtins.map recurse (aspect.includes or [ ]); 318 + childTraces = lib.concatMap (r: r.trace or [ ]) childResults; 319 + childPaths = lib.concatMap (r: r.paths or [ ]) childResults; 320 + in 321 + { 322 + trace = [ entry ] ++ childTraces; 323 + paths = collectSelfPath aspect ++ childPaths; 324 + } 325 + ); 326 + 240 327 in 241 328 { 242 329 inherit ··· 253 340 module 254 341 oneOfAspects 255 342 pathKey 343 + structuredTrace 256 344 substituteAspect 257 345 toPathSet 258 346 tombstone
+55 -2
nix/lib/aspects/default.nix
··· 1 1 { 2 2 lib, 3 3 den, 4 + inputs, 4 5 ... 5 6 }: 6 7 let 7 8 rawTypes = import ./types.nix { inherit den lib; }; 8 9 adapters = import ./adapters.nix { inherit den lib; }; 9 - resolve = import ./resolve.nix { inherit den lib; }; 10 + legacyResolve = import ./resolve.nix { inherit den lib; }; 10 11 hasAspect = import ./has-aspect.nix { inherit den lib; }; 12 + fx = import ./fx { inherit den lib; }; 11 13 14 + fxEnabled = den.fxPipeline or true; 15 + 16 + # When fxPipeline is enabled, resolve uses the unified aspectToEffect pipeline. 17 + # Raw functions (e.g. { class, aspect-chain }: ...) can reach resolve from 18 + # forward.nix's fromAspect. Wrap them so aspectToEffect handles them as parametric. 19 + fxResolveTree = 20 + class: resolved: 21 + let 22 + # builtins.isFunction is false for functor attrsets (sets with __functor). 23 + # Handle both raw lambdas and functors — forward.nix's fromAspect returns 24 + # fixedTo-wrapped aspects which are functor attrsets needing parametric resolution. 25 + # Only wrap functors whose inner function has named args (e.g. deepRecurse's 26 + # { class, aspect-chain }) — the default functor takes bare `ctx` (args={}) 27 + # and should go through compileStatic to preserve class keys. 28 + isRawFn = builtins.isFunction resolved; 29 + isFunctor = builtins.isAttrs resolved && resolved ? __functor; 30 + functorArgs = if isFunctor then builtins.functionArgs (resolved.__functor resolved) else { }; 31 + needsWrap = isRawFn || (isFunctor && functorArgs != { }); 32 + wrapped = 33 + if needsWrap then 34 + let 35 + innerFn = if isFunctor then resolved.__functor resolved else resolved; 36 + innerArgs = if isFunctor then functorArgs else builtins.functionArgs innerFn; 37 + in 38 + { 39 + __functor = _: innerFn; 40 + __functionArgs = innerArgs; 41 + name = resolved.name or "<function body>"; 42 + meta = resolved.meta or { }; 43 + includes = resolved.includes or [ ]; 44 + } 45 + else 46 + resolved; 47 + in 48 + fx.pipeline.fxResolve { 49 + inherit class; 50 + self = wrapped; 51 + ctx = { }; 52 + }; 53 + 54 + resolve = if fxEnabled then fxResolveTree else legacyResolve; 55 + 56 + # defaultFunctor is baked into the aspect type system (types.nix:178). 57 + # It uses parametric.withOwn which creates { class, aspect-chain } functors. 58 + # The fx pipeline provides aspect-chain = [] via constantHandler as a compat shim. 59 + # TODO: Replace with a simpler functor when the legacy pipeline is removed. 12 60 defaultFunctor = (den.lib.parametric { }).__functor; 13 61 typesConf = { inherit defaultFunctor; }; 14 62 types = lib.mapAttrs (_: v: v typesConf) rawTypes; 15 63 in 16 64 { 17 - inherit types adapters resolve; 65 + inherit 66 + types 67 + adapters 68 + resolve 69 + fx 70 + ; 18 71 inherit (hasAspect) hasAspectIn collectPathSet mkEntityHasAspect; 19 72 mkAspectsType = cnf': lib.mapAttrs (_: v: v (typesConf // cnf')) rawTypes; 20 73 }
+207
nix/lib/aspects/fx/aspect.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + let 7 + fx = den.lib.fx; 8 + identity = den.lib.aspects.fx.identity; 9 + 10 + structuralKeys = [ 11 + "name" 12 + "meta" 13 + "includes" 14 + "provides" 15 + "into" 16 + "__functor" 17 + "__functionArgs" 18 + ]; 19 + 20 + # Emit emit-class for each non-structural attr on the aspect. 21 + emitClasses = 22 + aspect: classKeys: nodeIdentity: 23 + fx.seq ( 24 + map ( 25 + k: 26 + fx.send "emit-class" { 27 + class = k; 28 + identity = nodeIdentity; 29 + module = aspect.${k}; 30 + } 31 + ) classKeys 32 + ); 33 + 34 + # Register constraints from meta.handleWith and meta.excludes. 35 + registerConstraints = 36 + aspect: 37 + let 38 + rawHandleWith = aspect.meta.handleWith or null; 39 + rawExcludes = aspect.meta.excludes or [ ]; 40 + handleWithList = 41 + if rawHandleWith == null then 42 + [ ] 43 + else if builtins.isList rawHandleWith then 44 + rawHandleWith 45 + else if builtins.isAttrs rawHandleWith then 46 + [ rawHandleWith ] 47 + else 48 + [ ]; 49 + excludeList = map (ref: { 50 + type = "exclude"; 51 + scope = "subtree"; 52 + identity = identity.pathKey (identity.aspectPath ref); 53 + }) rawExcludes; 54 + allConstraints = handleWithList ++ excludeList; 55 + owner = aspect.name or "<anon>"; 56 + in 57 + fx.seq (map (c: fx.send "register-constraint" (c // { inherit owner; })) allConstraints); 58 + 59 + # Fold includes through emit-include effects. 60 + emitIncludes = 61 + incs: 62 + builtins.foldl' ( 63 + acc: child: 64 + fx.bind acc ( 65 + results: fx.bind (fx.send "emit-include" child) (childResults: fx.pure (results ++ childResults)) 66 + ) 67 + ) (fx.pure [ ]) incs; 68 + 69 + # Emit into-transition effects for each key in aspect.into. 70 + # into is a function ctx → attrset. We pass the unevaluated function 71 + # to the handler which evaluates it with the current context. 72 + emitTransitions = 73 + aspect: 74 + if aspect ? into then 75 + fx.send "into-transition" { 76 + intoFn = aspect.into; 77 + self = aspect; 78 + } 79 + else 80 + fx.pure [ ]; 81 + 82 + # Self-provide: if aspect.provides.${aspect.name} exists, emit it as an include. 83 + emitSelfProvide = 84 + aspect: 85 + let 86 + name = aspect.name or "<anon>"; 87 + provides = aspect.provides or { }; 88 + in 89 + if provides ? ${name} then 90 + fx.send "emit-include" { 91 + inherit name; 92 + meta = { 93 + provider = (aspect.meta.provider or [ ]) ++ [ name ]; 94 + selfProvide = true; 95 + }; 96 + __functor = _: ctx: provides.${name} ctx; 97 + __functionArgs = { }; 98 + includes = [ ]; 99 + } 100 + else 101 + fx.pure [ ]; 102 + 103 + # Wrap a computation in chain-push/chain-pop if the node is meaningful. 104 + chainWrap = 105 + nodeIdentity: isMeaningful: comp: 106 + if isMeaningful then 107 + fx.bind (fx.send "chain-push" { identity = nodeIdentity; }) ( 108 + _: fx.bind comp (result: fx.bind (fx.send "chain-pop" null) (_: fx.pure result)) 109 + ) 110 + else 111 + comp; 112 + 113 + # Resolve children, assemble the result, and emit resolve-complete. 114 + resolveChildren = 115 + aspect: 116 + { isMeaningful, nodeIdentity }: 117 + fx.bind 118 + (chainWrap nodeIdentity isMeaningful ( 119 + fx.bind (emitSelfProvide aspect) ( 120 + selfProvResults: 121 + fx.bind (emitTransitions aspect) ( 122 + transitionResults: 123 + fx.bind (emitIncludes (aspect.includes or [ ])) ( 124 + children: fx.pure (selfProvResults ++ transitionResults ++ children) 125 + ) 126 + ) 127 + ) 128 + )) 129 + ( 130 + allChildren: 131 + let 132 + resolved = aspect // { 133 + includes = allChildren; 134 + }; 135 + in 136 + fx.bind (fx.send "resolve-complete" resolved) (_: fx.pure resolved) 137 + ); 138 + 139 + # Compile a static (non-functor) aspect into an effectful computation. 140 + compileStatic = 141 + aspect: 142 + let 143 + nodeIdentity = identity.pathKey (identity.aspectPath aspect); 144 + classKeys = builtins.filter (k: !(builtins.elem k structuralKeys)) (builtins.attrNames aspect); 145 + rawName = aspect.name or "<anon>"; 146 + isMeaningful = 147 + rawName != "<anon>" && rawName != "<function body>" && !(lib.hasPrefix "[definition " rawName); 148 + in 149 + fx.bind (fx.seq [ 150 + (emitClasses aspect classKeys nodeIdentity) 151 + (registerConstraints aspect) 152 + ]) (_: resolveChildren aspect { inherit isMeaningful nodeIdentity; }); 153 + 154 + # The aspect compiler. 155 + # 156 + # In the fx pipeline, __functor on aspects is NEVER the user's function. 157 + # The type system always sets it to defaultFunctor (parametric.withOwn). 158 + # User-defined parametric functions live in `includes` as bare children, 159 + # wrapped by wrapChild with __functionArgs carrying the real arg names. 160 + # 161 + # Two cases: 162 + # 1. __functionArgs has named args → parametric child (from wrapChild). 163 + # Resolve args via bind.fn, compile the result. 164 + # 2. Otherwise → static. Strip __functor/__functionArgs (legacy default), 165 + # compile the attrset directly. 166 + # 167 + # Factory aspects (ctx: { ... } with bare arg) are not supported in the 168 + # fx pipeline. Use destructured args: { host, ... }: { ... }. 169 + aspectToEffect = 170 + aspect: 171 + let 172 + userArgs = aspect.__functionArgs or { }; 173 + isParametric = userArgs != { } && aspect ? __functor; 174 + in 175 + if isParametric then 176 + let 177 + fn = aspect.__functor aspect; 178 + in 179 + fx.bind (fx.bind.fn { } fn) ( 180 + resolved: 181 + aspectToEffect ( 182 + { 183 + inherit (aspect) name; 184 + meta = (aspect.meta or { }) // (resolved.meta or { }); 185 + } 186 + // lib.optionalAttrs (aspect ? into) { inherit (aspect) into; } 187 + // lib.optionalAttrs (aspect ? provides) { inherit (aspect) provides; } 188 + // builtins.removeAttrs (resolved) [ "meta" ] 189 + ) 190 + ) 191 + else 192 + compileStatic ( 193 + builtins.removeAttrs aspect [ 194 + "__functor" 195 + "__functionArgs" 196 + ] 197 + ); 198 + 199 + in 200 + { 201 + inherit 202 + aspectToEffect 203 + emitIncludes 204 + emitTransitions 205 + emitSelfProvide 206 + ; 207 + }
+55
nix/lib/aspects/fx/constraints.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + let 7 + inherit (den.lib.aspects.fx.identity) aspectPath pathKey; 8 + 9 + # Add subtree (default) and .global variants to a constraint constructor. 10 + # mkFields returns the constraint record without scope. 11 + scoped = mkFields: { 12 + __functor = self: args: (mkFields args) // { scope = "subtree"; }; 13 + global = args: (mkFields args) // { scope = "global"; }; 14 + }; 15 + 16 + exclude = scoped ( 17 + ref: 18 + assert 19 + builtins.isAttrs ref || throw "fx.exclude: expected aspect attrset, got ${builtins.typeOf ref}"; 20 + { 21 + type = "exclude"; 22 + identity = pathKey (aspectPath ref); 23 + } 24 + ); 25 + 26 + substituteFields = 27 + ref: replacement: 28 + assert 29 + builtins.isAttrs ref 30 + || throw "fx.substitute: expected aspect attrset for ref, got ${builtins.typeOf ref}"; 31 + { 32 + type = "substitute"; 33 + identity = pathKey (aspectPath ref); 34 + replacementName = replacement.name or "<anon>"; 35 + getReplacement = _: replacement; 36 + }; 37 + 38 + substitute = { 39 + __functor = 40 + _: ref: replacement: 41 + substituteFields ref replacement // { scope = "subtree"; }; 42 + global = ref: replacement: substituteFields ref replacement // { scope = "global"; }; 43 + }; 44 + 45 + # Predicate-based filter. Excludes aspects where pred returns false. 46 + # pred receives the aspect attrset (with name, meta, includes, etc). 47 + filterBy = scoped (pred: { 48 + type = "filter"; 49 + predicate = pred; 50 + }); 51 + 52 + in 53 + { 54 + inherit exclude substitute filterBy; 55 + }
+14
nix/lib/aspects/fx/default.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + { 7 + identity = import ./identity.nix { inherit lib den; }; 8 + constraints = import ./constraints.nix { inherit lib den; }; 9 + includes = import ./includes.nix { inherit lib den; }; 10 + trace = import ./trace.nix { inherit lib den; }; 11 + handlers = import ./handlers { inherit lib den; }; 12 + aspect = import ./aspect.nix { inherit lib den; }; 13 + pipeline = import ./pipeline.nix { inherit lib den; }; 14 + }
+46
nix/lib/aspects/fx/handlers/ctx.nix
··· 1 + # constantHandler: Handles <arg-name> effects — resumes with context values for parametric aspects. 2 + # ctxSeenHandler: Handles ctx-seen — dedup tracking for context stages. 3 + # State reads: seen | State writes: seen 4 + { 5 + lib, 6 + den, 7 + ... 8 + }: 9 + let 10 + # Build handler set from context. 11 + # Each key in ctx becomes a handler that resumes with the value. 12 + constantHandler = 13 + ctx: 14 + builtins.mapAttrs ( 15 + _: value: 16 + { param, state }: 17 + { 18 + resume = value; 19 + inherit state; 20 + } 21 + ) ctx; 22 + 23 + # Dedup handler. Tracks seen keys in state.seen. 24 + ctxSeenHandler = { 25 + "ctx-seen" = 26 + { param, state }: 27 + let 28 + isFirst = !((state.seen or { }) ? ${param}); 29 + in 30 + { 31 + resume = { inherit isFirst; }; 32 + state = state // { 33 + seen = (state.seen or { }) // { 34 + ${param} = true; 35 + }; 36 + }; 37 + }; 38 + }; 39 + 40 + in 41 + { 42 + inherit 43 + constantHandler 44 + ctxSeenHandler 45 + ; 46 + }
+9
nix/lib/aspects/fx/handlers/default.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }@args: 6 + (import ./ctx.nix args) 7 + // (import ./tree.nix args) 8 + // (import ./include.nix args) 9 + // (import ./transition.nix args)
+169
nix/lib/aspects/fx/handlers/include.nix
··· 1 + # Standalone emit-include handler — owns recursion via aspectToEffect. 2 + # Handles: emit-include 3 + # Sends: check-constraint, resolve-complete, get-path-set (via resolveConditional) 4 + # State reads: (none directly — delegates to other handlers via effects) 5 + { 6 + lib, 7 + den, 8 + ... 9 + }: 10 + let 11 + fx = den.lib.fx; 12 + identity = den.lib.aspects.fx.identity; 13 + inherit (den.lib.aspects.fx.aspect) aspectToEffect emitIncludes; 14 + 15 + # Normalize a NixOS module function ({ config, lib, ... }: ...) into an aspect 16 + # attrset by running it through the type system's merge. This extracts class keys 17 + # (nixos, homeManager, etc.) from the module's return value. 18 + # Coupling note: this is the only handler-layer reference to den.lib.aspects.types. 19 + normalizeModuleFn = 20 + child: 21 + den.lib.aspects.types.aspectType.merge 22 + [ (child.name or "<deferred>") ] 23 + [ 24 + { 25 + file = "<deferred>"; 26 + value = child; 27 + } 28 + ]; 29 + 30 + # Wrap bare function includes in an aspect envelope. 31 + wrapChild = 32 + child: 33 + if lib.isFunction child then 34 + ( 35 + # For attrset-with-functor children, extract the actual inner function 36 + # to get the real args for bind.fn resolution. This bypasses stale 37 + # __functionArgs on the attrset and gives aspectToEffect the correct 38 + # isParametric decision — whether it's a deepRecurse wrapper needing 39 + # { class, aspect-chain } or a raw parametric aspect needing { host }. 40 + if builtins.isAttrs child then 41 + let 42 + innerFn = child.__functor child; 43 + # innerFn may be a function (parametric) or a value (factory functor). 44 + innerArgs = if builtins.isFunction innerFn then builtins.functionArgs innerFn else { }; 45 + in 46 + child 47 + // { 48 + __functor = _: if builtins.isFunction innerFn then innerFn else _: innerFn; 49 + __functionArgs = innerArgs; 50 + includes = child.includes or [ ]; 51 + } 52 + else 53 + let 54 + args = lib.functionArgs child; 55 + # NixOS module functions are deferred modules, not parametric aspects. 56 + isModuleFn = den.lib.canTake.upTo { 57 + lib = true; 58 + config = true; 59 + options = true; 60 + } child; 61 + in 62 + if isModuleFn then 63 + normalizeModuleFn child 64 + else 65 + { 66 + name = child.name or "<anon>"; 67 + meta = child.meta or { }; 68 + __functor = _: child; 69 + __functionArgs = args; 70 + includes = [ ]; 71 + } 72 + ) 73 + else 74 + child; 75 + 76 + tombstoneAll = 77 + aspects: 78 + builtins.foldl' ( 79 + acc: a: 80 + fx.bind acc ( 81 + results: 82 + let 83 + ts = identity.tombstone a { guardFailed = true; }; 84 + in 85 + fx.bind (fx.send "resolve-complete" ts) (_: fx.pure (results ++ [ ts ])) 86 + ) 87 + ) (fx.pure [ ]) aspects; 88 + 89 + # Handle includeIf guards via get-path-set. 90 + resolveConditional = 91 + condNode: 92 + fx.bind (fx.send "get-path-set" null) ( 93 + pathSet: 94 + let 95 + guardCtx = { 96 + hasAspect = ref: pathSet ? ${identity.pathKey (identity.aspectPath ref)}; 97 + }; 98 + pass = condNode.meta.guard guardCtx; 99 + in 100 + if pass then emitIncludes condNode.meta.aspects else tombstoneAll condNode.meta.aspects 101 + ); 102 + 103 + # Exclude: create tombstone and emit resolve-complete. 104 + excludeChild = 105 + child: owner: 106 + let 107 + ts = identity.tombstone child { excludedFrom = owner; }; 108 + in 109 + fx.bind (fx.send "resolve-complete" ts) (_: fx.pure [ ts ]); 110 + 111 + # Substitute: tombstone original, resolve replacement via aspectToEffect. 112 + substituteChild = 113 + child: decision: 114 + let 115 + ts = identity.tombstone child { 116 + excludedFrom = decision.owner; 117 + replacedBy = decision.replacement.name or "<anon>"; 118 + }; 119 + in 120 + fx.bind (fx.send "resolve-complete" ts) ( 121 + _: 122 + fx.bind (aspectToEffect decision.replacement) ( 123 + resolved: 124 + fx.pure [ 125 + ts 126 + resolved 127 + ] 128 + ) 129 + ); 130 + 131 + # Keep: resolve via aspectToEffect (which emits resolve-complete internally). 132 + keepChild = child: fx.bind (aspectToEffect child) (resolved: fx.pure [ resolved ]); 133 + 134 + # The handler. 135 + includeHandler = { 136 + "emit-include" = 137 + { param, state }: 138 + let 139 + child = wrapChild param; 140 + childIdentity = identity.pathKey (identity.aspectPath child); 141 + isConditional = builtins.isAttrs child && child ? meta && child.meta ? guard; 142 + in 143 + { 144 + resume = 145 + if isConditional then 146 + resolveConditional child 147 + else 148 + fx.bind 149 + (fx.send "check-constraint" { 150 + identity = childIdentity; 151 + aspect = child; 152 + }) 153 + ( 154 + decision: 155 + if decision.action == "exclude" then 156 + excludeChild child decision.owner 157 + else if decision.action == "substitute" then 158 + substituteChild child decision 159 + else 160 + keepChild child 161 + ); 162 + inherit state; 163 + }; 164 + }; 165 + 166 + in 167 + { 168 + inherit includeHandler wrapChild; 169 + }
+101
nix/lib/aspects/fx/handlers/transition.nix
··· 1 + # into-transition handler — processes context transitions with scoped sub-computations. 2 + # Handles: into-transition 3 + # Sends: ctx-seen (dedup), resolve-complete (missing transition tombstone), 4 + # then aspectToEffect in scoped constantHandler(parentCtx // newCtx) 5 + # State reads: currentCtx 6 + # External dependency: den.ctx (context aspect registry, looked up by transition path) 7 + { 8 + lib, 9 + den, 10 + ... 11 + }: 12 + let 13 + fx = den.lib.fx; 14 + inherit (den.lib.aspects.fx.handlers) constantHandler; 15 + inherit (den.lib.aspects.fx.aspect) aspectToEffect; 16 + 17 + # Flatten a nested into attrset into a flat list of { path, contexts }. 18 + flattenInto = 19 + attrset: prefix: 20 + lib.concatLists ( 21 + lib.mapAttrsToList ( 22 + name: v: 23 + let 24 + path = prefix ++ [ name ]; 25 + in 26 + if builtins.isList v then 27 + [ 28 + { 29 + inherit path; 30 + contexts = v; 31 + } 32 + ] 33 + else 34 + flattenInto v path 35 + ) attrset 36 + ); 37 + 38 + # Resolve a single context value by running aspectToEffect in a scoped handler. 39 + resolveContextValue = 40 + parentCtx: targetAspect: results: newCtx: 41 + let 42 + scopedCtx = parentCtx // newCtx; 43 + in 44 + fx.bind (fx.effects.scope.stateful (constantHandler scopedCtx) (aspectToEffect targetAspect)) ( 45 + childResult: fx.pure (results ++ [ childResult ]) 46 + ); 47 + 48 + # Resolve a single transition: look up target aspect, check dedup, resolve each context value. 49 + resolveTransition = 50 + currentCtx: results: transition: 51 + let 52 + key = lib.concatStringsSep "/" transition.path; 53 + # den.ctx is the global context aspect registry (e.g. den.ctx.user, den.ctx.host). 54 + # If the transition targets a path not in the registry, emit a diagnostic tombstone. 55 + targetAspect = lib.attrByPath transition.path null (den.ctx or { }); 56 + in 57 + if targetAspect == null then 58 + let 59 + ts = { 60 + name = "~<missing-transition:${key}>"; 61 + meta = { 62 + excluded = true; 63 + transitionMissing = true; 64 + transitionPath = key; 65 + }; 66 + includes = [ ]; 67 + }; 68 + in 69 + fx.bind (fx.send "resolve-complete" ts) (_: fx.pure (results ++ [ ts ])) 70 + else 71 + fx.bind (fx.send "ctx-seen" key) ( 72 + { isFirst }: 73 + if !isFirst then 74 + fx.pure results 75 + else 76 + builtins.foldl' ( 77 + acc: newCtx: 78 + fx.bind acc (innerResults: resolveContextValue currentCtx targetAspect innerResults newCtx) 79 + ) (fx.pure results) transition.contexts 80 + ); 81 + 82 + transitionHandler = { 83 + "into-transition" = 84 + { param, state }: 85 + let 86 + currentCtx = state.currentCtx or { }; 87 + intoResult = param.intoFn currentCtx; 88 + transitions = flattenInto intoResult [ ]; 89 + in 90 + { 91 + resume = builtins.foldl' ( 92 + acc: transition: fx.bind acc (results: resolveTransition currentCtx results transition) 93 + ) (fx.pure [ ]) transitions; 94 + inherit state; 95 + }; 96 + }; 97 + 98 + in 99 + { 100 + inherit transitionHandler; 101 + }
+165
nix/lib/aspects/fx/handlers/tree.nix
··· 1 + # constraintRegistryHandler: Handles register-constraint, check-constraint 2 + # State reads: constraintRegistry, constraintFilters, includesChain 3 + # State writes: constraintRegistry, constraintFilters 4 + # chainHandler: Handles chain-push, chain-pop 5 + # State reads/writes: includesChain 6 + # classCollectorHandler: Handles emit-class 7 + # State reads/writes: imports 8 + { 9 + lib, 10 + den, 11 + ... 12 + }: 13 + let 14 + # Constraint registry. Handles register-constraint and check-constraint effects. 15 + # Supports identity-based (exclude, substitute) and predicate-based (filter). 16 + constraintRegistryHandler = { 17 + "register-constraint" = 18 + { param, state }: 19 + let 20 + ownerChain = state.includesChain or [ ]; 21 + scope = param.scope or "subtree"; 22 + in 23 + if param.type == "filter" then 24 + { 25 + resume = null; 26 + state = state // { 27 + constraintFilters = (state.constraintFilters or [ ]) ++ [ 28 + { 29 + predicate = param.predicate; 30 + owner = param.owner or "<anon>"; 31 + inherit scope ownerChain; 32 + } 33 + ]; 34 + }; 35 + } 36 + else 37 + let 38 + existing = (state.constraintRegistry or { }).${param.identity} or [ ]; 39 + entry = { 40 + type = param.type; 41 + getReplacement = param.getReplacement or (_: null); 42 + owner = param.owner or "<anon>"; 43 + inherit scope ownerChain; 44 + }; 45 + in 46 + { 47 + resume = null; 48 + state = state // { 49 + constraintRegistry = (state.constraintRegistry or { }) // { 50 + ${param.identity} = existing ++ [ entry ]; 51 + }; 52 + }; 53 + }; 54 + 55 + # Check if an aspect should be excluded/substituted/filtered. 56 + # First checks identity-based registry, then predicate filters. 57 + # param = { identity; aspect; } or a bare identity string (used by tests). 58 + "check-constraint" = 59 + { param, state }: 60 + let 61 + identity = if builtins.isAttrs param then param.identity else param; 62 + aspect = if builtins.isAttrs param then param.aspect or null else null; 63 + registry = state.constraintRegistry or { }; 64 + filters = state.constraintFilters or [ ]; 65 + currentChain = state.includesChain or [ ]; 66 + # True when ownerChain is a prefix of currentChain (subtree membership). 67 + isAncestor = ownerChain: lib.take (builtins.length ownerChain) currentChain == ownerChain; 68 + inScope = entry: (entry.scope or "global") == "global" || isAncestor (entry.ownerChain or [ ]); 69 + mkDecision = action: extra: { 70 + resume = { 71 + inherit action; 72 + } 73 + // extra; 74 + inherit state; 75 + }; 76 + # Find first in-scope constraint for this identity (first-registered wins). 77 + entries = registry.${identity} or [ ]; 78 + scopedEntries = builtins.filter inScope entries; 79 + firstEntry = if scopedEntries == [ ] then null else builtins.head scopedEntries; 80 + in 81 + if firstEntry != null then 82 + if firstEntry.type == "exclude" then 83 + mkDecision "exclude" { owner = firstEntry.owner; } 84 + else if firstEntry.type == "substitute" then 85 + mkDecision "substitute" { 86 + replacement = firstEntry.getReplacement null; 87 + owner = firstEntry.owner; 88 + } 89 + else 90 + mkDecision "keep" { } 91 + else 92 + # No in-scope identity match — check predicate filters. 93 + let 94 + scopedFilters = builtins.filter inScope filters; 95 + failedFilter = 96 + if aspect != null then lib.findFirst (f: !(f.predicate aspect)) null scopedFilters else null; 97 + in 98 + if failedFilter != null then 99 + mkDecision "exclude" { owner = failedFilter.owner; } 100 + else 101 + mkDecision "keep" { }; 102 + }; 103 + 104 + # Maintains includes-path stack. chain-push appends identity, chain-pop removes last. 105 + chainHandler = { 106 + "chain-push" = 107 + { param, state }: 108 + { 109 + resume = null; 110 + state = state // { 111 + includesChain = (state.includesChain or [ ]) ++ [ param.identity ]; 112 + }; 113 + }; 114 + "chain-pop" = 115 + { param, state }: 116 + let 117 + chain = state.includesChain or [ ]; 118 + in 119 + { 120 + resume = null; 121 + state = state // { 122 + includesChain = 123 + if chain == [ ] then 124 + throw "fx: chain-pop on empty includesChain — push/pop mismatch in aspect compiler" 125 + else 126 + lib.init chain; 127 + }; 128 + }; 129 + }; 130 + 131 + # Accumulates class modules from emit-class effects. 132 + # Only collects modules for the specified target class. 133 + classCollectorHandler = 134 + { 135 + targetClass, 136 + }: 137 + { 138 + "emit-class" = 139 + { param, state }: 140 + if param.class != targetClass then 141 + { 142 + resume = null; 143 + inherit state; 144 + } 145 + else 146 + let 147 + identity = param.identity or "<anon>"; 148 + mod = lib.setDefaultModuleLocation "${param.class}@${identity}" param.module; 149 + in 150 + { 151 + resume = null; 152 + state = state // { 153 + imports = x: (state.imports x) ++ [ mod ]; 154 + }; 155 + }; 156 + }; 157 + 158 + in 159 + { 160 + inherit 161 + constraintRegistryHandler 162 + chainHandler 163 + classCollectorHandler 164 + ; 165 + }
+75
nix/lib/aspects/fx/identity.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + let 7 + aspectPath = a: (a.meta.provider or [ ]) ++ [ (a.name or "<anon>") ]; 8 + 9 + pathKey = path: lib.concatStringsSep "/" path; 10 + 11 + toPathSet = 12 + paths: 13 + builtins.listToAttrs ( 14 + builtins.map (p: { 15 + name = pathKey p; 16 + value = true; 17 + }) paths 18 + ); 19 + 20 + tombstone = resolved: extra: { 21 + name = "~${resolved.name or "<anon>"}"; 22 + meta = 23 + (resolved.meta or { }) 24 + // { 25 + excluded = true; 26 + originalName = resolved.name or "<anon>"; 27 + } 28 + // extra; 29 + includes = [ ]; 30 + }; 31 + 32 + collectPathsHandler = { 33 + "resolve-complete" = 34 + { param, state }: 35 + let 36 + isExcluded = param.meta.excluded or false; 37 + path = aspectPath param; 38 + key = pathKey path; 39 + in 40 + { 41 + resume = param; 42 + state = 43 + state 44 + // { 45 + paths = (state.paths or [ ]) ++ (lib.optional (!isExcluded) path); 46 + } 47 + // lib.optionalAttrs (!isExcluded) { 48 + pathSet = (state.pathSet or { }) // { 49 + ${key} = true; 50 + }; 51 + }; 52 + }; 53 + }; 54 + 55 + # Handler for get-path-set effect. Returns accumulated paths as a set. 56 + pathSetHandler = { 57 + "get-path-set" = 58 + { param, state }: 59 + { 60 + resume = state.pathSet or { }; 61 + inherit state; 62 + }; 63 + }; 64 + 65 + in 66 + { 67 + inherit 68 + aspectPath 69 + pathKey 70 + toPathSet 71 + tombstone 72 + collectPathsHandler 73 + pathSetHandler 74 + ; 75 + }
+24
nix/lib/aspects/fx/includes.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + let 7 + # Conditional inclusion based on a guard function. 8 + # The guard receives { hasAspect = ref: bool; } where hasAspect checks the 9 + # path set accumulated so far during resolution. Because resolution is sequential 10 + # (left-to-right through includes), guards can only see aspects resolved BEFORE 11 + # them in the tree. Reordering includes may change which guards pass. 12 + includeIf = guardFn: aspects: { 13 + name = "<includeIf>"; 14 + meta = { 15 + guard = guardFn; 16 + aspects = aspects; 17 + }; 18 + includes = [ ]; 19 + }; 20 + 21 + in 22 + { 23 + inherit includeIf; 24 + }
+142
nix/lib/aspects/fx/pipeline.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + let 7 + fx = den.lib.fx; 8 + handlers = den.lib.aspects.fx.handlers; 9 + identity = den.lib.aspects.fx.identity; 10 + inherit (den.lib.aspects.fx.aspect) aspectToEffect; 11 + 12 + # Compose two handler sets, chaining handlers for shared effect names. 13 + # For overlapping keys: b's resume wins, a's state wins (a runs on b's output state). 14 + # 15 + # IMPORTANT LIMITATIONS: 16 + # 1. Composed handlers MUST NOT write to the same state keys — a runs on b's output 17 + # state so shared keys would double-append. 18 + # 2. When b returns an effectful resume (computation), the sub-computation runs with 19 + # b's state, not a's. State changes from a are lost for the duration of the 20 + # sub-computation. Only correct when a does not produce effectful resumes for 21 + # shared effect names. 22 + # 23 + # Designed for the tracing use case: tracingHandler (b) controls resume, 24 + # defaultHandlers (a) accumulates paths/imports. Both constraints hold for this case. 25 + composeHandlers = 26 + a: b: 27 + let 28 + shared = builtins.intersectAttrs a b; 29 + sharedComposed = builtins.mapAttrs ( 30 + name: _: 31 + { param, state }: 32 + let 33 + rb = b.${name} { inherit param state; }; 34 + ra = a.${name} { 35 + inherit param; 36 + state = rb.state; 37 + }; 38 + in 39 + { 40 + resume = rb.resume; 41 + state = ra.state; 42 + } 43 + ) shared; 44 + in 45 + a // b // sharedComposed; 46 + 47 + # Default handler set for the unified pipeline. 48 + defaultHandlers = 49 + { class, ctx }: 50 + handlers.constantHandler ( 51 + ctx 52 + // { 53 + inherit class; 54 + # Provider functions from the type system (providerFnType.merge in types.nix) 55 + # create { class, aspect-chain } functors. These reach bind.fn through 56 + # aspectToEffect and send aspect-chain as an effect. Provide empty chain — 57 + # the fx pipeline uses chain-push/chain-pop for provenance tracking instead. 58 + # TODO(vic): Remove when type system no longer creates { class, aspect-chain } providers. 59 + "aspect-chain" = [ ]; 60 + } 61 + ) 62 + // handlers.classCollectorHandler { targetClass = class; } 63 + // handlers.constraintRegistryHandler 64 + // handlers.chainHandler 65 + // handlers.includeHandler 66 + // handlers.transitionHandler 67 + // handlers.ctxSeenHandler 68 + // identity.pathSetHandler 69 + // identity.collectPathsHandler 70 + // fx.effects.state.handler; 71 + 72 + defaultState = { 73 + seen = { }; 74 + imports = _: [ ]; 75 + constraintRegistry = { }; 76 + constraintFilters = [ ]; 77 + paths = [ ]; 78 + pathSet = { }; 79 + includesChain = [ ]; 80 + }; 81 + 82 + # Configurable pipeline builder. Runs aspectToEffect on the root aspect 83 + # with the full handler set. 84 + mkPipeline = 85 + { 86 + extraHandlers ? { }, 87 + extraState ? { }, 88 + class, 89 + }: 90 + { 91 + self, 92 + ctx, 93 + }: 94 + let 95 + comp = aspectToEffect self; 96 + # Override aspect-chain to include root aspect — consumed by type-system provider 97 + # functions (parametric.nix, home-env.nix) and legacy resolve pipeline. 98 + rootHandlers = 99 + defaultHandlers { inherit class ctx; } 100 + // handlers.constantHandler { 101 + "aspect-chain" = [ self ]; 102 + }; 103 + in 104 + fx.handle { 105 + handlers = composeHandlers rootHandlers extraHandlers; 106 + state = defaultState // extraState // { currentCtx = ctx; }; 107 + } comp; 108 + 109 + # Full pipeline: aspect compilation → handler-driven resolution → module collection. 110 + # Returns raw fx.handle result with { value, state }. 111 + fxFullResolve = 112 + { 113 + class, 114 + self, 115 + ctx, 116 + }: 117 + mkPipeline { inherit class; } { inherit self ctx; }; 118 + 119 + # Drop-in resolve shape: returns { imports = [...] }. 120 + fxResolve = 121 + { 122 + class, 123 + self, 124 + ctx, 125 + }: 126 + let 127 + result = mkPipeline { inherit class; } { inherit self ctx; }; 128 + in 129 + { 130 + imports = result.state.imports null; 131 + }; 132 + in 133 + { 134 + inherit 135 + composeHandlers 136 + defaultHandlers 137 + defaultState 138 + mkPipeline 139 + fxFullResolve 140 + fxResolve 141 + ; 142 + }
+117
nix/lib/aspects/fx/trace.nix
··· 1 + { 2 + lib, 3 + den, 4 + ... 5 + }: 6 + let 7 + inherit (den.lib.aspects.fx.identity) aspectPath pathKey; 8 + 9 + # Derive parent from includesChain, filtering out self-references. 10 + # The chain contains raw identity strings from chain-push (pathKey of aspectPath). 11 + # 12 + # In structuredTraceHandler: selfPath is the raw pathKey — filter is effective 13 + # for meaningful nodes whose identity matches a chain entry. 14 + # 15 + # In tracingHandler: selfFullPath may be a disambiguated name (e.g., 16 + # "host/resolve(desktop):provider") for anonymous nodes. Since anonymous nodes 17 + # don't push to the chain, the filter is a no-op for them — which is correct. 18 + # The filter only matters for meaningful (chain-pushing) nodes where 19 + # selfFullPath == raw pathKey. 20 + chainParent = 21 + chain: selfPath: 22 + let 23 + filtered = builtins.filter (p: p != selfPath) chain; 24 + in 25 + if filtered == [ ] then null else lib.last filtered; 26 + 27 + # Shared entry fields for both trace handlers. 28 + mkBaseEntry = class: param: { 29 + inherit class; 30 + provider = param.meta.provider or [ ]; 31 + excluded = param.meta.excluded or false; 32 + excludedFrom = param.meta.excludedFrom or null; 33 + replacedBy = param.meta.replacedBy or null; 34 + isProvider = (param.meta.provider or [ ]) != [ ]; 35 + handlers = param.meta.handleWith or [ ]; 36 + hasClass = param ? ${class}; 37 + isParametric = param.meta.isParametric or false; 38 + fnArgNames = param.meta.fnArgNames or [ ]; 39 + }; 40 + 41 + # Minimal trace handler — accumulates entries without disambiguation. 42 + # Use for tests that verify basic parent/entry structure. 43 + # For full tracing with anonymous entry disambiguation, use tracingHandler. 44 + structuredTraceHandler = class: { 45 + "resolve-complete" = 46 + { param, state }: 47 + let 48 + selfPath = pathKey (aspectPath param); 49 + entry = mkBaseEntry class param // { 50 + name = param.name or "<anon>"; 51 + parent = chainParent (state.includesChain or [ ]) selfPath; 52 + ctxStage = param.__ctxStage or null; 53 + ctxKind = param.__ctxKind or null; 54 + }; 55 + in 56 + { 57 + resume = param; 58 + state = state // { 59 + entries = (state.entries or [ ]) ++ [ entry ]; 60 + }; 61 + }; 62 + }; 63 + 64 + # Combined resolve-complete handler for tracing: collects trace entries and paths. 65 + # Module collection is handled by classCollectorHandler via emit-class effects. 66 + # Use as extraHandlers with mkPipeline. 67 + # 68 + # Disambiguates anonymous entries using context stage tags, matching the 69 + # legacy structuredTrace adapter's naming: stage/kind(aspect):provider. 70 + tracingHandler = class: { 71 + "resolve-complete" = 72 + { param, state }: 73 + let 74 + rawName = param.meta.originalName or param.name or "<anon>"; 75 + provPath = lib.concatStringsSep "/" (param.meta.provider or [ ]); 76 + ctxStage = param.__ctxStage or (state.currentStage or null); 77 + ctxKind = param.__ctxKind or (state.currentKind or null); 78 + ctxAspect = param.__ctxAspect or (state.currentCtxAspect or null); 79 + meaningful = 80 + n: n != "<anon>" && n != "<function body>" && !(lib.hasPrefix "[definition " n) && n != null; 81 + isAnon = !meaningful rawName; 82 + name = 83 + if isAnon && ctxStage != null then 84 + let 85 + stage = ctxStage; 86 + kind = if ctxKind != null then ctxKind else "resolve"; 87 + aspectTag = if ctxAspect != null then "(${ctxAspect})" else ""; 88 + provTag = lib.optionalString (provPath != "") ":${provPath}"; 89 + in 90 + "${stage}/${kind}${aspectTag}${provTag}" 91 + else 92 + rawName; 93 + selfFullPath = if provPath != "" then "${provPath}/${name}" else name; 94 + entry = mkBaseEntry class param // { 95 + inherit name ctxStage ctxKind; 96 + parent = chainParent (state.includesChain or [ ]) selfFullPath; 97 + }; 98 + in 99 + { 100 + resume = param; 101 + state = 102 + state 103 + // { 104 + entries = (state.entries or [ ]) ++ [ entry ]; 105 + } 106 + // lib.optionalAttrs (param ? __ctxStage) { 107 + currentStage = param.__ctxStage; 108 + currentKind = param.__ctxKind or null; 109 + currentCtxAspect = param.__ctxAspect or null; 110 + }; 111 + }; 112 + }; 113 + 114 + in 115 + { 116 + inherit structuredTraceHandler tracingHandler; 117 + }
+3
nix/lib/aspects/resolve.nix
··· 1 + # Legacy resolve pipeline — explicit recursive tree walking. 2 + # The fx pipeline (aspectToEffect + emit-include handler) replaces this. 3 + # Active when den.fxPipeline = false. 1 4 { lib, den, ... }: 2 5 let 3 6
+26 -2
nix/lib/aspects/types.nix
··· 106 106 freeformType = lib.types.lazyAttrsOf lib.types.unspecified; 107 107 config.self = config; 108 108 options.adapter = lib.mkOption { 109 - description = "Adapter to compose into resolution for this aspect's subtree"; 110 - type = lib.types.nullOr (lastFunctionTo lib.types.raw); 109 + description = "Legacy adapter function for resolution"; 110 + type = lib.types.nullOr ( 111 + lib.types.mkOptionType { 112 + name = "adapterFunction"; 113 + description = "function adapter"; 114 + check = lib.isFunction; 115 + merge = _: defs: (lib.last defs).value; 116 + } 117 + ); 111 118 default = null; 119 + }; 120 + options.handleWith = lib.mkOption { 121 + description = "Resolution handlers for this aspect's subtree"; 122 + type = lib.types.nullOr ( 123 + lib.types.mkOptionType { 124 + name = "handlerValue"; 125 + description = "handler record or list of handler records"; 126 + check = v: builtins.isAttrs v || builtins.isList v; 127 + merge = _: defs: (lib.last defs).value; 128 + } 129 + ); 130 + default = null; 131 + }; 132 + options.excludes = lib.mkOption { 133 + description = "Aspects to exclude from this subtree (sugar for handleWith)"; 134 + type = lib.types.listOf lib.types.unspecified; 135 + default = [ ]; 112 136 }; 113 137 options.provider = lib.mkOption { 114 138 internal = true;
+5 -1
nix/lib/ctx-apply.nix
··· 83 83 isFirst = !(item.seen ? ${item.key}); 84 84 selfProvider = item.self.provides.${item.self.name} or noop; 85 85 crossProvider = getCrossProvider item; 86 + # Strip into — ctxApply already processed it. Leaving it on would cause 87 + # aspectToEffect to re-attach it after parametric resolution, attempting 88 + # to call the function without the original context args. 89 + stripped = builtins.removeAttrs item.self [ "into" ]; 86 90 in 87 91 [ 88 - (if isFirst then parametric.fixedTo item.ctx item.self else parametric.atLeast item.self item.ctx) 92 + (if isFirst then parametric.fixedTo item.ctx stripped else parametric.atLeast stripped item.ctx) 89 93 (selfProvider item.ctx) 90 94 (crossProvider item.ctx) 91 95 ];
+1
nix/lib/default.nix
··· 33 33 take = ./take.nix; 34 34 lastFunctionTo = ./last-function-to.nix; 35 35 strict = ./strict.nix; 36 + fx = ./fx.nix; 36 37 }; 37 38 in 38 39 den-lib
+11
nix/lib/fx.nix
··· 1 + { inputs, lib, ... }: 2 + let 3 + lock = builtins.fromJSON (builtins.readFile ../../templates/ci/flake.lock); 4 + locked = lock.nodes.nix-effects.locked; 5 + nix-effects = builtins.fetchTarball { 6 + url = "https://github.com/${locked.owner}/${locked.repo}/archive/${locked.rev}.zip"; 7 + sha256 = locked.narHash; 8 + }; 9 + nfx = import nix-effects { inherit lib; }; 10 + in 11 + inputs.nix-effects.lib or nfx
+1 -1
nix/lib/home-env.nix
··· 74 74 forwardPathFn, 75 75 }: 76 76 { host, user }: 77 - den._.forward { 77 + den.provides.forward { 78 78 each = lib.singleton true; 79 79 fromClass = _: className; 80 80 intoClass = _: host.class;
+3 -1
nix/lib/parametric.nix
··· 15 15 name = self.name or "<anon>"; 16 16 meta = { 17 17 adapter = meta.adapter or null; 18 + handleWith = meta.handleWith or null; 19 + excludes = meta.excludes or [ ]; 18 20 provider = meta.provider or [ ]; 19 21 }; 20 22 } ··· 34 36 # When takeFn succeeds and returns a result with sub-includes, 35 37 # also try to resolve those sub-includes with takeFn. This handles 36 38 # provider sub-aspect functions nested inside include results: 37 - # e.g. wrapped_fn returns { includes = [foo._.sub]; } where foo._.sub 39 + # e.g. wrapped_fn returns { includes = [foo.provides.sub]; } where foo.provides.sub 38 40 # needs parametric context applied before reaching the static pipeline. 39 41 applyDeep = 40 42 takeFn: ctx: fn:
+3
nix/lib/statics.nix
··· 1 + # Legacy pipeline only — wraps aspects with { class, aspect-chain } functors. 2 + # The fx pipeline uses aspectToEffect + constantHandler instead. 3 + # Remove when the legacy pipeline is removed. 1 4 { lib, den, ... }: 2 5 let 3 6 owned = (lib.flip builtins.removeAttrs) [
+2
shell.nix
··· 14 14 }: 15 15 pkgs.mkShell { 16 16 buildInputs = with pkgs; [ 17 + jq 17 18 just 18 19 nix-unit 19 20 npins 20 21 pnpm 21 22 nodejs 22 23 hyperfine 24 + nix-eval-jobs 23 25 ]; 24 26 }
+45
templates/ci/flake.lock
··· 70 70 "type": "github" 71 71 } 72 72 }, 73 + "nix-effects": { 74 + "inputs": { 75 + "nix-unit": [ 76 + "nix-unit" 77 + ], 78 + "nixpkgs": [ 79 + "nixpkgs" 80 + ] 81 + }, 82 + "locked": { 83 + "lastModified": 1776349759, 84 + "narHash": "sha256-X4Vz5z9JQDPkzKK7DtVN1V1omgV+T2fK6NJoN2FF79A=", 85 + "owner": "vic", 86 + "repo": "nix-effects", 87 + "rev": "db85155059573188e2e24f413f5d45b1c80a5462", 88 + "type": "github" 89 + }, 90 + "original": { 91 + "owner": "vic", 92 + "repo": "nix-effects", 93 + "type": "github" 94 + } 95 + }, 96 + "nix-unit": { 97 + "inputs": { 98 + "nixpkgs": [ 99 + "nixpkgs" 100 + ] 101 + }, 102 + "locked": { 103 + "lastModified": 1762774186, 104 + "narHash": "sha256-hRADkHjNt41+JUHw2EiSkMaL4owL83g5ZppjYUdF/Dc=", 105 + "owner": "nix-community", 106 + "repo": "nix-unit", 107 + "rev": "1c9ab50554eed0b768f9e5b6f646d63c9673f0f7", 108 + "type": "github" 109 + }, 110 + "original": { 111 + "owner": "nix-community", 112 + "repo": "nix-unit", 113 + "type": "github" 114 + } 115 + }, 73 116 "nixpkgs": { 74 117 "locked": { 75 118 "lastModified": 1775710090, ··· 111 154 "den": "den", 112 155 "home-manager": "home-manager", 113 156 "import-tree": "import-tree", 157 + "nix-effects": "nix-effects", 158 + "nix-unit": "nix-unit", 114 159 "nixpkgs": "nixpkgs", 115 160 "provider": "provider" 116 161 }
+7
templates/ci/flake.nix
··· 22 22 import-tree.follows = "import-tree"; 23 23 den.follows = "den"; 24 24 }; 25 + 26 + nix-unit.url = "github:nix-community/nix-unit"; 27 + nix-unit.inputs.nixpkgs.follows = "nixpkgs"; 28 + 29 + nix-effects.url = "github:vic/nix-effects"; 30 + nix-effects.inputs.nixpkgs.follows = "nixpkgs"; 31 + nix-effects.inputs.nix-unit.follows = "nix-unit"; 25 32 }; 26 33 }
+2
templates/ci/modules/features/adapter-owner.nix
··· 50 50 pathKeys = map adapters.pathKey (pathResult.paths or [ ]); 51 51 in 52 52 { 53 + den.fxPipeline = false; 53 54 expr = { 54 55 dropExcluded = !(builtins.elem "drop" pathKeys); 55 56 keepPresent = builtins.elem "keep" pathKeys; ··· 109 110 pathKeys = map adapters.pathKey (pathResult.paths or [ ]); 110 111 in 111 112 { 113 + den.fxPipeline = false; 112 114 expr = { 113 115 deepDropExcluded = !(builtins.elem "deep-drop" pathKeys); 114 116 deepKeepPresent = builtins.elem "deep-keep" pathKeys;
+8
templates/ci/modules/features/adapter-propagation.nix
··· 7 7 test-resolve-honors-meta-adapter = denTest ( 8 8 { den, ... }: 9 9 { 10 + den.fxPipeline = false; 10 11 den.aspects.foo.includes = [ den.aspects.bar ]; 11 12 den.aspects.foo.meta.adapter = den.lib.aspects.adapters.filter (a: (a.name or null) != "bar"); 12 13 den.aspects.bar.nixos = { }; ··· 19 20 test-tags-includes-with-adapter = denTest ( 20 21 { den, trace, ... }: 21 22 { 23 + den.fxPipeline = false; 22 24 den.aspects.parent.includes = [ den.aspects.child ]; 23 25 den.aspects.parent.meta.adapter = den.lib.aspects.adapters.filter (a: (a.name or null) != "baz"); 24 26 den.aspects.child.includes = [ den.aspects.baz ]; ··· 39 41 test-child-inherits-parent-adapter = denTest ( 40 42 { den, trace, ... }: 41 43 { 44 + den.fxPipeline = false; 42 45 den.aspects.parent.includes = [ den.aspects.child ]; 43 46 den.aspects.parent.meta.adapter = den.lib.aspects.adapters.filter ( 44 47 a: (a.name or null) != "excluded" ··· 65 68 test-deep-chain-a-excludes-c-through-b = denTest ( 66 69 { den, trace, ... }: 67 70 { 71 + den.fxPipeline = false; 68 72 den.aspects.a.includes = [ den.aspects.b ]; 69 73 den.aspects.a.meta.adapter = den.lib.aspects.adapters.filter (a: (a.name or null) != "c"); 70 74 den.aspects.b.includes = [ ··· 89 93 test-diamond-a-excludes-d-through-both-paths = denTest ( 90 94 { den, trace, ... }: 91 95 { 96 + den.fxPipeline = false; 92 97 den.aspects.a.includes = [ 93 98 den.aspects.b 94 99 den.aspects.c ··· 118 123 test-ctx-carries-meta-adapter = denTest ( 119 124 { den, ... }: 120 125 { 126 + den.fxPipeline = false; 121 127 den.hosts.x86_64-linux.igloo = { }; 122 128 123 129 den.ctx.host.meta.adapter = den.lib.aspects.adapters.filter (a: a.name != "foo"); ··· 130 136 test-ctx-meta-adapter-null-when-unset = denTest ( 131 137 { den, ... }: 132 138 { 139 + den.fxPipeline = false; 133 140 den.hosts.x86_64-linux.igloo = { }; 134 141 135 142 expr = (den.ctx.host { host = den.hosts.x86_64-linux.igloo; }).meta.adapter; ··· 144 151 test-ctx-host-adapter-filters-transitively = denTest ( 145 152 { den, igloo, ... }: 146 153 { 154 + den.fxPipeline = false; 147 155 den.hosts.x86_64-linux.igloo.users.tux = { }; 148 156 149 157 den.ctx.host.meta.adapter = den.lib.aspects.adapters.filter (a: (a.name or null) != "blocked");
+3
templates/ci/modules/features/aspect-adapter.nix
··· 5 5 test-meta-adapter-filters-subtree = denTest ( 6 6 { den, trace, ... }: 7 7 { 8 + den.fxPipeline = false; 8 9 den.aspects.foo.includes = [ 9 10 den.aspects.bar 10 11 den.aspects.baz ··· 26 27 test-meta-adapter-only-affects-subtree = denTest ( 27 28 { den, trace, ... }: 28 29 { 30 + den.fxPipeline = false; 29 31 den.aspects.root.includes = [ 30 32 den.aspects.foo 31 33 den.aspects.baz ··· 51 53 test-meta-adapter-composes-with-caller = denTest ( 52 54 { den, trace, ... }: 53 55 { 56 + den.fxPipeline = false; 54 57 den.aspects.foo.includes = [ 55 58 den.aspects.bar 56 59 den.aspects.baz
+2 -2
templates/ci/modules/features/aspect-meta.nix
··· 80 80 KEYS 81 81 ; 82 82 }; 83 - expected.KEYS = "adapter:file:loc:name:provider:self"; 83 + expected.KEYS = "adapter:excludes:file:handleWith:loc:name:provider:self"; 84 84 } 85 85 ); 86 86 ··· 110 110 KEYS 111 111 ; 112 112 }; 113 - expected.KEYS = "adapter:file:foo:loc:name:provider:self"; 113 + expected.KEYS = "adapter:excludes:file:foo:handleWith:loc:name:provider:self"; 114 114 } 115 115 ); 116 116
+18 -6
templates/ci/modules/features/aspect-path.nix
··· 5 5 test-aspectPath-named = denTest ( 6 6 { den, ... }: 7 7 { 8 + den.fxPipeline = false; 8 9 den.aspects.foo.nixos = { }; 9 10 expr = den.lib.aspects.adapters.aspectPath den.aspects.foo; 10 11 expected = [ "foo" ]; ··· 14 15 test-aspectPath-with-provider = denTest ( 15 16 { den, ... }: 16 17 { 18 + den.fxPipeline = false; 17 19 den.aspects.monitoring = { 18 20 nixos = { }; 19 21 provides.node-exporter.nixos = { }; 20 22 }; 21 - expr = den.lib.aspects.adapters.aspectPath den.aspects.monitoring._.node-exporter; 23 + expr = den.lib.aspects.adapters.aspectPath den.aspects.monitoring.provides.node-exporter; 22 24 expected = [ 23 25 "monitoring" 24 26 "node-exporter" ··· 30 32 test-excludeAspect-tombstone-in-trace = denTest ( 31 33 { den, trace, ... }: 32 34 { 35 + den.fxPipeline = false; 33 36 den.aspects.foo.includes = [ 34 37 den.aspects.bar 35 38 den.aspects.baz ··· 53 56 test-excludeAspect-no-modules = denTest ( 54 57 { den, igloo, ... }: 55 58 { 59 + den.fxPipeline = false; 56 60 den.hosts.x86_64-linux.igloo = { }; 57 61 den.aspects.igloo.includes = [ 58 62 den.aspects.bar ··· 73 77 test-excludeAspect-propagates-to-subtree = denTest ( 74 78 { den, trace, ... }: 75 79 { 80 + den.fxPipeline = false; 76 81 den.aspects.root.includes = [ den.aspects.role ]; 77 82 den.aspects.root.meta.adapter = 78 83 inherited: den.lib.aspects.adapters.excludeAspect den.aspects.baz inherited; ··· 100 105 test-excludeAspect-by-provider = denTest ( 101 106 { den, trace, ... }: 102 107 { 108 + den.fxPipeline = false; 103 109 den.aspects.monitoring = { 104 110 nixos = { }; 105 111 provides.node-exporter.nixos = { }; ··· 107 113 }; 108 114 den.aspects.server.includes = with den.aspects; [ 109 115 monitoring 110 - monitoring._.node-exporter 111 - monitoring._.alerting 116 + monitoring.provides.node-exporter 117 + monitoring.provides.alerting 112 118 ]; 113 119 den.aspects.server.meta.adapter = 114 - inherited: den.lib.aspects.adapters.excludeAspect den.aspects.monitoring._.node-exporter inherited; 120 + inherited: 121 + den.lib.aspects.adapters.excludeAspect den.aspects.monitoring.provides.node-exporter inherited; 115 122 116 123 expr = trace "nixos" den.aspects.server; 117 124 # node-exporter tombstone visible, alerting kept ··· 128 135 test-excludeAspect-cascades-to-providers = denTest ( 129 136 { den, trace, ... }: 130 137 { 138 + den.fxPipeline = false; 131 139 den.aspects.monitoring = { 132 140 nixos = { }; 133 141 provides.node-exporter.nixos = { }; ··· 135 143 }; 136 144 den.aspects.server.includes = with den.aspects; [ 137 145 monitoring 138 - monitoring._.node-exporter 139 - monitoring._.alerting 146 + monitoring.provides.node-exporter 147 + monitoring.provides.alerting 140 148 ]; 141 149 den.aspects.server.meta.adapter = 142 150 inherited: den.lib.aspects.adapters.excludeAspect den.aspects.monitoring inherited; ··· 156 164 test-substituteAspect-replaces = denTest ( 157 165 { den, trace, ... }: 158 166 { 167 + den.fxPipeline = false; 159 168 den.aspects.foo.includes = [ 160 169 den.aspects.bar 161 170 den.aspects.baz ··· 181 190 test-substituteAspect-build-uses-replacement = denTest ( 182 191 { den, igloo, ... }: 183 192 { 193 + den.fxPipeline = false; 184 194 den.hosts.x86_64-linux.igloo = { }; 185 195 den.aspects.igloo.includes = [ den.aspects.bar ]; 186 196 den.aspects.igloo.meta.adapter = ··· 198 208 test-substituteAspect-propagates = denTest ( 199 209 { den, trace, ... }: 200 210 { 211 + den.fxPipeline = false; 201 212 den.aspects.root.includes = [ den.aspects.role ]; 202 213 den.aspects.root.meta.adapter = 203 214 inherited: den.lib.aspects.adapters.substituteAspect den.aspects.baz den.aspects.qux inherited; ··· 227 238 test-perHost-visible-in-trace = denTest ( 228 239 { den, trace, ... }: 229 240 { 241 + den.fxPipeline = false; 230 242 den.aspects.role.includes = with den.aspects; [ 231 243 leaf 232 244 param
+6 -6
templates/ci/modules/features/auto-parametric.nix
··· 82 82 ]; 83 83 }; 84 84 85 - den.ctx.user.includes = [ den._.mutual-provider ]; 86 - den.aspects.igloo._.to-users.includes = [ den.aspects.strict-helper ]; 85 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 86 + den.aspects.igloo.provides.to-users.includes = [ den.aspects.strict-helper ]; 87 87 88 88 # strict-helper requires exactly { host, user } — since ctx.host only provides 89 89 # { host }, strict-helper is skipped at host level (by exactly semantics). ··· 112 112 test-second-level-helper-owned-config-preserved = denTest ( 113 113 { den, igloo, ... }: 114 114 { 115 - den.ctx.user.includes = [ den._.mutual-provider ]; 115 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 116 116 den.hosts.x86_64-linux.igloo.users.tux = { }; 117 117 118 118 den.aspects.second-with-owned = { ··· 127 127 ]; 128 128 }; 129 129 den.aspects.helper.includes = [ den.aspects.second-with-owned ]; 130 - den.aspects.igloo._.to-users.includes = [ den.aspects.helper ]; 130 + den.aspects.igloo.provides.to-users.includes = [ den.aspects.helper ]; 131 131 132 132 expr = [ 133 133 igloo.networking.hostName ··· 143 143 test-second-provides-helper-owned-config-preserved = denTest ( 144 144 { den, igloo, ... }: 145 145 { 146 - den.ctx.user.includes = [ den._.mutual-provider ]; 146 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 147 147 den.hosts.x86_64-linux.igloo.users.tux = { }; 148 148 149 149 den.aspects.second.provides.with-owned = { ··· 158 158 ]; 159 159 }; 160 160 den.aspects.helper.includes = [ den.aspects.second.provides.with-owned ]; 161 - den.aspects.igloo._.to-users.includes = [ den.aspects.helper ]; 161 + den.aspects.igloo.provides.to-users.includes = [ den.aspects.helper ]; 162 162 163 163 expr = [ 164 164 igloo.networking.hostName
+4 -4
templates/ci/modules/features/batteries/define-user.nix templates/ci/modules/features/define-user.nix
··· 10 10 }: 11 11 { 12 12 den.hosts.x86_64-linux.igloo.users.tux = { }; 13 - den.aspects.tux.includes = [ den._.define-user ]; 13 + den.aspects.tux.includes = [ den.provides.define-user ]; 14 14 expr = igloo.users.users.tux.isNormalUser; 15 15 expected = true; 16 16 } ··· 25 25 }: 26 26 { 27 27 den.hosts.x86_64-linux.igloo.users.tux = { }; 28 - den.ctx.user.includes = [ den._.mutual-provider ]; 29 - den.aspects.igloo._.to-users.includes = [ den._.define-user ]; 28 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 29 + den.aspects.igloo.provides.to-users.includes = [ den.provides.define-user ]; 30 30 expr = igloo.users.users.tux.isNormalUser; 31 31 expected = true; 32 32 } ··· 41 41 }: 42 42 { 43 43 den.hosts.x86_64-linux.igloo.users.tux = { }; 44 - den.default.includes = [ den._.define-user ]; 44 + den.default.includes = [ den.provides.define-user ]; 45 45 expr = igloo.users.users.tux.isNormalUser; 46 46 expected = true; 47 47 }
+4 -2
templates/ci/modules/features/batteries/flake-parts.nix templates/ci/modules/features/flake-parts.nix
··· 9 9 ... 10 10 }: 11 11 { 12 + den.fxPipeline = false; 12 13 den.hosts.x86_64-linux.igloo.users.tux = { }; 13 14 den.default.homeManager.home.stateVersion = "25.11"; 14 15 15 - den.default.includes = [ den._.inputs' ]; 16 + den.default.includes = [ den.provides.inputs' ]; 16 17 den.aspects.igloo.nixos = 17 18 { inputs', ... }: 18 19 { ··· 32 33 ... 33 34 }: 34 35 { 36 + den.fxPipeline = false; 35 37 den.hosts.x86_64-linux.igloo.users.tux = { }; 36 38 den.default.homeManager.home.stateVersion = "25.11"; 37 39 38 - den.default.includes = [ den._.self' ]; 40 + den.default.includes = [ den.provides.self' ]; 39 41 den.aspects.igloo.nixos = 40 42 { self', ... }: 41 43 {
+8 -8
templates/ci/modules/features/batteries/hostname.nix templates/ci/modules/features/hostname.nix
··· 7 7 { 8 8 den.hosts.x86_64-linux.igloo.users.tux = { }; 9 9 10 - den.default.includes = [ den._.hostname ]; 10 + den.default.includes = [ den.provides.hostname ]; 11 11 12 12 expr = igloo.networking.hostName; 13 13 expected = "igloo"; ··· 22 22 users.tux = { }; 23 23 }; 24 24 25 - den.default.includes = [ den._.hostname ]; 25 + den.default.includes = [ den.provides.hostname ]; 26 26 27 27 expr = igloo.networking.hostName; 28 28 expected = "sahara"; ··· 34 34 { 35 35 den.hosts.x86_64-linux.igloo.users.tux = { }; 36 36 37 - den.aspects.igloo.includes = [ den._.hostname ]; 37 + den.aspects.igloo.includes = [ den.provides.hostname ]; 38 38 39 39 expr = igloo.networking.hostName; 40 40 expected = "igloo"; ··· 46 46 { 47 47 den.hosts.x86_64-linux.igloo.users.tux = { }; 48 48 49 - den.aspects.tux.includes = [ den._.hostname ]; 49 + den.aspects.tux.includes = [ den.provides.hostname ]; 50 50 51 51 expr = igloo.networking.hostName; 52 52 expected = "igloo"; ··· 58 58 { 59 59 den.hosts.x86_64-linux.igloo.users.tux = { }; 60 60 61 - # NOTE: foo needs parametric to pass over `{host}` context into den._.hostname 61 + # NOTE: foo needs parametric to pass over `{host}` context into den.provides.hostname 62 62 den.aspects.foo = den.lib.parametric { 63 - includes = [ den._.hostname ]; 63 + includes = [ den.provides.hostname ]; 64 64 }; 65 65 66 66 den.aspects.igloo.includes = [ den.aspects.foo ]; ··· 75 75 { 76 76 den.hosts.x86_64-linux.igloo.users.tux = { }; 77 77 78 - # NOTE: foo needs parametric to pass over `{host}` context into den._.hostname 78 + # NOTE: foo needs parametric to pass over `{host}` context into den.provides.hostname 79 79 den.aspects.foo = den.lib.parametric { 80 - includes = [ den._.hostname ]; 80 + includes = [ den.provides.hostname ]; 81 81 }; 82 82 83 83 den.aspects.tux.includes = [ den.aspects.foo ];
+2 -2
templates/ci/modules/features/batteries/import-tree.nix templates/ci/modules/features/import-tree.nix
··· 7 7 { 8 8 den.hosts.x86_64-linux.rockhopper.users.tux = { }; 9 9 den.default.includes = [ 10 - (den._.import-tree._.host ../../../non-dendritic/hosts) 10 + (den.provides.import-tree.provides.host ../../non-dendritic/hosts) 11 11 ]; 12 12 13 13 expr = config.flake.nixosConfigurations.rockhopper.config.auto-imported; ··· 20 20 { 21 21 den.hosts.x86_64-linux.igloo.users.tux = { }; 22 22 den.aspects.igloo.includes = [ 23 - (den._.import-tree ../../../non-dendritic/no-such-dir) 23 + (den.provides.import-tree ../../non-dendritic/no-such-dir) 24 24 ]; 25 25 26 26 expr = igloo ? auto-imported;
+1 -1
templates/ci/modules/features/batteries/insecure.nix templates/ci/modules/features/insecure.nix
··· 22 22 { 23 23 den.hosts.x86_64-linux.igloo.users.tux = { }; 24 24 den.aspects.igloo = { 25 - includes = [ (den._.insecure [ "hello-1.0.0" ]) ]; 25 + includes = [ (den.provides.insecure [ "hello-1.0.0" ]) ]; 26 26 environment.systemPackages = [ hello ]; 27 27 }; 28 28 expr = igloo.nixpkgs.config.permittedInsecurePackages;
+4 -4
templates/ci/modules/features/batteries/mutual-provider.nix templates/ci/modules/features/mutual-provider.nix
··· 7 7 { 8 8 den.hosts.x86_64-linux.igloo.users.tux = { }; 9 9 10 - den.ctx.user.includes = [ den._.mutual-provider ]; 10 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 11 11 12 12 den.aspects.igloo.provides.tux = den.lib.parametric { 13 13 homeManager.home.shellAliases.g = "git"; ··· 24 24 { 25 25 den.hosts.x86_64-linux.igloo.users.tux = { }; 26 26 27 - den.ctx.user.includes = [ den._.mutual-provider ]; 27 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 28 28 29 29 den.aspects.tux.provides.igloo = den.lib.parametric { 30 30 nixos.boot.crashDump.reservedMemory = "99999M"; ··· 41 41 { 42 42 den.hosts.x86_64-linux.igloo.users.tux = { }; 43 43 44 - den.ctx.user.includes = [ den._.mutual-provider ]; 44 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 45 45 46 46 den.aspects.igloo.provides.tux = den.lib.parametric { 47 47 homeManager.home.keyboard.model = "denboard"; ··· 68 68 { 69 69 den.hosts.x86_64-linux.igloo.users.tux = { }; 70 70 71 - den.ctx.user.includes = [ den._.mutual-provider ]; 71 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 72 72 73 73 den.aspects.igloo.provides.to-users = { 74 74 homeManager.home.keyboard.model = "denboard";
+1 -1
templates/ci/modules/features/batteries/primary-user.nix templates/ci/modules/features/primary-user.nix
··· 10 10 }: 11 11 { 12 12 den.hosts.x86_64-linux.igloo.users.tux = { }; 13 - den.aspects.tux.includes = [ den._.primary-user ]; 13 + den.aspects.tux.includes = [ den.provides.primary-user ]; 14 14 expr = igloo.users.users.tux.extraGroups; 15 15 expected = [ 16 16 "wheel"
+1 -1
templates/ci/modules/features/batteries/tty-autologin.nix templates/ci/modules/features/tty-autologin.nix
··· 6 6 { den, config, ... }: 7 7 { 8 8 den.hosts.x86_64-linux.igloo = { }; 9 - den.aspects.igloo.includes = [ (den._.tty-autologin "root") ]; 9 + den.aspects.igloo.includes = [ (den.provides.tty-autologin "root") ]; 10 10 11 11 expr = config.flake.nixosConfigurations.igloo.config.systemd.services ? "getty@tty1"; 12 12 expected = true;
+3 -3
templates/ci/modules/features/batteries/unfree.nix templates/ci/modules/features/unfree.nix
··· 6 6 { den, igloo, ... }: 7 7 { 8 8 den.hosts.x86_64-linux.igloo.users.tux = { }; 9 - den.aspects.igloo.includes = [ (den._.unfree [ "discord" ]) ]; 9 + den.aspects.igloo.includes = [ (den.provides.unfree [ "discord" ]) ]; 10 10 expr = igloo.nixpkgs.config.allowUnfreePredicate { pname = "discord"; }; 11 11 expected = true; 12 12 } ··· 17 17 { 18 18 den.hosts.x86_64-linux.igloo.users.tux = { }; 19 19 den.default.homeManager.home.stateVersion = "25.11"; 20 - den.aspects.tux.includes = [ (den._.unfree [ "vscode" ]) ]; 20 + den.aspects.tux.includes = [ (den.provides.unfree [ "vscode" ]) ]; 21 21 22 22 expr = tuxHm.nixpkgs.config.allowUnfreePredicate { pname = "vscode"; }; 23 23 expected = true; ··· 28 28 { den, igloo, ... }: 29 29 { 30 30 den.hosts.x86_64-linux.igloo.users.tux = { }; 31 - den.aspects.tux.includes = [ (den._.unfree [ "vscode" ]) ]; 31 + den.aspects.tux.includes = [ (den.provides.unfree [ "vscode" ]) ]; 32 32 33 33 expr = !(igloo.users.users.tux ? unfree); 34 34 expected = true;
+1 -1
templates/ci/modules/features/batteries/user-shell.nix templates/ci/modules/features/user-shell.nix
··· 12 12 { 13 13 den.hosts.x86_64-linux.igloo.users.tux = { }; 14 14 den.default.homeManager.home.stateVersion = "25.11"; 15 - den.aspects.tux.includes = [ (den._.user-shell "fish") ]; 15 + den.aspects.tux.includes = [ (den.provides.user-shell "fish") ]; 16 16 expr = { 17 17 defaultShell = igloo.users.users.tux.shell.pname; 18 18 osFish = igloo.programs.fish.enable;
+8 -2
templates/ci/modules/features/collect-paths.nix
··· 13 13 keys = map toKey paths; 14 14 in 15 15 { 16 + den.fxPipeline = false; 16 17 den.aspects.foo.includes = [ 17 18 den.aspects.bar 18 19 den.aspects.baz ··· 43 44 result = resolve.withAdapter adapters.collectPaths "nixos" den.aspects.alone; 44 45 in 45 46 { 47 + den.fxPipeline = false; 46 48 den.aspects.alone = { }; 47 49 48 50 expr = { ··· 64 66 keys = map (lib.concatStringsSep "/") paths; 65 67 in 66 68 { 69 + den.fxPipeline = false; 67 70 # role (static) includes a perHost parametric aspect; collectPaths 68 71 # should force the functor and include its entry in the path list. 69 72 den.aspects.role.includes = [ ··· 99 102 keys = map (lib.concatStringsSep "/") paths; 100 103 in 101 104 { 105 + den.fxPipeline = false; 102 106 den.aspects.root.includes = [ 103 107 den.aspects.keep 104 108 den.aspects.dropme ··· 127 131 sharedCount = builtins.length (builtins.filter (k: k == "shared") keys); 128 132 in 129 133 { 134 + den.fxPipeline = false; 130 135 # `shared` reached via both `a` and `b`. 131 136 den.aspects.root.includes = [ 132 137 den.aspects.a ··· 149 154 paths = (resolve.withAdapter adapters.collectPaths "nixos" den.aspects.root).paths or [ ]; 150 155 in 151 156 { 152 - den.aspects.root.includes = [ den.aspects.foo._.sub ]; 153 - den.aspects.foo._.sub.nixos = { }; 157 + den.fxPipeline = false; 158 + den.aspects.root.includes = [ den.aspects.foo.provides.sub ]; 159 + den.aspects.foo.provides.sub.nixos = { }; 154 160 155 161 expr = lib.elem [ "foo" "sub" ] paths; 156 162 expected = true;
+4 -4
templates/ci/modules/features/conditional-config.nix
··· 41 41 users.tux.hasBar = true; 42 42 }; 43 43 44 - den.ctx.user.includes = [ den._.mutual-provider ]; 44 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 45 45 46 - den.aspects.igloo._.to-users.includes = [ conditionalAspect ]; 46 + den.aspects.igloo.provides.to-users.includes = [ conditionalAspect ]; 47 47 48 48 expr = igloo.something; 49 49 expected = "was-true"; ··· 70 70 }; 71 71 72 72 den.default.homeManager.home.stateVersion = "25.11"; 73 - den.ctx.user.includes = [ den._.mutual-provider ]; 74 - den.aspects.igloo._.to-users.includes = [ git-for-linux-only ]; 73 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 74 + den.aspects.igloo.provides.to-users.includes = [ git-for-linux-only ]; 75 75 76 76 expr = [ 77 77 tuxHm.programs.git.enable
+2 -2
templates/ci/modules/features/context/apply-non-exact.nix templates/ci/modules/features/apply-non-exact.nix
··· 4 4 { den, funnyNames, ... }: 5 5 { 6 6 den.ctx.foobar.description = "{foo,bar} context"; 7 - den.ctx.foobar._.foobar = 7 + den.ctx.foobar.provides.foobar = 8 8 # use atLeast if you get error: function called with unexpected argument 9 9 den.lib.take.atLeast ( 10 10 { foo, bar }: ··· 31 31 { den, funnyNames, ... }: 32 32 { 33 33 den.ctx.foobar.description = "{foo,bar} context"; 34 - den.ctx.foobar._.foobar = 34 + den.ctx.foobar.provides.foobar = 35 35 # use exactly if you want to restrict to not having more args 36 36 den.lib.take.exactly ( 37 37 { foo, bar }:
+1 -1
templates/ci/modules/features/context/apply.nix templates/ci/modules/features/apply.nix
··· 4 4 { den, funnyNames, ... }: 5 5 { 6 6 den.ctx.foobar.description = "{foo,bar} context"; 7 - den.ctx.foobar._.foobar = 7 + den.ctx.foobar.provides.foobar = 8 8 { foo, bar }: 9 9 { 10 10 funny.names = [
+8 -8
templates/ci/modules/features/context/cross-provider.nix templates/ci/modules/features/cross-provider.nix
··· 11 11 }: 12 12 { 13 13 den.ctx.parent.description = "{x} context"; 14 - den.ctx.parent._.parent = 14 + den.ctx.parent.provides.parent = 15 15 { x }: 16 16 { 17 17 funny.names = [ "parent-${x}" ]; 18 18 }; 19 - den.ctx.parent._.child = 19 + den.ctx.parent.provides.child = 20 20 _: 21 21 { x, y }: 22 22 { ··· 31 31 } 32 32 ]; 33 33 34 - den.ctx.child._.child = 34 + den.ctx.child.provides.child = 35 35 { x, y }: 36 36 { 37 37 funny.names = [ "child-${y}" ]; ··· 55 55 }: 56 56 { 57 57 den.ctx.src.description = "source"; 58 - den.ctx.src._.src = 58 + den.ctx.src.provides.src = 59 59 { x }: 60 60 { 61 61 funny.names = [ x ]; 62 62 }; 63 - den.ctx.src._.dst = 63 + den.ctx.src.provides.dst = 64 64 _: 65 65 { x, i }: 66 66 { ··· 79 79 } 80 80 ]; 81 81 82 - den.ctx.dst._.dst = 82 + den.ctx.dst.provides.dst = 83 83 { x, i }: 84 84 { 85 85 funny.names = [ "dst-${toString i}" ]; ··· 105 105 }: 106 106 { 107 107 den.ctx.src.description = "source without cross-provider"; 108 - den.ctx.src._.src = 108 + den.ctx.src.provides.src = 109 109 { x }: 110 110 { 111 111 funny.names = [ x ]; 112 112 }; 113 113 den.ctx.src.into.dst = { x }: [ { y = x; } ]; 114 114 115 - den.ctx.dst._.dst = 115 + den.ctx.dst.provides.dst = 116 116 { y }: 117 117 { 118 118 funny.names = [ "dst-${y}" ];
+4 -4
templates/ci/modules/features/context/custom-ctx.nix templates/ci/modules/features/custom-ctx.nix
··· 11 11 }: 12 12 { 13 13 den.ctx.greeting.description = "{hello} context"; 14 - den.ctx.greeting._.greeting = 14 + den.ctx.greeting.provides.greeting = 15 15 { hello }: 16 16 { 17 17 funny.names = [ hello ]; 18 18 }; 19 19 den.ctx.greeting.into.shout = { hello }: [ { shout = lib.toUpper hello; } ]; 20 20 21 - den.ctx.shout._.shout = 21 + den.ctx.shout.provides.shout = 22 22 { shout }: 23 23 { 24 24 funny.names = [ shout ]; ··· 36 36 { den, funnyNames, ... }: 37 37 { 38 38 den.ctx.foo.description = "{foo} context"; 39 - den.ctx.foo._.foo = 39 + den.ctx.foo.provides.foo = 40 40 { foo }: 41 41 { 42 42 funny.names = [ foo ]; ··· 64 64 { den, funnyNames, ... }: 65 65 { 66 66 den.ctx.bar.description = "{x} context"; 67 - den.ctx.bar._.bar = 67 + den.ctx.bar.provides.bar = 68 68 { x }: 69 69 { 70 70 funny.names = [ x ];
templates/ci/modules/features/context/den-default.nix templates/ci/modules/features/den-default.nix
+2 -2
templates/ci/modules/features/context/host-propagation.nix templates/ci/modules/features/host-propagation.nix
··· 20 20 { 21 21 22 22 den.hosts.x86_64-linux.igloo.users.tux = { }; 23 - den.ctx.user.includes = [ den._.mutual-provider ]; 23 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 24 24 25 25 den.aspects.igloo.funny.names = [ "host-owned" ]; 26 26 den.aspects.igloo.includes = [ ··· 37 37 } 38 38 )) 39 39 ]; 40 - den.aspects.igloo._.to-users.includes = [ 40 + den.aspects.igloo.provides.to-users.includes = [ 41 41 { funny.names = [ "host-static" ]; } 42 42 43 43 (
+7 -7
templates/ci/modules/features/context/named-provider.nix templates/ci/modules/features/named-provider.nix
··· 6 6 { den, funnyNames, ... }: 7 7 { 8 8 den.ctx.greet.description = "{who} context"; 9 - den.ctx.greet._.greet = 9 + den.ctx.greet.provides.greet = 10 10 { who }: 11 11 { 12 12 funny.names = [ "hello-${who}" ]; ··· 21 21 { den, funnyNames, ... }: 22 22 { 23 23 den.ctx.greet.description = "{who} context"; 24 - den.ctx.greet._.greet = 24 + den.ctx.greet.provides.greet = 25 25 { who }: 26 26 { 27 27 funny.names = [ "hello-${who}" ]; ··· 44 44 ... 45 45 }: 46 46 { 47 - den.ctx.greet._.greet = 47 + den.ctx.greet.provides.greet = 48 48 { who }: 49 49 { 50 50 funny.names = [ "hello-${who}" ]; ··· 75 75 }: 76 76 { 77 77 den.ctx.greet.description = "{who} context"; 78 - den.ctx.greet._.greet = 78 + den.ctx.greet.provides.greet = 79 79 { who }: 80 80 { 81 81 funny.names = [ who ]; 82 82 }; 83 83 den.ctx.greet.into.yell = { who }: [ { shout = lib.toUpper who; } ]; 84 84 85 - den.ctx.yell._.yell = 85 + den.ctx.yell.provides.yell = 86 86 { shout }: 87 87 { 88 88 funny.names = [ shout ]; ··· 105 105 }: 106 106 { 107 107 den.ctx.greet.description = "{who} context"; 108 - den.ctx.greet._.greet = 108 + den.ctx.greet.provides.greet = 109 109 { who }: 110 110 { 111 111 funny.names = [ who ]; ··· 118 118 num = [ { number = lib.stringLength who; } ]; 119 119 }; 120 120 121 - den.ctx.yell._.yell = 121 + den.ctx.yell.provides.yell = 122 122 { shout }: 123 123 { 124 124 funny.names = [ shout ];
+4 -4
templates/ci/modules/features/context/nested-ctx-providers.nix templates/ci/modules/features/nested-ctx-providers.nix
··· 7 7 test-nested-cross-provider = denTest ( 8 8 { den, funnyNames, ... }: 9 9 { 10 - den.ctx.ns.inner._.inner = 10 + den.ctx.ns.inner.provides.inner = 11 11 { z }: 12 12 { 13 13 funny.names = [ "inner-${z}" ]; ··· 38 38 test-no-cross-provider-collision = denTest ( 39 39 { den, funnyNames, ... }: 40 40 { 41 - den.ctx.a.leaf._.leaf = 41 + den.ctx.a.leaf.provides.leaf = 42 42 { v }: 43 43 { 44 44 funny.names = [ "a-${v}" ]; 45 45 }; 46 - den.ctx.b.leaf._.leaf = 46 + den.ctx.b.leaf.provides.leaf = 47 47 { v }: 48 48 { 49 49 funny.names = [ "b-${v}" ]; ··· 74 74 test-nested-attrset-into = denTest ( 75 75 { den, funnyNames, ... }: 76 76 { 77 - den.ctx.ns.inner._.inner = 77 + den.ctx.ns.inner.provides.inner = 78 78 { z }: 79 79 { 80 80 funny.names = [ "inner-${z}" ];
+9 -9
templates/ci/modules/features/context/nested-ctx.nix templates/ci/modules/features/nested-ctx.nix
··· 5 5 test-two-level-nesting = denTest ( 6 6 { den, funnyNames, ... }: 7 7 { 8 - den.ctx.ns.inner._.inner = 8 + den.ctx.ns.inner.provides.inner = 9 9 { z }: 10 10 { 11 11 funny.names = [ "inner-${z}" ]; 12 12 }; 13 13 14 - den.ctx.root._.root = 14 + den.ctx.root.provides.root = 15 15 { v }: 16 16 { 17 17 funny.names = [ v ]; ··· 33 33 test-three-level-nesting = denTest ( 34 34 { den, funnyNames, ... }: 35 35 { 36 - den.ctx.a.b.c._.c = 36 + den.ctx.a.b.c.provides.c = 37 37 { z }: 38 38 { 39 39 funny.names = [ "abc-${z}" ]; ··· 53 53 test-dedup-by-full-path = denTest ( 54 54 { den, funnyNames, ... }: 55 55 { 56 - den.ctx.a.leaf._.leaf = 56 + den.ctx.a.leaf.provides.leaf = 57 57 { v }: 58 58 { 59 59 funny.names = [ "a-${v}" ]; 60 60 }; 61 - den.ctx.b.leaf._.leaf = 61 + den.ctx.b.leaf.provides.leaf = 62 62 { v }: 63 63 { 64 64 funny.names = [ "b-${v}" ]; ··· 80 80 test-flat-still-works = denTest ( 81 81 { den, funnyNames, ... }: 82 82 { 83 - den.ctx.flat._.flat = 83 + den.ctx.flat.provides.flat = 84 84 { x }: 85 85 { 86 86 funny.names = [ x ]; ··· 96 96 test-into-root-and-child-merge = denTest ( 97 97 { den, funnyNames, ... }: 98 98 { 99 - den.ctx.leaf._.leaf = 99 + den.ctx.leaf.provides.leaf = 100 100 { v }: 101 101 { 102 102 funny.names = [ v ]; ··· 137 137 test-into-mixed-flat-and-nested = denTest ( 138 138 { den, funnyNames, ... }: 139 139 { 140 - den.ctx.ns.deep._.deep = 140 + den.ctx.ns.deep.provides.deep = 141 141 { k }: 142 142 { 143 143 funny.names = [ "deep-${k}" ]; 144 144 }; 145 - den.ctx.flat._.flat = 145 + den.ctx.flat.provides.flat = 146 146 { k }: 147 147 { 148 148 funny.names = [ "flat-${k}" ];
+6 -5
templates/ci/modules/features/cross-context-forward.nix
··· 70 70 den.aspects.iceberg.includes = [ 71 71 ( 72 72 { host }: 73 - den._.forward { 73 + den.provides.forward { 74 74 each = lib.filter (h: h != host) (lib.attrValues den.hosts.${host.system}); 75 75 fromClass = _: "ssh-host-key"; 76 76 intoClass = _: host.class; ··· 96 96 den.aspects.iceberg.includes = [ 97 97 ( 98 98 { host }: 99 - den._.forward { 99 + den.provides.forward { 100 100 each = lib.filter (h: h != host) (lib.attrValues den.hosts.${host.system}); 101 101 fromClass = _: "test-class"; 102 102 intoClass = _: host.class; ··· 119 119 test-cross-context-adapter-data-collection = denTest ( 120 120 { den, iceberg, ... }: 121 121 { 122 + den.fxPipeline = false; 122 123 den.hosts.x86_64-linux.igloo = { }; 123 124 den.hosts.x86_64-linux.iceberg = { }; 124 125 ··· 185 186 (lib.attrValues host.users); 186 187 in 187 188 lib.optionalAttrs (primaryUser != null) ( 188 - den._.forward { 189 + den.provides.forward { 189 190 each = lib.singleton host; 190 191 fromAspect = h: den.lib.parametric.fixedTo { host = h; } h.aspect; 191 192 fromClass = _: "homeManager"; ··· 224 225 igloo = den.hosts.x86_64-linux.igloo; 225 226 user = lib.head (lib.attrValues host.users); 226 227 in 227 - den._.forward { 228 + den.provides.forward { 228 229 each = lib.singleton igloo; 229 230 fromClass = _: "homeManager"; 230 231 intoClass = _: host.class; ··· 261 262 den.aspects.iceberg.includes = [ 262 263 ( 263 264 { host }: 264 - den._.forward { 265 + den.provides.forward { 265 266 each = lib.singleton den.hosts.x86_64-linux.igloo; 266 267 fromClass = _: "host-identity"; 267 268 intoClass = _: host.class;
+3 -2
templates/ci/modules/features/deadbugs/cybolic-routes.nix
··· 12 12 ... 13 13 }: 14 14 { 15 + den.fxPipeline = false; 15 16 den.default.homeManager.home.stateVersion = "25.11"; 16 17 den.hosts.x86_64-linux.igloo.users.tux = { }; 17 18 18 19 den.aspects.routes = 19 20 let 20 21 inherit (den.lib) parametric; 21 - # eg, `<user>._.<host>` and `<host>._.<user>` 22 - mutual = from: to: from.aspect._.${to.aspect.name} or { }; 22 + # eg, `<user>.provides.<host>` and `<host>.provides.<user>` 23 + mutual = from: to: from.aspect.provides.${to.aspect.name} or { }; 23 24 24 25 routes = 25 26 { host, user, ... }@ctx:
+5 -5
templates/ci/modules/features/deadbugs/external-namespace-deep-aspect.nix
··· 26 26 inputs.provider 27 27 ]) 28 28 ]; 29 - expr = provider.tools._.dev ? _; 29 + expr = provider.tools.provides.dev ? _; 30 30 expected = true; 31 31 } 32 32 ); ··· 45 45 ]) 46 46 ]; 47 47 den.hosts.x86_64-linux.igloo.users.tux = { }; 48 - den.aspects.igloo.includes = [ provider.tools._.dev._.editors ]; 48 + den.aspects.igloo.includes = [ provider.tools.provides.dev.provides.editors ]; 49 49 expr = igloo.programs.vim.enable; 50 50 expected = true; 51 51 } ··· 65 65 ]) 66 66 ]; 67 67 den.hosts.x86_64-linux.igloo.users.tux = { }; 68 - den.aspects.igloo.includes = [ provider.tools._.dev._.host-stamp ]; 68 + den.aspects.igloo.includes = [ provider.tools.provides.dev.provides.host-stamp ]; 69 69 expr = igloo.environment.sessionVariables.PROVIDER_HOST; 70 70 expected = "igloo"; 71 71 } ··· 86 86 ]) 87 87 ]; 88 88 den.hosts.x86_64-linux.igloo.users.tux = { }; 89 - den.aspects.igloo._.to-users.includes = [ provider.tools._.dev._.user-stamp ]; 90 - den.ctx.user.includes = [ den._.mutual-provider ]; 89 + den.aspects.igloo.provides.to-users.includes = [ provider.tools.provides.dev.provides.user-stamp ]; 90 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 91 91 expr = igloo.users.users.tux.description; 92 92 expected = "user-of-igloo"; 93 93 }
+4 -4
templates/ci/modules/features/deadbugs/issue-201-forward-multiple-users.nix
··· 13 13 }: 14 14 { 15 15 den.default.homeManager.home.stateVersion = "25.11"; 16 - den.ctx.user.includes = [ den._.mutual-provider ]; 16 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 17 17 18 18 den.hosts.x86_64-linux.igloo.users = { 19 19 tux = { }; 20 20 pingu = { }; 21 21 }; 22 22 23 - den.aspects.igloo._.to-users.includes = [ 24 - den._.define-user 23 + den.aspects.igloo.provides.to-users.includes = [ 24 + den.provides.define-user 25 25 den.aspects.set-user-desc 26 26 ]; 27 - den.aspects.tux.includes = [ den._.primary-user ]; 27 + den.aspects.tux.includes = [ den.provides.primary-user ]; 28 28 29 29 den.aspects.set-user-desc = 30 30 { host, user }:
+3
templates/ci/modules/features/deadbugs/issue-254-ctx-hm-user-includes.nix
··· 12 12 ... 13 13 }: 14 14 { 15 + den.fxPipeline = false; 15 16 den.default.homeManager.home.stateVersion = "25.11"; 16 17 den.hosts.x86_64-linux.igloo.users.tux = { }; 17 18 ··· 44 45 ... 45 46 }: 46 47 { 48 + den.fxPipeline = false; 47 49 den.default.homeManager.home.stateVersion = "25.11"; 48 50 den.hosts.x86_64-linux.igloo.users.tux = { }; 49 51 ··· 98 100 ... 99 101 }: 100 102 { 103 + den.fxPipeline = false; 101 104 den.hosts.x86_64-linux.igloo.users.tux.classes = [ "hjem" ]; 102 105 103 106 # hijack a minimal module so hostConf doesn't complain about missing
+1
templates/ci/modules/features/deadbugs/issue-261-parametric-aspect-from-remote-namespace.nix
··· 34 34 }).config.flake; 35 35 in 36 36 { 37 + den.fxPipeline = false; 37 38 imports = [ (inputs.den.namespace "remote" input) ]; 38 39 39 40 den.aspects.local.includes = [
+1
templates/ci/modules/features/deadbugs/issue-292-hm-used-when-no-mutual-enabled.nix
··· 11 11 ... 12 12 }: 13 13 { 14 + den.fxPipeline = false; 14 15 den.hosts.x86_64-linux.igloo.users.tux = { }; 15 16 16 17 den.aspects.igloo.includes = [ den.aspects.bash ];
+11 -7
templates/ci/modules/features/deadbugs/issue-297-mutual-not-including-host-owned-and-included-statics.nix
··· 11 11 ... 12 12 }: 13 13 { 14 + den.fxPipeline = false; 14 15 den.hosts.x86_64-linux.igloo.users.tux.classes = [ "homeManager" ]; 15 - den.ctx.user.includes = [ den._.mutual-provider ]; 16 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 16 17 17 - den.aspects.igloo._.to-users.homeManager.home.keyboard.model = "denkbd"; 18 + den.aspects.igloo.provides.to-users.homeManager.home.keyboard.model = "denkbd"; 18 19 19 20 expr = tuxHm.home.keyboard.model; 20 21 expected = "denkbd"; ··· 30 31 ... 31 32 }: 32 33 { 34 + den.fxPipeline = false; 33 35 den.hosts.x86_64-linux.igloo.users.tux.classes = [ "homeManager" ]; 34 - den.ctx.user.includes = [ den._.mutual-provider ]; 36 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 35 37 36 38 den.aspects.base.homeManager.home.keyboard.model = "denkbd"; 37 - den.aspects.igloo._.to-users.includes = [ den.aspects.base ]; 39 + den.aspects.igloo.provides.to-users.includes = [ den.aspects.base ]; 38 40 39 41 expr = tuxHm.home.keyboard.model; 40 42 expected = "denkbd"; ··· 50 52 ... 51 53 }: 52 54 { 55 + den.fxPipeline = false; 53 56 den.hosts.x86_64-linux.igloo.users.tux.classes = [ "homeManager" ]; 54 - den.ctx.user.includes = [ den._.mutual-provider ]; 57 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 55 58 56 - den.aspects.igloo._.to-users.homeManager.options.foo = lib.mkOption { default = "foo"; }; 59 + den.aspects.igloo.provides.to-users.homeManager.options.foo = lib.mkOption { default = "foo"; }; 57 60 58 61 expr = tuxHm.foo; 59 62 expected = "foo"; ··· 69 72 ... 70 73 }: 71 74 { 75 + den.fxPipeline = false; 72 76 den.hosts.x86_64-linux.igloo.users.tux.classes = [ "homeManager" ]; 73 - den.ctx.user.includes = [ den._.mutual-provider ]; 77 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 74 78 75 79 # NOTE: this causes an error: Option already defined! 76 80 # This is because mutuality includes host configs again.
+1
templates/ci/modules/features/deadbugs/issue-311-nested-includes-are-parametric.nix
··· 10 10 ... 11 11 }: 12 12 { 13 + den.fxPipeline = false; 13 14 den.hosts.x86_64-linux.igloo.users.tux = { }; 14 15 15 16 den.aspects.tux.includes = [
+4 -4
templates/ci/modules/features/deadbugs/issue-352-exported-providers-function-arg-reflection.nix
··· 10 10 (inputs.den.namespace "test" true) 11 11 ]; 12 12 13 - test.aspect._.host = 13 + test.aspect.provides.host = 14 14 { host, ... }: 15 15 { 16 16 nixos.environment.sessionVariables.TEST_HOST = host.name; ··· 34 34 { 35 35 imports = [ internal ]; 36 36 37 - expr = lib.functionArgs test.aspect._.host; 37 + expr = lib.functionArgs test.aspect.provides.host; 38 38 expected = { 39 39 host = false; 40 40 }; ··· 49 49 ... 50 50 }: 51 51 { 52 - expr = lib.functionArgs (external inputs).denful.test.aspect._.host; 52 + expr = lib.functionArgs (external inputs).denful.test.aspect.provides.host; 53 53 expected = { 54 54 host = false; 55 55 }; ··· 67 67 { 68 68 imports = [ (inputs.den.namespace "test" [ (external inputs) ]) ]; 69 69 70 - expr = lib.functionArgs test.aspect._.host; 70 + expr = lib.functionArgs test.aspect.provides.host; 71 71 expected = { 72 72 host = false; 73 73 };
+7 -4
templates/ci/modules/features/deadbugs/issue-369-namespace-system-scoped-inputs.nix
··· 11 11 ... 12 12 }: 13 13 { 14 + den.fxPipeline = false; # CRASHES with fx 14 15 den.hosts.x86_64-linux.igloo.users.tux = { }; 15 16 den.default.homeManager.home.stateVersion = "25.11"; 16 17 17 - den.default.includes = [ den._.self' ]; 18 + den.default.includes = [ den.provides.self' ]; 18 19 den.aspects.tux.includes = [ den.aspects.hola ]; 19 20 20 21 den.aspects.hola.homeManager = ··· 37 38 ... 38 39 }: 39 40 { 41 + den.fxPipeline = false; # CRASHES with fx 40 42 den.hosts.x86_64-linux.igloo.users.tux = { }; 41 43 den.default.homeManager.home.stateVersion = "25.11"; 42 44 43 - den.default.includes = [ den._.inputs' ]; 45 + den.default.includes = [ den.provides.inputs' ]; 44 46 den.aspects.tux.includes = [ den.aspects.hola ]; 45 47 46 48 den.aspects.hola.homeManager = ··· 69 71 imports = [ (inputs.den.namespace "gloom" false) ]; 70 72 _module.args.__findFile = den.lib.__findFile; 71 73 74 + den.fxPipeline = false; # CRASHES with fx 72 75 den.hosts.x86_64-linux.igloo.users.tux = { }; 73 76 den.default.homeManager.home.stateVersion = "25.11"; 74 77 75 78 den.default.includes = [ 76 - den._.inputs' 77 - den._.define-user 79 + den.provides.inputs' 80 + den.provides.define-user 78 81 ]; 79 82 den.aspects.tux.includes = [ gloom.everywhere ]; 80 83 gloom.everywhere.includes = [ <gloom/apps/helix> ];
+6 -6
templates/ci/modules/features/deadbugs/issue-400-namespace-include-duplication.nix
··· 16 16 nixos.test = [ "aspect-${host.name}" ]; 17 17 }; 18 18 19 - test.provided._.provider = 19 + test.provided.provides.provider = 20 20 { host }: 21 21 { 22 22 nixos.test = [ "provider-${host.name}" ]; 23 23 }; 24 24 25 - test.provided._.included = { 25 + test.provided.provides.included = { 26 26 includes = [ 27 27 ( 28 28 { host }: ··· 61 61 62 62 den.ctx.host.includes = [ 63 63 test.aspect 64 - test.provided._.provider 65 - test.provided._.included 64 + test.provided.provides.provider 65 + test.provided.provides.included 66 66 ]; 67 67 68 68 expr = igloo.test; ··· 91 91 92 92 den.ctx.host.includes = [ 93 93 test.aspect 94 - test.provided._.provider 95 - test.provided._.included 94 + test.provided.provides.provider 95 + test.provided.provides.included 96 96 ]; 97 97 98 98 expr = igloo.test;
+5 -5
templates/ci/modules/features/deadbugs/issue-413-provider-bare-function.nix
··· 14 14 den.aspects.foo = 15 15 { host, ... }: 16 16 { 17 - includes = lib.optionals (host.foo._.sub.enable == true) [ 18 - den.aspects.foo._.sub 17 + includes = lib.optionals (host.foo.provides.sub.enable == true) [ 18 + den.aspects.foo.provides.sub 19 19 ]; 20 20 }; 21 21 }; 22 22 b = { 23 - den.schema.host.options.foo._.sub.enable = lib.mkEnableOption "sub-aspect toggle"; 23 + den.schema.host.options.foo.provides.sub.enable = lib.mkEnableOption "sub-aspect toggle"; 24 24 }; 25 25 c = { 26 - den.hosts.x86_64-linux.igloo.foo._.sub.enable = true; 26 + den.hosts.x86_64-linux.igloo.foo.provides.sub.enable = true; 27 27 }; 28 28 d = { 29 - den.aspects.foo._.sub = 29 + den.aspects.foo.provides.sub = 30 30 { host, ... }: 31 31 { 32 32 nixos = lib.optionalAttrs (host.hostName != "whatever") {
+2 -2
templates/ci/modules/features/deadbugs/issue-413-provider-sub-aspect-function.nix
··· 13 13 den.aspects.foo = 14 14 { host, ... }: 15 15 { 16 - includes = [ den.aspects.foo._.sub ]; 16 + includes = [ den.aspects.foo.provides.sub ]; 17 17 }; 18 18 } 19 19 { 20 - den.aspects.foo._.sub = 20 + den.aspects.foo.provides.sub = 21 21 { host, ... }: 22 22 { 23 23 nixos.networking.networkmanager.enable = true;
+3 -2
templates/ci/modules/features/deadbugs/issue-423-static-sub-aspect-parametric-parent.nix
··· 9 9 test-static-sub-aspect-from-parametric-parent = denTest ( 10 10 { den, igloo, ... }: 11 11 { 12 + den.fxPipeline = false; 12 13 den.hosts.x86_64-linux.igloo.users.tux = { }; 13 14 14 15 # Split across modules so the parametric parent and the static sub ··· 18 19 den.aspects.role = 19 20 { host, ... }: 20 21 { 21 - includes = [ den.aspects.role._.sub ]; 22 + includes = [ den.aspects.role.provides.sub ]; 22 23 }; 23 24 } 24 25 { 25 - den.aspects.role._.sub.nixos.networking.networkmanager.enable = true; 26 + den.aspects.role.provides.sub.nixos.networking.networkmanager.enable = true; 26 27 } 27 28 { 28 29 den.aspects.igloo.includes = [ den.aspects.role ];
+2
templates/ci/modules/features/deadbugs/issue-442-parametric-included-by-parametric.nix
··· 13 13 test-parametric-aspect-included-by-parametric-aspect = denTest ( 14 14 { den, igloo, ... }: 15 15 { 16 + den.fxPipeline = false; 16 17 den.hosts.x86_64-linux.igloo.users.tux = { }; 17 18 18 19 den.aspects.git = ··· 53 54 ... 54 55 }: 55 56 { 57 + den.fxPipeline = false; 56 58 den.hosts.x86_64-linux.igloo.users.tux = { }; 57 59 58 60 den.aspects.shell =
+2
templates/ci/modules/features/deadbugs/static-include-dup-package.nix
··· 11 11 ... 12 12 }: 13 13 { 14 + den.fxPipeline = false; 14 15 den.default.homeManager.home.stateVersion = "25.11"; 15 16 16 17 den.hosts.x86_64-linux.igloo.users.tux = { }; ··· 39 40 ... 40 41 }: 41 42 { 43 + den.fxPipeline = false; 42 44 den.default.homeManager.home.stateVersion = "25.11"; 43 45 44 46 den.hosts.x86_64-linux.igloo.users.tux = { };
+3 -1
templates/ci/modules/features/default-includes.nix
··· 27 27 { den, igloo, ... }: 28 28 { 29 29 den.hosts.x86_64-linux.igloo.users.tux = { }; 30 - den.default.includes = [ den._.hostname ]; 30 + den.default.includes = [ den.provides.hostname ]; 31 31 32 32 expr = igloo.networking.hostName; 33 33 expected = "igloo"; ··· 42 42 ... 43 43 }: 44 44 { 45 + den.fxPipeline = false; 45 46 den.hosts.x86_64-linux.igloo.users = { 46 47 tux = { }; 47 48 pingu = { }; ··· 63 64 test-dynamic-class-in-user-host-context = denTest ( 64 65 { den, igloo, ... }: 65 66 { 67 + den.fxPipeline = false; 66 68 den.hosts.x86_64-linux.igloo.users.tux = { }; 67 69 den.default.includes = [ 68 70 (
+2 -2
templates/ci/modules/features/dynamic-intopath.nix
··· 33 33 34 34 forwarded = 35 35 { class, aspect-chain }: 36 - den._.forward { 36 + den.provides.forward { 37 37 each = lib.singleton class; 38 38 fromClass = _: "src"; 39 39 intoClass = _: "nixos"; ··· 92 92 93 93 forwarded = 94 94 { class, aspect-chain }: 95 - den._.forward { 95 + den.provides.forward { 96 96 each = lib.singleton class; 97 97 fromClass = _: "src"; 98 98 intoClass = _: "homeManager";
+3 -3
templates/ci/modules/features/forward-alias-class.nix
··· 13 13 let 14 14 forwarded = 15 15 { class, aspect-chain }: 16 - den._.forward { 16 + den.provides.forward { 17 17 each = lib.singleton class; 18 18 fromClass = _: "home"; 19 19 intoClass = _: "homeManager"; ··· 82 82 let 83 83 forwarded = 84 84 { class, aspect-chain }: 85 - den._.forward { 85 + den.provides.forward { 86 86 each = lib.singleton class; 87 87 fromClass = _: "home"; 88 88 intoClass = _: "homeManager"; ··· 134 134 let 135 135 forwarded = 136 136 { class, aspect-chain }: 137 - den._.forward { 137 + den.provides.forward { 138 138 each = [ 139 139 "Linux" 140 140 "Darwin"
+2 -2
templates/ci/modules/features/forward-flake-level.nix
··· 10 10 ... 11 11 }: 12 12 let 13 - fwd = den._.forward { 13 + fwd = den.provides.forward { 14 14 each = [ { name = "moo"; } ]; 15 15 fromClass = item: "goofy"; 16 16 intoClass = _: "flake"; ··· 35 35 in 36 36 { 37 37 38 - den.ctx.foo._.foo = { name }: den.aspects.${name}; 38 + den.ctx.foo.provides.foo = { name }: den.aspects.${name}; 39 39 40 40 den.aspects.moo = { 41 41 goofy.names = [ "hello" ];
+5 -5
templates/ci/modules/features/forward-from-custom-class.nix
··· 12 12 let 13 13 forwarded = 14 14 { class, aspect-chain }: 15 - den._.forward { 15 + den.provides.forward { 16 16 each = lib.singleton class; 17 17 fromClass = _: "custom"; 18 18 intoClass = _: "nixos"; ··· 46 46 47 47 forwarded = 48 48 { class, aspect-chain }: 49 - den._.forward { 49 + den.provides.forward { 50 50 each = lib.singleton class; 51 51 fromClass = _: "src"; 52 52 intoClass = _: "nixos"; ··· 83 83 let 84 84 forwarded = 85 85 { class, aspect-chain }: 86 - den._.forward { 86 + den.provides.forward { 87 87 each = lib.singleton class; 88 88 fromClass = _: "git"; 89 89 intoClass = _: "homeManager"; ··· 124 124 let 125 125 forwarded = 126 126 { class, aspect-chain }: 127 - den._.forward { 127 + den.provides.forward { 128 128 each = [ 129 129 "nixos" 130 130 "homeManager" ··· 173 173 forwarded = 174 174 { host, user }: 175 175 { class, aspect-chain }: 176 - den._.forward { 176 + den.provides.forward { 177 177 each = lib.optional (lib.elem host.name [ 178 178 "igloo" 179 179 "iceberg"
+330
templates/ci/modules/features/fx-adapter-integration.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + let 8 + # Run the unified pipeline for a given aspect. 9 + runPipeline = 10 + den: 11 + { 12 + ctx ? { }, 13 + class ? "nixos", 14 + }: 15 + aspect: 16 + let 17 + fx = den.lib.fx; 18 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 19 + in 20 + fx.handle { 21 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { inherit ctx class; }; 22 + state = den.lib.aspects.fx.pipeline.defaultState; 23 + } comp; 24 + in 25 + { 26 + flake.tests.fx-adapter-integration = { 27 + 28 + # Default pipeline collects class modules from tree. 29 + test-default-pipeline-collects-modules = denTest ( 30 + { den, ... }: 31 + let 32 + root = { 33 + name = "root"; 34 + meta = { }; 35 + nixos = { 36 + a = 1; 37 + }; 38 + includes = [ 39 + { 40 + name = "child1"; 41 + meta = { }; 42 + nixos = { 43 + b = 2; 44 + }; 45 + includes = [ ]; 46 + } 47 + { 48 + name = "child2"; 49 + meta = { }; 50 + nixos = { 51 + c = 3; 52 + }; 53 + includes = [ ]; 54 + } 55 + ]; 56 + }; 57 + result = runPipeline den { } root; 58 + in 59 + { 60 + expr = builtins.length (result.state.imports null); 61 + expected = 3; # root + child1 + child2 (emit-class fires for each) 62 + } 63 + ); 64 + 65 + # exclude through full pipeline: tombstoned aspect's module not collected. 66 + test-exclude-through-pipeline = denTest ( 67 + { den, ... }: 68 + let 69 + target = { 70 + name = "drop"; 71 + meta = { 72 + provider = [ ]; 73 + }; 74 + }; 75 + root = { 76 + name = "root"; 77 + meta = { 78 + handleWith = den.lib.aspects.fx.constraints.exclude target; 79 + }; 80 + includes = [ 81 + { 82 + name = "keep"; 83 + meta = { 84 + provider = [ ]; 85 + }; 86 + nixos = { 87 + a = 1; 88 + }; 89 + includes = [ ]; 90 + } 91 + { 92 + name = "drop"; 93 + meta = { 94 + provider = [ ]; 95 + }; 96 + nixos = { 97 + b = 2; 98 + }; 99 + includes = [ ]; 100 + } 101 + ]; 102 + }; 103 + result = runPipeline den { } root; 104 + in 105 + { 106 + expr = builtins.length (result.state.imports null); 107 + expected = 1; 108 + } 109 + ); 110 + 111 + # substitute through full pipeline. 112 + test-substitute-through-pipeline = denTest ( 113 + { den, ... }: 114 + let 115 + old = { 116 + name = "old"; 117 + meta = { 118 + provider = [ ]; 119 + }; 120 + }; 121 + new = { 122 + name = "new"; 123 + meta = { 124 + provider = [ ]; 125 + }; 126 + nixos = { 127 + replaced = true; 128 + }; 129 + includes = [ ]; 130 + }; 131 + root = { 132 + name = "root"; 133 + meta = { 134 + handleWith = den.lib.aspects.fx.constraints.substitute old new; 135 + }; 136 + includes = [ 137 + { 138 + name = "old"; 139 + meta = { 140 + provider = [ ]; 141 + }; 142 + nixos = { 143 + original = true; 144 + }; 145 + includes = [ ]; 146 + } 147 + ]; 148 + }; 149 + result = runPipeline den { } root; 150 + tree = result.value; 151 + children = tree.includes or [ ]; 152 + names = map (c: c.name or "?") children; 153 + in 154 + { 155 + # Tombstone (~old) + replacement (new) both in tree, only new's module collected. 156 + expr = { 157 + importCount = builtins.length (result.state.imports null); 158 + hasTombstone = builtins.any (n: n == "~old") names; 159 + hasReplacement = builtins.any (n: n == "new") names; 160 + }; 161 + expected = { 162 + importCount = 1; 163 + hasTombstone = true; 164 + hasReplacement = true; 165 + }; 166 + } 167 + ); 168 + 169 + # includeIf with hasAspect through full pipeline. 170 + test-includeIf-through-pipeline = denTest ( 171 + { den, ... }: 172 + let 173 + sops = { 174 + name = "sops"; 175 + meta = { 176 + provider = [ ]; 177 + }; 178 + includes = [ ]; 179 + }; 180 + sopsConf = { 181 + name = "sops-conf"; 182 + meta = { 183 + provider = [ ]; 184 + }; 185 + nixos = { 186 + sops = true; 187 + }; 188 + includes = [ ]; 189 + }; 190 + root = { 191 + name = "root"; 192 + meta = { }; 193 + includes = [ 194 + sops 195 + (den.lib.aspects.fx.includes.includeIf (ctx: ctx.hasAspect sops) [ sopsConf ]) 196 + ]; 197 + }; 198 + result = runPipeline den { } root; 199 + in 200 + { 201 + expr = builtins.length (result.state.imports null); 202 + expected = 1; # sopsConf.nixos 203 + } 204 + ); 205 + 206 + # Context root adapter: adapter on root applies to all descendants. 207 + test-context-root-adapter = denTest ( 208 + { den, ... }: 209 + let 210 + wayland = { 211 + name = "wayland"; 212 + meta = { 213 + provider = [ ]; 214 + }; 215 + }; 216 + root = { 217 + name = "igloo"; 218 + meta = { 219 + handleWith = den.lib.aspects.fx.constraints.exclude wayland; 220 + }; 221 + includes = [ 222 + { 223 + name = "desktop"; 224 + meta = { 225 + provider = [ ]; 226 + }; 227 + includes = [ 228 + { 229 + name = "wayland"; 230 + meta = { 231 + provider = [ ]; 232 + }; 233 + nixos = { 234 + wl = true; 235 + }; 236 + includes = [ ]; 237 + } 238 + { 239 + name = "x11"; 240 + meta = { 241 + provider = [ ]; 242 + }; 243 + nixos = { 244 + x = true; 245 + }; 246 + includes = [ ]; 247 + } 248 + ]; 249 + } 250 + ]; 251 + }; 252 + result = runPipeline den { } root; 253 + in 254 + { 255 + expr = builtins.length (result.state.imports null); 256 + expected = 1; # only x11.nixos 257 + } 258 + ); 259 + 260 + # Parametric aspect + adapter through pipeline. 261 + test-parametric-with-adapter = denTest ( 262 + { den, ... }: 263 + let 264 + target = { 265 + name = "skip"; 266 + meta = { 267 + provider = [ ]; 268 + }; 269 + }; 270 + root = { 271 + name = "root"; 272 + meta = { 273 + handleWith = den.lib.aspects.fx.constraints.exclude target; 274 + }; 275 + includes = [ 276 + { 277 + name = "web"; 278 + meta = { 279 + provider = [ ]; 280 + }; 281 + __functor = 282 + _: 283 + { host }: 284 + { 285 + nixos.hostName = host; 286 + includes = [ 287 + { 288 + name = "skip"; 289 + meta = { 290 + provider = [ ]; 291 + }; 292 + nixos = { 293 + x = 1; 294 + }; 295 + includes = [ ]; 296 + } 297 + { 298 + name = "keep"; 299 + meta = { 300 + provider = [ ]; 301 + }; 302 + nixos = { 303 + y = 2; 304 + }; 305 + includes = [ ]; 306 + } 307 + ]; 308 + }; 309 + __functionArgs = { 310 + host = false; 311 + }; 312 + includes = [ ]; 313 + } 314 + ]; 315 + }; 316 + result = runPipeline den { 317 + ctx = { 318 + host = "igloo"; 319 + }; 320 + } root; 321 + in 322 + { 323 + # web.nixos (hostName) + keep.nixos (y), skip is tombstoned 324 + expr = builtins.length (result.state.imports null); 325 + expected = 2; 326 + } 327 + ); 328 + 329 + }; 330 + }
+290
templates/ci/modules/features/fx-aspect.nix
··· 1 + # Tests for den's aspectToEffect — the aspect compiler. 2 + { 3 + denTest, 4 + inputs, 5 + lib, 6 + ... 7 + }: 8 + let 9 + # Test handler set that collects emitted effects. 10 + collectHandlers = { 11 + "emit-class" = 12 + { param, state }: 13 + { 14 + resume = null; 15 + state = state // { 16 + classes = (state.classes or [ ]) ++ [ param ]; 17 + }; 18 + }; 19 + "emit-include" = 20 + { param, state }: 21 + { 22 + # For these tests, just return the child as-is (no recursive resolution). 23 + resume = [ param ]; 24 + inherit state; 25 + }; 26 + "register-constraint" = 27 + { param, state }: 28 + { 29 + resume = null; 30 + state = state // { 31 + constraints = (state.constraints or [ ]) ++ [ param ]; 32 + }; 33 + }; 34 + "chain-push" = 35 + { param, state }: 36 + { 37 + resume = null; 38 + inherit state; 39 + }; 40 + "chain-pop" = 41 + { param, state }: 42 + { 43 + resume = null; 44 + inherit state; 45 + }; 46 + "resolve-complete" = 47 + { param, state }: 48 + { 49 + resume = param; 50 + inherit state; 51 + }; 52 + }; 53 + in 54 + { 55 + flake.tests.fx-aspect = { 56 + 57 + # Static aspect: emits emit-class for each class key. 58 + test-aspectToEffect-static = denTest ( 59 + { den, ... }: 60 + let 61 + aspect = { 62 + name = "myAspect"; 63 + meta = { }; 64 + nixosModules = { 65 + enable = true; 66 + }; 67 + includes = [ ]; 68 + }; 69 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 70 + result = den.lib.fx.handle { 71 + handlers = collectHandlers; 72 + state = { }; 73 + } comp; 74 + in 75 + { 76 + expr = { 77 + classCount = builtins.length result.state.classes; 78 + className = (builtins.head result.state.classes).class; 79 + module = (builtins.head result.state.classes).module; 80 + resolvedName = result.value.name; 81 + }; 82 + expected = { 83 + classCount = 1; 84 + className = "nixosModules"; 85 + module = { 86 + enable = true; 87 + }; 88 + resolvedName = "myAspect"; 89 + }; 90 + } 91 + ); 92 + 93 + # Static aspect with multiple classes. 94 + test-aspectToEffect-multi-class = denTest ( 95 + { den, ... }: 96 + let 97 + aspect = { 98 + name = "multiClass"; 99 + meta = { }; 100 + nixosModules = { 101 + x = 1; 102 + }; 103 + homeModules = { 104 + y = 2; 105 + }; 106 + includes = [ ]; 107 + }; 108 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 109 + result = den.lib.fx.handle { 110 + handlers = collectHandlers; 111 + state = { }; 112 + } comp; 113 + classNames = map (c: c.class) result.state.classes; 114 + in 115 + { 116 + expr = builtins.sort builtins.lessThan classNames; 117 + expected = [ 118 + "homeModules" 119 + "nixosModules" 120 + ]; 121 + } 122 + ); 123 + 124 + # Parametric aspect: bind.fn resolves named args via handlers. 125 + test-aspectToEffect-parametric = denTest ( 126 + { den, ... }: 127 + let 128 + aspect = { 129 + name = "paramAspect"; 130 + meta = { }; 131 + __functor = 132 + self: 133 + { host }: 134 + { 135 + nixosModules = { 136 + hostName = host; 137 + }; 138 + includes = [ ]; 139 + }; 140 + __functionArgs = { 141 + host = false; 142 + }; 143 + includes = [ ]; 144 + }; 145 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 146 + result = den.lib.fx.handle { 147 + handlers = collectHandlers // { 148 + host = 149 + { param, state }: 150 + { 151 + resume = "igloo"; 152 + inherit state; 153 + }; 154 + }; 155 + state = { }; 156 + } comp; 157 + in 158 + { 159 + expr = { 160 + classCount = builtins.length result.state.classes; 161 + module = (builtins.head result.state.classes).module; 162 + resolvedName = result.value.name; 163 + }; 164 + expected = { 165 + classCount = 1; 166 + module = { 167 + hostName = "igloo"; 168 + }; 169 + resolvedName = "paramAspect"; 170 + }; 171 + } 172 + ); 173 + 174 + # Static aspect with class config: no functor needed in the den.lib.fx.pipeline. 175 + # Factory aspects (bare ctx arg) are not supported — use destructured args 176 + # or static attrsets. 177 + test-aspectToEffect-static-class = denTest ( 178 + { den, ... }: 179 + let 180 + aspect = { 181 + name = "staticAspect"; 182 + meta = { }; 183 + nixosModules = { 184 + enabled = true; 185 + }; 186 + includes = [ ]; 187 + }; 188 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 189 + result = den.lib.fx.handle { 190 + handlers = collectHandlers; 191 + state = { }; 192 + } comp; 193 + in 194 + { 195 + expr = { 196 + classCount = builtins.length result.state.classes; 197 + module = (builtins.head result.state.classes).module; 198 + }; 199 + expected = { 200 + classCount = 1; 201 + module = { 202 + enabled = true; 203 + }; 204 + }; 205 + } 206 + ); 207 + 208 + # Includes: emits emit-include for each child. 209 + test-aspectToEffect-includes = denTest ( 210 + { den, ... }: 211 + let 212 + childA = { 213 + name = "childA"; 214 + meta = { }; 215 + includes = [ ]; 216 + }; 217 + childB = { 218 + name = "childB"; 219 + meta = { }; 220 + includes = [ ]; 221 + }; 222 + aspect = { 223 + name = "parent"; 224 + meta = { }; 225 + includes = [ 226 + childA 227 + childB 228 + ]; 229 + }; 230 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 231 + result = den.lib.fx.handle { 232 + handlers = collectHandlers; 233 + state = { }; 234 + } comp; 235 + in 236 + { 237 + expr = { 238 + includeCount = builtins.length result.value.includes; 239 + firstChild = (builtins.head result.value.includes).name; 240 + }; 241 + expected = { 242 + includeCount = 2; 243 + firstChild = "childA"; 244 + }; 245 + } 246 + ); 247 + 248 + # Constraints: registers meta.handleWith entries. 249 + test-aspectToEffect-constraints = denTest ( 250 + { den, ... }: 251 + let 252 + target = { 253 + name = "targetAspect"; 254 + meta.provider = [ "pkg" ]; 255 + }; 256 + aspect = { 257 + name = "constrainedAspect"; 258 + meta = { 259 + handleWith = [ 260 + { 261 + type = "exclude"; 262 + scope = "subtree"; 263 + identity = "pkg/targetAspect"; 264 + } 265 + ]; 266 + }; 267 + includes = [ ]; 268 + }; 269 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 270 + result = den.lib.fx.handle { 271 + handlers = collectHandlers; 272 + state = { }; 273 + } comp; 274 + in 275 + { 276 + expr = { 277 + constraintCount = builtins.length result.state.constraints; 278 + firstType = (builtins.head result.state.constraints).type; 279 + owner = (builtins.head result.state.constraints).owner; 280 + }; 281 + expected = { 282 + constraintCount = 1; 283 + firstType = "exclude"; 284 + owner = "constrainedAspect"; 285 + }; 286 + } 287 + ); 288 + 289 + }; 290 + }
+717
templates/ci/modules/features/fx-constraints.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-constraints = { 9 + 10 + test-exclude-declaration = denTest ( 11 + { den, ... }: 12 + let 13 + fx = den.lib.fx; 14 + ref = { 15 + name = "drop"; 16 + meta = { 17 + provider = [ ]; 18 + }; 19 + }; 20 + decl = den.lib.aspects.fx.constraints.exclude ref; 21 + in 22 + { 23 + expr = { 24 + type = decl.type; 25 + identity = decl.identity; 26 + }; 27 + expected = { 28 + type = "exclude"; 29 + identity = "drop"; 30 + }; 31 + } 32 + ); 33 + 34 + test-substitute-declaration = denTest ( 35 + { den, ... }: 36 + let 37 + ref = { 38 + name = "old"; 39 + meta = { 40 + provider = [ ]; 41 + }; 42 + }; 43 + replacement = { 44 + name = "new"; 45 + meta = { 46 + provider = [ ]; 47 + }; 48 + includes = [ ]; 49 + }; 50 + decl = den.lib.aspects.fx.constraints.substitute ref replacement; 51 + in 52 + { 53 + expr = { 54 + type = decl.type; 55 + identity = decl.identity; 56 + replacementName = decl.replacementName; 57 + }; 58 + expected = { 59 + type = "substitute"; 60 + identity = "old"; 61 + replacementName = "new"; 62 + }; 63 + } 64 + ); 65 + 66 + test-exclude-via-registry = denTest ( 67 + { den, ... }: 68 + let 69 + fx = den.lib.fx; 70 + ref = { 71 + name = "drop"; 72 + meta = { 73 + provider = [ ]; 74 + }; 75 + }; 76 + decl = den.lib.aspects.fx.constraints.exclude ref; 77 + # Register then check-constraint 78 + comp = fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 79 + _: fx.send "check-constraint" "drop" 80 + ); 81 + result = fx.handle { 82 + handlers = den.lib.aspects.fx.handlers.constraintRegistryHandler; 83 + state = { 84 + constraintRegistry = { }; 85 + }; 86 + } comp; 87 + in 88 + { 89 + expr = result.value.action; 90 + expected = "exclude"; 91 + } 92 + ); 93 + 94 + test-check-constraint-default-keep = denTest ( 95 + { den, ... }: 96 + let 97 + fx = den.lib.fx; 98 + comp = fx.send "check-constraint" "unknown"; 99 + result = fx.handle { 100 + handlers = den.lib.aspects.fx.handlers.constraintRegistryHandler; 101 + state = { 102 + constraintRegistry = { }; 103 + }; 104 + } comp; 105 + in 106 + { 107 + expr = result.value.action; 108 + expected = "keep"; 109 + } 110 + ); 111 + 112 + test-substitute-via-registry = denTest ( 113 + { den, ... }: 114 + let 115 + fx = den.lib.fx; 116 + ref = { 117 + name = "old"; 118 + meta = { 119 + provider = [ ]; 120 + }; 121 + }; 122 + replacement = { 123 + name = "new"; 124 + meta = { 125 + provider = [ ]; 126 + }; 127 + includes = [ ]; 128 + }; 129 + decl = den.lib.aspects.fx.constraints.substitute ref replacement; 130 + comp = fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 131 + _: fx.send "check-constraint" "old" 132 + ); 133 + result = fx.handle { 134 + handlers = den.lib.aspects.fx.handlers.constraintRegistryHandler; 135 + state = { 136 + constraintRegistry = { }; 137 + }; 138 + } comp; 139 + in 140 + { 141 + expr = { 142 + action = result.value.action; 143 + replacementName = result.value.replacement.name; 144 + }; 145 + expected = { 146 + action = "substitute"; 147 + replacementName = "new"; 148 + }; 149 + } 150 + ); 151 + 152 + # Test classCollectorHandler collects imports via emit-class effects through the pipeline. 153 + test-classCollectorHandler-collects-imports = denTest ( 154 + { den, ... }: 155 + let 156 + fx = den.lib.fx; 157 + parent = { 158 + name = "root"; 159 + meta = { }; 160 + includes = [ 161 + { 162 + name = "a"; 163 + meta = { }; 164 + nixos = { 165 + enable = true; 166 + }; 167 + includes = [ ]; 168 + } 169 + { 170 + name = "b"; 171 + meta = { }; 172 + includes = [ ]; 173 + } 174 + ]; 175 + }; 176 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 177 + result = fx.handle { 178 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 179 + class = "nixos"; 180 + ctx = { }; 181 + }; 182 + state = den.lib.aspects.fx.pipeline.defaultState; 183 + } comp; 184 + in 185 + { 186 + expr = builtins.length (result.state.imports null); 187 + expected = 1; 188 + } 189 + ); 190 + 191 + test-collectPaths-excludes-tombstones = denTest ( 192 + { den, ... }: 193 + let 194 + fx = den.lib.fx; 195 + target = { 196 + name = "drop"; 197 + meta = { 198 + provider = [ ]; 199 + }; 200 + }; 201 + parent = { 202 + name = "root"; 203 + meta = { 204 + handleWith = den.lib.aspects.fx.constraints.exclude target; 205 + }; 206 + includes = [ 207 + { 208 + name = "keep"; 209 + meta = { 210 + provider = [ ]; 211 + }; 212 + includes = [ ]; 213 + } 214 + { 215 + name = "drop"; 216 + meta = { 217 + provider = [ ]; 218 + }; 219 + includes = [ ]; 220 + } 221 + ]; 222 + }; 223 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 224 + result = fx.handle { 225 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 226 + class = "nixos"; 227 + ctx = { }; 228 + }; 229 + state = den.lib.aspects.fx.pipeline.defaultState; 230 + } comp; 231 + in 232 + { 233 + # root + keep = 2 paths, drop is tombstoned and excluded from paths 234 + expr = builtins.length result.state.paths; 235 + expected = 2; 236 + } 237 + ); 238 + 239 + test-exclude-default-scope = denTest ( 240 + { den, ... }: 241 + let 242 + ref = { 243 + name = "drop"; 244 + meta.provider = [ ]; 245 + }; 246 + decl = den.lib.aspects.fx.constraints.exclude ref; 247 + in 248 + { 249 + expr = decl.scope; 250 + expected = "subtree"; 251 + } 252 + ); 253 + 254 + test-exclude-global-scope = denTest ( 255 + { den, ... }: 256 + let 257 + ref = { 258 + name = "drop"; 259 + meta.provider = [ ]; 260 + }; 261 + decl = den.lib.aspects.fx.constraints.exclude.global ref; 262 + in 263 + { 264 + expr = { 265 + type = decl.type; 266 + scope = decl.scope; 267 + identity = decl.identity; 268 + }; 269 + expected = { 270 + type = "exclude"; 271 + scope = "global"; 272 + identity = "drop"; 273 + }; 274 + } 275 + ); 276 + 277 + test-substitute-default-scope = denTest ( 278 + { den, ... }: 279 + let 280 + ref = { 281 + name = "old"; 282 + meta.provider = [ ]; 283 + }; 284 + replacement = { 285 + name = "new"; 286 + meta.provider = [ ]; 287 + includes = [ ]; 288 + }; 289 + decl = den.lib.aspects.fx.constraints.substitute ref replacement; 290 + in 291 + { 292 + expr = decl.scope; 293 + expected = "subtree"; 294 + } 295 + ); 296 + 297 + test-substitute-global-scope = denTest ( 298 + { den, ... }: 299 + let 300 + ref = { 301 + name = "old"; 302 + meta.provider = [ ]; 303 + }; 304 + replacement = { 305 + name = "new"; 306 + meta.provider = [ ]; 307 + includes = [ ]; 308 + }; 309 + decl = den.lib.aspects.fx.constraints.substitute.global ref replacement; 310 + in 311 + { 312 + expr = decl.scope; 313 + expected = "global"; 314 + } 315 + ); 316 + 317 + test-filter-default-scope = denTest ( 318 + { den, ... }: 319 + let 320 + decl = den.lib.aspects.fx.constraints.filterBy (_: true); 321 + in 322 + { 323 + expr = decl.scope; 324 + expected = "subtree"; 325 + } 326 + ); 327 + 328 + test-filter-global-scope = denTest ( 329 + { den, ... }: 330 + let 331 + decl = den.lib.aspects.fx.constraints.filterBy.global (_: true); 332 + in 333 + { 334 + expr = decl.scope; 335 + expected = "global"; 336 + } 337 + ); 338 + 339 + test-scoped-exclude-in-subtree = denTest ( 340 + { den, ... }: 341 + let 342 + fx = den.lib.fx; 343 + ref = { 344 + name = "drop"; 345 + meta.provider = [ ]; 346 + }; 347 + decl = den.lib.aspects.fx.constraints.exclude ref; 348 + comp = fx.bind (fx.send "chain-push" { identity = "parent"; }) ( 349 + _: 350 + fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 351 + _: 352 + fx.bind (fx.send "chain-push" { identity = "child"; }) ( 353 + _: 354 + fx.send "check-constraint" { 355 + identity = "drop"; 356 + aspect = null; 357 + } 358 + ) 359 + ) 360 + ); 361 + result = fx.handle { 362 + handlers = 363 + den.lib.aspects.fx.handlers.chainHandler // den.lib.aspects.fx.handlers.constraintRegistryHandler; 364 + state = { 365 + includesChain = [ ]; 366 + constraintRegistry = { }; 367 + constraintFilters = [ ]; 368 + }; 369 + } comp; 370 + in 371 + { 372 + expr = result.value.action; 373 + expected = "exclude"; 374 + } 375 + ); 376 + 377 + test-scoped-exclude-outside-subtree = denTest ( 378 + { den, ... }: 379 + let 380 + fx = den.lib.fx; 381 + ref = { 382 + name = "drop"; 383 + meta.provider = [ ]; 384 + }; 385 + decl = den.lib.aspects.fx.constraints.exclude ref; 386 + comp = fx.bind (fx.send "chain-push" { identity = "a"; }) ( 387 + _: 388 + fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 389 + _: 390 + fx.bind (fx.send "chain-pop" null) ( 391 + _: 392 + fx.bind (fx.send "chain-push" { identity = "b"; }) ( 393 + _: 394 + fx.send "check-constraint" { 395 + identity = "drop"; 396 + aspect = null; 397 + } 398 + ) 399 + ) 400 + ) 401 + ); 402 + result = fx.handle { 403 + handlers = 404 + den.lib.aspects.fx.handlers.chainHandler // den.lib.aspects.fx.handlers.constraintRegistryHandler; 405 + state = { 406 + includesChain = [ ]; 407 + constraintRegistry = { }; 408 + constraintFilters = [ ]; 409 + }; 410 + } comp; 411 + in 412 + { 413 + expr = result.value.action; 414 + expected = "keep"; 415 + } 416 + ); 417 + 418 + test-global-exclude-ignores-chain = denTest ( 419 + { den, ... }: 420 + let 421 + fx = den.lib.fx; 422 + ref = { 423 + name = "drop"; 424 + meta.provider = [ ]; 425 + }; 426 + decl = den.lib.aspects.fx.constraints.exclude.global ref; 427 + comp = fx.bind (fx.send "chain-push" { identity = "a"; }) ( 428 + _: 429 + fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 430 + _: 431 + fx.bind (fx.send "chain-pop" null) ( 432 + _: 433 + fx.bind (fx.send "chain-push" { identity = "b"; }) ( 434 + _: 435 + fx.send "check-constraint" { 436 + identity = "drop"; 437 + aspect = null; 438 + } 439 + ) 440 + ) 441 + ) 442 + ); 443 + result = fx.handle { 444 + handlers = 445 + den.lib.aspects.fx.handlers.chainHandler // den.lib.aspects.fx.handlers.constraintRegistryHandler; 446 + state = { 447 + includesChain = [ ]; 448 + constraintRegistry = { }; 449 + constraintFilters = [ ]; 450 + }; 451 + } comp; 452 + in 453 + { 454 + expr = result.value.action; 455 + expected = "exclude"; 456 + } 457 + ); 458 + 459 + test-scoped-filter-in-subtree = denTest ( 460 + { den, ... }: 461 + let 462 + fx = den.lib.fx; 463 + decl = den.lib.aspects.fx.constraints.filterBy (a: a.name != "drop"); 464 + aspect = { 465 + name = "drop"; 466 + meta.provider = [ ]; 467 + }; 468 + comp = fx.bind (fx.send "chain-push" { identity = "parent"; }) ( 469 + _: 470 + fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 471 + _: 472 + fx.send "check-constraint" { 473 + identity = "drop"; 474 + inherit aspect; 475 + } 476 + ) 477 + ); 478 + result = fx.handle { 479 + handlers = 480 + den.lib.aspects.fx.handlers.chainHandler // den.lib.aspects.fx.handlers.constraintRegistryHandler; 481 + state = { 482 + includesChain = [ ]; 483 + constraintRegistry = { }; 484 + constraintFilters = [ ]; 485 + }; 486 + } comp; 487 + in 488 + { 489 + expr = result.value.action; 490 + expected = "exclude"; 491 + } 492 + ); 493 + 494 + test-scoped-filter-outside-subtree = denTest ( 495 + { den, ... }: 496 + let 497 + fx = den.lib.fx; 498 + decl = den.lib.aspects.fx.constraints.filterBy (a: a.name != "drop"); 499 + aspect = { 500 + name = "drop"; 501 + meta.provider = [ ]; 502 + }; 503 + comp = fx.bind (fx.send "chain-push" { identity = "a"; }) ( 504 + _: 505 + fx.bind (fx.send "register-constraint" (decl // { owner = "test"; })) ( 506 + _: 507 + fx.bind (fx.send "chain-pop" null) ( 508 + _: 509 + fx.bind (fx.send "chain-push" { identity = "b"; }) ( 510 + _: 511 + fx.send "check-constraint" { 512 + identity = "drop"; 513 + inherit aspect; 514 + } 515 + ) 516 + ) 517 + ) 518 + ); 519 + result = fx.handle { 520 + handlers = 521 + den.lib.aspects.fx.handlers.chainHandler // den.lib.aspects.fx.handlers.constraintRegistryHandler; 522 + state = { 523 + includesChain = [ ]; 524 + constraintRegistry = { }; 525 + constraintFilters = [ ]; 526 + }; 527 + } comp; 528 + in 529 + { 530 + expr = result.value.action; 531 + expected = "keep"; 532 + } 533 + ); 534 + 535 + # classCollectorHandler skips tombstoned aspects (excluded = true means no emit-class). 536 + test-classCollectorHandler-skips-tombstones = denTest ( 537 + { den, ... }: 538 + let 539 + fx = den.lib.fx; 540 + target = { 541 + name = "drop"; 542 + meta = { 543 + provider = [ ]; 544 + }; 545 + }; 546 + parent = { 547 + name = "root"; 548 + meta = { 549 + handleWith = den.lib.aspects.fx.constraints.exclude target; 550 + }; 551 + includes = [ 552 + { 553 + name = "keep"; 554 + meta = { 555 + provider = [ ]; 556 + }; 557 + nixos = { 558 + a = 1; 559 + }; 560 + includes = [ ]; 561 + } 562 + { 563 + name = "drop"; 564 + meta = { 565 + provider = [ ]; 566 + }; 567 + nixos = { 568 + b = 2; 569 + }; 570 + includes = [ ]; 571 + } 572 + ]; 573 + }; 574 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 575 + result = fx.handle { 576 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 577 + class = "nixos"; 578 + ctx = { }; 579 + }; 580 + state = den.lib.aspects.fx.pipeline.defaultState; 581 + } comp; 582 + in 583 + { 584 + expr = builtins.length (result.state.imports null); 585 + expected = 1; 586 + } 587 + ); 588 + 589 + # meta.excludes sugar: exclude via list of refs 590 + test-excludes-sugar = denTest ( 591 + { den, ... }: 592 + let 593 + fx = den.lib.fx; 594 + target = { 595 + name = "drop"; 596 + meta.provider = [ ]; 597 + }; 598 + parent = { 599 + name = "root"; 600 + meta = { 601 + excludes = [ target ]; 602 + }; 603 + includes = [ 604 + { 605 + name = "keep"; 606 + meta.provider = [ ]; 607 + includes = [ ]; 608 + } 609 + { 610 + name = "drop"; 611 + meta.provider = [ ]; 612 + includes = [ ]; 613 + } 614 + ]; 615 + }; 616 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 617 + result = fx.handle { 618 + handlers = 619 + den.lib.aspects.fx.pipeline.composeHandlers 620 + (den.lib.aspects.fx.pipeline.defaultHandlers { 621 + class = "nixos"; 622 + ctx = { }; 623 + }) 624 + { 625 + "resolve-complete" = 626 + { param, state }: 627 + { 628 + resume = param; 629 + state = state // { 630 + excluded = (state.excluded or [ ]) ++ (lib.optional (param.meta.excluded or false) param.name); 631 + }; 632 + }; 633 + }; 634 + state = den.lib.aspects.fx.pipeline.defaultState // { 635 + excluded = [ ]; 636 + }; 637 + } comp; 638 + in 639 + { 640 + expr = result.state.excluded; 641 + expected = [ "~drop" ]; 642 + } 643 + ); 644 + 645 + # meta.handleWith as list of multiple handlers 646 + test-handleWith-list = denTest ( 647 + { den, ... }: 648 + let 649 + fx = den.lib.fx; 650 + targetA = { 651 + name = "a"; 652 + meta.provider = [ ]; 653 + }; 654 + targetB = { 655 + name = "b"; 656 + meta.provider = [ ]; 657 + }; 658 + parent = { 659 + name = "root"; 660 + meta = { 661 + handleWith = [ 662 + (den.lib.aspects.fx.constraints.exclude targetA) 663 + (den.lib.aspects.fx.constraints.exclude targetB) 664 + ]; 665 + }; 666 + includes = [ 667 + { 668 + name = "a"; 669 + meta.provider = [ ]; 670 + includes = [ ]; 671 + } 672 + { 673 + name = "b"; 674 + meta.provider = [ ]; 675 + includes = [ ]; 676 + } 677 + { 678 + name = "c"; 679 + meta.provider = [ ]; 680 + includes = [ ]; 681 + } 682 + ]; 683 + }; 684 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 685 + result = fx.handle { 686 + handlers = 687 + den.lib.aspects.fx.pipeline.composeHandlers 688 + (den.lib.aspects.fx.pipeline.defaultHandlers { 689 + class = "nixos"; 690 + ctx = { }; 691 + }) 692 + { 693 + "resolve-complete" = 694 + { param, state }: 695 + { 696 + resume = param; 697 + state = state // { 698 + excluded = (state.excluded or [ ]) ++ (lib.optional (param.meta.excluded or false) param.name); 699 + }; 700 + }; 701 + }; 702 + state = den.lib.aspects.fx.pipeline.defaultState // { 703 + excluded = [ ]; 704 + }; 705 + } comp; 706 + in 707 + { 708 + expr = builtins.sort builtins.lessThan result.state.excluded; 709 + expected = [ 710 + "~a" 711 + "~b" 712 + ]; 713 + } 714 + ); 715 + 716 + }; 717 + }
+353
templates/ci/modules/features/fx-ctx-apply.nix
··· 1 + # Tests for context traversal helpers: emitTransitions, emitSelfProvide, dedup. 2 + { 3 + denTest, 4 + inputs, 5 + lib, 6 + ... 7 + }: 8 + let 9 + 10 + collectHandlers = { 11 + "emit-class" = 12 + { param, state }: 13 + { 14 + resume = null; 15 + inherit state; 16 + }; 17 + "emit-include" = 18 + { param, state }: 19 + { 20 + resume = [ param ]; 21 + inherit state; 22 + }; 23 + "into-transition" = 24 + { param, state }: 25 + { 26 + resume = [ ]; 27 + state = state // { 28 + transitions = (state.transitions or [ ]) ++ [ 29 + { 30 + hasIntoFn = param ? intoFn; 31 + selfName = param.self.name or "<anon>"; 32 + } 33 + ]; 34 + }; 35 + }; 36 + "register-constraint" = 37 + { param, state }: 38 + { 39 + resume = null; 40 + inherit state; 41 + }; 42 + "chain-push" = 43 + { param, state }: 44 + { 45 + resume = null; 46 + inherit state; 47 + }; 48 + "chain-pop" = 49 + { param, state }: 50 + { 51 + resume = null; 52 + inherit state; 53 + }; 54 + "resolve-complete" = 55 + { param, state }: 56 + { 57 + resume = param; 58 + inherit state; 59 + }; 60 + }; 61 + in 62 + { 63 + flake.tests.fx-ctx-apply = { 64 + 65 + # emitSelfProvide: produces include from aspect.provides.${name}. 66 + test-self-provide = denTest ( 67 + { den, ... }: 68 + let 69 + fx = den.lib.fx; 70 + aspect = { 71 + name = "host"; 72 + meta = { }; 73 + provides = { 74 + host = ctx: { 75 + name = "host-provider"; 76 + meta = { }; 77 + nixos = { 78 + provided = true; 79 + }; 80 + includes = [ ]; 81 + }; 82 + }; 83 + includes = [ ]; 84 + }; 85 + comp = den.lib.aspects.fx.aspect.emitSelfProvide aspect; 86 + result = fx.handle { 87 + handlers = collectHandlers; 88 + state = { }; 89 + } comp; 90 + in 91 + { 92 + expr = { 93 + hasResult = builtins.isList result.value && builtins.length result.value >= 1; 94 + firstName = (builtins.head result.value).name; 95 + }; 96 + expected = { 97 + hasResult = true; 98 + firstName = "host"; 99 + }; 100 + } 101 + ); 102 + 103 + # emitTransitions: emits into-transition effect. 104 + test-into-transition-emits = denTest ( 105 + { den, ... }: 106 + let 107 + fx = den.lib.fx; 108 + aspect = { 109 + name = "host"; 110 + meta = { }; 111 + into = ctx: { 112 + user = [ 113 + { 114 + user = "tux"; 115 + } 116 + ]; 117 + }; 118 + provides = { }; 119 + includes = [ ]; 120 + }; 121 + comp = den.lib.aspects.fx.aspect.emitTransitions aspect; 122 + result = fx.handle { 123 + handlers = collectHandlers; 124 + state = { }; 125 + } comp; 126 + in 127 + { 128 + expr = { 129 + transitionCount = builtins.length (result.state.transitions or [ ]); 130 + selfName = (builtins.head result.state.transitions).selfName; 131 + hasIntoFn = (builtins.head result.state.transitions).hasIntoFn; 132 + }; 133 + expected = { 134 + transitionCount = 1; 135 + selfName = "host"; 136 + hasIntoFn = true; 137 + }; 138 + } 139 + ); 140 + 141 + # Into keys excluded from class emission by structuralKeys. 142 + test-into-not-class = denTest ( 143 + { den, ... }: 144 + let 145 + fx = den.lib.fx; 146 + aspect = { 147 + name = "host"; 148 + meta = { }; 149 + into = _: { }; 150 + nixos = { 151 + enable = true; 152 + }; 153 + includes = [ ]; 154 + }; 155 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 156 + result = fx.handle { 157 + handlers = collectHandlers // { 158 + "emit-class" = 159 + { param, state }: 160 + { 161 + resume = null; 162 + state = state // { 163 + classes = (state.classes or [ ]) ++ [ param ]; 164 + }; 165 + }; 166 + }; 167 + state = { }; 168 + } comp; 169 + classNames = map (c: c.class) (result.state.classes or [ ]); 170 + in 171 + { 172 + expr = classNames; 173 + expected = [ "nixos" ]; 174 + } 175 + ); 176 + 177 + # emitSelfProvide returns empty when no matching provide. 178 + test-self-provide-absent = denTest ( 179 + { den, ... }: 180 + let 181 + fx = den.lib.fx; 182 + aspect = { 183 + name = "host"; 184 + meta = { }; 185 + provides = { }; 186 + includes = [ ]; 187 + }; 188 + comp = den.lib.aspects.fx.aspect.emitSelfProvide aspect; 189 + result = fx.handle { 190 + handlers = collectHandlers; 191 + state = { }; 192 + } comp; 193 + in 194 + { 195 + expr = result.value; 196 + expected = [ ]; 197 + } 198 + ); 199 + 200 + # emitTransitions returns empty when no into. 201 + test-no-transitions = denTest ( 202 + { den, ... }: 203 + let 204 + fx = den.lib.fx; 205 + aspect = { 206 + name = "host"; 207 + meta = { }; 208 + includes = [ ]; 209 + }; 210 + comp = den.lib.aspects.fx.aspect.emitTransitions aspect; 211 + result = fx.handle { 212 + handlers = collectHandlers; 213 + state = { }; 214 + } comp; 215 + in 216 + { 217 + expr = result.value; 218 + expected = [ ]; 219 + } 220 + ); 221 + 222 + # Functor resolution preserves into and provides. 223 + test-functor-preserves-into = denTest ( 224 + { den, ... }: 225 + let 226 + fx = den.lib.fx; 227 + aspect = { 228 + name = "host"; 229 + meta = { }; 230 + into = ctx: { 231 + user = [ { user = "tux"; } ]; 232 + }; 233 + __functor = 234 + self: 235 + { host }: 236 + { 237 + nixos = { 238 + hostName = host; 239 + }; 240 + includes = [ ]; 241 + }; 242 + __functionArgs = { 243 + host = false; 244 + }; 245 + includes = [ ]; 246 + }; 247 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 248 + result = fx.handle { 249 + handlers = collectHandlers // { 250 + host = 251 + { param, state }: 252 + { 253 + resume = "igloo"; 254 + inherit state; 255 + }; 256 + }; 257 + state = { }; 258 + } comp; 259 + in 260 + { 261 + # Verify into is preserved through functor resolution 262 + # (the resolved aspect still has into, visible via structural attrs) 263 + expr = result.value ? into; 264 + expected = true; 265 + } 266 + ); 267 + 268 + # Dedup: same key second time gets isFirst=false (standalone test). 269 + test-dedup = denTest ( 270 + { den, ... }: 271 + let 272 + fx = den.lib.fx; 273 + comp = fx.bind (fx.send "ctx-seen" "k") ( 274 + a: 275 + fx.bind (fx.send "ctx-seen" "k") ( 276 + b: 277 + fx.pure { 278 + first = a.isFirst; 279 + second = b.isFirst; 280 + } 281 + ) 282 + ); 283 + result = fx.handle { 284 + handlers."ctx-seen" = 285 + { param, state }: 286 + let 287 + isFirst = !((state.seen or { }) ? ${param}); 288 + in 289 + { 290 + resume = { inherit isFirst; }; 291 + state = state // { 292 + seen = (state.seen or { }) // { 293 + ${param} = true; 294 + }; 295 + }; 296 + }; 297 + state = { 298 + seen = { }; 299 + }; 300 + } comp; 301 + in 302 + { 303 + expr = result.value; 304 + expected = { 305 + first = true; 306 + second = false; 307 + }; 308 + } 309 + ); 310 + 311 + # Self-provider standalone: ctx-provider effect resolves provides. 312 + test-self-provider = denTest ( 313 + { den, ... }: 314 + let 315 + fx = den.lib.fx; 316 + provFn = ctx: { name = "provided"; }; 317 + comp = fx.send "ctx-provider" { 318 + kind = "self"; 319 + self = { 320 + name = "host"; 321 + provides = { 322 + host = provFn; 323 + }; 324 + }; 325 + ctx = { }; 326 + key = "host"; 327 + prev = null; 328 + prevCtx = null; 329 + }; 330 + result = fx.handle { 331 + handlers."ctx-provider" = 332 + { param, state }: 333 + if param.kind == "self" then 334 + { 335 + resume = param.self.provides.${param.self.name} or null; 336 + inherit state; 337 + } 338 + else 339 + { 340 + resume = null; 341 + inherit state; 342 + }; 343 + state = { }; 344 + } comp; 345 + in 346 + { 347 + expr = (result.value { }).name; 348 + expected = "provided"; 349 + } 350 + ); 351 + 352 + }; 353 + }
+162
templates/ci/modules/features/fx-ctx-parametric.nix
··· 1 + { 2 + denTest, 3 + lib, 4 + ... 5 + }: 6 + { 7 + flake.tests.fx-ctx-parametric = { 8 + 9 + # Bare lambda include with context arg — pipeline provides host via ctx. 10 + test-bare-lambda-host = denTest ( 11 + { den, ... }: 12 + let 13 + fx = den.lib.fx; 14 + parent = { 15 + name = "parent"; 16 + meta = { }; 17 + nixos = { }; 18 + includes = [ 19 + ( 20 + { host, ... }: 21 + { 22 + nixos.networking.hostName = host; 23 + } 24 + ) 25 + ]; 26 + }; 27 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 28 + result = fx.handle { 29 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 30 + class = "nixos"; 31 + ctx = { 32 + host = "igloo"; 33 + }; 34 + }; 35 + state = den.lib.aspects.fx.pipeline.defaultState; 36 + } comp; 37 + in 38 + { 39 + expr = (builtins.head result.value.includes).nixos.networking.hostName; 40 + expected = "igloo"; 41 + } 42 + ); 43 + 44 + # Attrset-with-functor parametric child — explicit __functionArgs with host. 45 + test-attrset-functor-host = denTest ( 46 + { den, ... }: 47 + let 48 + fx = den.lib.fx; 49 + child = { 50 + name = "child"; 51 + meta = { }; 52 + __functor = 53 + _: 54 + { host }: 55 + { 56 + nixos.networking.hostName = host; 57 + }; 58 + __functionArgs = { 59 + host = false; 60 + }; 61 + includes = [ ]; 62 + }; 63 + parent = { 64 + name = "parent"; 65 + meta = { }; 66 + nixos = { }; 67 + includes = [ child ]; 68 + }; 69 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 70 + result = fx.handle { 71 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 72 + class = "nixos"; 73 + ctx = { 74 + host = "igloo"; 75 + }; 76 + }; 77 + state = den.lib.aspects.fx.pipeline.defaultState; 78 + } comp; 79 + in 80 + { 81 + expr = (builtins.head result.value.includes).nixos.networking.hostName; 82 + expected = "igloo"; 83 + } 84 + ); 85 + 86 + # fixedTo-wrapped aspect through full pipeline with ctx — manual pipeline setup. 87 + test-fixedto-with-ctx = denTest ( 88 + { den, ... }: 89 + let 90 + fx = den.lib.fx; 91 + parametric = den.lib.parametric; 92 + innerAspect = { 93 + name = "tux"; 94 + meta = { }; 95 + user = { 96 + description = "test-user"; 97 + }; 98 + includes = [ 99 + ( 100 + { host, ... }: 101 + lib.optionalAttrs (host == "igloo") { 102 + user.extraGroups = [ "wheel" ]; 103 + } 104 + ) 105 + ]; 106 + }; 107 + wrapped = parametric.fixedTo { 108 + host = "igloo"; 109 + } innerAspect; 110 + comp = den.lib.aspects.fx.aspect.aspectToEffect wrapped; 111 + # Provide host in ctx so the pipeline has a handler for it. 112 + result = fx.handle { 113 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 114 + class = "user"; 115 + ctx = { 116 + host = "igloo"; 117 + }; 118 + }; 119 + state = den.lib.aspects.fx.pipeline.defaultState; 120 + } comp; 121 + in 122 + { 123 + expr = builtins.length (result.state.imports null) > 0; 124 + expected = true; 125 + } 126 + ); 127 + 128 + # fixedTo-wrapped aspect through fxResolveTree — ctx is empty, deepRecurse 129 + # should handle context internally without needing host in pipeline handlers. 130 + test-fixedto-through-fxResolveTree = denTest ( 131 + { den, ... }: 132 + let 133 + parametric = den.lib.parametric; 134 + innerAspect = { 135 + name = "tux"; 136 + meta = { }; 137 + user = { 138 + description = "test-user"; 139 + }; 140 + includes = [ 141 + ( 142 + { host, ... }: 143 + lib.optionalAttrs (host == "igloo") { 144 + user.extraGroups = [ "wheel" ]; 145 + } 146 + ) 147 + ]; 148 + }; 149 + wrapped = parametric.fixedTo { 150 + host = "igloo"; 151 + } innerAspect; 152 + # This is what forward.nix calls: 153 + resolved = den.lib.aspects.resolve "user" wrapped; 154 + in 155 + { 156 + expr = builtins.length resolved.imports > 0; 157 + expected = true; 158 + } 159 + ); 160 + 161 + }; 162 + }
+194
templates/ci/modules/features/fx-e2e.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-e2e = { 9 + 10 + # Basic pipeline: host with includes produces correct module count. 11 + test-host-with-includes = denTest ( 12 + { den, ... }: 13 + let 14 + hostSelf = { 15 + name = "host"; 16 + into = _: { }; 17 + provides = { }; 18 + nixos = { 19 + networking.hostName = "igloo"; 20 + }; 21 + includes = [ 22 + { 23 + name = "desktop"; 24 + meta = { }; 25 + nixos = { 26 + wm = true; 27 + }; 28 + includes = [ ]; 29 + } 30 + ]; 31 + }; 32 + result = den.lib.aspects.fx.pipeline.fxResolve { 33 + class = "nixos"; 34 + self = hostSelf; 35 + ctx = { 36 + host = "igloo"; 37 + }; 38 + }; 39 + in 40 + { 41 + # host.nixos + desktop.nixos = 2 imports 42 + expr = builtins.length result.imports; 43 + expected = 2; 44 + } 45 + ); 46 + 47 + # Self-provide: emitSelfProvide produces an include from provides.${name}. 48 + # In the full module system, ctx-apply handles this before the pipeline. 49 + # Here we test emitSelfProvide directly. 50 + test-self-provider = denTest ( 51 + { den, ... }: 52 + let 53 + fx = den.lib.fx; 54 + hostSelf = { 55 + name = "host"; 56 + into = _: { }; 57 + provides = { 58 + host = ctx: { 59 + name = "host-provider"; 60 + meta = { }; 61 + nixos = { 62 + fromProvider = true; 63 + }; 64 + includes = [ ]; 65 + }; 66 + }; 67 + nixos = { 68 + base = true; 69 + }; 70 + includes = [ ]; 71 + }; 72 + comp = den.lib.aspects.fx.aspect.emitSelfProvide hostSelf; 73 + result = fx.handle { 74 + handlers = { 75 + "emit-include" = 76 + { param, state }: 77 + { 78 + resume = [ param ]; 79 + inherit state; 80 + }; 81 + }; 82 + state = { }; 83 + } comp; 84 + in 85 + { 86 + expr = { 87 + hasProvider = builtins.isList result.value && builtins.length result.value >= 1; 88 + providerName = (builtins.head result.value).name; 89 + isSelfProvide = (builtins.head result.value).meta.selfProvide or false; 90 + }; 91 + expected = { 92 + hasProvider = true; 93 + providerName = "host"; 94 + isSelfProvide = true; 95 + }; 96 + } 97 + ); 98 + 99 + # Root adapter excludes aspect. 100 + test-root-adapter-excludes = denTest ( 101 + { den, ... }: 102 + let 103 + wayland = { 104 + name = "wayland"; 105 + meta = { 106 + provider = [ ]; 107 + }; 108 + }; 109 + hostSelf = { 110 + name = "host"; 111 + into = _: { }; 112 + provides = { }; 113 + meta = { 114 + handleWith = den.lib.aspects.fx.constraints.exclude wayland; 115 + }; 116 + includes = [ 117 + { 118 + name = "wayland"; 119 + meta = { 120 + provider = [ ]; 121 + }; 122 + nixos = { 123 + wl = true; 124 + }; 125 + includes = [ ]; 126 + } 127 + { 128 + name = "x11"; 129 + meta = { 130 + provider = [ ]; 131 + }; 132 + nixos = { 133 + x = true; 134 + }; 135 + includes = [ ]; 136 + } 137 + ]; 138 + }; 139 + result = den.lib.aspects.fx.pipeline.fxResolve { 140 + class = "nixos"; 141 + self = hostSelf; 142 + ctx = { }; 143 + }; 144 + in 145 + { 146 + expr = builtins.length result.imports; 147 + expected = 1; 148 + } 149 + ); 150 + 151 + # includeIf with hasAspect through full pipeline. 152 + test-includeIf-e2e = denTest ( 153 + { den, ... }: 154 + let 155 + sops = { 156 + name = "sops"; 157 + meta = { 158 + provider = [ ]; 159 + }; 160 + includes = [ ]; 161 + }; 162 + sopsConf = { 163 + name = "sops-conf"; 164 + meta = { 165 + provider = [ ]; 166 + }; 167 + nixos = { 168 + sops = true; 169 + }; 170 + includes = [ ]; 171 + }; 172 + hostSelf = { 173 + name = "host"; 174 + into = _: { }; 175 + provides = { }; 176 + includes = [ 177 + sops 178 + (den.lib.aspects.fx.includes.includeIf (ctx: ctx.hasAspect sops) [ sopsConf ]) 179 + ]; 180 + }; 181 + result = den.lib.aspects.fx.pipeline.fxResolve { 182 + class = "nixos"; 183 + self = hostSelf; 184 + ctx = { }; 185 + }; 186 + in 187 + { 188 + expr = builtins.length result.imports; 189 + expected = 1; 190 + } 191 + ); 192 + 193 + }; 194 + }
+347
templates/ci/modules/features/fx-effectful-resolve.nix
··· 1 + # Tests for the unified emit-include handler with aspectToEffect. 2 + { 3 + denTest, 4 + inputs, 5 + lib, 6 + ... 7 + }: 8 + let 9 + # Minimal handler set for testing aspectToEffect + includeHandler. 10 + mkTestHandlers = 11 + { 12 + den, 13 + extraHandlers ? { }, 14 + }: 15 + den.lib.aspects.fx.handlers.includeHandler 16 + // den.lib.aspects.fx.handlers.constraintRegistryHandler 17 + // den.lib.aspects.fx.handlers.chainHandler 18 + // den.lib.aspects.fx.identity.pathSetHandler 19 + // den.lib.aspects.fx.identity.collectPathsHandler 20 + // { 21 + "emit-class" = 22 + { param, state }: 23 + { 24 + resume = null; 25 + state = state // { 26 + classes = (state.classes or [ ]) ++ [ param ]; 27 + }; 28 + }; 29 + "resolve-complete" = 30 + { param, state }: 31 + { 32 + resume = param; 33 + state = state // { 34 + names = (state.names or [ ]) ++ [ (param.name or "<anon>") ]; 35 + }; 36 + }; 37 + "check-constraint" = 38 + { param, state }: 39 + { 40 + resume = { 41 + action = "keep"; 42 + }; 43 + inherit state; 44 + }; 45 + } 46 + // extraHandlers; 47 + 48 + defaultState = { 49 + includesChain = [ ]; 50 + constraintRegistry = { }; 51 + constraintFilters = [ ]; 52 + paths = [ ]; 53 + }; 54 + in 55 + { 56 + flake.tests.fx-effectful-resolve = { 57 + 58 + # Basic: parent with child, both resolved via aspectToEffect. 59 + test-basic-aspectToEffect = denTest ( 60 + { den, ... }: 61 + let 62 + fx = den.lib.fx; 63 + parent = { 64 + name = "parent"; 65 + meta = { }; 66 + nixos = { 67 + a = 1; 68 + }; 69 + includes = [ 70 + { 71 + name = "child"; 72 + meta = { }; 73 + nixos = { 74 + b = 2; 75 + }; 76 + includes = [ ]; 77 + } 78 + ]; 79 + }; 80 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 81 + result = fx.handle { 82 + handlers = mkTestHandlers { inherit den; }; 83 + state = defaultState; 84 + } comp; 85 + in 86 + { 87 + expr = { 88 + parentName = result.value.name; 89 + childName = (builtins.head result.value.includes).name; 90 + classCount = builtins.length result.state.classes; 91 + resolvedNames = result.state.names; 92 + }; 93 + expected = { 94 + parentName = "parent"; 95 + childName = "child"; 96 + classCount = 2; 97 + resolvedNames = [ 98 + "child" 99 + "parent" 100 + ]; 101 + }; 102 + } 103 + ); 104 + 105 + # Constraint: exclude a child via handleWith. 106 + test-exclude-child = denTest ( 107 + { den, ... }: 108 + let 109 + fx = den.lib.fx; 110 + parent = { 111 + name = "parent"; 112 + meta = { 113 + handleWith = [ 114 + { 115 + type = "exclude"; 116 + scope = "subtree"; 117 + identity = "drop"; 118 + } 119 + ]; 120 + }; 121 + includes = [ 122 + { 123 + name = "keep"; 124 + meta = { }; 125 + nixos = { 126 + a = 1; 127 + }; 128 + includes = [ ]; 129 + } 130 + { 131 + name = "drop"; 132 + meta = { }; 133 + nixos = { 134 + b = 2; 135 + }; 136 + includes = [ ]; 137 + } 138 + ]; 139 + }; 140 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 141 + result = fx.handle { 142 + handlers = mkTestHandlers { inherit den; } // den.lib.aspects.fx.handlers.constraintRegistryHandler; 143 + state = defaultState; 144 + } comp; 145 + children = result.value.includes; 146 + in 147 + { 148 + expr = { 149 + count = builtins.length children; 150 + firstName = (builtins.elemAt children 0).name; 151 + secondExcluded = (builtins.elemAt children 1).meta.excluded; 152 + }; 153 + expected = { 154 + count = 2; 155 + firstName = "keep"; 156 + secondExcluded = true; 157 + }; 158 + } 159 + ); 160 + 161 + # Parametric child resolved through handler-provided args. 162 + test-parametric-child = denTest ( 163 + { den, ... }: 164 + let 165 + fx = den.lib.fx; 166 + parent = { 167 + name = "root"; 168 + meta = { }; 169 + includes = [ 170 + { 171 + name = "web"; 172 + meta = { }; 173 + __functor = 174 + _: 175 + { host }: 176 + { 177 + nixos.hostName = host; 178 + includes = [ ]; 179 + }; 180 + __functionArgs = { 181 + host = false; 182 + }; 183 + includes = [ ]; 184 + } 185 + ]; 186 + }; 187 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 188 + result = fx.handle { 189 + handlers = mkTestHandlers { inherit den; } // { 190 + host = 191 + { param, state }: 192 + { 193 + resume = "igloo"; 194 + inherit state; 195 + }; 196 + }; 197 + state = defaultState; 198 + } comp; 199 + child = builtins.head result.value.includes; 200 + in 201 + { 202 + expr = child.nixos.hostName; 203 + expected = "igloo"; 204 + } 205 + ); 206 + 207 + # resolve-complete fires for each node. 208 + test-resolve-complete-collects = denTest ( 209 + { den, ... }: 210 + let 211 + fx = den.lib.fx; 212 + parent = { 213 + name = "root"; 214 + meta = { }; 215 + includes = [ 216 + { 217 + name = "a"; 218 + meta = { }; 219 + includes = [ ]; 220 + } 221 + { 222 + name = "b"; 223 + meta = { }; 224 + includes = [ ]; 225 + } 226 + ]; 227 + }; 228 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 229 + result = fx.handle { 230 + handlers = mkTestHandlers { inherit den; }; 231 + state = defaultState; 232 + } comp; 233 + in 234 + { 235 + expr = result.state.names; 236 + expected = [ 237 + "a" 238 + "b" 239 + "root" 240 + ]; 241 + } 242 + ); 243 + 244 + # Nested excludes: inner excludes B, outer excludes A. 245 + test-nested-excludes = denTest ( 246 + { den, ... }: 247 + let 248 + fx = den.lib.fx; 249 + parent = { 250 + name = "root"; 251 + meta = { 252 + handleWith = [ 253 + { 254 + type = "exclude"; 255 + scope = "subtree"; 256 + identity = "A"; 257 + } 258 + ]; 259 + }; 260 + includes = [ 261 + { 262 + name = "inner"; 263 + meta = { 264 + handleWith = [ 265 + { 266 + type = "exclude"; 267 + scope = "subtree"; 268 + identity = "B"; 269 + } 270 + ]; 271 + }; 272 + includes = [ 273 + { 274 + name = "B"; 275 + meta = { }; 276 + nixos = { 277 + b = 1; 278 + }; 279 + includes = [ ]; 280 + } 281 + ]; 282 + } 283 + { 284 + name = "A"; 285 + meta = { }; 286 + nixos = { 287 + a = 1; 288 + }; 289 + includes = [ ]; 290 + } 291 + ]; 292 + }; 293 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 294 + result = fx.handle { 295 + handlers = mkTestHandlers { inherit den; } // den.lib.aspects.fx.handlers.constraintRegistryHandler; 296 + state = defaultState; 297 + } comp; 298 + excludedNames = builtins.filter (n: lib.hasPrefix "~" n) result.state.names; 299 + in 300 + { 301 + expr = builtins.sort builtins.lessThan excludedNames; 302 + expected = [ 303 + "~A" 304 + "~B" 305 + ]; 306 + } 307 + ); 308 + 309 + # Bare function include gets wrapped and resolved. 310 + test-bare-function-include = denTest ( 311 + { den, ... }: 312 + let 313 + fx = den.lib.fx; 314 + parent = { 315 + name = "root"; 316 + meta = { }; 317 + includes = [ 318 + ( 319 + { host }: 320 + { 321 + nixos.hostName = host; 322 + } 323 + ) 324 + ]; 325 + }; 326 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 327 + result = fx.handle { 328 + handlers = mkTestHandlers { inherit den; } // { 329 + host = 330 + { param, state }: 331 + { 332 + resume = "igloo"; 333 + inherit state; 334 + }; 335 + }; 336 + state = defaultState; 337 + } comp; 338 + child = builtins.head result.value.includes; 339 + in 340 + { 341 + expr = child.nixos.hostName; 342 + expected = "igloo"; 343 + } 344 + ); 345 + 346 + }; 347 + }
+30
templates/ci/modules/features/fx-flag.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-flag = { 9 + 10 + # Flag defaults to true. 11 + test-flag-default-false = denTest ( 12 + { den, ... }: 13 + { 14 + expr = den.fxPipeline; 15 + expected = true; 16 + } 17 + ); 18 + 19 + # Flag can be set to false. 20 + test-flag-settable = denTest ( 21 + { den, ... }: 22 + { 23 + den.fxPipeline = false; 24 + expr = den.fxPipeline; 25 + expected = false; 26 + } 27 + ); 28 + 29 + }; 30 + }
+174
templates/ci/modules/features/fx-full-pipeline.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-full-pipeline = { 9 + 10 + # Minimal: single aspect with nixos config, collects module. 11 + test-minimal-pipeline = denTest ( 12 + { den, ... }: 13 + let 14 + self = { 15 + name = "host"; 16 + meta = { }; 17 + nixos = { 18 + networking.hostName = "test"; 19 + }; 20 + includes = [ ]; 21 + }; 22 + result = den.lib.aspects.fx.pipeline.fxFullResolve { 23 + class = "nixos"; 24 + inherit self; 25 + ctx = { }; 26 + }; 27 + in 28 + { 29 + expr = builtins.length (result.state.imports null); 30 + expected = 1; 31 + } 32 + ); 33 + 34 + # Root + child: both class modules collected. 35 + test-root-and-child-modules = denTest ( 36 + { den, ... }: 37 + let 38 + self = { 39 + name = "host"; 40 + meta = { }; 41 + nixos = { 42 + a = 1; 43 + }; 44 + includes = [ 45 + { 46 + name = "child"; 47 + meta = { }; 48 + nixos = { 49 + b = 2; 50 + }; 51 + includes = [ ]; 52 + } 53 + ]; 54 + }; 55 + result = den.lib.aspects.fx.pipeline.fxFullResolve { 56 + class = "nixos"; 57 + inherit self; 58 + ctx = { }; 59 + }; 60 + in 61 + { 62 + expr = builtins.length (result.state.imports null); 63 + expected = 2; 64 + } 65 + ); 66 + 67 + # fxResolve returns { imports } shape. 68 + test-fxResolve-shape = denTest ( 69 + { den, ... }: 70 + let 71 + self = { 72 + name = "host"; 73 + meta = { }; 74 + nixos = { 75 + a = 1; 76 + }; 77 + includes = [ ]; 78 + }; 79 + result = den.lib.aspects.fx.pipeline.fxResolve { 80 + class = "nixos"; 81 + inherit self; 82 + ctx = { }; 83 + }; 84 + in 85 + { 86 + expr = result ? imports && builtins.isList result.imports; 87 + expected = true; 88 + } 89 + ); 90 + 91 + # Constraint (exclude) through full pipeline. 92 + test-adapter-through-pipeline = denTest ( 93 + { den, ... }: 94 + let 95 + target = { 96 + name = "drop"; 97 + meta.provider = [ ]; 98 + }; 99 + self = { 100 + name = "host"; 101 + meta = { 102 + handleWith = den.lib.aspects.fx.constraints.exclude target; 103 + }; 104 + includes = [ 105 + { 106 + name = "keep"; 107 + meta.provider = [ ]; 108 + nixos = { 109 + a = 1; 110 + }; 111 + includes = [ ]; 112 + } 113 + { 114 + name = "drop"; 115 + meta.provider = [ ]; 116 + nixos = { 117 + b = 2; 118 + }; 119 + includes = [ ]; 120 + } 121 + ]; 122 + }; 123 + result = den.lib.aspects.fx.pipeline.fxResolve { 124 + class = "nixos"; 125 + inherit self; 126 + ctx = { }; 127 + }; 128 + in 129 + { 130 + expr = builtins.length result.imports; 131 + expected = 1; 132 + } 133 + ); 134 + 135 + # Parametric child through full pipeline. 136 + test-parametric-through-pipeline = denTest ( 137 + { den, ... }: 138 + let 139 + self = { 140 + name = "host"; 141 + meta = { }; 142 + includes = [ 143 + { 144 + name = "web"; 145 + meta = { }; 146 + __functor = 147 + _: 148 + { host }: 149 + { 150 + nixos.hostName = host; 151 + }; 152 + __functionArgs = { 153 + host = false; 154 + }; 155 + includes = [ ]; 156 + } 157 + ]; 158 + }; 159 + result = den.lib.aspects.fx.pipeline.fxFullResolve { 160 + class = "nixos"; 161 + inherit self; 162 + ctx = { 163 + host = "igloo"; 164 + }; 165 + }; 166 + in 167 + { 168 + expr = builtins.length (result.state.imports null); 169 + expected = 1; 170 + } 171 + ); 172 + 173 + }; 174 + }
+251
templates/ci/modules/features/fx-handlers.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-handlers = { 9 + 10 + # constantHandler resumes with ctx value for known arg. 11 + test-parametric-handler-provides-value = denTest ( 12 + { den, ... }: 13 + let 14 + fx = den.lib.fx; 15 + ctx = { 16 + host = "igloo"; 17 + user = "tux"; 18 + }; 19 + handlers = builtins.mapAttrs ( 20 + name: value: 21 + { param, state }: 22 + { 23 + resume = value; 24 + inherit state; 25 + } 26 + ) ctx; 27 + comp = fx.send "host" false; 28 + result = fx.handle { 29 + inherit handlers; 30 + state = { }; 31 + } comp; 32 + in 33 + { 34 + expr = result.value; 35 + expected = "igloo"; 36 + } 37 + ); 38 + 39 + # constantHandler provides class. 40 + test-static-handler-provides-class = denTest ( 41 + { den, ... }: 42 + let 43 + fx = den.lib.fx; 44 + handlers = { 45 + "class" = 46 + { param, state }: 47 + { 48 + resume = "nixos"; 49 + inherit state; 50 + }; 51 + }; 52 + comp = fx.send "class" false; 53 + result = fx.handle { 54 + inherit handlers; 55 + state = { }; 56 + } comp; 57 + in 58 + { 59 + expr = result.value; 60 + expected = "nixos"; 61 + } 62 + ); 63 + 64 + # Combined handlers: constantHandler merges ctx + static in one handle call. 65 + test-combined-handlers = denTest ( 66 + { den, ... }: 67 + let 68 + fx = den.lib.fx; 69 + ctx = { 70 + host = "igloo"; 71 + }; 72 + parametric = builtins.mapAttrs ( 73 + _: v: 74 + { param, state }: 75 + { 76 + resume = v; 77 + inherit state; 78 + } 79 + ) ctx; 80 + static = { 81 + "class" = 82 + { param, state }: 83 + { 84 + resume = "nixos"; 85 + inherit state; 86 + }; 87 + }; 88 + aspect = 89 + { host, class }: 90 + { 91 + hostName = host; 92 + cls = class; 93 + }; 94 + comp = fx.bind.fn { } aspect; 95 + result = fx.handle { 96 + handlers = parametric // static; 97 + state = { }; 98 + } comp; 99 + in 100 + { 101 + expr = result.value; 102 + expected = { 103 + hostName = "igloo"; 104 + cls = "nixos"; 105 + }; 106 + } 107 + ); 108 + 109 + # Two-layer topology: rotate handles known, outer catches unknown. 110 + test-rotate-unknown-to-outer = denTest ( 111 + { den, ... }: 112 + let 113 + fx = den.lib.fx; 114 + ctx = { 115 + host = "igloo"; 116 + }; 117 + parametric = builtins.mapAttrs ( 118 + _: v: 119 + { param, state }: 120 + { 121 + resume = v; 122 + inherit state; 123 + } 124 + ) ctx; 125 + aspect = 126 + { host, missing-arg }: 127 + { 128 + inherit host missing-arg; 129 + }; 130 + comp = fx.bind.fn { } aspect; 131 + inner = fx.rotate { 132 + handlers = parametric; 133 + state = { }; 134 + } comp; 135 + result = fx.handle { 136 + handlers."missing-arg" = 137 + { param, state }: 138 + { 139 + resume = "caught"; 140 + inherit state; 141 + }; 142 + state = { }; 143 + } inner; 144 + in 145 + { 146 + expr = result.value.value; 147 + expected = { 148 + host = "igloo"; 149 + missing-arg = "caught"; 150 + }; 151 + } 152 + ); 153 + 154 + # constantHandler merges ctx values. 155 + test-constantHandler-denTest = denTest ( 156 + { den, ... }: 157 + let 158 + fx = den.lib.fx; 159 + handlers = den.lib.aspects.fx.handlers.constantHandler { 160 + host = "igloo"; 161 + class = "nixos"; 162 + }; 163 + aspect = 164 + { host, class }: 165 + { 166 + hostName = host; 167 + cls = class; 168 + }; 169 + comp = fx.bind.fn { } aspect; 170 + result = fx.handle { 171 + inherit handlers; 172 + state = { }; 173 + } comp; 174 + in 175 + { 176 + expr = result.value; 177 + expected = { 178 + hostName = "igloo"; 179 + cls = "nixos"; 180 + }; 181 + } 182 + ); 183 + 184 + # chainHandler: push appends identity to includesChain. 185 + test-chain-push-appends = denTest ( 186 + { den, ... }: 187 + let 188 + fx = den.lib.fx; 189 + comp = fx.bind (fx.send "chain-push" { identity = "a"; }) ( 190 + _: fx.send "chain-push" { identity = "b"; } 191 + ); 192 + result = fx.handle { 193 + handlers = den.lib.aspects.fx.handlers.chainHandler; 194 + state = { 195 + includesChain = [ ]; 196 + }; 197 + } comp; 198 + in 199 + { 200 + expr = result.state.includesChain; 201 + expected = [ 202 + "a" 203 + "b" 204 + ]; 205 + } 206 + ); 207 + 208 + # chainHandler: pop removes last element. 209 + test-chain-pop-removes-last = denTest ( 210 + { den, ... }: 211 + let 212 + fx = den.lib.fx; 213 + comp = fx.bind (fx.send "chain-push" { identity = "a"; }) ( 214 + _: fx.bind (fx.send "chain-push" { identity = "b"; }) (_: fx.send "chain-pop" null) 215 + ); 216 + result = fx.handle { 217 + handlers = den.lib.aspects.fx.handlers.chainHandler; 218 + state = { 219 + includesChain = [ ]; 220 + }; 221 + } comp; 222 + in 223 + { 224 + expr = result.state.includesChain; 225 + expected = [ "a" ]; 226 + } 227 + ); 228 + 229 + # chainHandler: pop on empty list throws (push/pop mismatch). 230 + test-chain-pop-empty-throws = denTest ( 231 + { den, ... }: 232 + let 233 + fx = den.lib.fx; 234 + comp = fx.send "chain-pop" null; 235 + raw = fx.handle { 236 + handlers = den.lib.aspects.fx.handlers.chainHandler; 237 + state = { 238 + includesChain = [ ]; 239 + }; 240 + } comp; 241 + # Force the includesChain thunk inside tryEval to catch the throw. 242 + result = builtins.tryEval (builtins.deepSeq raw.state.includesChain raw.state.includesChain); 243 + in 244 + { 245 + expr = result.success; 246 + expected = false; 247 + } 248 + ); 249 + 250 + }; 251 + }
+102
templates/ci/modules/features/fx-identity.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-identity = { 9 + 10 + test-aspectPath-with-provider = denTest ( 11 + { den, ... }: 12 + let 13 + a = { 14 + name = "sub"; 15 + meta = { 16 + provider = [ "monitoring" ]; 17 + }; 18 + }; 19 + in 20 + { 21 + expr = den.lib.aspects.fx.identity.aspectPath a; 22 + expected = [ 23 + "monitoring" 24 + "sub" 25 + ]; 26 + } 27 + ); 28 + 29 + test-aspectPath-no-provider = denTest ( 30 + { den, ... }: 31 + let 32 + a = { 33 + name = "base"; 34 + meta = { }; 35 + }; 36 + in 37 + { 38 + expr = den.lib.aspects.fx.identity.aspectPath a; 39 + expected = [ "base" ]; 40 + } 41 + ); 42 + 43 + test-pathKey = denTest ( 44 + { den, ... }: 45 + { 46 + expr = den.lib.aspects.fx.identity.pathKey [ 47 + "monitoring" 48 + "sub" 49 + ]; 50 + expected = "monitoring/sub"; 51 + } 52 + ); 53 + 54 + test-toPathSet = denTest ( 55 + { den, ... }: 56 + { 57 + expr = den.lib.aspects.fx.identity.toPathSet [ 58 + [ "a" ] 59 + [ 60 + "b" 61 + "c" 62 + ] 63 + ]; 64 + expected = { 65 + "a" = true; 66 + "b/c" = true; 67 + }; 68 + } 69 + ); 70 + 71 + test-tombstone-shape = denTest ( 72 + { den, ... }: 73 + let 74 + a = { 75 + name = "drop"; 76 + meta = { 77 + provider = [ ]; 78 + }; 79 + includes = [ "x" ]; 80 + }; 81 + ts = den.lib.aspects.fx.identity.tombstone a { excludedFrom = "parent"; }; 82 + in 83 + { 84 + expr = { 85 + name = ts.name; 86 + excluded = ts.meta.excluded; 87 + originalName = ts.meta.originalName; 88 + excludedFrom = ts.meta.excludedFrom; 89 + includes = ts.includes; 90 + }; 91 + expected = { 92 + name = "~drop"; 93 + excluded = true; 94 + originalName = "drop"; 95 + excludedFrom = "parent"; 96 + includes = [ ]; 97 + }; 98 + } 99 + ); 100 + 101 + }; 102 + }
+226
templates/ci/modules/features/fx-includeIf.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-includeIf = { 9 + 10 + test-guard-passes-includes = denTest ( 11 + { den, ... }: 12 + let 13 + fx = den.lib.fx; 14 + target = { 15 + name = "feature"; 16 + meta = { 17 + provider = [ ]; 18 + }; 19 + nixos = { 20 + a = 1; 21 + }; 22 + includes = [ ]; 23 + }; 24 + guarded = den.lib.aspects.fx.includes.includeIf (_: true) [ target ]; 25 + parent = { 26 + name = "root"; 27 + meta = { }; 28 + includes = [ guarded ]; 29 + }; 30 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 31 + result = fx.handle { 32 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 33 + class = "nixos"; 34 + ctx = { }; 35 + }; 36 + state = den.lib.aspects.fx.pipeline.defaultState; 37 + } comp; 38 + in 39 + { 40 + expr = (builtins.head result.value.includes).name; 41 + expected = "feature"; 42 + } 43 + ); 44 + 45 + test-guard-fails-tombstones = denTest ( 46 + { den, ... }: 47 + let 48 + fx = den.lib.fx; 49 + target = { 50 + name = "feature"; 51 + meta = { 52 + provider = [ ]; 53 + }; 54 + includes = [ ]; 55 + }; 56 + guarded = den.lib.aspects.fx.includes.includeIf (_: false) [ target ]; 57 + parent = { 58 + name = "root"; 59 + meta = { }; 60 + includes = [ guarded ]; 61 + }; 62 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 63 + result = fx.handle { 64 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 65 + class = "nixos"; 66 + ctx = { }; 67 + }; 68 + state = den.lib.aspects.fx.pipeline.defaultState; 69 + } comp; 70 + in 71 + { 72 + expr = (builtins.head result.value.includes).meta.excluded; 73 + expected = true; 74 + } 75 + ); 76 + 77 + test-hasAspect-guard = denTest ( 78 + { den, ... }: 79 + let 80 + fx = den.lib.fx; 81 + sops = { 82 + name = "sops"; 83 + meta = { 84 + provider = [ ]; 85 + }; 86 + includes = [ ]; 87 + }; 88 + sopsConf = { 89 + name = "sops-conf"; 90 + meta = { 91 + provider = [ ]; 92 + }; 93 + nixos = { 94 + sops = true; 95 + }; 96 + includes = [ ]; 97 + }; 98 + guarded = den.lib.aspects.fx.includes.includeIf (ctx: ctx.hasAspect sops) [ sopsConf ]; 99 + parent = { 100 + name = "root"; 101 + meta = { }; 102 + includes = [ 103 + sops 104 + guarded 105 + ]; 106 + }; 107 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 108 + result = fx.handle { 109 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 110 + class = "nixos"; 111 + ctx = { }; 112 + }; 113 + state = den.lib.aspects.fx.pipeline.defaultState; 114 + } comp; 115 + names = map (c: c.name) result.value.includes; 116 + in 117 + { 118 + expr = builtins.elem "sops-conf" names; 119 + expected = true; 120 + } 121 + ); 122 + 123 + test-hasAspect-guard-fails = denTest ( 124 + { den, ... }: 125 + let 126 + fx = den.lib.fx; 127 + sops = { 128 + name = "sops"; 129 + meta = { 130 + provider = [ ]; 131 + }; 132 + includes = [ ]; 133 + }; 134 + sopsConf = { 135 + name = "sops-conf"; 136 + meta = { 137 + provider = [ ]; 138 + }; 139 + includes = [ ]; 140 + }; 141 + guarded = den.lib.aspects.fx.includes.includeIf (ctx: ctx.hasAspect sops) [ sopsConf ]; 142 + # sops is NOT in includes — guard should fail 143 + parent = { 144 + name = "root"; 145 + meta = { }; 146 + includes = [ guarded ]; 147 + }; 148 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 149 + result = fx.handle { 150 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 151 + class = "nixos"; 152 + ctx = { }; 153 + }; 154 + state = den.lib.aspects.fx.pipeline.defaultState; 155 + } comp; 156 + in 157 + { 158 + expr = (builtins.head result.value.includes).meta.excluded; 159 + expected = true; 160 + } 161 + ); 162 + 163 + test-fallback-pattern = denTest ( 164 + { den, ... }: 165 + let 166 + fx = den.lib.fx; 167 + sops = { 168 + name = "sops"; 169 + meta = { 170 + provider = [ ]; 171 + }; 172 + includes = [ ]; 173 + }; 174 + sopsConf = { 175 + name = "sops-conf"; 176 + meta = { 177 + provider = [ ]; 178 + }; 179 + nixos = { }; 180 + includes = [ ]; 181 + }; 182 + ageConf = { 183 + name = "age-conf"; 184 + meta = { 185 + provider = [ ]; 186 + }; 187 + nixos = { }; 188 + includes = [ ]; 189 + }; 190 + parent = { 191 + name = "root"; 192 + meta = { }; 193 + includes = [ 194 + sops 195 + (den.lib.aspects.fx.includes.includeIf (ctx: ctx.hasAspect sops) [ sopsConf ]) 196 + (den.lib.aspects.fx.includes.includeIf (ctx: !ctx.hasAspect sops) [ ageConf ]) 197 + ]; 198 + }; 199 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 200 + result = fx.handle { 201 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 202 + class = "nixos"; 203 + ctx = { }; 204 + }; 205 + state = den.lib.aspects.fx.pipeline.defaultState; 206 + } comp; 207 + children = result.value.includes; 208 + names = map (c: c.name) children; 209 + in 210 + { 211 + expr = { 212 + hasSopsConf = builtins.elem "sops-conf" names; 213 + hasAgeConf = builtins.elem "age-conf" names; 214 + ageExcluded = 215 + (lib.findFirst (c: c.name == "~age-conf") { meta = { }; } children).meta.excluded or false; 216 + }; 217 + expected = { 218 + hasSopsConf = true; 219 + hasAgeConf = false; 220 + ageExcluded = true; 221 + }; 222 + } 223 + ); 224 + 225 + }; 226 + }
+63
templates/ci/modules/features/fx-integration.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-integration = { 9 + 10 + # Real den aspect resolves through fx pipeline. 11 + test-real-aspect-through-fx = denTest ( 12 + { den, igloo, ... }: 13 + { 14 + den.fxPipeline = true; 15 + den.hosts.x86_64-linux.igloo.users.tux = { }; 16 + den.aspects.igloo.nixos = 17 + { ... }: 18 + { 19 + networking.hostName = "fx-test"; 20 + }; 21 + expr = igloo.networking.hostName; 22 + expected = "fx-test"; 23 + } 24 + ); 25 + 26 + # Parametric aspect through fx pipeline. 27 + test-parametric-through-fx = denTest ( 28 + { den, igloo, ... }: 29 + { 30 + den.fxPipeline = true; 31 + den.hosts.x86_64-linux.igloo.users.tux = { }; 32 + den.aspects.web = 33 + { host, ... }: 34 + { 35 + nixos = 36 + { ... }: 37 + { 38 + networking.hostName = host.name; 39 + }; 40 + }; 41 + den.aspects.igloo.includes = [ den.aspects.web ]; 42 + expr = igloo.networking.hostName; 43 + expected = "igloo"; 44 + } 45 + ); 46 + 47 + # Flag off uses old pipeline — existing behavior preserved. 48 + test-flag-off-uses-legacy = denTest ( 49 + { den, igloo, ... }: 50 + { 51 + den.hosts.x86_64-linux.igloo.users.tux = { }; 52 + den.aspects.igloo.nixos = 53 + { ... }: 54 + { 55 + networking.hostName = "legacy"; 56 + }; 57 + expr = igloo.networking.hostName; 58 + expected = "legacy"; 59 + } 60 + ); 61 + 62 + }; 63 + }
+140
templates/ci/modules/features/fx-parametric-meta.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-parametric-meta = { 9 + 10 + # Parametric aspect resolves functor args through the pipeline. 11 + test-parametric-resolves-args = denTest ( 12 + { den, ... }: 13 + let 14 + fx = den.lib.fx; 15 + aspect = { 16 + name = "web"; 17 + meta = { }; 18 + __functor = 19 + _: 20 + { host, user }: 21 + { 22 + nixos = { 23 + hostName = host; 24 + userName = user; 25 + }; 26 + }; 27 + __functionArgs = { 28 + host = false; 29 + user = false; 30 + }; 31 + includes = [ ]; 32 + }; 33 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 34 + result = fx.handle { 35 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 36 + class = "nixos"; 37 + ctx = { 38 + host = "h"; 39 + user = "u"; 40 + }; 41 + }; 42 + state = den.lib.aspects.fx.pipeline.defaultState; 43 + } comp; 44 + in 45 + { 46 + expr = { 47 + hostName = result.value.nixos.hostName; 48 + userName = result.value.nixos.userName; 49 + name = result.value.name; 50 + }; 51 + expected = { 52 + hostName = "h"; 53 + userName = "u"; 54 + name = "web"; 55 + }; 56 + } 57 + ); 58 + 59 + # Static aspect has no parametric meta. 60 + test-static-no-parametric-meta = denTest ( 61 + { den, ... }: 62 + let 63 + fx = den.lib.fx; 64 + aspect = { 65 + name = "base"; 66 + meta = { }; 67 + nixos = { }; 68 + includes = [ ]; 69 + }; 70 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 71 + result = fx.handle { 72 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 73 + class = "nixos"; 74 + ctx = { }; 75 + }; 76 + state = den.lib.aspects.fx.pipeline.defaultState; 77 + } comp; 78 + in 79 + { 80 + expr = result.value.meta ? isParametric; 81 + expected = false; 82 + } 83 + ); 84 + 85 + # resolve-complete carries parent info from includesChain. 86 + test-resolve-complete-has-parent = denTest ( 87 + { den, ... }: 88 + let 89 + fx = den.lib.fx; 90 + parent = { 91 + name = "root"; 92 + meta = { 93 + provider = [ ]; 94 + }; 95 + includes = [ 96 + { 97 + name = "child"; 98 + meta = { 99 + provider = [ ]; 100 + }; 101 + includes = [ ]; 102 + } 103 + ]; 104 + }; 105 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 106 + result = fx.handle { 107 + handlers = 108 + den.lib.aspects.fx.pipeline.composeHandlers 109 + (den.lib.aspects.fx.pipeline.defaultHandlers { 110 + class = "nixos"; 111 + ctx = { }; 112 + }) 113 + { 114 + "resolve-complete" = 115 + { param, state }: 116 + let 117 + chain = state.includesChain or [ ]; 118 + parentName = if chain == [ ] then "ROOT" else lib.last chain; 119 + in 120 + { 121 + resume = param; 122 + state = state // { 123 + parents = (state.parents or [ ]) ++ [ parentName ]; 124 + }; 125 + }; 126 + }; 127 + state = den.lib.aspects.fx.pipeline.defaultState // { 128 + parents = [ ]; 129 + }; 130 + } comp; 131 + in 132 + { 133 + # child's parent should be "root" (derived from includesChain) 134 + expr = builtins.elem "root" result.state.parents; 135 + expected = true; 136 + } 137 + ); 138 + 139 + }; 140 + }
+207
templates/ci/modules/features/fx-regressions.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-regressions = { 9 + 10 + # #413/#423: Provider sub-aspect's includes contain parametric fns. 11 + # Old pipeline: context dropped during recursive descent. 12 + # Effects: each level independently sends what it needs. 13 + test-provider-sub-includes-get-context = denTest ( 14 + { den, ... }: 15 + let 16 + fx = den.lib.fx; 17 + inner = 18 + { host }: 19 + { 20 + nixos.networking.hostName = host; 21 + }; 22 + provider = { 23 + name = "monitoring"; 24 + meta = { 25 + provider = [ ]; 26 + }; 27 + includes = [ inner ]; 28 + }; 29 + comp = den.lib.aspects.fx.aspect.aspectToEffect provider; 30 + result = fx.handle { 31 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 32 + class = "nixos"; 33 + ctx = { 34 + host = "igloo"; 35 + }; 36 + }; 37 + state = den.lib.aspects.fx.pipeline.defaultState; 38 + } comp; 39 + child = builtins.head result.value.includes; 40 + in 41 + { 42 + expr = child.nixos.networking.hostName; 43 + expected = "igloo"; 44 + } 45 + ); 46 + 47 + # #426: Static sub inside parametric parent. applyDeep dropped static subs. 48 + # Effects: static subs have no parametric args, body passes through. 49 + test-static-sub-preserves-owned = denTest ( 50 + { den, ... }: 51 + let 52 + fx = den.lib.fx; 53 + staticBase = { 54 + name = "base"; 55 + meta = { }; 56 + nixos = { 57 + programs.git.enable = true; 58 + }; 59 + includes = [ ]; 60 + }; 61 + parametricDev = { 62 + name = "dev"; 63 + meta = { }; 64 + __functor = 65 + _: 66 + { user }: 67 + { 68 + includes = [ staticBase ]; 69 + }; 70 + __functionArgs = { 71 + user = false; 72 + }; 73 + includes = [ ]; 74 + }; 75 + comp = den.lib.aspects.fx.aspect.aspectToEffect parametricDev; 76 + result = fx.handle { 77 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 78 + class = "nixos"; 79 + ctx = { 80 + user = "tux"; 81 + }; 82 + }; 83 + state = den.lib.aspects.fx.pipeline.defaultState; 84 + } comp; 85 + child = builtins.head result.value.includes; 86 + in 87 + { 88 + expr = child.nixos.programs.git.enable; 89 + expected = true; 90 + } 91 + ); 92 + 93 + # #437: Factory function resolved as static (pre-applied by user). 94 + test-factory-resolves-as-static = denTest ( 95 + { den, ... }: 96 + let 97 + fx = den.lib.fx; 98 + factoryResult = { 99 + name = "greeter"; 100 + meta = { }; 101 + nixos = { 102 + users.users.tux.description = "hello"; 103 + }; 104 + includes = [ ]; 105 + }; 106 + comp = den.lib.aspects.fx.aspect.aspectToEffect factoryResult; 107 + result = fx.handle { 108 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 109 + class = "nixos"; 110 + ctx = { }; 111 + }; 112 + state = den.lib.aspects.fx.pipeline.defaultState; 113 + } comp; 114 + in 115 + { 116 + expr = result.value.nixos.users.users.tux.description; 117 + expected = "hello"; 118 + } 119 + ); 120 + 121 + # Meta carryover: meta.provider survives deep resolution. 122 + test-meta-provider-survives = denTest ( 123 + { den, ... }: 124 + let 125 + fx = den.lib.fx; 126 + child = { 127 + name = "sub"; 128 + meta = { 129 + provider = [ "monitoring" ]; 130 + }; 131 + nixos = { }; 132 + includes = [ ]; 133 + }; 134 + parent = { 135 + name = "monitoring"; 136 + meta = { 137 + provider = [ ]; 138 + }; 139 + includes = [ child ]; 140 + }; 141 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 142 + result = fx.handle { 143 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 144 + class = "nixos"; 145 + ctx = { }; 146 + }; 147 + state = den.lib.aspects.fx.pipeline.defaultState; 148 + } comp; 149 + childResult = builtins.head result.value.includes; 150 + in 151 + { 152 + expr = childResult.meta.provider; 153 + expected = [ "monitoring" ]; 154 + } 155 + ); 156 + 157 + # 3-level deep nesting with parametric at each level. 158 + test-three-level-deep-parametric = denTest ( 159 + { den, ... }: 160 + let 161 + fx = den.lib.fx; 162 + leaf = 163 + { host }: 164 + { 165 + nixos.networking.hostName = host; 166 + }; 167 + mid = { 168 + name = "mid"; 169 + meta = { }; 170 + __functor = 171 + _: 172 + { user }: 173 + { 174 + includes = [ leaf ]; 175 + }; 176 + __functionArgs = { 177 + user = false; 178 + }; 179 + includes = [ ]; 180 + }; 181 + root = { 182 + name = "root"; 183 + meta = { }; 184 + includes = [ mid ]; 185 + }; 186 + comp = den.lib.aspects.fx.aspect.aspectToEffect root; 187 + result = fx.handle { 188 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 189 + class = "nixos"; 190 + ctx = { 191 + host = "igloo"; 192 + user = "tux"; 193 + }; 194 + }; 195 + state = den.lib.aspects.fx.pipeline.defaultState; 196 + } comp; 197 + midResult = builtins.head result.value.includes; 198 + leafResult = builtins.head midResult.includes; 199 + in 200 + { 201 + expr = leafResult.nixos.networking.hostName; 202 + expected = "igloo"; 203 + } 204 + ); 205 + 206 + }; 207 + }
+533
templates/ci/modules/features/fx-resolve.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-resolve = { 9 + 10 + # Static aspect resolves with identity envelope through the pipeline. 11 + test-static-resolves-with-envelope = denTest ( 12 + { den, ... }: 13 + let 14 + fx = den.lib.fx; 15 + aspect = { 16 + name = "base"; 17 + meta = { }; 18 + nixos = { 19 + networking.hostName = "test"; 20 + }; 21 + includes = [ ]; 22 + }; 23 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 24 + result = fx.handle { 25 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 26 + class = "nixos"; 27 + ctx = { }; 28 + }; 29 + state = den.lib.aspects.fx.pipeline.defaultState; 30 + } comp; 31 + in 32 + { 33 + expr = { 34 + name = result.value.name; 35 + hasNixos = result.value ? nixos; 36 + includes = result.value.includes; 37 + }; 38 + expected = { 39 + name = "base"; 40 + hasNixos = true; 41 + includes = [ ]; 42 + }; 43 + } 44 + ); 45 + 46 + # Parametric aspect receives host from ctx via effect handler. 47 + test-parametric-single-arg = denTest ( 48 + { den, ... }: 49 + let 50 + fx = den.lib.fx; 51 + aspect = { 52 + name = "web"; 53 + meta = { }; 54 + __functor = 55 + _: 56 + { host }: 57 + { 58 + nixos.networking.hostName = host; 59 + }; 60 + __functionArgs = { 61 + host = false; 62 + }; 63 + includes = [ ]; 64 + }; 65 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 66 + result = fx.handle { 67 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 68 + class = "nixos"; 69 + ctx = { 70 + host = "igloo"; 71 + }; 72 + }; 73 + state = den.lib.aspects.fx.pipeline.defaultState; 74 + } comp; 75 + in 76 + { 77 + expr = result.value.nixos.networking.hostName; 78 + expected = "igloo"; 79 + } 80 + ); 81 + 82 + # Nested includes: parent has parametric child. 83 + test-nested-parametric-includes = denTest ( 84 + { den, ... }: 85 + let 86 + fx = den.lib.fx; 87 + child = { 88 + name = "child"; 89 + meta = { }; 90 + __functor = 91 + _: 92 + { host }: 93 + { 94 + nixos.networking.hostName = host; 95 + }; 96 + __functionArgs = { 97 + host = false; 98 + }; 99 + includes = [ ]; 100 + }; 101 + parent = { 102 + name = "parent"; 103 + meta = { }; 104 + nixos = { }; 105 + includes = [ child ]; 106 + }; 107 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 108 + result = fx.handle { 109 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 110 + class = "nixos"; 111 + ctx = { 112 + host = "igloo"; 113 + }; 114 + }; 115 + state = den.lib.aspects.fx.pipeline.defaultState; 116 + } comp; 117 + in 118 + { 119 + expr = (builtins.head result.value.includes).nixos.networking.hostName; 120 + expected = "igloo"; 121 + } 122 + ); 123 + 124 + # Static sub inside parametric parent preserves owned config. 125 + test-static-sub-in-parametric-parent = denTest ( 126 + { den, ... }: 127 + let 128 + fx = den.lib.fx; 129 + staticChild = { 130 + name = "base"; 131 + meta = { }; 132 + nixos = { 133 + programs.git.enable = true; 134 + }; 135 + includes = [ ]; 136 + }; 137 + parent = { 138 + name = "dev"; 139 + meta = { }; 140 + __functor = 141 + _: 142 + { user }: 143 + { 144 + includes = [ staticChild ]; 145 + }; 146 + __functionArgs = { 147 + user = false; 148 + }; 149 + includes = [ ]; 150 + }; 151 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 152 + result = fx.handle { 153 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 154 + class = "nixos"; 155 + ctx = { 156 + user = "tux"; 157 + }; 158 + }; 159 + state = den.lib.aspects.fx.pipeline.defaultState; 160 + } comp; 161 + childResult = builtins.head result.value.includes; 162 + in 163 + { 164 + expr = childResult.nixos.programs.git.enable; 165 + expected = true; 166 + } 167 + ); 168 + 169 + # Owned stripping: structural keys separated from config keys. 170 + test-owned-stripping-static = denTest ( 171 + { den, ... }: 172 + let 173 + fx = den.lib.fx; 174 + aspect = { 175 + name = "test"; 176 + meta = { }; 177 + nixos = { 178 + enable = true; 179 + }; 180 + includes = [ ]; 181 + }; 182 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 183 + result = 184 + (fx.handle { 185 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 186 + class = "nixos"; 187 + ctx = { }; 188 + }; 189 + state = den.lib.aspects.fx.pipeline.defaultState; 190 + } comp).value; 191 + in 192 + { 193 + expr = { 194 + hasNixos = result ? nixos; 195 + hasName = result ? name; 196 + hasMeta = result ? meta; 197 + hasIncludes = result ? includes; 198 + nixosVal = result.nixos; 199 + }; 200 + expected = { 201 + hasNixos = true; 202 + hasName = true; 203 + hasMeta = true; 204 + hasIncludes = true; 205 + nixosVal = { 206 + enable = true; 207 + }; 208 + }; 209 + } 210 + ); 211 + 212 + # Parametric aspect: __functor and __functionArgs are NOT in resolved output. 213 + test-owned-stripping-parametric = denTest ( 214 + { den, ... }: 215 + let 216 + fx = den.lib.fx; 217 + aspect = { 218 + name = "test"; 219 + meta = { }; 220 + __functor = 221 + _: 222 + { host }: 223 + { 224 + nixos = { 225 + hostName = host; 226 + }; 227 + }; 228 + __functionArgs = { 229 + host = false; 230 + }; 231 + includes = [ ]; 232 + }; 233 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 234 + result = 235 + (fx.handle { 236 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 237 + class = "nixos"; 238 + ctx = { 239 + host = "igloo"; 240 + }; 241 + }; 242 + state = den.lib.aspects.fx.pipeline.defaultState; 243 + } comp).value; 244 + in 245 + { 246 + expr = { 247 + hasFunctor = result ? __functor; 248 + hasFunctionArgs = result ? __functionArgs; 249 + hasNixos = result ? nixos; 250 + hasName = result ? name; 251 + }; 252 + expected = { 253 + hasFunctor = false; 254 + hasFunctionArgs = false; 255 + hasNixos = true; 256 + hasName = true; 257 + }; 258 + } 259 + ); 260 + 261 + # Mixed static and parametric includes resolve correctly. 262 + test-mixed-static-parametric-includes = denTest ( 263 + { den, ... }: 264 + let 265 + fx = den.lib.fx; 266 + staticChild = { 267 + name = "static-child"; 268 + meta = { }; 269 + nixos = { 270 + a = 1; 271 + }; 272 + includes = [ ]; 273 + }; 274 + parametricChild = { 275 + name = "param-child"; 276 + meta = { }; 277 + __functor = 278 + _: 279 + { host }: 280 + { 281 + nixos = { 282 + hostName = host; 283 + }; 284 + }; 285 + __functionArgs = { 286 + host = false; 287 + }; 288 + includes = [ ]; 289 + }; 290 + parent = { 291 + name = "parent"; 292 + meta = { }; 293 + includes = [ 294 + staticChild 295 + parametricChild 296 + ]; 297 + }; 298 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 299 + result = fx.handle { 300 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 301 + class = "nixos"; 302 + ctx = { 303 + host = "igloo"; 304 + }; 305 + }; 306 + state = den.lib.aspects.fx.pipeline.defaultState; 307 + } comp; 308 + children = result.value.includes; 309 + in 310 + { 311 + expr = { 312 + staticA = (builtins.elemAt children 0).nixos.a; 313 + paramHost = (builtins.elemAt children 1).nixos.hostName; 314 + }; 315 + expected = { 316 + staticA = 1; 317 + paramHost = "igloo"; 318 + }; 319 + } 320 + ); 321 + 322 + # Pipeline produces consistent results across invocations. 323 + test-pipeline-consistent-results = denTest ( 324 + { den, ... }: 325 + let 326 + fx = den.lib.fx; 327 + aspect = { 328 + name = "web"; 329 + meta = { }; 330 + __functor = 331 + _: 332 + { host }: 333 + { 334 + nixos.networking.hostName = host; 335 + }; 336 + __functionArgs = { 337 + host = false; 338 + }; 339 + includes = [ ]; 340 + }; 341 + ctx = { 342 + host = "igloo"; 343 + }; 344 + comp1 = den.lib.aspects.fx.aspect.aspectToEffect aspect; 345 + result1 = 346 + (fx.handle { 347 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 348 + class = "nixos"; 349 + inherit ctx; 350 + }; 351 + state = den.lib.aspects.fx.pipeline.defaultState; 352 + } comp1).value; 353 + 354 + comp2 = den.lib.aspects.fx.aspect.aspectToEffect aspect; 355 + result2 = 356 + (fx.handle { 357 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 358 + class = "nixos"; 359 + inherit ctx; 360 + }; 361 + state = den.lib.aspects.fx.pipeline.defaultState; 362 + } comp2).value; 363 + in 364 + { 365 + expr = result1.nixos.networking.hostName == result2.nixos.networking.hostName; 366 + expected = true; 367 + } 368 + ); 369 + 370 + # rotate topology: multi-arg aspect resolves correctly. 371 + test-rotate-multi-arg = denTest ( 372 + { den, ... }: 373 + let 374 + fx = den.lib.fx; 375 + aspect = { 376 + name = "multi"; 377 + meta = { }; 378 + __functor = 379 + _: 380 + { host, user }: 381 + { 382 + nixos.networking.hostName = host; 383 + home.username = user; 384 + }; 385 + __functionArgs = { 386 + host = false; 387 + user = false; 388 + }; 389 + includes = [ ]; 390 + }; 391 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 392 + result = 393 + (fx.handle { 394 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 395 + class = "nixos"; 396 + ctx = { 397 + host = "igloo"; 398 + user = "tux"; 399 + }; 400 + }; 401 + state = den.lib.aspects.fx.pipeline.defaultState; 402 + } comp).value; 403 + in 404 + { 405 + expr = { 406 + hostName = result.nixos.networking.hostName; 407 + username = result.home.username; 408 + }; 409 + expected = { 410 + hostName = "igloo"; 411 + username = "tux"; 412 + }; 413 + } 414 + ); 415 + 416 + # Missing required arg produces readable error. 417 + test-missing-arg-throws = denTest ( 418 + { den, ... }: 419 + let 420 + fx = den.lib.fx; 421 + aspect = { 422 + name = "broken"; 423 + meta = { }; 424 + __functor = 425 + _: 426 + { host }: 427 + { 428 + nixos.networking.hostName = host; 429 + }; 430 + __functionArgs = { 431 + host = false; 432 + }; 433 + includes = [ ]; 434 + }; 435 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 436 + in 437 + { 438 + expectedError = { 439 + type = "ThrownError"; 440 + msg = "host"; 441 + }; 442 + expr = fx.handle { 443 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 444 + class = "nixos"; 445 + ctx = { 446 + user = "tux"; 447 + }; 448 + }; 449 + state = den.lib.aspects.fx.pipeline.defaultState; 450 + } comp; 451 + } 452 + ); 453 + 454 + # Static aspect through pipeline is identical. 455 + test-rotate-static-passthrough = denTest ( 456 + { den, ... }: 457 + let 458 + fx = den.lib.fx; 459 + aspect = { 460 + name = "static"; 461 + meta = { }; 462 + nixos = { 463 + enable = true; 464 + }; 465 + includes = [ ]; 466 + }; 467 + comp = den.lib.aspects.fx.aspect.aspectToEffect aspect; 468 + result = 469 + (fx.handle { 470 + handlers = den.lib.aspects.fx.pipeline.defaultHandlers { 471 + class = "nixos"; 472 + ctx = { }; 473 + }; 474 + state = den.lib.aspects.fx.pipeline.defaultState; 475 + } comp).value; 476 + in 477 + { 478 + expr = result.nixos.enable; 479 + expected = true; 480 + } 481 + ); 482 + 483 + # composeHandlers: b's resume wins, a's state updates applied on top. 484 + test-composeHandlers-resume-from-b-state-from-a = 485 + let 486 + a = { 487 + "test-effect" = 488 + { param, state }: 489 + { 490 + resume = "a-resume"; 491 + state = state // { 492 + fromA = true; 493 + }; 494 + }; 495 + }; 496 + b = { 497 + "test-effect" = 498 + { param, state }: 499 + { 500 + resume = "b-resume"; 501 + state = state // { 502 + fromB = true; 503 + }; 504 + }; 505 + }; 506 + in 507 + denTest ( 508 + { den, ... }: 509 + let 510 + fx = den.lib.fx; 511 + composed = den.lib.aspects.fx.pipeline.composeHandlers a b; 512 + comp = fx.send "test-effect" null; 513 + result = fx.handle { 514 + handlers = composed; 515 + state = { }; 516 + } comp; 517 + in 518 + { 519 + expr = { 520 + resume = result.value; 521 + hasA = result.state.fromA or false; 522 + hasB = result.state.fromB or false; 523 + }; 524 + expected = { 525 + resume = "b-resume"; 526 + hasA = true; 527 + hasB = true; 528 + }; 529 + } 530 + ); 531 + 532 + }; 533 + }
+411
templates/ci/modules/features/fx-trace.nix
··· 1 + { 2 + denTest, 3 + inputs, 4 + lib, 5 + ... 6 + }: 7 + { 8 + flake.tests.fx-trace = { 9 + 10 + test-structured-trace-fields = denTest ( 11 + { den, ... }: 12 + let 13 + parent = { 14 + name = "root"; 15 + meta = { 16 + provider = [ ]; 17 + }; 18 + nixos = { 19 + a = 1; 20 + }; 21 + includes = [ 22 + { 23 + name = "child"; 24 + meta = { 25 + provider = [ ]; 26 + }; 27 + nixos = { 28 + b = 2; 29 + }; 30 + includes = [ ]; 31 + } 32 + ]; 33 + }; 34 + result = 35 + den.lib.aspects.fx.pipeline.mkPipeline 36 + { 37 + class = "nixos"; 38 + extraHandlers = den.lib.aspects.fx.trace.structuredTraceHandler "nixos"; 39 + extraState = { 40 + entries = [ ]; 41 + }; 42 + } 43 + { 44 + self = parent // { 45 + into = _: { }; 46 + provides = { }; 47 + }; 48 + ctx = { }; 49 + }; 50 + in 51 + { 52 + expr = 53 + let 54 + names = map (e: e.name) result.state.entries; 55 + in 56 + { 57 + hasRoot = builtins.elem "root" names; 58 + hasChild = builtins.elem "child" names; 59 + allHaveClass = builtins.all (e: e.class == "nixos") result.state.entries; 60 + }; 61 + expected = { 62 + hasRoot = true; 63 + hasChild = true; 64 + allHaveClass = true; 65 + }; 66 + } 67 + ); 68 + 69 + test-trace-excluded-aspect = denTest ( 70 + { den, ... }: 71 + let 72 + target = { 73 + name = "drop"; 74 + meta = { 75 + provider = [ ]; 76 + }; 77 + }; 78 + parent = { 79 + name = "root"; 80 + meta = { 81 + provider = [ ]; 82 + handleWith = den.lib.aspects.fx.constraints.exclude target; 83 + }; 84 + includes = [ 85 + { 86 + name = "drop"; 87 + meta = { 88 + provider = [ ]; 89 + }; 90 + nixos = { }; 91 + includes = [ ]; 92 + } 93 + ]; 94 + }; 95 + result = 96 + den.lib.aspects.fx.pipeline.mkPipeline 97 + { 98 + class = "nixos"; 99 + extraHandlers = den.lib.aspects.fx.trace.structuredTraceHandler "nixos"; 100 + extraState = { 101 + entries = [ ]; 102 + }; 103 + } 104 + { 105 + self = parent // { 106 + into = _: { }; 107 + provides = { }; 108 + }; 109 + ctx = { }; 110 + }; 111 + excluded = builtins.filter (e: e.excluded) result.state.entries; 112 + in 113 + { 114 + expr = 115 + let 116 + excludedNames = map (e: e.name) excluded; 117 + in 118 + { 119 + hasExcluded = builtins.elem "~drop" excludedNames; 120 + excludedFromSet = (builtins.head excluded).excludedFrom != null; 121 + }; 122 + expected = { 123 + hasExcluded = true; 124 + excludedFromSet = true; 125 + }; 126 + } 127 + ); 128 + 129 + test-ctx-trace-handler = denTest ( 130 + { den, ... }: 131 + let 132 + fx = den.lib.fx; 133 + comp = fx.send "ctx-traverse" { 134 + key = "host"; 135 + self = { 136 + name = "host"; 137 + provides = { }; 138 + }; 139 + ctx = { }; 140 + prev = null; 141 + prevCtx = null; 142 + }; 143 + result = fx.handle { 144 + handlers."ctx-traverse" = 145 + { param, state }: 146 + let 147 + item = { 148 + key = param.key; 149 + selfName = param.self.name or "<anon>"; 150 + }; 151 + in 152 + { 153 + resume = null; 154 + state = state // { 155 + ctxTrace = (state.ctxTrace or [ ]) ++ [ item ]; 156 + }; 157 + }; 158 + state = { 159 + ctxTrace = [ ]; 160 + }; 161 + } comp; 162 + in 163 + { 164 + expr = (builtins.head result.state.ctxTrace).key; 165 + expected = "host"; 166 + } 167 + ); 168 + 169 + test-mkPipeline-with-trace = denTest ( 170 + { den, ... }: 171 + let 172 + fx = den.lib.fx; 173 + self = { 174 + name = "host"; 175 + into = _: { }; 176 + provides = { }; 177 + nixos = { 178 + a = 1; 179 + }; 180 + includes = [ ]; 181 + }; 182 + result = 183 + den.lib.aspects.fx.pipeline.mkPipeline 184 + { 185 + class = "nixos"; 186 + extraHandlers = den.lib.aspects.fx.trace.structuredTraceHandler "nixos" // { 187 + "ctx-traverse" = 188 + { param, state }: 189 + { 190 + resume = null; 191 + inherit state; 192 + }; 193 + }; 194 + extraState = { 195 + entries = [ ]; 196 + ctxTrace = [ ]; 197 + }; 198 + } 199 + { 200 + inherit self; 201 + ctx = { }; 202 + }; 203 + in 204 + { 205 + expr = { 206 + hasHostEntry = builtins.any (e: e.name == "host") result.state.entries; 207 + allEntriesHaveClass = builtins.all (e: e.class == "nixos") result.state.entries; 208 + hasImports = (result.state.imports null) != [ ]; 209 + }; 210 + expected = { 211 + hasHostEntry = true; 212 + allEntriesHaveClass = true; 213 + hasImports = true; 214 + }; 215 + } 216 + ); 217 + 218 + # Self-provide aspects (pre-included by ctx-apply) are traced and collected. 219 + test-self-provide-traced = denTest ( 220 + { den, ... }: 221 + let 222 + # Simulate post-ctx-apply state: provides stays on the aspect, 223 + # but the provider is already materialized in includes. 224 + hostSelf = { 225 + name = "host"; 226 + meta = { }; 227 + provides = { 228 + host = ctx: { 229 + name = "host-provider"; 230 + meta = { }; 231 + nixos = { 232 + fromProv = true; 233 + }; 234 + includes = [ ]; 235 + }; 236 + }; 237 + nixos = { 238 + base = true; 239 + }; 240 + includes = [ 241 + { 242 + name = "host-provider"; 243 + meta = { }; 244 + nixos = { 245 + fromProv = true; 246 + }; 247 + includes = [ ]; 248 + } 249 + ]; 250 + }; 251 + result = 252 + den.lib.aspects.fx.pipeline.mkPipeline 253 + { 254 + class = "nixos"; 255 + extraHandlers = den.lib.aspects.fx.trace.structuredTraceHandler "nixos"; 256 + extraState = { 257 + entries = [ ]; 258 + }; 259 + } 260 + { 261 + self = hostSelf; 262 + ctx = { }; 263 + }; 264 + entryNames = map (e: e.name) result.state.entries; 265 + in 266 + { 267 + expr = { 268 + hasProvider = builtins.elem "host-provider" entryNames; 269 + hasHost = builtins.elem "host" entryNames; 270 + importCount = builtins.length (result.state.imports null); 271 + }; 272 + expected = { 273 + hasProvider = true; 274 + hasHost = true; 275 + importCount = 2; 276 + }; 277 + } 278 + ); 279 + 280 + # tracingHandler collects entries + paths via resolve-complete. 281 + # classCollectorHandler collects imports via emit-class effects. 282 + test-tracingHandler-collects-entries-and-paths = denTest ( 283 + { den, ... }: 284 + let 285 + parent = { 286 + name = "root"; 287 + meta = { 288 + provider = [ ]; 289 + }; 290 + nixos = { 291 + a = 1; 292 + }; 293 + includes = [ 294 + { 295 + name = "child"; 296 + meta = { 297 + provider = [ ]; 298 + }; 299 + nixos = { 300 + b = 2; 301 + }; 302 + includes = [ ]; 303 + } 304 + ]; 305 + }; 306 + result = 307 + den.lib.aspects.fx.pipeline.mkPipeline 308 + { 309 + class = "nixos"; 310 + extraHandlers = den.lib.aspects.fx.trace.tracingHandler "nixos" // { 311 + "ctx-traverse" = 312 + { param, state }: 313 + { 314 + resume = null; 315 + inherit state; 316 + }; 317 + }; 318 + extraState = { 319 + entries = [ ]; 320 + paths = [ ]; 321 + ctxTrace = [ ]; 322 + }; 323 + } 324 + { 325 + self = parent // { 326 + into = _: { }; 327 + provides = { }; 328 + }; 329 + ctx = { }; 330 + }; 331 + in 332 + { 333 + expr = { 334 + hasEntries = result.state.entries != [ ]; 335 + hasPaths = result.state.paths != [ ]; 336 + hasImports = (result.state.imports null) != [ ]; 337 + }; 338 + expected = { 339 + hasEntries = true; 340 + hasPaths = true; 341 + hasImports = true; 342 + }; 343 + } 344 + ); 345 + 346 + test-trace-parent-from-chain = denTest ( 347 + { den, ... }: 348 + let 349 + fx = den.lib.fx; 350 + parent = { 351 + name = "root"; 352 + meta = { }; 353 + includes = [ 354 + { 355 + name = "child"; 356 + meta = { }; 357 + includes = [ ]; 358 + } 359 + ]; 360 + }; 361 + comp = den.lib.aspects.fx.aspect.aspectToEffect parent; 362 + result = fx.handle { 363 + handlers = den.lib.aspects.fx.pipeline.composeHandlers (den.lib.aspects.fx.pipeline.defaultHandlers 364 + { 365 + class = "nixos"; 366 + ctx = { }; 367 + } 368 + ) (den.lib.aspects.fx.trace.tracingHandler "nixos"); 369 + state = den.lib.aspects.fx.pipeline.defaultState // { 370 + entries = [ ]; 371 + }; 372 + } comp; 373 + childEntry = lib.findFirst (e: e.name == "child") null result.state.entries; 374 + in 375 + { 376 + expr = childEntry.parent; 377 + expected = "root"; 378 + } 379 + ); 380 + 381 + test-trace-root-parent-null = denTest ( 382 + { den, ... }: 383 + let 384 + fx = den.lib.fx; 385 + root = { 386 + name = "root"; 387 + meta = { }; 388 + includes = [ ]; 389 + }; 390 + comp = den.lib.aspects.fx.aspect.aspectToEffect root; 391 + result = fx.handle { 392 + handlers = den.lib.aspects.fx.pipeline.composeHandlers (den.lib.aspects.fx.pipeline.defaultHandlers 393 + { 394 + class = "nixos"; 395 + ctx = { }; 396 + } 397 + ) (den.lib.aspects.fx.trace.tracingHandler "nixos"); 398 + state = den.lib.aspects.fx.pipeline.defaultState // { 399 + entries = [ ]; 400 + }; 401 + } comp; 402 + rootEntry = lib.findFirst (e: e.name == "root") null result.state.entries; 403 + in 404 + { 405 + expr = rootEntry.parent; 406 + expected = null; 407 + } 408 + ); 409 + 410 + }; 411 + }
+3 -3
templates/ci/modules/features/guarded-forward.nix
··· 31 31 let 32 32 forwarded = 33 33 { class, aspect-chain }: 34 - den._.forward { 34 + den.provides.forward { 35 35 each = lib.singleton class; 36 36 fromClass = _: "imper"; 37 37 intoClass = _: "nixos"; ··· 63 63 let 64 64 forwarded = 65 65 { class, aspect-chain }: 66 - den._.forward { 66 + den.provides.forward { 67 67 each = lib.singleton class; 68 68 fromClass = _: "imper"; 69 69 intoClass = _: "nixos"; ··· 110 110 111 111 vimer-home = 112 112 { class, aspect-chain }: 113 - den._.forward { 113 + den.provides.forward { 114 114 each = lib.singleton true; 115 115 fromAspect = _: lib.head aspect-chain; 116 116 fromClass = _: "home-pingu";
+12 -2
templates/ci/modules/features/has-aspect-lib.nix
··· 11 11 inherit (den.lib.aspects) hasAspectIn; 12 12 in 13 13 { 14 + den.fxPipeline = false; 14 15 den.aspects.root.includes = [ den.aspects.child ]; 15 16 den.aspects.child.nixos = { }; 16 17 ··· 29 30 inherit (den.lib.aspects) hasAspectIn; 30 31 in 31 32 { 33 + den.fxPipeline = false; 32 34 den.aspects.root.includes = [ den.aspects.child ]; 33 35 den.aspects.child.nixos = { }; 34 36 den.aspects.other.nixos = { }; ··· 48 50 inherit (den.lib.aspects) hasAspectIn adapters; 49 51 in 50 52 { 53 + den.fxPipeline = false; 51 54 den.aspects.root.includes = [ 52 55 den.aspects.keep 53 56 den.aspects.drop ··· 85 88 }; 86 89 in 87 90 { 88 - den.aspects.root.includes = [ den.aspects.foo._.bar ]; 89 - den.aspects.foo._.bar.nixos = { }; 91 + den.fxPipeline = false; 92 + den.aspects.root.includes = [ den.aspects.foo.provides.bar ]; 93 + den.aspects.foo.provides.bar.nixos = { }; 90 94 91 95 expr = { 92 96 hasRoot = s ? "root"; ··· 112 116 }; 113 117 in 114 118 { 119 + den.fxPipeline = false; 115 120 den.aspects.root.includes = [ den.aspects.child ]; 116 121 den.aspects.child.nixos = { }; 117 122 ··· 145 150 }; 146 151 in 147 152 { 153 + den.fxPipeline = false; 148 154 den.aspects.root.nixos = { }; 149 155 den.aspects.unrelated.nixos = { }; 150 156 ··· 164 170 }; 165 171 in 166 172 { 173 + den.fxPipeline = false; 167 174 den.aspects.root.includes = [ den.aspects.child ]; 168 175 den.aspects.child.nixos = { }; 169 176 ··· 184 191 }); 185 192 in 186 193 { 194 + den.fxPipeline = false; 187 195 den.aspects.root.nixos = { }; 188 196 189 197 # tryEval returns { success = false; value = false; } on throw ··· 206 214 }); 207 215 in 208 216 { 217 + den.fxPipeline = false; 209 218 den.aspects.root.nixos = { }; 210 219 211 220 expr = result.success; ··· 224 233 inherit (den.lib.aspects) adapters; 225 234 in 226 235 { 236 + den.fxPipeline = false; 227 237 den.aspects.myFactory = arg: { 228 238 nixos.environment.variables.FACTORY_ARG = arg; 229 239 };
+50 -19
templates/ci/modules/features/has-aspect.nix
··· 16 16 test-host-hasAspect-present-static = denTest ( 17 17 { den, ... }: 18 18 { 19 + den.fxPipeline = false; 19 20 den.hosts.x86_64-linux.igloo.users.tux = { }; 20 21 21 22 den.aspects.igloo.includes = [ den.aspects.feature ]; ··· 31 32 test-host-hasAspect-absent = denTest ( 32 33 { den, ... }: 33 34 { 35 + den.fxPipeline = false; 34 36 den.hosts.x86_64-linux.igloo.users.tux = { }; 35 37 36 38 den.aspects.igloo.nixos = { }; ··· 44 46 test-user-hasAspect-present = denTest ( 45 47 { den, ... }: 46 48 { 49 + den.fxPipeline = false; 47 50 den.hosts.x86_64-linux.igloo.users.tux = { }; 48 51 49 52 # denTest's default is classes = ["homeManager"] for users. ··· 58 61 test-hasAspect-forClass-explicit = denTest ( 59 62 { den, ... }: 60 63 { 64 + den.fxPipeline = false; 61 65 den.hosts.x86_64-linux.igloo.users.tux = { }; 62 66 63 67 den.aspects.igloo.includes = [ den.aspects.feature ]; ··· 71 75 test-hasAspect-forAnyClass = denTest ( 72 76 { den, ... }: 73 77 { 78 + den.fxPipeline = false; 74 79 den.hosts.x86_64-linux.igloo.users.tux = { }; 75 80 76 81 den.aspects.igloo.includes = [ den.aspects.feature ]; ··· 84 89 test-hasAspect-respects-tombstone = denTest ( 85 90 { den, ... }: 86 91 { 92 + den.fxPipeline = false; 87 93 den.hosts.x86_64-linux.igloo.users.tux = { }; 88 94 89 95 den.aspects.igloo.includes = [ ··· 109 115 test-hasAspect-angle-bracket-equivalent = denTest ( 110 116 { den, __findFile, ... }: 111 117 { 118 + den.fxPipeline = false; 112 119 _module.args.__findFile = den.lib.__findFile; 113 120 114 121 den.hosts.x86_64-linux.igloo.users.tux = { }; ··· 133 140 test-A-hosts-hasAspect-self = denTest ( 134 141 { den, ... }: 135 142 { 143 + den.fxPipeline = false; 136 144 den.hosts.x86_64-linux.igloo.users.tux = { }; 137 145 den.aspects.igloo.nixos = { }; 138 146 ··· 145 153 test-A-hosts-hasAspect-chained-transitively = denTest ( 146 154 { den, ... }: 147 155 { 156 + den.fxPipeline = false; 148 157 den.hosts.x86_64-linux.igloo.users.tux = { }; 149 158 150 159 den.aspects.igloo.includes = [ den.aspects.level1 ]; ··· 163 172 test-B-present-via-parametric-parent = denTest ( 164 173 { den, ... }: 165 174 { 175 + den.fxPipeline = false; 166 176 den.hosts.x86_64-linux.igloo.users.tux = { }; 167 177 168 178 den.aspects.igloo.includes = [ den.aspects.parent ]; ··· 179 189 ); 180 190 181 191 # #423 regression shape — parametric parent with static sub-aspect. 182 - # If applyDeep regresses, role._.sub vanishes from the tree. 192 + # If applyDeep regresses, role.provides.sub vanishes from the tree. 183 193 test-B-present-via-static-sub-aspect-in-parametric-parent = denTest ( 184 194 { den, ... }: 185 195 { 196 + den.fxPipeline = false; 186 197 den.hosts.x86_64-linux.igloo.users.tux = { }; 187 198 188 199 imports = [ ··· 190 201 den.aspects.role = 191 202 { host, ... }: 192 203 { 193 - includes = [ den.aspects.role._.sub ]; 204 + includes = [ den.aspects.role.provides.sub ]; 194 205 }; 195 206 } 196 207 { 197 - den.aspects.role._.sub.nixos.networking.networkmanager.enable = true; 208 + den.aspects.role.provides.sub.nixos.networking.networkmanager.enable = true; 198 209 } 199 210 { 200 211 den.aspects.igloo.includes = [ den.aspects.role ]; 201 212 } 202 213 ]; 203 214 204 - expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.role._.sub; 215 + expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.role.provides.sub; 205 216 expected = true; 206 217 } 207 218 ); ··· 212 223 test-B-present-via-bare-function-sub-aspect = denTest ( 213 224 { den, ... }: 214 225 { 226 + den.fxPipeline = false; 215 227 den.hosts.x86_64-linux.igloo.users.tux = { }; 216 228 217 229 imports = [ ··· 219 231 den.aspects.foo = 220 232 { host, ... }: 221 233 { 222 - includes = [ den.aspects.foo._.sub ]; 234 + includes = [ den.aspects.foo.provides.sub ]; 223 235 }; 224 236 } 225 237 { 226 - den.aspects.foo._.sub = 238 + den.aspects.foo.provides.sub = 227 239 { host, ... }: 228 240 { 229 241 nixos.networking.networkmanager.enable = true; ··· 234 246 } 235 247 ]; 236 248 237 - expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo._.sub; 249 + expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo.provides.sub; 238 250 expected = true; 239 251 } 240 252 ); ··· 242 254 test-B-absent-when-parametric-parent-omits = denTest ( 243 255 { den, lib, ... }: 244 256 { 257 + den.fxPipeline = false; 245 258 den.hosts.x86_64-linux.igloo.users.tux = { }; 246 259 247 260 den.aspects.igloo.includes = [ den.aspects.gated ]; ··· 267 280 test-C-factory-fn-aspect-present = denTest ( 268 281 { den, ... }: 269 282 { 283 + den.fxPipeline = false; 270 284 den.hosts.x86_64-linux.igloo.users.tux = { }; 271 285 272 286 den.aspects.facter = reportPath: { ··· 283 297 test-C-factory-fn-merged-with-static-sibling = denTest ( 284 298 { den, ... }: 285 299 { 300 + den.fxPipeline = false; 286 301 den.hosts.x86_64-linux.igloo.users.tux = { }; 287 302 288 303 imports = [ ··· 311 326 test-D-static-provider-sub-present = denTest ( 312 327 { den, ... }: 313 328 { 329 + den.fxPipeline = false; 314 330 den.hosts.x86_64-linux.igloo.users.tux = { }; 315 331 316 - den.aspects.igloo.includes = [ den.aspects.foo._.sub ]; 317 - den.aspects.foo._.sub.nixos = { }; 332 + den.aspects.igloo.includes = [ den.aspects.foo.provides.sub ]; 333 + den.aspects.foo.provides.sub.nixos = { }; 318 334 319 - expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo._.sub; 335 + expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo.provides.sub; 320 336 expected = true; 321 337 } 322 338 ); ··· 326 342 test-D-parametric-provider-sub-present = denTest ( 327 343 { den, ... }: 328 344 { 345 + den.fxPipeline = false; 329 346 den.hosts.x86_64-linux.igloo.users.tux = { }; 330 347 331 - den.aspects.igloo.includes = [ den.aspects.foo._.sub ]; 332 - den.aspects.foo._.sub = 348 + den.aspects.igloo.includes = [ den.aspects.foo.provides.sub ]; 349 + den.aspects.foo.provides.sub = 333 350 { host, ... }: 334 351 { 335 352 nixos.environment.variables.FOO_SUB = host.name; 336 353 }; 337 354 338 - expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo._.sub; 355 + expr = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo.provides.sub; 339 356 expected = true; 340 357 } 341 358 ); ··· 343 360 test-D-provider-sub-identity-distinct-from-homonym = denTest ( 344 361 { den, ... }: 345 362 { 363 + den.fxPipeline = false; 346 364 den.hosts.x86_64-linux.igloo.users.tux = { }; 347 365 348 - den.aspects.igloo.includes = [ den.aspects.bar._.foo ]; 349 - den.aspects.bar._.foo.nixos = { }; 366 + den.aspects.igloo.includes = [ den.aspects.bar.provides.foo ]; 367 + den.aspects.bar.provides.foo.nixos = { }; 350 368 # `foo` also exists at top level — different aspectPath. 351 369 den.aspects.foo.nixos = { }; 352 370 353 371 expr = { 354 - sub = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.bar._.foo; 372 + sub = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.bar.provides.foo; 355 373 top = den.hosts.x86_64-linux.igloo.hasAspect den.aspects.foo; 356 374 }; 357 375 expected = { ··· 366 384 test-E-present-via-provides-to-users = denTest ( 367 385 { den, ... }: 368 386 { 369 - den.ctx.user.includes = [ den._.mutual-provider ]; 387 + den.fxPipeline = false; 388 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 370 389 den.hosts.x86_64-linux.igloo.users.tux = { }; 371 390 372 391 den.aspects.igloo.provides.to-users = { ··· 382 401 test-E-present-via-provides-specific-user = denTest ( 383 402 { den, ... }: 384 403 { 385 - den.ctx.user.includes = [ den._.mutual-provider ]; 404 + den.fxPipeline = false; 405 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 386 406 den.hosts.x86_64-linux.igloo.users.tux = { }; 387 407 den.hosts.x86_64-linux.igloo.users.alice = { }; 388 408 ··· 405 425 test-E-present-via-user-to-hosts = denTest ( 406 426 { den, ... }: 407 427 { 408 - den.ctx.user.includes = [ den._.mutual-provider ]; 428 + den.fxPipeline = false; 429 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 409 430 den.hosts.x86_64-linux.igloo.users.tux = { }; 410 431 411 432 den.aspects.tux.provides.to-hosts = { ··· 423 444 test-F-respects-substituteAspect = denTest ( 424 445 { den, ... }: 425 446 { 447 + den.fxPipeline = false; 426 448 den.hosts.x86_64-linux.igloo.users.tux = { }; 427 449 428 450 den.aspects.igloo.includes = [ den.aspects.original ]; ··· 452 474 test-F-composes-at-different-levels = denTest ( 453 475 { den, ... }: 454 476 { 477 + den.fxPipeline = false; 455 478 den.hosts.x86_64-linux.igloo.users.tux = { }; 456 479 457 480 # igloo's adapter tombstones root-sibling at its own level. ··· 492 515 test-F-respects-oneOfAspects = denTest ( 493 516 { den, ... }: 494 517 { 518 + den.fxPipeline = false; 495 519 den.hosts.x86_64-linux.igloo.users.tux = { }; 496 520 497 521 den.aspects.igloo.includes = [ den.aspects.bundle ]; ··· 522 546 test-G-user-hasAspect-primary-class = denTest ( 523 547 { den, lib, ... }: 524 548 { 549 + den.fxPipeline = false; 525 550 den.schema.user.classes = lib.mkForce [ 526 551 "user" 527 552 "homeManager" ··· 541 566 test-G-user-hasAspect-forClass-explicit = denTest ( 542 567 { den, lib, ... }: 543 568 { 569 + den.fxPipeline = false; 544 570 den.schema.user.classes = lib.mkForce [ 545 571 "user" 546 572 "homeManager" ··· 565 591 test-G-user-hasAspect-forAnyClass-matches-any = denTest ( 566 592 { den, lib, ... }: 567 593 { 594 + den.fxPipeline = false; 568 595 den.schema.user.classes = lib.mkForce [ 569 596 "user" 570 597 "homeManager" ··· 582 609 test-G-user-hasAspect-forClass-unknown-class = denTest ( 583 610 { den, ... }: 584 611 { 612 + den.fxPipeline = false; 585 613 den.hosts.x86_64-linux.igloo.users.tux = { }; 586 614 587 615 den.aspects.tux.includes = [ den.aspects.target ]; ··· 601 629 test-H-conf-option-exists = denTest ( 602 630 { den, ... }: 603 631 { 632 + den.fxPipeline = false; 604 633 den.hosts.x86_64-linux.igloo.users.tux = { }; 605 634 606 635 # If conf owns the option, host imports conf, and therefore ··· 619 648 result = builtins.tryEval (den.hosts.x86_64-linux.igloo.hasAspect "not-a-ref"); 620 649 in 621 650 { 651 + den.fxPipeline = false; 622 652 den.hosts.x86_64-linux.igloo.users.tux = { }; 623 653 den.aspects.igloo.nixos = { }; 624 654 ··· 645 675 hostEntity = den.hosts.x86_64-linux.igloo; 646 676 in 647 677 { 678 + den.fxPipeline = false; 648 679 den.hosts.x86_64-linux.igloo.users.tux = { }; 649 680 650 681 den.aspects.igloo.includes = [
templates/ci/modules/features/home-manager/hm-host-isolation.nix templates/ci/modules/features/hm-host-isolation.nix
+1 -1
templates/ci/modules/features/home-manager/home-managed-home.nix templates/ci/modules/features/home-managed-home.nix
··· 34 34 den.hosts.x86_64-linux.igloo.users.tux = { }; 35 35 36 36 den.default.homeManager.home.stateVersion = "25.11"; 37 - den.default.includes = [ den._.define-user ]; 37 + den.default.includes = [ den.provides.define-user ]; 38 38 39 39 expr = tuxHm.home.homeDirectory; 40 40 expected = "/home/tux";
templates/ci/modules/features/home-manager/use-global-pkgs.nix templates/ci/modules/features/use-global-pkgs.nix
+6 -6
templates/ci/modules/features/homes.nix
··· 18 18 { 19 19 den.homes.x86_64-linux.tux = { }; 20 20 den.default.homeManager.home.stateVersion = "25.11"; 21 - den.default.includes = [ den._.define-user ]; 21 + den.default.includes = [ den.provides.define-user ]; 22 22 den.aspects.tux.homeManager.programs.fish.enable = true; 23 23 24 24 expr = config.flake.homeConfigurations.tux.config.programs.fish.enable; ··· 33 33 userName = "cameron"; 34 34 }; 35 35 den.default.homeManager.home.stateVersion = "25.11"; 36 - den.default.includes = [ den._.define-user ]; 36 + den.default.includes = [ den.provides.define-user ]; 37 37 38 38 expr = config.flake.homeConfigurations.cam.config.home.username; 39 39 expected = "cameron"; ··· 45 45 { 46 46 den.homes.x86_64-linux.tux = { }; 47 47 den.default.homeManager.home.stateVersion = "25.11"; 48 - den.default.includes = [ den._.define-user ]; 48 + den.default.includes = [ den.provides.define-user ]; 49 49 den.ctx.home.homeManager.programs.vim.enable = true; 50 50 51 51 expr = config.flake.homeConfigurations.tux.config.programs.vim.enable; ··· 63 63 { 64 64 den.homes.x86_64-linux."tux@igloo" = { }; 65 65 66 - den.aspects.tux.includes = [ den._.define-user ]; 66 + den.aspects.tux.includes = [ den.provides.define-user ]; 67 67 68 68 den.aspects.tux.homeManager = args: { 69 69 home.keyboard.model = if args ? osConfig then "os-bound" else "standalone"; 70 70 }; 71 71 72 - den.ctx.home.includes = [ den._.mutual-provider ]; 72 + den.ctx.home.includes = [ den.provides.mutual-provider ]; 73 73 den.aspects.tux.provides.igloo = { 74 74 homeManager.home.keyboard.layout = "enthium"; 75 75 includes = [ ··· 120 120 den.hosts.x86_64-linux.igloo.users.tux = { }; 121 121 122 122 den.aspects.igloo.nixos.networking.hostName = "blizzard"; 123 - den.aspects.tux.includes = [ den._.define-user ]; 123 + den.aspects.tux.includes = [ den.provides.define-user ]; 124 124 den.aspects.tux.homeManager = 125 125 { osConfig, ... }: 126 126 {
+3 -3
templates/ci/modules/features/host-options.nix
··· 20 20 { 21 21 den.hosts.x86_64-linux.igloo.users.tux = { }; 22 22 den.default.homeManager.home.stateVersion = "25.11"; 23 - den.default.includes = [ den._.hostname ]; 23 + den.default.includes = [ den.provides.hostname ]; 24 24 25 25 expr = igloo.networking.hostName; 26 26 expected = "igloo"; ··· 56 56 { den, igloo, ... }: 57 57 { 58 58 den.hosts.x86_64-linux.igloo.users.tux.userName = "penguin"; 59 - den.aspects.igloo._.to-users.includes = [ den._.define-user ]; 60 - den.ctx.user.includes = [ den._.mutual-provider ]; 59 + den.aspects.igloo.provides.to-users.includes = [ den.provides.define-user ]; 60 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 61 61 62 62 expr = igloo.users.users.penguin.isNormalUser; 63 63 expected = true;
+4
templates/ci/modules/features/identity-preservation.nix
··· 15 15 test-parametric-aspect-preserves-name = denTest ( 16 16 { den, ... }: 17 17 { 18 + den.fxPipeline = false; 18 19 den.aspects.igloo.includes = [ den.aspects.foo ]; 19 20 20 21 den.aspects.foo = ··· 31 32 test-parametric-child-preserves-name = denTest ( 32 33 { den, ... }: 33 34 { 35 + den.fxPipeline = false; 34 36 den.aspects.foo.includes = [ den.aspects.bar ]; 35 37 den.aspects.bar = 36 38 { host }: ··· 50 52 test-meta-preserved-through-functor = denTest ( 51 53 { den, ... }: 52 54 { 55 + den.fxPipeline = false; 53 56 den.aspects.foo.nixos = { }; 54 57 55 58 expr = (den.lib.aspects.resolve.withAdapter getName "nixos" den.aspects.foo).adapter; ··· 72 75 }; 73 76 in 74 77 { 78 + den.fxPipeline = false; 75 79 den.aspects.foo.includes = [ den.aspects.bar ]; 76 80 den.aspects.bar = 77 81 { host }:
+12 -6
templates/ci/modules/features/one-of-aspects.nix
··· 7 7 test-prefers-first-present = denTest ( 8 8 { den, trace, ... }: 9 9 { 10 + den.fxPipeline = false; 10 11 den.aspects.bundle.includes = [ 11 12 den.aspects.pref-a 12 13 den.aspects.pref-b ··· 31 32 test-falls-through-to-second = denTest ( 32 33 { den, trace, ... }: 33 34 { 35 + den.fxPipeline = false; 34 36 den.aspects.bundle.includes = [ den.aspects.only-b ]; 35 37 den.aspects.bundle.meta.adapter = den.lib.aspects.adapters.oneOfAspects [ 36 38 den.aspects.pref-a ··· 51 53 test-both-absent-no-effect = denTest ( 52 54 { den, trace, ... }: 53 55 { 56 + den.fxPipeline = false; 54 57 den.aspects.bundle.includes = [ den.aspects.neither ]; 55 58 den.aspects.bundle.meta.adapter = den.lib.aspects.adapters.oneOfAspects [ 56 59 den.aspects.pref-a ··· 73 76 test-composes-with-outer-adapter = denTest ( 74 77 { den, trace, ... }: 75 78 { 79 + den.fxPipeline = false; 76 80 # root sibling-filters bundle and sibling; bundle internally 77 81 # uses oneOfAspects. Verifies the two adapters both take effect 78 82 # at their own level without interfering: root's filter kills ··· 111 115 test-works-on-sub-aspects = denTest ( 112 116 { den, trace, ... }: 113 117 { 118 + den.fxPipeline = false; 114 119 den.aspects.bundle.includes = [ 115 - den.aspects.foo._.impl-a 116 - den.aspects.foo._.impl-b 120 + den.aspects.foo.provides.impl-a 121 + den.aspects.foo.provides.impl-b 117 122 ]; 118 123 den.aspects.bundle.meta.adapter = den.lib.aspects.adapters.oneOfAspects [ 119 - den.aspects.foo._.impl-a 120 - den.aspects.foo._.impl-b 124 + den.aspects.foo.provides.impl-a 125 + den.aspects.foo.provides.impl-b 121 126 ]; 122 - den.aspects.foo._.impl-a.nixos = { }; 123 - den.aspects.foo._.impl-b.nixos = { }; 127 + den.aspects.foo.provides.impl-a.nixos = { }; 128 + den.aspects.foo.provides.impl-b.nixos = { }; 124 129 125 130 expr = trace "nixos" den.aspects.bundle; 126 131 expected.trace = [ ··· 134 139 test-preserves-non-candidate-includes = denTest ( 135 140 { den, trace, ... }: 136 141 { 142 + den.fxPipeline = false; 137 143 den.aspects.bundle.includes = [ 138 144 den.aspects.pref-a 139 145 den.aspects.pref-b
+226
templates/ci/modules/features/parametric-context.nix
··· 1 + { denTest, ... }: 2 + { 3 + flake.tests.parametric = { 4 + 5 + test-parametric-forwards-context = denTest ( 6 + { den, igloo, ... }: 7 + let 8 + foo = den.lib.parametric { 9 + includes = [ 10 + ( 11 + { host, ... }: 12 + { 13 + nixos.users.users.tux.description = host.name; 14 + } 15 + ) 16 + ]; 17 + }; 18 + in 19 + { 20 + den.hosts.x86_64-linux.igloo.users.tux = { }; 21 + den.aspects.igloo.includes = [ foo ]; 22 + 23 + expr = igloo.users.users.tux.description; 24 + expected = "igloo"; 25 + } 26 + ); 27 + 28 + test-parametric-owned-config = denTest ( 29 + { den, igloo, ... }: 30 + let 31 + foo = den.lib.parametric { 32 + nixos.networking.hostName = "from-parametric-owned"; 33 + includes = [ ]; 34 + }; 35 + in 36 + { 37 + den.hosts.x86_64-linux.igloo.users.tux = { }; 38 + den.aspects.igloo.includes = [ foo ]; 39 + 40 + expr = igloo.networking.hostName; 41 + expected = "from-parametric-owned"; 42 + } 43 + ); 44 + 45 + test-parametric-fixedTo = denTest ( 46 + { den, igloo, ... }: 47 + let 48 + foo = 49 + { host, ... }: 50 + den.lib.parametric.fixedTo { planet = "Earth"; } { 51 + includes = [ 52 + ( 53 + { planet, ... }: 54 + { 55 + nixos.users.users.tux.description = planet; 56 + } 57 + ) 58 + ]; 59 + }; 60 + in 61 + { 62 + den.hosts.x86_64-linux.igloo.users.tux = { }; 63 + den.aspects.igloo.includes = [ foo ]; 64 + 65 + expr = igloo.users.users.tux.description; 66 + expected = "Earth"; 67 + } 68 + ); 69 + 70 + test-parametric-expands = denTest ( 71 + { den, igloo, ... }: 72 + let 73 + foo = den.lib.parametric.expands { planet = "Earth"; } { 74 + includes = [ 75 + ( 76 + { host, planet, ... }: 77 + { 78 + nixos.users.users.tux.description = "${host.name}/${planet}"; 79 + } 80 + ) 81 + ]; 82 + }; 83 + in 84 + { 85 + den.hosts.x86_64-linux.igloo.users.tux = { }; 86 + den.aspects.igloo.includes = [ foo ]; 87 + 88 + expr = igloo.users.users.tux.description; 89 + expected = "igloo/Earth"; 90 + } 91 + ); 92 + 93 + # Parametric aspect including a static named aspect — owned configs 94 + # on the static aspect must not be dropped by applyDeep recursion. 95 + test-parametric-including-static-named-aspect = denTest ( 96 + { den, igloo, ... }: 97 + { 98 + den.hosts.x86_64-linux.igloo.users.tux = { }; 99 + 100 + den.aspects.base.nixos = 101 + { ... }: 102 + { 103 + programs.git.enable = true; 104 + }; 105 + 106 + den.aspects.dev = 107 + { user, ... }: 108 + { 109 + includes = [ den.aspects.base ]; 110 + }; 111 + 112 + den.aspects.tux.includes = [ den.aspects.dev ]; 113 + 114 + expr = igloo.programs.git.enable; 115 + expected = true; 116 + } 117 + ); 118 + 119 + # Named aspect with both owned class config and parametric includes, 120 + # referenced from inside a parametric function's bare result. 121 + test-parametric-including-mixed-owned-and-parametric = denTest ( 122 + { 123 + den, 124 + igloo, 125 + lib, 126 + ... 127 + }: 128 + { 129 + den.hosts.x86_64-linux.igloo.users.tux = { }; 130 + 131 + den.aspects.tools = { 132 + nixos = 133 + { ... }: 134 + { 135 + programs.git.enable = true; 136 + }; 137 + includes = [ 138 + ( 139 + { user, ... }: 140 + { 141 + nixos = 142 + { ... }: 143 + { 144 + programs.zsh.enable = true; 145 + }; 146 + } 147 + ) 148 + ]; 149 + }; 150 + 151 + den.aspects.role = 152 + { user, ... }: 153 + { 154 + includes = [ den.aspects.tools ]; 155 + }; 156 + 157 + den.aspects.tux.includes = [ den.aspects.role ]; 158 + 159 + expr = { 160 + git = igloo.programs.git.enable; 161 + zsh = igloo.programs.zsh.enable; 162 + }; 163 + expected = { 164 + git = true; 165 + zsh = true; 166 + }; 167 + } 168 + ); 169 + 170 + # Factory function aspect called inside a nested parametric chain. 171 + test-factory-in-nested-parametric-chain = denTest ( 172 + { 173 + den, 174 + igloo, 175 + lib, 176 + ... 177 + }: 178 + { 179 + den.hosts.x86_64-linux.igloo.users.tux = { }; 180 + 181 + den.aspects.greeter = greeting: { 182 + nixos = 183 + { ... }: 184 + { 185 + users.users.tux.description = greeting; 186 + }; 187 + }; 188 + 189 + den.aspects.role = 190 + { user, ... }: 191 + { 192 + includes = [ (den.aspects.greeter "hello") ]; 193 + }; 194 + 195 + den.aspects.tux.includes = [ den.aspects.role ]; 196 + 197 + expr = igloo.users.users.tux.description; 198 + expected = "hello"; 199 + } 200 + ); 201 + 202 + test-never-matches-aspect-skipped = denTest ( 203 + { den, igloo, ... }: 204 + let 205 + never-matches = 206 + { never-exists, ... }: 207 + { 208 + nixos.networking.hostName = "NEVER"; 209 + }; 210 + in 211 + { 212 + den.hosts.x86_64-linux.igloo.users.tux = { }; 213 + den.aspects.igloo = den.lib.parametric { 214 + includes = [ 215 + den.provides.hostname 216 + never-matches 217 + ]; 218 + }; 219 + 220 + expr = igloo.networking.hostName; 221 + expected = "igloo"; 222 + } 223 + ); 224 + 225 + }; 226 + }
+85 -175
templates/ci/modules/features/parametric.nix
··· 1 - { denTest, ... }: 1 + { denTest, lib, ... }: 2 2 { 3 - flake.tests.parametric = { 3 + flake.tests.performance.parametric = { 4 4 5 - test-parametric-forwards-context = denTest ( 6 - { den, igloo, ... }: 5 + test-fixedTo-deep-chain = denTest ( 6 + { den, funnyNames, ... }: 7 7 let 8 - foo = den.lib.parametric { 9 - includes = [ 10 - ( 11 - { host, ... }: 12 - { 13 - nixos.users.users.tux.description = host.name; 14 - } 15 - ) 16 - ]; 8 + leaf = den.lib.parametric { 9 + funny.names = [ "leaf" ]; 10 + }; 11 + mid = den.lib.parametric { 12 + funny.names = [ "mid" ]; 13 + includes = [ leaf ]; 14 + }; 15 + top = den.lib.parametric.fixedTo { level = "deep"; } { 16 + funny.names = [ "top" ]; 17 + includes = lib.genList (_: mid) 20; 17 18 }; 18 19 in 19 20 { 20 - den.hosts.x86_64-linux.igloo.users.tux = { }; 21 - den.aspects.igloo.includes = [ foo ]; 22 - 23 - expr = igloo.users.users.tux.description; 24 - expected = "igloo"; 25 - } 26 - ); 27 - 28 - test-parametric-owned-config = denTest ( 29 - { den, igloo, ... }: 30 - let 31 - foo = den.lib.parametric { 32 - nixos.networking.hostName = "from-parametric-owned"; 33 - includes = [ ]; 21 + den.ctx.start = { 22 + _.start = 23 + { level }: 24 + { 25 + funny.names = [ level ]; 26 + }; 27 + includes = [ top ]; 34 28 }; 35 - in 36 - { 37 - den.hosts.x86_64-linux.igloo.users.tux = { }; 38 - den.aspects.igloo.includes = [ foo ]; 39 29 40 - expr = igloo.networking.hostName; 41 - expected = "from-parametric-owned"; 30 + expr = builtins.length (funnyNames (den.ctx.start { level = "deep"; })); 31 + expected = 42; 42 32 } 43 33 ); 44 34 45 - test-parametric-fixedTo = denTest ( 46 - { den, igloo, ... }: 35 + test-atLeast-wide = denTest ( 36 + { den, funnyNames, ... }: 47 37 let 48 - foo = 49 - { host, ... }: 50 - den.lib.parametric.fixedTo { planet = "Earth"; } { 38 + mkParam = 39 + i: 40 + den.lib.parametric { 41 + funny.names = [ "p${toString i}" ]; 51 42 includes = [ 52 43 ( 53 - { planet, ... }: 44 + { host, ... }: 54 45 { 55 - nixos.users.users.tux.description = planet; 46 + funny.names = [ "i${toString i}-${host}" ]; 56 47 } 57 48 ) 58 49 ]; 59 50 }; 51 + aspects = lib.genList mkParam 30; 60 52 in 61 53 { 62 - den.hosts.x86_64-linux.igloo.users.tux = { }; 63 - den.aspects.igloo.includes = [ foo ]; 54 + den.ctx.start = { 55 + _.start = 56 + { host }: 57 + { 58 + funny.names = [ host ]; 59 + }; 60 + includes = aspects; 61 + }; 64 62 65 - expr = igloo.users.users.tux.description; 66 - expected = "Earth"; 63 + expr = builtins.length (funnyNames (den.ctx.start { host = "h"; })); 64 + expected = 61; 67 65 } 68 66 ); 69 67 70 - test-parametric-expands = denTest ( 71 - { den, igloo, ... }: 68 + test-expands-propagation = denTest ( 69 + { den, funnyNames, ... }: 72 70 let 73 - foo = den.lib.parametric.expands { planet = "Earth"; } { 74 - includes = [ 75 - ( 76 - { host, planet, ... }: 77 - { 78 - nixos.users.users.tux.description = "${host.name}/${planet}"; 79 - } 80 - ) 81 - ]; 71 + inner = 72 + { host, planet, ... }: 73 + { 74 + funny.names = [ "${host}-${planet}" ]; 75 + }; 76 + expanded = den.lib.parametric.expands { planet = "mars"; } { 77 + funny.names = [ "exp" ]; 78 + includes = lib.genList (_: inner) 15; 82 79 }; 83 80 in 84 81 { 85 - den.hosts.x86_64-linux.igloo.users.tux = { }; 86 - den.aspects.igloo.includes = [ foo ]; 87 - 88 - expr = igloo.users.users.tux.description; 89 - expected = "igloo/Earth"; 90 - } 91 - ); 92 - 93 - # Parametric aspect including a static named aspect — owned configs 94 - # on the static aspect must not be dropped by applyDeep recursion. 95 - test-parametric-including-static-named-aspect = denTest ( 96 - { den, igloo, ... }: 97 - { 98 - den.hosts.x86_64-linux.igloo.users.tux = { }; 99 - 100 - den.aspects.base.nixos = 101 - { ... }: 102 - { 103 - programs.git.enable = true; 104 - }; 105 - 106 - den.aspects.dev = 107 - { user, ... }: 108 - { 109 - includes = [ den.aspects.base ]; 110 - }; 111 - 112 - den.aspects.tux.includes = [ den.aspects.dev ]; 82 + den.ctx.start = { 83 + _.start = 84 + { host }: 85 + { 86 + funny.names = [ host ]; 87 + }; 88 + includes = [ expanded ]; 89 + }; 113 90 114 - expr = igloo.programs.git.enable; 115 - expected = true; 91 + expr = builtins.length (funnyNames (den.ctx.start { host = "h"; })); 92 + expected = 17; 116 93 } 117 94 ); 118 95 119 - # Named aspect with both owned class config and parametric includes, 120 - # referenced from inside a parametric function's bare result. 121 - test-parametric-including-mixed-owned-and-parametric = denTest ( 122 - { 123 - den, 124 - igloo, 125 - lib, 126 - ... 127 - }: 128 - { 129 - den.hosts.x86_64-linux.igloo.users.tux = { }; 130 - 131 - den.aspects.tools = { 132 - nixos = 133 - { ... }: 134 - { 135 - programs.git.enable = true; 136 - }; 96 + test-dedup-parametric = denTest ( 97 + { den, funnyNames, ... }: 98 + let 99 + shared = den.lib.parametric { 100 + funny.names = [ "shared" ]; 137 101 includes = [ 138 102 ( 139 - { user, ... }: 103 + { host, ... }: 140 104 { 141 - nixos = 142 - { ... }: 143 - { 144 - programs.zsh.enable = true; 145 - }; 105 + funny.names = [ "inner-${host}" ]; 146 106 } 147 107 ) 148 108 ]; 149 109 }; 150 - 151 - den.aspects.role = 152 - { user, ... }: 153 - { 154 - includes = [ den.aspects.tools ]; 155 - }; 156 - 157 - den.aspects.tux.includes = [ den.aspects.role ]; 158 - 159 - expr = { 160 - git = igloo.programs.git.enable; 161 - zsh = igloo.programs.zsh.enable; 162 - }; 163 - expected = { 164 - git = true; 165 - zsh = true; 166 - }; 167 - } 168 - ); 169 - 170 - # Factory function aspect called inside a nested parametric chain. 171 - test-factory-in-nested-parametric-chain = denTest ( 110 + in 172 111 { 173 - den, 174 - igloo, 175 - lib, 176 - ... 177 - }: 178 - { 179 - den.hosts.x86_64-linux.igloo.users.tux = { }; 180 - 181 - den.aspects.greeter = greeting: { 182 - nixos = 183 - { ... }: 112 + den.ctx.a = { 113 + _.a = 114 + { host }: 184 115 { 185 - users.users.tux.description = greeting; 116 + funny.names = [ "a-${host}" ]; 186 117 }; 118 + into.b = { host }: [ { host = "${host}!"; } ]; 119 + includes = [ shared ]; 187 120 }; 188 - 189 - den.aspects.role = 190 - { user, ... }: 191 - { 192 - includes = [ (den.aspects.greeter "hello") ]; 193 - }; 194 - 195 - den.aspects.tux.includes = [ den.aspects.role ]; 196 - 197 - expr = igloo.users.users.tux.description; 198 - expected = "hello"; 199 - } 200 - ); 201 - 202 - test-never-matches-aspect-skipped = denTest ( 203 - { den, igloo, ... }: 204 - let 205 - never-matches = 206 - { never-exists, ... }: 207 - { 208 - nixos.networking.hostName = "NEVER"; 209 - }; 210 - in 211 - { 212 - den.hosts.x86_64-linux.igloo.users.tux = { }; 213 - den.aspects.igloo = den.lib.parametric { 214 - includes = [ 215 - den._.hostname 216 - never-matches 217 - ]; 121 + den.ctx.b = { 122 + _.b = 123 + { host }: 124 + { 125 + funny.names = [ "b-${host}" ]; 126 + }; 127 + includes = [ shared ]; 218 128 }; 219 129 220 - expr = igloo.networking.hostName; 221 - expected = "igloo"; 130 + expr = builtins.length (funnyNames (den.ctx.a { host = "v"; })); 131 + expected = 6; 222 132 } 223 133 ); 224 134
+3 -3
templates/ci/modules/features/perUser-perHost.nix
··· 77 77 pingu = { }; 78 78 }; 79 79 80 - den.ctx.user.includes = [ den._.mutual-provider ]; 80 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 81 81 82 82 den.aspects.igloo.nixos.options.funny = lib.mkOption { 83 83 default = [ ]; 84 84 type = lib.types.listOf lib.types.str; 85 85 }; 86 86 87 - den.aspects.igloo._.to-users.includes = [ 87 + den.aspects.igloo.provides.to-users.includes = [ 88 88 (den.lib.perHost { nixos.funny = [ (throw "atHost perHost static") ]; }) 89 89 (den.lib.perHost ( 90 90 { host }: ··· 139 139 }: 140 140 { 141 141 den.homes.x86_64-linux.tux = { }; 142 - den.default.includes = [ den._.define-user ]; 142 + den.default.includes = [ den.provides.define-user ]; 143 143 144 144 den.aspects.tux.homeManager.options.funny = lib.mkOption { 145 145 default = [ ];
+1 -1
templates/ci/modules/features/perf/ctx-chain.nix templates/ci/modules/features/ctx-chain.nix
··· 75 75 }; 76 76 into.leaf = { x }: lib.genList (i: { x = "${x}-${toString i}"; }) 20; 77 77 }; 78 - den.ctx.leaf._.leaf = 78 + den.ctx.leaf.provides.leaf = 79 79 { x }: 80 80 { 81 81 funny.names = [ "leaf-${x}" ];
+2 -2
templates/ci/modules/features/perf/ctx-pipeline.nix templates/ci/modules/features/ctx-pipeline.nix
··· 34 34 }; 35 35 into.leaf = { x }: lib.genList (i: { x = "${x}-${toString i}"; }) n; 36 36 }; 37 - den.ctx.leaf._.leaf = 37 + den.ctx.leaf.provides.leaf = 38 38 { x }: 39 39 { 40 40 funny.names = [ "leaf-${x}" ]; ··· 70 70 in 71 71 { den, ... }: 72 72 { 73 - den.ctx.${name}._.${name} = 73 + den.ctx.${name}.provides.${name} = 74 74 { v }: 75 75 { 76 76 funny.names = [ "${name}-${v}" ];
templates/ci/modules/features/perf/depth.nix templates/ci/modules/features/depth.nix
+2 -2
templates/ci/modules/features/perf/forward.nix templates/ci/modules/features/forward.nix
··· 5 5 test-forward-into-funny = denTest ( 6 6 { den, funnyNames, ... }: 7 7 let 8 - fwd = den._.forward { 8 + fwd = den.provides.forward { 9 9 each = lib.singleton true; 10 10 fromClass = _: "src"; 11 11 intoClass = _: "funny"; ··· 36 36 { den, funnyNames, ... }: 37 37 let 38 38 items = lib.genList (i: "item${toString i}") 20; 39 - fwd = den._.forward { 39 + fwd = den.provides.forward { 40 40 each = items; 41 41 fromClass = _: "src"; 42 42 intoClass = _: "target";
templates/ci/modules/features/perf/namespace.nix templates/ci/modules/features/namespace.nix
-136
templates/ci/modules/features/perf/parametric.nix
··· 1 - { denTest, lib, ... }: 2 - { 3 - flake.tests.performance.parametric = { 4 - 5 - test-fixedTo-deep-chain = denTest ( 6 - { den, funnyNames, ... }: 7 - let 8 - leaf = den.lib.parametric { 9 - funny.names = [ "leaf" ]; 10 - }; 11 - mid = den.lib.parametric { 12 - funny.names = [ "mid" ]; 13 - includes = [ leaf ]; 14 - }; 15 - top = den.lib.parametric.fixedTo { level = "deep"; } { 16 - funny.names = [ "top" ]; 17 - includes = lib.genList (_: mid) 20; 18 - }; 19 - in 20 - { 21 - den.ctx.start = { 22 - _.start = 23 - { level }: 24 - { 25 - funny.names = [ level ]; 26 - }; 27 - includes = [ top ]; 28 - }; 29 - 30 - expr = builtins.length (funnyNames (den.ctx.start { level = "deep"; })); 31 - expected = 42; 32 - } 33 - ); 34 - 35 - test-atLeast-wide = denTest ( 36 - { den, funnyNames, ... }: 37 - let 38 - mkParam = 39 - i: 40 - den.lib.parametric { 41 - funny.names = [ "p${toString i}" ]; 42 - includes = [ 43 - ( 44 - { host, ... }: 45 - { 46 - funny.names = [ "i${toString i}-${host}" ]; 47 - } 48 - ) 49 - ]; 50 - }; 51 - aspects = lib.genList mkParam 30; 52 - in 53 - { 54 - den.ctx.start = { 55 - _.start = 56 - { host }: 57 - { 58 - funny.names = [ host ]; 59 - }; 60 - includes = aspects; 61 - }; 62 - 63 - expr = builtins.length (funnyNames (den.ctx.start { host = "h"; })); 64 - expected = 61; 65 - } 66 - ); 67 - 68 - test-expands-propagation = denTest ( 69 - { den, funnyNames, ... }: 70 - let 71 - inner = 72 - { host, planet, ... }: 73 - { 74 - funny.names = [ "${host}-${planet}" ]; 75 - }; 76 - expanded = den.lib.parametric.expands { planet = "mars"; } { 77 - funny.names = [ "exp" ]; 78 - includes = lib.genList (_: inner) 15; 79 - }; 80 - in 81 - { 82 - den.ctx.start = { 83 - _.start = 84 - { host }: 85 - { 86 - funny.names = [ host ]; 87 - }; 88 - includes = [ expanded ]; 89 - }; 90 - 91 - expr = builtins.length (funnyNames (den.ctx.start { host = "h"; })); 92 - expected = 17; 93 - } 94 - ); 95 - 96 - test-dedup-parametric = denTest ( 97 - { den, funnyNames, ... }: 98 - let 99 - shared = den.lib.parametric { 100 - funny.names = [ "shared" ]; 101 - includes = [ 102 - ( 103 - { host, ... }: 104 - { 105 - funny.names = [ "inner-${host}" ]; 106 - } 107 - ) 108 - ]; 109 - }; 110 - in 111 - { 112 - den.ctx.a = { 113 - _.a = 114 - { host }: 115 - { 116 - funny.names = [ "a-${host}" ]; 117 - }; 118 - into.b = { host }: [ { host = "${host}!"; } ]; 119 - includes = [ shared ]; 120 - }; 121 - den.ctx.b = { 122 - _.b = 123 - { host }: 124 - { 125 - funny.names = [ "b-${host}" ]; 126 - }; 127 - includes = [ shared ]; 128 - }; 129 - 130 - expr = builtins.length (funnyNames (den.ctx.a { host = "v"; })); 131 - expected = 6; 132 - } 133 - ); 134 - 135 - }; 136 - }
+1 -1
templates/ci/modules/features/perf/pure-eval.nix templates/ci/modules/features/pure-eval.nix
··· 52 52 }; 53 53 into.b = { v }: [ { v = "${v}!"; } ]; 54 54 }; 55 - den.ctx.b._.b = 55 + den.ctx.b.provides.b = 56 56 { v }: 57 57 { 58 58 my.val = [ v ];
templates/ci/modules/features/perf/resolve.nix templates/ci/modules/features/resolve.nix
+9 -5
templates/ci/modules/features/provider-provenance.nix
··· 15 15 test-top-level-has-empty-provider = denTest ( 16 16 { den, ... }: 17 17 { 18 + den.fxPipeline = false; 18 19 den.aspects.foo.nixos = { }; 19 20 20 21 expr = (den.lib.aspects.resolve.withAdapter getProvenance "nixos" den.aspects.foo).provider; ··· 25 26 test-provided-aspect-has-provider-path = denTest ( 26 27 { den, ... }: 27 28 { 28 - den.aspects.foo.includes = [ den.aspects.foo._.bar ]; 29 - den.aspects.foo._.bar.nixos = { }; 29 + den.fxPipeline = false; 30 + den.aspects.foo.includes = [ den.aspects.foo.provides.bar ]; 31 + den.aspects.foo.provides.bar.nixos = { }; 30 32 31 33 expr = 32 34 let ··· 40 42 test-deep-provider-chain = denTest ( 41 43 { den, ... }: 42 44 { 43 - den.aspects.foo._.bar.includes = [ den.aspects.foo._.bar._.baz ]; 44 - den.aspects.foo._.bar._.baz.nixos = { }; 45 - den.aspects.foo.includes = [ den.aspects.foo._.bar ]; 45 + den.fxPipeline = false; 46 + den.aspects.foo.provides.bar.includes = [ den.aspects.foo.provides.bar.provides.baz ]; 47 + den.aspects.foo.provides.bar.provides.baz.nixos = { }; 48 + den.aspects.foo.includes = [ den.aspects.foo.provides.bar ]; 46 49 47 50 expr = 48 51 let ··· 60 63 test-namespace-provider-root = denTest ( 61 64 { den, ... }: 62 65 { 66 + den.fxPipeline = false; 63 67 den.provides.myaspect.nixos = { }; 64 68 65 69 expr = (den.lib.aspects.resolve.withAdapter getProvenance "nixos" den.provides.myaspect).provider;
+9 -9
templates/ci/modules/features/provides-parametric.nix
··· 27 27 28 28 den.aspects.igloo.includes = [ 29 29 ns.foo 30 - ns.bar._.baz 30 + ns.bar.provides.baz 31 31 ns.a 32 - ns.a._.b 33 - ns.a._.c 34 - ns.a._.d 35 - ns.a._.d._.e 32 + ns.a.provides.b 33 + ns.a.provides.c 34 + ns.a.provides.d 35 + ns.a.provides.d.provides.e 36 36 ]; 37 37 38 38 expr = igloo.networking.hostName; ··· 59 59 den.aspects.monitoring = 60 60 { host, ... }: 61 61 { 62 - includes = [ den.aspects.monitoring._.node-exporter ]; 62 + includes = [ den.aspects.monitoring.provides.node-exporter ]; 63 63 }; 64 64 } 65 65 { 66 - den.aspects.monitoring._.node-exporter = 66 + den.aspects.monitoring.provides.node-exporter = 67 67 { host, ... }: 68 68 { 69 69 nixos.networking.hostName = "${host.name}-monitored"; ··· 89 89 90 90 imports = [ 91 91 { 92 - den.aspects.monitoring.includes = [ den.aspects.monitoring._.agent ]; 92 + den.aspects.monitoring.includes = [ den.aspects.monitoring.provides.agent ]; 93 93 } 94 94 { 95 - den.aspects.monitoring._.agent = 95 + den.aspects.monitoring.provides.agent = 96 96 { host, ... }: 97 97 { 98 98 nixos.networking.hostName = "${host.name}-agent";
+3
templates/ci/modules/features/resolve-adapters.nix
··· 10 10 ... 11 11 }: 12 12 { 13 + den.fxPipeline = false; 13 14 14 15 den.aspects.foo.includes = [ den.aspects.bar ]; 15 16 den.aspects.bar.includes = [ den.aspects.baz ]; ··· 34 35 ... 35 36 }: 36 37 { 38 + den.fxPipeline = false; 37 39 38 40 den.aspects.foo.includes = [ den.aspects.bar ]; 39 41 den.aspects.bar.includes = [ den.aspects.baz ]; ··· 59 61 ... 60 62 }: 61 63 { 64 + den.fxPipeline = false; 62 65 den.hosts.x86_64-linux.igloo = { }; 63 66 den.hosts.x86_64-linux.iceberg = { }; 64 67
+1 -1
templates/ci/modules/features/special-args-custom-instantiate.nix
··· 27 27 programs.emacs.enable = osConfig.programs.vim.enable; 28 28 }; 29 29 30 - den.aspects.pingu.includes = [ den._.define-user ]; 30 + den.aspects.pingu.includes = [ den.provides.define-user ]; 31 31 32 32 expr = config.flake.homeConfigurations.pingu.config.programs.emacs.enable; 33 33 expected = true;
+4 -1
templates/ci/modules/features/top-level-parametric.nix
··· 12 12 }; 13 13 in 14 14 { 15 + den.fxPipeline = false; 15 16 den.hosts.x86_64-linux.igloo.users.tux = { }; 16 17 den.aspects.tux.includes = [ custom-user-config ]; 17 18 ··· 23 24 test-host-aspect-with-context = denTest ( 24 25 { den, igloo, ... }: 25 26 { 27 + den.fxPipeline = false; 26 28 den.hosts.x86_64-linux.igloo.users.tux = { }; 27 - den.aspects.igloo.includes = [ den._.hostname ]; 29 + den.aspects.igloo.includes = [ den.provides.hostname ]; 28 30 29 31 expr = igloo.networking.hostName; 30 32 expected = "igloo"; ··· 41 43 }; 42 44 in 43 45 { 46 + den.fxPipeline = false; 44 47 den.hosts.x86_64-linux.igloo.users.tux = { }; 45 48 den.aspects.tux.includes = [ from-both ]; 46 49
+7 -7
templates/ci/modules/features/user-host-mutual-config.nix
··· 42 42 pingu = { }; 43 43 }; 44 44 45 - den.ctx.user.includes = [ den._.mutual-provider ]; 46 - den.aspects.igloo._.to-users.homeManager.programs.direnv.enable = true; 45 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 46 + den.aspects.igloo.provides.to-users.homeManager.programs.direnv.enable = true; 47 47 48 48 expr = [ 49 49 tuxHm.programs.direnv.enable ··· 69 69 pingu = { }; 70 70 }; 71 71 72 - den.ctx.user.includes = [ den._.mutual-provider ]; 72 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 73 73 74 - den.aspects.igloo._.to-users.includes = [ 74 + den.aspects.igloo.provides.to-users.includes = [ 75 75 { 76 76 homeManager.programs.direnv.enable = true; 77 77 } ··· 136 136 pingu = { }; 137 137 }; 138 138 139 - den.ctx.user.includes = [ den._.mutual-provider ]; 139 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 140 140 141 - den.aspects.igloo._.to-users.includes = [ 141 + den.aspects.igloo.provides.to-users.includes = [ 142 142 ( 143 143 { host, user }: 144 144 { ··· 252 252 ... 253 253 }: 254 254 { 255 - den.ctx.user.includes = [ den._.mutual-provider ]; 255 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 256 256 257 257 den.hosts.x86_64-linux.igloo.users = { 258 258 tux = { };
+4 -4
templates/ci/provider/modules/den.nix
··· 10 10 (inputs.den.namespace "provider" true) 11 11 ]; 12 12 13 - provider.tools._.dev._.editors = { 13 + provider.tools.provides.dev.provides.editors = { 14 14 description = "Editor configurations from provider flake"; 15 15 nixos.programs.vim.enable = true; 16 16 homeManager = ··· 20 20 }; 21 21 }; 22 22 23 - provider.tools._.dev._.host-stamp = den.lib.parametric { 23 + provider.tools.provides.dev.provides.host-stamp = den.lib.parametric { 24 24 includes = [ 25 25 ( 26 26 { host, ... }: ··· 31 31 ]; 32 32 }; 33 33 34 - provider.tools._.dev._.user-stamp = den.lib.parametric.exactly { 34 + provider.tools.provides.dev.provides.user-stamp = den.lib.parametric.exactly { 35 35 includes = [ 36 36 ( 37 37 { host, user, ... }: ··· 43 43 }; 44 44 45 45 # A ctx entry shared to consumers — provides a self-provider function. 46 - provider.ctx.simple._.simple = _: { funny.names = [ "from-provider-ctx" ]; }; 46 + provider.ctx.simple.provides.simple = _: { funny.names = [ "from-provider-ctx" ]; }; 47 47 48 48 # A schema entry that can be shared to consumers. 49 49 provider.schema.entity = {
+1 -1
templates/default/modules/defaults.nix
··· 7 7 den.schema.user.classes = lib.mkDefault [ "homeManager" ]; 8 8 9 9 # host<->user provides 10 - den.ctx.user.includes = [ den._.mutual-provider ]; 10 + den.ctx.user.includes = [ den.provides.mutual-provider ]; 11 11 12 12 # User TODO: REMOVE THIS 13 13 den.aspects.tux.nixos = {
+1 -1
templates/example/modules/aspects/alice.nix
··· 46 46 home.packages = [ pkgs.htop ]; 47 47 }; 48 48 49 - # <user>.provides.<host>, via den._.mutual-provider 49 + # <user>.provides.<host>, via den.provides.mutual-provider 50 50 provides.igloo = 51 51 { host, ... }: 52 52 {
+2 -2
templates/example/modules/aspects/eg/vm.nix
··· 3 3 eg.vm.provides = { 4 4 gui.includes = [ 5 5 eg.vm 6 - eg.vm-bootable._.gui 6 + eg.vm-bootable.provides.gui 7 7 eg.xfce-desktop 8 8 ]; 9 9 10 10 tui.includes = [ 11 11 eg.vm 12 - eg.vm-bootable._.tui 12 + eg.vm-bootable.provides.tui 13 13 ]; 14 14 }; 15 15 }
+1 -1
templates/example/modules/aspects/igloo.nix
··· 10 10 environment.systemPackages = [ pkgs.hello ]; 11 11 }; 12 12 13 - # <host>.provides.<user>, via den._.mutual-provider 13 + # <host>.provides.<user>, via den.provides.mutual-provider 14 14 provides.alice = 15 15 { user, ... }: 16 16 {
+2 -2
templates/example/modules/vm.nix
··· 5 5 { 6 6 7 7 den.aspects.igloo.includes = [ 8 - eg.vm._.gui 9 - # eg.vm._.tui 8 + eg.vm.provides.gui 9 + # eg.vm.provides.tui 10 10 ]; 11 11 12 12 perSystem =
+1 -1
templates/flake-parts-modules/modules/den.nix
··· 17 17 }; 18 18 19 19 den.aspects.tux = { 20 - includes = [ den._.define-user ]; 20 + includes = [ den.provides.define-user ]; 21 21 user.description = "Bird"; 22 22 23 23 packages =
+1 -1
templates/flake-parts-modules/modules/perSystem-forward.nix
··· 4 4 perSystemFwd = 5 5 forwardArgs: 6 6 { class, aspect-chain }: 7 - den._.forward ( 7 + den.provides.forward ( 8 8 { 9 9 each = lib.optional (class == "flake-parts") forwardArgs; 10 10 intoClass = _: "flake-parts";
+1 -1
templates/microvm/modules/den.nix
··· 17 17 # 18 18 19 19 # automatically set hostname on all hosts. 20 - den.ctx.host.includes = [ den._.hostname ]; 20 + den.ctx.host.includes = [ den.provides.hostname ]; 21 21 }
+1 -1
templates/nvf-standalone/modules/nvf-integration.nix
··· 18 18 # a custom `vim` class that forwards to `nvf.vim` 19 19 vimClass = 20 20 { class, aspect-chain }: 21 - den._.forward { 21 + den.provides.forward { 22 22 each = lib.singleton true; 23 23 fromClass = _: "vim"; 24 24 intoClass = _: "nvf";