merge: P6, the evidence pipeline (branch P6, checkpoint ce366408)

The last of the paused branches, from its "not for merge" checkpoint:
extraction in ledger-import (text, pages, page images; capped, caught),
journaled as attachment.text_extracted; dedup by hash with
books.find_attachment and books.consolidate_duplicates; annotations and
books.set_entry_settles; the company record categories; the two fuzz
targets; feature attachment_text. Nineteen files conflicted with the
four P4b merges, resolved as plan 8.0.2 says: P6 retakes schema 18 (it
had taken 16, which S4.3 holds; S5 holds 17); the tool table is 129 and
the destructive count 31; both sides' appends kept with their seams
restored; one B-27 paragraph naming every feature; in the old-books
fixture P6's hand-written reimbursement is entry 7 (its ids collided
with S4.3's invoice); the replay fuzzer's list gains S7's
invoice.written_off; S4.3's issued-PDF test expects the extraction
record P6 journals right after a filing.

One integration defect between S7 and P6, found by the arap property
test and fixed here: under P6's dedup, a write-off whose evidence bytes
were filed already (the same collection letter for a second write-off of
the same invoice) was refused only after its entry had been journaled,
at the final link to the invoice ("already linked"). The write-off now
resolves the dedup before anything is journaled and keeps a link the
record already has to the invoice instead of refusing it; an explicit
existing_attachment_id already on an entry is still refused (S7's
one-receipt-once rule). P6's gate never ran before the pause; it runs on
main after this merge, and its ledger-store -> ledger-import dependency
is the open review point (plan 8.0.1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-28 23:08:59 -06:00
commit 2ad192bbcd
70 files changed
+4618 -384

No files matched your search

+47
View File
@@ -63,6 +63,7 @@ pub const KNOWN_FEATURES: &[&str] = &[
DOCUMENT_LINE_PROJECTS,
DOCUMENT_STANDING,
DOCUMENTS_V2,
ATTACHMENT_TEXT,
];
/// Feature: `period.closed` closes to members with `"rounding":
@@ -198,6 +199,15 @@ pub const DOCUMENT_STANDING: &str = "document_standing";
/// discount and the fee, or refuse the unknown values, silently.
pub const DOCUMENTS_V2: &str = "documents_v2";
/// Feature: the evidence pipeline (plan P6): `attachment.text_extracted`
/// (a file's text, journaled as an extraction record), `attachment.annotated`
/// (the facts a human read off a file), `entry.settles` (which entries a
/// payment pays back), and `attachment.withdrawn` carrying `duplicate_of`.
/// An older version doesn't know those event types, or would withdraw a
/// duplicate without saying what it duplicates; it stops at the
/// declaration instead (rules.md B-27, B-44).
pub const ATTACHMENT_TEXT: &str = "attachment_text";
/// The line type that declares the journal format.
pub const FORMAT_SET: &str = "journal.format_set";
@@ -218,7 +228,9 @@ pub const EVENT_TYPES: &[&str] = &[
"asset.serial_set",
"asset.tagged",
"asset.tax_basis_set",
"attachment.annotated",
"attachment.linked",
"attachment.text_extracted",
"attachment.unlinked",
"attachment.withdrawn",
"bill.created",
@@ -239,6 +251,7 @@ pub const EVENT_TYPES: &[&str] = &[
"entry.party_set",
"entry.posted",
"entry.reversed",
"entry.settles",
"entry.vendor_set",
"fx.rate_set",
"invoice.created",
@@ -860,6 +873,40 @@ mod tests {
assert_eq!(up.adds(), vec!["the journal feature document_standing"]);
}
/// The version before P6 (every feature through `documents_v2`) stops
/// at a declaration of `attachment_text` as newer, never dropping a
/// file's extraction, annotations or settlements; and books that
/// declared every earlier feature still wait for it (B-27, D-39).
#[test]
fn attachment_text_is_its_own_feature() {
assert!(KNOWN_FEATURES.contains(&ATTACHMENT_TEXT));
let before: Vec<&str> = KNOWN_FEATURES
.iter()
.copied()
.filter(|f| *f != ATTACHMENT_TEXT)
.collect();
let l = line(
FORMAT_SET,
json!({"format": 2, "features": [DOCUMENTS_V2, ATTACHMENT_TEXT]}),
);
match declared_knowing(&l, &before) {
Err(StoreError::NewerJournal { need, .. }) => {
assert_eq!(need, "journal features attachment_text")
}
other => panic!("expected NewerJournal, got {other:?}"),
}
let older = JournalFormat {
format: 2,
features: before.iter().map(|f| f.to_string()).collect(),
};
assert!(matches!(
older.require_feature(ATTACHMENT_TEXT),
Err(StoreError::FormatUpgradePending { .. })
));
let up = older.upgrade().expect("waits for it");
assert_eq!(up.adds(), vec!["the journal feature attachment_text"]);
}
/// The version before P4b S4.2 (every feature through
/// `document_standing`) stops at a declaration of `documents_v2` as
/// newer, never restoring a party's email or default terms as absent