Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ jobs:
assert schematron_registry.count_rules() > 0
PY

- name: Smoke test packaged oracle dispatcher
run: |
for engine in word excel pptx odf gsuite preflight eurooffice; do
.release-venv/bin/openxml-audit-oracle "$engine" --help >/dev/null
done

- name: Upload dist artifacts
uses: actions/upload-artifact@v6
with:
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.8] - 2026-08-09

Euro-Office conversion evidence and a packaging repair for the public oracle
dispatcher. PyPI installs can now run every dispatcher engine outside a source
checkout, including the new authenticated Euro-Office conversion oracle.

### Added
- **Euro-Office conversion oracle (Spec 038)** — a dependency-free
`openxml_audit.eurooffice` client and `openxml-audit-oracle eurooffice`
engine for the current Document Server `/converter` contract. The format
model is pinned to Nextcloud connector 11.0.1 and document-formats 3.2.0:
native OOXML editing, lossy ODT/ODS/ODP-to-OOXML editing, and ODG
view/conversion only. JWT secrets are environment-only, returned OOXML is
validated, and same-format conversions receive canonical per-part diffs.
Reports state their limit explicitly: this proves conversion, not browser
editing, save callbacks, or coauthoring. The first six-file live baseline
records two validator-clean native conversions, three returned packages with
validator findings (including unchanged source findings for PPTX), and one
ODP conversion failure; it also exposes the live 9.3.1.37 versus upstream
9.3.3 deployment-version gap.

### Fixed
- **Packaged oracle dispatcher** — wheels now include the `tools.oracle`
implementation package used by `openxml-audit-oracle`. PyPI installs no
longer fail with `ModuleNotFoundError: No module named 'tools'` when an
engine is selected outside a repository checkout.

## [0.7.7] - 2026-07-20

A reported bug that turned out to be the visible corner of a larger
Expand Down
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,58 @@ The report classifies each roundtrip across a `LossClass` taxonomy:
Drive uploads are deleted in `finally` after each roundtrip — the
oracle never leaves files in your account.

## Euro-Office Conversion Oracle

The `eurooffice` engine exercises Euro-Office Document Server's
synchronous `/converter` endpoint, downloads the result, validates the
returned OOXML package, and records a canonical per-part diff when the
source and target formats match.

The capability model is pinned to Nextcloud connector **11.0.1** and
its `ONLYOFFICE/document-formats` **3.2.0** dependency:

| Connector behavior | Formats | Conversion target |
|---|---|---|
| Native edit | DOCX/DOCM/DOTX/DOTM, XLSX/XLSM/XLTX/XLTM, PPTX/PPTM/POTX/POTM/PPSX/PPSM | Same format |
| Lossy edit | ODT/OTT, ODS/OTS, ODP/OTP | DOCX, XLSX, PPTX |
| View/convert only | ODG | PPTX |

This distinction matters: an ODT shown as editable by the connector is
converted to DOCX before editing, while ODG is not registered as an
editable format. See
[`specs/038-eurooffice-conversion-oracle.md`](specs/038-eurooffice-conversion-oracle.md)
for the pinned sources and evidence limits.

Configure the endpoint and JWT through environment variables. The
secret deliberately has no CLI flag, keeping it out of shell history
and process listings:

```bash
export EUROOFFICE_ORACLE_URL=https://office.example.test/
export EUROOFFICE_ORACLE_JWT_SECRET='read-from-your-secret-store'

# URL inputs can be fetched directly by Document Server.
openxml-audit-oracle eurooffice \
https://files.example.test/sample.odt \
--output eurooffice-report.json

# Local inputs require a public base URL that serves their basenames.
export EUROOFFICE_ORACLE_SOURCE_BASE_URL=https://files.example.test/corpus/
openxml-audit-oracle eurooffice ./corpus/ --output eurooffice-report.json
```

`euro-office` and `euro` are aliases. A successful observation proves
the Document Server fetch/conversion path and output validity. It does
not exercise the browser editor, Nextcloud save callback, or
coauthoring, so it is not an end-to-end editing claim.

The first live six-file snapshot is committed at
[`tools/oracle/baselines/eurooffice/2026-08-09.json`](tools/oracle/baselines/eurooffice/2026-08-09.json).
It records the live server's reported 9.3.1.37 build separately from the
current 9.3.3 upstream release, plus validator-clean DOCX/XLSX conversion,
pre-existing PPTX findings, ODF-target findings, and the observed ODP
conversion failure.

