llm-ingestion-okf/docs/2026-09-18-regnskapsgaten-herdet.md
Kjell Tore Guttormsen ed8d9d709f
test(accounting): row 6 sees a refusal, a second real corpus, and 34 of 34 mutants
Row 6 was GREEN with R761 100 % rejected: every element of a refused document
is booked as a coded rejection, so u = 0 and d = 0 and the bundle is empty.
`refused_whole` asks that question on its own now -- the build order asked for
an honest red there, and PM re-measured the green on 2026-09-18 with
`okf build` exiting 1 unseen.

A skipped row no longer leaves the verdict unqualified (`GATE GREEN (row 6 not
run: ...)`), and the exit code is non-zero locally when a corpus source is on
the machine and its row did not run. m-2.

N200 Vegbygging:2024 joins R761 as a second real corpus. R761 holds 0 `fig`,
0 formulas and 0 references, so the only real corpus could not have found the
hole in the STS role map; N200 carries 194 citations, 49 figures and 135
footnotes. A `.json` whose root holds an STS node tree is counted as STS
rather than as keys and leaves -- the container is not the content.

M-4: the review's 26 mutants, ported to the code as it stands, plus 8 for the
new checks. 34 of 34 killed. `tools/okf_gate_mutants.py` runs on a copy of the
tree, and a pattern that does not match is an ERROR and exit 2 -- a mutant
that could not be applied was never measured. That fired once, on M13, after a
refactor moved the line it edits.

m-3: `APPROVED_EXCEPTIONS` was read by no row, so approving one changed
nothing. Each pair is now checked against the witness's own vocabulary and the
run says why it moves no denominator. The gate also prints its OWN limits
beside the verdict, m-5 among them.

The product's accounting tests state the new truth instead of the old one:
`okf build --accounting` over the fixture corpus exits 1 with SIX unaccounted
elements in its own vocabulary -- its first real finding, reachable only now
that fixtures carry the constructs. Four shared element names disagree with
the witness, each pinned with its cause; one of the four is a double count
this package makes (a text box's paragraph, once inside the box and again in
the paragraph carrying it).

Three fixture defects were found and fixed while building them, each of which
would have reported a loss the build never had: a shared string table not
related to the workbook, a `graphicData` with no `uri`, and an odt
`styles.xml` without `<office:styles/>`.

Report: docs/2026-09-18-regnskapsgaten-herdet.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 02:48:34 +02:00

11 KiB

The content-accounting gate, hardened: the judge opens the bundle itself

2026-09-18. Order 20260917T142837Z-445369650, from an independent adversarial review of 0b00de4 commissioned after the accounting was built. The review's verdict was that the gate is "an honest RED today, but cannot be trusted as a judge once it turns green": it was a calculator over a report the judged party writes. This is what changed, measured.

Nothing in src/ moved. The instrument was hardened; the thing it judges was not touched.

1. BLOCKER B-1 — the gate never opened a concept file

okf_accounting_gate.account() compared BOOKED NUMBERS with the witness's counts. Reproduced here before any code moved, as two tests over the real fixture bundle: a report that changes not one byte of the bundle and books every element as carried gave GATE GREEN, exit 0 — and so did booking every element as rejected.

The witness now gives every element the PIECES OF TEXT IT IS MADE OF, and the gate looks for each of them in the concept bodies the run wrote. Pieces rather than one joined string, and that distinction is load-bearing: a reader writes a heading's ATX marker and a picture's pointer block between the parts of a container, so an STS <sec> is never one contiguous run in the bundle even when every word of it is there. Measured — with one joined string per element, 2 of the 13 fixture documents reported false losses; with pieces, 0.

The normalisation is the one the build already uses and no looser (strip non-alphanumerics, casefold, remove a converter attribute block such as {.mark}), and it is implemented in the gate rather than imported from accounting.py: a judge borrowing the judged party's own comparison agrees with it by construction.

Five more refusals, each driven from both sides by a test:

  • a negative booking (carried: 25, rejected: {x: -15} of 10 read as clean);
  • a document declared persisted that no concept in the bundle names;
  • a document declared rejected that the bundle holds a concept from;
  • a rejection code outside a closed list, at the document AND at the element;
  • an accounting_version the gate does not read.

