Skip to main content

blockworx/store/
manifest.rs

1//! `manifest.jsonl`: one row per rev, and one per tag.
2//! Rationale: `docs/log-vs-snapshot.md` §10.1 and §14.2, playbook D26.
3//!
4//! The document is `revs/{head}`; this file is the audit trail beside it.
5//! **A row carries names and circumstances, never document values** — who
6//! wrote it, when, where they were standing, what they were looking at,
7//! and which entities they named. Nothing folds a row, so nothing here can
8//! drift out from under a later build the way a replayed op could.
9//!
10//! D12's chain survives the log it was written for: each row links to the
11//! canonical bytes of the one before it, and stamps the blake3 of the rev
12//! file it names. Both are hashes over bytes somebody else wrote, so the
13//! whole check costs one pass over a small file
14//! (`TUNING.md`, Finding 9).
15//!
16//! Target-independent: a scratch session builds the same rows in memory,
17//! and Phase 8's browser reads the same text out of origin storage.
18
19use blockworx_doc::{
20    id::EntityRef,
21    rev::Rev,
22    trail::{JournalAs, Trail},
23};
24use serde::{Deserialize, Serialize};
25
26use super::record::{Camera, Digest, Identity, ScopePath, WallTime};
27use super::tags::{Tagging, Tags};
28
29/// How many entities a row names before it gives up and says so. §14.2's
30/// default: beyond it the reader's recourse is a diff of the two revs,
31/// which is an acceptable answer for a grep index and would not have been
32/// for a camera.
33pub const TOUCHED: usize = 64;
34
35/// Why a row was written. An undo is a forward record, so the manifest is
36/// a complete audit trail rather than an erasure — and this says which of
37/// its neighbours it took back.
38///
39/// Tags are pinned: serde ties them to variant names, so a rename would
40/// silently rewrite the durable format.
41#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
42pub enum RowKind {
43    #[serde(rename = "edit")]
44    Edit,
45    #[serde(rename = "undo")]
46    Undo { of: Rev },
47    #[serde(rename = "redo")]
48    Redo { of: Rev },
49    /// D18: a human-readable name for the rev this row's `rev` field
50    /// *names* — the one kind whose `rev` is a reference rather than a
51    /// position. It consumes no rev and never reaches the trail.
52    #[serde(rename = "tag")]
53    Tag,
54    /// The same reference, taking one name back off. §8.1 gives a rev a
55    /// set of tags, so removal names which one; the older encoding —
56    /// a `Tag` row with a blank label — could only mean "all of them".
57    #[serde(rename = "untag")]
58    Untag,
59}
60
61/// What replaying one row does. One method rather than an `Option` per
62/// question, so a row cannot answer that it both moves the trail and
63/// names a rev.
64#[derive(Clone, Copy, PartialEq, Eq, Debug)]
65pub enum Replays {
66    /// A rev row: it takes a position and moves the trail.
67    Trail(JournalAs),
68    /// A tag row: it consumes no rev and moves the vocabulary.
69    Vocabulary(Tagging),
70}
71
72impl RowKind {
73    /// What replaying this row does — the boundary the durable spelling
74    /// stops at, so [`Trail`] never hears about a file.
75    ///
76    /// A tag is *about* history rather than part of it, so `Ctrl+Z` after
77    /// tagging still takes back the last edit.
78    pub fn replays(self) -> Replays {
79        match self {
80            RowKind::Edit => Replays::Trail(JournalAs::Edit),
81            RowKind::Undo { of } => Replays::Trail(JournalAs::Undo { of }),
82            RowKind::Redo { of } => Replays::Trail(JournalAs::Redo { of }),
83            RowKind::Tag => Replays::Vocabulary(Tagging::Added),
84            RowKind::Untag => Replays::Vocabulary(Tagging::Removed),
85        }
86    }
87
88    /// Whether this row takes a rev of its own, or names one already
89    /// taken.
90    pub fn takes_a_rev(self) -> bool {
91        matches!(self.replays(), Replays::Trail(_))
92    }
93
94    /// The rev this row steps, where it steps one.
95    pub fn steps(self) -> Option<Rev> {
96        match self {
97            RowKind::Undo { of } | RowKind::Redo { of } => Some(of),
98            RowKind::Edit | RowKind::Tag | RowKind::Untag => None,
99        }
100    }
101}
102
103/// One line of `manifest.jsonl`.
104#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
105pub struct Row {
106    pub rev: Rev,
107    pub kind: RowKind,
108    pub wall_time: WallTime,
109    pub author: Identity,
110    pub label: String,
111    /// Where the author was standing: every scope is a coordinate space of
112    /// its own, so a camera without one means nothing (§10.1).
113    pub scope: ScopePath,
114    /// The same path as its author saw it spelled, outermost first —
115    /// §8.1's scope line. Recorded at seal beside the ids rather than
116    /// resolved at read time, so a row still reads its own path after the
117    /// blocks along it are renamed or deleted. Empty at the root.
118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
119    pub scope_names: Vec<String>,
120    pub camera: Camera,
121    /// The entities the act named, in `EntityRef`'s narration spelling —
122    /// the grep index, advisory in the same sense `scope` has always been.
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub touched: Vec<EntityRef>,
125    /// Whether [`TOUCHED`] cut the list short.
126    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
127    pub truncated: bool,
128    /// The blake3 of the rev file this row names. A tag has no rev file of
129    /// its own and stamps the head it was appended under, which is what
130    /// makes a tag written against another history detectable.
131    pub hash: Digest,
132    /// D12: the digest of the previous row's canonical bytes, or
133    /// [`Digest::genesis`] for the first.
134    pub parent: Digest,
135}
136
137impl Row {
138    /// The bytes this row is written as, and the bytes its successor's
139    /// `parent` hashes over: `serde_json` with object keys sorted and no
140    /// whitespace. One definition for both, so a round trip through the
141    /// file cannot break the chain that verifies it.
142    // A row is plain data: no map keys that are not strings, which is the
143    // only way `to_value` refuses a value.
144    #[expect(clippy::expect_used, clippy::missing_panics_doc)]
145    pub fn canonical_bytes(&self) -> Vec<u8> {
146        let value = serde_json::to_value(self).expect("a manifest row serializes infallibly");
147        let mut bytes = Vec::new();
148        write_canonical(&value, &mut bytes).expect("writing to a vector is infallible");
149        bytes
150    }
151
152    /// The link the next row carries as its `parent`.
153    pub fn digest(&self) -> Digest {
154        Digest::of(&self.canonical_bytes())
155    }
156
157    /// The same link, computed against the line a file actually holds, so
158    /// a row an older build spelled differently still links as it did.
159    // Re-serialization is infallible for a value that just parsed.
160    #[expect(clippy::expect_used, clippy::missing_panics_doc)]
161    pub fn digest_as_written(&self, line: &str) -> Digest {
162        let canonical = self.canonical_bytes();
163        if canonical == line.as_bytes() {
164            return Digest::of(&canonical);
165        }
166        let value: serde_json::Value =
167            serde_json::from_str(line).expect("the line parsed as a row");
168        let mut bytes = Vec::new();
169        write_canonical(&value, &mut bytes).expect("writing to a vector is infallible");
170        Digest::of(&bytes)
171    }
172
173    /// The names of the entities `touched` holds, cut to [`TOUCHED`].
174    pub fn naming(mut touched: Vec<EntityRef>) -> (Vec<EntityRef>, bool) {
175        let truncated = touched.len() > TOUCHED;
176        touched.truncate(TOUCHED);
177        (touched, truncated)
178    }
179}
180
181/// Sorted-key, whitespace-free JSON. Written out rather than delegated to
182/// `serde_json::to_vec` over a `Value`, whose key order is the `Map`
183/// backing `serde_json` happens to be compiled with — a cargo feature
184/// elsewhere in the tree must not be able to change what a manifest hashes
185/// to.
186pub(crate) fn write_canonical(
187    value: &serde_json::Value,
188    out: &mut Vec<u8>,
189) -> serde_json::Result<()> {
190    match value {
191        serde_json::Value::Object(entries) => {
192            let mut keys: Vec<&String> = entries.keys().collect();
193            keys.sort_unstable();
194            out.push(b'{');
195            for (position, key) in keys.into_iter().enumerate() {
196                if position > 0 {
197                    out.push(b',');
198                }
199                serde_json::to_writer(&mut *out, key)?;
200                out.push(b':');
201                write_canonical(&entries[key], out)?;
202            }
203            out.push(b'}');
204        }
205        serde_json::Value::Array(items) => {
206            out.push(b'[');
207            for (position, item) in items.iter().enumerate() {
208                if position > 0 {
209                    out.push(b',');
210                }
211                write_canonical(item, out)?;
212            }
213            out.push(b']');
214        }
215        leaf => serde_json::to_writer(out, leaf)?,
216    }
217    Ok(())
218}
219
220/// Where in the file something went wrong — enough to render a span-style
221/// report without re-reading it.
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223pub struct Located {
224    /// 1-based.
225    pub line: usize,
226    /// 1-based, in bytes from the start of the line.
227    pub column: usize,
228    /// Byte offset of the line's first byte from the start of the file.
229    pub offset: usize,
230    /// The line's length in bytes, its newline excluded.
231    pub len: usize,
232}
233
234impl std::fmt::Display for Located {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        write!(f, "line {}, column {}", self.line, self.column)
237    }
238}
239
240/// What a row failed on.
241#[derive(Debug)]
242pub enum Fault {
243    Malformed(serde_json::Error),
244    /// D12: the row does not link to the one before it, so the file has
245    /// been rewritten, reordered, or spliced at this point.
246    Chained {
247        expected: Digest,
248        found: Digest,
249    },
250    /// The row claims a position the manifest did not reach.
251    Misnumbered {
252        expected: Rev,
253        found: Rev,
254    },
255    /// A step names a rev no row before it holds.
256    Unreached {
257        of: Rev,
258    },
259    /// The rev file this row names is not there, or does not hash to what
260    /// the row says it does.
261    Unwitnessed {
262        at: Rev,
263        why: String,
264    },
265}
266
267impl std::fmt::Display for Fault {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        match self {
270            Fault::Malformed(error) => write!(f, "the row does not parse: {error}"),
271            Fault::Chained { expected, found } => write!(
272                f,
273                "the row links to {found}, but the row before it hashes to {expected} — \
274                 the manifest has been rewritten here",
275            ),
276            Fault::Misnumbered { expected, found } => write!(
277                f,
278                "the row claims rev {found}, but the manifest reaches {expected}",
279                found = found.get(),
280                expected = expected.get(),
281            ),
282            Fault::Unreached { of } => write!(
283                f,
284                "the row steps rev {of}, which no row before it holds",
285                of = of.get(),
286            ),
287            Fault::Unwitnessed { at, why } => write!(
288                f,
289                "the row names rev {at}, whose file this container cannot show: {why}",
290                at = at.get(),
291            ),
292        }
293    }
294}
295
296/// A fault and where it is. Kept apart from the rows it interrupted so a
297/// container can hold on to the report as its read-only reason without
298/// also holding the history it came with.
299#[derive(Debug)]
300pub struct BreakReport {
301    pub at: Located,
302    pub fault: Fault,
303}
304
305impl std::fmt::Display for BreakReport {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        write!(f, "{}: {}", self.at, self.fault)
308    }
309}
310
311/// A final row the writer never finished. Appends are one `write_all` of
312/// the line and its newline followed by an fsync, so a manifest that does
313/// not end in a newline ends in a write that was never made durable: the
314/// row is dropped and the file truncated back to the last whole line.
315#[derive(Clone, Copy, Debug)]
316pub struct DroppedTail {
317    pub at: Located,
318    /// Where the whole manifest ends.
319    pub good_bytes: u64,
320}
321
322/// Whether the manifest ended where it should have. Not a bool: a dropped
323/// tail is something the user is told about.
324#[derive(Clone, Copy, Debug)]
325pub enum Tail {
326    Whole,
327    Dropped(DroppedTail),
328}
329
330/// One row and where the file holds it.
331pub struct Verified {
332    pub row: Row,
333    pub at: Located,
334    /// The link the next row must name as its `parent`.
335    pub digest: Digest,
336}
337
338/// How the row sequence stopped.
339pub enum End {
340    Whole,
341    Truncated(DroppedTail),
342    Broken(BreakReport),
343}
344
345/// Parse and link, stopping at the first row that does not belong and
346/// keeping everything before it.
347pub struct Scan {
348    pub rows: Vec<Verified>,
349    pub end: End,
350}
351
352/// The history a prefix of rows makes: what the panel lists, what the
353/// tags call the revs, and where the session that wrote them stood.
354pub struct History {
355    /// One per rev, in rev order — tag rows are projected into `tags`
356    /// instead, since they consume no rev.
357    pub rows: Vec<Row>,
358    pub trail: Trail,
359    pub tags: Tags,
360    /// The digest the next appended row must name as its parent.
361    pub head: Digest,
362}
363
364impl History {
365    /// The rev the last row named, or [`Rev::ZERO`] for a manifest with no
366    /// revs in it.
367    pub fn rev(&self) -> Rev {
368        self.rows.last().map_or(Rev::ZERO, |row| row.rev)
369    }
370
371    pub fn row(&self, rev: Rev) -> Option<&Row> {
372        let at = usize::try_from(rev.get().checked_sub(1)?).ok()?;
373        self.rows.get(at)
374    }
375}
376
377/// Read `text` as a chain of rows, verifying every link, every position
378/// and every step as it goes.
379///
380/// The chain is over the bytes the file holds — a hash of one line — so
381/// this is the whole of D12 that survives the log, and it is cheap.
382pub fn scan(text: &str) -> Scan {
383    let mut rows: Vec<Verified> = Vec::new();
384    let mut parent = Digest::genesis();
385    let mut reached = Rev::ZERO;
386    let mut offset = 0;
387
388    for (number, chunk) in text.split_inclusive('\n').enumerate() {
389        let line = chunk.strip_suffix('\n');
390        let body = line.unwrap_or(chunk);
391        let at = Located {
392            line: number + 1,
393            column: 1,
394            offset,
395            len: body.len(),
396        };
397        if line.is_none() {
398            return Scan {
399                rows,
400                end: End::Truncated(DroppedTail {
401                    at,
402                    good_bytes: offset as u64,
403                }),
404            };
405        }
406        offset += chunk.len();
407
408        let row: Row = match serde_json::from_str(body) {
409            Ok(row) => row,
410            Err(error) => {
411                let at = Located {
412                    column: error.column().max(1),
413                    ..at
414                };
415                return broken(rows, at, Fault::Malformed(error));
416            }
417        };
418        if row.parent != parent {
419            let fault = Fault::Chained {
420                expected: parent,
421                found: row.parent,
422            };
423            return broken(rows, at, fault);
424        }
425        if let Some(fault) = misplaced(&row, reached) {
426            return broken(rows, at, fault);
427        }
428        if row.kind.takes_a_rev() {
429            reached = row.rev;
430        }
431        parent = row.digest_as_written(body);
432        rows.push(Verified {
433            row,
434            at,
435            digest: parent,
436        });
437    }
438
439    Scan {
440        rows,
441        end: End::Whole,
442    }
443}
444
445/// Whether `row` sits where the manifest can hold it: a rev row takes the
446/// next position, a tag names one already taken, and a step names a rev
447/// the trail can have reached.
448fn misplaced(row: &Row, reached: Rev) -> Option<Fault> {
449    if let Some(of) = row.kind.steps()
450        && of > reached
451    {
452        return Some(Fault::Unreached { of });
453    }
454    if !row.kind.takes_a_rev() {
455        // A tag names a rev the manifest already holds rather than taking
456        // one, so its `rev` is checked as a reference instead.
457        return (row.rev == Rev::ZERO || row.rev > reached).then_some(Fault::Misnumbered {
458            expected: reached,
459            found: row.rev,
460        });
461    }
462    let expected = reached.next();
463    (row.rev != expected).then_some(Fault::Misnumbered {
464        expected,
465        found: row.rev,
466    })
467}
468
469fn broken(rows: Vec<Verified>, at: Located, fault: Fault) -> Scan {
470    Scan {
471        rows,
472        end: End::Broken(BreakReport { at, fault }),
473    }
474}
475
476/// The history `rows` make: the trail replayed through its own policy, the
477/// tags projected, and the rows themselves in rev order.
478///
479/// One derivation, whether the rows were written this session or read off
480/// a disk — which is what keeps a reopened document standing where it was
481/// closed (F6).
482pub fn history(rows: &[Verified]) -> History {
483    let mut trail = Trail::default();
484    let mut tags = Tags::default();
485    let mut kept = Vec::new();
486    for verified in rows {
487        let row = &verified.row;
488        match row.kind.replays() {
489            Replays::Vocabulary(how) => tags.apply(row.rev, &row.label, how),
490            Replays::Trail(journal) => {
491                trail.record(row.rev, journal);
492                kept.push(row.clone());
493            }
494        }
495    }
496    History {
497        rows: kept,
498        trail,
499        tags,
500        head: rows.last().map_or_else(Digest::genesis, |last| last.digest),
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use blockworx_doc::fixtures::{block_id, rev};
508
509    fn row(rev: Rev, kind: RowKind, parent: Digest) -> Row {
510        Row {
511            rev,
512            kind,
513            wall_time: WallTime::from_unix_millis(1_756_000_000_000),
514            author: Identity::new("ada"),
515            label: "Added a block".to_owned(),
516            scope: ScopePath::default(),
517            scope_names: Vec::new(),
518            camera: Camera::UNSEEN,
519            touched: vec![EntityRef::Block(block_id(1))],
520            truncated: false,
521            hash: Digest::of(b"a rev file"),
522            parent,
523        }
524    }
525
526    /// `kinds` as a properly chained manifest.
527    fn chained(kinds: &[(Rev, RowKind)]) -> String {
528        let mut parent = Digest::genesis();
529        let mut text = String::new();
530        for (at, kind) in kinds {
531            let row = row(*at, *kind, parent);
532            parent = row.digest();
533            text.push_str(&String::from_utf8(row.canonical_bytes()).expect("utf-8"));
534            text.push('\n');
535        }
536        text
537    }
538
539    #[test]
540    fn a_row_round_trips_through_its_own_canonical_bytes() {
541        let written = row(rev(1), RowKind::Edit, Digest::genesis());
542        let bytes = written.canonical_bytes();
543        let parsed: Row = serde_json::from_slice(&bytes).expect("the row parses back");
544        assert_eq!(parsed, written);
545        assert_eq!(
546            parsed.canonical_bytes(),
547            bytes,
548            "and re-canonicalizes to the same bytes, which is what the chain rests on",
549        );
550    }
551
552    /// The chain is over the bytes a file holds, and canonicalization is
553    /// what lets a line that says the same thing differently still link.
554    #[test]
555    fn a_reformatted_line_links_exactly_as_the_line_it_reformats() {
556        let written = row(rev(1), RowKind::Edit, Digest::genesis());
557        let canonical = String::from_utf8(written.canonical_bytes()).expect("utf-8");
558        let pretty = serde_json::to_string_pretty(&written).expect("it re-serializes");
559        assert_ne!(pretty, canonical, "precondition: different bytes");
560        assert_eq!(
561            written.digest_as_written(&pretty),
562            written.digest_as_written(&canonical),
563        );
564        assert_eq!(written.digest_as_written(&canonical), written.digest());
565    }
566
567    /// The tags a rename must not be allowed to move.
568    #[test]
569    fn row_kinds_are_spelled_as_the_format_names_them() {
570        let spelling = |kind: RowKind| serde_json::to_string(&kind).expect("a kind serializes");
571        assert_eq!(spelling(RowKind::Edit), "\"edit\"");
572        assert_eq!(
573            spelling(RowKind::Undo { of: rev(3) }),
574            r#"{"undo":{"of":3}}"#
575        );
576        assert_eq!(
577            spelling(RowKind::Redo { of: rev(4) }),
578            r#"{"redo":{"of":4}}"#
579        );
580        assert_eq!(spelling(RowKind::Tag), "\"tag\"");
581        assert_eq!(spelling(RowKind::Untag), "\"untag\"");
582        for kind in [RowKind::Tag, RowKind::Untag] {
583            assert!(!kind.takes_a_rev(), "a tag row moves no stack: {kind:?}");
584        }
585    }
586
587    /// F6 through the file: the rows replay into the trail the session
588    /// that wrote them closed with, and a tag row leaves it alone.
589    #[test]
590    fn rows_replay_into_the_trail_and_the_tags() {
591        let text = chained(&[
592            (rev(1), RowKind::Edit),
593            (rev(2), RowKind::Edit),
594            (rev(2), RowKind::Tag),
595            (rev(3), RowKind::Undo { of: rev(2) }),
596        ]);
597        let scanned = scan(&text);
598        assert!(matches!(scanned.end, End::Whole), "the manifest chains");
599
600        let history = history(&scanned.rows);
601        assert_eq!(history.rev(), rev(3));
602        assert_eq!(history.rows.len(), 3, "a tag consumes no rev");
603        assert_eq!(history.tags.of(rev(2)), ["Added a block"]);
604        assert_eq!(history.trail.standing(), rev(1), "the undo stepped back");
605        assert_eq!(history.trail.next_redo(), Some(rev(3)));
606        assert_eq!(history.row(rev(2)).map(|row| row.rev), Some(rev(2)));
607    }
608
609    #[test]
610    fn a_rewritten_row_breaks_the_chain_at_its_successor() {
611        let text = chained(&[(rev(1), RowKind::Edit), (rev(2), RowKind::Edit)]);
612        let mut lines: Vec<String> = text.lines().map(str::to_owned).collect();
613        lines[0] = lines[0].replace("Added a block", "Added a bloke");
614        let scanned = scan(&(lines.join("\n") + "\n"));
615
616        assert_eq!(scanned.rows.len(), 1, "the prefix before the break stands");
617        let End::Broken(report) = scanned.end else {
618            panic!("a rewritten row was accepted");
619        };
620        assert!(matches!(report.fault, Fault::Chained { .. }));
621        assert_eq!(report.at.line, 2, "the successor is what detects it");
622    }
623
624    #[test]
625    fn a_step_naming_a_rev_no_row_holds_is_refused() {
626        let text = chained(&[
627            (rev(1), RowKind::Edit),
628            (rev(2), RowKind::Undo { of: rev(9) }),
629        ]);
630        let End::Broken(report) = scan(&text).end else {
631            panic!("a step into nothing was accepted");
632        };
633        assert!(
634            matches!(report.fault, Fault::Unreached { of } if of == rev(9)),
635            "{}",
636            report.fault,
637        );
638    }
639
640    #[test]
641    fn a_row_out_of_sequence_is_refused() {
642        let text = chained(&[(rev(1), RowKind::Edit), (rev(3), RowKind::Edit)]);
643        let End::Broken(report) = scan(&text).end else {
644            panic!("a gap in the revs was accepted");
645        };
646        assert!(matches!(report.fault, Fault::Misnumbered { .. }));
647
648        let unheld = chained(&[(rev(1), RowKind::Edit), (rev(4), RowKind::Tag)]);
649        let End::Broken(report) = scan(&unheld).end else {
650            panic!("a tag on a rev the manifest does not hold was accepted");
651        };
652        assert!(matches!(report.fault, Fault::Misnumbered { .. }));
653    }
654
655    /// An append is one write of the line and its newline, then an fsync,
656    /// so a file that does not end in one ends in a write nobody made
657    /// durable.
658    #[test]
659    fn a_partial_trailing_line_is_dropped_and_the_file_cut_back() {
660        let whole = chained(&[(rev(1), RowKind::Edit), (rev(2), RowKind::Edit)]);
661        let cut = whole.len() - 20;
662        let scanned = scan(&whole[..cut]);
663        assert_eq!(scanned.rows.len(), 1);
664        let End::Truncated(dropped) = scanned.end else {
665            panic!("a half-written row was taken");
666        };
667        assert_eq!(
668            dropped.good_bytes as usize,
669            whole.lines().next().expect("a first line").len() + 1,
670        );
671    }
672
673    /// §14.2's cap, and the flag that says it bit.
674    #[test]
675    fn a_touched_list_past_the_cap_is_cut_and_marked() {
676        let many: Vec<EntityRef> = (1..=200).map(|n| EntityRef::Block(block_id(n))).collect();
677        let (cut, truncated) = Row::naming(many);
678        assert_eq!(cut.len(), TOUCHED);
679        assert!(truncated);
680        assert_eq!(cut[0], EntityRef::Block(block_id(1)), "the cut is a prefix");
681
682        let (few, truncated) = Row::naming(vec![EntityRef::Document]);
683        assert_eq!(few, [EntityRef::Document]);
684        assert!(!truncated);
685    }
686
687    /// The invariant §10.1 holds the line on: a row is names and
688    /// circumstances. `{"x": 12}` in one and the log is back.
689    #[test]
690    fn a_row_carries_no_document_values() {
691        let written = row(rev(1), RowKind::Edit, Digest::genesis());
692        let text = String::from_utf8(written.canonical_bytes()).expect("utf-8");
693        let fields: serde_json::Map<String, serde_json::Value> =
694            serde_json::from_str(&text).expect("the row parses as an object");
695        let mut keys: Vec<&str> = fields.keys().map(String::as_str).collect();
696        keys.sort_unstable();
697        assert_eq!(
698            keys,
699            [
700                "author",
701                "camera",
702                "hash",
703                "kind",
704                "label",
705                "parent",
706                "rev",
707                "scope",
708                "touched",
709                "wall_time",
710            ],
711        );
712        assert_eq!(
713            fields["touched"],
714            serde_json::json!(["block b1"]),
715            "the index is names, not values",
716        );
717    }
718}