Den's FX pipeline is the core resolution engine that transforms declarative aspect definitions into NixOS/nix-darwin/home-manager module trees. It operates as an algebraic effects trampoline -- a loop that interprets effect values, dispatches them to handlers, and threads state through each step. Every state change is an effect; pure data transforms stay as functions.
This document covers the pipeline lifecycle from entity entry through final output assembly.
The pipeline resolves a single entity (flake, host, home, user) by walking its aspect tree. Each aspect is compiled according to its shape, gated for dedup and constraints, classified into output buckets, emitted as class modules, and then its children (includes, policies) are recursively resolved. The result is a set of NixOS-style module lists keyed by class name.
flowchart TD
Entry["Entity Entry<br/>(resolveEntity)"] --> mkPipeline
mkPipeline --> Resolve["resolve effect"]
Resolve --> Compile["compile (shape router)"]
Compile --> Gate["gate (dedup + constraint)"]
Gate --> Classify["classify keys"]
Classify --> Emit["emit-classes"]
Emit --> Children["resolve-children"]
Children --> Policies["installPolicies"]
Policies --> SchemaResolve["Schema entity resolution<br/>(push-scope, recurse)"]
SchemaResolve --> PostPipeline["Post-pipeline assembly"]
PostPipeline --> Output["{ imports = [...]; }"]
Key properties:
- Scope-partitioned state -- all collected modules, policies, routes, etc. are bucketed by scope ID, enabling per-entity subtree extraction
- Dedup by identity -- aspects are identified by
provider/namepath keys; duplicates within a scope are skipped - Lazy evaluation -- state fields wrapped as thunks (
_: value) survivebuiltins.deepSeqat each trampoline step without re-materializing large attrsets
Entities enter through den.lib.resolveEntity, called from modules/outputs.nix. An entity is a structural node (flake, host, home, user) that carries:
name-- the entity kind (e.g.,"host","home")includes-- self-provide wrapper + schema includesexcludes-- schema-level excludes__entityKind-- marks this as an entity root (triggers policy dispatch)__scopeHandlers-- constant handlers from augmented context
For entity kinds with .aspect on their schema entry (host, home, user), a parametric self-provide is injected: { __fn = c: c.${name}.aspect; __args = { ${name} = false; }; }. This defers the entity's own aspect resolution until its scope handlers are established.
mkPipeline constructs the pipeline for one entity resolution:
- Creates a root scope ID from the initial context (
mkScopeId ctx) - Composes default handlers with any extra handlers
- Initializes default state with the root scope
- Sends the initial
resolveeffect and runsfx.handle-- the trampoline loop
# Simplified mkPipeline flow
bootstrapAndResolve = fx.send "resolve" {
aspect = self;
identity = identity.key self;
ctx = ctx;
gated = true;
};
fx.handle { handlers; state; } bootstrapAndResolve;The flake pipeline entry resolves the "flake" entity, which triggers the to-systems policy to fan out per system, then to-os-outputs / to-hm-outputs to resolve hosts and homes.
The compile handler (handlers/compile.nix) inspects aspect metadata to determine shape:
| Shape | Detection | Handler | Purpose |
|---|---|---|---|
| Forward | meta.__forward present |
compile-forward |
Cross-class module routing |
| Conditional | meta.guard present |
compile-conditional |
Guard-gated includes |
| Parametric | __args != {} |
compile-parametric |
Scope-dependent resolution |
| Static | None of the above | compile-static |
Standard aspect with class keys |
The most common shape. Steps:
- Gate -- dedup + constraint check (skipped if
gated = true) - Classify -- partition keys into class, nested, pipe
- Emit-classes -- send class modules to collector
- Register constraints -- record aspect excludes
- Resolve-children -- recurse into includes + policies
Aspects with __args (scope-dependent functions). Steps:
- Gate check
- Bind -- probe scope handlers for required args
- If all args available: call
compileFnto resolve the function, producing a new aspect - If args missing: defer for later resolution
- If all args available: call
- If bound: re-enter pipeline via
resolvewith the result (re-routes through compile router)
Depth limit: 10 levels of parametric nesting.
Forwards move modules between classes. They bypass dedup and constraints entirely.
- Tier 1 (simple): source already collected, no adapter needed -- becomes a route spec
{ fromClass, intoClass, path } - Complex: full forward spec stored with
__complexForwardmarker
Resumes [] -- all work happens via route registration in state.
Guard-gated aspect includes. The guard function receives a context with hasAspect -- an exclude-aware function that checks both the pipeline's pathSet and the constraint registry.
- Guard passes: emit the guarded aspects as includes
- Guard fails: defer the conditional for re-evaluation when the pathSet grows (via
drain-conditionals)
The gate (handlers/gate.nix) is a composite effect combining dedup and constraint checks.
Identity-based dedup within a scope:
- Dedup key:
"${scopeId}/${identityKey}"whereidentityKey = identity.key aspect - Anonymous and synthetic names (wrapped in
<>) are never deduped - The
includeSeenthunk in state tracks seen keys
The constraint registry supports three types:
- exclude -- blocks an aspect by identity (prefix-matched)
- substitute -- replaces an aspect with an alternative
- filter -- predicate-based exclusion
Constraints are scoped via ownerChain (the includes chain at registration time). A constraint applies if (scope == "global") || isAncestor(ownerChain, currentChain).
Gate outcomes:
{ blocked = true; result = [...] }-- aspect excluded or substituted (tombstone emitted){ passed = true; owner? }-- aspect passes, optionally tagged with constraint owner
The classify handler (handlers/classify.nix) delegates to classifyKeys, which partitions an aspect's attribute keys into three buckets:
| Bucket | Condition | Example |
|---|---|---|
| classKeys | Key matches a registered class (den.classes) or an unregistered class key |
nixos, darwin, homeManager |
| nestedKeys | Key is provides, _, or a sub-aspect namespace |
_, provides |
| pipeKeys | Key matches a registered quirk (den.quirks) |
packages, checks |
Structural keys (name, meta, includes, __args, __fn, __scopeHandlers, etc.) are excluded from classification.
Iterates class keys and pipe keys, unwrapping content values and sending individual emit-class effects:
aspect.nixos = { networking.hostName = "test"; };
--> unwrapContentValuesList --> [ module ]
--> emit-class { class = "nixos"; identity = "myAspect"; module; ctx; ... }
Each class key may contain multiple modules (list-valued content). Multi-module entries get indexed identities: "myAspect[0]", "myAspect[1]".
The emit-class handler collects modules into scope-partitioned state:
- Location key:
"${class}@${baseIdentity}"(e.g.,"nixos@networking/hostname") - Dedup:
scopedEmittedLocstracks emitted locations per scope; duplicate locations are silently dropped - Storage:
scopedClassImports--{ scopeId -> { class -> [ module ] } }
Context-dependent aspects (those resolved through parametric binding with context args) preserve their full identity including context suffix, preventing incorrect dedup across scopes.
resolve-children (handlers/resolve-children.nix) is where the tree walk recurses. After an aspect's own class modules are emitted, it processes:
- chain-push -- push aspect identity onto the includes chain (for constraint scoping)
- emitAspectPolicies -- register self-provide policies from the aspect's
provides/_namespace - emitIncludes -- resolve each child in
aspect.includes - installPolicies -- if this is an entity root (
__entityKindpresent), dispatch entity-level policies - drain-conditionals -- at entity boundaries, re-evaluate deferred guards
- chain-pop + resolve-complete -- unwind chain and record the resolved aspect in pathSet
flowchart TD
RC["resolve-children"] --> ChainPush["chain-push"]
ChainPush --> EmitPolicies["emitAspectPolicies<br/>(provides)"]
EmitPolicies --> EmitIncludes["emitIncludes<br/>(each child)"]
EmitIncludes --> EntityCheck{Entity root?}
EntityCheck -->|Yes| InstallPolicies["installPolicies"]
EntityCheck -->|No| DrainCheck
InstallPolicies --> DrainCheck{Drain conditionals?}
DrainCheck -->|Yes| Drain["drain-conditionals"]
DrainCheck -->|No| Complete["resolve-complete"]
Drain --> Complete
Scopes partition the pipeline's state by entity context. Each entity resolution creates a new scope.
flowchart TD
Flake["flake<br/>scope: ''"] --> FlakeSystem["flake-system<br/>scope: system=x86_64-linux"]
FlakeSystem --> Host1["host: igloo<br/>scope: host=igloo,system=x86_64-linux"]
FlakeSystem --> Host2["host: thinkpad<br/>scope: host=thinkpad,system=x86_64-linux"]
Host1 --> User1["user: tux<br/>scope: host=igloo,system=x86_64-linux,user=tux"]
Host2 --> User2["user: tux<br/>scope: host=thinkpad,system=x86_64-linux,user=tux"]
mkScopeId ctx produces a canonical comma-separated "key=value" string from the context attrset, sorted by key. Example: "host=igloo,system=x86_64-linux".
Atomically updates state when entering a child entity's scope:
- Sets
currentScopeto the new scope ID - Records context in
scopeContexts - Records parent in
scopeParent - Initializes empty
scopedAspectPoliciesfor the new scope - Resets
inLateDispatch(each scope level gets its own late-dispatch opportunity) - Fans out any
scopedDeferredIncludesfrom the parent scope
Pops back to the parent scope after entity resolution completes. Restores currentScope and inLateDispatch from a stack.
Orchestrates the full scope transition for entity resolution:
push-scope-- create child scope with entity contextscope.provide-- install scope handlersresolve-entity-- build entity node from schemaresolve-- walk the entity's aspect treedrain-- resolve deferred includes now satisfiable with the new contextpropagate-routes-- propagate forward routes from child scoperestore-scope-- return to parent scope
Policies are user-defined functions that fire when their argument signature is satisfied by the current scope context. They produce typed effects that drive entity creation, module routing, and content delivery.
Entry point at entity boundaries. Steps:
- Read current state (scope, context, aspect policies)
- Check dedup (
dispatchKey = "${entityKind}@${scope}") - Call
iteratewith aspect policies and context
flowchart TD
Start["iterate"] --> Dispatch["dispatch-policies"]
Dispatch --> Check{"New enrichment<br/>keys?"}
Check -->|Yes| Widen["widen-context"]
Widen --> Dispatch
Check -->|No| RecordFired["record-fired"]
RecordFired --> EmitEffects["emit-policy-effects"]
EmitEffects --> Done["results"]
Enrichment is key-monotonic -- keys are only added, never changed. Convergence is guaranteed because each iteration adds at least one new key (or terminates). Max iterations: 10.
Runs each aspect policy against the resolve context:
- Filters to policies whose args are satisfied by context AND haven't already fired
- Calls policy function, validates returned effects
- Classifies results via
classifyPolicyResult - Returns tagged effects + enrichment + fired names
Partitions each policy's effects by type:
- resolve effects are further split into schema (entity-kind keys like
host,user) and enrichment (other keys) - include, exclude, route, instantiate, provide, pipe effects pass through tagged with source policy name
- Cross-provider detection: when a policy produces both schema resolves and includes, includes are attached to the schema entity for scoped resolution
| Effect | Builder | Purpose |
|---|---|---|
resolve |
den.lib.policy.resolve.to |
Create child entity scope (host, user, home) |
include |
den.lib.policy.include |
Add aspects to current entity |
exclude |
den.lib.policy.exclude |
Block an aspect by identity |
route |
den.lib.policy.route |
Move modules between classes |
instantiate |
den.lib.policy.instantiate |
Evaluate modules into flake outputs |
provide |
den.lib.policy.provide |
Inject modules into target classes |
pipe |
den.lib.policy.pipe |
Register pipe transform/collect stages |
Effects are applied in a specific order via emitPolicyEffectsThen:
- Excludes first -- register constraints before any includes are processed
- Routes -- register route specs
- Instantiates -- register entity instantiation specs
- Provides -- register provide specs
- Pipe effects -- register pipe transform stages
- Then: schema resolves + includes (the continuation)
This ordering ensures guards in conditional aspects see excludes before evaluating.
processSchemaResolves handles resolve.to effects:
- Decomposes each schema effect into target kind, bindings, scoped context, entity class
- Checks
ctx-seenfor dedup (prevents re-resolving same entity) - First visit: sends
resolve-schema-entityto create child scope and walk - Repeat visit with new aspects: sends supplemental resolution for just the new aspects
- Late dispatch: after all sibling entities resolve, re-dispatches policies registered by later siblings that may apply to earlier ones
Deferral handles aspects whose required scope arguments aren't yet available.
flowchart TD
Bind["bind: probe args"] --> Available{"All args<br/>available?"}
Available -->|Yes| CompileFn["compileFn(aspect)"]
Available -->|No| Defer["defer: store in<br/>scopedDeferredIncludes"]
Defer --> Stub["emit resolve-complete<br/>stub (deferred marker)"]
subgraph "Later: context expands"
Widen["scope-widened"] --> Drain["drain: partition<br/>by satisfiability"]
Drain --> Satisfiable["Re-resolve<br/>satisfiable aspects"]
Drain --> Remaining["Keep remaining<br/>deferred"]
end
Probes for required args using fx.effects.hasHandler:
- Checks scope handlers on the aspect
- Falls back to scope context from pipeline state (for child scopes)
- Detects pipe arg references (unconditionally defers -- pipe data is post-pipeline)
- Augments
__scopeHandlerswith available scope context values
Stores the deferred aspect in scopedDeferredIncludes and emits a stub with meta.deferred = true.
Partitions deferred includes by whether their required args are satisfied by the current context. Returns satisfiable items; remaining stay in state.
Triggered by widen-context during policy enrichment. Drains newly-satisfiable deferred includes and re-resolves them.
A final drain pass in fxResolve handles:
- Pipe-arg deferred -- aspects needing pipe data, now available from
assemblePipes - Enrichment-deferred -- aspects needing parent enrichment keys, resolved by walking
scopeParentto inherit ancestor context
Conditionals (compile-conditional.nix) use an exclude-aware hasAspect that checks both the pathSet and constraint registry. This prevents guards from seeing aspects that have been excluded by policy.
guardCtx = mkGuardCtx {
pathSet; -- all resolved aspect identities so far
constraintRegistry; -- scope-specific excludes (collected from ancestors)
scopeHandlers; -- provides entity stubs: { host.hasAspect = ...; }
};
pass = condNode.meta.guard guardCtx;drain-conditionals runs at entity boundaries with fixed-point iteration:
- Re-evaluate all deferred conditionals against current pathSet
- If any pass: emit their includes, mark as progressed, loop
- If none progress: tombstone the remaining guards (convergence -- guard dependencies can't be satisfied)
- Convergence guaranteed: each progressing pass resolves at least one guard
Routes move modules between classes (e.g., from packages class to flake class at a specific path). Applied post-pipeline in phase 3 of resolve.nix.
- Simple routes (Tier 1 forwards):
{ fromClass, intoClass, path, guard?, adaptArgs? }-- nest source modules at the target path - Complex routes (
__complexForward): full forward specs withmapModule,evalConfig, adapter support
For each route:
- Collect source modules from
scopedClassImports[sourceScopeId][fromClass] - If no modules collected: resolve source aspect as fallback
- Wrap modules with any adapter or guard
- Append wrapped modules to target class at target scope
Pipes (den.quirks) collect typed data from aspects during the pipeline walk, then assemble it post-pipeline into scope contexts for delivery to modules.
- Pipe keys on aspects are emitted via
emit-classwith__isPipeEntry = true - Policy
pipeeffects register stages inscopedPipeEffects
assemblePipes processes each scope:
- Collect base values from
scopedClassImports[scope][pipeName] - Merge exposed data from child scopes (via
pipe.exposestages, collected bottom-up) - Apply untargeted effects:
filter,transform,fold,append,for,collectstages - Build targeted data (
pipe.to): transform and deliver to specific aspects via__pipeTargeted - Process
pipe.as: rename pipe data across pipe boundaries - Resolve parametric values:
{ host, ... }: exprresolved against scope context - Mark config thunks:
{ config, ... }: exprmarked for deferred resolution insideevalModules - Inject assembled pipe data into scope contexts
fxResolve (resolve.nix) orchestrates four post-pipeline phases:
Wraps raw class imports per scope using wrapCollectedClasses:
- Merges enrichment into emit-time context
- Strips enrichment-only args from module function signatures (prevents NixOS probing for unknown
_module.args) - Deduplicates keyed modules across scopes (first occurrence wins)
Injects policy.provide modules into target classes:
- Deduplicates by composite key (
policyName/class/path) - Wraps modules with class module wrapping (collision policy, location)
Applies all registered routes (see section 13).
Creates flake outputs from entity specifications:
- Finds the host scope ID by searching
scopeParentchildren - Per-host subtree assembly: re-runs phases 1-3 scoped to just the host's subtree, producing correct routing without cross-host contamination
- Calls
spec.instantiate { modules; pkgs?; }to produce the evaluated configuration - Merges instantiate outputs into
classImports.flakeviarecursiveUpdate
{
imports = phase4.${class} or []; # e.g., phase4.nixos
}| Field | Type | Purpose |
|---|---|---|
seen |
thunk { } |
General seen tracking |
pathSet |
thunk { identity -> true } |
All resolved aspect identities (for hasAspect) |
includeSeen |
thunk { dedupKey -> true } |
Dedup tracking for includes |
flatConstraintRegistry |
{ identity -> [entry] } |
Pre-merged constraint lookup (avoids O(S) rebuild) |
flatConstraintFilters |
[entry] |
Pre-merged filter constraints |
flatAspectPolicies |
{ name -> policy } |
Pre-merged policy lookup |
| Field | Type | Purpose |
|---|---|---|
scopedClassImports |
thunk { scope -> { class -> [mod] } } |
Collected class modules |
scopedAspectPolicies |
thunk { scope -> { name -> policy } } |
Registered aspect policies |
scopedDeferredIncludes |
thunk { scope -> [deferred] } |
Pending parametric aspects |
scopedDeferredConditionals |
thunk { scope -> [conditional] } |
Pending guard evaluations |
scopedConstraintRegistry |
thunk { scope -> { identity -> [entry] } } |
Per-scope constraints |
scopedRoutes |
thunk { scope -> [routeSpec] } |
Registered routes |
scopedInstantiates |
thunk { scope -> [instantiateSpec] } |
Entity instantiation specs |
scopedProvides |
thunk { scope -> [provideSpec] } |
Provide specs |
scopedPipeEffects |
thunk { scope -> [pipeEffect] } |
Pipe transform stages |
scopedEmittedLocs |
thunk { scope -> { loc -> true } } |
Class collector dedup |
| Field | Type | Purpose |
|---|---|---|
rootScopeId |
string |
Root scope identity |
currentScope |
string |
Currently active scope |
scopeContexts |
thunk { scope -> ctx } |
Context attrset per scope |
scopeParent |
thunk { scope -> parentScope } |
Parent scope mapping |
| Field | Type | Purpose |
|---|---|---|
firedPolicyNames |
thunk { dispatchKey -> { name -> policyFired } } |
Which policies fired where |
dispatchedPolicies |
thunk { dispatchKey -> true } |
Prevents re-dispatching same entity kind at same scope |
inLateDispatch |
bool |
Per-scope late-dispatch flag (prevents O(N^2) re-dispatch) |
Aspect identity (identity.nix) is a path-based key:
identity.key aspect = concatStringsSep "/" (provider ++ [name] ++ optional ctxId)
Examples:
"networking/hostname"-- simple aspect"networking/hostname/{host=igloo,system=x86_64-linux}"-- context-qualified"~networking/hostname"-- tombstone (excluded aspect)
The pathSet tracks all resolved identities. hasAspect checks this set, minus excluded entries from the constraint registry.
Complete reference for all pipeline effects — their parameters, resume values, and state mutations.
| Effect | Param | Resume | State |
|---|---|---|---|
resolve |
{ aspect, identity, ctx, gated? } |
forwarded from compile |
— |
compile |
{ aspect, identity, ctx } |
routed to shape handler | — |
compile-static |
{ aspect } |
[resolved] |
class emissions via emit-class |
compile-parametric |
{ aspect, compileFn } |
{ value } or { deferred } |
— |
compile-forward |
{ aspect } |
[] |
route in scopedRoutes |
compile-conditional |
{ aspect } |
includes result or [] (deferred) |
conditional in scopedDeferredConditionals if guard fails |
gate |
{ aspect, identity } |
{ passed } or { blocked, result } |
dedup in includeSeen |
| Effect | Param | Resume | State |
|---|---|---|---|
classify |
{ aspect, targetClass } |
{ classKeys, nestedKeys, pipeKeys } |
— |
emit-classes |
{ aspect, classKeys, pipeKeys, identity } |
seq result | via emit-class |
emit-class |
{ class, identity, module, ctx, ... } |
null |
scopedClassImports, scopedEmittedLocs |
| Effect | Param | Resume | State |
|---|---|---|---|
resolve-children |
{ aspect, isMeaningful, chainIdentity } |
resolved aspect | drain-conditionals at entity level |
resolve-complete |
resolved aspect | param passthrough | pathSet updated (excluded aspects removed) |
emit-include |
{ child, __parentScopeHandlers?, ... } |
include results | — |
push-scope |
{ scopedCtx, entityClass, parentScope } |
{ scopeId, scopeHandlers } |
scope stack, scopeContexts, scopeParent |
restore-scope |
{ parentScope } |
null |
currentScope restored |
resolve-schema-entity |
{ targetKind, scopedCtx, entityClass, ... } |
results list | full scope lifecycle |
resolve-entity |
{ kind } |
entity record | — (reads den.schema) |
| Effect | Param | Resume | State |
|---|---|---|---|
dispatch-policies |
{ aspectPolicies, firedPolicies, resolveCtx } |
classified effects | — |
emit-policy-effects |
{ effects, entityKind, enrichedCtx } |
include/route results | constraints, routes registered |
record-fired |
{ entityKind, firedPolicies } |
null |
firedPolicyNames |
widen-context |
{ enrichment, currentCtx } |
null |
scopeContexts updated |
| Effect | Param | Resume | State |
|---|---|---|---|
register-constraint |
{ type, scope, identity, owner } |
null |
scopedConstraintRegistry, flatConstraintRegistry |
check-constraint |
{ identity, aspect } |
{ action, owner? } |
— |
check-dedup |
aspect | { isDuplicate, dedupKey } |
includeSeen |
| Effect | Param | Resume | State |
|---|---|---|---|
bind |
{ aspect, compileFn } |
{ value } or { deferred } |
— |
defer |
{ child, requiredKeys, requiredArgs } |
[] |
scopedDeferredIncludes |
drain |
ctx | satisfiable list | scopedDeferredIncludes cleaned |
scope-widened |
{ ctx } |
drain results | scopedDeferredIncludes |
defer-conditional |
condNode | null |
scopedDeferredConditionals |
drain-conditionals |
null |
emitted + tombstoned results | scopedDeferredConditionals cleaned |
| Effect | Param | Resume | State |
|---|---|---|---|
chain-push |
{ identity } |
null |
scopedIncludesChain |
chain-pop |
null |
null |
scopedIncludesChain |
register-aspect-policy |
{ name, fn, ownerIdentity } |
null |
scopedAspectPolicies, flatAspectPolicies |
register-route |
route spec | null |
scopedRoutes |
register-instantiate |
spec | null |
scopedInstantiates |
register-provide |
spec | null |
scopedProvides |
register-pipe-effect |
spec | null |
scopedPipeEffects |
propagate-routes |
{ scopeId } |
null |
parent scope scopedRoutes |
get-path-set |
null |
pathSet attrset | — |
Behavioral contracts validated by the pipeline test suite (templates/ci/modules/features/fx-*.nix):
- Named aspects dedup within a scope (same
provider/name→ gate blocks second occurrence) - Anonymous aspects (
<anon>:N) never dedup — each contributes independently - Tombstones (excluded aspects) are NOT added to the pathSet
meta.__forward→ forward (highest priority)meta.guard→ conditional__args != {}→ parametric- Else → static
- Scope-filtered via
ownerChainancestry: a subtree exclude only applies within its owning includes chain - Global scope excludes apply everywhere regardless of chain
- Constraint check returns
action:"exclude"|"substitute"|"keep"
- Policies only fire when
resolveArgsSatisfiedpasses (all required args present in context) - Already-fired policies (tracked per
entityKind@scope) are skipped - Enrichment convergence: key-monotonic (keys only added, never changed)
- Effect application order: excludes → routes → instantiates → provides → pipes → includes
- Scope handlers are probed first, then scope context from state as fallback
- Pipe arg references unconditionally defer (pipe data is post-pipeline)
- Depth limit: 10 levels of parametric nesting
- Guards receive exclude-aware
hasAspectbuilt from pathSet + scope-specific constraint registry - Guards that fail are deferred;
drain-conditionalsre-evaluates with fixed-point iteration - Convergence: each progressing pass resolves ≥1 guard; no progress → cross-dependent → tombstone
- Entity-shaped stubs (
{ host = { hasAspect }; ... }) provided from scope handler keys without evaluating entity config (avoids cycle)
fxResolvereturns{ imports = [...]; }— a NixOS modulefxFullResolvereturns{ value, state }withscopedClassImportsin state- Class modules are keyed modules with
_filelocation for NixOS merge diagnostics