feat(propose): a joined block names the grid rule beside the table rule
`build_plan` gains the keyword-only `table_grid` and threads it into `find_candidates`, and the `derived` composition becomes an ordered build in `_derived_names` rather than a two-branch conditional expression. Four combinations exist now, and the order is itself a claim: the marker, the rule that OPENED the span, Arm E's join, then Arm C's cut. The test that earns its keep here is `test_a_split_part_of_a_joined_block_names_all_three_rules_in_order`. Arm C rebuilds every part as a fresh frozen `Candidate` from an explicit keyword list, so a field not copied there is silently defaulted back -- the spans stay joined, the plan looks right, every diff-based control passes, and only the rule name is missing. That failure has no other detector. The module docstring's claim that "each entry names exactly one" rule is corrected rather than deleted: it is the ORIGIN that is single, not the list. `tests/test_propose_segments.py`'s `len(rules) == 1` is deliberately left alone -- it runs on the default fixture, where the original claim is still true, and widening it would weaken a true assertion about Arm B. Tests first: 5 red, then green. 1243 -> 1248. ruff check: exit 0. ruff format --check: exit 0. mypy --strict src/ tools/: 27 files, Success. pytest -q: exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7dae0d1a3a
commit
b293977ebf
2 changed files with 123 additions and 14 deletions
|
|
@ -1394,3 +1394,92 @@ def test_every_candidate_present_in_both_arms_keeps_its_start() -> None:
|
|||
assert common, "no common start offsets -- the comparison would be vacuous"
|
||||
for start in common:
|
||||
assert on[start].end >= off[start].end
|
||||
|
||||
|
||||
def arm_e_plan(
|
||||
tmp_path: Path, text: str, name: str = "arm-e.md", max_segment_chars: int = 0
|
||||
) -> dict:
|
||||
"""Arm E at the `build_plan` level -- no CLI, so this step stands alone."""
|
||||
source = write(tmp_path, text, name)
|
||||
return okf_propose_segments.build_plan(
|
||||
source,
|
||||
text,
|
||||
source.read_bytes(),
|
||||
okf_type="reference",
|
||||
proposed_at="2026-09-07T00:00:00Z",
|
||||
max_segment_chars=max_segment_chars,
|
||||
table_grid=True,
|
||||
)
|
||||
|
||||
|
||||
def test_a_joined_block_names_the_table_rule_and_the_grid_rule(tmp_path: Path) -> None:
|
||||
"""Exhaustive `==`, never `in`.
|
||||
|
||||
`assert RULE_TABLE_GRID in derived` would pass for an implementation that
|
||||
appended the name to every entry whenever the flag was on. Two names rather
|
||||
than one because the table rule is still what OPENED the span; dropping it
|
||||
would leave the entry traceable to nothing but the join.
|
||||
"""
|
||||
plan = arm_e_plan(tmp_path, GRID_TABLE)
|
||||
assert len(plan["entries"]) == 1
|
||||
assert plan["entries"][0]["derived"] == [
|
||||
okf_propose_segments.PROPOSED_MARKER,
|
||||
okf_propose_segments.RULE_TABLE_BLOCK,
|
||||
okf_propose_segments.RULE_TABLE_GRID,
|
||||
]
|
||||
|
||||
|
||||
def test_a_pipe_table_under_the_flag_does_not_claim_the_grid_rule(tmp_path: Path) -> None:
|
||||
"""The discriminator. Flag ON, nothing joined, so the name must be absent.
|
||||
|
||||
The `len == 1` comes first: an assertion over an empty entry list would be
|
||||
vacuously true and would prove nothing about the flag.
|
||||
"""
|
||||
plan = arm_e_plan(tmp_path, PIPE_TABLE)
|
||||
assert len(plan["entries"]) == 1
|
||||
assert plan["entries"][0]["derived"] == [
|
||||
okf_propose_segments.PROPOSED_MARKER,
|
||||
okf_propose_segments.RULE_TABLE_BLOCK,
|
||||
]
|
||||
|
||||
|
||||
def test_a_heading_entry_never_carries_the_grid_rule(tmp_path: Path) -> None:
|
||||
"""A heading is not a table, whatever else the document contains."""
|
||||
plan = arm_e_plan(tmp_path, GRID_GOLDEN_DOCUMENT)
|
||||
heading = plan["entries"][0]
|
||||
assert heading["title"] == "Romskjema"
|
||||
assert heading["derived"] == [
|
||||
okf_propose_segments.PROPOSED_MARKER,
|
||||
okf_propose_segments.RULE_HEADING,
|
||||
]
|
||||
|
||||
|
||||
def test_arm_e_spans_tile_and_each_anchor_quotes_its_own_span(tmp_path: Path) -> None:
|
||||
"""Joining must not leave a hole, and the anchor must still name its span."""
|
||||
plan = arm_e_plan(tmp_path, GRID_GOLDEN_DOCUMENT)
|
||||
spans = [tuple(entry["span"]) for entry in plan["entries"]]
|
||||
assert spans == sorted(spans)
|
||||
for first, second in zip(spans, spans[1:]):
|
||||
assert first[1] == second[0]
|
||||
for entry in plan["entries"]:
|
||||
start, end = entry["span"]
|
||||
assert entry["anchor"]["quote"] == GRID_GOLDEN_DOCUMENT[start:end]
|
||||
|
||||
|
||||
def test_a_split_part_of_a_joined_block_names_all_three_rules_in_order(tmp_path: Path) -> None:
|
||||
"""The proof that `subdivide` copies the field its rebuild could drop.
|
||||
|
||||
Arm C cuts the joined span into parts, and every part is a fresh
|
||||
`Candidate` built from an explicit keyword list. If `grid` is not copied
|
||||
there, the parts lose the join and only this test says so. The order is
|
||||
pinned too: the marker, the rule that OPENED the span, the join, then the
|
||||
size cut.
|
||||
"""
|
||||
plan = arm_e_plan(tmp_path, GRID_TABLE, max_segment_chars=60)
|
||||
assert len(plan["entries"]) > 1, "the cap must actually bind, or this proves nothing"
|
||||
assert plan["entries"][1]["derived"] == [
|
||||
okf_propose_segments.PROPOSED_MARKER,
|
||||
okf_propose_segments.RULE_TABLE_BLOCK,
|
||||
okf_propose_segments.RULE_TABLE_GRID,
|
||||
okf_propose_segments.RULE_SIZE_SPLIT,
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue