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,26 +1,17 @@
1
1
  # Read and review a file
2
2
 
3
- **Goal.** Understand an existing document before touching it: what it contains, how it is structured, and what is wrong with it.
4
-
5
- ## Read the content
3
+ ## Read
6
4
 
7
5
  ```bash
8
6
  "<python>" "<cli>" docx read --input "<file.docx>"
9
- ```
10
-
11
- Returns the title, author, paragraphs with their styles, headings, tables, image and equation counts, and page dimensions. Use it as the first look, including for files you produced earlier in the session.
12
-
13
- ## Read the structure
14
-
15
- ```bash
16
7
  "<python>" "<skill>/scripts/docx_kit.py" parts '<file.docx>' # every package part and its size
17
8
  "<python>" "<skill>/scripts/docx_kit.py" text '<file.docx>' # paragraphs with style and outline level
18
9
  "<python>" "<skill>/scripts/docx_kit.py" xml '<file.docx>' word/document.xml
19
10
  ```
20
11
 
21
- `parts` tells you which features exist at all (a `comments.xml` means the file carries comments; its absence means none, even if the user says otherwise). `text` is faster than reading raw XML when you only need the words and their heading levels.
12
+ `docx read` gives the title, author, styled paragraphs, headings, table/image/equation counts and page dimensions — the first look, including for files produced earlier in the session. `parts` shows which features exist at all: no `comments.xml`, no comments, whatever the user says. `text` is faster than raw XML when you only need the words and their heading levels.
22
13
 
23
- ## Audit what is there
14
+ ## Audit
24
15
 
25
16
  | Question | Command |
26
17
  |---|---|
@@ -32,19 +23,17 @@ Returns the title, author, paragraphs with their styles, headings, tables, image
32
23
  | Are there footnotes or endnotes? | `footnotes_report.py` |
33
24
  | What comments exist, and on what text? | `comments_extract.py` |
34
25
 
35
- Every audit script prints JSON and exits non-zero when it found something, so the exit code alone tells you whether the document needs attention. Read the `findings` array rather than the exit code when you need the reason.
26
+ Each audit prints JSON and exits non-zero when it found something: the exit code says whether the document needs attention, the `findings` array says why.
36
27
 
37
28
  ## Review for the user
38
29
 
39
- When the job is "review this document" rather than "change it":
40
-
41
- 1. Read the text, not just the audits. Point out the argument, not only the formatting.
30
+ 1. Read the text, not just the audits. Address the argument, not only the formatting.
42
31
  2. Separate defects (broken table, skipped heading level, missing alt text) from style preferences.
43
- 3. Cite locations by section heading and paragraph text — Word has no stable page numbers.
32
+ 3. Cite locations by section heading and paragraph text: Word has no stable page numbers.
44
33
  4. Offer the fix as a concrete next action, with the command that performs it.
45
34
 
46
35
  ## Pitfalls
47
36
 
48
- - `docx read` shows the body only; headers, footers, footnotes and comments need `docx_kit.py xml` or the dedicated report scripts.
49
- - Tracked changes are visible as plain text in a read; use `accept_tracked_changes.py --mode report` to see what is proposed.
50
- - A field's displayed text is a cached result. `fields_report.py` shows the instruction, which is the truth.
37
+ - `docx read` shows the body only; headers, footers, footnotes and comments need `docx_kit.py xml` or the report scripts.
38
+ - Tracked changes read as plain text; `accept_tracked_changes.py --mode report` shows what is proposed.
39
+ - A field's displayed text is a cached result; `fields_report.py` shows the instruction, which is the truth.
@@ -1,26 +1,29 @@
1
1
  # Redaction and anonymisation
2
2
 
3
- **Goal.** Remove personal data — addresses, phone numbers, names, account references — from a document before it leaves the organisation, in the text and in the places text hides.
4
-
5
- Text lives in `w:t` (and `w:delText` for deletions, `w:instrText` for field instructions). A redaction that only rewrites paragraphs via `python-docx` misses headers, footers, footnotes and comments.
3
+ Text lives in `w:t` (plus `w:delText` for deletions and `w:instrText` for field instructions). A redaction that only rewrites paragraphs through python-docx misses headers, footers, footnotes and comments.
6
4
 
7
5
  ## Redact
8
6
 
9
7
  ```bash
10
8
  "<python>" "<skill>/scripts/redact_docx.py" "<file.docx>" "<file-redacted.docx>" --emails --phones
9
+ ```
10
+
11
+ ```bash
11
12
  "<python>" "<skill>/scripts/redact_docx.py" "<file.docx>" "<file-redacted.docx>" --pattern "ACCT-\d{6}" --replacement "<account>"
12
13
  ```
13
14
 
14
- - Default run covers emails and phone numbers; add `--pattern` (repeatable) for account numbers, case ids, postcodes.
15
- - `--mask_char *` keeps the shape of the value visible (`***@***.com` style), `--replacement` swaps in a token, `--no_preserve_length` stops padding to the original length.
16
- - `--include_comments` also rewrites `word/comments.xml`; without it, comments keep the original text.
17
- - Applied to body, every header and footer, footnotes and endnotes.
15
+ `--pattern` is repeatable — use it for account numbers, case ids and postcodes. It applies to the body, every header and footer, footnotes and endnotes.
18
16
 
