# Concepts

---

# Objects and identity

_Universal Directory / Concepts_

> How UD models directory objects — GUID identity, the inode-shaped flat store, derived DNs and native paths, caseless type-blind sibling uniqueness, and the rules for rename, move, and delete.

Every entry in the directory — an OU, a user, a schema definition — is an **object**. UD's object model rests on one distinction it never blurs: **identity versus address**.

## Identity: the GUID

An object's identity is a **GUID**, assigned at creation and immutable for the object's entire life.

Everything that needs to point at an object stores that GUID, never a name or a path: a group's `member` values, a facet's attribute list, a class's composition. Renames and moves therefore never break references. Deleting an object leaves any remaining references visibly **dangling** at the dead GUID, rather than silently rebinding to whatever later takes the name.

## Address: derived, never stored

Internally the store is *inode-shaped*: a flat table of objects keyed by GUID, where each object records only its **parent's GUID** and its own **bare name** (`Sales`, `Jack`). The tree is an index over that table.

Both address forms are computed on demand:

- the **distinguished name** — leaf-first with typed components, `CN=Jack,OU=Sales`, where each prefix comes from the object's class (`dnPrefix`) and values are escaped per RFC 4514;
- the **native path** — root-first and untyped, `/Sales/Jack`, used by the console, the [JSON API](/universal-directory/reference/json-api.md)'s path resolution, and anywhere a human types an address. Path lookup is case-insensitive.

Because addresses are derived, renaming an OU is a single-object change. Every descendant's DN and path change *logically*, with zero writes, and nothing that references those descendants notices at all.

The tree has a single **root object** — empty name, class `domainRoot` — which never appears in DNs and cannot be renamed, moved, or deleted.

## Names and sibling uniqueness

Within one parent, names are unique under two deliberately strict rules.

**Caseless, case-preserving.** Names compare under Unicode canonical caseless matching, so `Jack`, `jack`, and a decomposed `Jack` are all the same name. The spelling you create is the spelling you see; the folding governs comparison and conflict only. See [naming rules](/universal-directory/reference/naming-rules.md) for the exact function.

**Type-blind.** Uniqueness ignores class entirely. An OU named `Sales` and a group named `sales` cannot be siblings. One container, one meaning per name.

Name validity is strict too — non-empty, no `/`, no control characters, no leading or trailing whitespace. Strict rules can be safely loosened later; a loose rule can never be tightened without breaking existing data.

## Rename and move

Rename and reparent are one operation, in the shape of LDAP's ModifyDN: change the name, the parent, or both, atomically.

The engine refuses:

- moves that would create a **cycle**, placing an object under itself or its own descendant;
- a destination where the caseless, type-blind name is already taken;
- any rename or move of the root, of **system objects**, or of [schema objects](/universal-directory/concepts/modifying-the-schema.md) — definitions are add and delete only — and any move *into* the schema subtree.

A case-only rename, `jack` → `Jack`, is always legal. It is the same caseless name, so it conflicts with nothing.

## Delete: the three-stage lifecycle

Deletion is **soft and subtree-wide**. Deleting an object tombstones it *and every descendant* in place, each with its own stamped existence transition.

A tombstone leaves the effective tree immediately — its name is free at once — but the full record survives, and every tombstoned object is individually **restorable**. Restoring is top-down: the parent must be alive and the name must still be free. It is an ordinary write, and a restore into a conflict simply refuses.

Beyond `Deleted` sit two harder states.

**Shredded** is security erasure, reachable directly from alive or as an escalation of a soft delete. The payload and even the *name* are destroyed; only a skeleton — GUID, parent GUID, stamps — remains. Every reference to the shredded GUID is physically **scrubbed out of every holder**, because erasure that leaves the object's GUID lingering in group member lists would fail its own purpose. The destruction [reaches the node's files](/universal-directory/concepts/persistence.md) within a guaranteed window. There is no way back.

**Purged** means the tombstone is gone entirely. This is never manual: purge happens automatically once a tombstone's age passes `maxTimeApart`.

### Aging is computed, not written

Two domain-wide knobs, stored as attributes on `/Configuration` with defaults of 30 and 180 days, drive the automatic transitions. Past **`timeUntilShred`** a soft-deleted record hardens: the restore window closes and the flesh is scrubbed to a skeleton. Past **`maxTimeApart`** the skeleton purges.

