From 25b0b8d91881e3c590dc2cbb7cecc4d75d62660e Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 16 Sep 2026 05:49:32 -0700 Subject: [PATCH 1/5] fix: merge an entity's list-valued keys instead of overwriting them Two files each writing `den.hosts...includes` kept one list and dropped the other, with no conflict warning. Reported by theutz on #678. `den.hosts`' freeform type merged with `lib.recursiveUpdate`, which treats a list as an opaque leaf, so one definition overwrote the other. An attrset key under the same two definitions merged normally, which is what made the collection look like it had no effect rather than like a merge failure. Concatenation is the module system's own rule for a list-valued option (`listOf` merges by `concatLists`) and den's rule at the aspect tier (`aspectContentType`'s `deepMerge`), so this makes the entity registry agree with both rather than add a third behaviour. `includes`, `excludes` and `classes` are the list-valued keys an entity carries, and all three accumulate everywhere else they appear. Latent until instance-level collections started being read, since before that nothing consumed them and there was no way to observe which list survived. Concatenates `bv ++ a.` rather than the other way round because defs reach this merge in reverse declaration order, measured rather than assumed. It is the same ordering that makes the scalar arm read as first-declaration-wins, and include order decides which aspect's content wins a conflict, so the direction is pinned by its own cell. Validation: `just ci` 1217/1217, exit 0, zero failures and zero errors. The three defect cells falsified against the unfixed merge with the tests held in place, where both control cells stay green in that arm, so they are controls rather than padding. --- nix/lib/entities/_types.nix | 37 ++++- .../deadbugs/instance-collection-merge.nix | 142 ++++++++++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 templates/ci/modules/features/deadbugs/instance-collection-merge.nix diff --git a/nix/lib/entities/_types.nix b/nix/lib/entities/_types.nix index 5bf9277a..7b85f42e 100644 --- a/nix/lib/entities/_types.nix +++ b/nix/lib/entities/_types.nix @@ -64,11 +64,46 @@ let # Recursive merge without forcing leaf values. Unlike lib.types.anything this # does not inspect values deeply (no mapAttrsRecursiveCond), avoiding infinite # recursion when values reference other options (e.g. den.aspects). + # Concatenates lists rather than overwriting them, which `lib.recursiveUpdate` + # does because it treats a list as an opaque leaf. Two files each writing + # `den.hosts..includes` therefore kept one list and dropped the other in + # silence, while an attrset key under the same two definitions merged + # normally (measured: `users.alice` and `users.bob` both survive). + # + # Concatenation is the module system's own rule for a list-valued option + # (`listOf` merges by `concatLists`) and den's rule at the aspect tier + # (`aspectContentType`'s `deepMerge`), so this makes the entity registry + # agree with both rather than introduce a third behaviour. It matters most + # for the collection keys: `includes`, `excludes` and `classes` are the + # list-valued keys an entity carries, and all three accumulate everywhere + # else they appear. deepMergeAttrs = lib.mkOptionType { name = "deepMergeAttrs"; description = "recursively merged attribute set"; check = builtins.isAttrs; - merge = _loc: defs: builtins.foldl' (acc: def: lib.recursiveUpdate acc def.value) { } defs; + merge = + _loc: defs: + let + merge2 = + a: b: + a + // builtins.mapAttrs ( + bk: bv: + if !(a ? ${bk}) then + bv + else if builtins.isAttrs a.${bk} && builtins.isAttrs bv then + merge2 a.${bk} bv + else if builtins.isList a.${bk} && builtins.isList bv then + # `bv` first: defs reach this merge in reverse declaration order, + # so `a` holds the LATER definition. Measured, not assumed, and + # it is the same ordering that makes the scalar arm below read as + # first-declaration-wins. + bv ++ a.${bk} + else + bv + ) b; + in + builtins.foldl' (acc: def: merge2 acc def.value) { } defs; }; # Single shared production run: imports + per-scope path set from ONE fx.handle. diff --git a/templates/ci/modules/features/deadbugs/instance-collection-merge.nix b/templates/ci/modules/features/deadbugs/instance-collection-merge.nix new file mode 100644 index 00000000..5e340225 --- /dev/null +++ b/templates/ci/modules/features/deadbugs/instance-collection-merge.nix @@ -0,0 +1,142 @@ +# Reported on #678 by theutz: two files each writing +# `den.hosts...includes` kept one list and dropped the other, +# with no conflict warning. +# +# `den.hosts`' freeform type merged with `lib.recursiveUpdate`, which treats a +# list as an opaque leaf, so one definition overwrote the other. An attrset key +# under the same two definitions merged normally, which is what made it look +# like the collection simply had no effect. +# +# Latent until #663 made instance-level collections mean something. Before +# that they were read by nothing, so there was no way to notice which list +# survived. +{ denTest, ... }: +{ + flake.tests.deadbugs.instance-collection-merge = { + + test-includes-merge-across-definitions = denTest ( + { den, igloo, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.includes = [ den.aspects.one ]; } + { den.hosts.x86_64-linux.igloo.includes = [ den.aspects.two ]; } + ]; + den.hosts.x86_64-linux.igloo.users.tux = { }; + den.aspects.one.nixos.environment.etc."one".text = "y"; + den.aspects.two.nixos.environment.etc."two".text = "y"; + + expr = { + one = igloo.environment.etc ? "one"; + two = igloo.environment.etc ? "two"; + }; + expected = { + one = true; + two = true; + }; + } + ); + + # `excludes` shares the freeform type, so it shared the defect. Two files + # each excluding one policy must suppress both, not whichever list won. + test-excludes-merge-across-definitions = denTest ( + { den, igloo, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.excludes = [ den.policies.alpha ]; } + { den.hosts.x86_64-linux.igloo.excludes = [ den.policies.beta ]; } + ]; + den.hosts.x86_64-linux.igloo.users.tux = { }; + + den.policies.alpha = _: [ + (den.lib.policy.include { nixos.environment.etc."alpha".text = "y"; }) + ]; + den.policies.beta = _: [ + (den.lib.policy.include { nixos.environment.etc."beta".text = "y"; }) + ]; + den.schema.host.includes = [ + den.policies.alpha + den.policies.beta + ]; + + expr = { + alpha = igloo.environment.etc ? "alpha"; + beta = igloo.environment.etc ? "beta"; + }; + expected = { + alpha = false; + beta = false; + }; + } + ); + + # CONTROL for the excludes cell: the two policies do fire when nothing + # excludes them, so the absences above read as suppression rather than as + # policies that never delivered. + test-control-both-policies-fire = denTest ( + { den, igloo, ... }: + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + + den.policies.alpha = _: [ + (den.lib.policy.include { nixos.environment.etc."alpha".text = "y"; }) + ]; + den.policies.beta = _: [ + (den.lib.policy.include { nixos.environment.etc."beta".text = "y"; }) + ]; + den.schema.host.includes = [ + den.policies.alpha + den.policies.beta + ]; + + expr = { + alpha = igloo.environment.etc ? "alpha"; + beta = igloo.environment.etc ? "beta"; + }; + expected = { + alpha = true; + beta = true; + }; + } + ); + + # Declaration order is preserved. Concatenating in either order makes both + # entries present, so the cells above pass under a reversed merge too; + # this is what pins the direction, since include order decides which + # aspect's content wins a conflict. + test-collection-merge-keeps-declaration-order = denTest ( + { den, config, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.includes = [ "A" ]; } + { den.hosts.x86_64-linux.igloo.includes = [ "B" ]; } + ]; + + expr = config.den.hosts.x86_64-linux.igloo.includes; + expected = [ + "A" + "B" + ]; + } + ); + + # An attrset key merged correctly throughout, and still must. This is the + # arm that localised the defect to list leaves rather than to the + # collection keys or to the registry's recursion. + test-attrset-keys-still-merge = denTest ( + { den, config, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.users.alice = { }; } + { den.hosts.x86_64-linux.igloo.users.bob = { }; } + ]; + + expr = builtins.attrNames config.den.hosts.x86_64-linux.igloo.users; + expected = [ + "alice" + "bob" + ]; + } + ); + + }; +} From 0020a1857de4f5cc1a3b24077927db5840418a19 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 16 Sep 2026 06:04:23 -0700 Subject: [PATCH 2/5] fix: refuse conflicting definitions of an entity's non-mergeable keys The same silent resolution as the list overwrite, one arm over. Two definitions of a key that is neither a mergeable attribute set nor a list took the first and dropped the other, and a TYPE MISMATCH resolved the same way: a list in one file against a string in another silently kept the list. Pre-existing, and measured as such against a tree predating the instance collections being read at all, where both arms resolve identically. It is also weaker than the module system, which errors on a DECLARED option with conflicting definitions. An entity's freeform keys bypassed that. Refuses when either side is a plain scalar and the values differ, which covers two scalars that disagree and a mismatch such as a list against a string. An earlier form required BOTH sides to be scalars and left the mismatch silent, which its own cell caught. Functions are excluded deliberately: `==` on two functions is always false, so comparing them would reject two identical definitions of a class module or of `instantiate`. A control cell pins that. The message names a value only where rendering it is safe. `builtins.toJSON` throws on a derivation or a function, and either can be one side of a conflict, so a message that always rendered both would fail while reporting a failure. Validation: `just ci` 1221/1221, exit 0, zero failures and zero errors. Both refusal cells falsified against the unfixed predicate with the tests held in place, where the two controls stay green, so they are controls rather than padding. --- nix/lib/entities/_types.nix | 35 +++++++++ .../deadbugs/instance-collection-merge.nix | 74 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/nix/lib/entities/_types.nix b/nix/lib/entities/_types.nix index 7b85f42e..3e8048ff 100644 --- a/nix/lib/entities/_types.nix +++ b/nix/lib/entities/_types.nix @@ -77,6 +77,26 @@ let # for the collection keys: `includes`, `excludes` and `classes` are the # list-valued keys an entity carries, and all three accumulate everywhere # else they appear. + # A value the module system would have merged by equality had the key been + # declared. Functions and derivations are excluded: `==` on two functions is + # always false, so comparing them would refuse two identical definitions. + isPlainScalar = + v: + builtins.elem (builtins.typeOf v) [ + "string" + "int" + "bool" + "float" + "null" + ]; + + # Values named by type, with the value itself only where rendering it is + # safe. `builtins.toJSON` on a derivation or a function throws, and one side + # of a conflict can be either, so a message that always rendered both would + # fail while reporting a failure. + show = + v: if isPlainScalar v then "`${builtins.toJSON v}`" else "a value of type ${builtins.typeOf v}"; + deepMergeAttrs = lib.mkOptionType { name = "deepMergeAttrs"; description = "recursively merged attribute set"; @@ -99,6 +119,21 @@ let # it is the same ordering that makes the scalar arm below read as # first-declaration-wins. bv ++ a.${bk} + else if + # Either side a plain scalar means the two are not both mergeable + # shapes, so reaching here with different values is a genuine + # conflict: two scalars that differ, or a type mismatch such as a + # list against a string. Both silently resolved to one definition + # before this. + (isPlainScalar a.${bk} || isPlainScalar bv) && a.${bk} != bv + then + throw '' + den: conflicting definitions for `${bk}` on an entity. + + ${show bv} and ${show a.${bk}} were both defined, and neither is a shape the other merges with. Attribute sets merge and lists concatenate; everything else has to agree. + + Remove one definition, or give the two a key each. + '' else bv ) b; diff --git a/templates/ci/modules/features/deadbugs/instance-collection-merge.nix b/templates/ci/modules/features/deadbugs/instance-collection-merge.nix index 5e340225..b8d84f52 100644 --- a/templates/ci/modules/features/deadbugs/instance-collection-merge.nix +++ b/templates/ci/modules/features/deadbugs/instance-collection-merge.nix @@ -119,6 +119,80 @@ } ); + # The same silent resolution, one arm over: two definitions of a key that + # is neither a mergeable attrset nor a list took the first and dropped the + # other. Pre-existing too, and weaker than the module system, which errors + # on a DECLARED option with conflicting definitions. An entity's freeform + # keys bypassed that. + test-scalar-conflict-refuses = denTest ( + { den, config, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.description = "first"; } + { den.hosts.x86_64-linux.igloo.description = "second"; } + ]; + + expr = config.den.hosts.x86_64-linux.igloo.description; + expectedError = { + type = "ThrownError"; + msg = "den: conflicting definitions for `description`"; + }; + } + ); + + # A TYPE MISMATCH is the sharper case and the one a narrower predicate + # misses: requiring both sides to be scalars leaves a list against a + # string still resolving in silence. + test-type-mismatch-refuses = denTest ( + { den, config, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.tags = [ "a" ]; } + { den.hosts.x86_64-linux.igloo.tags = "scalar"; } + ]; + + expr = config.den.hosts.x86_64-linux.igloo.tags; + expectedError = { + type = "ThrownError"; + msg = "den: conflicting definitions for `tags`"; + }; + } + ); + + # CONTROL: two definitions AGREEING is not a conflict. Without this the + # cells above pass for a predicate that refuses every repeated key, + # which would break any configuration that sets one twice harmlessly. + test-agreeing-definitions-are-not-a-conflict = denTest ( + { den, config, ... }: + { + imports = [ + { den.hosts.x86_64-linux.igloo.description = "same"; } + { den.hosts.x86_64-linux.igloo.description = "same"; } + ]; + + expr = config.den.hosts.x86_64-linux.igloo.description; + expected = "same"; + } + ); + + # CONTROL: a function-valued key must not refuse. `==` on two functions is + # always false, so a predicate that compared them would reject two + # identical definitions of `instantiate` or a class module. + test-function-valued-keys-do-not-refuse = denTest ( + { den, igloo, ... }: + { + den.hosts.x86_64-linux.igloo.users.tux = { }; + den.aspects.igloo.nixos = + { ... }: + { + environment.etc."fn".text = "y"; + }; + + expr = igloo.environment.etc ? "fn"; + expected = true; + } + ); + # An attrset key merged correctly throughout, and still must. This is the # arm that localised the defect to list leaves rather than to the # collection keys or to the registry's recursion. From cfc15edbef7126ec9deeeb5e023fd6ed5c560073 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 16 Sep 2026 07:29:59 -0700 Subject: [PATCH 3/5] chore: bump the gen hub to f140782c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves gen-schema f8e0e171 to 056ee9b513. gen-merge stays held at 08fcdd1efb, so discussion #672's fix is unaffected. Does NOT fix the `option ... is used but not defined` regression a user hit on a defaultless schema submodule option, and that is measured rather than assumed: their real configuration still fails against this bump. The cause is entry-path dependent and this pin does not touch it. The hub's ci lock pins gen-merge 08fcdd1efb while gen-schema's OWN ci lock pins f7e3afb3, which predates the `hasEmptyValue` guard. On the flake path the hub's `follows` override that and 08fcdd1efb wins, which is why this suite is green. On the standalone path there are no follows, each member resolves its own deps from its own ci lock, and gen-schema's pin wins — so one hub rev yields two different closures depending on how it is entered. den's fallback takes the standalone path, so every consumer that does not declare `gen` gets the older gen-merge. Validation: `just ci` 1221/1221, exit 0, zero failures and zero errors. `nix flake check` on `minimal` and `default`, both of which declare no `gen` input and so exercise the fetched fallback rather than the flake input — the path a previous bump validated only by accident. --- templates/ci/flake.lock | 52 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/templates/ci/flake.lock b/templates/ci/flake.lock index a16ee16f..5023e983 100644 --- a/templates/ci/flake.lock +++ b/templates/ci/flake.lock @@ -64,11 +64,11 @@ ] }, "locked": { - "lastModified": 1789511422, - "narHash": "sha256-Bh8GzWUbWoYRX/m56ACocEZ3l7SKPLm2YNav7FRaKJY=", + "lastModified": 1789568031, + "narHash": "sha256-oMPgzwQHoJxP0IjKvKwXhWBdEgKD29RQTfuZvzRejfg=", "owner": "sini", "repo": "gen", - "rev": "0b6fbd8d5d3ec37d96b739b13d957373fb89178c", + "rev": "f140782c9033a4199e5a2178dd094697bcd0ef32", "type": "github" }, "original": { @@ -112,11 +112,11 @@ ] }, "locked": { - "lastModified": 1789489469, - "narHash": "sha256-vnmLKXRvRxuaXW6V//lJc6kRDZbt67JUPgPs9/quZVE=", + "lastModified": 1789532954, + "narHash": "sha256-HRHozzS7YaCtFK3ZQJEMSiQ265Z44YRhFYIhkM3Getg=", "owner": "sini", "repo": "gen-aspects", - "rev": "f0d9d14c356210dfe1d20f918785a317bb82a549", + "rev": "085579937ab35c8b7978e54badd67f2f26dce776", "type": "github" }, "original": { @@ -142,17 +142,21 @@ }, "gen-bind": { "inputs": { + "gen-graph": [ + "gen", + "gen-graph" + ], "gen-prelude": [ "gen", "gen-prelude" ] }, "locked": { - "lastModified": 1789489422, - "narHash": "sha256-7iFjxTPsCiNYZbyD/ZuIvNp0Kk9m1dKKrazSVaoepfY=", + "lastModified": 1789560874, + "narHash": "sha256-g7WXpedTei3uaTFXbnw4pAXyO8kJ13P29TV6U5pOSGU=", "owner": "sini", "repo": "gen-bind", - "rev": "15262e1eb3ee4cdd6b7ef075ef2ceedd0b74ca09", + "rev": "27860c870ccbe6a69a72b9cf3bc1f9ee9a4e6843", "type": "github" }, "original": { @@ -286,11 +290,11 @@ ] }, "locked": { - "lastModified": 1789489617, - "narHash": "sha256-WDzF6yJNKJQvY2OXCxN6WYpy6s64EqFQnioMAkKZsrQ=", + "lastModified": 1789521260, + "narHash": "sha256-WqcZRmzyu0F8BydkJnlFyDV5DwP7UolTYwRUbj/8u7I=", "owner": "sini", "repo": "gen-link", - "rev": "365937e4ba8d896980a0afa521f60746fedcb742", + "rev": "475f47f21db5e9e32e7a0ac42da9a961eba3f88e", "type": "github" }, "original": { @@ -424,11 +428,11 @@ ] }, "locked": { - "lastModified": 1789510821, - "narHash": "sha256-55SiolgfGyaGv2tkn6OH//K7heGiVhilF2gjXzUXIi8=", + "lastModified": 1789523614, + "narHash": "sha256-nKYsvuP7mjH51ILcFVskZDppe2RgFfGYd/xwoP3adsY=", "owner": "sini", "repo": "gen-schema", - "rev": "f8e0e171d45e0ba872afe4a78843ec59f76165e9", + "rev": "056ee9b51362abce57360455321002402facfd82", "type": "github" }, "original": { @@ -478,11 +482,11 @@ ] }, "locked": { - "lastModified": 1789491559, - "narHash": "sha256-7NyGoDre+w2A2k448WRkfsdnJUtExx0DzprMEkVUVzU=", + "lastModified": 1789558867, + "narHash": "sha256-uLFCUV8aQnujhR6qegxQ745LkvuOUyN33PRnTWHRDGA=", "owner": "sini", "repo": "gen-scope", - "rev": "d24e0d983f55312b1ddada68e5a4737ec91021bb", + "rev": "41c7d9f5ead24f273b9517ca2d4744710aaa8f7f", "type": "github" }, "original": { @@ -499,11 +503,11 @@ ] }, "locked": { - "lastModified": 1789490250, - "narHash": "sha256-G3OWSbw2rBcgQWG4h6HchZxtHcKvDY+NZk50bUEE75s=", + "lastModified": 1789560881, + "narHash": "sha256-PUfsaVeOS6ZCZ47vAvEWow8A8xWHzw04Mjikh8N7KMM=", "owner": "sini", "repo": "gen-select", - "rev": "093b3d1600717c43cc00ad9d3a933e0124035e19", + "rev": "fe75c443e8e6dd55f9d94fda86013249d507671b", "type": "github" }, "original": { @@ -594,11 +598,11 @@ ] }, "locked": { - "lastModified": 1789472671, - "narHash": "sha256-CpB55naQXApJ2hMdX2kGIfEI7AEFFmIQz3pztzfnzpc=", + "lastModified": 1789555847, + "narHash": "sha256-5vKM3gmsLur1DHgAIG2jCuCNMIMzMFSWg5a2OkWJa3Q=", "owner": "sini", "repo": "gen-view", - "rev": "2656d3cc383ceb00a812b579dfed37bbd7a04c33", + "rev": "eccb0d2a787f8601616d1debc2db86beab38b133", "type": "github" }, "original": { From 721258c35497ef222de87cb5323d4801e20ea027 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 16 Sep 2026 08:49:44 -0700 Subject: [PATCH 4/5] chore: bump the gen hub to 0004f3c9 Moves gen-schema 056ee9b513 to 48cfb99289. gen-merge stays held at 08fcdd1efb on the flake path, so discussion #672's fix is unaffected. This one DOES fix the `option ... is used but not defined` regression on a defaultless schema submodule option, which the previous bump did not. The cause was never the hub rev: gen-schema's own locks pinned gen-merge f7e3afb3, which predates the `hasEmptyValue` guard, and den's fallback enters the hub standalone where no `follows` exist and each member resolves from its own lock. gen-schema 48cfb99289 carries 2a33f81444 in BOTH of its locks, so the two entry paths now agree. Validation, with a live control because a green suite here measures only the flake path: - The reporting user's real configuration, evaluated against this checkout: `home-manager.users..programs.helix.ignores` yields its list, exit 0. Same attribute at the previous pin, same instrument: exit 1, `gen-merge: the option ... is used but not defined`. - `just ci` 1221/1221, exit 0, zero failures and zero errors. - `nix flake check` on `minimal` and `default`, neither of which declares a `gen` input, so both exercise the fetched fallback that consumers take. --- templates/ci/flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/ci/flake.lock b/templates/ci/flake.lock index 5023e983..6726ac24 100644 --- a/templates/ci/flake.lock +++ b/templates/ci/flake.lock @@ -64,11 +64,11 @@ ] }, "locked": { - "lastModified": 1789568031, - "narHash": "sha256-oMPgzwQHoJxP0IjKvKwXhWBdEgKD29RQTfuZvzRejfg=", + "lastModified": 1789572515, + "narHash": "sha256-Vy6fYBwOiDokeyDvVy7lSqpjILG2Y7GrzK/YPP3zJTI=", "owner": "sini", "repo": "gen", - "rev": "f140782c9033a4199e5a2178dd094697bcd0ef32", + "rev": "0004f3c9dfd5634f47ee72575e56c54883450bcf", "type": "github" }, "original": { @@ -428,11 +428,11 @@ ] }, "locked": { - "lastModified": 1789523614, - "narHash": "sha256-nKYsvuP7mjH51ILcFVskZDppe2RgFfGYd/xwoP3adsY=", + "lastModified": 1789571357, + "narHash": "sha256-5VC3aTFNruM+Qwutz4879gURmHafSGxYQinkQk2BK8E=", "owner": "sini", "repo": "gen-schema", - "rev": "056ee9b51362abce57360455321002402facfd82", + "rev": "48cfb9928988c7de51f67087368d92d8e587040b", "type": "github" }, "original": { From 0268ac137ec2cfb3e0d0daceef26c69c470f6132 Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Wed, 16 Sep 2026 09:01:04 -0700 Subject: [PATCH 5/5] chore: drop the provider fixture's dead gen-schema input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `provider` test fixture declared `gen-schema` directly and referenced it nowhere — measured: its whole tree is `flake.nix`, `flake.lock` and `modules/den.nix`, and `gen-schema` appears only in the two input lines this removes. It was not inert. A direct pin put a SECOND gen-schema node in the CI lock at 4bd0f6eb17, a rev predating gen-merge entirely, so one lock carried two revisions of one library — the exact incoherence the hub indirection in `nix/lib/schema.nix` exists to prevent, sitting in the tree that documents it. Validation: `just ci` 1221/1221, exit 0, zero failures and zero errors. The fixture is exercised by the namespace-provider and external-namespace suites, so the suite measures the removal rather than just tolerating it. --- templates/ci/flake.lock | 22 ---------------------- templates/ci/provider/flake.lock | 21 --------------------- templates/ci/provider/flake.nix | 2 -- 3 files changed, 45 deletions(-) diff --git a/templates/ci/flake.lock b/templates/ci/flake.lock index 6726ac24..4f0fe8f6 100644 --- a/templates/ci/flake.lock +++ b/templates/ci/flake.lock @@ -441,27 +441,6 @@ "type": "github" } }, - "gen-schema_2": { - "inputs": { - "nixpkgs": [ - "provider", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1779986641, - "narHash": "sha256-KcZuS+hpaloICFcepNXNLpbehh6XoPjWPBteYpTqMRw=", - "owner": "sini", - "repo": "gen-schema", - "rev": "4bd0f6eb1799bf3c38eb3707419157b1f70eb1f5", - "type": "github" - }, - "original": { - "owner": "sini", - "repo": "gen-schema", - "type": "github" - } - }, "gen-scope": { "inputs": { "gen-graph": [ @@ -747,7 +726,6 @@ "den": [ "den" ], - "gen-schema": "gen-schema_2", "import-tree": [ "import-tree" ], diff --git a/templates/ci/provider/flake.lock b/templates/ci/provider/flake.lock index 9e745129..16324fcc 100644 --- a/templates/ci/provider/flake.lock +++ b/templates/ci/provider/flake.lock @@ -15,26 +15,6 @@ "type": "github" } }, - "gen-schema": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1779986641, - "narHash": "sha256-KcZuS+hpaloICFcepNXNLpbehh6XoPjWPBteYpTqMRw=", - "owner": "sini", - "repo": "gen-schema", - "rev": "4bd0f6eb1799bf3c38eb3707419157b1f70eb1f5", - "type": "github" - }, - "original": { - "owner": "sini", - "repo": "gen-schema", - "type": "github" - } - }, "import-tree": { "locked": { "lastModified": 1778781969, @@ -66,7 +46,6 @@ "root": { "inputs": { "den": "den", - "gen-schema": "gen-schema", "import-tree": "import-tree", "nixpkgs": "nixpkgs" } diff --git a/templates/ci/provider/flake.nix b/templates/ci/provider/flake.nix index 5c682e57..2380d730 100644 --- a/templates/ci/provider/flake.nix +++ b/templates/ci/provider/flake.nix @@ -10,7 +10,5 @@ nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; import-tree.url = "github:vic/import-tree"; den.url = "github:denful/den"; - gen-schema.url = "github:sini/gen-schema"; - gen-schema.inputs.nixpkgs.follows = "nixpkgs"; }; }