llm-ingestion-okf/tests/test_outline_measure.py
Kjell Tore Guttormsen 36c201cc8a chore(ruff): the acceptance was whatever the default happened to be [skip-docs]
`uv sync --frozen` resolved ruff 0.15.22 and the tree read clean. A loose
install resolves 0.16.6, under which the SAME untouched code reports 148
findings -- 4 more than round 9 counted, because this round added four files.
All of them are new rules rather than new defects: 0.16 widened the default
rule set to whole families (YTT, ASYNC, PL, ISC, C4, UP, B, SIM, FURB, ...).

(`[skip-docs]` is for CLAUDE.md, which a lint-configuration change does not
reach. README's developer section IS updated in this commit.)

THE DEFECT IS NOT THE 148, IT IS THAT NOBODY CHOSE THEM. `[tool.ruff]` set only
`line-length` and `target-version`, so the acceptance was ruff's default, and
the tree stayed green only as long as the lockfile froze an old ruff. `select`
is now written down: `E4`, `E7`, `E9`, `F` (the historical default), `I`
because this tree already keeps imports sorted, and `RUF100` so a `noqa` that
has stopped meaning anything is caught rather than left as decoration. Pin
`ruff>=0.9` -> `ruff>=0.16.6,<0.17`.

Per rule, before -> after: RUF100 50 -> 0, I001 20 -> 0, ISC004 19, PLW1510 8,
C408 8, EXE001 6, RUF007 5, PLE2515 4, UP031 3, B017 3, and fourteen more with
2 or fewer -- the families out of the declared set are 0 by selection, and 148
is the number to start from if they are adopted, which is a separate decision
and not one to take inside a version-pin commit. 57 were auto-fixed; one E402
was reintroduced by the import-sorting fix merging a block away from its
`noqa`, and got the directive back rather than a bare one.

`S` IS MEASURED OUT, NOT ASSUMED OUT: it reports 2657 `S101` on a suite whose
every assertion is an `assert`, and `S603` flags 19 subprocess calls of which
one was ever marked -- selecting it buys 18 suppressions and no defect. Two
`noqa` directives naming non-selected rules were dropped with that reason
recorded in the configuration instead.

THE TWO FILES 0.16 WOULD REFORMAT ARE MARKDOWN, NOT PYTHON: `README.md` and
`docs/2026-09-08-blindsone-below-k-k2.md`. 0.16 formats fenced Python inside
markdown, and both blocks are RECORDS -- the second is a quotation of
`COST_VOCABULARY` as it stood when that measurement was taken. Reformatting a
quotation makes it stop being one, so markdown is excluded from the formatter
and `ruff format --check .` stays in the acceptance over `.py`.

`tools/okf_consume_measure.py` is fenced by the order as run-not-edited, so its
three findings are exempted by path with the reason and the debt named, and its
bytes are untouched.

THE LOCKFILE TRAP IS CLOSED, NOT AVOIDED. `uv.lock` predated the `[ocr]` extra,
so any unlocked resolve wrote that extra's transitive tree back into it -- 681
insertions over 4 deletions, twice now, and round 9 recorded the cause as
`uv run` OUTSIDE the project when it is `uv run` without `--frozen` INSIDE it.
The relock is complete for every declared extra (703 insertions, 26 deletions),
and measured after it, an unfrozen `uv run` leaves the file alone.

`ruff check src tests tools`, `ruff format --check .` (0.16.6), `mypy src` over
21 files and 1535 tests, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 23:15:17 +02:00

153 lines
6.3 KiB
Python

"""The outline-reach instrument: phase 2 of order 20260906T213322Z.
Measures how far Arm D's outline rule reaches into a corpus, before and after
the orphan check that deletes a third of what it proposes. The instrument
exists because `docs/2026-09-04-k3-arm-c.md` published figures a reader could
not re-derive: a number without a committed script cannot be reproduced, and a
K3 row resting on one is an assertion rather than a measurement.
The identity test below is the one that matters most. An instrument that
re-implements the grammar it measures is measuring a SECOND definition, free to
drift from the shipped one without a single test going red -- so the instrument
imports `outline_lines`, `outline_runs` and `find_candidates` from the tool,
and this file pins that they are the same objects.
The negative control matters as much as the positive one, for the reason
`test_cid_measure.py` states: an instrument reporting reach on a corpus that
has none would inflate every number it ever produces. Here the control is
sharper than "zero" -- it is zero WITH a nonzero denominator, because "found
nothing" and "measured nothing" are different results and only one of them is
evidence.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
import okf_outline_measure
from llm_ingestion_okf import propose as okf_propose_segments
OUTLINE_DOC = """1 Innledning
Bakgrunn for prosjektet og omfanget.
2 Krav
Krav til seksjonering av bygget.
3 Gjennomfoering
Framdrift, faser og overlevering.
"""
NO_OUTLINE_DOC = """# Teknisk grunnlag
Innledende tekst uten nummerering i det hele tatt.
## 3.1 Brannkonsept
To uavhengige roemningsveier fra hver branncelle.
"""
def test_the_instrument_and_the_tool_are_one_definition() -> None:
"""Identity, not equality: the cheapest proof there is no second grammar."""
assert okf_outline_measure.outline_runs is okf_propose_segments.outline_runs
assert okf_outline_measure.outline_lines is okf_propose_segments.outline_lines
assert okf_outline_measure.find_candidates is okf_propose_segments.find_candidates
def test_a_hand_computed_document_reports_its_boundaries_both_sides_of_the_gate() -> None:
"""Three chapters, each with a body, so the gate deletes none of them."""
result = okf_outline_measure.measure_document(OUTLINE_DOC, "doc", run_length=3)
assert result.pre_gate_boundaries == 3
assert result.post_gate_boundaries == 3
assert result.arm_b_entries == 0
assert result.arm_d_entries == 3
assert result.deleted_arm_b == 0
def test_the_gate_deletes_an_arm_b_heading_the_outline_immediately_follows() -> None:
"""The 34 %-deletion mechanism, measured on a document small enough to check.
`# Teknisk grunnlag` is followed immediately by `1 Innledning`, so its body
is empty and the orphan check drops it. Arm B had one entry; Arm D has
three, and the one Arm B had is gone.
"""
text = "# Teknisk grunnlag\n\n1 Innledning\n\nA.\n\n2 Krav\n\nB.\n\n3 Slutt\n\nC.\n"
result = okf_outline_measure.measure_document(text, "doc", run_length=3)
assert result.arm_b_entries == 1
assert result.deleted_arm_b == 1
assert result.post_gate_boundaries == 3
def test_a_corpus_with_no_outline_reports_zero_with_a_nonzero_denominator() -> None:
"""The negative control. Zero reach is only evidence if something was measured."""
result = okf_outline_measure.measure_document(NO_OUTLINE_DOC, "doc", run_length=3)
assert result.pre_gate_boundaries == 0
assert result.post_gate_boundaries == 0
# The denominator: the document WAS measured, and Arm B did find boundaries
# in it, so a zero here is the rule declining rather than the probe failing.
assert result.arm_b_entries > 0
assert result.arm_d_entries == result.arm_b_entries
def test_an_empty_document_does_not_crash() -> None:
result = okf_outline_measure.measure_document("", "doc", run_length=3)
assert result.pre_gate_boundaries == 0
assert result.arm_b_entries == 0
assert result.arm_d_entries == 0
assert result.unique_paths == 0
def test_the_sample_draw_reproduces_a_known_ordering() -> None:
"""The draw is the method's, re-derived: hex SHA-256 of the NFC filename.
The expected list is computed by hand from the digests, not by calling the
function under test -- otherwise the assertion would only prove the code
agrees with itself.
"""
names = [
"alfa.pdf",
"beta.pdf",
"gamma.pdf",
"delta.docx",
"epsilon.docx",
"zeta.xlsx",
"eta.xlsx",
"theta.pdf",
]
drawn = okf_outline_measure.draw_sample(names, {"pdf": 2, "docx": 1, "xlsx": 1})
assert drawn == ["theta.pdf", "gamma.pdf", "eta.xlsx", "delta.docx"]
def test_the_draw_normalises_to_nfc_before_hashing() -> None:
"""macOS hands filenames over decomposed; the two forms hash differently.
Without this the same visual corpus would draw a different sample depending
on which normalisation the filenames arrived in -- the same defect the
library already fixed in `reduce_to_id_grammar`.
"""
# Escapes, not literals: a source file is stored in ONE normalisation, so
# writing both forms as literals would silently make them the same string
# and the control below would be green for the wrong reason. `oe` is used
# because U+00F8 has no canonical decomposition at all -- picking it would
# make this test vacuous in a second, quieter way.
composed = "caf\u00e9.pdf" # NFC: e-acute as one code point
decomposed = "cafe\u0301.pdf" # NFD: `e` plus combining acute
assert composed != decomposed # the control: they really are different strings
assert okf_outline_measure._draw_key(composed) == okf_outline_measure._draw_key(decomposed)
# And a second control: two names that are genuinely different still differ.
assert okf_outline_measure._draw_key("a.pdf") != okf_outline_measure._draw_key("b.pdf")
def test_a_title_with_no_alphabetic_word_is_counted_as_junk() -> None:
"""11 of the 95 surviving outline titles are junk; the report needs the count."""
text = "1 477 3 025\n\nA.\n\n2 Krav\n\nB.\n\n3 D L\n\nC.\n"
result = okf_outline_measure.measure_document(text, "doc", run_length=3)
assert result.post_gate_boundaries == 3
assert result.junk_titles == 2 # `477 3 025` and `D L` -- `Krav` is a word