feat(propose,consume,tools): the type that declares nothing, and the prefix that is not a word

Three of round 9's four measured holes, each closed with a rule chosen on a
measurement rather than named as a limit.

`rtf` GIVES 0 SEGMENTS -> 6 of 6 AUTHORED TITLES over N = 4. The container has
no heading style, so the author's title is bold text. The grammar is markdown,
not `rtf`: the converter already writes that title as `**...**` in the same
output every office row produces, so no `rtf`-only heading form exists. Three
parameters were swept over 47 readable documents and ONE carried -- refusing a
line that ends in terminal punctuation takes false-positive lines from 9-12 to
1-2. A maximum title length (unlimited/40/60/80/120) and a
must-stand-between-blank-lines clause are both FLAT, so neither is in the rule.
The last false positive is closed by G1, the principle `_gate_outline` already
carries: recovery yields to declaration. False positives are then 0 of the 31
declaring documents by construction, and 0 of 27 on the corpus. Reach: 2 of 39
corpus documents, both `docx`, 0 of 33 `pdf` and 0 of 2 `xlsx`. Behind
`--bold-title`, default OFF pending the hit@8 measurement; the default bundle
is byte-identical without it.

BOTH ALTERNATIVES THE ORDER NAMED WERE MEASURED AND FELLED. A fourth hand-laid
fixture DECLARES heading styles in a stylesheet and the converter discards
them, emitting the same bold line -- so "read the declared headings out of the
markdown" has nothing to read. `rtf` -> `docx` -> markdown yields 0 ATX
headings on that same document, because the loss is in the `rtf` READER before
any writer sees the style. Fixtures are hand-laid in `make_k2_office.py` with
the fasit written first; they live in their own directory because Door B walks
a drop directory recursively and `k2-office/` reads its N off the listing.

THE PREFIX OVER-MATCH: THREE CANDIDATES MEASURED, ALL THREE FAILED ON ONE ROW.
Re-measured on the pinned 453-concept bundle with the control run first:
`under` occurs 79 times by equality and matches 172 by prefix, `undersjoisk` 0
and 172, `bilateral` 0 and 400 of 453, `standhaftig` 0 and 219. The two extra
known-negatives were FOUND, not chosen -- every 4-character prefix ranked by
document frequency, then a real word taken from the widest. A longer floor
(5-8), a coverage share (0.5-0.8) and a long-words-only floor (>= 8) each cost
row 1 its rank on the default bundle and the whole row on Arm B. Decomposed:
row 1's token `prisene` reaches its gold document through
`pris|sammenstilling` on four characters -- 0.57 of one word and 0.22 of the
other -- so the over-match and the wanted match are one mechanism.

THE FOURTH CANDIDATE IS THE ANSWER: the shared prefix must be a WORD the bundle
uses. `pris` is; `bila` and `stan` are not. `bilateral` 400 -> 0 and 512 -> 0,
`standhaftig` 219 -> 56 and 235 -> 33, every hit@8 row keeping rank 1 on BOTH
bundles. `undersjoisk` stops at 162 because `under` IS a word here -- a genuine
Norwegian morpheme, so that residual is a different answer, not a ceiling. ON
by default (`--no-stem-prefix`), pinned with its own known-negative on the
shipped bytes.

THE SHIM: a path importer holds the object `module_from_spec` made, and
`sys.modules[__name__] = _impl` never reaches it. Measured under both counting
methods -- 3 of 76 public names by `vars()`. One line copies the public names
into this file's globals; the dunder filter is load-bearing, because an
unfiltered copy overwrites `__name__` before the next line uses it as the alias
key. It restores attribute ACCESS and not patch-through, which is why the alias
stays. A CHANGELOG note under 0.7.0 and a shim docstring line say so, since
what the consumer asked for was the note.

Suite 1515 -> 1535.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-09 23:05:45 +02:00
commit 191de89f41
16 changed files with 1100 additions and 22 deletions

242
tests/test_bold_title.py Normal file
View file

@ -0,0 +1,242 @@
"""A bold standalone line is a title where the document declares nothing else.
THE HOLE THIS CLOSES. Round 9 met `rtf` end to end for the first time and the
row came back at 0 of 0 declared headings, 0 concepts, 1368 of 1368 characters
in no segment, N = 1: the container has no heading style, so the author's title
is bold text, the proposer proposes nothing, and the document reaches Door B's
inbox as one flat concept. Content preserved, structure zero.
WHAT THE GRAMMAR READS, AND WHERE IT LIVES. Not `rtf` markup. The converter
already emits that bold title as `**Kravspesifikasjon for tunnelbelysning**` in
the SAME markdown the other office rows produce, so this rule is a markdown
rule and no `rtf`-only grammar exists -- the same shape of decision as the PDF
font reader emitting ATX rather than a PDF-only heading form. Bold INSIDE a
paragraph is `text **bold** text` and is not a whole line, which is the
discriminator the rule rests on.
THREE PARAMETERS WERE SWEPT AND ONE CARRIES. Over 47 readable documents (3
hand-built office fixtures, 5 in the operator's folder, 39 of the 43 corpus
files), with the false-positive denominator measured FIRST:
* a maximum title length (unlimited / 40 / 60 / 80 / 120 characters) is
FLAT -- identical counts at every value;
* requiring the line to stand alone between blank lines is FLAT -- the
converter already writes one paragraph per line;
* refusing a line that ends in terminal punctuation carries: false-positive
lines fall 9-12 to 1-2, and the four candidates it drops are
`er inngatt mellom:`, `Sted og dato:` and their kind -- sentence fragments
a contract's cover page sets in bold, never section names.
A parameter measured flat buys nothing, so neither of the two is in the rule.
THE GATE IS G1, ALREADY SHIPPED. One false positive survived the punctuation
clause: a corpus `docx` that DECLARES two headings of its own and sets one
later line (`EGENERKLAERING`) in bold. The repair is not a fourth parameter --
it is the principle `_gate_outline` and `fold_units` clause 2 already carry,
that recovery yields to declaration. A bold-title candidate is admitted only
where the document declares no heading of its own, which takes the
false-positive count to 0 of 31 declaring documents BY CONSTRUCTION.
WHAT WAS MEASURED AND NOT ADDED. `heading_reserve_applies` yields to an
admitted Arm D run as well as to a declaration, and that axis was measured
here before being left out: over all 4 documents that reach this rule, the
outline gate admits 0 runs and the reserve applies on every one. n = 4, which
is small; the axis is flat over it, so coupling this rule to `outline_run`
would add a knob no measurement asks for.
HONESTY LIMIT: the `rtf` row rests on hand-built documents, not corpus files.
The corpus contains ZERO `rtf`. Its evidence class stays `constructed`.
"""
from __future__ import annotations
import json
from pathlib import Path
from llm_ingestion_okf import cli
from llm_ingestion_okf.extract import extract_text
from llm_ingestion_okf.propose import RULE_BOLD_TITLE, RULE_HEADING, find_candidates
#: Variant A stays with the three-container set; round 10's three variants live
#: in their own directory, because `test_k2_office_fixtures.py` reads N off that
#: one by listing it and Door B walks a drop directory recursively.
FIXTURES = Path(__file__).parent / "fixtures" / "k2-office"
RTF_VARIANTS = Path(__file__).parent / "fixtures" / "k2-rtf-variants"
FASIT_PATH = Path(__file__).parent / "fixtures" / "k2-office-fasit.json"
DEFAULT = dict(
outline_run=cli.DEFAULT_OUTLINE_RUN,
table_grid=cli.DEFAULT_TABLE_GRID,
unit_fold=cli.DEFAULT_UNIT_FOLD,
keep_table_heading=cli.DEFAULT_KEEP_TABLE_HEADING,
sheet_section_rows=cli.DEFAULT_SHEET_SECTION_ROWS,
drop_wrapped_outline=cli.DEFAULT_DROP_WRAPPED_OUTLINE,
outline_gate=cli.DEFAULT_OUTLINE_GATE,
first_span_from_zero=cli.DEFAULT_FIRST_SPAN_FROM_ZERO,
close_span_gaps=cli.DEFAULT_CLOSE_SPAN_GAPS,
contents_name=cli.DEFAULT_CONTENTS_NAME,
)
#: The shape the converter hands back for the `rtf` fixture: a bold title, a
#: prose paragraph, then a grid table of label/value pairs.
BOLD_TITLED = """**Kravspesifikasjon for tunnelbelysning**
Dokumentet samler kravene til belysning i vegtunneler over 500 meter.
----------------------- ------------------------------
Dokumentnummer: SVV-2026-0417
Tittel: Tunnelbelysning i hovedlopet
----------------------- ------------------------------
**Luminansmatrise**
Matrisen gjelder per trafikklasse og er bindende.
"""
#: The known-negative for the whole-line clause. `**fet**` here is a phrase
#: inside a running sentence, which is what a bold RUN looks like and what the
#: rule must never read as a boundary.
BOLD_INSIDE_A_PARAGRAPH = """Dokumentet samler kravene, og et **fremhevet** ord
staar midt i en setning som fortsetter forbi det.
Neste avsnitt naevner **ogsaa** noe fremhevet i loepende tekst.
"""
#: The false-positive denominator in miniature: a document that declares its
#: own heading AND sets a later line in bold. The bold line must not become a
#: boundary here -- recovery yields to declaration.
DECLARES_AND_ALSO_BOLDS = """# Konkurransegrunnlag
Innledende avsnitt som beskriver konkurransen og dens omfang.
**EGENERKLAERING**
Leverandoren bekrefter at opplysningene i skjemaet er riktige.
"""
#: The punctuation clause's own material: a contract cover page whose bold
#: lines are sentence fragments, not section names.
BOLD_SENTENCE_FRAGMENTS = """**er inngaatt mellom:**
Byggherren og leverandoren, representert ved sine kontaktpersoner.
**Sted og dato:**
Oslo, 1. mars 2026, med signatur fra begge parter i to eksemplarer.
"""
def _fixture(name: str) -> Path:
"""Variant A in the three-container set, the three variants beside it."""
return FIXTURES / name if (FIXTURES / name).is_file() else RTF_VARIANTS / name
def _titles(text: str, **overrides: object) -> list[str]:
keywords = {**DEFAULT, **overrides}
return [c.title for c in find_candidates(text, **keywords) if c.rule == RULE_BOLD_TITLE]
def test_a_bold_standalone_line_becomes_a_title_candidate() -> None:
"""The `rtf` row's whole finding: the title the container never declared."""
assert _titles(BOLD_TITLED, bold_title=True) == [
"Kravspesifikasjon for tunnelbelysning",
"Luminansmatrise",
]
def test_bold_inside_a_paragraph_is_not_a_title_candidate() -> None:
"""The known-negative for the whole-line clause, in the same file."""
assert _titles(BOLD_INSIDE_A_PARAGRAPH, bold_title=True) == []
def test_a_declared_heading_closes_the_rule_for_the_whole_document() -> None:
"""G1: recovery yields to declaration, so the false positive cannot occur.
The document's own `# Konkurransegrunnlag` survives; the bold line below it
proposes nothing. This is the single false positive measured over the 47
documents, reduced to its mechanism.
"""
assert _titles(DECLARES_AND_ALSO_BOLDS, bold_title=True) == []
declared = [
c.title
for c in find_candidates(DECLARES_AND_ALSO_BOLDS, **{**DEFAULT, "bold_title": True})
if c.rule == RULE_HEADING
]
assert declared == ["Konkurransegrunnlag"], "the gate must not touch the declared heading"
def test_a_bold_line_ending_in_terminal_punctuation_is_not_a_title() -> None:
"""The one swept parameter that carried, pinned with its own material."""
assert _titles(BOLD_SENTENCE_FRAGMENTS, bold_title=True) == []
def test_the_rule_proposes_nothing_when_the_flag_is_off() -> None:
"""The opt-out reproduces the pre-rule bytes on the rule's own material."""
assert _titles(BOLD_TITLED, bold_title=False) == []
assert find_candidates(BOLD_TITLED, **{**DEFAULT, "bold_title": False}) == find_candidates(
BOLD_TITLED, **DEFAULT
)
def test_the_rtf_row_recovers_every_authored_title_over_four_documents() -> None:
"""The row this round exists to move, pinned against the hand-written fasit.
Round 9: 0 of 0 declared headings, 0 concepts, 1368 of 1368 characters in
no segment, N = 1. Here: 6 of 6 AUTHORED titles over N = 4, 0 false bold
titles, 0 of 1994 characters in no segment.
`authored_titles` is deliberately not a count of what the container
declares. `rtf` has no heading style, so a "declared heading" count for
this type is 0 by construction and says nothing about whether the document
has sections. The fasit counts what a reader of the SOURCE would call a
section name, hand-written before this measurement ran.
The fourth document is a control that failed informatively: it DECLARES
`\\s1` and `\\s2` in a stylesheet, and the vendored converter discards the
style and emits the same bold line. "Read the declared style" is therefore
not a route that exists for this container -- which is why the rule reads
markdown and not `rtf`.
"""
fasit = json.loads(FASIT_PATH.read_text(encoding="utf-8"))["rtf_variants"]
recovered = authored = false_titles = unsegmented = total = 0
for name, want in sorted(fasit["documents"].items()):
path = _fixture(name)
text = extract_text(path.name, path.read_bytes())
found = find_candidates(text, **{**DEFAULT, "bold_title": True})
titles = [c.title for c in found]
authored += want["authored_titles"]
recovered += sum(1 for t in want["titles"] if t in titles)
false_titles += sum(
1 for c in found if c.rule == RULE_BOLD_TITLE and c.title not in want["titles"]
)
total += len(text)
unsegmented += len(text) - sum(c.end - c.start for c in found)
assert (recovered, authored) == (6, 6), "every authored title, over four containers"
assert false_titles == 0, "the bold RUN inside a sentence must not become a title"
assert (unsegmented, total) == (0, 1994), "no character left outside a segment"
def test_the_row_is_zero_without_the_rule_which_is_round_nine_reproduced() -> None:
"""The known-negative for the row: turn the rule off and the finding returns."""
fasit = json.loads(FASIT_PATH.read_text(encoding="utf-8"))["rtf_variants"]
for name in sorted(fasit["documents"]):
path = _fixture(name)
text = extract_text(path.name, path.read_bytes())
found = find_candidates(text, **{**DEFAULT, "bold_title": False})
assert found == [], f"{name} proposes nothing without the rule"
def test_the_container_declaring_a_heading_style_still_reaches_us_as_bold() -> None:
"""The measurement that felled both alternatives the order named.
Alternative (i) was "read the declared headings out of the converter's
markdown". There are none: this document declares `\\s1`/`\\s2` and the
converter emits `**...**`. Alternative (ii) was "convert `rtf` to `docx`
and let the `docx` grammar work" -- measured separately, that yields 0 ATX
headings on this same document, because the loss happens in the `rtf`
READER before any writer sees the style.
"""
path = _fixture("krav-rikt-tekstformat-stil.rtf")
text = extract_text(path.name, path.read_bytes())
assert "**Kravspesifikasjon for tunnelbelysning**" in text
assert [line for line in text.splitlines() if line.startswith("#")] == []