Aging is a pure function of the object's existence stamp, the config windows, and the clock. Every node evaluates a tombstone's *effective* stage the same way, so nodes never race stamps about it. Local garbage collection merely catches physical storage up with what the aging function already answers.

The root and system objects are permanently protected, and schema definitions add their own [unused-delete rules](/universal-directory/concepts/modifying-the-schema.md). Because references store GUIDs, deleting a referenced object never edits the referrers: their values dangle visibly at the tombstone until the target is shredded, at which point they are scrubbed.

## System objects

Objects created at domain creation carry a **system** flag: the root, `/Configuration`, `/Configuration/Schema`, `/LostAndFound`, `/Domain Controllers`, and every base schema definition.

System objects refuse renames, moves, and deletion — they are the fixed points the rest of the directory is built around — but their **attributes are writable**. The replication knobs live as attributes on `/Configuration`, and a description on the root is legal. Schema definitions are the exception, fully immutable via the [schema-subtree rule](/universal-directory/concepts/modifying-the-schema.md).

## Structural repairs are derived, never written

Replication will merge writes that were each legal at their origin but collide when combined. UD repairs these **as a view**: the stored records stay exactly as written, and every node *derives* the same effective tree from the same atoms. No repair writes, no repair replication traffic.

Three rules, all favouring the **earliest claim** — the first legal write stands, because the later write is the one that caused the violation:

- **Name conflicts.** Two live siblings whose names fold equal: the earliest placement claim keeps the name, and the later one *displays* as `name CNF:<guid>`. It remains resolvable and renameable, and its stored name is untouched.
- **Orphans.** A live object whose stored parent is dead or missing derives into **`/LostAndFound`**. Restoring the parent snaps it back automatically.
- **Cycles.** Concurrent moves that form a loop break at the *newest* placement: the latest mover derives into `/LostAndFound`, and the earlier move survives intact.

Admin operations target the **effective view** — rename the CNF loser, move the orphan out, as ordinary writes — and the repair dissolves the moment the underlying violation is gone.

Objects whose *class definition* has not arrived yet are **ghosts**: visible in the debugger, holding their sibling name, but refusing every originating write until the schema converges.

## Replication stamps

Every replicated fact about an object decomposes into **merge atoms**:

- its **existence** — creation, with class and the system flag riding along;
- its **placement** — `(parent, name)` as one atom, so rename and move contend as a unit;
- each **attribute value**.

Every atom carries a **stamp**: a version, an originating time, and an originating invocation id. Together these decide conflicts deterministically — higher version wins, then later timestamp, then origin — and a transfer certificate drives change enumeration. Removing a value leaves a stamped **absence atom** rather than a hole, so the removal itself can win merges.

For how those atoms travel and merge, read [Replication](/universal-directory/concepts/replication.md).

---

# The schema model

_Universal Directory / Concepts_

> UD's compositional schema — globally defined attributes, facets as the unit of composition, nominal classes with no inheritance, per-instance attached facets, class extensions, and typed per-value metadata.

UD's schema is **compositional**. There is no class inheritance anywhere in the model, and never will be. The schema is built from three layers, each referencing the one below by GUID, with flat set-union as the only combining operation.

## The three layers

**Attributes are defined globally.** One definition per attribute — `surname`, `member`, `expiry` — carrying:

- its **syntax**: `string`, `integer`, `boolean`, `timestamp`, `reference`, `endpoint`, or `publicKey`;
- whether it is **single- or multi-valued**;
- its **matching rule**, `caseless` or `exact`, governing value comparison and set uniqueness;
- the **meta-attributes its values may carry** (`legalMeta`, below);
- an **aliases** slot recording alternative wire names.

Facets *reference* attributes; they never define them. So two facets that both want `description` agree by construction, because there is only one `description`.

**A facet is a named set of attribute references** — the smallest unit of composition. `securityPrincipal` carries `sid`; `person` carries `givenName`, `surname`, `email`; `membership` carries `member`. Each reference in a facet can be flagged **required**, shown in the console as `*`. Facets cannot include other facets: composition is flat, so it is order-free and collision-free.

**A class is a named set of facet references plus nominal properties.** `user` is `described` + `person` + `securityPrincipal`, plus a `dnPrefix` of `CN` supplying its DN component type.

Classes carry **zero attributes directly** — a class needing unique attributes gets its own facet. A class is *nominal*: an object **is** a `user`, queryably and permanently, since [class is immutable at birth](/universal-directory/concepts/objects-and-identity.md), but its attribute surface comes entirely from facets.

## An object's surface

