llm-ingestion-okf/tests/test_propose_segments.py
Kjell Tore Guttormsen 56c1205ec4 fix(consume,propose): hold an identifier number as one token, give an orphaned heading's name to its table
Two consumer-reported defects, one rebuild.

The pre-pass could not see a requirement number: `_TOKEN_SPLIT_RE` split
`10.2-2` into digit runs and `MIN_TOKEN_LENGTH` removed them, so a question
naming a requirement reached the ranker carrying only the word every concept
in a standards bundle carries. Measured on three real bundles (446, 1133 and
270 concepts), the named requirement was withheld `below_k` in three of three.
Numeric groups joined by `.` or `-` are now held together, dash variants fold
to the ASCII hyphen, and the noise floor is unchanged. The gold moves from
160 to 96, 143 to 9 and 100 to 35 -- a large move, and NOT a delivery: it is
still `below_k`, because `_overlap` is a count and an exact requirement number
is worth no more than a common verb. That weighting is a separate decision.

The rule was narrowed by a measurement: a version that joined alphanumeric
groups swallowed a document slug whole and cost a hit@8 row. An equality-only
variant was measured on all three bundles and falsified -- better on one,
worse on two.

The orphan gate destroyed a heading's name: a table opening directly below a
heading left that heading with an empty body, the orphan check dropped it, and
the surviving table block kept the mechanical `Tabell linje <n>`. A table that
orphans its heading now takes that heading's title and section number.
Conditioned on the drop, on adjacency, and carrying both members -- each of
the three measured or mutation-tested.

One K2 rebuild for both, from a frozen source tree: 629 concepts, `39 + 4 = 43
= N`, 2 of 629 ids moved and both moved BACK to the names the 2026-09-03
bundle carried, 1106 of 1108 files identical to it. New ref
sha256-tree:2f82fcfea91c3bd3f8ef7147f80cd613227d3ca7975c41d88810233f3f79ab4b
-- c26eed6a... is superseded. The regression the previous session measured is
closed: candidate rank 19 -> 10, and the delivering command is now
`--cost-vocabulary --k 12` inside the default budget at 58 907 o200k against
65 912 before. The specific question is unmoved at rank 1.

The tokeniser alone leaves the K2 control question byte-identical, measured
with the bundle held fixed and both published byte counts reproduced.

Report: docs/2026-09-08-kravnummer-tokenisering.md. 8 new tests, red first;
6 mutations, 6 red, one of them only after the survivor was read as code and
a missing fixture was added. Suite 1287 -> 1295.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:43:39 +02:00

1626 lines
63 KiB
Python

"""The proposer: it proposes, a human adjudicates, and it never pretends otherwise.
The research is explicit about why this tool exists and about what it may not
claim. Topic 2 measured that the OKF reference agent splits on **what a thing
is** -- four semantic gates that need a model this run path does not have --
while a handful of MECHANICAL rules port today. Topic 1b measured that heading
derivation is inert on most of the K2 corpus: 23 of 33 PDFs carry no outline at
all and 95 % of the outline entries that exist are CAD export metadata, so the
scoreable denominator is ONE document. A rule validated on n=1 is not validated.
So the tool marks every decision `PROPOSED` and nothing else. The distinction
is the whole design: a proposal a human has not looked at must never be
replayable as an adjudication, because replay is exactly what the run path does
with a plan, deterministically and forever.
It lives outside `src/`, so it never enters a wheel (`tests/test_packaging.py`
enforces that) and no consumer's install surface changes because it exists.
"""
from __future__ import annotations
import json
import socket
from pathlib import Path
import pytest
from llm_ingestion_okf.segmentation import parse_segmentation_plan
from llm_ingestion_okf import propose as okf_propose_segments
DOCUMENT = """# N500 Vegbygging
Innledende tekst om vegbygging og dens omfang.
## 3.1 Brannkonsept
Krav til seksjonering av bygget over flere etasjer.
## 3.2 Roemning
To uavhengige roemningsveier fra hver branncelle.
## Og
## 3.3 Baereevne
Hovedbaeresystemet skal ha R60 dokumentert baereevne.
"""
#: The golden fixture's document. It carries a BARE-INTEGER ascending run of
#: three (`1`, `2`, `3`), which today's rules do not match at all -- measured,
#: bare `1` / `1.` / `1)` yield 0 candidates. That is precisely why the golden
#: is taken over THIS text and not over `DOCUMENT`: `DOCUMENT` has no
#: bare-integer line, so a golden over it would stay byte-identical even if the
#: outline rule were accidentally defaulted ON, and the guard would be a trap
#: written down but never able to fire. The ATX heading and the dotted heading
#: are here so the shipping rules fire too, and the golden pins their output as
#: well as the outline rule's absence.
OUTLINE_DOCUMENT = """# Stange skole -- teknisk grunnlag
Innledende tekst som gir dokumentet en kropp foer kapitlene begynner.
1 Innledning
Bakgrunnen for prosjektet og omfanget av arbeidet.
2 Krav
Krav til seksjonering av bygget over flere etasjer.
3 Gjennomfoering
Framdrift, faser og overlevering av bygget.
1.1 Brannkonsept
To uavhengige roemningsveier fra hver branncelle.
"""
#: Frozen in the golden's bytes. An explicit argument because the artifact
#: carries it verbatim, and a wall-clock default would make the golden
#: unreproducible by construction.
GOLDEN_PROPOSED_AT = "2026-09-03T00:00:00Z"
def write(tmp_path: Path, text: str = DOCUMENT, name: str = "n500.md") -> Path:
path = tmp_path / name
path.write_text(text, encoding="utf-8", newline="")
return path
def propose(tmp_path: Path, **kwargs: object) -> dict:
source = kwargs.pop("source", None) or write(tmp_path)
out = tmp_path / "plan.json"
code = okf_propose_segments.main([str(source), "--out", str(out)])
assert code == 0, f"proposer exited {code}"
return json.loads(out.read_text(encoding="utf-8"))
# --- the artifact is a valid plan -----------------------------------------
def test_the_emitted_artifact_parses_as_a_segmentation_plan(tmp_path: Path) -> None:
plan = parse_segmentation_plan(propose(tmp_path))
assert plan.entries
def test_it_carries_its_provenance(tmp_path: Path) -> None:
payload = propose(tmp_path)
assert payload["source_sha256"]
assert payload["extractor_id"]
assert payload["extractor_version"]
assert payload["proposed_by"].startswith("okf-propose-segments")
def test_the_source_hash_is_the_hash_of_the_bytes_it_read(tmp_path: Path) -> None:
import hashlib
source = write(tmp_path)
payload = propose(tmp_path, source=source)
assert payload["source_sha256"] == hashlib.sha256(source.read_bytes()).hexdigest()
# --- PROPOSED, never adjudicated ------------------------------------------
def test_every_entry_is_marked_proposed(tmp_path: Path) -> None:
payload = propose(tmp_path)
assert payload["entries"]
for entry in payload["entries"]:
assert okf_propose_segments.PROPOSED_MARKER in entry["derived"]
def test_no_entry_claims_to_be_adjudicated(tmp_path: Path) -> None:
payload = propose(tmp_path)
assert payload["adjudicated"] is False
for entry in payload["entries"]:
assert "adjudicated" not in entry
def test_every_entry_names_the_rule_that_proposed_it(tmp_path: Path) -> None:
# Per-decision provenance. A proposal an operator cannot trace to a rule is
# one they can only accept or reject wholesale.
for entry in propose(tmp_path)["entries"]:
rules = [name for name in entry["derived"] if name.startswith("rule:")]
assert len(rules) == 1
assert rules[0] in okf_propose_segments.RULE_NAMES
# --- the mechanical rules that actually ported -----------------------------
def test_a_heading_of_only_stop_words_is_not_a_boundary(tmp_path: Path) -> None:
# "Og" is a stop word. Without the gate it would open a segment of its own.
titles = [entry["title"] for entry in propose(tmp_path)["entries"]]
assert "Og" not in titles
assert any("Brannkonsept" in title for title in titles)
def test_a_heading_with_no_body_proposes_nothing(tmp_path: Path) -> None:
"""The orphan check, observed through the status now that a zero-entry plan
is no longer written: proposing nothing and writing nothing are the same
outcome, and it is distinct from failing."""
out = tmp_path / "orphans.json"
source = write(tmp_path, "# Tom\n\n## Ogsaa tom\n")
assert okf_propose_segments.main([str(source), "--out", str(out)]) == 1
assert not out.exists()
def test_the_spans_it_proposes_are_slices_of_the_text_it_read(tmp_path: Path) -> None:
from llm_ingestion_okf.extract import extract_text
source = write(tmp_path)
payload = propose(tmp_path, source=source)
text = extract_text(source.name, source.read_bytes())
for entry in payload["entries"]:
start, end = entry["span"]
assert text[start:end].strip()
def test_every_entry_carries_an_anchor_quoting_its_own_span(tmp_path: Path) -> None:
"""Written at proposal time, when text and offsets are known to agree.
Reconstructed later it would quote whatever the extraction had already
become, which is precisely the drift the anchor exists to survive.
"""
from llm_ingestion_okf.extract import extract_text
source = write(tmp_path)
payload = propose(tmp_path, source=source)
text = extract_text(source.name, source.read_bytes())
assert payload["entries"]
for entry in payload["entries"]:
start, end = entry["span"]
anchor = entry["anchor"]
assert anchor["quote"] == text[start:end]
assert text[start - len(anchor["prefix"]) : start] == anchor["prefix"]
assert text[end : end + len(anchor["suffix"])] == anchor["suffix"]
def test_a_proposed_plan_survives_a_shift_of_the_text_it_was_made_against(
tmp_path: Path,
) -> None:
"""The round trip the anchor exists for, measured end to end.
Prepend a paragraph -- exactly the benign edit that leaves every offset
short -- and every body must still be the text the proposer quoted. Before
anchors this cut each segment early and landed on real prose, with nothing
anywhere able to tell.
"""
from llm_ingestion_okf.extract import extract_text
from llm_ingestion_okf.segmentation import slice_segments
source = write(tmp_path)
plan = parse_segmentation_plan(propose(tmp_path, source=source))
text = extract_text(source.name, source.read_bytes())
quoted = [text[start:end] for start, end in (item.span for item in plan.entries)]
shifted = "Et nytt avsnitt foran alt annet.\n\n" + text
assert [body for _, body in slice_segments(shifted, plan)] == quoted
def test_the_paths_it_proposes_are_unique_and_hierarchical(tmp_path: Path) -> None:
plan = parse_segmentation_plan(propose(tmp_path))
paths = [entry.path for entry in plan.entries]
assert len(paths) == len(set(paths))
assert any("/" in path for path in paths)
# --- it does not reach the network ----------------------------------------
def test_it_makes_no_network_call(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
def refuse(*args: object, **kwargs: object) -> None:
raise AssertionError("the proposer opened a socket")
monkeypatch.setattr(socket, "socket", refuse)
monkeypatch.setattr(socket, "create_connection", refuse)
parse_segmentation_plan(propose(tmp_path))
def test_the_socket_guard_can_actually_fire(monkeypatch: pytest.MonkeyPatch) -> None:
# The known-positive control. Without it, a guard that patched the wrong
# name would make "no network call" a statement about nothing.
def refuse(*args: object, **kwargs: object) -> None:
raise AssertionError("the proposer opened a socket")
monkeypatch.setattr(socket, "socket", refuse)
with pytest.raises(AssertionError, match="opened a socket"):
socket.socket()
# --- failure is loud ------------------------------------------------------
def test_a_missing_source_exits_two_and_says_so(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
code = okf_propose_segments.main([str(tmp_path / "absent.md"), "--out", str(tmp_path / "o")])
assert code == 2
assert "okf-propose-segments" in capsys.readouterr().err
def test_an_unknown_file_type_exits_two(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
source = write(tmp_path, "body\n", name="thing.unknown")
code = okf_propose_segments.main([str(source), "--out", str(tmp_path / "o")])
assert code == 2
assert "okf-propose-segments" in capsys.readouterr().err
# --- the per-document scope a multi-document corpus needs ------------------
def test_a_path_prefix_scopes_every_entry_under_one_directory(tmp_path: Path) -> None:
"""Measured on the K2 corpus, 2026-09-03: 39 documents proposed 618 entries
under 601 distinct paths -- **17 paths were claimed by two documents each**.
Every one of them would hit Door B's collision gate, and the gate refuses
per DOCUMENT, so those documents would land as coded rejections instead of
concepts. Section numbering is document-local (`1 Innledning` is in most of
them), so the collision is structural rather than unlucky.
The scope is an argument and not something this tool invents: the proposer
sees one document and has no way of knowing what else is in the bundle, so
the caller who does supplies the prefix.
"""
out = tmp_path / "plan.json"
assert (
okf_propose_segments.main(
[str(write(tmp_path)), "--out", str(out), "--path-prefix", "Del II Bilag 3.1"]
)
== 0
)
payload = json.loads(out.read_text(encoding="utf-8"))
assert payload["entries"]
for entry in payload["entries"]:
assert entry["path"].startswith("del-ii-bilag-3-1/")
# Still a valid plan: the prefix passes the same path grammar as any other
# component, rather than being spliced in behind the validator's back.
assert parse_segmentation_plan(payload).entries
def test_a_multi_component_prefix_carries_the_relative_directory(tmp_path: Path) -> None:
"""Door B now walks the inbox recursively and records a `source_file`
relative to its root, so the caller driving a nested corpus has a
DIRECTORY, not a name, to scope by. Each component is reduced on its own
and rejoined with `/`: reducing the whole string would fold the separator
into a `-` and flatten `sub/sub2` into one component named `sub-sub2`,
which is a different bundle shape than the inbox it came from.
"""
out = tmp_path / "plan.json"
assert (
okf_propose_segments.main(
[str(write(tmp_path)), "--out", str(out), "--path-prefix", "Bilag 3/Del II"]
)
== 0
)
payload = json.loads(out.read_text(encoding="utf-8"))
assert payload["entries"]
for entry in payload["entries"]:
assert entry["path"].startswith("bilag-3/del-ii/")
assert parse_segmentation_plan(payload).entries
def test_a_prefix_with_one_empty_component_is_refused(tmp_path: Path) -> None:
"""`a//b` and `a/###/b` are the multi-component form of the same defect the
single-component gate already refuses: an empty component would collapse
the path silently rather than scope it.
"""
for prefix in ("sub//two", "sub/###/two", "/sub", "sub/"):
code = okf_propose_segments.main(
[str(write(tmp_path)), "--out", str(tmp_path / "p.json"), "--path-prefix", prefix]
)
assert code == 2, prefix
assert not (tmp_path / "p.json").exists(), prefix
def test_without_a_prefix_the_artifact_is_byte_identical(tmp_path: Path) -> None:
"""Additive. Every plan already produced stays exactly what it was."""
first = tmp_path / "a.json"
second = tmp_path / "b.json"
source = write(tmp_path)
okf_propose_segments.main([str(source), "--out", str(first)])
okf_propose_segments.main([str(source), "--out", str(second), "--path-prefix", ""])
assert first.read_bytes() == second.read_bytes()
def test_a_prefix_that_reduces_to_nothing_is_refused(tmp_path: Path) -> None:
"""A prefix of punctuation would otherwise become an empty component and
silently produce the unscoped paths the caller asked to avoid."""
code = okf_propose_segments.main(
[str(write(tmp_path)), "--out", str(tmp_path / "p.json"), "--path-prefix", "###"]
)
assert code == 2
assert not (tmp_path / "p.json").exists()
# --- nothing to propose is an outcome, and it is not an artifact -----------
def test_a_document_with_no_proposable_structure_writes_no_artifact(tmp_path: Path) -> None:
"""Measured on the K2 corpus 2026-09-03: 11 of 39 documents proposed zero
segments -- overwhelmingly PDFs with no declared structure, which Topic 1b
already measured at 23 of 33.
An empty artifact is not a plan a run can replay: `process_inbox` refuses
one by design, because an empty plan would persist nothing for a document
that was dropped. So the only use a zero-entry file has is to fail a run
later, and writing it converts "this document proposes no split" into "this
corpus cannot be built". It gets its own exit status instead: distinct from
2, which means the tool could not do its job at all.
"""
flat = write(tmp_path, "Loepende tekst uten overskrifter i det hele tatt.\n", name="flat.md")
out = tmp_path / "flat.json"
assert okf_propose_segments.main([str(flat), "--out", str(out)]) == 1
assert not out.exists()
def test_nothing_to_propose_is_not_reported_as_a_failure(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The two outcomes must be distinguishable by a driver reading the status:
one means "this document lands as one flat concept", the other means "stop"."""
flat = write(tmp_path, "Loepende tekst uten overskrifter i det hele tatt.\n", name="flat.md")
assert okf_propose_segments.main([str(flat), "--out", str(tmp_path / "f.json")]) == 1
nothing = capsys.readouterr().err
assert "FAILED" not in nothing
assert "nothing to propose" in nothing.lower()
assert (
okf_propose_segments.main([str(tmp_path / "absent.md"), "--out", str(tmp_path / "a.json")])
== 2
)
assert "FAILED" in capsys.readouterr().err
# --- Arm C: the size-driven subdivision -----------------------------------
#
# Arm C is NOT defined in `docs/2026-09-02-k3-k4-k5-metode.md` -- that file
# contains no occurrence of the word at all -- so the definition these tests
# pin was written for order 20260904T145630Z and is marked as such wherever it
# is reported: Arm B's mechanical rules, plus ONE deterministic rule that
# subdivides any proposed span longer than a declared cap at the nearest
# paragraph boundary at or before it, the whole document counting as one span
# when the rules find no boundary at all.
#
# One rule and not two, on purpose. The two failure modes the K2 rebuild
# measured -- a document with no outline (Bilag 9.1, 217 472 characters) and a
# document whose headings are a table of contents so the trailing segment
# absorbs the body (Bilag 3.1, Bilag 1) -- are the same failure of SIZE, and a
# second rule aimed at each would confound which one moved the number.
#
# What Arm C deliberately does NOT change: the region before the first
# candidate is still covered by no segment. That is a COVERAGE defect the
# baseline's blind rater named, it is real, and fixing it here would put two
# changes behind one measurement.
LONG_PARAGRAPH = ("Krav til seksjonering av bygget over flere etasjer. " * 40).strip()
UNSTRUCTURED = "\n\n".join(f"{LONG_PARAGRAPH} Avsnitt {i}." for i in range(12)) + "\n"
STRUCTURED_WITH_A_LONG_TAIL = (
"# N500 Vegbygging\n\nInnledende tekst om vegbygging.\n\n"
"## 3.1 Brannkonsept\n\nKort avsnitt om seksjonering.\n\n"
"## 3.2 Roemning\n\n" + UNSTRUCTURED
)
def propose_arm_c(tmp_path: Path, text: str, cap: int, name: str = "arm-c.md") -> dict:
source = write(tmp_path, text, name)
out = tmp_path / "arm-c-plan.json"
code = okf_propose_segments.main(
[str(source), "--out", str(out), "--max-segment-chars", str(cap)]
)
assert code == 0, f"proposer exited {code}"
return json.loads(out.read_text(encoding="utf-8"))
def test_arm_c_off_is_byte_identical_to_the_default(tmp_path: Path) -> None:
"""The cap defaults to 0 = off. The standard profile must not move."""
source = write(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, "same.md")
without = tmp_path / "without.json"
with_zero = tmp_path / "with-zero.json"
assert okf_propose_segments.main([str(source), "--out", str(without)]) == 0
assert (
okf_propose_segments.main(
[str(source), "--out", str(with_zero), "--max-segment-chars", "0"]
)
== 0
)
assert without.read_bytes() == with_zero.read_bytes()
def test_a_span_longer_than_the_cap_is_split_into_contiguous_parts(
tmp_path: Path,
) -> None:
"""The parts must TILE the span they replace: no overlap, no gap.
Overlap would emit the same unit of knowledge twice, which is the
`duplicate` category the K3 method counts. A gap would drop text silently.
"""
plan = propose_arm_c(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, 2000)
spans = [tuple(entry["span"]) for entry in plan["entries"]]
assert spans == sorted(spans)
for (_, first_end), (second_start, _) in zip(spans, spans[1:]):
assert first_end == second_start, f"{first_end} != {second_start}"
def test_no_proposed_span_exceeds_the_cap(tmp_path: Path) -> None:
plan = propose_arm_c(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, 2000)
for entry in plan["entries"]:
start, end = entry["span"]
assert end - start <= 2000, f"{entry['path']} is {end - start} chars"
def test_it_cuts_at_a_paragraph_boundary_when_one_is_available(
tmp_path: Path,
) -> None:
"""A cut mid-sentence splits one unit of knowledge for no reason."""
plan = propose_arm_c(tmp_path, UNSTRUCTURED, 3000)
text = UNSTRUCTURED
for entry in plan["entries"][1:]:
start = entry["span"][0]
assert text[start - 2 : start] == "\n\n", repr(text[start - 8 : start + 8])
def test_a_document_with_no_proposable_structure_is_segmented_by_size(
tmp_path: Path,
) -> None:
"""Bilag 9.1's case: no outline at all, so Arm B proposes nothing."""
out = tmp_path / "arm-b.json"
assert okf_propose_segments.main([str(write(tmp_path, UNSTRUCTURED)), "--out", str(out)]) == 1
assert not out.exists()
plan = propose_arm_c(tmp_path, UNSTRUCTURED, 3000)
assert len(plan["entries"]) > 1
assert plan["entries"][0]["span"][0] == 0
assert plan["entries"][-1]["span"][1] == len(UNSTRUCTURED)
def test_a_short_unstructured_document_still_proposes_nothing(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Arm C fires on SIZE. Under the cap there is nothing for it to do, and a
one-entry plan would only dress the same single concept in a plan file."""
source = write(tmp_path, "Bare en kort tekst uten noen overskrift.\n", "kort.md")
out = tmp_path / "kort.json"
assert (
okf_propose_segments.main([str(source), "--out", str(out), "--max-segment-chars", "3000"])
== 1
)
assert not out.exists()
def test_every_split_part_names_the_size_rule_and_stays_proposed(
tmp_path: Path,
) -> None:
plan = propose_arm_c(tmp_path, UNSTRUCTURED, 3000)
for entry in plan["entries"]:
assert entry["derived"][0] == okf_propose_segments.PROPOSED_MARKER
assert okf_propose_segments.RULE_SIZE_SPLIT in entry["derived"]
def test_a_split_part_keeps_the_rule_that_proposed_its_origin_span(
tmp_path: Path,
) -> None:
"""Traceability: a part must still name the heading rule it came from."""
plan = propose_arm_c(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, 2000)
split = [e for e in plan["entries"] if okf_propose_segments.RULE_SIZE_SPLIT in e["derived"]]
assert split
assert all(okf_propose_segments.RULE_HEADING in e["derived"] for e in split)
def test_a_span_under_the_cap_is_left_exactly_as_arm_b_proposed_it(
tmp_path: Path,
) -> None:
plan = propose_arm_c(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, 2000)
untouched = [
e for e in plan["entries"] if okf_propose_segments.RULE_SIZE_SPLIT not in e["derived"]
]
assert untouched
for entry in untouched:
assert entry["derived"] == [
okf_propose_segments.PROPOSED_MARKER,
okf_propose_segments.RULE_HEADING,
]
def test_split_parts_get_unique_paths(tmp_path: Path) -> None:
plan = propose_arm_c(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, 2000)
paths = [entry["path"] for entry in plan["entries"]]
assert len(paths) == len(set(paths))
def test_the_anchor_quotes_the_part_and_not_the_origin_span(tmp_path: Path) -> None:
plan = propose_arm_c(tmp_path, UNSTRUCTURED, 3000)
for entry in plan["entries"]:
start, end = entry["span"]
assert entry["anchor"]["quote"] == UNSTRUCTURED[start:end]
def test_a_negative_cap_is_refused(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
source = write(tmp_path, UNSTRUCTURED)
code = okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "x.json"), "--max-segment-chars", "-1"]
)
assert code == 2
assert "max-segment-chars" in capsys.readouterr().err
def test_an_unbreakable_line_longer_than_the_cap_is_still_cut(
tmp_path: Path,
) -> None:
"""No paragraph boundary to cut at. The cap still binds -- a part that
silently exceeded it would make the cap unenforceable exactly where the
document is hardest."""
plan = propose_arm_c(tmp_path, "x" * 9000 + "\n", 3000, "unbreakable.md")
for entry in plan["entries"]:
start, end = entry["span"]
assert end - start <= 3000
# --- the default profile, pinned byte for byte ----------------------------
#
# The one guard that can go red on an accidental default-on regression of any
# rule added later. It compares the WHOLE artifact, so a new candidate, a moved
# span, a changed `derived` list or a renamed path all break it.
def test_the_default_artifact_matches_its_committed_golden(tmp_path: Path) -> None:
"""The default profile is a promise to consumers; this is what keeps it.
The golden transitively pins `observed_extractor_version` too, so a
converter bump turns this red. That is a legitimate red, not a defect:
read the diff and decide, per `tests/fixtures/README.md`.
"""
source = write(tmp_path, OUTLINE_DOCUMENT, "outline.md")
out = tmp_path / "outline-plan.json"
assert (
okf_propose_segments.main(
[str(source), "--out", str(out), "--proposed-at", GOLDEN_PROPOSED_AT]
)
== 0
)
golden = Path(__file__).parent / "fixtures" / "propose-golden-default.json"
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"
# --- Arm D: wiring the rule into find_candidates ---------------------------
DOTTED_AND_INTEGER_SIBLINGS = """1 Innledning
Bakgrunn for prosjektet og omfanget.
2 Krav
Krav til seksjonering av bygget.
1.2 Brannkonsept
To uavhengige roemningsveier fra hver branncelle.
3 Gjennomfoering
Framdrift, faser og overlevering.
"""
ARM_B_HEADING_THEN_OUTLINE = """# Teknisk grunnlag
1 Innledning
Bakgrunn for prosjektet.
2 Krav
Krav til seksjonering.
3 Gjennomfoering
Framdrift og faser.
"""
def arm_d_plan(tmp_path: Path, text: str, run_length: int, name: str = "arm-d.md") -> dict:
"""Arm D at the `build_plan` level -- no CLI, so step 5 stands alone."""
source = write(tmp_path, text, name)
return okf_propose_segments.build_plan(
source,
text,
source.read_bytes(),
okf_type="reference",
proposed_at="2026-09-07T00:00:00Z",
outline_run=run_length,
)
def test_an_outline_entry_names_the_outline_rule_and_nothing_else() -> None:
"""Exhaustive `==`, not `in`: a second rule name would be a second claim."""
candidates = okf_propose_segments.find_candidates(ALL_THREE_INTEGER_FORMS, outline_run=3)
assert [c.rule for c in candidates] == [okf_propose_segments.RULE_OUTLINE] * 3
assert [c.title for c in candidates] == ["Innledning", "Krav", "Gjennomfoering"]
def test_a_dotted_heading_and_its_integer_sibling_carry_different_rules() -> None:
"""The paired form, and the pairing is what makes it non-vacuous.
A lone "no outline rule fired on `1.2`" assertion is green TODAY for the
opposite reason: measured, `1.2 Brannkonsept` already yields a candidate via
`_NUMBERED` with `rule:heading`. Asserting both siblings in ONE document is
what makes the integer-only claim testable.
"""
candidates = okf_propose_segments.find_candidates(DOTTED_AND_INTEGER_SIBLINGS, outline_run=3)
by_title = {c.title: c.rule for c in candidates}
assert by_title["Brannkonsept"] == okf_propose_segments.RULE_HEADING
assert by_title["Innledning"] == okf_propose_segments.RULE_OUTLINE
assert by_title["Krav"] == okf_propose_segments.RULE_OUTLINE
def test_an_outline_candidate_can_orphan_the_arm_b_candidate_it_follows() -> None:
"""Characterization, with the mechanism named rather than implied.
The orphan check at the emit loop measures a heading's body up to the NEXT
candidate of ANY rule. So an outline candidate landing immediately after an
Arm B heading empties that heading's body and deletes it. This is the
measured 34 %-deletion behaviour, pinned here so a later change to the span
bound has to confront it.
"""
without = okf_propose_segments.find_candidates(ARM_B_HEADING_THEN_OUTLINE)
assert [c.title for c in without] == ["Teknisk grunnlag"]
with_arm_d = okf_propose_segments.find_candidates(ARM_B_HEADING_THEN_OUTLINE, outline_run=3)
# The ATX heading is gone: `1 Innledning` follows it immediately, so its
# body is empty and the orphan check drops it.
assert "Teknisk grunnlag" not in [c.title for c in with_arm_d]
assert [c.rule for c in with_arm_d] == [okf_propose_segments.RULE_OUTLINE] * 3
def test_arm_d_spans_tile_and_each_anchor_quotes_its_own_span(tmp_path: Path) -> None:
plan = arm_d_plan(tmp_path, DOTTED_AND_INTEGER_SIBLINGS, 3)
spans = [tuple(entry["span"]) for entry in plan["entries"]]
assert spans == sorted(spans)
for (_, first_end), (second_start, _) in zip(spans, spans[1:]):
assert first_end == second_start, f"gap or overlap: {first_end} != {second_start}"
for entry in plan["entries"]:
start, end = entry["span"]
assert entry["anchor"]["quote"] == DOTTED_AND_INTEGER_SIBLINGS[start:end]
def test_outline_entries_get_unique_paths(tmp_path: Path) -> None:
plan = arm_d_plan(tmp_path, DOTTED_AND_INTEGER_SIBLINGS, 3)
paths = [entry["path"] for entry in plan["entries"]]
assert len(paths) == len(set(paths))
assert any(
entry["derived"]
== [
okf_propose_segments.PROPOSED_MARKER,
okf_propose_segments.RULE_OUTLINE,
]
for entry in plan["entries"]
)
def test_no_line_can_match_the_outline_grammar_and_an_arm_b_rule() -> None:
"""Why this repo ships NO coincident-boundary dedupe, pinned as a test.
The plan called for a dedupe branch guarding two candidates on one line.
Measured over 2400 synthetic lines spanning every shape the four grammars
accept: **zero** lines match `_OUTLINE` and any of `_ATX`, `_NUMBERED` or
`_TABLE_ROW`. `_ATX` needs `#`, `_TABLE_ROW` needs `|`, `_NUMBERED` needs a
dot inside the number, and `_OUTLINE` needs whitespace right after an
integer -- so the collision is impossible BY CONSTRUCTION, not merely
absent from this corpus, and a dedupe branch would be dead code with a test
that could never go red for a real reason.
This test is what keeps that argument honest: widen `_OUTLINE` until it can
overlap and this goes red, at which point the dedupe branch is owed.
"""
import itertools
collisions = []
tried = 0
for pre, num, mark, title, sep in itertools.product(
["", " ", " ", " ", " "],
["1", "9", "12", "99", "1.", "1)", "1.2", "12.3.4"],
["", "#", "##", "######", "|"],
["Innledning", "A |", "| A |", "Krav 6"],
[" ", " ", "\t"],
):
line = f"{pre}{mark}{num}{sep}{title}"
tried += 1
if okf_propose_segments._OUTLINE.match(line) and (
okf_propose_segments._ATX.match(line)
or okf_propose_segments._NUMBERED.match(line)
or okf_propose_segments._TABLE_ROW.match(line)
):
collisions.append(line)
assert tried == 2400
assert collisions == []
# Known-positive controls: the probe CAN match, so the empty result above
# is a measurement and not a broken query.
assert okf_propose_segments._OUTLINE.match("1 Innledning")
assert okf_propose_segments._ATX.match("# 1.2 Krav")
assert okf_propose_segments._NUMBERED.match("1.2 Krav")
assert okf_propose_segments._TABLE_ROW.match("| a | b |")
def test_the_outline_rule_is_off_at_the_function_level_by_default() -> None:
"""`outline_run` defaults to 0 and 0 means Arm B, exactly."""
default = okf_propose_segments.find_candidates(ALL_THREE_INTEGER_FORMS)
explicit_zero = okf_propose_segments.find_candidates(ALL_THREE_INTEGER_FORMS, outline_run=0)
assert default == explicit_zero == []
# The known-positive control: the same text DOES produce candidates when asked.
assert len(okf_propose_segments.find_candidates(ALL_THREE_INTEGER_FORMS, outline_run=3)) == 3
# --- Arm D: the CLI flag ---------------------------------------------------
def propose_arm_d(tmp_path: Path, text: str, run_length: int, name: str = "arm-d.md") -> dict:
source = write(tmp_path, text, name)
out = tmp_path / "arm-d-cli.json"
code = okf_propose_segments.main(
[str(source), "--out", str(out), "--outline-run", str(run_length)]
)
assert code == 0, f"proposer exited {code}"
return json.loads(out.read_text(encoding="utf-8"))
def test_outline_run_zero_is_byte_identical_to_the_flag_being_absent(tmp_path: Path) -> None:
"""The default profile is a promise; 0 must not be a different code path."""
source = write(tmp_path, ALL_THREE_INTEGER_FORMS, "same.md")
without = tmp_path / "without.json"
with_zero = tmp_path / "with-zero.json"
assert okf_propose_segments.main([str(source), "--out", str(without)]) == 1
assert (
okf_propose_segments.main([str(source), "--out", str(with_zero), "--outline-run", "0"]) == 1
)
# Exit 1 both times: this text has no Arm B boundary at all, so neither run
# writes an artifact. The known-positive control that the text is not inert:
assert (
okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "on.json"), "--outline-run", "3"]
)
== 0
)
def test_the_two_arms_are_independent(tmp_path: Path) -> None:
"""`--outline-run 0` leaves Arm C's artifact untouched, and vice versa."""
source = write(tmp_path, STRUCTURED_WITH_A_LONG_TAIL, "both.md")
arm_c_only = tmp_path / "c.json"
arm_c_and_zero = tmp_path / "cz.json"
assert (
okf_propose_segments.main(
[str(source), "--out", str(arm_c_only), "--max-segment-chars", "2000"]
)
== 0
)
assert (
okf_propose_segments.main(
[
str(source),
"--out",
str(arm_c_and_zero),
"--max-segment-chars",
"2000",
"--outline-run",
"0",
]
)
== 0
)
assert arm_c_only.read_bytes() == arm_c_and_zero.read_bytes()
def test_a_negative_outline_run_is_refused_by_the_tool_not_by_argparse(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Exit 2 alone cannot separate the two refusals -- argparse exits 2 too.
`FAILED` is the tool's own two-line failure form, so asserting it is what
proves the refusal came from `run` and not from the parser.
"""
source = write(tmp_path, ALL_THREE_INTEGER_FORMS)
code = okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "x.json"), "--outline-run", "-1"]
)
assert code == 2
err = capsys.readouterr().err
assert "FAILED" in err
assert "outline-run" in err
def test_a_non_integer_outline_run_is_refused_by_argparse(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The other exit-2 path, so the two cannot be satisfied by one branch."""
source = write(tmp_path, ALL_THREE_INTEGER_FORMS)
with pytest.raises(SystemExit) as exit_info:
okf_propose_segments.main(
[str(source), "--out", str(tmp_path / "x.json"), "--outline-run", "three"]
)
assert exit_info.value.code == 2
assert "usage:" in capsys.readouterr().err
#: Which arm flags carry the literal "not defined upstream" in their own help
#: chunk, and which must NOT. Data rather than a hand-counted assertion: the
#: whole-output count below is derived from this table, so adding an arm is one
#: line here instead of an edit to a number nobody can trace back to a rule.
#: Arm C is False on purpose -- its help says "not defined in the K3 method
#: file" instead, and the point of the check is that each arm's attribution
#: sits in its OWN chunk.
ARM_ATTRIBUTION = {"outline-run": True, "table-grid": True, "max-segment-chars": False}
def test_each_arm_flag_carries_its_attribution_inside_its_own_option_chunk(
capsys: pytest.CaptureFixture[str],
) -> None:
"""Each attribution must sit in its own option's help, not merely in the file.
Sliced between option headers on purpose: a whole-output grep would be
satisfied by adding the literal to another arm's block, which would
attribute the wrong rule. And the squeeze is `tr -s '[:space:]'`, not a
newline swap -- argparse wraps its help, so a newline-only normalisation
leaves the run of spaces behind and the literal stays unfindable.
The whole-output count is DERIVED from `ARM_ATTRIBUTION` rather than
written down. A hard-coded `== 1` is correct only until a second arm is
attributed the same way, and it then goes red on an axis that has nothing
to do with whether the new arm is right.
"""
with pytest.raises(SystemExit) as exit_info:
okf_propose_segments.main(["--help"])
assert exit_info.value.code == 0
squeezed = " ".join(capsys.readouterr().out.split())
# Drop the usage line: it repeats every option name, so slicing the whole
# output would find two chunks per flag and neither would be the help.
assert "options:" in squeezed
options = squeezed.split("options:", 1)[1]
chunks = options.split(" --")
for flag, attributed in ARM_ATTRIBUTION.items():
matching = [c for c in chunks if c.startswith(flag)]
assert len(matching) == 1, f"expected exactly one {flag} chunk, got {len(matching)}"
if attributed:
assert "not defined upstream" in matching[0], flag
else:
assert "not defined upstream" not in matching[0], flag
expected = sum(1 for attributed in ARM_ATTRIBUTION.values() if attributed)
assert squeezed.count("not defined upstream") == expected
# --- Arm E: the grid-rule join --------------------------------------------
#
# Arm E is not defined in `docs/2026-09-02-k3-k4-k5-metode.md` -- that file
# contains no occurrence of the word "arm" at all. The definition below was
# written for order 20260907T075834Z-18584396-from-.claude and is the author's.
#
# Two things about the fixtures are load bearing rather than stylistic.
#
# FIRST: every count asserted below was MEASURED against today's code before
# the test was written, so a fixture cannot be green by accident. `GRID_TABLE`
# yields 3 candidates today and `TWO_TABLES_BLANK_SEPARATED` yields 3, not
# because those numbers look right but because that is what `find_candidates`
# returns on this text right now.
#
# SECOND: the second table in `TWO_TABLES_BLANK_SEPARATED` has exactly ONE row
# group, so it CANNOT be joined. That is the only shape that can catch a join
# whose "a rule line is pending" state is never cleared -- a table's trailing
# bottom rule sets it, and if the blank line after the table does not clear it,
# the next table's first row is marked joined with no join having occurred. A
# fixture whose second table also joins would stay green through that defect.
#
# The `propose()` helper above is deliberately NOT used anywhere in this band:
# it pops `source` and silently discards every other kwarg, so a test written
# as `propose(tmp_path, table_grid=True)` would measure the DEFAULT and pass.
GRID_TABLE = """+-------+-------+
| Navn | Verdi |
+=======+=======+
| Areal | 120 |
+-------+-------+
| Hoyde | 3 |
+-------+-------+
"""
TWO_TABLES_BLANK_SEPARATED = """+-------+-------+
| Navn | Verdi |
+=======+=======+
| Areal | 120 |
+-------+-------+
Mellomtekst som skiller de to tabellene.
+-------+-------+
| Rom | Antall|
+-------+-------+
Avsluttende avsnitt.
"""
TWO_TABLES_RULE_SEPARATED = """+-------+-------+
| Navn | Verdi |
+-------+-------+
| Rom | Antall|
+-------+-------+
"""
PIPE_TABLE = """| Navn | Verdi |
|-------|-------|
| Areal | 120 |
| Hoyde | 3 |
Etterfoelgende avsnitt.
"""
PROSE_WITH_A_STRAY_RULE = """# Notat
Et avsnitt med tekst.
+-------+-------+
Enda et avsnitt med tekst.
"""
GRID_GOLDEN_DOCUMENT = """# Romskjema
Innledende avsnitt om romskjemaet.
+-------+-------+
| Navn | Verdi |
+=======+=======+
| Areal | 120 |
+-------+-------+
| Hoyde | 3 |
+-------+-------+
"""
HEADING_ORPHANED_BY_ITS_TABLE = """## Prissammenstilling
| Post | Sum |
|-------|-------|
| A | 100 |
"""
HEADING_WITH_A_BODY_ABOVE_A_TABLE = """## Prissammenstilling
Innledende tekst om skjemaet.
| Post | Sum |
|-------|-------|
| A | 100 |
"""
ORPHAN_THEN_A_LIVE_HEADING_THEN_A_TABLE = """## Tomt kapittel
## Innledning
Tekst i innledningen.
| Post | Sum |
|-------|-------|
| A | 100 |
"""
NUMBERED_HEADING_ORPHANED_BY_ITS_TABLE = """## 3.1 Prissammenstilling
| Post | Sum |
|-------|-------|
| A | 100 |
"""
def test_a_table_that_orphans_its_heading_takes_that_headings_name() -> None:
"""MEASURED, twice, before the rule was written.
A spreadsheet row now renders as pipe rows, so `rule:table-block` opens on
the line after the sheet heading; the heading's own body is then empty and
the orphan check drops it; and the only surviving candidate carries the
mechanical `Tabell linje <n>`. The sheet's NAME is destroyed -- measured on
a 629-concept bundle, the concept fell from candidate rank 10 to 19 for a
question naming its subject, and restoring the title alone put it back at
10. The title is not lost here: it moves to the segment that holds the
heading's content.
"""
candidates = okf_propose_segments.find_candidates(HEADING_ORPHANED_BY_ITS_TABLE)
assert len(candidates) == 1
assert candidates[0].title == "Prissammenstilling"
assert candidates[0].rule == okf_propose_segments.RULE_TABLE_BLOCK
def test_a_table_below_a_heading_that_keeps_its_body_is_not_renamed() -> None:
"""The known-negative, and the reason the rule is conditioned on the drop.
Here the heading survives, so its title is still carried by a live
candidate. Renaming the table to it as well would put the same name on two
concepts and take a name away from neither.
"""
candidates = okf_propose_segments.find_candidates(HEADING_WITH_A_BODY_ABOVE_A_TABLE)
assert [c.title for c in candidates] == ["Prissammenstilling", "Tabell linje 5"]
def test_an_orphaned_headings_section_number_moves_with_its_title() -> None:
"""`_segment_path` reads BOTH: the number becomes the directory and is
stripped from the stem. Inheriting the title alone would emit
`3-1-prissammenstilling.md` at the top level -- a name the heading never
had."""
candidates = okf_propose_segments.find_candidates(NUMBERED_HEADING_ORPHANED_BY_ITS_TABLE)
assert len(candidates) == 1
assert candidates[0].title == "3.1 Prissammenstilling"
assert candidates[0].number == "3.1"
assert okf_propose_segments._segment_path(candidates[0], set()) == "3-1/prissammenstilling.md"
def test_an_orphaned_name_is_not_carried_past_the_candidate_that_follows_it() -> None:
"""The name goes to the ADJACENT segment or nowhere.
Written because a mutation that never cleared the carried name survived the
rest of this band: with the name held indefinitely, the table below would
take the title of a heading it does not contain, several candidates away.
That is an invention, not a rescue.
"""
candidates = okf_propose_segments.find_candidates(ORPHAN_THEN_A_LIVE_HEADING_THEN_A_TABLE)
assert [c.title for c in candidates] == ["Innledning", "Tabell linje 6"]
def test_todays_grid_table_cuts_one_table_into_one_concept_per_row_group() -> None:
"""Characterization. ONE table, three concepts -- the defect Arm E measures.
Committed BEFORE the rule exists, so the later claim that the default did
not move rests on a pinned artifact rather than on reading a diff.
"""
candidates = okf_propose_segments.find_candidates(GRID_TABLE)
assert len(candidates) == 3
assert [c.rule for c in candidates] == [okf_propose_segments.RULE_TABLE_BLOCK] * 3
assert [c.title for c in candidates] == [
"Tabell linje 2",
"Tabell linje 4",
"Tabell linje 6",
]
# The spans tile: the document is cut into three, not sampled at three points.
assert candidates[0].start == 18
assert [(c.start, c.end) for c in candidates] == [(18, 54), (54, 90), (90, 126)]
assert candidates[-1].end == len(GRID_TABLE)
def test_a_grid_rule_line_matches_no_shipping_grammar() -> None:
"""The four rules that ship today all decline a grid rule line.
Fed with the trailing newline every one of them actually sees, because
`find_candidates` iterates `splitlines(keepends=True)`. A stripped literal
would pass here while the shipped loop never met the same string.
"""
rules = ("+---+---+\n", "+===+===+\n", "+:--+--:+\n", " +---+---+\n")
for line in rules:
assert okf_propose_segments._TABLE_ROW.match(line) is None, line
assert okf_propose_segments._ATX.match(line) is None, line
assert okf_propose_segments._NUMBERED.match(line) is None, line
assert okf_propose_segments._OUTLINE.match(line) is None, line
# Known-positive controls: each grammar CAN match something, so the four
# `is None` assertions above are evidence rather than four dead regexes.
assert okf_propose_segments._TABLE_ROW.match("| a | b |\n") is not None
assert okf_propose_segments._ATX.match("# Tittel\n") is not None
assert okf_propose_segments._NUMBERED.match("3.1 Brannkonsept\n") is not None
assert okf_propose_segments._OUTLINE.match("3 Brannkonsept\n") is not None
def test_the_default_artifact_over_a_grid_table_matches_its_committed_golden(
tmp_path: Path,
) -> None:
"""The second golden, and the reason there has to be a second one.
`propose-golden-default.json` is taken over `OUTLINE_DOCUMENT`, which
contains no `|` row and no `+` rule line. It is therefore STRUCTURALLY
incapable of going red if a table rule were ever defaulted on -- a trap
written down but unable to fire. This one is taken over a document that
has a grid table, so it can.
"""
source = write(tmp_path, GRID_GOLDEN_DOCUMENT, "grid.md")
out = tmp_path / "grid-plan.json"
assert (
okf_propose_segments.main(
[str(source), "--out", str(out), "--proposed-at", GOLDEN_PROPOSED_AT]
)
== 0
)
golden = Path(__file__).parent / "fixtures" / "propose-golden-grid-default.json"
assert out.read_bytes() == golden.read_bytes(), (
"the default artifact over a grid table diverges from its committed golden bytes"
)
def test_the_grid_rule_name_is_registered() -> None:
"""A rule an operator cannot find in `RULE_NAMES` is an unnameable rule."""
assert okf_propose_segments.RULE_TABLE_GRID == "rule:table-grid"
assert okf_propose_segments.RULE_TABLE_GRID in okf_propose_segments.RULE_NAMES
def test_the_grid_rule_grammar_is_the_class_the_corpus_declared() -> None:
"""`[-=:+]`, and the `\\s*` tolerance, are both measured rather than guessed.
Measured on the three grid-bearing documents of the K2 corpus: 38 of 38
lines whose stripped form starts with `+` match this pattern, and the
complete character set on those lines is `+`, `-`, `:`, `=`. The `:` is
pandoc's column-alignment marker; a first pass with the class `[-=+]`
returned 37 and mis-read one document as having two tables instead of one.
Every input carries its trailing newline, because `find_candidates`
iterates `splitlines(keepends=True)` and that is the string the shipped
loop actually sees. A stripped-literal test would be green while the rule
never fired on an indented or trailing-space rule line.
"""
grid = okf_propose_segments._GRID_RULE
for line in ("+---+---+\n", "+===+===+\n", "+:--+--:+\n", "+---+\n", " +---+---+\n"):
assert grid.match(line) is not None, line
for line in ("+\n", "++\n", "|---|---|\n", "+--- +---+\n", "---+---\n", "+abc+\n"):
assert grid.match(line) is None, line
def test_a_grid_rule_does_not_close_an_open_table_block() -> None:
"""Both halves in ONE test, because either alone is worthless.
A flag-on assertion by itself stays green if the DEFAULT also moved; a
flag-off assertion by itself is true by absence before the rule exists.
Asserting `3` and `1` over the same text is what makes each half depend on
the rule actually running.
"""
off = okf_propose_segments.find_candidates(GRID_TABLE)
assert len(off) == 3
on = okf_propose_segments.find_candidates(GRID_TABLE, table_grid=True)
assert len(on) == 1
assert on[0].title == "Tabell linje 2"
assert on[0].start == 18
assert on[0].end == len(GRID_TABLE)
assert on[0].grid is True
def test_a_leading_grid_rule_does_not_open_a_block() -> None:
"""The rule line suppresses a CLOSE; it never opens.
Asserted as an offset rather than as a length: a span of the right size
starting at 0 would swallow the table's top border and would still have
"the right number of candidates". `18` is the offset of the first `|` row,
unchanged from Arm D, which is what keeps every surviving candidate's
`start` fixed and the orphan check monotone.
"""
on = okf_propose_segments.find_candidates(GRID_TABLE, table_grid=True)
assert on[0].start == 18
assert GRID_TABLE[on[0].start :].startswith("| Navn")
def test_two_tables_separated_by_a_blank_line_stay_two_concepts() -> None:
"""The test that catches a join whose pending state is never cleared.
The second table has exactly ONE row group, so it cannot be joined. If the
bottom rule of the FIRST table leaves "a rule line is pending" set, and the
blank line after it does not clear the flag, the second table's first row
is marked joined although nothing was joined -- and a test that checked
only the candidate COUNT would stay green through it.
"""
off = okf_propose_segments.find_candidates(TWO_TABLES_BLANK_SEPARATED)
assert len(off) == 3
on = okf_propose_segments.find_candidates(TWO_TABLES_BLANK_SEPARATED, table_grid=True)
assert len(on) == 2
assert on[0].grid is True
assert on[1].grid is False
def test_two_tables_separated_by_a_rule_line_alone_are_merged() -> None:
"""A declared limit, measured rather than argued away.
Pandoc puts a blank line between two adjacent tables, so it does not emit
this shape -- but that is a property of the WRITER, not of this code, and
`in_table` survives an arbitrary run of rule lines. The corpus diff is what
tests whether the shape occurs; this test states what happens if it does.
"""
off = okf_propose_segments.find_candidates(TWO_TABLES_RULE_SEPARATED)
assert len(off) == 2
on = okf_propose_segments.find_candidates(TWO_TABLES_RULE_SEPARATED, table_grid=True)
assert len(on) == 1
assert on[0].grid is True
def test_a_pipe_table_is_unchanged_by_the_flag() -> None:
"""A markdown pipe table has no rule line, so Arm E has nothing to say."""
off = okf_propose_segments.find_candidates(PIPE_TABLE)
on = okf_propose_segments.find_candidates(PIPE_TABLE, table_grid=True)
assert len(off) == 1
assert off == on
assert on[0].grid is False
def test_a_rule_line_outside_a_table_changes_nothing() -> None:
"""A `+---+` in prose is not inside a table, so no block is open to keep.
The `len == 1` is the denominator: an equality over two empty lists would
be true and would prove nothing.
"""
off = okf_propose_segments.find_candidates(PROSE_WITH_A_STRAY_RULE)
on = okf_propose_segments.find_candidates(PROSE_WITH_A_STRAY_RULE, table_grid=True)
assert len(off) == 1
assert off == on
def test_the_grid_rule_is_off_at_the_function_level_by_default() -> None:
"""Default and explicit-off agree, AND the count is the pre-change one.
The equality alone would stay green if both had moved together; the
`len == 3` is what makes it a guard. The known-positive control proves the
fixture is not inert.
"""
default = okf_propose_segments.find_candidates(GRID_TABLE)
explicit = okf_propose_segments.find_candidates(GRID_TABLE, table_grid=False)
assert default == explicit
assert len(default) == 3
assert len(okf_propose_segments.find_candidates(GRID_TABLE, table_grid=True)) == 1
def test_every_candidate_present_in_both_arms_keeps_its_start() -> None:
"""Monotonicity: Arm E only ever DELETES marks, so bodies only grow.
This is what makes the orphan check unable to drop a candidate it
previously kept -- the check is monotone in the line set, so a growing body
can only ever become non-empty. Asserted over all three table fixtures,
with the number of common starts printed as the denominator.
"""
for text in (GRID_TABLE, TWO_TABLES_BLANK_SEPARATED, TWO_TABLES_RULE_SEPARATED, PIPE_TABLE):
off = {c.start: c for c in okf_propose_segments.find_candidates(text)}
on = {c.start: c for c in okf_propose_segments.find_candidates(text, table_grid=True)}
common = sorted(set(off) & set(on))
assert common, "no common start offsets -- the comparison would be vacuous"
for start in common:
assert on[start].end >= off[start].end
def arm_e_plan(
tmp_path: Path, text: str, name: str = "arm-e.md", max_segment_chars: int = 0
) -> dict:
"""Arm E at the `build_plan` level -- no CLI, so this step stands alone."""
source = write(tmp_path, text, name)
return okf_propose_segments.build_plan(
source,
text,
source.read_bytes(),
okf_type="reference",
proposed_at="2026-09-07T00:00:00Z",
max_segment_chars=max_segment_chars,
table_grid=True,
)
def test_a_joined_block_names_the_table_rule_and_the_grid_rule(tmp_path: Path) -> None:
"""Exhaustive `==`, never `in`.
`assert RULE_TABLE_GRID in derived` would pass for an implementation that
appended the name to every entry whenever the flag was on. Two names rather
than one because the table rule is still what OPENED the span; dropping it
would leave the entry traceable to nothing but the join.
"""
plan = arm_e_plan(tmp_path, GRID_TABLE)
assert len(plan["entries"]) == 1
assert plan["entries"][0]["derived"] == [
okf_propose_segments.PROPOSED_MARKER,
okf_propose_segments.RULE_TABLE_BLOCK,
okf_propose_segments.RULE_TABLE_GRID,
]
def test_a_pipe_table_under_the_flag_does_not_claim_the_grid_rule(tmp_path: Path) -> None:
"""The discriminator. Flag ON, nothing joined, so the name must be absent.
The `len == 1` comes first: an assertion over an empty entry list would be
vacuously true and would prove nothing about the flag.
"""
plan = arm_e_plan(tmp_path, PIPE_TABLE)
assert len(plan["entries"]) == 1
assert plan["entries"][0]["derived"] == [
okf_propose_segments.PROPOSED_MARKER,
okf_propose_segments.RULE_TABLE_BLOCK,
]
def test_a_heading_entry_never_carries_the_grid_rule(tmp_path: Path) -> None:
"""A heading is not a table, whatever else the document contains."""
plan = arm_e_plan(tmp_path, GRID_GOLDEN_DOCUMENT)
heading = plan["entries"][0]
assert heading["title"] == "Romskjema"
assert heading["derived"] == [
okf_propose_segments.PROPOSED_MARKER,
okf_propose_segments.RULE_HEADING,
]
def test_arm_e_spans_tile_and_each_anchor_quotes_its_own_span(tmp_path: Path) -> None:
"""Joining must not leave a hole, and the anchor must still name its span."""
plan = arm_e_plan(tmp_path, GRID_GOLDEN_DOCUMENT)
spans = [tuple(entry["span"]) for entry in plan["entries"]]
assert spans == sorted(spans)
for first, second in zip(spans, spans[1:]):
assert first[1] == second[0]
for entry in plan["entries"]:
start, end = entry["span"]
assert entry["anchor"]["quote"] == GRID_GOLDEN_DOCUMENT[start:end]
def test_a_split_part_of_a_joined_block_names_all_three_rules_in_order(tmp_path: Path) -> None:
"""The proof that `subdivide` copies the field its rebuild could drop.
Arm C cuts the joined span into parts, and every part is a fresh
`Candidate` built from an explicit keyword list. If `grid` is not copied
there, the parts lose the join and only this test says so. The order is
pinned too: the marker, the rule that OPENED the span, the join, then the
size cut.
"""
plan = arm_e_plan(tmp_path, GRID_TABLE, max_segment_chars=60)
assert len(plan["entries"]) > 1, "the cap must actually bind, or this proves nothing"
assert plan["entries"][1]["derived"] == [
okf_propose_segments.PROPOSED_MARKER,
okf_propose_segments.RULE_TABLE_BLOCK,
okf_propose_segments.RULE_TABLE_GRID,
okf_propose_segments.RULE_SIZE_SPLIT,
]
def propose_arm_e(tmp_path: Path, text: str, name: str = "arm-e-cli.md", *flags: str) -> bytes:
"""Arm E through the CLI. Bytes, not a dict: the artifact is the promise."""
source = write(tmp_path, text, name)
out = tmp_path / f"{name}.json"
code = okf_propose_segments.main([str(source), "--out", str(out), *flags])
assert code == 0, f"proposer exited {code}"
return out.read_bytes()
def test_the_flag_absent_is_byte_identical_and_present_changes_the_bytes(
tmp_path: Path,
) -> None:
"""Absent is off; present is not a no-op. Both halves, same text."""
absent = propose_arm_e(tmp_path, GRID_TABLE, "absent.md")
present = propose_arm_e(tmp_path, GRID_TABLE, "present.md", "--table-grid")
assert absent != present
# And absent is the pre-Arm-E artifact, not merely "some other bytes":
# three entries, none of them naming the join.
assert json.loads(absent)["entries"].__len__() == 3
assert "rule:table-grid" not in absent.decode("utf-8")
assert json.loads(present)["entries"].__len__() == 1
def test_arm_d_and_arm_e_are_independent(tmp_path: Path) -> None:
"""Neither arm may quietly imply the other."""
d_only = propose_arm_e(tmp_path, GRID_TABLE, "d.md", "--outline-run", "3")
both = propose_arm_e(tmp_path, GRID_TABLE, "both.md", "--outline-run", "3", "--table-grid")
assert d_only != both
e_only = propose_arm_e(tmp_path, GRID_TABLE, "e.md", "--table-grid")
e_zero = propose_arm_e(tmp_path, GRID_TABLE, "e0.md", "--outline-run", "0", "--table-grid")
assert e_only == e_zero
def test_table_grid_takes_no_argument(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""Boolean, not an integer pretending to be one.
Arm D's gate is a run LENGTH and 0 means off; Arm E has no numeric
parameter, so inventing one would create a sweepable knob that means
nothing. `usage:` in stderr is what separates argparse's refusal from the
tool's own `ProposerError` exit, which is also 2.
Honest about its own limits: this test is green BEFORE the flag exists too,
because argparse rejects an unknown option with the same code. It becomes
evidence only once `--table-grid` is a real flag, and what it then pins is
that the flag stayed boolean.
"""
source = write(tmp_path, GRID_TABLE, "noarg.md")
out = tmp_path / "noarg.json"
with pytest.raises(SystemExit) as exit_info:
okf_propose_segments.main([str(source), "--out", str(out), "--table-grid", "3"])
assert exit_info.value.code == 2
assert "usage:" in capsys.readouterr().err