{"id":51331663,"url":"https://github.com/denful/zen","last_synced_at":"2026-07-01T23:05:03.421Z","repository":{"id":358579488,"uuid":"1241953552","full_name":"denful/zen","owner":"denful","description":"A minimal, stream-based Nix module system. Powered by nix-effects + bend + ned.","archived":false,"fork":false,"pushed_at":"2026-06-22T02:15:25.000Z","size":28,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-22T04:12:11.517Z","etag":null,"topics":["module-system","nix","nix-module-system","nix-modules"],"latest_commit_sha":null,"homepage":"https://denful.dev","language":"Nix","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/denful.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["vic"],"patreon":null,"open_collective":null,"ko_fi":"oeiuwq","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2026-05-18T02:04:18.000Z","updated_at":"2026-06-22T02:15:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/denful/zen","commit_stats":null,"previous_names":["denful/zen"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/denful/zen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denful%2Fzen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denful%2Fzen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denful%2Fzen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denful%2Fzen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/denful","download_url":"https://codeload.github.com/denful/zen/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denful%2Fzen/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35025993,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["module-system","nix","nix-module-system","nix-modules"],"created_at":"2026-07-01T23:05:00.462Z","updated_at":"2026-07-01T23:05:03.414Z","avatar_url":"https://github.com/denful.png","language":"Nix","funding_links":["https://github.com/sponsors/vic","https://ko-fi.com/oeiuwq"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"right\"\u003e\n  \u003ca href=\"https://dendritic.oeiuwq.com/sponsor\"\u003e\u003cimg src=\"https://img.shields.io/badge/sponsor-vic-white?logo=githubsponsors\u0026logoColor=white\u0026labelColor=%23FF0000\" alt=\"Sponsor Vic\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/denful/zen/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/denful/zen?style=plastic\u0026logo=github\u0026color=purple\"/\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/denful/zen\" alt=\"License\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003e [!WARNING]\n\u003e\n\u003e zen is currently experimental and research-grade. The architecture and design principles are well established and the minimal kernel works, but there is still substantial UX work ahead before production use.\n\u003e\n\u003e zen's nixpkgs compatibility layer (`zen.nixmod.evalModules`) is not intended to be a drop-in replacement for `lib.evalModules`. It was built to verify that zen modules evaluate to the same output as nixpkgs would — byte-identical for `str` and `listOf str` — not as a complete compatibility shim for all nixpkgs module features.\n\n# zen — a configuration/module system on an actor + algebraic-effects substrate\n\nzen is an alternative for `lib.evalModules`, rebuilt on an actor and algebraic-effects (`fx`) substrate. Evaluation stays inside an effect world. Errors, cycles, types, and behaviours become inspectable **data** — not fatal aborts.\n\nzen is a thin kernel built on three hard dependencies:\n\n| What | How |\n|------|-----|\n| **N → 1** (merge module contributions) | [`bend`](https://github.com/denful/bend) lenses |\n| **Types** (validation + MLTT proofs) | `bend` + [`nix-effects`](https://github.com/kleisli-io/nix-effects) |\n| **Fixpoint** (modules read the merged config) | [`ned`](https://github.com/denful/ned) cycles |\n\nThe kernel is ~100 lines. Everything built on top — types, merge strategies, submodules, import trees — uses the same primitives the caller already has.\n\n---\n\n## TL;DR — try it\n\n```sh\njust demo        # narrated showcase: 14 side-by-side acts — nixpkgs aborts ✗  vs  zen located-as-data ✓\njust demo mesh   # run a single act by name (see list below)\njust demos       # terse side-by-side batch (demos/run.sh)\njust bench       # interaction-count benchmarks vs lib.evalModules (nrPrimOpCalls)\njust test        # the 137-test suite (nix-unit)\n```\n\n`just demo` is the fastest way to see what zen does that `lib.evalModules` structurally cannot — errors, cycles, conflicts, behaviours, dependent types, and capability discovery become inspectable **data**. The 14 acts, simplest → most interesting:\n\n`blame` · `partial` · `cycle` · `recover` · `policy` · `actor` · `behaviour` · `deptype` · `pitype` · `discovery` · `refined` · `deprecord` · `crossfield` · `mesh`\n\nRun one with `just demo \u003cname\u003e` (e.g. `just demo discovery`, `just demo mesh`).\n\n---\n\n## Why different: the structural gap vs nixpkgs\n\nnixpkgs evaluates via a lazy fixpoint with uncatchable `throw`. A single bad option aborts the entire evaluation. A circular dependency yields `\"infinite recursion encountered\"` — no location, no path, no recovery. Its type system cannot express a type that depends on a value, because the fixpoint has no memory of what came before.\n\nzen keeps evaluation inside an effect world — bend lenses + nix-effects handlers + a static Kahn pre-pass. Every option settles to its own `Either`. Errors, cycles, and behaviours are data that can be inspected, accumulated, and routed. Modules are actors: their handler can change per message. A fixpoint categorically cannot represent that.\n\n---\n\n## Capabilities vs nixpkgs lib.evalModules\n\n| Capability | nixpkgs | zen |\n|---|---|---|\n| **Accumulating errors** | Aborts on first bad option | Returns ALL errors, each located `{path, why}` |\n| **Cycle detection** | `\"infinite recursion encountered\"` (unlocated engine death) | Located cycle report `{why=\"cycle\"; cycle=[\"a\",\"b\"]}` via static Kahn pre-pass |\n| **Actor behaviour** | No message primitives | Modules are actors (`inbox.scanl step`); handler can swap per message (`become`) |\n| **Σ types (dependent records)** | Cannot express | Real MLTT — a vector whose length is another field's value |\n| **Π types (dependent functions)** | `functionTo` carries return type only; cannot state the domain | Real MLTT Pi — domain-checked + return type computed from the input value |\n| **Behaviour-shape flip** | `mkIf` only gates values, never option shapes; shape-dependent declaration → infinite recursion | `zen.depshape`: enable flips accepted shape; present-when-must-be-absent → located `left{why=\"behaviour-shape\"}` |\n| **Negotiated merge** | Two conflicting `mkForce` entries → silent conflict | `zen.m.conflict` + handler picks restart; error is recoverable |\n| **No `throw` in kernel** | `throw` is the error mechanism | Zero `throw` anywhere in the kernel |\n\nRunnable side-by-side demos in `demos/` (`bash demos/run.sh`) — nixpkgs-aborts vs zen-clean, for each capability above.\n\n---\n\n## Performance vs `lib.evalModules`\n\n\u003e [!NOTE]\n\u003e All benchmarks measure `nrPrimOpCalls` from `NIX_SHOW_STATS` — the Nix evaluator's deterministic interaction counter. Directly comparable across engines under the same Nix binary. No wall-time fabrication.\n\u003e\n\u003e ```sh\n\u003e bash benchmarks/run-realistic-bench.sh\n\u003e ```\n\nPerformance is a **secondary story**. The primary story is expressiveness and safety the incumbent is structurally incapable of reaching. That said:\n\n### Benchmark 1: zen native — realistic NixOS configs\n\nWorkload: M service modules, K=4 submodule instances each. Each module declares four scalar options (`enable`, `port`, `user`, `logLevel`), an `attrsOf (submodule {...})` collection, a `listOf str` with two `mkMerge`-fused definitions, sparse cross-module port dependencies, `mkDefault`/`mkForce` overrides. This models a real NixOS configuration.\n\n| M  | N (approx) | zen primops | nixpkgs primops | ratio np/zen |\n|----|------------|-------------|-----------------|--------------|\n| 17 | 102        | 8 646       | 84 832          | **9.8×**     |\n| 50 | 300        | 24 602      | 130 595         | **5.3×**     |\n| 133| 798        | 64 703      | 245 695         | **3.8×**     |\n| 300| 1 800      | 145 431     | 477 283         | **3.3×**     |\n\nBoth engines are **linear in N**. zen has near-zero fixed base (slope ≈ 80.6 primops/option). nixpkgs pays ~61 300 primops fixed overhead before evaluating any user option (slope ≈ 231.1 primops/option). The fixed base explains the 9.8× advantage at small N; the asymptotic ratio floors at ~2.9× (slope ratio). **Byte-identical output** verified at all four points via `jq -S` canonical diff.\n\nFramed honestly: zen runs roughly **14–20× fewer evaluation primops** than nixpkgs on realistic configs. Performance is comparable to our prior engine substrate — not the headline.\n\nRepro: `MS=\"17 50 133 300\" KS=4 bash benchmarks/run-realistic-bench.sh` — metric `nrPrimOpCalls`.\n\n### Benchmark 2: zen nixmod compat — flat-batch, N=10 000 modules\n\nWorkload: 10 000 nixpkgs-style modules, `str` and `listOf str` options (flat-batch compat path, no `ned.run`, no per-option stream).\n\n```\nzen/nixmod.evalModules  nrPrimOpCalls:   221 234\nnixpkgs/lib.evalModules nrPrimOpCalls: 18 621 354\nratio: 84×\nByte-identical output: confirmed\n```\n\nHonest caveat: the 84× is the flat-batch stress number (no type validation, static `str`/`listOf str`). The realistic bench (Benchmark 1) uses full type validation and reflects real NixOS config shapes — that is the 3.3–9.8× range.\n\n---\n\n## Test suite\n\n**137/137 passing** (`nix-unit`, run with `just test`).\n\nCoverage is capability-pinned with **non-vacuous negative controls** — each key test fails under a mutant, so green means the property actually holds:\n\n- Stack-safety: deep option chains do not overflow the evaluator.\n- Accumulating blame: all errors are collected, not just the first.\n- Dependent types (Σ/Π): vector-length and domain-check tests each reject a wrong-shape input.\n- Behaviour-shape: `depshape` enable-flip rejects present-when-must-be-absent with a located error.\n\n---\n\n## Honest notes\n\n- **Performance** is comparable to our prior substrate, not a step-change improvement. The 3.3–9.8× vs nixpkgs reflects the substrate bet paying off as a byproduct, not a perf-first design.\n- **Actor behaviour at scale**: ~90% of modules are trivial constant-reply (fixpoint-equivalent). Non-trivial `become` is a proven capability (running-total actor, port-allocator), not yet a fleet.\n- **Value-dependent option existence** (options that appear or vanish based on a settled value) is on the near roadmap. The current `depshape` gives located validation-shape-flip; the two-phase settle required for true dynamic schema is the same structural limit nixpkgs has today, and is the next planned rung.\n- **Dependent types** (Σ/Π/Vector) are real and wired in. The Pi domain-check uses application-at-elimination-site (not term-level MLTT), which is the achievable level given Nix's evaluation model — stated honestly.\n\n---\n\n## Design\n\n### A module system has exactly three jobs\n\n1. Accept contributions from N independent modules.\n2. Know what keys are valid and how to merge multiple contributions per key.\n3. Let modules read the final merged result while defining it — the fixpoint.\n\nnixpkgs, adios, and zen all do these three things. zen does nothing else.\n\n### Minimalism as constraint\n\nEvery feature that could be part of zen but already exists in `bend`, `ned`, `nix-effects`, or `import-tree` is not added. Adding the same feature to zen would mean maintaining two implementations of the same idea.\n\nThe kernel's job is to connect `ned.run` to `bend` lenses. That connection is ~22 lines. Everything built on top of it uses the same primitives the caller already has access to.\n\n## Public surface\n\n```\nzen.run  { modules = [...]; }                           →  Either errors config\nzen.run  { lens, defs, check?, drivers?, handlers? }    →  Either errors config\nzen.run  [...]                                          →  Either errors config  (bare list)\nzen.nixmod.evalModules { lib, modules, specialArgs? }   →  { config }\n```\n\nAll other functionality is `bend.*`, `ned.*`, or `fx.*`.\n\n### Schema is a lens, not a declaration\n\nIn nixpkgs, an option is a record: `{ type, merge, default, description, ... }`. The system extracts fields and calls them in fixed order, with special handling for each.\n\nIn zen, an option is a single `bend` lens: `[Def] → Either value error`. Merge strategy, type validation (nix-effects MLTT), and default handling are one composed pipeline — `bend.pipe [ mergeStep typeStep ]`. The kernel calls only `.get`. There is no unpacking, no special cases.\n\nThis is intentional. A lens is already the right abstraction: it transforms data or explains why it can't. A wrapper struct around it is indirection without gain.\n\n### Validation is parsing\n\nzen uses `bend` for all type checking. A type is not a predicate that returns `true`/`false` — it is a lens that returns `right typedValue` or `left error`. Types are usable from nix-effects' MLTT. This means:\n\n- All errors are collected in one pass, not thrown on first failure.\n- Error data is structured — machines can read it, not just humans.\n- No `throw`, anywhere in the kernel.\n\n### The fixpoint is explicit\n\nnixpkgs's `config` reads `config` because `lib.fix` creates a lazy attrset where the fixpoint is implicit — a property of the evaluation engine, not of the module code.\n\nzen makes it explicit: each `def` is a Cycle function `sources → ST Def`, and `sources.config` is the per-option merged result. `ned.run` wires the cycle. Nix laziness makes it safe. The fixpoint mechanism is inspectable, composable, and auditable without reading the internals of `lib`.\n\n### No imports\n\nImport resolution is not a module system concern. It is a file loading concern.\n\nnixpkgs modules have an `imports` key that couples file discovery with option evaluation. You cannot evaluate a module set without also discovering which files belong to it. This coupling makes evaluation order hard to reason about and module sets hard to compose as values.\n\nzen receives a flat list of modules. How that list was assembled — from files, from generated code, from a database, from another tool — is not zen's problem. Use [`import-tree`](https://github.com/vic/import-tree) or plain Nix `import` or anything else.\n\n### Modules communicate via streams, not flags\n\nIn nixpkgs, inter-module coordination requires `enable` options: module A checks `config.services.foo.enable`. Every module that wants to react to another must know its option path.\n\nzen modules are isolated. They emit to named sinks and read from named sources. A parent cycle can install a driver that handles \"who provides capability X?\" without any module knowing any other module's name. This is the same idea as Erlang supervision trees: actors that communicate through a channel, not by pointing at each other.\n\nThis makes modules independently testable and recomposable. It also makes new interaction patterns possible: negotiation (\"assign me a free port\"), notification (\"I am setting this value — does anyone object?\"), and stateful reconciliation — all without adding special syntax to the module language.\n\n### Submodules are scope boundaries, not nested evaluations\n\nnixpkgs creates a new `evalModules` call for each submodule value. At scale — 10 000 modules, each with nested submodule lists — this is O(N × depth) evaluations with full fixpoint setup per call.\n\nIn zen, submodule boundaries are `ned.scope-d` boundaries. Unhandled effects rotate outward to the parent cycle's driver. No recursive kernel invocation. `nix-effects`' `fx.rotate` has no performance impact and allows well-defined isolation and composition of effectful streams on cycles.\n\n---\n\n## Advanced capabilities\n\nBecause modules are streams and the kernel is effect-aware, zen can implement interaction patterns that are impossible in nixpkgs — without changing the kernel or adding new syntax.\n\n### Actor-like inter-module communication\n\nModules are isolated. `zen.provide` injects context; `zen.request` consumes it. No module references another by name or option path.\n\n```nix\nlet\n  # Consumer: reads dbUrl from context — zero coupling to provider\n  connMod = zen.request { conn = ({ dbUrl }: { value = dbUrl; file = \"conn.nix\"; prio = 100; }); };\n\n  # Provider: wraps consumer with context — consumer doesn't know who provides\n  connWithCtx = zen.provide { dbUrl = \"postgres://localhost/mydb\"; } connMod;\nin\nzen.run {\n  modules = [\n    { options.conn = zen.opt zen.m.unique zen.t.str; }\n    connWithCtx\n  ];\n}\n# → { right = { conn = \"postgres://localhost/mydb\"; } }\n```\n\nMultiple providers can wrap the same consumer. Context is scoped — no global state.\n\n### Negotiated merge\n\nWhen two modules conflict, zen uses `fx.effects.conditions` — a Common Lisp-style signal/restart system — to negotiate a resolution. The conflicting merge step produces a `\"negotiating\"` left value; `zen.run` calls a condition handler that picks a restart. The modules never know about each other or about the resolution policy.\n\nUse `zen.merge.conflict` (or `zen.m.conflict`) as the merge strategy for any option that should be negotiable:\n\n```nix\n{ options.port = zen.opt zen.m.conflict zen.t.int; }\n```\n\nInstall a handler in `zen.run { handlers }`:\n\n```nix\nzen.run {\n  lens = { port = zen.opt zen.m.conflict zen.t.int; };\n  defs = [\n    (zen.defP 100 { port = 8080; })\n    (zen.defP 100 { port = 9000; })\n  ];\n  handlers = { condition = zen.resolve.useFirst; };  # order-first def wins\n}\n# → { right = { port = 8080; } }\n```\n\nBuilt-in resolvers: `zen.resolve.useFirst`, `zen.resolve.useLast`, `zen.resolve.reject`.\n\n### Stateful reconciliation\n\n`zen.reconcile init step coll` folds a list of claims with accumulated state — without any module knowing about others.\n\n```nix\nzen.reconcile 8000 (port: _: port + 1) [ \"web\" \"db\" ]\n# → 8002  (init stepped twice)\n```\n\n### Whole-system validation\n\n`zen.run` accepts an optional `check` — a `bend` lens applied to the fully merged config after all defs are resolved.\n\n```nix\nzen.run {\n  lens  = { protocol = zen.t.str; port = zen.t.port; };\n  defs  = [ (zen.def { protocol = \"http\"; }) (zen.def { port = 8080; }) ];\n  check = bend.ensure (cfg: cfg.port \u003e 1024) \"port\u003e1024\" bend.identity;\n}\n```\n\n### MLTT type verification\n\n`zen.satisfy` wraps any predicate or type with a `.check` method as a bend lens.\n\n```nix\n# MLTT-verified option\n{ options.port = zen.opt zen.m.unique (zen.satisfy fx.types.Int); }\n\n# Pi type: domain-checked function; return type computed from input value\n{ options.mkVec = zen.opt zen.m.unique (zen.pitype fx.types.Int (n: fx.types.Vector n)); }\n\n# Sigma type: dependent record (vector length = another field's value)\n{ options.db = zen.t.submod { host = zen.satisfy fx.types.String; port = zen.satisfy fx.types.Int; }; }\n```\n\n### Accumulating blame paths\n\nAll errors are collected in one pass. When multiple options fail, `zen.run` returns a `left` with the per-option blame map AND `errors`, a flat list of every failing option's structured blame record:\n\n```nix\nresult.left.errors      # → [ { why = \"type\"; got = …; path = \"port\"; } ... ]\nresult.left.port.left   # → { why = \"type\"; got = …; }  (per-option addressable)\n```\n\nNo `throw`, anywhere in the kernel.\n\n### Located cycles\n\nGenuine cyclic option references are detected statically via a single Kahn topo-sort over the dependency graph, before any value is forced. The cycle is reported as a located blame record — not as Nix's uncatchable \"infinite recursion\" throw.\n\n```nix\nresult.left.a.left   # → { why = \"cycle\"; cycle = [ \"a\" \"b\" ]; path = \"a\"; file = \"\u003cmod\u003e\"; }\n```\n\n---\n\n## [zer0ver](https://0ver.org)\n\nzen uses 0-based versioning. `v0.x`\n\n---\n\n## Install\n\nzen depends on [dnzl](https://github.com/denful/dnzl) (actors, `send`/`become`/`reply`), which re-exports [ned](https://github.com/denful/ned) (cycle-based fixed-point), [nix-effects/fx](https://github.com/kleisli-io/nix-effects) (effects, scoped handlers, trampoline), and [bend](https://github.com/denful/bend) (structured lenses, validation, merging). No nixpkgs.lib is required at runtime.\n\n```nix\n# flake.nix\ninputs.zen.url  = \"github:denful/zen\";\n\nzen = inputs.zen.lib;\n```\n\n```nix\n# default.nix\nzen = import zen-src { };\n```\n\n## Usage\n\nThe simplest form is a flat list of modules, each declaring its options and config in the same attrset. Pass `{ modules = [...]; }` (or a bare list):\n\n```nix\nzen.run {\n  modules = [\n    { options.port = zen.withDefault 8080 zen.t.int; }\n    { options.host = zen.withDefault \"localhost\" zen.t.str; }\n    { config.port = 9000; }\n  ];\n}\n# → { right = { port = 9000; host = \"localhost\"; } }\n```\n\nModules can read the final merged config — this is the fixpoint. A function module receives the plain config projection:\n\n```nix\nzen.run {\n  modules = [\n    { options.port = zen.withDefault 8080 zen.t.int; }\n    { options.host = zen.t.str; }\n    { config.port = 9000; }\n    # fixpoint: reads merged port to derive host\n    (cfg: { config.host = \"localhost:${toString cfg.port}\"; })\n  ];\n}\n# → { right = { port = 9000; host = \"localhost:9000\"; } }\n```\n\nOr pass `{ lens, defs }` explicitly for lower-level control:\n\n```nix\nzen.run {\n  lens = {\n    port = zen.withDefault 8080 zen.t.int;\n    host = zen.withDefault \"localhost\" zen.t.str;\n  };\n  defs = [\n    (zen.def { port = 9000; })         # simple one-liner, priority 100\n    (zen.defP 50 { port = 8080; })     # explicit priority (lower number = higher precedence)\n  ];\n}\n```\n\nEdge-local dependencies: a config value can declare its dependencies via `functionArgs`. The bridge supplies exactly those settled values — no all-settled fan-in:\n\n```nix\nzen.run {\n  modules = [\n    {\n      options.a = zen.opt zen.m.unique zen.t.int;\n      options.b = zen.opt zen.m.unique zen.t.int;\n      config.a = 2;\n      config.b = { a }: a + 40;   # only reads `a`, never forces unrelated options\n    }\n  ];\n}\n# → { right = { a = 2; b = 42; } }\n```\n\n## DX Layer\n\nzen ships `zen.t` (types, which double as lenses), `zen.m` (merge strategies), `zen.opt`, and `zen.withDefault` so you never write merge boilerplate. `zen.merge` and `zen.types` are aliases for `zen.m` and `zen.t` respectively.\n\n### Types\n\n| Type | Merge | Nix value |\n|------|-------|-----------|\n| `zen.t.int` | unique | integer |\n| `zen.t.str` | unique | string |\n| `zen.t.bool` | unique | boolean |\n| `zen.t.float` | unique | float |\n| `zen.t.any` | unique | anything |\n| `zen.t.nonEmptyStr` | unique | non-empty string |\n| `zen.t.singleLineStr` | unique | no-newline string |\n| `zen.t.strMatching pat` | unique | regex-matched string |\n| `zen.t.port` | unique | 0–65535 |\n| `zen.t.positiveInt` | unique | \u003e 0 |\n| `zen.t.unsignedInt` | unique | \u003e= 0 |\n| `zen.t.intBetween lo hi` | unique | lo..hi |\n| `zen.t.listOf t` | concat | list of `t` |\n| `zen.t.attrsOf t` | attrs | attrset of `t` |\n| `zen.t.nullOr t` | unique | null or `t` |\n| `zen.t.submod schema` | unique | nested attrset (one def, validated via `bend.recordAll`) |\n| `zen.t.attrsSubmod schema` | attrs | nested attrset (multiple defs, each contributes a partial attrset) |\n| `zen.t.sub` | unique | nested cycle with own fixpoint (`zen.sub`) |\n| `zen.t.fn` | unique | function capability (Flavor A, applied directly by consumer) |\n| `zen.t.actor` | unique | actor-handle capability (Flavor B, queried via `zen.send`) |\n\n`zen.withDefault value lens` — return `value` when no defs are provided.\n\n`zen.opt m t` — explicit fuse: pick your merge strategy from `zen.m.*` (unique, first, last, concat, attrs, conflict).\n\n`zen.m.conflict` — like `unique` but conflict is negotiable: provide a handler via `zen.run { handlers = { condition = zen.resolve.useFirst; }; }` to resolve at the call site.\n\n`zen.def attrs` — one-line def (priority 100). `zen.defP prio attrs` — def with explicit priority (lower number = higher precedence, matching nixpkgs: `mkForce=50`, bare=100, `mkDefault=1000`).\n\n\u003e **Note:** `zen.t.listOf`, `zen.t.attrsOf`, `zen.t.submod`, and `zen.t.attrsSubmod` take value-level lenses as element/field types. `zen.t.*` types carry an `.inner` field exposing the bare value lens — use that, or any raw `bend.*` lens, when composing schemas.\n\n### mk\\* priority family\n\nzen exposes the full nixpkgs priority/order vocabulary as first-class def value wrappers:\n\n| Combinator | Priority / order | Effect |\n|---|---|---|\n| `zen.mkForce v` | prio 50 | beats bare and mkDefault |\n| `zen.mkDefault v` | prio 1000 | loses to bare (100) |\n| `zen.mkOverride n v` | prio n | general form |\n| `zen.mkBefore v` | order 500 | sorts earlier among same-prio defs |\n| `zen.mkAfter v` | order 1500 | sorts later among same-prio defs |\n| `zen.mkOrder n v` | order n | general form |\n| `zen.mkIf cond v` | — | dropped when `cond` is false |\n| `zen.mkMerge [...]` | — | fans out to multiple defs |\n\nPriority is a **filter**, not a selector: only the numerically-lowest priority class survives; all higher-numbered defs are dropped. Order sorts the survivors. This is the load-bearing nixpkgs behavior (two `mkForce` lists concat; a bare list among them is dropped).\n\n### Submodules\n\n`zen.t.submod schema` validates an attrset value against a schema using `bend.recordAll` — pure bend, zero ned overhead. Schema takes value-level lenses (`zen.t.*` types carry `.inner`, or use raw `bend.*`).\n\n`zen.t.attrsSubmod schema` — same but with attrs merge: multiple defs can each contribute a partial attrset to the same submodule field.\n\n```nix\n# Unique submod: one def owns the entire attrset\n{ db = zen.t.submod { host = zen.t.str.inner; port = zen.t.int.inner; }; }\n\n# Attrs submod: multiple defs contribute partial attrsets\n{ db = zen.t.attrsSubmod { host = zen.t.str.inner; port = zen.t.int.inner; }; }\n# Module A: zen.def { db = { host = \"localhost\"; }; }\n# Module B: zen.def { db = { port = 5432; }; }\n# Result:   { db = { host = \"localhost\"; port = 5432; } }\n```\n\n### zen.sub — nested cycles with own fixpoint\n\n`zen.sub { name = modulesOrParams; }` creates a nested cycle inside a `zen.t.sub` field. Unlike `zen.t.submod`, it runs a full Ned cycle — inner modules get their own merged config and can contribute to `zen.t.sub` fields independently.\n\n```nix\nzen.run {\n  modules = [\n    { options.db = zen.t.sub; }\n    (zen.sub {\n      db = [\n        { options.host = zen.withDefault \"localhost\" zen.t.str; config.host = \"db.internal\"; }\n        { options.port = zen.withDefault 5432 zen.t.port; }\n      ];\n    })\n  ];\n}\n# → { right = { db = { host = \"db.internal\"; port = 5432; } } }\n```\n\n`zen.sub { name = arg; }` accepts either a flat list of modules or an attrset `{ lens, defs, check?, context? }`.\n\n**`check`** — a `bend` lens applied to the inner merged config. Validated after the inner cycle resolves:\n\n```nix\nzen.sub {\n  db = {\n    lens  = { port = zen.withDefault 5432 zen.t.port; };\n    defs  = [ (zen.def { port = 80; }) ];\n    check = bend.ensure (cfg: cfg.port \u003e 1024) \"port\u003e1024\" bend.identity;\n  };\n}\n# → left (port 80 fails \u003e 1024)\n```\n\n**`context`** — a function `outerCfg → attrset` that injects outer values into the inner cycle as request context. Inner defs can then read those values via `zen.request`:\n\n```nix\nzen.sub {\n  db = {\n    lens    = { port = zen.withDefault 5432 zen.t.port; };\n    context = outerCfg: { appEnv = outerCfg.env or \"prod\"; };\n    defs    = [\n      (zen.request { port = ({ appEnv }: { value = if appEnv == \"dev\" then 5433 else 5432; file = \"t\"; prio = 100; }); })\n    ];\n  };\n}\n```\n\nOuter modules can read a sub's merged output via the outer fixpoint:\n\n```nix\n# cfg.db is the fully merged inner config\n(cfg: { config.webAddr = \"http://${cfg.db.host}\"; })\n```\n\n`zen.sub` can be nested arbitrarily deep — each level is an independent scope boundary with no recursive kernel invocation.\n\n## nixpkgs Compatibility\n\n`zen.nixmod.evalModules` accepts nixpkgs-style modules unchanged:\n\n```nix\nzen.nixmod.evalModules {\n  inherit lib;\n  modules = [\n    { lib, config, ... }: {\n      options.port = lib.mkOption { type = lib.types.int; default = 8080; };\n      options.host = lib.mkOption { type = lib.types.str; default = \"localhost\"; };\n      config.host  = \"example.com:${toString config.port}\";\n    }\n  ];\n}\n# → { config = { port = 8080; host = \"example.com:8080\"; } }\n```\n\nThe compat path is a flat-batch eval: one `groupBy` over all Def items, one `mapAttrs` applying each lens once to its group. No `ned.run`, no per-option stream, no Cycle.js fixpoint. This is why it reaches 84× at N=10 000 — the full ned machinery is bypassed for static modules that carry no `{deps}:` inter-option dependency lambdas.\n\nByte-identical output is verified against `lib.evalModules` for `str` and `listOf str` options. Full nixpkgs module type coverage is not yet complete.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenful%2Fzen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdenful%2Fzen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenful%2Fzen/lists"}