Measured 2026-09-20 on an official documentation corpus of 594 sources built with the shipped default gate `guard-trusted-source`: 17 sources were refused OUTRIGHT -- `fail_secure` 3, `quarantine_review` 14 -- and 16 of them were among 197 official documentation pages, the pages on hooks, skills, permissions, errors, env-vars and authentication among them. The summary said only `fail_secure`: 3/594. Three of the four facts a reader needs were missing: the COUNT of documents the gate dropped (the existing `rejected (coded)` line sums gate refusals and extraction failures, two failures with two different remedies), the NAMES, and the way out. Rebuilt with `--gate none`, all 17 went through untouched, so the refusal is the gate and not the readers. `okf build` now prints a `Documents the gate refused WHOLE` section directly under the denominator, carrying all four: the count with its denominator, the names capped at ten with the rest in the bundle's `log.md`, the codes, and `--gate none` for a source you vouch for yourself. The same fact goes to stderr in one line, built from the same field, because `okf build > report.txt` is an ordinary thing to do. `log.md` gains one bullet naming every refused document, uncapped. The exit code deliberately does not move. The build is valid -- every refusal is coded, the conservation identity holds, and the bundle is a true record of what the gate allowed. What was wrong was the silence. A run the gate refused nothing from is byte-identical in both places, which is the known-negative in the new suite: no bundle this repository ships was built with a gate refusal, so this cannot have moved a byte measured here. Also, and measuring nothing new: - README gains `Known limitations` high up -- the gate's refusals and the way out, the absent ceiling on what one run pays for images (a 70 KB PDF with 16 images under the declared limit reached 851 MB peak RSS; RLIMIT_AS is not enforceable on this platform, so the 512 MiB per-link budget is the whole bound), the three gates of this repository that are RED today (retrieval 5/7/8/9, MCP 2, accounting 2/3/6 -- all three re-run on this commit), what the content accounting does not count, and the rough edges nothing is planned for. - The two `pip install` lines under "Install in detail" install `[extract]`. The first screen does; those two did not, so the two recipes produced different installations and the detailed one reports `resolved converter path: unresolved (extractor_extra_missing)`. - Version `1.0.0`, synced across pyproject, `__version__`, `uv.lock`, the four README install lines, the install prose, the current-tag entry and the CHANGELOG, where the two "after the 0.10.1 notes were written, untagged" sections are folded in. It adds no capability over `v0.10.1`; what it adds is that the tool says what it does not do. Suite: 2325 passed, 2 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
196 lines
7.5 KiB
Python
196 lines
7.5 KiB
Python
"""A document the gate refuses WHOLE is named in the run's own summary.
|
||
|
||
Measured on 2026-09-20 against a real documentation corpus (594 source files,
|
||
197 official documentation pages) built with the shipped default gate: 17
|
||
sources -- 16 of the 197 pages -- were refused outright, `fail_secure` 3 and
|
||
`quarantine_review` 14, and the pages lost were hooks, skills, permissions,
|
||
errors, env-vars and authentication. The build exited 0 and said so like this:
|
||
|
||
- `fail_secure`: 3/594
|
||
- `quarantine_review`: 14/594
|
||
|
||
Three of the four facts a reader needs were absent. The code was there (c);
|
||
the COUNT of documents the gate refused whole was not (the `rejected (coded)`
|
||
line sums gate refusals and extraction failures into one number), the NAMES
|
||
were not (b), and the way out -- `--gate none` for a source you vouch for
|
||
yourself -- was not (d). So a bundle could lose the pages it exists for while
|
||
its summary read like a clean run.
|
||
|
||
The exit code deliberately does NOT move. The build is valid: every refusal is
|
||
coded, the conservation identity holds, and the bundle is a true record of what
|
||
the gate allowed. What was wrong was the silence, not the status.
|
||
|
||
The fixture is a two-document inbox, never the real corpus: `CARRIER` carries a
|
||
zero-width space inside a word and is measured `fail_secure` under BOTH guard
|
||
presets, and `BENIGN` clears both, so the run has a refusal AND a survivor and
|
||
cannot pass by refusing everything.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from llm_ingestion_okf import cli, corpus
|
||
|
||
BUNDLE_ID = "gate-loud-fixture"
|
||
OKF_VERSION = "0.2"
|
||
|
||
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 _section(out: str) -> str:
|
||
"""Exactly the refusal section, bounded at the next heading.
|
||
|
||
Taken to the end of the report, the slice would also hold `## Rejection
|
||
codes`, and an assertion about how many names the section lists would count
|
||
that section's totals too.
|
||
"""
|
||
after = out.split(corpus.REFUSED_HEADING, 1)[1]
|
||
end = after.find("\n## ")
|
||
return after if end < 0 else after[:end]
|
||
|
||
|
||
def _run(
|
||
tmp_path: Path, documents: dict[str, str], capsys: pytest.CaptureFixture[str]
|
||
) -> tuple[int, str, str]:
|
||
pytest.importorskip("llm_ingestion_guard")
|
||
inbox = _inbox(tmp_path, documents)
|
||
code = _build(inbox, tmp_path / "bundle")
|
||
captured = capsys.readouterr()
|
||
return code, captured.out, captured.err
|
||
|
||
|
||
def test_the_refused_documents_are_counted_with_their_denominator(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""(a) how many documents the gate refused whole -- its own number.
|
||
|
||
`rejected (coded): 1/2` already existed and is not this number: it also
|
||
counts a file the extractor could not read, which is a different fact about
|
||
a different failure and points at a different remedy.
|
||
"""
|
||
code, out, _ = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
|
||
assert code == 0, "a build whose gate refused one of two documents is still a valid build"
|
||
assert corpus.REFUSED_HEADING in out
|
||
assert "1 of 2" in out
|
||
|
||
|
||
def test_the_refused_documents_are_named(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""(b) the file names, and (c) the code beside each one."""
|
||
_, out, _ = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
|
||
section = _section(out)
|
||
assert "carrier.md" in section
|
||
assert "fail_secure" in section
|
||
assert "benign.md" not in section, "a document that reached the bundle is not a refusal"
|
||
|
||
|
||
def test_the_way_out_is_stated_in_the_summary(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""(d) the exact command, not a hint that one exists."""
|
||
_, out, _ = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
|
||
section = _section(out)
|
||
assert "--gate none" in section
|
||
assert "trust" in section.lower()
|
||
|
||
|
||
def test_a_run_the_gate_refused_nothing_from_says_nothing(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""The known-positive: the section must be earned, not unconditional.
|
||
|
||
Without this, every assertion above is satisfied by a constant string and
|
||
the test says nothing about whether a refusal was observed.
|
||
"""
|
||
code, out, err = _run(tmp_path, {"benign.md": BENIGN}, capsys)
|
||
assert code == 0
|
||
assert corpus.REFUSED_HEADING not in out
|
||
assert "--gate none" not in err
|
||
|
||
|
||
def test_the_refusal_also_reaches_stderr(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""Loud where a redirected stdout cannot hide it.
|
||
|
||
`okf build > report.txt` is an ordinary thing to do, and it puts the whole
|
||
summary in a file the reader opens later, if at all.
|
||
"""
|
||
_, _, err = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
|
||
banner = [line for line in err.splitlines() if "refused" in line]
|
||
assert banner, "stderr must carry the refusal"
|
||
assert "carrier.md" in banner[0]
|
||
assert "--gate none" in banner[0]
|
||
|
||
|
||
def test_a_long_list_is_capped_and_points_at_the_whole_one(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""A capped list is only honest if the rest is somewhere a reader can go.
|
||
|
||
The bundle's own `log.md` is that somewhere: SPEC section 9 already holds
|
||
this run's denominator and gate name, and a document the gate dropped is
|
||
the same class of fact -- the one thing about the run the bundle cannot
|
||
otherwise recover.
|
||
"""
|
||
# Distinct titles, not thirteen copies: two documents reducing to one
|
||
# concept name hit the section 3 collision refusal BEFORE the gate, which
|
||
# would make this row green over a set the gate never saw.
|
||
documents = {
|
||
f"carrier-{index:02d}.md": CARRIER.replace("Kostnader", f"Kostnader {index:02d}")
|
||
for index in range(corpus.REFUSED_NAME_CAP + 3)
|
||
}
|
||
documents["benign.md"] = BENIGN
|
||
_, out, _ = _run(tmp_path, documents, capsys)
|
||
section = _section(out)
|
||
assert section.count("`fail_secure`") == corpus.REFUSED_NAME_CAP + 1, (
|
||
"the per-name list is capped; the count line carries the code totals"
|
||
)
|
||
assert "3 more" in section
|
||
assert corpus.LOG_NAME in section
|
||
log = (tmp_path / "bundle" / corpus.LOG_NAME).read_text(encoding="utf-8")
|
||
for name in sorted(documents):
|
||
if name != "benign.md":
|
||
assert name in log, "log.md names every refused document, uncapped"
|
||
|
||
|
||
def test_the_log_is_unchanged_when_the_gate_refused_nothing(
|
||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||
) -> None:
|
||
"""Exposure, measured rather than argued: a clean run's bytes do not move.
|
||
|
||
Every bundle this repository ships was built without a gate refusal, so a
|
||
bullet written only when there is one cannot have moved a byte of them.
|
||
"""
|
||
_run(tmp_path, {"benign.md": BENIGN}, capsys)
|
||
log = (tmp_path / "bundle" / corpus.LOG_NAME).read_text(encoding="utf-8")
|
||
assert "refused" not in log.lower()
|