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>
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
persistedthat no concept in the bundle names; - a document declared
rejectedthat the bundle holds a concept from; - a rejection code outside a closed list, at the document AND at the element;
- an
accounting_versionthe 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 buildexiting 1 unseen: every element had a fate, sou = 0, d = 0. The row asks the question separately now. - A skipped row never leaves the verdict unqualified.
GATE GREENnow readsGATE 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.jsonfile 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