minimal-agent 0.8.2 → 0.10.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 (484) hide show
  1. package/README.md +14 -12
  2. package/package.json +34 -1
  3. package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
  4. package/plugins/cron-runner/commands/cron.md +26 -0
  5. package/plugins/cron-runner/commands/crons.md +7 -0
  6. package/plugins/cron-runner/plugin.js +34 -0
  7. package/plugins/cron-runner/src/commands.js +348 -0
  8. package/plugins/cron-runner/src/cronTime.js +148 -0
  9. package/plugins/cron-runner/src/lock.js +68 -0
  10. package/plugins/cron-runner/src/runs.js +89 -0
  11. package/plugins/cron-runner/src/scheduleEngine.js +72 -0
  12. package/plugins/cron-runner/src/scheduler.js +355 -0
  13. package/plugins/cron-runner/src/schema.js +140 -0
  14. package/plugins/cron-runner/src/service.js +167 -0
  15. package/plugins/cron-runner/src/store.js +298 -0
  16. package/plugins/cron-runner/src/types.js +1 -0
  17. package/plugins/workflow-runner/commands/workflow.md +4 -4
  18. package/plugins/workflow-runner/commands/workflows.md +1 -1
  19. package/plugins/workflow-runner/plugin.js +3 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +249 -51
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +198 -15
  23. package/plugins/workflow-runner/src/index.js +40 -8
  24. package/plugins/workflow-runner/src/jsonPayload.js +1 -1
  25. package/plugins/workflow-runner/src/loader.js +286 -72
  26. package/plugins/workflow-runner/src/multimodal.js +37 -0
  27. package/plugins/workflow-runner/src/recovery.js +49 -39
  28. package/plugins/workflow-runner/src/runner.js +802 -287
  29. package/plugins/workflow-runner/src/stepExecutors/agent.js +157 -39
  30. package/plugins/workflow-runner/src/stepExecutors/assert.js +16 -3
  31. package/plugins/workflow-runner/src/stepExecutors/llm.js +60 -111
  32. package/plugins/workflow-runner/src/stepExecutors/skill.js +47 -47
  33. package/plugins/workflow-runner/src/stepExecutors/tool.js +76 -10
  34. package/plugins/workflow-runner/src/stepExecutors/verify.js +27 -10
  35. package/plugins/workflow-runner/src/taskIdentity.js +296 -0
  36. package/plugins/workflow-runner/src/types.js +19 -9
  37. package/plugins/workflow-runner/src/workflowState.js +516 -33
  38. package/skills/adversarial-review/SKILL.md +0 -1
  39. package/skills/ai-image/SKILL.md +89 -0
  40. package/skills/ai-image/scripts/generate.mjs +497 -0
  41. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  42. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  43. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  44. package/skills/browser-automation/SKILL.md +47 -0
  45. package/skills/docx/SKILL.md +25 -3
  46. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  53. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  54. package/skills/docx/scripts/opc_check.py +253 -0
  55. package/skills/image-gen-openrouter/SKILL.md +10 -2
  56. package/skills/image-stats/SKILL.md +65 -0
  57. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  58. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  59. package/skills/image-stats/scripts/image_stats.py +368 -0
  60. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  61. package/skills/pptx/SKILL.md +123 -95
  62. package/skills/pptx/design.md +82 -0
  63. package/skills/pptx/examples/README.md +29 -0
  64. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  65. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  66. package/skills/pptx/pptxgenjs.md +41 -0
  67. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  68. package/skills/pptx/scripts/layout_lint.py +293 -0
  69. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  70. package/skills/pptx/scripts/opc_check.py +339 -0
  71. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  72. package/skills/pptx/svg.md +67 -0
  73. package/skills/tts-zh/SKILL.md +19 -2
  74. package/skills/ui-design/SKILL.md +158 -0
  75. package/skills/ui-design/claude_design.md +309 -0
  76. package/skills/ui-design/evals/evals.json +254 -0
  77. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  78. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  79. package/skills/ui-design/references/design-context.md +53 -0
  80. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  81. package/skills/ui-design/references/export-pptx.md +124 -0
  82. package/skills/ui-design/references/layout-composition.md +654 -0
  83. package/skills/ui-design/references/preview-feedback.md +37 -0
  84. package/skills/ui-design/references/source-image-layout.md +189 -0
  85. package/skills/ui-design/references/starter-components.md +78 -0
  86. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  87. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  88. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  89. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  90. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  91. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  92. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  93. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  94. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  95. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  96. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  97. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  98. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  99. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  100. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  140. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  141. package/skills/ui-design/scripts/assemble_material.py +217 -0
  142. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  143. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  144. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  145. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  146. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  147. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  148. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  149. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  150. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  151. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  152. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  153. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  154. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  155. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  156. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  157. package/skills/ui-design/scripts/design_style_check.py +397 -0
  158. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  159. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  160. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  161. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  162. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  163. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  164. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  165. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  166. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  167. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  168. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  169. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  170. package/skills/ui-design/scripts/layout_balance.py +881 -0
  171. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  172. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  173. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  174. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  175. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  176. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  177. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  178. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  179. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  180. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  181. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  182. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  183. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  184. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  185. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  186. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  187. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  188. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  195. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  196. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  205. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  206. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  221. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  222. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  225. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  226. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  239. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  240. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  248. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  249. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  261. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  262. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  275. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  292. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  293. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  316. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  324. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  325. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  328. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  329. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  337. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  338. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  339. package/skills/video-compose/SKILL.md +8 -1
  340. package/skills/web-crawler/SKILL.md +70 -0
  341. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  342. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  343. package/skills/xlsx/SKILL.md +12 -0
  344. package/skills/xlsx/scripts/opc_check.py +253 -0
  345. package/src/advisorState.js +22 -0
  346. package/src/askBridge.js +97 -3
  347. package/src/bootstrap/workingDir.js +12 -1
  348. package/src/cli/args.js +17 -1
  349. package/src/cli/configMenu.js +236 -0
  350. package/src/cli/configWizard.js +7 -5
  351. package/src/cli/print.js +37 -4
  352. package/src/cli/streamJson.js +77 -24
  353. package/src/cli/wizards/advisorWizard.js +147 -0
  354. package/src/cli/wizards/fieldForm.js +149 -0
  355. package/src/cli/wizards/imageWizard.js +60 -0
  356. package/src/cli/wizards/openRouterWizard.js +23 -0
  357. package/src/cli/wizards/providerPresets.js +95 -0
  358. package/src/cli/wizards/providerWizard.js +219 -0
  359. package/src/cli/wizards/searchWizard.js +23 -0
  360. package/src/cli/wizards/ttsWizard.js +34 -0
  361. package/src/config/configFile.js +441 -83
  362. package/src/config/configService.js +446 -0
  363. package/src/config.js +24 -64
  364. package/src/context/agentHome.js +9 -0
  365. package/src/context/archive.js +7 -2
  366. package/src/context/compact.js +8 -5
  367. package/src/context/contextLock.js +81 -0
  368. package/src/context/persistContext.js +30 -11
  369. package/src/context/reactiveCompact.js +4 -2
  370. package/src/context/recentDirs.js +21 -61
  371. package/src/context/sessionPath.js +97 -23
  372. package/src/context/sessionRegistry.js +303 -24
  373. package/src/context/tokenCounter.js +18 -0
  374. package/src/context/workspaceIndex.js +731 -0
  375. package/src/infra/verification.js +28 -15
  376. package/src/llm/client.js +394 -159
  377. package/src/llm/providers/anthropicFormat.js +241 -0
  378. package/src/llm/providers/openaiFormat.js +190 -0
  379. package/src/loop.js +219 -32
  380. package/src/main.js +61 -11
  381. package/src/messageQueue.js +97 -0
  382. package/src/multimodal.js +203 -0
  383. package/src/plugin-sdk.js +15 -0
  384. package/src/plugins/commandRouter.js +3 -0
  385. package/src/plugins/hookEngine.js +5 -1
  386. package/src/plugins/pluginLifecycle.js +28 -0
  387. package/src/plugins/pluginLoader.js +2 -1
  388. package/src/plugins/pluginRunner.js +49 -21
  389. package/src/plugins/transcript.js +4 -3
  390. package/src/prompts/pluginList.js +5 -5
  391. package/src/prompts/projectInstructions.js +3 -3
  392. package/src/prompts/promptAssembly.js +133 -144
  393. package/src/prompts/skillList.js +12 -22
  394. package/src/prompts/system.js +0 -1
  395. package/src/session/runAgentTurn.js +70 -0
  396. package/src/session/runTurn.js +41 -6
  397. package/src/toolGate.js +17 -0
  398. package/src/tools/advisor/advisor.js +379 -0
  399. package/src/tools/agent/agent.js +555 -0
  400. package/src/tools/agent/agentTypes.js +59 -0
  401. package/src/tools/agent/coordinator.js +93 -0
  402. package/src/tools/agent/worktree.js +270 -0
  403. package/src/tools/ask/ask.js +7 -2
  404. package/src/tools/bash/bash.js +112 -18
  405. package/src/tools/download/download.js +144 -0
  406. package/src/tools/edit/edit.js +1 -0
  407. package/src/tools/edit/multi-edit.js +1 -0
  408. package/src/tools/genimage/genimage.js +242 -0
  409. package/src/tools/genspeech/genspeech.js +67 -0
  410. package/src/tools/glob/glob.js +1 -0
  411. package/src/tools/grep/grep.js +3 -0
  412. package/src/tools/grep/rgPath.js +2 -0
  413. package/src/tools/index.js +25 -2
  414. package/src/tools/read/read.js +1 -0
  415. package/src/tools/shared/killTree.js +52 -0
  416. package/src/tools/toolProgressBridge.js +48 -0
  417. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  418. package/src/tools/webbrowser/browser.js +59 -42
  419. package/src/tools/webbrowser/browserControl.js +118 -0
  420. package/src/tools/webbrowser/browserDaemon.js +97 -0
  421. package/src/tools/webbrowser/browserService.js +27 -0
  422. package/src/tools/webbrowser/browserStream.js +119 -0
  423. package/src/tools/webbrowser/webbrowser.js +699 -130
  424. package/src/tools/webfetch/webfetch.js +1 -0
  425. package/src/tools/websearch/websearch.js +134 -51
  426. package/src/tools/write/write.js +1 -0
  427. package/src/ui/App.js +249 -22
  428. package/src/ui/AskQuestion.js +29 -10
  429. package/src/ui/InputBox.js +104 -12
  430. package/src/ui/LiveArea.js +112 -19
  431. package/src/ui/MessageList.js +100 -14
  432. package/src/ui/ModelPicker.js +56 -0
  433. package/src/ui/QueuedMessagesBanner.js +14 -0
  434. package/src/ui/Root.js +18 -10
  435. package/src/ui/SessionPicker.js +102 -0
  436. package/src/ui/StatusLine.js +13 -4
  437. package/src/ui/WorkspacePicker.js +55 -0
  438. package/src/ui/figures.js +44 -0
  439. package/src/ui/hooks/askSlot.js +82 -0
  440. package/src/ui/hooks/useChat.js +358 -64
  441. package/src/ui/hooks/usePasteHandler.js +11 -1
  442. package/src/ui/hooks/useTextBuffer.js +1 -3
  443. package/src/ui/liveViewport.js +5 -1
  444. package/src/ui/markdown.js +206 -0
  445. package/src/ui/textBuffer.js +0 -7
  446. package/src/ui/theme.js +32 -0
  447. package/src/utils/debugLog.js +0 -4
  448. package/src/utils/resourcePaths.js +2 -2
  449. package/src/utils/stripThink.js +50 -0
  450. package/src/web/browserBridge.js +447 -0
  451. package/src/web/protocol.js +19 -0
  452. package/workflows/adaptive-research.yaml +13 -5
  453. package/workflows/book-review-short.yaml +36 -10
  454. package/workflows/e2e-write-greet.yaml +6 -2
  455. package/workflows/illustrate.yaml +227 -0
  456. package/workflows/plan.yaml +460 -179
  457. package/workflows/ppt.yaml +1659 -0
  458. package/workflows/ralph-loop.yaml +199 -0
  459. package/workflows/schema.json +81 -23
  460. package/workflows/webppt.yaml +496 -0
  461. package/workflows/word.yaml +710 -0
  462. package/workflows/youtube-shorts.yaml +46 -15
  463. package/MINIMAL-TRACE.md +0 -77
  464. package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
  465. package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
  466. package/plugins/ralph-wiggum/README.md +0 -197
  467. package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
  468. package/plugins/ralph-wiggum/commands/help.md +0 -137
  469. package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
  470. package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
  471. package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
  472. package/plugins/ralph-wiggum/plugin.js +0 -280
  473. package/plugins/ralph-wiggum/src/goalState.js +0 -258
  474. package/plugins/ralph-wiggum/src/sentinels.js +0 -76
  475. package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
  476. package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
  477. package/plugins/workflow-runner/src/miniReAct.js +0 -223
  478. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  479. package/skills/config/SKILL.md +0 -123
  480. package/skills/frontend-design/LICENSE.txt +0 -177
  481. package/skills/frontend-design/SKILL.md +0 -42
  482. package/src/ui/ProgressPanel.js +0 -54
  483. package/src/ui/StreamingBlock.js +0 -11
  484. package/src/ui/ToolStatus.js +0 -11
