dsh-office-kit 0.4.2 → 0.4.4

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 (123) 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/scripts/add_tracked_replacements.py +4 -3
  21. package/assets/skills/office-documents/scripts/docx_kit.py +68 -27
  22. package/assets/skills/office-documents/scripts/heading_audit.py +3 -4
  23. package/assets/skills/office-documents/scripts/internal_nav.py +3 -1
  24. package/assets/skills/office-documents/scripts/merge_docx_append.py +104 -0
  25. package/assets/skills/office-documents/scripts/table_geometry.py +13 -3
  26. package/assets/skills/office-documents/scripts/watermark_add.py +6 -5
  27. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +6 -4
  28. package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
  29. package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
  30. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
  31. package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
  32. package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
  33. package/assets/skills/office-documents/tasks/create_edit.md +14 -16
  34. package/assets/skills/office-documents/tasks/equations.md +17 -28
  35. package/assets/skills/office-documents/tasks/fields_update.md +14 -16
  36. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
  37. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
  38. package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
  39. package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
  40. package/assets/skills/office-documents/tasks/images_figures.md +15 -25
  41. package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
  42. package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
  43. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
  44. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
  45. package/assets/skills/office-documents/tasks/read_review.md +9 -20
  46. package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
  47. package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
  48. package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
  49. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
  50. package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
  51. package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
  52. package/assets/skills/office-documents/tasks/verify_render.md +11 -23
  53. package/assets/skills/office-documents/tasks/watermarks_background.md +10 -14
  54. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +6 -6
  55. package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
  56. package/assets/skills/office-pdf/SKILL.md +36 -51
  57. package/assets/skills/office-pdf/references/form-fields.md +20 -20
  58. package/assets/skills/office-pdf/references/geometry.md +10 -10
  59. package/assets/skills/office-pdf/references/layout-quality.md +13 -13
  60. package/assets/skills/office-pdf/references/limits.md +12 -56
  61. package/assets/skills/office-pdf/tasks/annotations.md +5 -5
  62. package/assets/skills/office-pdf/tasks/create.md +6 -9
  63. package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
  64. package/assets/skills/office-pdf/tasks/forms.md +15 -15
  65. package/assets/skills/office-pdf/tasks/read.md +9 -9
  66. package/assets/skills/office-pdf/tasks/redact.md +18 -18
  67. package/assets/skills/office-pdf/tasks/stamp.md +10 -10
  68. package/assets/skills/office-presentations/SKILL.md +71 -91
  69. package/assets/skills/office-presentations/container_tools/preview_render.py +4 -1
  70. package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
  71. package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
  72. package/assets/skills/office-presentations/recipes/README.md +24 -35
  73. package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
  74. package/assets/skills/office-presentations/recipes/connectors.md +11 -12
  75. package/assets/skills/office-presentations/recipes/images.md +11 -12
  76. package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
  77. package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
  78. package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
  79. package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
  80. package/assets/skills/office-presentations/recipes/tables.md +14 -14
  81. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
  82. package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
  83. package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
  84. package/assets/skills/office-presentations/references/finalization.md +10 -12
  85. package/assets/skills/office-presentations/references/native-bullets.md +14 -18
  86. package/assets/skills/office-presentations/references/native-evidence.md +18 -32
  87. package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
  88. package/assets/skills/office-presentations/references/templates.md +31 -60
  89. package/assets/skills/office-presentations/routing/google_slides.md +4 -4
  90. package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
  91. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
  92. package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
  93. package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
  94. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
  95. package/assets/skills/office-presentations/tasks/template-following.md +23 -28
  96. package/assets/skills/office-spreadsheets/SKILL.md +32 -39
  97. package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
  98. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
  99. package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
  100. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
  101. package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
  102. package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
  103. package/assets/skills/office-spreadsheets/references/charts.md +15 -34
  104. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
  105. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
  106. package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
  107. package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
  108. package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
  109. package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
  110. package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
  111. package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
  112. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
  113. package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
  114. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
  115. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
  116. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
  117. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
  118. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
  119. package/assets/skills/shared/annotation-recipes.md +98 -120
  120. package/assets/skills/shared/common-workflow.md +70 -80
  121. package/dist/index.js +1 -1
  122. package/package.json +1 -1
  123. package/python/office_cli.py +38 -4
@@ -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.
@@ -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`).