Docxodus
CLAUDE.md115 starsChanged 39 days agoMITView on GitHub
Guidance for Claude Code (claude.ai/code) working in this repository. This file is a map, not a manual. It covers what you cannot discover quickly from the code: repository layout, which surfaces a change must ripple through, build gotchas, and where the real documentation lives. Per-module design detail belongs in docs/architecture/ — see the index below. Do not re-document a subsystem here; a duplicated description is one that will go stale. - Never credit yourself in commits. Do not add "Generated…
- Installs packages
- Commits and pushes
In this repositoryOne other file it gives its agents.
- docs/llms.txtllms.txt
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
19 rules. About 0 are common, 9 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
Never credit yourself in commits: Do not add "Generated with Claude Code" or "Co-Authored-By: Claude" to commit messages.
- Distinctive: line 15
Review adversarially before opening a PR: Every PR gets one round of adversarial review of its own diff before it is opened: read it as a …
- Very specific: line 108
When a public DocxSession method or setting changes, update Docxodus/Internal/DocxSessionOps.cs first — both bridges and both clients pick …
- Very specific: line 144
Defaults are declared once: Read them from the settings object rather than repeating literals per surface — DocxSessionJson.ParseSettings …
- Very specific: line 211
When touching image/font/color code, check that it compiles under WASM_BUILD — the npm build fails loudly if it doesn't.
- Distinctive: line 254
Read the linked doc before non-trivial changes.
- Distinctive: line 306
Don't reintroduce either category.
- Distinctive: line 329
Prove it first — for bugs and technical gaps: When the issue reports a defect or a technical gap (wrong output, a crash, a spec or …
- Distinctive: line 338
Fix the cause once: When a defect has several symptoms or call sites, fix the point they share rather than adding a branch per site, and …
- Distinctive: line 342
Keep the PR diff as short as the fix allows: prefer a succinct rewrite of existing code over another special case, and share fixture setup …
- Distinctive: line 347
Stay in scope: No unrelated fixes or refactors in the change.
- Very specific: line 351
Pin it in the fidelity bench: Every defect reproduced in step 1 also gets authored fixtures in JSv4/docx-fidelity-bench, so every Docxodus …
- Very specific: line 361
When our output differs from Word or LibreOffice, document the finding in docs/ooxml_corner_cases.md.
- Distinctive: line 422
Create annotated tags at that exact commit and push them explicitly:
- Very specific: line 432
Create the GitHub Release with prepared notes: gh release create vX.Y.Z --title vX.Y.Z --notes-file <body.md> --latest --verify-tag.
- Very specific: line 434
If release-assets already created it, use gh release edit with the notes file and title instead.
- Very specific: line 452
npm staging is not publication: Read the npm packages awaiting 2FA approval summary in the Publish run, review both stage IDs, then approve …
- Very specific: line 460
Verify all five NuGet packages, both npm versions (including export's exact peer dependency), and docx-scalpel==A.B.C on PyPI.
- Very specific: line 486
After both npm packages are approved, fetch the versioned embed.bundle.js and embed.iife.js from jsDelivr successfully.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
Guidance for Claude Code (claude.ai/code) working in this repository.
This file is a map, not a manual. It covers what you cannot discover quickly from the
code: repository layout, which surfaces a change must ripple through, build gotchas, and
where the real documentation lives. Per-module design detail belongs in
docs/architecture/ — see the index below. Do not re-document a subsystem here; a
duplicated description is one that will go stale.
Important Instructions
- Never credit yourself in commits. Do not add "Generated with Claude Code" or "Co-Authored-By: Claude" to commit messages.
- Review adversarially before opening a PR. Every PR gets one round of adversarial review of its own diff before it is opened: read it as a hostile reviewer would (missed cases, other call paths, content the change could corrupt, tests that would still pass without the fix), then fix every finding that is more than a nit before opening. Nits may ride along or be left, at your discretion. Say in the PR description that the round happened and what it changed.
- Write PR descriptions for a human reviewer, not for the next agent. Explain, in plain language, why the change exists, how it works at a mechanism level, and how it was validated. Don't invent terminology, lean on internal issue-number shorthand, or use jargon a reader outside the change wouldn't recognize — a reviewer who hasn't read the code should be able to follow the description on its own.
- Never put a Microsoft copyright header on a new file.
// Copyright (c) Microsoft. All rights reserved.belongs only on the 41 files that are genuine derivatives of the original MicrosoftOpenXmlPowerToolssource —DocumentBuilder.cs,PtOpenXmlUtil.cs,WmlToHtmlConverter.cs, theGetListItemText_*.cslocale files and their like-named tests among them. Everything else in this repo —DocxSession,History/,Verification/,Delivery/,Internal/,Ir/, the editor, the MCP server, the npm/Python/WASM layers, and every other file with no ancestor in OfficeDev/Open-Xml-PowerTools — is original work and must not claim Microsoft's copyright. Give a new file no header at all, or// Copyright (c) John Scrudato IV. All rights reserved.if you want the same two-line header form the rest of the library uses. This was a real bug, not a hypothetical: ~195 files had the Microsoft line copy-pasted onto wholly new post-fork source before a dedicated pass corrected them. The enforced keep-list lives inDocxodus.Tests/SourceFileCopyrightTests.cs, which asserts set equality in both directions — edit that list, not a copy of it here, when a file's lineage genuinely changes. - Never add a
#nullable enableor#nullable disabledirective to a file.Docxodus.csprojalready sets<Nullable>enable</Nullable>project-wide (see Nullable Reference Types below), so a per-file directive is never correct:#nullable enableis a redundant no-op and#nullable disableis a regression. New and refactored files get neither.SourceFileCopyrightTestsasserts thedisablehalf.
Coding Standards
Nullable Reference Types
Docxodus.csproj sets <Nullable>enable</Nullable> (issue #13): every file is
nullable-checked by default, so a new file needs no directive — not #nullable enable (redundant)
and not #nullable disable (a regression). The inherited
OpenXmlPowerTools core has fully migrated (issue #645): no file under Docxodus/
carries a #nullable disable header anymore. grep -l "^#nullable disable" Docxodus/*.cs
should return nothing — reintroducing the header on a new or refactored file is a
regression, not a shortcut. Docxodus.csproj no longer has a NoWarn property at all
(issue #651) — CS8632, CS8073 and CA2200 are gone along with the last opted-out
file that could fire them.
Warnings
Directory.Build.props sets TreatWarningsAsErrors=true for Release, and the core library
builds with zero warnings in both default and WASM_BUILD mode, so any new library warning fails
CI's Release build (ci.yml), the WASM build and publishing. Fix the warning; don't suppress it
locally or re-add the opt-out. Debug builds don't fail on warnings, so check with
dotnet build Docxodus/Docxodus.csproj -c Release --no-incremental before pushing.
rules.ruleset turns off two StyleCop rules that contradict the codebase's conventions, each with
its reason inline: SA1206 (the code writes public required) and SA1633 (new files may have no
header — see the copyright-attribution rule above; SourceFileCopyrightTests enforces attribution).
Line endings
Library output breaks lines with \n on every OS. Docxodus/BannedSymbols.txt bans the APIs that
default to Environment.NewLine (AppendLine, XNode.ToString, XDocument.Save, new
XmlWriterSettings, JSON indentation, …) as error RS0030 in every configuration; use the
replacements in Docxodus/Internal/LineEndings.cs.
Docxodus.Tests.csproj still opts out with TreatWarningsAsErrors=false: the test project builds
with 434 warnings, nearly all nullable-reference warnings in test code. Don't add to that
number. Measure with --no-incremental, because a warm incremental build reports zero.
Repository Layout
This is not just a .NET library — it ships a multi-layer stack. Public-surface changes usually ripple through all of it.
| Layer | Path | Purpose |
| Core library | Docxodus/ | All WordprocessingML logic. NuGet package Docxodus. |
| Shared facades | Docxodus/Internal/{DocxSessionOps,DocxDiffOps,HtmlConversionOps,SessionRegistry,DocxSessionJson}.cs | Single-owner op + wire-shape layer every transport routes through. |
| Delivery | Docxodus/Delivery/ | Delivery bundles: manifest, publisher, revision policy, host renderer. See delivery_bundle.md. |
| Verification | Docxodus/Verification/ | Package manifests, semantic diff, redline-reversibility proof, deliverable inspection. See deliverable_verification.md, package_manifests.md, semantic_diff.md, redline_reversibility_proof.md. |
| Unit tests | Docxodus.Tests/ | xUnit, ~3,900 tests, ~4 min. |
| CLI tools | tools/redline/, tools/docx2html/, tools/docx2oc/ | Thin dotnet tool wrappers. Release binaries are trimmed; see cli_trimming_and_native_aot.md. |
| WASM bridge | wasm/DocxodusWasm/ | [JSExport] shells over the facades. |
| Stdio host | tools/python-host/ | NDJSON-over-stdin host (docxodus-pyhost) that the docx-scalpel pip package subprocesses. |
| Agent server | tools/mcp-server/ | NuGet package Docxodus.McpServer. JSON-RPC 2.0 / MCP stdio server (docxodus-mcp): lifecycle, grouped-intent, and sessionless tools. See docs/architecture/docx_agent_server.md. |
| Python client | python/ | docx-scalpel on PyPI. |
| npm/TypeScript | npm/ | Browser package: src/core.ts (engine API), src/index.ts (browser barrel), src/editor.ts (block editor), src/ribbon.ts (shipped UI surface), src/embed.ts (CDN entries), src/react.ts, worker proxy. |
| Export package | npm-export/ | @docxodus/export — deterministic standalone HTML + PDF through a pinned Chromium. Has its own dist/, tests, and docxodus doctor preflight. See standalone_paginated_export.md. |
| Workflow evals | eval/ + Docxodus.Tests/Eval/ | Deterministic document-workflow scenarios scored on task completion, target precision, and collateral change. Corpus and contract in eval/README.md. |
| Pages demo | docs/demo/ | Static pages hosting the shipped surface via createRibbonEditor. They contain no editor UI of their own — change npm/src/ribbon.ts, not these files. |
| Out-of-solution tools | tools/diffharness/, tools/manifest-fuzz/, tools/screenshots/, tools/op-codegen/, benchmarks/ | Not in Docxodus.sln; build them by path (the test project references op-codegen). LibreOffice-backed diff verification, AFL manifest fuzzing, README screenshot capture, the session-plumbing generator, a form-document edit benchmark, and a DocxDiff perf/output-parity stress harness. |
The single-owner rule
When a public DocxSession method or setting changes, update Docxodus/Internal/DocxSessionOps.cs
first — both bridges and both clients pick the change up from there. Then ripple outward.
The same pattern governs the stateless surfaces: HtmlConversionOps owns DOCX→HTML,
DocxDiffOps owns the DocxDiff engine, and DocxCompare owns the
the shared comparison front door (its input-revision policy). Change the facade first, then the two bridges
and two clients.
Ripple checklist
| Change type | Core | Tests | *Ops facade | WASM | npm/TS | stdio + Python | MCP | Docs |
| New setting or method | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| New public enum | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Bug fix | ✓ | ✓ | – | – | – | – | – | CHANGELOG |
| Internal refactor | ✓ | ✓ | – | – | – | – | – | – |
Concretely, a new session op touches: the matching DocxSession.<Area>.cs partial → DocxSessionOps.cs →
DocxSessionJson.cs → wasm/DocxodusWasm/DocxSessionBridge.cs → npm/src/types.ts +
npm/src/core.ts → tools/python-host/Dispatcher.cs → python/src/docx_scalpel/{types,session}.py
→ tools/mcp-server/{ToolCatalog,Dispatcher}.cs → its family's description in tools/op-descriptions/
(SessionOpDescriptionDriftTests fails until the op is described or listed in not-described.json).
Described ops are a ripple site. An op family with a file in tools/op-descriptions/ is
checked against every transport by SessionOpDescriptionDriftTests; change the description in the
same change as the op. A family marked "generate": true (so far comments) has its stdio and MCP
argument parsing, MCP schema properties and WASM shims generated from the description: edit the
description, then run dotnet run --project tools/op-codegen; GeneratedSessionPlumbingTests fails on
stale output. See docs/architecture/session_op_descriptions.md.
The generated-PDF benchmark is a ripple site for PageMap shape.
npm/tests/visual-parity/pdf-result.ts validates the supported convertDocxToPdf envelope with
exact key matching on pageMap, each page, and each fragment, so adding an optional field to any
of them fails the benchmark from a test file none of the tables above name. Update it in the same
change.
Defaults are declared once. Read them from the settings object rather than repeating
literals per surface — DocxSessionJson.ParseSettings and the MCP dispatcher had each
hardcoded their own copy of undoDepth, and it drifted.
Build Commands
dotnet build Docxodus.sln # needs the wasm-tools workload
dotnet build Docxodus/Docxodus.csproj # library only, no workload needed
dotnet build -c Release Docxodus.sln
./scripts/build-wasm.sh # WASM target (sets WASM_BUILD, excludes SkiaSharp)
./scripts/record-aot-profile.sh # re-record wasm/DocxodusWasm/docxodus.aotprofile (~4 min)
cd npm && npm run build # build-wasm.sh + tsc + esbuild bundlesA full-solution build requires dotnet workload install wasm-tools. Without it, build
Docxodus/Docxodus.csproj and Docxodus.Tests/Docxodus.Tests.csproj directly.
Test Commands
dotnet test Docxodus.Tests/Docxodus.Tests.csproj
dotnet test --filter "FullyQualifiedName~DB001_DocumentBuilderKeepSections" # test ids are feature-prefixed
dotnet test --filter "FullyQualifiedName~DbTests"
cd npm
npm install && npx playwright install chromium # first time only
npm run build # produces dist/ that the harness loads
npm test # all Playwright specs: the pool lane, then the serial lane
npm test -- worker.spec.ts # one spec, whichever lane it is in
npm run test:lanes # the lane split still partitions the spec files
npx playwright test --grep "Document Structure"
npx tsc --noEmitPlaywright serves from npm/dist/wasm/. If you edit C#, .ts, or the harness HTML,
re-run npm run build first or you will test stale artifacts.
Playwright lanes. npm test runs the suite as two lanes: the pool (every ordinary spec, on
several workers) and the serial lane (one worker) for the specs that assert a latency, throughput
or frame-rate bound, which another worker's WASM boot would inflate. A new spec of that kind goes in
serialLaneSpecs in npm/scripts/playwright-lanes.mjs; npm run test:lanes fails when a spec file
is in both lanes or neither. CI runs each lane as its own job with DOCXODUS_PLAYWRIGHT_LANE, the
pool as twelve shards balanced by npm/scripts/playwright-spec-durations.json (refresh it with
scripts/record-spec-durations.mjs when the suite's shape changes; test:lanes fails if it names a
deleted spec), and history-client.spec.ts in its own job with the history-file check.
Allocation gate. On pull requests that touch Docxodus/, perf.yml runs the DocxDiff stress
and complex-form benchmarks against the base commit's library and the PR's, and fails when a case
allocates more than 10% above base (time is reported, not gated). Reproduce it with
scripts/perf-run.sh origin/main /tmp/perf-results; details in benchmarks/docxdiff-stress/README.md.
The .NET suite no longer dirties TestFiles/ — the SH* spreadsheet tests that used to
write back to their own committed .xlsx fixtures were removed with the SpreadsheetML
modules. If git status shows modified fixtures after a run, a test is writing where it
should be using a temp copy; fix the test rather than reflexively git checkout -- TestFiles/.
WASM Conditional Compilation
The library compiles in two modes, controlled by the WASM_BUILD MSBuild property
(set by scripts/build-wasm.sh):
- Default: includes
SkiaSharp+SkiaSharp.NativeAssets.Linux.NoDependencies. WASM_BUILD=true: defines theWASM_BUILDconstant and excludes SkiaSharp (no native deps in the browser). Code needing SkiaSharp must be guarded with#if !WASM_BUILDor routed through a no-op fallback.
When touching image/font/color code, check that it compiles under WASM_BUILD — the npm
build fails loudly if it doesn't.
Output isolation: the WASM-mode assembly builds into Docxodus/bin/wasm/ +
Docxodus/obj/wasm/ (see the WASM_BUILD PropertyGroup in Docxodus.csproj) so it can never
clobber the default-mode assembly — a solution build compiles Docxodus twice. If you ever see
error CS1061: 'ImageInfo' does not contain a definition for 'SaveImage', a stale
pre-isolation artifact is lingering: delete Docxodus*/bin + Docxodus*/obj once.
Core Concepts
Document wrappers. DocxodusDocument (base, holds DocumentByteArray + FileName) with
WmlDocument for Word. Immutable-style manipulation goes through
OpenXmlMemoryStreamDocument:
using (var streamDoc = new OpenXmlMemoryStreamDocument(doc))
{
using (var document = streamDoc.GetWordprocessingDocument()) { /* modify */ }
return streamDoc.GetModifiedWmlDocument();
}Anchors. kind:scope:unid (e.g. p:body:a1b2c3d4) is the addressing system shared by the
markdown projection, DocxSession, DocxDiff revisions, and the editor's data-anchor
attributes. It is an addressing overlay over the live OOXML, which remains the model of record
— there is no IR→OOXML writer.
Comparison engine. DocxDiff is the only comparison engine. WmlComparer was removed in
v11.0.0; its cross-package merge helpers survive as PackageMerge.cs (styles, numbering and
related-part copying — never comparison logic), which the markup renderers call. The parity
evidence gathered before removal is frozen under
docs/architecture/wmlcomparer_parity_baseline/, and DocxDiffCorpusBaselineTests is the live
regression net that replaced the differential harness.
DocxCompare vs DocxDiff. DocxCompare.Compare is the front door every transport routes
through; it always applies PreAcceptInputRevisions (compare the accepted view of each input), which the
raw DocxDiff API leaves opt-in. It does not preserve the inputs' own revisions — see
docs/ooxml_corner_cases.md, "DocxCompare: the original document's own pending changes". Calling DocxDiff.Compare with fresh settings is NOT equivalent —
on revision-bearing inputs it emits whole-document churn.
Module Map
Read the linked doc before non-trivial changes. These docs are the source of truth for design detail; this file deliberately does not restate them.
| Module | What it does | Design doc |
DocxSession*.cs | Stateful anchor-addressed editing API (text, structural, formatting, tables, notes, comments, revisions, annotations, raw XML, undo/redo). One partial class split by area: state, lifecycle and shared mutation helpers in DocxSession.cs, public value types in DocxSession.Types.cs, each op family in its own DocxSession.<Area>.cs | docx_mutation_api.md |
DocxDiff.cs + Ir/Diff/ | Structure-aware comparison → native tracked changes; edit script as data; N-way consolidate | ir_diff_engine.md |
WmlToHtmlConverter.cs | DOCX → HTML, the render fidelity oracle | docx_converter.md, comment_rendering.md, paginated_headers_footers.md, wml_to_html_converter_gaps.md |
HtmlToWmlConverter.cs | HTML → DOCX | — |
WmlToMarkdownConverter.cs | Anchor-addressed markdown projection | markdown_projection.md |
npm/src/editor.ts + ribbon.ts | In-browser block editor and its shipped UI surface | ir_editor_feasibility.md, ir_editor_roadmap.md, editor_ui_surface.md |
ExternalAnnotationProjector.cs | Incremental annotation overlay on pre-converted HTML | incremental_annotation_overlay.md, custom_annotations.md |
OpenContractExporter.cs | Export to OpenContracts format | opencontracts_export.md |
DocumentBuilder.cs | Merge / split DOCX | — |
OpenXmlRegex.cs | Regex search/replace in DOCX | — |
RevisionProcessor.cs | Accept/reject tracked revisions | tracked_changes.md |
FormattingAssembler.cs | Resolve and flatten formatting | — |
MetricsGetter.cs | Document metrics (styles, fonts, languages) | — |
PageMap.cs + npm/src/pagination.ts | Page geometry and the paginated view | page_map.md, paginated_headers_footers.md |
npm/src/markup-balloons.ts + Internal/WordFormatWording.cs | Word's All Markup balloon print layout (commentColumnPlacement: "balloons") | all_markup_layout.md |
Ir/ | The structure-aware IR the diff and markdown projection read | document_ir.md |
Delivery/ | Delivery bundles and change receipts | delivery_bundle.md, delivery_change_receipt.md |
Verification/ | Package manifests, semantic change sets, reversibility proof | deliverable_verification.md, package_manifests.md, semantic_diff.md, redline_reversibility_proof.md |
npm-export/ | Standalone HTML/PDF export via pinned Chromium | standalone_paginated_export.md |
Editor internals: editor_block_drag_handles.md, editor_inline_formatting_on_edit.md,
native_content_controls.md, native_images.md, unsupported_content_placeholders.md,
browser_llm_demo.md. Diff/comparison detail: docxdiff_libreoffice_findings.md,
token_stream_arrangement.md. Smoke-test contracts: s1_smoke_test_features.md,
epic_435_acceptance_smoke.md.
WASM/browser work: wasm-packaging.md (trimming, profile-guided AOT, Brotli, size budget,
measured payload and speed frontier), ui_responsiveness.md. Python wrapper:
python_docxodus.md.
Scope: the DOCX toolchain, nothing else
Docxodus handles .docx/.docm/.dotx/.dotm, and every module in Docxodus/ earns its
place by serving DocxSession, DocxDiff, or the render/projection paths.
Two rounds of the inherited OpenXmlPowerTools fork were removed on that rule — see the
### Removed entry in CHANGELOG.md for the full list:
- Non-Wordprocessing formats.
SpreadsheetWriter,WorksheetAccessor,SmlToHtmlConverter,XlsxTables,ChartUpdater,PresentationBuilder,TextReplacer, and theSmlDocument/PmlDocumentwrappers.GetDocumentType()still recognises XLSX and PPTX packages so that feeding one in throws a clearPowerToolsDocumentExceptioninstead of failing deeper in the stack. - DOCX code with no callers.
WmlToXml(DOCX → custom XML),DocumentAssembler(content-control templating),ReferenceAdder(TOC/TOF/TOA fields, plus theWmlDocumentpartial that exposed them),PowerToolsBlock,PowerToolsBlockExtensions,StronglyTypedBlock, andOxPtHelpers.
Don't reintroduce either category. Before adding a module here, ask which of the three engines it serves; if the answer is "none", it belongs somewhere else.
The TestFiles/DA*.docx fixtures outlived DocumentAssembler deliberately — they are
content-control-heavy real Word documents, and the Ir* tests glob TestFiles/**/*.docx as
a corpus. Deleting an unreferenced .docx silently shrinks that corpus; check the globs
before you do.
Feature Development Workflow
- CHANGELOG.md — add an entry under
[Unreleased]. - Tests — add to the matching file in
Docxodus.Tests/. ReuseTestFiles/fixtures where possible; programmatic fixtures need all required parts (StyleDefinitionsPart,DocumentSettingsPart, …). - Ripple — follow the checklist above.
- Docs — update
docs/architecture/for significant features, anddocs/ooxml_corner_cases.mdfor any Word-behaviour discovery (see below).
Issue Resolution Workflow
An issue is a claim to verify, not a spec to implement. Work it in this order, then finish with the Feature Development Workflow above.
- Prove it first — for bugs and technical gaps. When the issue reports a defect or a technical
gap (wrong output, a crash, a spec or validator violation, missing coverage of existing
behaviour), reproduce the reported behaviour on current
main(a scratch program or a test) before changing code. If it doesn't reproduce, say so on the issue instead of "fixing" it. An issue that requests a new feature has no existing behaviour to reproduce; skip this step and start at step 2 by writing tests for the requested behaviour that fail until it exists. - Red, then green (TDD). Turn the reproducer into tests that fail for the reported reason, then make them pass. A guard test that passes before the fix proves nothing until a mutated fix makes it fail.
- Fix the cause once. When a defect has several symptoms or call sites, fix the point they share rather than adding a branch per site, and take the architectural route whenever it is also the smaller diff. Reuse or relocate an existing helper before writing a second one; give new code one responsibility. Keep the PR diff as short as the fix allows: prefer a succinct rewrite of existing code over another special case, and share fixture setup instead of duplicating tests. Small diffs must still include readable regressions that fail without the fix and meaningful control cases.
- Tests a reviewer can read. Pair focused unit tests with isolated integration tests through the public entry point. Keep each short, named for the behaviour, and failing without the fix.
- Stay in scope. No unrelated fixes or refactors in the change. File a new issue for an
unrelated find only when it reproduces on
main, affects realistic documents (not invalid OOXML, not unverified speculation) and is at least moderate in severity; group related lesser finds into one checklist issue per area rather than one issue each. - Pin it in the fidelity bench. Every defect reproduced in step 1 also gets authored fixtures in
JSv4/docx-fidelity-bench, so every Docxodus version can be re-scored against it and a regression cannot return unnoticed. Rendering and pagination defects go in its PDF track; comparison defects (DocxDiff,DocxCompare, Consolidate) go in its redline track, which scores accept against the revised document, reject against the original, and schema validity. Add a control case beside each reproducer, follow that repository's fixture-authoring and rights rules, and link the bench PR from the Docxodus PR.
OOXML Corner Cases
When our output differs from Word or LibreOffice, document the finding in
docs/ooxml_corner_cases.md. Word does not always follow the spec, these cases are expensive
to rediscover, and each one should eventually get a test.
Record: a minimal XML reproducer, a Word / LibreOffice / Docxodus comparison table, your analysis of why they differ, the Docxodus code involved, and the fix if known.
Release Process
A coordinated release is release notes + tags + a GitHub Release with binaries + verified registry packages + updated demos. A green Publish run alone is not enough: npm packages still need maintainer approval, and Python has its own workflow.
Release surfaces and versions
| Surface | Version source | Publication and verification |
NuGet: Docxodus, Redline, Docx2Html, Docx2OC, Docxodus.McpServer | vX.Y.Z | publish.yml; verify all five registry versions. The MCP server is packed by scripts/pack-mcp-server.sh, which stamps the version into its .mcp/server.json. |
npm: docxodus, @docxodus/export | vX.Y.Z; export's peer dependency is exactly X.Y.Z | publish.yml stages both packages; approve each with maintainer 2FA, then verify public versions and CDN assets. |
| CLI downloads: redline, docx2html, docx2oc | vX.Y.Z | publish.yml attaches twelve binaries (three tools × four RIDs) and SHA256SUMS to the GitHub Release. |
PyPI: docx-scalpel | docx-scalpel-vA.B.C and python/pyproject.toml | python-publish.yml; verify four platform wheels and one sdist, then install a wheel and ping its bundled host. |
| Hosted demos | Deployed source commit | pages.yml builds the runtime and site together; verify the Pages run and live editor. Consumer CDN snippets stay on the last publicly available npm version until approval. |
.NET project versions remain 1.0.0; both npm manifests remain 0.0.0 in source.
CI injects release versions, including the export peer dependency. Do not bump those
placeholders. Python's in-tree version does move for a Python release; its installed
__version__ comes from wheel metadata. The MCP server's .mcp/server.json also stays at
0.0.0 in source. Delivery and development tools are built from source and are not additional
packages published by these workflows.
The tag namespaces are independent so Python-only releases remain possible. For a request
to release all surfaces, bump and tag docx-scalpel too: its wheel embeds the core host, so
a core fix needs a new wheel even when the Python wrapper itself has not changed. Use the
same release commit for both tags. See Python releasing and
npm staged publishing for their detailed procedures.
Prepare and tag remote main
Semver on vMAJOR.MINOR.PATCH tags, chosen from what accumulated in [Unreleased]:
| Bump | When |
| Patch | only ### Fixed |
| Minor | any ### Added / ### Changed, no breaking change |
| Major | a breaking public-API change |
- Fetch remote
mainand inspect commits since the last release, current tags, registry versions and CI. Work fromorigin/main, preferably in an isolated worktree; never tag the current feature branch just because it is checked out. Confirm the intended fixes are merged and the relevant CI checks pass. - Choose an unused core version with the table above and, for a coordinated release, an
unused Python version. Commit
python/pyproject.tomland any release-process corrections first. Leave consumer CDN pins alone until npm is public. - In
CHANGELOG.md, insert## [X.Y.Z] - YYYY-MM-DDunder## [Unreleased], leaving the accumulated entries beneath it and[Unreleased]empty above. Commit changelog-only:docs(changelog): cut vX.Y.Z release notes. - Open and merge a release-preparation PR into
main, following its required PR and code-scanning rules. Fetch again and use the merged commit on remotemain, not the local PR head (a squash merge changes its SHA). Ifmainadvanced, reassess the new changes, notes, version and checks before tagging; never force-push over someone else's work. Verify the release SHA withgit ls-remote origin refs/heads/main. Create annotated tags at that exact commit and push them explicitly:
git tag -a vX.Y.Z -m vX.Y.Z <release-sha> git tag -a docx-scalpel-vA.B.C -m docx-scalpel-vA.B.C <release-sha> git push origin vX.Y.Z docx-scalpel-vA.B.COmit the Python tag only when Python is deliberately outside the release scope. Never move an already-published tag or reuse a registry version.
- Create the GitHub Release with prepared notes:
gh release create vX.Y.Z --title vX.Y.Z --notes-file <body.md> --latest --verify-tag. Ifrelease-assetsalready created it, usegh release editwith the notes file and title instead. A tag without a GitHub Release is incomplete.
Release body opens with a one-line lead linking the CHANGELOG anchor
(…/CHANGELOG.md#XYZ---YYYY-MM-DD, digits only). Patch/minor: the lead plus the
### Added/### Changed/### Fixed sections verbatim (see v7.1.0). Major: the lead plus
### Highlights and ### Breaking changes as a summary — a major's accumulated entries are
far too long to dump (see v7.0.0, v8.0.0). ### Breaking changes must say what silently
changes for a caller who passes nothing, and how to pin the old behaviour.
Verify packages and approve npm
Monitor both publish.yml and python-publish.yml through completion. The core workflow
runs the .NET tests, builds and checks both npm packages, tests the export companion, and
runs scripts/release-preflight.sh. Python smoke-tests each self-contained host and runs
lifecycle tests against each built wheel before publishing. Check every job: NuGet, npm,
CLI assets and PyPI can succeed or fail independently.
npm staging is not publication. Read the npm packages awaiting 2FA approval summary
in the Publish run, review both stage IDs, then approve docxodus first and the matching
@docxodus/export second. Use npm's Staged Packages UI or
npx npm@11.19.1 stage view <stage-id> / stage approve <stage-id>; a maintainer completes
their own 2FA. If human interaction is required, provide the concrete stage IDs and continue
the independent release checks while waiting. Do not claim either npm package is public
until npm view <package>@X.Y.Z version succeeds.
Verify all five NuGet packages, both npm versions (including export's exact peer dependency),
and docx-scalpel==A.B.C on PyPI. A PyPI release must contain four platform wheels and an
sdist; install the matching wheel into a fresh environment with DOCXODUS_HOST unset and
call docx_scalpel.ping() to exercise the bundled host. Verify the GitHub Release contains
all twelve CLI binaries plus SHA256SUMS, and check downloaded assets against those sums.
A successful NuGet push can precede public availability: validation and indexing usually take less than 15 minutes. Keep checking the exact versions rather than re-publishing; see NuGet validation and indexing if the registry remains unavailable.
Recover partial releases without rebuilding a version from moving main. Inspect the
failed job before retrying. Re-run failed jobs at the original release SHA when possible.
NuGet uses --skip-duplicate; npm staged and published versions share a namespace and
cannot be overwritten. If only one npm package staged, preserve it and retry only the
missing package from the release tag (see docs/npm-releases.md). Do not blindly dispatch
the entire publish workflow on current main with an old version. Source fixes require a
new release version; existing public artifacts stay immutable.
Finally update and verify the demos
The hosted pages use docs/demo/engine.js to import their same-build
./embed.bundle.js. pages.yml runs npm run build and deploys npm/dist/site; the site
does not wait for npm approval to receive core changes. Keep this local runtime default.
The copyable CDN examples are separately pinned to public npm releases.
After both npm packages are approved, fetch the versioned embed.bundle.js and
embed.iife.js from jsDelivr successfully. Then update every active docxodus@X.Y.Z pin
in the demo HTML/README, docs/npm-package.md, npm/README.md, npm/examples/embed.html,
the examples in npm/src/{embed,index,core}.ts, and RELEASE_ENGINE in
npm/tests/social-demo.spec.ts. The inventory in docs/demo/tools/engine-pin.test.mjs
guards against drift. Preserve historical prose and deliberate @latest examples.
From npm/, validate and build the actual release source:
npm ci
npm run build
npm run pretest
DOCXODUS_CHECK_CDN=1 node ../docs/demo/tools/engine-pin.test.mjs
npx playwright test social-demo.spec.ts --project=chromium --reporter=lineMerge the demo and snippet PR into main after publication. Wait for the Pages
deployment on that commit (or dispatch pages.yml on main if needed), then open
https://jsv4.github.io/Docxodus/demo/ and /demo/app.html and verify an editor boots and
the embed dialog names the released version. Report release/package URLs, deployment
status, checks performed and any outstanding human approval; staging alone is not done.
Dependencies
- DocumentFormat.OpenXml 3.5.1
- SkiaSharp 4.148.0 (+
SkiaSharp.NativeAssets.Linux.NoDependencies), excluded underWASM_BUILD
Target framework: net10.0 for both library and tests.
Legacy Migration Notes
Docxodus forks OpenXmlPowerTools, upgraded net45/net46/netstandard2.0 → .NET 10 and Open XML SDK 2.8.1 → 3.x. Artifacts of that migration worth knowing:
GetPackage()inPtOpenXmlUtil.cs— SDK 3.x made the internalPackageprivate; we reach it by reflection. Use this extension, notOpenXmlPackage.Package.PartTypeInforeplaces SDK 2.x'sFontPartType/ImagePartTypeenums when adding parts.Dispose(), not.Close()— SDK 3.x droppedClose().- SkiaSharp replaces System.Drawing —
SKColor/SKBitmap/SKTypeface/SKEncodedImageFormat, helpers inSkiaSharpHelpers.cs(notablyColorHelper). Remember the WASM build excludes it. - The upstream
archived-examples/console projects were removed with the SpreadsheetML and PresentationML modules — they exercised the spreadsheet/presentation modules and were never in the solution.git loghas them.
For bugfix history, use git log rather than maintaining a list here.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

