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