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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v3
- name: Install deps
run: uv sync --extra dev
run: uv sync --extra dev --extra rust --extra aspose
- name: Unit tests with coverage
run: >
uv run pytest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,6 @@ output/
logs/
*.log
.vercel

# regenerable fixture-merge byproduct (scripts/build_tier4_fixtures.py)
fixtures/excel/tier4_manifest_fragment.json
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ uv run excelbench html
# Generate fidelity-vs-throughput scatter plots
uv run excelbench scatter

# Run template-mutation suite (wall time, RSS, preservation)
uv run excelbench mutation --output results/mutation

# Run formula-recalculation tier (cache-free fixture, LibreOffice oracle)
uv run excelbench calc --output results/calc

# Run cross-language context snapshot (POI/Excelize/zavora when built)
uv run excelbench cross-language-context --output results-cross-language

# Lint + typecheck
uv run ruff check
uv run mypy
Expand Down
48 changes: 45 additions & 3 deletions METHODOLOGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ This allows reliable local verification while keeping CI runnable.
Two fixture paths exist:
- `fixtures/excel/` (tracked): **canonical Excel-generated files** used in CI.
- `fixtures/excel_xls/` (tracked): **canonical .xls fixtures** for legacy format testing.
- `fixtures/excel/tier4/` (tracked): **openpyxl-structural Tier 4 fixtures** (sheet
protection, page setup, chart anchoring). Generated by
`scripts/build_tier4_fixtures.py`; expectations are verifier-grade openpyxl
readbacks, marked `generator: openpyxl-structural` in the manifest notes.
- `fixtures/mutation/` (tracked): corporate-model template + mutation manifest for
the template-mutation lane (`scripts/build_mutation_template.py`).
- `fixtures/calc/` (tracked): **cache-free** formula fixture plus LibreOffice-oracle
expected values (`scripts/build_calc_fixture.py`). The fixture must never ship
cached formula values; the builder asserts this and fails loudly.
- `test_files/` (gitignored): local scratch output for development.

Generate canonical fixtures with:
Expand All @@ -46,10 +55,13 @@ Features are organized into tiers reflecting complexity:
| **Tier 1** (Formatting) | alignment, background_colors, borders, dimensions, number_formats, text_formatting | 6 |
| **Tier 2** (Advanced) | comments, conditional_formatting, data_validation, freeze_panes, hyperlinks, images, merged_cells, pivot_tables | 8 |
| **Tier 3** (Workbook Metadata) | named_ranges, tables | 2 |
| **Tier 4** (Production surfaces, 2026-09-08) | chart_anchor, page_setup, sheet_protection | 3 |

**Framework coverage:** 19 modeled features total.
**Fresh release snapshot:** 19 tested features, where 18 are scoreable per-library in
the 2026-04-29 wheel-backed WolfXL 2.0 rerun (pivot_tables is tested but N/A across adapters on macOS fixtures).
**Framework coverage:** 22 modeled features total.
**2026-04-29 release snapshot:** 19 tested features, 18 scoreable (pivot_tables N/A on macOS fixtures).
**2026-09-08 competitor snapshot:** all 22 features scored; Tier 4 fixtures are
openpyxl-structural (see Fixtures Policy), so Tier 4 expectations encode openpyxl
semantics, not Excel-authored ground truth.

## Scoring

Expand Down Expand Up @@ -81,3 +93,33 @@ uv run excelbench report --input results/xlsx/results.json --output results/xlsx
- **Secondary tables** can include cross-language libraries such as Apache POI, ClosedXML, Excelize, and ExcelJS.
- Cross-language results should be labeled as ecosystem context unless the report is explicitly cross-language-focused.
- The current rollout plan for the first two cross-language candidates lives in `docs/trackers/apache-poi-excelize-rollout-plan.md`.

## Mutation Lane

`excelbench mutation` measures surgical template editing: each engine changes two
cells in `fixtures/mutation/template_corporate_model.xlsx` in an isolated
subprocess and saves the result.

- **Wall time**: median over `--repeats` (default 3) subprocess runs.
- **Peak RSS**: median subprocess peak resident set size.
- **Preservation score**: fraction of original package parts retained after the
edit, minus damage penalties; outputs must reopen cleanly with the openpyxl
verifier or the engine is marked `integrity-failed` with score 0.

Preservation is a structural proxy (ZIP part inventory), not a semantic
equivalence claim. Cite wall time and preservation together.

## Calculation Lane

`excelbench calc` measures formula recalculation against
`fixtures/calc/expected_values.json` (133 formula cells, LibreOffice headless
oracle, oracle version pinned in the artifact).

- The fixture ships **no cached formula values**; an engine only scores by
producing values itself. Cache passthrough cannot score (DEC-026).
- Status semantics are strict: `passed` requires a full match against the
oracle; any wrong or missing value reports `failed` with matched/total and
per-cell mismatch details.
- Formulas avoid `IFS` and `XLOOKUP` because the LibreOffice oracle build
returns `#NAME?` for them on this path; nested `IF` and INDEX/MATCH are used
instead. This constrains function coverage and is documented per artifact.
86 changes: 69 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@ Most Excel library comparisons focus on speed. ExcelBench focuses on the questio

> Can this library handle my real spreadsheet without breaking the parts I care about?

ExcelBench models 19 XLSX features across 12+ Python adapters. In the fresh
wheel-backed WolfXL 2.0 release snapshot, 18 are scoreable across libraries
(pivot tables remain N/A on macOS fixtures).
ExcelBench models 22 XLSX features across 14 Python adapters plus a
cross-language context lane (Apache POI, Excelize, zavora-xlsx). Tier 4
(sheet protection, page setup, chart anchoring) was added in the
2026-09-08 competitor snapshot.

## Results at a Glance

> Competitor snapshot: 2026-09-08 | WolfXL 2.1.0 wheel | aspose-cells-foss 26.7 | [Fidelity](results-2026-09-08/xlsx/README.md) | [Mutation](results-2026-09-08/mutation/README.md) | [Calc](results-2026-09-08/calc/README.md) | [Zavora cross-language](results-2026-09-08/cross-language/README.md)
>
> In that snapshot: openpyxl 22/22, WolfXL 19/22, aspose-cells-foss 11/22
> green features (WolfXL 2.1.0 gaps: conditional-formatting read,
> named-range and print-title writes).
>
> Mutation suite (two-cell edit on a corporate template): WolfXL patches in
> 1.5s with 100% preservation; openpyxl and aspose rewrite at 60%;
> zavora-xlsx 0.1.2 corrupts package relationships (0%).
>
> Calc tier (133-formula financial DAG, cache-free fixture, LibreOffice
> oracle): LibreOffice 133/133, WolfXL 55/133, aspose-cells-foss 25/133,
> zavora 0/133.
>
> Python release snapshot: 2026-04-29 UTC | wheel-backed WolfXL 2.0 rerun | [Fidelity](results-release-2026-04-28/README.md) | [Perf](results-release-2026-04-28/perf/README.md) | [Dashboard](results-release-2026-04-28/DASHBOARD.md)
>
> In that snapshot, WolfXL reaches `18/18` green features with `100%` pass rate.
Expand All @@ -21,7 +36,7 @@ wheel-backed WolfXL 2.0 release snapshot, 18 are scoreable across libraries
> Pivot capability lane: [separate artifact](results-cross-language-pivots/README.md) because the shipped macOS pivot fixture is not scoreable, while `excelize` can still emit pivot-bearing workbooks.
>
> Historical public baseline: 2026-02-17 | Excel 16.105.3 | macOS (Apple Silicon) | [Full results](results/xlsx/README.md)

>
> Newer performance snapshot: 2026-04-20 | [Perf results](results/perf/README.md)
>
> Read [Public Reporting Status](docs/public-reporting.md) before quoting numbers across snapshots.
Expand All @@ -38,28 +53,36 @@ Cross-language libraries matter too, but for a different reason: they show how s

- `Apache POI`
- `Excelize`
- `zavora-xlsx` (Rust writer, added 2026-09-08)

Pivot tables sit in a separate capability lane. On macOS, the shipped pivot fixture does not currently contain scoreable pivot OOXML, so the pivot story is tracked as a dedicated artifact instead of being mixed into the scored lane.

See [cross-language comparison strategy](docs/trackers/cross-language-comparison-strategy.md).

## Three Lanes
## Lanes

1. **Python replacement lane**
Use this when the question is: what should a Python team use instead of `openpyxl`?

