docs(accounting): record the operator's two decisions of 2026-09-17

The content accounting stays OPT-IN until the losses it reports on the
reference corpus are fixed; a default-on door would fail builds that pass
today. Of the three exceptions the gate proposed, only the PDF one
(heading, paragraph, table) is approved. Approving it moves no number,
because no witness counts a heading in a PDF: what it changes is that the
gap is a stated limit of the instrument rather than an open question
about the build. An image in a workbook, or in md/txt/csv/json/odt/rtf,
stays unaccounted and therefore stays red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-18 00:37:04 +02:00
commit 751441c17d
5 changed files with 50 additions and 15 deletions

View file

@ -33,6 +33,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
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

View file

@ -937,9 +937,12 @@ and fixtures, never code.
from byte equality, which would credit R761's 7 unpointed duplicates.
- **R761 is refused whole by guard 1.4.0 because of its 71 U+00AD soft
hyphens.** That is the security repo's question, and it has been asked.
- Three exceptions are PROPOSED and none is approved: PDF
headings/paragraphs/tables, xlsx images, and images in md, txt, csv,
json, odt and rtf.
- **Operator 2026-09-17:** the door stays OPT-IN until the reference
corpus's losses are fixed, and of the three proposed exceptions only the
PDF one (heading, paragraph, table) is APPROVED. It moves no number --
no witness counts a heading in a PDF -- so what it changes is that the
gap is a stated limit. An image in xlsx, md, txt, csv, json, odt or rtf
stays unaccounted, and therefore red.
- Reports: `docs/2026-09-17-innholdsregnskapet-rod-gate.md`,
`docs/2026-09-17-innholdsregnskapet-bygget.md`.
- **Judge a bundle: `okf quality <bundle>` (G37, 2026-09-12).** A per-file-type

View file

@ -362,6 +362,14 @@ short element such as a section label can be found elsewhere in the same
document. The judge is `tools/okf_accounting_gate.py`, which compares the
inventory against an independent witness.
**Two operator decisions, 2026-09-17.** The accounting stays OPT-IN until the
losses it reports on the reference corpus are fixed, because a default-on door
would fail builds that pass today. And of the three exceptions the gate
proposed, only the PDF one is approved: a PDF without a structure tree
declares no heading, paragraph or table, so no witness can count them. An
image in a workbook, or in md, txt, csv, json, odt or rtf, stays unaccounted
and therefore stays red.
Two things hold with or without the flag:
- `okf build` exits 1 when it extracted at least one document and persisted

View file

@ -380,8 +380,25 @@ def test_row6_without_its_source_is_red_locally_and_skipped_in_ci(tmp_path: Path
def test_the_proposed_exceptions_are_not_applied() -> None:
assert gate.APPROVED_EXCEPTIONS == frozenset()
assert "NOT APPROVED" in gate.render([])
assert not {suffix for suffix, _ in gate.APPROVED_EXCEPTIONS} & {
item["suffix"] for item in gate.PROPOSED_EXCEPTIONS
}, "an exception cannot be both proposed and approved"
def test_the_operator_approved_the_pdf_exception_and_nothing_else() -> None:
"""Operator 2026-09-17, answering the gate's three proposals: the PDF one
only. It moves no number -- no witness counts a heading in a PDF -- so what
it changes is that the gap is a stated limit rather than an open question.
"""
assert gate.APPROVED_EXCEPTIONS == frozenset(
{(".pdf", "heading"), (".pdf", "paragraph"), (".pdf", "table")}
)
assert [item["suffix"] for item in gate.PROPOSED_EXCEPTIONS] == [
".xlsx",
".md .txt .csv .json .odt .rtf",
]
assert gate.APPROVED_ON in gate.render([])
def test_bad_usage_exits_two() -> None:

View file

@ -100,19 +100,20 @@ RED = "RED"
SKIPPED = "SKIPPED"
DIAGNOSTIC = "DIAGNOSTIC"
#: Exceptions the operator has approved, by (suffix, element). Empty: the
#: first time an exception arises it is the operator's to approve.
APPROVED_EXCEPTIONS: frozenset[tuple[str, str]] = frozenset()
#: Exceptions the operator has APPROVED, by (suffix, element), with the date.
#: Approving one does not move a number here: the witness counts no such
#: element in the first place, which is the whole reason the exception exists.
#: What approval changes is that the gap is now a stated limit of this
#: instrument rather than an open question about the build.
APPROVED_EXCEPTIONS: frozenset[tuple[str, str]] = frozenset(
{(".pdf", "heading"), (".pdf", "paragraph"), (".pdf", "table")}
)
#: When, and by whom.
APPROVED_ON = "2026-09-17, operator"
#: Exceptions this gate PROPOSES. Listed in every run; none of them is applied.
PROPOSED_EXCEPTIONS: tuple[dict[str, str], ...] = (
{
"suffix": ".pdf",
"element": "heading, paragraph, table",
"reason": "a PDF without a structure tree declares none of them, so no "
"witness can count them; the witness counts pages and image placements",
"carried_instead": "the page text, with headings recovered by rule",
},
{
"suffix": ".xlsx",
"element": "image",
@ -633,7 +634,9 @@ def render(rows: list[Row]) -> str:
f" - {item['suffix']} {item['element']}: {item['reason']}; "
f"carried instead: {item['carried_instead']}"
)
lines.append(f"approved exceptions: {len(APPROVED_EXCEPTIONS)}")
lines.append(f"exceptions approved ({APPROVED_ON}), and none moves a denominator:")
for suffix, element in sorted(APPROVED_EXCEPTIONS) or [("(none)", "")]:
lines.append(f" - {suffix} {element}")
failing = [str(r.number) for r in rows if r.fails]
lines.append("")
lines.append(