dsh-office-kit 0.1.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 (198) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/LICENSE.md +21 -21
  3. package/README.md +108 -106
  4. package/assets/skills/excel-live-control/SKILL.md +234 -0
  5. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  6. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  7. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  8. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  9. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  10. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  11. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  12. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  13. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  14. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  15. package/assets/skills/office-documents/SKILL.md +90 -77
  16. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  17. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  18. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  19. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  20. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  21. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  22. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  23. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  24. package/assets/skills/office-documents/render_docx.py +92 -0
  25. package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
  26. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
  27. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
  28. package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
  29. package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
  30. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
  31. package/assets/skills/office-documents/scripts/comments_add.py +136 -0
  32. package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
  33. package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
  34. package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
  35. package/assets/skills/office-documents/scripts/content_controls.py +192 -0
  36. package/assets/skills/office-documents/scripts/core_properties.py +124 -0
  37. package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
  38. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
  39. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
  40. package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
  41. package/assets/skills/office-documents/scripts/fields_report.py +59 -0
  42. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
  43. package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
  44. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
  45. package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
  46. package/assets/skills/office-documents/scripts/images_audit.py +80 -0
  47. package/assets/skills/office-documents/scripts/insert_note.py +126 -0
  48. package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
  49. package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
  50. package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
  51. package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
  52. package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
  53. package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
  54. package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
  55. package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
  56. package/assets/skills/office-documents/scripts/replace_text.py +154 -0
  57. package/assets/skills/office-documents/scripts/section_audit.py +91 -0
  58. package/assets/skills/office-documents/scripts/set_protection.py +92 -0
  59. package/assets/skills/office-documents/scripts/style_lint.py +111 -0
  60. package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
  61. package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
  62. package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
  63. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
  64. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
  65. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  66. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  67. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  68. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  69. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  70. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  71. package/assets/skills/office-documents/tasks/equations.md +59 -0
  72. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  73. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  74. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  75. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  76. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  77. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  78. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  79. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  80. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  81. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  82. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  83. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  84. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  85. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  86. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  87. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  88. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  89. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  90. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  91. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  92. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  93. package/assets/skills/office-pdf/SKILL.md +73 -56
  94. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  95. package/assets/skills/office-pdf/references/geometry.md +44 -0
  96. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  97. package/assets/skills/office-pdf/references/limits.md +58 -0
  98. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  99. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  100. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  101. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  102. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  103. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  104. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  105. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  106. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  107. package/assets/skills/office-pdf/tasks/create.md +75 -0
  108. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  109. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  110. package/assets/skills/office-pdf/tasks/read.md +62 -0
  111. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  112. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  113. package/assets/skills/office-presentations/SKILL.md +141 -87
  114. package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
  115. package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
  116. package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
  118. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
  119. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
  120. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
  121. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
  122. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
  123. package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
  124. package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
  125. package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
  126. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
  127. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  128. package/assets/skills/office-presentations/recipes/README.md +71 -0
  129. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  130. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  131. package/assets/skills/office-presentations/recipes/images.md +80 -0
  132. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  133. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  134. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  135. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  136. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  137. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  138. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  139. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  140. package/assets/skills/office-presentations/references/finalization.md +63 -0
  141. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  142. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  143. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  144. package/assets/skills/office-presentations/references/templates.md +76 -0
  145. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  146. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  147. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  148. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  149. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  150. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  151. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  152. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
  153. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
  154. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
  155. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
  156. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
  157. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  158. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
  159. package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
  160. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
  161. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
  162. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
  163. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
  164. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
  165. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
  166. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
  167. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  168. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  169. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  170. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  171. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  172. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  173. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  174. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  175. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  176. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  177. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  178. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  179. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  180. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  181. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  182. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  183. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  184. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  185. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  186. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  187. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  188. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  189. package/assets/skills/shared/annotation-recipes.md +252 -0
  190. package/assets/skills/shared/check_runtime.py +253 -0
  191. package/assets/skills/shared/common-workflow.md +139 -0
  192. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  193. package/dist/client.js +4 -4
  194. package/dist/client.js.map +1 -1
  195. package/dist/index.js +239 -89
  196. package/package.json +146 -149
  197. package/postinstall.mjs +92 -82
  198. package/python/office_cli.py +171 -246
