feat(propose): Arm D reads a document's own numbered outline
This commit is contained in:
parent
c97e250f69
commit
47ae1ed7e3
2 changed files with 277 additions and 0 deletions
|
|
@ -80,11 +80,20 @@ RULE_POPPLER_SIZE_AND_BOLD = "rule:poppler-size-and-bold"
|
|||
#: begins. It is emitted ALONGSIDE the rule that proposed the origin span, so
|
||||
#: an operator reading a part can still see what opened it.
|
||||
RULE_SIZE_SPLIT = "rule:size-split"
|
||||
#: Arm D only. Like Arm C it is NOT one of Topic 2's ported rules and NOT
|
||||
#: defined upstream: `docs/2026-09-02-k3-k4-k5-metode.md` contains no
|
||||
#: occurrence of the word "arm" at all, so this definition was written for the
|
||||
#: brief of order 20260906T213322Z and is reported as the author's. Unlike Arm
|
||||
#: C it says nothing about size -- it names the fact that the DOCUMENT ITSELF
|
||||
#: declared a chapter there, by numbering it in an ascending run its own
|
||||
#: outline sustains.
|
||||
RULE_OUTLINE = "rule:outline"
|
||||
RULE_NAMES = (
|
||||
RULE_HEADING,
|
||||
RULE_TABLE_BLOCK,
|
||||
RULE_POPPLER_SIZE_AND_BOLD,
|
||||
RULE_SIZE_SPLIT,
|
||||
RULE_OUTLINE,
|
||||
)
|
||||
|
||||
#: How many characters of context each side of a quote anchor carries. Enough
|
||||
|
|
@ -140,9 +149,29 @@ STOP_WORDS = frozenset(
|
|||
# A BARE integer is not a section number, for the same reason `structure.py`
|
||||
# refuses one: `12 ting` is an ordinary line and admitting it would cut a
|
||||
# document at every list item.
|
||||
#
|
||||
# That claim still holds, and Arm D does not weaken it. `_OUTLINE` below admits
|
||||
# a bare integer ONLY inside an ascending run the document sustains for at
|
||||
# least a declared length -- which is a property of the whole text, not of the
|
||||
# line -- and the rule is off unless a caller asks for it. An UNGATED widening
|
||||
# was measured and rejected: 1681 raw hits against 618 candidates, admitting
|
||||
# list items, quantities and page furniture. The gate is what makes the signal
|
||||
# a signal.
|
||||
_ATX = re.compile(r"^(?P<hashes>#{1,6})\s+(?P<title>\S.*?)\s*$")
|
||||
_NUMBERED = re.compile(r"^(?P<number>\d+(?:\.\d+)+)\s+(?P<title>\S.*?)\s*$")
|
||||
_TABLE_ROW = re.compile(r"^\s*\|.*\|\s*$")
|
||||
# Arm D's grammar. Integer-only BY CONSTRUCTION: `\s+` after the optional
|
||||
# separator is what keeps `1.1 Brannkonsept` out, because `_NUMBERED` requires
|
||||
# a dot and this requires whitespace, so no line can match both. No exclusion
|
||||
# clause is written for that: a filter with a measured effect of zero is dead
|
||||
# code that reads like a guard.
|
||||
_OUTLINE = re.compile(r"^\s{0,4}(?P<number>\d{1,2})[.)]?\s+(?P<title>\S.*?)\s*$")
|
||||
# A contents line carries the page it points at (`Innledning 6`). Measured on
|
||||
# the K2 corpus: stripping it changes 0 of the 144 outline counts and 9 emitted
|
||||
# titles. It is load-bearing anyway, because titles become concept paths
|
||||
# through `_segment_path` -- an unstripped page number would become part of a
|
||||
# filename.
|
||||
_TRAILING_PAGE_NUMBER = re.compile(r"[\s.]+\d{1,4}\s*$")
|
||||
|
||||
|
||||
class ProposerError(Exception):
|
||||
|
|
@ -170,6 +199,66 @@ def _is_stop_word_only(title: str) -> bool:
|
|||
return bool(words) and all(word in STOP_WORDS for word in words)
|
||||
|
||||
|
||||
def _strip_page_number(title: str) -> str:
|
||||
"""Remove a trailing page number from a contents-listing title.
|
||||
|
||||
Deliberately NOT applied to a title that is only digits: `477` has no
|
||||
separator before the number, so the pattern cannot match it and the title
|
||||
survives for the stop-word and junk paths to see. Emptying it would fall
|
||||
back to the `seksjon` stem and dress junk as a named section.
|
||||
"""
|
||||
return _TRAILING_PAGE_NUMBER.sub("", title)
|
||||
|
||||
|
||||
def outline_lines(text: str) -> list[tuple[int, int, str]]:
|
||||
"""Every line the outline grammar admits, as `(line index, integer, title)`.
|
||||
|
||||
Module level and importable on purpose: the reach instrument measures this
|
||||
rule, and an instrument that re-implements the grammar it measures is
|
||||
measuring a second definition that can silently drift from the shipped one.
|
||||
"""
|
||||
found: list[tuple[int, int, str]] = []
|
||||
for index, line in enumerate(text.splitlines()):
|
||||
match = _OUTLINE.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
title = _strip_page_number(match.group("title")).strip()
|
||||
if not title or _is_stop_word_only(title):
|
||||
continue
|
||||
found.append((index, int(match.group("number")), title))
|
||||
return found
|
||||
|
||||
|
||||
def outline_runs(
|
||||
entries: list[tuple[int, int, str]], minimum: int
|
||||
) -> list[list[tuple[int, int, str]]]:
|
||||
"""The maximal ascending runs among `entries`, each at least `minimum` long.
|
||||
|
||||
A run is anchored at `1` and every later member is its predecessor plus
|
||||
one; a number that is neither is skipped without closing the run, so a
|
||||
stray page number between two chapters does not truncate the outline. A new
|
||||
`1` closes the current run and opens the next, which is what makes a
|
||||
contents listing and the body it lists two runs rather than one.
|
||||
|
||||
Returned in document order. The CALLER chooses among them -- last-run
|
||||
selection was measured against the alternatives and is stated where it is
|
||||
applied, not hidden in here.
|
||||
"""
|
||||
runs: list[list[tuple[int, int, str]]] = []
|
||||
current: list[tuple[int, int, str]] = []
|
||||
for entry in entries:
|
||||
number = entry[1]
|
||||
if number == 1:
|
||||
if current:
|
||||
runs.append(current)
|
||||
current = [entry]
|
||||
elif current and number == current[-1][1] + 1:
|
||||
current.append(entry)
|
||||
if current:
|
||||
runs.append(current)
|
||||
return [run for run in runs if len(run) >= minimum]
|
||||
|
||||
|
||||
def find_candidates(text: str) -> list[Candidate]:
|
||||
"""Every boundary the mechanical rules propose, in document order.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue