feat(propose): Arm D reads a document's own numbered outline

This commit is contained in:
Kjell Tore Guttormsen 2026-09-07 01:23:29 +02:00
commit 47ae1ed7e3
2 changed files with 277 additions and 0 deletions

View file

@ -575,3 +575,191 @@ def test_the_default_artifact_matches_its_committed_golden(tmp_path: Path) -> No
assert out.read_bytes() == golden.read_bytes(), (
"the default artifact diverges from its committed golden bytes"
)
# --- Arm D: the outline rule ----------------------------------------------
#
# Arm D is NOT defined in `docs/2026-09-02-k3-k4-k5-metode.md` -- that file
# contains no occurrence of the word -- so the definition these tests pin was
# written for the brief of order 20260906T213322Z and is reported as the
# author's, not as a ratified one: Arm B's mechanical rules, plus ONE rule that
# reads the document's OWN numbered outline -- the bare integers `N`, `N.`,
# `N)` that the shipping grammar cannot match because `_NUMBERED` requires a
# dot -- admitted only where the numbers form a maximal ascending run of at
# least a declared length, taking the LAST such run when the outline repeats,
# because a contents listing precedes the body it lists.
#
# EVERY fixture below uses BARE lines with no `#` prefix, and that is load
# bearing rather than stylistic. Measured on today's code: `# 1 Innledning`
# already yields 2 candidates via `_ATX`, and `1.1 Brannkonsept` already yields
# 2 via `_NUMBERED` -- so a fixture using either form would be green before the
# rule exists and would prove nothing. Bare `1` / `1.` / `1)` yield 0.
RUN_OF_THREE_THEN_RUN_OF_TWO = """Forord uten nummerering.
1 Innledning
Bakgrunn for prosjektet og omfanget.
2 Krav
Krav til seksjonering av bygget.
3 Gjennomfoering
Framdrift, faser og overlevering.
1 Vedlegg A
Foerste vedlegg til dokumentet.
2 Vedlegg B
Andre vedlegg til dokumentet.
"""
TWO_MAXIMAL_RUNS = """1 Innledning
Foerste forekomst, i innholdslista.
2 Krav
Andre forekomst, i innholdslista.
3 Gjennomfoering
Tredje forekomst, i innholdslista.
1 Innledning i kroppen
Her begynner selve teksten.
2 Krav i kroppen
Kravene slik de er skrevet ut.
3 Gjennomfoering i kroppen
Gjennomfoeringen slik den er skrevet ut.
"""
ALL_THREE_INTEGER_FORMS = """1 Innledning
Bakgrunn for prosjektet.
2. Krav
Krav til seksjonering.
3) Gjennomfoering
Framdrift og faser.
"""
NUMBER_WITHOUT_A_TITLE = """1 Innledning
Bakgrunn for prosjektet.
2
3 Gjennomfoering
Framdrift og faser.
"""
RUN_NOT_STARTING_AT_ONE = """2 Krav
Krav til seksjonering.
3 Gjennomfoering
Framdrift og faser.
4 Overlevering
Overlevering av bygget.
"""
def outline_titles(runs: list[list[tuple[int, int, str]]]) -> list[list[str]]:
return [[title for _, _, title in run] for run in runs]
def test_a_run_of_three_fires_and_a_run_of_two_in_the_same_document_does_not() -> None:
"""The paired form, and the pairing is the point.
A lone "a 2-run must not fire" assertion is true by absence before the rule
exists and true forever after, so it can never go red. Asserting both in ONE
document makes the negative half depend on the rule actually running.
"""
entries = okf_propose_segments.outline_lines(RUN_OF_THREE_THEN_RUN_OF_TWO)
runs = okf_propose_segments.outline_runs(entries, 3)
assert outline_titles(runs) == [["Innledning", "Krav", "Gjennomfoering"]]
# The known-positive control for the same fixture: at a minimum of 2 the
# second run IS admitted, so its absence above is the gate, not the text.
relaxed = okf_propose_segments.outline_runs(entries, 2)
assert outline_titles(relaxed) == [
["Innledning", "Krav", "Gjennomfoering"],
["Vedlegg A", "Vedlegg B"],
]
def test_runs_restart_at_every_new_one_and_the_last_maximal_run_is_the_last() -> None:
"""Two identical runs; the LAST is the body, the first is the contents."""
entries = okf_propose_segments.outline_lines(TWO_MAXIMAL_RUNS)
runs = okf_propose_segments.outline_runs(entries, 3)
assert len(runs) == 2
assert outline_titles(runs)[-1] == [
"Innledning i kroppen",
"Krav i kroppen",
"Gjennomfoering i kroppen",
]
# And the last run sits later in the document than the first.
assert runs[-1][0][0] > runs[0][-1][0]
def test_all_three_integer_forms_are_admitted() -> None:
"""`N`, `N.` and `N)` are the same declaration in three typographies."""
entries = okf_propose_segments.outline_lines(ALL_THREE_INTEGER_FORMS)
runs = okf_propose_segments.outline_runs(entries, 3)
assert outline_titles(runs) == [["Innledning", "Krav", "Gjennomfoering"]]
def test_a_number_with_no_title_is_not_a_candidate() -> None:
"""A bare `2` on its own line names no unit of knowledge.
It also breaks the run, which is the honest outcome: the document did not
declare a chapter there, so the rule may not invent one.
"""
entries = okf_propose_segments.outline_lines(NUMBER_WITHOUT_A_TITLE)
assert [number for _, number, _ in entries] == [1, 3]
assert okf_propose_segments.outline_runs(entries, 3) == []
def test_a_sequence_not_starting_at_one_is_not_a_run() -> None:
"""A run is anchored at `1`. `2, 3, 4` is page furniture until proven otherwise."""
entries = okf_propose_segments.outline_lines(RUN_NOT_STARTING_AT_ONE)
assert [number for _, number, _ in entries] == [2, 3, 4]
assert okf_propose_segments.outline_runs(entries, 3) == []
def test_a_trailing_page_number_is_stripped_from_an_outline_title() -> None:
"""`Innledning 6` is a contents line; the page number is not part of the title.
Load-bearing rather than cosmetic: titles become concept paths through
`_segment_path`, so a page number left on would become part of a filename.
"""
assert okf_propose_segments._strip_page_number("Innledning 6") == "Innledning"
assert okf_propose_segments._strip_page_number("Vurdering av restrisiko 25") == (
"Vurdering av restrisiko"
)
def test_a_title_that_is_only_digits_is_left_alone_rather_than_emptied() -> None:
"""The negative control for the stripper: it must not eat the whole title.
Emptying it here would hide junk from the stop-word/junk path that is
supposed to see it, and an empty stem falls back to `seksjon`, which would
make junk look like a named section.
"""
assert okf_propose_segments._strip_page_number("477") == "477"