And: a document the build PERSISTED whose report carries nothing from it is never clean. "Everything rejected" satisfied u = 0, d = 0.

m-1: an asset proved a carry by NAME, so a zero-byte file called <sha12>-x.png read as a carry. It is now checked on the BYTES and on the content address. The first attempt re-implemented the build's full naming rule and reported 50 of 50 carried R761 images as missing — the build lowercases the basename, folds separator runs and takes the suffix from the bytes. The readable tail is the build's cosmetic choice and is deliberately not reproduced here.

2. MAJOR M-1 and M-2 — what the witnesses could not see

Rows 2 and 3 require the build's inventory to EQUAL the witness's, so what the witness does not count, nothing can lose visibly. Thirteen classes were added, each with a red test first:

format now counted
.docx header/footer, comment, endnote, text box
.pptx speaker note, hidden slide (show="0", no longer an ordinary slide)
.xlsx formula, hidden sheet
.odt header/footer (they live in styles.xml), annotation
.xml mixed-citation, mml:math, fig, a figure's caption
.pdf the page's own TEXT (it counted pages and picture placements only)

A text box's paragraphs are the box's own, or the text is booked twice. A hidden sheet's state lives in workbook.xml and is reached through the relationship id, so the sheet part itself says nothing about it. An annotation counted as prose makes the accounting demand that a reader carry a note the author wrote to themselves.

M-2: the two STS witnesses shared ONE role map, so row 5 — "two witnesses agree" — could not see a hole in it. They are written apart now. Measured on R761 2026-09-18: the XML delivery places a section's label at sec/label 7 714 times and inside a title 0 times; the JSON delivery puts 2 760 of them inside the title. The XML map was narrowed to the XML fact, and a JSON twin fixture carrying the nested placement holds the difference live — without it, swapping the maps changed nothing measurable.

3. MAJOR M-3 — 20 of 63 element types had a count of zero

A witness cannot be caught being wrong about something it never sees; that is why six of seven witness mutants survived the original suite. Seven hand-built documents close it (tests/fixtures/accounting/make_accounting_fixtures.py), every element type now occurs at least once, and all twenty documents carry a hand count read off the fixture's own bytes — four did before. The operator's own proposed .xlsx image exception could not be exercised at all until now.

Building them found three defects in the fixtures themselves, each of which would have reported a loss the build never had: a shared string table not related to the workbook (every t="s" cell converts empty), a graphicData with no uri (pandoc does not see a table), and an odt styles.xml without <office:styles/> (pandoc refuses the document).

4. Row 6 and the verdict line

  • R761 refused whole under the default gate is now RED. It was GREEN with 31 096 of 31 096 elements booked as coded rejections and okf build exiting 1 unseen: every element had a fate, so u = 0, d = 0. The row asks the question separately now.
  • A skipped row never leaves the verdict unqualified. GATE GREEN now reads GATE GREEN (row 6 not run: <reason>), and the exit code is non-zero locally when a corpus source exists on the machine and the row did not run.
  • N200 Vegbygging:2024 is a second real corpus. R761 holds 0 fig, 0 formulas and 0 references, so the gate's only real corpus could not have found the hole in the role map. N200 carries 194 citations, 49 figures and 135 footnotes. A .json file whose root carries an STS node tree is counted as STS rather than as keys and leaves — the container is not the content.

5. MAJOR M-4 — mutation

The review's 26 mutants, ported to the code as it stands, plus 8 written for the new checks: 34 of 34 killed (21 of the review's 26 were killed before the last seven tests were added; all 26 are killed now, against the order's bar of 23). The harness is tools/okf_gate_mutants.py, it runs on a copy of the tree, and a pattern that does not match is an ERROR and exit 2 — a mutant that could not be applied was never measured, and counting it as killed is the same mistake as reading an empty search as an absence. That fired once, on M13, after a refactor moved the line it edits.

6. The gate is REDDER than before, and that is the movement

