"""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, "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 | +-------+-------+ """ 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