dendropsis — Specification
version 0.1
This document specifies the dendropsis protocol: a
structural-decomposition extension over a
metatheca vault. dendropsis runs a frozen
segmenter pipeline against the content blobs reachable from a
metatheca state, organizes the resulting hierarchical decomposition
into a tree of content-addressed nodes whose components are
themselves deposita on a dedicated dendropsis cella under the same
vault root, and exposes recursive structural diff, exact move
detection, and partial-tree materialization against any historical
tree-state.
The specification is language-agnostic. The reference implementation
is in Rust (dendropsis-rs/).
dendropsis is structurally an extension over metatheca, not
a parallel store. It introduces no new storage primitive — every byte
it writes is a syntheca depositum and every mutable pointer it holds
is an apotheca pinax — but it does occupy its own dedicated cella
under the metatheca vault root, at the fixed path
<vault>/ext/dendropsis/cella/. What it adds is a view —
a hierarchical decomposition of metatheca’s content optimized for
tree-shaped change analysis. The name reflects this: ὄψις
(opsis) means "view, sight," and dendropsis names the
tree-shaped view (from δένδρον, "tree") over the underlying vault.
The suffix mirrors English synopsis and the sibling extensions
logopsis and
semopsis.
dendropsis is the structural complement to the search-shaped extensions. Where logopsis builds a lexical view (terms → posting lists) and semopsis builds a semantic view (embeddings → vector index), dendropsis builds a compositional view: each indexed blob is decomposed into a tree of content-addressed sub-blobs, and queries operate on tree structure (walk, diff, move-detect) rather than on tokens or vectors. The three extensions are independent and may be deployed in any combination; cross-extension coordination, when wanted, is the responsibility of an upstairs layer.
For the substrate primitives this document builds on, see
metatheca SPEC.md,
../../../syntheca/SPEC.md,
and
../../../apotheca/SPEC.md.
This document covers what sits above those: the tree-node wire
format, the segmenter-pipeline identity contract, the tree-state
chain, the tree-head pinax, the query algorithms (walk, diff,
move-detect), and the CLI surface.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.
1 Scope and terminology
This document specifies dendropsis Phase 1: a single local tree-decomposition view over a single metatheca vault, with rebuild-only reindex semantics (no incremental tree-node mutation), a single fixed segmenter-pipeline identity per chain, and three core queries (walk, recursive structural diff, exact move detection) operating against any historical tree-state. Phase 1 is the v0.1 release surface; deferred items are listed in §10–§11.
Terminology not redefined here is inherited from metatheca SPEC §1, syntheca SPEC §1, and apotheca SPEC §1. In particular: cella, depositum, pinax, hash (BLAKE3), digest (SHA-256), vault, entry, fact, state, head.
1.1 Segmenter
A segmenter is a deterministic function from input bytes
plus a segmentation context to an ordered sequence of child
references (§1.5). A segmenter is identified by a
tuple (family, version) recorded as part of the pipeline
declaration (§1.3). The family is an opaque string
(e.g. unicode-sentence, markdown-section,
fs-directory); the version is an opaque string (e.g.
v1.2).
The segmenter’s contract is to be a deterministic pure function: two invocations of the same segmenter identity on identical input bytes under identical context MUST produce byte-identical output child-reference sequences.
dendropsis does not specify what a segmenter does internally (parser grammar, AST traversal rules, boundary detection, normalization). These are implementation-defined within the segmenter family/version. The protocol guarantees only that segmenter output is reproducible under a recorded segmenter identity.
A segmenter MAY emit one of three structural shapes (§1.2). The shape is declared in the pipeline (§1.3) and is fixed for that segmenter’s role in the pipeline.
1.2 Segmenter shapes
dendropsis recognizes three segmenter shapes; together they cover the decomposition patterns this specification supports.
Terminal (
bytes → ordered list of leaves). The segmenter emits leaves only; its output children are syntheca-blob references (§1.5). Used at the bottom of a pipeline.Recursive (
bytes → ordered list of (leaves and self-references)). The segmenter emits a mixture of leaves and inline-tree references whose contents the same segmenter is expected to process. The substrate recurses by invoking the segmenter on each non-leaf output’s referenced bytes. Used for self-similar decompositions (e.g. a filesystem directory walker).Chained (
bytes → ordered list of (leaves and inline-tree references for stage S)). The segmenter emits a mixture of leaves and inline-tree references tagged with the name of a downstream segmenter stage that the pipeline declaration names as the consumer for those references. The substrate recurses by invoking the named downstream segmenter on each non-leaf output’s referenced bytes. Used for typed-pipeline decompositions (e.g. paragraph segmenter → sentence segmenter).
A pipeline declaration (§1.3) MAY mix shapes: a chained segmenter at the root, a terminal segmenter at the leaves, recursive segmenters in between.
1.3 Pipeline
A pipeline is a directed acyclic graph of segmenter roles.
Each node in the graph is a role: a named position in the
pipeline bound to a segmenter identity (§1.1) and a
segmenter shape (§1.2). Each edge declares "outputs of
role A tagged for stage B are consumed by role
B." Exactly one role is marked as the entry role: the
role invoked on the source content blob.
The pipeline is consumer-defined: dendropsis does not bless segmenter identities, does not run segmenter code, and does not interpret role types. dendropsis records the pipeline declaration as data, dispatches segmenters per the declaration, and validates that segmenter outputs conform to their declared shapes.
1.4 Pipeline identity
A pipeline identity is the BLAKE3 hash of a content-addressed pipeline-declaration blob (§4.5). The identity is fixed by the genesis tree-state (§1.8) and inherited unchanged by every subsequent tree-state in the chain.
dendropsis MUST refuse to advance the tree-state chain to a
tree-state whose pipeline identity differs from the genesis pipeline
identity (§5.4). Changing the pipeline — adding a
segmenter, upgrading a segmenter version, altering composition edges
— is out of Phase 1 scope (§10) and is supported
by dnops destroy (§5.10) followed by re-init.
The freezing rule reflects the design principle that a dendropsis chain’s identity is its pipeline. Trees built under different pipelines are distinct artifacts; the substrate refuses to conflate them. The underlying entry bytes are unaffected by pipeline changes (they live in metatheca’s cella); only the dendropsis-side derived view is rebuilt.
1.5 Child reference
A child reference identifies one child of a tree node. A child reference carries:
An optional key: a UTF-8 string naming the child, used when the parent represents a keyed/named collection (e.g. a filesystem directory entry’s filename). A node either has keys on all its children or on none; the choice is per-node.
A kind:
leaf(the referenced bytes are opaque to dendropsis and live in syntheca as a depositum) ortree(the referenced bytes are a dendropsis tree node, recursively addressable).A hash: the BLAKE3 hash of the referenced depositum.
The substrate distinguishes leaf from tree at the
protocol level so walks can terminate without parsing leaf bytes. The
substrate does not record any further type information about
what a leaf or subtree means; that interpretation lives in the
segmenter pipeline.
1.6 Tree and tree root
A tree is a tree node together with its transitive children resolved through child references. A tree root is the BLAKE3 hash of the topmost tree node for a given entry’s decomposition. Given a tree root and a backing dendropsis cella, the entire tree is reachable via lazy descent (§5.5).
Trees are immutable: a given tree root names a specific tree forever. Changes to an entry’s content produce new tree roots; old tree roots remain valid pointers to historical decompositions of historical bytes.
1.7 Tree manifest
A tree manifest is the dendropsis-side projection of "for each metatheca entry indexed by the current tree-state, here is its tree root." It is materialized as a single content-addressed depositum (§4.3) per tree-state.
The manifest records, per entry: the entry-ID, the BLAKE3 hash of the metatheca content blob from which the tree was derived (so callers can detect when the blob-ref has advanced past this manifest), and the tree root. Entries reachable from the metatheca state but not yet indexed by dendropsis (e.g. because they were added after the most recent reindex) do not appear in the manifest.
1.8 Tree-state
A tree-state is a moment in the extension’s history. Each tree-state is materialized as a content-addressed tree-state blob (§4.4) capturing the tree manifest, the pipeline identity, the metatheca state it was built against, and a back-pointer to the prior tree-state. The set of tree-states linked through these back-pointers is the tree-state chain.
The genesis tree-state is the unique tree-state with
previous = null, created by dnops init
(§5.1).
1.9 Tree-head
The tree-head is the single mutable pointer identifying the current tree-state. It is realized as an apotheca pinax (§6) on the dendropsis cella, holding the BLAKE3 hash of the current tree-state blob.
The tree-head pinax is disjoint from metatheca’s head pinax
(metatheca SPEC §6.1) and from any sibling extension’s head: the
pinakes live in disjoint cellae and advance independently. dendropsis
reindex advances tree-head; metatheca commits advance metatheca’s
head; the two cadences are decoupled.
1.10 Tree-stateref
A tree-state reference (or tree-stateref) follows the same grammar as metatheca’s stateref (metatheca SPEC §1.7), resolving against the tree-state chain instead of the metatheca state chain:
a full BLAKE3 hash;
a hash prefix of at least 8 lowercase hex digits, unambiguous within the chain;
an ISO-8601 date or date-time, denoting the most recent tree-state with
created_at_ns ≤ that_instant;a relative reference:
~or~NforNnon-negative;the literal
current.
A tree-stateref MUST resolve to exactly one tree-state or fail.
1.11 Move detection
Move detection is the dendropsis-specific query of
identifying subtrees that appear at different positions in two trees.
Because every tree node is content-addressed, a hash present at
position P1 in tree T1 and at position P2 in
tree T2 is the same subtree by content. Move detection
compares the set of (hash, position) pairs across the two trees and
reports hashes that appear in both at different positions. The cost
is proportional to the size of the differing portions of the trees,
not to total tree size. dendropsis treats move detection as a
first-class query (§5.7) because the content-addressing
makes it free relative to the cost of structural diff.
2 Relationship to metatheca and siblings
dendropsis is an extension over metatheca: it consumes the
content made addressable by the metatheca chain and adds a derived,
queryable view. It introduces no new storage primitive — every byte
is a syntheca depositum and every mutable pointer is an apotheca
pinax — but it does add a dedicated cella. All dendropsis durable
state lives in a syntheca cella at the fixed vault-relative path
<vault>/ext/dendropsis/cella/, separate from metatheca’s
source-of-truth cella at <vault>/cella/ (metatheca SPEC §7).
The split reflects the layers’ differing status: metatheca’s cella is
the source of truth and is never wholesale-deletable; dendropsis’s
cella is wholly derived and MAY be discarded and rebuilt at any
time (§5.10).
The host/extension relationship has three concrete consequences:
Content authority lives in metatheca. dendropsis MUST NOT mutate any metatheca-emitted fact, state blob, or the metatheca
headpinax. dendropsis reads from metatheca’s cella to determine the set of content blobs to decompose; it does not write back.dendropsis writes are confined to dendropsis’s cella. dendropsis writes only deposita carrying dendropsis’s wire formats (§4) and the single tree-head pinax (§6) on its own cella. It MUST NOT write to any other cella in the vault.
The chains advance independently. A metatheca commit does not trigger a dendropsis reindex, and a dendropsis reindex does not require a metatheca commit.
dnops reindexmay be invoked against any metatheca state in the chain; dendropsis records which metatheca state it was built against (§4.4metatheca) so that any tree-state can be traced back to a precise point in the host’s history.
Where logopsis or semopsis is also deployed, its state lives in yet
another dedicated cella under <vault>/ext/. dendropsis
MUST NOT read from or write to any sibling extension’s cella.
Cross-extension non-interference is enforced naturally by cella
separation.
A vault MAY contain a metatheca chain without a dendropsis chain.
A dendropsis chain MUST NOT exist without a metatheca chain on the
same vault; dnops init (§5.1) requires metatheca’s
head pinax to be present at <vault>/cella/.
3 Identifiers and time
3.1 Hashes
Content-addressing throughout dendropsis uses syntheca’s BLAKE3 hash (syntheca SPEC §1.1, §1.2). Wire form: 64 lowercase hex digits.
3.2 Time
All timestamps follow metatheca SPEC §2.3: nanoseconds since the Unix epoch as a signed 64-bit integer, monotonically non-decreasing within a single vault process.
3.3 Strings
All UTF-8 strings on the wire (segmenter family/version identifiers,
role names, child-reference keys) are valid UTF-8. Implementations
MUST reject any wire blob containing invalid UTF-8 with
Malformed. Strings are compared byte-wise in UTF-8
lexicographic order wherever sorting is required.
4 Wire formats
4.1 Canonical JSON
All JSON blobs defined by this specification (tree node §4.2, tree manifest §4.3, tree-state §4.4, pipeline declaration §4.5) are serialized as canonical JSON per RFC 8785, identical to metatheca SPEC §3.1. Phase 1 uses JSON throughout; a future phase may introduce a binary tree-node format for dense trees (§11).
4.2 Tree-node blob
A tree-node blob is a canonical-JSON object:
{
"type": "dendropsis/node",
"version": 1,
"keyed": <bool>,
"children": [
{ "key": "<utf8>"?, "kind": "leaf" | "tree", "hash": "<blake3-hex>" }
]
}
Field constraints:
typeMUST be the literal string"dendropsis/node".versionMUST be the integer1.keyedMUST betrueif every child has akeyfield,falseif no child has akeyfield. A node MUST NOT mix keyed and non-keyed children.childrenMUST be a JSON array. Whenkeyed = true, the array MUST be sorted in strictly ascending UTF-8 byte order of thekeyfield; duplicates are forbidden. Whenkeyed = false, children are ordered as the segmenter emitted them and the order is significant (it is part of the node’s identity).Each child’s
kindMUST be one of"leaf"or"tree".Each child’s
hashMUST be a BLAKE3 hash naming a depositum present in the dendropsis cella (fortree) or in the metatheca cella’s content blobs (forleaf).
A tree-node’s identity is the BLAKE3 hash of its canonical-JSON bytes.
The canonical JSON encoding gives two important properties: deterministic identity (same logical content → same bytes → same hash) and trivial recursive comparison (two nodes are equal iff their hashes are equal; otherwise children are compared positionally or keywise).
4.3 Tree-manifest blob
A tree-manifest blob is a canonical-JSON object:
{
"type": "dendropsis/manifest",
"version": 1,
"entries": [
{ "entry": "<uuid-v7>", "blob": "<blake3-hex>", "tree": "<blake3-hex>" }
]
}
Field constraints:
typeMUST be the literal string"dendropsis/manifest".versionMUST be the integer1.entriesMUST be a JSON array sorted in ascending order ofentry(UUID v7 sort order, equivalent to ascending generation time).Each entry’s
blobMUST be the BLAKE3 hash of the metatheca content blob from which the tree was derived.Each entry’s
treeMUST be the BLAKE3 hash of a tree-node blob (§4.2) present in the dendropsis cella.
A tree-manifest’s identity is the BLAKE3 hash of its canonical-JSON bytes.
The manifest is separated from the tree-state blob so that two tree-states sharing the same manifest deduplicate naturally on the cella (e.g. a reindex against the same metatheca state under the same pipeline).
4.4 Tree-state blob
A tree-state blob is a canonical-JSON object:
{
"type": "dendropsis/tree-state",
"version": 1,
"previous": "<blake3-hex>" | null,
"metatheca": "<blake3-hex>",
"pipeline": "<blake3-hex>",
"manifest": "<blake3-hex>",
"created_at_ns": <integer>
}
Field constraints:
typeMUST be the literal string"dendropsis/tree-state".versionMUST be the integer1.previousMUST be either a BLAKE3 hash naming a tree-state blob present in the cella, ornull(genesis tree-state only). Exactly one tree-state in any chain hasprevious = null.metathecaMUST be a BLAKE3 hash naming a metatheca state blob present in the metatheca cella (metatheca SPEC §3.3).pipelineMUST be the BLAKE3 hash of a pipeline-declaration blob (§4.5) present in the dendropsis cella. This is the pipeline identity (§1.4); it is fixed at genesis and inherited unchanged by every subsequent tree-state.manifestMUST be the BLAKE3 hash of a tree-manifest blob (§4.3) present in the cella.created_at_nsMUST be an integer per §3.2.
A tree-state’s identity is the BLAKE3 hash of its canonical-JSON bytes.
4.5 Pipeline-declaration blob
A pipeline-declaration blob is a canonical-JSON object:
{
"type": "dendropsis/pipeline",
"version": 1,
"entry": "<role-name>",
"roles": [
{
"name": "<role-name>",
"segmenter": { "family": "<utf8>", "version": "<utf8>" },
"shape": "terminal" | "recursive" | "chained",
"edges": { "<output-stage-tag>": "<role-name>" }
}
]
}
Field constraints:
typeMUST be the literal string"dendropsis/pipeline".versionMUST be the integer1.entryMUST be the name of a role declared inroles.rolesMUST be a JSON array sorted in strictly ascending UTF-8 byte order of thenamefield; duplicates are forbidden.Each role’s
shapeMUST be one of"terminal","recursive", or"chained".Each role’s
edgesMUST be a JSON object whose keys are UTF-8 strings (stage tags emitted by this role’s segmenter on its tree-kind outputs) and whose values are names of other roles inroles. Forterminalroles,edgesMUST be an empty object. Forrecursiveroles,edgesMUST be either empty (the segmenter handles its own recursion implicitly) or contain a single key whose value is the role’s own name. Forchainedroles,edgescarries one entry per downstream stage.The graph formed by roles as nodes and
edgesvalues as outgoing edges MUST be acyclic, with the single exception of self-loops onrecursiveroles.
A pipeline-declaration’s identity is the BLAKE3 hash of its
canonical-JSON bytes. This identity is what is recorded as
pipeline in every tree-state blob (§4.4) and
what is fixed at genesis (§1.4).
5 Operations
A dendropsis-equipped vault exposes the following protocol operations. Each is total: it terminates with one of the outcomes listed for it.
Throughout this section, deposit and fetch of dendropsis blobs (tree
nodes, manifests, tree-states, pipeline declarations) refer to
operations on the dendropsis cella at
<vault>/ext/dendropsis/cella/. Operations that fetch metatheca
state blobs or content blobs target metatheca’s cella at
<vault>/cella/. Step-level cella targets are noted only where
the destination is non-obvious from context.
5.1 init
Creates a genesis tree-state on a vault that already has a metatheca chain.
Verify metatheca’s
headpinax is present on metatheca’s cella at<vault>/cella/(§2); fail withNotFoundotherwise.Verify no dendropsis cella exists at
<vault>/ext/dendropsis/cella/; fail withConflictotherwise.Open a fresh syntheca cella at
<vault>/ext/dendropsis/cella/, creating any missing parent directories. All subsequent deposits and the tree-head pinax target this cella.Construct a pipeline-declaration blob (§4.5) from a caller-supplied pipeline description. Validate that the graph is well-formed per §4.5. Deposit it; let
pbe the returned hash.Construct an empty tree-manifest blob (
entries = []). Deposit it; letmbe the returned hash.Construct the genesis tree-state blob:
previous = null,metatheca = <current metatheca head>,pipeline = p,manifest = m,created_at_ns = now(). Deposit it; letgbe the returned hash.Set the tree-head pinax (§6) on the dendropsis cella to
gwithexpected = None.
The genesis tree-state contains no indexed entries; it exists so that
reindex (§5.4) has a chain to advance from and
so that the pipeline identity is fixed at init time.
On any failure prior to step 7, the implementation MUST leave no partially-initialized dendropsis cella. If the cella was created in step 3, it MUST be removed before returning the failure.
5.2 Decompose a blob
Given a content blob b and a pipeline P, produce a tree
root by recursive segmenter application:
Fetch
bytes = syntheca.get(b)from metatheca’s cella.Invoke the entry-role segmenter on
bytesper §1.1; obtain an ordered sequence of child-reference outputs per §1.5.For each output child reference of
kind = leaf: the child is a syntheca-depositum reference. Verify the referenced bytes are present in metatheca’s cella or, if produced by the segmenter and not already present, deposit them; let the resulting hash be the child’shash.For each output child reference of
kind = tree: identify the downstream role per the pipeline’s edges (§4.5) — for aterminalsegmenter this case is empty; for arecursivesegmenter the same role is invoked on the referenced bytes; for achainedsegmenter the role named inedges[<output-stage-tag>]is invoked. Recursively apply §5.2 with the downstream role as entry; let the resulting tree root be the child’shash.Construct a tree-node blob (§4.2) from the resulting child-reference sequence. Deposit it; let
tbe the returned hash. Returnt.
The segmenter functions are opaque to this specification. Their only contract is determinism (§1.1): identical input bytes under identical pipeline MUST produce identical output sequences.
Tree-node deposits are content-addressed; identical sub-trees deduplicate naturally on the cella. This is the source of cross-tree subtree identity (§1.11) and cross-entry citation graph queries (§11).
5.3 Build the tree manifest
Given a metatheca state M and a pipeline P, materialize
the tree manifest:
Walk
M’s cumulative fact set (metatheca SPEC §3.3) and collect the set ofcore/blob-refblobs current in the projection (metatheca SPEC §5.5).For each such (entry, blob) pair
(e, b): a. Computet = decompose(b, P)per §5.2. b. Append(e, b, t)to the manifest entries.Sort the manifest entries by
entryascending per §4.3. Construct the tree-manifest blob and deposit it; letmbe the returned hash. Returnm.
5.4 Reindex
Atomically advances tree-head from the current tree-state to a new tree-state built against a chosen metatheca state.
Inputs: a metatheca stateref (default: metatheca’s current head).
Resolve the metatheca stateref per metatheca SPEC §5.3 to a metatheca state hash
M.Read the current tree-state blob via the tree-head pinax (§6); extract its pipeline identity
p. Compute the storage digestdof the tree-head pinax bytes:sha256over the bytes returned bysyntheca.get_pinax, or remembered from the prior successfulset_pinax(apotheca SPEC §2.5/§3.5expectedsemantics).Fetch the pipeline-declaration blob at
p; reconstruct the pipelineP.Build the tree manifest per §5.3 against
MunderP; obtainm.Construct the new tree-state blob:
previous = <current tree-head>,metatheca = M,pipeline = p,manifest = m,created_at_ns = now(). Deposit it; letsbe the returned hash.Call
syntheca.set_pinax(name = "head", bytes = s_bytes, expected = Some(d))against the dendropsis cella.
The commit point is step 6. Steps 1–5 are pre-commit and produce only content-addressed deposita that are safe under interruption.
On Conflict from step 6, another writer advanced tree-head
concurrently. The caller MAY retry from step 2.
The pipeline identity is fixed by the genesis tree-state and
inherited unchanged by every subsequent tree-state. reindex
MUST refuse to write a tree-state with a pipeline field
that differs from the current tree-state’s. Changing the pipeline is
out of Phase 1 scope (§10) and is supported by
destroy + re-init (§5.10).
5.5 Walk a tree
Given a tree root and an optional depth limit, return the tree’s structure lazily:
Fetch the tree-node blob at the given root hash from the dendropsis cella; parse per §4.2. Let its child-reference list be
cs.Yield
(root, cs)to the caller.For each child
cincs: a. Ifc.kind = leaf: yield(c.hash, leaf)to the caller. b. Ifc.kind = treeand the depth limit has not been reached: recurse withc.hashas root and the depth limit decremented.
Walk is read-only. Implementations MAY add a local node-resolution
cache; the cache is not part of the protocol. A walk with depth limit
0 returns the root node and its immediate children’s
references without resolving any subtree — useful for shallow
inspection.
5.6 Recursive structural diff
Given two tree roots r1 and r2, classify their
difference:
If
r1 == r2: return identical. No reads required; no recursion.Otherwise: fetch both tree-node blobs; parse per §4.2.
If the two nodes have different
keyedvalues: return shape-mismatch. (A keyed node is structurally incompatible with an unkeyed one at the same position.)If
keyed = true: for each key present in one node and not the other, record an added or removed difference. For each key present in both, if the child hashes differ, recurse with the two child hashes; the recursion’s result is recorded as a nested difference at this key.If
keyed = false: compute the longest common subsequence of the two child-reference lists by hash equality. Children present in one list but not the other are recorded as inserted or deleted differences. Children at corresponding positions whose hashes differ are recorded as nested differences via recursion. (Phase 1 uses LCS; future phases may add specialized algorithms for large lists.)Return the resulting difference tree.
The cost is proportional to the size of the differing portions of the trees, not to total tree size. Identical subtrees are detected by hash equality at step 1 and skipped without recursion. This is the source of the O(differing nodes) cost claim (§1.11).
Diff is read-only.
5.7 Move detection
Given two tree roots r1 and r2, identify subtrees
present in both trees at different positions:
Walk
r1(§5.5) without depth limit. Build a mapm1: hash → list of positions, where a position is the path of(key | index)steps from the root to the subtree.Walk
r2similarly to buildm2.For each hash
hpresent in bothm1andm2: if the position sets differ, reporthtogether with the set difference. (A subtree that appears at the same position in both is not a move; a subtree that appears at one new position and remains at another is a copy; a subtree that appears at exactly one position in each, with the positions different, is a move.)Return the resulting move list.
Move detection is read-only. The cost is proportional to the sum of node counts in the two trees. Implementations MAY share walk state with §5.6 when both queries are requested over the same pair.
Move detection is symmetric on hash; it does not distinguish "the original" from "the copy" because content-addressed identity makes that distinction meaningless. Caller-side heuristics (e.g. "the position with an earlier timestamp is the original") are outside the scope of this protocol.
5.8 Resolve a tree-state reference
Given a tree-stateref (§1.10) and the current tree-head, return the unique tree-state hash it denotes. Algorithmically identical to metatheca SPEC §5.3 with the tree-state chain substituted for the metatheca state chain.
5.9 Walk the tree-state chain
Algorithmically identical to metatheca SPEC §5.4 with the tree-state chain substituted.
5.10 Destroy
Removes the dendropsis cella and all dendropsis state from the vault.
Inputs: none.
If
<vault>/ext/dendropsis/cella/is absent, fail withNotFound.Recursively remove the directory
<vault>/ext/dendropsis/(including the cella and any extension-private auxiliary state under it).
Destroy is total: on completion, no dendropsis state remains on the
vault. metatheca state and any sibling extension state are
unaffected. A subsequent dnops init (§5.1)
succeeds if and only if destroy completed.
Destroy is the supported mechanism for changing the segmenter
pipeline (§10): destroy the chain, then re-run
dnops init with the desired pipeline. It is also the recovery
mechanism for a corrupted dendropsis cella, since the chain is wholly
derivable from metatheca’s cella by reindexing.
Destroy MUST NOT touch <vault>/cella/,
<vault>/index.db, or any other entry in <vault>/ext/.
6 Tree-head pinax
6.1 Name
The tree-head pinax MUST be stored on the dendropsis cella under
the apotheca pinax name head (four ASCII octets: h,
e, a, d). This name is fixed by this
specification.
The name is a valid apotheca name (apotheca SPEC §4.1): non-empty,
length 4, no /, no NUL. dendropsis therefore imposes no
multi-segment-name requirement on the underlying apotheca
implementation.
The pinax shares only its name with metatheca’s head
(metatheca SPEC §6.1) and with any sibling extension’s head; the
pinakes live in disjoint cellae and never collide. The same naming
convention is used by logopsis and semopsis.
6.2 Bytes
The tree-head pinax bytes follow metatheca SPEC §6.2 verbatim: the 64-octet ASCII encoding of the current tree-state’s BLAKE3 hash as 64 lowercase hex digits, with no trailing newline or whitespace.
6.3 Compare-and-swap protocol
All updates to tree-head MUST go through syntheca.set_pinax
with expected set to the SHA-256 digest of the current
tree-head bytes, identical in mechanism to metatheca SPEC §6.3. The
genesis case (init, §5.1) sets tree-head with
expected = None.
7 CLI surface
The reference CLI binary is dendropsis with the recommended
alias dnops. It exposes the protocol against the vault rooted
at the current working directory unless overridden with
--vault <PATH>.
This section specifies the protocol-bearing subcommands. Diagnostic output formatting is implementation-defined unless otherwise noted.
7.1 dnops init
dnops init --pipeline <pipeline-path-or-hash> [<path>]
Initialize a dendropsis chain on a vault per §5.1. The
vault root is <path> if supplied, otherwise the current
directory. The vault MUST already contain a metatheca chain. Exits
non-zero if metatheca is absent or a dendropsis chain already exists.
--pipeline accepts either a path to a pipeline-declaration
JSON file (which the implementation validates and deposits) or a
BLAKE3 hash of a pipeline-declaration blob already present on a cella
accessible to the caller (the caller supplies the bytes via the
implementation’s configured mechanism). Implementations SHOULD
warn if the chosen pipeline identity is new to the cella, since this
is now permanent for the chain.
7.2 dnops reindex
dnops reindex [--as-of <metatheca-stateref>]
Build a new tree-state per §5.4 against the named metatheca state (default: metatheca’s current head). On success, prints the new tree-state hash to standard output.
7.3 dnops tree
dnops tree [--as-of <tree-stateref>] <entry-uuid>
Print the tree root for an entry as recorded in the given (or current) tree-state’s manifest. Exits non-zero if the entry is not present in the manifest.
7.4 dnops walk
dnops walk [--depth <int>] <tree-root>
Walk a tree per §5.5 from the given root hash. Default depth is unbounded. Output format: one line per visited node or leaf, prefixed with depth and node hash:
<depth> <kind> <hash> [<key>]
Where <kind> is node or leaf and <key> is
the child key (for keyed parents) or the child index (for unkeyed
parents).
7.5 dnops diff
dnops diff <tree-root-1> <tree-root-2>
Compute the recursive structural diff per §5.6. Output format is implementation-defined; the reference implementation emits a hierarchical text representation of the difference tree.
7.6 dnops moves
dnops moves <tree-root-1> <tree-root-2>
Compute the move list per §5.7. Output format per move, one line:
<subtree-hash> <from-path> <to-path>
Where paths are /-joined sequences of keys or indices from the
respective tree roots.
7.7 dnops state
dnops state [<tree-stateref>]
Show metadata for a tree-state: hash, created_at_ns,
previous, metatheca, pipeline identity, manifest hash,
manifest entry count. Default argument: current.
7.8 dnops log
dnops log [--since <iso8601>] [--until <iso8601>] [-n <count>]
Walk the tree-state chain from current tree-head backward, emitting one line per tree-state with its short hash, ISO-8601 timestamp, and metatheca state short hash.
7.9 dnops fsck
dnops fsck
Verify: the tree-head pinax bytes encode a tree-state present in the
cella; every tree-state in the chain parses; the pipeline identity is
constant across the chain and references a pipeline-declaration blob
present in the cella; every tree-manifest parses, its entries
array is sorted ascending in UUID v7 order, and every referenced tree
root is a tree-node blob present in the cella; every tree-node blob
parses, has consistent keyed/no-key per
§4.2, and has sorted child arrays where required;
every reachable child of kind = leaf is present in metatheca’s
cella; every tree-state’s metatheca field names a metatheca
state present in metatheca’s cella. Implementations MAY rely on
syntheca and apotheca verification for byte-level integrity.
7.10 dnops destroy
dnops destroy [--force] [<path>]
Destroy the dendropsis cella per §5.10. The vault root
is <path> if supplied, otherwise the current directory. Unless
--force is supplied, implementations SHOULD prompt for
confirmation before proceeding, since destroy is irreversible without
a reindex (§5.4) to rebuild the chain — and
reinitializing with a different pipeline produces tree hashes
that differ from any previously published. Exits non-zero if no
dendropsis cella exists at the resolved path.
7.11 Reserved
The verb gc is reserved for future phases.
8 Errors
The error conditions defined by this specification are:
NotFound— a tree-stateref, entry, or referenced blob did not resolve.Ambiguous— a hash-prefix tree-stateref matched more than one tree-state.Conflict— tree-head CAS lost a race; the caller MAY retry (§5.4).Malformed— a wire-format blob did not parse per §4 (including invalid UTF-8 in strings, child-ref ordering violations, mixed-keyed nodes, manifest sort violations, or pipeline graph violations), or an argument did not parse per §1.10.PipelineMismatch— a reindex attempted to change the pipeline identity fixed at genesis (§5.4), or a tree-node blob’s child references reference a tree built under a different pipeline (detected by an implementation’s choice of provenance tracking; not load-bearing in Phase 1).SegmenterError— a configured segmenter returned an error or violated its determinism contract. Implementation-defined details accompany the error.IntegrityError— inherited from syntheca / apotheca; surfaces unchanged.
Implementations MAY surface additional implementation-defined errors (I/O failure, segmenter unavailable, …); such errors MUST be distinguishable from the protocol-defined errors above.
9 Conformance
A Phase 1 implementation MUST implement:
§2 (host/extension relationship)
§3 (identifiers, time, strings)
§4 (wire formats)
§5.1–§5.10 (operations), including pipeline-identity pinning and inheritance, and the destroy operation
§6 (tree-head pinax)
§7 (CLI), with the exception that
--as-ofMAY be omitted fromdnops tree,dnops walk,dnops diff, anddnops movesif the implementation always operates againstcurrent§8 (errors)
A Phase 1 implementation MAY omit:
§10 items.
10 Out of scope (Phase 1)
The following are deliberately out of Phase 1 scope. Their absence is load-bearing for the v0.1 surface and consumers MUST NOT rely on them.
Pipeline migration. A tree-state’s pipeline identity is immutable across the chain. Switching pipelines (upgrading a segmenter version, adding a stage, changing composition) requires destroying the dendropsis chain (§5.10) and re-running
dnops init. A future phase may define rotation operations that preserve the chain.Incremental reindex. Phase 1 reindex rebuilds all trees from scratch. Content-addressing means unchanged subtrees deduplicate at the cella, but the work of recomputing trees is full each time. A consumer-level optimization — skip decomposition when an entry’s
(blob, pipeline)pair is unchanged from the prior manifest — is permitted but not specified; future phases will specify it normatively.Binary tree-node format. Phase 1 uses canonical JSON for tree nodes (§4.2). For dense trees with millions of nodes the JSON overhead is significant; a Phase 2 binary format with the same logical content and stable hashes is anticipated.
External-pointer child references. Phase 1 child references are either
leaf(syntheca depositum) ortree(dendropsis tree-node). A future phase may add anexternalkind referencing a metatheca entry UUID directly, useful for trees whose nodes correspond to addressable metatheca entries (e.g. directory entries that should retain stable identity through tree restructuring).Reverse-lookup index. "What trees reference this leaf/subtree?" — high-value for citation, refactor-extract, and quotation tracking — is deferred. Likely emerges as a sibling extension or a Phase 2 query over the manifest chain; not a Phase 1 query.
Specialized large-list diff. Phase 1 unkeyed-list diff uses LCS (§5.6). For lists with millions of children, LCS is too expensive; Phase 2 will offer specialized algorithms (Myers, histogram, custom) selectable per pipeline role.
Conflict resolution for concurrent edits. A merge-shaped operation that combines two tree-states under conflict rules is out of scope. Consumers (e.g. version-control systems) implement their own merge semantics above the protocol.
Garbage collection. Old tree-states and their referenced manifests, tree nodes, and leaves remain on the cella indefinitely. Blocked by absence of delete in apotheca and syntheca.
Cross-vault tree sharing. As logopsis §10.
Segmenter implementations. dendropsis specifies the segmenter contract (§1.1) but ships no segmenters. Consumer projects ship segmenters appropriate to their domain (literary text, markdown, code, filesystem, structured records).
11 Future work (non-normative)
Binary tree-node format. A Phase 2 format with a small fixed header, compact child-record encoding (varint counts, raw 32-octet hashes, length-prefixed keys), and streaming-friendly serialization. Stable hashes require a canonical byte representation; the dictionary entry would gain a
formatdiscriminator so old and new nodes can coexist on the same cella during migration.External-pointer child references. A child reference with
kind = externalwhosehashis replaced by a metatheca entry UUID. Useful when a subtree corresponds to an addressable metatheca entity with its own identity, history, and facts — a directory’s files, a citation graph’s referenced documents, a refactoring target’s promoted methods.Pipeline rotation. A new tree-state kind that records both an old and a new pipeline identity, allowing graceful migration without destroying the chain. The reindex computes new trees under the new pipeline; consumers gain a transition period during which both pipelines’ trees coexist. Useful for segmenter-version upgrades, segmenter-set extensions, and pipeline-shape evolution.
Incremental reindex. Skip decomposition for entries whose
(blob, pipeline)pair already appears in a recent manifest; only recompute trees for entries whose underlying blob has advanced. The optimization is consumer-visible only as faster reindex; results are identical to a full rebuild.Reverse-lookup index as a Phase 2 sibling extension or a dendropsis Phase 2 query. The index maps
leaf hash → list of trees containing it (with position)andsubtree hash → list of trees containing it (with position). Citation queries, refactor-extract detection, and cross-document quotation graphs all become single index lookups.Specialized large-list diff algorithms. Phase 2 will allow a role to declare its preferred diff algorithm (LCS for short ordered lists, histogram for ordered code-like lists, set-diff for keyed maps, Myers with heuristics for long ordered lists). The choice is per-role, recorded in the pipeline declaration.
Skip-list pointers in the tree-state blob, mirroring metatheca SPEC §11 and logopsis SPEC §11, to make as-of resolution sublinear in chain length.
Pipeline-aware tree provenance. A tree-node blob optionally carries a
pipelinefield naming the pipeline identity under which it was produced. EnablesPipelineMismatchdetection at walk time when consumers (incorrectly) attempt to compose nodes built under different pipelines. Not load-bearing in Phase 1 because cella separation already prevents most misuse.A remote-cella backend (apotheca §10) makes the per-node-depositum layout natural for object-storage deployment. Tree walks fan out by
O(depth)round trips withO(fan-out)reads per level; partial materialization (§5.5) keeps the per-query byte cost bounded. dendropsis on R2 is well-suited to object-storage deployment.