The Ent, DagWood & Trace DAG

The versioning spine adapted from the original Udanax-Gold C++ code: partial-order positions, branch topologies, the navigation cache, assertion-based content, and conflict-preserving materialization.

1. Heritage: From Udanax-Gold to Rust 2. The Three Layers 3. TracePosition: The Coordinate 4. BranchKind: Root, Tree, Dag 5. Position Numbering: Why Position 3? 6. The Partial Order: is_le and Visibility 7. The Navigation Cache: Max-Over-Paths 8. TraceView: The Read Face 9. The Branch Binary Tree: install_branch 10. AssertionPayload: The 13 Operations 11. AssertionStore and Visibility Filtering 12. Materialization: History Determines Meaning 13. AlternativeSet: Conflict Preservation 14. The H-Tree: Linking Trace to Canopy 15. BackfollowEngine: Bridging Ent to Editions 16. WASM Bindings: DagWood in the Browser 17. Properties Proven by the Test Suite 18. Source Code Map

1. Heritage: From Udanax-Gold to Rust

The ent/ module is one of the most direct lineages from the original Udanax-Gold C++ codebase. The original source files—entx.hxx, dagwoodx.hxx, tracepx.hxx, branchx.hxx—defined a partial-ordering data structure called a trace DAG that tracked the history of every edit, fork, and merge in a Xanadu document.

Xudanu preserves this architecture faithfully. Every struct and method carries [Original] and [Adapted] comments citing the exact C++ source line numbers. The three C++ class hierarchies—Ent, DagWood, and BranchDescription—become Rust structs with the same names (modulo CamelCase-to-snake_case conventions).

Why "DagWood"? The name is a triple pun from the original Xanadu team: (1) it is a DAG (directed acyclic graph) made of wood (a tree-like material), (2) it references the comic strip character Dagwood Bumstead, and (3) "DagWood" as one word was the C++ class name. Xudanu keeps the spelling intact.

2. The Three Layers

The ent/ module has seven files organized into three conceptual layers:

FileLinesRole
ent.rs85Thin wrapper; owns a DagWood, delegates new_trace()
dagwood.rs1,532The DAG itself: position allocation, ordering (is_le), navigation cache, TraceView
branch.rs603Branch identity, topology (Root/Tree/Dag), binary-tree-of-branches, stub eviction
trace.rs140TracePosition — the (branch, position) coordinate
content.rs2,549AssertionPayload, AssertionStore, materialization, AlternativeSet
htree.rs452History crumbs (H-crum) linking trace positions to the canopy
backfollow.rs1,892Bridges the ent DAG to the Edition/O-tree model (in edition/)
VERSIONING SPINE CONTENT MODEL BRIDGE Ent owns, delegates DagWood positions, is_le, cache BranchStore Root / Tree / Dag branches TracePosition (branch, pos) AssertionStore 13 payload types Materialize AlternativeSet TraceView frozen visibility filters by visibility H-Tree (HUpperCrumData) trace × canopy junction BackfollowEngine bridges to Edition / O-tree Edition / O-Tree the document model bridge_edition_to_assertions

The three layers: versioning spine (blue/green), content model (purple), and bridge to the Edition world (amber/cyan).

3. TracePosition: The Coordinate

Every point in the version history is identified by a TracePosition: a pair of a BranchId and a u32 position within that branch.

pub struct TracePosition {
    branch: BranchId,    // which branch of the DAG
    position: u32,       // how far along the branch
}

This replaces the original C++ BoundedTrace class hierarchy. The original had an abstract TracePosition with one concrete subclass; the Rust version collapses to a single struct since "only one concrete subclass existed in the original" (tracepx.hxx lines 57–204).

Equality: Two positions are equal iff they have the same branch AND the same position number. The hash formula uses the same arbitrary primes as the original: (branch_hash + position) * 10993 & 0x7FFFFFF.

4. BranchKind: Root, Tree, Dag

The original C++ had three subclasses of BranchDescription: RootBranch (no parents), TreeBranch (one parent), and DagBranch (two parents). In Rust, these become a single enum:

pub enum BranchKind {
    Root,
    Tree { parent: TracePosition },
    Dag  { parent1: TracePosition, parent2: TracePosition },
}
Root no parents 1 3 4 5 entry point Tree one parent parent fork/extend Dag two parents (merge) p1 p2 merge point

The three branch topologies. A Dag branch has two parents and is the mechanism for merge points.

Each Branch also maintains:

5. Position Numbering: Why Position 3?

