test(accounting): row 6 sees a refusal, a second real corpus, and 34 of 34 mutants
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>
This commit is contained in:
parent
e5dc21ec2f
commit
ed8d9d709f
11 changed files with 1146 additions and 85 deletions
|
|
@ -614,10 +614,16 @@ def test_an_image_booked_carried_without_its_bytes_is_unverified(tmp_path: Path)
|
|||
|
||||
def test_a_negative_booking_is_never_clean(tmp_path: Path) -> None:
|
||||
declared = _all_carried()
|
||||
declared["documents"][0]["fates"]["heading"] = {"carried": 25, "rejected": {"x": -15}}
|
||||
declared["documents"][0]["fates"]["heading"] = {
|
||||
"carried": 25,
|
||||
"rejected": {"extractor_unknown": -15},
|
||||
}
|
||||
unit = gate.account(_inventory(), _build(accounting=declared), _corpus(tmp_path))[0]
|
||||
assert unit.invalid >= 1
|
||||
assert not unit.clean
|
||||
# 25 + (-15) = 10 booked against a source holding 2, so eight are booked
|
||||
# twice. Absorbing the sign would read 40 and report thirty-eight.
|
||||
assert unit.double == 8
|
||||
|
||||
|
||||
def test_a_document_declared_persisted_that_is_not_in_the_bundle_is_never_clean(
|
||||
|
|
@ -686,6 +692,29 @@ def test_an_asset_with_the_right_name_and_the_wrong_bytes_is_not_carried(tmp_pat
|
|||
assert gate.account(_inventory(), build, corpus)[1].unaccounted == 1
|
||||
|
||||
|
||||
def test_an_asset_under_a_reduced_name_still_proves_the_carry(tmp_path: Path) -> None:
|
||||
"""The build lowercases and folds the source's basename and sniffs the
|
||||
suffix from the bytes. Measured on R761, a judge checking the FULL name
|
||||
called 50 of 50 carried images missing -- the judge's defect, not the
|
||||
build's, so the content address is the check and the readable tail is not.
|
||||
"""
|
||||
corpus = _corpus(tmp_path)
|
||||
source = corpus / "graphics" / "x.png"
|
||||
digest = gate._sha256(source)
|
||||
reduced = {f"{digest[:12]}-25-0143-tabeller-r761-r762.jpeg": digest}
|
||||
build = _build(accounting=_declared(fate="carried"), assets=reduced)
|
||||
assert gate.account(_inventory(), build, corpus)[1].clean
|
||||
|
||||
|
||||
def test_an_asset_holding_the_bytes_under_a_foreign_address_is_not_a_carry(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
corpus = _corpus(tmp_path)
|
||||
source = corpus / "graphics" / "x.png"
|
||||
build = _build(accounting=_declared(fate="carried"), assets={"x.png": gate._sha256(source)})
|
||||
assert gate.account(_inventory(), build, corpus)[1].unaccounted == 1
|
||||
|
||||
|
||||
def test_the_cheat_that_books_everything_carried_makes_row3_red(tmp_path: Path) -> None:
|
||||
"""The review's `MODE=carried`: a report that changes not one byte of the
|
||||
bundle and books every element as carried."""
|
||||
|
|
@ -747,13 +776,146 @@ def test_row5_is_red_when_a_witness_is_missing() -> None:
|
|||
|
||||
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)
|
||||
local = gate.row6(missing, None, ci=False)
|
||||
ci = gate.row6(missing, None, 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_a_skipped_row_never_leaves_the_verdict_unqualified() -> None:
|
||||
"""m-2: `CI=1` with a missing source printed `GATE GREEN` with nothing
|
||||
beside it, so the one line most readers stop at said the corpus passed."""
|
||||
skipped = gate.Row(6, "real corpora", 0, 0, gate.SKIPPED, "not measured, source missing: x")
|
||||
rendered = gate.render([skipped])
|
||||
assert "GATE GREEN (row 6 not run: not measured, source missing: x)" in rendered
|
||||
|
||||
|
||||
def test_a_corpus_refused_whole_under_the_default_gate_is_red(tmp_path: Path) -> None:
|
||||
"""Row 6 was GREEN with R761 100 % rejected: every element booked as a
|
||||
coded rejection satisfies u = 0 and d = 0. The build order asked for an
|
||||
honest red there, so the row says this on its own."""
|
||||
inventory = {
|
||||
"documents": {
|
||||
"a.md": {
|
||||
"suffix": ".md",
|
||||
"elements": {"heading": 2},
|
||||
"texts": {"heading": [["A"], ["B"]]},
|
||||
"images": [],
|
||||
}
|
||||
},
|
||||
"files": {},
|
||||
}
|
||||
declared = {
|
||||
"accounting_version": 1,
|
||||
"documents": [
|
||||
{
|
||||
"source_file": "a.md",
|
||||
"status": "rejected",
|
||||
"code": "fail_secure",
|
||||
"inventory": {"heading": 2},
|
||||
"fates": {"heading": {"rejected": {"fail_secure": 2}}},
|
||||
}
|
||||
],
|
||||
"files": [],
|
||||
}
|
||||
build = _build(accounting=declared, sources=set(), exit_code=1)
|
||||
units = gate.account(inventory, build, tmp_path)
|
||||
assert all(u.clean for u in units), "every element has a coded fate"
|
||||
assert gate.refused_whole(inventory["documents"], build) is not None
|
||||
|
||||
|
||||
def test_the_two_real_corpora_are_named_and_the_second_is_not_r761() -> None:
|
||||
"""R761 holds 0 `fig`, 0 formulas and 0 references, so the gate's only
|
||||
real corpus could not see the hole in the STS role map."""
|
||||
corpora = gate.real_corpora(Path("/r761"), Path("/n200.json"))
|
||||
assert [c.label.split()[0] for c in corpora] == ["R761", "N200"]
|
||||
|
||||
|
||||
def test_a_unit_clean_in_only_one_of_the_two_builds_is_not_clean() -> None:
|
||||
"""M13: the two gates see different things, so either build could cover
|
||||
for the other."""
|
||||
clean = gate.Unit("a", "document", 0, 0)
|
||||
dirty = gate.Unit("a", "document", 1, 0)
|
||||
assert gate.clean_in_every_run([[clean], [clean]]) == 1
|
||||
assert gate.clean_in_every_run([[clean], [dirty]]) == 0
|
||||
|
||||
|
||||
def test_the_gate_exits_one_when_a_row_is_red(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""M14: nothing checked the real command's exit code, so `main` could
|
||||
return 0 over a red table and no test would notice."""
|
||||
pytest.importorskip("pdfplumber")
|
||||
pytest.importorskip("pypandoc")
|
||||
monkeypatch.delenv("CI", raising=False)
|
||||
code = gate.main(["--r761", "/no/such/corpus", "--n200", "/no/such/file.json"])
|
||||
assert code == 1
|
||||
|
||||
|
||||
def test_a_file_with_no_declaration_is_unaccounted_when_conservation_failed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""M17: without the door, a file that is not merged counts as a coded
|
||||
rejection ONLY because the build's own conservation identity held. A run
|
||||
that exited non-zero has not earned that inference."""
|
||||
corpus = _corpus(tmp_path)
|
||||
failed = _build(exit_code=1, log="K1b FAILED")
|
||||
assert gate.account(_inventory(), failed, corpus)[1].unaccounted == 1
|
||||
assert gate.account(_inventory(), _build(), corpus)[1].clean
|
||||
|
||||
|
||||
def test_a_merged_file_declared_carried_without_its_bytes_is_still_a_false_claim(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""M05: with `fates` summing to one anyway, the false claim is the only
|
||||
thing that can see it."""
|
||||
corpus = _corpus(tmp_path)
|
||||
build = _build(accounting=_declared(fate="carried"), sources={"a.md", "graphics/x.png"})
|
||||
unit = gate.account(_inventory(), build, corpus)[1]
|
||||
assert (unit.unaccounted, unit.clean) == (1, False)
|
||||
|
||||
|
||||
def test_a_rejection_code_inside_an_element_fate_must_also_be_known(tmp_path: Path) -> None:
|
||||
"""M24: the document's own `code` was checked and the per-element ones
|
||||
were not, so a report could name any reason it liked for an element."""
|
||||
declared = _all_carried()
|
||||
declared["documents"][0]["fates"]["heading"] = {"rejected": {"because_i_said_so": 2}}
|
||||
unit = gate.account(_inventory(), _build(accounting=declared), _corpus(tmp_path))[0]
|
||||
assert unit.invalid >= 1
|
||||
assert "because_i_said_so" in "; ".join(unit.notes)
|
||||
|
||||
|
||||
def test_the_json_twin_is_read_with_the_json_role_map(tmp_path: Path) -> None:
|
||||
"""M-2, measured on R761 2026-09-18: the XML delivery places a section's
|
||||
label at `sec/label` (7 714, and 0 inside a title); the JSON delivery puts
|
||||
2 760 of them inside the title. Read with the XML map, the twin loses
|
||||
every one of those."""
|
||||
twin = gate.FIXTURES / "witness" / "sts-label-in-title.twin.json"
|
||||
assert witness.count_sts_json(twin.read_bytes()).counts["section_label"] == 1
|
||||
assert witness._sts_role_xml("label", "title", "sec") is None
|
||||
assert witness._sts_role_json("label", "title", "sec") == "section_label"
|
||||
|
||||
|
||||
def test_an_approved_exception_is_read_and_says_what_it_does() -> None:
|
||||
"""m-3: `APPROVED_EXCEPTIONS` was read by no row, so approving one changed
|
||||
nothing and the list could have said anything."""
|
||||
for suffix, element in gate.APPROVED_EXCEPTIONS:
|
||||
assert "no denominator moves" in gate.exception_effect(suffix, element)
|
||||
assert "WARNING" in gate.exception_effect(".pdf", "page")
|
||||
assert "names nothing" in gate.exception_effect(".doc", "heading")
|
||||
rendered = gate.render([])
|
||||
assert "no denominator moves" in rendered
|
||||
|
||||
|
||||
def test_the_gate_states_its_own_limits() -> None:
|
||||
rendered = gate.render([])
|
||||
assert "what this gate cannot check" in rendered
|
||||
assert len(gate.LIMITS) >= 5
|
||||
|
||||
|
||||
def test_every_witnessed_type_has_a_vocabulary() -> None:
|
||||
assert set(gate.FORMAT_VOCABULARY) == set(witness.WITNESSED_SUFFIXES)
|
||||
|
||||
|
||||
def test_the_proposed_exceptions_are_not_applied() -> None:
|
||||
assert "NOT APPROVED" in gate.render([])
|
||||
assert not {suffix for suffix, _ in gate.APPROVED_EXCEPTIONS} & {
|
||||
|
|
@ -789,7 +951,7 @@ def test_bad_usage_exits_two() -> None:
|
|||
def real_rows() -> list[gate.Row]:
|
||||
pytest.importorskip("pdfplumber")
|
||||
pytest.importorskip("pypandoc")
|
||||
return gate.evaluate(r761=None, ci=True, consume=False)
|
||||
return gate.evaluate(r761=None, n200=None, ci=True, consume=False)
|
||||
|
||||
|
||||
def _cheating_report(inventory: dict[str, Any], mode: str) -> dict[str, Any]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue