`tools/okf_table_measure.py` answers how far Arm E's join reaches and what it costs, and it imports `find_candidates`, `_GRID_RULE`, `_TABLE_ROW` and both rule names from the shipped module rather than carrying a copy. `tests/test_table_measure.py` pins that with `is`, not `==`: `re.compile` returns a distinct object for an equal pattern, so equality would be satisfied by a pasted literal and only identity catches it. `draw_sample` is imported from `okf_outline_measure` for the same reason -- one K3 draw in the repository, not two that can disagree. The column this round actually needs is the `|`-row count, for EVERY file including the ones the door refuses. It is the ceiling: a document with no table row cannot be moved by this arm. Asserting the ceiling from entry counts instead would assume the orphan check kept every table candidate, which nobody measured -- and one of the K3 sample documents produces no plan at all, so its zero would be an absence with no denominator. Constants split the way `okf_outline_measure.py` splits them. 38 grid-rule lines across 3 documents is a DECLARED expectation measured before the rule was written; a different value means the shipped grammar is not the measured one and no figure below it may be read. The Arm D entry totals are REFERENCE values, printed beside the measured ones and gating nothing. A file the door refuses becomes a `Row` with its reason named rather than a missing row, because `extractable / len(rows)` is the door count this round reports as 39/43 and a silent drop would make it unmeasurable. Tests first: 7 red (the module did not exist), then green. 1251 -> 1258. [skip-docs] on the same measured precedent as the flag commit: `grep -c` for "okf_outline_measure" and "okf_cid_measure" in README.md returns 0. A measurement instrument is documented by its own docstring and by the round's report, and it never enters the wheel. ruff check: exit 0. ruff format --check: exit 0. mypy --strict src/ tools/: 28 files, Success. pytest -q: exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
261 lines
11 KiB
Python
261 lines
11 KiB
Python
"""Measure how far Arm E's grid-rule join reaches into a corpus. Measure, don't build.
|
|
|
|
Order 20260907T075834Z-18584396-from-.claude. `docs/2026-09-07-k3-arm-d.md`
|
|
moved K3's first-rater row from 8/4/0/0 to 4/5/0/3 and left five documents at
|
|
`too fine`. Three of those five are dominated by `rule:table-block`, because the
|
|
converter emits pandoc GRID tables whose rows are separated by rule lines the
|
|
table grammar cannot match -- so one table becomes one concept per row group.
|
|
Arm E stops a rule line closing an open block. This instrument answers how far
|
|
that reaches, and what it costs.
|
|
|
|
It builds nothing. No new rule, no extractor, no bundle, no threshold -- the
|
|
threshold is the operator's, and only once an arm moves the number.
|
|
|
|
**It imports the shipped functions rather than re-implementing them.** An
|
|
instrument carrying its own copy of the grammar measures a second definition
|
|
that can drift from the tool's without a test going red, and every figure it
|
|
publishes would then be about code nobody ships. `tests/test_table_measure.py`
|
|
pins the identity with `is` rather than `==`, because `re.compile` returns a
|
|
distinct object for an equal pattern and a pasted literal would satisfy `==`.
|
|
|
|
## What is an expectation here and what is not
|
|
|
|
**38 grid-rule lines across 3 documents is the declared expectation.** It was
|
|
measured on this corpus before the rule was written, and a different value means
|
|
the shipped grammar is not the measured one -- no figure below it may be read.
|
|
|
|
**The `|`-row column is the CEILING, and it is a measurement, not a reference.**
|
|
Arm E cannot move a document that carries no table row. Asserting the ceiling
|
|
from entry counts alone would assume the orphan check kept every table
|
|
candidate, which nobody measured; this column counts rows in the text itself,
|
|
for every file including the ones the door refuses.
|
|
|
|
**Entry counts are reference values**, printed beside the measured ones and
|
|
gating nothing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from okf_outline_measure import draw_sample # noqa: E402
|
|
|
|
from llm_ingestion_okf.errors import ExtractionError # noqa: E402
|
|
from llm_ingestion_okf.extract import extract_text # noqa: E402
|
|
from llm_ingestion_okf.propose import ( # noqa: E402
|
|
RULE_TABLE_BLOCK,
|
|
RULE_TABLE_GRID,
|
|
_GRID_RULE,
|
|
_TABLE_ROW,
|
|
find_candidates,
|
|
)
|
|
|
|
#: The gate Arm D is measured at, and the value Arm E is measured ALONGSIDE.
|
|
#: Declared, not swept -- it belongs to Arm D and is reproduced here so the two
|
|
#: arms are compared on the same outline.
|
|
RUN_LENGTH = 3
|
|
|
|
#: The declared expectation, measured on this corpus before the rule existed. A
|
|
#: different value means the shipped grammar is not the grammar that was
|
|
#: measured, and no figure below it may be read.
|
|
EXPECTED_GRID_RULE_LINES = 38
|
|
EXPECTED_GRID_DOCUMENTS = 3
|
|
|
|
#: Reference values only -- the Arm D corpus totals, printed beside the measured
|
|
#: ones and never compared against them.
|
|
REFERENCE_ARM_D_ENTRIES = 709
|
|
REFERENCE_TABLE_BLOCK_ENTRIES = 33
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TableMeasurement:
|
|
"""One document, both sides of the join."""
|
|
|
|
name: str
|
|
#: Lines matching the SHIPPED table grammar. This is the ceiling column: a
|
|
#: document with none of these cannot be moved by Arm E, whatever the
|
|
#: orphan check later did to its candidates.
|
|
pipe_rows: int
|
|
#: Lines matching the SHIPPED grid-rule grammar.
|
|
grid_rule_lines: int
|
|
#: Table-block candidates surviving the orphan check, before and after.
|
|
blocks_before: int
|
|
blocks_after: int
|
|
#: Blocks Arm E ACTUALLY joined -- not blocks whose span contains a rule
|
|
#: line. A single-row grid table has a rule line and joins nothing.
|
|
joined_blocks: int
|
|
#: All candidates, not only table blocks: what the plan would carry.
|
|
entries_before: int
|
|
entries_after: int
|
|
|
|
def line(self) -> str:
|
|
return (
|
|
f"| {self.name} | {self.pipe_rows} | {self.grid_rule_lines} "
|
|
f"| {self.blocks_before} | {self.blocks_after} | {self.joined_blocks} "
|
|
f"| {self.entries_before} | {self.entries_after} |"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Row:
|
|
"""A corpus file: measured, or a stated reason it was not.
|
|
|
|
A refused file stays in the list rather than being dropped, because
|
|
`extractable / len(rows)` is the door count this round reports and a
|
|
silently missing row would make it unmeasurable.
|
|
"""
|
|
|
|
name: str
|
|
measurement: TableMeasurement | None
|
|
skip_reason: str | None
|
|
|
|
def line(self) -> str:
|
|
if self.measurement is None:
|
|
return f"| {self.name} | -- | -- | -- | -- | -- | -- | -- |"
|
|
return self.measurement.line()
|
|
|
|
|
|
def measure_document(text: str, name: str, run_length: int = RUN_LENGTH) -> TableMeasurement:
|
|
"""Both sides of the join for one document, from the SHIPPED functions."""
|
|
lines = text.splitlines(keepends=True)
|
|
before = find_candidates(text, outline_run=run_length)
|
|
after = find_candidates(text, outline_run=run_length, table_grid=True)
|
|
return TableMeasurement(
|
|
name=name,
|
|
pipe_rows=sum(1 for line in lines if _TABLE_ROW.match(line)),
|
|
grid_rule_lines=sum(1 for line in lines if _GRID_RULE.match(line)),
|
|
blocks_before=sum(1 for c in before if c.rule == RULE_TABLE_BLOCK),
|
|
blocks_after=sum(1 for c in after if c.rule == RULE_TABLE_BLOCK),
|
|
joined_blocks=sum(1 for c in after if c.grid),
|
|
entries_before=len(before),
|
|
entries_after=len(after),
|
|
)
|
|
|
|
|
|
def run(corpus: Path, run_length: int = RUN_LENGTH) -> list[Row]:
|
|
rows: list[Row] = []
|
|
for path in sorted(corpus.iterdir(), key=lambda p: p.name):
|
|
if not path.is_file():
|
|
continue
|
|
try:
|
|
text = extract_text(path.name, path.read_bytes())
|
|
except ExtractionError as exc:
|
|
rows.append(Row(name=path.name, measurement=None, skip_reason=f"{exc.code}: {exc}"))
|
|
continue
|
|
rows.append(
|
|
Row(
|
|
name=path.name,
|
|
measurement=measure_document(text, path.name, run_length),
|
|
skip_reason=None,
|
|
)
|
|
)
|
|
return rows
|
|
|
|
|
|
def render(corpus: Path, rows: list[Row], run_length: int = RUN_LENGTH) -> str:
|
|
measured = [row.measurement for row in rows if row.measurement is not None]
|
|
extractable = len(measured)
|
|
pipe_docs = sum(1 for m in measured if m.pipe_rows > 0)
|
|
pipe_rows = sum(m.pipe_rows for m in measured)
|
|
grid_docs = sum(1 for m in measured if m.grid_rule_lines > 0)
|
|
grid_lines = sum(m.grid_rule_lines for m in measured)
|
|
joined_docs = sum(1 for m in measured if m.joined_blocks > 0)
|
|
joined = sum(m.joined_blocks for m in measured)
|
|
blocks_before = sum(m.blocks_before for m in measured)
|
|
blocks_after = sum(m.blocks_after for m in measured)
|
|
entries_before = sum(m.entries_before for m in measured)
|
|
entries_after = sum(m.entries_after for m in measured)
|
|
changed = [m for m in measured if m.entries_before != m.entries_after]
|
|
|
|
names = [row.name for row in rows]
|
|
sample = draw_sample(names)
|
|
by_name = {m.name: m for m in measured}
|
|
|
|
lines = [
|
|
f"# Arm E grid-table reach, {corpus}",
|
|
"",
|
|
f"Outline gate: {run_length} (Arm D's, reproduced so both arms share an outline).",
|
|
f"N = {len(rows)} (corpus directory file count). Extractable: {extractable}/{len(rows)}.",
|
|
"",
|
|
"## The ceiling -- documents Arm E could move at all (MEASURED)",
|
|
"",
|
|
f"- documents with at least one table row: **{pipe_docs}**/{extractable}",
|
|
f"- table rows in total: **{pipe_rows}**",
|
|
f"- documents with at least one grid-rule line: **{grid_docs}**/{extractable} "
|
|
f"(expected {EXPECTED_GRID_DOCUMENTS})",
|
|
f"- grid-rule lines in total: **{grid_lines}** (expected {EXPECTED_GRID_RULE_LINES})",
|
|
"",
|
|
"A document with no table row cannot be moved by this arm, whatever the",
|
|
"orphan check later did to its candidates. That is what makes this the",
|
|
"ceiling rather than the entry counts below.",
|
|
"",
|
|
"## What the join does, and what it costs",
|
|
"",
|
|
f"- documents where any block was joined: **{joined_docs}**/{extractable}",
|
|
f"- blocks joined: **{joined}**",
|
|
f"- table-block candidates: **{blocks_before}** -> **{blocks_after}** "
|
|
f"(reference {REFERENCE_TABLE_BLOCK_ENTRIES})",
|
|
f"- entries, whole corpus: **{entries_before}** -> **{entries_after}** "
|
|
f"(delta {entries_after - entries_before:+d}, reference {REFERENCE_ARM_D_ENTRIES})",
|
|
f"- documents whose entry count changed: **{len(changed)}**/{extractable}",
|
|
"",
|
|
"## Per document",
|
|
"",
|
|
"| file | rows | grid rules | blocks before | blocks after | joined | "
|
|
"entries before | entries after |",
|
|
"|---|---|---|---|---|---|---|---|",
|
|
]
|
|
lines.extend(row.line() for row in rows)
|
|
lines.extend(["", "## Files the door refused, with their reason", ""])
|
|
refused = [row for row in rows if row.measurement is None]
|
|
if refused:
|
|
lines.extend(f"- {row.name} -- {row.skip_reason}" for row in refused)
|
|
else:
|
|
lines.append("- none")
|
|
lines.extend(["", "## The K3 sample, in canonical draw order", ""])
|
|
for position, name in enumerate(sample):
|
|
measurement = by_name.get(name)
|
|
if measurement is None:
|
|
state = "not extractable"
|
|
else:
|
|
state = (
|
|
f"rows {measurement.pipe_rows}, grid rules {measurement.grid_rule_lines}, "
|
|
f"entries {measurement.entries_before} -> {measurement.entries_after}"
|
|
)
|
|
lines.append(f"- {position}: {name} -- {state}")
|
|
lines.extend(["", f"Rule names in play: `{RULE_TABLE_BLOCK}`, `{RULE_TABLE_GRID}`.", ""])
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--corpus", type=Path, required=True, help="the directory to measure")
|
|
parser.add_argument("--report", type=Path, required=True, help="where to write the report")
|
|
parser.add_argument(
|
|
"--run-length",
|
|
type=int,
|
|
default=RUN_LENGTH,
|
|
metavar="N",
|
|
help="Arm D's outline gate, reproduced so both arms are measured on one outline",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
if not args.corpus.is_dir():
|
|
print(
|
|
f"okf-table-measure: FAILED - no corpus directory at {args.corpus}",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
rows = run(args.corpus, args.run_length)
|
|
args.report.write_text(render(args.corpus, rows, args.run_length), encoding="utf-8", newline="")
|
|
print(f"okf-table-measure: wrote {args.report}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|