Skip to main content

docx-scalpel

Anchor-addressed DOCX editing for LLM agents — a thin client over Docxodus' DocxSession.

docx-scalpel exposes Docxodus' stateful DOCX editor over a long-running .NET subprocess (docxodus-pyhost). The session lives in the host's memory until you explicitly release it, so an LLM agent can issue dozens of small edits against one document without paying the OOXML parse + Unid annotation + projection cost on every call.

Status: Beta. Wheels ship a bundled docxodus-pyhost for linux-x64, linux-arm64, osx-arm64, and win-x64; any other platform installs from the sdist and needs a host of its own (see below).

Installation

pip install docx-scalpel

That resolves a wheel on linux-x64, linux-arm64, osx-arm64, or win-x64 — each carrying a self-contained docxodus-pyhost built from the same commit as the release, so there's no .NET runtime to install and no version to pin.

Source installs (pip install of the sdist, or pip install -e . from a dev clone) don't include a bundled host. Set DOCXODUS_HOST=/path/to/docxodus-pyhost to point at one you built, or run dotnet build tools/python-host/pyhost.csproj inside a Docxodus monorepo clone — the locator auto-discovers it.

Quick start

from docx_scalpel import open_session, FormatOp, Position

with open("contract.docx", "rb") as f:
    docx_bytes = f.read()

with open_session(docx_bytes) as session:
    # Walk template placeholders and fill them. The picker returns a string to
    # replace, or None to skip. fill_placeholders handles reverse-offset
    # ordering, $-prefix preservation, and multi-pass nested-bracket convergence
    # in one call.
    result = session.fill_placeholders(lambda p: "filled value")
    print(f"filled {result.filled} placeholders in {result.passes} passes")

    # Add a heading after the first body paragraph.
    proj = session.project()
    first_p = next(
        t for t in proj.anchor_index.values()
        if t.kind in ("p", "h") and t.scope == "body"
    )
    session.insert_paragraph(first_p.id, Position.AFTER, "## Reviewed by counsel")

    # Bold the first 8 characters of that paragraph.
    session.apply_format_by_substring(first_p.id, "Reviewed", FormatOp(bold=True))

    new_bytes = session.save()

with open("filled.docx", "wb") as f:
    f.write(new_bytes)

Atomic mutation batches

Use execute_batch when a plan spans several edits that must either all land or all disappear. Atomic is the default; success is one version/undo unit and failure returns the indexed operation error after restoring the complete package and history state. Pass transaction_id="..." so a retry after a lost response returns the original result instead of applying again (result.transaction carries the identity; reusing an id for a different batch fails with transaction_conflict):

from docx_scalpel import MutationBatchStep

result = session.execute_batch([
    MutationBatchStep("replace_text", {
        "anchorId": first_p.id,
        "markdown": "Replacement text",
    }),
    MutationBatchStep("set_header_text", {
        "anchorId": first_p.id,
        "kind": "default",
        "markdown": "Confidential",
    }),
])
if not result.success:
    print(result.failure.index, result.failure.action, result.failure.error)

Select MutationBatchMode.BEST_EFFORT explicitly only when retaining successful steps after another step fails is intended.

Isolated previews

preview_batch takes the same steps and predicts their outcome on a complete clone of the package. The live session is never a mutation target, so its bytes, version and undo/redo history are untouched whatever the steps do:

from docx_scalpel import MutationPreviewHtmlMode

preview = session.preview_batch(
    [
        MutationBatchStep("replace_text", {
            "anchorId": first_p.id,
            "markdown": "Proposed replacement",
        }),
    ],
    html_mode=MutationPreviewHtmlMode.FULL,
)
print(preview.html)
print(preview.revision_changes.added, preview.comment_changes.added, preview.warnings)

Both preview_batch and execute_batch return the enriched receipt: base_version, result_version, package_hash, {added, removed, modified} change sets for revisions, comments and annotations, and warnings. MutationPreviewHtmlMode.SCOPED renders one block and requires html_anchor_id. package_hash is None — never "" — when it could not be computed, so check it before using it as a replay assertion.

A preview predicts generated values (new anchor ids, comment ids, revision timestamps), but a later execute_batch runs afresh and may generate them differently. retain=True keeps the successful preview's exact result package, and commit_preview makes it the live document as previewed — one undo step, same ids, same package_hash:

preview = session.preview_batch(steps, retain=True)
show_to_reviewer(preview.html, preview.revision_changes)
commit = session.commit_preview(preview.retention.preview_id, transaction_id="plan-42-commit")
assert commit.package_hash == preview.package_hash

