Skip to main content

macrame/
branch.rs

1//! Lineage identity and its lifecycle (§15.2, §15.4).
2//!
3//! The read half of branching shipped first, at 0.14.2 through 0.14.6: the
4//! ledger tables carry a `branch_id`, [`crate::graph::TraversalBuilder::on_branch`]
5//! resolves along the ancestry, and 0.14.6 bounded that resolution by the fork
6//! point ([D-223]). Nothing in that half could *create* a lineage — only raw
7//! SQL could, which is why 0.14.6 could repair the read's semantics without
8//! breaking anyone. This module is the other half: the name, and the one write
9//! that registers it.
10//!
11//! # Why the read shipped first, and why that ordering is not an accident
12//!
13//! A write that creates something unreadable is the worse order. Had `fork()`
14//! landed at 0.14.2, every branch created between then and 0.14.6 would have
15//! been readable only through a query that silently absorbed its parent's later
16//! writes — and the repair would have been a semantic break on stored data
17//! rather than a correction to an unreachable path. That is [D-160] → [D-174]'s
18//! ordering, applied a third time and recorded in [D-223].
19//!
20//! [D-160]: ../../docs/architecture/s13-decision-register.md#d-160
21//! [D-174]: ../../docs/architecture/s13-decision-register.md#d-174
22//! [D-223]: ../../docs/architecture/s13-decision-register.md#d-223
23
24use crate::error::{DbError, Result};
25use crate::schema::ddl;
26
27/// Longest accepted lineage name.
28///
29/// A sanity bound rather than a schema limit — `branches.branch_id` is `TEXT`
30/// and SQLite would take a megabyte. It is set well above every identifier
31/// shape the motivating use case generates (a ULID is 26 characters, a
32/// hyphenated UUID 36, a path-like `turn/17/alt/3` shorter still) and well
33/// below anything that would make a `branches` listing unreadable.
34pub const MAX_BRANCH_ID: usize = 128;
35
36/// A validated lineage name.
37///
38/// # This is not [`ModelName`](crate::vector::ModelName)'s reason, and the rule
39/// is deliberately different
40///
41/// `ModelName` exists because a model name is spliced into a table identifier
42/// and SQLite cannot bind an identifier as a parameter, so the validation is
43/// what makes the splice safe. **None of that applies here.** A `branch_id`
44/// reaches SQL as a bound value at every one of its call sites; there is no
45/// splice to protect. Copying `ModelName`'s `[a-z][a-z0-9_]*` rule would be
46/// borrowing a justification that does not hold, and it would reject the two
47/// name shapes the use case in §15.5 actually produces — a hyphenated UUID and
48/// a path-like turn id.
49///
50/// What this type is for is narrower and has nothing to do with SQL:
51///
52/// * `branch_id` is a primary key that four ledger tables hold a foreign key
53///   into, and `branches` is append-only under two unconditional triggers
54///   ([`CREATE_BRANCHES_GUARD_UPDATE`](crate::schema::ddl::CREATE_BRANCHES_GUARD_UPDATE)).
55///   A name is therefore written **once** and can never be corrected — not by
56///   the crate, not by raw SQL. Validation has one chance, and this is it.
57/// * A name that differs from the caller's intent by a trailing space is not a
58///   typo, it is a **second lineage that reads as the first**. Every subsequent
59///   `on_branch("release ")` resolves to a different ancestry than
60///   `on_branch("release")`, both succeed, and neither reports anything. That
61///   is the failure shape this wave keeps finding, and it is cheapest to refuse
62///   at [D-034]'s boundary.
63///
64/// So the rule is: non-empty, at most [`MAX_BRANCH_ID`] bytes, no ASCII control
65/// characters, and no leading or trailing whitespace. Refused rather than
66/// trimmed, on §4.1's principle — a silent repair here becomes two lineages
67/// sharing one intent later.
68///
69/// # `main` is constructible and not forkable
70///
71/// [`Database::branches`](crate::Database::branches) returns the trunk and
72/// every read may name it, so `BranchId::new("main")` must succeed. What is
73/// refused is *creating* it a second time, and that refusal belongs to
74/// [`Database::fork`](crate::Database::fork) rather than to this type: the
75/// trunk is a lineage like any other to a reader, and only a writer has cause
76/// to care that it already exists.
77///
78/// [D-034]: ../../docs/architecture/s13-decision-register.md#d-034
79#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
80pub struct BranchId(String);
81
82impl BranchId {
83    /// Validate `raw` as a lineage name, or explain why it is not one.
84    pub fn new(raw: impl AsRef<str>) -> Result<Self> {
85        let raw = raw.as_ref();
86        let invalid = || DbError::InvalidBranchId(raw.to_string());
87
88        if raw.is_empty() || raw.len() > MAX_BRANCH_ID {
89            return Err(invalid());
90        }
91        if raw.chars().any(|c| c.is_control()) {
92            return Err(invalid());
93        }
94        // `trim` and not `trim_ascii`: a non-breaking space is invisible in
95        // every terminal this name will be read in, which is the whole argument
96        // above for refusing the ASCII one.
97        if raw.trim() != raw {
98            return Err(invalid());
99        }
100        Ok(Self(raw.to_string()))
101    }
102
103    /// The trunk, which every database has from its first migration.
104    pub fn main() -> Self {
105        Self(ddl::MAIN_BRANCH.to_string())
106    }
107
108    /// Adopt a name already stored in `branches`, without revalidating it.
109    ///
110    /// Infallible on purpose. `branches` may hold rows this type never saw:
111    /// written by raw SQL, or by a build older than 0.14.7, both of which the
112    /// schema permits and neither of which the append-only guards allow anyone
113    /// to repair. A listing that returned `Err` on one such row would be
114    /// unusable for the one thing it is for — finding out what is in there —
115    /// and would report the *listing* as broken rather than the row.
116    pub(crate) fn from_stored(raw: impl Into<String>) -> Self {
117        Self(raw.into())
118    }
119
120    /// The name, as it is stored.
121    pub fn as_str(&self) -> &str {
122        &self.0
123    }
124
125    /// Whether this names the trunk.
126    pub fn is_main(&self) -> bool {
127        self.0 == ddl::MAIN_BRANCH
128    }
129}
130
131impl std::fmt::Display for BranchId {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.write_str(&self.0)
134    }
135}
136
137impl AsRef<str> for BranchId {
138    fn as_ref(&self) -> &str {
139        &self.0
140    }
141}
142
143/// So a `BranchId` can be handed straight to the 0.14.4 read surface.
144///
145/// [`TraversalBuilder::on_branch`](crate::graph::TraversalBuilder::on_branch)
146/// and [`query_as_of_edges_on`](crate::temporal::query_as_of_edges_on) take
147/// `impl Into<String>`, and they shipped two releases before this type existed.
148/// This impl is what makes `on_branch(id)` compile rather than
149/// `on_branch(id.as_str())` — additive, and cheaper than widening four
150/// signatures that are already public.
151impl From<BranchId> for String {
152    fn from(id: BranchId) -> Self {
153        id.0
154    }
155}
156
157/// One row of `branches`: a lineage, its parent, and where it was cut.
158///
159/// # `#[non_exhaustive]` costs nothing here, and that is a fact about direction
160///
161/// [`EdgeBelief`](crate::temporal::EdgeBelief) needed a constructor to go with
162/// the attribute, because `save_snapshot` is public and *takes* one — without
163/// `EdgeBelief::new` the attribute would not have made the next field additive,
164/// it would have made a public function uncallable (0.14.5). This type only
165/// ever travels outward: [`Database::branches`](crate::Database::branches)
166/// returns it and nothing public accepts it. So the attribute buys the additive
167/// field and takes nothing back, and no constructor is owed. §15.4's
168/// abandonment arm is the field it is being kept open for.
169#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
170#[non_exhaustive]
171pub struct Branch {
172    /// The lineage's own name.
173    pub id: BranchId,
174    /// The lineage it was cut from, or `None` for the trunk.
175    pub parent: Option<BranchId>,
176    /// The transaction-time instant it was cut at, or `None` for the trunk.
177    ///
178    /// This is the visibility cutoff 0.14.6 reads: the branch sees its parent's
179    /// history up to and including this instant, and nothing the parent records
180    /// after it ([D-223]).
181    ///
182    /// [D-223]: ../../docs/architecture/s13-decision-register.md#d-223
183    pub forked_at: Option<String>,
184    /// When the row was written.
185    ///
186    /// A separate column from `forked_at` rather than a duplicate of it, and
187    /// the two are equal for every branch this release can create. They are
188    /// separate so that forking from a *past* instant is an additive change
189    /// later: a historical fork has a `forked_at` behind its `created_at`, and
190    /// the schema has always allowed it — `CHECK (forked_at <= created_at)`.
191    pub created_at: String,
192}
193
194/// One lineage's handle on the ledger (§15.4, 0.14.9, [D-226]).
195///
196/// A `Database` plus a [`BranchId`], so a caller who forked writes and reads
197/// through the fork instead of naming it at every call. Every operation here
198/// exists on [`Database`](crate::Database) already and takes a lineage there —
199/// **this type buys ergonomics and no capability**, which is what makes it the
200/// last piece of §15.4's first bullet rather than a fifth release of it.
201///
202/// ```no_run
203/// # use std::sync::Arc;
204/// # use macrame::graph::EdgeAssertion;
205/// # use macrame::{Database, BranchId};
206/// # async fn f(db: Arc<Database>) -> macrame::Result<()> {
207/// let alt = db.fork(BranchId::new("turn/17/alt/1")?, BranchId::main()).await?;
208/// let view = db.view(alt.id);
209///
210/// view.assert_edge(EdgeAssertion::new("a", "b", "CITES").valid_from(ts())).await?;
211/// let seen = view.traversal("a").execute_ids(view.read_conn(), ts()).await?;
212/// # Ok(())
213/// # }
214/// # fn ts() -> &'static str { "2020-01-01T00:00:00.000000Z" }
215/// ```
216///
217/// # It holds an `Arc<Database>` and cannot close it
218///
219/// [`Database::close`](crate::Database::close) takes `self` by value, and an
220/// `Arc` cannot give that up while any clone survives. So the borrow is
221/// structural rather than a documented request: a caller who forks a view,
222/// reads it and drops it is not one call away from stopping the actor everyone
223/// else is using. That is why the view is a **separate type** over an
224/// `Arc<Database>` and not a `Database` with a field added, and it is the same
225/// argument [D-203] made when `Database: Clone` was declined — a handle that can
226/// be cloned freely must not carry the right to end the thing it handles.
227///
228/// `Clone` is therefore free of that concern and is derived: the view owns no
229/// lifecycle, so cloning it is cloning an `Arc` and a short string.
230///
231/// # What it does with an assertion that names a lineage
232///
233/// It stamps its own on one that names none, and **refuses** one that names a
234/// different lineage with [`DbError::BranchMismatch`].
235/// Stamping over is the shape a caller building through the view produces and
236/// costs nothing; relabelling a write that already named somewhere else would
237/// discard a belief rather than contradict it. See that variant for the failure
238/// it catches.
239///
240/// [D-203]: ../../docs/architecture/s13-decision-register.md#d-203
241/// [D-226]: ../../docs/architecture/s13-decision-register.md#d-226
242#[derive(Clone)]
243pub struct BranchView {
244    db: std::sync::Arc<crate::Database>,
245    branch: BranchId,
246}
247
248/// Hand-written because [`Database`](crate::Database) is not `Debug` — it owns
249/// a connection, a channel and a clock, none of which prints usefully. What a
250/// reader of this type wants is which lineage against which file, so that is
251/// what it prints.
252impl std::fmt::Debug for BranchView {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        f.debug_struct("BranchView")
255            .field("branch", &self.branch)
256            .field("path", &self.db.path())
257            .finish()
258    }
259}
260
261impl BranchView {
262    /// Bind a lineage to a handle.
263    ///
264    /// Infallible and does no I/O: the name is already validated by
265    /// [`BranchId`], and whether it is *registered* is a question every
266    /// operation on this view asks for itself, answering
267    /// [`DbError::UnknownBranch`] by name. A
268    /// constructor that checked would buy one round trip's worth of earlier
269    /// notice and cost the type its `const`-cheapness, and the check would be
270    /// stale by the next call anyway — `branches` is append-only, but the view
271    /// outlives the answer.
272    pub fn new(db: std::sync::Arc<crate::Database>, branch: BranchId) -> Self {
273        Self { db, branch }
274    }
275
276    /// The lineage this view reads and writes.
277    pub fn id(&self) -> &BranchId {
278        &self.branch
279    }
280
281    /// The handle underneath, for the operations that are not lineage-scoped.
282    ///
283    /// `archive`, `checkpoint`, `verify` and the rest are properties of the
284    /// file rather than of a lineage, so they are reached through here rather
285    /// than duplicated onto a view that would answer the same thing for every
286    /// branch.
287    pub fn database(&self) -> &std::sync::Arc<crate::Database> {
288        &self.db
289    }
290
291    /// The read connection, so a [`TraversalBuilder`] from
292    /// [`Self::traversal`] can be executed without reaching for the handle.
293    ///
294    /// [`TraversalBuilder`]: crate::graph::TraversalBuilder
295    pub fn read_conn(&self) -> &libsql::Connection {
296        self.db.read_conn()
297    }
298
299    /// A traversal already pointed at this lineage.
300    ///
301    /// The one read this type needs to wrap, because everything downstream of
302    /// it — `execute_ids`, `execute`,
303    /// [`load_subgraph_with`](crate::Database::load_subgraph_with) — takes the
304    /// lineage *from the builder*. Seeding it here is therefore the whole of
305    /// the read side rather than a first method of several.
306    pub fn traversal(&self, start_node: impl Into<String>) -> crate::graph::TraversalBuilder {
307        crate::graph::TraversalBuilder::new(start_node).on_branch(self.branch.as_str())
308    }
309
310    /// [`Database::load_subgraph`](crate::Database::load_subgraph) on this
311    /// lineage.
312    ///
313    /// The sugar form has no builder to carry the branch, so it is wrapped;
314    /// `load_subgraph_with` is not, because a builder from [`Self::traversal`]
315    /// already carries it.
316    pub async fn load_subgraph(
317        &self,
318        start_node: &str,
319        max_hops: u32,
320        now_ts: &str,
321        byte_budget: usize,
322    ) -> Result<crate::graph::Subgraph> {
323        self.db
324            .load_subgraph_with(
325                &self
326                    .traversal(start_node)
327                    .max_depth(max_hops as usize)
328                    .min_weight(f64::NEG_INFINITY),
329                now_ts,
330                byte_budget,
331            )
332            .await
333    }
334
335    /// Every edge this lineage believes in at `ts`.
336    #[allow(clippy::type_complexity)]
337    pub async fn query_as_of_edges(
338        &self,
339        ts: &str,
340    ) -> Result<Vec<(String, String, String, String, String)>> {
341        crate::temporal::query_as_of_edges_on(self.read_conn(), ts, Some(self.branch.as_str()))
342            .await
343    }
344
345    /// Assert an edge on this lineage.
346    /// What this lineage believes that `other` does not.
347    ///
348    /// [`Database::diff`](crate::Database::diff) with this view's lineage as
349    /// the first argument. The direction matters and is not symmetric: this
350    /// answers *what did I conclude that they do not know*, which is the
351    /// question the view's holder is in a position to ask.
352    pub async fn diff(&self, other: &BranchId) -> Result<Vec<Divergence>> {
353        diff(self.read_conn(), &self.branch, other).await
354    }
355
356    pub async fn assert_edge(&self, edge: crate::graph::EdgeAssertion) -> Result<()> {
357        self.db.assert_edge(self.claim_edge(edge)?).await
358    }
359
360    /// Retire an edge on this lineage — [`Database::retire_edge_on`] without
361    /// the argument.
362    ///
363    /// An inherited edge is retired by writing this lineage's **own** closed
364    /// row at the ancestor's key; the parent's row is never touched.
365    ///
366    /// [`Database::retire_edge_on`]: crate::Database::retire_edge_on
367    pub async fn retire_edge(
368        &self,
369        source: impl Into<String>,
370        target: impl Into<String>,
371        edge_type: impl Into<String>,
372        valid_from: &str,
373        valid_to: &str,
374    ) -> Result<()> {
375        self.db
376            .retire_edge_on(
377                source,
378                target,
379                edge_type,
380                valid_from,
381                valid_to,
382                self.branch.clone(),
383            )
384            .await
385    }
386
387    /// Mint a concept on this lineage.
388    ///
389    /// A branch **inherits** its parent's concepts and may not restate one:
390    /// `concepts` is keyed by identity, so that is
391    /// [`DbError::CrossLineage`].
392    pub async fn upsert_concept(&self, concept: crate::ConceptUpsert) -> Result<()> {
393        self.db.upsert_concept(self.claim_concept(concept)?).await
394    }
395
396    /// [`Database::write_bulk_atomic`](crate::Database::write_bulk_atomic) with
397    /// every edge on this lineage.
398    pub async fn write_bulk_atomic(
399        &self,
400        edges: Vec<crate::graph::EdgeAssertion>,
401    ) -> Result<usize> {
402        self.db.write_bulk_atomic(self.claim_edges(edges)?).await
403    }
404
405    /// [`Database::bulk_import`](crate::Database::bulk_import) with every edge
406    /// on this lineage.
407    pub async fn bulk_import(
408        &self,
409        edges: Vec<crate::graph::EdgeAssertion>,
410    ) -> crate::error::BulkResult<usize> {
411        let edges = self
412            .claim_edges(edges)
413            .map_err(|cause| crate::error::BulkInterrupted { written: 0, cause })?;
414        self.db.bulk_import(edges).await
415    }
416
417    /// [`Database::write_concepts`](crate::Database::write_concepts) with every
418    /// concept on this lineage.
419    pub async fn write_concepts(
420        &self,
421        concepts: Vec<crate::ConceptUpsert>,
422    ) -> crate::error::BulkResult<usize> {
423        let concepts = concepts
424            .into_iter()
425            .map(|c| self.claim_concept(c))
426            .collect::<Result<Vec<_>>>()
427            .map_err(|cause| crate::error::BulkInterrupted { written: 0, cause })?;
428        self.db.write_concepts(concepts).await
429    }
430
431    /// Refuse a foreign lineage, stamp an unnamed one.
432    fn claim_edge(&self, edge: crate::graph::EdgeAssertion) -> Result<crate::graph::EdgeAssertion> {
433        match &edge.branch {
434            Some(named) if named != &self.branch => Err(DbError::BranchMismatch {
435                view: self.branch.as_str().to_string(),
436                named: named.as_str().to_string(),
437            }),
438            Some(_) => Ok(edge),
439            None => Ok(edge.on_branch(self.branch.clone())),
440        }
441    }
442
443    /// [`Self::claim_edge`] for a batch, refusing on the **first** foreign
444    /// lineage rather than reporting all of them.
445    ///
446    /// A batch that names two lineages is a caller error about the batch, not a
447    /// list of independent mistakes, and the first one names the confusion as
448    /// well as the tenth would.
449    fn claim_edges(
450        &self,
451        edges: Vec<crate::graph::EdgeAssertion>,
452    ) -> Result<Vec<crate::graph::EdgeAssertion>> {
453        edges.into_iter().map(|e| self.claim_edge(e)).collect()
454    }
455
456    /// [`Self::claim_edge`] for a concept.
457    fn claim_concept(&self, concept: crate::ConceptUpsert) -> Result<crate::ConceptUpsert> {
458        match &concept.branch {
459            Some(named) if named != &self.branch => Err(DbError::BranchMismatch {
460                view: self.branch.as_str().to_string(),
461                named: named.as_str().to_string(),
462            }),
463            Some(_) => Ok(concept),
464            None => Ok(concept.on_branch(self.branch.clone())),
465        }
466    }
467}
468
469/// Register a lineage, refusing the three things a `CHECK` cannot (§15.2).
470///
471/// Runs inside the write actor, so the three reads and the insert are one turn
472/// against one connection and nothing can register a colliding name between the
473/// check and the write.
474///
475/// # What the schema already refuses, and what is left over
476///
477/// `branches` carries a foreign key on `parent_id`, a primary key on
478/// `branch_id`, and two row-local `CHECK`s. So a missing parent and a duplicate
479/// name would both fail at the engine anyway — they are checked here to be
480/// *named*, because `classify` would otherwise surface a duplicate fork as a
481/// constraint violation naming a column, and the caller's question was about a
482/// branch.
483///
484/// # The third refusal, and the invariant that turned out not to be checkable
485///
486/// [`CREATE_BRANCHES_TABLE`]'s comment left this to `fork()`: *"the fork point
487/// is at or after the parent's creation" is not [row-local], and a `CHECK`
488/// cannot see another row. The cross-row half is `fork()`'s to enforce at
489/// D-034's boundary.* Enforcing it as written **refuses every fork on every
490/// injected-clock database in the crate**, and the reason is not the clock the
491/// test chose. `seed_root_branch` stamps `main.created_at` from
492/// `SystemTime::now()` inside `migrations::run`, which runs *before* the
493/// database's clock is resolved — the floor that clock is raised against is
494/// read from tables the migration has to create first, so the order cannot
495/// simply be swapped. **`branches.created_at` is therefore not on the ledger's
496/// timeline**, and on a [`FakeClock`](crate::util::FakeClock) database it sits
497/// years in the future of every row.
498///
499/// What *is* comparable is `forked_at`: every one of them is issued by this
500/// function from the same clock as every `recorded_at`. So the check is against
501/// the parent's fork point, and the trunk — whose `forked_at` is `NULL` because
502/// it was cut from nothing — constrains nothing, which is right: as far as any
503/// ledger row is concerned the trunk has always existed.
504///
505/// The narrower rule is also the one worth having. It makes fork points
506/// **non-decreasing down any root path**, which is precisely the property
507/// [`ancestry_cte`](crate::graph::lineage) clamps for defensively. The clamp
508/// stays — `branches` accepts raw-SQL rows this function never saw — but for
509/// rows the crate wrote it is now a belt beside braces rather than the only
510/// thing holding the shape.
511///
512/// What it refuses is a fork point earlier than its parent's. That branch would
513/// inherit **nothing whatever from the parent it names** — every row the parent
514/// wrote is after the parent's own fork point, so all of them fall past the
515/// child's cutoff — leaving a lineage whose `parent_id` says one thing and
516/// whose visible history says another. A sibling wearing a child's parent
517/// pointer, and silent about it.
518///
519/// [`CREATE_BRANCHES_TABLE`]: crate::schema::ddl::CREATE_BRANCHES_TABLE
520pub(crate) async fn fork(
521    conn: &libsql::Connection,
522    name: &BranchId,
523    parent: &BranchId,
524    stamp: &str,
525) -> Result<Branch> {
526    // `EXISTS` separately from `forked_at`, because the trunk's `forked_at` is
527    // legitimately NULL and a single nullable column cannot tell "no such
528    // branch" apart from "the root".
529    let mut rows = conn
530        .query(
531            "SELECT (SELECT COUNT(*) FROM branches WHERE branch_id = ?1), \
532                    (SELECT COUNT(*) FROM branches WHERE branch_id = ?2), \
533                    (SELECT forked_at FROM branches WHERE branch_id = ?2)",
534            libsql::params![name.as_str(), parent.as_str()],
535        )
536        .await?;
537    let row = rows.next().await?.ok_or_else(|| {
538        // A three-aggregate SELECT always returns a row; if it did not, the
539        // honest report is that the parent could not be established.
540        DbError::UnknownBranch(parent.as_str().to_string())
541    })?;
542    let taken: i64 = row.get(0)?;
543    let parent_exists: i64 = row.get(1)?;
544    let parent_forked_at: Option<String> = row.get(2)?;
545
546    if taken > 0 {
547        return Err(DbError::BranchExists(name.as_str().to_string()));
548    }
549    if parent_exists == 0 {
550        return Err(DbError::UnknownBranch(parent.as_str().to_string()));
551    }
552    if let Some(parent_forked_at) = parent_forked_at {
553        if stamp < parent_forked_at.as_str() {
554            return Err(DbError::ForkPrecedesParent {
555                branch: name.as_str().to_string(),
556                parent: parent.as_str().to_string(),
557                forked_at: stamp.to_string(),
558                parent_forked_at,
559            });
560        }
561    }
562
563    conn.execute(
564        "INSERT INTO branches (branch_id, parent_id, forked_at, created_at) \
565         VALUES (?1, ?2, ?3, ?3)",
566        libsql::params![name.as_str(), parent.as_str(), stamp],
567    )
568    .await?;
569
570    Ok(Branch {
571        id: name.clone(),
572        parent: Some(parent.clone()),
573        forked_at: Some(stamp.to_string()),
574        created_at: stamp.to_string(),
575    })
576}
577
578/// One belief `a` holds that `b` does not (§15.4, 0.14.11, [D-228]).
579///
580/// Returned by [`Database::diff`](crate::Database::diff), one per edge key on
581/// which the two lineages disagree, ordered by that key. The fields describe
582/// **`a`'s** side; `b`'s side is `diff(b, a)`, which is the same question asked
583/// the other way round.
584///
585/// # `branch_id` is the interesting field
586///
587/// It is the lineage on `a`'s ancestry that actually holds this row, and it is
588/// what separates *what this exploration concluded* from *what it still holds
589/// while the trunk moved on*. When it equals `a`, the divergence is `a`'s own
590/// assertion. When it does not, `a` is retaining an ancestor's belief that `b`
591/// no longer shares — which happens whenever `b` churned the key after `a`
592/// forked, and is exactly the case that makes "the divergence is the set of
593/// rows carrying the branch's own id" false. See [D-228] for the two shapes
594/// that break it.
595///
596/// # No constructor, and no `Eq`
597///
598/// `#[non_exhaustive]` with nothing owed, for [`Branch`]'s reason: this type
599/// only ever travels outward and nothing public accepts one, so the attribute
600/// buys the additive field and takes nothing back.
601///
602/// `weight` is an `f64`, so `Eq`, `Ord` and `Hash` are not derivable. That is
603/// a real difference from [`EdgeBelief`](crate::temporal::EdgeBelief), which
604/// carries no weight and is compared as a canonical form by the snapshot
605/// suite. Ordering here comes from the query instead, on the edge key, so two
606/// diffs of the same pair are still directly comparable.
607///
608/// [D-228]: ../../docs/architecture/s13-decision-register.md#d-228
609#[derive(Debug, Clone, PartialEq, PartialOrd)]
610#[non_exhaustive]
611pub struct Divergence {
612    /// The edge's source concept.
613    pub source_id: String,
614    /// The edge's target concept.
615    pub target_id: String,
616    /// The edge's type.
617    pub edge_type: String,
618    /// When the asserted interval opens — part of the key, not of the belief.
619    pub valid_from: String,
620    /// When `a` believes the interval closes, or the open sentinel.
621    pub valid_to: String,
622    /// The weight `a` believes.
623    pub weight: f64,
624    /// The lineage on `a`'s ancestry holding this row. See the type docs.
625    pub branch_id: String,
626}
627
628/// The beliefs `a` holds that `b` does not (§15.4, 0.14.11, [D-228]).
629///
630/// # What the plan expected, and the two shapes that break it
631///
632/// §15.4 says divergence "is exactly the set of rows carrying the branch's own
633/// id", and that is true only when `b` is `a`'s parent **and has not churned
634/// since the fork**. Two counterexamples, both ordinary:
635///
636/// * `b` retires or reweights an inherited edge after `a` forked. `a` still
637///   sees the pre-fork version — that is the whole of
638///   [D-223](../../docs/architecture/s13-decision-register.md#d-223) — so `a`
639///   believes something `b` does not, and the row carries **`b`'s** id, or the
640///   trunk's, never `a`'s.
641/// * `a` and `b` are siblings and `b` shadow-retires an edge both inherited.
642///   `a` believes it and `b` does not, and the row belongs to their common
643///   ancestor.
644///
645/// A row-provenance answer misses both, and it also *adds* one it should not:
646/// `a` re-asserting an inherited edge at the value it already had writes a row
647/// on `a` and changes no belief. So the answer is a difference of the two
648/// resolved views, which is what the reader already computes for one lineage
649/// at a time, and the cheap characterisation is a special case rather than the
650/// definition.
651///
652/// # Cost, and the narrowing that is recorded rather than built
653///
654/// This is O(ledger): `a`'s visible set is the whole ledger from `a`'s point of
655/// view, so the join has to see all of it. The narrowing is real and known —
656/// keys that both lineages resolve to the *same* lineage are identical by
657/// construction and cannot differ, so only keys held on the symmetric
658/// difference of the two ancestries can — but it is not built here, per F-33:
659/// the shape is understood, the trigger is a branched workload large enough to
660/// notice, and there is no measurement yet that says it is worth the second
661/// query shape.
662///
663/// [D-228]: ../../docs/architecture/s13-decision-register.md#d-228
664pub(crate) async fn diff(
665    conn: &libsql::Connection,
666    a: &BranchId,
667    b: &BranchId,
668) -> Result<Vec<Divergence>> {
669    use crate::graph::lineage::{lineage_shape, LineageShape};
670
671    // Both names are checked before any work, and each refusal names its own
672    // lineage rather than the pair — a caller who mistyped one wants to know
673    // which one.
674    let shape = lineage_shape(conn, Some(a.as_str())).await?;
675    lineage_shape(conn, Some(b.as_str())).await?;
676    if shape == LineageShape::Trunk {
677        // `Trunk` is `branches` holding one row, and both names were just found
678        // in it, so `a` and `b` are the same lineage and their views are equal
679        // by construction. Exact rather than an optimisation, for the reason
680        // `lineage_shape` gives about its own sufficient condition — and it is
681        // reached only by `diff(main, main)` on a ledger that never forked.
682        return Ok(Vec::new());
683    }
684
685    let mut rows = conn
686        .query(
687            &crate::graph::lineage::diff_sql(),
688            libsql::params![a.as_str(), b.as_str()],
689        )
690        .await?;
691    let mut out = Vec::new();
692    while let Some(row) = rows.next().await? {
693        out.push(Divergence {
694            source_id: row.get(0)?,
695            target_id: row.get(1)?,
696            edge_type: row.get(2)?,
697            valid_from: row.get(3)?,
698            valid_to: row.get(4)?,
699            weight: row.get(5)?,
700            branch_id: row.get(6)?,
701        });
702    }
703    Ok(out)
704}
705
706/// Every lineage the ledger knows about, trunk first, then by creation.
707///
708/// Read through the read connection rather than the actor: `branches` is
709/// append-only, so a listing cannot be torn by a concurrent write in any way a
710/// caller could act on — the worst a racing `fork` can do is not appear yet.
711///
712/// Ordered rather than left to the engine, and ordered by `created_at` rather
713/// than by name, because the useful reading of this list is the shape of the
714/// tree over time. The trunk is pinned first because it is the one row that is
715/// always there and is nobody's child.
716pub(crate) async fn list(conn: &libsql::Connection) -> Result<Vec<Branch>> {
717    let mut rows = conn
718        .query(
719            "SELECT branch_id, parent_id, forked_at, created_at FROM branches \
720             ORDER BY (parent_id IS NOT NULL), created_at, branch_id",
721            (),
722        )
723        .await?;
724    let mut out = Vec::new();
725    while let Some(row) = rows.next().await? {
726        out.push(Branch {
727            id: BranchId::from_stored(row.get::<String>(0)?),
728            parent: row.get::<Option<String>>(1)?.map(BranchId::from_stored),
729            forked_at: row.get(2)?,
730            created_at: row.get(3)?,
731        });
732    }
733    Ok(out)
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739
740    #[test]
741    fn accepts_the_name_shapes_the_use_case_generates() {
742        for ok in [
743            "main",
744            "b9",
745            "01ARZ3NDEKTSV4RRFFQ69G5FAV",           // ULID
746            "3f2504e0-4f89-11d3-9a0c-0305e82c3301", // hyphenated UUID
747            "turn/17/alt/3",                        // path-like turn id
748            "explore Kant's second critique",       // a human sentence
749        ] {
750            assert_eq!(BranchId::new(ok).unwrap().as_str(), ok);
751        }
752    }
753
754    /// The two that matter are the whitespace pair: each is a second lineage
755    /// that reads as the first everywhere it is printed.
756    #[test]
757    fn refuses_what_cannot_be_corrected_later() {
758        for bad in [
759            "",
760            " release", // leading space
761            "release ", // trailing space
762            "release\n",
763            "rel\tease", // control character in the middle
764            "rel\0ease",
765        ] {
766            assert!(
767                BranchId::new(bad).is_err(),
768                "{bad:?} was accepted, and `branches` is append-only"
769            );
770        }
771        assert!(BranchId::new("x".repeat(MAX_BRANCH_ID)).is_ok());
772        assert!(BranchId::new("x".repeat(MAX_BRANCH_ID + 1)).is_err());
773    }
774
775    /// `ModelName`'s rule is the one this type is most likely to be "fixed" to
776    /// match. Pinned so the fix has to argue with a test.
777    #[test]
778    fn the_rule_is_not_model_names_rule() {
779        for accepted_here_rejected_there in ["Release-1", "3f2504e0-4f89", "a.b"] {
780            assert!(BranchId::new(accepted_here_rejected_there).is_ok());
781            assert!(crate::vector::ModelName::new(accepted_here_rejected_there).is_err());
782        }
783    }
784
785    #[test]
786    fn the_trunk_knows_itself() {
787        assert!(BranchId::main().is_main());
788        assert!(BranchId::new("main").unwrap().is_main());
789        assert!(!BranchId::new("mains").unwrap().is_main());
790    }
791}