dsh-office-kit 0.2.0 → 0.3.0

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 +92 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +136 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +192 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +124 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +59 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +80 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +126 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +154 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +91 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +92 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +111 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -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 +136 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -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 +176 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -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 +253 -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 +239 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +171 -246
@@ -0,0 +1,66 @@
1
+ # Create a workbook
2
+
3
+ `"<python>" "<cli>" xlsx create --spec "<spec.json>" --out "<book.xlsx>"` writes one sheet per `sheets[]` entry, top-left at A1, and treats any string starting with `=` as a formula.
4
+
5
+ ## Spec keys (only these exist)
6
+
7
+ ```json
8
+ {
9
+ "sheets": [{
10
+ "name": "Model",
11
+ "rows": [["Revenue (Q3 actual)", 1840000], ["Growth rate", 0.06]],
12
+ "formulas": { "B4": "=B2*(1+B3)" },
13
+ "styles": {
14
+ "A1": { "bold": true, "size": 16, "color": "FFFFFF", "fill": "1F3A5F", "align": "left" },
15
+ "B2:B3": { "number_format": "#,##0" }
16
+ },
17
+ "widths": { "A": 26, "B": 16 },
18
+ "row_heights": { "1": 24 },
19
+ "freeze": "A2",
20
+ "merged": ["A1:F1"],
21
+ "gridlines": false,
22
+ "charts": [{
23
+ "type": "column",
24
+ "title": "Q4 plan (USD)",
25
+ "anchor": "A16",
26
+ "height": 7,
27
+ "width": 14,
28
+ "data": { "min_col": 2, "min_row": 1, "max_col": 2, "max_row": 4 },
29
+ "categories": { "min_col": 1, "min_row": 2, "max_row": 4 }
30
+ }],
31
+ "images": [{ "path": "<absolute png path>", "anchor": "H2", "width": 240 }]
32
+ }]
33
+ }
34
+ ```
35
+
36
+ - `styles` values: `bold`, `italic`, `size`, `color`, `fill`, `number_format`, `align` (`left`/`center`/`right`, vertically centered). A range key styles every cell in it.
37
+ - `formulas` is address → formula string; the leading `=` is required.
38
+ - `charts[].data` and `.categories` use 1-based column/row indices on the same sheet, so build the label column before the value column and keep both over the same rows.
39
+ - `merged` takes A1 strings. Merge only for presentation (one title across empty header cells); merged cells cannot be resized, sorted, or filled per cell, so keep data out of them.
40
+
41
+ ## Order of work
42
+
43
+ 1. Sketch the sheets as roles, not tabs: inputs, calculations, outputs, checks. A five-line model belongs on one sheet with labelled blocks.
44
+ 2. Put the number formats on the cells, not on a copy of the values. `#,##0` for currency-ish amounts, `0.0%` for percentages, `yyyy-mm-dd` for real dates.
45
+ 3. Write the calculation as visible steps (quantity × rate, opening + movements = closing) and link the output to the build rather than repeating the arithmetic.
46
+ 4. Add a chart only when it answers a comparison question the numbers alone answer slowly; see `references/charts.md`.
47
+ 5. Render and read the PNG, then recalc if formulas exist.
48
+
49
+ ## What needs more than the spec
50
+
51
+ | Need | Route |
52
+ | --- | --- |
53
+ | Excel Table (filter buttons, banded style, structured references) | `recipes/data-tables.md` |
54
+ | Conditional formatting, data bars, colour scales | `recipes/conditional-formats.md` |
55
+ | Defined names used by formulas or validation | `recipes/named-ranges.md` |
56
+ | Sparkline-style in-cell bars | `recipes/sparklines.md` |
57
+ | Chart types or settings beyond bar/column/line/pie | `recipes/charts.md` |
58
+ | Page setup, print area, tab colours, sheet protection | openpyxl snippet (`ws.page_setup`, `ws.print_area`, `ws.sheet_properties.tabColor`); see `references/style-guidelines.md` |
59
+
60
+ ## Pitfalls
61
+
62
+ - Values written through the CLI keep their type: `1840000` is a number, `"1,840,000"` is text and will not sum. Format numbers, never pre-format them as strings.
63
+ - A chart whose `max_row` is larger than the data plots empty points as zero, which reads as a collapse to the floor. Match the range to the data.
64
+ - `anchor` is where the chart's top-left corner sits; a chart outside the print area gets cut off by `xlsx render`. Put it below or beside the data inside the printed range.
65
+ - Dates must be real dates (`date(2026, 1, 31)` via a snippet, or Excel serial numbers), never `"Jan-26"` strings, or period filters fail silently.
66
+ - Set the font family explicitly if the file will be opened on another machine; a missing Calibri falls back to a serif in some renderers and every column width shifts.
@@ -0,0 +1,38 @@
1
+ # Diff two workbooks
2
+
3
+ ```bash
4
+ "<python>" "<skill>/container_tools/diff_workbooks.py" --before "<original.xlsx>" --after "<edited.xlsx>"
5
+ ```
6
+
7
+ Exit code `0` means the files match at the requested level, `1` means differences (not a failure), `2` is a usage or read error. The report is JSON on stdout.
8
+
9
+ ## Options
10
+
11
+ | Flag | Effect |
12
+ | --- | --- |
13
+ | `--no-formats` | Compare values and formulas only; ignore styling, number formats, widths |
14
+ | `--sheet NAME` | Restrict to one sheet (repeatable) |
15
+ | `--limit N` | Cap the cell-level changes listed per sheet (default 200); counts stay exact |
16
+
17
+ ## What it compares
18
+
19
+ - **Structure** — sheet list and order, added/removed sheets, merged ranges, column widths, hidden state.
20
+ - **Cells** — value and formula changes, reported as `{cell, before, after}` per sheet, with a count even when the list is truncated.
21
+ - **Formatting** — number format, fill, font weight and colour, alignment; dropped by `--no-formats`.
22
+ - **Objects** — Excel Table names and refs, defined names, chart counts and series, conditional-formatting rule sets.
23
+
24
+ ## Using it as an edit gate
25
+
26
+ Run it as `--before original --after original-edited` immediately after any edit, and read the "changed" sections as the diff you are about to describe to the user:
27
+
28
+ 1. Anything under cells that you did not intend to touch is collateral damage — revert and redo with a narrower edit.
29
+ 2. A sheet, table, name, or chart in "removed" is a lost object; rebuild it from `recipes/charts.md` / `recipes/named-ranges.md` before presenting.
30
+ 3. Formatting-only differences across many rows usually mean a style object was reapplied to a range instead of a cell.
31
+
32
+ Diffing a file against itself is a useful sanity check that the tool and the file load cleanly: it should return `identical: true`.
33
+
34
+ ## Limits
35
+
36
+ - Cached formula results are not compared; use `recalc_gate.py` for that.
37
+ - Pivot caches and VBA are compared by part presence, not content.
38
+ - Chart internals are compared by count and series references; a restyled chart of the same data reads as unchanged.
@@ -0,0 +1,69 @@
1
+ # Edit an existing workbook
2
+
3
+ The file is the specification. Read its conventions before writing a cell, then make the smallest change that satisfies the request.
4
+
5
+ ## Procedure
6
+
7
+ 1. `"<python>" "<cli>" xlsx read --input "<book.xlsx>" --json-out "<dump>.json"` — every sheet, its used range, and its formulas.
8
+ 2. `"<python>" "<skill>/container_tools/inspect_workbook.py" --input "<book.xlsx>"` — what the read verb does not show: merged ranges, conditional formatting, defined names, Excel Tables, charts and their series, freeze panes, protections. It prints JSON; `--no-values` skips the second load that looks for cached results.
9
+ 3. Render the sheets you are about to touch and look at them: `xlsx render`, then `read_image`. Match the surrounding format, not your personal default.
10
+ 4. Apply the edit with the narrowest tool that fits (below), or with an openpyxl snippet when the change is not one of those shapes.
11
+ 5. Read back and re-check structure, recalculate if formulas changed, render again, compare.
12
+
13
+ ## Mutation tools
14
+
15
+ There is no `xlsx edit` verb. Each tool loads the workbook, changes only what you name, and saves a copy.
16
+
17
+ | Change | Command |
18
+ | --- | --- |
19
+ | Write values and formulas | `set_cells.py --input "<book.xlsx>" --out "<step1.xlsx>" --sheet Model --set B3=0.07 --set D9="=SUM(D2:D8)"` |
20
+ | Reuse a file of assignments | `set_cells.py --input "<book.xlsx>" --spec "<cells.json>"` |
21
+ | Format cells | `style_cells.py --input "<step1.xlsx>" --out "<step2.xlsx>" --sheet Model --range B11:B14 --style '{"number_format":"#,##0"}'` |
22
+ | Append records | `append_rows.py --input "<book.xlsx>" --sheet Pipeline --rows '[["Delta Supply","R. Osei","Proposal",98000,0.4]]'` |
23
+ | Add, rename, delete, reorder sheets | `sheet_ops.py --input "<book.xlsx>" --add Checks` / `--rename Sheet1=Assumptions` / `--delete Scratch` / `--order "Outputs,Assumptions,Build"` |
24
+ | Add a native chart | `add_chart.py --input "<book.xlsx>" --sheet Trend --type line --title "Monthly revenue (USD)" --anchor A6 --data-min-col 2 --data-min-row 3 --data-max-col 13 --data-max-row 3 --cat-min-row 2 --cat-max-row 2` |
25
+ | Embed an image | `add_image.py --input "<book.xlsx>" --sheet Cover --path logo.png --anchor B2 --width 320` |
26
+
27
+ Each tool writes `<input>-edited.xlsx` unless you pass `--out`, so chain by naming outputs, never by editing the file you just read and forgetting the step in between.
28
+
29
+ `cells.json` is `{"sheet": "Model", "cells": {"B3": 0.07, "D9": "=SUM(D2:D8)"}}` or a list of those. `styles.json` is `{"sheet": "Model", "styles": {"A11:B11": {"bold": true}, "C11:C14": {"number_format": "0.0%"}}}`.
30
+
31
+ Two tool limits are deliberate: `append_rows.py` does not extend an Excel Table, copy the formula from the row above, or grow a chart — do those with a snippet from `recipes/data-tables.md` and `recipes/charts.md`, or the new rows will sit outside the table and the chart will stop at the old last row. `add_chart.py` treats a one-row data range as one series of many points (which is what a monthly row means) but cannot set axis number formats or series colours; fix those in the same snippet.
32
+
33
+ ## What must survive an edit
34
+
35
+ | Thing | Why it breaks | Guard |
36
+ | --- | --- | --- |
37
+ | Formula cells outside the edit | openpyxl keeps the formula text, so they survive — unless something flattens them | Never write computed results over formulas |
38
+ | Pivot tables | openpyxl does not model pivots; a save-backed rewrite can drop the cache | Inspect before/after, or edit with a snippet that touches only cells |
39
+ | Excel Tables (`ListObject`) | Rows appended below a table are outside it | Extend `table.ref` when you add rows |
40
+ | Shared formulas and array formulas | Re-saving can materialise or drop them | If the area uses them, edit only the cells asked and diff after |
41
+ | Named ranges, conditional formats, validation | Not always round-tripped by third-party writers | Confirm with `inspect_workbook.py` after the save |
42
+ | Charts, images, sparklines | Anchored objects; some are dropped on re-save | Count them before and after; rebuild from `recipes/charts.md` if lost |
43
+ | Freeze panes, hidden/grouped rows, tab colours, protection | Structural metadata | Diff structure before and after |
44
+ | Sheet names used in formulas | Renaming breaks every reference | Prefer adding a sheet over renaming one |
45
+
46
+ New conditional formatting that covers a whole table must extend to the rows you add, or the new rows look unfinished. A dynamic chart range whose source grows needs the same extension, or the chart stops at the old last row.
47
+
48
+ ## Cells: the smallest change
49
+
50
+ - Write to the sheet and address you resolved; never "the first empty row" of an unnamed sheet.
51
+ - A text edit changes run-level text only in a cell; the format stays unless the request names it.
52
+ - A number-format change is a format change, not a value change: do not replace `0.0649` with `6.5%` text.
53
+ - Do not autofit every column or restyle a table to fix one clipped cell. Widen the column, or shrink the font only for that block.
54
+ - Do not add spacer rows, helper sheets, or "Notes" tabs unless the request needs them.
55
+
56
+ ## Errors you did not cause
57
+
58
+ If `xlsx read` shows `#REF!`, `#DIV/0!`, or a formula pointing at a sheet that no longer exists, it is pre-existing. Summarise where it is and ask before touching it — repairing it silently changes numbers the user may already have reported.
59
+
60
+ ## Verify
61
+
62
+ ```bash
63
+ "<python>" "<cli>" xlsx recalc --input "<book-edited.xlsx>" --out "<book-edited.xlsx>"
64
+ "<python>" "<skill>/container_tools/recalc_gate.py" --input "<book-edited.xlsx>"
65
+ "<python>" "<skill>/container_tools/diff_workbooks.py" --before "<book.xlsx>" --after "<book-edited.xlsx>"
66
+ "<python>" "<cli>" xlsx render --input "<book-edited.xlsx>" --out-dir "<render-dir>"
67
+ ```
68
+
69
+ The diff is the closest thing to a guarantee that nothing else moved: it reports changed cells and formulas, changed formatting unless you pass `--no-formats`, and structure changes (sheets, merges, widths, tables, charts, defined names). Exit code `1` means differences — not failure. Use `--sheet` to narrow and `--limit` to cap the per-sheet cell list.
@@ -0,0 +1,38 @@
1
+ # Inspect a workbook
2
+
3
+ Two readers, different jobs:
4
+
5
+ - `"<python>" "<cli>" xlsx read --input "<book.xlsx>" [--data-only]` — the cell values, per sheet, as arrays without coordinates. `--data-only` returns cached results instead of formula text.
6
+ - `"<python>" "<skill>/container_tools/inspect_workbook.py" --input "<book.xlsx>"` — the structure around the cells: formats that govern how they read, objects that live beside them, and whether the formulas were ever computed.
7
+
8
+ Run both before editing, and after editing to prove nothing was lost.
9
+
10
+ ## What the inspector reports
11
+
12
+ | Field | Reading it |
13
+ | --- | --- |
14
+ | `workbook.sheets`, `active_sheet` | Tab order and what opens first; order carries meaning (outputs lead) |
15
+ | `workbook.defined_names` | `name`, `refers_to`, `scope` (`workbook` or a sheet), `hidden`. A broken `refers_to` (`#REF!`) breaks every formula using the name |
16
+ | `workbook.calc_mode`, `full_calc_on_load`, `date_system` | `manual` calc mode means saved values can be stale; `1904` date system shifts every date by four years |
17
+ | `container.*` | Counts of tables, pivot tables/caches, charts, drawings, images, comment parts, external links, VBA, shared strings — the objects a rewrite can silently drop |
18
+ | `sheets[].dimensions`, `max_row`, `max_column` | The used range: what the file *thinks* has content, including formatting far from the data |
19
+ | `sheets[].print_area`, `freeze_panes`, `gridlines` | Layout choices a render will honour |
20
+ | `sheets[].merged` | Merged ranges; a value lives in the top-left cell only |
21
+ | `sheets[].tables` | Excel Table names and refs; rows appended below a `ref` land outside the table |
22
+ | `sheets[].charts[].anchor`, `.kind`, `.series` | Where a chart sits and what it plots; `anchor` outside `print_area` means the chart is cut off when printed |
23
+ | `sheets[].conditional_formats[].rules` | `type` (`cellIs`, `colorScale`, `dataBar`, `expression`), `operator`, `formula`, `priority`. A rule whose range stops short of new rows silently stops applying |
24
+ | `sheets[].data_validations` | Dropdowns and constraints an edit must preserve |
25
+ | `sheets[].hidden_rows`, `hidden_columns` | Content the user deliberately folded away |
26
+ | `sheets[].formulas.shared`, `.array`, `.data_tables` | Non-trivial formula storage; re-saving can materialise or drop these |
27
+ | `sheets[].values.cached`, `.missing`, `.errors` | How many formula cells carry a cached result, carry none, or carry an error value |
28
+
29
+ `--no-values` skips the second load that reads cached results (faster on big files, blind to whether formulas were ever computed). `--sample N` sets how many formula samples per sheet are printed (default 5).
30
+
31
+ ## How to read it
32
+
33
+ - `values.missing` equal to `formulas.count` means nothing has ever calculated this file. Do not quote any formula result; recalculate first (`tasks/recalc-and-check.md`).
34
+ - `values.errors` non-empty is pre-existing damage (or a deliberately `#N/A` sentinel). Report it, do not fix it unasked.
35
+ - `container.pivot_tables > 0` or `has_vba_project: true` means a plain openpyxl rewrite is risky: back up, change the fewest cells possible, and re-inspect after saving.
36
+ - `formulas.shared > 0` means a spreadsheet app wrote the file; copy an existing formula down rather than inventing a new pattern.
37
+ - A sheet with a huge `dimensions` and few real cells is a formatting artefact (fill applied to whole columns). Do not "clean" it; note it if the user asks why the file is slow.
38
+ - `print_area` and chart `anchor` are the two fields that explain most render surprises: charts clipped at the page edge, or entire data blocks missing from a PDF because they fall outside the print range.
@@ -0,0 +1,57 @@
1
+ # Recalculate and gate consistency
2
+
3
+ A workbook written by openpyxl or by a script has formula text and no cached
4
+ results. Open it in a viewer that does not calculate and every formula cell shows
5
+ blank. `xlsx recalc` runs the real engine to fill the cache; the gate then proves
6
+ every formula cell got a value and none of them is an error.
7
+
8
+ ```bash
9
+ "<python>" "<cli>" xlsx recalc --input "<book.xlsx>" --out "<book-recalculated.xlsx>"
10
+ "<python>" "<skill>/container_tools/recalc_gate.py" --input "<book-recalculated.xlsx>"
11
+ ```
12
+
13
+ - `recalc` needs a calculation engine on this host — Microsoft Excel or WPS
14
+ Spreadsheets, driven over COM. Without one the verb fails with a plain message
15
+ and changes nothing.
16
+ - `recalc_gate.py` runs `recalc` for you when given `--cli`: it takes
17
+ `--input`, writes `<stem>-recalculated.xlsx` unless `--out` is given, then
18
+ scans every formula cell.
19
+ - It exits `0` clean, `1` with failures, `2` on a usage or file error, and `3`
20
+ when recalculation was requested but no engine exists. Exit `3` with
21
+ `verdict: unverified` is an honest result — report it as "structure verified,
22
+ numbers not recalculated", never as "numbers verified".
23
+
24
+ ## What the gate flags
25
+
26
+ | Flag | Meaning | Action |
27
+ | --- | --- | --- |
28
+ | `errors` | Cached value is `#REF!`, `#DIV/0!`, `#NAME?`, `#VALUE!`, `#N/A`, … | Fix the formula or the input; do not paint the cell red and move on |
29
+ | `missing` | Formula cell with no cached value | Either recalc did not reach it or it was written after the recalc |
30
+ | `dangling` | Formula references a sheet that does not exist | Renaming a sheet without updating formulas does this; restore the name or repoint the reference |
31
+ | `ignored` | Cells you passed with `--ignore` | Pre-existing errors the user asked you to leave alone |
32
+
33
+ `--strict` treats missing values as failures instead of notes, which is what you
34
+ want after a successful recalc. `--ignore 'Rates!B7,B9'` keeps a known,
35
+ user-accepted error out of the verdict without hiding the rest.
36
+
37
+ ## Consistency checks the gate cannot do
38
+
39
+ Recalculation proves formulas evaluate, not that they are right. Check by hand:
40
+
41
+ - Totals equal the sum of the rows above them, and detail rows add to the
42
+ summary — not just that both computed something.
43
+ - Percentages are stored as fractions and formatted, not typed as `6.4`.
44
+ - A case selector drives every tab that claims to follow the case. Two tabs
45
+ showing different cases is the most expensive defect in a model.
46
+ - Hard-coded overrides are visible (a different fill or a comment) and the
47
+ formula path still exists. If an override replaced a formula, the workbook can
48
+ no longer update.
49
+ - Every input the user expects to change is in a cell they can find, not inside a
50
+ formula constant.
51
+
52
+ ## When there is no engine
53
+
54
+ Report exactly what happened: the file was created and structurally verified, the
55
+ numbers are formulas, and they will populate the first time the user opens the
56
+ file in Excel, LibreOffice, or Sheets. Offer to render so the user sees the layout
57
+ and labels. Do not invent values to make the preview look complete.
@@ -0,0 +1,252 @@
1
+ # Annotation recipes
2
+
3
+ The user may reply to a rendered preview with an `office-annotations` block: a
4
+ JSON array wrapped in `<office-annotations>` … `</office-annotations>` tags.
5
+ Each format sends a slightly different item; the shapes below are what the
6
+ client actually emits.
7
+
8
+ | Format | Item keys | `page` means |
9
+ | --- | --- | --- |
10
+ | pdf | `target`, `fileName`, `page`, `text`, `requirement`, `region`, `imageName` | 1-based page of the PDF itself |
11
+ | docx | `target`, `fileName`, `page`, `text`, `requirement`, `region` | page of the rendered PDF preview, not a Word page |
12
+ | pptx | `target`, `fileName`, `page`, `text`, `requirement`, `region` | rendered PDF page; one slide per page, so page N is slide N |
13
+ | xlsx | `target`, `fileName`, `text: ""`, `requirement`, `region`, `imageName`, `unmapped: true` | no `page`, no `sheet`, no `range` |
14
+
15
+ - `target` and `fileName` name the file.
16
+ - `region` is normalised 0–1 against the rendered preview page the user drew on.
17
+ - `requirement` is natural language. Read it as a change request.
18
+ - `text` is the words under the mark. For docx and pdf it is stronger evidence
19
+ than `region`; for xlsx it is always empty.
20
+ - `imageName` marks a location screenshot that arrived as a normal image
21
+ attachment.
22
+
23
+ Rules that apply everywhere:
24
+
25
+ 1. Render the *current* file first and look at the same page the user annotated.
26
+ Annotation geometry is only meaningful against a render you can reproduce.
27
+ 2. Locate the concrete object, then apply the smallest edit that satisfies
28
+ `requirement`. Do not restyle the document, do not rewrite neighbours.
29
+ 3. Re-render and look at the edited page again. The item is done when the region
30
+ shows the requested change and the layout still holds.
31
+ 4. If the payload cannot be tied to an object (empty region, no geometry, no
32
+ matching text), say what you could not locate. Never guess and never move the
33
+ region to make it fit.
34
+
35
+ Worked examples below use `report.docx`, `report.pdf`, `deck.pptx` and
36
+ `book.xlsx` and the `python`/`cli` values from the `## Office runtime` block.
37
+
38
+ ## docx — rendered-PDF page plus text matching
39
+
40
+ `page` is the page of the **rendered PDF preview**, not a Word page number.
41
+
42
+ ```bash
43
+ "<python>" "<cli>" docx render --input "report.docx" --out-dir "render"
44
+ ```
45
+
46
+ This writes `render/page-1.png` and keeps `render/report.pdf`. In the example
47
+ the user circled a sentence on preview page 1 and asked to round it.
48
+
49
+ Locate it from the text, not from the pixels:
50
+
51
+ - `"<python>" "<cli>" docx read --input "report.docx"` lists every paragraph with
52
+ its style. Find the paragraph containing `text`.
53
+ - Use `region` only to break ties between two similar paragraphs, or to confirm
54
+ the preview page number. If they disagree, trust `text` and say so.
55
+
56
+ Extract the words under the region when you need confirmation. Save this as
57
+ `locate.py` and run it with the runtime python:
58
+
59
+ ```python
60
+ import sys, pymupdf
61
+ pdf, page_no, l, t, w, h = sys.argv[1], int(sys.argv[2]), *map(float, sys.argv[3:7])
62
+ page = pymupdf.open(pdf)[page_no - 1]
63
+ r = page.rect
64
+ clip = pymupdf.Rect(r.x0 + l * r.width, r.y0 + t * r.height,
65
+ r.x0 + (l + w) * r.width, r.y0 + (t + h) * r.height)
66
+ print(" ".join(word[4] for word in page.get_text("words", clip=clip)))
67
+ ```
68
+
69
+ Apply the edit and write `<original>-edited.docx`:
70
+
71
+ ```json
72
+ {
73
+ "replace": [{
74
+ "find": "Revenue grew 18.4% year over year.",
75
+ "replace": "Revenue grew 19% year over year."
76
+ }]
77
+ }
78
+ ```
79
+
80
+ ```bash
81
+ "<python>" "<cli>" docx edit --input "report.docx" --spec "edit.json" --out "report-edited.docx"
82
+ ```
83
+
84
+ The report returns `replacements`. `0` means `find` did not match verbatim —
85
+ re-read the paragraph text instead of guessing. Then re-render
86
+ `report-edited.docx` and `read_image` page 1. Note that one longer sentence can
87
+ reflow the rest of the page: check the paragraphs below the edit, not just the
88
+ edited line.
89
+
90
+ ## pdf — convert the region and work on the converted rect
91
+
92
+ `page` is a 1-based page of the PDF itself. `region × page.rect` is the real
93
+ place in the file, so convert once and use the device rect directly.
94
+
95
+ ```python
96
+ import sys, pymupdf
97
+ pdf, page_no, l, t, w, h = sys.argv[1], int(sys.argv[2]), *map(float, sys.argv[3:7])
98
+ page = pymupdf.open(pdf)[page_no - 1]
99
+ r = page.rect
100
+ rect = pymupdf.Rect(r.x0 + l * r.width, r.y0 + t * r.height,
101
+ r.x0 + (l + w) * r.width, r.y0 + (t + h) * r.height)
102
+ print(rect, " ".join(word[4] for word in page.get_text("words", clip=rect)))
103
+ ```
104
+
105
+ Example: page 1, region over the title, `text` "Field report", requirement
106
+ "make the title say Field report (revised), keep the author". The converted rect
107
+ confirms the words under the mark are `Field report`. `imageName`, when present,
108
+ is a screenshot of that region — open it with `read_image` if the text layer is
109
+ ambiguous or the page is scanned.
110
+
111
+ Choose the edit that the helper actually supports:
112
+
113
+ - Metadata, rotation, page order, merge, page removal → `pdf edit`:
114
+
115
+ ```json
116
+ { "metadata": { "title": "Field report (revised)" } }
117
+ ```
118
+
119
+ ```bash
120
+ "<python>" "<cli>" pdf edit --input "report.pdf" --spec "edit.json" --out "report-edited.pdf"
121
+ ```
122
+
123
+ - Text or graphics **on** the page → the helper has no in-place content edit.
124
+ Rebuild the page from a `pdf create` spec using the text from `pdf read`, then
125
+ say plainly that the PDF was rebuilt rather than patched. Do not describe a
126
+ rebuild as a surgical edit.
127
+
128
+ Verify with `<cli> pdf render --input "report-edited.pdf" --out-dir "render"` and
129
+ `read_image render/page-1.png`. `pdf render` uses PyMuPDF and works without
130
+ LibreOffice or Office, so a PDF annotation is never blocked by a missing
131
+ renderer.
132
+
133
+ ## pptx — select the shapes that overlap the region
134
+
135
+ `page` is the rendered PDF page, and the deck renders one slide per page, so
136
+ page N is slide N. `pptx read` returns slide text but **no shape geometry**, so
137
+ compute overlap yourself with python-pptx. Shape boxes and the payload region are
138
+ both fractions of the slide, so the comparison is direct.
139
+
140
+ ```python
141
+ import sys
142
+ from pptx import Presentation
143
+ prs = Presentation(sys.argv[1])
144
+ slide_no, l, t, w, h = int(sys.argv[2]), *map(float, sys.argv[3:7])
145
+ sw, sh = prs.slide_width, prs.slide_height
146
+ for shape in prs.slides[slide_no - 1].shapes:
147
+ x0, y0 = (shape.left or 0) / sw, (shape.top or 0) / sh
148
+ x1, y1 = x0 + (shape.width or 0) / sw, y0 + (shape.height or 0) / sh
149
+ if x1 > l and x0 < l + w and y1 > t and y0 < t + h and shape.has_text_frame:
150
+ print(shape.shape_id, shape.text_frame.text.replace("\n", " | "))
151
+ ```
152
+
153
+ Example: page 2 (slide 2), region over the second bullet, `text` "Renewal 92%",
154
+ requirement "make it 94%". Two shapes overlap the region — the title placeholder
155
+ and the body — and `text` picks the body.
156
+
157
+ Apply with the run-level replacement:
158
+
159
+ ```json
160
+ { "replace": [{ "find": "Renewal 92%", "replace": "Renewal 94%" }] }
161
+ ```
162
+
163
+ ```bash
164
+ "<python>" "<cli>" pptx edit --input "deck.pptx" --spec "edit.json" --out "deck-edited.pptx"
165
+ ```
166
+
167
+ This replacement is not scoped to one slide. Check the returned count, and if
168
+ the same string appeared on other slides, fix those with per-slide `slides[]`
169
+ entries in the same spec. Re-render and `read_image` the page PNG for slide 2.
170
+ If type overflows its shape after the change, shrink the run or shorten the text
171
+ rather than leaving it clipped.
172
+
173
+ ## xlsx — no cell reference, so resolve it yourself and say so
174
+
175
+ xlsx items are always `unmapped: true` with `text: ""`: you get a region, a
176
+ screenshot in `imageName`, and a requirement — no page, no sheet, no A1 range.
177
+ The sheet and cells must be resolved from evidence, never invented.
178
+
179
+ 1. **Look at the screenshot.** `read_image` the attachment named `imageName`.
180
+ Note the visible column headers, row labels and values inside the marked
181
+ region. If there is no screenshot, you only have `region` and
182
+ `requirement` — say so instead of guessing.
183
+ 2. **Dump the workbook.** `"<python>" "<cli>" xlsx read --input "book.xlsx"
184
+ --json-out "book.json"`. Each sheet gives `name`, `dimensions`, `rows` (arrays
185
+ without coordinates) and `formulas`.
186
+ 3. **Resolve the cells.** Match the visible values and headers from step 1
187
+ against the dumped rows, then derive the A1 address from the row/column
188
+ index:
189
+
190
+ ```python
191
+ import json, sys
192
+ from openpyxl.utils import get_column_letter
193
+ data = json.load(open(sys.argv[1], encoding="utf-8"))
194
+ needle = sys.argv[2]
195
+ for sheet in data["sheets"]:
196
+ for r, row in enumerate(sheet["rows"], start=1):
197
+ for c, value in enumerate(row, start=1):
198
+ if isinstance(value, str) and needle in value:
199
+ print(sheet["name"], f"{get_column_letter(c)}{r}", value)
200
+ ```
201
+
202
+ Run it with the runtime python. If several sheets have the same label, or the
203
+ screenshot does not pin the column, ask which one before editing.
204
+
205
+ 4. **State the resolution before editing.** Tell the user which sheet and A1
206
+ range you resolved, e.g. "Revenue is `Q4!B2:C2` (10, 12); formatting those
207
+ cells as currency." Only edit after that, so a wrong guess is visible before
208
+ it touches the file.
209
+
210
+ 5. **Apply and verify.** Example: screenshot shows `Revenue 10 12` and the
211
+ requirement is "show these as currency with 2 decimals"; `xlsx read` finds
212
+ `Revenue` in `Q4!A2` with `10` and `12` beside it:
213
+
214
+ ```json
215
+ {
216
+ "operations": [
217
+ { "action": "style", "sheet": "Q4", "cells": { "B2:C2": { "number_format": "#,##0.00" } } }
218
+ ]
219
+ }
220
+ ```
221
+
222
+ ```bash
223
+ "<python>" "<cli>" xlsx edit --input "book.xlsx" --spec "edit.json" --out "book-edited.xlsx"
224
+ ```
225
+
226
+ A `style` operation applies to the whole A1:B2 range in one go. Use
227
+ `set_cells` to write values, `append_rows` to add rows, `add_sheet` /
228
+ `delete_sheet` / `rename_sheet` for structure. Then read the edited workbook
229
+ again to confirm the values.
230
+
231
+ Forbidden, because both produce plausible-looking edits in the wrong place:
232
+
233
+ - converting `region` pixels into a cell address — the spreadsheet preview has
234
+ its own zoom and scroll;
235
+ - naming a sheet the screenshot does not show or `xlsx read` cannot match.
236
+
237
+ Visual confirmation needs `xlsx render`, which requires LibreOffice or Office;
238
+ without one, say the change is verified structurally but not visually. Formula
239
+ results stay uncalculated unless `xlsx recalc` runs on Windows with Microsoft
240
+ Excel; do not present openpyxl's empty cache as a computed value.
241
+
242
+ ## When to stop and ask
243
+
244
+ Ask one short question, listing what you did find, when:
245
+
246
+ - the region covers text that exists on several pages and `text` is empty;
247
+ - two shapes on a slide both match `text`;
248
+ - the xlsx screenshot does not identify the sheet, or the visible values match
249
+ more than one row or column;
250
+ - the requirement needs an operation the helper does not support (page-content
251
+ editing in PDF, animation, comments) — offer the rebuild path instead of a
252
+ silent partial change.