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.
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).
The ent/ module has seven files organized into three conceptual layers:
| File | Lines | Role |
|---|---|---|
ent.rs | 85 | Thin wrapper; owns a DagWood, delegates new_trace() |
dagwood.rs | 1,532 | The DAG itself: position allocation, ordering (is_le), navigation cache, TraceView |
branch.rs | 603 | Branch identity, topology (Root/Tree/Dag), binary-tree-of-branches, stub eviction |
trace.rs | 140 | TracePosition — the (branch, position) coordinate |
content.rs | 2,549 | AssertionPayload, AssertionStore, materialization, AlternativeSet |
htree.rs | 452 | History crumbs (H-crum) linking trace positions to the canopy |
backfollow.rs | 1,892 | Bridges the ent DAG to the Edition/O-tree model (in edition/) |
The three layers: versioning spine (blue/green), content model (purple), and bridge to the Edition world (amber/cyan).
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).
(branch_hash + position) * 10993 & 0x7FFFFFF.
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 },
}
The three branch topologies. A Dag branch has two parents and is the mechanism for merge points.
Each Branch also maintains:
last_position: u32 — the highest position allocated on this branch (starts at 2).left / right: Option<BranchId> — a binary tree of child branches (see Section 9).
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:
| Position | Meaning |
|---|---|
1 | The implicit entry position of every branch (returned by create_root/create_tree/create_dag) |
2 | Reserved — never assigned by next_position |
3 | First usable position on any branch — returned by new_position() |
4, 5, 6… | Successive positions allocated by next_position |
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)."
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.
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):
is_le(p, p) = trueis_le(a,b) && is_le(b,a) ⇒ a == bis_le(a,b) && is_le(b,c) ⇒ is_le(a,c)
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:
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:
For a given position (branch, pos):
branch is not yet visited (None in cache): store pos, then recurse to parents.max(old, pos), no recursion.Follows the branch topology:
Root → stop (the recursion ends here).Tree { parent } → cache_in_into(parent).Dag { parent1, parent2 } → recurse into both parents.// 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,
}
// 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!
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."
Two paths converge at W. The cache stores the maximum entry position (4), not the minimum.
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:
| Method | Returns | Semantics |
|---|---|---|
is_visible(pos) | bool | Same logic as is_le(pos, reference) |
visible_max(branch_id) | Option<u32> | Max position visible on that branch |
visible_branches() | Iterator | All reachable branches with their max positions |
branch_count() | usize | How many branches are reachable |
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.
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;
}
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.
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:
| Category | Payload | Effect |
|---|---|---|
| Nodes | CreateNode { 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 | |
| Spans | CreateSpan { span_id } | Create a text span |
SetSpanText { span_id, text } | Set the text content | |
DeleteSpan { span_id } | Tombstone the span | |
| Attachments | AttachSpanToNode { node_id, span_id, ordinal } | Place span in node at ordinal |
DetachSpanFromNode { node_id, span_id } | Remove span from node | |
| Annotations | CreateAnnotation { 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
}
TracePosition
determines when it happened and from which branches it is visible.
The payload determines what it does. These two concerns are cleanly separated.
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()
}
materialize_document turns a flat assertion log into a tree structure
(nodes containing spans and annotations). The process:
VisibleIndex — O(1) lookup HashMaps by entity ID.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)); }
}
// ...
}
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
}
Two branches set different text for the same span. After merge, both values are preserved as alternatives. The system never silently discards either.
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.
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:
hcut: TracePosition — where in the version history this crum livesbert_crum: Arc<CanopyCrumData> — the corresponding canopy crum (permissions/endorsement flags)o_parents: Vec<Arc<dyn HPart>> — parent crumbs in the H-tree
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);
}
}
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.
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:
dagwood.new_position())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.
The bridge is bidirectional. Editions are translated into assertions (forward); assertions are materialized back into documents (reverse).
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.
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:
| Test | Category | What It Verifies |
|---|---|---|
D7–D8 | Ordering axioms | Monotonic ordering, reflexivity |
D9, D20 | Ordering axioms | Antisymmetry, transitivity |
D10–D15 | Fork/merge | Ancestry through forks, merge points, DAG convergence |
D13, D22 | Max-over-paths | Cache stores max entry position across multiple paths |
D24–D25, D29 | Cache equivalence | Cached is_le == from-scratch recomputation for all pairs |
D26–D28 | Property tests | Reflexivity, antisymmetry, transitivity on 5 random DAGs |
V6 | Property test | TraceView.is_visible == DagWood.is_le for all pairs |
S7 | Successor invariant | Every successor S of P satisfies is_le(P, S) |
P1 (ignored) | Stress | 100K-position linear chain: O(B) traversal |
P2 (ignored) | Stress | 16K leaves + 14 merge levels: ~32K branches, cycle prevention |
P7 (ignored) | Stress | 20K alternating-reference comparisons (cache thrashing) |
P10 (ignored) | Marathon | 50 random DAGs × 500 ops: all-pairs all-triples property checks |
#[ignore] and run with
cargo test -- --ignored. They exercise performance and correctness at
scales that are slow for CI but important for confidence.
| File | Path | Key Types |
|---|---|---|
ent.rs | src/ent/ent.rs | Ent |
trace.rs | src/ent/trace.rs | TracePosition |
branch.rs | src/ent/branch.rs | BranchId, BranchKind, Branch, BranchStore, BranchStub, BranchState |
dagwood.rs | src/ent/dagwood.rs | DagWood, TraceView |
content.rs | src/ent/content.rs | AssertionPayload, Assertion, AssertionStore, AlternativeSet, MaterializedDocument/Node/Span/Annotation, VisibleIndex |
htree.rs | src/ent/htree.rs | HPart, HUpperCrumData, HistoryCrumBase |
backfollow.rs | src/edition/backfollow.rs | BackfollowEngine, EditionMeta |
wasm.rs | src/wasm.rs | WasmDagWood, WasmTracePosition, WasmTraceView, WasmAssertionStore |