Skip to content

feat: port entity schema to gen-schema#563

Draft
sini wants to merge 33 commits into
denful:mainfrom
sini:feat/entity-gen-schema-port
Draft

feat: port entity schema to gen-schema#563
sini wants to merge 33 commits into
denful:mainfrom
sini:feat/entity-gen-schema-port

Conversation

@sini

@sini sini commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replace hand-rolled schemaEntryType in options.nix with gen-schema's mkSchemaOption (sidecars: includes, excludes; computed: isEntity)
  • Add flat-form entity declarations alongside legacy two-level form for both hosts and homes
  • Wire _topology and _meta introspection from gen-schema
  • Extract resolvedCtxModule to shared _types.nix for reuse across entity types

Details

Schema port: den.schema now uses gen-schema's mkSchemaEntryType which provides sidecar extraction, computed fields, and __functor wrapping. The resolvedCtxModule (id_hash, resolved, collisionPolicy) is extracted to _types.nix and injected into entity submodule imports.

Flat form: Both den.hosts and den.homes now accept flat declarations:

den.hosts.igloo = { system = "x86_64-linux"; users.tux = { }; };
den.homes."tux@igloo" = { system = "x86_64-linux"; };

A deepMergeAttrs custom type accepts both forms, and apply preprocesses flat entries into the canonical two-level shape via preprocessHosts. All 6 consumers see the unchanged { system.name = entity } shape.

Tests: 18 new tests (843 total, up from 825) covering flat hosts, flat homes, id_hash, freeform attrs, topology, meta introspection, isEntity computed, and schema sidecars.

Test plan

  • All 843 CI tests pass (nix develop -c just ci)
  • Flat host form produces correct two-level shape
  • Flat home form with @-name parsing works
  • Mixed flat + legacy forms coexist
  • Cross-entity host lookup from homes preserved
  • Existing templates (default, minimal, example) unaffected

@github-actions github-actions Bot added the allow-ci allow all CI integration tests label May 21, 2026
@sini sini force-pushed the feat/entity-gen-schema-port branch from 6574fac to a370d30 Compare May 21, 2026 22:37
@sini sini force-pushed the feat/entity-gen-schema-port branch 6 times, most recently from 407bd03 to 56bbc59 Compare June 2, 2026 19:51
sini added 22 commits June 5, 2026 12:14
Replace standalone captureWithPathsWith + resolveEntity captures with
diagram.projectScope which projects the fleet capture onto scope
subtrees. One fleet pipeline run, N projections — more accurate (sees
full policy resolution) and more performant.

Both fleet-demo and diagram-demo updated. Pin den-diagram to
feat/fleet-subtree-context.
Replaces hand-rolled schemaEntryType with gen-schema mkSchemaOption.
Sidecars: includes, excludes. Computed: isEntity (structural content only).
Extracts resolvedCtxModule (id_hash, resolved, collisionPolicy) to
_types.nix for entity type reuse. collisionPolicy flows through deferred
module merge to entity instances (not a sidecar) preserving existing
ctx.host.collisionPolicy resolution path.
den.hosts now accepts both forms:
  - Legacy: den.hosts.x86_64-linux.igloo = { ... }
  - Flat:   den.hosts.igloo = { system = "x86_64-linux"; ... }

The outer option type uses a permissive submodule with deepMergeAttrs
freeformType (lib.recursiveUpdate-based merge that avoids the infinite
recursion lib.types.anything causes with cross-option references).
The apply function preprocesses flat entries into two-level form and
re-evaluates through the original attrsOf systemType, so all 6
consumers see the canonical { system.name = hostConfig } shape.
Same pattern as den.hosts: deepMergeAttrs + preprocessHosts + apply.
Cross-entity host lookup and osConfig injection preserved.
Covers: id_hash, freeform, topology, meta introspection,
isEntity computed, schema includes sidecar.
sini added 11 commits June 5, 2026 12:30
Update flake inputs and references to match the renamed repo
at github:sini/gen-schema.
gen-schema flattened _meta into _-prefixed options and renamed
sidecars → collections. nix-effects changed bindAttrs so true is a
literal param, not an optionality marker — translate __args values
to fx.bind.optionalArg before bind.fn.
Expose entity.aspects alongside entity.hasAspect: the flat list of all resolved aspect nodes (every depth), each the resolved node augmented with .identity (base FQN, ctx-stripped), .identityKey (full unique key incl {ctxId}), and .isNamed. Excludes the entity root and tombstoned/excluded aspects; anonymous nodes are included so callers can surface them.

One fxFullResolve per class now yields both the membership pathSet (hasAspect) and a parallel resolvedNodes state map, so the accessor adds no extra resolution beyond what hasAspect already pays. Nodes are stored behind the existing state thunk, preserving deepSeq-safety (class-content bodies are never forced by state deepSeq; reading .aspects forces only name/meta/identity).

isNamed inspects the full identity, not just the node name: isMeaningfulName misses nested anonymous instances like roles/dev/<anon>:3 (whose name slips past the exact-<anon> check), so consumers (e.g. colmena tags) can filter cleanly on .isNamed.

Adds Group K has-aspect tests: flat coverage across depths, exclude-aware, entity-root excluded, anonymous exposure, nested identity distinctness, and the named-have-clean-identity invariant.
A policy's destructured args double as a dispatch guard: `{ <kind>, ... }:`
fans the policy across every entity of that kind. There was no way to say
"fire once at my own scope, don't fan" — and naming the scope's own kind
doesn't work when that kind isn't bound in the scope's ctx. A flake-scope
resolution policy is the motivating case: `{ flake, ... }:` fails
resolveArgsSatisfied (flake isn't in its own ctx), so the policy silently
never fires — which is exactly how a stale `{ flake-system, ... }:` on
to-fleet broke the fleet→env→cluster cascade.

Add `self`: always bound in the dispatch ctx (to the scope's own context), so
`{ self, ... }:` is satisfiable and callable at any scope, and excluded from
the late-sibling fan so it fires only at its registration scope.

- dispatch.nix: inject `self` at the single dispatch chokepoint, used for both
  resolveArgsSatisfied and the policy call. Backwards compatible — existing
  policies use `...` and ignore the extra key.
- policy/schema.nix: drop `self`-guarded policies from the late-dispatch
  fan-out so they fire once, at their own scope.

`self` is the right guard for resolution policies (to-fleet, fleet-to-envs, …)
that create children and must fire once; `{ <kind>, ... }:` stays the tool for
genuine fan-out.

Tests (self-guard): self fires at its registration scope (host OS config set);
a host-registered self policy does not fan to user children, whereas
`{ user, ... }:` fans to each.
@sini sini force-pushed the feat/entity-gen-schema-port branch from b2bcfd4 to 1b56211 Compare June 5, 2026 19:36
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.

1 participant