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:
Kjell Tore Guttormsen 2026-09-17 15:40:37 +02:00
commit 0b00de4408
26 changed files with 2598 additions and 0 deletions

View file

@ -175,6 +175,34 @@ The version range that carries this lives in `pyproject.toml`'s
`[project.optional-dependencies] extract`, with the same reasoning at the
declaration site.
## The content-accounting fixtures (`accounting/`)
The fasit side of `tools/okf_accounting_gate.py`. `accounting/corpus/` holds
one document per row of README's file-type table (13 of 13) plus a `graphics/`
directory next to them that the HTML, STS and markdown documents point at --
the layout under which a picture is carried through a document AND booked as a
rejected file. `accounting/rejected/` holds one HTML document with a
zero-width space in its prose, which the guard refuses at every tier, and the
image it points at.
`inventory.json` and `rejected-inventory.json` are what `tools/okf_witness.py`
counts in those two directories, committed as data and regenerated only with
that tool:
```
python3 tools/okf_witness.py tests/fixtures/accounting/corpus > tests/fixtures/accounting/inventory.json
python3 tools/okf_witness.py tests/fixtures/accounting/rejected > tests/fixtures/accounting/rejected-inventory.json
```
`witness/prosess-84-sts.twin.json` is the STS document written by hand in the
publisher's JSON node form (`standardContent`, nodes with `e`/`t`/`x`), so the
two STS witnesses can be compared on a fixture as well as on R761. Eight of
the thirteen documents are byte copies of fixtures documented above
(`image-inbox/`, `k2-office/`, `prisark.xlsx`); the other five
(`notat.md`, `logg.txt`, `mengder.csv`, `parametre.json`, `side.htm`) are
written here, and `notat.md` carries a fenced `# ...` line that is not a
heading.
## What these fixtures do not cover
Structured table recovery. Measured on real Vegnormalene, only 45 of 196

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

View file

@ -0,0 +1,52 @@
{\rtf1\ansi\ansicpg1252\deff0{\fonttbl{\f0\froman Times New Roman;}}\pard\sa180\b\fs32 Kravspesifikasjon for tunnelbelysning\b0\fs24\par
\pard\sa180 Dokumentet samler kravene til belysning i vegtunneler over 500 meter.\par
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Dokumentnummer:\cell \pard\intbl SVV-2026-0417\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Tittel:\cell \pard\intbl Tunnelbelysning i hovedl\u248 ?pet\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Ansvarlig avdeling:\cell \pard\intbl Utbyggingsdivisjonen\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Fagomr\u229 ?de:\cell \pard\intbl Elektro og belysning\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Versjon:\cell \pard\intbl 2.3 godkjent\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Gyldig fra:\cell \pard\intbl 01.03.2026\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Erstatter:\cell \pard\intbl SVV-2024-0188\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Tunnellengde:\cell \pard\intbl 1240 meter\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Dimensjonerende fart:\cell \pard\intbl 80 km/t\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl \u197 ?rsd\u248 ?gntrafikk:\cell \pard\intbl 12400 kj\u248 ?ret\u248 ?y\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Terskelluminans:\cell \pard\intbl 145 candela\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Overgangssone:\cell \pard\intbl Tre trinn nedtrapping\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Innerstrekning:\cell \pard\intbl 3,0 candela\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Utkj\u248 ?ringssone:\cell \pard\intbl Ingen forsterkning\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Fargetemperatur:\cell \pard\intbl 4000 kelvin\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Fargegjengivelse:\cell \pard\intbl Ra st\u248 ?rre enn 70\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl N\u248 ?dbelysning:\cell \pard\intbl 60 minutter drift\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Vedlikeholdsfaktor:\cell \pard\intbl 0,80 beregnet\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl M\u229 ?lemetode:\cell \pard\intbl M\u229 ?ling med luminanskamera\cell \row
\trowd\trgaph108\cellx3000\cellx6000
\pard\intbl Avviksh\u229 ?ndtering:\cell \pard\intbl S\u248 ?knad om fravik\cell \row
\pard\sa180 Luminansmatrise per trafikklasse\par
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
\pard\intbl Sone\cell \pard\intbl Klasse A\cell \pard\intbl Klasse B\cell \pard\intbl Klasse C\cell \row
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
\pard\intbl Terskel\cell \pard\intbl 150 cd\cell \pard\intbl 120 cd\cell \pard\intbl 95 cd\cell \row
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
\pard\intbl Overgang\cell \pard\intbl 45 cd\cell \pard\intbl 36 cd\cell \pard\intbl 28 cd\cell \row
\trowd\trgaph108\cellx3000\cellx6000\cellx9000\cellx12000
\pard\intbl Indre\cell \pard\intbl 3,5 cd\cell \pard\intbl 3,0 cd\cell \pard\intbl 2,5 cd\cell \row
}

