Skip to content

Commit 9a475a8

Browse files
fix: harden OOXML boundaries and atomic generation
1 parent 1b0afdf commit 9a475a8

38 files changed

Lines changed: 792 additions & 120 deletions

.github/workflows/ci.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ jobs:
88
# Model-free lane: lint + typecheck (informational) + the full pytest suite.
99
# External renderers (soffice/pdftoppm) are absent here, so the visual QA
1010
# tests degrade/skip exactly as on a developer machine without LibreOffice.
11-
# The matrix enforces the supported-Python floor (pyproject requires-python
12-
# ">=3.10"): the suite must pass on 3.10 (the floor), 3.11 and 3.12.
11+
# The matrix enforces the complete supported range declared by pyproject:
12+
# 3.10 (the floor), 3.11, 3.12 and 3.13.
1313
test:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
python-version: ["3.10", "3.11", "3.12"]
18+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1919
env:
2020
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
2121
steps:
@@ -24,14 +24,15 @@ jobs:
2424
with:
2525
python-version: ${{ matrix.python-version }}
2626
cache: pip
27-
cache-dependency-path: requirements.txt
28-
- run: python -m pip install -r requirements.txt pytest
29-
# Lint gate: pinned to the version used locally so CI and dev agree. The
27+
cache-dependency-path: |
28+
requirements.txt
29+
requirements-ci.txt
30+
- run: python -m pip install -r requirements-ci.txt
31+
# Lint gate: requirements-ci.txt pins the version used locally. The
3032
# tracked ruff.toml (defaults + line-length 88, target py310) is the source
3133
# of truth; both commands must be green before pytest runs.
3234
- name: Ruff lint
3335
run: |
34-
python -m pip install ruff==0.15.2
3536
ruff check .
3637
ruff format --check .
3738
# Typecheck is informational only: the engine is lxml-Optional-heavy and
@@ -40,8 +41,7 @@ jobs:
4041
- name: Mypy (informational)
4142
continue-on-error: true
4243
run: |
43-
python -m pip install mypy
44-
mypy scripts/brandkit || true
44+
mypy scripts/brandkit
4545
- name: Tests
4646
run: PYTHONPATH=scripts python -m pytest -q
4747

@@ -61,7 +61,7 @@ jobs:
6161
- uses: actions/setup-python@v6
6262
with:
6363
python-version: "3.11"
64-
- run: python -m pip install -r requirements.txt pytest
64+
- run: python -m pip install -r requirements-ci.txt
6565
- name: Install LibreOffice and poppler
6666
run: |
6767
sudo apt-get update

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,38 @@
22

33
All notable changes to BrandDocs are documented in this file.
44

5+
## [Unreleased]
6+
7+
### Security
8+
9+
- The live extract, generate, deterministic-QA and visual-render boundaries now
10+
validate OOXML packages before third-party parsers see them. Archive size,
11+
inflated part/package size, entry count, compression ratio, duplicate members,
12+
unsafe paths and the required content-types part are enforced centrally, so the
13+
existing decompression-bomb defense is no longer limited to an unused unpacker.
14+
15+
### Fixed
16+
17+
- XLSX generation now restores the template's x14 sparkline groups after the
18+
openpyxl save, and the new fail-closed `extension_survival` L0 check detects any
19+
worksheet extension family/count lost between shell and output.
20+
- `generate` validates schema/resolvers/provenance before authoring and writes to a
21+
same-directory temporary file, publishing with an atomic replace only after QA
22+
passes. Failed runs no longer overwrite an existing good output or print it as
23+
generated.
24+
- Whole-row/whole-column Excel names are bounded to the worksheet's used rectangle;
25+
discontiguous names fail with a clear error instead of being silently truncated;
26+
XLSX text scans and demo clearing visit materialized cells only.
27+
- Fixture-builder import/build errors now fail CI instead of becoming skips, and
28+
the example DOCX builder uses character-style names instead of deprecated
29+
style-id lookup.
30+
31+
### Changed
32+
33+
- CI/dev dependencies are pinned in `requirements-ci.txt`, warnings are errors
34+
except for the explicitly accounted-for openpyxl sparkline warning, and the
35+
tested/supported Python range is now explicit at 3.10-3.13.
36+
537
## [0.10.0] - 2026-06-10
638

739
Hero-image polish release.

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ to keep that promise true while many hands touch the code.
1111
git clone https://github.com/ferdinandobons/brand-docs.git
1212
cd brand-docs
1313
python3 -m venv .venv && . .venv/bin/activate
14-
pip install -r requirements.txt pytest ruff
14+
pip install -r requirements-ci.txt
1515
```
1616

1717
Optional but recommended, the visual QA renderers (LibreOffice + Poppler,
@@ -30,7 +30,7 @@ Without them the engine still works; visual QA degrades to deterministic-only
3030
The suite has three lanes. The first is the one every PR must keep green:
3131

3232
```bash
33-
# 1) Full model-free suite (what CI runs on 3.10 / 3.11 / 3.12)
33+
# 1) Full model-free suite (what CI runs on 3.10 / 3.11 / 3.12 / 3.13)
3434
PYTHONPATH=scripts python -m pytest -q
3535

3636
# 2) Real-render lane (needs LibreOffice + Poppler; CI runs it in its own job)

CONVENTIONS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,11 @@ saved profile's history). New ids follow `{entity}_{predicate}` in lowercase sna
378378
deviate from the pattern (e.g. `appearance_table_targets`, `schema`) are grandfathered,
379379
not a template to copy.
380380

381+
`extension_survival` is a load-bearing XLSX structural diff: every worksheet
382+
extension URI/count present in the shell must survive generation. The generator
383+
restores self-contained sparkline extensions at the raw OOXML layer; an unsupported
384+
extension that cannot be preserved fails explicitly rather than disappearing.
385+
381386
---
382387

383388
## 10. Units

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
**BrandDocs is a set of agent skills that learn your existing Word, PowerPoint and Excel templates and generate new on-brand documents from them.** Unlike generic AI document generators, it preserves **brand, structure, styles and formulas by construction**. Built for Claude Code, Codex and compatible AI agents.
1010

