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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-21 08:26:20 +02:00
commit f7cd84c5e6
4 changed files with 235 additions and 9 deletions

View file

@ -354,6 +354,73 @@ class Concept:
#: `True` when the concept carries a `parent` key that resolves to no
#: concept of its own document -- the third state, as for `sources`.
parent_unresolved: bool = False
#: The title the FILE carries, set only when `title` was inherited from
#: the heading the concept stands under (:func:`inherit_table_titles`).
own_title: str | None = None
#: The name the proposer gives a table block that has no heading of its own
#: (`propose`, `rule:table-block`): the line the block starts on. A position,
#: not a name -- held against the proposer's output by a test, so the two
#: cannot drift apart.
MECHANICAL_TITLE = re.compile(r"Tabell linje \d+")
_FIRST_NUMBER = re.compile(r"\s*\[\s*(\d+)")
#: The locators a concept's place in its document is read off, in order of
#: preference. One key per DOCUMENT, never mixed: an offset and a line number
#: are not on one scale.
_POSITION_KEYS = ("source_offset", "source_lines")
def _position(concept: Concept, key: str) -> int | None:
match = _FIRST_NUMBER.match(concept.locators.get(key, ""))
return int(match.group(1)) if match else None
def inherit_table_titles(concepts: Sequence[Concept]) -> list[Concept]:
"""Every concept, a table fragment with no name of its own read under the
heading it stands under (v1.1 C3).
`Tabell linje <n>` is a line number: searched, it matches no question, and
shown, it tells a reader nothing about what the table is. So such a concept
takes the title of the nearest concept ABOVE it in the same source
document -- in reading order, the heading the table stands under -- and
keeps its own in `own_title`, so the name shown is never mistaken for the
one in the file. A table with nothing named above it keeps its own title,
and a document whose concepts do not all carry one position key is left
alone rather than ordered by a guess.
A READING, never a write: no bundle byte moves, and the ranking, the near
misses and the excerpt all see the same title because they all read it
from here.
"""
by_source: dict[str, list[int]] = {}
for index, concept in enumerate(concepts):
if concept.source_file:
by_source.setdefault(concept.source_file, []).append(index)
out = list(concepts)
for indices in by_source.values():
if not any(MECHANICAL_TITLE.fullmatch(concepts[index].title) for index in indices):
continue
key = next(
(
key
for key in _POSITION_KEYS
if all(_position(concepts[index], key) is not None for index in indices)
),
None,
)
if key is None:
continue
heading: str | None = None
for index in sorted(indices, key=lambda each: (_position(concepts[each], key), each)):
concept = concepts[index]
if not MECHANICAL_TITLE.fullmatch(concept.title):
heading = concept.title
elif heading is not None:
out[index] = replace(concept, title=heading, own_title=concept.title)
return out
#: The key a concept names its enclosing section under, and the key that
@ -1828,6 +1895,8 @@ def excerpt_for(concept: Concept) -> dict[str, object] | None:
"bundle_id_inherited": concept.bundle_id_inherited,
"title": concept.title,
}
if concept.own_title is not None:
excerpt["own_title"] = concept.own_title
if concept.req_number:
excerpt["req_number"] = concept.req_number
if concept.sources:
@ -2429,15 +2498,17 @@ def _load(
)
root_bundle_id = root_bundle_id_of(bundle_root, profile=profile)
concept_ids = enumerate_concepts(bundle_root, profile=profile)
concepts = link_parents(
[
read_concept(
read_path_in_bundle(bundle_root, f"{concept_id}{profile.paths.concept_suffix}"),
bundle_root=bundle_root,
root_bundle_id=root_bundle_id,
)
for concept_id in concept_ids
]
concepts = inherit_table_titles(
link_parents(
[
read_concept(
read_path_in_bundle(bundle_root, f"{concept_id}{profile.paths.concept_suffix}"),
bundle_root=bundle_root,
root_bundle_id=root_bundle_id,
)
for concept_id in concept_ids
]
)
)
# The text the two lexical signals read, tokenised ONCE: the stem
# vocabulary, the rarity `df` and the coverage report below all count over