One of the most subtle aspects of the DagWood is its position numbering. Every new DagWood starts with a root branch, then immediately creates a trunk branch:

// dagwood.rs constructor
let (root_branch_id, root_pos) = branches.create_root();  // root at position 1
let _ = dagwood.create_after(root_branch_id, root);       // trunk branch created

The numbering convention is:

PositionMeaning
1The implicit entry position of every branch (returned by create_root/create_tree/create_dag)
2Reserved — never assigned by next_position
3First usable position on any branch — returned by new_position()
4, 5, 6…Successive positions allocated by next_position
Why is position 2 skipped? The original comment at branchx.cxx line 241 says lastPosition "starts at 2; position 1 is the implicit entry position." Then nextPosition increments before returning, so the first call returns 3. Position 2 is the gap between the entry marker and the first real content—a reserved sentinel. Test T4 explicitly documents this: "Position 2 is reserved (never assigned by nextPosition)."

6. The Partial Order: is_le and Visibility

The DagWood defines a partial ordering of TracePositions. is_le(a, b) returns true if position a is "less than or equal to" position b—meaning a is an ancestor of (or identical to) b in the DAG.

This is the fundamental semantic operation. It answers: "Does the history leading to b include everything at a?" If yes, then content written at a is visible from b.

root pos 1 pos 3 A branch-a, pos 3 B branch-b, pos 3 A2 (pos 4) M (merge) dag-branch, pos 3 is_le(root, M) = true is_le(A, M) = true is_le(B, M) = true is_le(A, B) = false A and B are incomparable

A fork-and-merge DAG. Positions A and B are incomparable (neither is_le the other). Merge point M has both as ancestors.

The partial order satisfies three properties that the test suite verifies exhaustively on random DAGs (tests D26–D29):

7. The Navigation Cache: Max-Over-Paths

The naive way to compute is_le(a, b) is to walk upward from b through parent links and check if you reach a's branch. But this is O(branches) per query, and repeated queries re-traverse the same paths.

The original Udanax-Gold code solved this with a navigation cache that stores, for each branch reachable from the reference position, the maximum position visible on that branch. The key insight is the max-over-paths invariant:

The Invariant: If position b is reachable from a via multiple DAG paths that enter a's branch at different positions, the cache stores the maximum of those entry positions. This is because a later entry point on a branch subsumes an earlier one: if you can see position 5, you can certainly see position 3 on the same branch.

The algorithm has three pieces, mirroring the original cacheIn / cacheRecur methods:

cache_in_into(pos)

For a given position (branch, pos):

cache_recur_into(branch_id)

Follows the branch topology:

is_le(a, b)

// Step 1: cache b's upward reachability
self.cache_trace_pos(b);

// Step 2: check if a's branch is reachable with a.position <= cached max
match self.nav_cache.get(&a.branch()) {
    Some(&mark) => a.position() <= mark,
    None => false,
}
Worked Example: DAG Convergence
// Structure:
//   trunk: root → pos3 → pos4 → pos5
//   fork from pos3 → Y (pos 3)
//   fork from pos4 → Z (pos 3)
//   merge(Y, Z) → W (pos 3)

// Path W → Z → trunk enters at position 4
// Path W → Y → trunk enters at position 3
// Cache[trunk] = max(3, 4) = 4

// Therefore:
is_le(trunk_pos3, W) = true   // 3 <= 4
is_le(trunk_pos4, W) = true   // 4 <= 4
is_le(trunk_pos5, W) = false  // 5 > 4  ← not visible!
Cache Reuse: The cache is keyed by the reference position. If you call is_le many times against the same reference b, the cache is reused without rebuilding (test D16). Changing the reference invalidates and rebuilds (test D17). The original comment: "many comparisons IN THE SAME ORDER will occur very fast."
trunk branch 3 4 5 Y enters trunk at 3 Z enters trunk at 4 W (merge) cache[trunk] = max(3, 4) = 4

Two paths converge at W. The cache stores the maximum entry position (4), not the minimum.

8. TraceView: The Read Face

TraceView is a frozen snapshot of visibility from a single reference position. It owns its own navigation cache, independent of DagWood's mutable internal cache:

pub struct TraceView {
    reference: TracePosition,
    nav_cache: HashMap<BranchId, u32>,
}

This is important for concurrent reads: multiple TraceViews can exist simultaneously without interfering with each other or with DagWood's internal cache used by is_le. Test V2 verifies this independence explicitly.

The TraceView API:

