Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save sini/711513ef3eb581403bdc2758bb8e4a0c to your computer and use it in GitHub Desktop.

Select an option

Save sini/711513ef3eb581403bdc2758bb8e4a0c to your computer and use it in GitHub Desktop.
den v1 delivery-edge unification — delivery-half debt elimination design (edge algebra, Π(root), trace-gated ports)

Delivery-edge unification — v1 delivery-half debt elimination

  • Date: 2026-06-12
  • Status: Design approved (sectioned review with maintainer); pending spec review + implementation plan.
  • Scope: den v1 fx-pipeline, delivery/collection half. Full sweep, audit-driven. Public API free to improve, with route/provides retained as shims (see §4).
  • Strategic premise: v2/HOAG timing is uncertain — v1 is the product and must be as correct as possible. This completes the stabilization the binding-half decoupling (2026-06-12 core-resolver spec) started: that effort purified who binds what, where emission lands; this one purifies how content reaches instantiation roots.
  • Baseline: branch feat/entity-gen-schema-port @ b1619a47 (rebased on vic/den main fe63b4bf), CI 947/947, nix-config parity green.

1. Problem — the delivery half is legacy debt

Audit (2026-06-12, this branch): the delivery/collection half is 3141 LOC across 12 files (assemble-pipes 967, resolve 864, route/apply 383, route/wrap 193, spawn-node 171, wrap-classes 190, …) of which an estimated ~45% is genuinely load-bearing; ~30% is duplicated walks/folds; ~25% is exception clusters.

1.1 Nine mechanisms, one hidden concept

Mechanism (source → target, mode) implementation home
descendant class-fold (default) subtree scopes → root class bucket, merge (key-dedup) resolve.nix extractSubtreeModules; spawn-node.nix
provides provide spec scope → targetClass at path, nest+merge provide.nix → resolve.nix applyProvides
simple route source class → intoClass at path, nest (nestPlain/nestWithAdaptArgs) route/apply.nix applySimpleRoute + route/wrap.nix
route verbatim (reinstantiate) source class → intoClass at path, nest-verbatim (keys intact) route/wrap.nix nestVerbatim
complex forward (HM bridge) forward source → intoClass, buildForwardAspect → merge route/apply.nix applyComplexRoute; forward.nix
host-aspects spawn projection parent record aspect → own-scope buckets, re-walk + merge resolve.nix spawn materializer + spawn-node.nix
instantiates host subtree → flake output path, re-walk → thunk instantiate.nix → resolve.nix applyInstantiates
pipe expose child scope → parent pipe key, bottom-up fold assemble-pipes.nix collectAllExposed
pipe collect/collectAll sibling/all scopes → current pipe value, fan-in assemble-pipes.nix

The first seven are all (scope-set S → target T at attrpath P, mode M). The last two are data flow, not delivery (§7).

1.2 Structural debt (audit inventory, abridged)

  • Write-only state: scopedConstraintFilters (constraint.nix:70, pipeline.nix:154), flatAspectPolicies (policy.nix:22, pipeline.nix:149) — zero readers.
  • Six scope-tree-walk implementations, four of which (isInSubtree ×4 + the duplicated extractSubtreeModules/collectFromSubtree fold) should be one isolation-aware subtreeScopes helper; ancestor-climbs and sibling-filters form a second shareable family.
  • Three phase-fold re-entries (top-level phases 1–3, mkInstantiateArgs per-host re-run, spawn-node inline block) that should be one assembleSubtree — they currently differ in which state they thread (raw vs augmented vs merged), which is itself a latent-bug surface.
  • Provenance-forked stage interpreter: processStagesWithCollect (assemble-pipes:302-450, ~150 LOC) mirrors applyStage with __pv/__ps tagging.
  • Exception clusters: findHostScopeId name-infix heuristic (resolve.nix:176-211); appendToParent marker that inverts the isolation-skip just applied (route/apply:170-247); filterRootModules den.default carve-out (route/apply:16-30); getCollectedSource/resolveSourceFallback dual-path; route triple-pass (dedupRoutes/findChildScopeKeys/topoSortRoutes); disambiguated @system collision repair (resolve.nix:422-463); mergedSpawnRoutes re-dedup (spawn-node:128-140); nest-strategy triple (nestPlain/nestWithAdaptArgs/nestVerbatim).
  • Markers: isolated consumed as a mid-walk filter; reinstantiate as a per-route annotation the author must know to set.

2. The formal rule (the arbiter)

