From a7ea0609e2812af75a8fb9b2b4dcb5a0c3bce206 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:33:35 +0530 Subject: [PATCH 01/29] ci: make test imports deterministic and scope web lint exception --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 263c34d..468b9f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ packages = ["src/docmergeforge"] [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["."] addopts = "-q --strict-markers" markers = [ "integration: tests requiring document libraries", @@ -86,6 +87,9 @@ target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I", "B", "UP", "SIM", "C4"] +[tool.ruff.lint.per-file-ignores] +"src/docmergeforge/web/app.py" = ["E501"] + [tool.black] line-length = 100 target-version = ["py312"] From 8db9434913b64419c2e04a9b80b473c0fc19004b Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:33:43 +0530 Subject: [PATCH 02/29] ci: install web dependencies in regression gate --- .github/workflows/regression.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 0199990..e7e0f68 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -28,7 +28,7 @@ jobs: sudo apt-get update sudo apt-get install -y libegl1 - run: python -m pip install --upgrade pip - - run: pip install -e ".[dev]" + - run: pip install -e ".[dev,web]" - run: python scripts/generate_120_fixture.py fixtures/generated/sql-120 - run: pytest -m "regression or integration" tests/regression tests/integration - run: docmergeforge validate --input fixtures/generated/sql-120 --parts 1-120 From 04ed22be7cc3949e430557403ba6b04e04af29b2 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:34:11 +0530 Subject: [PATCH 03/29] style: format project synchronization helper --- src/docmergeforge/project/sync.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/docmergeforge/project/sync.py b/src/docmergeforge/project/sync.py index f9683d2..f0449e0 100644 --- a/src/docmergeforge/project/sync.py +++ b/src/docmergeforge/project/sync.py @@ -78,7 +78,10 @@ def to_dict(self) -> dict[str, object]: } -def _eligible_documents(project: MergeProject, discovered: list[InputDocument]) -> list[InputDocument]: +def _eligible_documents( + project: MergeProject, + discovered: list[InputDocument], +) -> list[InputDocument]: start = project.settings.expected_start end = project.settings.expected_end unique: list[InputDocument] = [] From 4b1d89b9ef05b725546301ed2756e16e379eab35 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:34:26 +0530 Subject: [PATCH 04/29] style(ui): wrap synchronization accessibility copy --- src/docmergeforge/ui/desktop_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/docmergeforge/ui/desktop_entry.py b/src/docmergeforge/ui/desktop_entry.py index dfea376..085d0af 100644 --- a/src/docmergeforge/ui/desktop_entry.py +++ b/src/docmergeforge/ui/desktop_entry.py @@ -32,7 +32,8 @@ def __init__(self) -> None: self.sync_project_button = QPushButton("Synchronize Project Sources") self.sync_project_button.setAccessibleName("Synchronize project sources") self.sync_project_button.setAccessibleDescription( - "Browse for a saved project, preview source-selection changes, and optionally apply them." + "Browse for a saved project, preview source-selection changes, " + "and optionally apply them." ) self.sync_project_button.setMinimumHeight(58) self.sync_project_button.clicked.connect(self._synchronize_project) From 18c08aa6c183eda43038beb11d5706deb76e0808 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:34:41 +0530 Subject: [PATCH 05/29] style(ui): wrap project sync guidance --- src/docmergeforge/ui/project_sync_dialog.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/docmergeforge/ui/project_sync_dialog.py b/src/docmergeforge/ui/project_sync_dialog.py index f19b734..20840a2 100644 --- a/src/docmergeforge/ui/project_sync_dialog.py +++ b/src/docmergeforge/ui/project_sync_dialog.py @@ -61,7 +61,9 @@ def __init__(self, project_path: Path, plan: ProjectSyncPlan) -> None: "make the automatic selection ambiguous. Resolve the duplicates and preview again." ) elif not plan.changed: - guidance = "The saved selected-file list already matches the current automatic proposal." + guidance = ( + "The saved selected-file list already matches the current automatic proposal." + ) elif plan.removed: guidance = ( "Review the removals carefully. Applying this proposal requires a separate removal " @@ -69,8 +71,8 @@ def __init__(self, project_path: Path, plan: ProjectSyncPlan) -> None: ) else: guidance = ( - "Review the complete proposal before applying it. A versioned backup of the project " - "JSON will be created before the guarded update." + "Review the complete proposal before applying it. A versioned backup of the " + "project JSON will be created before the guarded update." ) self.guidance = QLabel(guidance) From 691d7d1d6ade0804bd22bb06c8277559e39f82a1 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:35:08 +0530 Subject: [PATCH 06/29] style(tests): normalize UNO process-group imports --- tests/integration/test_lo_uno_process_group.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/test_lo_uno_process_group.py b/tests/integration/test_lo_uno_process_group.py index 2e0dd43..d29c19e 100644 --- a/tests/integration/test_lo_uno_process_group.py +++ b/tests/integration/test_lo_uno_process_group.py @@ -4,10 +4,8 @@ import time import pytest - from docmergeforge.docx import libreoffice_uno_merge - pytestmark = pytest.mark.skipif( os.name != "posix", reason="LibreOffice UNO process-group acceptance currently uses POSIX semantics.", From 3c58edb6c586d6c1279b964091600b65ca8f9231 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:35:23 +0530 Subject: [PATCH 07/29] style(tests): sort OOXML risk imports --- tests/unit/test_ooxml_risk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_ooxml_risk.py b/tests/unit/test_ooxml_risk.py index ed54fc3..7f459cb 100644 --- a/tests/unit/test_ooxml_risk.py +++ b/tests/unit/test_ooxml_risk.py @@ -1,5 +1,5 @@ -import zipfile from pathlib import Path +import zipfile from docmergeforge.validation.ooxml import risky_docx_constructs From 666c04a4c3d15ba4feb20be81f6962e0d47e586b Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:35:40 +0530 Subject: [PATCH 08/29] style(tests): satisfy canonical URL assertion lint --- tests/unit/test_ui_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_ui_resources.py b/tests/unit/test_ui_resources.py index cc407c5..cc7906d 100644 --- a/tests/unit/test_ui_resources.py +++ b/tests/unit/test_ui_resources.py @@ -10,7 +10,7 @@ def test_desktop_support_links_match_canonical_project_values() -> None: assert REPOSITORY_URL == "https://github.com/sanskarIN/DocMergeForge" - assert DOCS_URL == f"{REPOSITORY_URL}/tree/main/docs" + assert f"{REPOSITORY_URL}/tree/main/docs" == DOCS_URL assert BMC_URL == "https://buymeacoffee.com/sanskarIN" assert X_URL == "https://x.com/x_sanskarIN" assert BUSINESS_EMAIL == "sanskarin@outlook.in" From 474bbc3b8f0e7456f9703e4ed1c03d2320a6f770 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:35:54 +0530 Subject: [PATCH 09/29] lint: preserve explicit UI checkpoint guard --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 468b9f2..2d6cca6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,7 @@ target-version = "py312" select = ["E", "F", "I", "B", "UP", "SIM", "C4"] [tool.ruff.lint.per-file-ignores] +"src/docmergeforge/ui/main.py" = ["SIM103"] "src/docmergeforge/web/app.py" = ["E501"] [tool.black] From 5c6628882763ffece2459e129f0396c3cc31203d Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:37:24 +0530 Subject: [PATCH 10/29] style(tests): apply Ruff import grouping for UNO test --- tests/integration/test_lo_uno_process_group.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_lo_uno_process_group.py b/tests/integration/test_lo_uno_process_group.py index d29c19e..a79e2a1 100644 --- a/tests/integration/test_lo_uno_process_group.py +++ b/tests/integration/test_lo_uno_process_group.py @@ -4,6 +4,7 @@ import time import pytest + from docmergeforge.docx import libreoffice_uno_merge pytestmark = pytest.mark.skipif( From 9cb9821a11f072903445f172bba9af5a5f6675eb Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:37:37 +0530 Subject: [PATCH 11/29] style(tests): apply Ruff OOXML import ordering --- tests/unit/test_ooxml_risk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_ooxml_risk.py b/tests/unit/test_ooxml_risk.py index 7f459cb..49f5e07 100644 --- a/tests/unit/test_ooxml_risk.py +++ b/tests/unit/test_ooxml_risk.py @@ -1,9 +1,8 @@ -from pathlib import Path import zipfile +from pathlib import Path from docmergeforge.validation.ooxml import risky_docx_constructs - _CONTENT_TYPES = """ From 5d234f10b880a161719f31c4326ebe34928b0ee7 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:40:04 +0530 Subject: [PATCH 12/29] ci: align formatting gate with Ruff pre-commit formatter --- .github/workflows/quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 78e9980..681f7f7 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -32,7 +32,7 @@ jobs: - run: pip install -e ".[dev,web]" - run: pre-commit validate-config - run: ruff check . - - run: black --check --diff . + - run: ruff format --check . - run: mypy src/docmergeforge - run: python scripts/check_docs_links.py - run: python scripts/check_repository_reference.py From a3db25df969814b14ed93cd230df9fa457092e4a Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:40:17 +0530 Subject: [PATCH 13/29] build: remove conflicting Black formatter dependency --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2d6cca6..9f1f3d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ dev = [ "pytest>=8.3", "pytest-cov>=6", "ruff>=0.9", - "black>=25.1", "mypy>=1.15", "pre-commit>=4", "types-setuptools", @@ -91,10 +90,6 @@ select = ["E", "F", "I", "B", "UP", "SIM", "C4"] "src/docmergeforge/ui/main.py" = ["SIM103"] "src/docmergeforge/web/app.py" = ["E501"] -[tool.black] -line-length = 100 -target-version = ["py312"] - [tool.mypy] python_version = "3.12" strict = true From a6f4694fc3a68de9009e230a13d3e6a6baa1ee15 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:40:34 +0530 Subject: [PATCH 14/29] ci: pin Ruff to the pre-commit formatter revision --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9f1f3d9..1dc4937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ Funding = "https://buymeacoffee.com/sanskarIN" dev = [ "pytest>=8.3", "pytest-cov>=6", - "ruff>=0.9", + "ruff==0.16.3", "mypy>=1.15", "pre-commit>=4", "types-setuptools", From 60c5245d87df440f783e10b84418172ef8b5d5ad Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:43:29 +0530 Subject: [PATCH 15/29] ci: add one-shot release formatting helper --- .github/workflows/release-ruff-format.yml | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/release-ruff-format.yml diff --git a/.github/workflows/release-ruff-format.yml b/.github/workflows/release-ruff-format.yml new file mode 100644 index 0000000..9395f5e --- /dev/null +++ b/.github/workflows/release-ruff-format.yml @@ -0,0 +1,34 @@ +name: Release Ruff Format + +on: + push: + branches: [release-ready-0.1.0] + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: release-ready-0.1.0 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - run: python -m pip install ruff==0.16.3 + - name: Apply canonical repository formatting + run: ruff format . + - name: Commit formatting changes + run: | + if git diff --quiet; then + echo "Repository is already Ruff-formatted." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "style: normalize repository with Ruff formatter" + git push origin HEAD:release-ready-0.1.0 From 8cbb50991df054104a34cdb3e1b0bd80dba6eedb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:13:37 +0000 Subject: [PATCH 16/29] style: normalize repository with Ruff formatter --- scripts/check_docx_fidelity_acceptance.py | 4 +-- scripts/generate_120_fixture.py | 3 +- src/docmergeforge/cli/main.py | 4 +-- src/docmergeforge/diagnostics/logging.py | 4 +-- src/docmergeforge/discovery/scanner.py | 4 +-- src/docmergeforge/docx/engine.py | 2 +- src/docmergeforge/docx/fidelity_acceptance.py | 12 ++----- src/docmergeforge/docx/fidelity_corpus.py | 6 +--- .../docx/libreoffice_uno_acceptance.py | 4 +-- .../docx/libreoffice_uno_merge.py | 11 +++---- src/docmergeforge/docx/section_evidence.py | 8 ++--- .../docx/word_merge_acceptance.py | 31 ++++++------------- src/docmergeforge/project/store.py | 25 +++++---------- src/docmergeforge/project/sync.py | 8 ++--- src/docmergeforge/validation/service.py | 4 +-- src/docmergeforge/web/app.py | 6 ++-- .../test_lo_uno_supervised_smoke.py | 4 +-- ...est_word_native_merge_acceptance_script.py | 4 +-- ..._word_timeout_cleanup_acceptance_script.py | 16 +++------- tests/unit/test_atomic.py | 7 +++-- tests/unit/test_docx_fidelity_corpus.py | 4 +-- tests/unit/test_docx_section_evidence.py | 6 ++-- tests/unit/test_docx_word_merge.py | 12 ++----- tests/unit/test_docx_word_merge_acceptance.py | 4 +-- .../test_docx_word_merge_cleanup_failure.py | 4 +-- ..._docx_word_merge_page_number_acceptance.py | 17 +++------- tests/unit/test_lo_uno_acceptance_workflow.py | 7 +---- .../unit/test_output_transaction_hardening.py | 4 +-- tests/unit/test_scanner_exclusions.py | 4 +-- 29 files changed, 64 insertions(+), 165 deletions(-) diff --git a/scripts/check_docx_fidelity_acceptance.py b/scripts/check_docx_fidelity_acceptance.py index ab3e0c3..6763c9b 100644 --- a/scripts/check_docx_fidelity_acceptance.py +++ b/scripts/check_docx_fidelity_acceptance.py @@ -13,9 +13,7 @@ def build_fixture(path: Path) -> None: document = Document() document.core_properties.title = "DocMergeForge Fidelity Acceptance" document.add_heading("Fidelity Acceptance", level=1) - document.add_paragraph( - "Representative smoke content with bold, italic, and list formatting." - ) + document.add_paragraph("Representative smoke content with bold, italic, and list formatting.") formatted = document.add_paragraph() formatted.add_run("Bold text").bold = True formatted.add_run(" and ") diff --git a/scripts/generate_120_fixture.py b/scripts/generate_120_fixture.py index 4125fbe..207f964 100644 --- a/scripts/generate_120_fixture.py +++ b/scripts/generate_120_fixture.py @@ -47,8 +47,7 @@ def main() -> int: zf.writestr("example.sql", f"-- Part {part}\nSELECT {part};\n") print( - "Generated 120 PDF, 120 DOCX, and 120 independent companion ZIP fixtures " - f"in {args.output}" + f"Generated 120 PDF, 120 DOCX, and 120 independent companion ZIP fixtures in {args.output}" ) return 0 diff --git a/src/docmergeforge/cli/main.py b/src/docmergeforge/cli/main.py index 65b6c8f..480a58d 100644 --- a/src/docmergeforge/cli/main.py +++ b/src/docmergeforge/cli/main.py @@ -278,9 +278,7 @@ def _run_direct_merge(args: argparse.Namespace) -> int: "ready": False, "missing": validation_result.missing_parts, "duplicates": validation_result.duplicate_parts, - "diagnostics": [ - item.to_dict() for item in validation_result.diagnostics - ], + "diagnostics": [item.to_dict() for item in validation_result.diagnostics], }, indent=2, ) diff --git a/src/docmergeforge/diagnostics/logging.py b/src/docmergeforge/diagnostics/logging.py index 5acbb9d..dbbe678 100644 --- a/src/docmergeforge/diagnostics/logging.py +++ b/src/docmergeforge/diagnostics/logging.py @@ -15,9 +15,7 @@ r"(?P=quote)?" ) _BEARER_PATTERN = re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+") -_AUTH_HEADER_PATTERN = re.compile( - r"(?i)\bAuthorization\s*:\s*(?:Basic|Bearer)\s+[^\s,;]+" -) +_AUTH_HEADER_PATTERN = re.compile(r"(?i)\bAuthorization\s*:\s*(?:Basic|Bearer)\s+[^\s,;]+") _API_KEY_HEADER_PATTERN = re.compile(r"(?i)\b(?:X-)?Api-Key\s*:\s*[^\s,;]+") diff --git a/src/docmergeforge/discovery/scanner.py b/src/docmergeforge/discovery/scanner.py index e5f099f..51abc7a 100644 --- a/src/docmergeforge/discovery/scanner.py +++ b/src/docmergeforge/discovery/scanner.py @@ -58,9 +58,7 @@ def _iter_directory( for directory, directory_names, file_names in os.walk(root, followlinks=False): directory_path = Path(directory) directory_names[:] = [ - name - for name in directory_names - if not _is_excluded(directory_path / name, excluded) + name for name in directory_names if not _is_excluded(directory_path / name, excluded) ] for name in file_names: path = directory_path / name diff --git a/src/docmergeforge/docx/engine.py b/src/docmergeforge/docx/engine.py index d67f067..39edd56 100644 --- a/src/docmergeforge/docx/engine.py +++ b/src/docmergeforge/docx/engine.py @@ -132,7 +132,7 @@ def merge( diagnostics = validate_docx_package(temporary) if any(diag.level.value in {"ERROR", "FATAL"} for diag in diagnostics): raise ValidationError( - "Output DOCX package validation failed: " f"{diagnostics[0].message}" + f"Output DOCX package validation failed: {diagnostics[0].message}" ) Document(str(temporary)) diff --git a/src/docmergeforge/docx/fidelity_acceptance.py b/src/docmergeforge/docx/fidelity_acceptance.py index 3ed5140..d8df50a 100644 --- a/src/docmergeforge/docx/fidelity_acceptance.py +++ b/src/docmergeforge/docx/fidelity_acceptance.py @@ -128,12 +128,8 @@ def snapshot_docx_structure(path: Path) -> DocxStructureSnapshot: inline_shapes=len(document.inline_shapes), sections=len(document.sections), headings=headings, - header_paragraphs=sum( - len(section.header.paragraphs) for section in document.sections - ), - footer_paragraphs=sum( - len(section.footer.paragraphs) for section in document.sections - ), + header_paragraphs=sum(len(section.header.paragraphs) for section in document.sections), + footer_paragraphs=sum(len(section.footer.paragraphs) for section in document.sections), header_tables=sum(len(section.header.tables) for section in document.sections), footer_tables=sum(len(section.footer.tables) for section in document.sections), ) @@ -152,9 +148,7 @@ def snapshot_docx_content(path: Path) -> DocxContentSnapshot: footer_texts.extend(_table_texts(section.footer.tables)) return DocxContentSnapshot( - body_paragraphs_sha256=_digest_texts( - paragraph.text for paragraph in document.paragraphs - ), + body_paragraphs_sha256=_digest_texts(paragraph.text for paragraph in document.paragraphs), tables_sha256=_digest_texts(_table_texts(document.tables)), headers_sha256=_digest_texts(header_texts), footers_sha256=_digest_texts(footer_texts), diff --git a/src/docmergeforge/docx/fidelity_corpus.py b/src/docmergeforge/docx/fidelity_corpus.py index 2ed4065..1b12f6c 100644 --- a/src/docmergeforge/docx/fidelity_corpus.py +++ b/src/docmergeforge/docx/fidelity_corpus.py @@ -67,11 +67,7 @@ def stopped_early(self) -> bool: @property def accepted(self) -> bool: - return ( - self.discovered_count > 0 - and not self.stopped_early - and self.failed_count == 0 - ) + return self.discovered_count > 0 and not self.stopped_early and self.failed_count == 0 def to_dict(self) -> dict[str, Any]: return { diff --git a/src/docmergeforge/docx/libreoffice_uno_acceptance.py b/src/docmergeforge/docx/libreoffice_uno_acceptance.py index 8940693..6c5507f 100644 --- a/src/docmergeforge/docx/libreoffice_uno_acceptance.py +++ b/src/docmergeforge/docx/libreoffice_uno_acceptance.py @@ -227,9 +227,7 @@ def run_libreoffice_uno_acceptance( capability = require_fidelity_automation("libreoffice") uno_python = find_uno_python() if uno_python is None: - raise ValidationError( - "LibreOffice UNO acceptance requires a Python UNO bridge." - ) + raise ValidationError("LibreOffice UNO acceptance requires a Python UNO bridge.") libreoffice_uno_merge_documents( ordered, output, diff --git a/src/docmergeforge/docx/libreoffice_uno_merge.py b/src/docmergeforge/docx/libreoffice_uno_merge.py index 470748f..1eb165d 100644 --- a/src/docmergeforge/docx/libreoffice_uno_merge.py +++ b/src/docmergeforge/docx/libreoffice_uno_merge.py @@ -20,7 +20,7 @@ ) from docmergeforge.utilities.hashing import sha256_file -_UNO_WORKER = r''' +_UNO_WORKER = r""" from __future__ import annotations import argparse @@ -142,7 +142,7 @@ def main() -> int: if __name__ == "__main__": raise SystemExit(main()) -'''.strip() +""".strip() @dataclass(slots=True, frozen=True) @@ -371,9 +371,7 @@ def libreoffice_uno_merge_documents( text=True, ) try: - worker_stdout, worker_stderr = worker_process.communicate( - timeout=timeout_seconds - ) + worker_stdout, worker_stderr = worker_process.communicate(timeout=timeout_seconds) except subprocess.TimeoutExpired as exc: worker_process.kill() worker_stdout, worker_stderr = worker_process.communicate() @@ -383,8 +381,7 @@ def libreoffice_uno_merge_documents( if worker_process.returncode != 0: detail = worker_stderr.strip() or worker_stdout.strip() raise ValidationError( - "LibreOffice UNO merge worker failed" - + (f": {detail}" if detail else ".") + "LibreOffice UNO merge worker failed" + (f": {detail}" if detail else ".") ) finally: if worker_process is not None and worker_process.poll() is None: diff --git a/src/docmergeforge/docx/section_evidence.py b/src/docmergeforge/docx/section_evidence.py index f0b3022..d4e0bad 100644 --- a/src/docmergeforge/docx/section_evidence.py +++ b/src/docmergeforge/docx/section_evidence.py @@ -68,9 +68,7 @@ def page_number_section_records(path: Path) -> tuple[PageNumberSectionRecord, .. try: document_xml = archive.read(_DOCUMENT_XML) except KeyError as exc: - raise ValidationError( - f"DOCX package is missing {_DOCUMENT_XML}: {path}" - ) from exc + raise ValidationError(f"DOCX package is missing {_DOCUMENT_XML}: {path}") from exc except zipfile.BadZipFile as exc: raise ValidationError(f"Invalid DOCX ZIP container: {path}") from exc @@ -118,8 +116,6 @@ def page_number_properties_sha256(paths: Sequence[Path]) -> str: global_section_index = 0 for path in paths: for record in page_number_section_records(path): - canonical_records.append( - f"section={global_section_index}|{record.canonical()}" - ) + canonical_records.append(f"section={global_section_index}|{record.canonical()}") global_section_index += 1 return _digest_records(canonical_records) diff --git a/src/docmergeforge/docx/word_merge_acceptance.py b/src/docmergeforge/docx/word_merge_acceptance.py index 84e8523..1e42023 100644 --- a/src/docmergeforge/docx/word_merge_acceptance.py +++ b/src/docmergeforge/docx/word_merge_acceptance.py @@ -130,9 +130,7 @@ def _table_texts(tables: Iterable[Any]) -> list[str]: def _header_texts(document: Any) -> list[str]: values: list[str] = [] for section in document.sections: - values.extend( - paragraph.text for paragraph in section.header.paragraphs if paragraph.text - ) + values.extend(paragraph.text for paragraph in section.header.paragraphs if paragraph.text) values.extend(_table_texts(section.header.tables)) return values @@ -140,9 +138,7 @@ def _header_texts(document: Any) -> list[str]: def _footer_texts(document: Any) -> list[str]: values: list[str] = [] for section in document.sections: - values.extend( - paragraph.text for paragraph in section.footer.paragraphs if paragraph.text - ) + values.extend(paragraph.text for paragraph in section.footer.paragraphs if paragraph.text) values.extend(_table_texts(section.footer.tables)) return values @@ -181,18 +177,13 @@ def _section_record(section: Any) -> str: f"gutter={_scalar(section.gutter)}", f"header_distance={_scalar(section.header_distance)}", f"footer_distance={_scalar(section.footer_distance)}", - "different_first_page=" - f"{int(bool(section.different_first_page_header_footer))}", + f"different_first_page={int(bool(section.different_first_page_header_footer))}", f"header_linked={int(bool(section.header.is_linked_to_previous))}", - "first_header_linked=" - f"{int(bool(section.first_page_header.is_linked_to_previous))}", - "even_header_linked=" - f"{int(bool(section.even_page_header.is_linked_to_previous))}", + f"first_header_linked={int(bool(section.first_page_header.is_linked_to_previous))}", + f"even_header_linked={int(bool(section.even_page_header.is_linked_to_previous))}", f"footer_linked={int(bool(section.footer.is_linked_to_previous))}", - "first_footer_linked=" - f"{int(bool(section.first_page_footer.is_linked_to_previous))}", - "even_footer_linked=" - f"{int(bool(section.even_page_footer.is_linked_to_previous))}", + f"first_footer_linked={int(bool(section.first_page_footer.is_linked_to_previous))}", + f"even_footer_linked={int(bool(section.even_page_footer.is_linked_to_previous))}", ) ) @@ -205,9 +196,7 @@ def _section_properties_sha256(paths: Sequence[Path]) -> str: for path in paths: document = Document(str(path)) for section in document.sections: - records.append( - f"section={global_section_index}|{_section_record(section)}" - ) + records.append(f"section={global_section_index}|{_section_record(section)}") global_section_index += 1 return _digest_texts(records) @@ -315,9 +304,7 @@ def _validate_acceptance_inputs( return ordered_sources -def _verify_source_hashes( - sources: Sequence[Path], source_hashes: Sequence[str] -) -> None: +def _verify_source_hashes(sources: Sequence[Path], source_hashes: Sequence[str]) -> None: for source, expected_hash in zip(sources, source_hashes, strict=True): verify_native_source_unchanged(source, expected_hash) diff --git a/src/docmergeforge/project/store.py b/src/docmergeforge/project/store.py index caf36f6..7800351 100644 --- a/src/docmergeforge/project/store.py +++ b/src/docmergeforge/project/store.py @@ -72,9 +72,8 @@ def _required_string(data: dict[str, Any], key: str) -> str: def _path_list(value: object, label: str, *, allow_empty: bool) -> list[Path]: - if ( - not isinstance(value, list) - or not all(isinstance(item, str) and item.strip() for item in value) + if not isinstance(value, list) or not all( + isinstance(item, str) and item.strip() for item in value ): raise ValueError(f"Project field '{label}' must be a JSON array of non-empty path strings.") if not allow_empty and not value: @@ -167,9 +166,7 @@ def _pdf_settings(data: dict[str, Any]) -> PdfSettings: def _docx_settings(data: dict[str, Any]) -> DocxSettings: label = "settings.docx" return DocxSettings( - start_each_part_on_new_page=_bool_value( - data, "start_each_part_on_new_page", True, label - ), + start_each_part_on_new_page=_bool_value(data, "start_each_part_on_new_page", True, label), preserve_sections=_bool_value(data, "preserve_sections", True, label), fidelity_mode=_choice( data, @@ -196,9 +193,7 @@ def _docx_settings(data: dict[str, Any]) -> DocxSettings: ), header_text=_optional_string(data, "header_text", label), footer_text=_optional_string(data, "footer_text", label), - continuous_page_numbering=_bool_value( - data, "continuous_page_numbering", True, label - ), + continuous_page_numbering=_bool_value(data, "continuous_page_numbering", True, label), ) @@ -223,16 +218,10 @@ def _project_from_raw(raw: object) -> MergeProject: settings = MergeSettings( expected_start=expected_start, expected_end=expected_end, - checksum_generation=_bool_value( - settings_data, "checksum_generation", True, "settings" - ), - automatic_validation=_bool_value( - settings_data, "automatic_validation", True, "settings" - ), + checksum_generation=_bool_value(settings_data, "checksum_generation", True, "settings"), + automatic_validation=_bool_value(settings_data, "automatic_validation", True, "settings"), overwrite=_bool_value(settings_data, "overwrite", False, "settings"), - profile_name=_string_value( - settings_data, "profile_name", "Exact Preservation", "settings" - ), + profile_name=_string_value(settings_data, "profile_name", "Exact Preservation", "settings"), filename_template=_string_value( settings_data, "filename_template", "{series}_Master", "settings" ), diff --git a/src/docmergeforge/project/sync.py b/src/docmergeforge/project/sync.py index f0449e0..6d4f371 100644 --- a/src/docmergeforge/project/sync.py +++ b/src/docmergeforge/project/sync.py @@ -130,9 +130,7 @@ def _missing_parts( end: int, ) -> tuple[int, ...]: found = { - item.part.number - for item in documents - if item.kind == kind and item.part.number is not None + item.part.number for item in documents if item.kind == kind and item.part.number is not None } if not found: return () @@ -160,9 +158,7 @@ def plan_project_sync( proposed_set = set(proposed_keys) added = tuple( - path - for path, key in zip(proposed, proposed_keys, strict=True) - if key not in current_set + path for path, key in zip(proposed, proposed_keys, strict=True) if key not in current_set ) removed = tuple( path for path, key in zip(current, current_keys, strict=True) if key not in proposed_set diff --git a/src/docmergeforge/validation/service.py b/src/docmergeforge/validation/service.py index b9e8923..6484ca2 100644 --- a/src/docmergeforge/validation/service.py +++ b/src/docmergeforge/validation/service.py @@ -46,9 +46,7 @@ def validate_part_set( ) ) if kind == DocumentKind.PDF and item.encrypted and is_merge_input: - part_label = ( - f"Part {item.part.number}" if item.part.number is not None else "Selected" - ) + part_label = f"Part {item.part.number}" if item.part.number is not None else "Selected" if allow_encrypted_pdf: diagnostics.append( Diagnostic( diff --git a/src/docmergeforge/web/app.py b/src/docmergeforge/web/app.py index 1d534d5..b8bcb46 100644 --- a/src/docmergeforge/web/app.py +++ b/src/docmergeforge/web/app.py @@ -200,9 +200,7 @@ def ordered_documents(input_root: Path) -> tuple[DocumentKind, list[InputDocumen """Discover one homogeneous PDF/DOCX upload set and order numbered parts naturally.""" documents = [ - item - for item in scan([input_root]) - if item.kind in {DocumentKind.PDF, DocumentKind.DOCX} + item for item in scan([input_root]) if item.kind in {DocumentKind.PDF, DocumentKind.DOCX} ] if not documents: raise ValueError("No PDF or DOCX files were uploaded.") @@ -425,4 +423,4 @@ async def merge( detail="Merge failed. Check the DocMergeForge host logs for details.", ) from exc - return app \ No newline at end of file + return app diff --git a/tests/integration/test_lo_uno_supervised_smoke.py b/tests/integration/test_lo_uno_supervised_smoke.py index af5bc39..b255d9c 100644 --- a/tests/integration/test_lo_uno_supervised_smoke.py +++ b/tests/integration/test_lo_uno_supervised_smoke.py @@ -60,9 +60,7 @@ def fake_acceptance( first_doc = Document(str(first)) second_doc = Document(str(second)) payload = json.loads( - (output_dir / "libreoffice-uno-merge-evidence.json").read_text( - encoding="utf-8" - ) + (output_dir / "libreoffice-uno-merge-evidence.json").read_text(encoding="utf-8") ) assert exit_code == 0 diff --git a/tests/integration/test_word_native_merge_acceptance_script.py b/tests/integration/test_word_native_merge_acceptance_script.py index 85e01fc..b5bf876 100644 --- a/tests/integration/test_word_native_merge_acceptance_script.py +++ b/tests/integration/test_word_native_merge_acceptance_script.py @@ -61,9 +61,7 @@ def fake_acceptance( timeout_seconds: int, start_each_on_new_page: bool, ) -> WordMergeAcceptanceEvidence: - calls.append( - (sources, destination, timeout_seconds, start_each_on_new_page) - ) + calls.append((sources, destination, timeout_seconds, start_each_on_new_page)) return _accepted_evidence(destination) monkeypatch.setattr(script, "run_word_merge_acceptance", fake_acceptance) diff --git a/tests/integration/test_word_timeout_cleanup_acceptance_script.py b/tests/integration/test_word_timeout_cleanup_acceptance_script.py index a163562..130fd8d 100644 --- a/tests/integration/test_word_timeout_cleanup_acceptance_script.py +++ b/tests/integration/test_word_timeout_cleanup_acceptance_script.py @@ -34,9 +34,7 @@ def test_word_timeout_cleanup_acceptance_records_timeout_and_cleanup( lambda mode: _capability(), ) - def fake_run( - command: list[str], *, timeout_seconds: int - ) -> NativeCommandResult: + def fake_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult: nonlocal captured_script identity = Path(command[command.index("-ProcessIdentityFile") + 1]) identity.write_text( @@ -49,9 +47,7 @@ def fake_run( ), encoding="utf-8", ) - captured_script = Path(command[command.index("-File") + 1]).read_text( - encoding="utf-8" - ) + captured_script = Path(command[command.index("-File") + 1]).read_text(encoding="utf-8") raise ValidationError( f"Native DOCX fidelity command timed out after {timeout_seconds} seconds." ) @@ -78,9 +74,7 @@ def fake_run( ] ) payload = json.loads( - (output_dir / "word-timeout-cleanup-evidence.json").read_text( - encoding="utf-8" - ) + (output_dir / "word-timeout-cleanup-evidence.json").read_text(encoding="utf-8") ) assert exit_code == 0 @@ -103,9 +97,7 @@ def test_word_timeout_cleanup_acceptance_rejects_non_timeout_failure( lambda mode: _capability(), ) - def fail_before_timeout( - command: list[str], **kwargs: object - ) -> NativeCommandResult: + def fail_before_timeout(command: list[str], **kwargs: object) -> NativeCommandResult: raise ValidationError("Word COM automation failed before timeout.") monkeypatch.setattr(script, "run_native_command", fail_before_timeout) diff --git a/tests/unit/test_atomic.py b/tests/unit/test_atomic.py index d280585..7128a87 100644 --- a/tests/unit/test_atomic.py +++ b/tests/unit/test_atomic.py @@ -76,9 +76,10 @@ def failing_fsync(fd: int) -> None: monkeypatch.setattr(atomic.os, "fsync", failing_fsync) - with pytest.raises(OSError, match="simulated fsync failure"), atomic_output( - target, overwrite=True - ) as temp: + with ( + pytest.raises(OSError, match="simulated fsync failure"), + atomic_output(target, overwrite=True) as temp, + ): temp.write_bytes(b"new") assert target.read_bytes() == b"published" diff --git a/tests/unit/test_docx_fidelity_corpus.py b/tests/unit/test_docx_fidelity_corpus.py index 38e230e..8ae6981 100644 --- a/tests/unit/test_docx_fidelity_corpus.py +++ b/tests/unit/test_docx_fidelity_corpus.py @@ -68,9 +68,7 @@ def test_run_fidelity_corpus_keeps_report_paths_relative( monkeypatch.setattr( fidelity_corpus, "run_fidelity_roundtrip_acceptance", - lambda source, destination, mode, **kwargs: _accepted_evidence( - source, destination, mode - ), + lambda source, destination, mode, **kwargs: _accepted_evidence(source, destination, mode), ) report = fidelity_corpus.run_fidelity_corpus(corpus, output, "libreoffice") payload = report.to_dict() diff --git a/tests/unit/test_docx_section_evidence.py b/tests/unit/test_docx_section_evidence.py index 83ddbd5..8393216 100644 --- a/tests/unit/test_docx_section_evidence.py +++ b/tests/unit/test_docx_section_evidence.py @@ -136,9 +136,9 @@ def test_page_number_fingerprint_binds_document_order(tmp_path: Path) -> None: chapter_separator="", ) - assert page_number_properties_sha256( - [first, second] - ) != page_number_properties_sha256([second, first]) + assert page_number_properties_sha256([first, second]) != page_number_properties_sha256( + [second, first] + ) def test_page_number_evidence_rejects_non_docx(tmp_path: Path) -> None: diff --git a/tests/unit/test_docx_word_merge.py b/tests/unit/test_docx_word_merge.py index 34111c5..d9a7998 100644 --- a/tests/unit/test_docx_word_merge.py +++ b/tests/unit/test_docx_word_merge.py @@ -59,9 +59,7 @@ def fake_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult destination = Path(command[command.index("-Destination") + 1]) sources = json.loads(manifest.read_text(encoding="utf-8")) captured["sources"] = sources - captured["script"] = Path(command[command.index("-File") + 1]).read_text( - encoding="utf-8" - ) + captured["script"] = Path(command[command.index("-File") + 1]).read_text(encoding="utf-8") _write_identity_from_command(command) merged = Document() @@ -74,9 +72,7 @@ def fake_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult cleanup_calls: list[tuple[Path, str]] = [] - def fake_cleanup( - identity_file: Path, *, powershell: str - ) -> WordProcessCleanupResult: + def fake_cleanup(identity_file: Path, *, powershell: str) -> WordProcessCleanupResult: cleanup_calls.append((identity_file, powershell)) return _clean_process_result() @@ -157,9 +153,7 @@ def failing_run(command: list[str], *, timeout_seconds: int) -> NativeCommandRes _write_identity_from_command(command) raise TimeoutError("simulated PowerShell timeout") - def fake_cleanup( - identity_file: Path, *, powershell: str - ) -> WordProcessCleanupResult: + def fake_cleanup(identity_file: Path, *, powershell: str) -> WordProcessCleanupResult: cleanup_calls.append(identity_file) return WordProcessCleanupResult( identity_present=True, diff --git a/tests/unit/test_docx_word_merge_acceptance.py b/tests/unit/test_docx_word_merge_acceptance.py index a05072a..8e9bf3f 100644 --- a/tests/unit/test_docx_word_merge_acceptance.py +++ b/tests/unit/test_docx_word_merge_acceptance.py @@ -67,9 +67,7 @@ def _synthetic_merge(sources: tuple[Path, ...], output: Path) -> None: for index, source in enumerate(sources): current = Document(str(source)) target_section = ( - merged.sections[0] - if index == 0 - else merged.add_section(WD_SECTION.NEW_PAGE) + merged.sections[0] if index == 0 else merged.add_section(WD_SECTION.NEW_PAGE) ) _copy_section_layout(current.sections[0], target_section) for paragraph in current.paragraphs: diff --git a/tests/unit/test_docx_word_merge_cleanup_failure.py b/tests/unit/test_docx_word_merge_cleanup_failure.py index 1543d07..9766fd2 100644 --- a/tests/unit/test_docx_word_merge_cleanup_failure.py +++ b/tests/unit/test_docx_word_merge_cleanup_failure.py @@ -23,9 +23,7 @@ def test_word_native_merge_surfaces_cleanup_failure_over_original_timeout( output = tmp_path / "merged.docx" _write_docx(source) - def failing_run( - command: list[str], *, timeout_seconds: int - ) -> NativeCommandResult: + def failing_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult: identity = Path(command[command.index("-ProcessIdentityFile") + 1]) identity.write_text( json.dumps( diff --git a/tests/unit/test_docx_word_merge_page_number_acceptance.py b/tests/unit/test_docx_word_merge_page_number_acceptance.py index 76b5594..20d59b7 100644 --- a/tests/unit/test_docx_word_merge_page_number_acceptance.py +++ b/tests/unit/test_docx_word_merge_page_number_acceptance.py @@ -28,9 +28,7 @@ def _inject_page_number_properties(path: Path, *, start: str, fmt: str) -> None: if marker not in document_xml: raise AssertionError("Expected section columns marker in generated DOCX fixture") page_number = f''.encode() - members["word/document.xml"] = document_xml.replace( - marker, page_number + marker, 1 - ) + members["word/document.xml"] = document_xml.replace(marker, page_number + marker, 1) with ZipFile(path, "w", compression=ZIP_DEFLATED) as output: for name, payload in members.items(): output.writestr(name, payload) @@ -65,17 +63,13 @@ def _copy_section_layout(source: object, target: object) -> None: target_story.is_linked_to_previous = source_story.is_linked_to_previous -def _merge_without_page_number_properties( - sources: tuple[Path, ...], output: Path -) -> None: +def _merge_without_page_number_properties(sources: tuple[Path, ...], output: Path) -> None: merged = Document() merged._body.clear_content() for index, source in enumerate(sources): current = Document(str(source)) target_section = ( - merged.sections[0] - if index == 0 - else merged.add_section(WD_SECTION.NEW_PAGE) + merged.sections[0] if index == 0 else merged.add_section(WD_SECTION.NEW_PAGE) ) _copy_section_layout(current.sections[0], target_section) for paragraph in current.paragraphs: @@ -137,10 +131,7 @@ def fake_merge( evidence.expected_content.body_paragraphs_sha256 == evidence.output_content.body_paragraphs_sha256 ) - assert ( - evidence.expected_content.tables_sha256 - == evidence.output_content.tables_sha256 - ) + assert evidence.expected_content.tables_sha256 == evidence.output_content.tables_sha256 assert ( evidence.expected_content.section_properties_sha256 == evidence.output_content.section_properties_sha256 diff --git a/tests/unit/test_lo_uno_acceptance_workflow.py b/tests/unit/test_lo_uno_acceptance_workflow.py index b12e9a2..40b38e4 100644 --- a/tests/unit/test_lo_uno_acceptance_workflow.py +++ b/tests/unit/test_lo_uno_acceptance_workflow.py @@ -3,12 +3,7 @@ def _workflow_text() -> str: repository_root = Path(__file__).resolve().parents[2] - workflow = ( - repository_root - / ".github" - / "workflows" - / "libreoffice-uno-acceptance.yml" - ) + workflow = repository_root / ".github" / "workflows" / "libreoffice-uno-acceptance.yml" return workflow.read_text(encoding="utf-8") diff --git a/tests/unit/test_output_transaction_hardening.py b/tests/unit/test_output_transaction_hardening.py index f358618..f05952a 100644 --- a/tests/unit/test_output_transaction_hardening.py +++ b/tests/unit/test_output_transaction_hardening.py @@ -34,9 +34,7 @@ def _entry( "backup_name": backup_name, "staged_size": len(staged_data) if staged_size is None else staged_size, "staged_sha256": ( - hashlib.sha256(staged_data).hexdigest() - if staged_sha256 is None - else staged_sha256 + hashlib.sha256(staged_data).hexdigest() if staged_sha256 is None else staged_sha256 ), } diff --git a/tests/unit/test_scanner_exclusions.py b/tests/unit/test_scanner_exclusions.py index 8cf186c..c9b3df5 100644 --- a/tests/unit/test_scanner_exclusions.py +++ b/tests/unit/test_scanner_exclusions.py @@ -107,9 +107,7 @@ def test_non_recursive_iter_files_still_honors_excluded_root(tmp_path: Path) -> included.write_text("one", encoding="utf-8") excluded_file.write_text("two", encoding="utf-8") - discovered = list( - scanner.iter_files([source], recursive=False, exclude_roots=[excluded]) - ) + discovered = list(scanner.iter_files([source], recursive=False, exclude_roots=[excluded])) assert discovered == [included] From f37d0cc9aff40e4e366f579614df26b31540489c Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:43:51 +0530 Subject: [PATCH 17/29] ci: remove one-shot release formatting helper --- .github/workflows/release-ruff-format.yml | 34 ----------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/release-ruff-format.yml diff --git a/.github/workflows/release-ruff-format.yml b/.github/workflows/release-ruff-format.yml deleted file mode 100644 index 9395f5e..0000000 --- a/.github/workflows/release-ruff-format.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Release Ruff Format - -on: - push: - branches: [release-ready-0.1.0] - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - ref: release-ready-0.1.0 - - uses: actions/setup-python@v7 - with: - python-version: "3.12" - - run: python -m pip install ruff==0.16.3 - - name: Apply canonical repository formatting - run: ruff format . - - name: Commit formatting changes - run: | - if git diff --quiet; then - echo "Repository is already Ruff-formatted." - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "style: normalize repository with Ruff formatter" - git push origin HEAD:release-ready-0.1.0 From 81aa1a2b0246b6744b901bc50d20fc75b2abf060 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:45:20 +0530 Subject: [PATCH 18/29] ci: align project sync safety with Ruff formatting --- .github/workflows/project-sync-safety.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/project-sync-safety.yml b/.github/workflows/project-sync-safety.yml index 9362a5f..a7f90b0 100644 --- a/.github/workflows/project-sync-safety.yml +++ b/.github/workflows/project-sync-safety.yml @@ -41,10 +41,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.12" cache: pip @@ -54,7 +54,7 @@ jobs: - name: Check focused formatting run: >- - python -m black --check --diff + python -m ruff format --check src/docmergeforge/project/sync.py src/docmergeforge/project/drift.py src/docmergeforge/project/discovery.py From bf30c826653495a73c8ade1b02b3a7c25d832c00 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:45:45 +0530 Subject: [PATCH 19/29] fix(types): define logging handler once before fallback --- src/docmergeforge/diagnostics/logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/docmergeforge/diagnostics/logging.py b/src/docmergeforge/diagnostics/logging.py index dbbe678..441966c 100644 --- a/src/docmergeforge/diagnostics/logging.py +++ b/src/docmergeforge/diagnostics/logging.py @@ -59,9 +59,10 @@ def configure_logging(path: Path, level: str = "INFO") -> logging.Logger: handler.close() logger.removeHandler(handler) + handler: logging.Handler try: path.parent.mkdir(parents=True, exist_ok=True) - handler: logging.Handler = RotatingFileHandler( + handler = RotatingFileHandler( path, maxBytes=5 * 1024 * 1024, backupCount=3, From 50ab841f5c52da5562ea034cb4ae6dffdfe0e94a Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:48:02 +0530 Subject: [PATCH 20/29] test: create real files in scanner pruning fixture --- tests/unit/test_scanner_exclusions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_scanner_exclusions.py b/tests/unit/test_scanner_exclusions.py index c9b3df5..cc16e23 100644 --- a/tests/unit/test_scanner_exclusions.py +++ b/tests/unit/test_scanner_exclusions.py @@ -66,6 +66,9 @@ def test_recursive_iter_files_prunes_excluded_directory_before_descent( source.mkdir() excluded = source / "Master" content = source / "Content" + content.mkdir() + (source / "root.txt").write_text("root", encoding="utf-8") + (content / "Part 1.docx").write_text("part", encoding="utf-8") directory_names = ["Master", "Content"] walk_calls: list[tuple[Path, bool]] = [] From ed06d33b925dcfeb55a88a7ef4c7ff1770584e83 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:49:59 +0530 Subject: [PATCH 21/29] fix(types): avoid logging handler name reuse --- src/docmergeforge/diagnostics/logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docmergeforge/diagnostics/logging.py b/src/docmergeforge/diagnostics/logging.py index 441966c..f4b885a 100644 --- a/src/docmergeforge/diagnostics/logging.py +++ b/src/docmergeforge/diagnostics/logging.py @@ -55,9 +55,9 @@ def configure_logging(path: Path, level: str = "INFO") -> logging.Logger: logger.setLevel(getattr(logging, level.upper(), logging.INFO)) logger.propagate = False - for handler in list(logger.handlers): - handler.close() - logger.removeHandler(handler) + for existing_handler in list(logger.handlers): + existing_handler.close() + logger.removeHandler(existing_handler) handler: logging.Handler try: From 7ce2359c6321d32c43f431f7522cbe24160e5c6b Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:51:37 +0530 Subject: [PATCH 22/29] ci: add one-shot archived link correction --- .github/workflows/release-doc-link-fix.yml | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/release-doc-link-fix.yml diff --git a/.github/workflows/release-doc-link-fix.yml b/.github/workflows/release-doc-link-fix.yml new file mode 100644 index 0000000..0b7dccd --- /dev/null +++ b/.github/workflows/release-doc-link-fix.yml @@ -0,0 +1,39 @@ +name: Release Documentation Link Fix + +on: + push: + branches: [release-ready-0.1.0] + +permissions: + contents: write + +jobs: + fix: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: release-ready-0.1.0 + - name: Fix archived history link + run: | + python - <<'PY' + from pathlib import Path + + path = Path('docs/history/what_changed-through-2026-08-20-cross-platform.md') + text = path.read_text(encoding='utf-8') + old = '[`docs/history/what_changed-through-2026-08-18.md`](docs/history/what_changed-through-2026-08-18.md)' + new = '[`what_changed-through-2026-08-18.md`](what_changed-through-2026-08-18.md)' + if old not in text: + raise SystemExit('expected historical link not found') + path.write_text(text.replace(old, new, 1), encoding='utf-8') + PY + - name: Remove one-shot helper + run: git rm .github/workflows/release-doc-link-fix.yml + - name: Commit corrected documentation + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/history/what_changed-through-2026-08-20-cross-platform.md + git commit -m "docs: fix archived history link" + git push origin HEAD:release-ready-0.1.0 From 4692e6b56d5b06abb59ee0f1d0fa513a7e21331d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:21:43 +0000 Subject: [PATCH 23/29] docs: fix archived history link --- .github/workflows/release-doc-link-fix.yml | 39 ------------------- ...anged-through-2026-08-20-cross-platform.md | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .github/workflows/release-doc-link-fix.yml diff --git a/.github/workflows/release-doc-link-fix.yml b/.github/workflows/release-doc-link-fix.yml deleted file mode 100644 index 0b7dccd..0000000 --- a/.github/workflows/release-doc-link-fix.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Release Documentation Link Fix - -on: - push: - branches: [release-ready-0.1.0] - -permissions: - contents: write - -jobs: - fix: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - ref: release-ready-0.1.0 - - name: Fix archived history link - run: | - python - <<'PY' - from pathlib import Path - - path = Path('docs/history/what_changed-through-2026-08-20-cross-platform.md') - text = path.read_text(encoding='utf-8') - old = '[`docs/history/what_changed-through-2026-08-18.md`](docs/history/what_changed-through-2026-08-18.md)' - new = '[`what_changed-through-2026-08-18.md`](what_changed-through-2026-08-18.md)' - if old not in text: - raise SystemExit('expected historical link not found') - path.write_text(text.replace(old, new, 1), encoding='utf-8') - PY - - name: Remove one-shot helper - run: git rm .github/workflows/release-doc-link-fix.yml - - name: Commit corrected documentation - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add docs/history/what_changed-through-2026-08-20-cross-platform.md - git commit -m "docs: fix archived history link" - git push origin HEAD:release-ready-0.1.0 diff --git a/docs/history/what_changed-through-2026-08-20-cross-platform.md b/docs/history/what_changed-through-2026-08-20-cross-platform.md index 9b1e0e7..79af9ea 100644 --- a/docs/history/what_changed-through-2026-08-20-cross-platform.md +++ b/docs/history/what_changed-through-2026-08-20-cross-platform.md @@ -1,6 +1,6 @@ # What Changed -This file records the current DocMergeForge development pass, verification evidence, and remaining release gates. Earlier detailed development history is preserved in [`docs/history/what_changed-through-2026-08-18.md`](docs/history/what_changed-through-2026-08-18.md) so this top-level record stays readable instead of growing without bound. +This file records the current DocMergeForge development pass, verification evidence, and remaining release gates. Earlier detailed development history is preserved in [`what_changed-through-2026-08-18.md`](what_changed-through-2026-08-18.md) so this top-level record stays readable instead of growing without bound. An item is not treated as finished merely because code was pushed. CI, packaging, platform acceptance, external-office fidelity evidence, accessibility review, and release-signing evidence remain separate completion gates. From a4309ab3124ac5e5a24c8bb2179b8ba1b642ac8e Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:52:40 +0530 Subject: [PATCH 24/29] ci: document canonical Ruff quality gate --- .github/workflows/quality.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 681f7f7..f87cd95 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -31,6 +31,7 @@ jobs: - run: python -m pip install --upgrade pip - run: pip install -e ".[dev,web]" - run: pre-commit validate-config + # Keep CI formatting aligned with the pinned Ruff formatter in pre-commit. - run: ruff check . - run: ruff format --check . - run: mypy src/docmergeforge From 6c46e7425a25cf14b676e280b0fc40c347d4bc39 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:54:27 +0530 Subject: [PATCH 25/29] fix(docx): reject same-path Word roundtrip before overwrite guard --- src/docmergeforge/docx/word.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docmergeforge/docx/word.py b/src/docmergeforge/docx/word.py index a1fe3ef..b48b0eb 100644 --- a/src/docmergeforge/docx/word.py +++ b/src/docmergeforge/docx/word.py @@ -70,10 +70,10 @@ def word_roundtrip_copy( raise ValidationError("Microsoft Word fidelity round-trip accepts DOCX paths only.") if not source.exists() or not source.is_file(): raise FileNotFoundError(source) - if destination.exists(): - raise FileExistsError(f"Refusing to overwrite existing DOCX output: {destination}") if source.resolve() == destination.resolve(): raise ValidationError("Microsoft Word fidelity round-trip requires a separate output path.") + if destination.exists(): + raise FileExistsError(f"Refusing to overwrite existing DOCX output: {destination}") host = powershell or find_word_powershell_host() if host is None: From 5cd919b654908fa033cf1e069de114c95140802a Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:54:52 +0530 Subject: [PATCH 26/29] ci: add one-shot release checklist correction --- .../release-checklist-format-fix.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/release-checklist-format-fix.yml diff --git a/.github/workflows/release-checklist-format-fix.yml b/.github/workflows/release-checklist-format-fix.yml new file mode 100644 index 0000000..fa4f9c4 --- /dev/null +++ b/.github/workflows/release-checklist-format-fix.yml @@ -0,0 +1,39 @@ +name: Release Checklist Formatter Fix + +on: + push: + branches: [release-ready-0.1.0] + +permissions: + contents: write + +jobs: + fix: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: release-ready-0.1.0 + - name: Align release checklist with Ruff formatting + run: | + python - <<'PY' + from pathlib import Path + + path = Path('docs/build/release-checklist.md') + text = path.read_text(encoding='utf-8') + old = '- [ ] Black green.' + new = '- [ ] Ruff formatting green.' + if old not in text: + raise SystemExit('expected Black checklist entry not found') + path.write_text(text.replace(old, new, 1), encoding='utf-8') + PY + - name: Remove one-shot helper + run: git rm .github/workflows/release-checklist-format-fix.yml + - name: Commit corrected checklist + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/build/release-checklist.md + git commit -m "docs: align release checklist with Ruff formatter" + git push origin HEAD:release-ready-0.1.0 From af67a4e7560952f4eec88854c30efff180eb6ce0 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:55:30 +0530 Subject: [PATCH 27/29] ci: force-add tracked release checklist correction --- .github/workflows/release-checklist-format-fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-checklist-format-fix.yml b/.github/workflows/release-checklist-format-fix.yml index fa4f9c4..68e6c2c 100644 --- a/.github/workflows/release-checklist-format-fix.yml +++ b/.github/workflows/release-checklist-format-fix.yml @@ -34,6 +34,6 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add docs/build/release-checklist.md + git add -f docs/build/release-checklist.md git commit -m "docs: align release checklist with Ruff formatter" git push origin HEAD:release-ready-0.1.0 From c8bbd709a8b2a81872f7f3b633229cca6e78287d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:25:36 +0000 Subject: [PATCH 28/29] docs: align release checklist with Ruff formatter --- .../release-checklist-format-fix.yml | 39 ------------------- docs/build/release-checklist.md | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .github/workflows/release-checklist-format-fix.yml diff --git a/.github/workflows/release-checklist-format-fix.yml b/.github/workflows/release-checklist-format-fix.yml deleted file mode 100644 index 68e6c2c..0000000 --- a/.github/workflows/release-checklist-format-fix.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Release Checklist Formatter Fix - -on: - push: - branches: [release-ready-0.1.0] - -permissions: - contents: write - -jobs: - fix: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - ref: release-ready-0.1.0 - - name: Align release checklist with Ruff formatting - run: | - python - <<'PY' - from pathlib import Path - - path = Path('docs/build/release-checklist.md') - text = path.read_text(encoding='utf-8') - old = '- [ ] Black green.' - new = '- [ ] Ruff formatting green.' - if old not in text: - raise SystemExit('expected Black checklist entry not found') - path.write_text(text.replace(old, new, 1), encoding='utf-8') - PY - - name: Remove one-shot helper - run: git rm .github/workflows/release-checklist-format-fix.yml - - name: Commit corrected checklist - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -f docs/build/release-checklist.md - git commit -m "docs: align release checklist with Ruff formatter" - git push origin HEAD:release-ready-0.1.0 diff --git a/docs/build/release-checklist.md b/docs/build/release-checklist.md index f293d07..b7390e7 100644 --- a/docs/build/release-checklist.md +++ b/docs/build/release-checklist.md @@ -26,7 +26,7 @@ At the intended release commit: - [ ] Quality workflow green. - [ ] `pre-commit validate-config` green. - [ ] Ruff green. -- [ ] Black green. +- [ ] Ruff formatting green. - [ ] strict mypy green. - [ ] repository-local Markdown link integrity green. - [ ] full pytest green. From b89796649f531f062d03d78cee93fa7165e46d25 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Wed, 26 Aug 2026 06:55:57 +0530 Subject: [PATCH 29/29] ci: finalize Ruff quality-gate documentation --- .github/workflows/quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f87cd95..07fb237 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -31,7 +31,7 @@ jobs: - run: python -m pip install --upgrade pip - run: pip install -e ".[dev,web]" - run: pre-commit validate-config - # Keep CI formatting aligned with the pinned Ruff formatter in pre-commit. + # Keep CI formatting aligned with the pinned Ruff pre-commit policy. - run: ruff check . - run: ruff format --check . - run: mypy src/docmergeforge