The commit refuses with EditErrorCode.PREVIEW_STALE, editing nothing, if the session's version, package content, tracked-changes mode or revision author moved since the preview, and with PREVIEW_NOT_FOUND once the preview expired, was evicted (8 previews, 64 MiB, 15 minutes per session) or was already committed.

Delivery receipts from captured evidence

A session opened with DocxSessionSettings(capture_delivery_evidence=True) records the evidence a delivery change receipt needs as its edits execute — the exact package before and after every mutation, every direct operation's request (the host describes each one), each execute_batch step, transaction ids, and undo/redo lineage — and build_delivery_receipt mints a receipt verify_delivery_receipt accepts:

from docx_scalpel import DeliveryReceiptPrivacyProfile, DocxSessionSettings, verify_delivery_receipt

with open_session(docx_bytes, DocxSessionSettings(capture_delivery_evidence=True)) as session:
    session.replace_text(first_p.id, "Final wording")
    session.execute_batch(steps, transaction_id="plan-42")
    bundle = session.build_delivery_receipt(privacy_profile=DeliveryReceiptPrivacyProfile.HASH_AND_SUMMARY)

receipt = bundle.artifact("change-receipt")
artifacts = {a.artifact_id: a.bytes for a in bundle.artifacts
             if a.bytes is not None and a.artifact_id != "change-receipt"}
assert verify_delivery_receipt(receipt.bytes.decode(), artifacts).is_valid

Deliver the bundle's final-docx artifact: it is the bytes the receipt attests. get_delivery_evidence_status() names the first reason a receipt cannot be minted (capture off, retention of 256 states / 512 MiB exceeded); the bundle is then incomplete and the receipt artifact carries the reason instead of claiming a history.

A step's operation is any mutating session operation, including the structural table ops (insert_table, insert_table_row, merge_cells, …); read-only operations, undo/redo, and session configuration are rejected as invalid_batch_step.

The with block is the documented lifecycle path — it calls session.close() on the way out, which releases the session from the host's SessionRegistry. A __del__ finalizer is a fallback for forgotten sessions but should not be relied on; interpreter shutdown may skip it.

Why a subprocess?

DocxSession holds a parsed WordprocessingDocument, an AnchorIndex of Unid-stamped block-level targets, a cached MarkdownProjection, and a bounded UndoRing of per-part XDocument snapshots. Recreating it costs tens of ms on small docs and seconds on large ones. The subprocess model lets one Python process drive many sessions across many calls, all in one host's memory, until you decide to close them.

Architecture:

Python process                 docxodus-pyhost (.NET 10)
─────────────                  ──────────────────────────
DocxSession  ──NDJSON──>       Dispatcher
                               │
                               ▼
                               DocxSessionOps
                               │
                               ▼
                               SessionRegistry (handle → DocxSession)

