Skip to content

fix: keep unnameable context values out of scope identity - #682

Merged
sini merged 3 commits into
mainfrom
test/quirk-entrypoint-parity
Sep 18, 2026
Merged

sini merged 3 commits into
mainfrom
test/quirk-entrypoint-parity

Conversation

@sini

@sini sini commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Narrows mkScopeId to context values it can name, as an unnameable value falls through to the type placeholder branch and renders as den=<set:den>, so it carries no identity and only splits one logical scope across two ids depending on which module args happened to be bound on the path.
  • Fixes a quirk reached through den.hosts.<name>.includes arriving empty while the same aspect through den.aspects.<name>.includes delivers, because pipe emits are bucketed by scope id and assemble-pipes gives a consumer its own bucket, with no plain ancestor inheritance of raw emits outside policyBoundAncestor.
  • Leaves the keys in the context itself, where binding still reads them, so only the identity is narrowed.
  • Accounts for class content being unaffected, since it is not bucketed by scope id. In the reporting configuration two aspects delivering darwin.homebrew.casks through the same instance list arrived while one delivering a casks quirk did not.
  • Adds fifteen cells covering quirk collection through both entrypoints, including the darwin class and the flat den.hosts.<name> spelling that no existing cell reached.

Validation

  • nix develop -c just ci: 1236/1236, exit 0, read off stderr.
  • Falsified the new unit cell against the unfixed tree. It returns "host=igloo,inputs=<set:inputs>,lib=<set:lib>,user=tux" where it expects "host=igloo,user=tux", and its control cell stays green in that same run, so the cell measures the exclusion rather than a scope id that dropped every key.
  • Reproduced the report end to end on the reporter's own configuration against den 20d1e76, changing one token in one file: den.aspects.kocaeli.includes yields the tap, den.hosts.kocaeli.includes yields [ ], both at exit 0 with no error.
  • Re-ran that same pristine tree and that same one token change against this branch, which yields the tap.
  • Traced the scope pushes on that host. The host scope pushed ctx keys host|system while the user scope pushed den|host|inputs|lib|system|user, which is where the placeholder keys entered the identity.
  • Confirmed the class versus quirk contrast inside the reporter's config using his own aspects, where slack and spotify reach homebrew.casks through that instance list via a class key and rootshell does not via a quirk key.

follow-up to the report behind #681, which asked whether a quirk reached
through `den.hosts.<sys>.<name>.includes` is collected differently from one
reached through `den.aspects.<name>.includes`.

ten cells, each pairing an instance spelling with its aspect control: host
scope, user scope, the flat `den.hosts.<name>` spelling, a producer and
consumer split across the two entrypoints, a pipe policy over an
instance-included producer, and a flat host declaring `system` in one module
and content in another. all ten pass on #681 with no further change, so this
records parity rather than fixing anything.
@github-actions github-actions Bot added the allow-ci allow all CI integration tests label Sep 18, 2026
@theutz

theutz commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator
CleanShot 2026-09-18 at 21 54 22@2x CleanShot 2026-09-18 at 21 54 59@2x

theutz posted two screenshots on #682 showing `den.hosts.kocaeli.includes`
yielding an empty `homebrew.taps` where `den.aspects.kocaeli.includes` yields
the tap, a two-byte diff between the two runs.

that report is darwin and uses the flat `den.hosts.<name>` spelling with the
host declared in one module and the collection added from another. every
existing cell is nixos, x86_64-linux and two-level, so none of it was covered.

three cells mirroring the reported shape, all green at 20d1e76, so the
divergence does not reproduce on this tree.
`mkScopeId` built the scope identity from every ctx key, including ones whose
value it cannot name. Those fell through to the type placeholder branch and
rendered as `den=<set:den>`, `inputs=<set:inputs>`, `lib=<set:lib>`.

a value that can only render as its own type distinguishes nothing, so it adds
no identity. all it does is split one logical scope across two ids depending on
which module args happened to be bound on the path. pipe emits are bucketed by
scope id and a consumer reads its own bucket, so a producer and a consumer that
belong to the same logical scope landed in different buckets and the collection
came back empty.

reported by theutz on #682: a quirk reached through `den.hosts.<name>.includes`
arrived empty while the same aspect through `den.aspects.<name>.includes`
delivered. traced on his host, where the user scope pushed ctx keys
`den|host|inputs|lib|system|user` and the producer emitted at
`host=kocaeli,system=aarch64-darwin` while the surviving consumer bound at the
polluted id.

the keys stay in the context, where binding still reads them. only the identity
is narrowed.
@sini
sini requested a review from vic as a code owner September 18, 2026 20:28
@sini sini changed the title test: pin quirk collection as entrypoint-agnostic fix: keep unnameable context values out of scope identity Sep 18, 2026
@sini
sini requested a review from theutz September 18, 2026 20:43
@sini
sini enabled auto-merge (squash) September 18, 2026 20:55
@sini
sini merged commit 61ed76c into main Sep 18, 2026
43 of 50 checks passed
@sini
sini deleted the test/quirk-entrypoint-parity branch September 18, 2026 21:03
sini added a commit that referenced this pull request Sep 19, 2026
Closes #683.