At the shipped defaults, python3 tools/okf_accounting_gate.py, 3 m 51 s:

1 file types with a fasit fixture       | 13 of 13  | GREEN
2 source inventory before build         |  8 of 13  | RED
3 accounting after build                | 17 of 22  | RED
4 a rejected document is reported       |  1 of 1   | GREEN
5 two witnesses agree                   |  4 of 4   | GREEN
6 real corpora                          | 110 of 111| RED
GATE RED: rows 2, 3, 6

Row 3: u = 19 unaccounted, d = 2 double-booked, 0 booked carried and not in the bundle, 0 declarations the gate refuses — and 314 carried elements FOUND in the bundle, 6 carrying no text the gate can check. The zero matters as much as the nineteen: nothing the build DOES book as carried failed the bundle check, so every red is the build's and none is the instrument's.

The new "not carried" classes, per file type, with counts on the fixtures — the raw material for the next capability order and for the operator's exception list:

file type not carried on the fixtures
.docx comment, endnote, footnote, header/footer, text box 1, 1, 1, 2, 1
.odt annotation, header/footer, image 1, 2, 1
.pptx hidden slide, speaker note 1, 1
.xlsx formula, hidden sheet, image 1, 1, 1
.xml (STS) citation, figure, figure caption, math 1, 1, 1, 1

Two are DOUBLE-booked rather than lost: a hidden slide and a hidden sheet are carried into the bundle but booked as an ordinary slide and an ordinary sheet. A third is a genuine double count in this package: a text box's paragraph is counted twice, once inside the box and once in the paragraph carrying it.

On the real corpora: R761 under --gate none is 31 081 carried elements found in the bundle, 0 claimed and not found, 65 the gate cannot check, over a document holding 31 096. N200 is 16 549 elements with no fate at all, because okf build exits 2 on it (see § 7).

okf build --accounting over the fixture corpus now exits 1 with 6 unaccounted in the build's OWN vocabulary — its first real finding, and it only became reachable when the fixtures carried the constructs.

7. Two facts about okf build this run establishes

R761 is felled 100 % by ONE rule, and it is not tier-dependent. The document is refused whole under the default guard-trusted-source with fail_secure. Asked directly, guard 1.4.0 answers any-tier: invisible carrier (output:zero-width-present), severity HIGH, assessment SEVERE — and PRESET_USER_UPLOAD gives the same disposition, so no tier this package offers persists it. The cause is measured rather than inferred: the document carries 71 U+00AD soft hyphens, and removing them takes the same text to WARN, which persists. Whether that is intended is below.

N200 gets 0 plans and exit 2. The JSON delivery is fenced verbatim, so no line-anchored boundary grammar fires — the same behaviour a structureless .txt produces, documented in CLAUDE.md. It is not an accounting defect, and the gate reports it as what it is: 16 549 elements with no fate.

8. What this gate still cannot check

Printed on every run, beside the verdict, because a gate that reports only the build's gaps invites a green row to be read as a guarantee:

  • a short element often stands elsewhere in the same document, so finding it proves it is present and not that THIS one is;
  • two pointed files with identical bytes are one content-addressed asset, so one of them losing its pointer is invisible here (m-5);
  • an image embedded in a binary container has no source file to hash;
  • absence is never verified: an element booked REJECTED is not looked for;
  • the witness is a second implementation of the same definitions, so a definition that is wrong for a format is wrong on both sides at once.

Every witness also prints, per file type, the named list of what it still does not count. APPROVED_EXCEPTIONS was read by no row at all (m-3); each pair is now checked against the witness's vocabulary and the run says, per pair, why it moves no denominator — and would say WARNING if one ever did.

9. Reproduction

uv run python tools/okf_accounting_gate.py            # 3 m 51 s, exit 1
uv run python tools/okf_gate_mutants.py               # 34 of 34 killed, exit 0
uv run pytest tests/test_accounting_gate.py tests/test_content_accounting.py -q
uv run python tools/okf_witness.py tests/fixtures/accounting/corpus
python3 tests/fixtures/accounting/make_accounting_fixtures.py