A delivery edge is a 4-tuple (S, T, P, M):

  • S — source, one of three kinds:
    • collected(scope, class) — the class bucket of a scope-subtree, isolation-filtered;
    • rewalk(aspect, bindings, class) — content produced by re-resolving an aspect under bindings (the spawn-projection case);
    • synthesize(forwardSpec, sourceModule) — content constructed by a forward adapter (buildForwardAspect: the complex-forward/HM-bridge case builds a NEW aspect from the source module; it is neither a plain collect nor a re-resolution of an existing aspect).
  • T — target: an instantiation root (entity-root scope, class), or a flake-output path (instantiates). The flake-output arm carries its own edge-construction rules — notably @system disambiguation for colliding output names — which are T-arm-local, never general materializer steps.
  • P — attrpath inside T's config; [] = merge at root.
  • M — mode, exhaustive: merge (key-deduped module-list union) | nest (evaluate-and-place at P) | nest-verbatim (place keyed module wrappers at P by reference, because T re-instantiates). Mechanisms that look like hybrids decompose rather than extend M: a provides edge is M = nest whose materialized module then joins the target root's bucket through the default fold edge (nest ∘ merge as edge composition, matching today's setAttrByPath-then-append implementation) — M stays a closed enum.

The rule: the final configuration of any root (R, C) is exactly the materialization, in dependency order, of all edges targeting it. Nothing else moves content.

Context projection Π(root) (first-class, not an afterthought): edges are materialized against a projected context slice, not the global state:

assembleSubtree(root) = materialize( edges(root) ) over Π(root)

