table2rules 0.5.1__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {table2rules-0.5.1/src/table2rules.egg-info → table2rules-0.5.2}/PKG-INFO +1 -1
- {table2rules-0.5.1 → table2rules-0.5.2}/pyproject.toml +1 -1
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/_core.py +21 -1
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/simple_repair.py +33 -1
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/spans.py +15 -0
- {table2rules-0.5.1 → table2rules-0.5.2/src/table2rules.egg-info}/PKG-INFO +1 -1
- {table2rules-0.5.1 → table2rules-0.5.2}/LICENSE +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/README.md +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/setup.cfg +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/__init__.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/__main__.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/cleanup.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/errors.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/exporters/__init__.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/exporters/base.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/exporters/rules.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/grid_parser.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/maze_pathfinder.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/models.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/py.typed +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/quality_gate.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/report.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/SOURCES.txt +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/dependency_links.txt +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/entry_points.txt +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/requires.txt +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/top_level.txt +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_correctness_oracle.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_determinism.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_public_api.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_regression_golds.py +0 -0
- {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_robustness_mutations.py +0 -0
|
@@ -12,6 +12,7 @@ from .models import LogicRule
|
|
|
12
12
|
from .quality_gate import GateResult, assess_confidence
|
|
13
13
|
from .report import RenderMode, RenderReport, TableReport
|
|
14
14
|
from .simple_repair import simple_repair
|
|
15
|
+
from .spans import is_full_width_note
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def _split_compound_tables(soup) -> None:
|
|
@@ -155,6 +156,22 @@ def _build_rules(grid) -> List[LogicRule]:
|
|
|
155
156
|
colspan = cell.get("colspan", 1)
|
|
156
157
|
outcome_norm = cell["text"].strip().lower()
|
|
157
158
|
|
|
159
|
+
# A wide <td> that reaches the last column AND covers a majority of
|
|
160
|
+
# the grid's columns is structurally a full-width note/description
|
|
161
|
+
# (e.g. a benefit name "Accidental death and permanent disability"
|
|
162
|
+
# or "If the departure of your public transport is delayed…"
|
|
163
|
+
# spanning the whole value region), not a per-column value. We still
|
|
164
|
+
# emit at every spanned position — so the gate detects an
|
|
165
|
+
# overlapping-span corruption (a rowspan intruding into the note's
|
|
166
|
+
# row) as a conflict and fails open to flat — but attribute every
|
|
167
|
+
# position to the *origin* column's header path. The exporter's
|
|
168
|
+
# origin-aware dedup then collapses the identical lines to one,
|
|
169
|
+
# instead of stamping the sentence under each plan×cover header.
|
|
170
|
+
# Legitimate narrow spans (a right-edge colspan=2 amount covering
|
|
171
|
+
# INDIVIDUAL+FAMILY of one plan) fail the majority test and keep
|
|
172
|
+
# their genuine per-column attribution.
|
|
173
|
+
note = is_full_width_note(col_idx, colspan, n_cols)
|
|
174
|
+
|
|
158
175
|
for r_offset in range(rowspan):
|
|
159
176
|
for c_offset in range(colspan):
|
|
160
177
|
target_row = row_idx + r_offset
|
|
@@ -163,7 +180,8 @@ def _build_rules(grid) -> List[LogicRule]:
|
|
|
163
180
|
if target_row >= len(grid) or target_col >= len(grid[0]):
|
|
164
181
|
continue
|
|
165
182
|
|
|
166
|
-
|
|
183
|
+
header_col = col_idx if note else target_col
|
|
184
|
+
row_headers, col_headers = find_headers_for_cell(grid, target_row, header_col)
|
|
167
185
|
|
|
168
186
|
rules.append(
|
|
169
187
|
LogicRule(
|
|
@@ -302,6 +320,8 @@ def _run(
|
|
|
302
320
|
table_index = 0
|
|
303
321
|
|
|
304
322
|
for table in all_tables:
|
|
323
|
+
if not isinstance(table, Tag):
|
|
324
|
+
continue
|
|
305
325
|
# Skip nested tables — they're folded into their parent's cell text.
|
|
306
326
|
if table.find_parent("table"):
|
|
307
327
|
continue
|
|
@@ -301,6 +301,29 @@ def detect_header_block(rows):
|
|
|
301
301
|
first_data_idx = r
|
|
302
302
|
break
|
|
303
303
|
|
|
304
|
+
# Full-width section dividers cap the header. A row whose only non-empty
|
|
305
|
+
# content is a single DOM cell spanning the whole width (e.g. a benefits
|
|
306
|
+
# schedule "1. PERSONAL ACCIDENT" <td colspan="8"> row) reads, under the
|
|
307
|
+
# colspan-expanded non-empty count used above, as a full multi-cell header
|
|
308
|
+
# row — so without this the header sweep swallows the divider *and* the
|
|
309
|
+
# body rows between it and the first clean data row, bleeding them onto
|
|
310
|
+
# every line as fabricated column headers. When such dividers form a series
|
|
311
|
+
# (>= 2) they are body section dividers, not a one-off header subtitle like
|
|
312
|
+
# "(Dollars in thousands)"; the header ends at the first one. They stay in
|
|
313
|
+
# the body as plain cells (rendered as full-width notes downstream).
|
|
314
|
+
full_width_divider_idxs = []
|
|
315
|
+
for r in range(n):
|
|
316
|
+
origins = {grid[r][c]["origin"] for c in range(max_cols) if grid[r][c]["nonempty"]}
|
|
317
|
+
if len(origins) != 1:
|
|
318
|
+
continue
|
|
319
|
+
(orow, ocol) = next(iter(origins))
|
|
320
|
+
if grid[orow][ocol]["cs"] >= max_cols:
|
|
321
|
+
full_width_divider_idxs.append(r)
|
|
322
|
+
if len(full_width_divider_idxs) >= 2:
|
|
323
|
+
first_divider = full_width_divider_idxs[0]
|
|
324
|
+
if first_divider > 0 and (first_data_idx is None or first_divider < first_data_idx):
|
|
325
|
+
first_data_idx = first_divider
|
|
326
|
+
|
|
304
327
|
if first_data_idx is None or first_data_idx == 0:
|
|
305
328
|
return None
|
|
306
329
|
|
|
@@ -720,7 +743,16 @@ def simple_repair(html: str) -> str:
|
|
|
720
743
|
# counter stays in sync with the grid, otherwise a cell
|
|
721
744
|
# at logical col > 0 in a subsequent row would be
|
|
722
745
|
# mistaken for the first-column cell.
|
|
723
|
-
|
|
746
|
+
#
|
|
747
|
+
# A row whose single cell spans multiple columns is a
|
|
748
|
+
# section divider / full-width note, not a row label —
|
|
749
|
+
# promoting it to <th scope="row"> strands it (it has no
|
|
750
|
+
# value column to anchor a rule, so it vanishes). Leave it
|
|
751
|
+
# a <td> so it is emitted once as a full-width note.
|
|
752
|
+
is_full_width_single = (
|
|
753
|
+
len(cells) == 1 and clamped_span(first.get("colspan")) > 1
|
|
754
|
+
)
|
|
755
|
+
if first.name == "td" and not is_full_width_single:
|
|
724
756
|
first.name = "th"
|
|
725
757
|
first["scope"] = "row"
|
|
726
758
|
rowspan = clamped_span(first.get("rowspan"))
|
|
@@ -27,6 +27,21 @@ def clamped_span(raw) -> int:
|
|
|
27
27
|
return value
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def is_full_width_note(col_idx: int, colspan: int, n_cols: int) -> bool:
|
|
31
|
+
"""True when a wide data cell is structurally a full-width note/description.
|
|
32
|
+
|
|
33
|
+
A ``<td>`` that reaches the last column AND spans a majority of the grid's
|
|
34
|
+
columns (e.g. a benefit name or a "If the departure…" sentence spanning the
|
|
35
|
+
whole value region of a plan×cover matrix) is a description, not a
|
|
36
|
+
per-column value. Such a cell must collapse to a single rule rather than fan
|
|
37
|
+
out across every spanned column — and the confidence gate must count it as a
|
|
38
|
+
single candidate position to match. Legitimate narrow spans (a right-edge
|
|
39
|
+
``colspan=2`` amount covering two sub-columns of one group) fail the majority
|
|
40
|
+
test and keep their per-column fan-out.
|
|
41
|
+
"""
|
|
42
|
+
return colspan > 1 and (col_idx + colspan == n_cols) and (colspan * 2 > n_cols)
|
|
43
|
+
|
|
44
|
+
|
|
30
45
|
def assert_grid_size(rows: int, cols: int) -> None:
|
|
31
46
|
"""Raise if a logical grid shape would exceed the configured cell cap."""
|
|
32
47
|
total_cells = rows * cols
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|