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,53 @@
1
+ # Tracked changes
2
+
3
+ Tracked changes are markup, not a diff. The file contains both the old and the new text, wrapped in elements that say which is which, plus enough metadata for Word to show reviewer names and accept or reject each edit.
4
+
5
+ ## The elements
6
+
7
+ | Element | Meaning |
8
+ |---|---|
9
+ | `w:ins` | Text inserted (contains runs) |
10
+ | `w:del` | Text deleted (contains runs whose text is `w:delText`) |
11
+ | `w:moveFrom` / `w:moveTo` | Moved text, paired by `w:name` |
12
+ | `w:rPrChange` / `w:pPrChange` | Formatting change, with the previous properties inside |
13
+ | `w:delText` | The deleted characters — not `w:t` |
14
+ | `w:delInstrText` | A deleted field instruction |
15
+
16
+ Each carries `w:id`, `w:author`, `w:date`. Ids must be unique within the document; reusing one makes Word show the wrong reviewer.
17
+
18
+ A deletion is a `w:del` containing runs with `w:delText`, not a run with a strikethrough:
19
+
20
+ ```xml
21
+ <w:del w:id="101" w:author="dsh-office" w:date="2026-10-21T00:00:00Z">
22
+ <w:r><w:delText>Revenue grew</w:delText></w:r>
23
+ </w:del>
24
+ <w:ins w:id="102" w:author="dsh-office" w:date="2026-10-21T00:00:00Z">
25
+ <w:r><w:t>Revenue climbed</w:t></w:r>
26
+ </w:ins>
27
+ ```
28
+
29
+ ## Tracking has to be on
30
+
31
+ `<w:trackRevisions/>` in `word/settings.xml` makes *Word* track subsequent edits. It does not affect markup already in the file. Write the markup directly when you are generating the change; turn tracking on when the user will continue editing.
32
+
33
+ ## Accepting and rejecting
34
+
35
+ - **Accept** — keep `w:ins` content and unwrap it; delete `w:del` entirely; for moves keep `w:moveTo`.
36
+ - **Reject** — delete `w:ins` content; keep `w:del` content and convert `w:delText` back to `w:t` (unwrap the `w:del`).
37
+ - Either way, remove the revision containers afterwards; leaving an empty `w:ins` shows a stray marker.
38
+ - Format-change elements have no accept/reject choice in the content sense: accepted means the new properties stay, rejected means the old ones are restored.
39
+
40
+ ## Where tracking hides
41
+
42
+ - Inside headers, footers, footnotes, endnotes, table cells, text boxes.
43
+ - Table-level changes: `w:trPr/w:ins` for an inserted row, `w:tcPr/w:cellIns` for a cell.
44
+ - Paragraph-level: a paragraph mark inserted or deleted uses `w:rPr/w:ins` in `w:pPr`, so "delete this paragraph" is not a `w:del` around the paragraph.
45
+ - Comments are separate markup; a comment can survive its being accepted.
46
+
47
+ ## Reading markup without a renderer
48
+
49
+ ```bash
50
+ "<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode report
51
+ ```
52
+
53
+ The report lists insertions and deletions with authors and the text on each side. That is the honest way to answer "what changed" when there is no rendered view, and it is also how you confirm a generated change landed as markup rather than as plain text.
@@ -0,0 +1,83 @@
1
+ # DOCX structure reference
2
+
3
+ ## The package
4
+
5
+ ```
6
+ [Content_Types].xml extensions and per-part overrides
7
+ _rels/.rels package relationships (document, core props)
8
+ docProps/core.xml author, title, subject, keywords, revision
9
+ docProps/app.xml application, company, manager
10
+ word/document.xml the body: paragraphs, tables, sections
11
+ word/styles.xml paragraph/character/table style definitions
12
+ word/numbering.xml abstract lists and their num instances
13
+ word/settings.xml protection, tracking, updateFields
14
+ word/theme/theme1.xml fonts and colour scheme
15
+ word/header1.xml, footer1.xml
16
+ word/media/* images
17
+ word/comments.xml, footnotes.xml, endnotes.xml optional
18
+ ```
19
+
20
+ ## The body
21
+
22
+ `word/document.xml` is a sequence of block-level elements: `w:p` (paragraph), `w:tbl` (table), `w:sdt` (content control), and finally one `w:sectPr` per section (the last one sits in the body, earlier ones inside a paragraph's `pPr`).
23
+
24
+ A paragraph is:
25
+
26
+ ```xml
27
+ <w:p>
28
+ <w:pPr> <!-- paragraph properties -->
29
+ <w:pStyle w:val="Heading1"/>
30
+ <w:spacing w:before="240" w:after="80"/>
31
+ <w:keepNext/>
32
+ <w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr>
33
+ <w:jc w:val="center"/>
34
+ </w:pPr>
35
+ <w:r> <!-- run -->
36
+ <w:rPr><w:b/><w:sz w:val="24"/></w:rPr> <!-- sz is half-points: 24 = 12pt -->
37
+ <w:t xml:space="preserve">text</w:t>
38
+ </w:r>
39
+ </w:p>
40
+ ```
41
+
42
+ Everything a reader sees is a run inside a paragraph; everything that makes it look different from its style is direct formatting in `pPr`/`rPr`. Direct formatting is why `style_normalize.py` exists.
43
+
44
+ A table is `w:tbl` > `w:tblPr` (style, width, layout) + `w:tblGrid` (column widths) + `w:tr` > `w:tc` > `w:p`. A cell always contains at least one paragraph.
45
+
46
+ ## Units
47
+
48
+ | Unit | Where | Conversion |
49
+ |---|---|---|
50
+ | twip (1/1440 in) | margins, widths, indents, cell widths | 1440 per inch |
51
+ | half-point | `w:sz` | 2 per point |
52
+ | eighth-point | border widths (`w:sz` on borders) | 8 per point |
53
+ | EMU (914400/in) | drawing extents | 914400 per inch |
54
+
55
+ A Letter page is 12240×15840 twips. Text width = page width − left − right margins (default 9360 twips with 1in margins).
56
+
57
+ ## Styles
58
+
59
+ `w:style` with `w:type` of `paragraph`, `character`, `table` or `numbering`. Attributes: `w:styleId` (the internal name), `w:name` (the UI name), `w:basedOn`, `w:next`, `w:link`, plus `w:pPr`/`w:rPr`.
60
+
61
+ - `Normal` is the root; `Heading1`..`Heading9` drive the outline level and the TOC.
62
+ - Outline level can also be set explicitly with `w:outlineLvl` (0-based) in `pPr`; a paragraph can look like a heading without being one, and the TOC will not see it.
63
+ - Latent styles do not exist in the file until used; a document may reference `Heading3` while `styles.xml` has no definition for it.
64
+
65
+ ## Numbering
66
+
67
+ `word/numbering.xml` holds `w:abstractNum` (the list definition, per level: format, text, indentation) and `w:num` (`w:numId` → `w:abstractNumId`, optionally overridden). A paragraph joins a list with `w:numPr` referencing a `numId`. Two paragraphs with the same `numId` share a counter.
68
+
69
+ ## Sections
70
+
71
+ `w:sectPr` carries page size (`w:pgSz`), margins (`w:pgMar`), orientation (implied by width vs height, or `w:orient`), columns, and the header/footer references. A document with one section has one `sectPr`; a new section is a paragraph whose `pPr` contains the *previous* section's `sectPr`.
72
+
73
+ ## Fields
74
+
75
+ Simple (`w:fldSimple`) and complex (`fldChar` begin/separate/end with `w:instrText`). The visible text is a cache. `w:updateFields` in `settings.xml` asks the editor to refresh on open. See `ooxml/hyperlinks_and_fields.md`.
76
+
77
+ ## Revision markup
78
+
79
+ `w:ins`, `w:del` (with `w:delText`), `w:moveFrom`/`w:moveTo`, `w:rPrChange`/`w:pPrChange`, each with author and date. See `ooxml/tracked_changes.md`.
80
+
81
+ ## Minimum valid package
82
+
83
+ `[Content_Types].xml`, `_rels/.rels`, `word/document.xml`, `word/_rels/document.xml.rels`. Word accepts a document without styles or settings; most libraries add them. When hand-building, write `[Content_Types].xml` first in the zip.
@@ -0,0 +1,49 @@
1
+ # Eliciting a template
2
+
3
+ A user who says "use our house style" has a file somewhere. Getting it is worth one question; guessing produces a document that has to be redone.
4
+
5
+ ## Ask for the artefact, not the description
6
+
7
+ Description ("blue headings, maybe 11pt") produces a different document every time. The artefact (`house-template.dotx`, an old `.docx` of the same kind, a PDF of the last report) carries the actual styles, margins and fonts.
8
+
9
+ One sentence is enough:
10
+
11
+ > Is there an existing document in this style I should match? Send the file (`.dotx`, or any `.docx` in that style) and I will follow it exactly.
12
+
13
+ If they have no file, ask for two or three specifics that cannot be guessed and choose the rest from `SKILL.md` defaults:
14
+
15
+ - page size and margins, if their organisation is not US Letter,
16
+ - the heading/body typeface and sizes,
17
+ - anything required by policy (logo in the header, classification mark in the footer).
18
+
19
+ ## Read the template before using it
20
+
21
+ ```bash
22
+ "<python>" "<cli>" docx read --input "<template.docx>"
23
+ "<python>" "<skill>/scripts/docx_kit.py" xml '<template.docx>' word/styles.xml
24
+ "<python>" "<skill>/scripts/section_audit.py" "<template.docx>"
25
+ ```
26
+
27
+ From `styles.xml`, note the `styleId` of what will be used: `Heading1`..`Heading3`, `Title`, `Caption`, `Quote`, `ListParagraph`. From the section audit, record page size, margins, header/footer text and whether the first page is different.
28
+
29
+ ## Apply it
30
+
31
+ ```bash
32
+ "<python>" "<skill>/scripts/apply_template_styles.py" --template "<template.docx>" --target "<draft.docx>" --out "<styled.docx>"
33
+ ```
34
+
35
+ This merges style definitions (and numbering) by id — the body content is untouched, so anything written with direct formatting keeps it. Follow with:
36
+
37
+ ```bash
38
+ "<python>" "<skill>/scripts/style_normalize.py" "<styled.docx>" --out "<styled-final.docx>" --clear_paragraph_format --enforce_heading_spacing
39
+ ```
40
+
41
+ Now the text picks up the template's look instead of the draft's.
42
+
43
+ ## When the template is a PDF or an image
44
+
45
+ Extract structure, not styles: heading hierarchy, section order, table layout, caption convention, margin proportions. Then reproduce them with named styles in a generated document. Say which parts were matched approximately — a PDF does not tell you the font size of a heading, only how it looks.
46
+
47
+ ## When there is no template and no details
48
+
49
+ Fall back to the defaults in `SKILL.md` (Letter, 1in margins, 11–12pt body, black headings, `#D9D9D9` table borders) and state the assumption in one line with the delivered file. Do not stall on a question the user may not care about.
@@ -0,0 +1,49 @@
1
+ # Writing quality
2
+
3
+ Content quality is the part of a document the format cannot fix. These are the rules to apply while drafting, before any rendering.
4
+
5
+ ## Structure before prose
6
+
7
+ - Every heading states a claim the reader can disagree with ("Services carried the quarter", not "Services"). A heading that only names a topic is a missed opportunity to be clear.
8
+ - A section that cannot be summarised in one sentence is two sections.
9
+ - Three levels of heading is the practical maximum; a fourth usually means the content should be a table or an appendix.
10
+ - Nothing between a heading and its first paragraph. No label line, no restated title.
11
+
12
+ ## Paragraphs
13
+
14
+ - One idea per paragraph, first sentence carries it.
15
+ - 3–6 sentences is the comfortable range; a 1600-word paragraph is a formatting defect even if the prose is good.
16
+ - No em-dash pile-ups, no "furthermore/moreover" chains, no sentence that only announces the next sentence.
17
+ - The last paragraph of a section says what it meant, not that it is finished.
18
+
19
+ ## Lists and tables
20
+
21
+ - Bullets for parallel items of equal weight; never for a sequence (that is a numbered list).
22
+ - If items have two attributes, a table beats a bulleted list with parentheticals.
23
+ - Table headers are nouns, units in the header ("Revenue ($m)"), numbers right-aligned.
24
+ - Every table earns its caption; a table without a purpose is a screenshot of data.
25
+
26
+ ## Voice
27
+
28
+ - Active unless the actor is unknown or unimportant; "We found", not "It was found".
29
+ - Present tense for findings, past for actions taken.
30
+ - No unsupported superlatives. "18% growth" beats "strong growth"; if a number is unknown, say so rather than adding an adverb.
31
+ - Define an acronym once, then use it. Never define it twice in different places.
32
+
33
+ ## Numbers
34
+
35
+ - Consistent precision. Do not mix "12.4%" and "8%" in one table unless the source did.
36
+ - Percentages below 1 take a leading zero (0.4%, not .4%).
37
+ - State the period and the currency in the table or its caption, not in the surrounding prose.
38
+ - A number in prose must match the number in the table; rendering does not catch this, re-reading does.
39
+
40
+ ## What to cut
41
+
42
+ - Opening paragraphs that explain that the document will explain something.
43
+ - Closing paragraphs that thank the reader.
44
+ - Bold on every second phrase: emphasis used everywhere is emphasis used nowhere.
45
+ - Sentences that begin "It is important to note that".
46
+
47
+ ## Where this connects
48
+
49
+ `references/docx-structure.md` covers how the structure is expressed in Word — styles, captions, table layout. `tasks/verify_render.md` covers checking the result on a page. Neither replaces reading the text once with the question "would a busy reader get the point from the headings alone?"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python
2
+ """Render a .docx to page images through the helper CLI's render verb.
3
+
4
+ Rendering is the only honest check that a document looks right: XML can be valid
5
+ and still overlap, clip or reflow badly. This script keeps the plumbing in one
6
+ place so tasks can say "render it, look at the images" and mean a single command.
7
+
8
+ The render verb needs a real Word engine (Microsoft Word, WPS Writer, or
9
+ LibreOffice). When none is installed the script reports exactly that and exits 3,
10
+ because a placeholder image would be worse than no image.
11
+
12
+ Exit codes: 0 rendered, 1 render failed, 3 no renderer installed, 2 bad input.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent / "scripts"))
22
+ import docx_kit as kit
23
+
24
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
25
+ for _stream in (sys.stdout, sys.stderr):
26
+ try:
27
+ _stream.reconfigure(encoding="utf-8", errors="replace")
28
+ except (AttributeError, ValueError):
29
+ pass
30
+
31
+
32
+ def resize(path: Path, width: int | None, height: int | None) -> None:
33
+ try:
34
+ from PIL import Image
35
+ except ImportError:
36
+ return
37
+ if not (width or height):
38
+ return
39
+ with Image.open(path) as image:
40
+ size = (width or image.width, height or image.height)
41
+ image.resize(size).save(path)
42
+
43
+
44
+ def main(argv: list[str] | None = None) -> int:
45
+ parser = argparse.ArgumentParser(description="Render a .docx to PNG page images (and optionally keep the PDF).")
46
+ parser.add_argument("input_docx", help="Input .docx")
47
+ parser.add_argument("--output_dir", default=None, help="Directory for page images (default: next to the input)")
48
+ parser.add_argument("--dpi", type=int, default=150)
49
+ parser.add_argument("--width", type=int, default=None, help="Resize page images to this width")
50
+ parser.add_argument("--height", type=int, default=None, help="Resize page images to this height")
51
+ parser.add_argument("--emit_pdf", action="store_true", help="Keep the intermediate PDF and report its path")
52
+ parser.add_argument("--verbose", action="store_true", help="Include the CLI's raw output")
53
+ args = parser.parse_args(argv)
54
+
55
+ source = Path(args.input_docx).expanduser().resolve()
56
+ if not source.is_file():
57
+ print("error: %s not found" % source, file=sys.stderr)
58
+ return 2
59
+ out_dir = Path(args.output_dir).expanduser().resolve() if args.output_dir else source.parent / (source.stem + "-render")
60
+ out_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ command = ["docx", "render", "--input", str(source), "--out-dir", str(out_dir)]
63
+ if args.dpi:
64
+ command += ["--dpi", str(args.dpi)]
65
+ code, stdout, stderr = kit.run_office_cli(command)
66
+ payload = kit.cli_json(stdout) or kit.cli_json(stderr) or {}
67
+ pages = payload.get("pages") or []
68
+
69
+ if code != 0 or not pages:
70
+ detail = payload.get("error") or payload.get("reason") or ""
71
+ if not detail:
72
+ lines = [line for line in (stderr or stdout or "").splitlines() if line.strip() not in ("", "{", "}")]
73
+ detail = lines[-1].strip() if lines else "render failed"
74
+ if "renderer" in detail.lower() or "libreoffice" in detail.lower():
75
+ kit.emit({"ok": False, "reason": "no_renderer", "detail": detail, "remedy": "Install Microsoft Office, WPS Office, or LibreOffice, then re-run this command."})
76
+ return 3
77
+ kit.emit({"ok": False, "reason": "render_failed", "detail": detail})
78
+ return 1
79
+
80
+ for page in pages:
81
+ resize(Path(page), args.width, args.height)
82
+
83
+ result = {
84
+ "ok": True,
85
+ "input": str(source),
86
+ "pages": pages,
87
+ "page_count": len(pages),
88
+ "renderer": payload.get("renderer"),
89
+ }
90
+ if args.emit_pdf and payload.get("pdf"):
91
+ result["pdf"] = payload["pdf"]
92
+ if args.verbose:
93
+ result["stdout"] = stdout.strip()
94
+ kit.emit(result)
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python
2
+ """Accessibility audit for a .docx, with two safe repair switches.
3
+
4
+ Checks images for missing alternative text, tables for a repeating header row,
5
+ heading runs for skipped levels, links for vague link text, and the package for
6
+ a document title. Repairs are opt-in and never guess at meaning: alt text is
7
+ derived from the image file name and table headers come from the first row.
8
+
9
+ Exit codes: 0 clean (or all findings repaired), 1 findings remain, 2 bad input.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import posixpath
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+ import docx_kit as kit
21
+
22
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
23
+ for _stream in (sys.stdout, sys.stderr):
24
+ try:
25
+ _stream.reconfigure(encoding="utf-8", errors="replace")
26
+ except (AttributeError, ValueError):
27
+ pass
28
+
29
+ GENERIC_LINK_TEXT = {"click here", "here", "link", "this link", "read more"}
30
+ DETAIL_PARTS = ("word/document.xml", "word/header1.xml", "word/footer1.xml")
31
+
32
+
33
+ def header_footer_parts(doc: kit.Docx) -> list[str]:
34
+ names = doc.names("word/header") + doc.names("word/footer")
35
+ return [name for name in names if name.endswith(".xml") and "_rels" not in name]
36
+
37
+
38
+ def scan_images(doc: kit.Docx) -> list[dict]:
39
+ findings = []
40
+ for part in ["word/document.xml"] + header_footer_parts(doc):
41
+ if not doc.exists(part):
42
+ continue
43
+ rels = {rel["id"]: rel for rel in doc.relationships(part)}
44
+ for docpr in doc.xml(part).iter(kit.q("docPr", kit.WP)):
45
+ blip = docpr.getparent().find(".//" + kit.q("blip", kit.A))
46
+ rid = blip.get(kit.q("embed", kit.R)) if blip is not None else None
47
+ target = rels.get(rid, {}).get("resolved", "")
48
+ alt = (docpr.get("descr") or docpr.get("title") or "").strip()
49
+ findings.append(
50
+ {
51
+ "code": "image_missing_alt",
52
+ "part": part,
53
+ "name": docpr.get("name"),
54
+ "target": target,
55
+ "ok": bool(alt),
56
+ }
57
+ )
58
+ return findings
59
+
60
+
61
+ def scan_tables(doc: kit.Docx) -> list[dict]:
62
+ findings = []
63
+ for index, table in enumerate(doc.xml("word/document.xml").iter(kit.q("tbl"))):
64
+ rows = table.findall(kit.q("tr"))
65
+ if not rows:
66
+ continue
67
+ props = rows[0].find(kit.q("trPr"))
68
+ repeat = props is not None and props.find(kit.q("tblHeader")) is not None
69
+ findings.append(
70
+ {
71
+ "code": "table_missing_header_row",
72
+ "table_index": index,
73
+ "rows": len(rows),
74
+ "ok": repeat,
75
+ "style": (table.find(kit.q("tblPr")).find(kit.q("tblStyle")).get(kit.q("val")) if table.find(kit.q("tblPr")) is not None and table.find(kit.q("tblPr")).find(kit.q("tblStyle")) is not None else ""),
76
+ }
77
+ )
78
+ return findings
79
+
80
+
81
+ def scan_headings(doc: kit.Docx) -> list[dict]:
82
+ findings = []
83
+ previous = 0
84
+ for index, paragraph in enumerate(doc.paragraphs()):
85
+ level = kit.heading_level(paragraph)
86
+ if not level:
87
+ continue
88
+ text = kit.paragraph_text(paragraph).strip()
89
+ if previous and level > previous + 1:
90
+ findings.append({"code": "heading_level_jump", "paragraph": index, "from": previous, "to": level, "text": text, "ok": False})
91
+ if not text:
92
+ findings.append({"code": "empty_heading", "paragraph": index, "level": level, "ok": False})
93
+ previous = level
94
+ return findings
95
+
96
+
97
+ def scan_links(doc: kit.Docx) -> list[dict]:
98
+ findings = []
99
+ for part in ["word/document.xml"] + header_footer_parts(doc):
100
+ if not doc.exists(part):
101
+ continue
102
+ for link in doc.xml(part).iter(kit.q("hyperlink")):
103
+ text = kit.text_of(link).strip().lower()
104
+ if not text:
105
+ findings.append({"code": "empty_link", "part": part, "ok": False})
106
+ elif text in GENERIC_LINK_TEXT:
107
+ findings.append({"code": "vague_link_text", "part": part, "text": text, "ok": False})
108
+ return findings
109
+
110
+
111
+ def scan_title(doc: kit.Docx) -> list[dict]:
112
+ if not doc.exists("docProps/core.xml"):
113
+ return [{"code": "missing_document_title", "ok": False}]
114
+ core = doc.xml("docProps/core.xml")
115
+ title = core.find("{http://purl.org/dc/elements/1.1/}title")
116
+ text = (title.text or "").strip() if title is not None else ""
117
+ return [{"code": "missing_document_title", "ok": bool(text)}] if not text else []
118
+
119
+
120
+ def fix_alt_text(doc: kit.Docx) -> int:
121
+ changed = 0
122
+ for part in ["word/document.xml"] + header_footer_parts(doc):
123
+ if not doc.exists(part):
124
+ continue
125
+ rels = {rel["id"]: rel for rel in doc.relationships(part)}
126
+ for docpr in doc.xml(part).iter(kit.q("docPr", kit.WP)):
127
+ if (docpr.get("descr") or "").strip():
128
+ continue
129
+ blip = docpr.getparent().find(".//" + kit.q("blip", kit.A))
130
+ rid = blip.get(kit.q("embed", kit.R)) if blip is not None else None
131
+ target = rels.get(rid, {}).get("resolved", "")
132
+ stem = posixpath.splitext(posixpath.basename(target))[0]
133
+ label = (stem or docpr.get("name") or "image").replace("_", " ").replace("-", " ").strip()
134
+ docpr.set("descr", label.title() or "Image")
135
+ changed += 1
136
+ return changed
137
+
138
+
139
+ def fix_table_headers(doc: kit.Docx) -> int:
140
+ changed = 0
141
+ for table in doc.xml("word/document.xml").iter(kit.q("tbl")):
142
+ rows = table.findall(kit.q("tr"))
143
+ if not rows:
144
+ continue
145
+ props = rows[0].find(kit.q("trPr"))
146
+ if props is None:
147
+ props = rows[0].makeelement(kit.q("trPr"), {})
148
+ rows[0].insert(0, props)
149
+ if props.find(kit.q("tblHeader")) is None:
150
+ props.append(props.makeelement(kit.q("tblHeader"), {}))
151
+ changed += 1
152
+ return changed
153
+
154
+
155
+ def main(argv: list[str] | None = None) -> int:
156
+ parser = argparse.ArgumentParser(description="Audit a .docx for accessibility blockers, optionally repairing the two safe cases.")
157
+ parser.add_argument("in_docx", help="Input .docx")
158
+ parser.add_argument("--fix_image_alt", choices=["from_filename"], help="Derive missing alt text from the image file name")
159
+ parser.add_argument("--fix_table_headers", choices=["first_row"], help="Mark the first row as a repeating header")
160
+ parser.add_argument("--out", help="Write the repaired .docx here (required with any --fix)")
161
+ parser.add_argument("--out_json", help="Write the full report JSON here instead of stdout")
162
+ args = parser.parse_args(argv)
163
+
164
+ try:
165
+ doc = kit.Docx(args.in_docx)
166
+ except (OSError, KeyError) as exc:
167
+ print("error: %s" % exc, file=sys.stderr)
168
+ return 2
169
+
170
+ findings = scan_images(doc) + scan_tables(doc) + scan_headings(doc) + scan_links(doc) + scan_title(doc)
171
+ issues = [item for item in findings if not item["ok"]]
172
+
173
+ repairs = {}
174
+ if args.fix_image_alt or args.fix_table_headers:
175
+ if not args.out:
176
+ parser.error("--out is required when applying fixes")
177
+ if args.fix_image_alt:
178
+ repairs["image_alt"] = fix_alt_text(doc)
179
+ if args.fix_table_headers:
180
+ repairs["table_headers"] = fix_table_headers(doc)
181
+ if repairs and args.out:
182
+ doc.save(args.out)
183
+ issues = [item for item in (scan_images(doc) + scan_tables(doc) + scan_headings(doc) + scan_links(doc) + scan_title(doc)) if not item["ok"]]
184
+
185
+ counts: dict[str, int] = {}
186
+ for item in issues:
187
+ counts[item["code"]] = counts.get(item["code"], 0) + 1
188
+ report = {
189
+ "ok": not issues,
190
+ "input": str(Path(args.in_docx)),
191
+ "output": str(Path(args.out)) if args.out and repairs else None,
192
+ "repairs": repairs,
193
+ "counts": counts,
194
+ "findings": issues,
195
+ }
196
+ kit.emit(report, args.out_json)
197
+ return 1 if issues else 0
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())