diff --git a/.github/workflows/project-sync-safety.yml b/.github/workflows/project-sync-safety.yml index 9362a5f..d7cd6cb 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 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 78e9980..cc232db 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -29,11 +29,11 @@ jobs: sudo apt-get update sudo apt-get install -y libegl1 - run: python -m pip install --upgrade pip - - run: pip install -e ".[dev,web]" + - run: python -m pip install -e ".[dev,web]" - run: pre-commit validate-config - run: ruff check . - run: black --check --diff . - run: mypy src/docmergeforge - run: python scripts/check_docs_links.py - run: python scripts/check_repository_reference.py - - run: pytest --cov=docmergeforge --cov-report=term-missing + - run: python -m pytest --cov=docmergeforge --cov-report=term-missing diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 0199990..545883d 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: python -m 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: python -m pytest -m "regression or integration" tests/regression tests/integration - run: docmergeforge validate --input fixtures/generated/sql-120 --parts 1-120 diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 60a82a7..162d757 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -5,25 +5,59 @@ This file is the compact continuation checkpoint for future development sessions ## Current checkpoint - Repository: `sanskarIN/DocMergeForge` -- Branch: `main` -- Version declared in `pyproject.toml`: `0.1.0` -- Checkpoint immediately before this state-file commit: `b2379226f7681ffe64a2dcfa3b9d59c75006bf30` -- Continuation base for the latest completed feature: `9775190f38e613e33f20aafc82b678a1ca3a233d` -- Development status: pre-stable; do not claim `v1.0.0`, production certification, native mobile packaging, or completed signing/notarization from source changes alone. +- Target branch: `main` +- Active release-preparation branch: `release/2.8.5-prep` +- Version declared in `pyproject.toml`: `2.8.5` +- Release-preparation base: `aac5bf6d275991e21b68e15f5ad31f084fbc72e2` +- Development/release status: `2.8.5` candidate preparation; package versioning does **not** by itself certify production readiness, native mobile packaging, signing/notarization, human accessibility acceptance, or external-office fidelity. -## Latest completed continuation: guarded desktop project synchronization +## Latest continuation: 2.8.5 release preparation -The desktop application now exposes the existing review-first project synchronization model that was previously available through the CLI. +The `2.8.5` preparation pass is intentionally evidence-first. It updates package/runtime version metadata and release-facing documentation without converting unobserved CI, packaging, device, accessibility, stress, signing, or fidelity gates into passing claims. + +### Release metadata + +The candidate version is synchronized across: + +- `pyproject.toml` — package version `2.8.5`; +- `src/docmergeforge/__init__.py` — runtime `__version__ = "2.8.5"`; +- `tests/unit/test_version_metadata.py` — package/runtime synchronization plus an explicit `2.8.5` candidate pin. + +The existing pre-alpha distribution classifier remains intentionally conservative. A numeric version change is not used as a substitute for release acceptance evidence. + +### Workflow-generation cleanup + +The focused Project Sync Safety workflow was the remaining workflow still using older GitHub Action majors. The release-preparation branch aligns it with the repository's maintained Node-24-era action generations: + +- `actions/checkout@v7`; +- `actions/setup-python@v7`. + +This incorporates the substantive changes proposed independently by Dependabot PRs #3 and #4 into one reviewed release-preparation line. The PRs should be closed as superseded after the release branch is merged, not represented as separately required product work. + +### Recent-project synchronization status corrected + +The desktop convenience shortcut from **Recent Projects** into the guarded synchronization workflow is already implemented on current `main` and covered by integration tests. It is no longer future work. + +`ProjectSyncMainWindow` exposes both: + +- **Synchronize Project Sources** — browse for a project JSON; +- **Synchronize Recent Project** — select from maintained recent-project history. + +Both routes converge on the same `_synchronize_project_path(...)` guarded workflow and therefore preserve the same preview, duplicate blocking, separate removal approval, exact-revision propagation, backup, stale-write, and metadata-only semantics. + +## Previous completed continuation: guarded desktop project synchronization + +The desktop application exposes the existing review-first project synchronization model that was previously available through the CLI. ### Maintained public desktop entry -`pyproject.toml` now routes: +`pyproject.toml` routes: ```text docmergeforge-gui = "docmergeforge.ui.desktop_entry:main" ``` -`src/docmergeforge/ui/desktop_entry.py` provides `ProjectSyncMainWindow`, an extension of the established desktop `MainWindow` that adds **Synchronize Project Sources** without duplicating project discovery/synchronization business logic. +`src/docmergeforge/ui/desktop_entry.py` provides `ProjectSyncMainWindow`, an extension of the established desktop `MainWindow` that adds synchronization actions without duplicating project discovery/synchronization business logic. Normal installed desktop startup and normal packaged desktop startup both route through this synchronization-enabled entry. @@ -31,7 +65,7 @@ Normal installed desktop startup and normal packaged desktop startup both route The maintained desktop flow is: -1. select a saved project JSON; +1. select a saved project JSON directly or through recent-project history; 2. load the project and exact SHA-256 content revision from the same byte snapshot; 3. call the shared `plan_project_sync(...)` planner; 4. show a read-only preview containing current/proposed counts, additions, removals, reordering, duplicate parts, missing parts, and complete proposed order; @@ -64,35 +98,42 @@ Desktop and CLI synchronization intentionally share these rules: The SHA-256 project revision mechanism is still an **optimistic stale-write guard**, not a universal cooperative cross-process lock. Do not represent simultaneous multi-writer editing as solved unless a separate coordinated locking/revision protocol is designed and accepted. -## New/changed runtime paths in the latest continuation +## Runtime and documentation paths associated with desktop synchronization -### Added +### Added in the desktop-sync continuation - `src/docmergeforge/ui/desktop_entry.py` — synchronization-enabled maintained desktop startup and workflow orchestration. - `src/docmergeforge/ui/project_sync_dialog.py` — accessible synchronization review dialog. -- `tests/integration/test_project_sync_desktop.py` — offscreen Qt/workflow regression coverage for the new desktop path. -- `docs/history/what_changed-through-2026-08-20-cross-platform.md` — verbatim archive of the previous top-level development record before this desktop-sync continuation. +- `tests/integration/test_project_sync_desktop.py` — offscreen Qt/workflow regression coverage for the desktop path, including browse and recent-project routing. +- `docs/history/what_changed-through-2026-08-20-cross-platform.md` — verbatim archive of the previous top-level development record before the desktop-sync continuation. -### Changed +### Changed across the synchronization/release-prep boundary -- `pyproject.toml` — public GUI console-script target. +- `pyproject.toml` — public GUI console-script target and `2.8.5` candidate package version. +- `src/docmergeforge/__init__.py` — `2.8.5` runtime package version. - `src/docmergeforge/ui/packaged_entry.py` — packaged startup/smoke uses the synchronization-enabled window. -- `src/docmergeforge/packaging/desktop.py` — packaging preflight requires the new desktop entry/dialog modules. +- `src/docmergeforge/packaging/desktop.py` — packaging preflight requires the desktop entry/dialog modules. +- `.github/workflows/project-sync-safety.yml` — focused safety matrix and maintained action majors. - `tests/unit/test_build_desktop.py` — packaging prerequisite coverage. -- `tests/unit/test_version_metadata.py` — pins maintained CLI/GUI/web public entry points. -- `README.md` — public desktop synchronization feature/safety description. +- `tests/unit/test_version_metadata.py` — pins maintained CLI/GUI/web public entry points and the `2.8.5` candidate version. +- `README.md` — public desktop synchronization feature/safety description and release-preparation status. - `docs/desktop-guide.md` — operator workflow. - `docs/project-sync.md` — shared desktop/CLI synchronization contract. - `docs/source-code-reference.md` — runtime responsibility map. - `docs/test-suite-reference.md` — test ownership/evidence map. -- `docs/repository-reference-cross-platform.md` — tracked-path coverage for the new files/archive. -- `what_changed.md` — current continuation record only; the preceding complete record is archived under `docs/history/`. +- `docs/repository-reference-cross-platform.md` — tracked-path coverage for the cross-platform/desktop additions. +- `docs/release-process.md` — versioning/release evidence policy. +- `docs/release-evidence.md` — evidence ledger; candidate evidence must use exact run/checkpoint IDs. +- `CHANGELOG.md` — release-preparation record. +- `what_changed.md` — active continuation record. ## Regression coverage added/expanded `tests/integration/test_project_sync_desktop.py` protects: -- presence/accessibility of the desktop synchronization action; +- presence/accessibility of both desktop synchronization actions; +- browse-project routing; +- recent-project routing; - accessible complete preview content; - disabled apply for ambiguous duplicate parts; - exact revision propagation into the shared apply path; @@ -104,8 +145,8 @@ The SHA-256 project revision mechanism is still an **optimistic stale-write guar Additional related coverage: - `tests/unit/test_build_desktop.py` requires the base window, desktop entry, sync dialog, and packaged entry in build-root preflight; -- `tests/unit/test_version_metadata.py` pins `docmergeforge`, `docmergeforge-gui`, and `docmergeforge-web` entry targets; -- `tests/integration/test_packaged_entry_smoke.py` now reaches the synchronization-enabled packaged window before the existing real temporary PDF/DOCX publication smoke. +- `tests/unit/test_version_metadata.py` pins `docmergeforge`, `docmergeforge-gui`, and `docmergeforge-web` entry targets and candidate version metadata; +- `tests/integration/test_packaged_entry_smoke.py` reaches the synchronization-enabled packaged window before the existing real temporary PDF/DOCX publication smoke. Committed test source is implementation evidence only until execution is observed. @@ -146,25 +187,15 @@ The tracked-file documentation checker reads the maintained reference corpus: - `docs/repository-reference.md`; - `docs/repository-reference-cross-platform.md`. -The latest continuation added exact backticked references for every new tracked runtime/test/history path before or alongside the corresponding file becoming part of the maintained checkpoint. - -The checker is configured in Quality and pre-commit. Configuration is not the same as an observed passing current-head execution. +The checker is configured in Quality and pre-commit. Configuration is not the same as an observed passing candidate-head execution. ## Verification boundary Do not infer a green build merely from commits being present. -During the latest continuation: - -- focused GitHub commit/tree/file inspection was used to keep changes scoped; -- packaging/source/test/documentation dependencies were cross-checked against the repository; -- an unintended temporary Ruff-rule-set expansion introduced while changing the GUI entry was immediately reverted in the next focused commit; -- the maintained Ruff rule set remains `E/F/I/B/UP/SIM/C4`; -- a raw GitHub archive/checkout could not be obtained in the execution environment, so local quality/test execution was not available; -- repository workflow definitions remain configured for Quality/Build Smoke, but no fresh passing current-head execution is claimed without observed run evidence. - -Until observed for the current head, no fresh pass is claimed for: +For the `2.8.5` candidate, no fresh pass is claimed until the exact release-preparation head is observed for the relevant gates. Required source evidence includes: +- pre-commit configuration validation; - Ruff; - Black check; - strict mypy; @@ -174,31 +205,42 @@ Until observed for the current head, no fresh pass is claimed for: - Quality workflow matrix; - 120-Part Regression; - Build Smoke; -- Package Desktop / Onefile Acceptance; -- Security/CodeQL; +- Project Sync Safety matrix; +- Security/CodeQL. + +Packaging/release evidence remains separate: + +- Package Desktop / Onefile Acceptance on Windows, macOS, and Linux; +- downloaded-artifact verification; - representative Android/iOS/iPadOS/ChromeOS/manual browser acceptance; -- human desktop accessibility/clean-machine acceptance. +- human desktop accessibility/clean-machine acceptance; +- external-office fidelity acceptance; +- measured stress at the workload class actually claimed; +- Windows signing; +- macOS signing/notarization; +- final distribution/installer acceptance where applicable. -External-office, measured stress, signing/notarization, and other release gates remain independent. +Older recorded passing runs remain historical evidence for their exact checkpoints and must not be relabeled as `2.8.5` candidate evidence. -## Repository administration state observed during this continuation +## Repository administration state -GitHub branch metadata reported `main` as not protected, with required status checks disabled at the repository-rules layer at the inspected checkpoint. +At the release-preparation base, GitHub branch metadata reported `main` as not protected, with required status checks disabled at the repository-rules layer. This is an administrative governance state, not an application correctness failure. If enforced review/CI policy on `main` is desired, configure branch protection/rulesets through repository administration with the intended required checks. Do not claim protection is enabled until repository metadata confirms it. ## Recommended next development work -1. Observe a current-head Quality run; fix any lint/format/type/test/link/reference failure without weakening maintained rules. -2. Review current Build Smoke and packaged-app results specifically for the synchronization-enabled desktop entry on Windows, macOS, and Linux. -3. If a convenience shortcut from **Recent Projects** into synchronization is added, preserve the same preview, duplicate blocking, separate removal approval, exact revision, and backup semantics; do not bypass them. -4. Keep synchronization domain rules centralized in `project.sync` rather than forking CLI and desktop business logic. +1. Open/review the `release/2.8.5-prep` pull request and observe Quality plus the focused Project Sync Safety checks for the exact candidate head. +2. Fix any Ruff/Black/mypy/docs/reference/pytest/CI failure without weakening maintained checks. +3. Review current 120-Part Regression, Build Smoke, Security/CodeQL, Package Desktop, and Onefile Acceptance evidence for the exact candidate commit. +4. Keep synchronization domain rules centralized in `project.sync`; both browse and recent-project desktop actions must continue to converge on the shared guarded workflow. 5. Perform representative manual browser/device acceptance for the responsive cross-platform client. 6. If Internet/untrusted-network hosting is intentionally supported later, define and acceptance-test an explicit HTTPS reverse-proxy/authentication/body-limit/timeout/concurrency/host-hardening deployment profile. 7. If simultaneous multi-writer project editing becomes a supported requirement, design a separate coordinated lock/revision protocol rather than relabeling the optimistic revision guard. 8. Continue independent release-gate work for native-office fidelity, measured multi-gigabyte stress, human accessibility, clean-machine packaged applications, Windows signing, and macOS signing/notarization. -9. Keep README, project-sync/desktop/source/test references, repository-reference corpus, `what_changed.md`, and this checkpoint synchronized whenever the boundary changes. +9. Native Android APK/AAB and native iOS IPA delivery remain separate implementation tracks; browser support must not be relabeled as native packaging. +10. Keep README, changelog, release-process/evidence docs, project-sync/desktop/source/test references, repository-reference corpus, `what_changed.md`, and this checkpoint synchronized whenever the release boundary changes. ## Continuation rule -Future sessions should inspect the actual current `main` head, read this file plus `what_changed.md`, and consult the repository/source/test/automation/configuration references before modifying a subsystem. Continue from repository evidence instead of re-opening completed work, and never turn configured automation or committed tests into claimed passing evidence without an observed run. +Future sessions should inspect the actual current `main` and any active release branch, read this file plus `what_changed.md`, and consult the repository/source/test/automation/configuration/release references before modifying a subsystem. Continue from repository evidence instead of re-opening completed work, and never turn configured automation or committed tests into claimed passing evidence without an observed run for the exact checkpoint. diff --git a/docs/release-process.md b/docs/release-process.md index 8893889..e0b6134 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -1,8 +1,8 @@ # Release Process -This document defines the evidence required to move DocMergeForge from a development checkpoint to a release candidate and, eventually, a stable `v1.0.0` release. +This document defines the evidence required to move DocMergeForge from a development checkpoint to a release candidate and, eventually, an intentionally production-accepted release. -A release is not complete merely because source tests pass or PyInstaller creates an archive. +A release is not complete merely because source tests pass, a version number changes, or PyInstaller creates an archive. ## Release evidence levels @@ -17,7 +17,9 @@ Documentation, changelogs, and releases must not collapse these levels into a si ## Versioning -The project follows semantic-versioning intent. The package remains pre-stable (`0.x`) while the release matrix is being completed. `1.0.0` is reserved for the first intentionally accepted stable public contract. +The current release-preparation target is `2.8.5`. DocMergeForge uses semantic-version-shaped package identifiers, but the repository's evidence model remains authoritative for readiness: a major version greater than zero or one does **not** by itself mean that human/production acceptance, signing/notarization, native mobile packaging, external-office fidelity, or claimed-scale stress gates have passed. + +The distribution classifier and release documentation must remain conservative until the corresponding evidence is accepted. For `2.8.5`, treat package/runtime metadata as a candidate identifier first; promote/tag/publish only after the intended release gates for that distribution claim are green and reviewed. ## 1. Freeze release scope @@ -29,13 +31,16 @@ Review/update as appropriate: ```text pyproject.toml +src/docmergeforge/__init__.py +tests/unit/test_version_metadata.py CHANGELOG.md README.md docs/ +PROJECT_STATE.md what_changed.md ``` -Every documented command and support claim must match the candidate implementation. +Every documented command and support claim must match the candidate implementation. Package metadata and runtime `__version__` must agree, and the release candidate should have an explicit regression pin so accidental version drift fails before publication. ## 3. Source quality and documentation gate @@ -47,6 +52,7 @@ ruff check . black --check --diff . mypy src/docmergeforge python scripts/check_docs_links.py +python scripts/check_repository_reference.py pytest --cov=docmergeforge --cov-report=term-missing ``` @@ -111,7 +117,7 @@ Run: 32033135355 Checkpoint: 59dc14bbf1d4301177e475ac350694bdd9d90ada ``` -All Windows/macOS/Ubuntu build-host and fresh-runner jobs passed. +All Windows/macOS/Ubuntu build-host and fresh-runner jobs passed for that historical checkpoint. Re-run/review candidate-appropriate packaging evidence before attributing it to `2.8.5`. ## 13. Optional onefile gate @@ -124,7 +130,7 @@ Run: 32033541414 Checkpoint: dc624e23d07e0ce94ef345245630d153ee60091a ``` -All Windows/macOS/Ubuntu build-host and fresh-runner jobs passed. +All Windows/macOS/Ubuntu build-host and fresh-runner jobs passed for that historical checkpoint. Do not relabel it as `2.8.5` evidence unless the exact candidate commit is the checkpoint being verified. ## 14. Build provenance and SBOM gate @@ -149,7 +155,7 @@ Before a production support claim, use representative clean end-user machines/VM ## 16. Platform signing/notarization gate -Current artifacts are explicitly unsigned development builds. +Current artifacts are explicitly unsigned development builds unless final-stage evidence says otherwise. ### Windows @@ -185,7 +191,7 @@ Release notes should state supported platforms/architectures/build modes, worklo A `v*` tag can trigger packaging, but the tag itself is not acceptance and does not make current artifacts signed. -Tag only the chosen reviewed commit after required gates for the release claim are green/accepted. +Tag only the chosen reviewed commit after required gates for the release claim are green/accepted. For the current cycle, do not create or describe `v2.8.5` as accepted merely because package metadata says `2.8.5`. ## 21. Post-release verification @@ -199,9 +205,11 @@ After publishing through the real user-facing channel: 6. confirm no private/debug files were uploaded; 7. retain run IDs, provenance/SBOM/checksums, trust evidence, and human acceptance records. -## Stable `v1.0.0` gate +## Production-acceptance gate + +Do not claim a production-accepted release until required areas for the intended support statement are intentionally accepted, including core merge correctness, transaction/recovery safety, representative large/stress workloads, real-world fidelity, human accessibility, downloaded-artifact and human clean-machine package acceptance, production signing/notarization where distributed, and complete documentation/support/security processes. -Do not claim `v1.0.0` until required areas for the intended support statement are intentionally accepted, including core merge correctness, transaction/recovery safety, representative large/stress workloads, real-world fidelity, human accessibility, downloaded-artifact and human clean-machine package acceptance, production signing/notarization where distributed, and complete documentation/support/security processes. +This gate applies to `2.8.5` regardless of its numeric major version. ## Release evidence template @@ -210,6 +218,7 @@ Version/tag: Commit SHA: Date: Quality run: +Project Sync Safety run: 120-Part Regression run: Build Smoke run: Security run: diff --git a/pyproject.toml b/pyproject.toml index 263c34d..2546b23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "docmergeforge" -version = "0.1.0" +version = "2.8.5" description = "Local-first, validation-driven PDF and DOCX merger for large multi-part publications." readme = "README.md" requires-python = ">=3.12" 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/src/docmergeforge/__init__.py b/src/docmergeforge/__init__.py index 48f7c91..c986183 100644 --- a/src/docmergeforge/__init__.py +++ b/src/docmergeforge/__init__.py @@ -1,3 +1,3 @@ """DocMergeForge package.""" -__version__ = "0.1.0" +__version__ = "2.8.5" 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/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..e7f7ca9 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))}", + "different_first_page=" f"{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))}", + "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"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))}", + "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))}", ) ) @@ -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 f9683d2..4e58fb1 100644 --- a/src/docmergeforge/project/sync.py +++ b/src/docmergeforge/project/sync.py @@ -78,7 +78,9 @@ 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] = [] @@ -127,9 +129,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 () @@ -157,9 +157,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/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) diff --git a/src/docmergeforge/ui/main.py b/src/docmergeforge/ui/main.py index 19cbbdb..4cd8401 100644 --- a/src/docmergeforge/ui/main.py +++ b/src/docmergeforge/ui/main.py @@ -254,9 +254,7 @@ def _confirm_project_order(self, project: MergeProject, *, checkpoint: bool = Tr if order_dialog.exec() != int(order_dialog.DialogCode.Accepted): return False project.selected_files = order_dialog.ordered_paths() - if checkpoint and not self._checkpoint_project(project, "ordering"): - return False - return True + return not checkpoint or self._checkpoint_project(project, "ordering") def _new_project(self, initial_source: Path | None = None) -> None: dialog = ProjectSetupDialog(initial_source) 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) 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..17e5ab5 100644 --- a/src/docmergeforge/web/app.py +++ b/src/docmergeforge/web/app.py @@ -45,12 +45,25 @@ input, button { box-sizing: border-box; width: 100%; min-height: 48px; border-radius: 12px; } input { border: 1px solid #46547a; background: #0f1629; color: #eef2ff; padding: 10px 12px; } input[type=file] { padding: 9px; } - button { margin-top: 20px; border: 0; background: #6d7cff; color: white; font-weight: 800; cursor: pointer; } + button { + margin-top: 20px; + border: 0; + background: #6d7cff; + color: white; + font-weight: 800; + cursor: pointer; + } button:disabled { opacity: .55; cursor: progress; } .muted { color: #b7c0d9; } .status { min-height: 28px; margin-top: 16px; font-weight: 650; } .grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } - .pill { display: inline-block; border: 1px solid #46547a; border-radius: 999px; padding: 6px 10px; margin: 4px 4px 0 0; } + .pill { + display: inline-block; + border: 1px solid #46547a; + border-radius: 999px; + padding: 6px 10px; + margin: 4px 4px 0 0; + } a { color: #aeb8ff; } @media (prefers-color-scheme: light) { body { background: #f4f6fb; color: #182035; } @@ -64,12 +77,25 @@

DocMergeForge

-

Merge PDF or DOCX parts from Windows, macOS, Linux, Android, iPhone/iPad, ChromeOS, or any modern browser.

-

Files are processed by the DocMergeForge Python host you connected to. The default server binds only to this computer; LAN use requires an access token.

+

+ Merge PDF or DOCX parts from Windows, macOS, Linux, Android, iPhone/iPad, ChromeOS, + or any modern browser. +

+

+ Files are processed by the DocMergeForge Python host you connected to. The default server + binds only to this computer; LAN use requires an access token. +

- +
@@ -83,8 +109,16 @@
- -

Leave blank for the loopback-only host. LAN tokens stay in this browser tab session and are sent only in the merge request header.

+ +

+ Leave blank for the loopback-only host. LAN tokens stay in this browser tab session and + are sent only in the merge request header. +

@@ -100,7 +134,9 @@