2. **Cross-language context lane**
Use this when the question is: how does WolfXL compare to serious spreadsheet tooling in Java and Go?
Use this when the question is: how does WolfXL compare to serious spreadsheet tooling in Java, Go, and Rust?

3. **Pivot capability lane**
Use this when the question is: can the cross-language helpers detect or emit pivot-bearing workbooks even when the main scored fixture is not valid on macOS?

4. **Template-mutation lane**
Use this when the question is: which engine can surgically edit a corporate template fastest while preserving the package?

5. **Formula-recalculation lane**
Use this when the question is: which engine actually computes a 133-formula financial DAG from scratch (cache-free fixture, LibreOffice oracle)?

### Library Comparison

| Library | Caps | Fidelity | Read Speed | Write Speed | Modify |
|---------|:----:|:--------:|:----------:|:-----------:|:------:|
| **wolfxl** | R+W | 18/18 in 2026-04-29 release snapshot | workload-specific | workload-specific | Patch |
| openpyxl | R+W | 18/18 in 2026-04-29 release snapshot | 1x (baseline per workload) | 1x (baseline per workload) | Rewrite |
| **wolfxl** | R+W | 19/22 in 2026-09-08 competitor snapshot (18/18 in 2026-04-29 snapshot) | workload-specific | workload-specific | Patch (100% preservation in mutation suite) |
| openpyxl | R+W | 22/22 in 2026-09-08 competitor snapshot | 1x (baseline per workload) | 1x (baseline per workload) | Rewrite (60% preservation in mutation suite) |
| aspose-cells-foss | R+W | 11/22 in 2026-09-08 competitor snapshot | workload-specific | workload-specific | Rewrite (60% preservation) |
| xlsxwriter | W | 15/18 in 2026-04-29 release snapshot | -- | ~1x | No |
| xlsxwriter-constmem | W | 12/18 in 2026-04-29 release snapshot | -- | ~2x | No |
| python-calamine | R | 1/18 in 2026-04-29 release snapshot | ~1.3x | -- | No |
Expand All @@ -69,13 +92,15 @@ Use this when the question is: can the cross-language helpers detect or emit piv
> Speed numbers are snapshot-specific. Always cite the artifact date, workload, and profile. See [performance results](results/perf/README.md), [METHODOLOGY.md](METHODOLOGY.md), and [Public Reporting Status](docs/public-reporting.md).

### Key Findings

- **High-fidelity libraries are rare**: in the fresh wheel-backed release snapshot, only openpyxl and WolfXL reach 18/18 green features
- **Patch modify is structurally different**: WolfXL's `load_workbook(path, modify=True)` uses surgical ZIP patching rather than a full workbook rewrite
- **High-fidelity libraries are rare**: in the 2026-04-29 release snapshot, only openpyxl and WolfXL reached 18/18 green features; in the 2026-09-08 competitor snapshot (22 features), openpyxl holds 22/22 while WolfXL 2.1.0 drops to 19/22
- **WolfXL 2.1.0 regression signal**: conditional-formatting read scores 0 and named-range / print-title writes score 2 in the 2026-09-08 snapshot; tracked for the WolfXL repo
- **Patch modify is structurally different**: WolfXL's `load_workbook(path, modify=True)` uses surgical ZIP patching; it is the only engine reaching 100% package preservation in the mutation suite
- **The abstraction tax is real**: pandas wraps openpyxl but drops from 16 to 3 green features due to DataFrame coercion (errors become NaN)
- **Speed vs fidelity tradeoff is measurable**: use the perf snapshot together with the fidelity matrix rather than quoting one without the other
- **Optimization modes have clear costs**: openpyxl-readonly loses 13 green features for streaming speed
- **Cross-language context is now strong too**: both `Apache POI` and `Excelize` land at `18/18` in the current scored write lane
- **Cross-language context is now strong too**: `Apache POI` and `Excelize` land at `18/18` in the scored write lane; `zavora-xlsx` 0.1.2 writes fast but corrupts hyperlink relationships on mutate and cannot recalculate
- **Calculation is a differentiator**: only LibreOffice computes the full 133-formula DAG; WolfXL 2.1.0 covers 55/133 (power-operator family returns None), aspose-cells-foss 25/133


See the [release snapshot dashboard](results-release-2026-04-28/DASHBOARD.md) for the fresh wheel-backed combined view, or the [historical dashboard](results/DASHBOARD.md) for the older public baseline.

