Skip to main content

blockworx_store/
handle.rs

1//! The store: a container and the [`Repo`] it holds, joined so that every
2//! accepted commit reaches the files before the call returns.
3//!
4//! This is the only door. A caller can read the repo but never write it,
5//! so "folded but not written" — the state a crash would silently keep —
6//! is not something an outside caller can produce.
7
8use std::path::Path;
9use std::time::Duration;
10
11use blockworx_doc::{
12    commit::Commit,
13    document::Document,
14    id::EntityRef,
15    repo::Repo,
16    rev::Rev,
17    trail::{Direction, Trail},
18};
19
20use super::Refusal;
21use super::container::{Access, Container, ContainerError, Drained, ReadOnlyReason, WriteRefusal};
22use super::manifest::{self, End, Fault, Row, RowKind, Tail};
23use super::record::{Attribution, Digest, Identity, WallTime};
24use super::revs;
25use super::storage::{Name, Storage, ready_now};
26use super::tags::{Tagging, Tags};
27
28/// Where a row's wall time comes from. The system clock is the only
29/// production value; the pinned arm is the seam a byte-stable golden and
30/// a repeatable test need, and it advances so a fixture's rows are
31/// ordered in time without depending on one.
32#[derive(Clone, Copy, Debug)]
33pub enum Clock {
34    System,
35    Pinned { at: WallTime, step: Duration },
36}
37
38impl Clock {
39    pub(super) fn tick(&mut self) -> WallTime {
40        match self {
41            Clock::System => super::history::now(),
42            Clock::Pinned { at, step } => {
43                let now = *at;
44                *at = WallTime::from_unix_millis(now.unix_millis() + step.as_millis() as u64);
45                now
46            }
47        }
48    }
49}
50
51/// Why a container could not be seeded from a session's commits — the
52/// Save-As-container path, which lays out a container and then replays a
53/// scratch session's commits into it through the ordinary write door.
54#[derive(Debug, thiserror::Error)]
55pub enum SeedFailure {
56    #[error(transparent)]
57    Create(#[from] ContainerError),
58    #[error("seeding stopped at commit {at}: {why}")]
59    Append { at: usize, why: Refusal },
60}
61
62/// What a history step is, before the repo has taken it.
63enum Step {
64    Edit(Commit),
65    /// A commit that is already the document's past rather than a step this
66    /// session took — the seeding path. Written like an edit, trailed like
67    /// nothing: no one may take back a step they did not make.
68    Seed(Commit),
69    Undo(Rev),
70    Redo(Rev),
71}
72
73pub struct Store<S: Storage> {
74    container: Container<S>,
75    repo: Repo,
76    /// Where this session's history stands. Beside the repo rather than
77    /// in it: a step adopts the rev copy this container holds, which is
78    /// something only the store can read (`docs/log-vs-snapshot.md`).
79    trail: Trail,
80    clock: Clock,
81    /// The digest the next row must name as its parent.
82    head: Digest,
83    /// Every row this container's manifest holds that spends a rev, in
84    /// rev order: `rows()[i]` describes rev `i + 1`. What the history
85    /// panel, the rev pick and `blockworx log` all read.
86    rows: Vec<Row>,
87    /// What the manifest's tag rows name its revs. Rebuilt at every open
88    /// and never handed to the repo.
89    tags: Tags,
90    tail: Tail,
91}
92
93impl<S: Storage> Store<S> {
94    /// Lay out a new container and open it at the empty document.
95    ///
96    /// # Errors
97    /// As [`Container::create`].
98    pub fn create(storage: S, clock: Clock) -> Result<Self, ContainerError> {
99        ready_now(Self::creating(storage, clock))
100    }
101
102    /// [`Self::create`] for a storage whose futures are not ready when they
103    /// are made.
104    ///
105    /// # Errors
106    /// As [`Self::create`].
107    pub async fn creating(storage: S, mut clock: Clock) -> Result<Self, ContainerError> {
108        let container = Container::creating(storage, clock.tick()).await?;
109        Ok(Self {
110            container,
111            repo: Repo::default(),
112            trail: Trail::default(),
113            clock,
114            head: Digest::genesis(),
115            rows: Vec::new(),
116            tags: Tags::default(),
117            tail: Tail::Whole,
118        })
119    }
120
121    /// Lay out a new container and put `commits` into it, each as one row
122    /// and one rev of its own.
123    ///
124    /// This is how a *scratch* session becomes durable: each commit lands
125    /// as its own rev, so the container's history *is* the session's
126    /// history. Nothing is trailed — the commits are the new container's
127    /// past.
128    ///
129    /// A session that already has a container does **not** come this way:
130    /// commits are all this can carry, so the wall times, the authors, the
131    /// edit/undo/redo kinds and the tags would be flattened into a run of
132    /// fresh edits by whoever pressed Save-as. That path copies the
133    /// manifest's own rows instead ([`super::prefix::save_through`]).
134    ///
135    /// # Errors
136    /// [`SeedFailure::Create`] as [`Self::create`], and
137    /// [`SeedFailure::Append`] naming the commit that did not land — which
138    /// leaves a container holding the prefix that did.
139    pub fn seeded(
140        storage: S,
141        clock: Clock,
142        commits: &[Commit],
143        author: &Identity,
144    ) -> Result<Self, SeedFailure> {
145        Self::create(storage, clock)?.seeded_with(commits, author)
146    }
147
148    /// [`Self::seeded`] for a storage whose futures are not ready when they
149    /// are made. The commits themselves are folded and written
150    /// synchronously either way; what a resident container owes its storage
151    /// afterwards is [`Self::drain`]'s.
152    ///
153    /// # Errors
154    /// As [`Self::seeded`].
155    pub async fn seeding(
156        storage: S,
157        clock: Clock,
158        commits: &[Commit],
159        author: &Identity,
160    ) -> Result<Self, SeedFailure> {
161        Self::creating(storage, clock)
162            .await?
163            .seeded_with(commits, author)
164    }
165
166    fn seeded_with(mut self, commits: &[Commit], author: &Identity) -> Result<Self, SeedFailure> {
167        for (at, commit) in commits.iter().enumerate() {
168            self.write(Step::Seed(commit.clone()), author.into())
169                .map_err(|why| SeedFailure::Append { at, why })?;
170        }
171        Ok(self)
172    }
173
174    /// Open a container: read the manifest, verify its chain, and read the
175    /// head rev.
176    ///
177    /// A manifest that does not verify is not a failure to open: the
178    /// container opens read-only at the last good prefix, with the break as
179    /// its [`ReadOnlyReason`], because the reader is owed a look at the
180    /// past that *is* intact. A manifest with a partial trailing row opens
181    /// writable, the row dropped and the file cut back to the last whole
182    /// line.
183    ///
184    /// # Errors
185    /// As [`Container::open`] — there is no container, or it cannot be
186    /// read.
187    pub fn open(storage: S, clock: Clock) -> Result<Self, ContainerError> {
188        ready_now(Self::opening(storage, clock))
189    }
190
191    /// [`Self::open`] for a storage whose futures are not ready when they
192    /// are made.
193    ///
194    /// The open is where a resident container is read in — the manifest,
195    /// every rev and every payload — so everything after it, this
196    /// function's own second half included, reads out of memory.
197    ///
198    /// # Errors
199    /// As [`Self::open`].
200    pub async fn opening(storage: S, mut clock: Clock) -> Result<Self, ContainerError> {
201        Self::over(Container::opening(storage, clock.tick()).await?, clock)
202    }
203
204    /// Open a container to be read and never written.
205    ///
206    /// The same read as [`Self::open`] over a handle that never claimed
207    /// the lock ([`Container::reading`]), so reading someone's document —
208    /// or a fixture out of `fixtures/` — cannot cost its owner the right
209    /// to write it. Every write door refuses with [`Refusal::ReadOnly`],
210    /// which is why this takes no [`Clock`]: nothing here mints a row.
211    ///
212    /// # Errors
213    /// As [`Self::open`].
214    pub fn reading(storage: S) -> Result<Self, ContainerError> {
215        ready_now(Self::opening_to_read(storage))
216    }
217
218    /// [`Self::reading`] for a storage whose futures are not ready when
219    /// they are made.
220    ///
221    /// # Errors
222    /// As [`Self::reading`].
223    pub async fn opening_to_read(storage: S) -> Result<Self, ContainerError> {
224        Self::over(Container::opening_to_read(storage).await?, Clock::System)
225    }
226
227    /// Read an already-attached container — [`Self::open`]'s second half,
228    /// shared with the prefix Save-as, which lays a container out with a
229    /// manifest already in it.
230    ///
231    /// # Errors
232    /// The manifest that could not be read.
233    pub(super) fn over(mut container: Container<S>, clock: Clock) -> Result<Self, ContainerError> {
234        let text = container.read_manifest()?;
235        let scanned = manifest::scan(&text);
236        // The head is the document this session opens on, so its bytes are
237        // checked here — one blake3 over one file. Every *other* rev is
238        // `blockworx verify`'s business.
239        let (rows, broken) = whole(&container, scanned.rows);
240        let history = manifest::history(&rows);
241        let head = match container.read_rev(history.rev()) {
242            Ok(document) => document,
243            Err(fault) => {
244                // The prefix `whole` handed back ends at a rev whose bytes
245                // hash to what its row says, so this is artwork the
246                // container cannot show rather than a rev that is not
247                // itself. Either way the reader is owed the empty document
248                // over a crash.
249                tracing::error!("{fault}");
250                container.demote(ReadOnlyReason::WriteFailed(std::io::Error::other(
251                    fault.to_string(),
252                )));
253                Document::default()
254            }
255        };
256        let mut tail = Tail::Whole;
257        match (broken, scanned.end) {
258            (Some(report), _) | (None, End::Broken(report)) => {
259                container.demote(ReadOnlyReason::HistoryBroken(report));
260            }
261            (None, End::Truncated(dropped)) => {
262                tail = Tail::Dropped(dropped);
263                if let Err(WriteRefusal::Io(error)) =
264                    container.truncate_manifest(dropped.good_bytes)
265                {
266                    container.demote(ReadOnlyReason::WriteFailed(error));
267                }
268            }
269            (None, End::Whole) => {}
270        }
271        Ok(Self {
272            container,
273            repo: Repo::at(head),
274            trail: history.trail,
275            clock,
276            head: history.head,
277            rows: history.rows,
278            tags: history.tags,
279            tail,
280        })
281    }
282
283    /// What this document is called.
284    pub fn name(&self) -> Name {
285        self.container.name()
286    }
287
288    /// Where this container sits on a filesystem, for a shell that can
289    /// name one. `None` for a container that is not on one.
290    pub fn path(&self) -> Option<&Path> {
291        self.container.storage().disk_path()
292    }
293
294    /// Rename this store's container, keeping the lock and the manifest
295    /// with it. The document is untouched: only what it is called changes,
296    /// so nothing is appended and no rev is spent.
297    ///
298    /// # Errors
299    /// [`Refusal::ReadOnly`] on a container this session may not write —
300    /// renaming someone else's open document is not a read — and
301    /// [`Refusal::Rename`] when the container does not move, which leaves
302    /// the store where it was.
303    pub fn rename(&mut self, to: &Name) -> Result<(), Refusal> {
304        if self.read_only_reason().is_some() {
305            return Err(Refusal::ReadOnly);
306        }
307        self.container.rename(to).map_err(Refusal::Rename)
308    }
309
310    /// [`Self::rename`] for a storage whose futures are not ready when they
311    /// are made.
312    ///
313    /// # Errors
314    /// As [`Self::rename`].
315    pub async fn renaming_to(&mut self, to: &Name) -> Result<(), Refusal> {
316        if self.read_only_reason().is_some() {
317            return Err(Refusal::ReadOnly);
318        }
319        self.container
320            .renaming_to(to)
321            .await
322            .map_err(Refusal::Rename)
323    }
324
325    /// How many writes this session has made to its container's memory and
326    /// not yet to its storage — what a status line reads "writing…" off.
327    /// Always zero where the writes are made as they are asked for.
328    pub fn pending(&self) -> usize {
329        self.container.pending()
330    }
331
332    /// Make the writes this session owes its storage, in the order it made
333    /// them. See [`Container::drain`]: a write that does not land costs the
334    /// container its lock, as a failed append does.
335    ///
336    /// # Errors
337    /// The write that did not land.
338    pub async fn drain(&mut self) -> std::io::Result<Drained> {
339        self.container.drain().await
340    }
341
342    /// Readable, never writable: [`Self::submit_edit`] and its two
343    /// neighbours are the only way a commit gets into this repo, which is
344    /// what keeps the repo and the files in step.
345    pub fn repo(&self) -> &Repo {
346        &self.repo
347    }
348
349    /// Where this session's history stands: what one press would take
350    /// back, what one would put back, and which rev's document the head
351    /// holds.
352    pub fn trail(&self) -> &Trail {
353        &self.trail
354    }
355
356    pub fn document(&self) -> &Document {
357        self.repo.document()
358    }
359
360    pub fn access(&self) -> &Access {
361        self.container.access()
362    }
363
364    pub fn read_only_reason(&self) -> Option<&ReadOnlyReason> {
365        match self.container.access() {
366            Access::ReadOnly(reason) => Some(reason),
367            Access::Writable => None,
368        }
369    }
370
371    /// Whether load dropped an unfinished row, and which.
372    pub fn tail(&self) -> Tail {
373        self.tail
374    }
375
376    /// The manifest's rows, one per rev, in rev order.
377    pub fn rows(&self) -> &[Row] {
378        &self.rows
379    }
380
381    /// The row rev `at` was written under, or `None` for a rev this
382    /// history does not hold.
383    pub fn row(&self, at: Rev) -> Option<&Row> {
384        let ndx = usize::try_from(at.get().checked_sub(1)?).ok()?;
385        self.rows.get(ndx)
386    }
387
388    /// The row a reader frames rev `at` from: its own, or — for a step —
389    /// the row of the act it moves.
390    ///
391    /// A step frames what it *moved*, so the scope it opens and the view
392    /// it replays are the ones the act was made in, not where the hand
393    /// that pressed undo happened to be standing. The row itself still
394    /// records the undoer's own circumstances; the derivation happens at
395    /// read time.
396    pub fn framing(&self, at: Rev) -> Option<&Row> {
397        let mut row = self.row(at)?;
398        // Bounded by the rows behind it: every `of` names an earlier rev,
399        // which the manifest checked when it was read.
400        for _ in 0..self.rows.len() {
401            let Some(of) = row.kind.steps() else {
402                return Some(row);
403            };
404            row = self.row(of)?;
405        }
406        Some(row)
407    }
408
409    /// What this manifest's tag rows call its revs.
410    pub fn tags(&self) -> &Tags {
411        &self.tags
412    }
413
414    /// The document this container holds at `at`, payloads and all.
415    ///
416    /// # Errors
417    /// [`Refusal::Unreachable`] for a rev whose copy the container cannot
418    /// show.
419    pub fn document_at(&self, at: Rev) -> Result<Document, Refusal> {
420        self.container
421            .read_rev(at)
422            .map_err(|why| Refusal::Unreachable {
423                at,
424                why: why.to_string(),
425            })
426    }
427
428    /// What the head row stamps. Read off the row rather than recomputed:
429    /// the row was written after the bytes landed, and the open checked
430    /// them against each other.
431    fn head_hash(&self) -> Digest {
432        self.rows
433            .last()
434            .map_or_else(|| Digest::of(&[]), |row| row.hash)
435    }
436
437    /// Fold `commit`, write its rev, and append its row. See [`Store`]'s
438    /// note on the write path: a write that fails costs the container its
439    /// lock.
440    ///
441    /// # Errors
442    /// [`Refusal::ReadOnly`] when this handle may not write,
443    /// [`Refusal::Fold`] when the fold refuses the commit, and
444    /// [`Refusal::Append`] when the row does not reach the file.
445    pub fn submit_edit<'a>(
446        &mut self,
447        commit: Commit,
448        by: impl Into<Attribution<'a>>,
449    ) -> Result<Rev, Refusal> {
450        self.write(Step::Edit(commit), by.into())
451    }
452
453    /// Adopt the document the trail's top entry restores, recorded as an
454    /// undo of `edit`.
455    ///
456    /// # Errors
457    /// As [`Self::submit_edit`], with [`Refusal::Step`] in place of
458    /// [`Refusal::Fold`] when the trail will not take the step, and
459    /// [`Refusal::Unreachable`] for a rev this container cannot read back.
460    pub fn undo<'a>(&mut self, edit: Rev, by: impl Into<Attribution<'a>>) -> Result<Rev, Refusal> {
461        self.write(Step::Undo(edit), by.into())
462    }
463
464    /// # Errors
465    /// As [`Self::undo`].
466    pub fn redo<'a>(&mut self, edit: Rev, by: impl Into<Attribution<'a>>) -> Result<Rev, Refusal> {
467        self.write(Step::Redo(edit), by.into())
468    }
469
470    /// Put a name on `at`, or take one off.
471    ///
472    /// One appended row, chained like any other, that no rev is spent on
473    /// and the trail never hears about: tagging is not an edit, and undo
474    /// after it still takes back the last one.
475    ///
476    /// # Errors
477    /// [`Refusal::ReadOnly`] when this handle may not write,
478    /// [`Refusal::NoSuchRev`] for a rev this history does not hold, and
479    /// [`Refusal::Append`] when the row does not reach the file.
480    pub fn tag<'a>(
481        &mut self,
482        at: Rev,
483        name: &str,
484        how: Tagging,
485        by: impl Into<Attribution<'a>>,
486    ) -> Result<(), Refusal> {
487        if self.read_only_reason().is_some() {
488            return Err(Refusal::ReadOnly);
489        }
490        if at == Rev::ZERO || at > self.repo.rev() {
491            return Err(Refusal::NoSuchRev(at));
492        }
493        let by = by.into();
494        let wall_time = self.clock.tick();
495        // A tag has no rev file of its own, so it stamps the head it was
496        // appended under — which is what makes a tag written against
497        // another history detectable.
498        let row = self.row_for(Written {
499            rev: at,
500            kind: match how {
501                Tagging::Added => RowKind::Tag,
502                Tagging::Removed => RowKind::Untag,
503            },
504            label: name.to_owned(),
505            touched: Vec::new(),
506            hash: self.head_hash(),
507            wall_time,
508            by,
509        });
510        match self.container.append_row(&row) {
511            Ok(()) => {
512                self.head = row.digest();
513                self.tags.apply(at, name, how);
514                Ok(())
515            }
516            Err(WriteRefusal::ReadOnly) => Err(Refusal::ReadOnly),
517            Err(WriteRefusal::Io(error)) => Err(self.parted_from(error)),
518        }
519    }
520
521    /// Take the step, write the rev, then append the row. A write that
522    /// fails leaves this session's document ahead of the files, so the
523    /// container gives up its lock rather than carrying on writing into a
524    /// history with a hole in it.
525    ///
526    /// # Errors
527    /// [`Refusal::ReadOnly`] when this handle may not write,
528    /// [`Refusal::Fold`] or [`Refusal::Step`] when the repo refuses the
529    /// step, and [`Refusal::Append`] when the row does not reach the file.
530    fn write(&mut self, step: Step, by: Attribution<'_>) -> Result<Rev, Refusal> {
531        if self.read_only_reason().is_some() {
532            return Err(Refusal::ReadOnly);
533        }
534        let (rev, kind, label, touched) = match step {
535            Step::Edit(commit) => {
536                let (label, touched) = (commit.label().to_owned(), named(&commit));
537                let _s = tracing::info_span!("fold", ops = commit.ops().len()).entered();
538                let rev = self.repo.submit(commit, &mut self.trail)?;
539                (rev, RowKind::Edit, label, touched)
540            }
541            Step::Seed(commit) => {
542                let (label, touched) = (commit.label().to_owned(), named(&commit));
543                let rev = self.repo.fold_one(commit)?.rev();
544                self.trail.seeded(rev);
545                (rev, RowKind::Edit, label, touched)
546            }
547            Step::Undo(edit) => self.step(edit, Direction::Undo)?,
548            Step::Redo(edit) => self.step(edit, Direction::Redo)?,
549        };
550        let wall_time = self.clock.tick();
551        // The rev's own copy of the document — and the payloads it names —
552        // land before the row that names them, so the manifest never names
553        // bytes a crash could still take away.
554        let _write = tracing::info_span!("write_rev", rev = rev.get()).entered();
555        let hash = match self.container.write_rev(rev, self.repo.document()) {
556            Ok(hash) => hash,
557            Err(WriteRefusal::ReadOnly) => return Err(Refusal::ReadOnly),
558            Err(WriteRefusal::Io(error)) => return Err(self.parted_from(error)),
559        };
560        let row = self.row_for(Written {
561            rev,
562            kind,
563            label,
564            touched,
565            hash,
566            wall_time,
567            by,
568        });
569        match self.container.append_row(&row) {
570            Ok(()) => {
571                self.head = row.digest();
572                self.rows.push(row);
573                Ok(rev)
574            }
575            Err(WriteRefusal::ReadOnly) => Err(Refusal::ReadOnly),
576            Err(WriteRefusal::Io(error)) => Err(self.parted_from(error)),
577        }
578    }
579
580    /// Take one history step, adopting the rev copy this container holds,
581    /// and carry the stepped row's own `touched` names onto the row this
582    /// step writes — so a step frames what it moved.
583    fn step(
584        &mut self,
585        edit: Rev,
586        direction: Direction,
587    ) -> Result<(Rev, RowKind, String, Vec<EntityRef>), Refusal> {
588        // The trail is asked first, so a spent step is reported as one
589        // rather than as a rev this history does not hold.
590        let entry = self.trail.stepping(edit, direction)?;
591        let stepped = self.row(entry.rev).ok_or(Refusal::NoSuchRev(edit))?;
592        let (label, touched) = (stepped.label.clone(), stepped.touched.clone());
593        let rev = crate::doc::stepped(
594            &mut self.repo,
595            &mut self.trail,
596            crate::doc::Stepping {
597                edit,
598                direction,
599                label: &label,
600            },
601            |at| {
602                self.container
603                    .read_rev(at)
604                    .map_err(|why| Refusal::Unreachable {
605                        at,
606                        why: why.to_string(),
607                    })
608            },
609        )?;
610        let kind = match direction {
611            Direction::Undo => RowKind::Undo { of: edit },
612            Direction::Redo => RowKind::Redo { of: edit },
613        };
614        Ok((rev, kind, format!("{} {label}", direction.verb()), touched))
615    }
616
617    fn row_for(&mut self, written: Written<'_>) -> Row {
618        let (touched, truncated) = Row::naming(written.touched);
619        Row {
620            rev: written.rev,
621            kind: written.kind,
622            wall_time: written.wall_time,
623            author: written.by.author.clone(),
624            label: written.label,
625            scope: written.by.standing.path().clone(),
626            scope_names: written.by.standing.names().to_vec(),
627            camera: written.by.camera,
628            touched,
629            truncated,
630            hash: written.hash,
631            parent: self.head,
632        }
633    }
634
635    /// This session's document is now ahead of the files, so the container
636    /// gives up its lock rather than carrying on writing into a history
637    /// with a hole in it.
638    fn parted_from(&mut self, error: std::io::Error) -> Refusal {
639        let echo = std::io::Error::other(error.to_string());
640        self.container.demote(ReadOnlyReason::WriteFailed(error));
641        Refusal::Append(echo)
642    }
643}
644
645/// Everything a row is minted from beyond the chain it links into.
646struct Written<'a> {
647    rev: Rev,
648    kind: RowKind,
649    label: String,
650    touched: Vec<EntityRef>,
651    hash: Digest,
652    wall_time: WallTime,
653    by: Attribution<'a>,
654}
655
656/// The entities a commit named, spelled as the manifest records them.
657fn named(commit: &Commit) -> Vec<EntityRef> {
658    let mut touched: Vec<EntityRef> = Vec::new();
659    for target in commit
660        .ops()
661        .iter()
662        .map(blockworx_doc::opcode::OpCodes::target)
663    {
664        if !touched.contains(&target) {
665            touched.push(target);
666        }
667    }
668    touched
669}
670
671/// The longest prefix of `rows` whose last rev the container can show,
672/// with the break that cut it short.
673///
674/// Only the *head* is checked at open: every other rev file is
675/// `blockworx verify`'s business, and a hole in one of them is a finding
676/// rather than a reason to refuse the document.
677fn whole<S: Storage>(
678    container: &Container<S>,
679    mut rows: Vec<manifest::Verified>,
680) -> (Vec<manifest::Verified>, Option<manifest::BreakReport>) {
681    let backing = container.revs();
682    let mut broken = None;
683    // A tag spends no rev and stamps the head it was appended under, so
684    // the row that names the *document* is the last one that journals.
685    while let Some(head) = rows
686        .iter()
687        .rev()
688        .find(|verified| verified.row.kind.takes_a_rev())
689    {
690        let (at, named) = (head.row.rev, head.row.hash);
691        let Err(fault) = revs::witnessed(&backing, at, named) else {
692            break;
693        };
694        broken.get_or_insert(manifest::BreakReport {
695            at: head.at,
696            fault: Fault::Unwitnessed {
697                at,
698                why: fault.to_string(),
699            },
700        });
701        rows.retain(|verified| verified.row.rev < at);
702    }
703    (rows, broken)
704}