@@ -0,0 +1,93 @@
1
+ # Task: edit an existing deck
2
+
3
+ Editing is mutation, and mutation lives in this pack's tools, not in the helper
4
+ CLI: `container_tools/replace_text.py`, `add_slides.py`, `core_properties.py`,
5
+ plus the recipes for everything structural.
6
+
7
+ **Never write over the original.** Every tool defaults to
8
+ `<original>-edited.pptx`; keep that default unless the user names a path.
9
+
10
+ ## 1. Understand the file first
11
+
12
+ ```bash
13
+ "<python>" "<cli>" pptx read --input deck.pptx
14
+ "<python>" "<skill>/container_tools/inspect_presentation_layout_geometry.py" --input deck.pptx
15
+ ```
16
+
17
+ `pptx read` gives titles, text, tables and chart data per slide. The geometry
18
+ gate gives every shape's rect in inches, which is what you need to decide
19
+ whether a new block fits.
20
+
21
+ ## 2. Text changes: run-level replace
22
+
23
+ ```bash
24
+ "<python>" "<skill>/container_tools/replace_text.py" --input deck.pptx \
25
+ --find "Q4 2025" --replace "Q4 2026" --out deck-edited.pptx
26
+ ```
27
+
28
+ Batch form:
29
+
30
+ ```json
31
+ {
32
+ "replacements": [
33
+ { "find": "Q4 2025", "replace": "Q4 2026" },
34
+ { "find": "€", "replace": "EUR ", "match_case": true },
35
+ { "find": "draft", "replace": "final", "slide": 7 }
36
+ ]
37
+ }
38
+ ```
39
+
40
+ ```bash
41
+ "<python>" "<skill>/container_tools/replace_text.py" --input deck.pptx --spec replacements.json
42
+ ```
43
+
44
+ Semantics that matter: a pattern only matches **inside a single run**. Text that
45
+ PowerPoint split for formatting ("**Q4** 2025") will not match "Q4 2025" and the
46
+ report lists it under `unmatched`. When a pattern cannot match, that is the
47
+ signal to edit that paragraph with a run-level snippet
48
+ ([rich text runs](../recipes/rich-text-runs.md)) rather than widening the match.
49
+
50
+ Exit code 1 means a pattern matched nothing; the file is still written, so read
51
+ `unmatched` before delivering.
52
+
53
+ ## 3. Adding slides
54
+
55
+ ```bash
56
+ "<python>" "<skill>/container_tools/add_slides.py" --input deck.pptx --spec extra-slides.json
57
+ ```
58
+
59
+ The spec is the same slide object as `pptx create` (`layout`, `title`,
60
+ `subtitle`, `bullets`, `table`, `chart`, `image`). For anything outside that
61
+ vocabulary, add the slide with `add_slides.py` and then place content with a
62
+ recipe.
63
+
64
+ ## 4. Metadata
65
+
66
+ ```bash
67
+ "<python>" "<skill>/container_tools/core_properties.py" --input deck.pptx \
68
+ --title "Automation Review" --author "Warehouse Ops" --subject "FY2026 capital plan"
69
+ ```
70
+
71
+ ## 5. Structural changes
72
+
73
+ | Change | Route |
74
+ |---|---|
75
+ | Move or resize a shape | recipe: set `.left/.top/.width/.height` on the shape from `slide.shapes` |
76
+ | Restyle a family | [masters and layouts](../recipes/master-and-layouts.md) |
77
+ | Add notes | [speaker notes](../recipes/speaker-notes.md) |
78
+ | Replace a chart's data | [native charts](../recipes/native-charts.md) — edit the series, do not paste a picture |
79
+ | Fix a table's total | [tables](../recipes/tables.md), then the arithmetic gate |
80
+ | Delete a slide | `prs.slides._sldIdLst.remove(...)` after `prs.part.drop_rel(rId)` |
81
+
82
+ ## 6. Verify before delivering
83
+
84
+ Deleting or inserting a slide commonly breaks nothing visible and everything
85
+ relational:
86
+
87
+ ```bash
88
+ "<python>" "<skill>/container_tools/inspect_presentation_package_integrity.py" --input deck-edited.pptx
89
+ "<python>" "<skill>/container_tools/render_slides.py" --input deck-edited.pptx --out-dir preview
90
+ ```
91
+
92
+ Then look at the changed pages. A text replacement that pushes a title onto a
93
+ third line is a layout change, not a text change.
@@ -0,0 +1,107 @@
1
+ # Task: inspect and verify a deck
2
+
3
+ Verification is reading the artefact back and looking at it. The tools below
4
+ print one JSON object each; `passed` is the decision.
5
+
6
+ ## 1. What is in the file
7
+
8
+ ```bash
9
+ "<python>" "<cli>" pptx read --input deck.pptx
10
+ ```
11
+
12
+ Per slide: title, all text, tables as rows, charts with type/title/categories/
13
+ series, image count. This is the cheapest sanity check that the content you
14
+ meant to write is the content that was written.
15
+
16
+ ## 2. Is the package coherent
17
+
18
+ ```bash
19
+ "<python>" "<skill>/container_tools/inspect_presentation_package_integrity.py" --input deck.pptx
20
+ ```
21
+
22
+ Checks required parts, slide/master presence, dangling relationships, XML that
23
+ does not parse, slide count drift and duplicate parts, then reports part counts
24
+ (`slides`, `layouts`, `masters`, `charts`, `notesSlides`, `media`,
25
+ `embeddings`). A chart with an `embeddings` count of zero has lost its data.
26
+
27
+ ## 3. Does the layout hold
28
+
29
+ ```bash
30
+ "<python>" "<skill>/container_tools/inspect_presentation_layout_geometry.py" --input deck.pptx --json-out geometry.json
31
+ ```
32
+
33
+ `fail` findings (block a delivery): shape outside the slide, degenerate size,
34
+ text that cannot fit its frame, table outside the slide, chart or table covered
35
+ by another shape. `review` findings (read them, then decide): text overlap,
36
+ empty placeholder, title under 28 pt, body under 14 pt, heading ending in a full
37
+ stop, connector across text, a chart duplicating a table's labels.
38
+
39
+ Add `--strict` to make review findings blocking for a formal deck.
40
+
41
+ ## 4. Are the numbers real
42
+
43
+ ```bash
44
+ "<python>" "<skill>/container_tools/native_chart_title_gate.py" --input deck.pptx
45
+ "<python>" "<skill>/container_tools/native_quantitative_chart_gate.py" --input deck.pptx --all-slides
46
+ "<python>" "<skill>/container_tools/native_table_arithmetic_gate.py" --input deck.pptx --contracts totals.json
47
+ ```
48
+
49
+ - Chart titles: fails on leftover "Chart Title" or an empty title, unless the
50
+ chart is explicitly suppressed.
51
+ - Quantitative charts: every declared chart must be a real chart part with a
52
+ readable series, and in the default `portable` mode must carry its embedded
53
+ workbook.
54
+ - Table arithmetic: only declared totals are enforced, compared against the
55
+ displayed rounding; columns whose header is not additive (unit price, ratio,
56
+ percent) are skipped rather than summed wrongly.
57
+
58
+ `totals.json` names the contract explicitly:
59
+
60
+ ```json
61
+ { "contracts": [{ "slide": 4, "table": 1, "total_row": 6, "columns": [1, 2] }] }
62
+ ```
63
+
64
+ ## 5. Rendering and looking
65
+
66
+ ```bash
67
+ "<python>" "<skill>/container_tools/render_slides.py" --input deck.pptx --out-dir preview
68
+ ```
69
+
70
+ Writes `slide-NN.png` plus `render.json`. Then `read_image` every PNG — an
71
+ unlooked-at render is not verification.
72
+
73
+ To see the whole deck at once:
74
+
75
+ ```bash
76
+ "<python>" "<skill>/template_following_scripts/make_contact_sheet.py" --dir preview --out sheet.png --columns 3
77
+ ```
78
+
79
+ or `"<skill>/container_tools/create_montage.py` with an explicit file list.
80
+
81
+ Look for: clipped text, overlapping blocks, a title that reflowed, a chart whose
82
+ point is invisible at thumbnail size, a photograph stretched, a slide with no
83
+ title.
84
+
85
+ ## 6. When nothing can render
86
+
87
+ With no Microsoft Office, WPS Office, or LibreOffice installed
88
+ (`office_status` → `renderers.kingsoft_wps` / `microsoft_office` / `libreoffice`),
89
+ `render_slides.py` falls back to `--engine draft`, a PyMuPDF geometry preview. It
90
+ shows placement, overflow and overlap — not typography or theme chrome. Say so
91
+ when reporting: "verified as a geometry preview" is honest, "looks great" from an
92
+ approximate render is not.
93
+
94
+ If even the preview cannot run, the delivery is unverified: report the failing
95
+ command, the file's slide count from `pptx read`, and the gate results, and tell
96
+ the user which renderer to install for a visual check.
97
+
98
+ ## Gate summary
99
+
100
+ | Gate | Command | Blocking |
101
+ |---|---|---|
102
+ | Package integrity | `inspect_presentation_package_integrity.py` | yes |
103
+ | Layout geometry | `inspect_presentation_layout_geometry.py` | `fail` always, `review` with `--strict` |
104
+ | Chart titles | `native_chart_title_gate.py` | yes |
105
+ | Chart data | `native_quantitative_chart_gate.py` | yes for declared slides |
106
+ | Table arithmetic | `native_table_arithmetic_gate.py` | yes for declared contracts |
107
+ | Template fidelity | `template_source_fidelity_gate.py` | yes when requirements are supplied |
@@ -0,0 +1,132 @@
1
+ # Task: follow a supplied template
2
+
3
+ Use this when the user hands over a `.pptx` and says "use our template" or "match
4
+ this deck". The design is already decided; your job is to put content into its
5
+ own slides.
6
+
7
+ ## 1. Inspect
8
+
9
+ ```bash
10
+ "<python>" "<skill>/template_following_scripts/inspect_template_deck.py" --template brand.pptx --out-dir template-inspect
11
+ ```
12
+
13
+ Outputs:
14
+
15
+ - `template-inspect.ndjson` — one JSON line per slide: layout name, layout
16
+ placeholders, slide placeholders (index, kind, rect in inches, text), shape
17
+ counts, text excerpt, whether the slide has full-bleed background art.
18
+ - `template-manifest.json` — slide size, aspect ratio, layout names, theme font
19
+ names, and `backgroundArtSlides`.
20
+
21
+ Read `backgroundArtSlides` before adding anything: those slides already carry
22
+ artwork, so a new opaque panel will cover it.
23
+
24
+ ## 2. Plan
25
+
26
+ Write `frame-map.json`:
27
+
28
+ ```json
29
+ {
30
+ "template": "brand.pptx",
31
+ "newPrimitives": { "allowed": false, "justification": "" },
32
+ "outputSlides": [
33
+ {
34
+ "slide": 1,
35
+ "mode": "duplicate-slide",
36
+ "sourceSlide": 1,
37
+ "elements": [
38
+ {
39
+ "elementId": "title",
40
+ "action": "fill-placeholder",
41
+ "target": { "placeholder": "title", "text": "Automation Review" }
42
+ },
43
+ {
44
+ "elementId": "subtitle",
45
+ "action": "fill-placeholder",
46
+ "target": { "placeholder": "subtitle", "text": "Warehouse network · FY2026" }
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "slide": 2,
52
+ "mode": "duplicate-slide",
53
+ "sourceSlide": 3,
54
+ "elements": [
55
+ {
56
+ "elementId": "title",
57
+ "action": "rewrite",
58
+ "target": { "placeholder": "title", "text": "Throughput rose while touches fell" }
59
+ }
60
+ ]
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+
66
+ Allowed actions: `rewrite`, `rewrite-and-reposition`, `replace`, `delete`,
67
+ `keep`, `fill-placeholder`. `target.zone` is `[x, y, w, h]` in 0–1 slide
68
+ fractions, used with `rewrite-and-reposition`. Elements whose `role` is
69
+ `brand`, `logo`, `bumper`, `divider`, `section`, `chrome` or `footer` must stay
70
+ `keep` — they are the template.
71
+
72
+ Adding new shapes is opt-in: set `newPrimitives.allowed` and write a
73
+ justification. The validator rejects shapes added without it, because "one extra
74
+ box" is how a branded deck stops matching brand.
75
+
76
+ ## 3. Validate
77
+
78
+ ```bash
79
+ "<python>" "<skill>/template_following_scripts/validate_template_plan.py" --plan frame-map.json
80
+ ```
81
+
82
+ Rejects: unknown action, preserved role edited, zone outside 0–1, non-sequential
83
+ slide numbers, missing justification for new primitives, and duplicate source
84
+ slides (a warning — it means two output slides share artwork, usually fine for a
85
+ repeated section divider, usually a copy-paste mistake otherwise).
86
+
87
+ ## 4. Build
88
+
89
+ ```bash
90
+ "<python>" "<skill>/template_following_scripts/prepare_template_starter_deck.py" --template brand.pptx --plan frame-map.json --out starter.pptx
91
+ ```
92
+
93
+ The starter deck keeps only the mapped source slides, in plan order, and applies
94
+ the actions. It refuses duplicate source slides (it reuses slides rather than
95
+ cloning parts). The report lists applied actions and any placeholder left empty.
96
+
97
+ From here, extend with `container_tools/add_slides.py` or the recipes when the
98
+ template has no family for a slide you need.
99
+
100
+ ## 5. Check fidelity
101
+
102
+ ```bash
103
+ "<python>" "<skill>/template_following_scripts/check_template_fidelity.py" --template brand.pptx --final starter.pptx --plan frame-map.json
104
+ "<python>" "<skill>/container_tools/template_source_fidelity_gate.py" --template brand.pptx --final starter.pptx --requirements fidelity.json
105
+ ```
106
+
107
+ `fidelity.json`:
108
+
109
+ ```json
110
+ {
111
+ "reference_slide_indices": [1, 3],
112
+ "mapping": { "1": 1, "2": 3 },
113
+ "minimum_coverage_ratio": 1.0,
114
+ "require_exact_dimensions": true,
115
+ "require_placeholder_geometry": true,
116
+ "require_photographic_background": false,
117
+ "geometry_tolerance_emu": 9144
118
+ }
119
+ ```
120
+
121
+ Both print `passed`. With no requirements, the fidelity gate is a deliberate
122
+ no-op pass — pass a requirements file or you have not checked anything.
123
+
124
+ ## 6. Look at it
125
+
126
+ ```bash
127
+ "<python>" "<skill>/container_tools/render_slides.py" --input starter.pptx --out-dir starter-preview
128
+ ```
129
+
130
+ Compare against the source slides. A filled placeholder that overflows, a title
131
+ in the wrong family, or a panel over the corner logo is exactly what this step
132
+ exists to catch.
@@ -0,0 +1,176 @@
1
+ """Catch the traces an authoring pass leaves in a template-based deck.
2
+
3
+ Purpose: a deck can keep the template's layouts and still be wrong — a
4
+ "Click to add title" prompt left in place, the template's instruction text
5
+ still sitting in a body, or an opaque panel dropped exactly over the artwork
6
+ the design relies on. None of those are visible in a plan; all of them are
7
+ cheap to detect afterwards.
8
+
9
+ Exit codes: 0 clean, 1 findings, 2 unreadable input.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
20
+
21
+ from ooxml_helpers import box, has_visible_text, is_picture, overlap_ratio, text_of # noqa: E402
22
+
23
+ PLACEHOLDER_PATTERNS = [
24
+ re.compile(pattern, re.IGNORECASE)
25
+ for pattern in (
26
+ r"^slide number$",
27
+ r"^date$",
28
+ r"^footer$",
29
+ r"^click to add (title|subtitle|text|body)",
30
+ r"^(title|subtitle|name|text|body) goes here$",
31
+ r"^lorem ipsum\b",
32
+ r"\btemplate instruction\b",
33
+ r"^\[.*\]$",
34
+ )
35
+ ]
36
+ ART_COVER_MIN_RATIO = 0.9
37
+
38
+
39
+ def _has_solid_fill(shape) -> bool:
40
+ try:
41
+ from pptx.enum.dml import MSO_FILL
42
+
43
+ return shape.fill.type == MSO_FILL.SOLID
44
+ except Exception: # noqa: BLE001 - shapes without a fill API are never opaque panels
45
+ return False
46
+
47
+
48
+ def _slide_size_in(slide) -> tuple[float, float]:
49
+ presentation = slide.part.package.presentation_part.presentation
50
+ return presentation.slide_width / 914400, presentation.slide_height / 914400
51
+
52
+
53
+ def check(template_path: str | Path, final_path: str | Path, plan_path: str | Path | None = None) -> dict:
54
+ from pptx import Presentation
55
+
56
+ template_file = Path(template_path)
57
+ final_file = Path(final_path)
58
+ for candidate in (template_file, final_file):
59
+ if not candidate.is_file():
60
+ return {"gate": "template-authoring-fidelity", "passed": False, "findings": [{"kind": "missing_file", "detail": str(candidate)}]}
61
+
62
+ template = Presentation(str(template_file))
63
+ final = Presentation(str(final_file))
64
+ template_slides = list(template.slides)
65
+ final_slides = list(final.slides)
66
+ plan = {}
67
+ if plan_path and Path(plan_path).is_file():
68
+ plan = json.loads(Path(plan_path).read_text(encoding="utf-8"))
69
+
70
+ findings: list[dict] = []
71
+ mapping: list[tuple[int, int]] = []
72
+ for entry in plan.get("outputSlides") or []:
73
+ mapping.append((int(entry.get("slide", len(mapping) + 1)), int(entry.get("sourceSlide", 0))))
74
+ if not mapping:
75
+ mapping = [(index, index) for index in range(1, min(len(template_slides), len(final_slides)) + 1)]
76
+
77
+ for output_index, source_index in mapping:
78
+ if not (1 <= output_index <= len(final_slides) and 1 <= source_index <= len(template_slides)):
79
+ findings.append({"kind": "mapping_out_of_range", "severity": "fail", "message": f"mapping {output_index}->{source_index} is outside the deck"})
80
+ continue
81
+ output_slide = final_slides[output_index - 1]
82
+ source_slide = template_slides[source_index - 1]
83
+
84
+ if output_slide.slide_layout.name != source_slide.slide_layout.name:
85
+ findings.append(
86
+ {
87
+ "kind": "layout_drift",
88
+ "severity": "review",
89
+ "slide": output_index,
90
+ "message": f"layout is '{output_slide.slide_layout.name}', template slide used '{source_slide.slide_layout.name}'",
91
+ }
92
+ )
93
+
94
+ for shape in output_slide.shapes:
95
+ if not has_visible_text(shape):
96
+ continue
97
+ text = text_of(shape).strip()
98
+ for pattern in PLACEHOLDER_PATTERNS:
99
+ if pattern.search(text):
100
+ findings.append(
101
+ {
102
+ "kind": "leftover_placeholder_text",
103
+ "severity": "fail",
104
+ "slide": output_index,
105
+ "shape": shape.name,
106
+ "text": text[:80],
107
+ "message": "template prompt text is still on the slide",
108
+ }
109
+ )
110
+ break
111
+ if not text and shape.is_placeholder:
112
+ findings.append({"kind": "empty_placeholder", "severity": "review", "slide": output_index, "shape": shape.name, "message": "placeholder was left empty"})
113
+
114
+ inherited_art = [box(shape) for shape in source_slide.shapes if is_picture(shape) and box(shape)]
115
+ slide_w, slide_h = _slide_size_in(source_slide)
116
+ full_bleed = {
117
+ index
118
+ for index, art in enumerate(inherited_art)
119
+ if art[0] <= 0.02 and art[1] <= 0.02 and art[2] >= slide_w * 0.98 and art[3] >= slide_h * 0.98
120
+ }
121
+ for shape in output_slide.shapes:
122
+ # only an opaque panel is a problem: text over a background image is the template working
123
+ if is_picture(shape) or not _has_solid_fill(shape):
124
+ continue
125
+ geometry = box(shape)
126
+ if geometry is None:
127
+ continue
128
+ for art_index, art in enumerate(inherited_art):
129
+ if art_index in full_bleed:
130
+ continue
131
+ ratio = overlap_ratio(geometry, art)
132
+ if ratio >= ART_COVER_MIN_RATIO:
133
+ findings.append(
134
+ {
135
+ "kind": "template_art_covered",
136
+ "severity": "fail",
137
+ "slide": output_index,
138
+ "shape": shape.name,
139
+ "overlap": round(ratio, 2),
140
+ "message": "a text or table block sits on top of the template artwork",
141
+ }
142
+ )
143
+ break
144
+
145
+ blocking = [item for item in findings if item["severity"] == "fail"]
146
+ return {
147
+ "gate": "template-authoring-fidelity",
148
+ "template": str(template_file.resolve()),
149
+ "final": str(final_file.resolve()),
150
+ "plan": str(plan_path) if plan_path else None,
151
+ "mappedSlides": len(mapping),
152
+ "findings": findings,
153
+ "passed": not blocking,
154
+ }
155
+
156
+
157
+ def main(argv: list[str] | None = None) -> int:
158
+ import argparse
159
+
160
+ parser = argparse.ArgumentParser(description="Find leftover template prompts, layout drift and covered artwork.")
161
+ parser.add_argument("--template", required=True, help="the template .pptx")
162
+ parser.add_argument("--final", required=True, help="the finished .pptx")
163
+ parser.add_argument("--plan", help="optional template-frame-map.json for the slide mapping")
164
+ parser.add_argument("--json-out", help="also write the report here")
165
+ args = parser.parse_args(argv)
166
+
167
+ report = check(args.template, args.final, args.plan)
168
+ if args.json_out:
169
+ Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
170
+ json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
171
+ sys.stdout.write("\n")
172
+ return 0 if report["passed"] else 1
173
+
174
+
175
+ if __name__ == "__main__":
176
+ raise SystemExit(main())
@@ -0,0 +1,154 @@
1
+ """Read a template deck into records an author can plan against.
2
+
3
+ Purpose: matching a supplied deck starts with knowing what it actually is —
4
+ slide size, which layouts exist, what each slide inherits, where the title and
5
+ body sit, which elements repeat on every slide, what type and colours the theme
6
+ carries. Guessing from memory of "a normal 16:9 deck" is how a template gets
7
+ flattened.
8
+
9
+ Writes:
10
+ <out-dir>/template-inspect.ndjson one JSON record per slide
11
+ <out-dir>/template-manifest.json deck level summary
12
+
13
+ Exit codes: 0 written, 2 unreadable input.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
23
+
24
+ from ooxml_helpers import box, has_visible_text, is_chart, is_picture, is_table, placeholder_kind, slide_size_in, text_of # noqa: E402
25
+
26
+
27
+ def _theme(presentation) -> dict:
28
+ fonts: set[str] = set()
29
+ fills: set[str] = set()
30
+ for master in presentation.slide_masters:
31
+ for layout in master.slide_layouts:
32
+ for shape in layout.shapes:
33
+ if has_visible_text(shape):
34
+ for paragraph in shape.text_frame.paragraphs:
35
+ for run in paragraph.runs:
36
+ if run.font.name:
37
+ fonts.add(run.font.name)
38
+ for slide in presentation.slides:
39
+ for shape in slide.shapes:
40
+ if has_visible_text(shape):
41
+ for paragraph in shape.text_frame.paragraphs:
42
+ for run in paragraph.runs:
43
+ if run.font.name:
44
+ fonts.add(run.font.name)
45
+ return {"fontsSeen": sorted(fonts), "colourSourcesNote": "check layouts and masters in PowerPoint for exact theme colours"}
46
+
47
+
48
+ def inspect_template(template_path: str | Path, out_dir: str | Path) -> dict:
49
+ from pptx import Presentation
50
+
51
+ source = Path(template_path)
52
+ if not source.is_file():
53
+ return {"ok": False, "error": {"code": "input_missing", "message": f"no such file: {source}"}}
54
+ presentation = Presentation(str(source))
55
+ slide_w, slide_h = slide_size_in(presentation)
56
+ destination = Path(out_dir)
57
+ destination.mkdir(parents=True, exist_ok=True)
58
+
59
+ records: list[dict] = []
60
+ for index, slide in enumerate(presentation.slides, start=1):
61
+ layout = slide.slide_layout
62
+ placeholders = []
63
+ for shape in slide.shapes:
64
+ if not shape.is_placeholder:
65
+ continue
66
+ placeholders.append(
67
+ {
68
+ "idx": shape.placeholder_format.idx,
69
+ "kind": placeholder_kind(shape) or "",
70
+ "type": int(shape.placeholder_format.type) if shape.placeholder_format.type is not None else None,
71
+ "bboxIn": [round(value, 3) for value in (box(shape) or (0, 0, 0, 0))],
72
+ "text": text_of(shape)[:120],
73
+ }
74
+ )
75
+ layout_placeholders = [
76
+ {
77
+ "idx": shape.placeholder_format.idx,
78
+ "kind": placeholder_kind(shape) or "",
79
+ "bboxIn": [round(value, 3) for value in (box(shape) or (0, 0, 0, 0))],
80
+ }
81
+ for shape in layout.shapes
82
+ if shape.is_placeholder
83
+ ]
84
+ records.append(
85
+ {
86
+ "slide": index,
87
+ "layout": layout.name,
88
+ "layoutIndex": list(presentation.slide_layouts).index(layout) if layout in list(presentation.slide_layouts) else None,
89
+ "layoutPlaceholders": layout_placeholders,
90
+ "placeholders": placeholders,
91
+ "shapes": len(slide.shapes),
92
+ "pictures": sum(1 for shape in slide.shapes if is_picture(shape)),
93
+ "charts": sum(1 for shape in slide.shapes if is_chart(shape)),
94
+ "tables": sum(1 for shape in slide.shapes if is_table(shape)),
95
+ "textExcerpt": " | ".join(text_of(shape)[:80] for shape in slide.shapes if has_visible_text(shape))[:240],
96
+ "backgroundPicture": any(
97
+ is_picture(shape) and box(shape) and box(shape)[2] >= slide_w * 0.95 and box(shape)[3] >= slide_h * 0.95
98
+ for shape in slide.shapes
99
+ ),
100
+ }
101
+ )
102
+
103
+ manifest = {
104
+ "template": str(source.resolve()),
105
+ "slideSizeIn": [round(slide_w, 3), round(slide_h, 3)],
106
+ "aspect": "16:9" if abs(slide_w / slide_h - 16 / 9) < 0.02 else "4:3" if abs(slide_w / slide_h - 4 / 3) < 0.02 else "custom",
107
+ "slideCount": len(presentation.slides),
108
+ "layouts": [layout.name for layout in presentation.slide_layouts],
109
+ "masters": len(presentation.slide_masters),
110
+ "slides": [{key: record[key] for key in ("slide", "layout", "shapes", "backgroundPicture")} for record in records],
111
+ "theme": _theme(presentation),
112
+ }
113
+
114
+ ndjson = destination / "template-inspect.ndjson"
115
+ ndjson.write_text("\n".join(json.dumps(record, ensure_ascii=False) for record in records) + "\n", encoding="utf-8")
116
+ manifest_path = destination / "template-manifest.json"
117
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
118
+
119
+ return {
120
+ "ok": True,
121
+ "template": str(source.resolve()),
122
+ "records": len(records),
123
+ "ndjson": str(ndjson.resolve()),
124
+ "manifest": str(manifest_path.resolve()),
125
+ "summary": {
126
+ "slideCount": manifest["slideCount"],
127
+ "slideSizeIn": manifest["slideSizeIn"],
128
+ "aspect": manifest["aspect"],
129
+ "layouts": manifest["layouts"],
130
+ "backgroundArtSlides": [record["slide"] for record in records if record["backgroundPicture"]],
131
+ },
132
+ }
133
+
134
+
135
+ def main(argv: list[str] | None = None) -> int:
136
+ import argparse
137
+
138
+ parser = argparse.ArgumentParser(description="Summarise a template deck: layouts, placeholders, geometry, theme.")
139
+ parser.add_argument("template", nargs="?", help="path to the template .pptx")
140
+ parser.add_argument("--template", dest="template_opt", help="path to the template .pptx (same as the positional)")
141
+ parser.add_argument("--out-dir", required=True, help="directory for template-inspect.ndjson and template-manifest.json")
142
+ args = parser.parse_args(argv)
143
+ template = args.template_opt or args.template
144
+ if not template:
145
+ parser.error("a template .pptx path is required (positional or --template)")
146
+
147
+ report = inspect_template(template, args.out_dir)
148
+ json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
149
+ sys.stdout.write("\n")
150
+ return 0 if report.get("ok") else 2
151
+
152
+
153
+ if __name__ == "__main__":
154
+ raise SystemExit(main())