@@ -0,0 +1,881 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ layout_balance.py —— HTML→PPTX「构图均衡」确定性闸门(与 opc_check.py / layout_lint.py 互补)。
5
+
6
+ 为什么需要这个脚本?
7
+ ====================
8
+ `export_pptx_from_html.mjs` 是**忠实搬运**:把 HTML 里每个元素的 getBoundingClientRect 线性映射到
9
+ 幻灯片坐标。所以 HTML 里「内容只占半张、挤在一角、文字框互相叠加」会被**逐像素照搬**进 PPT。
10
+ 这些缺陷此前**没有任何确定性检测器**:`check_ui_artifact.py` 只查分层标注,`opc_check.py` 只查
11
+ OPC 合规,`pptx/layout_lint.py` 是为**原生 addShape 路径**调的(残桩连线 / shape 填充对比),在
12
+ HTML 导出产物上几乎全部失效。于是「散文级构图铁律(黄金比例 / 禁止空半 / 禁止挤角)」只活在
13
+ `references/layout-composition.md`,弱模型根本不遵守,构图全靠运气。
14
+
15
+ 本脚本读 `export_pptx_from_html.mjs` 写出的 `export-manifest.json`(其中 `slides[].balance` 已含
16
+ 确定性几何指标),把三类**可计算**的构图缺陷在产物上拦下,给弱模型一道**绕不过的防呆门槛**
17
+ (与 layout_lint.py 同哲学,但作用在**对的产物**——HTML 几何,覆盖**对的缺陷**——均衡/空半/叠加)。
18
+
19
+ 检查项(任一 ERROR → 非零退出码 + 结构化问题:类型 · 位置 · 量化范围 + 可选修法提示)
20
+ ======================================================
21
+ 设计哲学(按用户原则):闸门**只确定性地描述「哪种失衡 · 在哪页/哪半区 · 量化到多少」**,像 zod 报错——
22
+ **不替 LLM 决定怎么改**。每条消息后缀的「|可参考修法(非强制)」仅为提示,具体修法由调用方自定。
23
+ ★★ 当前策略(2026-06-19 起):死守「重叠 / 截图夹带文本」,放宽「空」。
24
+ 占空比族(B 页面过空 / 过半页面过空 / C·C' 空半挤角 / D 空心卡片)重新参与检测,
25
+ 但默认作为 WARNING 反馈给生成循环,不作为硬门反复烧轮次;`content_boxes==0`「整页啥也没有」
26
+ 仍是最小 sanity。重叠/裁切的「可参考修法」顺序为 **移动/重排 > 精炼文案 > 缩字(单次 ≤20%、仍 < 主标题)**。
27
+ ★ 死守「重叠」、放宽「空」:
28
+ A. 内容相互叠加:非包含关系的文本/前景框显著相交(封面「标签/副标题压在大标题上」即此类)。**始终 ERROR。**
29
+ (同一前景块内的文本互叠——如 SVG 架构图密排标签——由导出器豁免,不计入。)
30
+ B. 页面过空:内容覆盖率 < COVER_MIN。**单页仅 WARNING + 记账**;「过半有内容页都过空」也只做 WARNING。
31
+ 整页啥也没有(content_boxes==0):**有真实底图/视觉(hasBgImage/hasForegroundVisual)→ WARNING(纯图封面合法);
32
+ 否则 ERROR** —— 这条历来称「始终 ERROR」实为「无真实视觉才 ERROR」,坏页曾因 hasBgImage 误判(见 export
33
+ Gap A 修复)被误当纯图封面降级放行,故此处口径与代码严格对齐。
34
+ C0. 内容严重飞出画布(catastrophic):某元素可见部分 < OFFCANVAS_VISIBLE_MIN(如轨道被大图撑爆致内容跑到 x≈0.99)
35
+ = 观众根本看不到 → 计入 catastrophic 集合;`--strict-offcanvas` 时升 ERROR,`--catastrophic-only` 时是唯一硬门来源。
36
+ C. 空半 / 挤角:某半区几乎无内容、对侧却满,或内容挤在一角。**有底图/前景视觉块 → WARNING**;无任何视觉才 ERROR。
37
+ D. 空心卡片:**大**纯文本卡「卡内文字面积 ÷ 卡面积」< CARD_FILL_MIN(小卡/装饰件豁免)。有视觉/底图 → WARNING,
38
+ 唯「大卡 + 无任何视觉」才 ERROR。媒体卡(hasMedia:含 img/svg/图标/大视觉块)本就不进检查。
39
+ E. 文本裁切:文本内容显著超出自身框(导出会缩字/截断)。WARNING(提示,不拦;HTML 阶段就该让文字装得下)。
40
+
41
+ 「空」类(B/C/D)降级依据 = `hasBgImage`(真实大图铺满)**或** `hasForegroundVisual`(切出图的 img/svg/图标/
42
+ 大视觉块)——「有图不算空」。背景渐变/纯色不计入内容覆盖。**主门恒是「真重叠」(A,始终 ERROR)。**
43
+
44
+ 和 opc_check / layout_lint 一样:只用标准库(json / argparse / os / sys),忠实于最终产物,不替代视觉 QA。
45
+
46
+ 用法
47
+ ====
48
+ python layout_balance.py <export-manifest.json> # 或传 workdir 目录,自动找 export-manifest.json
49
+ python layout_balance.py <dir> --verbose # 额外打印每页指标明细
50
+ """
51
+
52
+ import argparse
53
+ import json
54
+ import os
55
+ import sys
56
+
57
+ # 阈值(与 export_pptx_from_html.mjs 的 16×8 网格 + computeSlideBalance 配套)
58
+ #
59
+ # ★ 策略大改(2026-06-17):实跑反复卡「空/不够满」→ 来回改 → 烧光 provider 5h 配额。用户裁决:
60
+ # **死守「重叠」,大幅放宽「空」**。重叠是真碰撞、一眼可见、改一次就好,必须硬拦;「空」是主观的、
61
+ # 每页已要求 ≥1 视觉元素(AI图/SVG/图标),有图的页本就不算空,逼 AI 把每页填到高覆盖率只会
62
+ # 无限烧轮次。于是「空」类全面降级:
63
+ # - **始终 ERROR(硬门)**:① 内容相互叠加(主门,报元素+重叠区+归一化坐标)
64
+ # ② 截图夹带 HTML/SVG 文本 ③ 整页啥也没有(content_boxes==0 且无视觉)。
65
+ # - **降级 WARNING(软门,提示不拦)**:单页过空、空半/挤角、空心卡片、文本裁切——尤其当本页
66
+ # **有底图或前景视觉块(图/SVG/图标)时**一律 WARNING(「有图不算空」)。
67
+ #
68
+ # COVER_MIN 0.26(标定史 0.42→0.55→0.45→0.40→0.26):仅用于「单页是否记一笔过空账 + 是否参与
69
+ # 『过半页空』统计」,单页低覆盖不再直接 FAIL。content_boxes==0 的真空页仍 ERROR(「就怕啥也没有」)。
70
+ # CARD_FILL_MIN 0.28:纯文本卡填充率阈值(放宽自 0.35);且仅 **大卡**(面积 ≥ CARD_MIN_AREA_FRAC)
71
+ # 才量——小卡(引文 quote-pull / 装饰条 / 药丸)豁免(空心门本意是抓「大卡装一句话」,非装饰件)。
72
+ # 命中也按 downgradeable 处理:有视觉/底图的页降 WARNING,唯「大卡 + 无任何视觉」才 ERROR。
73
+ # 媒体卡(hasMedia:含 img/svg/图标/大视觉块)本就不进 cards——「卡内有图标不算空」已被导出器豁免。
74
+ COVER_MIN = 0.26 # 单页内容覆盖率低于此 → 记一笔过空账(单页 WARNING;过半也只整体 WARNING)
75
+ CARD_FILL_MIN = 0.28 # 大纯文本卡填充率低于此 → 空心卡(可降级:有视觉/底图→WARNING)
76
+ CARD_MIN_AREA_FRAC = 0.06 # 卡面积(归一化 w×h)低于此 = 小卡(引文/装饰条/药丸),豁免空心检查
77
+ EMPTY_PAGE_RATIO = 0.5 # 过空页占(有内容页)比例高于此 → 整体 WARNING(「不能页页空」)
78
+ MIN_PAGES_FOR_EMPTY_RATIO = 3 # 有内容页 ≥ 此数才做「过半页空」整体判定(1~2 页稀疏可能是设计选择,只 WARNING)
79
+ EMPTY_FRAC = 0.06 # 半区占用率低于此 → 视为「该半区几乎空白」
80
+ OPP_MIN = 0.16 # 对侧半区占用率高于此,才把「这侧空」判为失衡(排除整页都空的退化情形)
81
+ MAX_OVERLAP_REPORT = 6 # 单页最多列几条重叠,避免刷屏
82
+ OFFCANVAS_VISIBLE_MIN = 0.20 # 越界元素「可见部分 ÷ 元素自身面积」低于此 = 内容实质飞出画布(catastrophic)。
83
+ # s9 事故:grid 轨道被大图撑爆,标题/正文全跑到 x≈0.99,可见 ~6% → 观众看不到。轻微越界(可见>20%)仍只 WARNING。
84
+
85
+ # ★ --ppt-strict 专属阈值(2026-07-02 逐页体检裁决,见 ppt.yaml 阶段18):默认模式一概不读,webppt 行为不变。
86
+ # 重叠去噪:标题/副标题行盒贴边(0.01h 细带)与「膨胀行盒尾部盖住下方块」是 box 模型伪象,不是视觉碰撞——
87
+ # 真机 deck 一页能刷出十几条,把真重叠(标题被卡压/药丸穿标题)淹没,修复 agent 追鬼。三条豁免规则见
88
+ # _ppt_strict_overlap_noise()。空卡升级:大面积纯空卡(slide05 白板/slide14 巨卡)从「有图可接受」升 ERROR,
89
+ # 但被媒体块盖住 ≥PPT_STRICT_MEDIA_COVER 的卡是假空(图为兄弟绝对定位、DOM 不在卡内),豁免。
90
+ PPT_STRICT_THIN_BAND = 0.02 # 重叠带宽/高低于此(归一化) + 占比小 = 行盒贴边噪声
91
+ PPT_STRICT_THIN_FRAC = 0.30 # 贴边带同时要求 fracOfSmaller 低于此才豁免(大占比贴边仍是真碰撞)
92
+ PPT_STRICT_CONTAIN_FRAC = 0.90 # 小框 ≥90% 被大框含住
93
+ PPT_STRICT_CONTAIN_RATIO = 3.0 # 且大框面积 ≥3× 小框 → 父级膨胀行盒包含子元素,非碰撞
94
+ PPT_STRICT_TAIL_FRAC = 0.35 # 重叠位于文本框底部 ≤35% 条带且高度 ≤35% 框高 → 行盒尾部伪象
95
+ PPT_STRICT_CARD_AREA_MIN = 0.15 # 卡面积 ≥15% 页面才算「大空卡」候选
96
+ PPT_STRICT_CARD_FILL_MAX = 0.10 # 填充 <10% 才升 ERROR(10%~28% 仍走默认 WARNING)
97
+ PPT_STRICT_MEDIA_COVER = 0.70 # 单个媒体块盖住卡 ≥70% = 图卡假空,豁免
98
+ PPT_STRICT_ESCAPE_TOL = 0.012 # 文字溢出卡片容差(占画布边长比例):行盒舍入/降部噪声,低于此不报。
99
+ PPT_STRICT_CAPTION_CONTAIN = 0.85 # 文字 ≥85% 落在媒体卡内 = caption/嵌套版式(fgParent 断链)→ 降 WARNING
100
+ PPT_STRICT_MEDIA_TAGS = {"img", "svg", "picture", "video", "canvas"} # 裸媒体元素自身作前景块(hasMedia 只查后代恒 false)
101
+ PPT_STRICT_INFLATION_TOL = 0.008 # R6 原点匹配容差:重叠框与原始 textRun 的 (x,y) 距离(归一化)
102
+ PPT_STRICT_RAW_OVERLAP_MIN = 0.05 # R6 用原始行盒重算后交叠占比低于此 = 导出加宽框幻影,剔除
103
+
104
+ # ★ 信息集锦/综述型客户图(source_layout_plan.json 的 is_dense_info=true)渲染宽度下限:
105
+ # 2026-07-07 真机 p8(20+ 圆形应用场景缩略图集锦)连续 3 次生成都没达到应有的视觉份量——
106
+ # ①小绝对定位框 ②满宽但 height:auto 撑爆画布 ③crash 修好但 flex 列共享缩水到 26% 宽——
107
+ # 即便按 min_canvas_fraction(面积)算已达标(如 47%宽×26%高≈31%面积,压线通过通用 hero 下限
108
+ # 0.30),object-fit:contain 在偏方/偏高宽高比的宽扁槽里仍会让密集图标挤压到实际可见宽度过窄、
109
+ # 20+ 个并列信息点不可辨认——这是本身按面积算「合格」但按宽度看仍不达标的独立盲区,故用单独的
110
+ # 宽度专属阈值(不是用它去改写/加严 min_canvas_fraction 的既有面积语义,避免误伤其它 display_role
111
+ # 的合法窄高画幅证据图)。只在 source_layout_plan.json 存在且有 is_dense_info=true 条目时才激活,
112
+ # webppt 无此文件、此检查恒不触发。
113
+ DENSE_INFO_MIN_WIDTH_FRACTION = 0.55
114
+ # 注意:文字-父卡包含关系在导出器重叠检查里是**豁免项**(卡内文字属正常结构),所以「字顶出自家卡」
115
+ # 此前完全不可见(slide13 CTA 恰因 fgParent 断链才漏网成一条重叠)。此检查读 measure-only 报告的
116
+ # 顶层 textRuns(原始 HTML px 矩形,非 Office 加宽框)× balance.fgRects(全部前景块,含媒体卡),
117
+ # 纯数据补上这个盲区;旧 report 缺字段时静默跳过。
118
+
119
+ # ★ 占空比检测总开关(2026-06-18 停用 → 2026-06-19 用户裁决「加回但宽松」重新启用):
120
+ # 占空比族——页面过空(B) / 过半页面过空 / 空半·挤角(C/C') / 空心卡片(D)——重新参与检测,但**全程宽松**:
121
+ # - 单页缺陷一律走 `add(..., downgradeable=True)`:本页有底图/前景视觉块(图/SVG/图标)→ WARNING 不拦
122
+ # (配合「每页 ≥1 视觉元素」铁律,正常 deck 几乎都降 WARNING);唯「真空白页 + 无任何视觉」才 ERROR。
123
+ # - 「过半页面过空」整体判定也降为 **WARNING**(不再整体 ERROR)。
124
+ # 动因:用户要它**提示**「目录右半空」这类失衡(暴露给 build/export 循环去改),但**不硬拦**——守住「别再为
125
+ # 『够不够满』反复改、烧光 provider 配额」的教训(真实事故)。重叠 / 截图夹带文本 / 字号 / 溢出各检测互不影响。
126
+ # 置回 False 即一键全停(阈值常量 COVER_MIN/EMPTY_*/CARD_* 与代码全部保留、未删,仅被本开关守卫)。
127
+ OCCUPANCY_CHECKS_ENABLED = True
128
+
129
+
130
+ def _fmt_rect(r):
131
+ """把归一化 rect {x,y,w,h}(0–1,原点左上)格式化为可读坐标串;
132
+ 缺字段(旧 manifest / 未富化的 measure 产物)时返回空串,调用方优雅降级。"""
133
+ if not isinstance(r, dict):
134
+ return ""
135
+ try:
136
+ return (
137
+ f"[x{float(r.get('x', 0)):.2f} y{float(r.get('y', 0)):.2f} "
138
+ f"w{float(r.get('w', 0)):.2f} h{float(r.get('h', 0)):.2f}]"
139
+ )
140
+ except (TypeError, ValueError):
141
+ return ""
142
+
143
+
144
+ def _visible_fraction(rect) -> float:
145
+ """归一化 rect{x,y,w,h}(0–1,原点左上)与画布 [0,1]×[0,1] 相交面积 ÷ 元素自身面积。
146
+ = 元素「有多少落在可视区内」。1.0=完全在内,接近 0=几乎全飞出画布。
147
+ 字段缺失/退化时返回 1.0(保守:宁可不误判为 catastrophic)。"""
148
+ if not isinstance(rect, dict):
149
+ return 1.0
150
+ try:
151
+ x = float(rect.get("x", 0.0))
152
+ y = float(rect.get("y", 0.0))
153
+ w = float(rect.get("w", 0.0))
154
+ h = float(rect.get("h", 0.0))
155
+ except (TypeError, ValueError):
156
+ return 1.0
157
+ if w <= 0 or h <= 0:
158
+ return 1.0
159
+ vis_w = max(0.0, min(x + w, 1.0) - max(x, 0.0))
160
+ vis_h = max(0.0, min(y + h, 1.0) - max(y, 0.0))
161
+ return (vis_w * vis_h) / (w * h)
162
+
163
+
164
+ def _rect_area(r) -> float:
165
+ if not isinstance(r, dict):
166
+ return 0.0
167
+ try:
168
+ return max(0.0, float(r.get("w") or 0.0)) * max(0.0, float(r.get("h") or 0.0))
169
+ except (TypeError, ValueError):
170
+ return 0.0
171
+
172
+
173
+ def _rect_intersect_area(a, b) -> float:
174
+ """两个归一化 rect 的相交面积;字段缺失返回 0。"""
175
+ if not isinstance(a, dict) or not isinstance(b, dict):
176
+ return 0.0
177
+ try:
178
+ ax, ay, aw, ah = (float(a.get(k) or 0.0) for k in ("x", "y", "w", "h"))
179
+ bx, by, bw, bh = (float(b.get(k) or 0.0) for k in ("x", "y", "w", "h"))
180
+ except (TypeError, ValueError):
181
+ return 0.0
182
+ iw = min(ax + aw, bx + bw) - max(ax, bx)
183
+ ih = min(ay + ah, by + bh) - max(ay, by)
184
+ return max(0.0, iw) * max(0.0, ih)
185
+
186
+
187
+ def _ppt_strict_overlap_noise(ov) -> bool:
188
+ """--ppt-strict 的重叠去噪:True = 判为 box 模型伪象、剔除不报。
189
+
190
+ 四条规则(2026-07-02 逐页体检标定,对照真机 deck 的 57 条重叠逐条验证):
191
+ R1 行盒贴边:重叠带极细(min(w,h) < THIN_BAND)且占小框比例低 —— 标题/副标题 line-box 挨着。
192
+ R2 膨胀父盒包含:小框 ≥90% 被含住且大框 ≥3× —— 段落 rect 比字形区大得多,把下方 chip 圈进来。
193
+ R3 行盒尾部(纵向):重叠只落在某文本框底部 ≤35% 条带内且高度 ≤35% 框高 —— 两行文字的 DOM 框
194
+ 比实际字形高(h≈0.21 而字只占上 0.12),尾部空白盖住下方块。
195
+ R4 行盒尾部(横向):重叠只落在某文本框右侧 ≤35% 条带内且宽度 ≤35% 框宽 —— 短文字撑满整列的
196
+ DOM 框(「一套系统」4 个字、框宽 0.34),右侧空白蹭到相邻卡。
197
+ 真重叠(标题被卡压 22%/0.04h、buried 文本堆叠 88%)四条都豁免不掉,仍照报 ERROR。
198
+ """
199
+ r = ov.get("rect") or {}
200
+ try:
201
+ rw = float(r.get("w") or 0.0)
202
+ rh = float(r.get("h") or 0.0)
203
+ ry = float(r.get("y") or 0.0)
204
+ frac = float(ov.get("fracOfSmaller") or 0.0)
205
+ except (TypeError, ValueError):
206
+ return False
207
+ # R1 行盒贴边
208
+ if min(rw, rh) < PPT_STRICT_THIN_BAND and frac < PPT_STRICT_THIN_FRAC:
209
+ return True
210
+ a_rect = ov.get("aRect") or {}
211
+ b_rect = ov.get("bRect") or {}
212
+ area_a = _rect_area(a_rect)
213
+ area_b = _rect_area(b_rect)
214
+ big = max(area_a, area_b)
215
+ small = min(area_a, area_b)
216
+ # R2 膨胀父盒包含——仅 text⊃text(段落行盒比字形区大得多、把下方文字 chip 圈进来是伪象)。
217
+ # text⊃fg(文本框整个含住一张前景卡)**不豁免**:那是「字骑在卡上」(slide13 CTA 真缺陷,
218
+ # Office 加宽框恰好把面积比推过 3× 曾被此规则误杀);fg 含 text 的正常结构已在导出器按
219
+ # fgParent 豁免,走到这里的 fg×text 包含都值得报。
220
+ if frac >= PPT_STRICT_CONTAIN_FRAC and small > 0 and big >= PPT_STRICT_CONTAIN_RATIO * small:
221
+ if ov.get("aKind") == "text" and ov.get("bKind") == "text":
222
+ return True
223
+ # R3/R4 行盒尾部(对文本框逐个试;纵向底部条带 / 横向右侧条带)
224
+ try:
225
+ rx = float(r.get("x") or 0.0)
226
+ except (TypeError, ValueError):
227
+ rx = 0.0
228
+ for rect, kind in ((a_rect, ov.get("aKind")), (b_rect, ov.get("bKind"))):
229
+ if kind != "text" or not isinstance(rect, dict):
230
+ continue
231
+ try:
232
+ th = float(rect.get("h") or 0.0)
233
+ ty = float(rect.get("y") or 0.0)
234
+ tw = float(rect.get("w") or 0.0)
235
+ tx = float(rect.get("x") or 0.0)
236
+ except (TypeError, ValueError):
237
+ continue
238
+ if th > 0 and rh <= PPT_STRICT_TAIL_FRAC * th and ry >= ty + (1.0 - PPT_STRICT_TAIL_FRAC) * th:
239
+ return True
240
+ if tw > 0 and rw <= PPT_STRICT_TAIL_FRAC * tw and rx >= tx + (1.0 - PPT_STRICT_TAIL_FRAC) * tw:
241
+ return True
242
+ return False
243
+
244
+
245
+ def _run_rect_norm(run, canvas_w, canvas_h):
246
+ """textRun 的原始 HTML px 矩形 → 归一化 rect;字段坏/画布无效返回 None。"""
247
+ try:
248
+ rect = {
249
+ "x": float(run.get("x") or 0.0) / canvas_w,
250
+ "y": float(run.get("y") or 0.0) / canvas_h,
251
+ "w": float(run.get("w") or 0.0) / canvas_w,
252
+ "h": float(run.get("h") or 0.0) / canvas_h,
253
+ }
254
+ except (TypeError, ValueError, ZeroDivisionError):
255
+ return None
256
+ if rect["w"] <= 0 or rect["h"] <= 0:
257
+ return None
258
+ return rect
259
+
260
+
261
+ def _match_raw_run(rect, raw_runs, canvas_w, canvas_h):
262
+ """按 (x,y) 原点把重叠框匹配回同 slide 的原始 textRun 行盒(导出加宽只改 w/h、不动原点)。
263
+ 同一原点多个匹配取面积最小的(最接近真实字形区);无匹配返回 None。"""
264
+ try:
265
+ rx = float(rect.get("x") or 0.0)
266
+ ry = float(rect.get("y") or 0.0)
267
+ except (TypeError, ValueError):
268
+ return None
269
+ best = None
270
+ for run in raw_runs:
271
+ rr = _run_rect_norm(run, canvas_w, canvas_h)
272
+ if not rr:
273
+ continue
274
+ if abs(rr["x"] - rx) <= PPT_STRICT_INFLATION_TOL and abs(rr["y"] - ry) <= PPT_STRICT_INFLATION_TOL:
275
+ if best is None or rr["w"] * rr["h"] < best["w"] * best["h"]:
276
+ best = rr
277
+ return best
278
+
279
+
280
+ def _ppt_strict_frame_inflation(ov, raw_runs, canvas_w, canvas_h) -> bool:
281
+ """R6 导出加宽框幻影(2026-07-03 真机 09 页副标题案):重叠几何用的是导出文本框
282
+ (自动换行/行距余量加宽后,h 可达真实行盒 2×)——副标题在 HTML 里与下方卡片根本不相交,
283
+ 也会被报成【内容相互叠加】ERROR(09 页一页 3 条全是它)。用同 slide 的原始 textRuns
284
+ (HTML px 矩形)按原点匹配回真实行盒、重算交叠:几乎为零则判幻影剔除。
285
+ 找不到匹配原始行盒 → 不动(旧报告缺 textRuns 时行为不变)。"""
286
+ sides = []
287
+ matched_any = False
288
+ for rect_key, kind_key in (("aRect", "aKind"), ("bRect", "bKind")):
289
+ rect = ov.get(rect_key)
290
+ if not isinstance(rect, dict):
291
+ return False
292
+ if ov.get(kind_key) == "text":
293
+ raw = _match_raw_run(rect, raw_runs, canvas_w, canvas_h)
294
+ if raw is not None:
295
+ # 原始行盒比导出框还大属异常,不采信(守恒:只用来收窄,不放大)。
296
+ if raw["w"] * raw["h"] < _rect_area(rect):
297
+ rect = raw
298
+ matched_any = True
299
+ sides.append(rect)
300
+ if not matched_any:
301
+ return False
302
+ inter = _rect_intersect_area(sides[0], sides[1])
303
+ small = min(_rect_area(sides[0]), _rect_area(sides[1]))
304
+ if small <= 0:
305
+ return False
306
+ return (inter / small) < PPT_STRICT_RAW_OVERLAP_MIN
307
+
308
+
309
+ def _ppt_strict_media_caption(ov, media_fg_names) -> bool:
310
+ """R5 媒体卡内文字(2026-07-03 裁决):文字基本整体(≥85%)落在**含媒体**前景卡内 =
311
+ caption/图内说明字类合法嵌套版式(fgParent 断链才漏成重叠)→ 几何门降 WARNING,
312
+ 可读性交给像素门(text_readability_pixel 已覆盖卡内文字)。
313
+ 不豁免:文字框反把卡含住(text⊃fg,slide13 CTA 真缺陷)、跨卡骑边(部分压边)、
314
+ 非媒体卡包含(不透明白卡盖字是像素门盲区,宁可误报)。"""
315
+ a_kind, b_kind = ov.get("aKind"), ov.get("bKind")
316
+ if {a_kind, b_kind} != {"text", "fg"}:
317
+ return False
318
+ if a_kind == "text":
319
+ t_rect, f_rect, f_name = ov.get("aRect"), ov.get("bRect"), ov.get("b")
320
+ else:
321
+ t_rect, f_rect, f_name = ov.get("bRect"), ov.get("aRect"), ov.get("a")
322
+ if f_name not in media_fg_names:
323
+ return False
324
+ t_area = _rect_area(t_rect)
325
+ f_area = _rect_area(f_rect)
326
+ if t_area <= 0 or f_area <= 0 or t_area > f_area:
327
+ return False
328
+ return (_rect_intersect_area(t_rect, f_rect) / t_area) >= PPT_STRICT_CAPTION_CONTAIN
329
+
330
+
331
+ def _stem(name):
332
+ return os.path.splitext(os.path.basename(str(name or "")))[0]
333
+
334
+
335
+ def _load_dense_info_stems(source_layout_plan_path):
336
+ """读 source_layout_plan.json,只为 is_dense_info==true 的条目建 {stem: 原始 file 名} 映射,
337
+ 候选 stem 同时含原图 file 与 enhanced_file(与 source_usage_check.py::_derived_candidates 同一套
338
+ 约定——deck.html 引用的通常是 enhanced_ 派生图)。路径缺失/解析失败/无此类条目一律返回空 dict,
339
+ 调用方据此静默跳过(该文件是 ppt 独有产物,webppt 不产出,天然不影响 webppt 行为)。"""
340
+ if not source_layout_plan_path or not os.path.isfile(source_layout_plan_path):
341
+ return {}
342
+ try:
343
+ with open(source_layout_plan_path, "r", encoding="utf-8") as f:
344
+ plan = json.load(f)
345
+ except (OSError, json.JSONDecodeError):
346
+ return {}
347
+ if not isinstance(plan, dict):
348
+ return {}
349
+ result = {}
350
+ for im in (plan.get("images") or []):
351
+ if not isinstance(im, dict) or not im.get("is_dense_info"):
352
+ continue
353
+ orig_file = im.get("file") or "?"
354
+ for name in (im.get("file"), im.get("enhanced_file")):
355
+ if isinstance(name, str) and name:
356
+ result[_stem(name)] = orig_file
357
+ return result
358
+
359
+
360
+ def _resolve_manifest_path(path: str) -> str:
361
+ """传目录则补报告文件,传文件原样返回。
362
+
363
+ 优先 layout-report.json(export --measure-only 的廉价 HTML 层量构图,不出 pptx),
364
+ 其次 export-manifest.json(全量导出产物)。两者 schema 同源(mode + slides[].balance),
365
+ 判定逻辑完全一致——构图闸门前置到 HTML 层后,循环读的就是 layout-report.json。
366
+ """
367
+ if os.path.isdir(path):
368
+ layout_report = os.path.join(path, "layout-report.json")
369
+ if os.path.isfile(layout_report):
370
+ return layout_report
371
+ return os.path.join(path, "export-manifest.json")
372
+ return path
373
+
374
+
375
+ def check_balance(manifest_path: str, verbose: bool = False, occupancy_checks_enabled=None,
376
+ strict_offcanvas: bool = False, catastrophic_only: bool = False,
377
+ ppt_strict: bool = False, source_layout_plan_path: str = None):
378
+ """返回 (problems, warnings, detail_lines)。problems 非空 = 有 ERROR 级构图缺陷。
379
+
380
+ strict_offcanvas:内容严重飞出画布(可见 < OFFCANVAS_VISIBLE_MIN)升为 ERROR(默认仅 WARNING,行为不变)。
381
+ catastrophic_only:只把「内容不可见灾难」(严重越界 + 无真实视觉的空页)作为 problems 返回,
382
+ 抑制重叠/夹带/占空族等既有信号——供 visual_quality_gate 做「回归安全」的硬门(不误伤只是相邻/嵌套的
383
+ 良性重叠计数,守住『别再为占空反复烧配额』红线)。默认 False,逐字节保持既有行为。
384
+ ppt_strict:ppt 工作流视觉门专用严格档(2026-07-02 逐页体检裁决)——①重叠去噪(剔行盒贴边/膨胀父盒/
385
+ 行盒尾部三类伪象,真重叠照报)②严重越界升 ERROR(含 strict_offcanvas)③大面积纯空卡升 ERROR
386
+ (被媒体块盖住 ≥70% 的图卡豁免,需 manifest 带 mediaRects)。默认 False,webppt 等默认调用行为不变。
387
+ source_layout_plan_path:可选,传入 ppt 工作流的 source_layout_plan.json 路径时,额外对
388
+ is_dense_info==true 的客户图按 mediaRects.sourceFile 反查其真实渲染宽度,低于
389
+ DENSE_INFO_MIN_WIDTH_FRACTION 报 WARNING(2026-07-07 p8 连续 3 次未达标后新增,只读不硬拦)。
390
+ 默认 None:不做此检查,webppt 等既有调用行为不变。"""
391
+ if occupancy_checks_enabled is None:
392
+ occupancy_checks_enabled = OCCUPANCY_CHECKS_ENABLED
393
+ if ppt_strict:
394
+ strict_offcanvas = True # 严格档天然含「严重越界升 ERROR」
395
+ if catastrophic_only:
396
+ strict_offcanvas = True # 灾难集合天然含严重越界
397
+ problems = []
398
+ warnings = []
399
+ details = []
400
+ catastrophic = [] # 「内容不可见」灾难(严重越界 + 无真实视觉空页);catastrophic_only 时作为唯一 problems 返回
401
+ dense_info_stems = _load_dense_info_stems(source_layout_plan_path)
402
+
403
+ if not os.path.isfile(manifest_path):
404
+ return ([f"找不到 manifest:{manifest_path}(先跑 export_pptx_from_html.mjs 产出 export-manifest.json)"], [], [])
405
+
406
+ try:
407
+ with open(manifest_path, "r", encoding="utf-8") as f:
408
+ manifest = json.load(f)
409
+ except (OSError, json.JSONDecodeError) as exc:
410
+ return ([f"manifest 解析失败:{exc}"], [], [])
411
+
412
+ mode = manifest.get("mode")
413
+ slides = manifest.get("slides") or []
414
+ try:
415
+ canvas_w = float(manifest.get("width") or 1920)
416
+ canvas_h = float(manifest.get("height") or 1080)
417
+ except (TypeError, ValueError):
418
+ canvas_w, canvas_h = 1920.0, 1080.0
419
+ if mode != "hybrid":
420
+ # flat 整页一张图,无前景/文本几何可量——不闸门(但说清楚,避免「以为查过了」)。
421
+ return ([], [f"导出模式为 {mode!r},非 hybrid:构图均衡闸门不适用(flat 是整页一张图)。"], [])
422
+
423
+ # 跨页统计「过空」:单页过空只记账(WARNING);过半有内容页都过空也只整体 WARNING(不硬拦)。
424
+ empty_pages = [] # 覆盖率 < COVER_MIN 的页 label
425
+ content_page_count = 0 # 有内容页(content_boxes>0)总数,作过空占比的分母
426
+
427
+ for slide in slides:
428
+ idx = slide.get("index")
429
+ label = slide.get("label") or f"slide {idx}"
430
+ b = slide.get("balance")
431
+ if not b:
432
+ warnings.append(f"{label}:缺 balance 指标(跳过;可能是旧 manifest 或非 hybrid 页)。")
433
+ continue
434
+
435
+ has_bg = bool(b.get("hasBgImage"))
436
+ has_fg_visual = bool(b.get("hasForegroundVisual")) # 切出图的 img/svg/图标/大视觉块
437
+ has_visual = has_bg or has_fg_visual # 「有图不算空」的统一判据
438
+ coverage = b.get("coverage") or 0.0
439
+ ho = b.get("halfOccupancy") or {}
440
+ overlaps = b.get("overlaps") or []
441
+ content_boxes = b.get("contentBoxes") or 0
442
+ cards = b.get("cards") or [] # 卡片/底板填充率(文本+子块面积÷卡面积;填充不足软门)
443
+ overflow_runs = b.get("overflowRuns") or [] # 显著裁切的文本(溢出软门)
444
+ small_text = b.get("smallText") or [] # 字号低于放映级下限的文本段(字号软门)
445
+ baked_text = b.get("bakedText") or [] # 背景态仍可见的 HTML/伪元素文本(截图夹带,硬门)
446
+ off_canvas = b.get("offCanvas") or [] # 越出画布的文本/前景块(超画布软门)
447
+ low_contrast = b.get("lowContrast") or [] # 文字/背景对比度不足(深底深字等;一律 WARNING,不硬拦)
448
+
449
+ def add(msg, downgradeable):
450
+ """无任何视觉 → ERROR;有底图/前景视觉块(图/SVG/图标)→ 放宽为 WARNING(「有图不算空」)。"""
451
+ if has_visual and downgradeable:
452
+ warnings.append(msg + "(本页有底图/前景视觉元素,按可接受处理)")
453
+ else:
454
+ problems.append(msg)
455
+
456
+ if verbose:
457
+ empties = [k for k in ("top", "bottom", "left", "right") if (ho.get(k) or 0.0) < EMPTY_FRAC]
458
+ hollow = [c for c in cards if (c.get("fill") or 0.0) < CARD_FILL_MIN]
459
+ details.append(
460
+ f"{label}: 覆盖 {coverage*100:.0f}% | 半区 上{(ho.get('top') or 0)*100:.0f}"
461
+ f"/下{(ho.get('bottom') or 0)*100:.0f}/左{(ho.get('left') or 0)*100:.0f}/右{(ho.get('right') or 0)*100:.0f}"
462
+ f"{' | 空半['+'/'.join(empties)+']' if empties else ''}"
463
+ f"{' | 重叠'+str(len(overlaps)) if overlaps else ''}"
464
+ f"{' | 卡'+str(len(cards))+'(空心'+str(len(hollow))+')' if cards else ''}"
465
+ f"{' | 裁切'+str(len(overflow_runs)) if overflow_runs else ''}"
466
+ f" | 底图{'有' if has_bg else '无'} 前景图{'有' if has_fg_visual else '无'} | 块{content_boxes}"
467
+ )
468
+
469
+ # ── A. 内容相互叠加(始终 ERROR,封面撞车的克星) ──
470
+ # 消息格式:【问题类型】页 · 位置/对象 · 量化范围 |可参考修法(非强制):…
471
+ # ppt_strict:先剔除行盒伪象(贴边/膨胀父盒/行盒尾部),只报真碰撞——修复 agent 不追鬼。
472
+ if ppt_strict:
473
+ overlaps = [ov for ov in overlaps if not _ppt_strict_overlap_noise(ov)]
474
+ # R6 导出加宽框幻影:用原始 textRuns 行盒重算,交叠归零的整条剔除(09 页副标题案)。
475
+ raw_runs = slide.get("textRuns") or []
476
+ if raw_runs:
477
+ overlaps = [
478
+ ov for ov in overlaps
479
+ if not _ppt_strict_frame_inflation(ov, raw_runs, canvas_w, canvas_h)
480
+ ]
481
+ # R5 媒体卡内文字:caption/图内说明字类合法嵌套(fgParent 断链)→ 降 WARNING 可见化,
482
+ # 不再吃修复轮预算;可读性由像素门裁决。旧 report 缺 fgRects → 集合为空、行为不变。
483
+ media_fg_names = {
484
+ fr.get("name")
485
+ for fr in (b.get("fgRects") or [])
486
+ if isinstance(fr, dict) and fr.get("name")
487
+ and (fr.get("hasMedia") or str(fr.get("tag") or "").lower() in PPT_STRICT_MEDIA_TAGS)
488
+ }
489
+ if media_fg_names:
490
+ caption_ids = {
491
+ id(ov) for ov in overlaps if _ppt_strict_media_caption(ov, media_fg_names)
492
+ }
493
+ if caption_ids:
494
+ for ov in overlaps:
495
+ if id(ov) not in caption_ids:
496
+ continue
497
+ t_name = ov.get("a") if ov.get("aKind") == "text" else ov.get("b")
498
+ f_name = ov.get("b") if ov.get("aKind") == "text" else ov.get("a")
499
+ warnings.append(
500
+ f"【媒体卡内文字】{label} · 「{t_name}」基本整体落在媒体卡「{f_name}」内"
501
+ f"(caption/嵌套版式,fgParent 断链)——几何门不判错,可读性由像素门裁决;"
502
+ f"若确属压图看不清请给文字加紧贴 scrim 或恢复 DOM 嵌套"
503
+ )
504
+ overlaps = [ov for ov in overlaps if id(ov) not in caption_ids]
505
+ for ov in overlaps[:MAX_OVERLAP_REPORT]:
506
+ a_rect = _fmt_rect(ov.get("aRect"))
507
+ b_rect = _fmt_rect(ov.get("bRect"))
508
+ o_rect = _fmt_rect(ov.get("rect"))
509
+ # 富报:哪两个元素@归一化坐标 × 重叠区@归一化坐标(坐标 = 元素左上/宽高 ÷ 画布宽高,0–1)。
510
+ # 字段缺失(旧 manifest)时降级为元素名对。
511
+ if o_rect:
512
+ where = (
513
+ f" · 「{ov.get('a')}」{a_rect} ×「{ov.get('b')}」{b_rect}"
514
+ f" · 重叠区{o_rect}(归一化 0–1,原点左上)"
515
+ )
516
+ else:
517
+ where = f" · 「{ov.get('a')}」×「{ov.get('b')}」"
518
+ # ppt_strict:文字与前景卡高包含重叠、且导出器没按 fgParent 豁免掉 = 多半是 wrapper
519
+ # 断链的「字骑出自家卡」(slide13 CTA 案)——补一条针对性的修法方向。
520
+ escape_hint = ""
521
+ if (
522
+ ppt_strict
523
+ and {ov.get("aKind"), ov.get("bKind")} == {"text", "fg"}
524
+ and (ov.get("fracOfSmaller") or 0.0) >= 0.8
525
+ ):
526
+ escape_hint = ";若该文字本属此卡片:加大卡片或让文字换行收纳"
527
+ problems.append(
528
+ f"【内容相互叠加】{label}{where} · "
529
+ f"重叠 {ov.get('areaIn')} in²(约占较小框 {ov.get('fracOfSmaller', 0)*100:.0f}%)"
530
+ f" |可参考修法(非强制):先移动/重排躲开(拉开间距·换槽位·调 grid 模板)"
531
+ f" → 再精炼/精简文案使其更短 → 最后缩小字体(最多缩 20%,且仍 < 该页主标题)"
532
+ f"{escape_hint}"
533
+ )
534
+ if len(overlaps) > MAX_OVERLAP_REPORT:
535
+ problems.append(
536
+ f"【内容相互叠加】{label} · 另有 {len(overlaps) - MAX_OVERLAP_REPORT} 处重叠未列出。"
537
+ )
538
+
539
+ # ── 文字溢出所属卡片(仅 ppt_strict;ERROR)──
540
+ # 「卡内文字」在重叠检查里是豁免项(正常结构),故字顶出自家卡此前完全不可见。
541
+ # 数据源:顶层 slide.textRuns(原始 HTML px 矩形)× balance.fgRects(全部前景块,含媒体卡);
542
+ # 旧 report / 全量 manifest 缺任一字段 → 静默跳过(向后兼容)。
543
+ if ppt_strict:
544
+ fg_by_name = {}
545
+ for fr in (b.get("fgRects") or []):
546
+ if isinstance(fr, dict) and fr.get("name") and isinstance(fr.get("rect"), dict):
547
+ fg_by_name[fr["name"]] = fr["rect"]
548
+ escapes = []
549
+ for run in (slide.get("textRuns") or []) if fg_by_name else []:
550
+ if not isinstance(run, dict):
551
+ continue
552
+ parent = run.get("fgParent")
553
+ card = fg_by_name.get(parent)
554
+ if not parent or not isinstance(card, dict):
555
+ continue
556
+ try:
557
+ rx = float(run.get("x") or 0.0) / canvas_w
558
+ ry = float(run.get("y") or 0.0) / canvas_h
559
+ rw = float(run.get("w") or 0.0) / canvas_w
560
+ rh = float(run.get("h") or 0.0) / canvas_h
561
+ cx = float(card.get("x") or 0.0)
562
+ cy = float(card.get("y") or 0.0)
563
+ cw = float(card.get("w") or 0.0)
564
+ ch = float(card.get("h") or 0.0)
565
+ except (TypeError, ValueError):
566
+ continue
567
+ if rw <= 0 or rh <= 0 or cw <= 0 or ch <= 0:
568
+ continue
569
+ sides = []
570
+ for zh, overhang, px_scale in (
571
+ ("左缘", cx - rx, canvas_w),
572
+ ("右缘", (rx + rw) - (cx + cw), canvas_w),
573
+ ("上缘", cy - ry, canvas_h),
574
+ ("下缘", (ry + rh) - (cy + ch), canvas_h),
575
+ ):
576
+ if overhang > PPT_STRICT_ESCAPE_TOL:
577
+ sides.append(f"{zh} {overhang * px_scale:.0f}px")
578
+ if sides:
579
+ escapes.append((run, parent, sides, {"x": rx, "y": ry, "w": rw, "h": rh}))
580
+ for run, parent, sides, rrect in escapes[:MAX_OVERLAP_REPORT]:
581
+ problems.append(
582
+ f"【文字溢出卡片】{label} · 「{(run.get('text') or 'text')[:16]}」{_fmt_rect(rrect)}"
583
+ f" 超出所属卡片「{parent}」{_fmt_rect(fg_by_name.get(parent))}:{'、'.join(sides)}"
584
+ f" |可参考修法(非强制):加大卡片(padding/尺寸随内容自然撑开)/ 让文字换行收进卡内 / 精炼文案——别缩字"
585
+ )
586
+ if len(escapes) > MAX_OVERLAP_REPORT:
587
+ problems.append(
588
+ f"【文字溢出卡片】{label} · 另有 {len(escapes) - MAX_OVERLAP_REPORT} 处溢出未列出。"
589
+ )
590
+
591
+ # ── 截图夹带文本(始终 ERROR,与重叠同级硬门——用户硬要求:底图/中层切图绝不能带 HTML/SVG 文本) ──
592
+ # 探针 full 态查「未被 auto-text 覆盖的可见文本」:source=dom(漏标的真实文本,含前景块内→会烧进**中层切图**)
593
+ # / source=pseudo(::before/::after 文字)。会被烧进截图、不可编辑,且若上层又还原一层 → 双层。**唯一例外 AI 插图自带字。**
594
+ for bt in baked_text[:MAX_OVERLAP_REPORT]:
595
+ src = bt.get("source")
596
+ how = (
597
+ "改成真实 DOM 文本节点(h*/p/span),别放进 ::before/::after content"
598
+ if src == "pseudo"
599
+ else "确认该文字是真实可见文本节点(导出器会自动隐藏并提成可编辑框);若刻意烧进图请标 data-pptx-export=\"ignore\""
600
+ )
601
+ problems.append(
602
+ f"【截图夹带文本】{label} · 「{bt.get('text')}」{_fmt_rect(bt.get('rect'))}"
603
+ f" · 未被隐藏、会烧进截图({'伪元素 ' + src if src == 'pseudo' else 'DOM 漏标'})"
604
+ f" |可参考修法(非强制):{how}"
605
+ )
606
+ if len(baked_text) > MAX_OVERLAP_REPORT:
607
+ problems.append(
608
+ f"【截图夹带文本】{label} · 另有 {len(baked_text) - MAX_OVERLAP_REPORT} 处未列出。"
609
+ )
610
+
611
+ # ── 文字/背景对比度不足(治「深紫底 + 黑字」不可读):**一律 WARNING、不硬拦**。──
612
+ # 为什么不做 ERROR:硬门会逼导出步去改 deck.html 颜色,弱模型(doubao)改 HTML 极易出错/编码损坏 → 整步放弃 halt
613
+ # (真实事故,与当年占空门停用同因)。对比度治本在**上游设计铁律**(深底白字/浅底深字,见 layout-composition §0.9 +
614
+ # build/design prompt);这里只确定性**报问题**给导出步顺手改(深底转白字,用 Edit),改不动也照常导出、不阻塞。
615
+ for lc in low_contrast[:MAX_OVERLAP_REPORT]:
616
+ ratio = lc.get("ratio") or 0.0
617
+ warnings.append(
618
+ f"【文字对比度不足】{label} · 「{lc.get('text')}」{_fmt_rect(lc.get('rect'))}"
619
+ f" · 文字 {lc.get('color')} / 背景 {lc.get('bgColor')} 对比度仅 {ratio}(放映级需 ≥3.0、正常 ≥4.5)"
620
+ f" |可参考修法(非强制):深底配浅/白字、浅底配深墨字——绝不深底深字或浅底浅字(VI 深色仅用于封面/章节且文字转白)"
621
+ )
622
+ if len(low_contrast) > MAX_OVERLAP_REPORT:
623
+ warnings.append(
624
+ f"【文字对比度不足】{label} · 另有 {len(low_contrast) - MAX_OVERLAP_REPORT} 处未列出。"
625
+ )
626
+
627
+ # ── 卡片/分组填充不足(D,占空族):禁用时整体停用(迭代空列表即跳过)。──
628
+ # fill = 卡内(文本 + 子块)面积 ÷ 卡面积 → 同抓「大白卡装一句话」与「图元挤一角、旁边大片留白」(crammed group)。
629
+ # ppt_strict 附加语义(需 manifest 带 mediaRects,旧 manifest 缺字段时不升级、只走默认 WARNING):
630
+ # - 被单个媒体块盖住 ≥70% 的卡 = 图卡假空(图为兄弟绝对定位、DOM 不在卡内)→ 整条豁免;
631
+ # - 大面积(≥15% 页)且填充 <10% 的真空板(slide05 白板/slide14 巨卡事故)→ 升 ERROR 不再「有图可接受」。
632
+ media_rects = b.get("mediaRects") or []
633
+ has_media_info = isinstance(b.get("mediaRects"), list)
634
+
635
+ # ── 信息集锦/综述型客户图槽位偏窄(仅 source_layout_plan_path 传入且有 is_dense_info 条目时激活):
636
+ # WARNING 不硬拦——先观察几轮再考虑要不要升级,避免重蹈已回退的 canvas_fraction_violations
637
+ # 覆辙(那次是测错了维度;这次测的是 mediaRects 真实渲染框宽度,只是阈值本身仍需真机验证)。
638
+ if dense_info_stems:
639
+ for m in media_rects:
640
+ src = m.get("sourceFile") if isinstance(m, dict) else None
641
+ if not src:
642
+ continue
643
+ orig_file = dense_info_stems.get(_stem(src))
644
+ if not orig_file:
645
+ continue
646
+ actual_w = m.get("w") or 0.0
647
+ if actual_w < DENSE_INFO_MIN_WIDTH_FRACTION:
648
+ warnings.append(
649
+ f"【信息集锦图槽位偏窄】{label} · {orig_file} · "
650
+ f"渲染宽度实测仅画布 {actual_w*100:.0f}%,此类图建议 ≥ {DENSE_INFO_MIN_WIDTH_FRACTION*100:.0f}%"
651
+ f"(is_dense_info 客户图,20+ 并列信息点需要足够宽度才可辨认)"
652
+ f" |可参考修法(非强制):给它独立整页,或与另一张图对半平分页面(各占约 50% 宽度)——"
653
+ f"不要用共享列/共享行的纵向堆叠把它挤成次要辅助证据"
654
+ )
655
+
656
+ for c in (cards if occupancy_checks_enabled else []):
657
+ fill = c.get("fill") or 0.0
658
+ rect = c.get("rect") or {}
659
+ area_frac = (rect.get("w") or 0.0) * (rect.get("h") or 0.0)
660
+ if area_frac < CARD_MIN_AREA_FRAC:
661
+ continue # 小卡(引文 quote-pull / 装饰条 / 药丸)豁免:本意只抓「大卡/大底板填充不足」
662
+ if fill >= CARD_FILL_MIN:
663
+ continue
664
+ if ppt_strict and has_media_info and area_frac > 0:
665
+ media_cover = max(
666
+ (_rect_intersect_area(rect, m) / area_frac for m in media_rects),
667
+ default=0.0,
668
+ )
669
+ if media_cover >= PPT_STRICT_MEDIA_COVER:
670
+ continue # 图卡假空:媒体块盖着,不是真空板
671
+ if area_frac >= PPT_STRICT_CARD_AREA_MIN and fill < PPT_STRICT_CARD_FILL_MAX:
672
+ problems.append(
673
+ f"【空心大卡】{label} · 卡片/底板 {_fmt_rect(c.get('rect'))} · "
674
+ f"占页面 {area_frac*100:.0f}% 而卡内内容仅 {fill*100:.0f}%(真空板)"
675
+ f" |可参考修法(非强制):按内容收紧/删掉该卡 / 放大内部图元铺满 / 补实质内容"
676
+ )
677
+ continue
678
+ add(
679
+ f"【卡片/分组填充不足】{label} · 卡片/底板 {_fmt_rect(c.get('rect'))} · "
680
+ f"卡内内容(文字+子块)仅占 {fill*100:.0f}% < 阈值 {CARD_FILL_MIN*100:.0f}%(归一化 0–1,原点左上)"
681
+ f" |可参考修法(非强制):减小卡的 padding / 放大内部图元铺满 / 补 2–3 行实质文字或辅助元素"
682
+ f" / 按内容收紧卡尺寸(放大字号是末选,别只靠它把空卡撑满)",
683
+ downgradeable=True,
684
+ )
685
+
686
+ # ── 文本显著裁切 / 溢出(E,「挤压」语义,始终保留):WARNING,导出会触发缩字或截断;HTML 阶段就该让文字装得下 ──
687
+ for o in overflow_runs:
688
+ warnings.append(
689
+ f"【文本裁切】{label} · 「{o.get('text')}」{_fmt_rect(o.get('rect'))}"
690
+ f" · 内容超出框 {o.get('overflowPx')}px"
691
+ f" |可参考修法(非强制):先加大框高/挪位给文字空间 → 再精炼/缩短文案 → 最后缩小字体(最多缩 20%),使其在框内不溢出"
692
+ )
693
+
694
+ # ── 字号过小(放映级,WARNING 不拦):低于放映级下限的文本(投影上偏小)。治本在设计/生成阶段按 §0.85 阶梯放大 ──
695
+ for s in small_text[:MAX_OVERLAP_REPORT]:
696
+ warnings.append(
697
+ f"【字号过小】{label} · 「{s.get('text')}」{_fmt_rect(s.get('rect'))}"
698
+ f" · 实际 {s.get('fontSize')}px,低于放映级下限"
699
+ f" |可参考修法(非强制):按 layout-composition.md §0.85 放映级阶梯放大(正文 ~38px、注释 ~30px、图内文字同标准且图形画大装下)"
700
+ )
701
+ if len(small_text) > MAX_OVERLAP_REPORT:
702
+ warnings.append(
703
+ f"【字号过小】{label} · 另有 {len(small_text) - MAX_OVERLAP_REPORT} 处偏小文本未列出。"
704
+ )
705
+
706
+ # ── 超出画布(WARNING 不拦,与占空开关无关):文本/前景块右/下越界(内容比 1920×1080 大、会被裁出可视区)。──
707
+ # 修法顺序 = 用户裁决:① 精炼文案 + 图文并茂(能用图表/SVG/图标表达的就别堆文字)→ ② 改两列 → ③ 拆成两页。
708
+ # 「内容多就换两页、别死磕一页」「字不能再小」——所以**绝不靠缩字**,靠精炼/图形化/拆页。
709
+ for oc in off_canvas[:MAX_OVERLAP_REPORT]:
710
+ base = (
711
+ f"【超出画布】{label} · {'文本' if oc.get('kind') == 'text' else '前景块'}「{oc.get('name')}」"
712
+ f"{_fmt_rect(oc.get('rect'))} 越出页面边界(内容塞太多)"
713
+ )
714
+ vis = _visible_fraction(oc.get("rect"))
715
+ if vis < OFFCANVAS_VISIBLE_MIN:
716
+ # 内容实质飞出画布、观众根本看不到(s9:轨道被大图撑爆,元素跑到 x≈0.99,可见 ~6%)。
717
+ msg = (
718
+ base + f" · 可见部分仅 {vis*100:.0f}%(< {OFFCANVAS_VISIBLE_MIN*100:.0f}%,内容实质不可见)"
719
+ f" |可参考修法(非强制):多为 grid/flex 轨道被大图/长内容撑爆 → 给分栏子项加 min-width:0;"
720
+ f"或 ① 精炼文案图文并茂 → ② 改两列 → ③ 拆两页。**绝不缩字到 <24px**"
721
+ )
722
+ catastrophic.append(msg)
723
+ (problems if strict_offcanvas else warnings).append(msg)
724
+ else:
725
+ # ppt_strict:越界(>8px 容差后仍出界)一律 ERROR——「内容超出 PPT 下边缘」是用户点名缺陷
726
+ # (slide09 L4 卡 42% 被裁仍非 catastrophic,默认档只 WARNING 无人修)。默认档行为不变。
727
+ (problems if ppt_strict else warnings).append(
728
+ base
729
+ + " |可参考修法(非强制):① 精炼文案 + 图文并茂(图表/SVG/图标省字)→ ② 改两列 → ③ 拆成两页(别死磕一页)。**绝不缩字到 <24px**"
730
+ )
731
+ if len(off_canvas) > MAX_OVERLAP_REPORT:
732
+ warnings.append(
733
+ f"【超出画布】{label} · 另有 {len(off_canvas) - MAX_OVERLAP_REPORT} 处越界未列出。"
734
+ )
735
+
736
+ # 整页几乎无内容(连标题都没量到)——有**真实**底图/视觉则 WARNING(纯图封面合法),否则 ERROR(「就怕啥也没有」)。
737
+ # has_visual 的准确性依赖 export 侧 hasBgImage 不误判(Gap A 修复:撑爆/越界的大图不再算"背景")。
738
+ if content_boxes == 0:
739
+ msg = (
740
+ f"【未量到内容】{label} · 全页无文本/前景框"
741
+ f" |可参考检查(非强制):是否标了 data-pptx-kind=\"text\" / data-pptx-layer=\"foreground\""
742
+ )
743
+ if has_visual:
744
+ warnings.append(msg + "(本页有底图/前景视觉元素,按可接受处理)")
745
+ else:
746
+ # 无任何真实视觉的空页 = 内容不可见灾难,恒 ERROR + 计入 catastrophic。
747
+ problems.append(msg)
748
+ catastrophic.append(msg)
749
+ continue
750
+
751
+ content_page_count += 1
752
+
753
+ # ── B. 页面过空(占空族):禁用时停用(不记账、不 WARNING)。──
754
+ if occupancy_checks_enabled and coverage < COVER_MIN:
755
+ empty_pages.append(label)
756
+ warnings.append(
757
+ f"【页面过空】{label} · 内容覆盖 {coverage*100:.0f}% < 阈值 {COVER_MIN*100:.0f}%"
758
+ f"{'(有底图/视觉元素,可接受)' if has_visual else '(单页记一笔;过半页空才整体失败)'}"
759
+ f" |可参考修法(非强制):先从 draft.md 补更多实质要点 / 加辅助解释的 HTML 元素"
760
+ f"(图示·图标·要点卡·SVG·分隔带)/ 调分栏让两侧互补(放大字号·拉大卡片不是办法)"
761
+ )
762
+
763
+ # ── C. 空半(占空族):一侧空、对侧满 = 失衡;禁用时停用。──
764
+ for empty_side, full_side, zh in (
765
+ ("top", "bottom", "上空下满"),
766
+ ("bottom", "top", "下空上满"),
767
+ ("left", "right", "左空右满"),
768
+ ("right", "left", "右空左满"),
769
+ ):
770
+ if (occupancy_checks_enabled
771
+ and (ho.get(empty_side) or 0.0) < EMPTY_FRAC
772
+ and (ho.get(full_side) or 0.0) > OPP_MIN):
773
+ add(
774
+ f"【构图失衡·{zh}】{label} · {empty_side} 半区占用 {(ho.get(empty_side) or 0)*100:.0f}%"
775
+ f" / {full_side} 半区 {(ho.get(full_side) or 0)*100:.0f}%"
776
+ f" |可参考修法(非强制):空侧补实质要点或辅助解释 HTML 图形(图标/图示/要点卡)/ 调分栏让两侧互补填满",
777
+ downgradeable=True,
778
+ )
779
+
780
+ # ── C'. 挤角(占空族):横竖各有一侧空 → 内容堆在一角;禁用时停用。──
781
+ empties = {k for k in ("top", "bottom", "left", "right") if (ho.get(k) or 0.0) < EMPTY_FRAC}
782
+ if occupancy_checks_enabled and empties & {"top", "bottom"} and empties & {"left", "right"}:
783
+ corner = ("下" if "top" in empties else "上") + ("右" if "left" in empties else "左")
784
+ add(
785
+ f"【内容挤角·{corner}角】{label} · 空半区 [{'/'.join(sorted(empties))}]"
786
+ f" |可参考修法(非强制):把焦点移到黄金分割/三分线附近 / 空区补实质内容或辅助解释 HTML 元素,图文互补填满版面",
787
+ downgradeable=True,
788
+ )
789
+
790
+ # ── 「不能页页空」:单页过空只记账,过半有内容页都过空 = 整体失衡。**降为 WARNING(不硬拦)**——
791
+ # 占空族「加回但宽松」裁决:只提示别人去填,不阻断导出(避免为「够不够满」反复改烧配额)。
792
+ # 仅对 ≥ MIN_PAGES_FOR_EMPTY_RATIO 页的 deck 生效(1~2 页稀疏可能是设计选择,不误伤)。
793
+ if (
794
+ occupancy_checks_enabled
795
+ and content_page_count >= MIN_PAGES_FOR_EMPTY_RATIO
796
+ and len(empty_pages) > EMPTY_PAGE_RATIO * content_page_count
797
+ ):
798
+ listed = "、".join(empty_pages[:8]) + (" …" if len(empty_pages) > 8 else "")
799
+ warnings.append(
800
+ f"【过半页面过空】{len(empty_pages)}/{content_page_count} 个有内容页覆盖率 < {COVER_MIN*100:.0f}%"
801
+ f"(页页空、整体偏空):{listed}"
802
+ f" |可参考修法(非强制):给空页补实质要点 + 辅助解释 HTML 元素(图标/图示/要点卡/SVG/图表),"
803
+ f"整体提升内容密度(不是放大字号/拉大卡片)"
804
+ )
805
+
806
+ # catastrophic_only:只交回「内容不可见灾难」,抑制重叠/夹带/占空族噪声(回归安全的硬门来源)。
807
+ if catastrophic_only:
808
+ return (catastrophic, warnings, details)
809
+ return (problems, warnings, details)
810
+
811
+
812
+ def main() -> int:
813
+ parser = argparse.ArgumentParser(
814
+ description="HTML→PPTX 构图均衡 确定性闸门(读 export-manifest.json;与 opc_check / layout_lint 互补,仅标准库)"
815
+ )
816
+ parser.add_argument(
817
+ "manifest",
818
+ help="layout-report.json / export-manifest.json 路径,或其所在 workdir 目录"
819
+ "(目录时优先 layout-report.json)",
820
+ )
821
+ parser.add_argument("-v", "--verbose", action="store_true", help="打印每页指标明细")
822
+ parser.add_argument("--no-occupancy", action="store_true", help="关闭占空比族 warning(页面过空/空半/挤角/卡片填充不足)")
823
+ parser.add_argument("--strict-offcanvas", action="store_true",
824
+ help="内容严重飞出画布(可见 < %d%%)升为 ERROR(默认仅 WARNING)" % int(OFFCANVAS_VISIBLE_MIN * 100))
825
+ parser.add_argument("--catastrophic-only", action="store_true",
826
+ help="只把「内容不可见灾难」(严重越界 + 无真实视觉空页)作为 ERROR,抑制重叠/占空族噪声"
827
+ "(供 visual_quality_gate 做回归安全的硬门)")
828
+ parser.add_argument("--ppt-strict", action="store_true",
829
+ help="ppt 视觉门严格档:重叠去噪(剔行盒伪象)+ 严重越界升 ERROR + 大空卡升 ERROR"
830
+ "(默认关;webppt 等既有调用行为不变)")
831
+ parser.add_argument("--source-layout-plan", default=None,
832
+ help="ppt 工作流 source_layout_plan.json 路径(可选):传入时额外检查"
833
+ "is_dense_info 客户图的真实渲染宽度是否达标(WARNING,不硬拦);"
834
+ "webppt 无此文件,不传则跳过,行为不变")
835
+ args = parser.parse_args()
836
+
837
+ manifest_path = _resolve_manifest_path(args.manifest)
838
+ problems, warnings, details = check_balance(
839
+ manifest_path,
840
+ verbose=args.verbose,
841
+ occupancy_checks_enabled=(OCCUPANCY_CHECKS_ENABLED and not args.no_occupancy),
842
+ strict_offcanvas=args.strict_offcanvas,
843
+ catastrophic_only=args.catastrophic_only,
844
+ ppt_strict=args.ppt_strict,
845
+ source_layout_plan_path=args.source_layout_plan,
846
+ )
847
+
848
+ if args.verbose and details:
849
+ print("每页构图指标:")
850
+ for d in details:
851
+ print(" " + d)
852
+
853
+ if warnings:
854
+ for w in warnings:
855
+ print("WARNING: " + w)
856
+
857
+ if problems:
858
+ print(
859
+ f"构图均衡检查 FAILED —— {manifest_path} 存在 {len(problems)} 处缺陷"
860
+ f"(每条 = 【问题类型】页 · 位置 · 量化范围):"
861
+ )
862
+ for p in problems:
863
+ print(" - " + p)
864
+ print(
865
+ "说明:本闸门只确定性地**报问题与位置/范围**(像 zod 报错),「|可参考修法」仅为提示、不强制——"
866
+ "具体怎么改由你决定。构图铁律见 ui-design/references/layout-composition.md。"
867
+ "**ERROR(必清零):内容相互叠加(重叠)+ 截图夹带文本(底图/切图绝不能带 HTML/SVG 文字)+ 整页啥也没有。**"
868
+ "**WARNING(提示不拦):文本溢出 + 字号过小(放映级)+ 超出画布 + 文字对比度不足(深底深字等)+ 占空比族(页面过空/空半/挤角/卡片·分组填充不足,已加回但宽松、有视觉即降级)。**"
869
+ "修法方向:**修「重叠/溢出」先移动/重排躲开 → 再精炼描述文案使其更短 → 最后才缩小字体(单次最多 20%,仍 < 主标题);**"
870
+ "**「截图夹带文本」把该文字改成真实 DOM 文本节点(别放 ::before/::after / 别烧进图)。**"
871
+ "图被挤压时**绝不变形**:等比缩放 + object-position 按内容热区裁切平移、按槽位选对 aspect。"
872
+ "(本闸门不替代视觉 QA。)"
873
+ )
874
+ return 1
875
+
876
+ print(f"构图均衡检查 PASSED —— {manifest_path} 无可计算的构图缺陷(视觉 QA 仍需进行)。")
877
+ return 0
878
+
879
+
880
+ if __name__ == "__main__":
881
+ sys.exit(main())