Binary file not shown.

View file

@ -0,0 +1,6 @@
Driftslogg for tunnelen.
Vask utført i mars.
Lys kontrollert i april.
Ingen avvik registrert.

View file

@ -0,0 +1,3 @@
Post,Mengde,Enhet
84.1,120,m3
84.2,45,m2
1 Post Mengde Enhet
2 84.1 120 m3
3 84.2 45 m2

View file

@ -0,0 +1,19 @@
# Drift av tunneler
Tunnelen driftes etter håndboka.
## Vask
Vask skjer to ganger i året.
| Sone | Intervall |
| --- | --- |
| Inngang | 6 mnd |
| Hovedløp | 12 mnd |
![Figur 1 Vaskeplan](graphics/tabell-84-2.png)
```bash
# ikke en overskrift
okf build .
```

View file

@ -0,0 +1 @@
{"tunnel": "Eksempeltunnelen", "lengde_m": 1240, "soner": ["inngang", "hovedlop"], "belysning": {"terskel_cd": 145, "fart_kmt": 80}}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<standard xmlns:xlink="http://www.w3.org/1999/xlink">
<front><std-ident><doc-number>R761</doc-number></std-ident></front>
<body>
<sec>
<label>84</label>
<title>Konstruksjoner av betong</title>
<sec>
<label>84.1</label>
<title>Toleranser</title>
<p>Toleranseklasse er gitt i tabell 84-2.</p>
<graphic xlink:href="graphics/tabell-84-2.png"/>
<p>Figur 84-1 viser prinsippet.</p>
<graphic xlink:href="figur-84-1.png"/>
</sec>
</sec>
</body>
</standard>

Binary file not shown.

View file

@ -0,0 +1,14 @@
<!doctype html>
<html><head><title>Prosess 84</title></head>
<body>
<h1>84 Konstruksjoner av betong</h1>
<p>Toleranseklasse for de enkelte konstruksjonsdeler er gitt i tabell 84-2.</p>
<img src="graphics/tabell-84-2.png" alt="Tabell 84-2 Toleranseklasser">
<p>Figuren under viser prinsippet.</p>
<figure>
<img src="graphics/figur-84-1.png" alt="Figur 84-1 Prinsippskisse">
<figcaption>Figur 84-1 Prinsippskisse</figcaption>
</figure>
<p>Og en som ligger et annet sted:</p>
<img src="https://example.invalid/ekstern.png" alt="Ekstern figur">
</body></html>

View file

@ -0,0 +1,9 @@
<!doctype html>
<html><head><title>Side</title></head>
<body>
<h1>Belysning</h1>
<p>Terskelluminansen er 145 candela.</p>
<h2>Soner</h2>
<ul><li>Inngang</li><li>Hovedløp</li></ul>
<table><tr><th>Sone</th><th>Lengde</th></tr><tr><td>Inngang</td><td>120 m</td></tr></table>
</body></html>

230
tests/fixtures/accounting/inventory.json vendored Normal file
View file

