Skip to main content

Module cause

Module cause 

Source
Expand description

Qualified cause references — naming a causal ancestor that lives in ANOTHER object store.

§Why this module exists

A node carries caused_by: Vec<String> (crate::store::Node), and every entry in it is a bare BLAKE2b object hash. The causal edges those hashes stand for are materialised in crate::graph as filesystem paths, graph/{from_hash}/{edge_type}/{to_hash}, and TRACE walks them.

That works for exactly as long as there is one object store, because with one store “the store” is not information — there is nothing to say. The moment branches exist as child stores it becomes the most important thing in the reference and it is the one thing a bare hash cannot express. A merge write in the destination store has to point at a node that was written in the branch’s store, and on read TRACE gets two bad outcomes and no good one:

hash absent from the reading store  → the edge dangles, trace truncates
hash present in BOTH stores         → resolves against the wrong one, silently

The second is the dangerous one. A dangling edge is a visible failure; a confidently-wrong resolution is a corrupt causal history that verifies. Content addressing makes the collision case unlikely-but-real: two stores that share ancestry genuinely DO hold the same hashes, because the same bytes hash the same way everywhere. That is the point of content addressing, and it is precisely why a hash alone cannot be a locator.

So a cause reference needs two parts — WHAT (the hash) and WHERE (the store) — while staying a plain string, because caused_by is Vec<String> on the wire today and changing the shape of a node is a storage-format break we are not willing to take for this.

§The format

local      64 hex chars                 e.g. 3f9a...c1   (legacy, still valid)
qualified  64 hex chars '@' store id    e.g. 3f9a...c1@branch-feature-x

Cause::Local is not a deprecated form to be migrated away from. It is the correct encoding of “this cause lives wherever I do”, which is what every intra-store edge means and what every node written before this module existed says. Those resolve against the reading store and always did; this module just gives that behaviour a name.

§Why @

The separator has to be a character that can appear in NEITHER side of the reference, or the encoding is ambiguous and the parse is a guess:

  • hex is [0-9a-f], so anything outside that alphabet is safe on the left;
  • store ids are STORE_ID_ALPHABET ([A-Za-z0-9_.-]), which excludes @ by construction, and StoreId::new REFUSES any id containing it rather than escaping it. An escape layer is a second encoding to get wrong, and a store id that can break the reference format is not a valid store id — it is a bug that has not been reported yet.

Among the characters satisfying that, @ is chosen over the alternatives for reasons that are operational rather than aesthetic:

  • : is not a legal filename character on Windows, and store ids and hashes both end up as path components in graph/ and objects/;
  • / is a path separator on every platform, so it would let a store id escape its directory;
  • #, ?, & are URL-significant and these strings appear in query strings and HTTP paths on the server surface;
  • @ is filesystem-safe everywhere, shell-safe unquoted, needs no URL escaping in a path segment, and already means “at this location” to every reader who has seen an email address.

Hash goes FIRST, store second — {hash}@{store} rather than {store}@{hash} — so that the hash occupies the same leading bytes in both variants. Every existing display path that abbreviates a cause by taking a prefix (&h[..8], the usual short-hash rendering) keeps showing a hash instead of suddenly showing a store name, and sorting a mixed list still groups by hash the way it does today. Grouping by store is the rarer query and can afford a parse.

§What is strict, and why

A hash must be exactly 64 LOWERCASE hex characters. Uppercase is refused, not normalised, and that is deliberate: normalising means 3F9A… and 3f9a… are two spellings of one reference, and a content-addressed system does not get to have two spellings of anything. The instant two exist, equality, deduplication, edge-path construction and set membership must all remember to canonicalise, one of them eventually forgets, and the graph grows a duplicate edge under a second path. Refusing at the boundary costs one error and buys the invariant everywhere downstream.

Structs§

StoreId
A validated store identifier.

Enums§

Cause
A reference to a causal ancestor.
CauseParseError
Why a cause reference was refused.
StoreIdError
Why a store id was refused.

Constants§

MAX_STORE_ID_LEN
Upper bound on a store id, in bytes.
SEPARATOR
Separator between hash and store id. See the module docs for why this character and not another one.
STORE_ID_ALPHABET
The characters a store id may contain, for error messages and docs.

Functions§

parse
Parse a cause reference.
render
Render a cause to its wire form.
target_store
Resolve a cause to the store it should be read from.