test(accounting): content-accounting gate for okf build, written red
Capability loop step 3, no capability. tools/okf_accounting_gate.py asks, per README file type, how many of the elements a SOURCE holds okf build books as carried / pointer / coded rejection, with unaccounted and double-booked both required to be 0. Exit 1 today on rows 2, 3, 4 and 6. The fasit is tools/okf_witness.py (stdlib + pdfplumber + poppler, no package import; tested on the live import graph), committed as tests/fixtures/accounting/*inventory.json over one fixture per type. Measured: no source inventory (0 of 13); two graphics/ files carried through documents AND counted extractor_unknown (50 on R761 under --gate none); a refused document logged "0 carried of 0 found"; R761 refused whole because guard 1.4.0 treats its 71 U+00AD soft hyphens as an invisible carrier (asked of the security repo). The two R761 witnesses agree once STS labels are counted by role, not tag. Report: docs/2026-09-17-innholdsregnskapet-rod-gate.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bc39e8091f
commit
0b00de4408
26 changed files with 2598 additions and 0 deletions
149
docs/2026-09-17-innholdsregnskapet-rod-gate.md
Normal file
149
docs/2026-09-17-innholdsregnskapet-rod-gate.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Content accounting in `okf build`: the gate, written red
|
||||
|
||||
Capability loop, step 3. This session adds no capability. It adds the gate a
|
||||
later capability has to turn green, together with the fasit that gate judges
|
||||
against.
|
||||
|
||||
- Command: `python3 tools/okf_accounting_gate.py [--json] [--r761 DIR] [--consume]`
|
||||
- Exit codes: 0 when every failing row (1–6) is green, 1 otherwise, 2 when
|
||||
the gate did not run.
|
||||
- Fasit: `tools/okf_witness.py`, committed as
|
||||
`tests/fixtures/accounting/*inventory.json`.
|
||||
- Tests: `tests/test_accounting_gate.py`, 42 tests, green.
|
||||
|
||||
## Why a gate at all
|
||||
|
||||
`okf build`'s conservation identity, `merged + coded rejections == N`, counts
|
||||
FILES. Two things happen that the identity cannot see, and both were
|
||||
reproduced here on `bc39e80` (v0.10.0) before any code was written:
|
||||
|
||||
- **A file can be merged while content inside it is gone.** No inventory of
|
||||
the source exists, so nothing can state what a merged file lost.
|
||||
- **A file can be carried and rejected at once (finding B).** The fixture
|
||||
corpus has a `graphics/` directory next to its documents. Its two PNGs are
|
||||
carried into `assets/` through the documents that point at them, and the
|
||||
same two files are also counted as `extractor_unknown` rejections. Each has
|
||||
two fates.
|
||||
- **A rejected document is logged as if it held nothing (finding C).** The
|
||||
fixture document the guard refuses produces
|
||||
`Images: 0 carried of 0 found`, although its source declares one image.
|
||||
|
||||
## The fasit is independent
|
||||
|
||||
The witness imports no `llm_ingestion_okf` module. A test runs every witness
|
||||
in a fresh process and then reads `sys.modules`. A control run of the same
|
||||
check, with the package pre-loaded, shows the check fires.
|
||||
|
||||
The witness counts each format by the format's own rules:
|
||||
|
||||
- STS: ElementTree, and the publisher's JSON node tree.
|
||||
- docx, pptx, xlsx and odt: the zip members' own XML.
|
||||
- PDF: pdfplumber objects, and poppler (`pdfinfo`, `pdfimages -list`).
|
||||
- HTML: `html.parser`.
|
||||
- md, txt, csv, json and rtf: stdlib readers, and a control-word scan for rtf.
|
||||
|
||||
Four documents are also pinned to hand counts.
|
||||
|
||||
**Two witnesses disagreed by tag, and both disagreements were about placement
|
||||
rather than content** (R761, measured). A section's label is `sec/label` on
|
||||
7 714 sections in the XML. The JSON puts it at `sec/label` on 4 954 of them
|
||||
and at `sec/title/label` on the other 2 760. A table's label is
|
||||
`table-wrap/label` in the XML (10) and `table-wrap/table/caption` in the JSON
|
||||
(10). The witness therefore counts ROLES, and the mapping is written in
|
||||
`okf_witness._sts_role` together with these numbers. After the mapping, the
|
||||
two R761 witnesses agree on all ten roles.
|
||||
|
||||
The PM's figures were re-measured and all hold: `p` 12 528, `title` 2 761,
|
||||
`td` 197, `list-item` 111, `graphic` 50, 109 other files in the zip (50
|
||||
pointed at, 59 not). The PDF witnesses agree at 701 pages and 50 images;
|
||||
pdfplumber takes 50 s for that and poppler 1 s.
|
||||
|
||||
## The door the capability must open
|
||||
|
||||
The gate reads `okf build --accounting PATH`: one JSON object with a
|
||||
per-document `inventory` taken before extraction and before the gate,
|
||||
per-element `fates` (`carried` / `pointer` / `rejected{code}`), and exactly
|
||||
one `fate` per inbox file that is not a document. It also reads one line in
|
||||
`log.md` per rejected document:
|
||||
|
||||
<source_file>: <M> elements found in the source, 0 carried: document rejected `<code>`
|
||||
|
||||
The full contract is in the module docstring. Until the flag exists, row 2 is
|
||||
0 of 13, and row 3 books every element as unaccounted. For a file, it derives
|
||||
the file's fates from the bundle bytes (assets are named by content) and from
|
||||
K1b.
|
||||
|
||||
## Finding A: why the guard refuses R761, and the decision
|
||||
|
||||
The cause is measured. The R761 XML carries **71 U+00AD SOFT HYPHEN** and no
|
||||
other invisible codepoint, and the extracted text carries the same 71. Guard
|
||||
1.4.0 lists U+00AD in `_ZERO_WIDTH_CPS` (`output.py:250`). One soft hyphen is
|
||||
enough to fail_secure at both tiers; the same string without it is `clean`.
|
||||
Image pointers are not the cause: `--no-assets` gives the same refusal, as the
|
||||
PM also measured.
|
||||
|
||||
This is a question for the guard. It was sent to
|
||||
`llm-ingestion-pipeline-security` with the repro, and nothing was built there.
|
||||
|
||||
**Decision on exit 0 when nothing was persisted:** `okf build` should NOT exit
|
||||
0 when at least one document was extracted and none was persisted. The
|
||||
conservation identity holding is necessary, not sufficient.
|
||||
|
||||
- This repository already refuses the analogous case: a run with zero plans
|
||||
fails (exit 2) rather than emitting a flat bundle and calling it success.
|
||||
- `okf build` is the trusted-source path, an operator pointing at their own
|
||||
folder, where "everything refused" is a finding and not a normal outcome.
|
||||
- A script running `okf build && okf skill` would otherwise wire a skill to an
|
||||
empty bundle.
|
||||
|
||||
Door B's library function keeps its behaviour, because for a hostile inbox
|
||||
"all rejected" is correct. The change is an exit code, which this order
|
||||
forbids, so it belongs to the capability order and not here.
|
||||
|
||||
## Exceptions proposed, none approved
|
||||
|
||||
The gate lists three exceptions in every run. None of them lowers a
|
||||
denominator:
|
||||
|
||||
- **PDF headings, paragraphs and tables.** A PDF without a structure tree
|
||||
declares none of them, so no witness can count them.
|
||||
- **xlsx images.** The reader deliberately does not carry them.
|
||||
- **Images in md, txt, csv, json, odt and rtf.** No reader for these types
|
||||
carries image bytes.
|
||||
|
||||
Approving any of them is the operator's decision.
|
||||
|
||||
## Output at this commit
|
||||
|
||||
See the order's closing message for the full text. In short:
|
||||
|
||||
| Row | Result | Status |
|
||||
|---|---|---|
|
||||
| 1 | 13 of 13 | GREEN |
|
||||
| 2 | 0 of 13 | RED |
|
||||
| 3 | 0 of 15, u = 248, d = 2 | RED |
|
||||
| 4 | 0 of 1 | RED |
|
||||
| 5 | 4 of 4 | GREEN |
|
||||
| 6 | 59 of 110 (R761, two builds) | RED |
|
||||
|
||||
Row 6 runs two builds:
|
||||
|
||||
- **Default gate:** the document is refused, so every element is unaccounted.
|
||||
- **`--gate none`:** the document is persisted, and the 50 pictures it points at
|
||||
are both carried and rejected, which is finding B at scale (d = 50). A first
|
||||
run reported 57: 7 of the 59 unpointed files share bytes with pointed ones,
|
||||
and bytes in `assets/` had been credited to them. A file nothing points at
|
||||
is never carried; a test holds that.
|
||||
|
||||
Row 7 (`--consume`) is a diagnostic and never fails.
|
||||
|
||||
## Limits
|
||||
|
||||
- The witness vocabulary is ours. A format element the witness does not name
|
||||
is invisible to the gate.
|
||||
- The fixture corpus is constructed, one document per type. The real-corpus
|
||||
row is N = 1 (R761).
|
||||
- Row 6 needs the R761 source on this machine. Without it the row is RED
|
||||
locally and SKIPPED when `CI` is set, never green.
|
||||
- The `--r761` default points into another repository's data directory, and
|
||||
the gate only reads from it.
|
||||
Loading…
Add table
Add a link
Reference in a new issue