## Open XML SDK (Standalone)

Run the .NET SDK validator separately (requires .NET SDK 8.x or Docker):
Expand Down
6 changes: 5 additions & 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 = "openxml-audit"
version = "0.7.7"
version = "0.7.8"
description = "Validate OOXML and ODF files in pure Python — no .NET required"
readme = "README.md"
license = {file = "LICENSE"}
Expand Down Expand Up @@ -76,6 +76,10 @@ packages = ["src/openxml_audit"]
[tool.hatch.build.targets.wheel.force-include]
"data/openxml" = "openxml_audit/data/openxml"
"data/pptx_oracle" = "openxml_audit/data/pptx_oracle"
# The packaged dispatcher imports ``tools.oracle.*``. Keep that implementation
# package in the wheel as well as the thin entry point under ``openxml_audit``;
# otherwise ``openxml-audit-oracle <engine>`` only works from a repository checkout.
"tools/oracle" = "tools/oracle"

[tool.ruff]
target-version = "py310"
Expand Down
138 changes: 138 additions & 0 deletions specs/038-eurooffice-conversion-oracle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Spec 038: Euro-Office Conversion Oracle

## Status

Implemented (August 9, 2026): pinned format model, authenticated client,
packaged dispatcher engine, unit contract tests, and live conversion
baseline. The live server reported 9.3.1.37; upstream 9.3.3 is modeled and
recorded separately rather than mislabeling the snapshot.

## Problem

`openxml-audit` has local application oracles for Microsoft Office and
LibreOffice, plus a Google Workspace import/export oracle. The Opus95 office
path now uses Euro-Office Document Server with the Nextcloud connector, but the
repository had no executable description of that connector's current format
semantics and no way to capture conversion-path evidence.

Calling ODF simply "editable" is misleading. The connector converts ODT, ODS,
and ODP to OOXML before editing; ODG is viewable and convertible but is not an
editable format. The oracle must preserve those distinctions and must not turn
a conversion API check into a claim about the browser editor.

## Pinned Upstream Contract

This implementation is calibrated to:

- Euro-Office/Document Server 9.3.3 (released August 4, 2026)
- ONLYOFFICE Nextcloud connector 11.0.1 (released June 30, 2026)
- `ONLYOFFICE/document-formats` 3.2.0, connector-pinned commit
`7d7576a3fe2337c30f4c9b40fae70a69dc68ba08`

Primary upstream references:

- <https://github.com/ONLYOFFICE/DocumentServer/releases/tag/v9.3.3>
- <https://github.com/ONLYOFFICE/onlyoffice-nextcloud/releases/tag/v11.0.1>
- <https://github.com/ONLYOFFICE/document-formats/tree/7d7576a3fe2337c30f4c9b40fae70a69dc68ba08>

The format matrix relevant to this validator is:

| Mode | Word | Cell | Slide |
|---|---|---|---|
| Native edit | DOCX, DOCM, DOTX, DOTM | XLSX, XLSM, XLTX, XLTM | PPTX, PPTM, POTX, POTM, PPSX, PPSM |
| Lossy edit | ODT, OTT -> DOCX | ODS, OTS -> XLSX | ODP, OTP -> PPTX |
| View/convert only | - | - | ODG -> PPTX |

`src/openxml_audit/eurooffice/formats.py` is the machine-readable version of
this table. Formats outside it are reported as `unsupported`; the oracle does
not infer capability from a file merely being a ZIP/XML container.

## Client Contract

`EuroOfficeClient` uses only the Python standard library and implements four
operations:

1. `GET /healthcheck`
2. `POST /coauthoring/CommandService.ashx` with `{"c":"version"}`
3. `POST /converter?shardKey=<key>` with the connector-compatible synchronous
conversion body
4. download of the returned `fileUrl`

When `EUROOFFICE_ORACLE_JWT_SECRET` is set, requests receive both forms used by
connector 11.0.1: the configured header token (Authorization/Bearer by default)
and the signed `token` field in the JSON body. HS256 is implemented with
stdlib `hmac`; no JWT dependency is added.

Security rules:

- secrets have no command-line option;
- error messages omit response bodies and URL query strings;
- reports never contain JWTs, response URLs, or the source URL;
- conversion keys are deterministic SHA-256 prefixes, restricted to the
Document Server key character/length contract.

