Date: 2026-05-27 Status: Design approved Libraries affected: gen-schema, gen-aspects, gen-algebra, gen-scope Consumer: den (wiring only — no gen-level domain concepts) Supersedes: 2026-05-27-gen-settings-design.md (draft)
-
gen-aspects and gen-schema are parallel type systems. Aspects are typed records with identity, collections, extension, and introspection — exactly what gen-schema provides — but implemented independently. This duplicates infrastructure and prevents cross-cutting features.
-
Aspects need tunable settings: typed configuration declared on aspects, assigned at entity scope levels, composed via scope graph resolution (D > I > P), validated lazily. Building this requires bridging two separate type systems — or unifying them.
-
Users cannot attach arbitrary typed metadata to aspects without framework changes (
cnf.aspectModules,cnf.metaModules). With gen-schema, extension is natural: any module canconfig.schema.aspect.options.myField = ....
Gen provides primitives. The consumer (den) composes them. No gen library knows about "settings," "hosts," or "aspects" as domain concepts. Each library contributes general-purpose machinery.
- gen-schema gains pluggable entry types —
mkTypeparameter onmkSchemaEntryType. Default:deferredModule(backwards compatible). Custom: any NixOS type. - gen-aspects ports onto gen-schema — aspects use gen-schema's kind-level infrastructure (collections, introspection, extension) with a custom entry type. Clean API break (gen is unshipped greenfield).
- Settings become a gen-schema collection on aspect kinds — schema declared on aspects, values composed via scope graph, validated lazily.
- gen-scope gains a neron traverse mode — D > I > P ordered collection for fold-based composition.
gen-schema's role for aspects is kind-level infrastructure only:
- gen-schema DOES: collection extraction (settings, classes), introspection, extension, computed fields, topology
- gen-schema DOES NOT: instance registry (
mkInstanceRegistry), content-based identity (id_hash), strict validation (mkStrictModule)
Aspects use positional identity (path in aspect tree), not content identity (hash of field values). Two aspects with identical content at different positions are different aspects — the opposite of entities. Parametric aspects make content identity meaningless (same function resolves to different content per scope).
Instance-level infrastructure (mkInstanceType, mkInstanceRegistry) remains for flat entity registries (hosts, users). Aspects use their own recursive option type (lazyAttrsOf aspectType) directly.
mkSchemaEntryType gains an optional mkType parameter:
mkSchemaEntryType = {
baseModule ? null,
collections ? {},
computed ? null,
mixins ? [],
mkType ? null, # NEW — custom type constructor
}: ...When mkType is null (default): current behavior — deferredModuleWith { staticModules = [kindModule]; }. Zero breaking change.
When mkType is provided: receives { kindModule, collections, schema }, returns a NixOS type. gen-schema uses the returned type for kind entries instead of deferredModule.
These operate on config.schema.<name>, not on individual entries:
- Collection extraction from kind definitions
_kindNames,_topology,_roots,_leaves_edges,_refEdges_kindMetarenderDocs- Computed fields
- How individual entries are typed and merged
- Whether freeform is allowed and what shape it takes
- How function values are handled
gen-schema extracts collections during merge by inspecting raw defs. This only applies to inline attrset defs — path defs and function defs get the collection's default value. This is compatible with gen-aspects: function-valued aspect defs are guard functions (deferred resolution), not inline collection contributors. Collections like includes and neededBy are set on inline attrset defs only.
When mkType is provided, gen-schema's collection extraction runs first on the raw defs, strips collection keys, then passes the stripped defs to the custom type's merge. The custom type never sees collection keys — they're already extracted. Mixin pipeline (baseRecord → applyMixin → emitModule) is skipped when mkType is provided — the custom type owns the full type construction. __functor wrapping on the merged result is likewise the custom type's responsibility.
~20 lines — conditional dispatch on mkType presence in mkSchemaEntryType.
gen-aspects becomes a thin configuration layer over gen-schema. It provides the aspect-specific entry type and utilities; gen-schema provides the infrastructure.
gen-aspects provides (reduced surface):
aspectType— Palmer flat dispatch merge function (attrset → submodule, function → guard wrap, primitive → passthrough)canTake/mkIsModuleFn— function arg introspection for module vs guard classificationidentity— key, aspectPath, pathKey computationmkAspectSchema— convenience constructor that configures gen-schema for aspects
gen-schema provides (already exists, used directly):
- Collection extraction for kind-level data (
settings,classes— extracted before merge, available as schema data) - Extension (any module can
config.schema.aspect.options.myField = ...) - Introspection (
_kindMeta,_kindNames) - Identity hashing (via
mkIdentityModule) - Validation and refinements
- Topology
Important distinction — collections vs options:
includes,excludes,provides,policies,meta: Options on the aspect entry type (per-instance, consumed at runtime by the pipeline). These live in the aspectType's submodule, not as gen-schema collections.settings,classes: Gen-schema collections (kind-level data extracted before merge, available for schema-level operations).classesreplaces den's current manual collection walk inaspect-schema.nix.neededBy: New option on the aspect entry type (concept exists in gen-aspects README, not yet implemented in den).
mkAspectSchema returns two things: a schema option (kind-level infrastructure) and a type constructor (for creating aspect option points).
aspects = gen-aspects { inherit lib; gen-schema = gen-schema { inherit lib; }; };
cnf = {
classes = { nixos = {}; homeManager = {}; };
moduleArgs = { lib = true; config = true; options = true; pkgs = true; };
collections = {
settings = { default = {}; };
classes = { default = {}; };
};
};
# Schema option — kind-level infrastructure (one per den instance)
schema = aspects.mkAspectSchema cnf;
options.schema = schema.schemaOption; # provides config.schema.aspect
# Aspect option points — multiple, sharing the same type
options.den.aspects = schema.mkAspectOption {}; # providerPrefix = []
options.den.ful = lib.mkOption {
type = lib.types.attrsOf (schema.mkNamespaceType {}); # per-namespace
};
# Usage is standard gen-schema + aspects:
config.aspects.networking = {
nixos.networking.hostName = "myhost";
includes = [ config.aspects.firewall ];
};
# User-defined metadata — just extend the kind:
config.schema.aspect.options.priority = lib.mkOption {
type = lib.types.int;
default = 50;
};
config.aspects.networking.priority = 10;mkAspectSchema returns:
{
# Kind-level: gen-schema option for collections, introspection, extension
schemaOption = schemaLib.mkSchemaOption {
inherit collections computed;
mkType = { kindModule, ... }: aspectType {
inherit kindModule;
classes = cnf.classes;
moduleArgs = cnf.moduleArgs;
};
};
# Instance-level: creates an aspect option point with providerPrefix
mkAspectOption = { providerPrefix ? [] }: lib.mkOption {
type = lib.types.lazyAttrsOf (aspectType {
inherit (cnf) classes moduleArgs;
inherit providerPrefix;
});
default = {};
};
# Namespace-level: submodule with schema, classes, and aspect freeform
mkNamespaceType = {}: lib.types.submodule ({ name, ... }: {
options.schema = lib.mkOption { /* per-namespace schema overrides */ };
options.classes = lib.mkOption { /* class declarations */ };
freeformType = lib.types.lazyAttrsOf (aspectType {
inherit (cnf) classes moduleArgs;
providerPrefix = [ name ];
});
});
}gen-schema provides kind-level infrastructure on config.schema.aspect. gen-aspects provides the recursive entry type. Multiple option points share the same type with different providerPrefix for identity scoping.
Current mechanism: den.ful.<name> namespaces use namespaceType — a submodule with freeformType = aspectsType { providerPrefix = [name] }. External flakes export flake.denful.<name>, imported via den.namespace "name" [inputs.externalFlake].
With gen-schema: Same structure, but:
- All namespaces share one
config.schema.aspectkind definition - Schema extensions from external flakes apply everywhere
- Collections (
settings,classes) compose across namespace boundaries providerPrefixscopes positional identity:myLib/nginxvsaspects/nginx
# External flake (nginx-aspects)
{
outputs = { ... }: {
flake.denful.nginx-lib = {
nginx = {
settings.port = { type = types.int; default = 80; };
nixos = { ... };
};
};
# Schema extension — applies to ALL aspect option points
flakeModules.default = { ... }: {
config.schema.aspect.options.monitoring = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = {};
};
};
};
}
# Consumer
{
imports = [
(den.namespace "nginx" [ inputs.nginx-aspects ])
inputs.nginx-aspects.flakeModules.default
];
# Imported aspects available via namespace
den.hosts.web1.settings.nginx.port = 8080;
# Schema extension visible on all aspects
den.aspects.myApp.monitoring.enabled = true;
}Key properties:
- One shared kind definition, multiple option points
- Schema extensions are global (kind-level)
- Settings schemas compose across imports
- Identity is namespace-scoped via
providerPrefix stripAliasesin namespace.nix unchanged (entry-type concern)
| Component | Purpose | Stays because |
|---|---|---|
aspectType |
Palmer flat dispatch in merge | Aspect-specific merge behavior; gen-schema has no equivalent |
canTake / mkIsModuleFn |
Module vs guard function classification | Depends on cnf.moduleArgs; aspect-specific concern |
identity |
Key/path computation from name/meta | Aspect-specific identity scheme (not gen-schema's id_hash) |
| Recursive freeform setup | lazyAttrsOf (aspectType cnf) |
Self-recursive type; aspect-specific |
| Class option generation | deferredModule per cnf.classes |
Aspect-specific content routing |
| Current | New |
|---|---|
aspectsType cnf |
mkAspectSchema { ... } |
aspectSubmodule cnf |
Internal to mkAspectSchema |
aspectType cnf |
Internal to mkAspectSchema, or exported for advanced use |
cnf.aspectModules |
Extend the aspect kind: config.schema.aspect.options.* |
cnf.metaModules |
Extend meta: config.schema.aspect.options.meta.* |
cnf.classes |
mkAspectSchema { classes = { ... }; } |
gen-schema consumes gen-algebra via inputs ? {} with CI flake.lock fallback, lib passed separately, specific functions threaded to sub-modules. gen-aspects follows identically:
# gen-aspects/nix/lib/default.nix
{
inputs ? {},
lib,
}:
let
lock = builtins.fromJSON (builtins.readFile ../../ci/flake.lock);
inherit (lock.nodes.gen-schema) locked;
schemaSrc = builtins.fetchTarball {
url = "https://github.com/${locked.owner}/${locked.repo}/archive/${locked.rev}.zip";
sha256 = locked.narHash;
};
genSchema = inputs.gen-schema or (import schemaSrc { inherit lib; });
in
{
inherit (genSchema) mkSchemaOption mkSchemaEntryType;
# ... gen-aspects exports
}Key pattern rules:
- gen-schema is NOT a flake input — resolved via CI flake.lock fallback
libpassed separately (both gen-schema and gen-aspects receive it independently)- Specific genSchema functions threaded to sub-modules, not the whole genSchema attrset
- Re-exports genSchema functions that consumers need (e.g.,
mkSchemaOption)
gen-aspects gains gen-schema as a dependency (currently independent). Acceptable: gen-aspects was reimplementing half of gen-schema's functionality.
gen-algebra (pure primitives)
├── gen-schema (typed registries)
│ └── gen-aspects (aspect types — NEW dependency)
├── gen-select (selector algebra)
│ └── gen-derive (rule dispatch)
│
gen-scope (HOAG evaluator) ← independent
gen-graph (graph queries) ← independent
gen-bind (module binding) ← independent
config.aspects.nginx.settings = {
port = { type = types.int; default = 80; };
workers = { type = types.positive; default = 4; };
locations = {
type = types.attrsOf (types.submodule {
options.proxy_pass = lib.mkOption { type = types.str; };
});
default = {};
merge = "recursive";
};
};settings is a gen-schema collection on the aspect kind with { default = {}; } and attrset merge. Collection extraction pulls settings declarations out of the aspect definition and exposes them on the merged result.
Entity-level settings are plain attrset assignments on scope node declarations. They do NOT module-system-merge across scope levels — each entity has its own scope node with its own declarations:
den.environments.prod.settings.nginx.port = 443;
den.hosts.web1.settings.nginx.port = 8080;
den.hosts.web1.settings.nginx.workers = 16;gen-scope collects contributions via D > I > P traversal (section 4). The fold uses gen-algebra's record with scoped labels (Leijen 2005):
Each scope level's contribution is pushed onto the record via record.extend. record.select returns the head (most specific). Schema defaults sit at the bottom — pushed first, shadowed by any explicit value. No mkDefault needed.
The fold function respects per-field merge strategies from the settings schema:
| Strategy | Semantics | Use case |
|---|---|---|
"replace" (default) |
record.select — head of stack wins |
Scalars, enums, booleans |
"append" |
List concatenation across all layers | IP allowlists, package lists |
"recursive" |
Nested attrset merge, recurse with sub-field strategies | Location blocks, nested config |
Example resolution for host:web1 with P-edge to env:prod:
D: { port = 8080; workers = 16; } ← host:web1 (most specific)
P: { port = 443; } ← env:prod
base: { port = 80; workers = 4; } ← schema defaults
Result: { port = 8080; workers = 16; }
The composed result wraps each field with a lazy refinement contract from the settings schema:
composed.port
→ contract.lazy { name = "nginx.port"; check = isInt; } 8080
→ 8080 # validates on access, zero cost until forced- Type checking: from the settings schema
typefield - Cross-field validation: optional
validatorscollection on the settings schema, run eagerly on the full composed result only when the settings attrset itself is forced - Blame: contract violations identify the aspect (schema source) and scope level (value source) via Palmer identity on contributions
Each settings contribution carries identity: { aspect = "nginx"; scope = "host:web1"; }. When the same contribution arrives via two graph paths (diamond), Palmer identity deduplicates before composition. Same pattern as gen-aspects' diamond dedup in fold-based collect.
Aspects use positional identity — key = pathKey(aspectPath(a)). This is the aspect's position in the tree, NOT a hash of its content.
- Static aspects:
key="networking"or"networking/firewall"(path chain) - Guard functions:
key=pathKey(meta.loc)(merge location from module system — Palmer program-point identity) - Parametric aspects: identity from
mkIntensionalname ormeta.locfallback - Anonymous aspects (inline in includes): synthetic names from position, never appear as top-level entries
- Namespaced aspects:
providerPrefixscopes identity —"nginx-lib/nginx"vs"aspects/nginx"
gen-schema's id_hash (content-based SHA-256) is NOT used for aspects. Parametric aspects resolve to different content per scope, making content hashing meaningless. id_hash remains for flat entity registries (hosts, users) where content identity is correct.
Aspects map to gen-scope graph nodes:
# Each top-level aspect → root node
{ id = "networking"; type = "aspect"; parent = null;
decls = { settings = { ... }; __edges.I = [ "firewall" ]; }; }
# Nested aspect → child node
{ id = "networking/firewall"; type = "aspect"; parent = "networking";
decls = { ... }; }
# includes → I-edges (decls.__edges.I)
# neededBy → reverse I-edges (computed attribute)
# Nesting → P-edges (parent field)
# Entity scoping → separate subtree with P-edges to entity nodes| Library | Addition | Size estimate |
|---|---|---|
| gen-algebra | Per-field-strategy fold over record layers (uses existing record.extend/record.select) |
~40 lines |
| gen-schema | Settings-aware collection type (schema declarations with type/default/merge) | ~60 lines |
gen-scope's collectionAttr supports traverse modes: "imports", "children", "siblings", "ancestors", "label:<name>", and custom functions. None do full D > I > P ordered collection. query does D > I > P but returns a single shadowed result, not the full ordered list needed for fold-based composition.
collectionAttr {
traverse = "neron";
extract = self: id: (self.node id).decls.settings or null;
combine = layers: foldWithStrategies strategies layers;
}Returns all contributions in specificity order (most-specific first): D (self), then I-edges, then recursively up the P-chain. Each P-node contributes its own D + I before yielding to its parent.
Plugs into the existing traverse dispatch in collectionAttr:
else if traverse == "neron" then
let
neronCollect = seen: nid:
let
node = self.node nid;
direct = [ nid ];
importIds = self.get nid "imports";
unseenImports = builtins.filter (iid: !(seen ? ${iid})) importIds;
importContribs = unseenImports;
newSeen = seen
// { ${nid} = true; }
// builtins.listToAttrs
(map (iid: { name = iid; value = true; }) importIds);
parentContribs =
if node.parent != null && !(newSeen ? ${node.parent})
then neronCollect newSeen node.parent
else [];
in
direct ++ importContribs ++ parentContribs;
in
neronCollect { ${id} = true; } idquery |
collectionAttr { traverse = "neron" } |
|
|---|---|---|
| Returns | Single shadowed result | Ordered list of all contributions |
| Use case | Name resolution (one winner) | Value aggregation (fold all layers) |
| Shadowing | D shadows I shadows P | Caller's combine function controls |
- D > I > P ordering: direct first, then imports, then parent chain (Neron 2015)
- Recursive: parent's contribution includes its own D + I before its parent; memoized via
_eval - Cycle-safe:
seenset prevents re-visiting nodes (same pattern asquery's_seen) - Returns node IDs:
collectionAttr's existingextractandcombinehandle value extraction and folding - Not settings-specific: any attribute needing specificity-ordered aggregation
~30-40 lines — new branch in traverse dispatch + recursive collect function.
Aspects have fixed string identity via pathKey(aspectPath(a)) — this is how bracket notation (<den/networking/firewall>) works. The recursive tree is a declaration convenience; a flat view by path identity is the queryable surface for gen-graph/gen-select.
The recursive aspect tree flattens to a registry keyed by path identity:
# Recursive tree (declaration surface)
config.aspects.networking = {
nixos = { ... };
tags = [ "infra" ];
firewall = {
nixos = { ... };
tags = [ "security" "infra" ];
includes = [ config.aspects.logging ];
};
};
# Flat view (query surface)
{
"networking" = {
tags = [ "infra" ];
classKeys = [ "nixos" ];
includes = [ ... ];
};
"networking/firewall" = {
tags = [ "security" "infra" ];
classKeys = [ "nixos" ];
includes = [ "logging" ];
};
}Parent relationships are implicit in the path key: "networking/firewall" → parent is "networking". Flatten does not inject any fields — entries are the aspect values unchanged.
Namespaced aspects include their prefix: "nginx-lib/nginx", "nginx-lib/certbot".
A computed field on the aspect kind materializes the flat view from the recursive tree. Available before the scope engine swap — works with den's current fx pipeline.
schema = aspects.mkAspectSchema {
# ...
computed = collections: defs: {
flat = flattenAspectTree defs;
};
};
# Query via gen-graph
g = {
nodes = builtins.attrNames config.schema.aspect.flat;
edges = id: config.schema.aspect.flat.${id}.includes or [];
parent = id: let parts = lib.splitString "/" id; in
if builtins.length parts <= 1 then null
else lib.concatStringsSep "/" (lib.init parts);
nodeData = id: config.schema.aspect.flat.${id};
};
graph.select g (d: lib.elem "security" (d.tags or []))
# → [ "networking/firewall" ]
graph.reachableFrom g "networking"
# → [ "networking/firewall" ]
# Query via gen-select
ctx = sel.adapters.graph.mkContext g;
sel.matches
(sel.when (id: ctx: lib.elem "infra" ((ctx.data id).tags or [])))
"networking" ctx
# → trueProperties:
- Lazy — the flat view only materializes when accessed
- Static — reflects the declaration-time aspect tree, no scope resolution
- Useful for pre-pipeline discovery: "what aspects exist?", "which aspects have tag X?"
- Does not reflect parametric aspects (unresolved until scope context available)
With gen-scope, aspects ARE scope graph nodes. result.allNodes is the flat view. gen-graph and gen-select wire directly to gen-scope's memoized accessors — no separate flattening needed.
result = engine.eval {
roots = aspectRoots;
attributes = {
children = self: id: /* nested aspects */;
imports = self: id: /* includes as I-edges */;
tags = self: id: (self.node id).decls.tags or [];
classKeys = self: id: /* registered class keys on this aspect */;
settings-schema = self: id: (self.node id).decls.settings or {};
};
};
# gen-graph wires to gen-scope accessors — O(1) amortized via _eval
g = {
nodes = builtins.attrNames result.allNodes; # Tier 2: forces full tree
edges = id: result.get id "imports"; # Tier 1: O(1) cached
parent = id: (result.node id).parent;
nodeData = id: result.node id;
};
# gen-select wires via adapter
ctx = sel.adapters.scope.mkContext {
node = result.node;
get = result.get;
};
# Queries hit memoized attributes — zero redundant computation
sel.matches
(sel.when (id: ctx: lib.elem "security" (result.get id "tags")))
"networking/firewall" ctx
# → true
# Selective materialization — avoid Tier 2 when possible
result.allNodesWhere (n: lib.elem "security" (n.decls.tags or []))
# → only materializes nodes matching predicateProperties:
- Dynamic — reflects resolved aspects including parametric (post-scope-context)
- Memoized — every attribute evaluates once per node via
_eval - No separate flattening — the scope graph IS the flat registry
- Full gen-graph/gen-select/gen-derive integration via accessor pattern
- Selective materialization avoids forcing the full tree for targeted queries
| Path A (computed field) | Path B (gen-scope) | |
|---|---|---|
| When available | Now (pre-scope-engine) | After scope engine swap |
| Data source | Declaration-time recursive tree | Resolved scope graph |
| Parametric aspects | Not visible (unresolved) | Fully resolved per scope |
| Settings values | Schema only (no composition) | Composed via neron traverse |
| Performance | Lazy but materializes full tree | Tier 1 O(1), Tier 2 O(n) with memoization |
| gen-graph/gen-select | Via flat attrset accessors | Via gen-scope memoized accessors |
Path A is a stepping stone. Path B is the target architecture. Both expose the same query surface (gen-graph/gen-select), just with different backing stores. Consumer code that uses gen-graph/gen-select accessors works unchanged across both paths.
| Library | Change | Size | Risk |
|---|---|---|---|
| gen-schema | mkType parameter on mkSchemaEntryType |
~20 lines | None — additive, default preserves current behavior |
| gen-schema | Settings collection type (field declarations with type/default/merge) | ~60 lines | Low — new collection variant |
| gen-aspects | Port onto gen-schema, mkAspectSchema + mkAspectOption + mkNamespaceType |
~250 lines delta | Medium — clean break, new dependency |
| gen-aspects | Flat registry computed field (path A) | ~40 lines | Low — recursive tree walk, lazy |
| gen-algebra | Per-field-strategy fold over record layers | ~40 lines | Low — uses existing record primitives |
| gen-scope | "neron" traverse mode for collectionAttr |
~40 lines | Medium — new traverse mode, needs topology testing |
| Total | ~450 lines |
- Default
mkType(null): existing behavior unchanged, all current tests pass - Custom
mkType: receiveskindModuleandcollections, returns a working type - Kind-level infrastructure (introspection, topology, collections) works with custom entry type
- Custom entry type with freeform: unregistered keys handled by the custom type, not gen-schema
mkAspectSchemaproduces working aspect type with class content, includes, neededBy- Class content is clean deferredModule (no structural keys injected)
- Nested aspects via recursive freeform still work
- Guard functions detected and wrapped (Palmer/Reynolds)
- Module functions evaluated by submodule
- Primitive passthrough unchanged
- Identity computation (key, aspectPath) unchanged
- User-defined metadata via schema extension:
config.schema.aspect.options.myField - Collections (includes, neededBy) extracted and accessible
- gen-schema introspection works on aspect kind
- Aspect declares settings schema, accessible via collection
- Entity assigns settings values at scope level
- Composed result respects per-field merge strategies (replace, append, recursive)
- Schema defaults used when no scope provides a value
- Explicit values override schema defaults without priority annotations
- Lazy: unaccessed fields never validated (verify with
builtins.trace) - Type checking via contract on access
- Cross-field validators run when settings attrset forced
- Diamond dedup: same contribution via two paths contributes once
- Blame identifies aspect (schema) and scope level (value)
- Recursive tree flattens to
{ "a" = ...; "a/b" = ...; "ns/c" = ...; }by path identity - Parent derived from key:
"a/b"→ parent is"a"(no injected field) - Namespaced aspects include
providerPrefixin path:"nginx-lib/nginx" - Lazy: only materializes when accessed
- gen-graph accessor record built from flat view works with
reachableFrom,select,cycles - gen-select queries match against
nodeData(tags, classKeys, settings schema) - Parametric aspects (guard functions) appear as entries but with unresolved content
- P-only graph: contributions from self → parent → grandparent in order
- I-edge graph: contributions from self → imports → parent chain
- D > I > P ordering: direct appears before import appears before parent
- Recursive parent: parent's I-edge contributions appear before grandparent's D
- Diamond dedup: node reachable via two paths contributes once (via
seen) - Empty nodes: nodes with null extraction skipped
- Cycle safety: circular P/I edges don't infinite-loop
| Mechanism | Paper | Library |
|---|---|---|
| Scoped labels / shadow stacks | Leijen 2005 (Extensible Records with Scoped Labels) | gen-algebra record |
| Mixin composition direction | Bracha & Cook 1990 (Mixin-Based Inheritance) | gen-algebra record |
| Collection attributes with fold | Van Wyk et al. 2010 (Silver); Sloane et al. 2010 (Kiama) | gen-scope collectionAttr |
| Intensional identity / dedup | Palmer et al. 2024 (Intensional Functions) | gen-algebra, gen-aspects |
| Lazy contracts | Chitil 2012 (Practical Typed Lazy Contracts) | gen-schema refinements |
| D < I < P resolution | Neron et al. 2015 (A Theory of Name Resolution) | gen-scope query, neron traverse |
| Palmer flat typing | Palmer et al. 2024 §2 (one type, dispatch in merge) | gen-aspects aspectType |
| Guard defunctionalization | Reynolds 1972 (Definitional Interpreters) | gen-aspects canTake/wrap |
Den does not use gen-aspects today. Den has its own 685-line type system in nix/lib/aspects/types.nix with providerType (6+ case merge), aspectContentType (content wrapping with __contentValues/__provider), and aspectSubmodule (structural options). gen-aspects is the clean-room replacement library (40 tests, shipped separately) that den will adopt when the scope-engine swap lands.
Den already uses gen-schema for entities. den.schema is a mkSchemaOption with collections for includes, excludes, and isEntity on entity kinds (host, user, home). den.schema.aspect currently exists only for strict mode injection — the actual aspect type system is separate.
den.reservedKeys is a string exclusion list (["settings", "tags"]) preventing pipeline dispatch on those keys. With gen-aspects on gen-schema, this becomes unnecessary — reserved keys are declared options on the aspect kind, and gen-schema's option/freeform separation handles classification naturally.
aspect-schema.nix manually walks the aspect tree to collect .classes declarations and merge them into den.classes. This is exactly what a gen-schema collection with attrset merge would handle automatically.
-
I-edge ordering among imports. When a node has multiple I-edges, what determines relative priority? Declaration order? A priority attribute on the edge? gen-scope's existing
queryuses_seentracking — the neron traverse mode should follow the same ordering. Initial implementation: declaration order (list order ofdecls.__edges.I). -
Interaction with gen-derive. Policies can inject settings via rules. Policy-produced settings enter the scope graph as node declarations before
collectionAttrtraverses. The timing is: gen-derive dispatch → enrich node decls → gen-scope attribute evaluation. This is existing pipeline order; settings does not change it. -
Nested settings namespaces. Aspects at
den.aspects.services.nginx.settings— nested aspect paths create nested settings namespaces. Theextractfunction incollectionAttrhandles nested path lookup. This is den wiring, not a gen concern.