fix(b-gate): a heading is not a section -- the rest of N3, and one rewrapped ledger line [skip-docs]

The measured escape: a runbook that was the five contract-named headings plus "x" (125 characters
in all), with a correctly checksummed attestation, read `2 av 2 GRØNN`. Only a file whose whole
content was "x" had been refused -- the rule asked whether the section NAME appeared in the text,
never whether anything stood under it. A table of contents is not a runbook anyone can follow.

Section names are now bound to a HEADING line, and what counts is the body beneath it
(`section_bodies`, 80 non-whitespace characters as a floor). The row says so itself, and says what
the floor is not: a length is never a measure of whether the runbook is true. That stays the
operator's, which is why row 6 is still `IKKE MÅLT`.

Two rc-0 controls in the suite carried section bodies of "noe" and "steg 1: naviger pakken" -- both
would now fail on the new rule rather than on the rule they were written for, so both got a real
body. A control that falls on the wrong rule has stopped controlling.

Also: the one very long line in the ledger (the arm name mid-paragraph) rewrapped. Cosmetic, named
in the 2026-09-20 checkpoint's leftovers.

NOT taken from that same list: freezing the outbox shape so the round-builder's denominator arm
stops skipping outside a tree with `scratchpad/`. It needs a checked-in fixture of the measured
form, which is not the "only if cheap" the order allowed, and the checkpoint proposed no round for
it either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-20 09:05:08 +02:00
commit f430b790a3
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 79 additions and 10 deletions

View file

@ -996,11 +996,18 @@ def test_row6_denominator_comes_from_the_artefacts_the_contract_names(tmp_path:
# --- rad 6: attesteringen kan felles ----------------------------------------------------------
#: Én seksjons kropp i rc-0-kontrollen. Lengden er ikke pynt: kontrakten krever at det STÅR noe
#: under hver overskrift, og en fixtur med to ord ville gjort kontrollen rød sammen med juksen.
_SECTION_BODY = (
"steg 1: naviger pakken med toolbox-doeren og les filene den rapporterer.\n"
"steg 2: hent kostnadsgrunnlaget fra det prisede skjemaet i samme base.\n"
)
def _runbook(root: Path) -> str:
"""En kjørebok som bærer seksjonene kontrakten navngir — ellers er «x» en kjørebok (N3)."""
sections = "\n".join(
f"## {s}\n\nsteg 1: naviger pakken\n" for s in _CONFIG["runbook"]["sections"]
)
"""En kjørebok som bærer seksjonene kontrakten navngir, med tekst under hver — ellers er «x»
en kjørebok (N3), og de fem overskriftene alene er det også (resten av N3, målt 19.09)."""
sections = "\n".join(f"## {s}\n\n{_SECTION_BODY}" for s in _CONFIG["runbook"]["sections"])
text = f"# kjørebok\n\n{sections}"
_write(root / _CONFIG["runbook"]["path"], text)
return hashlib.sha256(text.encode("utf-8")).hexdigest()
@ -1463,7 +1470,10 @@ def test_a_runbook_without_the_sections_the_contract_names_is_not_green(tmp_path
cfg = _CONFIG["runbook"]
sections = list(cfg.get("sections", ()))
assert sections, "kontrakten navngir ingen seksjoner — da er «x» en kjørebok"
text = "# kjørebok\n\n" + "\n".join(f"## {s}\n\nnoe\n" for s in sections)
# Kroppen er _SECTION_BODY og ikke «noe»: fra 20.09 er en overskrift uten tekst under seg
# ikke en seksjon (se test_a_runbook_of_headings_alone_is_not_a_runbook), og en rc-0-kontroll
# som faller på DEN regelen måler ikke lenger den den er skrevet for.
text = "# kjørebok\n\n" + "\n".join(f"## {s}\n\n{_SECTION_BODY}" for s in sections)
_write(tmp_path / cfg["path"], text)
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
_attestation_lines(tmp_path, digest)
@ -1951,3 +1961,27 @@ def test_a_door_call_that_can_never_run_is_not_an_execution(
row = gate.score_toolbox(_one_step(), src, _fake_run_path(), _FAKE_PASSED, tmp_path)
assert (row.k, row.n) == (0, 1), (form, row.exceptions)
assert any("rører ikke døren" in x for x in row.exceptions), (form, row.exceptions)
def test_a_runbook_of_headings_alone_is_not_a_runbook(tmp_path: Path) -> None:
"""Resten av N3: «x» var avvist, men de fem overskriftene PLUSS «x» (125 tegn i alt) ga
`2 av 2 GRØNN`. Kontrakten krevde at navnet STO der, ikke at det sto noe under det og en
innholdsfortegnelse er ikke en kjørebok noen kan følge."""
digest = _runbook(tmp_path)
_attestation_lines(tmp_path, digest)
kontroll = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
assert (kontroll.k, kontroll.n, kontroll.status) == (2, 2, gate.GREEN), kontroll.exceptions
tynn = "\n".join(f"## {s}" for s in _CONFIG["runbook"]["sections"]) + "\nx\n"
assert len(tynn) < 200, "armen skal måle den formen sjekkpunktet målte"
_write(tmp_path / _CONFIG["runbook"]["path"], tynn)
_attestation_lines(tmp_path, hashlib.sha256(tynn.encode("utf-8")).hexdigest())
row = gate.score_runbook(_CONFIG["runbook"], tmp_path, now=_NOW)
assert row.status != gate.GREEN
assert row.k == 1, row.exceptions
assert any("står ingenting under" in x for x in row.exceptions), row.exceptions
def test_the_runbook_rule_says_that_a_heading_alone_is_not_a_section() -> None:
assert "overskrift" in gate.RUNBOOK_RULE
assert gate.RUNBOOK_MIN_BODY > 0