feat(propose): Arm C cuts an over-long span at a paragraph boundary

Arm C is NOT defined in docs/2026-09-02-k3-k4-k5-metode.md -- that file
contains no occurrence of the word, and neither Arm A nor Arm B is defined
there either. The definition implemented here was written for order
20260904T145630Z and is reported as the author's, never as a ratified one.

    Arm C = Arm B's mechanical rules, plus one deterministic rule that cuts
    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 PDF with no outline (Bilag 9.1, 217 472 characters) and a PDF
whose headings are its 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.

`--max-segment-chars` defaults to 0, which is OFF: the artifact is then
byte-identical to Arm B's, pinned by a test that writes both and compares
bytes. The standard profile does not move, and the K2 bundle a consumer is
running against right now is not rebuilt.

What Arm C deliberately does NOT change: the region before the first
candidate is still covered by no segment. That is a real coverage defect --
the K3 baseline's blind rater named it -- and fixing it here would put two
changes behind one measurement.

A part carries TWO rule names in `derived`: the heading rule that opened the
span, and `rule:size-split` for the cut. Dropping the first would make a
part traceable to arithmetic and nothing else.

Tests first: 13 red, then green. 1055 -> 1068.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 17:06:58 +02:00
commit 0c7a485c11
2 changed files with 340 additions and 4 deletions

View file

@ -325,3 +325,189 @@ def test_nothing_to_propose_is_not_reported_as_a_failure(
== 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