feat: move to gen-schema HEAD via the gen hub, and resolve host-qualified aspects - #680
Merged
Merged
Conversation
Moves den from gen-schema 4bd0f6e to HEAD 8cf90f5, which is 112 commits and a
module-system swap away (gen-merge replaces lib.evalModules + lib.types).
Two things had to change, and neither is a gen defect.
IDENTITY KEYS ARE DECLARED AT THE KIND. gen-schema closes the identity-key
set the moment the kind is a value, so an option contributed through
`mkInstanceType`'s `extraModules` cannot be one, and the reflected set drops
`internal` options outright. den named `__scopeName` and `system` in
`_identity.keys` for the home kind and declared both on the instance, which
used to succeed silently and is now a loud, named refusal. Both are declared
on the kind instead; the per-system value stays where it is computed, since
identity reflects declarations rather than config.
RESOLUTION GOES THROUGH THE HUB. gen-schema's root declares
`{ prelude, merge, algebra, identity }` with no `...`, so den's old fallback
— `import gen-schema { inherit lib; }` — is a hard error at every consumer
that does not declare the input, which is eleven of den's twelve templates.
The hub's root takes the vestigial `{ }` and hands back a resolved roster, so
the fallback has something total to call. It also fixes coherence: pinning
gen-schema directly left its transitive closure to den and produced three
revisions of gen-prelude in one lock, which reads as one value while being
three builds. Through the hub it is one.
Validation: `just ci` 1201/1201, exit 0, zero failures and zero errors, with
the summary read off stderr and the collected count agreeing with the
numerator. `nix flake check` on `minimal` and `default` — both declare no gen
input, so both exercise the fetched fallback rather than the flake input.
The kind-level identity declarations were left on the documented option surface on the belief that gen-schema forced it. It does not: `isPrimitiveOption` reads `internal` and `identity`, and NOT `visible`, so `visible = false` keeps both keys out of rendered option docs while leaving them identity-eligible. `internal = true` is still unavailable, and that part is a real constraint rather than a preference — an internal option is dropped from the reflected identity set outright, so marking these internal would un-declare the very keys `_identity.keys` names. Recorded as resting on `visible` not being read, because that is an implementation fact and not a documented contract: if gen-schema ever folds `visible` into the same presentation exclusion, these keys silently leave the identity set.
Picks up denful/den-diagram#2, which reads the provenance chain under both `provider` and `aspect-chain`. This branch's rename left den-diagram reading a field den no longer emits, so `diagram-demo` aborted with `attribute 'provider' missing` and the fleet and namespace views silently read an empty chain through their `or [ ]` fallbacks. Neither template passes `nix flake check` yet, and neither failure is this: `diagram-demo` hits a `files` input API drift on `perSystem.<system>.files.files.*.path` and `fleet-demo` hits incomplete `fileSystems`/`boot.loader.grub` config. Both reproduce unchanged on origin/main, and both are the errors these templates showed BEFORE the rename — so the bump restores them to their prior state rather than fixing them. `attribute 'provider' missing` is gone from both.
`den.aspects."<user>@<host>"` was accepted by the aspect option and consulted by nothing — inert config with no diagnostic, the same silent-drop shape as #663. A user-scoped entity now resolves it alongside the bare `den.aspects.<user>`, and the same two names serve a user declared under a host and a standalone home keyed `user@host`, so a user's aspects resolve identically either way. COMPOSITION, NOT PRECEDENCE, and it is forced rather than preferred. `modules/aspects/definition.nix` registers a stub aspect per entity so an entity's class keys always exist, which makes `den.aspects ? <name>` true for every declared entity whether or not anyone wrote that aspect — measured: a host declared with no aspect at all still answers true. A stub is also structurally indistinguishable from a written aspect (same keys, `name` defaulted from the attr path, no content), so no predicate over `den.aspects` can separate the two. A first-match lookup would therefore match the stub and shadow the bare aspect for every entity. Composing sidesteps the question: a stub contributes empty class keys, so including one is a no-op and nothing has to know which is which. It is also the better semantic — shared and host-specific aspects both apply. A single present candidate is returned as itself rather than wrapped, so the common case keeps its own identity and provenance exactly as before. A home's `name` is now its registry key rather than the bare user name, which is what identifies it and what aspect content reading `home.name` sees; the user it configures is `userName`. `_identity.keys` follows to `name`+`system`, so the `__scopeName` override and its kind-level declaration are gone — identity is native where gen-schema already puts it. BEHAVIOUR CHANGES - `home.name` read from a policy or class module is the registry key (`tux@igloo`), not the bare user name. Read `home.userName` for the user. - `den.batteries.import-tree` home variant therefore imports `<root>/<user>@<host>` rather than `<root>/<user>`. Left reading `home.name` deliberately: the battery's host and user variants both key on the entity's own `.name`, and breaking that symmetry for homes alone would be the odder rule. No test covers this path. - An entity with both a qualified and a bare aspect now applies both, where the qualified one was previously ignored entirely. `fx-coverage`'s self-provide cell reads the declaring aspect rather than `home.aspect`, since a composed home's `aspect` is the wrapper and the provides live on the aspect that declared them; the invariant it tests is unchanged. Docs: host-qualified aspects in explanation/entities.mdx with the applied-name table, the guides/home-manager.mdx standalone section contrasting it with `provides.<host>` routing, and the home/user schema reference tables, whose `name` row stated the superseded "part before `@`" rule.
Two changes that belong together: the hub bump is what makes den's documented
schema shape fail, and shipping it alone would close one issue while leaving
every documented example broken.
LIB AS A MODULE ARGUMENT. gen's module system deliberately ships no nixpkgs
`lib` — it has its own types — so a schema module written the way den
documents it, `den.schema.host = { host, lib, ... }: …`, fails with
`gen-merge: module argument `lib' is not defined`. That shape predates
gen-schema and is what five doc sites and real user configs use, so den
injects `lib` rather than asking gen to default to it or rewriting the docs.
Injected on `den.schema.conf`, which every kind imports, so it is one site
rather than one per kind.
This was introduced by the gen-schema port, not by the bump: measured across
three pins — passes on the pre-port tree, fails on the port, fails here. The
suite stayed green throughout because den's own schema tests take `lib` from
OUTER scope while the docs tell users to take it as a module argument. A
contract nothing executed, so nothing caught it. The two cells added here are
the execution, and they are red without the injection.
HUB BUMP to gen 0b6fbd8, which carries gen-merge 08fcdd1 — the isOptLeaf
door. A `mk*` wrapper in a schema module's `options` position now refuses by
name and the message reaches den's own frame:
gen-merge: option `den.hosts.<sys>.<host>.users.<user>' is declared as the
`merge' combinator … move it under `config'/`imports', or write one plain
attrset here
That is discussion #672, which previously surfaced frames away as
`An option declaration for ….contents has type `list'`, naming neither the
schema nor the wrapper. Verified through den rather than relayed. The
refusal is catchable, where the predecessor was an uncatchable abort.
The hub sha was re-derived rather than taken from a report: an earlier
reading of f6241d6 was already stale. Closure stays coherent — gen-prelude
resolves to one revision, which is the property the hub routing exists for.
Validation: `just ci` 1210/1210, exit 0, zero failures and zero errors, read
off stderr with the collected count agreeing with the numerator. Both new
cells falsified against the unfixed tree with the tests held in place, so the
red arm measures the fix rather than the cells' absence.
…ng attr Closing the structural-key registry by rule dropped the `structuralKeysSet` export, and a consumer reading it got a bare `attribute 'structuralKeysSet' missing` naming neither den nor the replacement. Found by evaluating a real configuration against this branch: it reads the export to skip structural keys while building a settings type. It cannot be aliased faithfully. `isStructuralKey` derives the `__`-prefixed half BY RULE over an infinite domain, so no attrset can answer `?` for it and Nix offers no way to intercept `?`. Re-exporting the listed half alone would answer false for every `__` key, which is the silent-drop hazard closing the registry by rule removed, reintroduced under a compatibility name. So the name throws by itself instead, stating the rule and the migration. A thunk, so `inherit`ing it stays quiet and the message lands at first use, which is where the caller is rather than where the import is. Validation: `just ci` 1212/1212, exit 0, zero failures and zero errors. The message verified by evaluating the unmigrated consumer against this tree and reading the thrown text, rather than by inspecting the string.
sini
force-pushed
the
feat/gen-schema-head
branch
from
September 15, 2026 23:29
7dc89f7 to
8253c01
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#672
Summary
Resolution:
{ prelude, merge, algebra, identity }with no...while den's fallback passed{ lib }, which is a hard error at the eleven templates that do not declare the input.Identity:
mkInstanceType'sextraModulesarrives after that.visible = falserather thaninternal = true, asisPrimitiveOptionreadsinternalandidentityand notvisible, so marking them internal would un-declare the very keys_identity.keysnames.visibleroute rests on an implementation fact rather than a stated contract, so a later folding ofvisibleinto the same presentation exclusion would drop these keys from identity in silence.Module arguments:
libto schema modules onden.schema.conf, which every kind imports, because gen's module system ships no nixpkgs lib and den documentsden.schema.host = { host, lib, ... }:at five sites.Aspects:
den.aspects."<user>@<host>"for a user declared under a host and for a standalone home keyeduser@host, as the qualified spelling was accepted by the aspect option and consulted by nothing.modules/aspects/definition.nixregisters a stub aspect per entity soden.aspects ? <name>is true for every declared entity, and a stub is structurally indistinguishable from a written aspect.nameto its registry key rather than the bare user name, so the field that identifies a home is the one that tells two homes for one user apart, anduserNamecarries the user it configures.Diagnostics:
08fcdd1through gen0b6fbd8, so amk*wrapper in a schema module'soptionsposition refuses by name rather than failing frames away as acontentsoption of the wrong type.structuralKeysSetremoval at the point of use, as closing the structural-key registry by rule dropped that export and a consumer reading it got a bareattribute 'structuralKeysSet' missing. It cannot be aliased, becauseisStructuralKeyderives the__-prefixed half by rule over an infinite domain and no attrset can answer?for that.Behaviour changes
Content that was previously inert becomes live, so a rebuild can emit modules it did not emit before:
den.aspects."<user>@<host>"is resolved where it was accepted and ignored.A field changes meaning, which can change an existing configuration:
home.nameread from a policy or class module is the registry key such astux@igloo, not the bare user name. Readhome.userNamefor the user.den.batteries.import-tree's home variant therefore imports<root>/<user>@<host>rather than<root>/<user>. It keeps readinghome.namedeliberately, as its host and user variants both key on the entity's own.name. No test covers this path.Input that was previously accepted is now refused:
optionsthroughmkMergeor anothermk*wrapper refuses by name, where it previously declared options called_typeandcontentsand failed on a later read._identity.keysnaming a key the kind does not declare refuses, where it previously succeeded and minted over whatever the reflection reached.keyClassification.structuralKeysSetthrows by name with its migration, where it previously read as a missing attribute. ReplacestructuralKeysSet ? kwithisStructuralKey k.Validation
nix develop -c just ci: 1212/1212, exit 0, zero failures and zero errors, read off stderr with the collected count agreeing with the numerator.nix flake checkper template rather than readingjust all check's exit code, which aborts atcion aflake.packagesdouble declaration that reproduces onmain. Nine templates pass, anddiagram-demo,fleet-demoandterranix-demofail for reasons that also reproduce onmain.id-hash.nixrather than only the suite, as explicit_identity.keysstill drive the hash and membership is validated rather than substituted, so the same-name and cross-system distinctness cells are not passing for the wrong reason.libregression across three pins, since it passes before the port, fails on the port and fails after the bump, which places it in the port rather than the bump.tryEval.main, and all seven NixOS machines produce byte-identical derivations while the darwin machine evaluates identically. That sweep is what surfaced thestructuralKeysSetremoval, as the configuration consumes it.mainafter fix: resolve identity by definition value and split the provenance field #678 merged, with the rebased tree diffing empty against the pre-rebase commit, so the replay changed no content.Known limitation, not addressed here
lookupAspect's missing-aspect warning is dead code in the normal path, because the per-entity stub above makesden.aspects ? <entity>always true. Issue #663 cites that warning as the only signal a user gets. Making it fire needs a design decision rather than a predicate change, as an entity legitimately may have no own aspect when it is configured throughden.defaultor schema includes.