MethodReturnsSemantics
is_visible(pos)boolSame logic as is_le(pos, reference)
visible_max(branch_id)Option<u32>Max position visible on that branch
visible_branches()IteratorAll reachable branches with their max positions
branch_count()usizeHow many branches are reachable
TraceView vs is_le: For any pair of positions, view.is_visible(pos) must equal dagwood.is_le(pos, reference). Test V6 verifies this as a property test on 5 random DAGs with 25 operations each, checking all position pairs.

9. The Branch Binary Tree: install_branch

When a new branch is forked off from an existing position, it must be registered as a child of that position's branch. The original Udanax-Gold code used a peculiar insertion algorithm that builds a binary tree of branches with a swap-after-recurse pattern:

pub fn install_branch(&mut self, parent_id: BranchId, child_id: BranchId) {
    if parent_id == child_id { return; }       // identity guard

    // If left slot is empty, install there
    if parent.left.is_none() {
        parent.left = Some(child_id);
        return;
    }

    // Left is occupied: recurse into left child, THEN swap left/right
    let left_child = parent.left.unwrap();
    self.install_branch(left_child, child_id)?;

    // The swap (not a standard rotation!)
    let tmp = parent.left;
    parent.left = parent.right;
    parent.right = tmp;
}
Why swap? The original comment at branchx.cxx lines 183–185 is terse: tmpBr = myLeft; myLeft = myRight; myRight = tmpBr;. The effect is to distribute growth across both subtrees by alternating which side the recursive insertion targets. This is not a standard tree rotation—it is a peculiar Xanaux-Gold balancing heuristic. Tests T5–T8 verify the observed behavior, and the 100K-child stress test (P6) confirms all children remain reachable.

This binary tree of branches is stored in the trunk map, which maps TracePosition → BranchId. When new_position() is called, it creates a new branch as a child of the root and registers it in the trunk.

10. AssertionPayload: The 13 Operations

On top of the versioning spine sits the content model. Content is expressed as a log of assertions—immutable, append-only operations stamped with a TracePosition. There are 13 payload types:

CategoryPayloadEffect
NodesCreateNode { node_id, kind }Create a tree node (document, paragraph, etc.)
AttachChild { parent_id, child_id, ordinal }Attach child under parent at ordinal
DetachChild { parent_id, child_id }Remove child from parent
DeleteNode { node_id }Tombstone the node
SpansCreateSpan { span_id }Create a text span
SetSpanText { span_id, text }Set the text content
DeleteSpan { span_id }Tombstone the span
AttachmentsAttachSpanToNode { node_id, span_id, ordinal }Place span in node at ordinal
DetachSpanFromNode { node_id, span_id }Remove span from node
AnnotationsCreateAnnotation { annotation_id, kind, payload }Create annotation (bold, italic, comment...)
AttachAnnotationToNode { annotation_id, node_id }Attach annotation to a node
AttachAnnotationToSpan { annotation_id, span_id }Attach annotation to a span
DeleteAnnotation { annotation_id }Tombstone the annotation

Each assertion is stored with its TracePosition:

pub struct Assertion {
    pub id: AssertionId,
    pub position: TracePosition,   // WHERE in the version DAG
    pub payload: AssertionPayload, // WHAT was done
}
The key design principle: An assertion's TracePosition determines when it happened and from which branches it is visible. The payload determines what it does. These two concerns are cleanly separated.

11. AssertionStore and Visibility Filtering

The AssertionStore is a simple append-only log:

pub struct AssertionStore {
    assertions: Vec<Assertion>,
    next_id: u64,
}

The sole filtering mechanism is visibility. Given a TraceView, the store returns only assertions whose position is visible from the view's reference:

pub fn visible_assertions<'a>(&'a self, view: &TraceView) -> Vec<&'a Assertion> {
    self.assertions
        .iter()
        .filter(|a| view.is_visible(a.position))
        .collect()
}
No other rules apply. The comment in the source is emphatic: "This is the SOLE filtering mechanism." There are no soft deletes, no hidden flags, no server-side curation. If an assertion's position is visible, it participates in materialization. If not, it doesn't.

12. Materialization: History Determines Meaning

materialize_document turns a flat assertion log into a tree structure (nodes containing spans and annotations). The process:

  1. Filter assertions by TraceView visibility.
  2. Build a VisibleIndex — O(1) lookup HashMaps by entity ID.
  3. Recursively materialize the root node, its children, their spans, and annotations.

For single-valued properties (like span text or child ordinals), when multiple assertions exist on the same branch, the one at the highest position wins (last-writer-wins within a branch). This is how sequential edits work: position 4 overrides position 3 on the same branch.

// Within a single branch, latest position wins
fn collect_span_text_indexed(idx: &VisibleIndex, span_id: SpanId) -> AlternativeSet<String> {
    let mut per_branch: HashMap<BranchId, (u32, String)> = HashMap::new();

    for assertion in idx.set_span_text.get(&span_id) {
        let branch = assertion.position.branch();
        let pos = assertion.position.position();
        // Latest position on each branch wins
        if pos > existing_pos { per_branch.insert(branch, (pos, text)); }
    }
    // ...
}

13. AlternativeSet: Conflict Preservation

This is where the DagWood's conflict-preservation philosophy becomes visible. When two branches diverge and then merge, both versions of a span's text are visible. The materializer does not pick one. Instead, it returns an AlternativeSet:

pub enum AlternativeSet<T> {
    Single(T),              // no conflict: one value
    Alternatives(Vec<T>),   // conflict: all visible divergent values
}
root CreateSpan(1) Branch A SetSpanText(1, "Hello!") Branch B SetSpanText(1, "Hello world") Merge M dag-branch (parents: A, B) materialize_span(span=1, view=TraceView(M)) → AlternativeSet::Alternatives(["Hello!", "Hello world"])

Two branches set different text for the same span. After merge, both values are preserved as alternatives. The system never silently discards either.

DO NOT silently resolve. The source code states this as a design axiom (line 219): "For single-valued properties where multiple branches disagree. DO NOT silently resolve — preserve all visible alternatives."

This is the philosophical heart of the system. Unlike Git (which requires manual conflict resolution) or operational transforms (which merge automatically but may lose intent), the DagWood preserves all divergent states and lets the consumer decide what to present. The frontend can show alternatives side-by-side, offer a choice, or apply a merge strategy—but the data layer never throws anything away.

14. The H-Tree: Linking Trace to Canopy

The H-Tree (history tree) connects the trace DAG to the canopy — the hierarchical crum structure used for filtered queries (see Transclusion Engine for canopy details).

Each HUpperCrumData node sits at a TracePosition and holds:

The H-tree enables delayed_store_backfollow — a filtered backward traversal through version history. Starting from a crum, it walks parent links, applying a PropFinder filter at each level. Only crumbs whose permission flags pass the filter are visited:

pub fn delayed_store_backfollow(
    crum: &Arc<Mutex<HUpperCrumData>>,
    finder: &PropFinder,
    hcrum_cache: &mut HashSet<u32>,   // cycle prevention
    visitor: &mut dyn FnMut(&Arc<Mutex<HUpperCrumData>>),
) {
    // Skip if already visited (by hash)
    if hcrum_cache.contains(&hash) { return; }
    hcrum_cache.insert(hash);

    // Narrow the filter by this crum's flags
    let new_finder = finder.pass(crum_flags);
    if new_finder.is_empty() { return; } // filter closed: nothing more matches

    visitor(crum);  // visit this crum

    // Recurse into parents
    for parent in &o_parents {
        Self::delayed_store_backfollow(parent, &new_finder, cache, visitor);
    }
}
The HPart trait: HUpperCrumData implements the HPart trait, which requires h_crum() -> Option<Arc<...>>. This allows any node in the tree to optionally expose its H-crum, enabling the BackfollowEngine's EditionMeta to participate in the tree as a polymorphic parent.

15. BackfollowEngine: Bridging Ent to Editions

The BackfollowEngine (in edition/backfollow.rs) is the bridge between the ent DAG world and the Edition/O-tree world. It owns:

pub struct BackfollowEngine {
    transclusion_index: TransclusionIndex,
    bert_canopy: BertCanopy,
    sensor_canopy: SensorCanopy,
    dagwood: DagWood,                    // ← the ent DAG
    assertion_store: AssertionStore,     // ← the content model
    edition_metas: HashMap<u64, EditionMeta>,
    fingerprint_to_works: HashMap<[u8;32], HashSet<u64>>,
    work_spans: HashMap<u64, Vec<SpanId>>,
    // ...
}

When a work is registered, the engine:

  1. Allocates a TracePosition in the DagWood (dagwood.new_position())
  2. Creates an H-crum at that position with the work's permission flags
  3. Calls bridge_edition_to_assertions to translate the Edition's O-tree content into assertions

The bridge translates each O-tree entry into CreateSpan + SetSpanText + AttachSpanToNode assertions:

