Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
253aed4
ci(sync): align focused workflow action majors
sanskarIN Aug 24, 2026
4eda839
chore(version): prepare package metadata for 2.8.5
sanskarIN Aug 24, 2026
1823ff8
chore(version): expose 2.8.5 package version
sanskarIN Aug 24, 2026
67015d6
test(version): pin 2.8.5 release candidate metadata
sanskarIN Aug 24, 2026
1ee0e76
docs(state): advance checkpoint to 2.8.5 preparation
sanskarIN Aug 24, 2026
a456bf4
docs(progress): record 2.8.5 preparation work
sanskarIN Aug 24, 2026
5503955
docs(release): define evidence-first 2.8.5 candidate policy
sanskarIN Aug 24, 2026
3c7ca61
style(discovery): apply maintained Black formatting
sanskarIN Aug 24, 2026
2eb2fbf
style(sync): apply maintained Black formatting
sanskarIN Aug 24, 2026
2d85c31
style(test): format scanner exclusion regression
sanskarIN Aug 24, 2026
c9f9c39
style(cli): format validation diagnostics
sanskarIN Aug 24, 2026
01d1089
test(discovery): create real files for pruning regression
sanskarIN Aug 24, 2026
1b03b5a
ci(regression): install integration extras and preserve repo imports
sanskarIN Aug 24, 2026
ae302a4
ci(quality): preserve repository imports during pytest
sanskarIN Aug 24, 2026
254ab01
docs(progress): record candidate CI fixes
sanskarIN Aug 24, 2026
c3404fb
style(ui): wrap desktop sync accessibility text
sanskarIN Aug 24, 2026
d77fd9e
style(ui): wrap project sync guidance
sanskarIN Aug 24, 2026
a59cacd
style(test): normalize UNO process-group imports
sanskarIN Aug 24, 2026
6f0d412
style(test): normalize OOXML risk imports
sanskarIN Aug 24, 2026
9c50f64
style(test): satisfy canonical URL assertion rule
sanskarIN Aug 24, 2026
10a9add
style(ui): simplify ordering checkpoint result
sanskarIN Aug 24, 2026
f36e397
style(web): wrap responsive client source lines
sanskarIN Aug 24, 2026
7b73c30
style(test): apply Ruff UNO import layout
sanskarIN Aug 24, 2026
5f8a0f3
style(test): normalize OOXML risk imports
sanskarIN Aug 24, 2026
209bed5
style(fidelity): apply Black to acceptance fixture
sanskarIN Aug 24, 2026
1f030c4
style(logging): apply Black formatting
sanskarIN Aug 24, 2026
8afdbf6
style(docx): normalize fidelity acceptance formatting
sanskarIN Aug 24, 2026
dcf8fd5
style(docx): normalize fidelity corpus formatting
sanskarIN Aug 24, 2026
c6ad707
style(docx): normalize UNO acceptance formatting
sanskarIN Aug 24, 2026
4bb0d90
style(docx): apply Black to UNO merge runtime
sanskarIN Aug 24, 2026
1bd585d
style(docx): normalize section evidence formatting
sanskarIN Aug 24, 2026
69b8762
style(docx): apply Black to Word merge acceptance
sanskarIN Aug 24, 2026
2c9fa6c
style(project): apply Black to project store
sanskarIN Aug 24, 2026
2c98611
style(validation): apply Black formatting
sanskarIN Aug 24, 2026
1c97d69
style(test): normalize supervised UNO smoke
sanskarIN Aug 24, 2026
7517e78
style(test): normalize Word acceptance script
sanskarIN Aug 24, 2026
de60cfe
style(test): apply Black to atomic tests
sanskarIN Aug 24, 2026
5aaeb5f
style(test): normalize Word timeout acceptance
sanskarIN Aug 24, 2026
6886e4d
style(test): normalize fidelity corpus tests
sanskarIN Aug 24, 2026
7111c8f
style(test): normalize section evidence tests
sanskarIN Aug 24, 2026
c169390
style(test): normalize Word cleanup failure test
sanskarIN Aug 24, 2026
11087e2
style(test): normalize Word merge acceptance tests
sanskarIN Aug 24, 2026
bd6a160
style(test): normalize Word merge tests
sanskarIN Aug 24, 2026
dc2d2ba
style(test): normalize UNO workflow test formatting
sanskarIN Aug 24, 2026
1074660
style(test): normalize Word page-number acceptance
sanskarIN Aug 24, 2026
58c30c0
style(test): normalize output transaction hardening
sanskarIN Aug 24, 2026
28bab77
style(web): complete Black formatting
sanskarIN Aug 24, 2026
492e786
style(ui): normalize desktop entry newline
sanskarIN Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/project-sync-safety.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
140 changes: 91 additions & 49 deletions PROJECT_STATE.md

Large diffs are not rendered by default.

29 changes: 19 additions & 10 deletions docs/release-process.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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
```

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -210,6 +218,7 @@ Version/tag:
Commit SHA:
Date:
Quality run:
Project Sync Safety run:
120-Part Regression run:
Build Smoke run:
Security run:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 1 addition & 3 deletions scripts/check_docx_fidelity_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ")
Expand Down
2 changes: 1 addition & 1 deletion src/docmergeforge/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""DocMergeForge package."""

__version__ = "0.1.0"
__version__ = "2.8.5"
4 changes: 1 addition & 3 deletions src/docmergeforge/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
4 changes: 1 addition & 3 deletions src/docmergeforge/diagnostics/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,;]+")


Expand Down
4 changes: 1 addition & 3 deletions src/docmergeforge/discovery/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 3 additions & 9 deletions src/docmergeforge/docx/fidelity_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand All @@ -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),
Expand Down
6 changes: 1 addition & 5 deletions src/docmergeforge/docx/fidelity_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 1 addition & 3 deletions src/docmergeforge/docx/libreoffice_uno_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 4 additions & 7 deletions src/docmergeforge/docx/libreoffice_uno_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)
from docmergeforge.utilities.hashing import sha256_file

_UNO_WORKER = r'''
_UNO_WORKER = r"""
from __future__ import annotations

import argparse
Expand Down Expand Up @@ -142,7 +142,7 @@ def main() -> int:

if __name__ == "__main__":
raise SystemExit(main())
'''.strip()
""".strip()


@dataclass(slots=True, frozen=True)
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand Down
8 changes: 2 additions & 6 deletions src/docmergeforge/docx/section_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
31 changes: 9 additions & 22 deletions src/docmergeforge/docx/word_merge_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,15 @@ 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


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

Expand Down Expand Up @@ -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))}",
)
)

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
Loading
Loading