## Summary

- Keeps `__providesForwarded` intact when an already-merged aspect is
merged a second time, so a `provides` child whose name matches a
registered class stays a child instead of being reclassified as the
aspect's own class content and emitted into that class. In the reporting
configuration a `provides.packages` child reached through an
`<angle/bracket>` include arrived as `flake.packages.<system>` carrying
fourteen of the child aspect's own fields — `name`, `includes`, `meta`,
`provides`, `_`, `__functor`, `homeManager`, `darwin` among them — which
a derivation-typed package schema then fails on.
- Routes the shadow test through `mkUnderscore`, which already holds
both the aspect and the candidate set, rather than leaving a copy at
each of the three construction sites. All three derived it from `!(own ?
k)`, which cannot tell a name the author wrote at top level from a name
an earlier merge forwarded there; the earlier merge published exactly
that distinction, so the predicate reads the marker instead of guessing
from key presence.
- Fixes all three sites, not the reported one. #683 bisects to #678,
which is correct for the path it hits (`mergeFunctions`, site B) but not
for the defect: `d50f0fc` already carried the identical test at
`mergeWithAspectMeta` and `aspectContentType` (lines 132 and 579), so
aliasing a merged aspect to a root slot or to a nested freeform key lost
the marker before #678. #678 extended the flaw to the include path.
- Leaves `packages` registered as a class. The leak is not name-specific
— `provides.nixos` reproduces it on stock den with no flake outputs
imported — so un-reserving `packages` would address one spelling and
leave `nixos`, `darwin`, `homeManager`, `apps`, `checks`, `devShells`,
`legacyPackages`, `os`, `user`, `hjem`, `maid` and `wsl` unchanged,
while breaking `packages-merge.nix`, the #572 regression asserting
`den.aspects.<name>.packages` reaches `flake.packages.<system>`.
- Adds two cells: the marker and its classification at the two re-merge
sites readable by marker, with the once-merged source as a live control
in the same run; and the reporter's shape end to end, since the symptom
is the child's own fields arriving as flake output content rather than a
marker value.

## Behaviour changes

- A provides child sharing a name with a registered class is no longer
emitted as that class's content when its aspect is re-merged. A
configuration that was receiving the child aspect's fields in a flake
output, or its `nixos`/`darwin`/`homeManager` content delivered twice,
stops receiving them. That content was never the class content the key
named.
- A provides child with an ordinary name is no longer reclassified as a
nested key on re-merge. Nested keys are not auto-walked, so this was
silent.
- A name the author genuinely writes at top level alongside a same-named
provides child keeps its direct value and stays classified, unchanged.
That case is what the shadow test exists for and the predicate still
answers it.

## Validation

- `nix develop -c just ci`: 1238/1238, exit 0, zero `❌` and zero `☢️`,
counted off a capture with stderr merged. The summary numerator agrees
with the counted passes, so the `pass + fail` identity is not standing
in for a truncated run.
- Ran the same suite on the tree with only `types.nix` reverted: 1238
collected, 1236 passing, exit 1, and the two failures are exactly the
two new cells. Same population both runs, so nothing else moved in
either direction. 1236 is also the count #682 landed at, which is the
independent check that the population is right.
- Falsified both new cells against the unfixed tree on their actual
symptom rather than on a mismatch: `aMarker`/`cMarker` read `[ ]`
against `[ "nixos" ]` and `aClassified`/`cClassified` read `[ "nixos" ]`
against `[ ]`, while the once-merged control in the same cell stays
green — so the cell measures the re-merge rather than a marker that
vanished everywhere.
- Reproduced #683 as filed before touching anything, against the local
checkout rather than the pinned revision: `originalForwarded = [
"packages" ]`, `remergedForwarded = [ ]`, `remergedClasses.classKeys = [
"packages" ]`, and thirteen names in `flake.packages.x86_64-linux`.
- Measured the name-independence claim rather than reasoning to it:
`provides.nixos` loses the marker and gains `classKeys = [ "nixos" ]` on
re-merge with no flake output module imported at all, and a
`provides.plainchild` child is reclassified as a nested key in the same
run.
- Measured the three key positions separately, because they do not share
a verdict: a root aspect named `apps` classifies its own content
normally (`classKeys = [ "nixos" ]`), a nested key named `apps` is read
as class content (`classKeys = [ "apps" ]`) and is correctly reserved,
and a `provides` child named `apps` is exempt by marker (`classKeys = [
]`). Only the third is this bug.
- `nix develop -c just fmt`: 577 files processed, 0 changed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

allow-ci allow all CI integration tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants