Until now "run the door over a folder" was a shell loop over two scripts under `tools/`, with nine flags between them and a `--path-prefix` rule that lived in a code block in a measurement report. Neither script was packaged (`pyproject.toml` ships `src/llm_ingestion_okf` only), so the path the published K1/K2 numbers were measured on was reachable from a clone and nowhere else. `okf build <folder> --bundle <dir>` is that path, packaged, declared as a console script and installed with the wheel. It is orchestration only: the proposer and the corpus harness MOVED into the package (`llm_ingestion_okf.propose`, `llm_ingestion_okf.corpus`) and the two `tools/` scripts became thin entry points to them, so the published reproduction blocks still run and there is exactly one implementation of each rule. Neither move adds a dependency or a model call. Two decisions belong to this layer and are stated where they are made. A document's proposed paths are scoped by its RELATIVE PATH minus the extension, not its basename: the door walks recursively now, and two documents named alike in different folders would otherwise collide on a path Door B is supposed to make impossible rather than merely detect. And omitted timestamps do not come from the clock -- `--ingested-at` and `--proposed-at` default to one shared epoch constant, because a wall-clock default would put a changing byte in the artifact and take rebuild-equals-incremental away from every caller who did not pass them. Arm C and Arm D stay off and are not exposed here. Measured on the 43-file K2 corpus, one invocation against the two-script bundle of 2026-09-03: N = 43 computed, merged 39/43, coded rejections 4/43 (`extractor_unknown` 3, `extractor_empty_pdf` 1), K1b 39 + 4 = 43, exit 0, 779.43 s. 1107 of 1108 files byte-identical. The one that differs is the root `index.md`, by exactly the `log.md` link a commit fifteen hours younger than the stored artifact adds -- appending that line to the stored file reproduces the new one byte for byte. Against the two scripts at THIS commit the trees agree in full, which is what the byte-identity test holds. Suite 1127 passed after `git add` (1113 before), mypy --strict clean, ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
6.4 KiB
Python
153 lines
6.4 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 # noqa: E402
|
|
|
|
from llm_ingestion_okf import propose as okf_propose_segments # noqa: E402
|
|
|
|
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
|