19
- Matching happens per text run, because merging runs would destroy the surrounding formatting. A value split across runs (bolded digits inside a phone number) is not found — an acceptable trade only if you check the result; when the file mixes formatting inside the sensitive value, flatten that paragraph first (`tasks/style_lint_normalize.md`).
17
+ | Flag | Effect |
18
+ |---|---|
19
+ | `--mask_char *` | masks the shape of the value instead of replacing it (`***@***.com`) |
20
+ | `--replacement` | swap in a token instead of masking |
21
+ | `--no_preserve_length` | stop padding the result to the original length |
22
+ | `--include_comments` | also rewrite `word/comments.xml`; without it comments keep the original text |
20
23
 
21
- ## Redaction versus anonymisation
24
+ Matching happens per text run, because merging runs destroys formatting, so a value split across runs is not found. That is acceptable only if you check the result; when formatting is mixed inside the sensitive value, flatten that paragraph first (`tasks/style_lint_normalize.md`).
22
25
 
23
- Redaction removes the value; anonymisation replaces it consistently. If the same account number appears four times and the argument depends on it matching, replace all occurrences with the same token rather than `[redacted]` every time — `--replacement "<account-A>"` per pattern does that.
26
+ Redaction removes a value; anonymisation replaces it consistently. If the same account number appears four times and the argument depends on it matching, replace all occurrences with the same token rather than `[redacted]` every time — per-pattern `--replacement "<account-A>"` does that.
24
27
 
25
28
  ## Check the whole file
26
29
 
@@ -29,14 +32,10 @@ Redaction removes the value; anonymisation replaces it consistently. If the same
29
32
  "<python>" "<skill>/scripts/docx_kit.py" xml '<file-redacted.docx>' word/document.xml | grep -c "example.com"
30
33
  ```
31
34
 
32
- A grep over dumped XML is the honest check: run it for each sensitive term, across the document, header, footer, footnote and comment parts.
33
-
34
- ## Verify
35
-
36
- Render, then read the pages the data appeared on. A redaction that changed the line length enough to shift the layout is worth a look; a redaction that left a `<w:t>` intact shows up in the grep, not the page.
35
+ Grepping the dumped XML for each sensitive term across document, header, footer, footnote and comment parts is the honest check. Then render and read the pages the data appeared on: check line-length shifts, and remember an intact `<w:t>` shows in the grep, not on the page.
37
36
 
38
37
  ## Pitfalls
39
38
 
40
- - Field results and field instructions can both contain data; the script rewrites `w:t` and `w:delText` only.
41
- - Images are not scanned — a screenshot of a spreadsheet is not redacted by any text pass. Say so plainly.
42
- - Charts store category labels in `word/charts/*.xml`; inspect those parts when a chart carried the sensitive data.
39
+ - Field results and field instructions can both hold data; the script rewrites `w:t` / `w:delText` only.
40
+ - Images are not scanned. Say so plainly instead of implying the file is clean.
41
+ - Charts store category labels in `word/charts/*.xml`; inspect them when a chart carried the data.
@@ -1,8 +1,6 @@
1
- # Sections and page layout
1
+ # Sections, page setup, headers
2
2
 
3
- **Goal.** Page size, orientation, margins, columns, headers and footers that are deliberate at every point in the document, including after a mid-document change.
4
-
5
- Page setup lives in a `w:sectPr` element. The last one sits at the end of the body; earlier ones sit inside the last paragraph of the section they end (`w:pPr/w:sectPr`). That is why adding a landscape page means adding a section break before and after, not changing one global setting.
3
+ Page setup lives in `w:sectPr`. The last one sits at the end of the body; earlier ones sit inside the last paragraph of the section they end (`w:pPr/w:sectPr`). A landscape page therefore needs a section break before and after it, not one global setting.
6
4
 
7
5
  ## Audit
8
6
 
@@ -10,45 +8,40 @@ Page setup lives in a `w:sectPr` element. The last one sits at the end of the bo
10
8
  "<python>" "<skill>/scripts/section_audit.py" "<file.docx>"
11
9
  ```
12
10
 
13
- Per section: width and height in inches, orientation derived from them, every margin, column count, section start type, header and footer parts referenced, `titlePg` (different first page) and any explicit page-number start.
14
-
15
- Findings worth fixing: `mixed_page_sizes` (sections that disagree, usually a template artefact), `narrow_text_column` (left plus right margin eats most of the width), `portrait_wider_than_letter`.
11
+ Reports per section: width and height in inches, orientation derived from them, every margin, column count, section start type, header and footer parts referenced, `titlePg`, explicit page-number start. Findings: `mixed_page_sizes`, `narrow_text_column` (left and right margin eat most of the width), `portrait_wider_than_letter`.
16
12
 
17
13
  ## Common changes
18
14
 
19
- | Wanted | Route |
20
- |---|---|
21
- | New document defaults | `docx create` spec: `page_size`, `orientation`, `top_margin`/`bottom_margin`/`left_margin`/`right_margin` in inches |
22
- | A landscape page inside a portrait document | two section breaks with a landscape section between them |
23
- | Different first page header | set `titlePg` on the section and fill `first_page_header` |
24
- | Continuous section change (e.g. narrower margins) | `w:type val="continuous"` on the new section |
15
+ - New-document defaults: `docx create` spec keys `page_size`, `orientation`, `top_margin` / `bottom_margin` / `left_margin` / `right_margin` (inches).
16
+ - A landscape page inside a portrait document: two section breaks with the landscape section between them.
17
+ - A different first-page header: set `titlePg` and fill `first_page_header`.
18
+ - A continuous change such as narrower margins: `w:type val="continuous"`.
25
19
 
26
- Landscape and columns are section properties, so a snippet is often faster than a flag:
20
+ Landscape and columns are section properties, so a snippet is often faster than a script:
27
21
 
28
- ```bash
29
- "<python>" - <<'PY'
22
+ ```python
30
23
  from docx import Document
31
24
  from docx.enum.section import WD_ORIENT, WD_SECTION
25
+
32
26
  d = Document(r"<file.docx>")
33
27
  s = d.add_section(WD_SECTION.NEW_PAGE)
34
28
  s.orientation = WD_ORIENT.LANDSCAPE
35
29
  s.page_width, s.page_height = s.page_height, s.page_width
36
30
  d.save(r"<file-edited.docx>")
37
- PY
38
31
  ```
39
32
 
40
- Swap the width and height explicitly: setting the orientation alone does not swap them.
33
+ Setting the orientation alone does not swap the two size values.
41
34
 
42
35
  ## Headers and footers
43
36
 
44
- A header is `word/headerN.xml` referenced from `w:headerReference`. Headers inherit from the previous section unless the section sets `headerReference` itself. Page numbers go in the footer as a `PAGE` field, page counts as `NUMPAGES`; `docx_ooxml_patch.py --add-page-numbers` writes one.
37
+ `word/headerN.xml` is referenced from `w:headerReference`. A header inherits from the previous section unless the section sets its own reference. Page numbers in the footer are a `PAGE` field; counts are `NUMPAGES`; `docx_ooxml_patch.py --add-page-numbers` writes one.
45
38
 
46
39
  ## Verify
47
40
 
48
- Render and flip through every page, including the ones after a section break: header content, page numbers, orientation, margins. A section break that leaves an empty page is the classic defect — check for a stray empty paragraph carrying the break.
41
+ Render and flip through every page, including the pages after a section break: header content, page numbers, orientation, margins. The classic defect is a section break that leaves an empty page — look for a stray empty paragraph carrying it.
49
42
 
50
43
  ## Pitfalls
51
44
 
52
- - `w:pgSz` is width then height; a portrait page taller than wide is normal, a "landscape" 11×8.5 that still says portrait in the panel means only the size changed.
53
- - `titlePg` without filling the first-page header leaves page one with a blank header, not with the normal one.
54
- - Deleting a section break merges two sections and keeps the earlier section's settings; edit before deleting.
45
+ - `w:pgSz` holds width then height; a "landscape" 11×8.5 still marked portrait means only the size values changed.
46
+ - `titlePg` without a filled first-page header leaves page one blank instead of showing the normal header.
47
+ - Deleting a section break merges two sections and keeps the earlier settings.
@@ -1,44 +1,40 @@
1
- # Styles: lint and normalize
1
+ # Style lint and normalisation
2
2
 
3
- **Goal.** Formatting that comes from styles, not from pressing bold on each run, so the document can be restyled, exported and understood later.
4
-
5
- A paragraph carries a `pStyle` reference; a run carries an `rStyle` plus direct formatting (`w:b`, `w:i`, `w:sz`, `w:color`). Direct formatting wins over the style, which is why a "make all body text 11pt" request fails on the runs someone bolded and resized by hand.
3
+ A paragraph carries `pStyle`; a run carries `rStyle` plus direct formatting (`w:b`, `w:i`, `w:sz`, `w:color`). Direct formatting wins over the style, which is why "make all body text 11pt" fails on hand-formatted runs.
6
4
 
7
5
  ## Lint
8
6
 
9
7
  ```bash
10
- "<python>" "<skill>/scripts/style_lint.py" "<file.docx>" --json "<report.json>"
8
+ "<python>" "<skill>/scripts/style_lint.py" "<file.docx>" --json "<report.json"
11
9
  ```
12
10
 
13
- Findings: undefined style references, headings carrying their own paragraph or run formatting, empty runs, manual bullets (`•` typed into text instead of a list style), double spaces, trailing spaces, body sizes that appear once or twice against the dominant size, tables without a style.
14
-
15
- Exit code 1 means findings; the JSON has the counts and examples, which is what you want when the number is large.
11
+ Findings: undefined style references, headings carrying their own paragraph or run formatting, empty runs, manual bullets (`•` typed instead of a list style), double spaces, trailing spaces, body sizes appearing once or twice against the dominant size, tables without a style. Exit 1 means findings; the JSON holds counts and examples.
16
12
 
17
- ## Normalize
13
+ ## Normalise
18
14
 
19
15
  ```bash
20
- "<python>" "<skill>/scripts/style_normalize.py" "<file.docx>" --out "<file-clean.docx>" \
21
- --clear_paragraph_format --enforce_heading_spacing
16
+ "<python>" "<skill>/scripts/style_normalize.py" "<file.docx>" --out "<file-clean.docx" --clear_paragraph_format --enforce_heading_spacing
22
17
  ```
23
18
 
24
- - `--clear_paragraph_format` strips everything from each run's `rPr` except `rStyle`, `rtl` and `lang`, and drops the paragraph-mark run properties. The style then controls the look.
25
- - `--enforce_heading_spacing` sets space-before/space-after per heading level and `keepNext`, so a heading cannot sit alone at the bottom of a page.
19
+ - `--clear_paragraph_format` strips everything from each run's `rPr` except `rStyle`, `rtl` and `lang`, and drops the paragraph-mark run properties, so the style controls the look.
20
+ - `--enforce_heading_spacing` sets space-before and space-after per heading level plus `keepNext`.
26
21
 
27
- Run this before applying a template; a template cannot win against direct formatting.
22
+ Run it before applying a template: a template cannot win against direct formatting (`tasks/templates_style_packs.md`).
28
23
 
29
- ## What "correct" looks like
24
+ ## Correct look
30
25
 
31
- - Body text 11–12pt from `Normal`; headings from `Heading 1`–`Heading 3` with no direct bold or size.
32
- - Bold used for emphasis inside a sentence, via a character style or a single `w:b` run — not for whole paragraphs that are really headings.
33
- - Lists from `List Bullet` / `List Number`, never typed bullet characters.
26
+ - Body text 11–12pt from `Normal`.
27
+ - Headings from `Heading 1`–`Heading 3`, with no direct bold or size.
28
+ - Bold for in-sentence emphasis via a character style or one `w:b` run, never for whole paragraphs that are really headings.
29
+ - Lists from `List Bullet` / `List Number`, never typed bullets.
34
30
  - One table style per document, usually `Table Grid`.
35
31
 
36
32
  ## Verify
37
33
 
38
- Re-run `style_lint.py` (counts should drop), then render: clearing direct formatting can change the visual hierarchy when a document faked emphasis with size. Compare the rendered page with the original before you claim the clean-up is neutral.
34
+ Re-run `style_lint.py` and watch the counts drop, then render. Clearing direct formatting can change the hierarchy when a document faked emphasis with size; compare against the original before claiming the change was neutral.
39
35
 
40
36
  ## Pitfalls
41
37
 
42
- - Clearing `rPr` removes field-result formatting too; re-run the field-related scripts afterwards if the number must be bold.
43
- - A style referenced but not defined falls back to `Normal` silently — the lint is the only warning you get.
44
- - `w:rsid` and proofing attributes are noise, not style; `privacy_scrub.py` removes them when the file leaves the organisation.
38
+ - Clearing `rPr` also removes field-result formatting; re-run the field scripts if a number must stay bold.
39
+ - A style that is referenced but not defined falls back to `Normal` silently — the lint is the only warning.
40
+ - `w:rsid` and proofing attributes are noise, removed by `privacy_scrub.py` when the file leaves (`tasks/privacy_scrub_metadata.md`).
@@ -1,64 +1,54 @@
1
- # Tables and spreadsheets
1
+ # Tables and layout geometry
2
2
 
3
- **Goal.** A table that fits the page, repeats its header row across pages, and survives an edit without cells drifting out of alignment.
4
-
5
- A table's geometry is three things that must agree: the grid (`w:tblGrid`, one `w:gridCol` per column), the table width (`w:tblW`), and each cell width (`w:tcW`). When they disagree Word shows a table wider than the text block or columns that jump on the first edit.
3
+ Table geometry is three things that must agree: the grid (`w:tblGrid`, one `w:gridCol` per column), the table width (`w:tblW`) and each cell width (`w:tcW`). When they disagree, Word shows a table wider than the text block, or columns that jump on the first edit.
6
4
 
7
5
  ## Create
8
6
 
9
- ```bash
10
- "<python>" "<cli>" docx create --spec "<spec.json>" --out "<out.docx>"
11
- ```
7
+ Use `docx create --spec` with a table block:
12
8
 
13
9
  ```json
14
- {
15
- "type": "table",
16
- "headers": ["Region", "Revenue", "Growth"],
17
- "rows": [["EMEA", "4.1M", "12%"], ["APAC", "3.2M", "9%"]],
18
- "style": "Table Grid"
19
- }
10
+ { "type": "table", "headers": ["Region", "Q1"], "rows": [["North", "10"]], "style": "Table Grid" }
20
11
  ```
21
12
 
22
- Header cells are bold. For a document with the header repeating on every page, follow up with the a11y fix, which writes `w:tblHeader` on row 1:
13
+ Header cells are bold. To repeat the header row on every page:
23
14
 
24
15
  ```bash
25
- "<python>" "<skill>/scripts/a11y_audit.py" "<out.docx>" --fix_table_headers first_row --out "<out-fixed.docx>"
16
+ "<python>" "<skill>/scripts/a11y_audit.py" "<out.docx>" --fix_table_headers first_row --out "<out-fixed.docx"
26
17
  ```
27
18
 
19
+ That writes `w:tblHeader` on row 1.
20
+
28
21
  ## Inspect and fix geometry
29
22
 
30
23
  ```bash
31
24
  "<python>" "<skill>/scripts/table_geometry.py" "<file.docx>"
32
- "<python>" "<skill>/scripts/table_geometry.py" "<file.docx>" --fix --out "<file-fixed.docx>"
25
+ "<python>" "<skill>/scripts/table_geometry.py" "<file.docx>" --fix --out "<file-fixed.docx"
33
26
  ```
34
27
 
35
- The audit reports `tblW`, layout mode, every grid column in twips and the sum, plus the narrowest and widest cell. `--fix` writes a fixed-layout table at the text width (page width minus margins), rescales the grid proportionally and rewrites each `tcW` including `gridSpan`.
36
-
37
- Text width on Letter with 1in margins is 9360 twips — that is the number a full-width table should total.
28
+ The audit reports `tblW`, the layout mode, every grid column in twips with their sum, and the narrowest and widest cell. `--fix` writes a fixed-layout table at the text width (page width minus margins), rescales the grid proportionally and rewrites each `tcW`, including `gridSpan`. On Letter with 1in margins the text width is 9360 twips.
38
29
 
39
- ## Convert
30
+ ## Move data between formats
40
31
 
41
32
  ```bash
42
- "<python>" "<skill>/scripts/docx_table_to_csv.py" "<file.docx>" --table_index 0 --out "<table.csv>"
33
+ "<python>" "<skill>/scripts/docx_table_to_csv.py" "<file.docx>" --table_index 0 --out "<table.csv"
43
34
  "<python>" "<skill>/scripts/xlsx_to_docx_table.py" "<book.xlsx>" --sheet Revenue --out "<table.docx>" --title "Revenue by region"
44
35
  ```
45
36
 
46
- The first flattens a Word table (expanding horizontal spans, repeating vertically merged values) for data work. The second imports a worksheet as a Word table with a bold header row and `w:tblHeader`, which is the route for "put this spreadsheet in the report" — not a screenshot and not an embedded object.
37
+ `docx_table_to_csv.py` flattens the table, expands horizontal spans and repeats vertically merged values. `xlsx_to_docx_table.py` writes a bold header row plus `w:tblHeader`; it is the route for "put this spreadsheet in the report", not a screenshot or an embedded object.
47
38
 
48
39
  ## Design
49
40
 
50
- - Bold header row, light grey `#D9D9D9` borders, no shading inside the body.
51
- - Numeric columns right-aligned; text left-aligned. Thousands separators in the cells themselves.
52
- - Two or three columns of prose at most; a wide table wants landscape, and that means a section break (`tasks/sections_layout.md`), not a smaller font.
53
- - Caption above the table (`tasks/captions_crossrefs.md`), and keep it with the table.
54
- - Avoid vertical merges: they look fine in the file and confuse every downstream reader.
41
+ - Bold header row, light grey `#D9D9D9` borders, no shading in the body.
42
+ - Numeric right-aligned, text left-aligned, thousands separators inside the cells.
43
+ - Two or three prose columns at most. A wide table wants landscape through a section break (`tasks/sections_layout.md`), not a smaller font.
44
+ - Caption above the table (`tasks/captions_crossrefs.md`). Avoid vertical merges.
55
45
 
56
46
  ## Verify
57
47
 
58
- Render and look at the page the table lands on: nothing spills past the right margin, header row intact, no split row across pages, and the last column not truncated. A table that fits in the XML but overflows the page is a real defect, and only the render shows it.
48
+ Render the page: nothing past the right margin, header row intact, no row split across pages, last column not truncated. A table that fits in XML but overflows the page is a real defect that only the render shows.
59
49
 
60
50
  ## Pitfalls
61
51
 
62
- - A table inside a table cell inherits the cell width; check the outer table first.
63
- - `autofit` is a request, not a guarantee — fixed layout plus explicit widths is the only thing that renders the same everywhere.
64
- - Cell margins default to 108 twips left/right; that is padding, not missing width.
52
+ - A table inside a cell inherits the cell width, so check the outer table first.
53
+ - `autofit` is a request, not a guarantee.
54
+ - Cell margins default to 108 twips of padding.
@@ -1,53 +1,49 @@
1
1
  # Templates and style packs
2
2
 
3
- **Goal.** Make a new or existing document look like the house template, without copying the template's content into it.
4
-
5
- A `.dotx` is a `.docx` whose package is a template: `styles.xml` defines the look, `numbering.xml` defines lists, and the body is empty. Copying the body would import placeholder text; what you actually need is the styles part, the numbering definitions they reference, and the theme.
3
+ A `.dotx` is a `.docx` whose package is a template: `styles.xml` defines the look, `numbering.xml` defines lists, the body is empty. What you need is the styles part, the numbering definitions they reference, and the theme. Copying the body imports placeholder text.
6
4
 
7
5
  ## Ask before guessing
8
6
 
9
- Do not invent a house style. Get from the user: heading fonts and sizes, body font and size, accent colour, margins, table style, and whether a logo or cover page is required. `references/template-elicitation.md` has the question set that produces a spec.
7
+ Never invent a house style. Get heading fonts and sizes, body font and size, accent colour, margins, table style, and any logo or cover requirement. `references/template-elicitation.md` holds the question set.
10
8
 
11
- ## Apply a template
9
+ ## Apply
12
10
 
13
11
  ```bash
14
- "<python>" "<skill>/scripts/apply_template_styles.py" --template "<house.dotx>" --target "<draft.docx>" --out "<draft-styled.docx>"
12
+ "<python>" "<skill>/scripts/apply_template_styles.py" --template "<house.dotx>" --target "<draft.docx>" --out "<draft-styled.docx"
15
13
  ```
16
14
 
17
- The script merges the template's style definitions by `styleId` (replacing same-id definitions, appending missing ones, taking the template's `docDefaults`), and copies `numbering.xml` with its relationship and content-type override. The target's body is untouched, so the content survives and the look changes.
15
+ Merges style definitions by `styleId` (replacing same-id, appending missing, taking the template's `docDefaults`) and copies `numbering.xml` with its relationship and content-type override. The target's body is untouched.
18
16
 
19
- Then normalize so direct formatting does not override the template:
17
+ Then normalise so direct formatting does not override the imported styles:
20
18
 
21
19
  ```bash
22
- "<python>" "<skill>/scripts/style_normalize.py" "<draft-styled.docx>" --out "<draft-final.docx>" --clear_paragraph_format
20
+ "<python>" "<skill>/scripts/style_normalize.py" "<draft-styled.docx>" --out "<draft-final.docx" --clear_paragraph_format
23
21
  ```
24
22
 
25
23
  ## Restyle without a template
26
24
 
27
- When the user describes the style instead of supplying a file, set it on the styles rather than on runs:
25
+ Set values on styles, not on runs:
28
26
 
29
- ```bash
30
- "<python>" - <<'PY'
27
+ ```python
31
28
  from docx import Document
32
29
  from docx.shared import Pt, RGBColor
30
+
33
31
  d = Document(r"<draft.docx>")
34
- normal = d.styles["Normal"]
35
- normal.font.name = "Calibri"
36
- normal.font.size = Pt(11)
32
+ d.styles["Normal"].font.name = "Calibri"
33
+ d.styles["Normal"].font.size = Pt(11)
37
34
  for level, size in ((1, 18), (2, 14), (3, 12)):
38
35
  style = d.styles[f"Heading {level}"]
39
36
  style.font.size = Pt(size)
40
37
  style.font.color.rgb = RGBColor(0, 0, 0)
41
- d.save(r"<draft-styled.docx>")
42
- PY
38
+ d.save(r"<draft-restyled.docx>")
43
39
  ```
44
40
 
45
41
  ## Verify
46
42
 
47
- Render and compare against the template's own preview page by page: heading sizes, body leading, margins, table borders. A style page that looks right in a style list but wrong on the page usually means a direct-formatting run left over — run the lint.
43
+ Render and compare page by page with the template's own preview: heading sizes, body leading, margins, table borders. A style that looks right in the pane but wrong on the page usually has a leftover direct-formatting run — run the lint (`tasks/style_lint_normalize.md`).
48
44
 
49
45
  ## Pitfalls
50
46
 
51
- - A template applied over content that references missing styles produces the fallback look, not an error; run `style_lint.py` to find undefined references.
52
- - Themes (`word/theme/theme1.xml`) carry fonts and accent colours; a style that names a theme font changes when the theme does.
53
- - Keeping the template's `numbering.xml` without its `abstractNum` ids produces lists that all share one counter.
47
+ - A template applied over content that references missing styles produces the fallback look, not an error; run `style_lint.py`.
48
+ - Themes (`word/theme/theme1.xml`) carry fonts and accent colours, so theme-font styles change with the theme.
49
+ - Keeping the template's `numbering.xml` without its abstractNum ids makes lists share one counter.
@@ -1,26 +1,22 @@
1
1
  # Table of contents
2
2
 
3
- **Goal.** Put a navigable contents list at the front of the document, built from heading styles rather than typed by hand.
3
+ A TOC is a `TOC` field: Word fills it on recalculation and the file stores the last result. A hand-typed list looks identical, then goes wrong on the first edit.
4
4
 
5
- The TOC is a `TOC` field. Word fills it in when it recalculates; the file stores the last result. A hand-typed list looks identical in a screenshot and then goes wrong on the first edit.
6
-
7
- ## Insert
5
+ Insert:
8
6
 
9
7
  ```bash
10
8
  "<python>" "<skill>/scripts/insert_toc.py" "<file.docx>" --out "<file-toc.docx>" --levels 1-3
11
9
  ```
12
10
 
13
- The script looks for a placeholder paragraph containing `[[TOC]]` (override with `--placeholder`). If there is none it inserts the field at the start of the body. It writes `TOC \o "1-3" \h \z \u`, sets `w:updateFields`, and adds a short cached hint so the page is not blank before recalculation.
11
+ The script fills a placeholder paragraph containing `[[TOC]]` (override with `--placeholder`); with no placeholder it inserts the field at the start of the body. It writes `TOC \o "1-3" \h \z \u`, sets `w:updateFields`, and leaves a short cached hint so the page is not blank before recalculation.
14
12
 
15
- Check that every heading you want in the list uses a heading style: `heading_audit.py` finds headings formatted by hand, which the TOC will silently skip.
13
+ Every heading you want listed must use a heading style. `heading_audit.py` finds hand-formatted headings that the TOC silently skips.
16
14
 
17
15
  ## Rebuild after edits
18
16
 
19
- If headings changed, the cached list is stale. Either ask the user to update the field in Word (right-click → Update Field), or regenerate: strip the old field with `fields_materialize.py`? — no, that freezes it. Instead delete the field paragraphs and run `insert_toc.py` again. Freezing a TOC is almost never what a reader wants.
20
-
21
- ## Static contents list
17
+ The cached list goes stale after a structural edit. Ask the user to update the field in Word (right-click → Update Field), or delete the field paragraphs and re-run `insert_toc.py`. Do not freeze a TOC with `fields_materialize.py`.
22
18
 
23
- Sometimes the requirement really is a fixed list — a PDF export that will not be recalculated, or a template with a hand-built contents page. `internal_nav.py` builds one: a heading plus hyperlink entries to every heading, with optional back-links.
19
+ For a fixed PDF export or a hand-built template contents page, generate a static list with `internal_nav.py` instead:
24
20
 
25
21
  ```bash
26
22
  "<python>" "<skill>/scripts/internal_nav.py" "<file.docx>" --out "<file-nav.docx>" --levels 1 2
@@ -30,10 +26,10 @@ See `tasks/navigation_internal_links.md`.
30
26
 
31
27
  ## Verify
32
28
 
33
- Render the first pages and read them. A TOC that shows one line of grey hint text means the field never recalculated in that renderer; a TOC that shows the old headings means the document was rendered before the field updated. Say which one happened instead of claiming the TOC is correct.
29
+ Render the first pages. A line of grey hint text means the renderer never recalculated the field; old headings mean it rendered before the update. Say which happened instead of claiming the contents are correct.
34
30
 
35
31
  ## Pitfalls
36
32
 
37
- - TOC entries come from outline levels, not from text that merely looks like a heading.
38
- - A TOC inside a header or a text box does not work; it belongs in the body.
39
- - Level 4+ headings are excluded unless you widen `--levels`.
33
+ - TOC entries come from outline levels, not from text that looks like a heading.
34
+ - A TOC in a header or text box does not work — body only.
35
+ - Level 4 and deeper are excluded unless you widen `--levels`.
@@ -1,50 +1,38 @@
1
1
  # Visual verification
2
2
 
3
- **Goal.** See the pages before the user does. A document that parses and a document that looks right are different claims.
4
-
5
- ## Render
3
+ See the pages before the user does. A document that parses and a document that looks right are different claims.
6
4
 
7
5
  ```bash
8
6
  "<python>" "<cli>" docx render --input "<file.docx>" --out-dir "<render-dir>"
9
7
  ```
10
8
 
11
- Writes one PNG per page and prints their paths plus the renderer it used (`libreoffice-kit`); add `--pdf` to also keep a PDF. Use the same command through `render_docx.py` when you want the JSON report and page list:
9
+ Writes one PNG per page and prints the paths plus the renderer it used (`libreoffice-kit`); add `--pdf` to keep a PDF as well. For a JSON report and page list use `render_docx.py`:
12
10
 
13
11
  ```bash
14
12
  "<python>" "<skill>/render_docx.py" "<file.docx>" --output_dir "<render-dir>" --dpi 150
15
13
  ```
16
14
 
17
- ## Inspect
18
-
19
15
  Open every PNG with `read_image` and check, in this order:
20
16
 
21
17
  1. **Hierarchy** — does the page show the structure the user asked for, at a glance?
22
18
  2. **Fit** — nothing past the right margin, no clipped table column, no text over a watermark.
23
19
  3. **Breaks** — no heading alone at the bottom, no orphaned caption, no half-empty page before a section break.
24
- 4. **Text** — the words that were meant to change did change, and nothing else moved.
20
+ 4. **Text** — the intended words changed, and nothing else moved.
25
21
  5. **Glyphs** — dashes, quotes, CJK, currency symbols and maths render, not as boxes.
26
- 6. **Header and footer** — present, correct, not repeated on a first page that should not have it.
22
+ 6. **Header and footer** — present, correct, and not repeated on a first page that should not have one.
27
23
 
28
- ## When no renderer exists
24
+ Re-render after any script edit. Package-valid is not page-valid: a caption placed before the paragraph it describes, a table grown past the margin, a style merge that changed heading sizes — all are invisible in the XML and obvious in the image. To compare two revisions, `render_and_diff.py` writes a heatmapped difference page; read the diff images yourself, because a ratio is not a conclusion.
29
25
 
30
- The runtime reports `renderers.can_render.docx: false` because the DSH LibreOffice kit was not found. `render` then exits non-zero with a JSON error naming the missing renderer, and `render_docx.py` exits 3 with `reason: no_renderer`.
26
+ ## When no renderer exists
31
27
 
32
- In that state:
28
+ `office_status` reports `renderers.available: false` (with a `remedy`) because the DSH LibreOffice kit was not found. `render` then exits non-zero with a JSON error naming the missing renderer, and `render_docx.py` exits 3 with `reason: no_renderer`.
33
29
 
34
30
  - Verify structure with `docx read`, `docx_kit.py parts|text|xml` and the audit scripts.
35
- - Tell the user plainly that layout was not visually verified, and that the fix is a harness install that ships the kit (the plugin records its path in `config.json` when it loads), not installing Word/LibreOffice.
36
- - Do not claim a file "looks good" from XML evidence alone.
37
-
38
- ## After any script edit
39
-
40
- Re-render. Package-valid is not page-valid: a caption inserted before the paragraph it describes, a table grown past the margin, a style merge that changed heading sizes — all are invisible to the XML and obvious in the image.
41
-
42
- ## Comparing revisions
43
-
44
- `render_and_diff.py` renders two files and writes a heatmapped difference page. Read the diff images yourself; a ratio is not a conclusion.
31
+ - Say plainly that layout was not visually verified, and that the fix is a harness install shipping the kit (the plugin records its path in `config.json` when it loads), not installing Word or LibreOffice.
32
+ - Never claim a file "looks good" from XML evidence alone.
45
33
 
46
34
  ## Pitfalls
47
35
 
48
36
  - Different renderers paginate differently. Compare like with like, and do not promise Word-perfect pagination from a LibreOffice render.
49
- - A render fails silently into a blank page when a font is missing; a blank page is a finding, not a rendering bug. `missing_fonts` / `glyph_fallback` in the render JSON tells you substitution happened.
50
- - Render into a fresh directory; stale PNGs from an earlier run are the classic false pass.
37
+ - A missing font turns into a blank page: that is a finding, not a rendering bug. `missing_fonts` / `glyph_fallback` in the render JSON shows substitution happened.
38
+ - Render into a fresh directory: stale PNGs from an earlier run are the classic false pass.
@@ -1,35 +1,31 @@
1
- # Watermarks and background text
1
+ # Watermarks and background marks
2
2
 
3
- **Goal.** A diagonal DRAFT / CONFIDENTIAL mark behind the page content, and the ability to find and remove one that a template left behind.
4
-
5
- A Word watermark is not a document feature with its own element: it is a VML shape (`v:shape` with `v:textpath`) in a header part, positioned behind the text. That is why it disappears if the header is replaced, and why it can appear on some pages and not others when sections have their own headers.
3
+ A Word watermark is not its own element: it is a VML shape (`v:shape` with `v:textpath`) inside a header part, behind the text. That is why it disappears when the header is replaced and can differ per section.
6
4
 
7
5
  ## Add
8
6
 
9
7
  ```bash
10
- "<python>" "<skill>/scripts/watermark_add.py" "<file.docx>" --out "<file-draft.docx>" --text "DRAFT"
8
+ "<python>" "<skill>/scripts/watermark_add.py" "<file.docx>" --out "<file-draft.docx" --text "DRAFT"
11
9
  ```
12
10
 
13
- Writes the shape into the default header, light grey, rotated -45°, behind the body. The script refuses to add a second watermark when it finds one — remove the old one first, or you get two overlapping marks.
14
-
15
- For a per-page mark across a document with several sections, each section's header needs the shape; that is a loop over sections, not one insertion.
11
+ Writes the shape into the default header: light grey, rotated -45°, behind the body. The script refuses to add a second watermark when it finds one, so remove the old one first. For a mark on every page across sections, each section's header needs the shape — loop over the sections.
16
12
 
17
13
  ## Find and remove
18
14
 
19
15
  ```bash
20
16
  "<python>" "<skill>/scripts/watermark_audit_remove.py" "<file.docx>" --mode report
21
- "<python>" "<skill>/scripts/watermark_audit_remove.py" "<file.docx>" --mode remove --out "<file-clean.docx>"
17
+ "<python>" "<skill>/scripts/watermark_audit_remove.py" "<file.docx>" --mode remove --out "<file-clean.docx"
22
18
  ```
23
19
 
24
- The report lists the text of every VML text path and the part it lives in, and exits 1 when any watermark exists. `--contains` narrows to a substring, which is the case "the template says Company Confidential and it should not". Remove deletes the enclosing paragraph from the header.
20
+ The report lists every VML text path and the part it lives in, and exits 1 when any watermark exists. `--contains` narrows it to a substring, for the "template says Company Confidential" case. Remove deletes the enclosing paragraph from the header.
25
21
 
26
22
  ## Verify
27
23
 
28
- Render and read the pages: the mark sits behind the text (not over it), stays inside the page, and appears on every page the user expects. A watermark that vanishes on page two means that section has a different header.
24
+ Render and read the pages: the mark sits behind the text, inside the page, on every expected page. A watermark missing on page two means that section has a different header.
29
25
 
30
26
  ## Pitfalls
31
27
 
32
- - A watermark in a "different first page" header only shows on page one.
33
- - Removing the header paragraph can leave an empty header that adds vertical space; check the top margin on the rendered page.
34
- - An image watermark is an `v:imagedata` shape, not a text path — the report will not name it, so inspect the header XML when the visual mark is a logo.
35
- - Watermarks are not security. See `tasks/protection_restrict_editing.md` for what actually restricts editing.
28
+ - A watermark in a "different first page" header shows only on page one.
29
+ - Removing the header paragraph can leave an empty header that adds vertical space — check the top margin.
30
+ - An image watermark is `v:imagedata`, not a text path, so the report will not name it; inspect the header XML.
31
+ - A watermark is not security (`tasks/protection_restrict_editing.md`).