Π(root) is the root-determined context projection: the subtree-plus-ancestor-filtered, class-resolved slice of scopeContexts (today's three phase-fold re-entries each build a variant of it by hand — top-level uses the full augmented contexts; mkInstantiateArgs filters to subtree+ancestors and injects the resolved class; spawn-node merges parent state with spawn state). The toposort of corollary 5 orders edges within a fixed Π; Π itself is a function of the root, never of edge dependencies. Unifying the three re-entries means making Π explicit, not just re-sorting edges — the census in §8 verifies which inter-variant differences are deliberate before Π is formalized.

Corollaries:

  1. Default fold edges exist by construction: every entity scope's subtree contributes collected(subtree, C) → (own root, C), P=[], M=merge. Per-user nixos landing on the host is this edge.
  2. isolated is edge-absence, not a filter: an isolated kind's scope has no default fold edge into its parent's root — it is a root. No walker consults an isolation flag mid-walk; the edge set encodes it.
  3. reinstantiate is M = nest-verbatim — an edge property, not route plumbing.
  4. Routes, provides, host-aspects projection, instantiates are edge constructors, differing only in which (S, T, P, M) they declare.
  5. Ordering is toposort over edge dependencies (a route's source is assembled before it nests) — the definition of phase ordering, replacing the hardcoded 1→2→3→4 sequence as an emergent property of code layout.

The rule plays the role spec §2 (core-resolver decoupling) played for binding: every test flip, exception cluster, trace deviation, and API question is classified against it — not against incumbent code.

Outside the rule: pipes/quirks (§7) — data flow between scopes during resolution, not config delivery to roots. Forcing them into the algebra would be false unification.

3. Components

3a. Edge-trace extractor (Phase 0, read-only)

A pure function over the pipeline's existing end-state (scopeContexts, scopeParent, scopeIsolated, scopedClassImports, scopedRoutes, scopedProvides, scopedSpawns, scopedInstantiates) rendering the delivery decisions the current code makes as a normalized edge list [(S, T, P, M)], stably sorted and hashable. Surfaced as a debug output (fxResolveFull gains edgeTrace) plus a denTest snapshot helper. Built before any cut; it is the migration gate for every Phase-2 port.

Scoped approximate-then-converge. The current code never materializes edge objects, and several decisions are path-dependent (route dedup/suppression across the triple-pass, findHostScopeId root selection, @system requalification) — independently re-deriving those from raw state would mean re-implementing the very logic Phase 2 deletes. So the extractor's v0 captures the clean edges exactly (default folds, simple routes, provides, instantiate targets) and records the path-dependent decisions as annotations (e.g. suppressedBy = <child route key>); annotation fidelity converges with the Phase-2 constructor port by port. The extractor and the constructors converge on one edge definition — the extractor is the constructor's spec for the clean subset and its falsifier for the rest.

3b. Phase-1 mechanical cuts (rule-independent, behavior-identical)

  1. Delete write-only state (scopedConstraintFilters, flatAspectPolicies) + handler appends.
  2. One scopeWalk.subtree helper (isolation-aware; explicit blind variant) replacing the four isInSubtree closures and the duplicated subtree-fold; shared dedupByKey.
  3. Unify the provenance-forked stage interpreter via one stage-lift over wrapped values.

3c. Edge materializer (Phase 2, the core)

Two stages replacing phases 1–4's per-mechanism implementations:

  • Edge collection: each mechanism's recorded state becomes an edge constructor — default folds from the scope tree (minus isolated subtrees), routes, provides, spawn projections, instantiates.
  • Materialization: toposort; one generic walker collects each collected(…) source via scopeWalk.subtree; one nesting function with the mode switch (collapsing the nestPlain/nestWithAdaptArgs/nestVerbatim triple — adaptArgs becomes an edge property like verbatim). The three phase-fold re-entries collapse into one assembleSubtree = "materialize all edges targeting this root."

3d. Exception-cluster dispositions (classified against §2 during each port)

  • findHostScopeId name-infix heuristic → dissolve: record the spec→scope link at scope-creation time (resolve.to creates the scope; carry its id) instead of reconstructing by name matching.
  • appendToParent + isolation-skip inversion → dissolve: it is an edge whose T is the parent root; declare it as such.
  • filterRootModules carve-out, getCollectedSource dual-path, route triple-pass, disambiguated @system repair, mergedSpawnRoutes re-dedup → each becomes a documented edge-construction rule or dies. None survives as an inline special case in the materializer.

4. API resurface (Phase 3)

  • deliver (working name): the user-facing edge constructor — deliver { from; to; at; mode ? "merge"; }.
  • route and provides are retained as shims over deliver — they may live on permanently as user-API sugar. Each shim carries # TODO: add deprecation warning before any future removal. Nothing user-facing is removed in this sweep. The guests policy (nix-config) migrates to declaring its edge explicitly (mode = "verbatim").
  • reinstantiate flag dies at the surface: mode is explicit on the edge. (Derivation — "delivering an isolated entity's own class ⇒ verbatim" — was considered and rejected as magic; revisit only if a real consumer is burned by explicitness.)
  • appendToParent dies entirely (internal-only; it is to = parent).
  • Templates/nix-config consumers keep working through the shims; migrations are mechanical.

5. Verification

5.1 Deviation-classification protocol (amended from byte-identity)

Byte-identical traces are the expected common case per port, not the requirement — a hard identity gate would enshrine bugs the way pre-decoupling tests enshrined #609. The gate is no unexplained deviation: every trace diff is classified against §2 —

  • bug-in-old → adopt new behavior, record verdict, flip/fix affected tests;
  • bug-in-new → fix the port;
  • intentional → rule-sanctioned improvement (e.g. an absorbed exception cluster), recorded. One-line rule-grounded verdict per delta, accumulated in the implementation plan (same discipline as the binding work's §6).

5.2 Gates per port

  • Edge-trace diff (classified per §5.1) against committed fixtures for representative topologies: host+users, fleet (environment ancestor), microvm guest, standalone home (incl. #605 synthetic host), home-extraction, multi-system same-name (exercises the @system disambiguation arm), a darwin host (different class set for default-fold construction), and fleet pipe value flowing through a delivery edge (pipe-collect feeding a route/provides — the spawn subtree-restriction's reason for existing).
  • Full den CI green.
  • Standing nix-config parity: cortex toplevel nix-diff (flake-source-only delta), agenix identityPaths /persist/etc/ssh, cortex-cuda guest resolves + vfio-gate identical, darwin host builds.

5.3 New tests

delivery-edges suite pinning the rule directly — one test per §2 corollary: default-fold-edge existence, isolation-as-edge-absence, verbatim mode, toposort ordering, plus the trace-fixture snapshots.

6. Sequencing

Phase 0 (rule §2 = this doc + extractor + fixtures) → Phase 1 cuts (each its own CI-green commit) → Phase 2 ports in dependency order: default folds → routes → provides → spawn projection → instantiates, dissolving exception clusters as encountered, deleting dead per-mechanism code as each port completes → Phase 3 API resurface + consumer migration (shims kept). Same branch lineage (feat/entity-gen-schema-port).

7. Out of scope

  • Pipes/quirks model. Stays as-is conceptually; only the Phase-1 stage-interpreter dedup touches assemble-pipes. Re-modeling pipe data flow is its own (unscheduled) effort.
  • HOAG/v2. Unaffected. If v2 happens, the edge trace doubles as the v1↔v2 parity oracle (edges diff against the HOAG graph directly), and §2 maps onto spawn/edge/neededBy vocabulary.
  • Binding-half changes. Done (core-resolver decoupling spec); untouched here.

8. Risks / open questions (each becomes an early plan task, not a mid-port discovery)

  • Π(root) census (the big one): the three phase-fold re-entries thread materially different state (full-augmented vs subtree-filtered+class-injected vs parent-merged with cross-source route dedup). Before Π is formalized, census which differences are deliberate semantics vs accident — same toggle-and-observe method as the carrier census. assembleSubtree is under-built without this.
  • adaptArgs interaction matrix: §3c asserts the nest triple collapses to one mode switch, but adaptArgs currently interacts with path, guard, and reinstantiate (route/wrap.nix dispatch). The plan must enumerate the adaptArgs × path × verbatim × guard matrix and show each cell maps to one edge form before writing the switch.
  • Toposort scope: the unified materializer's toposort must subsume topoSortRoutes' single-level sort; whether cross-mechanism cycles (provides→route→provides) are even reachable is a plan-phase question. Rule: edges form a DAG; a detected cycle is a loud config error.
  • Trace normalization stability: edge identity must be stable across eval orders (sort key: T, P, S, M with id_hash-based scope naming) or fixture diffs will be noisy.
  • rewalk/synthesize source determinism: these edges embed re-resolution/construction; the trace records the identity triple (aspect identity, bindings, class / forwardSpec identity), not resolved content — content-level verification stays with CI/parity gates.
  • assemble-pipes coupling: pipes read scope structure too; Phase-1's scopeWalk consolidation must not change pipe visibility semantics (fleet-pipe tests are the canary).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment