fix(package): books.start runs the real integrity check (U0b)

books.start passed Integrity::default() to the attention engine, so it
asserted "database matches, no file failures". On a book whose database
differs from the journal, or whose stored file is missing or corrupt, the
viewer showed the database/stored-files items and the server did not: the
two lists diverged exactly where they matter most.

Call find, which computes the check: every distinct stored blob is read
and hash-checked once (a damaged blob is still reported per attachment
filed from it), and Store::disk_check now makes the viewer's comparison
through a snapshot of its data directory instead of reporting a match.

The parity test now damages its own fixture (removes the receipt's blob,
then changes a projected account row) and asserts both lists still agree
item for item and both name the damage. On the demo books (138
attachments, 138 distinct blobs, debug build) books.start takes about
0.26 s warm, 0.29 s cold, so the real check costs no decision.
This commit is contained in:
swaits committed 2026-10-27 06:12:55 -06:00
1 parent 0cf2f8f5c5
commit 26d3a9941b
6 files changed
+146 -27

No files matched your search

+12 -3
View File
@@ -192,12 +192,21 @@ impl Default for Integrity {
impl Integrity {
/// Check the database and every stored file: the attachment's bytes
/// are read (and hash-checked) through the books.
/// are read (and hash-checked) through the books, once per blob that
/// is intact; a damaged blob is reported for each attachment filed
/// from it.
pub fn check(books: &impl PackageBooks) -> Integrity {
let mut intact: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut file_failures = Vec::new();
for a in books.all_attachments().unwrap_or_default() {
if let Err(e) = books.attachment_bytes(a.id) {
file_failures.push((a.id, e.to_string()));
if intact.contains(&a.blob_sha) {
continue;
}
match books.attachment_bytes(a.id) {
Ok(_) => {
intact.insert(a.blob_sha.clone());
}
Err(e) => file_failures.push((a.id, e.to_string())),
}
}
Integrity {
+13 -3
View File
@@ -62,9 +62,8 @@ pub trait PackageBooks {
fn reconciliations(&self) -> Result<Vec<Reconciliation>, StoreError>;
/// How the writer's database compares with the journal. A snapshot
/// compares them; the store is the only writer and applies each line
/// before it returns (a failed apply heals on its next open), so it
/// reports a match.
/// compares them; the store makes the same comparison (see its
/// implementation).
fn disk_check(&self) -> DiskCheck {
DiskCheck::Matches
}
@@ -104,6 +103,17 @@ impl PackageBooks for Store {
fn reconciliations(&self) -> Result<Vec<Reconciliation>, StoreError> {
Store::reconciliations(self, None)
}
/// The same comparison the viewer makes, not an assumption: the
/// store's database is the file the viewer checks, and it can be
/// damaged or changed from outside. Loading a snapshot of the data
/// directory compares it against a fresh projection of the journal.
fn disk_check(&self) -> DiskCheck {
match Snapshot::load(Store::data_dir(self)) {
Ok(s) => Snapshot::check_disk(&s),
Err(e) => DiskCheck::Unreadable(e.to_string()),
}
}
}
impl PackageBooks for Snapshot {