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:
commit
2ad192bbcd
70 files changed
+4618
-384
No files matched your search
+80
-15
@@ -365,7 +365,8 @@ that changes anything also has a `destructiveHint`, true for these 30:
|
||||
`books.set_tin` (both overwrite a vault record), `books.purge_tin`,
|
||||
`books.create_invoice`, `books.create_bill`, `books.receive_payment`,
|
||||
`books.pay_bill`, `books.void_document`, `books.write_off_invoice`,
|
||||
`books.run_payroll`, `books.remit_payroll_taxes`; and an
|
||||
`books.run_payroll`, `books.remit_payroll_taxes`,
|
||||
`books.consolidate_duplicates`; and an
|
||||
`idempotentHint` on the ten that a repeat with the same arguments leaves
|
||||
the journal and the vault as they were: `books.verify`,
|
||||
`books.run_depreciation`, `books.run_schedules`, `books.run_recurring`,
|
||||
@@ -1598,22 +1599,82 @@ and no control or invisible (Unicode format) characters; a MIME type
|
||||
subject it is linked to, its filing entry first). Withdrawn attachments
|
||||
are omitted unless `include_withdrawn` is true. `entry_id` lists the
|
||||
files filed on the entry and linked to it.
|
||||
- `books.get_receipt { attachment_id, view?: "meta" | "bytes" }` → `{
|
||||
attachment_id, sha256, entry_id?, category, file_name, mime_type,
|
||||
size_bytes, description?, links, file? }`. `meta` (the default) returns
|
||||
no file. `bytes` adds `file`, the file handed over (a `HandedFile`, read
|
||||
hash-verified, whatever its size: D-46), whose `download` saves a copy
|
||||
as a new file in a folder the human names. The stored file is also
|
||||
fetched by `wanderbooks download --book B <id> --to <a folder>` (`GET
|
||||
- `books.get_receipt { attachment_id, view?: "meta" | "text" |
|
||||
"page_image" | "bytes", page?, max_px? }` → `{ attachment_id, sha256,
|
||||
entry_id?, category, file_name, mime_type, size_bytes, description?,
|
||||
links, text?, text_truncated, text_status, pages, receipt_number?,
|
||||
annotation?, file? }`. `meta` (the default) returns no file: the first
|
||||
8 KiB of the file's extracted text (`text`, `text_truncated` when cut),
|
||||
its page count (`pages`), and `text_status` (`ok`, `none`, `failed`, or
|
||||
`not_extracted` for books filed before this version: `wanderbooks books
|
||||
extract`). `text` gives the whole extracted text (past 64 KiB it comes
|
||||
as a handed-over `.txt` instead). `page_image` with `page` (1 is the
|
||||
first) and `max_px` (default 1600, longest side) gives one page as a
|
||||
PNG, handed over (D-46; D-15: no OCR, so a picture-only receipt is read
|
||||
from its pages). Read the text first; a page picture only when the text
|
||||
is empty or looks wrong; never `bytes` into context. `bytes` adds
|
||||
`file`, the file handed over (a `HandedFile`, read hash-verified,
|
||||
whatever its size: D-46), whose `download` saves a copy as a new file
|
||||
in a folder the human names. The stored file is also fetched by
|
||||
`wanderbooks download --book B <id> --to <a folder>` (`GET
|
||||
/b/{book}/a/{attachment}`, books:read, "Transports").
|
||||
- `books.file_document { category: "formation" | "legal" | "tax" |
|
||||
"accounting" | "statement" | "contract" | "photo" | "warranty" |
|
||||
"import_source" | "other", file_name?, mime_type?, upload_id,
|
||||
description?, link_to? }` → `{ attachment_id,
|
||||
sha256, deduplicated, links }`. The bytes, name and type as in
|
||||
`books.attach_receipt`. The Drive-folder replacement (today's 01
|
||||
Formation / 02 Legal / 03 Tax / 04 Accounting). Never a W-9: it shows
|
||||
the full TIN (`books.set_w9 w9_upload_id`).
|
||||
"import_source" | "notices" | "correspondence" | "plans" | "history" |
|
||||
"other", file_name?, mime_type?, upload_id, description?, link_to? }` →
|
||||
`{ attachment_id, sha256, deduplicated, links }`. The bytes, name and
|
||||
type as in `books.attach_receipt`. Every company record has a home
|
||||
(D-20): the Drive folders' replacement, and formation papers,
|
||||
registrations, plans, notices, correspondence and history besides what
|
||||
the CPA asks for. Never a W-9: it shows the full TIN (`books.set_w9
|
||||
w9_upload_id`).
|
||||
- **One file, one record** (plan P6 step 4): filing bytes already stored
|
||||
(and not withdrawn) returns that attachment's `attachment_id` with
|
||||
`deduplicated: true` and links it to the new subject, instead of filing
|
||||
a second record. Records an older version filed twice stay as written
|
||||
until `books.consolidate_duplicates`.
|
||||
|
||||
## Tools — evidence
|
||||
|
||||
What the books know about a filed file beyond its bytes (plan P6): its
|
||||
extracted text, the facts a human read off it, and which entries a payment
|
||||
settles. Extraction is journaled as it is made (`attachment.text_extracted`,
|
||||
content-addressed, one record per attachment, extractor and version): an
|
||||
extractor upgrade adds a record, never replaces one, and `books.verify`
|
||||
checks the text blob's presence and hash.
|
||||
|
||||
- `books.find_attachment { sha256 | receipt_number | text }` → `{
|
||||
attachments: [...] }`. Exactly one of the three: the file's exact
|
||||
SHA-256 (64 hex digits), the number recorded with
|
||||
`books.annotate_attachment`, or words its extracted text says
|
||||
(case-insensitively). Withdrawn files are listed too, marked, so a
|
||||
duplicate is found before it is consolidated. Each attachment is as in
|
||||
`books.list_receipts`, with its links.
|
||||
- `books.annotate_attachment { attachment_id, fields { receipt_number?,
|
||||
payee?, amount?, date?, instrument?, category?, source?,
|
||||
reimbursable_to? }, note? }` → `{ attachment_id, fields,
|
||||
next_receipt_number }`. The closed list of what a human reads off a
|
||||
file: `amount` an exact decimal string, `date` YYYY-MM-DD, `instrument`
|
||||
a payment method's id or label. It changes no amount, no file and no
|
||||
posting; a later annotation replaces it, journaled as
|
||||
`attachment.annotated`. `next_receipt_number` is one past the highest
|
||||
number annotated, so numbers run without reuse. At least one field or
|
||||
the note is required; an unknown or withdrawn attachment is refused.
|
||||
- `books.set_entry_settles { entry_id, settles }` → `{ ok, entry_id,
|
||||
settles }`. Records which entries an entry pays back (a reimbursement
|
||||
settling the expenses a member paid personally), journaled as
|
||||
`entry.settles`; an empty list clears it. Every named entry must exist,
|
||||
none may be the paying entry itself or be named twice. It changes no
|
||||
amount. "The member was paid back" is read from this record only, never
|
||||
guessed (IZ-18); Needs attention asks until it is recorded.
|
||||
- `books.consolidate_duplicates { confirmed? }` → `{ status: "preview" |
|
||||
"consolidated", groups: [{ sha256, keep, withdraw, links }], withdrawn
|
||||
}`. For every set of attachments holding the same bytes it keeps the
|
||||
oldest (`keep`), links it to every subject the others had, unlinks the
|
||||
others and withdraws them as duplicates (`attachment.withdrawn` with
|
||||
`duplicate_of`; nothing is deleted). A posted entry that would be left
|
||||
with no receipt the policy requires refuses the whole call, journaling
|
||||
nothing. Without `confirmed`, a preview that changes nothing.
|
||||
|
||||
## Receipt enforcement
|
||||
|
||||
@@ -1868,8 +1929,12 @@ Hand-rolled on `ServerHandler` (rmcp 3.x has no resource macros):
|
||||
this month so far
|
||||
- `books://handbook` (the index) and `books://handbook/{section}` (one per
|
||||
section, Markdown)
|
||||
- `books://receipts/{id}` — receipt metadata (as `books.get_receipt`
|
||||
- `books://receipts/{id}` (an alias of `books://attachments/{id}`) —
|
||||
receipt metadata and the first 8 KiB of its text (as `books.get_receipt`
|
||||
view `meta`; no bytes)
|
||||
- `books://attachments/{id}/page/{n}` — page `n` (from 1) as a PNG blob,
|
||||
the first binary resource (plan P6; D-46: the bytes are the resource's
|
||||
contents, never a path)
|
||||
|
||||
Resources exist so read-only clients (dashboards, the viewer,
|
||||
auditors) never need tool credentials. Tested end to end through a real
|
||||
|
||||
Reference in new issue
Block a user