Which attributes an object may hold — its **surface** — is the union of three facet sets:

1. its **class's facets**;
2. facets grafted onto its class by **class extensions**, covered in [modifying the schema](/universal-directory/concepts/modifying-the-schema.md);
3. its own **attached facets** — facets added to this one instance.

Setting an attribute outside the surface is refused. The console renders the entire surface, set and unset attributes alike, grouped by the facet that provides them, so the surface is always visible rather than discovered by trial and error.

**Attached facets** are ordinary data: the engine-level `attachedFacets` attribute holds facet references on the object itself. Attaching a facet immediately widens the surface. Detaching is refused while any attribute *only that facet* provides still holds values — clear the values first, explicitly. Nothing is ever silently destroyed.

## Values and per-value metadata

Attributes are **natively multi-valued**: an attribute holds an ordered set of values, unique under the attribute's matching rule.

Each value can carry **metadata**: a generic map of meta-attribute name to values, with nothing hardcoded. Which meta keys are legal on which attribute is itself schema data — an attribute's `legalMeta` lists other *attribute definitions*, so meta values are typed and validated by the same machinery as everything else.

Two invariants hold:

**Meta never forks identity.** The value itself is the set key. `member` cannot contain the same GUID twice with different metadata; changing a value's meta is an update to that value, not a new value.

**Meta is validated, not yet interpreted.** The canonical example — a group membership with an expiry — is expressible today and fully validated: `member` permits the `expiry` meta key, a timestamp. Engine *semantics* for expiry, filtering expired values at read, are not yet active.

## Self-hosting

The schema is not a config file. It is **objects in the directory**, flat under `/Configuration/Schema`, one per attribute, facet, and class, browsable and inspectable through the same console and API as everything else. `CN=surname,OU=Schema,OU=Configuration` is a real DN.

The recursion grounds out cleanly: `attributeDefinition`, `facetDefinition`, and `classDefinition` are themselves classes, composed of facets like `attributeSchema` — and `classDefinition` is an instance of itself. The engine works from a cache compiled off these objects, but the objects are the truth.

The complete contents of the base schema — 26 attributes, 11 facets, 11 classes — are tabulated in the [base schema reference](/universal-directory/reference/base-schema.md).

---

# Modifying the schema

_Universal Directory / Concepts_

> How UD's schema is extended at runtime — namespaces as OUs, definitions born whole and immutable, the unused-delete rule, and class extensions — with a complete worked example.

UD's schema is writable at runtime. The rules are designed so that a directory can be extended for decades without accumulating the classic schema scar tissue: colliding names, orphaned definitions that can never be removed, and third-party leftovers polluting the core forever.

## Namespaces

All new definitions live in a **namespace**: a `schemaNamespace` OU created directly under `/Configuration/Schema`.

The flat set of unprefixed base definitions is **reserved forever**. Even the local organisation extends the schema through its own namespace — `/Configuration/Schema/Homelab/`, `/Configuration/Schema/Acme/`. Namespaces hold definitions only; they cannot nest.

Namespace names may be dotted, and **reverse-DNS (`com.acme.tools`) is the convention for anything redistributable**, which makes vendor collisions structurally unlikely rather than merely hoped against. Short names remain fine for org-internal extensions. `gip` and `ud` are permanently reserved, being GIP's service namespaces.

A namespaced definition's **wire name** is `Namespace-name`. A facet `badging` in the `Acme` namespace is addressed everywhere — API, console, attribute lookups — as `Acme-badging`. The hyphen is *structural*: it is the namespace separator, which is exactly why native definition names may not contain one. See [naming rules](/universal-directory/reference/naming-rules.md). Collisions between namespaces are therefore structurally impossible, not conventionally unlikely.

## Definitions are born whole, then frozen

A definition is created **complete, in one atomic call** — object and attributes together. The API's create accepts an `attributes` map; the console's create modal generates a field per attribute of the chosen class. The engine fills in `lifecycle: active` and `system: false` itself.

Every schema write then passes a **trial recompile** of the whole schema. An attribute definition missing its `syntax`, an extension whose target is not a class, a dangling facet reference — any incoherence rejects the write atomically, with the compiler's message, as if it never happened.

Once created, a definition is **immutable and immovable**: no attribute edits, no renames, no moves. Add and delete are the only verbs. To fix a definition, delete it and recreate it.

The recreated definition is a **new definition with a new GUID**, so anything that referenced the old one dangles visibly rather than silently rebinding. This is the *no-resurrection guarantee*: a deleted definition's name can be reused, but its identity can never be impersonated.

## The unused-delete rule

A definition can be deleted only when **provably unused**.

| Kind | Blocked while… |
|---|---|
| attribute | any object holds values for it, any value uses it as a meta key, any attribute lists it in `legalMeta`, or any facet references it |
| facet | any class composes it, any extension grafts it, or any object has it attached |
| class | any instance exists — including restorable soft-deleted ones; shredded skeletons do not count — or any extension targets it |
| class extension | any instance of the target class holds values in attributes the extension *exclusively* provides |

Refusals **enumerate their blockers by name**: `attribute still has values on: /Sales/Jack`, `facet is used by class Acme-badgeHolder`. Teardown is a matter of reading the error.

Two shapes of teardown work:

- **Dependency order** — clear values, remove the extension, then the facet, then the attribute, then the namespace.
- **Wholesale** — deleting a namespace takes its entire contents down in one atomic subtree delete, and usage *between the dying definitions themselves does not count*. Only usage from outside the namespace blocks.

Definition deletion is the same [soft delete](/universal-directory/concepts/objects-and-identity.md) as everywhere else. A deleted definition sits restorably in the morgue — restoring it recompiles the schema and revives its wire name, refused if the name has been retaken — then hardens to a skeleton when the restore window closes. Values stored under a definition die at *its* shred: data whose meaning can never return is not kept.

## Class extensions

A **class extension** grafts facets onto a class its author does not own. It is the mechanism for "our app needs a tracking number on every user" without touching the `user` class.

An extension is a definition of class `classExtension`, living in the *extender's* namespace, declaring a `targetClass` and the `facets` to add. From the moment it exists, every instance of the target class carries those facets in its surface. Delete the extension and the surface shrinks back — refused, per the table above, while instances still hold values only the extension legitimises.

The target class object is never modified. The extension is entirely the extender's property, living in their namespace, removable with their namespace.

## Worked example: badges for users

The full flow, as API calls. The console can do all of this through the create modal.

First the namespace:

```
POST /api/objects/{schema-ou}/children
{ "name": "Acme", "class": "schemaNamespace" }
```

An attribute, born whole:

```
POST /api/objects/{acme}/children
{ "name": "badgeColor", "class": "attributeDefinition", "attributes": {
    "syntax":       [{ "value": "string" }],
    "singleValued": [{ "value": "true" }],
    "matching":     [{ "value": "caseless" }]
} }
```

A facet carrying it, then an extension grafting that facet onto `user`. Reference values are GUIDs; the console lets you type `/paths` instead:

```
POST /api/objects/{acme}/children
{ "name": "badging", "class": "facetDefinition", "attributes": {
    "attributes": [{ "value": "{badgeColor-guid}" }]
} }

POST /api/objects/{acme}/children
{ "name": "userBadging", "class": "classExtension", "attributes": {
    "targetClass": [{ "value": "{user-class-guid}" }],
    "facets":      [{ "value": "{badging-guid}" }]
} }
```

Every user in the directory now reports `Acme-badging` among its facets, and accepts:

```
PUT /api/objects/{some-user}/attributes/Acme-badgeColor
{ "values": [{ "value": "red" }] }
```

---

# Replication

_Universal Directory / Concepts_

> How UD replicates — multi-master convergence from per-atom merge stamps, the pull protocol with watermarks and up-to-dateness vectors, deterministic conflict repair, and rollback self-defence.

UD is **multi-master and convergent**: every node accepts writes, nodes exchange state by pulling from each other, and any two nodes that have seen the same writes hold the same directory — regardless of the order the writes arrived. There is no primary, no write lock, and no repair coordinator.

Today the engine replicates between **in-process nodes**. The test fleet includes a seeded chaos simulation that asserts byte-equal convergence under partitions, clock skew, power loss, and hostile restores, with every node running the real [persistence](/universal-directory/concepts/persistence.md) path. A network transport for `udd`-to-`udd` sync is a later slice.

Everything on this page is engine behaviour you can observe through the [debug console](/universal-directory/the-console/the-debug-console.md).

## Nodes, invocations, and coordinates

Every node has a permanent **node id** and an **invocation id** naming one *incarnation of its database*. The node keeps a monotonic local **USN**, an update sequence number, and the pair `(invocationId, usn)` is the globally meaningful coordinate of a write.

