Skip to content
Back to skills

Rebind Office

ASecurity

Pipeline stage 3 (the Office/docx surgical output engine) for report-regeneration — the Office analogue of rebind-html. Applies a Binding Manifest to a COPY of a Word .docx template and emits the regenerated .docx: frozen OPC parts + nodes stay byte-identical, surgical/regenerate nodes are rebuilt under the zero-literal construction rule (strip the old value, THEN write the new), rasters are re-captured (never transplanted), needs-review nodes are left untouched but visibly marked + logged. S...

  • 7 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 23, 2026
ai-agentspythongobashnodeexpresssecurity

Works with

  • cli

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 23, 2026

npx -y skills add mcorbett51090/RavenClaude --skill rebind-office --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rebind Office?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Rebind Office
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mcorbett51090-rebind-office/badge)](https://www.skillsdirectory.com/skills/mcorbett51090-rebind-office)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: rebind-office
description: "Pipeline stage 3 (the Office/docx surgical output engine) for report-regeneration — the Office analogue of rebind-html. Applies a Binding Manifest to a COPY of a Word .docx template and emits the regenerated .docx: frozen OPC parts + nodes stay byte-identical, surgical/regenerate nodes are rebuilt under the zero-literal construction rule (strip the old value, THEN write the new), rasters are re-captured (never transplanted), needs-review nodes are left untouched but visibly marked + logged. Stdlib-only (zipfile/xml.etree), edits via the shared rr_anchor OOXML resolver, python-docx/docxtpl optional, runs on Python 3.9. NOT for structure inference (infer-office, an earlier stage), the fidelity harness (a separate downstream track), or HTML output (the rebind-html lane)."
---

# Skill: rebind-office

## What this is

The **Office (Word/`.docx`) surgical output engine** for `report-regeneration` — pipeline
stage 3, the exact analogue of [`rebind-html`](../rebind-html/SKILL.md). Given a `.docx`
template, a Binding Manifest
([`../../knowledge/binding-manifest.schema.json`](../../knowledge/binding-manifest.schema.json),
`format: "office"`), and resolved new-source data, it produces a **same-format review-ready
draft** by performing schema-validated surgery on a **copy** of the template — never by
re-rendering from an abstract model. This is the "surgeon, not a renderer" model from
[`../../knowledge/core-architecture-spec.md`](../../knowledge/core-architecture-spec.md)
§1, applied to the Office/Word→PDF output format.

The engine works on a copy the whole way: it reads `word/document.xml` out of the OPC/ZIP
package with stdlib `zipfile`, edits **only** those bytes via the **shared OOXML resolver**
in [`../../scripts/rr_anchor.py`](../../scripts/rr_anchor.py), then re-zips — every other OPC
part is copied through byte-for-byte. It consumes a manifest; it does **not** infer structure
or propose bindings (that is [`infer-office`](../infer-office/SKILL.md), an earlier stage) and
it does **not** run the fidelity harness (V1–V6 + period-coherence — a separate, downstream
track). Its one job: **apply exactly what the manifest says, per node class, and prove the
frozen ones (parts and nodes) didn't move.**

It **never re-implements anchor resolution.** Every anchor is resolved by `rr_anchor`'s
`ooxml_resolve` / `ooxml_try_resolve`, which OWNS the shared OOXML grammar (`body`-rooted
`body/p[3]/r[1]` paths and `bookmark(NAME)` paths) that `infer-office` produces — so producer
and consumer cannot drift.

## The four node classes (§4 of the architecture spec)

| Class | What this engine does |
|---|---|
| `frozen` | **No mutation.** After every other binding is applied, the engine re-resolves the node in both template and output and proves its **outer OOXML byte span is identical** (`_assert_frozen_unchanged`) — a defensive proof, not an assumption. `frozen` is EARNED upstream (the manifest's job); rebind only guarantees a frozen node did not move. |
| `surgical` | **Strip, then write** — two separate byte splices, in that order. `_strip_value` empties **every** `w:t` under the anchored run/cell (zero-literal); `_write_value` writes the new value into the first `w:t`. At the instant between the two calls the node provably carries no old instance value — by construction, not a downstream check. Run properties (`w:rPr`) and every other child are preserved; the edit touches only `w:t` text. |
| `regenerate` | Same strip-then-write zero-literal invariant. A **prose/run** node is rebuilt from new data via stdlib `string.Template` (dotted-placeholder subclass — `jinja2` is optional acceleration for native `{{ }}`/`{% %}` control syntax). A **raster/`w:drawing`** node is force-regenerated by **replacing the referenced `word/media/*` binary** with a fresh capture supplied in new-data — never by transplanting the old pixels, per the spec's construction rule: "a transplanted binary blob cannot be proven data-free." A regenerate raster with no fresh capture is a loud error, never a silent transplant. |
| `needs-review` | **Left completely untouched** — content byte-for-byte unchanged — but a **visible OOXML marker** run (or a marker paragraph, for a table cell) carrying a machine-readable token (`[[REBIND-NEEDS-REVIEW]]`) is inserted adjacent to it, and it is logged in the change-manifest. Never ships silently — this is guarantee #2 made mechanical for Office output. |

Bindings are applied in **reverse document order** (RT1-F10): each anchor is a stable node
identity re-resolved on the current bytes just before its edit, so processing from the end of
the document backward means an earlier edit's byte/index shift can never invalidate a
not-yet-applied anchor.

## Usage

```bash
python3 plugins/report-regeneration/skills/rebind-office/rebind_office.py \
    --template  tests/fixtures/report-regeneration/sample-report.docx \
    --manifest  path/to/binding-manifest.json \
    --new-data  path/to/new-data.json \
    --out       tests/fixtures/report-regeneration/_out/regenerated.docx
```

Add `--pretty` to pretty-print the printed JSON change-manifest. Every invocation prints a
JSON object to stdout: `{"schema": "report-regeneration/rebind-office@1", "ok": true|false,
..., "changes": [...]}`. Exit code `0` = success; `2` = usage / path-guard / manifest-schema /
anchor-not-found / missing-data-query / missing-new-data-key / invalid-docx error (never a
silent partial write — a failure aborts the whole run and touches `--out` not at all).

`--template`/`--manifest`/`--new-data`/`--out` are **relative (or in-repo absolute) paths,
path-guarded** (mirrors [`../rebind-html/rebind_html.py`](../rebind-html/rebind_html.py) and
[`../../scripts/seed_defects.py`](../../scripts/seed_defects.py)): no `..` traversal, must
resolve inside the repo root. `--out` may never equal `--template` — this script works on a
**copy**; the template file on disk is never mutated (verified in the CLI test via a
before/after SHA-256 hash of the template file).

## The new-data lookup + template contract

A binding's `data_query.expression` is a **dot-path** into the `--new-data` JSON (e.g.
`"revenue.total"` looks up `{"revenue": {"total": ...}}`). `surgical` writes the resolved value
(XML-escaped) directly into the run's `w:t`. `regenerate` treats the resolved value as either:

- a **plain string** — a template, rendered with `${dotted.path}` placeholders resolved against
  the SAME new-data object (stdlib-only, always works); or a native Jinja template
  (`{{ }}`/`{% %}`) — rendered via `jinja2` if importable, else a loud error (never a silent
  stdlib mis-render of control-flow syntax); or
- an **object** — for a raster/`w:drawing` node, `{"media_base64": "<fresh png/jpeg bytes>"}`
  (the fresh capture, base64-encoded); for a text node with an explicit template key,
  `{"template": "..."}`.

A missing dot-path segment, an unresolvable anchor, or a class/data_query mismatch against the
schema's rule (frozen carries no `data_query`; every other class must) is a loud error → exit
2 — never a guess.

## Anchor support (this is an Office-only engine)

Only `anchor.kind == "ooxml_path"` is resolvable — resolved via `rr_anchor.ooxml_resolve`
against the exact grammar `infer-office` emits (`body/p[3]/r[1]`,
`body/tbl[1]/tr[2]/tc[2]/p[1]/r[1]`, `bookmark(revenue_total)`). `json_pointer` /
`element_id` / `css_selector` anchors are HTML-lane anchors from the same manifest schema;
this engine rejects them loudly rather than guessing — HTML output is the separate
[`rebind-html`](../rebind-html/SKILL.md) lane.

## Security

Stdlib XML parsers are XXE / billion-laughs vulnerable and `defusedxml` is deliberately off
this plugin's dependency path, so this engine — like `infer-office` and `rr_anchor` — **rejects
any DOCTYPE/DTD/ENTITY** in any OOXML part it parses (`word/document.xml`, the `.rels`) before
`xml.etree` ever expands an entity. A valid OOXML part never carries one; the rejection closes
external-entity and entity-expansion attacks at the source and treats a hostile template as
**data, never instructions** (§6).

## Tests

[`tests/test_rebind_office.py`](tests/test_rebind_office.py) — stdlib `unittest`, no pytest
required, run end-to-end against the real
[`tests/fixtures/report-regeneration/sample-report.docx`](../../../../tests/fixtures/report-regeneration/sample-report.docx)
corpus fixture:

- **`TestRebindOffice`** — a hand-built manifest covering one binding of every class (frozen
  heading run, surgical bookmarked currency value, regenerate narrative run, regenerate
  raster, needs-review percent run): frozen **OPC parts byte-identical** (everything but the
  edited `word/document.xml` + regenerated media) and `[Content_Types].xml` intact; a frozen
  **node** byte-identical (+ a must-fail check that a genuinely mutated frozen node IS caught);
  surgical value replaced with the OLD value absent from that run **and** the whole document;
  strip-then-write asserted at the midpoint; regenerate text reflects new data; regenerate
  raster swaps the media binary (old bytes gone, fresh in) and refuses a transplant with no
  fresh capture; needs-review flagged + logged, content untouched; the output is a **valid
  docx** (re-openable via `zipfile` + `xml.etree`, `testzip()` clean); manifest-schema
  rejections (non-office format, non-ooxml anchor, frozen-with-data_query); DTD/XXE rejection.
- **`TestCLI`** — process-boundary: happy path (exit 0, valid docx written, `$new` present /
  `$old` absent, template SHA-256 unchanged), `--out == --template` rejection, path-traversal
  rejection, absolute-path rejection (all exit 2).

Run: `python3 plugins/report-regeneration/skills/rebind-office/tests/test_rebind_office.py -v`

The producer↔resolver anchor cross-check lives in
[`../../scripts/tests/test_rr_anchor.py`](../../scripts/tests/test_rr_anchor.py).

## Purity contract

- **Stdlib-only** — `argparse`, `base64`, `io`, `json`, `posixpath`, `re`, `string`, `sys`,
  `zipfile`, `xml.etree`. No pip installs required. `python-docx` / `docxtpl` are detected for
  parity only (the byte-splice engine is the **sole** code path); `jinja2` is imported via a
  graceful `try/except` and is optional acceleration for regenerate templates that use native
  Jinja control syntax — every `${dotted.path}`-only template works with jinja2 absent.
- **Runs unmodified on Python 3.9.6** — `from __future__ import annotations`; no PEP 604 union
  syntax (`X | Y`), no `match` statement.
- **No network, no subprocess** inside the engine.
- **Path-guarded** — see Usage above.
- **Exit-coded** — deterministic `0`/`2`; no partial write on failure.
- **Zero-literal construction, by construction** — every surgical/regenerate value mutation is
  a strip call followed by a write call; there is no code path that writes a new value without
  first stripping the old one at that anchor, and no old instance value survives a
  surgical/regenerate node.

Files in this skill

  • SKILL.md10.8 KB
  • rebind_office.py35.9 KB
  • tests/test_rebind_office.py17.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…