fn bridge_edition_to_assertions(&mut self, work_id: u64, edition: &Edition, tp: TracePosition) {
    let node_id = DocumentId::new(work_id).node_id();

    // First time: create the document node
    if first_time {
        self.assertion_store.add(tp, AssertionPayload::CreateNode {
            node_id, kind: "document".into(),
        });
    }

    // For each O-tree entry, create/update a span
    for (i, (_, carrier)) in edition.all_entries().iter().enumerate() {
        let span_id = spans[i];
        let text = carrier.element.as_text().unwrap_or("");
        self.assertion_store.add(tp, AssertionPayload::SetSpanText { span_id, text });
    }
}

This means the Edition model (per-line attribution, O-tree positions) and the ent model (assertions, visibility, alternatives) are two views of the same data. The Edition is the "fast path" for current-state access; the ent DAG is the "history path" for version-aware queries and cross-work materialization.

Edition (O-tree) RangeElement entries SpanProvenance bridge_edition_to_assertions for each entry → CreateSpan + SetSpanText + AttachSpanToNode AssertionStore stamped with TracePosition visible via TraceView materialize_document(store, view, doc_id) → MaterializedDocument Frontend / WASM consumer

The bridge is bidirectional. Editions are translated into assertions (forward); assertions are materialized back into documents (reverse).

16. WASM Bindings: DagWood in the Browser

The wasm.rs module exposes the DagWood, TraceView, and AssertionStore to JavaScript via wasm-bindgen. The wrappers are thin:

// JavaScript-callable API
let dw = WasmDagWood.new();
let pos1 = dw.new_position();
let pos2 = dw.new_position_after(pos1);
let merged = dw.new_successor_after(pos1, anotherPos);

let view = dw.trace_view(merged);
console.log(view.is_visible(pos1));   // true

let store = WasmAssertionStore.new();
store.add(pos1, JSON.stringify({
    CreateNode: { node_id: 1, kind: "document" }
}));

let doc = store.materialize_document_json(view, 1);
// → JSON tree of MaterializedNode → MaterializedSpan → MaterializedAnnotation

The assertion payload is passed as a JSON string and parsed into AssertionPayload internally. This means the full assertion model is available in the browser without recompiling—the same Rust code runs in the server and in WASM.

17. Properties Proven by the Test Suite

The ent/ module has one of the most rigorous test suites in the codebase. Beyond unit tests, it includes property-based tests on randomly generated DAGs and stress tests at scale:

TestCategoryWhat It Verifies
D7–D8Ordering axiomsMonotonic ordering, reflexivity
D9, D20Ordering axiomsAntisymmetry, transitivity
D10–D15Fork/mergeAncestry through forks, merge points, DAG convergence
D13, D22Max-over-pathsCache stores max entry position across multiple paths
D24–D25, D29Cache equivalenceCached is_le == from-scratch recomputation for all pairs
D26–D28Property testsReflexivity, antisymmetry, transitivity on 5 random DAGs
V6Property testTraceView.is_visible == DagWood.is_le for all pairs
S7Successor invariantEvery successor S of P satisfies is_le(P, S)
P1 (ignored)Stress100K-position linear chain: O(B) traversal
P2 (ignored)Stress16K leaves + 14 merge levels: ~32K branches, cycle prevention
P7 (ignored)Stress20K alternating-reference comparisons (cache thrashing)
P10 (ignored)Marathon50 random DAGs × 500 ops: all-pairs all-triples property checks
Stress tests are #[ignore] and run with cargo test -- --ignored. They exercise performance and correctness at scales that are slow for CI but important for confidence.

18. Source Code Map

FilePathKey Types
ent.rssrc/ent/ent.rsEnt
trace.rssrc/ent/trace.rsTracePosition
branch.rssrc/ent/branch.rsBranchId, BranchKind, Branch, BranchStore, BranchStub, BranchState
dagwood.rssrc/ent/dagwood.rsDagWood, TraceView
content.rssrc/ent/content.rsAssertionPayload, Assertion, AssertionStore, AlternativeSet, MaterializedDocument/Node/Span/Annotation, VisibleIndex
htree.rssrc/ent/htree.rsHPart, HUpperCrumData, HistoryCrumBase
backfollow.rssrc/edition/backfollow.rsBackfollowEngine, EditionMeta
wasm.rssrc/wasm.rsWasmDagWood, WasmTracePosition, WasmTraceView, WasmAssertionStore
Ent wrapper
Versioning spine
Content model
H-Tree
Bridge