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,229 @@
1
+ """Compare a finished deck against the template it claims to follow.
2
+
3
+ Purpose: "it looks similar" is not fidelity. This gate takes explicit
4
+ requirements — which source slides must be represented, how close the slide
5
+ size and placeholder geometry must be, whether a photographic background must
6
+ survive — and answers yes or no. No requirements supplied means nothing to
7
+ enforce, so the gate passes neutrally instead of inventing a standard.
8
+
9
+ Requirements file:
10
+ {
11
+ "reference_slide_indices": [1, 3, 4],
12
+ "mapping": {"1": 1, "2": 3, "3": 4},
13
+ "minimum_coverage_ratio": 1.0,
14
+ "require_exact_dimensions": true,
15
+ "require_placeholder_geometry": true,
16
+ "require_photographic_background": false,
17
+ "geometry_tolerance_emu": 9144
18
+ }
19
+
20
+ Exit codes: 0 pass, 1 fail, 2 unreadable input.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
30
+
31
+ from ooxml_helpers import box, is_picture, slide_size_in # noqa: E402
32
+
33
+ EMU_PER_INCH = 914400
34
+ DEFAULT_TOLERANCE_EMU = 9144 # 0.01 in
35
+
36
+
37
+ def _placeholder_map(slide) -> dict[tuple[int, str], tuple[float, float, float, float]]:
38
+ entries: dict[tuple[int, str], tuple[float, float, float, float]] = {}
39
+ for shape in slide.shapes:
40
+ if not shape.is_placeholder:
41
+ continue
42
+ geometry = box(shape)
43
+ if geometry is None:
44
+ continue
45
+ entries[(shape.placeholder_format.idx, shape.name)] = geometry
46
+ return entries
47
+
48
+
49
+ def _background_picture(slide, slide_w: float, slide_h: float) -> bool:
50
+ for shape in slide.shapes:
51
+ if not is_picture(shape):
52
+ continue
53
+ geometry = box(shape)
54
+ if geometry is None:
55
+ continue
56
+ left, top, width, height = geometry
57
+ if left <= 0.02 and top <= 0.02 and width >= slide_w * 0.98 and height >= slide_h * 0.98:
58
+ return True
59
+ return False
60
+
61
+
62
+ def check_fidelity(template_path: str | Path, final_path: str | Path, requirements: dict | None) -> dict:
63
+ from pptx import Presentation
64
+
65
+ requirements = requirements or {}
66
+ template_file = Path(template_path)
67
+ final_file = Path(final_path)
68
+ for candidate in (template_file, final_file):
69
+ if not candidate.is_file():
70
+ return {"gate": "template-source-fidelity", "passed": False, "findings": [{"kind": "missing_file", "detail": str(candidate)}]}
71
+
72
+ if not requirements:
73
+ return {
74
+ "gate": "template-source-fidelity",
75
+ "passed": True,
76
+ "neutral": True,
77
+ "note": "no fidelity requirements were supplied, so nothing was enforced",
78
+ "findings": [],
79
+ }
80
+
81
+ template = Presentation(str(template_file))
82
+ final = Presentation(str(final_file))
83
+ template_w, template_h = slide_size_in(template)
84
+ final_w, final_h = slide_size_in(final)
85
+ tolerance_in = float(requirements.get("geometry_tolerance_emu", DEFAULT_TOLERANCE_EMU)) / EMU_PER_INCH
86
+ findings: list[dict] = []
87
+ checks: list[dict] = []
88
+
89
+ if requirements.get("require_exact_dimensions", True):
90
+ same = abs(template_w - final_w) <= tolerance_in and abs(template_h - final_h) <= tolerance_in
91
+ checks.append({"check": "dimensions", "passed": same, "template": [template_w, template_h], "final": [final_w, final_h]})
92
+ if not same:
93
+ findings.append(
94
+ {
95
+ "kind": "dimension_mismatch",
96
+ "severity": "fail",
97
+ "message": f"final deck is {final_w:.2f}x{final_h:.2f} in, template is {template_w:.2f}x{template_h:.2f} in",
98
+ }
99
+ )
100
+
101
+ template_slides = list(template.slides)
102
+ final_slides = list(final.slides)
103
+ mapping = requirements.get("mapping") or {}
104
+ pairs: list[tuple[int, int]] = []
105
+ for output_index, source_index in mapping.items():
106
+ pairs.append((int(output_index), int(source_index)))
107
+ if not pairs:
108
+ pairs = [(index, index) for index in range(1, min(len(template_slides), len(final_slides)) + 1)]
109
+
110
+ matched = 0
111
+ for output_index, source_index in pairs:
112
+ if not (1 <= output_index <= len(final_slides) and 1 <= source_index <= len(template_slides)):
113
+ findings.append(
114
+ {
115
+ "kind": "mapping_out_of_range",
116
+ "severity": "fail",
117
+ "message": f"mapping {output_index}->{source_index} is outside the deck",
118
+ }
119
+ )
120
+ continue
121
+ source_slide = template_slides[source_index - 1]
122
+ output_slide = final_slides[output_index - 1]
123
+ slide_pass = True
124
+
125
+ if source_slide.slide_layout.name != output_slide.slide_layout.name:
126
+ slide_pass = False
127
+ findings.append(
128
+ {
129
+ "kind": "layout_changed",
130
+ "severity": "fail",
131
+ "slide": output_index,
132
+ "sourceSlide": source_index,
133
+ "message": f"layout went from '{source_slide.slide_layout.name}' to '{output_slide.slide_layout.name}'",
134
+ }
135
+ )
136
+
137
+ if requirements.get("require_placeholder_geometry", True):
138
+ source_placeholders = _placeholder_map(source_slide)
139
+ output_placeholders = _placeholder_map(output_slide)
140
+ for key, source_box in source_placeholders.items():
141
+ output_box = output_placeholders.get(key)
142
+ if output_box is None:
143
+ continue
144
+ drift = max(abs(a - b) for a, b in zip(source_box, output_box))
145
+ if drift > tolerance_in:
146
+ slide_pass = False
147
+ findings.append(
148
+ {
149
+ "kind": "placeholder_geometry_drift",
150
+ "severity": "fail",
151
+ "slide": output_index,
152
+ "placeholder": key[1],
153
+ "driftIn": round(drift, 3),
154
+ "message": f"placeholder '{key[1]}' moved {drift:.2f} in from the template position",
155
+ }
156
+ )
157
+
158
+ if requirements.get("require_photographic_background"):
159
+ if _background_picture(source_slide, template_w, template_h) and not _background_picture(output_slide, final_w, final_h):
160
+ slide_pass = False
161
+ findings.append(
162
+ {
163
+ "kind": "background_lost",
164
+ "severity": "fail",
165
+ "slide": output_index,
166
+ "sourceSlide": source_index,
167
+ "message": "the template slide carries a full-bleed background image; the output does not",
168
+ }
169
+ )
170
+
171
+ checks.append({"check": "slide", "outputSlide": output_index, "sourceSlide": source_index, "passed": slide_pass})
172
+ matched += 1 if slide_pass else 0
173
+
174
+ coverage = matched / len(pairs) if pairs else 0.0
175
+ minimum = float(requirements.get("minimum_coverage_ratio", 1.0))
176
+ checks.append({"check": "coverage", "ratio": round(coverage, 4), "minimum": minimum, "passed": coverage + 1e-9 >= minimum})
177
+ if coverage + 1e-9 < minimum:
178
+ findings.append(
179
+ {
180
+ "kind": "coverage_below_minimum",
181
+ "severity": "fail",
182
+ "message": f"{coverage:.0%} of mapped slides preserved the template, minimum is {minimum:.0%}",
183
+ }
184
+ )
185
+
186
+ reference = requirements.get("reference_slide_indices") or []
187
+ if reference and not pairs:
188
+ findings.append({"kind": "no_usable_mapping", "severity": "fail", "message": f"reference slides {reference} could not be mapped to output slides"})
189
+
190
+ return {
191
+ "gate": "template-source-fidelity",
192
+ "template": str(template_file.resolve()),
193
+ "final": str(final_file.resolve()),
194
+ "requirements": requirements,
195
+ "checks": checks,
196
+ "findings": findings,
197
+ "coverage": round(coverage, 4),
198
+ "passed": not [item for item in findings if item["severity"] == "fail"],
199
+ }
200
+
201
+
202
+ def main(argv: list[str] | None = None) -> int:
203
+ import argparse
204
+
205
+ parser = argparse.ArgumentParser(description="Verify that a deck preserved the template it was built from.")
206
+ parser.add_argument("--template", required=True, help="the source template .pptx")
207
+ parser.add_argument("--final", required=True, help="the finished .pptx")
208
+ parser.add_argument("--requirements", help="JSON file with the fidelity requirements; omit for a neutral pass")
209
+ parser.add_argument("--json-out", help="also write the report here")
210
+ args = parser.parse_args(argv)
211
+
212
+ requirements = None
213
+ if args.requirements:
214
+ try:
215
+ requirements = json.loads(Path(args.requirements).read_text(encoding="utf-8"))
216
+ except Exception as exc: # noqa: BLE001
217
+ json.dump({"gate": "template-source-fidelity", "passed": False, "error": {"code": "bad_requirements", "message": str(exc)}}, sys.stdout)
218
+ sys.stdout.write("\n")
219
+ return 2
220
+ report = check_fidelity(args.template, args.final, requirements)
221
+ if args.json_out:
222
+ Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
223
+ json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
224
+ sys.stdout.write("\n")
225
+ return 0 if report["passed"] else 1
226
+
227
+
228
+ if __name__ == "__main__":
229
+ raise SystemExit(main())
@@ -0,0 +1,66 @@
1
+ # Finance decks
2
+
3
+ The mechanics are the same as any deck; the failure modes are specific.
4
+
5
+ ## What a finance audience checks first
6
+
7
+ 1. Does the total add up? A slide whose numbers do not sum loses the argument
8
+ before the argument starts. Run the arithmetic gate on every declared total.
9
+ 2. Is the period stated? "Revenue up 12%" without a base period is not a claim.
10
+ 3. Is the unit stated? Thousands, millions, currency, and whether it is gross or
11
+ net. Put it in the header, once.
12
+ 4. Is the comparison like-for-like? Full quarter against full quarter; do not
13
+ put a 13-week quarter beside a 12-week one without saying so.
14
+ 5. Does the chart axis start at zero? For bars, yes, always — a truncated bar
15
+ chart in a finance deck reads as manipulation even when it is not.
16
+
17
+ ## The financial slide kit
18
+
19
+ | Slide | Content | Note |
20
+ |---|---|---|
21
+ | Headline | One number, one claim, one source | The number the meeting is about |
22
+ | Bridge | Waterfall from last period to this one | Native chart; label the middle bars, not the ends |
23
+ | Results table | Period rows, metric columns, total row | Totals computed, not typed |
24
+ | Variance | Actual vs plan vs prior, and why | The "why" is a sentence, not a column |
25
+ | Outlook | Ranges, not points | A single forecast point implies precision you do not have |
26
+ | Ask | The decision, the amount, the date | Last slide, one line |
27
+
28
+ ## Numbers on slides
29
+
30
+ - Two significant figures is usually enough for a headline (`£2.4m`, not
31
+ `£2,384,117`). Keep full precision in the appendix table.
32
+ - Percentages: one decimal at most, and say what they are a percentage *of*.
33
+ - Round consistently: if the headline says 2.4m, the table should not say
34
+ 2.38m in one row and 2.4m in the next.
35
+ - Negative numbers: use a minus sign consistently (`−12%`) and keep
36
+ parentheses for tables where accounting convention is expected.
37
+ - A variance is meaningless without its sign and its direction; state whether
38
+ favourable is up or down (revenue vs cost).
39
+
40
+ ## Charts that survive a CFO
41
+
42
+ - Waterfall for a bridge, column for periods, line for a trend, never pie for
43
+ more than three slices.
44
+ - Label the axis unit once, in the caption: "£m, FY2026 actuals".
45
+ - One series per chart unless the comparison *is* the point.
46
+ - Do not chart a table of six numbers; the table is more honest and faster to
47
+ read.
48
+ - Forecast periods are visually distinct (lighter fill, dashed line) and named
49
+ in the caption.
50
+
51
+ ## Provenance
52
+
53
+ Every non-obvious number gets a note with: source file or system, extraction
54
+ date, period covered, and any adjustment made. The speaker notes are the right
55
+ place; a footnote-heavy slide is a slide nobody reads.
56
+
57
+ ## Check before delivery
58
+
59
+ ```bash
60
+ "<python>" "<skill>/container_tools/native_table_arithmetic_gate.py" --input deck.pptx --contracts totals.json
61
+ "<python>" "<skill>/container_tools/native_quantitative_chart_gate.py" --input deck.pptx --all-slides
62
+ "<python>" "<skill>/container_tools/inspect_presentation_layout_geometry.py" --input deck.pptx
63
+ ```
64
+
65
+ Then read the slides as a sceptic: which number would you challenge first, and
66
+ does the deck already answer it?
@@ -0,0 +1,71 @@
1
+ # Slide mechanics and paste-runnable recipes
2
+
3
+ The helper CLI covers the common path: a small deck with a title, bullet slides,
4
+ a table, a chart and a picture. Everything else — precise coordinates, master
5
+ edits, notes, theme tokens, connectors — is ordinary `python-pptx` work, and
6
+ these recipes are the honest route.
7
+
8
+ Read the mechanics first, then paste the recipe. Every snippet runs as-is from a
9
+ shell, from any working directory:
10
+
11
+ ```bash
12
+ "<python>" - <<'PY'
13
+ from pptx import Presentation
14
+ ...
15
+ PY
16
+ ```
17
+
18
+ ## The four numbers that decide every layout
19
+
20
+ | Question | Answer |
21
+ |---|---|
22
+ | Where is this shape? | `left`, `top`, `width`, `height` in **EMU** (English Metric Units). |
23
+ | What is an inch? | `914400` EMU. `pptx.util.Inches(1)` is the polite wrapper. |
24
+ | What is a point? | `12700` EMU. Font sizes use points, geometry uses EMU, and mixing them silently breaks layout. |
25
+ | What is a slide? | `prs.slide_width` / `prs.slide_height`; check them, never assume 16:9. |
26
+
27
+ Helper conversions worth memorising: `Emu(914400) == Inches(1)`,
28
+ `Pt(28) == Emu(355600)`, and a fraction of the slide is
29
+ `int(0.1 * prs.slide_width)`.
30
+
31
+ ## Z-order
32
+
33
+ Shapes paint in the order they live in `slide.shapes`. A shape added later
34
+ covers one added earlier. To reorder without deleting:
35
+
36
+ ```python
37
+ tree = slide.shapes._spTree
38
+ shape = slide.shapes[3]
39
+ tree.remove(shape._element)
40
+ tree.insert(2, shape._element) # after nvGrpSpPr and grpSpPr
41
+ ```
42
+
43
+ Index `2` is the first painted shape; that is where a full-bleed background
44
+ image belongs.
45
+
46
+ ## What this pack ships
47
+
48
+ | Recipe | Use it when |
49
+ |---|---|
50
+ | [textbox-at-coordinate](textbox-at-coordinate.md) | Text must land on an exact rect, not "somewhere under the title". |
51
+ | [autolayout](autolayout.md) | The same build has to fit 3 bullets or 9 without hand-tuning. |
52
+ | [tables](tables.md) | Numbers need a grid with column widths and per-cell formatting. |
53
+ | [native-charts](native-charts.md) | Data must stay editable, not become a picture of a chart. |
54
+ | [master-and-layouts](master-and-layouts.md) | The whole deck changes at once: title bar, accent line, footer. |
55
+ | [speaker-notes](speaker-notes.md) | The presenter needs a script attached to each slide. |
56
+ | [theme-tokens](theme-tokens.md) | Colours and fonts must come from one place. |
57
+ | [rich-text-runs](rich-text-runs.md) | One paragraph mixes bold, accent colour and a footnote. |
58
+ | [images](images.md) | A picture needs a crop, a cover fit or a real alt text. |
59
+ | [connectors](connectors.md) | Two boxes are joined by a line that must survive a move. |
60
+
61
+ ## Order of work that keeps decks out of trouble
62
+
63
+ 1. Read the target geometry off the template or the user's reference
64
+ (`container_tools/inspect_presentation_layout_geometry.py` prints it as JSON).
65
+ 2. Place shapes in EMU, top-down: background, chrome, title, body, evidence.
66
+ 3. Set type sizes by the *smallest* screen the deck will be shown on; body 18 pt
67
+ and up, footnotes 12 pt and up.
68
+ 4. Fill every placeholder you keep and delete the ones you do not — an empty
69
+ placeholder prints "Click to add text" in edit view and looks unfinished.
70
+ 5. Render, look, then fix. `container_tools/render_slides.py` plus `read_image`
71
+ is the loop; a deck nobody looked at is not verified.
@@ -0,0 +1,88 @@
1
+ # Auto-layout rules
2
+
3
+ ## Mechanics
4
+
5
+ PowerPoint has two different "autofit" ideas and mixing them up wastes a round
6
+ trip:
7
+
8
+ - `word_wrap` decides whether text wraps inside the width. Turn it on for every
9
+ authored box.
10
+ - `auto_size` decides whether the *frame* follows the text (`MSO_AUTO_SIZE.
11
+ SHAPE_TO_FIT_TEXT`) or the text shrinks to fit the frame
12
+ (`TEXT_TO_FIT_SHAPE`). `TEXT_TO_FIT_SHAPE` is a **render-time** behaviour:
13
+ PowerPoint recalculates it when the file is opened, so a deck that looks fine
14
+ in a draft preview can still shrink to 8 pt on a real screen.
15
+
16
+ The reliable pattern is: fixed frame, explicit sizes, content measured against
17
+ the frame, no text-to-fit. Estimate, then verify by rendering.
18
+
19
+ ## Recipe: fit a variable number of bullets
20
+
21
+ ```python
22
+ "<python>" - <<'PY'
23
+ from pptx import Presentation
24
+ from pptx.util import Inches, Pt
25
+ from pptx.enum.text import MSO_ANCHOR
26
+
27
+ BULLETS = [
28
+ "Picking is 41% of floor labour hours",
29
+ "Travel between aisles adds 12 minutes per order",
30
+ "Manual cycle counts stop the line twice a week",
31
+ "Two cells idle below 60% utilisation",
32
+ "Overtime covers the Friday peak",
33
+ ]
34
+
35
+ def fits(text: str, width_in: float, height_in: float, size_pt: float) -> bool:
36
+ per_line = max(1, int(width_in * 96 / (0.55 * size_pt)))
37
+ lines = max(1, -(-len(text) // per_line))
38
+ return lines * 1.2 * size_pt / 72 <= height_in
39
+
40
+ prs = Presentation()
41
+ prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
42
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
43
+
44
+ width, height = 8.4, 4.6 # the body rect you reserved
45
+ box = slide.shapes.add_textbox(Inches(0.9), Inches(1.6), Inches(width), Inches(height))
46
+ frame = box.text_frame
47
+ frame.word_wrap = True
48
+ frame.vertical_anchor = MSO_ANCHOR.TOP
49
+
50
+ size = next(pt for pt in (24, 22, 20, 18) if fits(" ".join(BULLETS), width, height, pt))
51
+ needed = sum(
52
+ max(1, -(-len(item) // max(1, int(width * 96 / (0.55 * size))))) * 1.2 * size / 72
53
+ for item in BULLETS
54
+ )
55
+ for index, item in enumerate(BULLETS):
56
+ paragraph = frame.paragraphs[0] if index == 0 else frame.add_paragraph()
57
+ paragraph.text = item
58
+ paragraph.font.size = Pt(size)
59
+ paragraph.space_after = Pt(10)
60
+
61
+ prs.save("autofit.pptx")
62
+ print(f"size={size}pt lines_estimate_in={needed:.2f} frame_in={height}")
63
+ PY
64
+ ```
65
+
66
+ ## Rules that hold regardless of content
67
+
68
+ 1. Body text is 18 pt or larger; below that the back row cannot read it.
69
+ 2. Six bullets or three lines of body is the point where a slide should split.
70
+ 3. Longer text shrinks the number of *words*, not the type size. If the fit loop
71
+ lands below the body floor, cut copy or split the slide.
72
+ 4. Keep 0.5 in of bottom margin for the footer and for projectors that crop.
73
+ 5. Two columns beat one narrow column: a 4.2 in column of 18 pt text is a wall.
74
+
75
+ ## Pitfalls this avoids
76
+
77
+ - `frame.text` then `paragraph.font.size` on paragraph 0 only: a multi-paragraph
78
+ assignment leaves paragraphs 1..n at the inherited 18 pt.
79
+ - Text-to-fit hiding the overflow: the file opens fine and the audience reads
80
+ 9 pt. Measure, do not delegate the fit to the renderer.
81
+ - A frame height that exactly equals the text height: one extra word on edit and
82
+ the block runs into the evidence below. Reserve 10% slack.
83
+
84
+ ## CLI fast path
85
+
86
+ `pptx create` with `layout: "bullets"` fills the layout's body placeholder at
87
+ its own size. Use the loop above when the number of bullets is not known before
88
+ the content is written.
@@ -0,0 +1,83 @@
1
+ # Connectors
2
+
3
+ ## Mechanics
4
+
5
+ A connector is a line with two attachment sites. `add_connector` takes a begin
6
+ and end shape *plus* connection indices, which is what makes the line follow the
7
+ boxes when they move. If you pass coordinates instead, the line is a floating
8
+ line: it looks right in the file you made and wrong the first time a box is
9
+ nudged.
10
+
11
+ - `MSO_CONNECTOR.STRAIGHT | ELBOW | CURVE`
12
+ - `connector.begin_connect(shape, idx)` / `end_connect(shape, idx)`; `idx` is
13
+ the site index on the shape's outline (0 = top, 1 = left, 2 = bottom,
14
+ 3 = right for a rectangle).
15
+ - Arrowheads are line properties: `line.end_arrowhead = MSO_ARROWHEAD.TRIANGLE`.
16
+
17
+ ## Recipe
18
+
19
+ ```python
20
+ "<python>" - <<'PY'
21
+ from pptx import Presentation
22
+ from pptx.util import Inches, Pt
23
+ from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
24
+ from pptx.dml.color import RGBColor
25
+ from pptx.enum.dml import MSO_LINE_DASH_STYLE
26
+
27
+ prs = Presentation()
28
+ prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
29
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
30
+
31
+ def box(left, top, text, width=2.6, height=1.1):
32
+ shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(left), Inches(top), Inches(width), Inches(height))
33
+ shape.text_frame.text = text
34
+ shape.text_frame.paragraphs[0].font.size = Pt(14)
35
+ shape.fill.solid()
36
+ shape.fill.fore_color.rgb = RGBColor(0xF2, 0xF5, 0xF9)
37
+ shape.line.color.rgb = RGBColor(0xC8, 0xD2, 0xDF)
38
+ return shape
39
+
40
+ order = box(0.9, 2.2, "Order booked")
41
+ pick = box(4.4, 2.2, "Picked")
42
+ ship = box(7.9, 2.2, "Shipped")
43
+
44
+ for start, end in ((order, pick), (pick, ship)):
45
+ connector = slide.shapes.add_connector(
46
+ MSO_CONNECTOR.STRAIGHT, 0, 0, Inches(0.1), Inches(0.1)
47
+ )
48
+ connector.begin_connect(start, 3) # right edge
49
+ connector.end_connect(end, 1) # left edge
50
+ connector.line.width = Pt(2)
51
+ connector.line.color.rgb = RGBColor(0x5F, 0x6B, 0x7A)
52
+ connector.line.end_arrowhead = 3 # triangle
53
+
54
+ prs.save("connectors.pptx")
55
+ from pptx.oxml.ns import qn
56
+ print("connectors:", sum(1 for s in slide.shapes if s._element.tag == qn("p:cxnSp")))
57
+ PY
58
+ ```
59
+
60
+ ## Rules
61
+
62
+ 1. Connect sides that face each other: right → left for a row, bottom → top for
63
+ a column. A line crossing its own boxes reads as an error.
64
+ 2. Elbow connectors for orthogonal flows, straight for a single hop, curve almost
65
+ never.
66
+ 3. Two crossing connectors in one diagram is the point where the diagram becomes
67
+ a picture of a diagram; split it.
68
+ 4. Label the connector ("12 min", "auto") with a small textbox near its middle,
69
+ not with arrow direction alone.
70
+
71
+ ## Pitfalls this avoids
72
+
73
+ - `add_connector` with free coordinates: the line is positioned but unattached,
74
+ and the deck's own edits break it silently.
75
+ - Arrowhead index confusion: arrowhead values are a bitmask enum, not a boolean.
76
+ - Connectors behind filled shapes because they were added before the boxes.
77
+
78
+ ## CLI fast path
79
+
80
+ There is no connector verb in `pptx create`; this recipe is the route. After
81
+ building, `container_tools/inspect_presentation_layout_geometry.py` flags
82
+ `connector_over_text` when a line crosses a text shape, which is the failure
83
+ mode you cannot see in a thumbnail.
@@ -0,0 +1,80 @@
1
+ # Images
2
+
3
+ ## Mechanics
4
+
5
+ `add_picture(path, left, top, width=…, height=…)`:
6
+
7
+ - Give **width only** and the height follows the aspect ratio. Give both and
8
+ the picture is stretched — a deliberate choice for a full-bleed background,
9
+ a mistake for a product photo.
10
+ - `picture.crop_left/right/top/bottom` are fractions of the *source*; they crop
11
+ without changing the frame, which is how a "cover" fit is done.
12
+ - The bytes live in `ppt/media/`; the same file added twice is stored once,
13
+ keyed by content hash.
14
+ - `picture._element.nvPicPr.cNvPr.set("descr", "…")` is the alt text. Decks get
15
+ read aloud and exported; an untagged picture is an empty sentence.
16
+
17
+ ## Recipe: cover-fit into a fixed frame
18
+
19
+ ```python
20
+ "<python>" - <<'PY'
21
+ from pptx import Presentation
22
+ from pptx.util import Inches
23
+ from PIL import Image
24
+
25
+ def cover(picture, frame_w_in, frame_h_in, source_w, source_h):
26
+ """Crop the picture so it fills the frame without distortion."""
27
+ frame_ratio = frame_w_in / frame_h_in
28
+ source_ratio = source_w / source_h
29
+ if source_ratio > frame_ratio: # too wide: crop the sides
30
+ keep = frame_ratio / source_ratio
31
+ picture.crop_left = picture.crop_right = (1 - keep) / 2
32
+ elif source_ratio < frame_ratio: # too tall: crop top and bottom
33
+ keep = source_ratio / frame_ratio
34
+ picture.crop_top = picture.crop_bottom = (1 - keep) / 2
35
+
36
+ prs = Presentation()
37
+ prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
38
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
39
+
40
+ with Image.open("aisle.png") as image:
41
+ source_w, source_h = image.size
42
+
43
+ frame = slide.shapes.add_picture("aisle.png", Inches(0.9), Inches(1.7), Inches(8.0), Inches(4.5))
44
+ cover(frame, 8.0, 4.5, source_w, source_h)
45
+ frame._element.nvPicPr.cNvPr.set("descr", "Racking aisle in the pilot cell at peak hour")
46
+
47
+ # full-bleed background, sent to the back
48
+ background = slide.shapes.add_picture("aisle.png", 0, 0, prs.slide_width, prs.slide_height)
49
+ tree = slide.shapes._spTree
50
+ tree.remove(background._element)
51
+ tree.insert(2, background._element)
52
+ prs.save("image.pptx")
53
+ print("placed", source_w, "x", source_h)
54
+ PY
55
+ ```
56
+
57
+ ## Rules
58
+
59
+ 1. Never upscale past the source resolution: a 900 px wide image at 8 in is
60
+ ~110 ppi and will look soft on a projector. Check `Image.open(...).size`.
61
+ 2. One dominant image per slide. A slide with a hero and three thumbnails is two
62
+ slides.
63
+ 3. Full-bleed backgrounds need text contrast handled deliberately — a dark
64
+ scrim rectangle between picture and text, not hope.
65
+ 4. Alt text on every picture, in the language of the deck.
66
+
67
+ ## Pitfalls this avoids
68
+
69
+ - Passing both width and height and getting a squashed photograph nobody can
70
+ name as the cause.
71
+ - `add_picture` after the text: the picture covers the words. Add backgrounds
72
+ first, or move them to index 2.
73
+ - Re-using a screenshot of a chart or a table as "evidence": it is unreadable at
74
+ projector size and unverifiable. Use a native chart or table.
75
+
76
+ ## CLI fast path
77
+
78
+ `pptx create` accepts `{"image": {"path": "...", "left": 0.9, "top": 1.6,
79
+ "width": 8.0}}` and derives the height from the aspect ratio — no crop, no alt
80
+ text. Cropping, alt text and background placement are the recipe's job.