From acebb00b50ab9dd8af12bfeec2c9117f067ed21e Mon Sep 17 00:00:00 2001 From: ssweber <57631333+ssweber@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:00:46 -0400 Subject: [PATCH] fix(csv): downgrade trailing blank row mismatch to warning in writer validation Rungs with blank rows beyond a tall instruction's visual height caused WriterError because the stripped continuation absorbed the extra blank row on read-back. Warn instead of failing when the lost rows are pure padding. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 8 ++++++++ src/laddercodec/csv/writer.py | 28 ++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358bc4a..231de31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.8 + +### Fixed +- **CSV writer**: downgrade trailing blank row mismatch from error to warning + when a tall instruction's stripped continuation absorbs extra blank rows on + read-back — fixes `WriterError` on rungs with blank rows beyond an + instruction's visual height + ## 0.1.7 ### Fixed diff --git a/src/laddercodec/csv/writer.py b/src/laddercodec/csv/writer.py index 80b9ce2..568cc2a 100644 --- a/src/laddercodec/csv/writer.py +++ b/src/laddercodec/csv/writer.py @@ -365,10 +365,26 @@ def _validate_roundtrip(rung: Rung, rows: Sequence[Sequence[str]]) -> None: rebuilt = _rebuild_rung_from_rows(rows) if rebuilt.logical_rows != rung.logical_rows: - raise WriterError( - "CSV round-trip validation failed: " - f"logical row count mismatch: expected {rung.logical_rows}, got {rebuilt.logical_rows}" - ) + # Trailing blank rows (no conditions, no AF) can be lost when + # a tall instruction's stripped continuation absorbs them on + # read-back. This is cosmetic — warn instead of failing. + extra = rung.logical_rows - rebuilt.logical_rows + if extra > 0 and all( + _is_padding_row(rung.conditions[r]) and rung.instructions[r] == "" + for r in range(rebuilt.logical_rows, rung.logical_rows) + ): + import warnings + + warnings.warn( + f"CSV round-trip lost {extra} trailing blank row(s) " + f"(logical_rows {rung.logical_rows} → {rebuilt.logical_rows})", + stacklevel=2, + ) + else: + raise WriterError( + "CSV round-trip validation failed: " + f"logical row count mismatch: expected {rung.logical_rows}, got {rebuilt.logical_rows}" + ) if rebuilt.comment != rung.comment: raise WriterError( @@ -377,7 +393,7 @@ def _validate_roundtrip(rung: Rung, rows: Sequence[Sequence[str]]) -> None: ) for row_idx, (expected_row, actual_row) in enumerate( - zip(rung.conditions, rebuilt.conditions, strict=True), + zip(rung.conditions[: rebuilt.logical_rows], rebuilt.conditions, strict=True), start=1, ): for col_idx, (expected, actual) in enumerate(zip(expected_row, actual_row, strict=True)): @@ -389,7 +405,7 @@ def _validate_roundtrip(rung: Rung, rows: Sequence[Sequence[str]]) -> None: ) for row_idx, (expected, actual) in enumerate( - zip(rung.instructions, rebuilt.instructions, strict=True), + zip(rung.instructions[: rebuilt.logical_rows], rebuilt.instructions, strict=True), start=1, ): if not _af_tokens_match(expected, actual):