dsh-office-kit 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/assets/skills/excel-live-control/SKILL.md +119 -152
  2. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +42 -58
  3. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +36 -42
  4. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +33 -34
  5. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +34 -36
  6. package/assets/skills/excel-live-control/references/office-automation.md +127 -153
  7. package/assets/skills/excel-live-control/references/read_only_qna.md +40 -45
  8. package/assets/skills/excel-live-control/style_guidelines.md +56 -64
  9. package/assets/skills/excel-live-control/workflows/create_workflows.md +40 -47
  10. package/assets/skills/excel-live-control/workflows/edit_workflows.md +37 -45
  11. package/assets/skills/office-documents/SKILL.md +24 -60
  12. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +5 -5
  13. package/assets/skills/office-documents/ooxml/comments.md +8 -8
  14. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +10 -10
  15. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +7 -7
  16. package/assets/skills/office-documents/ooxml/tracked_changes.md +12 -12
  17. package/assets/skills/office-documents/references/docx-structure.md +11 -13
  18. package/assets/skills/office-documents/references/template-elicitation.md +11 -13
  19. package/assets/skills/office-documents/references/writing-quality.md +20 -20
  20. package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
  21. package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
  22. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
  23. package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
  24. package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
  25. package/assets/skills/office-documents/tasks/create_edit.md +14 -16
  26. package/assets/skills/office-documents/tasks/equations.md +17 -28
  27. package/assets/skills/office-documents/tasks/fields_update.md +14 -16
  28. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
  29. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
  30. package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
  31. package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
  32. package/assets/skills/office-documents/tasks/images_figures.md +15 -25
  33. package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
  34. package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
  35. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
  36. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
  37. package/assets/skills/office-documents/tasks/read_review.md +9 -20
  38. package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
  39. package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
  40. package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
  41. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
  42. package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
  43. package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
  44. package/assets/skills/office-documents/tasks/verify_render.md +11 -23
  45. package/assets/skills/office-documents/tasks/watermarks_background.md +11 -15
  46. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +6 -6
  47. package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
  48. package/assets/skills/office-pdf/SKILL.md +36 -51
  49. package/assets/skills/office-pdf/references/form-fields.md +20 -20
  50. package/assets/skills/office-pdf/references/geometry.md +10 -10
  51. package/assets/skills/office-pdf/references/layout-quality.md +13 -13
  52. package/assets/skills/office-pdf/references/limits.md +12 -56
  53. package/assets/skills/office-pdf/tasks/annotations.md +5 -5
  54. package/assets/skills/office-pdf/tasks/create.md +6 -9
  55. package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
  56. package/assets/skills/office-pdf/tasks/forms.md +15 -15
  57. package/assets/skills/office-pdf/tasks/read.md +9 -9
  58. package/assets/skills/office-pdf/tasks/redact.md +18 -18
  59. package/assets/skills/office-pdf/tasks/stamp.md +10 -10
  60. package/assets/skills/office-presentations/SKILL.md +71 -91
  61. package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
  62. package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
  63. package/assets/skills/office-presentations/recipes/README.md +24 -35
  64. package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
  65. package/assets/skills/office-presentations/recipes/connectors.md +11 -12
  66. package/assets/skills/office-presentations/recipes/images.md +11 -12
  67. package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
  68. package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
  69. package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
  70. package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
  71. package/assets/skills/office-presentations/recipes/tables.md +14 -14
  72. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
  73. package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
  74. package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
  75. package/assets/skills/office-presentations/references/finalization.md +10 -12
  76. package/assets/skills/office-presentations/references/native-bullets.md +14 -18
  77. package/assets/skills/office-presentations/references/native-evidence.md +18 -32
  78. package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
  79. package/assets/skills/office-presentations/references/templates.md +31 -60
  80. package/assets/skills/office-presentations/routing/google_slides.md +4 -4
  81. package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
  82. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
  83. package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
  84. package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
  85. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
  86. package/assets/skills/office-presentations/tasks/template-following.md +23 -28
  87. package/assets/skills/office-spreadsheets/SKILL.md +32 -39
  88. package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
  89. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
  90. package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
  91. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
  92. package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
  93. package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
  94. package/assets/skills/office-spreadsheets/references/charts.md +15 -34
  95. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
  96. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
  97. package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
  98. package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
  99. package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
  100. package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
  101. package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
  102. package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
  103. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
  104. package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
  105. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
  106. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
  107. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
  108. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
  109. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
  110. package/assets/skills/shared/annotation-recipes.md +98 -120
  111. package/assets/skills/shared/common-workflow.md +70 -80
  112. package/dist/index.js +1 -1
  113. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- # Fixtures and edge cases
1
+ # Fixtures for edge cases
2
2
 
3
- **Goal.** Have a file that contains the awkward cases *before* you are asked to handle them, so a fix is tested against a 1600-word paragraph and not only against clean prose.
3
+ Build a file containing the awkward cases before being asked to handle them: a fix proven on clean prose may still mangle a 1600-word paragraph.
4
4
 
5
- ## Build the fixture set
5
+ ## Build
6
6
 
7
7
  ```bash
8
8
  "<python>" "<skill>/scripts/make_fixtures.py" --outdir "<work-dir>" --only all
@@ -10,41 +10,27 @@
10
10
 
11
11
  Writes:
12
12
 
13
- | File | Contains |
14
- |---|---|
15
- | `tracked.docx` | A replacement as real `w:del`+`w:ins` markup, tracking switched on |
16
- | `watermark.docx` | A DRAFT watermark in the header |
17
- | `comments.docx` | One anchored comment in `word/comments.xml` |
18
- | `edge_cases.docx` | Empty paragraph, H1 to H3 jump, 1600-word paragraph, email and phone, 2×10 unstyled table, image with no alt text |
13
+ - `tracked.docx` — a replacement as real `w:del`+`w:ins`, tracking on.
14
+ - `watermark.docx` — a DRAFT watermark in the header.
15
+ - `comments.docx` — one anchored comment in `word/comments.xml`.
16
+ - `edge_cases.docx` — an empty paragraph, an H1→H3 jump, a 1600-word paragraph, an email and a phone number, a 2×10 unstyled table, and an image with no alt text.
19
17
 
20
- `--only tracked|watermark|comments|edge` builds one.
18
+ `--only tracked|watermark|comments|edge` builds a single file. These are test inputs, not templates: never ship them.
21
19
 
22
- These are inputs for testing a change, not templates. Do not ship them.
20
+ ## Cases that break naive edits
23
21
 
24
- ## The cases that break naive edits
22
+ - **Long paragraphs.** A replacement that rebuilds the paragraph loses run formatting over thousands of characters. Check with `read_image`, not only a text dump.
23
+ - **Nested tables.** Text inside `table > cell > table > cell` is missed by `document.paragraphs`; walk `cell.tables` or use the pack scripts.
24
+ - **Header and footer text.** Body find-and-replace never sees it.
25
+ - **Tables without a style.** Borders come from the renderer default and change elsewhere: apply `Table Grid` or explicit `#D9D9D9` borders.
26
+ - **Images with no alt text.** An accessibility finding and a sign of pasted content.
27
+ - **Manual spacing.** Empty paragraphs and spaces render differently; normalize first (`tasks/style_lint_normalize.md`).
28
+ - **Mixed page sizes.** A landscape section in a portrait document confuses headers and footers; `section_audit.py` reports it.
25
29
 
26
- - **Long paragraphs** — a replacement that rebuilds the paragraph loses run formatting across thousands of characters; check the result with `read_image`, not only with a text dump.
27
- - **Nested tables** — text inside `table > cell > table > cell` is missed by `document.paragraphs`; walk `cell.tables` or use the pack scripts, which do.
28
- - **Header/footer text** — a find-and-replace over the body never sees it.
29
- - **Tables without a style** — borders come from the renderer default and change when the file is opened elsewhere; apply `Table Grid` or set explicit `#D9D9D9` borders.
30
- - **Images with no alt text** — an accessibility finding, and also a sign the image was pasted rather than placed.
31
- - **Manual spacing** — a document padded with empty paragraphs and spaces looks right in one renderer and wrong in the next; normalize instead (`tasks/style_lint_normalize.md`).
32
- - **Mixed page sizes** — a landscape section inside a portrait document confuses footers and headers; `section_audit.py` reports it.
30
+ ## Regression pass
33
31
 
34
- ## Regression pass before delivery
35
-
36
- ```bash
37
- for f in "<work-dir>"/*.docx; do
38
- echo "== $f"
39
- "<python>" "<skill>/scripts/heading_audit.py" "$f" || true
40
- "<python>" "<skill>/scripts/style_lint.py" "$f" || true
41
- "<python>" "<skill>/scripts/a11y_audit.py" "$f" || true
42
- "<python>" "<skill>/scripts/section_audit.py" "$f" || true
43
- done
44
- ```
45
-
46
- An audit that now reports nothing where it previously reported a real problem is the signal that a fix regressed. Keep the fixture out of the user's workspace; put it under the temp directory.
32
+ Loop over `"<work-dir>"/*.docx` running `heading_audit.py`, `style_lint.py`, `a11y_audit.py` and `section_audit.py`, each with `|| true`. An audit that now reports nothing where it used to report a real problem is a regression. Keep fixtures under the temp directory, out of the user's workspace.
47
33
 
48
34
  ## Verify
49
35
 
50
- Whatever the script produced, render it. Fixtures exist to be looked at: a 1600-word paragraph and a table that is too wide both fail visually first.
36
+ Render whatever the script produced — fixtures exist to be looked at. The long paragraph and the too-wide table fail visually first.
@@ -1,18 +1,16 @@
1
1
  # Footnotes and endnotes
2
2
 
3
- **Goal.** Attach a reference note at exactly one place in the text, and keep the note body in the notes part rather than in the body.
3
+ Notes live outside the text flow: `word/footnotes.xml` (or `word/endnotes.xml`) holds the note text, a `w:footnoteReference w:id="N"` run in the body points at it, and the part needs the separator notes (ids -1 and 0) or Word repairs the file.
4
4
 
5
- Notes live outside the flow: `word/footnotes.xml` (or `word/endnotes.xml`) holds the text, and a `w:footnoteReference w:id="N"` run in the body points at it. The part also needs the separator notes (ids -1 and 0) or Word repairs the file.
5
+ ## Insert
6
6
 
7
- ## Insert a note
8
-
9
- Put a marker where the reference belongs, then convert it:
7
+ Put a marker where the reference belongs, then run:
10
8
 
11
9
  ```bash
12
- "<python>" "<skill>/scripts/insert_note.py" "<file.docx>" --kind footnote --text "Source: ledger extract, June." --out "<file-notes.docx>"
10
+ "<python>" "<skill>/scripts/insert_note.py" "<file.docx>" --kind footnote --text "Source: ledger extract, June." --out "<file-notes.docx"
13
11
  ```
14
12
 
15
- The marker defaults to `[[NOTE]]`; change it with `--marker`. The script creates the notes part (with separator and continuation-separator entries) if it does not exist, allocates the next free id, splits the surrounding run so only the marker is replaced by the reference, and styles the note text with `FootnoteText` and the reference with `FootnoteReference`. `--kind endnote` writes to the endnotes part instead.
13
+ The marker defaults to `[[NOTE]]` (`--marker`). The script creates the notes part with separator and continuation-separator entries when they are missing, allocates the next free id, splits the surrounding run so only the marker is replaced, and styles the note text `FootnoteText` and the reference `FootnoteReference`. `--kind endnote` writes the endnotes part instead.
16
14
 
17
15
  ## Report
18
16
 
@@ -20,19 +18,19 @@ The marker defaults to `[[NOTE]]`; change it with `--marker`. The script creates
20
18
  "<python>" "<skill>/scripts/footnotes_report.py" "<file.docx>"
21
19
  ```
22
20
 
23
- Lists each note's text, how many references point at it, and which references dangle or which notes are orphaned. Orphans appear when content is copied between documents; dangling references appear when a note is deleted but its marker stays.
21
+ Lists each note's text and reference count, dangling references (note deleted, marker kept) and orphaned notes (no reference — usually copied content).
24
22
 
25
23
  ## Renumbering
26
24
 
27
- Note ids are numbers, but Word displays them sequentially by position, so inserting a note in the middle renumbers the rest automatically. Do not try to keep ids in display order — keep them unique.
25
+ Ids are numbers, but Word displays notes sequentially by position, so inserting in the middle renumbers automatically. Keep ids unique, not in display order.
28
26
 
29
27
  ## Verify
30
28
 
31
- Render the page with the note and read the note area: the marker should sit immediately after the word it annotates, with no space before it, and the note text should match. Also check the last page — an endnote section that lands after the signature block is a layout defect.
29
+ Render the page with the note. The marker sits immediately after the word it annotates, with no space before it, and the note text matches. Check the last page too: an endnote section after the signature block is a layout defect.
32
30
 
33
31
  ## Pitfalls
34
32
 
35
- - A footnote reference inside a table cell is allowed; inside a header it is not.
36
- - Copying a paragraph with `python-docx` copies the reference but not the note body — recreate the note in the target document.
37
- - Deleting note text without deleting the reference leaves a broken document that opens with a repair prompt.
38
- - Field instructions are not allowed in the note separator; keep the built-in separator notes untouched.
33
+ - A footnote reference is allowed in a table cell, not in a header.
34
+ - Copying a paragraph with python-docx copies the reference but not the note body.
35
+ - Deleting note text without its reference leaves a repair prompt.
36
+ - Field instructions are not allowed in the note separator; leave the built-in separators untouched.
@@ -1,48 +1,44 @@
1
- # Forms and content controls
1
+ # Content controls (form fields)
2
2
 
3
- **Goal.** Fill or create a structured field — a name, a date, a choice — as a real content control, so the value is labelled in the XML instead of being plain text between two labels.
3
+ A content control is a `w:sdt` with a `w:sdtPr` (tag, alias, id, type) and a `w:sdtContent`. The tag is the machine name; the alias is what Word shows in the properties panel.
4
4
 
5
- A content control is `w:sdt` with `w:sdtPr` (tag, alias, id, type) and `w:sdtContent`. The tag is the machine name; the alias is what Word shows in the properties panel.
6
-
7
- ## List what exists
5
+ ## List
8
6
 
9
7
  ```bash
10
8
  "<python>" "<skill>/scripts/content_controls.py" list "<file.docx>"
11
9
  ```
12
10
 
13
- Reports each control's part, tag, alias, id, type and current text, across the body, headers and footers. `--no_headers_footers` limits it to the body.
11
+ Reports part, tag, alias, id, type and current text across the body, headers and footers. `--no_headers_footers` limits the scan to the body.
14
12
 
15
13
  ## Create
16
14
 
17
15
  ```bash
18
- "<python>" "<skill>/scripts/content_controls.py" create "<file.docx>" \
19
- --placeholder "Client name" --tag client_name --alias "Client name" --out "<file-form.docx>"
16
+ "<python>" "<skill>/scripts/content_controls.py" create "<file.docx>" --placeholder "Client name" --tag client_name --alias "Client name" --out "<file-form.docx"
20
17
  ```
21
18
 
22
- The script wraps the paragraph containing the placeholder text (or appends a new one) in a `w:sdt`, so the field is a drop-in replacement for a typed placeholder line. `--json` prints the created control instead of a summary.
19
+ Wraps the paragraph containing the placeholder text (or appends one) in a `w:sdt`, so the field is a drop-in replacement for a typed placeholder line. `--json` prints the created control.
23
20
 
24
21
  ## Fill
25
22
 
26
23
  ```bash
27
- "<python>" "<skill>/scripts/content_controls.py" set "<file-form.docx>" \
28
- --set client_name="Northwind" --set period="Q2 FY26" --out "<file-filled.docx>"
24
+ "<python>" "<skill>/scripts/content_controls.py" set "<file-form.docx>" --set client_name="Northwind" --set period="Q2 FY26" --out "<file-filled.docx"
29
25
  ```
30
26
 
31
- `--set TAG=VALUE` matches on the tag. The command exits 1 and lists `missing_tags` when a tag is not present — that is the check for a scripted fill, so a typo does not silently write nothing.
27
+ `--set TAG=VALUE` matches on the tag. It exits 1 and lists `missing_tags` when a tag is absent — the check for a scripted fill.
32
28
 
33
29
  ## Design
34
30
 
35
- - One control per value; do not put a whole table into one control.
36
- - Tag in `snake_case`, alias in the user's words.
37
- - A choice control lists its options in `w:sdtPr`; a date control has `w:date` with a format. Plain text is the default and the safest thing to generate.
38
- - Leave the surrounding labels in the body text; the alias is visible only in Word's properties panel.
31
+ - One control per value, not one per table.
32
+ - Tag `snake_case`, alias in the user's words.
33
+ - A choice control lists options in `w:sdtPr`; a date control has `w:date` with a format; plain text is the default and safest.
34
+ - Leave surrounding labels in body text: the alias is visible only in Word's properties panel.
39
35
 
40
36
  ## Verify
41
37
 
42
- `content_controls.py list` after filling: tags present with the expected text. Then render — a control's content is normal text on the page, so the visual check is that nothing shifted and the filled value fits its line.
38
+ `content_controls.py` `list` after filling (tags present with the expected text), then render. A control is ordinary text on the page, so check that nothing shifted and the filled value fits its line.
43
39
 
44
40
  ## Pitfalls
45
41
 
46
- - Deleting an `sdt` wrapper while trying to "clear" a field deletes the value and the structure; set the text instead.
47
- - Two controls with the same tag make `--set` ambiguous; the script updates the first and reports the rest.
48
- - A content control cannot span two paragraphs without `w:sdtContent` holding both — keep one value per paragraph when generating.
42
+ - Deleting the `sdt` wrapper to clear a field deletes value and structure; set the text instead.
43
+ - Two controls with the same tag make `--set` ambiguous: it updates the first and reports the rest.
44
+ - A control cannot span two paragraphs unless `w:sdtContent` holds both, so generate one value per paragraph.
@@ -1,8 +1,6 @@
1
1
  # Headings and numbering
2
2
 
3
- **Goal.** A single outline a reader can follow and Word can turn into a TOC: no skipped levels, no headings faked with bold text, consistent spacing.
4
-
5
- Heading level comes from the paragraph style (`Heading 1`…`Heading 9`, `Title` uses outline level 0), not from size or weight. `heading_audit.py` reads that structure and flags the common breakages.
3
+ A heading level comes from the paragraph style (`Heading 1` … `Heading 9`, `Title` = outline level 0), not from size or weight.
6
4
 
7
5
  ## Audit
8
6
 
@@ -12,44 +10,41 @@ Heading level comes from the paragraph style (`Heading 1`…`Heading 9`, `Title`
12
10
 
13
11
  Findings:
14
12
 
15
- - `skipped_level` — a level 3 heading directly under a level 1. Fix by inserting the missing level or by promoting the deeper heading.
16
- - `empty_heading` — a heading with no text; a stale outline entry that will appear in the TOC as a blank line.
17
- - `manual_formatting` — a real heading that also carries direct bold or a large font size; the style already does that, so the extra formatting fights restyling.
18
- - `heading_not_styled` — body text that looks like a heading (bold, 13pt+) but is styled `Normal`. This is the expensive one: the TOC misses it and every later style change misses it.
13
+ - `skipped_level` — L3 directly under L1: insert the missing level or promote the heading.
14
+ - `empty_heading` — a blank line in the TOC.
15
+ - `manual_formatting` — a real heading carrying direct bold or a large size, which fights restyling.
16
+ - `heading_not_styled` — bold 13pt+ text in `Normal`: the TOC and every later style change miss it.
19
17
 
20
18
  ## Fix
21
19
 
22
- Prefer styles over direct formatting. `style_normalize.py --clear_paragraph_format --enforce_heading_spacing` strips direct run formatting and sets standard space-before/after on headings:
20
+ Prefer real styles. To strip direct run formatting and set standard heading spacing:
23
21
 
24
22
  ```bash
25
- "<python>" "<skill>/scripts/style_normalize.py" "<file.docx>" --out "<file-fixed.docx>" --clear_paragraph_format --enforce_heading_spacing
23
+ "<python>" "<skill>/scripts/style_normalize.py" "<file.docx>" --out "<file-fixed.docx" --clear_paragraph_format --enforce_heading_spacing
26
24
  ```
27
25
 
28
- For a heading that is only bold text, promote it to the right style rather than making it bolder:
26
+ To promote bold text to a real heading:
29
27
 
30
- ```bash
31
- "<python>" - <<'PY'
32
- from docx import Document
33
- d = Document(r"<file.docx>")
28
+ ```python
29
+ d = Document("<file.docx>")
34
30
  for p in d.paragraphs:
35
31
  if p.text.strip() == "<exact heading text>":
36
32
  p.style = d.styles["Heading 1"]
37
- d.save(r"<file-fixed.docx>")
38
- PY
33
+ d.save("<file-fixed.docx")
39
34
  ```
40
35
 
41
36
  ## Numbered headings
42
37
 
43
- Numbering is a numbering definition attached to the heading style, not text like `1.2`. Two consequences: a typed number keeps lying after an insert, and a numbered outline needs `numPr` on the style with an `abstractNum` that uses `%1.%2` level text. When a document must have numbered sections and the original numbering is lost, restore the style's `numPr` rather than typing numbers into the headings.
38
+ Numbering is a definition attached to the heading style, not text such as `1.2`: a typed number lies after the next insert. A numbered outline needs `numPr` on the style, with an `abstractNum` whose level text is `%1.%2`. Restore the style's `numPr` instead of typing numbers.
44
39
 
45
- Lists are the same machinery at body level: `List Number` restarts only when the numbering definition says so, which is why two separate numbered lists sometimes continue each other.
40
+ Lists are the same machinery at body level. `List Number` restarts only when the numbering definition says so, which is why two separate lists sometimes continue each other.
46
41
 
47
42
  ## Verify
48
43
 
49
- `heading_audit.py` should exit 0, then render and read the page: heading hierarchy visible, no orphaned heading at the bottom of a page (`keepNext`), and the same spacing before every heading of a level.
44
+ `heading_audit.py` exits 0 on a clean file. Render, then check that no heading is orphaned at the bottom of a page (`keepNext`) and that headings of one level share their spacing.
50
45
 
51
46
  ## Pitfalls
52
47
 
53
- - Changing a heading's style does not change its text, but it can change its size and colour — re-render.
54
- - A heading inside a table cell still counts for the outline, which is rarely wanted; use a bold paragraph there.
55
- - Copying a heading from another document brings a direct-formatting payload with it; run the audit afterwards.
48
+ - Changing a heading's style leaves its text alone but can change size and colour: re-render.
49
+ - A heading inside a table cell still counts for the outline, which is rarely wanted — use a bold paragraph.
50
+ - Copying a heading brings a direct-formatting payload, so run the audit afterwards.
@@ -1,8 +1,6 @@
1
1
  # Images and figures
2
2
 
3
- **Goal.** Pictures that are described for screen readers, sized for the page, and captioned consistently.
4
-
5
- An inline picture is a `w:drawing` containing `wp:inline` (or `wp:anchor` for a floating one), with the picture relationship in `a:blip r:embed` and the display size in `wp:extent` (EMU). Alt text lives in `wp:docPr @descr`.
3
+ An inline picture is a `w:drawing` containing `wp:inline` (`wp:anchor` for floating), the picture relationship in `a:blip r:embed`, the display size in `wp:extent` (EMU), and the alt text in `wp:docPr @descr`.
6
4
 
7
5
  ## Audit
8
6
 
@@ -10,54 +8,46 @@ An inline picture is a `w:drawing` containing `wp:inline` (or `wp:anchor` for a
10
8
  "<python>" "<skill>/scripts/images_audit.py" "<file.docx>" --max_rows 20
11
9
  ```
12
10
 
13
- For every image: the part it sits in, its name, the media file behind it, its size in inches, and the alt text. Findings are `missing_alt` and `oversized` (wider than the text block). The workable check is "does the alt text describe what a reader loses if the picture is gone".
11
+ Reports each picture's part, name, media file, size in inches and alt text, plus `missing_alt` and `oversized` (wider than the text block). The real test is whether the alt text describes what a reader loses without the picture.
14
12
 
15
13
  ## Fix alt text
16
14
 
17
15
  ```bash
18
- "<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --fix_image_alt from_filename --out "<file-fixed.docx>"
16
+ "<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --fix_image_alt from_filename --out "<file-fixed.docx"
19
17
  ```
20
18
 
21
- That writes the file name when no description exists — better than nothing, not as good as real text. For real descriptions, set `descr` yourself; decorative images get an empty description and `title`, while informative images get a sentence:
19
+ A file name is better than nothing but not as good as a real description. For real text, patch `wp:docPr` directly:
22
20
 
23
- ```bash
24
- "<python>" - <<'PY'
25
- import sys
26
- from pathlib import Path
21
+ ```python
27
22
  sys.path.insert(0, r"<skill>/scripts")
28
23
  import docx_kit as kit
29
24
  d = kit.Docx(r"<file.docx>")
30
25
  for doc_pr in d.xml("word/document.xml").iter(kit.q("docPr", kit.WP)):
31
26
  if doc_pr.get("name") == "Picture 3":
32
27
  doc_pr.set("descr", "Bar chart: revenue by region, EMEA ahead of APAC.")
33
- out = Path(r"<file-fixed.docx>")
34
- d.save(out)
35
- print("saved", out)
36
- PY
28
+ d.save("<file.docx>")
37
29
  ```
38
30
 
31
+ Decorative images get an empty description plus a `title`; informative images get a sentence.
32
+
39
33
  ## Insert
40
34
 
41
- ```bash
42
- "<python>" "<cli>" docx create --spec "<spec.json>" --out "<out.docx>"
43
- ```
35
+ `docx create --spec` block:
44
36
 
45
37
  ```json
46
38
  { "type": "image", "path": "<absolute.png>", "width": 5.5 }
47
39
  ```
48
40
 
49
- Width is in inches; the height follows the aspect ratio. An image wider than the text block gets clipped or shrinks unpredictably — 5.5in is a safe full-width figure on Letter with 1in margins.
50
-
51
- ## Figures
41
+ Width is in inches and height follows the aspect ratio. A picture wider than the text block is clipped or shrinks unpredictably; 5.5in is a safe full-width figure on Letter with 1in margins.
52
42
 
53
- Caption a picture the same way as a table (`tasks/captions_crossrefs.md`): `captions_and_crossrefs.py --figures` adds `Figure N` with a `SEQ` field and a `figN` bookmark. Keep the caption in the same block so the figure and its caption do not separate across a page break.
43
+ Figures get captions with `captions_and_crossrefs.py --figures` (`tasks/captions_crossrefs.md`), which adds `Figure N` with a `SEQ` field and a `figN` bookmark. Keep the caption in the same block so picture and caption do not separate.
54
44
 
55
45
  ## Verify
56
46
 
57
- Render and read the page: the image is not stretched, not blurry beyond the source resolution, not clipped at the margin, and its caption sits under it. Add alt text as a separate check — the render cannot show it.
47
+ Render: not stretched, not blurry beyond the source resolution, not clipped at the margin, caption underneath. Alt text is a separate check the render cannot show.
58
48
 
59
49
  ## Pitfalls
60
50
 
61
- - Cropping in Word is not lossy for the file but changes the displayed extent; alt text and size checks still apply.
62
- - A floating image (`wp:anchor`) can overlap text at any zoom level; prefer inline for documents that will be edited.
63
- - Replacing an image file in `word/media` in place keeps the relationship but not the dimensions — the extent is stored separately.
51
+ - Cropping changes the displayed extent; alt text and size checks still apply.
52
+ - A floating `wp:anchor` image can overlap text at any zoom — prefer inline for documents that will be edited.
53
+ - Replacing a file in `word/media` keeps the relationship but not the dimensions: the extent is stored separately.
@@ -1,43 +1,33 @@
1
1
  # Merge documents
2
2
 
3
- **Goal.** Put one document after another and keep both looking the same — styles, numbering, images, hyperlinks — instead of pasting content that loses half of it.
4
-
5
- Concatenating packages by hand fails on the details: style ids collide, `numId` values point at the wrong list, media parts need new names and relationships, and `r:id` values are document-scoped. Word also requires one `sectPr` at the end of the body, so the appended content has to land before it.
3
+ Concatenating packages by hand fails: style ids collide, `numId` values point at the wrong lists, media parts need new names and relationships, `r:id` values are document-scoped, and Word requires one `sectPr` at the end of the body, so appended content must land before it.
6
4
 
7
5
  ## Append
8
6
 
9
7
  ```bash
10
- "<python>" "<skill>/scripts/merge_docx_append.py" "<base.docx>" "<addition.docx>" --out "<merged.docx>"
8
+ "<python>" "<skill>/scripts/merge_docx_append.py" "<base.docx>" "<addition.docx>" --out "<merged.docx"
11
9
  ```
12
10
 
13
- What it carries across: body content (minus the addition's final `sectPr`), media parts renamed under `word/media` with fresh relationships and content types, style definitions referenced by the copied content (following `basedOn`/`next`/`link`), numbering definitions rebuilt with fresh ids and every `w:numId` rewritten, and hyperlink relationships recreated as external relationships.
14
-
15
- Drawings are refused by default: a floating image or a text box carries anchor geometry tied to the original page. Pass `--allow_drawings` when the user accepts that the layout may shift, then check that page carefully.
16
-
17
- ## Merge into a template
11
+ The script carries the body content minus the addition's final `sectPr`, renames media parts under `word/media` with fresh relationships and content types, copies style definitions referenced by the copied content (following `basedOn`/`next`/`link`), rebuilds numbering definitions with fresh ids and rewrites every `w:numId`, and recreates hyperlink relationships as external relationships.
18
12
 
19
- The usual job is "put these sections into the house template". Apply the template first, then append, so the appended content inherits the template's style ids:
13
+ Drawings are refused by default: a floating image or text box carries anchor geometry tied to its original page. Use `--allow_drawings` when the user accepts a possible layout shift, then check that page.
20
14
 
21
- ```bash
22
- "<python>" "<skill>/scripts/apply_template_styles.py" --template "<house.dotx>" --target "<draft.docx>" --out "<draft-styled.docx>"
23
- "<python>" "<skill>/scripts/merge_docx_append.py" "<draft-styled.docx>" "<section2.docx>" --out "<merged.docx>"
24
- ```
15
+ ## Into a template
25
16
 
26
- Then normalize direct formatting so the merged content matches:
17
+ Apply the template first, then append, so content inherits the template's style ids:
27
18
 
28
19
  ```bash
29
- "<python>" "<skill>/scripts/style_normalize.py" "<merged.docx>" --out "<merged-final.docx>" --clear_paragraph_format
20
+ "<python>" "<skill>/scripts/apply_template_styles.py" --template "<house.dotx>" --target "<draft.docx>" --out "<draft-styled.docx"
21
+ "<python>" "<skill>/scripts/merge_docx_append.py" "<draft-styled.docx>" "<section2.docx>" --out "<merged.docx"
22
+ "<python>" "<skill>/scripts/style_normalize.py" "<merged.docx>" --out "<merged-final.docx" --clear_paragraph_format
30
23
  ```
31
24
 
32
25
  ## Verify
33
26
 
34
- - Page count should equal the sum of the inputs, unless a page break changed.
35
- - Headers and footers come from the base document; say so if the addition had its own.
36
- - Render and read the join: the page where the second document starts often shows a stray page break or a heading that lost its style.
37
- - `style_lint.py` on the merged file catches undefined style references imported from the addition.
27
+ Page count equals the sum of the inputs unless a page break changed. Headers and footers come from the base document — say so if the addition had its own. Render the join to catch a stray page break or a heading that lost its style. `style_lint.py` catches undefined style references imported from the addition.
38
28
 
39
29
  ## Pitfalls
40
30
 
41
- - Footnotes and comments in the addition are not merged; port them deliberately if they must survive.
42
- - `w:id` values for bookmarks and comments can collide across documents; the merge keeps ids and names as they were.
43
- - A landscape section in the addition is not carried over — section properties were dropped with the `sectPr`.
31
+ - Footnotes and comments in the addition are not merged (the port is deliberate).
32
+ - Bookmark and comment `w:id` values can collide; the merge keeps ids and names.
33
+ - A landscape section in the addition is not carried over: section properties are dropped with the `sectPr`.
@@ -1,38 +1,35 @@
1
- # Internal navigation
1
+ # Navigation and internal links
2
2
 
3
- **Goal.** Let a reader jump around a long document — from a contents list to a section, and back — without relying on the TOC field being recalculated.
3
+ An internal link is `w:hyperlink w:anchor="bookmark"`; the target is a `w:bookmarkStart`/`w:bookmarkEnd` pair. Both live in the body, so PDF export keeps the links.
4
4
 
5
- A hyperlink to a place inside the document is `w:hyperlink w:anchor="bookmark"`; the target is a `w:bookmarkStart`/`w:bookmarkEnd` pair with that name. Both live in the body, so an export to PDF keeps the links.
6
-
7
- ## Build a navigation set
5
+ ## Build navigation
8
6
 
9
7
  ```bash
10
- "<python>" "<skill>/scripts/internal_nav.py" "<file.docx>" --out "<file-nav.docx>" --levels 1 2 \
11
- --toc_title "Contents"
8
+ "<python>" "<skill>/scripts/internal_nav.py" "<file.docx>" --out "<file-nav.docx>" --levels 1 2 --toc_title "Contents"
12
9
  ```
13
10
 
14
- What it does, in order:
11
+ The script:
15
12
 
16
- 1. Bookmarks every heading as `nav_h1`, `nav_h2`, … using its outline level.
17
- 2. Inserts a `Contents` heading and a quick-link bar (one link per heading) at the top.
18
- 3. Indents each contents entry by 360 twips per level so the hierarchy is visible.
19
- 4. Appends a small `Back to contents` link after each heading.
20
- 5. Bookmarks the top, the contents list and the bottom (`nav_top`, `nav_toc`, `nav_bottom`).
13
+ - bookmarks every heading `nav_h1`, `nav_h2`, … by outline level;
14
+ - inserts a `Contents` heading and a quick-link bar at the top;
15
+ - indents contents entries 360 twips per level;
16
+ - appends a `Back to contents` link after each heading;
17
+ - bookmarks the top, the contents and the bottom as `nav_top`, `nav_toc`, `nav_bottom`.
21
18
 
22
- Flags: `--no_quicklinks` skips the horizontal bar, `--no_back_to_toc` skips the per-heading back link, `--no_top_bottom` skips the two edge targets.
19
+ Flags: `--no_quicklinks`, `--no_back_to_toc`, `--no_top_bottom`.
23
20
 
24
- The script reports `missing` when a link target could not be created — read that list, because a link without a bookmark is dead in Word and looks fine in a render.
21
+ It reports `missing` when a target could not be created. Read that list: a dead link looks fine in a render.
25
22
 
26
- ## Static versus field-driven
23
+ ## Static versus field
27
24
 
28
- `internal_nav.py` writes literal entries: it costs a rebuild when headings change, and it always looks the same in every renderer. `insert_toc.py` writes a live `TOC` field: it updates itself but needs a recalculation before it shows anything useful. A document that will be edited repeatedly wants the field; a document going out as a PDF export wants the static list.
25
+ `internal_nav.py` writes literal entries: rebuild when headings change, and every renderer shows the same result. `insert_toc.py` writes a live `TOC` field: self-updating, but it needs recalculation. A repeatedly edited document wants the field; a PDF export wants the static list.
29
26
 
30
27
  ## Verify
31
28
 
32
- Render, then click through the pages you can see: entry indentation, that the contents heading is styled as a heading, and that `Back to contents` did not push a paragraph onto a new page. Dead anchors do not show in a screenshot — check `bookmarkStart` names against the `w:anchor` values when the script reports a miss.
29
+ Render and click through: entry indentation, the contents heading styled as a heading, and `Back to contents` not pushing a paragraph onto a new page. Dead anchors do not show in a screenshot — when the script reports a miss, check `bookmarkStart` names against the `w:anchor` values.
33
30
 
34
31
  ## Pitfalls
35
32
 
36
- - Bookmarks must be unique per document; re-running the script on its own output creates `nav_h1` twice, so start from the original.
37
- - A hyperlink anchor is case-sensitive.
38
- - Link styling comes from the `Hyperlink` character style; if links look like body text, the style is missing, not the link.
33
+ - Bookmarks must be unique per document. Re-running on its own output duplicates `nav_h1`, so start from the original.
34
+ - Hyperlink anchors are case-sensitive.
35
+ - Link styling comes from the `Hyperlink` character style.
@@ -1,29 +1,23 @@
1
- # Privacy: metadata and residue
1
+ # Privacy and document metadata
2
2
 
3
- **Goal.** A file that carries only the content the user meant to send: no author name, no tracked review history, no comments, no template identity, no editing session ids.
3
+ A `.docx` stores more than text: `docProps/core.xml` (author, last modifier, title, keywords, description), `docProps/app.xml` (company, manager, template), `docProps/custom.xml`, `word/comments.xml`, revision data and per-author `w:rsid` stamps.
4
4
 
5
- `.docx` stores more than the text: `docProps/core.xml` (author, last modifier, title, keywords, description), `docProps/app.xml` (company, manager, template), `docProps/custom.xml`, `word/comments.xml`, revision data and per-author `w:rsid` attributes.
6
-
7
- ## Scrub metadata
5
+ ## Scrub
8
6
 
9
7
  ```bash
10
- "<python>" "<skill>/scripts/privacy_scrub.py" "<file.docx>" --out "<file-clean.docx>"
8
+ "<python>" "<skill>/scripts/privacy_scrub.py" "<file.docx>" --out "<file-clean.docx"
11
9
  ```
12
10
 
13
- Clears creator, last-modified-by, keywords, description, subject, title, category, company, manager and application; resets the revision counter; drops `docProps/custom.xml` and its relationships and content-type override; strips `w:rsid*` attributes, `w:proofErr` and `w:lastRenderedPageBreak` across the Word parts.
14
-
15
- `--keep_title` leaves the title in place when the title is real content the user wants.
11
+ Clears creator, last-modified-by, keywords, description, subject, title, category, company, manager and application; resets the revision counter; drops `docProps/custom.xml` with its relationships and content-type override; strips `w:rsid*`, `w:proofErr` and `w:lastRenderedPageBreak` across the Word parts. `--keep_title` preserves a real title.
16
12
 
17
- ## The rest of the residue
13
+ ## Other residue
18
14
 
19
- | Residue | Command |
20
- |---|---|
21
- | Comments and their authors | `comments_strip.py` |
22
- | Tracked changes and reviewer names | `accept_tracked_changes.py --mode accept` (or reject) |
23
- | Watermarks naming the old client | `watermark_audit_remove.py --mode remove` |
24
- | Emails and phone numbers in the text | `redact_docx.py` |
15
+ - Comments and their authors → `comments_strip.py`.
16
+ - Tracked changes and reviewer names → `accept_tracked_changes.py --mode accept` (or reject).
17
+ - A watermark naming the old client → `watermark_audit_remove.py --mode remove`.
18
+ - Emails and phone numbers → `redact_docx.py`.
25
19
 
26
- Run them in that order — scrub metadata, resolve markup, then redact the text that survived.
20
+ Order matters: scrub metadata, resolve markup, then redact the surviving text.
27
21
 
28
22
  ## Check before delivery
29
23
 
@@ -32,14 +26,14 @@ Run them in that order — scrub metadata, resolve markup, then redact the text
32
26
  "<python>" "<skill>/scripts/docx_kit.py" xml '<file-clean.docx>' docProps/core.xml
33
27
  ```
34
28
 
35
- `parts` shows whether `docProps/custom.xml` or `word/comments.xml` still exist; the core part shows what a recipient would see in File > Info. Both are cheap and both catch the mistake the scripts did not.
29
+ `parts` shows whether `custom.xml` and `comments.xml` still exist; `core` shows what a recipient sees under File > Info.
36
30
 
37
31
  ## Verify
38
32
 
39
- Metadata is not visible in a page render. Verify with the commands above, and render to be sure the document still looks right after the scrub — clearing `w:lastRenderedPageBreak` can change where a viewer chooses to break a long line.
33
+ Metadata never appears in a render, so use those two commands. Render afterwards to confirm the document still looks right: clearing `w:lastRenderedPageBreak` can change where a viewer breaks a long line.
40
34
 
41
35
  ## Pitfalls
42
36
 
43
- - Author names also appear in comment and revision entries, not only in core properties.
44
- - `docProps/app.xml` names the editing application; it is regenerated on next save by whatever app opens the file.
45
- - Removing revision ids does not undo content — the text is still there.
37
+ - Author names also appear in comment and revision entries.
38
+ - `docProps/app.xml` names the editing application and is regenerated on the next save.
39
+ - Removing revision ids does not undo content.
@@ -1,41 +1,39 @@
1
1
  # Restrict editing
2
2
 
3
- **Goal.** Stop a reviewer from changing the body while still letting them comment or track changes — the "send for review" state, not encryption.
3
+ Restrictions live in `w:documentProtection` inside `word/settings.xml`. Modes: `readOnly`, `comments`, `trackedChanges`, `forms`, each with `enforcement`. This is a guardrail against accidental edits, not encryption: a determined reader can remove it.
4
4
 
5
- Editing restrictions live in `w:documentProtection` inside `word/settings.xml`. The modes are `readOnly`, `comments`, `trackedChanges`, `forms`, plus `enforcement` to switch the restriction on. This is a guardrail against accidental edits; it does not encrypt content and a determined reader can remove it.
6
-
7
- ## Set a mode
5
+ ## Set
8
6
 
9
7
  ```bash
10
- "<python>" "<skill>/scripts/set_protection.py" "<file.docx>" --mode comments --out "<file-review.docx>"
11
- "<python>" "<skill>/scripts/set_protection.py" "<file.docx>" --mode readOnly --no_enforce --out "<file-open.docx>"
8
+ "<python>" "<skill>/scripts/set_protection.py" "<file.docx>" --mode comments --out "<file-review.docx"
9
+ "<python>" "<skill>/scripts/set_protection.py" "<file.docx>" --mode readOnly --no_enforce --out "<file-open.docx"
12
10
  ```
13
11
 
14
- Modes and aliases: `off`/`none`, `readOnly`/`read_only`, `comments`/`comment`, `trackedChanges`/`tracked_changes`, `forms`/`form`. `--no_enforce` writes the setting without switching it on, which is useful when the opening dialog should be avoided.
12
+ Mode aliases: `off` / `none`, `readOnly` / `read_only`, `comments` / `comment`, `trackedChanges` / `tracked_changes`, `forms` / `form`. `--no_enforce` writes the setting without switching it on, which avoids the dialog on open.
15
13
 
16
- `--hash` also writes a password hash (SHA-512, 100000 spins; `--salt`, `--spin_count` to change it). Be honest with the user: the hash deters casual edits, it is not encryption, and losing the password means the restriction has to be stripped from the XML.
14
+ `--hash` also writes a password hash (SHA-512, 100000 spins; `--salt` and `--spin_count` change the parameters). Say plainly that it deters casual edits, is not encryption, and that losing the password means stripping the restriction from the XML.
17
15
 
18
- ## Which mode to pick
16
+ ## Choose a mode
19
17
 
20
- | Intent | Mode |
18
+ | Requirement | Mode |
21
19
  |---|---|
22
- | Review comments only, no text edits | `comments` |
23
- | All edits should be tracked | `trackedChanges` |
24
- | Fill a form, nothing else | `forms` |
20
+ | Comments only | `comments` |
21
+ | All edits tracked | `trackedChanges` |
22
+ | Fill a form | `forms` |
25
23
  | View only | `readOnly` |
26
24
 
27
- ## Remove
25
+ Remove the restriction with `--mode off --out "<file-open.docx"`.
26
+
27
+ ## Verify
28
28
 
29
29
  ```bash
30
- "<python>" "<skill>/scripts/set_protection.py" "<file-protected.docx>" --mode off --out "<file-open.docx>"
30
+ "<python>" "<skill>/scripts/docx_kit.py" xml '<file.docx>' word/settings.xml | grep documentProtection
31
31
  ```
32
32
 
33
- ## Verify
34
-
35
- `docx_kit.py xml '<file.docx>' word/settings.xml | grep documentProtection` shows the element and its enforcement attribute. Then open behaviour matters, not the page — render will not show a restriction. Say which mode you applied, and that removing it needs the same script or Word's Review pane.
33
+ The element and its enforcement setting appear in the output. Open behaviour is what matters, not the page: a render never shows a restriction. State the mode you applied and that removing it needs the same script or Word's Review pane.
36
34
 
37
35
  ## Pitfalls
38
36
 
39
- - Protected documents that also contain tracked changes and comments often prompt on open; apply the restriction last.
40
- - `forms` mode without form fields leaves a document nobody can edit usefully.
41
- - Protection marked with a hash cannot be turned off in Word without the password, but `--mode off` still clears it — that is why it is a guardrail, not security.
37
+ - A protected document that also holds tracked changes or comments often prompts on open; apply the restriction last.
38
+ - `forms` without form fields leaves a document nobody can use.
39
+ - A hashed restriction cannot be turned off in Word without the password, yet `--mode off` clears it — which is why this is a guardrail, not security.