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.
Files changed (32) hide show
  1. {table2rules-0.5.1/src/table2rules.egg-info → table2rules-0.5.2}/PKG-INFO +1 -1
  2. {table2rules-0.5.1 → table2rules-0.5.2}/pyproject.toml +1 -1
  3. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/_core.py +21 -1
  4. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/simple_repair.py +33 -1
  5. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/spans.py +15 -0
  6. {table2rules-0.5.1 → table2rules-0.5.2/src/table2rules.egg-info}/PKG-INFO +1 -1
  7. {table2rules-0.5.1 → table2rules-0.5.2}/LICENSE +0 -0
  8. {table2rules-0.5.1 → table2rules-0.5.2}/README.md +0 -0
  9. {table2rules-0.5.1 → table2rules-0.5.2}/setup.cfg +0 -0
  10. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/__init__.py +0 -0
  11. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/__main__.py +0 -0
  12. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/cleanup.py +0 -0
  13. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/errors.py +0 -0
  14. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/exporters/__init__.py +0 -0
  15. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/exporters/base.py +0 -0
  16. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/exporters/rules.py +0 -0
  17. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/grid_parser.py +0 -0
  18. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/maze_pathfinder.py +0 -0
  19. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/models.py +0 -0
  20. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/py.typed +0 -0
  21. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/quality_gate.py +0 -0
  22. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules/report.py +0 -0
  23. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/SOURCES.txt +0 -0
  24. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/dependency_links.txt +0 -0
  25. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/entry_points.txt +0 -0
  26. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/requires.txt +0 -0
  27. {table2rules-0.5.1 → table2rules-0.5.2}/src/table2rules.egg-info/top_level.txt +0 -0
  28. {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_correctness_oracle.py +0 -0
  29. {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_determinism.py +0 -0
  30. {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_public_api.py +0 -0
  31. {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_regression_golds.py +0 -0
  32. {table2rules-0.5.1 → table2rules-0.5.2}/tests/test_robustness_mutations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table2rules
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Convert HTML tables to flat, LLM-friendly rules using spatial pathfinding.
5
5
  Author: PebbleRoad Pte Ltd
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "table2rules"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "Convert HTML tables to flat, LLM-friendly rules using spatial pathfinding."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- row_headers, col_headers = find_headers_for_cell(grid, target_row, target_col)
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
- if first.name == "td":
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table2rules
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Convert HTML tables to flat, LLM-friendly rules using spatial pathfinding.
5
5
  Author: PebbleRoad Pte Ltd
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes