dsh-office-kit 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/README.md +3 -1
  3. package/assets/skills/excel-live-control/SKILL.md +234 -0
  4. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  5. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  6. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  7. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  8. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  9. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  10. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  11. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  12. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  13. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  14. package/assets/skills/office-documents/SKILL.md +90 -77
  15. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  16. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  17. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  18. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  19. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  20. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  21. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  22. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  23. package/assets/skills/office-documents/render_docx.py +99 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +143 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +199 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +131 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +66 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +87 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +133 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +161 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +98 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +99 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +118 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -0
  64. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  65. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  66. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  67. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  68. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  69. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  70. package/assets/skills/office-documents/tasks/equations.md +59 -0
  71. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  72. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  73. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  74. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  75. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  76. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  77. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  78. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  79. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  80. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  81. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  82. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  83. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  84. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  85. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  86. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  87. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  88. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  89. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  90. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  91. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  92. package/assets/skills/office-pdf/SKILL.md +73 -56
  93. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  94. package/assets/skills/office-pdf/references/geometry.md +44 -0
  95. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  96. package/assets/skills/office-pdf/references/limits.md +58 -0
  97. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  98. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  99. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  100. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  101. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  102. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  103. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  104. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  105. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  106. package/assets/skills/office-pdf/tasks/create.md +75 -0
  107. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  108. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  109. package/assets/skills/office-pdf/tasks/read.md +62 -0
  110. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  111. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  112. package/assets/skills/office-presentations/SKILL.md +141 -87
  113. package/assets/skills/office-presentations/container_tools/add_slides.py +143 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -0
  126. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  127. package/assets/skills/office-presentations/recipes/README.md +71 -0
  128. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  129. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  130. package/assets/skills/office-presentations/recipes/images.md +80 -0
  131. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  132. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  133. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  134. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  135. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  136. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  137. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  138. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  139. package/assets/skills/office-presentations/references/finalization.md +63 -0
  140. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  141. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  142. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  143. package/assets/skills/office-presentations/references/templates.md +76 -0
  144. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  145. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  146. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  147. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  148. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  149. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  150. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  151. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +183 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -0
  166. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  167. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  168. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  169. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  170. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  171. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  172. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  173. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  174. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  175. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  176. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  177. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  178. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  179. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  180. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  181. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  182. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  183. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  184. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  185. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  186. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  187. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  188. package/assets/skills/shared/annotation-recipes.md +252 -0
  189. package/assets/skills/shared/check_runtime.py +260 -0
  190. package/assets/skills/shared/common-workflow.md +139 -0
  191. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  192. package/dist/client.js +4 -4
  193. package/dist/client.js.map +1 -1
  194. package/dist/index.js +246 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +213 -254
@@ -0,0 +1,246 @@
1
+ """Build a starter deck out of the slides the plan keeps.
2
+
3
+ Purpose: the safe way to follow a supplied design is to keep its actual slides,
4
+ not to redraw a lookalike. This copies the template file, keeps the mapped
5
+ source slides in plan order, drops the rest, then applies the element actions
6
+ that only move text: keep, delete, fill-placeholder, rewrite and
7
+ rewrite-and-reposition.
8
+
9
+ Ceiling: slides are reused, not cloned. Two output slides may not map to the
10
+ same source slide, and 'replace' is left to the author (build the new element
11
+ with a recipe, then run check_template_fidelity). Charts are never rebuilt here.
12
+
13
+ Exit codes: 0 starter written, 1 plan rejected, 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, placeholder_kind, slide_size_in # noqa: E402
25
+
26
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
27
+ for _stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ _stream.reconfigure(encoding="utf-8", errors="replace")
30
+ except (AttributeError, ValueError):
31
+ pass
32
+
33
+ PLACEHOLDER_ALIASES = {
34
+ "title": ("TITLE", "CENTER_TITLE"),
35
+ "subtitle": ("SUBTITLE",),
36
+ "body": ("BODY", "OBJECT"),
37
+ }
38
+
39
+
40
+ def _slides(prs) -> list:
41
+ return list(prs.slides)
42
+
43
+
44
+ def _drop_slide(prs, slide) -> None:
45
+ slide_id = None
46
+ for element in prs.slides._sldIdLst:
47
+ if element.rId == slide.part.relate_to(slide.part, "") if False else False:
48
+ break
49
+ # find the sldId whose relationship points at this slide part
50
+ rels = prs.part.rels
51
+ rId = None
52
+ for key, relationship in rels.items():
53
+ if relationship.reltype.endswith("/slide") and relationship.target_part is slide.part:
54
+ rId = key
55
+ break
56
+ if rId is None:
57
+ return
58
+ for element in list(prs.slides._sldIdLst):
59
+ if element.rId == rId:
60
+ slide_id = element
61
+ break
62
+ if slide_id is not None:
63
+ prs.slides._sldIdLst.remove(slide_id)
64
+ prs.part.drop_rel(rId)
65
+
66
+
67
+ def _keep_only(prs, wanted: list[int]) -> None:
68
+ ordered = [element for element in prs.slides._sldIdLst]
69
+ keep_rids = []
70
+ for position in wanted:
71
+ keep_rids.append(ordered[position - 1].rId)
72
+ for slide in list(prs.slides):
73
+ current = None
74
+ for key, relationship in prs.part.rels.items():
75
+ if relationship.reltype.endswith("/slide") and relationship.target_part is slide.part:
76
+ current = key
77
+ break
78
+ if current not in keep_rids:
79
+ _drop_slide(prs, slide)
80
+ # reorder what survived to match the plan
81
+ by_rid = {element.rId: element for element in prs.slides._sldIdLst}
82
+ for element in list(prs.slides._sldIdLst):
83
+ prs.slides._sldIdLst.remove(element)
84
+ for rId in keep_rids:
85
+ prs.slides._sldIdLst.append(by_rid[rId])
86
+
87
+
88
+ def _find_shape(slide, element_id: str):
89
+ shapes = list(slide.shapes)
90
+ if element_id.isdigit():
91
+ index = int(element_id) - 1
92
+ return shapes[index] if 0 <= index < len(shapes) else None
93
+ for shape in shapes:
94
+ if shape.name == element_id:
95
+ return shape
96
+ if element_id.lower().startswith("shape-") and element_id[6:].isdigit():
97
+ index = int(element_id[6:]) - 1
98
+ return shapes[index] if 0 <= index < len(shapes) else None
99
+ return None
100
+
101
+
102
+ def _find_placeholder(slide, alias: str):
103
+ wanted = PLACEHOLDER_ALIASES.get(alias.lower())
104
+ for shape in slide.shapes:
105
+ if not shape.is_placeholder:
106
+ continue
107
+ kind = (placeholder_kind(shape) or "").upper()
108
+ if alias.isdigit() and shape.placeholder_format.idx == int(alias):
109
+ return shape
110
+ if wanted and kind in wanted:
111
+ return shape
112
+ if alias.lower() == (placeholder_kind(shape) or "").lower():
113
+ return shape
114
+ return None
115
+
116
+
117
+ def _set_text(shape, text: str) -> None:
118
+ frame = shape.text_frame
119
+ paragraphs = list(frame.paragraphs)
120
+ first = paragraphs[0]
121
+ runs = list(first.runs)
122
+ if runs:
123
+ runs[0].text = text
124
+ for run in runs[1:]:
125
+ run._r.getparent().remove(run._r)
126
+ for paragraph in paragraphs[1:]:
127
+ paragraph._p.getparent().remove(paragraph._p)
128
+ else:
129
+ frame.text = text
130
+
131
+
132
+ def build_starter(template_path: str | Path, plan_path: str | Path, out_path: str | Path) -> dict:
133
+ from pptx import Presentation
134
+
135
+ template = Path(template_path)
136
+ plan_file = Path(plan_path)
137
+ if not template.is_file():
138
+ return {"ok": False, "error": {"code": "template_missing", "message": str(template)}}
139
+ if not plan_file.is_file():
140
+ return {"ok": False, "error": {"code": "plan_missing", "message": str(plan_file)}}
141
+
142
+ plan = json.loads(plan_file.read_text(encoding="utf-8"))
143
+ entries = plan.get("outputSlides") or []
144
+ if not entries:
145
+ return {"ok": False, "error": {"code": "plan_empty", "message": "outputSlides is empty"}}
146
+
147
+ sources = [int(entry.get("sourceSlide", 0)) for entry in entries]
148
+ duplicates = sorted({value for value in sources if sources.count(value) > 1})
149
+ if duplicates:
150
+ return {
151
+ "ok": False,
152
+ "error": {
153
+ "code": "duplicate_source_slide",
154
+ "message": f"source slides {duplicates} are mapped more than once; this builder reuses slides instead of cloning them",
155
+ },
156
+ }
157
+
158
+ prs = Presentation(str(template))
159
+ total = len(prs.slides)
160
+ out_of_range = [value for value in sources if not 1 <= value <= total]
161
+ if out_of_range:
162
+ return {"ok": False, "error": {"code": "source_out_of_range", "message": f"template has {total} slides, plan asks for {out_of_range}"}}
163
+
164
+ slide_w, slide_h = slide_size_in(prs)
165
+ _keep_only(prs, sources)
166
+ kept = _slides(prs)
167
+
168
+ applied: list[dict] = []
169
+ for position, entry in enumerate(entries):
170
+ slide = kept[position]
171
+ for element in entry.get("elements", []):
172
+ element_id = str(element.get("elementId", ""))
173
+ action = str(element.get("action", "keep"))
174
+ target = element.get("target") or {}
175
+ if action == "keep":
176
+ continue
177
+ if action == "fill-placeholder":
178
+ alias = str(target.get("placeholder") or "")
179
+ shape = _find_placeholder(slide, alias) if alias else _find_shape(slide, element_id)
180
+ if shape is None:
181
+ applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "not_found"})
182
+ continue
183
+ if target.get("text") is not None:
184
+ _set_text(shape, str(target["text"]))
185
+ applied.append({"slide": position + 1, "element": shape.name, "action": action, "status": "applied"})
186
+ continue
187
+ shape = _find_shape(slide, element_id)
188
+ if shape is None:
189
+ applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "not_found"})
190
+ continue
191
+ if action == "delete":
192
+ shape._element.getparent().remove(shape._element)
193
+ applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "applied"})
194
+ continue
195
+ if action in {"rewrite", "rewrite-and-reposition"}:
196
+ _set_text(shape, str(target.get("text", "")))
197
+ if action == "rewrite-and-reposition" and isinstance(target.get("zone"), list):
198
+ x, y, width, height = (float(value) for value in target["zone"])
199
+ shape.left = int(x * slide_w * 914400)
200
+ shape.top = int(y * slide_h * 914400)
201
+ shape.width = int(width * slide_w * 914400)
202
+ shape.height = int(height * slide_h * 914400)
203
+ applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "applied"})
204
+ continue
205
+ applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "left_for_author"})
206
+
207
+ destination = Path(out_path)
208
+ destination.parent.mkdir(parents=True, exist_ok=True)
209
+ prs.save(str(destination))
210
+
211
+ leftovers = []
212
+ for index, slide in enumerate(kept, start=1):
213
+ for shape in slide.shapes:
214
+ if shape.is_placeholder and not (shape.text_frame.text or "").strip():
215
+ leftovers.append({"slide": index, "placeholder": shape.name, "bboxIn": [round(value, 2) for value in (box(shape) or (0, 0, 0, 0))]})
216
+
217
+ return {
218
+ "ok": True,
219
+ "starter": str(destination.resolve()),
220
+ "slides": len(kept),
221
+ "sourceSlides": sources,
222
+ "applied": applied,
223
+ "emptyPlaceholders": leftovers,
224
+ "note": "empty placeholders still show their prompt in edit view; fill or delete them before delivery",
225
+ }
226
+
227
+
228
+ def main(argv: list[str] | None = None) -> int:
229
+ import argparse
230
+
231
+ parser = argparse.ArgumentParser(description="Build a starter deck from the template slides a plan keeps.")
232
+ parser.add_argument("--template", required=True, help="path to the template .pptx")
233
+ parser.add_argument("--plan", required=True, help="path to template-frame-map.json")
234
+ parser.add_argument("--out", required=True, help="path of the starter .pptx to write")
235
+ args = parser.parse_args(argv)
236
+
237
+ report = build_starter(args.template, args.plan, args.out)
238
+ json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
239
+ sys.stdout.write("\n")
240
+ if report.get("ok"):
241
+ return 0
242
+ return 2 if report["error"]["code"] in {"template_missing", "plan_missing"} else 1
243
+
244
+
245
+ if __name__ == "__main__":
246
+ raise SystemExit(main())
@@ -0,0 +1,192 @@
1
+ """Check a template plan before any slide is touched.
2
+
3
+ Purpose: a template plan is a contract between "what the design gives you" and
4
+ "what you intend to write". Validating it first turns a silent redesign into a
5
+ list of violations, because the expensive mistakes (deleting a logo, inventing
6
+ new blocks the brand never had, covering inherited art) are all visible on
7
+ paper before anything is built.
8
+
9
+ Plan shape (template-frame-map.json):
10
+ {
11
+ "template": "path/to/template.pptx",
12
+ "newPrimitives": {"allowed": false, "justification": ""},
13
+ "outputSlides": [
14
+ {"slide": 1, "mode": "duplicate-slide", "sourceSlide": 3,
15
+ "elements": [{"elementId": "shape-7", "action": "fill-placeholder",
16
+ "target": {"placeholder": "title", "text": "Q3 results"}}]}
17
+ ]
18
+ }
19
+
20
+ Exit codes: 0 valid, 1 violations, 2 unreadable plan.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
30
+ for _stream in (sys.stdout, sys.stderr):
31
+ try:
32
+ _stream.reconfigure(encoding="utf-8", errors="replace")
33
+ except (AttributeError, ValueError):
34
+ pass
35
+
36
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
37
+
38
+ ALLOWED_ACTIONS = {"rewrite", "rewrite-and-reposition", "replace", "delete", "keep", "fill-placeholder"}
39
+ PRESERVE_ONLY_ROLES = {"brand", "logo", "bumper", "divider", "section", "chrome", "footer"}
40
+ MODES = {"duplicate-slide"}
41
+
42
+
43
+ def _violation(code: str, message: str, severity: str = "fail", **extra) -> dict:
44
+ return {"code": code, "severity": severity, "message": message, **extra}
45
+
46
+
47
+ def validate_plan(plan: dict) -> dict:
48
+ violations: list[dict] = []
49
+ warnings: list[dict] = []
50
+
51
+ output_slides = plan.get("outputSlides")
52
+ if not isinstance(output_slides, list) or not output_slides:
53
+ return {"gate": "template-plan", "passed": False, "violations": [_violation("missing_output_slides", "the plan declares no output slides")], "checks": 0}
54
+
55
+ indices = []
56
+ sources: dict[int, int] = {}
57
+ element_actions = 0
58
+ for position, entry in enumerate(output_slides, start=1):
59
+ if not isinstance(entry, dict):
60
+ violations.append(_violation("malformed_slide", f"output slide #{position} is not an object"))
61
+ continue
62
+ slide_no = entry.get("slide", position)
63
+ indices.append(slide_no)
64
+ mode = entry.get("mode", "duplicate-slide")
65
+ if mode not in MODES:
66
+ violations.append(_violation("unsupported_mode", f"slide {slide_no}: mode '{mode}' is not supported", slide=slide_no))
67
+ source = entry.get("sourceSlide")
68
+ if not isinstance(source, int) or source < 1:
69
+ violations.append(_violation("missing_source_slide", f"slide {slide_no}: sourceSlide must be a positive integer", slide=slide_no))
70
+ else:
71
+ if source in sources:
72
+ warnings.append(
73
+ _violation(
74
+ "duplicated_source_slide",
75
+ f"slides {sources[source]} and {slide_no} both duplicate source slide {source}",
76
+ severity="review",
77
+ slide=slide_no,
78
+ )
79
+ )
80
+ sources[source] = slide_no
81
+ elements = entry.get("elements", [])
82
+ if not isinstance(elements, list):
83
+ violations.append(_violation("malformed_elements", f"slide {slide_no}: elements must be a list", slide=slide_no))
84
+ continue
85
+ for element in elements:
86
+ if not isinstance(element, dict) or not element.get("elementId"):
87
+ violations.append(_violation("missing_element_id", f"slide {slide_no}: an element has no elementId", slide=slide_no))
88
+ continue
89
+ element_actions += 1
90
+ element_id = element["elementId"]
91
+ action = element.get("action")
92
+ target = element.get("target") or {}
93
+ if action not in ALLOWED_ACTIONS:
94
+ violations.append(
95
+ _violation(
96
+ "unknown_action",
97
+ f"slide {slide_no} element {element_id}: '{action}' is not one of {sorted(ALLOWED_ACTIONS)}",
98
+ slide=slide_no,
99
+ elementId=element_id,
100
+ )
101
+ )
102
+ continue
103
+ role = str(target.get("role") or element.get("role") or "").lower()
104
+ if role in PRESERVE_ONLY_ROLES and action != "keep":
105
+ violations.append(
106
+ _violation(
107
+ "preserve_only_element",
108
+ f"slide {slide_no} element {element_id}: role '{role}' must stay untouched, found '{action}'",
109
+ slide=slide_no,
110
+ elementId=element_id,
111
+ )
112
+ )
113
+ if action == "fill-placeholder" and not (target.get("placeholder") or target.get("text")):
114
+ violations.append(
115
+ _violation("placeholder_target_missing", f"slide {slide_no} element {element_id}: fill-placeholder needs a placeholder or text target", slide=slide_no, elementId=element_id)
116
+ )
117
+ if action in {"rewrite", "rewrite-and-reposition"} and not target.get("text"):
118
+ violations.append(
119
+ _violation("rewrite_text_missing", f"slide {slide_no} element {element_id}: {action} needs target.text", slide=slide_no, elementId=element_id)
120
+ )
121
+ if action == "rewrite-and-reposition":
122
+ zone = target.get("zone")
123
+ if not (isinstance(zone, list) and len(zone) == 4 and all(isinstance(value, (int, float)) for value in zone)):
124
+ violations.append(
125
+ _violation("zone_missing", f"slide {slide_no} element {element_id}: rewrite-and-reposition needs a zone [x, y, w, h]", slide=slide_no, elementId=element_id)
126
+ )
127
+ else:
128
+ x, y, width, height = zone
129
+ if not all(0.0 <= value <= 1.0 for value in zone) or x + width > 1.0001 or y + height > 1.0001:
130
+ violations.append(
131
+ _violation("zone_out_of_bounds", f"slide {slide_no} element {element_id}: zone {zone} does not fit inside 0-1", slide=slide_no, elementId=element_id)
132
+ )
133
+ if action == "replace":
134
+ warnings.append(
135
+ _violation(
136
+ "replace_needs_authoring",
137
+ f"slide {slide_no} element {element_id}: 'replace' is applied by the author with a recipe, not by the starter-deck builder",
138
+ severity="review",
139
+ slide=slide_no,
140
+ elementId=element_id,
141
+ )
142
+ )
143
+
144
+ if indices != list(range(1, len(output_slides) + 1)):
145
+ violations.append(_violation("non_sequential_slides", f"output slide numbers must start at 1 and be sequential, found {indices}"))
146
+
147
+ primitives = plan.get("newPrimitives") or {}
148
+ if primitives.get("allowed") is True and not str(primitives.get("justification") or "").strip():
149
+ violations.append(_violation("new_primitives_unjustified", "newPrimitives.allowed is true but no justification was given"))
150
+
151
+ blocking = [item for item in violations if item["severity"] == "fail"]
152
+ return {
153
+ "gate": "template-plan",
154
+ "passed": not blocking,
155
+ "checks": {"outputSlides": len(output_slides), "elementActions": element_actions},
156
+ "violations": violations,
157
+ "warnings": warnings,
158
+ }
159
+
160
+
161
+ def main(argv: list[str] | None = None) -> int:
162
+ import argparse
163
+
164
+ parser = argparse.ArgumentParser(description="Validate a template-frame-map before building the deck.")
165
+ parser.add_argument("--plan", required=True, help="path to template-frame-map.json")
166
+ parser.add_argument("--template", help="optionally confirm the plan names an existing template file")
167
+ parser.add_argument("--json-out", help="also write the report here")
168
+ args = parser.parse_args(argv)
169
+
170
+ try:
171
+ plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
172
+ except Exception as exc: # noqa: BLE001 - a bad plan is the finding
173
+ json.dump({"gate": "template-plan", "passed": False, "error": {"code": "unparsable_plan", "message": str(exc)}}, sys.stdout)
174
+ sys.stdout.write("\n")
175
+ return 2
176
+
177
+ report = validate_plan(plan)
178
+ if args.template:
179
+ declared = plan.get("template") or args.template
180
+ report["template"] = str(declared)
181
+ if not Path(declared).is_file():
182
+ report["violations"].append({"code": "template_missing", "severity": "fail", "message": f"template not found: {declared}"})
183
+ report["passed"] = False
184
+ if args.json_out:
185
+ Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
186
+ json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
187
+ sys.stdout.write("\n")
188
+ return 0 if report["passed"] else 1
189
+
190
+
191
+ if __name__ == "__main__":
192
+ raise SystemExit(main())