llm-ingestion-okf/tests/test_cli_gate.py
Kjell Tore Guttormsen 864570b320 feat(accounting): okf build accounts for every source element
okf build --accounting PATH inventories every source document before
extraction, in the gate's per-format vocabulary, and after the run gives
each element one fate (carried / pointer / coded rejection), written as
JSON and summarised in log.md. "carried" is checked against the written
concept bodies, so a gate that drops a line is found (test). Exit 1 on
anything unaccounted or double-booked. Opt-in: +744 s (+19 %) on the
43-document reference corpus, and that corpus fails the check on 24 real
losses (22 images on text-less PDF pages, 2 docx Title paragraphs).

Changed without the flag:
- okf build exits 1 when it extracted documents and persisted none.
  Door B and corpus.measure are unchanged. One test relied on exit 0.
- An image file carried through a persisted document is its own K1b
  column, no longer also extractor_unknown. The set is what the resolver
  actually carried (ExtractedDocument.files), never a byte match.

tools/okf_accounting_gate.py (checks untouched) is green on all six rows,
R761 110 of 110 under both gates.

Report: docs/2026-09-17-innholdsregnskapet-bygget.md

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

331 lines
13 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""`okf build` runs a NAMED gate, and the bundle says which one (F1).
Reported from outside on 2026-09-15 by `claude-code-llm-wiki`, verified here
before a line moved: `corpus.measure` wired an unconditional approve-everything
stub (`corpus._gate`, returning the persist disposition for any text) into
`process_inbox`, and no `add_argument` call anywhere in the package named a
gate. So the only path people actually use -- `okf build`, and `okf project`
through it -- screened nothing, while `pyproject.toml` makes the guard a
MANDATORY runtime dependency and the README recommends exactly the composition
that was never reachable. A permissive stub that approves everything is the
shape this repository's own README names as the danger; it was on our own door.
Three separate things are pinned, because they fail in three different ways:
- **the gate runs.** A document the real guard refuses is not persisted. Under
the stub it was, which is the defect itself; `CARRIER` is measured to fail
secure in BOTH presets (an invisible carrier is an any-tier finding), so this
test cannot pass by picking a lenient tier.
- **the gate is named in the artifact.** SS 9's `log.md` already carries `N`
because it is the one fact about a run the bundle cannot otherwise recover.
Which gate screened the bytes is the same class of fact: without it a bundle
built with `--gate none` is indistinguishable from one the guard cleared, and
that indistinguishability is the whole trust trap, not the stub alone.
- **the seam composes with segmentation.** Measured before the fix:
`grep -rl inbox_gate tests/` gave ONE file (`tests/test_guard_adapter.py`)
and `grep -c segment` in it gave 0, while the nine files passing
`segmentation=`/`segmentations=` all injected a local warn-stub. The
composition the README recommends -- `process_inbox(segmentations=...,
gate=inbox_gate)` -- had no test at all, which is how the defect survived.
The default is `guard-trusted-source`, set by the operator 2026-09-15 on a
measurement rather than on caution: over the 453 concept bodies of the pinned
default bundle, `PRESET_TRUSTED_SOURCE` returns the persist disposition on
453 of 453 (the artifact survives the move), while `PRESET_USER_UPLOAD` holds
1 of them and would take one of the 39 source documents out of the bundle.
Pointing `okf build` at your own folder is not an untrusted upload; Door B's
library default stays `PRESET_USER_UPLOAD` because an inbox drop is.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from llm_ingestion_okf import cli, corpus
from llm_ingestion_okf.inbox import process_inbox
from llm_ingestion_okf.profiles import SEGMENTED_OKF_V0_2
from llm_ingestion_okf.propose import build_plan
from llm_ingestion_okf.segmentation import parse_segmentation_plan
BUNDLE_ID = "cli-gate-fixture"
OKF_VERSION = "0.2"
INGESTED_AT = "2026-09-15T00:00:00Z"
# Measured against the pinned guard 1.4.0, both presets, before this suite was
# written: `fail_secure` under `PRESET_TRUSTED_SOURCE` AND under
# `PRESET_USER_UPLOAD` ("any-tier: invisible carrier"). A fixture only one tier
# refuses would make this suite pass for the wrong reason.
CARRIER = "# Kostnader\n\nEn merknad med et nullbreddetegn i seg.\n"
BENIGN = "# Kostnader\n\nKvartalstall for plattformgruppen, uten funn.\n"
def _inbox(root: Path, documents: dict[str, str]) -> Path:
inbox = root / "inbox"
inbox.mkdir(parents=True, exist_ok=True)
for name, body in documents.items():
(inbox / name).write_text(body, encoding="utf-8", newline="")
return inbox
def _build(inbox: Path, bundle: Path, *extra: str) -> int:
return cli.main(
[
"build",
str(inbox),
"--bundle",
str(bundle),
"--bundle-id",
BUNDLE_ID,
"--okf-version",
OKF_VERSION,
*extra,
]
)
def _concepts(bundle: Path) -> list[Path]:
return [
path
for path in sorted(bundle.rglob("*.md"))
if path.name not in {"index.md", corpus.LOG_NAME}
]
# --- the gate runs ---------------------------------------------------------
def test_build_refuses_a_document_the_real_guard_refuses(tmp_path: Path) -> None:
"""The defect itself, as a red test: the stub persisted this document.
`CARRIER` carries a zero-width space inside a word. The guard refuses it in
any trust tier, so a build that persists it has not consulted the guard --
whatever the dependency table says.
"""
pytest.importorskip("llm_ingestion_guard")
inbox = _inbox(tmp_path, {"carrier.md": CARRIER})
bundle = tmp_path / "bundle"
# 1, not 0, since the content accounting: a build that extracted a document
# and persisted none is not a success, however every refusal was coded.
assert _build(inbox, bundle) == 1
assert _concepts(bundle) == [], "a document the guard refuses must not reach the bundle"
def test_build_persists_a_document_the_real_guard_clears(tmp_path: Path) -> None:
"""The known-positive beside it: the gate must not simply refuse everything.
A gate that blocked every document would pass the test above and destroy
the command. Both halves are needed, on the same default, in the same run.
"""
pytest.importorskip("llm_ingestion_guard")
inbox = _inbox(tmp_path, {"benign.md": BENIGN})
bundle = tmp_path / "bundle"
assert _build(inbox, bundle) == 0
assert _concepts(bundle), "a clean document must still reach the bundle"
def test_gate_none_is_reachable_and_still_permissive(tmp_path: Path) -> None:
"""`--gate none` keeps the old behaviour, deliberately and by name.
The stub is not deleted -- the corpus harness reproduces published numbers
with it, and a caller measuring segmentation alone has a legitimate reason
to take the gate out of the picture. What changes is that asking for it is
now an act, and the bundle records it.
"""
inbox = _inbox(tmp_path, {"carrier.md": CARRIER})
bundle = tmp_path / "bundle"
assert _build(inbox, bundle, "--gate", "none") == 0
assert _concepts(bundle), "--gate none is the documented permissive path"
def test_user_upload_preset_is_reachable(tmp_path: Path) -> None:
"""The stricter tier is a flag value, not a fork of the adapter.
Door B's own default stays `PRESET_USER_UPLOAD`; the CLI exposes it so a
caller whose folder IS an untrusted drop does not have to write their own
entry point to reach the tier this library already ships.
"""
pytest.importorskip("llm_ingestion_guard")
inbox = _inbox(tmp_path, {"benign.md": BENIGN})
bundle = tmp_path / "bundle"
assert _build(inbox, bundle, "--gate", "guard-user-upload") == 0
assert _concepts(bundle)
def test_an_unknown_gate_name_is_refused(tmp_path: Path) -> None:
"""A misspelled tier must not fall back to the permissive one.
Falling back would reproduce the defect with an extra step: the caller
believes they asked for the guard, and the run approves everything.
"""
inbox = _inbox(tmp_path, {"benign.md": BENIGN})
bundle = tmp_path / "bundle"
with pytest.raises(SystemExit):
_build(inbox, bundle, "--gate", "guard-trusted")
assert not bundle.exists() or not _concepts(bundle)
# --- the gate is named in the artifact -------------------------------------
def test_the_log_names_the_gate_that_screened_the_bundle(tmp_path: Path) -> None:
"""SS 9's log carries the gate for the same reason it carries `N`.
A consumer holding a bundle cannot otherwise tell a screened one from an
unscreened one, and that is the trust trap in its pure form -- a stub is
only dangerous because nothing downstream can see it.
"""
pytest.importorskip("llm_ingestion_guard")
inbox = _inbox(tmp_path, {"benign.md": BENIGN})
bundle = tmp_path / "bundle"
assert _build(inbox, bundle) == 0
log = (bundle / corpus.LOG_NAME).read_text(encoding="utf-8")
assert corpus.GATE_GUARD_TRUSTED_SOURCE in log
def test_the_log_says_when_nothing_was_screened(tmp_path: Path) -> None:
"""`--gate none` must be legible as "nothing was screened", not as silence.
An omitted line reads as "no gate section in this log format"; a present
line naming the permissive stub reads as what it is.
"""
inbox = _inbox(tmp_path, {"benign.md": BENIGN})
bundle = tmp_path / "bundle"
assert _build(inbox, bundle, "--gate", "none") == 0
log = (bundle / corpus.LOG_NAME).read_text(encoding="utf-8")
assert corpus.GATE_NONE in log
assert "NOTHING WAS SCREENED" in log
def test_every_gate_name_renders_a_log_line() -> None:
"""No gate name may render a log without a gate line.
A name added later without a description would produce a bundle that
declares its gate as the empty string, which reads as an absent field.
"""
for name in corpus.GATE_NAMES:
report = corpus.CorpusReport(
corpus="corpus",
ingested_at=INGESTED_AT,
n=0,
extracted=0,
gated=0,
persisted=0,
substantive=0,
degenerate=0,
rejected=0,
seconds_total=0.0,
converter_path="none",
converter_version="none",
codes=(),
unaccounted=(),
gate=name,
)
assert f"**Gate**: {name}" in report.render_log()
# --- the seam composes with segmentation -----------------------------------
def test_segmentation_and_the_real_guard_compose(tmp_path: Path) -> None:
"""`process_inbox(segmentations=..., gate=inbox_gate)`, the README's own form.
Measured before this test existed: no test in the repository ran a real
guard verdict and a segmentation plan in the same call. The composition is
what the README recommends to consumers, so leaving it untested left the
recommendation unverified -- and it is the exact call `okf build` now makes.
"""
guard_adapter = pytest.importorskip("llm_ingestion_okf.guard_adapter")
document = (
"# 1 Innledning\n\nDette dokumentet beskriver krav til seksjonering.\n\n"
"# 2 Omfang\n\nOmfanget er hele anlegget og alle tilhoerende systemer.\n"
)
inbox = _inbox(tmp_path, {"alpha.md": document})
bundle = tmp_path / "bundle"
source = inbox / "alpha.md"
plans = {
"alpha.json": parse_segmentation_plan(
build_plan(
source,
document,
source.read_bytes(),
okf_type="reference",
proposed_at=INGESTED_AT,
path_prefix="alpha",
)
)
}
result = process_inbox(
inbox,
bundle,
INGESTED_AT,
okf_type="reference",
gate=guard_adapter.inbox_gate,
profile=SEGMENTED_OKF_V0_2,
root_frontmatter_values={"okf_version": OKF_VERSION, "bundle_id": BUNDLE_ID},
segmentations=plans,
)
assert len(result.persisted) == 1
assert not result.rejected and not result.quarantined
assert len(_concepts(bundle)) > 1, "the plan's segments must be what the guard screened"
def test_segmentation_and_the_real_guard_refuse_together(tmp_path: Path) -> None:
"""The known-negative for the composition above, on the same call shape.
One refused segment body refuses the whole file, which is Door B's stated
rule; without this half the test above would pass against a gate that never
said no.
"""
guard_adapter = pytest.importorskip("llm_ingestion_okf.guard_adapter")
document = (
"# 1 Innledning\n\nDette dokumentet beskriver krav til seksjonering.\n\n"
"# 2 Omfang\n\nEn merknad med et nullbreddetegn i seg.\n"
)
inbox = _inbox(tmp_path, {"alpha.md": document})
bundle = tmp_path / "bundle"
source = inbox / "alpha.md"
plans = {
"alpha.json": parse_segmentation_plan(
build_plan(
source,
document,
source.read_bytes(),
okf_type="reference",
proposed_at=INGESTED_AT,
path_prefix="alpha",
)
)
}
result = process_inbox(
inbox,
bundle,
INGESTED_AT,
okf_type="reference",
gate=guard_adapter.inbox_gate,
profile=SEGMENTED_OKF_V0_2,
root_frontmatter_values={"okf_version": OKF_VERSION, "bundle_id": BUNDLE_ID},
segmentations=plans,
)
assert not result.persisted
assert result.rejected, "a carrier in one segment refuses the file"
# --- the adapter's second tier ---------------------------------------------
def test_the_trusted_source_adapter_is_the_same_shape(tmp_path: Path) -> None:
"""The second tier is a second three-line adapter, not a parameter.
`guard_adapter`'s own docstring says a caller needing another tier writes
their own three-line adapter; this is that adapter, shipped, so the CLI
does not have to reach past the seam into the guard's presets.
"""
guard_adapter = pytest.importorskip("llm_ingestion_okf.guard_adapter")
clean = guard_adapter.inbox_gate_trusted_source(BENIGN)
assert clean.disposition == "warn"
assert clean.sanitized_text == BENIGN
held = guard_adapter.inbox_gate_trusted_source(CARRIER)
assert held.disposition != "warn", "an any-tier finding is refused at every tier"
assert held.reasons, "the guard's audit trail is carried across verbatim"