minimal-agent 0.9.0 → 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 (467) hide show
  1. package/README.md +11 -8
  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 +2 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +172 -9
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +150 -13
  23. package/plugins/workflow-runner/src/index.js +39 -8
  24. package/plugins/workflow-runner/src/loader.js +216 -64
  25. package/plugins/workflow-runner/src/multimodal.js +37 -0
  26. package/plugins/workflow-runner/src/recovery.js +26 -24
  27. package/plugins/workflow-runner/src/runner.js +693 -287
  28. package/plugins/workflow-runner/src/stepExecutors/agent.js +126 -37
  29. package/plugins/workflow-runner/src/stepExecutors/assert.js +3 -2
  30. package/plugins/workflow-runner/src/stepExecutors/llm.js +38 -30
  31. package/plugins/workflow-runner/src/stepExecutors/skill.js +27 -38
  32. package/plugins/workflow-runner/src/stepExecutors/tool.js +56 -3
  33. package/plugins/workflow-runner/src/stepExecutors/verify.js +19 -10
  34. package/plugins/workflow-runner/src/taskIdentity.js +185 -30
  35. package/plugins/workflow-runner/src/types.js +18 -7
  36. package/plugins/workflow-runner/src/workflowState.js +355 -55
  37. package/skills/ai-image/SKILL.md +89 -0
  38. package/skills/ai-image/scripts/generate.mjs +497 -0
  39. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  40. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  41. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  42. package/skills/browser-automation/SKILL.md +47 -0
  43. package/skills/docx/SKILL.md +25 -3
  44. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  45. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  46. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/opc_check.py +253 -0
  53. package/skills/image-gen-openrouter/SKILL.md +10 -2
  54. package/skills/image-stats/SKILL.md +65 -0
  55. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  56. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  57. package/skills/image-stats/scripts/image_stats.py +368 -0
  58. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  59. package/skills/pptx/SKILL.md +123 -95
  60. package/skills/pptx/design.md +82 -0
  61. package/skills/pptx/examples/README.md +29 -0
  62. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  63. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  64. package/skills/pptx/pptxgenjs.md +41 -0
  65. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  66. package/skills/pptx/scripts/layout_lint.py +293 -0
  67. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  68. package/skills/pptx/scripts/opc_check.py +339 -0
  69. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  70. package/skills/pptx/svg.md +67 -0
  71. package/skills/tts-zh/SKILL.md +19 -2
  72. package/skills/ui-design/SKILL.md +158 -0
  73. package/skills/ui-design/claude_design.md +309 -0
  74. package/skills/ui-design/evals/evals.json +254 -0
  75. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  76. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  77. package/skills/ui-design/references/design-context.md +53 -0
  78. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  79. package/skills/ui-design/references/export-pptx.md +124 -0
  80. package/skills/ui-design/references/layout-composition.md +654 -0
  81. package/skills/ui-design/references/preview-feedback.md +37 -0
  82. package/skills/ui-design/references/source-image-layout.md +189 -0
  83. package/skills/ui-design/references/starter-components.md +78 -0
  84. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  85. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  86. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  87. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  88. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  89. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  90. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  91. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  92. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  93. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  94. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  95. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  96. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  97. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  98. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  99. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  100. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/assemble_material.py +217 -0
  140. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  141. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  142. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  143. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  144. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  145. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  146. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  147. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  148. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  149. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  150. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  151. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  152. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  153. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  154. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  155. package/skills/ui-design/scripts/design_style_check.py +397 -0
  156. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  157. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  158. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  159. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  160. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  161. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  162. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  163. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  164. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  165. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  166. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  167. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  168. package/skills/ui-design/scripts/layout_balance.py +881 -0
  169. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  170. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  171. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  172. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  173. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  174. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  175. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  176. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  177. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  178. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  179. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  180. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  181. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  182. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  183. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  184. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  185. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  186. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  187. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  188. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  195. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  196. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  205. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  206. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  221. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  222. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  225. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  226. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  239. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  240. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  248. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  249. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  261. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  262. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  275. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  292. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  293. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  316. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  324. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  325. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  328. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  329. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  337. package/skills/video-compose/SKILL.md +8 -1
  338. package/skills/web-crawler/SKILL.md +70 -0
  339. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  340. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  341. package/skills/xlsx/SKILL.md +12 -0
  342. package/skills/xlsx/scripts/opc_check.py +253 -0
  343. package/src/advisorState.js +22 -0
  344. package/src/askBridge.js +87 -3
  345. package/src/bootstrap/workingDir.js +12 -1
  346. package/src/cli/args.js +17 -1
  347. package/src/cli/configMenu.js +236 -0
  348. package/src/cli/configWizard.js +7 -5
  349. package/src/cli/print.js +37 -4
  350. package/src/cli/streamJson.js +77 -24
  351. package/src/cli/wizards/advisorWizard.js +147 -0
  352. package/src/cli/wizards/fieldForm.js +149 -0
  353. package/src/cli/wizards/imageWizard.js +60 -0
  354. package/src/cli/wizards/openRouterWizard.js +23 -0
  355. package/src/cli/wizards/providerPresets.js +95 -0
  356. package/src/cli/wizards/providerWizard.js +219 -0
  357. package/src/cli/wizards/searchWizard.js +23 -0
  358. package/src/cli/wizards/ttsWizard.js +34 -0
  359. package/src/config/configFile.js +441 -83
  360. package/src/config/configService.js +446 -0
  361. package/src/config.js +24 -64
  362. package/src/context/agentHome.js +9 -0
  363. package/src/context/archive.js +3 -2
  364. package/src/context/compact.js +8 -5
  365. package/src/context/contextLock.js +81 -0
  366. package/src/context/persistContext.js +30 -11
  367. package/src/context/reactiveCompact.js +4 -2
  368. package/src/context/recentDirs.js +21 -61
  369. package/src/context/sessionPath.js +97 -23
  370. package/src/context/sessionRegistry.js +303 -24
  371. package/src/context/tokenCounter.js +18 -0
  372. package/src/context/workspaceIndex.js +731 -0
  373. package/src/infra/verification.js +3 -2
  374. package/src/llm/client.js +293 -206
  375. package/src/llm/providers/anthropicFormat.js +241 -0
  376. package/src/llm/providers/openaiFormat.js +190 -0
  377. package/src/loop.js +211 -27
  378. package/src/main.js +61 -11
  379. package/src/messageQueue.js +97 -0
  380. package/src/multimodal.js +203 -0
  381. package/src/plugin-sdk.js +15 -0
  382. package/src/plugins/commandRouter.js +3 -0
  383. package/src/plugins/hookEngine.js +5 -1
  384. package/src/plugins/pluginLifecycle.js +28 -0
  385. package/src/plugins/pluginRunner.js +40 -0
  386. package/src/plugins/transcript.js +4 -3
  387. package/src/prompts/pluginList.js +2 -2
  388. package/src/prompts/projectInstructions.js +3 -3
  389. package/src/prompts/promptAssembly.js +133 -109
  390. package/src/prompts/skillList.js +12 -22
  391. package/src/session/runAgentTurn.js +70 -0
  392. package/src/session/runTurn.js +41 -6
  393. package/src/toolGate.js +17 -0
  394. package/src/tools/advisor/advisor.js +379 -0
  395. package/src/tools/agent/agent.js +555 -0
  396. package/src/tools/agent/agentTypes.js +59 -0
  397. package/src/tools/agent/coordinator.js +93 -0
  398. package/src/tools/agent/worktree.js +270 -0
  399. package/src/tools/ask/ask.js +7 -2
  400. package/src/tools/bash/bash.js +112 -18
  401. package/src/tools/download/download.js +144 -0
  402. package/src/tools/edit/edit.js +1 -0
  403. package/src/tools/edit/multi-edit.js +1 -0
  404. package/src/tools/genimage/genimage.js +242 -0
  405. package/src/tools/genspeech/genspeech.js +67 -0
  406. package/src/tools/glob/glob.js +1 -0
  407. package/src/tools/grep/grep.js +3 -0
  408. package/src/tools/grep/rgPath.js +2 -0
  409. package/src/tools/index.js +25 -2
  410. package/src/tools/read/read.js +1 -0
  411. package/src/tools/shared/killTree.js +52 -0
  412. package/src/tools/toolProgressBridge.js +48 -0
  413. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  414. package/src/tools/webbrowser/browser.js +59 -42
  415. package/src/tools/webbrowser/browserControl.js +118 -0
  416. package/src/tools/webbrowser/browserDaemon.js +97 -0
  417. package/src/tools/webbrowser/browserService.js +27 -0
  418. package/src/tools/webbrowser/browserStream.js +119 -0
  419. package/src/tools/webbrowser/webbrowser.js +699 -130
  420. package/src/tools/webfetch/webfetch.js +1 -0
  421. package/src/tools/websearch/websearch.js +134 -51
  422. package/src/tools/write/write.js +1 -0
  423. package/src/ui/App.js +249 -22
  424. package/src/ui/AskQuestion.js +19 -17
  425. package/src/ui/InputBox.js +34 -9
  426. package/src/ui/LiveArea.js +112 -19
  427. package/src/ui/MessageList.js +100 -14
  428. package/src/ui/ModelPicker.js +56 -0
  429. package/src/ui/QueuedMessagesBanner.js +14 -0
  430. package/src/ui/Root.js +18 -10
  431. package/src/ui/SessionPicker.js +102 -0
  432. package/src/ui/StatusLine.js +13 -4
  433. package/src/ui/WorkspacePicker.js +55 -0
  434. package/src/ui/figures.js +44 -0
  435. package/src/ui/hooks/askSlot.js +82 -0
  436. package/src/ui/hooks/useChat.js +345 -64
  437. package/src/ui/hooks/usePasteHandler.js +11 -1
  438. package/src/ui/hooks/useTextBuffer.js +1 -3
  439. package/src/ui/liveViewport.js +5 -1
  440. package/src/ui/markdown.js +206 -0
  441. package/src/ui/textBuffer.js +0 -7
  442. package/src/ui/theme.js +32 -0
  443. package/src/utils/debugLog.js +0 -4
  444. package/src/utils/resourcePaths.js +2 -2
  445. package/src/utils/stripThink.js +50 -0
  446. package/src/web/browserBridge.js +447 -0
  447. package/src/web/protocol.js +19 -0
  448. package/workflows/adaptive-research.yaml +9 -2
  449. package/workflows/book-review-short.yaml +31 -7
  450. package/workflows/e2e-write-greet.yaml +3 -0
  451. package/workflows/illustrate.yaml +227 -0
  452. package/workflows/plan.yaml +437 -207
  453. package/workflows/ppt.yaml +1659 -0
  454. package/workflows/ralph-loop.yaml +142 -112
  455. package/workflows/schema.json +81 -23
  456. package/workflows/webppt.yaml +496 -0
  457. package/workflows/word.yaml +710 -0
  458. package/workflows/youtube-shorts.yaml +38 -10
  459. package/MINIMAL-TRACE.md +0 -77
  460. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  461. package/skills/config/SKILL.md +0 -123
  462. package/skills/frontend-design/LICENSE.txt +0 -177
  463. package/skills/frontend-design/SKILL.md +0 -42
  464. package/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
  465. package/src/ui/ProgressPanel.js +0 -54
  466. package/src/ui/StreamingBlock.js +0 -11
  467. package/src/ui/ToolStatus.js +0 -11
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ text_readability_pixel.py —— 像素级「文字压图可读性」确定性闸门(ppt 视觉门专用)。
5
+
6
+ 为什么需要这个脚本(2026-07-02 逐页体检根因 ⑤)
7
+ ====================
8
+ 既有对比度检查(export mjs 的 resolveBg → layout_balance 的 lowContrast)只沿 DOM 找 CSS
9
+ background-color——文字压在**背景图片**上时拿到的是页面底色:深色图页刷出大量「白字/浅底 1.05」
10
+ 假阳,而真正压图不可读处(标题横穿明暗跨界照片)拿到的也是同一个错误数字。CSS 层面**物理上**
11
+ 测不出图片背衬的可读性,必须采真实像素。
12
+
13
+ 工作方式
14
+ ========
15
+ 读 export_pptx_from_html.mjs `--measure-only --with-bg-shots` 的产物:
16
+ - layout-report.json 的 slides[].textRuns(文本原始 px 矩形 + 颜色 + fgParent)
17
+ - slide-NN-background.png(背景态整页截图:隐前景块 + 隐全部文本 = 直接压背景层的文字底下的真实背衬)
18
+ - slide-NN-foreground.png(foreground 态整页截图:隐全部文字、卡片/SVG/渐变/图片原样渲染
19
+ = 卡内/前景块内文字底下的真实背衬。2026-07-03 新增——此前 fgParent 非空的文本被整体跳过,
20
+ 真机实测占比高达 81%,深底深字/压卡不可读几乎是检测盲区;CSS 层 resolveBg 也测不准同类场景
21
+ (渐变会跳过继续往上找、读不懂 SVG fill)。两张截图同源同机制,只是隐藏对象不同。)
22
+ 对每条文本:`fgParent` 为空 → 从背景截图采样;`fgParent` 非空 → 从 foreground 截图采样
23
+ (该文本本就嵌在某前景块/卡片里,foreground 截图里那块内容原样可见,直接是它的真实背衬,
24
+ 无需再猜测卡片颜色)。在其矩形内网格采样像素,逐点算 WCAG 对比度:
25
+ - 失败占比 ≥ FAIL_FRAC_ERROR → ERROR(文字实质不可读)
26
+ - 失败占比 ≥ FAIL_FRAC_WARN → WARNING(部分区域可读性差)
27
+ - 分段局部判定(2026-07-03):矩形横切 6 段逐段判,任一段失败率 ≥70% →
28
+ 【文字局部压图不可读】ERROR——治「列表行一半压平板黑边框、另一半净区,整框平均被稀释到
29
+ 连 WARNING 都不报」(真机 06 页);窄文本(段内采样列不足)不分段,防小样本误报。
30
+ - 系统性同色归组(2026-07-03):ERROR 里同一文字颜色出现 ≥3 次时追加一行归组提示——真机复盘
31
+ 27 处 ERROR 里 15+ 处共享同一 VI 强调色 CSS 变量(`.t-eyebrow`/`.label` 之类多处复用),
32
+ 逐条加局部 scrim 的修复策略在有限轮数内修不完;提示指向「改一次共享 CSS 规则/变量能同时
33
+ 清零全部同色实例」,纯汇总已收集数据、不新增判断。
34
+ 报错带 页 · 文本 · 归一化矩形 · 实测数据 + 修法建议(加 scrim / 移净区 / 换字色)。
35
+ `slide-NN-foreground.png` 缺失(旧 report / 未传 `--with-bg-shots`)时,fgParent 非空的文本
36
+ 优雅降级为跳过(与改前行为一致,不新增失败模式)。
37
+
38
+ 依赖:Pillow(ppt 工作流 stage_extract 已依赖 PIL,与其同环境)。缺 PIL / 缺截图 / 缺
39
+ textRuns 字段(旧 report)一律**响亮 SKIP 但 exit 0**(fail-open:像素检查是增强层,
40
+ 缺依赖不阻断工作流;SKIP 行会进 visual_quality_report 让人看见「本轮没做像素检查」)。
41
+
42
+ 用法:python text_readability_pixel.py <workdir> # 含 layout-report.json + slide-NN-background.png
43
+ """
44
+
45
+ import argparse
46
+ import json
47
+ import os
48
+ import re
49
+ import sys
50
+ from collections import Counter
51
+
52
+ # 采样与阈值(对照 2026-07-02 真机 deck 23 页逐页标定:02/08/12/15/23 该抓、03/22 像素实测可读不误报、
53
+ # 04/07/13/16/21 章节页装饰序号不误伤)
54
+ SAMPLE_GRID_W = 48 # 矩形内横向最多采样列数
55
+ SAMPLE_GRID_H = 16 # 纵向最多采样行数
56
+ CONTRAST_FLOOR = 3.0 # 放映级大字 AA 阈(与 layout_balance lowContrast 同口径)
57
+ FAIL_FRAC_ERROR = 0.35 # ≥35% 采样点低于阈 → ERROR(文字实质不可读)
58
+ FAIL_FRAC_WARN = 0.15 # ≥15% → WARNING
59
+ MIN_TEXT_LEN = 2 # 单字符/空串不查(装饰符)
60
+ MIN_RECT_PX = 24 # 矩形宽或高小于此不查(图钉级小元素)
61
+ DECOR_NUM_FONT = 96 # 纯数字 ≤3 字符且字号 ≥ 此值 = 章节页水印式装饰序号(低对比是设计意图),豁免
62
+ FG_COVER_SKIP = 0.60 # 文本矩形被某前景块(媒体图/卡)盖住 ≥60% → 真实背衬是该前景块而非背景图,
63
+ # 背景截图采样会错测(slide12 SCENE 文字底下是图块)→ 跳过;文字×前景的碰撞由重叠门负责。
64
+ # 分段局部判定(2026-07-03 真机 06 页根因:列表行一半压在平板黑边框上完全不可读,另一半是净区——
65
+ # 整框平均后失败率被稀释到阈下,连 WARNING 都没报)。把矩形横切 SEG_COUNT 段逐段判:任一段失败率
66
+ # ≥ SEG_FAIL_FRAC 即【文字局部压图不可读】ERROR。段内采样列数不足(窄文本)不做分段判定,防小样本误报。
67
+ SEG_COUNT = 6 # 横向分段数(6 段:单段全挂时整框最低 0.7/6≈11.7%,低于 WARN 线的局部遮挡也抓得到)
68
+ SEG_FAIL_FRAC = 0.70 # 单段失败率 ≥70% → 局部实质不可读
69
+ SEG_MIN_COLS = 6 # 每段至少采样列数(gw ≥ 36 才分段;窄文本整框判定已足够)
70
+ SYSTEMIC_COLOR_MIN_COUNT = 3 # 同一文字颜色的 ERROR 数达此值 → 判定系统性 CSS 变量问题,追加归组提示
71
+
72
+
73
+ def _parse_color(c):
74
+ """rgb()/rgba()/#hex → (r,g,b) 0-255;解析不了返回 None。"""
75
+ if not isinstance(c, str):
76
+ return None
77
+ m = re.match(r"rgba?\(([^)]+)\)", c.strip())
78
+ if m:
79
+ parts = [p.strip() for p in m.group(1).split(",")]
80
+ try:
81
+ vals = [float(p) for p in parts[:3]]
82
+ except ValueError:
83
+ return None
84
+ if len(vals) >= 3:
85
+ return tuple(min(255, max(0, int(v))) for v in vals)
86
+ return None
87
+ m = re.match(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$", c.strip())
88
+ if m:
89
+ h = m.group(1)
90
+ if len(h) == 3:
91
+ h = "".join(ch * 2 for ch in h)
92
+ return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
93
+ return None
94
+
95
+
96
+ def _rel_lum(rgb):
97
+ """WCAG 相对亮度(0-1,含 gamma 线性化)。"""
98
+ def f(v):
99
+ c = v / 255.0
100
+ return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
101
+ r, g, b = rgb[:3]
102
+ return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b)
103
+
104
+
105
+ def _contrast(l1, l2):
106
+ hi, lo = (l1, l2) if l1 >= l2 else (l2, l1)
107
+ return (hi + 0.05) / (lo + 0.05)
108
+
109
+
110
+ def _fg_cover_fraction(run, fg_rects, width, height):
111
+ """文本 px 矩形被前景块(归一化 rect)盖住的最大比例。"""
112
+ try:
113
+ rx, ry, rw, rh = (float(run[k]) for k in ("x", "y", "w", "h"))
114
+ except (KeyError, TypeError, ValueError):
115
+ return 0.0
116
+ if rw <= 0 or rh <= 0 or width <= 0 or height <= 0:
117
+ return 0.0
118
+ nx, ny, nw, nh = rx / width, ry / height, rw / width, rh / height
119
+ best = 0.0
120
+ for fr in fg_rects:
121
+ if not isinstance(fr, dict):
122
+ continue
123
+ try:
124
+ fx, fy, fw, fh = (float(fr.get(k) or 0.0) for k in ("x", "y", "w", "h"))
125
+ except (TypeError, ValueError):
126
+ continue
127
+ iw = min(nx + nw, fx + fw) - max(nx, fx)
128
+ ih = min(ny + nh, fy + fh) - max(ny, fy)
129
+ if iw > 0 and ih > 0:
130
+ best = max(best, (iw * ih) / (nw * nh))
131
+ return best
132
+
133
+
134
+ def _fmt_rect_px(run, width, height):
135
+ try:
136
+ return (
137
+ f"[x{run['x'] / width:.2f} y{run['y'] / height:.2f} "
138
+ f"w{run['w'] / width:.2f} h{run['h'] / height:.2f}]"
139
+ )
140
+ except (KeyError, TypeError, ZeroDivisionError):
141
+ return ""
142
+
143
+
144
+ def check_readability(workdir, report_path=None, verbose=False):
145
+ """返回 (problems, warnings, skips)。缺依赖/缺产物 → 全空 + skips 说明(fail-open)。"""
146
+ problems, warnings, skips = [], [], []
147
+ # ERROR 项的文字颜色(并行于 problems,供归组提示用;2026-07-03,见下方归组段落)。
148
+ problem_colors = []
149
+
150
+ try:
151
+ from PIL import Image # noqa: PLC0415 (可选依赖,缺则响亮降级)
152
+ except ImportError:
153
+ skips.append("Pillow(PIL) 未安装:跳过像素级文字可读性检查(本轮未做像素检查)。")
154
+ return problems, warnings, skips
155
+
156
+ report_path = report_path or os.path.join(workdir, "layout-report.json")
157
+ if not os.path.isfile(report_path):
158
+ skips.append(f"{report_path} 不存在:先跑 export --measure-only --with-bg-shots。")
159
+ return problems, warnings, skips
160
+ try:
161
+ with open(report_path, "r", encoding="utf-8") as f:
162
+ report = json.load(f)
163
+ except (OSError, json.JSONDecodeError) as exc:
164
+ skips.append(f"layout-report.json 解析失败:{exc}")
165
+ return problems, warnings, skips
166
+
167
+ width = report.get("width") or 1920
168
+ height = report.get("height") or 1080
169
+
170
+ for slide in report.get("slides") or []:
171
+ idx = slide.get("index")
172
+ label = slide.get("label") or f"slide {idx}"
173
+ runs = slide.get("textRuns")
174
+ if runs is None:
175
+ skips.append(f"{label}:report 无 textRuns 字段(旧 measure 产物),跳过。")
176
+ continue
177
+ shot = os.path.join(workdir, f"slide-{int(idx):02d}-background.png")
178
+ if not os.path.isfile(shot):
179
+ skips.append(f"{label}:缺 {os.path.basename(shot)}(measure 未带 --with-bg-shots),跳过。")
180
+ continue
181
+ try:
182
+ img = Image.open(shot).convert("RGB")
183
+ except OSError as exc:
184
+ skips.append(f"{label}:背景截图打开失败({exc}),跳过。")
185
+ continue
186
+ iw, ih = img.size
187
+ sx = iw / width
188
+ sy = ih / height
189
+ # foreground 态截图(文字隐藏、卡片/SVG/渐变原样渲染):给 fgParent 非空的文本采真实背衬。
190
+ # 缺失(旧 report/未传 --with-bg-shots)→ 这类文本优雅降级跳过,不报 skip(沿用改前行为,避免刷屏)。
191
+ fg_shot = os.path.join(workdir, f"slide-{int(idx):02d}-foreground.png")
192
+ fg_img = None
193
+ if os.path.isfile(fg_shot):
194
+ try:
195
+ fg_img = Image.open(fg_shot).convert("RGB")
196
+ except OSError:
197
+ fg_img = None
198
+ balance = slide.get("balance") or {}
199
+ # 前景块矩形(媒体图 + 卡/底板):fgParent=null 的文本被它们盖住时真实背衬不是背景图
200
+ fg_rects = list(balance.get("mediaRects") or [])
201
+ fg_rects += [c.get("rect") for c in (balance.get("cards") or []) if isinstance(c, dict)]
202
+
203
+ for run in runs:
204
+ text = (run.get("text") or "").strip()
205
+ if len(text) < MIN_TEXT_LEN:
206
+ continue
207
+ if text.isdigit() and len(text) <= 3 and (run.get("fontSize") or 0) >= DECOR_NUM_FONT:
208
+ continue # 章节页水印式大号序号(01-05):低对比是设计意图,不是缺陷
209
+ has_fg_parent = bool(run.get("fgParent"))
210
+ if has_fg_parent:
211
+ if fg_img is None:
212
+ continue # 无 foreground 截图,优雅降级跳过
213
+ sample_img, samp_sx, samp_sy = fg_img, fg_img.size[0] / width, fg_img.size[1] / height
214
+ else:
215
+ if fg_rects and _fg_cover_fraction(run, fg_rects, width, height) >= FG_COVER_SKIP:
216
+ continue # 底下垫着前景图块/卡但 DOM 未挂 fgParent:真实背衬非背景图;碰撞类问题由重叠门负责
217
+ sample_img, samp_sx, samp_sy = img, sx, sy
218
+ color = _parse_color(run.get("color"))
219
+ if color is None:
220
+ continue
221
+ try:
222
+ x, y, w, h = (float(run[k]) for k in ("x", "y", "w", "h"))
223
+ except (KeyError, TypeError, ValueError):
224
+ continue
225
+ if w < MIN_RECT_PX or h < MIN_RECT_PX:
226
+ continue
227
+ # 裁剪到画布内(越界文本归超画布门管);缩放系数按实际采样图自身尺寸算(foreground/background
228
+ # 截图理论同尺寸,但不假设——各自图自己算最稳妥)。
229
+ x0 = max(0.0, x) * samp_sx
230
+ y0 = max(0.0, y) * samp_sy
231
+ x1 = min(width, x + w) * samp_sx
232
+ y1 = min(height, y + h) * samp_sy
233
+ if x1 - x0 < 2 or y1 - y0 < 2:
234
+ continue
235
+ crop = sample_img.crop((int(x0), int(y0), int(x1), int(y1)))
236
+ # 缩到采样网格:几百个点足够统计,帧级快
237
+ gw = max(1, min(SAMPLE_GRID_W, crop.width))
238
+ gh = max(1, min(SAMPLE_GRID_H, crop.height))
239
+ crop = crop.resize((gw, gh))
240
+ text_lum = _rel_lum(color)
241
+ fails = 0
242
+ worst = None
243
+ total = gw * gh
244
+ col_fails = [0] * gw # 逐列失败计数(行主序 getdata:i % gw = 列号),供分段局部判定
245
+ for i, px in enumerate(crop.getdata()):
246
+ ratio = _contrast(text_lum, _rel_lum(px))
247
+ if ratio < CONTRAST_FLOOR:
248
+ fails += 1
249
+ col_fails[i % gw] += 1
250
+ if worst is None or ratio < worst:
251
+ worst = ratio
252
+ frac = fails / total if total else 0.0
253
+ # 分段局部判定:任一横段失败率 ≥ SEG_FAIL_FRAC = 局部实质不可读(整框平均会稀释它)。
254
+ seg_hit = None
255
+ if gw >= SEG_MIN_COLS * SEG_COUNT and gh > 0:
256
+ for s in range(SEG_COUNT):
257
+ c0 = s * gw // SEG_COUNT
258
+ c1 = (s + 1) * gw // SEG_COUNT
259
+ seg_total = (c1 - c0) * gh
260
+ if seg_total <= 0:
261
+ continue
262
+ seg_frac = sum(col_fails[c0:c1]) / seg_total
263
+ if seg_frac >= SEG_FAIL_FRAC:
264
+ seg_hit = (s + 1, seg_frac)
265
+ break
266
+ if verbose:
267
+ print(f" {label} · 「{text[:16]}」 fail={frac*100:.0f}% worst={worst or 0:.2f}"
268
+ + (f" seg{seg_hit[0]}={seg_hit[1]*100:.0f}%" if seg_hit else ""))
269
+ fix_hint = (
270
+ f" |可参考修法(非强制):文字下加局部 scrim/暗角底板(紧贴文字,禁大面积白卡)"
271
+ f" → 或把文字移动到背景净区 → 或换字色(深底白字/浅底深墨字)"
272
+ )
273
+ if frac >= FAIL_FRAC_WARN:
274
+ seg_note = (
275
+ f";左起第 {seg_hit[0]}/{SEG_COUNT} 段 {seg_hit[1]*100:.0f}% 局部实质不可读"
276
+ if seg_hit and frac < FAIL_FRAC_ERROR else ""
277
+ )
278
+ msg = (
279
+ f"【文字压图不可读】{label} · 「{text[:16]}」{_fmt_rect_px(run, width, height)}"
280
+ f" · 实测 {frac*100:.0f}% 采样点对比度 <{CONTRAST_FLOOR}(最差 {worst:.2f},文字 {run.get('color')}{seg_note})"
281
+ + fix_hint
282
+ )
283
+ if frac >= FAIL_FRAC_ERROR or seg_hit:
284
+ problems.append(msg)
285
+ problem_colors.append(run.get("color"))
286
+ else:
287
+ warnings.append(msg)
288
+ elif seg_hit:
289
+ problems.append(
290
+ f"【文字局部压图不可读】{label} · 「{text[:16]}」{_fmt_rect_px(run, width, height)}"
291
+ f" · 左起第 {seg_hit[0]}/{SEG_COUNT} 段 {seg_hit[1]*100:.0f}% 采样点对比度 <{CONTRAST_FLOOR}"
292
+ f"(整框仅 {frac*100:.0f}%——局部遮挡被整框平均稀释,文字 {run.get('color')})"
293
+ + fix_hint
294
+ )
295
+ problem_colors.append(run.get("color"))
296
+
297
+ # 系统性同色归组提示(2026-07-03 真机复盘:27 处 ERROR 里 15+ 处共享同一 CSS 变量取值,
298
+ # 逐条加局部 scrim 的修复策略在有限轮数内根本修不完;真正的修法是改一次共享 CSS 规则/变量)。
299
+ # 纯汇总已收集的 problem_colors,不新增判断/猜测——只是把「这些问题其实是一件事」的信号显式标出来。
300
+ color_counts = Counter(c for c in problem_colors if c)
301
+ for color, count in color_counts.most_common():
302
+ if count >= SYSTEMIC_COLOR_MIN_COUNT:
303
+ problems.append(
304
+ f"⚠️ {count} 处文字压图问题共享同一文字颜色 {color}:疑似同一 CSS 变量/class 系统性问题"
305
+ f"(如 `.t-eyebrow`/`.label` 之类被多处复用的强调色),建议**一次性改共享 CSS 规则/变量**"
306
+ f"(改深该颜色,或给该 class 统一加色块/半透明底),而非逐条加局部 scrim——"
307
+ f"逐条修在有限轮数内修不完,改一次能同时清零全部同色实例。"
308
+ )
309
+
310
+ return problems, warnings, skips
311
+
312
+
313
+ def main() -> int:
314
+ parser = argparse.ArgumentParser(
315
+ description="像素级文字压图可读性闸门(读 measure-only --with-bg-shots 产物;缺依赖响亮 SKIP 不阻断)"
316
+ )
317
+ parser.add_argument("workdir", help="含 layout-report.json + slide-NN-background.png 的目录")
318
+ parser.add_argument("--report", help="layout-report.json 路径(默认 <workdir>/layout-report.json)")
319
+ parser.add_argument("-v", "--verbose", action="store_true", help="打印每条文本的采样明细")
320
+ args = parser.parse_args()
321
+
322
+ problems, warnings, skips = check_readability(args.workdir, report_path=args.report, verbose=args.verbose)
323
+
324
+ for s in skips:
325
+ print("SKIP: " + s)
326
+ for w in warnings:
327
+ print("WARNING: " + w)
328
+ if problems:
329
+ print(f"文字压图可读性检查 FAILED —— {len(problems)} 处文字实质不可读:")
330
+ for p in problems:
331
+ print(" - " + p)
332
+ return 1
333
+ print("文字压图可读性检查 PASSED"
334
+ + ("(部分页跳过,见 SKIP)" if skips else "")
335
+ + f"(WARNING {len(warnings)} 条)")
336
+ return 0
337
+
338
+
339
+ if __name__ == "__main__":
340
+ sys.exit(main())
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ text_readability_pixel_test.py —— 像素级文字可读性门的回归锚(stdlib unittest + Pillow)。
5
+
6
+ 用 PIL 合成「左黑右白」背景图 + 手搓 layout-report.json,锁定:
7
+ - 白字压亮区 → ERROR;白字压暗区 → 通过(像素实测,不是 CSS 猜测);
8
+ - fgParent 文本跳过(卡内文字背衬是卡);
9
+ - 章节页水印式大号纯数字序号豁免;
10
+ - 文本被前景块盖住 ≥60% 跳过(真实背衬非背景图);
11
+ - 缺截图 / 旧 report 无 textRuns → SKIP 不报错(fail-open)。
12
+
13
+ 运行:python skills/ui-design/scripts/text_readability_pixel_test.py
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import sys
19
+ import tempfile
20
+ import unittest
21
+
22
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
+
24
+ try:
25
+ from PIL import Image
26
+ HAS_PIL = True
27
+ except ImportError:
28
+ HAS_PIL = False
29
+
30
+ from text_readability_pixel import check_readability # noqa: E402
31
+
32
+ W, H = 640, 360 # 测试画布(report 声明尺寸 = 截图尺寸,省缩放歧义)
33
+
34
+
35
+ def run(text="标题文字", x=0, y=0, w=W, h=64, color="rgb(255, 255, 255)",
36
+ font_size=40.0, fg_parent=None):
37
+ return {"text": text, "x": x, "y": y, "w": w, "h": h, "fontSize": font_size,
38
+ "color": color, "bgColor": None, "bgSolid": False, "fgParent": fg_parent}
39
+
40
+
41
+ @unittest.skipUnless(HAS_PIL, "Pillow 未安装(真实环境该门会响亮 SKIP,见 test_missing 系列)")
42
+ class TextReadabilityPixelTest(unittest.TestCase):
43
+ def setUp(self):
44
+ self.dir = tempfile.mkdtemp()
45
+
46
+ def tearDown(self):
47
+ for f in os.listdir(self.dir):
48
+ try:
49
+ os.remove(os.path.join(self.dir, f))
50
+ except OSError:
51
+ pass
52
+ os.rmdir(self.dir)
53
+
54
+ def _write(self, slides, shots=True, fg_shots=False, fg_inverted=False):
55
+ """slides: [(textRuns, balance_extra)];每页配一张左黑右白的背景图。
56
+ fg_shots=True 时额外落一张 foreground 截图(fg_inverted=True 时明暗左右互换,
57
+ 用来验证 fgParent 文本确实采样自 foreground 图、不是背景图)。"""
58
+ report = {"mode": "hybrid", "width": W, "height": H, "slides": []}
59
+ for i, (runs, balance_extra) in enumerate(slides, start=1):
60
+ balance = {"mediaRects": [], "cards": []}
61
+ balance.update(balance_extra or {})
62
+ report["slides"].append(
63
+ {"index": i, "label": f"{i:02d} 测试页", "balance": balance, "textRuns": runs})
64
+ if shots:
65
+ img = Image.new("RGB", (W, H), (250, 250, 250)) # 右/整体亮
66
+ dark = Image.new("RGB", (W // 2, H), (10, 12, 20)) # 左半暗
67
+ img.paste(dark, (0, 0))
68
+ img.save(os.path.join(self.dir, f"slide-{i:02d}-background.png"))
69
+ if fg_shots:
70
+ # 默认与背景截图同花色(左暗右亮);fg_inverted=True 时左右互换,
71
+ # 用来证明采样确实来自 foreground 图(背景图此处会给出相反的可读性结论)。
72
+ left, right = ((250, 250, 250), (10, 12, 20)) if fg_inverted else ((10, 12, 20), (250, 250, 250))
73
+ fg = Image.new("RGB", (W, H), right)
74
+ fg.paste(Image.new("RGB", (W // 2, H), left), (0, 0))
75
+ fg.save(os.path.join(self.dir, f"slide-{i:02d}-foreground.png"))
76
+ with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
77
+ json.dump(report, f, ensure_ascii=False)
78
+
79
+ def test_white_text_on_bright_area_is_error(self):
80
+ # 白字整框压在右侧亮区 → 100% 采样点对比度 <3 → ERROR。
81
+ self._write([([run(x=W // 2, w=W // 2)], None)])
82
+ problems, warnings, skips = check_readability(self.dir)
83
+ self.assertTrue(any("压图不可读" in p for p in problems), (problems, warnings, skips))
84
+
85
+ def test_white_text_on_dark_area_passes(self):
86
+ # 白字压左侧暗区 → 像素实测可读(CSS 检查在这类页会误报 1.05,这里是真采样)。
87
+ self._write([([run(x=0, w=W // 2)], None)])
88
+ problems, warnings, _s = check_readability(self.dir)
89
+ self.assertEqual(problems, [], problems)
90
+ self.assertEqual(warnings, [], warnings)
91
+
92
+ def test_partial_bright_tail_escalates_via_segment(self):
93
+ # 框 x80-400、暗区 0-320 → 亮区占框尾部 25%:整框 25%(原本只 WARNING),
94
+ # 但尾段失败率 100% ≥70% → 分段判定升 ERROR(2026-07-03:局部遮挡不再被整框平均稀释)。
95
+ self._write([([run(x=W // 8, w=W // 2)], None)])
96
+ problems, warnings, _s = check_readability(self.dir)
97
+ hits = [p for p in problems if "压图不可读" in p]
98
+ self.assertEqual(len(hits), 1, (problems, warnings))
99
+ self.assertIn("局部实质不可读", hits[0])
100
+
101
+ def test_bottom_stripe_stays_warning(self):
102
+ # 亮区是横向贯通的底部条纹(每列失败 25% 均匀分布)→ 无单段 ≥70% → 仍 WARNING 不升级。
103
+ report = {"mode": "hybrid", "width": W, "height": H, "slides": [
104
+ {"index": 1, "label": "01 测试页", "balance": {"mediaRects": [], "cards": []},
105
+ "textRuns": [run(x=0, w=W, h=160)]}]}
106
+ with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
107
+ json.dump(report, f, ensure_ascii=False)
108
+ img = Image.new("RGB", (W, H), (10, 12, 20)) # 整体暗(白字可读)
109
+ img.paste(Image.new("RGB", (W, 40), (250, 250, 250)), (0, 120)) # 框内底部 40/160=25% 亮条
110
+ img.save(os.path.join(self.dir, "slide-01-background.png"))
111
+ problems, warnings, _s = check_readability(self.dir)
112
+ self.assertEqual(problems, [], problems)
113
+ self.assertTrue(any("压图不可读" in w for w in warnings), warnings)
114
+
115
+ def test_localized_below_warn_threshold_caught_by_segment(self):
116
+ # 亮区只占整框 ~12.5%(低于 15% WARN 线,改前完全不报),但集中在第 6 段(~86% 段失败率)
117
+ # → 【文字局部压图不可读】ERROR。正是真机 06 页「列表行局部压平板黑边框」的形态。
118
+ report = {"mode": "hybrid", "width": W, "height": H, "slides": [
119
+ {"index": 1, "label": "01 测试页", "balance": {"mediaRects": [], "cards": []},
120
+ "textRuns": [run(x=0, w=W, h=64)]}]}
121
+ with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
122
+ json.dump(report, f, ensure_ascii=False)
123
+ img = Image.new("RGB", (W, H), (10, 12, 20)) # 整体暗
124
+ img.paste(Image.new("RGB", (80, H), (250, 250, 250)), (548, 0)) # 亮块 x548-628(第 6 段内)
125
+ img.save(os.path.join(self.dir, "slide-01-background.png"))
126
+ problems, warnings, _s = check_readability(self.dir)
127
+ hits = [p for p in problems if "文字局部压图不可读" in p]
128
+ self.assertEqual(len(hits), 1, (problems, warnings))
129
+ self.assertIn("第 6/6 段", hits[0])
130
+ self.assertEqual(warnings, [], warnings)
131
+
132
+ def test_card_text_skipped_without_foreground_shot(self):
133
+ # 卡内文字(fgParent 非空)+ 无 foreground 截图(旧 report/未传 --with-bg-shots)→ 优雅降级跳过。
134
+ self._write([([run(x=W // 2, w=W // 2, fg_parent="fg-0-1")], None)])
135
+ problems, warnings, _s = check_readability(self.dir)
136
+ self.assertEqual(problems, [], problems)
137
+ self.assertEqual(warnings, [], warnings)
138
+
139
+ def test_card_text_sampled_from_foreground_shot(self):
140
+ # 2026-07-03:有 foreground 截图时卡内文字不再跳过,从它采样——白字压 foreground 图的亮区 → ERROR。
141
+ # 背景截图右半同样是亮区(同花色未 inverted),如果误采背景图结论一致,不能单独证明用对了图;
142
+ # 用 test_card_text_uses_foreground_not_background 补一个明暗相反的对照用例来坐实。
143
+ self._write([([run(x=W // 2, w=W // 2, fg_parent="fg-0-1")], None)], fg_shots=True)
144
+ problems, warnings, _s = check_readability(self.dir)
145
+ self.assertTrue(any("压图不可读" in p for p in problems), (problems, warnings))
146
+
147
+ def test_card_text_uses_foreground_not_background(self):
148
+ # foreground 截图与背景截图明暗互换:文字位置在背景图是「暗区可读」,在 foreground 图是「亮区不可读」。
149
+ # 若采样源用错(误采背景图)本例会通过;实际应因采了 foreground 图而报 ERROR,坐实采样源正确。
150
+ self._write([([run(x=0, w=W // 2, fg_parent="fg-0-1")], None)], fg_shots=True, fg_inverted=True)
151
+ problems, warnings, _s = check_readability(self.dir)
152
+ self.assertTrue(any("压图不可读" in p for p in problems), (problems, warnings))
153
+
154
+ def test_card_text_readable_via_foreground_shot(self):
155
+ # 卡内文字压在 foreground 图的暗区 → 像素实测可读,不报。
156
+ self._write([([run(x=0, w=W // 2, fg_parent="fg-0-1")], None)], fg_shots=True)
157
+ problems, warnings, _s = check_readability(self.dir)
158
+ self.assertEqual(problems, [], problems)
159
+ self.assertEqual(warnings, [], warnings)
160
+
161
+ def test_decorative_numeral_exempt(self):
162
+ # 章节页水印式大号序号(纯数字 ≤3 字符、字号 ≥96):低对比是设计意图。
163
+ self._write([([run(text="01", x=W // 2, w=200, h=180, font_size=170.0)], None)])
164
+ problems, warnings, _s = check_readability(self.dir)
165
+ self.assertEqual(problems, [], problems)
166
+ self.assertEqual(warnings, [], warnings)
167
+
168
+ def test_systemic_color_grouping_hint_at_three_plus(self):
169
+ # 2026-07-03:3 处 ERROR 共享同一文字颜色 → 追加归组提示(真机复盘:15+ 处共享
170
+ # 同一 VI 强调色变量,逐条加 scrim 在有限轮数内修不完,需要一次性改共享 CSS 规则)。
171
+ runs = [
172
+ run(text=f"标题{i}", x=W // 2, y=i * 60, w=W // 2, h=50)
173
+ for i in range(3)
174
+ ] # 同色(默认白字)、都压在右侧亮区 → 3 处 ERROR
175
+ self._write([(runs, None)])
176
+ problems, _w, _s = check_readability(self.dir)
177
+ hits = [p for p in problems if "共享同一文字颜色" in p]
178
+ self.assertEqual(len(hits), 1, problems)
179
+ self.assertIn("rgb(255, 255, 255)", hits[0])
180
+ self.assertIn("3 处", hits[0])
181
+
182
+ def test_no_grouping_hint_below_threshold(self):
183
+ # 仅 2 处同色 ERROR(未达 SYSTEMIC_COLOR_MIN_COUNT=3)→ 不追加归组提示。
184
+ runs = [run(text=f"标题{i}", x=W // 2, y=i * 60, w=W // 2, h=50) for i in range(2)]
185
+ self._write([(runs, None)])
186
+ problems, _w, _s = check_readability(self.dir)
187
+ self.assertFalse(any("共享同一文字颜色" in p for p in problems), problems)
188
+
189
+ def test_fg_covered_text_skipped(self):
190
+ # 文本被前景媒体块盖住 ≥60% → 真实背衬是该图块(slide12 SCENE 案)→ 跳过。
191
+ media = [{"x": 0.5, "y": 0.0, "w": 0.5, "h": 1.0}]
192
+ self._write([([run(x=W // 2, w=W // 2)], {"mediaRects": media})])
193
+ problems, warnings, _s = check_readability(self.dir)
194
+ self.assertEqual(problems, [], problems)
195
+ self.assertEqual(warnings, [], warnings)
196
+
197
+ def test_missing_shot_is_skip_not_error(self):
198
+ self._write([([run()], None)], shots=False)
199
+ problems, _w, skips = check_readability(self.dir)
200
+ self.assertEqual(problems, [], problems)
201
+ self.assertTrue(any("缺" in s for s in skips), skips)
202
+
203
+ def test_old_report_without_textruns_is_skip(self):
204
+ report = {"mode": "hybrid", "width": W, "height": H,
205
+ "slides": [{"index": 1, "label": "01 旧页", "balance": {}}]}
206
+ with open(os.path.join(self.dir, "layout-report.json"), "w", encoding="utf-8") as f:
207
+ json.dump(report, f, ensure_ascii=False)
208
+ problems, _w, skips = check_readability(self.dir)
209
+ self.assertEqual(problems, [], problems)
210
+ self.assertTrue(any("textRuns" in s for s in skips), skips)
211
+
212
+ def test_missing_report_is_skip(self):
213
+ problems, _w, skips = check_readability(self.dir)
214
+ self.assertEqual(problems, [], problems)
215
+ self.assertTrue(skips, skips)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ unittest.main(verbosity=2)