dsh-office-kit 0.2.0 → 0.3.1

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 (197) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/README.md +3 -1
  3. package/assets/skills/excel-live-control/SKILL.md +234 -0
  4. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  5. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  6. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  7. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  8. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  9. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  10. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  11. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  12. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  13. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  14. package/assets/skills/office-documents/SKILL.md +90 -77
  15. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  16. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  17. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  18. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  19. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  20. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  21. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  22. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  23. package/assets/skills/office-documents/render_docx.py +99 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +143 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +199 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +131 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +66 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +87 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +133 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +161 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +98 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +99 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +118 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -0
  64. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  65. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  66. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  67. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  68. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  69. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  70. package/assets/skills/office-documents/tasks/equations.md +59 -0
  71. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  72. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  73. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  74. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  75. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  76. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  77. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  78. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  79. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  80. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  81. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  82. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  83. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  84. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  85. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  86. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  87. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  88. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  89. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  90. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  91. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  92. package/assets/skills/office-pdf/SKILL.md +73 -56
  93. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  94. package/assets/skills/office-pdf/references/geometry.md +44 -0
  95. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  96. package/assets/skills/office-pdf/references/limits.md +58 -0
  97. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  98. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  99. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  100. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  101. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  102. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  103. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  104. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  105. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  106. package/assets/skills/office-pdf/tasks/create.md +75 -0
  107. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  108. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  109. package/assets/skills/office-pdf/tasks/read.md +62 -0
  110. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  111. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  112. package/assets/skills/office-presentations/SKILL.md +141 -87
  113. package/assets/skills/office-presentations/container_tools/add_slides.py +143 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -0
  126. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  127. package/assets/skills/office-presentations/recipes/README.md +71 -0
  128. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  129. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  130. package/assets/skills/office-presentations/recipes/images.md +80 -0
  131. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  132. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  133. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  134. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  135. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  136. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  137. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  138. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  139. package/assets/skills/office-presentations/references/finalization.md +63 -0
  140. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  141. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  142. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  143. package/assets/skills/office-presentations/references/templates.md +76 -0
  144. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  145. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  146. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  147. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  148. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  149. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  150. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  151. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +183 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -0
  166. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  167. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  168. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  169. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  170. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  171. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  172. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  173. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  174. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  175. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  176. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  177. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  178. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  179. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  180. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  181. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  182. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  183. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  184. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  185. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  186. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  187. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  188. package/assets/skills/shared/annotation-recipes.md +252 -0
  189. package/assets/skills/shared/check_runtime.py +260 -0
  190. package/assets/skills/shared/common-workflow.md +139 -0
  191. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  192. package/dist/client.js +4 -4
  193. package/dist/client.js.map +1 -1
  194. package/dist/index.js +246 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +213 -254
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python
2
+ """Copy a worksheet range into a Word table.
3
+
4
+ The header row is bolded and marked as a repeating header so it appears again
5
+ when the table breaks across pages, which is the detail hand-built tables usually
6
+ miss. This script uses python-docx directly because it creates a new document
7
+ rather than editing an existing one.
8
+
9
+ Exit codes: 0 success, 1 sheet or data not found, 2 bad input.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from lxml import etree
19
+
20
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
21
+ import docx_kit as kit
22
+
23
+ from docx import Document
24
+ from docx.enum.table import WD_TABLE_ALIGNMENT
25
+ from docx.shared import Pt
26
+
27
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
28
+ for _stream in (sys.stdout, sys.stderr):
29
+ try:
30
+ _stream.reconfigure(encoding="utf-8", errors="replace")
31
+ except (AttributeError, ValueError):
32
+ pass
33
+
34
+
35
+ def cell_text(value) -> str:
36
+ if value is None:
37
+ return ""
38
+ if isinstance(value, float) and value.is_integer():
39
+ return str(int(value))
40
+ return str(value).strip()
41
+
42
+
43
+ def main(argv: list[str] | None = None) -> int:
44
+ parser = argparse.ArgumentParser(description="Build a .docx whose body is a table filled from an .xlsx sheet.")
45
+ parser.add_argument("xlsx", help="Input .xlsx")
46
+ parser.add_argument("output_docx", nargs="?", default=None, help="Output .docx")
47
+ parser.add_argument("--out", default=None, help="Output .docx (alternative to the positional argument)")
48
+ parser.add_argument("--sheet", default=None, help="Sheet name (default: the active sheet)")
49
+ parser.add_argument("--header_rows", type=int, default=1, help="Rows to treat as headers (default 1)")
50
+ parser.add_argument("--title", default=None, help="Heading added above the table")
51
+ args = parser.parse_args(argv)
52
+
53
+ out = args.out or args.output_docx
54
+ if not out:
55
+ parser.error("provide an output path (positional or --out)")
56
+
57
+ try:
58
+ from openpyxl import load_workbook
59
+ except ImportError:
60
+ print("error: openpyxl is required to read .xlsx", file=sys.stderr)
61
+ return 2
62
+
63
+ try:
64
+ workbook = load_workbook(args.xlsx, data_only=True, read_only=True)
65
+ except OSError as exc:
66
+ print("error: %s" % exc, file=sys.stderr)
67
+ return 2
68
+
69
+ if args.sheet:
70
+ if args.sheet not in workbook.sheetnames:
71
+ print("error: sheet %r not found (have %s)" % (args.sheet, ", ".join(workbook.sheetnames)), file=sys.stderr)
72
+ return 1
73
+ sheet = workbook[args.sheet]
74
+ else:
75
+ sheet = workbook[workbook.sheetnames[0]]
76
+
77
+ rows = [[cell_text(value) for value in row] for row in sheet.iter_rows(values_only=True)]
78
+ rows = [row for row in rows if any(cell for cell in row)]
79
+ if not rows:
80
+ print("error: sheet %r has no data" % sheet.title, file=sys.stderr)
81
+ return 1
82
+
83
+ width = max(len(row) for row in rows)
84
+ rows = [row + [""] * (width - len(row)) for row in rows]
85
+ header_count = max(0, min(args.header_rows, len(rows)))
86
+
87
+ document = Document()
88
+ if args.title:
89
+ document.add_heading(args.title, level=1)
90
+
91
+ table = document.add_table(rows=len(rows), cols=width)
92
+ table.style = "Table Grid"
93
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
94
+ for row_index, row in enumerate(rows):
95
+ for column_index, value in enumerate(row):
96
+ cell = table.cell(row_index, column_index)
97
+ cell.text = value
98
+ if row_index < header_count:
99
+ for paragraph in cell.paragraphs:
100
+ for run in paragraph.runs:
101
+ run.bold = True
102
+ run.font.size = Pt(11)
103
+
104
+ document.save(out)
105
+ payload = {"ok": True, "output": str(Path(out)), "sheet": sheet.title, "rows": len(rows), "columns": width, "header_rows": header_count}
106
+
107
+ if header_count:
108
+ doc = kit.Docx(out)
109
+ first_row = doc.body().find(".//" + kit.q("tr"))
110
+ if first_row is not None:
111
+ props = first_row.find(kit.q("trPr"))
112
+ if props is None:
113
+ props = etree.Element(kit.q("trPr"))
114
+ first_row.insert(0, props)
115
+ etree.SubElement(props, kit.q("tblHeader"))
116
+ doc.save(out)
117
+ payload["header_row_repeats"] = True
118
+
119
+ kit.emit(payload)
120
+ return 0
121
+
122
+
123
+ if __name__ == "__main__":
124
+ raise SystemExit(main())
@@ -0,0 +1,45 @@
1
+ # Accessibility
2
+
3
+ **Goal.** A document a screen reader can navigate: real headings, described images, tables with a header row, no fake emphasis carrying meaning.
4
+
5
+ The accessibility tree in Word comes from structure, not appearance. Bold text announces as bold; a `Heading 1` announces as a heading and appears in the navigation pane. Alt text lives in `wp:docPr @descr`.
6
+
7
+ ## Audit
8
+
9
+ ```bash
10
+ "<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --out_json "<report.json>"
11
+ ```
12
+
13
+ Checks: images without a description or title, tables without a repeating header row, heading levels that jump, empty headings, links whose text is only "click here" / "here" / "link", and a missing core title.
14
+
15
+ Exit code 1 means at least one finding; `--out_json` writes the same report for a larger pass.
16
+
17
+ ## Fix the mechanical parts
18
+
19
+ ```bash
20
+ "<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" \
21
+ --fix_image_alt from_filename --fix_table_headers first_row \
22
+ --out "<file-a11y.docx>"
23
+ ```
24
+
25
+ Both fixes are honest placeholders: the file name as alt text is better than an empty description, and a header row that repeats across pages helps a reader who lands mid-table. Neither substitutes for real text.
26
+
27
+ ## Fix the parts that need judgement
28
+
29
+ - **Alt text.** Describe what the reader would lose without the picture: "Bar chart: revenue by region, EMEA 4.1M ahead of APAC 3.2M." Decorative rules and dividers get an empty description.
30
+ - **Link text.** "Download the Q2 ledger" instead of "click here".
31
+ - **Reading order.** Decorative text boxes and floating images insert themselves at awkward places in the reading order; convert to inline if the flow matters.
32
+ - **Colour.** Meaning carried only by colour (red = bad) fails for a colour-blind reader; add a word.
33
+ - **Tables for layout.** A layout table announces as a data table. Restructure into paragraphs if the content is not tabular.
34
+
35
+ Long documents also want a TOC (`tasks/toc_workflow.md`), which is the screen-reader equivalent of the navigation pane, and language tags so a screen reader pronounces correctly.
36
+
37
+ ## Verify
38
+
39
+ Re-run the audit to zero findings, then render and read the page: heading hierarchy visible, alt text is not visible (it is metadata), table header row distinct. If a screen reader is available, open the document and navigate by heading — that is the true test.
40
+
41
+ ## Pitfalls
42
+
43
+ - Alt text on a decorative image is worse than none; an empty description plus `title` marks it decorative.
44
+ - A table header row is `w:tblHeader` on the row, not a bold first row.
45
+ - Images inside a table cell still need descriptions.
@@ -0,0 +1,49 @@
1
+ # Captions and cross-references
2
+
3
+ **Goal.** Number figures and tables automatically and refer to them from the prose without typing a number that will drift.
4
+
5
+ Two parts: a caption paragraph with a `SEQ` field (`SEQ Table \* ARABIC`), and a bookmark around it that the prose reaches with a `REF` field. Without the bookmark a `REF` has nothing to point at.
6
+
7
+ ## Add captions
8
+
9
+ ```bash
10
+ "<python>" "<skill>/scripts/captions_and_crossrefs.py" "<file.docx>" "<file-captioned.docx>" \
11
+ --tables --figures --bookmarks --caption_text "Source: finance ledger"
12
+ ```
13
+
14
+ - `--tables` adds `Table N` under each table, `--figures` adds `Figure N` under each picture. With neither flag, both run.
15
+ - Captions use the `Caption` style and sit inside the same visual block as the object, so a page break cannot orphan them.
16
+ - `--bookmarks` names the captions `tbl1`, `tbl2`, `fig1`, … so cross-references have targets.
17
+ - `--materialize` writes literal numbers instead of `SEQ` fields; use it for a frozen PDF export, not for a document that will change.
18
+ - `--caption_text` appends a fixed note after the number.
19
+
20
+ The bookmarks the script creates (`tblN`, `figN`) are what `insert_ref_fields.py` expects by default.
21
+
22
+ ## Add cross-references
23
+
24
+ Write placeholders in your content, then convert them:
25
+
26
+ ```bash
27
+ "<python>" "<skill>/scripts/insert_ref_fields.py" "<file.docx>" "<file-refs.docx>"
28
+ ```
29
+
30
+ `[[REF:tbl1]]` becomes a `REF tbl1 \h` field, which renumbers when the table moves. `--pattern` accepts your own regular expression as long as it has a named group `bookmark`; `--prefix` prepends text such as `Table ` to each result.
31
+
32
+ If the reference should read "Table 2" rather than just "2", set the `REF` field's `\n` behaviour off and include the label in the prose — a field cannot know whether its target is a table or a figure.
33
+
34
+ ## Order of operations
35
+
36
+ 1. Add or restructure tables and figures.
37
+ 2. Run `captions_and_crossrefs.py` (creates captions and bookmarks).
38
+ 3. Run `insert_ref_fields.py` (turns placeholders into fields).
39
+ 4. Re-render: caption positions and numbering are a layout decision.
40
+
41
+ ## Verify
42
+
43
+ `fields_report.py`: one `SEQ` per caption, one `REF` per reference, no `REF` left with an empty result. Then read the rendered page — a caption above a table instead of below, or a caption separated from its object by a page break, is the usual defect.
44
+
45
+ ## Pitfalls
46
+
47
+ - Renumbering happens on field update. Two captions claim `Table 2` until Word recalculates.
48
+ - A caption added by hand with the number typed in will not renumber; use the script or a `SEQ` field.
49
+ - Bookmarks must be unique; copying a caption copies its bookmark name and breaks the reference.
@@ -0,0 +1,49 @@
1
+ # Tracked changes
2
+
3
+ **Goal.** Propose edits as tracked changes so a reviewer can accept or reject them, or clean up a file that arrives full of markup.
4
+
5
+ A tracked edit is markup, not text: an insertion is `w:ins` wrapping runs, a deletion is `w:del` wrapping `w:delText`, and formatting changes are `w:rPrChange` / `w:pPrChange`. The setting `w:trackRevisions` in `word/settings.xml` makes Word record new edits the same way.
6
+
7
+ ## Propose an edit
8
+
9
+ ```bash
10
+ "<python>" "<skill>/scripts/add_tracked_replacements.py" "<file.docx>" --out "<file-tracked.docx>" \
11
+ --author "Review" --replace "18%=21%" --replace "flat=steady"
12
+ ```
13
+
14
+ Each `--replace` is `OLD=NEW`; the script splits runs at the match so only the changed characters are marked, keeps the surrounding formatting, and turns on `trackRevisions`. Quote the `--author` the reviewer should see.
15
+
16
+ Limit: one replacement per pattern per paragraph, and a match split across runs is still found because the split is computed on paragraph text. Reword the pattern if the paragraph repeats the phrase.
17
+
18
+ ## Inspect what is proposed
19
+
20
+ ```bash
21
+ "<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode report
22
+ "<python>" "<skill>/scripts/docx_kit.py" xml "<file.docx>" word/document.xml
23
+ ```
24
+
25
+ The report counts insertions, deletions, move pairs and format changes. Use `docx_kit.py xml` when you need to know exactly which words change.
26
+
27
+ ## Accept or reject
28
+
29
+ ```bash
30
+ "<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode accept --out "<file-accepted.docx>"
31
+ "<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode reject --out "<file-rejected.docx>"
32
+ ```
33
+
34
+ Accept keeps inserted text and drops deleted text; reject does the reverse and restores `w:delText` as ordinary text. Format-only changes are discarded either way. `--keep_tracking_on` leaves `trackRevisions` enabled, so the next person's edits are also recorded.
35
+
36
+ ## Before delivery
37
+
38
+ Rendered pages contain no markup, so a document can look finished while still carrying redlines. Always check `--mode report` on a file you are about to send, and accept or reject explicitly.
39
+
40
+ ## Verify
41
+
42
+ After accepting, `docx_kit.py text` should show the new wording and no `w:ins`/`w:del` should remain in `word/document.xml`. Render and read the affected page.
43
+
44
+ ## Pitfalls
45
+
46
+ - `w:del` contains `w:delText`, not `w:t`; a text extraction that only looks at `w:t` silently loses deletions.
47
+ - Tables, headers and footnotes can each carry their own tracked changes — the script covers every text part, but a hand edit in one part can miss another.
48
+ - Accepting a move pair (`w:moveFrom`/`w:moveTo`) as two unrelated edits duplicates content; the script treats them as a pair.
49
+ - Comments are not tracked changes. See `comments_manage.md`.
@@ -0,0 +1,59 @@
1
+ # Comments
2
+
3
+ **Goal.** Add a review comment, read the comments someone else left, reply or resolve them, and remove comments before the document goes out.
4
+
5
+ Comments are not text: each one is a `w:comment` in `word/comments.xml`, anchored in the body by a `w:commentRangeStart` / `w:commentRangeEnd` pair plus a `w:commentReference` run. Editing the anchor without the comments part (or the reverse) leaves a document Word reports as corrupt.
6
+
7
+ ## Add
8
+
9
+ ```bash
10
+ "<python>" "<skill>/scripts/comments_add.py" "<file.docx>" --out "<file-commented.docx>" \
11
+ --author "Review" --add "Renewals=Check the source of this claim" --add "42 new accounts=Who verified this?"
12
+ ```
13
+
14
+ Each `--add` is `NEEDLE=COMMENT`. The needle is matched inside a paragraph; the anchored range covers exactly the matched text, so pick a distinctive phrase. `--ignore_case` relaxes matching, `--require_all` writes nothing and exits 1 if any needle is missing — use it when the comment set must be complete.
15
+
16
+ ## Extract
17
+
18
+ ```bash
19
+ "<python>" "<skill>/scripts/comments_extract.py" "<file.docx>" --out "<comments.json>"
20
+ ```
21
+
22
+ Returns `id`, `author`, `initials`, `date`, `text`, `anchored_text`, `part` and `resolved`. An empty array means the file has no comments — do not assume comments exist because the user mentioned them.
23
+
24
+ ## Patch or resolve
25
+
26
+ ```bash
27
+ "<python>" "<skill>/scripts/comments_apply_patch.py" "<file.docx>" "<patch.json>" --out "<file-patched.docx>"
28
+ ```
29
+
30
+ ```json
31
+ {
32
+ "ops": [
33
+ { "id": 1, "append": "Confirmed against the ledger." },
34
+ { "id": 2, "replace": "Resolved: revenue restated in Q3." },
35
+ { "id": 1, "resolved": true }
36
+ ]
37
+ }
38
+ ```
39
+
40
+ `replace` rewrites the comment's first paragraph, `append` adds a paragraph, `resolved` sets the `w15:done` flag. The command exits 1 and names the ids it could not find.
41
+
42
+ ## Remove
43
+
44
+ ```bash
45
+ "<python>" "<skill>/scripts/comments_strip.py" "<file.docx>" --out "<file-clean.docx>"
46
+ ```
47
+
48
+ Removes every anchor, drops `word/comments.xml`, its relationship and content-type override. Removing comments is required before sharing a file with a customer; check `privacy_scrub_metadata.md` too.
49
+
50
+ ## Verify
51
+
52
+ `comments_extract.py` on the output (count and anchors), then render the page the comment sat on — a stripped anchor that leaves an empty run can show as a stray space.
53
+
54
+ ## Pitfalls
55
+
56
+ - A comment anchored to a paragraph mark rather than text shows no `anchored_text`; that is normal for "comment on the whole paragraph".
57
+ - Two comments cannot share an id. `comments_add.py` continues from the highest existing id.
58
+ - The comment body is ordinary paragraphs, so a long comment can contain several; `comments_extract.py` joins them with newlines.
59
+ - Replying in-thread needs Word's `commentsExtended` part. Anchoring a new comment on the same phrase is the portable equivalent.
@@ -0,0 +1,40 @@
1
+ # Compare two versions
2
+
3
+ **Goal.** Say what actually changed between two files, with evidence — not "the text differs somewhere".
4
+
5
+ Two useful comparisons: text-level (what words changed) and page-level (what the reader sees changed). Use both; a change in a style definition can leave the text identical and move every paragraph.
6
+
7
+ ## Page-level diff
8
+
9
+ ```bash
10
+ "<python>" "<skill>/scripts/render_and_diff.py" "<before.docx>" "<after.docx>" --outdir "<render-dir>" --dpi 120
11
+ ```
12
+
13
+ Renders both through the helper CLI, pairs the pages, and writes `diff-NN.png` coloured by pixel difference. Reports which pages changed, the fraction of pixels differing, and a page-count delta.
14
+
15
+ The exit code is 3 when no renderer is available: render both files another way, or compare text only and say which comparison you could not run.
16
+
17
+ ## Text-level diff
18
+
19
+ ```bash
20
+ "<python>" "<skill>/scripts/docx_kit.py" text '<before.docx>' > before.json
21
+ "<python>" "<skill>/scripts/docx_kit.py" text '<after.docx>' > after.json
22
+ ```
23
+
24
+ Diff the two paragraph dumps (any diff tool, or a few lines of Python). This is the reliable way to separate content changes from layout changes.
25
+
26
+ ## Review mode versus diff mode
27
+
28
+ If the user wants the *document* to carry the changes, use tracked changes (`tasks/clean_tracked_changes.md`) rather than a report: the reviewer accepts or rejects inside Word. A diff is evidence for a conversation; tracked changes are the artefact.
29
+
30
+ For a version that arrived with markup, `accept_tracked_changes.py --mode report` is the change list, with reviewer names and where each edit sits.
31
+
32
+ ## Verify
33
+
34
+ Open the diff PNGs with `read_image` and look at them: a coloured block on a page that should not have changed usually means a font substitution or a margin change, not a content change. Cross-check with the text diff before writing a conclusion.
35
+
36
+ ## Pitfalls
37
+
38
+ - Different renderers paginate differently; compare renders made by the same renderer and the same settings.
39
+ - Metadata and revision ids differ between saves and never appear on a page — a text diff of `word/document.xml` is noisy for that reason.
40
+ - An image replaced in place keeps the same relationship and page position; only a pixel comparison notices.
@@ -0,0 +1,107 @@
1
+ # Create or edit a document
2
+
3
+ **Goal.** Produce a `.docx` that opens cleanly in Word, with real heading styles, real lists and real tables, then change it later without destroying what is already there.
4
+
5
+ ## Create
6
+
7
+ Write a JSON spec next to the output, then run:
8
+
9
+ ```bash
10
+ "<python>" "<cli>" docx create --spec "<spec.json>" --out "<out.docx>"
11
+ ```
12
+
13
+ ```json
14
+ {
15
+ "title": "Regional revenue review",
16
+ "subtitle": "FY26 Q2",
17
+ "author": "Finance",
18
+ "header": "Internal — do not circulate",
19
+ "footer": "FY26 Q2 review",
20
+ "page_size": "Letter",
21
+ "orientation": "portrait",
22
+ "blocks": [
23
+ { "type": "heading", "level": 1, "text": "Summary" },
24
+ { "type": "paragraph", "text": "Revenue grew 18% year over year." },
25
+ { "type": "bullets", "items": ["Renewals at 92%", "42 new accounts"] },
26
+ { "type": "table", "headers": ["Region", "Revenue"], "rows": [["EMEA", "4.1M"], ["APAC", "3.2M"]] },
27
+ { "type": "page_break" }
28
+ ]
29
+ }
30
+ ```
31
+
32
+ Block types and keys: `heading` (`level`, `text`), `paragraph` (`text`, `bold`, `italic`, `size`, `color`, `align`, `style`), `bullets` / `numbered` (`items`), `table` (`headers`, `rows`, optional `style`, default `Table Grid`), `image` (`path`, optional `width` in inches), `equation` (`latex`), `quote`, `page_break`, `spacer`. Page keys: `page_size` (`Letter` or `A4`), `orientation`, and `top_margin` / `bottom_margin` / `left_margin` / `right_margin` in inches.
33
+
34
+ The command prints counts for headings, paragraphs, list items, tables, images and equations. Check `equation_fallbacks`: a non-zero value means some LaTeX was emitted as styled text instead of OMML.
35
+
36
+ ## Edit
37
+
38
+ The helper CLI does not mutate documents. Every edit is a script that reads the original, writes a copy, and refuses to overwrite its input. Omit `--out` and the copy lands next to the source as `<name>-edited.docx`.
39
+
40
+ Replace text — body, tables, and every header and footer:
41
+
42
+ ```bash
43
+ "<python>" "<skill>/scripts/replace_text.py" --input "<original.docx>" --out "<original-edited.docx>" \
44
+ --find "18%" --replace "21%" --find "Hardware flat" --replace "Hardware steady"
45
+ ```
46
+
47
+ `--find` / `--replace` are paired in order; `--spec edits.json` takes `{"replace": [{"find": "...", "replace": "..."}]}` instead. The command prints a per-pair count. A match inside one run keeps that run's formatting; a match that straddles runs collapses the paragraph into its first run, so keep needles inside a single styled span when mixed formatting matters. `--require-all` exits 1 if a pair matched nothing, which is the check you want for a scripted edit.
48
+
49
+ Append blocks — the same block types `docx create` accepts:
50
+
51
+ ```bash
52
+ "<python>" "<skill>/scripts/append_blocks.py" --input "<original.docx>" --out "<original-edited.docx>" --blocks "blocks.json"
53
+ ```
54
+
55
+ ```json
56
+ [{ "type": "heading", "level": 1, "text": "Appendix" }, { "type": "table", "headers": ["Region", "Revenue"], "rows": [["EMEA", "4.1M"]] }]
57
+ ```
58
+
59
+ `--spec` takes a full document spec and appends its `blocks` array. The builder is the CLI's own, so anything that works in `docx create` works here, including images (`path`, `width`) and equations (`latex`).
60
+
61
+ Set properties:
62
+
63
+ ```bash
64
+ "<python>" "<skill>/scripts/core_properties.py" --input "<original.docx>" --out "<original-edited.docx>" \
65
+ --title "Regional revenue review (final)" --author "Finance" --keywords "revenue, FY26"
66
+ ```
67
+
68
+ `--show` reports the current properties without writing; the same names work in `--spec core.json` under a `core` object.
69
+
70
+ Three independent scripts means a pipeline writes an intermediate copy (append, then properties). That is deliberate: each step is re-runnable and its output is a valid document you can inspect.
71
+
72
+ ## When the CLI is not enough
73
+
74
+ | Need | Route |
75
+ |---|---|
76
+ | Edit inside a header, footer, footnote or comment | `scripts/docx_kit.py xml <file> <part>` to inspect, then the snippet below or a purpose script |
77
+ | Mark edits as tracked changes | `scripts/add_tracked_replacements.py`, `tasks/clean_tracked_changes.md` |
78
+ | Header date, footer page numbers, first hyperlink | `scripts/docx_ooxml_patch.py` |
79
+ | Replace a paragraph but keep its formatting | `python-docx` snippet below |
80
+
81
+ ```bash
82
+ "<python>" - <<'PY'
83
+ from docx import Document
84
+ d = Document(r"<file.docx>")
85
+ for p in d.paragraphs:
86
+ if "18%" in p.text:
87
+ runs = p.runs or [p.add_run()]
88
+ runs[0].text = p.text.replace("18%", "21%")
89
+ for r in runs[1:]:
90
+ r.text = ""
91
+ break
92
+ d.save(r"<file-edited.docx>")
93
+ PY
94
+ ```
95
+
96
+ Editing `w:t` text directly (via `docx_kit`) is safe for plain word swaps; anything that changes paragraph structure should go through `python-docx` so the surrounding XML stays consistent.
97
+
98
+ ## Verify
99
+
100
+ `docx read` to confirm text and counts, then render and look at every page (`tasks/verify_render.md`). A spec that produces three pages instead of two means the table or margins need work, not that the renderer is wrong.
101
+
102
+ ## Pitfalls
103
+
104
+ - Deleting and retyping a heading loses its outline level; edit the text, keep the style.
105
+ - `append_blocks.py` inserts before the final section properties, so a trailing landscape section or page setup keeps working.
106
+ - Replacing text that lives in a field result changes only the cached value; the field recalculates on open in Word.
107
+ - Copying content between documents by hand drops styles and relationships — use `scripts/merge_docx_append.py`.
@@ -0,0 +1,59 @@
1
+ # Equations
2
+
3
+ **Goal.** Displayed mathematics that Word treats as maths: selectable, reflowable, editable — not a picture and not italic text.
4
+
5
+ Word stores native equations as OMML inside `m:oMathPara`/`m:oMath`. The helper CLI converts LaTeX to OMML on creation; a picture of an equation cannot be edited by the user and scales badly.
6
+
7
+ ## Create
8
+
9
+ ```bash
10
+ "<python>" "<cli>" docx create --spec "<spec.json>" --out "<out.docx>"
11
+ ```
12
+
13
+ ```json
14
+ {
15
+ "blocks": [
16
+ { "type": "heading", "level": 1, "text": "Model" },
17
+ { "type": "equation", "latex": "E = mc^2" },
18
+ { "type": "equation", "latex": "\\frac{a^2+b^2}{c^2}" }
19
+ ]
20
+ }
21
+ ```
22
+
23
+ The command reports `equations` and `equation_fallbacks`. Non-zero fallbacks mean some expression was emitted as italic text because the converter could not handle it — check the counter, then simplify the LaTeX (the converter covers fractions, superscripts, subscripts, roots, sums, Greek letters and common operators).
24
+
25
+ Displayed equations belong in their own paragraph; inline maths is ordinary runs plus OMML, which is harder to place and easy to break.
26
+
27
+ ## Check what a file contains
28
+
29
+ ```bash
30
+ "<python>" "<cli>" docx read --input "<file.docx>"
31
+ "<python>" "<skill>/scripts/docx_kit.py" xml '<file.docx>' word/document.xml | grep -c "oMath"
32
+ ```
33
+
34
+ `oMath` in the XML is the honest test. Text that merely looks mathematical (italic Times, a slash) is not an equation and will reflow badly.
35
+
36
+ ## When the converter cannot express it
37
+
38
+ Build the OMML by hand for the specific construct, or split the expression: a matrix or a piecewise definition is often clearer as a small table than as one enormous formula. If neither works, render the expression as an image with a caption and tell the user it is no longer editable.
39
+
40
+ The runtime ships the `latex2mathml` and `mathml2omml` packages, so a custom snippet can produce OMML directly:
41
+
42
+ ```bash
43
+ "<python>" - <<'PY'
44
+ from latex2mathml.converter import convert
45
+ from mathml2omml import convert as to_omml
46
+ mathml = convert(r"\sum_{i=1}^{n} x_i")
47
+ print(to_omml(mathml)[:200])
48
+ PY
49
+ ```
50
+
51
+ ## Verify
52
+
53
+ Render the page and look at the equation: fractions stacked, limits above and below the operator, Greek letters as glyphs not boxes, and the equation not clipped at the right margin. A fallback shows up as italic Latin text where a symbol should be.
54
+
55
+ ## Pitfalls
56
+
57
+ - An equation inside a table cell or a caption is legal but almost always the wrong layout decision.
58
+ - Copying OMML between documents is safe; copying a picture of it is not.
59
+ - Numeric labels belong in the text (`(3)` right-aligned), not inside the equation object, if the user will renumber later.
@@ -0,0 +1,44 @@
1
+ # Fields and field updates
2
+
3
+ **Goal.** Find out what a document computes, and decide whether to keep it live or freeze it.
4
+
5
+ A field is an instruction the reader's Word evaluates: `SEQ` numbers captions, `REF` repeats a bookmark, `PAGE`/`NUMPAGES` count pages, `TOC` builds the contents list, `DATE` prints today. Until Word recalculates it, the file shows a cached result — which is why a TOC in a freshly edited file can look stale.
6
+
7
+ ## Inspect
8
+
9
+ ```bash
10
+ "<python>" "<skill>/scripts/fields_report.py" "<file.docx>" --max-examples 5
11
+ ```
12
+
13
+ Reports the instruction, whether the field is a simple (`w:fldSimple`, one attribute) or complex field (begin/separate/end runs), and the cached result. `docx_kit.py xml` shows the raw runs when the report is not enough:
14
+
15
+ ```bash
16
+ "<python>" "<skill>/scripts/docx_kit.py" xml '<file.docx>' word/document.xml | grep -o 'instrText[^<]*' | head
17
+ ```
18
+
19
+ ## Freeze a field to its current value
20
+
21
+ ```bash
22
+ "<python>" "<skill>/scripts/fields_materialize.py" "<file.docx>" --out "<file-static.docx>" --only SEQ REF
23
+ "<python>" "<skill>/scripts/flatten_ref_fields.py" "<file.docx>" --out "<file-static.docx>"
24
+ ```
25
+
26
+ Materialising replaces the field span with one run holding the cached text, keeping the run formatting. `flatten_ref_fields.py` is the narrow version for `REF` and `PAGEREF` only.
27
+
28
+ Freeze when: the document is going to a reader who must see exactly this number, the target bookmark is being deleted, or a downstream tool mangles fields. Keep it live when the document will be edited again — a frozen caption number stops following its table.
29
+
30
+ ## Keep it live
31
+
32
+ - Word recalculates on open when `w:updateFields val="true"` is set in `word/settings.xml`. `insert_toc.py` sets it for a new TOC; if you need it for an existing file, add the element yourself and tell the user the first open will prompt.
33
+ - LibreOffice and the renderer also refresh fields; a stale cached value in a preview usually means the field was never recalculated, not that the field is broken.
34
+ - `insert_ref_fields.py` converts `[[REF:bookmark]]` placeholders into real `REF` fields, which is the reliable way to build cross-references after restructuring.
35
+
36
+ ## Verify
37
+
38
+ `fields_report.py` after the change: frozen fields disappear from the report, live ones keep their instruction. Render and read the page to confirm the number the reader sees.
39
+
40
+ ## Pitfalls
41
+
42
+ - A field split across paragraphs is invalid; keep begin/separate/end together.
43
+ - `w:fldSimple` and complex fields both appear in the report — do not assume the absence of `w:fldSimple` means there are no fields.
44
+ - Copying a field to another document copies the instruction but not the bookmark it refers to; recreate it there.
@@ -0,0 +1,50 @@
1
+ # Fixtures and edge cases
2
+
3
+ **Goal.** Have a file that contains the awkward cases *before* you are asked to handle them, so a fix is tested against a 1600-word paragraph and not only against clean prose.
4
+
5
+ ## Build the fixture set
6
+
7
+ ```bash
8
+ "<python>" "<skill>/scripts/make_fixtures.py" --outdir "<work-dir>" --only all
9
+ ```
10
+
11
+ Writes:
12
+
13
+ | File | Contains |
14
+ |---|---|
15
+ | `tracked.docx` | A replacement as real `w:del`+`w:ins` markup, tracking switched on |
16
+ | `watermark.docx` | A DRAFT watermark in the header |
17
+ | `comments.docx` | One anchored comment in `word/comments.xml` |
18
+ | `edge_cases.docx` | Empty paragraph, H1 to H3 jump, 1600-word paragraph, email and phone, 2×10 unstyled table, image with no alt text |
19
+
20
+ `--only tracked|watermark|comments|edge` builds one.
21
+
22
+ These are inputs for testing a change, not templates. Do not ship them.
23
+
24
+ ## The cases that break naive edits
25
+
26
+ - **Long paragraphs** — a replacement that rebuilds the paragraph loses run formatting across thousands of characters; check the result with `read_image`, not only with a text dump.
27
+ - **Nested tables** — text inside `table > cell > table > cell` is missed by `document.paragraphs`; walk `cell.tables` or use the pack scripts, which do.
28
+ - **Header/footer text** — a find-and-replace over the body never sees it.
29
+ - **Tables without a style** — borders come from the renderer default and change when the file is opened elsewhere; apply `Table Grid` or set explicit `#D9D9D9` borders.
30
+ - **Images with no alt text** — an accessibility finding, and also a sign the image was pasted rather than placed.
31
+ - **Manual spacing** — a document padded with empty paragraphs and spaces looks right in one renderer and wrong in the next; normalize instead (`tasks/style_lint_normalize.md`).
32
+ - **Mixed page sizes** — a landscape section inside a portrait document confuses footers and headers; `section_audit.py` reports it.
33
+
34
+ ## Regression pass before delivery
35
+
36
+ ```bash
37
+ for f in "<work-dir>"/*.docx; do
38
+ echo "== $f"
39
+ "<python>" "<skill>/scripts/heading_audit.py" "$f" || true
40
+ "<python>" "<skill>/scripts/style_lint.py" "$f" || true
41
+ "<python>" "<skill>/scripts/a11y_audit.py" "$f" || true
42
+ "<python>" "<skill>/scripts/section_audit.py" "$f" || true
43
+ done
44
+ ```
45
+
46
+ An audit that now reports nothing where it previously reported a real problem is the signal that a fix regressed. Keep the fixture out of the user's workspace; put it under the temp directory.
47
+
48
+ ## Verify
49
+
50
+ Whatever the script produced, render it. Fixtures exist to be looked at: a 1600-word paragraph and a table that is too wide both fail visually first.