@ -0,0 +1,230 @@
{
"documents": {
"krav-rikt-tekstformat.rtf": {
"elements": {
"cell": 56,
"image": 0,
"paragraph": 3,
"table_row": 24
},
"images": [],
"suffix": ".rtf",
"witness": "rtf control words"
},
"krav-tekstdokument.odt": {
"elements": {
"cell": 56,
"heading": 1,
"image": 0,
"list_item": 0,
"paragraph": 2,
"table": 2
},
"images": [],
"suffix": ".odt",
"witness": "odt zip xml"
},
"logg.txt": {
"elements": {
"line": 4,
"paragraph": 3
},
"images": [],
"suffix": ".txt",
"witness": "text lines"
},
"mengder.csv": {
"elements": {
"cell": 6,
"header_cell": 3,
"row": 2
},
"images": [],
"suffix": ".csv",
"witness": "csv"
},
"notat.md": {
"elements": {
"code_block": 1,
"heading": 2,
"image": 1,
"paragraph": 2,
"table": 1,
"table_row": 2
},
"images": [
{
"kind": "local",
"ref": "graphics/tabell-84-2.png",
"target": "graphics/tabell-84-2.png"
}
],
"suffix": ".md",
"witness": "markdown lines"
},
"parametre.json": {
"elements": {
"key": 6,
"value": 6
},
"images": [],
"suffix": ".json",
"witness": "json"
},
"prisark.xlsx": {
"elements": {
"cell": 18,
"image": 0,
"row": 9,
"sheet": 2
},
"images": [],
"suffix": ".xlsx",
"witness": "xlsx zip xml"
},
"prosess-84-notat.docx": {
"elements": {
"cell": 0,
"footnote": 0,
"heading": 0,
"image": 1,
"paragraph": 2,
"table": 0
},
"images": [
{
"kind": "embedded",
"ref": "",
"target": null
}
],
"suffix": ".docx",
"witness": "docx zip xml"
},
"prosess-84-presentasjon.pptx": {
"elements": {
"cell": 0,
"image": 1,
"paragraph": 0,
"slide": 1,
"table": 0,
"title": 1
},
"images": [
{
"kind": "embedded",
"ref": "",
"target": null
}
],
"suffix": ".pptx",
"witness": "pptx zip xml"
},
"prosess-84-sts.xml": {
"elements": {
"cell": 0,
"footnote": 0,
"image": 2,
"list_item": 0,
"paragraph": 2,
"section": 2,
"section_label": 2,
"table": 0,
"table_label": 0,
"title": 2
},
"images": [
{
"kind": "local",
"ref": "graphics/tabell-84-2.png",
"target": "graphics/tabell-84-2.png"
},
{
"kind": "local",
"ref": "figur-84-1.png",
"target": "graphics/figur-84-1.png"
}
],
"suffix": ".xml",
"witness": "xml.etree"
},
"prosess-84-tabell.pdf": {
"elements": {
"image": 2,
"page": 1
},
"images": [
{
"kind": "embedded",
"ref": "",
"target": null
},
{
"kind": "embedded",
"ref": "",
"target": null
}
],
"suffix": ".pdf",
"witness": "pdfplumber objects"
},
"prosess-84-web.html": {
"elements": {
"cell": 0,
"heading": 1,
"image": 3,
"list_item": 0,
"paragraph": 3,
"table": 0
},
"images": [
{
"kind": "local",
"ref": "graphics/tabell-84-2.png",
"target": "graphics/tabell-84-2.png"
},
{
"kind": "local",
"ref": "graphics/figur-84-1.png",
"target": "graphics/figur-84-1.png"
},
{
"kind": "remote",
"ref": "https://example.invalid/ekstern.png",
"target": null
}
],
"suffix": ".html",
"witness": "html.parser"
},
"side.htm": {
"elements": {
"cell": 4,
"heading": 2,
"image": 0,
"list_item": 2,
"paragraph": 1,
"table": 1
},
"images": [],
"suffix": ".htm",
"witness": "html.parser"
}
},
"files": {
"graphics/figur-84-1.png": {
"pointed_at_by": [
"prosess-84-sts.xml",
"prosess-84-web.html"
]
},
"graphics/tabell-84-2.png": {
"pointed_at_by": [
"notat.md",
"prosess-84-sts.xml",
"prosess-84-web.html"
]
}
},
"witness_version": 1
}

View file

@ -0,0 +1,31 @@
{
"documents": {
"avvist.html": {
"elements": {
"cell": 0,
"heading": 1,
"image": 1,
"list_item": 0,
"paragraph": 1,
"table": 0
},
"images": [
{
"kind": "local",
"ref": "graphics/figur.png",
"target": "graphics/figur.png"
}
],
"suffix": ".html",
"witness": "html.parser"
}
},
"files": {
"graphics/figur.png": {
"pointed_at_by": [
"avvist.html"
]
}
},
"witness_version": 1
}

View file

@ -0,0 +1,7 @@
<!doctype html>
<html><head><title>Avvist</title></head>
<body>
<h1>Skjult</h1>
<p>Denne teksten bærer et usynlig tegn.</p>
<img src="graphics/figur.png" alt="Figur 1">
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

