- 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/providesretained 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 mainfe63b4bf), CI 947/947, nix-config parity green.
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).
- 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 duplicatedextractSubtreeModules/collectFromSubtreefold) should be one isolation-awaresubtreeScopeshelper; ancestor-climbs and sibling-filters form a second shareable family. - Three phase-fold re-entries (top-level phases 1–3,
mkInstantiateArgsper-host re-run, spawn-node inline block) that should be oneassembleSubtree— 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) mirrorsapplyStagewith__pv/__pstagging. - Exception clusters:
findHostScopeIdname-infix heuristic (resolve.nix:176-211);appendToParentmarker that inverts the isolation-skip just applied (route/apply:170-247);filterRootModulesden.default carve-out (route/apply:16-30);getCollectedSource/resolveSourceFallbackdual-path; route triple-pass (dedupRoutes/findChildScopeKeys/topoSortRoutes);disambiguated@system collision repair (resolve.nix:422-463);mergedSpawnRoutesre-dedup (spawn-node:128-140); nest-strategy triple (nestPlain/nestWithAdaptArgs/nestVerbatim). - Markers:
isolatedconsumed as a mid-walk filter;reinstantiateas a per-route annotation the author must know to set.
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@systemdisambiguation 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: aprovidesedge isM = nestwhose materialized module then joins the target root's bucket through the default fold edge (nest ∘ mergeas 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:
- 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. isolatedis 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.reinstantiateisM = nest-verbatim— an edge property, not route plumbing.- Routes, provides, host-aspects projection, instantiates are edge constructors, differing only in which
(S, T, P, M)they declare. - 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.
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.
- Delete write-only state (
scopedConstraintFilters,flatAspectPolicies) + handler appends. - One
scopeWalk.subtreehelper (isolation-aware; explicit blind variant) replacing the fourisInSubtreeclosures and the duplicated subtree-fold; shareddedupByKey. - Unify the provenance-forked stage interpreter via one stage-lift over wrapped values.
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 viascopeWalk.subtree; one nesting function with the mode switch (collapsing thenestPlain/nestWithAdaptArgs/nestVerbatimtriple —adaptArgsbecomes an edge property like verbatim). The three phase-fold re-entries collapse into oneassembleSubtree= "materialize all edges targeting this root."
findHostScopeIdname-infix heuristic → dissolve: record the spec→scope link at scope-creation time (resolve.tocreates 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.filterRootModulescarve-out,getCollectedSourcedual-path, route triple-pass,disambiguated@system repair,mergedSpawnRoutesre-dedup → each becomes a documented edge-construction rule or dies. None survives as an inline special case in the materializer.
deliver(working name): the user-facing edge constructor —deliver { from; to; at; mode ? "merge"; }.routeandprovidesare retained as shims overdeliver— 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").reinstantiateflag 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.)appendToParentdies entirely (internal-only; it isto = parent).- Templates/nix-config consumers keep working through the shims; migrations are mechanical.
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).
- 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
@systemdisambiguation 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.
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.
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).
- 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/
neededByvocabulary. - Binding-half changes. Done (core-resolver decoupling spec); untouched here.
- Π(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.
assembleSubtreeis under-built without this. adaptArgsinteraction matrix: §3c asserts the nest triple collapses to one mode switch, butadaptArgscurrently interacts withpath,guard, andreinstantiate(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/synthesizesource 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
scopeWalkconsolidation must not change pipe visibility semantics (fleet-pipe tests are the canary).