A node that restores from backup re-mints its invocation id and lets the counter run on. The new id keeps old coordinates truthful, which is what makes restore survivable at all.

## Atoms and merge

Replication moves **merge atoms**: existence, placement, each attribute value, and the shred latch. Each carries a [stamp](/universal-directory/concepts/objects-and-identity.md).

Merging is per-atom, last-writer-wins by the stamp's total order — version, then timestamp, then origin. It is deterministic, so every node picks the same winner.

Three disciplines keep the merge honest:

- An apply that changes nothing — identical value *and* stamp — writes nothing, so duplicates die on arrival instead of echoing around the mesh.
- An equal value under a *stronger* stamp adopts the stamp, because future tiebreaks must agree everywhere.
- Replicated applies are **never re-validated**. A write that was legal at its origin always applies, even if this node's schema has not caught up. Conflicts are repaired, never refused.

The one refusal is the anomaly case: the same GUID arriving with a different class or system flag is two histories wearing one identity. The record is refused, the [journal](/universal-directory/the-console/the-debug-console.md) records it, and the sync cursor stalls so the refusal repeats until an operator intervenes. The engine never guesses.

## The pull protocol

A node syncs by **pulling**: *give me everything above my watermark*.

Per partner, keyed by invocation id, it remembers a **high-watermark** in the partner's local USN space. Alongside travels its **up-to-dateness vector** — per origin invocation, the highest coordinate it holds — which the sender uses for *dampening*: atoms the puller already covers are stripped from the stream, so data learned via a third node is never re-sent.

Answers come in chunks. A chunk with nothing to say still advances the watermark. Vectors only grow at a cleanly completed stream, never mid-flight, where a gap could be silently claimed.

A brand-new node **joins by pulling**. A zero watermark and an empty vector are the entire initial sync: the root is discovered from the records themselves, the schema compiles as definitions arrive — incomplete ones wait in [quarantine](/universal-directory/concepts/modifying-the-schema.md) — and objects whose class has not landed yet are *ghosts*, visible in the debugger, refusing writes, dissolving on convergence.

The pull handshake compares root GUIDs first. Different root, different domain, sync refused.

## Conflict repair is derived

Writes that were each legal at their origin can collide when merged. UD repairs these [as a view, never as a write](/universal-directory/concepts/objects-and-identity.md): colliding sibling names keep the *earliest* claim and CNF-display the later one, while orphans and broken move-cycles derive into `/LostAndFound`. Repairs dissolve the moment an ordinary write removes the violation.

Same atoms, same repair, on every node, with zero repair traffic.

## Deletion that replicates

The [three-stage lifecycle](/universal-directory/concepts/objects-and-identity.md) is built for merge.

Tombstones propagate as stamped existence transitions. A **shred** additionally sets a *latch* — a set-once atom that can never lose a race — so no concurrently written restore or edit can resurrect destroyed data anywhere. Aging, meaning the restore window and the purge horizon, is computed from stamps and the replicated config knobs, never raced via writes.

A node whose watermark predates knowledge its partner has already purged is refused with *full resync required*: wipe, re-mint, pull from zero, rather than silently missing deletes.

## Rollback self-defence

A node restored from a snapshot *without* re-minting — the classic VM-revert disaster — is detected from protocol evidence: a partner claiming this node's own stream beyond its own counter.

That claim takes either of two forms, and UD watches for both: an up-to-dateness **vector** entry, or a **watermark**. The bookmark survives even when the only transfer was an abandoned stream, where vectors never absorb. Evidence flows both ways, since requests carry the puller's watermark and chunks carry the sender's bookmark of the puller's stream back.

On detection the node latches. It re-mints its invocation, re-certifies its holdings into the new stream so nothing it wrote gets swallowed by dampening — with [persistence](/universal-directory/concepts/persistence.md), publishing the salvage as a live-store snapshot — suspends originating writes, and alarms. One clean pull then heals it.

Detection is strongest at first contact, and first contact is why the protocol has **probes**: a zero-record hello carrying only status. Applying chunks consumes local USNs, and every consumed USN burns a unit of evidence margin, so a restored node probes every reachable peer *before* applying or originating anything.

## Watching it happen

The console's header shows the node's identity, counter, and vectors. The **journal** tab shows every merge that discarded concurrent input, every refusal, every derived repair, and every rollback event.

Multi-master's classic sin is that lost updates are silent. UD's are not.

---

# Persistence

_Universal Directory / Concepts_