Unsigned requests remain possible when the target server has JWT disabled.

## Oracle Semantics

For each supported input, `tools/oracle/eurooffice_conversion_oracle.py`:

1. resolves a URL that Document Server can fetch;
2. calls synchronous conversion using the matrix target;
3. downloads the result;
4. validates the returned OOXML package with `OpenXmlValidator`;
5. for same-format conversion, runs `package_diff.compare_packages` over all
XML and relationship parts;
6. emits a JSON observation and aggregate outcome counts.

Outcomes:

- `preserved`: same-format output has no canonical XML/relationship diff;
- `rewritten`: same-format output is valid but canonical parts changed;
- `converted`: a cross-format target is valid;
- `unsupported`: the pinned connector matrix has no matching capability;
- `request_failed`, `download_failed`, `invalid_output`,
`source_unavailable`: operational failures, all producing exit status 1.

Unsupported capability is data, not a run failure. Native OOXML, lossy ODF,
and ODG view-only modes remain separate report fields even when conversion
succeeds.

## Source Reachability

Document Server fetches the input itself. HTTP(S) inputs can therefore be
passed directly. A local input needs `--source-base-url` or
`EUROOFFICE_ORACLE_SOURCE_BASE_URL`; the server must be able to retrieve the
local file's URL-escaped basename from that base URL. The oracle copies the
local bytes only for validation and diffing—it does not start an HTTP server or
upload source files.

## Evidence Boundary

This is a **conversion-path oracle**, not a roundtrip editor oracle. It verifies
Document Server health/version, remote fetch, conversion, artifact download,
and static output validity. It does not exercise:

- Nextcloud discovery or connector configuration;
- browser editor boot and WebSocket/coauthoring paths;
- user interaction, comments, macros, or collaborative edits;
- the Nextcloud callback and persistence of an edited file;
- visual or semantic fidelity after conversion.

A full user-path claim still requires a real browser/editor open-edit-save
check against Nextcloud. The report embeds this limitation in
`evidence_scope` so downstream summaries cannot silently erase it.

## Acceptance

- Exact positive and negative format-matrix tests.
- JWT envelope, health/version, conversion-error, and secret non-leakage tests.
- Network-free oracle tests for native preservation, lossy ODF conversion, ODG
view-only conversion, unsupported formats, and report scope.
- `eurooffice`, `euro-office`, and `euro` dispatcher routes work from an
installed wheel.
- A built sdist-to-wheel artifact contains `tools.oracle` and all dispatcher
engines import outside the checkout.
- Live health, version, and representative OOXML/ODF conversions are recorded
in `tools/oracle/baselines/eurooffice/2026-08-09.json` without committing
credentials; failures and version drift remain visible evidence.
2 changes: 1 addition & 1 deletion src/openxml_audit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from openxml_audit.validator import OpenXmlValidator, is_valid_pptx, validate_pptx
from openxml_audit.verdict import AppPrediction, AppVerdict, predict

__version__ = "0.7.7"
__version__ = "0.7.8"

__all__ = [
# Main API
Expand Down
35 changes: 35 additions & 0 deletions src/openxml_audit/eurooffice/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Euro-Office format capabilities and Document Server client."""

from openxml_audit.eurooffice.client import (
ConversionResult,
EuroOfficeClient,
EuroOfficeConfigError,
EuroOfficeConversionError,
EuroOfficeError,
EuroOfficeRequestError,
)
from openxml_audit.eurooffice.formats import (
CONNECTOR_VERSION,
DOCUMENT_FORMATS_COMMIT,
DOCUMENT_FORMATS_VERSION,
DOCUMENT_SERVER_RELEASE,
EuroOfficeFormatMode,
EuroOfficeFormatSupport,
format_support,
)

__all__ = [
"CONNECTOR_VERSION",
"DOCUMENT_SERVER_RELEASE",
"DOCUMENT_FORMATS_COMMIT",
"DOCUMENT_FORMATS_VERSION",
"ConversionResult",
"EuroOfficeClient",
"EuroOfficeConfigError",
"EuroOfficeConversionError",
"EuroOfficeError",
"EuroOfficeFormatMode",
"EuroOfficeFormatSupport",
"EuroOfficeRequestError",
"format_support",
]
Loading
Loading