agentleFS
JSv4JSv4/Docxodus/CLAUDE.md

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.

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
  1. Distinctive:

    Never credit yourself in commits: Do not add "Generated with Claude Code" or "Co-Authored-By: Claude" to commit messages.

    line 13
  2. Distinctive:

    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 …

    line 15
  3. Very specific:

    When a public DocxSession method or setting changes, update Docxodus/Internal/DocxSessionOps.cs first — both bridges and both clients pick …

    line 108
  4. Very specific:

    Defaults are declared once: Read them from the settings object rather than repeating literals per surface — DocxSessionJson.ParseSettings …

    line 144
  5. Very specific:

    When touching image/font/color code, check that it compiles under WASM_BUILD — the npm build fails loudly if it doesn't.

    line 211
  6. Distinctive:

    Read the linked doc before non-trivial changes.

    line 254
  7. Distinctive:

    Don't reintroduce either category.

    line 306
  8. Distinctive:

    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 …

    line 329
  9. Distinctive:

    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 …

    line 338
  10. Distinctive:

    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 …

    line 342
  11. Distinctive:

    Stay in scope: No unrelated fixes or refactors in the change.

    line 347
  12. Very specific:

    Pin it in the fidelity bench: Every defect reproduced in step 1 also gets authored fixtures in JSv4/docx-fidelity-bench, so every Docxodus …

    line 351
  13. Very specific:

    When our output differs from Word or LibreOffice, document the finding in docs/ooxml_corner_cases.md.

    line 361
  14. Distinctive:

    Create annotated tags at that exact commit and push them explicitly:

    line 422
  15. Very specific:

    Create the GitHub Release with prepared notes: gh release create vX.Y.Z --title vX.Y.Z --notes-file <body.md> --latest --verify-tag.

    line 432
  16. Very specific:

    If release-assets already created it, use gh release edit with the notes file and title instead.

    line 434
  17. Very specific:

    npm staging is not publication: Read the npm packages awaiting 2FA approval summary in the Publish run, review both stage IDs, then approve …

    line 452
  18. Very specific:

    Verify all five NuGet packages, both npm versions (including export's exact peer dependency), and docx-scalpel==A.B.C on PyPI.

    line 460
  19. Very specific:

    After both npm packages are approved, fetch the versioned embed.bundle.js and embed.iife.js from jsDelivr successfully.

    line 486

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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 Microsoft OpenXmlPowerTools source — DocumentBuilder.cs, PtOpenXmlUtil.cs, WmlToHtmlConverter.cs, the GetListItemText_*.cs locale 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 in Docxodus.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 enable or #nullable disable directive to a file. Docxodus.csproj already sets <Nullable>enable</Nullable> project-wide (see Nullable Reference Types below), so a per-file directive is never correct: #nullable enable is a redundant no-op and #nullable disable is a regression. New and refactored files get neither. SourceFileCopyrightTests asserts the disable half.

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.

LayerPathPurpose
Core libraryDocxodus/All WordprocessingML logic. NuGet package Docxodus.
Shared facadesDocxodus/Internal/{DocxSessionOps,DocxDiffOps,HtmlConversionOps,SessionRegistry,DocxSessionJson}.csSingle-owner op + wire-shape layer every transport routes through.
DeliveryDocxodus/Delivery/Delivery bundles: manifest, publisher, revision policy, host renderer. See delivery_bundle.md.
VerificationDocxodus/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 testsDocxodus.Tests/xUnit, ~3,900 tests, ~4 min.
CLI toolstools/redline/, tools/docx2html/, tools/docx2oc/Thin dotnet tool wrappers. Release binaries are trimmed; see cli_trimming_and_native_aot.md.
WASM bridgewasm/DocxodusWasm/[JSExport] shells over the facades.
Stdio hosttools/python-host/NDJSON-over-stdin host (docxodus-pyhost) that the docx-scalpel pip package subprocesses.
Agent servertools/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 clientpython/docx-scalpel on PyPI.
npm/TypeScriptnpm/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 packagenpm-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 evalseval/ + Docxodus.Tests/Eval/Deterministic document-workflow scenarios scored on task completion, target precision, and collateral change. Corpus and contract in eval/README.md.
Pages demodocs/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 toolstools/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 typeCoreTests*Ops facadeWASMnpm/TSstdio + PythonMCPDocs
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 bundles

A 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 --noEmit

Playwright 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 the WASM_BUILD constant and excludes SkiaSharp (no native deps in the browser). Code needing SkiaSharp must be guarded with #if !WASM_BUILD or 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.

ModuleWhat it doesDesign doc
DocxSession*.csStateful 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>.csdocx_mutation_api.md
DocxDiff.cs + Ir/Diff/Structure-aware comparison → native tracked changes; edit script as data; N-way consolidateir_diff_engine.md
WmlToHtmlConverter.csDOCX → HTML, the render fidelity oracledocx_converter.md, comment_rendering.md, paginated_headers_footers.md, wml_to_html_converter_gaps.md
HtmlToWmlConverter.csHTML → DOCX—
WmlToMarkdownConverter.csAnchor-addressed markdown projectionmarkdown_projection.md
npm/src/editor.ts + ribbon.tsIn-browser block editor and its shipped UI surfaceir_editor_feasibility.md, ir_editor_roadmap.md, editor_ui_surface.md
ExternalAnnotationProjector.csIncremental annotation overlay on pre-converted HTMLincremental_annotation_overlay.md, custom_annotations.md
OpenContractExporter.csExport to OpenContracts formatopencontracts_export.md
DocumentBuilder.csMerge / split DOCX—
OpenXmlRegex.csRegex search/replace in DOCX—
RevisionProcessor.csAccept/reject tracked revisionstracked_changes.md
FormattingAssembler.csResolve and flatten formatting—
MetricsGetter.csDocument metrics (styles, fonts, languages)—
PageMap.cs + npm/src/pagination.tsPage geometry and the paginated viewpage_map.md, paginated_headers_footers.md
npm/src/markup-balloons.ts + Internal/WordFormatWording.csWord's All Markup balloon print layout (commentColumnPlacement: "balloons")all_markup_layout.md
Ir/The structure-aware IR the diff and markdown projection readdocument_ir.md
Delivery/Delivery bundles and change receiptsdelivery_bundle.md, delivery_change_receipt.md
Verification/Package manifests, semantic change sets, reversibility proofdeliverable_verification.md, package_manifests.md, semantic_diff.md, redline_reversibility_proof.md
npm-export/Standalone HTML/PDF export via pinned Chromiumstandalone_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 the SmlDocument/PmlDocument wrappers. GetDocumentType() still recognises XLSX and PPTX packages so that feeding one in throws a clear PowerToolsDocumentException instead 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 the WmlDocument partial that exposed them), PowerToolsBlock, PowerToolsBlockExtensions, StronglyTypedBlock, and OxPtHelpers.

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

  1. CHANGELOG.md — add an entry under [Unreleased].
  2. Tests — add to the matching file in Docxodus.Tests/. Reuse TestFiles/ fixtures where possible; programmatic fixtures need all required parts (StyleDefinitionsPart, DocumentSettingsPart, …).
  3. Ripple — follow the checklist above.
  4. Docs — update docs/architecture/ for significant features, and docs/ooxml_corner_cases.md for 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

SurfaceVersion sourcePublication and verification
NuGet: Docxodus, Redline, Docx2Html, Docx2OC, Docxodus.McpServervX.Y.Zpublish.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/exportvX.Y.Z; export's peer dependency is exactly X.Y.Zpublish.yml stages both packages; approve each with maintainer 2FA, then verify public versions and CDN assets.
CLI downloads: redline, docx2html, docx2ocvX.Y.Zpublish.yml attaches twelve binaries (three tools × four RIDs) and SHA256SUMS to the GitHub Release.
PyPI: docx-scalpeldocx-scalpel-vA.B.C and python/pyproject.tomlpython-publish.yml; verify four platform wheels and one sdist, then install a wheel and ping its bundled host.
Hosted demosDeployed source commitpages.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]:

BumpWhen
Patchonly ### Fixed
Minorany ### Added / ### Changed, no breaking change
Majora breaking public-API change
  1. Fetch remote main and inspect commits since the last release, current tags, registry versions and CI. Work from origin/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.
  2. Choose an unused core version with the table above and, for a coordinated release, an unused Python version. Commit python/pyproject.toml and any release-process corrections first. Leave consumer CDN pins alone until npm is public.
  3. In CHANGELOG.md, insert ## [X.Y.Z] - YYYY-MM-DD under ## [Unreleased], leaving the accumulated entries beneath it and [Unreleased] empty above. Commit changelog-only: docs(changelog): cut vX.Y.Z release notes.
  4. 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 remote main, not the local PR head (a squash merge changes its SHA). If main advanced, reassess the new changes, notes, version and checks before tagging; never force-push over someone else's work. Verify the release SHA with git ls-remote origin refs/heads/main.
  5. 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.C

    Omit the Python tag only when Python is deliberately outside the release scope. Never move an already-published tag or reuse a registry version.

  6. Create the GitHub Release with prepared notes: gh release create vX.Y.Z --title vX.Y.Z --notes-file <body.md> --latest --verify-tag. If release-assets already created it, use gh release edit with 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=line

Merge 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 under WASM_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() in PtOpenXmlUtil.cs — SDK 3.x made the internal Package private; we reach it by reflection. Use this extension, not OpenXmlPackage.Package.
  • PartTypeInfo replaces SDK 2.x's FontPartType/ImagePartType enums when adding parts.
  • Dispose(), not .Close() — SDK 3.x dropped Close().
  • SkiaSharp replaces System.Drawing — SKColor/SKBitmap/SKTypeface/SKEncodedImageFormat, helpers in SkiaSharpHelpers.cs (notably ColorHelper). 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 log has them.

For bugfix history, use git log rather than maintaining a list here.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.