llm-ingestion-okf/tests/test_table_title_inheritance.py
Kjell Tore Guttormsen f7cd84c5e6 feat(consume): a table fragment is read under the heading it stands under
C3, the remaining half. The proposer names a table block with no heading of
its own after the line it starts on (`Tabell linje <n>`). A position, not a
name: searched, it matched no question, and shown, it told a reader nothing
about what the table is.

`consume.inherit_table_titles` reads such a concept under the title of the
nearest concept ABOVE it in the same source document -- in reading order, the
heading the table stands under -- ordered by `source_offset`, else
`source_lines`, one key per document and never mixed. Applied where the bundle
is loaded, so the ranking, the excerpt and the near misses all see the same
title; the excerpt keeps the file's own as `own_title`, so the name shown is
never mistaken for the one in the file. A table with nothing named above it,
or a document whose concepts do not all carry one position key, is left as it
is.

Chose to do it in the reading and not in the build: no bundle byte moves, no
collection has to be rebuilt, and the proposer's goldens stay pinned.
`consume.MECHANICAL_TITLE` is held against the proposer's own output by a test.

Suite on a clean tree after `git add`: 2418 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean. The search gate's table for the
previous commit is kept in local state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:26:20 +02:00

144 lines
6 KiB
Python

"""A table fragment with no name of its own takes the heading above it (v1.1 C3).
The proposer names a table block that has no heading of its own after the
line it starts on (`Tabell linje <n>`, `rule:table-block`). That is a
position, not a name: searched, it matches no question, and shown, it tells a
reader nothing about what the table is. `okf consume` reads such a concept
under the heading it stands under -- the nearest concept ABOVE it in the same
source document -- both in what is ranked and in what the excerpt shows, and
the excerpt keeps the concept's own title beside it as `own_title`, so the
name shown is never mistaken for the one in the file.
Nothing in a bundle moves: the inheritance is a reading, done in `consume`.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from llm_ingestion_okf import consume, contract_check
from llm_ingestion_okf import skill as okf_skill
TOOLS = Path(__file__).resolve().parent.parent / "tools"
if str(TOOLS) not in sys.path:
sys.path.insert(0, str(TOOLS))
import okf_retrieval_gate as retrieval # noqa: E402
TABLE = "| Room | Heater |\n| --- | --- |\n| Hall | Panel |\n| Loft | Stove |"
#: (slug, title, body, first line, last line), in document order.
CABIN = (
("water", "Water", "The well is drained in autumn.", 1, 10),
("tabell-linje-11", "Tabell linje 11", TABLE, 11, 14),
("heating", "Heating", "The cabin is kept warm through the winter.", 15, 20),
("tabell-linje-21", "Tabell linje 21", TABLE, 21, 24),
)
#: A document whose FIRST concept is a table: nothing stands above it.
LEDGER = (("tabell-linje-1", "Tabell linje 1", TABLE, 1, 4),)
def _write(root: Path) -> Path:
spec = retrieval.BundleSpec(
"table-titles",
tuple(
retrieval.DocumentSpec(
name,
f"{name}.md",
tuple(
retrieval.ConceptSpec(slug=slug, title=title, body=body)
for slug, title, body, _, _ in rows
),
)
for name, rows in (("cabin", CABIN), ("ledger", LEDGER))
),
)
bundle = retrieval.build_bundle(root, spec)
for name, rows in (("cabin", CABIN), ("ledger", LEDGER)):
for slug, title, body, first, last in rows:
path = bundle / name / f"{slug}.md"
text = path.read_text(encoding="utf-8")
text = text.replace(
f"source_file: {name}.md\n",
f"source_file: {name}.md\nsource_lines: [{first}, {last}]\n",
)
if title.startswith("Tabell linje"):
# A real table block carries no heading line of its own.
text = text.replace(f"## {title}\n\n", "")
path.write_text(text, encoding="utf-8")
return bundle
@pytest.fixture(scope="module")
def bundle(tmp_path_factory: pytest.TempPathFactory) -> Path:
return _write(tmp_path_factory.mktemp("table-titles") / "bundle")
def _excerpts(bundle: Path, question: str) -> dict[str, dict[str, object]]:
payload = consume.build_payload(bundle, question=question, k=8, source_quota=None)
excerpts = payload["excerpts"]
assert isinstance(excerpts, list)
return {str(excerpt["concept_id"]): excerpt for excerpt in excerpts}
def test_a_table_fragment_shows_the_heading_it_stands_under(bundle: Path) -> None:
excerpts = _excerpts(bundle, "room heater")
heating_table = excerpts["cabin/tabell-linje-21"]
water_table = excerpts["cabin/tabell-linje-11"]
assert heating_table["title"] == "Heating"
assert heating_table["own_title"] == "Tabell linje 21"
assert water_table["title"] == "Water"
assert water_table["own_title"] == "Tabell linje 11"
def test_a_concept_with_a_name_of_its_own_is_untouched(bundle: Path) -> None:
excerpt = _excerpts(bundle, "cabin warm winter")["cabin/heating"]
assert excerpt["title"] == "Heating"
assert "own_title" not in excerpt
def test_a_table_with_nothing_above_it_keeps_its_own_title(bundle: Path) -> None:
excerpt = _excerpts(bundle, "room heater")["ledger/tabell-linje-1"]
assert excerpt["title"] == "Tabell linje 1"
assert "own_title" not in excerpt
def test_the_inherited_heading_is_searched(bundle: Path) -> None:
"""Two identical tables; the question names the heading ONE stands under.
Without the inheritance they tie and the id decides, which puts the table
under `Water` first (`tabell-linje-11` sorts before `tabell-linje-21`).
"""
payload = consume.build_payload(bundle, question="heating room", k=8, source_quota=None)
order = [str(excerpt["concept_id"]) for excerpt in payload["excerpts"]] # type: ignore[union-attr]
assert order.index("cabin/tabell-linje-21") < order.index("cabin/tabell-linje-11")
def test_the_near_misses_name_the_inherited_heading(bundle: Path) -> None:
payload = consume.build_payload(bundle, question="room heater", k=1, source_quota=None)
withheld = payload["withheld"]
assert isinstance(withheld, dict)
titles = {entry["concept_id"]: entry["title"] for entry in withheld["nearest"]}
assert titles.get("cabin/tabell-linje-11", "Water") == "Water"
assert titles.get("cabin/tabell-linje-21", "Heating") == "Heating"
assert "Tabell linje 11" not in titles.values()
assert "Tabell linje 21" not in titles.values()
def test_the_contract_checker_accepts_it(bundle: Path) -> None:
payload = consume.build_payload(bundle, question="room heater")
assert contract_check.check(okf_skill.render_generic(), payload).findings == ()
def test_the_mechanical_name_is_the_proposers() -> None:
"""The pattern read here is the one `propose` writes, held against its output."""
from llm_ingestion_okf import propose
text = "Intro line.\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n"
titles = [candidate.title for candidate in propose.find_candidates(text)]
table_titles = [title for title in titles if "linje" in title]
assert table_titles, "the premise: the proposer names a table block"
assert all(consume.MECHANICAL_TITLE.fullmatch(title) for title in table_titles)