1111
[![License: MIT](https://img.shields.io/badge/License-MIT-3B82F6.svg)](LICENSE)
12-
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
12+
[![Python 3.10-3.13](https://img.shields.io/badge/python-3.10--3.13-3776AB.svg)](https://www.python.org/)
1313
[![CI](https://github.com/ferdinandobons/brand-docs/actions/workflows/ci.yml/badge.svg)](https://github.com/ferdinandobons/brand-docs/actions/workflows/ci.yml)
1414
[![Website](https://img.shields.io/badge/website-GitHub%20Pages-16A34A.svg)](https://ferdinandobons.github.io/brand-docs/)
1515
[![Latest release](https://img.shields.io/github/v/release/ferdinandobons/brand-docs?label=latest%20release)](https://github.com/ferdinandobons/brand-docs/releases/latest)
@@ -54,13 +54,18 @@ All three share one engine and expose the same verbs: **`extract` → `comprehen
5454

5555
**Two-phase by design:** the deterministic engine works with **no model at all** (extract / verify / generate, fully offline); the model-assisted verbs sit ON TOP and can only NAME captured facts - every proposal is validated fail-closed, so the brand guarantee never depends on a model being right.
5656

57+
**Published only after proof:** generation preflights the profile and shell, writes
58+
to a same-directory temporary file, and atomically publishes the requested output
59+
only after deterministic QA passes. XLSX also preserves formulas and raw worksheet
60+
extensions such as sparkline groups; extension loss is a blocking QA error.
61+
5762
---
5863

5964
## Prerequisites
6065

6166
BrandDocs runs locally and needs a few things installed **before first use**:
6267

63-
- **Python 3.10** plus the packages in [`requirements.txt`](requirements.txt) (`python-docx`, `python-pptx`, `openpyxl`, `lxml`, `Pillow`):
68+
- **Python 3.10-3.13** plus the packages in [`requirements.txt`](requirements.txt) (`python-docx`, `python-pptx`, `openpyxl`, `lxml`, `Pillow`):
6469
```bash
6570
pip install -r requirements.txt
6671
```

SECURITY.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ BrandDocs is in **alpha** (`0.x`). Security fixes are applied to the latest
77

88
| Version | Supported |
99
|---------|-----------|
10-
| `0.1.x` ||
11-
| `< 0.1` ||
10+
| Latest `0.x` release ||
11+
| Older `0.x` releases ||
1212

1313
## Reporting a vulnerability
1414

@@ -40,4 +40,8 @@ BrandDocs parses untrusted Office (OOXML) files. Areas of particular interest:
4040

4141
The engine is self-contained (`python-docx` / `python-pptx` / `openpyxl` +
4242
`lxml`) with no cloud calls, which limits exposure, but template input is still
43-
untrusted and handled accordingly.
43+
untrusted and handled accordingly. Every live extract/generate/QA/render entrypoint
44+
validates the OOXML ZIP metadata before handing bytes to a third-party parser:
45+
compressed and inflated size ceilings, entry-count and per-part limits, duplicate
46+
parts, unsafe paths, compression ratio, and the required content-types part are
47+
checked centrally.

documentation/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ which were degraded, and what to repair next.
106106

107107
| Layer | What it proves | What happens on failure |
108108
|---|---|---|
109-
| **Preflight** | `doctor` checks required Python packages, optional renderers (`soffice`, `pdftoppm`, PyMuPDF/`fitz`), and optional OCR (`tesseract`) before work starts. | Missing required packages must be installed/repaired. Missing visual/OCR tools downgrade only that proof layer. |
110-
| **L0 deterministic QA** | Schema validity, resolver targets, allowed styles/layouts/ranges, residual demo text, markdown leaks, structural diffs, formula preservation. | The gate fails or emits explicit findings before the output is treated as clean. |
109+
| **Preflight** | `doctor` checks dependencies; the shared OOXML boundary checks archive/part sizes, entry count, duplicate/unsafe paths and compression ratio before any Office parser or renderer opens a file. Generate also validates profile, resolver and shell provenance before authoring. | Missing dependencies must be repaired; unsafe packages and invalid/drifted profiles fail before an output is created. Missing visual/OCR tools downgrade only that proof layer. |
110+
| **L0 deterministic QA** | Schema validity, resolver targets, allowed styles/layouts/ranges, residual demo text, markdown leaks, structural diffs, formula preservation, and XLSX worksheet-extension survival. | The gate fails before the temporary output is atomically published; a failed run cannot replace a previous good deliverable. |
111111
| **L1 visual proxies** | Rendered-page signals such as blank pages, zero pages, content near page/slide edges, and optional OCR hits for visible residual template text. | Findings are warnings because the engine can detect symptoms, not intent. |
112112
| **L2 visual judgement** | The orchestrator opens the PNGs from `visual_manifest.json`, judges checklist items, and decides whether the result is visually acceptable. `strict` turns unclean visual evidence into gate errors. | Apply a targeted repair, regenerate, and rerun `--qa deep` or `--qa strict` until clean or honestly blocked. |
113113

documentation/DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
```bash
44
python3 -m venv .venv && . .venv/bin/activate
5-
pip install -r requirements.txt pytest ruff
5+
pip install -r requirements-ci.txt
66
PYTHONPATH=scripts pytest -q # docx / pptx / xlsx / security / integration / smoke suites
77
```
88

documentation/INSTALLATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ skills to your agent.
1717

1818
### Required: core extract / generate / deterministic QA
1919

20-
- **Python 3.10**: Python packages (from [`requirements.txt`](../requirements.txt)):
20+
- **Python 3.10-3.13**: Python packages (from [`requirements.txt`](../requirements.txt)):
2121
`python-docx>=1.1`, `python-pptx>=1.0`, `openpyxl>=3.1`, `lxml>=5.0`, `Pillow>=10.0`
2222

2323
```bash

documentation/PLUGIN_WORKFLOW.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ generated/<job>/
208208
| Native object loss | `component_survival` warning | ensure the native writer is used (charts, SmartArt, merged tables, docx TOC); explain only a genuinely unsupported object (e.g. a `divider` on pptx, which has no native form) |
209209
| XLSX range overflow | named range bounds error | split data, shrink input, or ask user to expand template range |
210210
| Formula loss | formula preservation finding | stop and repair generator; never ship silently |
211+
| XLSX extension loss | `extension_survival` error | restore the supported raw OOXML extension or reject the output; never ship a workbook with silently removed native objects |
211212

212213
## Round Budget Policy
213214

0 commit comments

Comments
 (0)