> How UD survives restarts — a RAM-resident store with a durability footprint on disk, generations of snapshots and write-ahead logs, blind-replay recovery, background compaction, and the disk-erasure guarantee behind shredding.

UD is **RAM-resident by design**. Every read and every merge runs against memory, and the disk exists for exactly one reason: so a node's state survives restarts.

This is the posture the directory world already runs in when healthy, made explicit. Active Directory's sizing guidance is RAM greater than or equal to the whole database; OpenLDAP's LMDB is a memory-mapped file; etcd caps its store at what mmap keeps resident. Scale-out comes from [replication](/universal-directory/concepts/replication.md) partitioning the tree across DCs, never from paging within one.

## Generations: snapshot plus write-ahead log

On disk lives a chain of **generations**. `snap-N` is the complete sorted state at the start of `wal-N`, and the WAL holds every change since as **state deltas** — the post-state of each touched atom, never logical operations, so replaying an old log can never depend on the code that wrote it.

One originating operation is one CRC-checksummed frame, and a subtree delete travels whole. One applied replication chunk is one frame. Every file opens with a self-checksummed, format-versioned header.

The encodings are hand-written, not derived. The disk format is a compatibility surface, and no struct rename may silently change bytes.

## Booting is blind replay

Load the newest valid snapshot, apply every later WAL frame with no merge logic and no clock reads, then recompute everything derived — the tree index, back-references, the compiled schema, structural repairs — exactly as a replication apply would.

A torn tail on the newest WAL is crash residue: nothing beyond it was ever acknowledged, and it truncates exactly. Damage anywhere else is corruption, and `udd` refuses to start rather than guess. That includes a torn sealed generation, valid frames *after* damage, and frames after a generation's closing seal.

## The durability contract

Nothing becomes visible to the outside before it is durable.

- A **mutating API call returns only after its frame is fsynced**. Concurrent writers share flushes through group commit, and the fsync never runs under the store lock.
- **Protocol speech is durable-gated.** Served records, chunk bookmarks, and advertised [up-to-dateness claims](/universal-directory/concepts/replication.md) never reference state a crash could erase. The deadly edge is a partner that *dampens* what you claim and can never resend it after your crash.
- A **failed fsync is fatal**, never retried. The daemon stops rather than acknowledge state the disk does not hold, and restart recovers to the last durable point.

Local reads may see RAM a few milliseconds ahead of the disk. No replication evidence ever does.

## Background compaction

Snapshots are produced by **rebuilding from the sealed files** — the recovery code path run ahead of time on a background thread — never by serialising the live store.

Rotation seals the active WAL, and the seal is durable before a successor exists. The rebuild replays the sealed chain, publishes the next snapshot atomically (write-temp, fsync, rename, fsync the directory), and only then deletes the generations it supersedes.

Recovery is therefore *production-exercised*: every compaction is a rehearsal of boot. Any mutation that forgot to write its delta shows up as a rebuild that disagrees with the live store, an invariant the test suite asserts continuously.

The one exception is the [rollback-salvage snapshot](/universal-directory/concepts/replication.md), which serialises the live store because the salvage exists only in RAM.

## Erasure reaches the disk

[Shredding](/universal-directory/concepts/objects-and-identity.md) destroys a payload in memory, but its bytes linger in older WAL frames and snapshots until compaction deletes those files.

That gap is managed as an explicit **erasure debt**. Every destructive transition starts a clock — a shred, a GC hardening, or an *applied* replicated shred, since the obligation follows the data rather than the command — and UD guarantees the destroyed payload has left the node's files **within two minutes** of the acknowledgment. In practice compaction kicks immediately; the window exists so a burst of shreds coalesces into one rebuild.

The debt is never written down. At boot it is re-derived from the replayed frames themselves, due immediately, so a node that dies mid-obligation finishes it promptly on restart.

The claim is deliberately scoped to the *filesystem*. What remains in unallocated blocks or an SSD's wear-levelled cells is the platform's concern, as it is for every directory product.

## Crashes, restores, and the sim

The persistence layer is torture-tested through a deterministic in-memory filesystem — torn writes at chosen bytes, failed fsyncs, power-loss simulation — and by the same [chaos simulation](/universal-directory/concepts/replication.md) that proved the merge.

Every simulated node runs on a faux disk through the real recovery path, crashes mid-anything, compacts under load, and is restored from forked disk images, the honest VM-revert. The fleet is still required to converge byte-for-byte at clear skies. A thin suite repeats the full lifecycle against a real filesystem.
