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}