{"record":{"id":"e1186e0bed7e0c86","repo":"docling-project/docling","slug":"legacy-box-notes-the-pre-august-2022-atext-pool-f","errorCode":null,"errorMessage":"Legacy Box Notes (the pre-August-2022 atext/pool format) are not supported yet; only the current Box Note format can be converted.","messagePattern":"Legacy Box Notes \\(the pre-August-2022 atext/pool format\\) are not supported yet; only the current Box Note format can be converted\\.","errorType":"exception","errorClass":"DocumentLoadError","httpStatus":null,"severity":"error","filePath":"docling/backend/boxnote_backend.py","lineNumber":69,"sourceCode":"\n        self.data: dict[str, Any] = {}\n        try:\n            raw = \"\"\n            if isinstance(self.path_or_stream, BytesIO):\n                raw = self.path_or_stream.getvalue().decode(\"utf-8\")\n            elif isinstance(self.path_or_stream, Path):\n                raw = self.path_or_stream.read_text(encoding=\"utf-8\")\n            if raw.strip():\n                loaded = json.loads(raw)\n                if isinstance(loaded, dict):\n                    self.data = loaded\n        except (json.JSONDecodeError, UnicodeDecodeError, OSError) as e:\n            raise DocumentLoadError(\n                f\"Could not load Box Note document with hash {self.document_hash}.\"\n            ) from e\n\n        if \"atext\" in self.data and not self.is_valid():\n            raise DocumentLoadError(\n                \"Legacy Box Notes (the pre-August-2022 atext/pool format) are not \"\n                \"supported yet; only the current Box Note format can be converted.\"\n            )\n\n    @override\n    def is_valid(self) -> bool:\n        return isinstance(self.data.get(\"doc\"), dict)\n\n    @classmethod\n    @override\n    def supports_pagination(cls) -> bool:\n        return False\n\n    @classmethod\n    @override\n    def supported_formats(cls) -> set[InputFormat]:\n        return {InputFormat.BOXNOTE}\n","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/docling-project/docling/blob/61d76f1ff3f8428065465889f7b4577da7df704c/docling/backend/boxnote_backend.py#L51-L87","documentation":"The second post-conversion assertion in _run_benchmark(): after confirming exactly one table, it checks that the table's data grid is merge_count rows by 4 columns, matching the generated workbook (one merged A:C range plus a value in D per row, no header row). It fires when Docling's XLSX parsing returns a different grid: extra/missing rows (e.g. a header row added, empty trailing rows included) or different column count (merged A:C collapsed to one column instead of expanded to three, or an extra index column). It signals that the merged-cell expansion semantics assumed by the benchmark no longer hold.","triggerScenarios":"A Docling version change in how the XLSX backend expands merged ranges into the table grid (collapsing merged cells to a single column yields num_cols == 2 instead of 4); the backend adding a header row or omitting all-empty rows (num_rows becomes merge_count + 1 or fewer); openpyxl/docling-core TableData changes to num_rows/num_cols accounting; benchmarking a branch where cell-level dedup or row-span handling changed.","commonSituations":"Running the perf harness across a Docling upgrade to compare performance without updating the structural expectations first; a PR that changes MsDocumentBackend merged-cell handling; benchmark results invalidated because the assertion encodes the exact grid contract of one specific docling-core version; running against an installed docling older/newer than the checkout.","solutions":["Inspect table.data.grid (or export the document to HTML/markdown) to see the actual shape: num_cols == 2 means merged ranges are collapsed rather than expanded; num_rows == merge_count + 1 means a header row was added.","Update the assertion to the current backend contract, e.g. `if table.data.num_rows != merge_count + 1` if headers are now emitted, or `num_cols != 2` if merges are collapsed — and update non_empty_cells / rectangle_area bookkeeping in BenchmarkResult to match.","Pin the benchmark to the docling version whose merged-cell expansion (A:C expanded to 3 columns, 4 total) you intend to measure, e.g. run inside the repo checkout with `uv run python perfs/xlsx_merged_cells.py`.","If the change is unintended (a regression in merged-cell expansion), fix the XLSX backend so num_cols stays 4 and num_rows equals merge_count, keeping the benchmark as the regression guard."],"exampleFix":"// before (perfs/xlsx_merged_cells.py:153-156)\nif table.data.num_rows != merge_count or table.data.num_cols != 4:\n    raise RuntimeError(\n        f\"unexpected table dimensions: {table.data.num_rows}x{table.data.num_cols}\"\n    )\n\n// after - encode the backend's actual contract explicitly\nEXPECTED_COLS = 4  # A:C merged range expanded to 3 columns + column D\nheader_offset = 1 if table.data.num_rows == merge_count + 1 else 0\nif table.data.num_cols != EXPECTED_COLS:\n    raise RuntimeError(\n        f\"unexpected column count: {table.data.num_cols} != {EXPECTED_COLS}\"\n    )\nif table.data.num_rows - header_offset != merge_count:\n    raise RuntimeError(\n        f\"unexpected row count: {table.data.num_rows} != {merge_count}\"\n    )","handlingStrategy":"validation","validationCode":"table = document.tables[0]\nnum_rows, num_cols = table.data.num_rows, table.data.num_cols\n# generated workbook: merge_count rows (A:C merged per row) + column D, no header\nif num_cols not in (4,):  # extend if the backend collapses merges (2) or adds columns\n    print(f\"column count changed: {num_cols}\", file=sys.stderr)\nrow_delta = num_rows - merge_count\nif row_delta not in (0, 1):  # 1 == backend now emits a header row\n    print(f\"row count off by {row_delta}\", file=sys.stderr)","typeGuard":"def table_matches_workbook(table, merge_count: int, num_cols: int = 4) -> bool:\n    \"\"\"True when the parsed table grid matches the generated merged-range workbook.\"\"\"\n    return (\n        table is not None\n        and table.data is not None\n        and table.data.num_rows == merge_count\n        and table.data.num_cols == num_cols\n    )","tryCatchPattern":"if not table_matches_workbook(table, merge_count):\n    # keep the sweep alive but flag the row/col deviation for triage\n    tqdm.write(\n        f\"dimension drift for merge_count={merge_count}: \"\n        f\"{table.data.num_rows}x{table.data.num_cols}\",\n        file=sys.stderr,\n    )","preventionTips":["After any docling / docling-core / openpyxl upgrade, dump one converted workbook (document.export_to_html() or table.data.grid) and eyeball the grid before running the full benchmark matrix.","Keep the workbook generator (_create_workbook) and the dimension assertion in the same change set: if one changes, change the other and the BenchmarkResult bookkeeping (non_empty_cells, rectangle_area) too.","Record docling_version and commit_sha with every result (already done) so a dimension failure can be bisected to the exact backend change."],"tags":["benchmark","xlsx","merged-cells","table-dimensions","assertion","version-drift"],"backgroundTag":null,"analyzedSha":"61d76f1ff3f8428065465889f7b4577da7df704c","analyzedAt":"2026-08-14T23:53:18.727Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}