One host per Python process. Many sessions inside the host. atexit sends shutdown and (if the host doesn't comply) terminates / kills.

Full design + wire-protocol spec: docs/architecture/python_docxodus.md. Delta-spec for the docx-scalpel rebrand: docs/superpowers/specs/2026-05-26-docx-scalpel-design.md.

Development

Build the host binary (one-time)

# From the Docxodus repo root:
dotnet build tools/python-host/pyhost.csproj -c Release

This produces tools/python-host/bin/Release/net10.0/docxodus-pyhost. _host_locator.py discovers it automatically when you pip install -e . from a monorepo clone.

For non-monorepo development, set DOCXODUS_HOST=/path/to/docxodus-pyhost to override the discovery path.

A dotnet build host is framework-dependent, so it needs the .NET 10 runtime at launch. If your system dotnet is older and .NET 10 lives elsewhere (e.g. ~/.dotnet), the host will exit with You must install or update .NET to run this application; export DOTNET_ROOT to point at the newer install. Released wheels are unaffected — they bundle a self-contained host with no runtime lookup.

export DOTNET_ROOT="$HOME/.dotnet"

Editable install + tests

cd python
python -m venv .venv
.venv/bin/pip install -e .[test]
.venv/bin/pytest -v

Test layout

  • tests/test_smoke.py — end-to-end mirror of Docxodus.Tests/DocxSessionSmokeTest.cs. v1 acceptance gate.
  • tests/test_lifecycle.py — proves session persistence, idempotent close, singleton host, finalizer fallback.
  • tests/test_table_addressing.py — canonical table identities, coordinate resolution, every table mutation, mappings, and anchor-stable reopen.

Tests share the Docxodus monorepo's TestFiles/ corpus so divergence between Python and .NET on identical inputs is detectable.

API surface

The DocxSession class exposes every op in Docxodus.Internal.DocxSessionOps as a snake-case method:

Package verification is available statelessly as generate_package_manifest(docx_bytes) and for the current logical checkpoint as session.get_package_manifest(). Both return frozen typed dataclasses for the schema documented in package_manifests.md; validation failures are structured findings rather than editable-package exceptions. Closed wire vocabularies decode to str enums, and decimal-string ZIP64 sizes decode to exact Python int values.

The default delivery gate is likewise available as verify_deliverable(docx_bytes, baseline=None) and session.verify_deliverable(). Both return a typed DeliverableVerificationResult. The stateless form binds the report to the exact supplied bytes; the session form verifies its clean-save checkpoint and, with the default initial capture, compares it with the exact opening bytes.

Tier Methods
Lifecycle save, close, undo, redo, get_version, get_package_manifest, verify_deliverable, execute_batch, preview_batch, to_html, register_page_map, get_page_map_status, get_page_citation
Projection project, project_anchor
Discovery grep, grep_cross_block, find_placeholders, find_by_text, find_all_by_text, find_by_regex, find_by_kind, find_by_annotation, find_by_label, find_by_bookmark, list_annotations, exists, get_anchor_info, get_anchor_infos, get_edit_summary, remaining_placeholders, get_diff
Inspection list_styles, get_formatting, list_inline_spans, get_block_metadata, get_block_metadatas, get_list_membership, get_section_info
Native links/bookmarks list_hyperlinks, add_hyperlink, update_hyperlink, remove_hyperlink, list_bookmarks, add_bookmark, move_bookmark, rename_bookmark, remove_bookmark
Native images get_image_capabilities, list_images, insert_image, replace_image, embed_linked_image, set_image_dimensions, set_image_metadata, set_image_floating_layout, remove_image
A: text mutations replace_text, replace_text_range, replace_text_at_span, replace_inner, replace_match, delete_block, move_block, delete_range, delete_section
B: structural insert_paragraph, split_paragraph, merge_paragraphs
B: headers/footers/page numbers set_header_text, set_footer_text, ensure_header_footer_visible, set_header_footer_kind_enabled, insert_page_number_field, set_page_numbering, clear_page_numbering, set_page_setup
B: footnotes/endnotes insert_footnote, insert_endnote
B: native comments add_comment, add_comment_to_revision, add_comment_reply, update_comment, set_comment_resolved, remove_comment, list_comments
C: formatting apply_format, apply_format_by_substring, set_paragraph_style, set_paragraph_format, set_list_level, remove_list_membership, apply_list_format, apply_list_format_range, set_list_start_override, clear_list_start_override
D: tables get_table_metadata, resolve_table_cell_anchor, resolve_table_cell_coordinate, insert_table, insert_table_row, insert_table_column, delete_table_row, delete_table_column, merge_cells, unmerge_cells, set_column_widths, set_table_borders, set_cell_shading, set_repeat_header_row, set_table_row_options, replace_cell_content
D: tracked changes set_tracked_changes, set_revision_author, list_revisions, accept_revision, reject_revision
E: annotations add_annotation, remove_annotation, update_annotation, move_annotation
Raw XML session.raw.get_xml, session.raw.insert_xml, session.raw.replace_xml

Every mutation method returns an EditResult envelope — transport-level failures raise DocxodusTransportError, but a business outcome (anchor_not_found, malformed_markdown, etc.) returns EditResult(success=False, error=EditError(...)). Never an exception across the API boundary.

PageMap accepts physical pagination materialized by an external renderer. Registration requires the session's exact document version and validates the renderer fingerprint, page/section order, canonical anchors, geometry, story, table ownership, and fragment order. Pass the same PageCitationRequest to search/scoped reads to attach citations. Continuous/no-map and stale layouts return typed unavailable results; the client never guesses page numbers. See the portable PageMap contract.

For optimistic concurrency, build a MutationPreconditions object and use session.check_preconditions(...) for a read-only probe or with session.preconditioned(guards): ... to attach it to each mutation request in the block. The guard can require the document version, anchor hash/exact visible text or range/kind/scope, and an exact replacement match count. A mismatch returns EditErrorCode.PRECONDITION_FAILED with structured expected/actual/current target metadata and leaves bytes, version, and undo history unchanged.

Stateless functions

Alongside the session API, the package exposes stateless one-shot functions at the module root — no session handle, they take DOCX bytes in and return bytes / data out:

Function Signature Returns
convert_docx_to_html (data, options=None) HTML str
docx_diff_compare (left, right, settings=None) redlined DOCX bytes (native w:ins/w:del/w:moveFrom/w:moveTo/w:rPrChange markup)
docx_diff_get_revisions (left, right, settings=None) tuple[DocxDiffRevision, ...]
docx_diff_get_edit_script (left, right, settings=None) edit-script JSON str
docx_diff_get_semantic_changes (left, right, settings=None) versioned SemanticChangeSet with typed operations, families, locations, and before/after values
docx_diff_accept_revisions (redline) bytes — accept every tracked change (≡ the right side of the diff)
docx_diff_reject_revisions (redline) bytes — reject every tracked change (≡ the left side)
docx_diff_consolidate (base, reviewers, settings=None) multi-author redlined DOCX bytes — merge N DocxDiffReviewer diffs against one shared base
docx_diff_get_conflicts (base, reviewers, settings=None) tuple[DocxDiffConflict, ...]
docx_diff_get_consolidated_revisions (base, reviewers, settings=None) tuple[DocxDiffConsolidatedRevision, ...]
docx_diff_get_consolidated_edit_script (base, reviewers, settings=None) edit-script JSON str

The docx_diff_* family is a thin client over Docxodus' DocxDiff IR diff engine. Tune pairwise comparisons with DocxDiffSettings and N-way consolidation with DocxDiffConsolidateSettings (whose conflict_resolution takes a ConflictResolution value). DetectMoves/format-change tracking, header/footer comparison, and per-reviewer attribution all round-trip through these calls.

For an open session, session.get_semantic_changes() compares the exact opening package with the current logical checkpoint. It requires the default DocxSessionSettings(capture_initial_projection=True). The schema and package-suppression rules are documented in semantic_diff.md.

from docx_scalpel import docx_diff_compare, docx_diff_get_revisions, DocxDiffSettings

with open("v1.docx", "rb") as f: left = f.read()
with open("v2.docx", "rb") as f: right = f.read()

redline = docx_diff_compare(left, right, DocxDiffSettings(author_for_revisions="Reviewer"))
for rev in docx_diff_get_revisions(left, right):
    print(rev.type, rev.text)

Portable history files

open_history_archive(bytes) opens a standalone readonly history. history.document(id) binds version controls; history.import_history_archive(bytes) resumes exact history in host-owned storage. See the short usage guide and real archives. No transport or autosave is added.

License

MIT. Built on top of Docxodus, which is itself a fork of Open-Xml-PowerTools.

Metadata

Release files for docx-scalpel 0.6.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for docx-scalpel 0.6.5
File Size Uploaded
docx_scalpel-0.6.5.tar.gz 137.3 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for docx-scalpel 0.6.5
File
docx_scalpel-0.6.5-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
docx_scalpel-0.6.5-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
docx_scalpel-0.6.5-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
docx_scalpel-0.6.5-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 233.8 MB

Release files / docx_scalpel-0.6.5.tar.gz

Download URL docx_scalpel-0.6.5.tar.gz
Size 137.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0954bea48a26784747e5f1239c9e04ee888b7459de096f3cb4b6af43cbd28944
BLAKE2b-256 checksum
How to use checksums
8f4cffab995c964ca5f30135048f60c3a8967ec33823e31d60fef8609ee737fc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / docx_scalpel-0.6.5-py3-none-win_amd64.whl

Download URL docx_scalpel-0.6.5-py3-none-win_amd64.whl
Size 59.5 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
3cdb3bc4fcc5af61fb8910f721e6271f9abd62a210555dda8cd48318dfae2ca4
BLAKE2b-256 checksum
How to use checksums
bf70ed4746ac9791ca51be9e99a1c0d217cb67696549b79c40aaac3e962dd9fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / docx_scalpel-0.6.5-py3-none-manylinux_2_28_x86_64.whl

Download URL docx_scalpel-0.6.5-py3-none-manylinux_2_28_x86_64.whl
Size 59.7 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
bac5a7288a8a30ca5267c57605f3efd8f534974bc493a995295c248b3c98ba5c
BLAKE2b-256 checksum
How to use checksums
bb815481d00c6460b02b9ee85abba7addc432d6814c382b83ff4bc0c567fae17
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / docx_scalpel-0.6.5-py3-none-manylinux_2_28_aarch64.whl

Download URL docx_scalpel-0.6.5-py3-none-manylinux_2_28_aarch64.whl
Size 55.4 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
e94c9e7bac71ee8f81af1f34f237de1ce78f538cd3d0211cb0a9a4c9790fdb14
BLAKE2b-256 checksum
How to use checksums
2f772542166a861383f7aee41a3192cdbf23a1822204dec637c8ada45ef0d32d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / docx_scalpel-0.6.5-py3-none-macosx_11_0_arm64.whl

Download URL docx_scalpel-0.6.5-py3-none-macosx_11_0_arm64.whl
Size 59.1 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b459795227bae05114d96a08226d0a3aa673781d7a87732f89eb82e1a99ce9a8
BLAKE2b-256 checksum
How to use checksums
98bf6a0a50f13f78f52584449f09707fef6b90f4c2a7125fbd340bd317c86ee7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page