Expand All @@ -90,11 +115,13 @@ See the [release snapshot dashboard](results-release-2026-04-28/DASHBOARD.md) fo

## Libraries Tested

### XLSX Profile (12 adapters)
### XLSX Profile (14 adapters)

| Library | Version | Lang | Caps | Green Features |
|:--------|:--------|:-----|:-----|:--------------:|
| [openpyxl](https://openpyxl.readthedocs.io/) | 3.1.5 | Python | R+W | 18/18 |
| [WolfXL](https://github.com/SynthGL/wolfxl) | 2.1.0 | Python (Rust core) | R+W | 19/22 (2026-09-08 snapshot) |
| [openpyxl](https://openpyxl.readthedocs.io/) | 3.1.5 | Python | R+W | 22/22 (2026-09-08 snapshot) |
| [aspose-cells-foss](https://pypi.org/project/aspose-cells-foss/) | 26.7 | Python (JVM-core FOSS) | R+W | 11/22 (2026-09-08 snapshot) |
| [XlsxWriter](https://xlsxwriter.readthedocs.io/) | 3.2.9 | Python | W | 15/18 |
| [xlsxwriter-constmem](https://xlsxwriter.readthedocs.io/) | 3.2.9 | Python | W | 12/18 |
| [openpyxl-readonly](https://openpyxl.readthedocs.io/) | 3.1.5 | Python | R | 3/18 |
Expand All @@ -107,6 +134,18 @@ See the [release snapshot dashboard](results-release-2026-04-28/DASHBOARD.md) fo
| [xlwt](https://github.com/python-excel/xlwt) | 1.3.0 | Python | W | 4/18 |
| [xlrd](https://github.com/python-excel/xlrd) | 2.0.2 | Python | R | .xls only |

> Green-feature counts are per dated snapshot; `x/18` numbers come from the
> 2026-04-29 release snapshot and `x/22` from the 2026-09-08 competitor
> snapshot (Tier 4 added). Never mix counts across snapshots.

### Cross-Language Context (oracle helpers)

| Library | Version | Lang | Caps | Notes |
|:--------|:--------|:-----|:-----|:------|
| Apache POI | 5.x | Java | W | 18/18 scored write lane (2026-04-29) |
| Excelize | 2.x | Go | W | 18/18 scored write lane (2026-04-29) |
| zavora-xlsx | 0.1.2 | Rust | W | Fast writer; 0.1.2 corrupts hyperlink relationships on mutate; no recalc |

### XLS Profile (2 adapters)

| Library | Green Features | Notes |
Expand Down Expand Up @@ -143,6 +182,7 @@ ExcelBench now ships a separate cross-language context snapshot for mature non-P

- `Apache POI` (Java)
- `Excelize` (Go)
- `zavora-xlsx` (Rust writer, added 2026-09-08; helper in `tools/external-oracles/zavora`)

These are not framed as Python drop-in replacements. They answer a different question: how strong is WolfXL relative to serious spreadsheet tooling outside Python?

Expand Down Expand Up @@ -214,6 +254,12 @@ uv sync
# Run the benchmark against pre-built fixtures (no Excel required)
uv run excelbench benchmark --tests fixtures/excel --output results

# Template-mutation suite (wall time, RSS, preservation)
uv run excelbench mutation

# Formula-recalculation tier (cache-free fixture, LibreOffice oracle)
uv run excelbench calc

# Generate the heatmap
uv run excelbench heatmap

Expand All @@ -232,24 +278,30 @@ uv run excelbench generate --output fixtures/excel

## Feature Coverage

### Tested (19 features; 18 currently scoreable in the release snapshot)
### Tested (22 features)

| Tier | Features | Count |
|:-----|:---------|:-----:|
| **Tier 0** -- Core | Cell values, formulas, multiple sheets | 3 |
| **Tier 1** -- Formatting | Text formatting, background colors, number formats, alignment, borders, dimensions | 6 |
| **Tier 2** -- Advanced | Merged cells, conditional formatting, data validation, hyperlinks, images, comments, freeze panes, pivot tables | 8 |
| **Tier 3** -- Workbook metadata | Named ranges, tables | 2 |
| **Tier 4** -- Production surfaces (2026-09-08) | Sheet protection, page setup, chart anchoring | 3 |

> Pivot tables are tested but score N/A across all adapters in the current macOS run.
> Library green-feature scores therefore use an /18 denominator in the fresh release snapshot.
> Green-feature denominators: /18 in the 2026-04-29 release snapshot, /22 in the 2026-09-08 competitor snapshot.

### Planned

Charts, print settings, protection.
None currently queued. Tier 4 (charts anchoring, print settings, protection) shipped 2026-09-08.

## Detailed Results

- **[Competitor snapshot fidelity](results-2026-09-08/xlsx/README.md)** -- 22 features x 14 Python adapters, WolfXL 2.1.0 + aspose-cells-foss
- **[Competitor snapshot heatmap](results-2026-09-08/xlsx/heatmap.png)** ([SVG](results-2026-09-08/xlsx/heatmap.svg)) -- 22x14 visual score matrix
- **[Competitor snapshot mutation](results-2026-09-08/mutation/README.md)** -- template mutation: wall time, RSS, preservation
- **[Competitor snapshot calc](results-2026-09-08/calc/README.md)** -- 133-formula financial DAG vs LibreOffice oracle
- **[Competitor snapshot zavora cross-language](results-2026-09-08/cross-language/README.md)** -- zavora-xlsx 0.1.2 Rust writer context
- **[XLSX results](results/xlsx/README.md)** -- per-library, per-test-case breakdowns with tier list
- **[Release snapshot results](results-release-2026-04-28/README.md)** -- fresh wheel-backed WolfXL 2.0 rerun
- **[XLS results](results/xls/README.md)** -- legacy format results
Expand Down
26 changes: 25 additions & 1 deletion TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
> self-contained sprint (one branch, one PR, one row flip). Resume cold by reading this file
> and the most recent `[*INCOMPLETE*]` marker.

**Last updated**: 2026-04-27 (S3 shipped)
**Last updated**: 2026-09-08 (competitor snapshot landed; S4+ perf sprints still queued)

## Status Table

Expand All @@ -17,6 +17,7 @@
| S5 | Workbook complexity perf | Planned | M (1 wk) | `feat/perf-complexity` | — | — |
| S6 | Cold-start / warm path | Planned | S (3–5 d) | `feat/perf-cold-start` | — | — |
| S7 | Round-trip fidelity (LibreOffice) | Planned | L (~2 wk) | `feat/fidelity-roundtrip` | — | — |
| C1 | Competitor snapshot: aspose-foss + zavora adapters, Tier 4, mutation + calc lanes | Shipped (branch) | L | `feat/competitor-snapshot-2026-09` | — | this branch |

**Status legend**: `Planned` → `In Progress` → `Shipped` (or `Blocked` with reason).

Expand Down Expand Up @@ -182,3 +183,26 @@ Use this template when appending to **Acceptance Notes** below.
- Decisions: [`decisions.md`](decisions.md)
- Key seams: `src/excelbench/perf/runner.py`, `src/excelbench/harness/adapters/base.py`,
`src/excelbench/results/html_dashboard.py`.

## Session Log: Competitor Snapshot (2026-09-08)

Branch `feat/competitor-snapshot-2026-09` (this branch).

**Shipped**:
- `aspose-cells-foss` adapter (Python lane, `aspose` extra) — 11/22 green.
- `zavora-xlsx` external oracle adapter (cross-language lane) + Rust helper in
`tools/external-oracles/zavora`.
- Tier 4 features: sheet_protection, page_setup, chart_anchor (22 scored
features), openpyxl-structural fixtures.
- wolfxl adapter ported to WolfXL 2.1.0 public API (19/22; CF read 0,
named-range/print-title writes 2).
- Template-mutation lane (`excelbench mutation`): wolfxl 100% preservation;
openpyxl/aspose 60%; zavora 0.1.2 integrity-failed.
- Calc tier (`excelbench calc`): cache-free 133-formula fixture, LibreOffice
oracle; LO 133/133, wolfxl 55/133 (power-operator family returns None),
aspose 25/133, zavora 0/133.
- Snapshot artifacts: `results-2026-09-08/{xlsx,mutation,calc,cross-language}`.
- Decisions DEC-023..DEC-026.

**Verification**: full pytest suite + ruff on this branch (see PR body for
exact receipts).
Loading
Loading