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

@ -137,9 +137,16 @@ BUDGET_IN_B = (
RUNBOOK_RULE = (
"rad 6 regner gaten aldri ut: en kjørebok kan være komplett på papiret og likevel ikke ha "
"drevet én analyse. Skrittet til grønt er operatørens egen bekreftelse på at han kjørte den "
"og at den virket — en fil gaten ALDRI skriver selv, like lite som attesteringen på v1-gaten"
"og at den virket — en fil gaten ALDRI skriver selv, like lite som attesteringen på v1-gaten. "
"Det gaten kan se er FORMEN: hver seksjon kontrakten navngir må stå som en overskrift MED "
"tekst under seg. En overskrift alene er en innholdsfortegnelse, ikke en kjørebok noen kan "
"følge — målt 19.09: de fem overskriftene pluss «x» ga 2 av 2 GRØNN. Lengdekravet er et gulv "
"mot den tomme overskriften, aldri et mål på om kjøreboka er sann"
)
#: Hvor mange tegn (blanktegn ikke talt) som må stå under en overskrift før seksjonen er skrevet.
RUNBOOK_MIN_BODY = 80
CONTRACT = """\
Radene, og hva som flytter hver av dem:
@ -1406,6 +1413,26 @@ def read_runbook_attestation(
return RunbookAttestation(True, True, "")
def section_bodies(text: str, sections: Sequence[str]) -> dict[str, str]:
"""``seksjon -> tegnene under overskriften``, blanktegn ikke talt, fram til neste overskrift.
Målt 19.09 (resten av N3): kravet var at seksjonsnavnet fantes I TEKSTEN. En kjørebok som var
de fem navnene som overskrifter pluss «x» ga `2 av 2 GRØNN`. Navnet bindes derfor til en
OVERSKRIFTSLINJE, og det som teller er hva som står under den."""
found: dict[str, str] = {}
current: str | None = None
for line in text.splitlines():
if line.lstrip().startswith("#"):
heading = line.lstrip("# ").strip()
current = next((s for s in sections if s in heading), None)
if current is not None:
found.setdefault(current, "")
continue
if current is not None:
found[current] += "".join(line.split())
return found
def score_runbook(
config: Mapping[str, Any],
repo_root: Path,
@ -1421,8 +1448,10 @@ def score_runbook(
#: attestering, ga «2 av 2 GRØNN». Tom fil var alt som var avvist. Kontrakten navngir derfor
#: seksjonene en kjørebok må bære — det gjør den ikke sann, men det gjør «x» umulig.
sections = [str(s) for s in config.get("sections", ())]
missing_sections = [s for s in sections if s not in text]
has_runbook = bool(text.strip()) and not missing_sections
bodies = section_bodies(text, sections)
missing_sections = [s for s in sections if s not in bodies]
thin_sections = [s for s in sections if s in bodies and len(bodies[s]) < RUNBOOK_MIN_BODY]
has_runbook = bool(text.strip()) and not missing_sections and not thin_sections
attest_path = attest if attest is not None else repo_root / str(config["attestation"])
attestation = read_runbook_attestation(
attest_path, list(config["keys"]), runbook, str(config["path"]), now
@ -1437,6 +1466,11 @@ def score_runbook(
f"kjørebok: {config['path']} mangler seksjonene kontrakten navngir "
f"({', '.join(missing_sections)})"
)
elif thin_sections:
exceptions.append(
f"kjørebok: {config['path']} har seksjoner det står ingenting under "
f"({', '.join(thin_sections)}) — en overskrift er ikke en seksjon"
)
if not attestation.ok:
exceptions.append(f"attestering: {attestation.why}")
return Row(