View file

@ -0,0 +1,216 @@
{
"productInfo": {
"meta": {
"productTitle": "R761 fixture twin"
}
},
"standardContent": {
"id": null,
"tag": "#document",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "standard",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "front",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "std-ident",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "doc-number",
"a": {},
"c": [
{
"e": 2,
"t": "R761",
"x": null
}
]
}
}
]
}
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "body",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "sec",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "label",
"a": {},
"c": [
{
"e": 2,
"t": "84",
"x": null
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "title",
"a": {},
"c": [
{
"e": 2,
"t": "Konstruksjoner av betong",
"x": null
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "sec",
"a": {},
"c": [
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "label",
"a": {},
"c": [
{
"e": 2,
"t": "84.1",
"x": null
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "title",
"a": {},
"c": [
{
"e": 2,
"t": "Toleranser",
"x": null
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "p",
"a": {},
"c": [
{
"e": 2,
"t": "Toleranseklasse er gitt i tabell 84-2.",
"x": null
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "graphic",
"a": {
"xlink:href": "graphic_0001.png"
},
"c": []
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "p",
"a": {},
"c": [
{
"e": 2,
"t": "Figur 84-1 viser prinsippet.",
"x": null
}
]
}
},
{
"e": 1,
"t": null,
"x": {
"id": null,
"tag": "graphic",
"a": {
"xlink:href": "graphic_0002.png"
},
"c": []
}
}
]
}
}
]
}
}
]
}
}
]
}
}
]
}
}

View file

@ -0,0 +1,425 @@
"""The content-accounting gate's own tests (`tools/okf_accounting_gate.py`).
The gate is RED today by design: `okf build` has no source inventory and no
per-element accounting. These tests are GREEN and prove three things.
1. The fasit is independent. `tools/okf_witness.py` imports no
`llm_ingestion_okf` module -- checked on the live import graph of a process
that ran every witness, with a control that shows the check fires -- and
the committed inventories are exactly what the witness counts today, pinned
again to hand counts on four documents.
2. Every row CAN turn green and CAN turn red, each boundary driven from both
sides with synthetic build output (the door the capability must open).
3. Run against the real `okf build` at this commit, the gate sees the three
defects it was ordered for: no inventory, a file carried AND rejected, and
a rejected document logged as "0 carried of 0 found".
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
from typing import Any
import pytest
TOOLS = Path(__file__).resolve().parents[1] / "tools"
sys.path.insert(0, str(TOOLS))
import okf_accounting_gate as gate # noqa: E402
import okf_witness as witness # noqa: E402
TABLE = [
".csv",
".docx",
".htm",
".html",
".json",
".md",
".odt",
".pdf",
".pptx",
".rtf",
".txt",
".xlsx",
".xml",
]
# --- 1. the fasit ------------------------------------------------------------
_IMPORT_PROBE = """
import sys
sys.path.insert(0, {tools!r})
{preload}
import okf_witness as w
from pathlib import Path
fixtures = Path({fixtures!r})
w.witness_inbox(fixtures / "corpus")
w.witness_inbox(fixtures / "rejected")
w.count_sts_json((fixtures / "witness" / "prosess-84-sts.twin.json").read_bytes())
w.pdf_poppler(fixtures / "corpus" / "prosess-84-tabell.pdf")
print(sorted(m for m in sys.modules if m.split(".")[0] == "llm_ingestion_okf"))
"""
def _package_modules_after_witness(preload: str) -> list[str]:
script = _IMPORT_PROBE.format(tools=str(TOOLS), fixtures=str(gate.FIXTURES), preload=preload)
out = subprocess.run(
[sys.executable, "-c", script], capture_output=True, text=True, check=True
).stdout
result: list[str] = json.loads(out.strip().splitlines()[-1].replace("'", '"'))
return result
def test_the_witness_imports_no_module_of_the_package_it_judges() -> None:
pytest.importorskip("pdfplumber")
assert _package_modules_after_witness("") == []
def test_the_import_check_fires_when_the_package_is_loaded() -> None:
pytest.importorskip("pdfplumber")
assert "llm_ingestion_okf" in _package_modules_after_witness("import llm_ingestion_okf")
@pytest.mark.parametrize(
("inbox", "committed"),
[(gate.CORPUS, gate.INVENTORY), (gate.REJECTED, gate.REJECTED_INVENTORY)],
)
def test_the_committed_fasit_is_what_the_witness_counts(inbox: Path, committed: Path) -> None:
pytest.importorskip("pdfplumber")
assert witness.witness_inbox(inbox) == gate.load_inventory(committed)
def test_the_fixture_corpus_covers_every_readme_file_type() -> None:
inventory = gate.load_inventory(gate.INVENTORY)
assert gate.readme_types() == TABLE
assert {entry["suffix"] for entry in inventory["documents"].values()} == set(TABLE)
# Counted by hand from the fixture bytes, not by the witness.
HAND_COUNTS = {
"notat.md": {
"code_block": 1,
"heading": 2,
"image": 1,
"paragraph": 2,
"table": 1,
"table_row": 2,
},
"side.htm": {"cell": 4, "heading": 2, "image": 0, "list_item": 2, "paragraph": 1, "table": 1},
"krav-rikt-tekstformat.rtf": {"cell": 56, "image": 0, "paragraph": 3, "table_row": 24},
"prosess-84-sts.xml": {
"cell": 0,
"footnote": 0,
"image": 2,
"list_item": 0,
"paragraph": 2,
"section": 2,
"section_label": 2,
"table": 0,
"table_label": 0,
"title": 2,
},
}
@pytest.mark.parametrize("name", sorted(HAND_COUNTS))
def test_the_witness_matches_a_hand_count(name: str) -> None:
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / name)
assert inventory.elements == HAND_COUNTS[name]
def test_a_fenced_heading_is_not_a_heading_to_the_witness() -> None:
elements, _ = witness.count_markdown("# Real\n\n```bash\n# not one\n```\n")
assert elements["heading"] == 1
assert elements["code_block"] == 1
def test_the_sts_image_reference_resolves_through_the_graphics_directory() -> None:
inventory = witness.witness_file(gate.CORPUS, gate.STS_FIXTURE)
assert [ref.target for ref in inventory.images] == [
"graphics/tabell-84-2.png",
"graphics/figur-84-1.png",
]
def test_a_reference_above_the_document_resolves_to_nothing(tmp_path: Path) -> None:
(tmp_path / "secret.png").write_bytes(b"x")
(tmp_path / "docs").mkdir()
document = tmp_path / "docs" / "a.html"
assert witness.resolve_local(tmp_path, document, "../secret.png") is None
def test_the_witness_refuses_a_doctype() -> None:
with pytest.raises(witness.WitnessRefused):
witness.count_sts_xml(b'<!DOCTYPE x [<!ENTITY a "b">]><standard/>')
# --- 2. every row can go both ways -------------------------------------------
def _inventory() -> dict[str, Any]:
return {
"documents": {
"a.md": {"suffix": ".md", "elements": {"heading": 2, "image": 1}, "images": []},
},
"files": {"graphics/x.png": {"pointed_at_by": ["a.md"]}},
}
def _build(
*,
accounting: dict[str, Any] | None = None,
sources: set[str] | None = None,
assets: set[str] | None = None,
log: str = "",
exit_code: int = 0,
) -> gate.Build:
return gate.Build(
exit_code=exit_code,
log=log,
accounting=accounting,
source_files={"a.md"} if sources is None else sources,
asset_prefixes=assets or set(),
)
def _declared(heading: int = 2, image: int = 1, fate: str = "rejected") -> dict[str, Any]:
return {
"accounting_version": 1,
"documents": [
{
"source_file": "a.md",
"status": "persisted",
"code": None,
"inventory": {"heading": 2, "image": 1},
"fates": {
"heading": {"carried": heading},
"image": {"pointer": image},
},
}
],
"files": [{"source_file": "graphics/x.png", "fate": fate, "code": "extractor_unknown"}],
}
def _corpus(tmp_path: Path) -> Path:
(tmp_path / "graphics").mkdir()
(tmp_path / "graphics" / "x.png").write_bytes(b"png bytes")
return tmp_path
def test_row1_is_green_when_every_type_has_a_fasit() -> None:
inventory = {
"documents": {f"f{s}": {"suffix": s, "elements": {}} for s in TABLE},
"files": {},
}
row = gate.row1(TABLE, inventory, inventory)
assert (row.k, row.m, row.status) == (13, 13, gate.GREEN)
def test_row1_is_red_when_one_type_lacks_a_fasit() -> None:
inventory = {
"documents": {f"f{s}": {"suffix": s, "elements": {}} for s in TABLE[:-1]},
"files": {},
}
row = gate.row1(TABLE, inventory, inventory)
assert (row.k, row.m, row.status) == (12, 13, gate.RED)
def test_row1_is_red_when_the_committed_fasit_is_stale() -> None:
inventory = {"documents": {"f.md": {"suffix": ".md", "elements": {"heading": 1}}}}
fresh = {"documents": {"f.md": {"suffix": ".md", "elements": {"heading": 2}}}}
row = gate.row1([".md"], inventory, fresh)
assert (row.k, row.status) == (0, gate.RED)
def test_row2_is_red_without_the_door() -> None:
row = gate.row2([".md"], _inventory(), _build(), door=False)
assert (row.k, row.m, row.status) == (0, 1, gate.RED)
def test_row2_is_green_when_the_declared_inventory_equals_the_witness() -> None:
row = gate.row2([".md"], _inventory(), _build(accounting=_declared()), door=True)
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
def test_row2_is_red_when_the_declared_inventory_is_one_off() -> None:
declared = _declared()
declared["documents"][0]["inventory"]["heading"] = 3
row = gate.row2([".md"], _inventory(), _build(accounting=declared), door=True)
assert (row.k, row.status) == (0, gate.RED)
def test_row3_is_green_when_every_element_and_file_has_one_fate(tmp_path: Path) -> None:
units = gate.account(_inventory(), _build(accounting=_declared()), _corpus(tmp_path))
row = gate.row3(units, door=True)
assert (row.k, row.m, row.status) == (2, 2, gate.GREEN)
@pytest.mark.parametrize(("heading", "u", "d"), [(1, 1, 0), (3, 0, 1)])
def test_row3_is_red_one_element_either_side(tmp_path: Path, heading: int, u: int, d: int) -> None:
units = gate.account(
_inventory(), _build(accounting=_declared(heading=heading)), _corpus(tmp_path)
)
document = units[0]
assert (document.unaccounted, document.double) == (u, d)
assert gate.row3(units, door=True).status == gate.RED
def test_row3_is_red_when_no_fate_is_declared(tmp_path: Path) -> None:
units = gate.account(_inventory(), _build(), _corpus(tmp_path))
assert units[0].unaccounted == 3
assert gate.row3(units, door=False).status == gate.RED
def test_a_file_carried_through_a_document_and_rejected_is_double_booked(tmp_path: Path) -> None:
corpus = _corpus(tmp_path)
carried = {gate._sha12(corpus / "graphics" / "x.png")}
units = gate.account(_inventory(), _build(accounting=_declared(), assets=carried), corpus)
assert (units[1].unaccounted, units[1].double) == (0, 1)
def test_a_file_carried_through_a_document_and_declared_carried_is_clean(tmp_path: Path) -> None:
corpus = _corpus(tmp_path)
carried = {gate._sha12(corpus / "graphics" / "x.png")}
build = _build(accounting=_declared(fate="carried"), assets=carried)
assert gate.account(_inventory(), build, corpus)[1].clean
def test_a_file_declared_carried_without_its_bytes_is_unaccounted(tmp_path: Path) -> None:
build = _build(accounting=_declared(fate="carried"))
unit = gate.account(_inventory(), build, _corpus(tmp_path))[1]
assert (unit.unaccounted, unit.double) == (1, 0)
def test_an_unpointed_file_sharing_bytes_with_a_carried_one_is_not_carried(
tmp_path: Path,
) -> None:
corpus = _corpus(tmp_path)
(corpus / "graphics" / "twin.png").write_bytes(b"png bytes")
inventory = _inventory()
inventory["files"]["graphics/twin.png"] = {"pointed_at_by": []}
carried = {gate._sha12(corpus / "graphics" / "x.png")}
units = gate.account(inventory, _build(assets=carried), corpus)
assert [(u.name, u.double) for u in units[1:]] == [
("graphics/twin.png", 0),
("graphics/x.png", 1),
]
def test_without_the_door_double_booking_is_derived_from_conservation(tmp_path: Path) -> None:
corpus = _corpus(tmp_path)
carried = {gate._sha12(corpus / "graphics" / "x.png")}
assert gate.account(_inventory(), _build(assets=carried), corpus)[1].double == 1
assert gate.account(_inventory(), _build(), corpus)[1].clean
_HONEST_LOG = (
"* **Images**: 0 carried of 1 found, written to `assets/`.\n"
"* a.md: 3 elements found in the source, 0 carried: document rejected `fail_secure`\n"
)
def _rejected_inventory() -> dict[str, Any]:
inventory = _inventory()
inventory["documents"]["a.md"]["images"] = [{"kind": "local"}]
return inventory
def test_row4_is_green_when_the_log_names_what_the_rejected_document_held() -> None:
row = gate.row4(_rejected_inventory(), _build(sources=set(), log=_HONEST_LOG))
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
@pytest.mark.parametrize(
"log",
[
_HONEST_LOG.replace("0 carried of 1 found", "0 carried of 0 found"),
_HONEST_LOG.replace("3 elements", "2 elements"),
_HONEST_LOG.replace(" `fail_secure`", ""),
],
)
def test_row4_is_red_when_the_log_understates_the_rejected_document(log: str) -> None:
row = gate.row4(_rejected_inventory(), _build(sources=set(), log=log))
assert (row.k, row.status) == (0, gate.RED)
def test_row4_cannot_be_green_when_nothing_was_rejected() -> None:
row = gate.row4(_rejected_inventory(), _build(log=_HONEST_LOG))
assert (row.m, row.status) == (0, gate.RED)
def test_row5_is_green_when_the_witnesses_agree() -> None:
row = gate.row5([("pair", gate.compare({"p": 3}, {"p": 3}))], [])
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
def test_row5_is_red_with_both_numbers_when_they_disagree_by_one() -> None:
row = gate.row5([("pair", gate.compare({"p": 3}, {"p": 4}))], [])
assert row.status == gate.RED
assert row.details == ["pair: p: 3 vs 4"]
def test_row5_is_red_when_a_witness_is_missing() -> None:
assert gate.row5([("pair", gate.compare({"p": 3}, None))], []).status == gate.RED
def test_row6_without_its_source_is_red_locally_and_skipped_in_ci(tmp_path: Path) -> None:
missing = tmp_path / "absent"
local = gate.row6(missing, ci=False)
ci = gate.row6(missing, ci=True)
assert (local.status, local.fails) == (gate.RED, True)
assert (ci.status, ci.fails) == (gate.SKIPPED, False)
assert "source missing" in ci.reason
def test_the_proposed_exceptions_are_not_applied() -> None:
assert gate.APPROVED_EXCEPTIONS == frozenset()
assert "NOT APPROVED" in gate.render([])
def test_bad_usage_exits_two() -> None:
with pytest.raises(SystemExit) as exc:
gate.main(["--no-such-flag"])
assert exc.value.code == 2
# --- 3. the real build at this commit ----------------------------------------
@pytest.fixture(scope="module")
def real_rows() -> list[gate.Row]:
pytest.importorskip("pdfplumber")
pytest.importorskip("pypandoc")
if gate.door_available():
pytest.skip("the accounting door exists; these rows describe the build before it")
return gate.evaluate(r761=None, ci=True, consume=False)
def test_the_real_gate_is_red_on_rows_two_three_and_four(real_rows: list[gate.Row]) -> None:
assert [r.number for r in real_rows if r.fails] == [2, 3, 4]
assert [(r.number, r.status) for r in real_rows if not r.fails] == [
(1, gate.GREEN),
(5, gate.GREEN),
(6, gate.SKIPPED),
]
def test_the_real_gate_sees_the_graphics_carried_and_rejected(real_rows: list[gate.Row]) -> None:
row3 = real_rows[2]
assert "d = 2 double-booked" in row3.reason
doubled = [d for d in row3.details if "AND rejected" in d]
assert [d.split(":")[0] for d in doubled] == [
"file graphics/figur-84-1.png",
"file graphics/tabell-84-2.png",
]
def test_the_real_gate_sees_the_rejected_document_logged_as_empty(
real_rows: list[gate.Row],
) -> None:
assert "log says 0 carried of 0 found; the source declares 1" in "\n".join(real_rows[3].details)