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,713 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+
11
+
12
+ SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "deck_visual_style_check.py")
13
+
14
+
15
+ def run_check(html, design, image_report=None, source_plan=None, strict=False):
16
+ with tempfile.TemporaryDirectory() as d:
17
+ deck = os.path.join(d, "deck.html")
18
+ design_path = os.path.join(d, "design.json")
19
+ with open(deck, "w", encoding="utf-8") as f:
20
+ f.write(html)
21
+ with open(design_path, "w", encoding="utf-8") as f:
22
+ json.dump(design, f, ensure_ascii=False)
23
+ cmd = [sys.executable, SCRIPT, deck, "--design", design_path, "-v"]
24
+ if image_report is not None:
25
+ report_path = os.path.join(d, "image_generation_report.json")
26
+ with open(report_path, "w", encoding="utf-8") as f:
27
+ json.dump(image_report, f, ensure_ascii=False)
28
+ cmd.extend(["--image-report", report_path])
29
+ if source_plan is not None:
30
+ plan_path = os.path.join(d, "source_layout_plan.json")
31
+ with open(plan_path, "w", encoding="utf-8") as f:
32
+ json.dump(source_plan, f, ensure_ascii=False)
33
+ cmd.extend(["--source-plan", plan_path])
34
+ if strict:
35
+ cmd.append("--strict-product-detail")
36
+ # 显式 UTF-8:中文 Windows 上 text=True 默认 locale=GBK,解码子进程 UTF-8 中文输出必崩
37
+ # (与 visual_quality_gate.py::_run 同一防御;bash.ts 的 PYTHONUTF8 只护工作流内运行)。
38
+ return subprocess.run(
39
+ cmd,
40
+ text=True,
41
+ encoding="utf-8",
42
+ errors="replace",
43
+ stdout=subprocess.PIPE,
44
+ stderr=subprocess.PIPE,
45
+ check=False,
46
+ )
47
+
48
+
49
+ def design(style="scenario-fullbleed", grammar="hero-scene"):
50
+ return {
51
+ "pages": [
52
+ {
53
+ "page": 1,
54
+ "composition_style": style,
55
+ "page_grammar": grammar,
56
+ "max_visible_words": 80,
57
+ "supporting_visual_layer": "前景产品特写",
58
+ "title": "测试页",
59
+ }
60
+ ]
61
+ }
62
+
63
+
64
+ def section(body, label="01 测试页", cls="slide fullbleed"):
65
+ return f"<html><body><section class='{cls}' data-screen-label='{label}'>{body}</section></body></html>"
66
+
67
+
68
+ class DeckVisualStyleCheckTest(unittest.TestCase):
69
+ def test_good_fullbleed_passes(self):
70
+ html = section(
71
+ """
72
+ <div class="edit-bg"><img class="bg-img" src="assets/ai_p1.png"></div>
73
+ <div class="edit-scrim"></div>
74
+ <div class="edit-content"><h1>全过程可追溯</h1><p>一眼看见关键质量链路。</p><img class="product-visual" src="assets/hero.png"></div>
75
+ """
76
+ )
77
+ res = run_check(html, design())
78
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
79
+ self.assertIn("DECK_VISUAL_STYLE_OK", res.stdout)
80
+
81
+ def test_speaker_notes_aside_excluded_from_scan(self):
82
+ # 讲稿 aside(hidden,导出进 PPT 备注栏):正文提到 matrix/panel 是在讲内容,
83
+ # 不得被源码级 CARD/TABLE 扫描误判成卡片墙/表格墙(check_section 先剥 aside)。
84
+ html = section(
85
+ """
86
+ <div class="edit-bg"><img class="bg-img" src="assets/ai_p1.png"></div>
87
+ <div class="edit-scrim"></div>
88
+ <div class="edit-content"><h1>全过程可追溯</h1><p>一眼看见关键质量链路。</p><img class="product-visual" src="assets/hero.png"></div>
89
+ <aside class="speaker-notes" hidden>讲稿:这页论证对应 matrix 对比与 panel panel 参数细节,演讲时口述,不上屏。</aside>
90
+ """
91
+ )
92
+ res = run_check(html, design())
93
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
94
+ self.assertIn("DECK_VISUAL_STYLE_OK", res.stdout)
95
+
96
+ def test_card_wall_fails(self):
97
+ cards = "".join(f"<div class='dark-card'><h3>{i}</h3><p>内容</p></div>" for i in range(5))
98
+ html = section(f"<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>{cards}")
99
+ res = run_check(html, design("feature-theater"))
100
+ self.assertNotEqual(res.returncode, 0)
101
+ self.assertIn("卡片墙", res.stdout)
102
+
103
+ def test_table_wall_needs_data_visual(self):
104
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div><div class='compare-grid'>矩阵</div>")
105
+ res = run_check(html, design("scenario-fullbleed"))
106
+ self.assertNotEqual(res.returncode, 0)
107
+ self.assertIn("表格/矩阵", res.stdout)
108
+
109
+ def test_data_visual_allows_compare_grid(self):
110
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div><div class='compare-grid'><svg class='chart bar-chart'></svg>矩阵</div>")
111
+ d = design("data-visual", "data-visual")
112
+ d["pages"][0]["supporting_visual_layer"] = "HTML/SVG 数据图"
113
+ res = run_check(html, d)
114
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
115
+
116
+ def test_data_visual_without_chart_fails(self):
117
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div><div class='compare-grid'>矩阵</div>")
118
+ d = design("data-visual", "data-visual")
119
+ d["pages"][0]["supporting_visual_layer"] = "HTML/SVG 数据图"
120
+ res = run_check(html, d)
121
+ self.assertNotEqual(res.returncode, 0)
122
+ self.assertIn("data-visual", res.stdout)
123
+
124
+ def test_image_mosaic_photo_tiles_pass(self):
125
+ tiles = "".join(
126
+ f"<figure class='image-tile'><img src='assets/shot{i}.png'><figcaption>短标签{i}</figcaption></figure>"
127
+ for i in range(4)
128
+ )
129
+ html = section(f"<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div><div class='mosaic'>{tiles}</div>")
130
+ d = design("gallery-strip", "image-mosaic")
131
+ d["pages"][0]["supporting_visual_layer"] = "影像拼贴"
132
+ d["pages"][0]["max_visible_words"] = 120
133
+ res = run_check(html, d)
134
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
135
+
136
+ def test_image_mosaic_text_card_grid_fails(self):
137
+ cards = "".join(f"<div class='image-tile'><h3>标题{i}</h3><p>这是一段较长说明文字,不是图片短标签。</p></div>" for i in range(4))
138
+ html = section(f"<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>{cards}")
139
+ d = design("gallery-strip", "image-mosaic")
140
+ d["pages"][0]["supporting_visual_layer"] = "影像拼贴"
141
+ res = run_check(html, d)
142
+ self.assertNotEqual(res.returncode, 0)
143
+ self.assertIn("image-mosaic", res.stdout)
144
+
145
+ def test_image_mosaic_gallery_with_supplementary_explanation_cards_passes(self):
146
+ # 2026-07-06/07 两次真机复盘同一结论:"3 张真实照片图集 + 4-5 张解释文字卡片"是合理的
147
+ # 混合版式(图集本身已充分满足图片证据,卡片是独立补充说明区,不是把图片顶替成了文字卡片
148
+ # 墙),此前 card_count>=img_count 就报"网格必须以图片为主体"会误伤这种版式;已改为只在
149
+ # img_count<3(真的没凑够图片数量)时才检查卡片是否顶替了图片。
150
+ gallery = "".join(f"<img class='image-tile' src='assets/shot{i}.png'>" for i in range(3))
151
+ cards = "".join(
152
+ f"<div class='image-tile'><h3>标题{i}</h3><p>这是一段解释这个痛点的说明文字。</p></div>"
153
+ for i in range(5)
154
+ )
155
+ html = section(f"<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>{gallery}{cards}")
156
+ d = design("gallery-strip", "image-mosaic")
157
+ d["pages"][0]["supporting_visual_layer"] = "影像拼贴"
158
+ res = run_check(html, d)
159
+ self.assertNotIn("网格必须以图片为主体", res.stdout)
160
+
161
+ def test_missing_background_fails(self):
162
+ html = section("<div class='edit-content'><h1>没有背景</h1><img src='assets/hero.png'></div>")
163
+ res = run_check(html, design("product-hero"))
164
+ self.assertNotEqual(res.returncode, 0)
165
+ self.assertIn("缺少背景", res.stdout)
166
+
167
+ def test_object_fit_fill_fails(self):
168
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png' style='object-fit:fill'></div><img src='assets/hero.png'>")
169
+ res = run_check(html, design("product-hero"))
170
+ self.assertNotEqual(res.returncode, 0)
171
+ self.assertIn("object-fit:fill", res.stdout)
172
+
173
+ def test_image_with_white_padding_frame_fails(self):
174
+ html = section(
175
+ """
176
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>
177
+ <div class='fig-fill' style='background:white;padding:24px;border:1px solid #ddd'>
178
+ <img src='assets/product.png'>
179
+ </div>
180
+ """
181
+ )
182
+ res = run_check(html, design("product-hero", "product-closeup"))
183
+ self.assertNotEqual(res.returncode, 0)
184
+ self.assertIn("裱成小卡", res.stdout)
185
+
186
+ def test_source_enhanced_hero_cannot_be_white_framed(self):
187
+ html = section(
188
+ """
189
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p9.png'></div>
190
+ <figure class='product-visual' style='background:#fff;padding:32px;border:1px solid #ddd'>
191
+ <img src='assets/enhanced_src_4.png' data-source-file='src_4.png'>
192
+ </figure>
193
+ """
194
+ )
195
+ source_plan = {
196
+ "enhance_entries": [
197
+ {
198
+ "file": "enhanced_src_4.png",
199
+ "source_file": "src_4.png",
200
+ "enhance_style": "hero",
201
+ "image_role": "foreground_hero",
202
+ }
203
+ ]
204
+ }
205
+ res = run_check(html, design("feature-theater", "cinematic-feature"), source_plan=source_plan, strict=True)
206
+ self.assertNotEqual(res.returncode, 0)
207
+ self.assertIn("source-enhanced", res.stdout)
208
+
209
+ def test_source_enhanced_allows_text_padding_elsewhere(self):
210
+ html = section(
211
+ """
212
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p9.png'></div>
213
+ <img class='product-visual' src='assets/enhanced_src_4.png' data-source-file='src_4.png'>
214
+ <div style='padding:32px;border-left:4px solid #2477a6'>
215
+ <h1>全景证据</h1><p>这只是文字净区,不是图片白框。</p>
216
+ </div>
217
+ """
218
+ )
219
+ source_plan = {
220
+ "enhance_entries": [
221
+ {
222
+ "file": "enhanced_src_4.png",
223
+ "source_file": "src_4.png",
224
+ "enhance_style": "hero",
225
+ "image_role": "foreground_hero",
226
+ }
227
+ ]
228
+ }
229
+ res = run_check(html, design("feature-theater", "cinematic-feature"), source_plan=source_plan, strict=True)
230
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
231
+
232
+ def test_quote_focus_card_summary_fails(self):
233
+ cards = "".join(f"<div class='panel'><h3>总结{i}</h3><p>文字</p></div>" for i in range(3))
234
+ html = section(f"<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>{cards}")
235
+ d = design("quote-focus", "hero-scene")
236
+ d["pages"][0]["supporting_visual_layer"] = "底部场景锚点"
237
+ res = run_check(html, d)
238
+ self.assertNotEqual(res.returncode, 0)
239
+ self.assertIn("卡片", res.stdout)
240
+
241
+ def test_before_after_cards_fail(self):
242
+ html = section(
243
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
244
+ "<div class='card'>Before</div><div class='card'>After</div><img class='product-visual' src='assets/hero.png'>"
245
+ )
246
+ res = run_check(html, design("feature-theater", "before-after-scene"))
247
+ self.assertNotEqual(res.returncode, 0)
248
+ self.assertIn("before-after", res.stdout)
249
+
250
+ def test_supporting_visual_required(self):
251
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div><h1>只有文字</h1>")
252
+ res = run_check(html, design("scenario-fullbleed", "hero-scene"))
253
+ self.assertNotEqual(res.returncode, 0)
254
+ self.assertIn("辅助视觉层", res.stdout)
255
+
256
+ def test_supporting_visual_exempt_when_contract_declares_no_foreground_asset(self):
257
+ # 2026-07-06 真机误报:design.json 自己的 visual_asset_contract.foreground_asset=""
258
+ # 是设计阶段明确表态"这页前景本就不打算配真实图片"(如章节页副标题浮前景本身就是纯文字层),
259
+ # 此时不该再靠 supporting_visual_layer 里的"前景/特写"关键词猜测误判缺图。
260
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div><h1>只有文字</h1>")
261
+ d = design("scenario-fullbleed", "hero-scene")
262
+ d["pages"][0]["visual_asset_contract"] = {
263
+ "background_asset": "ai_p1.png", "foreground_role": "html_svg_marks", "foreground_asset": "",
264
+ }
265
+ res = run_check(html, d)
266
+ self.assertNotIn("辅助视觉层", res.stdout)
267
+
268
+ def test_style_needs_image_exempt_when_contract_declares_no_image_at_all(self):
269
+ # 同一原理用于 composition_style/page_grammar 的"需要图片"检查:目录页这类纯 HTML/SVG
270
+ # 索引页,contract 的 background_asset 和 foreground_asset 都显式声明为空,不该被
271
+ # feature-theater/cinematic-feature 这类样式标签反过来强加"必须有 img"的要求。
272
+ html = section("<div class='edit-bg' style='background:#fff'><h1>目录</h1></div>")
273
+ d = design("feature-theater", "cinematic-feature")
274
+ d["pages"][0]["visual_asset_contract"] = {
275
+ "background_role": "vi_fallback", "background_asset": "",
276
+ "foreground_role": "html_svg_marks", "foreground_asset": "",
277
+ }
278
+ res = run_check(html, d)
279
+ self.assertEqual(res.returncode, 0, res.stdout)
280
+
281
+ def test_strict_real_background_image_check_exempt_when_asset_declared_in_foreground(self):
282
+ # 2026-07-06 真机误报:design 常故意把真实图片契约声明在前景槽(background_role=
283
+ # "vi_fallback" 留白背景、真实主视觉走 foreground_asset,产品实物图放前景插图而非铺满
284
+ # 整页背景),--strict-product-detail 的"没有真实背景图片"检查只认 bg_srcs,把这种
285
+ # 合法设计误判成"没有真实图片"。
286
+ html = section(
287
+ "<div class='edit-bg' style='background:var(--vi-fallback)'></div>"
288
+ "<h1>技术成熟度</h1><img class='product-visual' src='assets/enhanced_src_7.png'>"
289
+ )
290
+ d = design("feature-theater", "cinematic-feature")
291
+ d["pages"][0]["visual_asset_contract"] = {
292
+ "background_role": "vi_fallback", "background_asset": "",
293
+ "foreground_role": "source_cutout", "foreground_asset": "enhanced_src_7.png",
294
+ }
295
+ res = run_check(html, d, strict=True)
296
+ self.assertNotIn("没有真实背景图片", res.stdout)
297
+
298
+ def test_strict_real_background_image_check_still_fires_without_contract(self):
299
+ # 回归锁:没有 visual_asset_contract 字段(旧版 design.json)时,原有基于 bg_srcs 的
300
+ # 粗糙检查必须继续生效,不能被本次修复连带弱化。
301
+ html = section("<div class='edit-bg' style='background:#eee'></div><h1>标题</h1>")
302
+ res = run_check(html, design("feature-theater", "cinematic-feature"), strict=True)
303
+ self.assertIn("没有真实背景图片", res.stdout)
304
+
305
+ def test_decorative_crosshair_svg_lines_do_not_count_as_orphan_connector(self):
306
+ # crosshair-svg 是模型在提示词统一命名 vi-deco 之前自造的同义装饰类名(2026-07-06 真机
307
+ # 复盘);checker 的宽松兜底同义词列表需要认得它,不只认 vi-deco。
308
+ html = section(
309
+ """
310
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p2.png'></div>
311
+ <svg class='crosshair-svg' viewBox='0 0 1920 1080'>
312
+ <circle cx='960' cy='540' r='220'></circle>
313
+ <line x1='960' y1='0' x2='960' y2='1080'></line>
314
+ </svg>
315
+ <h1>目录</h1>
316
+ """
317
+ )
318
+ res = run_check(html, design())
319
+ self.assertNotIn("孤立", res.stdout)
320
+
321
+ def test_concentric_circle_radar_svg_without_any_class_not_orphan_connector(self):
322
+ # 2026-07-06 真机三度复盘:这次模型连 class 都没写(命名约定型豁免 DECOR_SVG_RE 管不到),
323
+ # 结构特征识别(同心圆)应该不靠命名也能认出这是纯装饰雷达准线,不是缺节点的图表连线。
324
+ html = section(
325
+ """
326
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p14.png'></div>
327
+ <svg viewBox='0 0 600 600'>
328
+ <circle cx='300' cy='300' r='280'></circle>
329
+ <circle cx='300' cy='300' r='220'></circle>
330
+ <circle cx='300' cy='300' r='160'></circle>
331
+ <line x1='20' y1='300' x2='580' y2='300'></line>
332
+ <line x1='300' y1='20' x2='300' y2='580'></line>
333
+ </svg>
334
+ <h1>联系我们</h1>
335
+ """
336
+ )
337
+ res = run_check(html, design())
338
+ self.assertNotIn("孤立", res.stdout)
339
+
340
+ def test_non_concentric_circles_still_count_as_orphan_connector(self):
341
+ # 回归锁:多个圆心不同的圆(不是同心圆)不该被结构豁免误伤——这类 line 缺节点的检测
342
+ # 必须继续生效。
343
+ html = section(
344
+ """
345
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p14.png'></div>
346
+ <svg viewBox='0 0 600 600'>
347
+ <circle cx='100' cy='100' r='20'></circle>
348
+ <circle cx='400' cy='400' r='20'></circle>
349
+ <line x1='100' y1='100' x2='400' y2='400'></line>
350
+ </svg>
351
+ <h1>关系链路</h1>
352
+ """
353
+ )
354
+ res = run_check(html, design())
355
+ self.assertIn("孤立", res.stdout)
356
+
357
+ def test_labeled_rect_comparison_diagram_without_class_not_orphan_connector(self):
358
+ # 2026-07-07 真机三度复盘(同一"命名约定不可靠"主题第三种表现):真实 4 组 rect+text
359
+ # 对比节点用带箭头的 line 连接,但模型没给 rect 写任何 node/step/chip/pill 类名,
360
+ # DIAGRAM_NODE_RE 纯命名匹配抓不到。结构特征(形状+≥2 文字标签)应该不靠命名也能
361
+ # 认出这是真实图表节点,不是缺节点的孤立连线。
362
+ html = section(
363
+ """
364
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p6.png'></div>
365
+ <svg viewBox='0 0 600 300'>
366
+ <rect x='20' y='40' width='120' height='60'></rect>
367
+ <text x='30' y='70'>记录主体→机器</text>
368
+ <rect x='460' y='40' width='120' height='60'></rect>
369
+ <text x='470' y='70'>空间覆盖→360°</text>
370
+ <line x1='140' y1='70' x2='460' y2='70' marker-end='url(#arrow)'></line>
371
+ </svg>
372
+ <h1>4维翻转</h1>
373
+ """
374
+ )
375
+ res = run_check(html, design())
376
+ self.assertNotIn("孤立", res.stdout)
377
+
378
+ def test_unlabeled_shapes_with_line_still_count_as_orphan_connector(self):
379
+ # 回归锁:形状元素配文字标签不足 2 个(或干脆没有)时,结构豁免不该生效——
380
+ # 这类真的缺节点归属的孤立连线检测必须继续工作。
381
+ html = section(
382
+ """
383
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p14.png'></div>
384
+ <svg viewBox='0 0 600 300'>
385
+ <rect x='20' y='40' width='120' height='60'></rect>
386
+ <line x1='140' y1='70' x2='460' y2='70'></line>
387
+ </svg>
388
+ <h1>关系链路</h1>
389
+ """
390
+ )
391
+ res = run_check(html, design())
392
+ self.assertIn("孤立", res.stdout)
393
+
394
+ def test_degraded_ai_image_fails(self):
395
+ html = section(
396
+ """
397
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p7.png'></div>
398
+ <div class='edit-content'><h1>场景页</h1><img class='product-visual' src='assets/hero.png'></div>
399
+ """
400
+ )
401
+ report = {
402
+ "qualityOk": False,
403
+ "degraded": [{"file": "ai_p7.png", "fallback": "gradient-png", "reason": "总时长预算耗尽"}],
404
+ }
405
+ res = run_check(html, design("scenario-fullbleed", "hero-scene"), image_report=report, strict=True)
406
+ self.assertNotEqual(res.returncode, 0)
407
+ self.assertIn("来自生图降级", res.stdout)
408
+
409
+ def test_same_image_background_and_foreground_fails(self):
410
+ html = section(
411
+ """
412
+ <div class='edit-bg'><img class='bg-img' src='assets/enhanced_src_6.png'></div>
413
+ <img class='product-visual' src='assets/enhanced_src_6.png'>
414
+ """
415
+ )
416
+ res = run_check(html, design("feature-theater", "cinematic-feature"), strict=True)
417
+ self.assertNotEqual(res.returncode, 0)
418
+ self.assertIn("同时作为背景和前景", res.stdout)
419
+
420
+ def test_ai_background_name_cannot_be_foreground(self):
421
+ html = section(
422
+ """
423
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p7.png'></div>
424
+ <img class='product-visual' src='assets/ai_p7.png'>
425
+ """
426
+ )
427
+ res = run_check(html, design("feature-theater", "cinematic-feature"), strict=True)
428
+ self.assertNotEqual(res.returncode, 0)
429
+ self.assertIn("ai_pN_fg.png", res.stdout)
430
+
431
+ def test_cutout_source_cannot_be_framed(self):
432
+ html = section(
433
+ """
434
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p11.png'></div>
435
+ <div class='product-visual' style='background:white;padding:28px;border:1px solid #ddd'>
436
+ <img src='assets/enhanced_src_9.png' data-source-file='src_9.png'>
437
+ </div>
438
+ """
439
+ )
440
+ source_plan = {
441
+ "enhance_entries": [
442
+ {
443
+ "file": "enhanced_src_9.png",
444
+ "source_file": "src_9.png",
445
+ "enhance_style": "cutout",
446
+ "image_role": "foreground_cutout",
447
+ }
448
+ ]
449
+ }
450
+ res = run_check(html, design("detail-closeup", "product-closeup"), source_plan=source_plan, strict=True)
451
+ self.assertNotEqual(res.returncode, 0)
452
+ self.assertIn("cutout 客户图", res.stdout)
453
+
454
+ def test_small_svg_text_fails(self):
455
+ html = section(
456
+ """
457
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p13.png'></div>
458
+ <svg class='data-viz'><text font-size='22'>过小标签</text><rect class='bar-chart'></rect></svg>
459
+ """
460
+ )
461
+ d = design("data-visual", "data-visual")
462
+ d["pages"][0]["supporting_visual_layer"] = "HTML/SVG 数据图"
463
+ res = run_check(html, d)
464
+ self.assertNotEqual(res.returncode, 0)
465
+ self.assertIn("SVG 图表文字过小", res.stdout)
466
+
467
+ def test_orphan_connector_fails(self):
468
+ html = section(
469
+ """
470
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p14.png'></div>
471
+ <svg><line x1='0' y1='0' x2='100' y2='100'></line></svg>
472
+ <h1>关系链路</h1>
473
+ """
474
+ )
475
+ res = run_check(html, design("feature-theater", "cinematic-feature"))
476
+ self.assertNotEqual(res.returncode, 0)
477
+ self.assertIn("孤立", res.stdout)
478
+
479
+ def test_decorative_vi_deco_svg_lines_do_not_count_as_orphan_connector(self):
480
+ # 2026-07-06 真机误报:layout-composition.md §0.8 明确要求没配图的页用「装饰几何
481
+ # (连线/节点/分隔光带)」填充视觉活力,`class="vi-deco"` 是这类纯装饰 SVG 的既有约定名;
482
+ # 但孤立连线检查此前不分青红皂白见 line/path/polyline 就报,跟这条指导自相矛盾
483
+ # (同一份 deck 里 3 个章节分隔页同时中招)。装饰几何不该被当成"缺节点的图表连线"。
484
+ html = section(
485
+ """
486
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p5.png'></div>
487
+ <svg class='vi-deco' viewBox='0 0 1920 1080'>
488
+ <circle cx='960' cy='540' r='420' class='stroke'></circle>
489
+ <line x1='540' y1='540' x2='1380' y2='540' class='stroke'></line>
490
+ </svg>
491
+ <h1>范式跃迁</h1>
492
+ """
493
+ )
494
+ res = run_check(html, design())
495
+ self.assertNotIn("孤立", res.stdout)
496
+
497
+ def test_cutout_over_independent_background_passes(self):
498
+ html = section(
499
+ """
500
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p11.png'></div>
501
+ <img class='product-visual cutout-foreground' src='assets/enhanced_src_9.png' data-source-file='src_9.png'>
502
+ <h1>多终端可达</h1>
503
+ """
504
+ )
505
+ source_plan = {
506
+ "enhance_entries": [
507
+ {
508
+ "file": "enhanced_src_9.png",
509
+ "source_file": "src_9.png",
510
+ "enhance_style": "cutout",
511
+ "image_role": "foreground_cutout",
512
+ }
513
+ ]
514
+ }
515
+ res = run_check(html, design("detail-closeup", "product-closeup"), source_plan=source_plan, strict=True)
516
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
517
+
518
+ def test_contract_foreground_none_rejects_extra_image(self):
519
+ html = section(
520
+ """
521
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p3.png'></div>
522
+ <div class='edit-content'><h1>背景已经说明内容</h1><img class='product-visual' src='assets/redundant.png'></div>
523
+ """
524
+ )
525
+ d = design("scenario-fullbleed", "cinematic-feature")
526
+ d["pages"][0]["visual_asset_contract"] = {
527
+ "background_role": "ai_background",
528
+ "background_asset": "ai_p3.png",
529
+ "foreground_role": "none",
530
+ "foreground_asset": "",
531
+ }
532
+ res = run_check(html, d, strict=True)
533
+ self.assertNotEqual(res.returncode, 0)
534
+ self.assertIn("foreground_role=none", res.stdout)
535
+
536
+ def test_oversized_text_backplate_fails(self):
537
+ html = section(
538
+ """
539
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p5.png'></div>
540
+ <div class='frosted-caption' style='width:980px;padding:26px;background:rgba(255,255,255,.8)'>
541
+ 这是一条过长的文字背板,会横跨画面并遮挡背景图片,不符合产品详情页式净区浮字。
542
+ </div>
543
+ <img class='product-visual' src='assets/hero.png'>
544
+ """
545
+ )
546
+ res = run_check(html, design("feature-theater", "cinematic-feature"), strict=True)
547
+ self.assertNotEqual(res.returncode, 0)
548
+ self.assertIn("文字背板", res.stdout)
549
+
550
+ def test_narrow_text_box_fails(self):
551
+ html = section(
552
+ """
553
+ <div class='edit-bg'><img class='bg-img' src='assets/ai_p9.png'></div>
554
+ <div style='width:80px'>这是一个会被挤成一字一行的中文说明</div>
555
+ <img class='product-visual' src='assets/hero.png'>
556
+ """
557
+ )
558
+ res = run_check(html, design("feature-theater", "cinematic-feature"), strict=True)
559
+ self.assertNotEqual(res.returncode, 0)
560
+ self.assertIn("文本框过窄", res.stdout)
561
+
562
+ # ── 素材使用契约(2026-07-02 逐页体检裁决:slide03 坠机原图 / slide22 旧 PPT 截图当背景 / slide10·14 同族重复)──
563
+ def _plan(self, **image_overrides):
564
+ image = {"file": "src_1.jpg", "usage_intent": "source_enhanced_hero",
565
+ "enhance_required": False, "has_embedded_text": False,
566
+ "target_layer": "foreground", "enhanced_file": "enhanced_src_1.jpg"}
567
+ image.update(image_overrides)
568
+ return {"images": [image], "enhance_entries": [
569
+ {"file": "enhanced_src_1.jpg", "source_file": "src_1.jpg"}]}
570
+
571
+ def test_raw_source_with_text_policy_fails(self):
572
+ # 规划标 enhance_required(图上带字须重绘):原图直接引用 → ERROR(slide03 坠机字幕图事故)。
573
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
574
+ "<h1>标题标题标题</h1><img class='src-figure' src='assets/src_1.jpg'>")
575
+ res = run_check(html, design(), source_plan=self._plan(enhance_required=True))
576
+ self.assertNotEqual(res.returncode, 0)
577
+ self.assertIn("不得直接引用", res.stdout)
578
+ self.assertIn("enhanced_src_1.jpg", res.stdout)
579
+
580
+ def test_enhanced_reference_passes_contract(self):
581
+ # 引用去字重绘版(不引原图)→ 该契约不报。
582
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
583
+ "<h1>标题标题标题</h1><img class='src-figure' src='assets/enhanced_src_1.jpg'>")
584
+ res = run_check(html, design(), source_plan=self._plan(enhance_required=True))
585
+ self.assertNotIn("不得直接引用", res.stdout)
586
+
587
+ def test_enhanced_reference_with_onerror_fallback_passes_contract(self):
588
+ # 2026-07-06 真机误报:IMG_SRC_RE 贪婪匹配抓到 onerror 里 this.src='assets/src_1.jpg'
589
+ # 这个 JS 兜底赋值(reconcile-missing 标准写法),而不是真正生效的 src= 属性,
590
+ # 把合法的 enhanced 引用误判成"直接引用原图"。此处 src 是真 enhanced,onerror 才是原图兜底。
591
+ html = section(
592
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
593
+ "<h1>标题标题标题</h1>"
594
+ "<img class='src-figure' src='assets/enhanced_src_1.jpg' "
595
+ "data-source-file='src_1.jpg' "
596
+ "onerror=\"this.onerror=null;this.src='assets/src_1.jpg'\">"
597
+ )
598
+ res = run_check(html, design(), source_plan=self._plan(enhance_required=True))
599
+ self.assertNotIn("不得直接引用", res.stdout)
600
+
601
+ def test_evidence_image_as_background_fails(self):
602
+ # direct_contain(旧 PPT 截图/密信息图)铺成整页背景 → ERROR(slide22 事故)。
603
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/src_1.jpg'></div>"
604
+ "<h1>标题标题标题</h1><img class='product-visual' src='assets/hero.png'>")
605
+ plan = self._plan(usage_intent="direct_contain", target_layer="evidence")
606
+ res = run_check(html, design(), source_plan=plan)
607
+ self.assertNotEqual(res.returncode, 0)
608
+ self.assertIn("不得用作整页背景", res.stdout)
609
+
610
+ def test_discarded_image_in_deck_fails(self):
611
+ # 规划判弃用(不相关/负面)的图出现在 deck → ERROR。
612
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
613
+ "<h1>标题标题标题</h1><img class='src-figure' src='assets/src_1.jpg'>")
614
+ res = run_check(html, design(), source_plan=self._plan(usage_intent="discard"))
615
+ self.assertNotEqual(res.returncode, 0)
616
+ self.assertIn("弃用", res.stdout)
617
+
618
+ def test_family_duplicate_bg_and_fg_fails(self):
619
+ # 背景 enhanced_src_1 + 前景 src_1(同主体两个版本同页)→ 同族重复 ERROR(slide10/14 事故)。
620
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/enhanced_src_1.jpg'></div>"
621
+ "<h1>标题标题标题</h1><img class='src-figure' src='assets/src_1.jpg'>")
622
+ res = run_check(html, design(), source_plan=self._plan())
623
+ self.assertNotEqual(res.returncode, 0)
624
+ self.assertIn("同族重复", res.stdout)
625
+
626
+ def test_systemic_font_size_grouping_hint_at_three_plus(self):
627
+ # 2026-07-05 真机复盘:单份 deck 里 10 处 ERROR 共享同一错误字号 font-size:22px,分散在 3 个
628
+ # 不同页面/组件——跟 text_readability_pixel.py 已验证过的「系统性同色归组」同一根因,逐条改在
629
+ # 有限修复轮数内修不完,需要一次性批量替换该数值的归组提示。
630
+ html = (
631
+ "<html><body>"
632
+ + section(
633
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
634
+ "<h1>标题标题标题</h1><div style='font-size:22px;'>标签一</div>",
635
+ label="01 页一",
636
+ ).replace("<html><body>", "").replace("</body></html>", "")
637
+ + section(
638
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p2.png'></div>"
639
+ "<h1>标题标题标题</h1><div style='font-size:22px;'>标签二</div>",
640
+ label="02 页二",
641
+ ).replace("<html><body>", "").replace("</body></html>", "")
642
+ + section(
643
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p3.png'></div>"
644
+ "<h1>标题标题标题</h1><div style='font-size:22px;'>标签三</div>",
645
+ label="03 页三",
646
+ ).replace("<html><body>", "").replace("</body></html>", "")
647
+ + "</body></html>"
648
+ )
649
+ d = design()
650
+ d["pages"] = [{"page": i, "composition_style": "scenario-fullbleed", "page_grammar": "hero-scene",
651
+ "max_visible_words": 80, "title": f"页{i}"} for i in (1, 2, 3)]
652
+ res = run_check(html, d)
653
+ self.assertNotEqual(res.returncode, 0)
654
+ hits = [line for line in res.stdout.splitlines() if "共享同一数值" in line]
655
+ self.assertEqual(len(hits), 1, res.stdout)
656
+ self.assertIn("font-size:22px", hits[0])
657
+ self.assertIn("3 处", hits[0])
658
+
659
+ def test_no_font_size_grouping_hint_below_threshold(self):
660
+ # 仅 2 处同数值(未达 SYSTEMIC_FONT_SIZE_MIN_COUNT=3)→ 不追加归组提示。
661
+ html = section(
662
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
663
+ "<h1>标题标题标题</h1>"
664
+ "<div style='font-size:22px;'>标签一</div><div style='font-size:22px;'>标签二</div>"
665
+ )
666
+ res = run_check(html, design())
667
+ self.assertNotIn("共享同一数值", res.stdout)
668
+
669
+ def test_word_budget_is_warning_not_error(self):
670
+ # 知情反转:文字量超预算降为 WARNING(×3 阈值)——视觉良好页曾被误判「文字墙」。
671
+ long_text = "这是一段用来撑爆设计预算的正文内容。" * 30 # ≈540 字 > 80×3
672
+ html = section("<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
673
+ f"<h1>标题</h1><p>{long_text}</p><img class='product-visual' src='assets/hero.png'>")
674
+ res = run_check(html, design())
675
+ self.assertIn("WARNING", res.stdout)
676
+ self.assertIn("超过设计预算", res.stdout)
677
+ self.assertNotIn("文字墙", res.stdout)
678
+
679
+ # ── on-dark/on-light 工具类组合缺口(2026-07-06 真机复盘:deck-title/deck-subtitle 用了
680
+ # on-dark 但样式表只给 t-eyebrow/t-takeaway/chip 等其它组件类定义了 .X.on-dark,标题/副标题
681
+ # 落到默认深墨字,直接压在照片上肉眼实测几乎不可读)──
682
+ def test_on_dark_modifier_missing_definition_fails(self):
683
+ html = (
684
+ "<html><head><style>"
685
+ ".deck-title{color:#111}.chip.on-dark{color:#fff}"
686
+ "</style></head><body>"
687
+ "<section class='slide fullbleed' data-screen-label='01 测试页'>"
688
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
689
+ "<h2 class='deck-title on-dark'>标题标题标题</h2>"
690
+ "<img class='product-visual' src='assets/hero.png'>"
691
+ "</section></body></html>"
692
+ )
693
+ res = run_check(html, design())
694
+ self.assertNotEqual(res.returncode, 0)
695
+ self.assertIn("deck-title.on-dark", res.stdout)
696
+
697
+ def test_on_dark_modifier_with_matching_definition_passes(self):
698
+ html = (
699
+ "<html><head><style>"
700
+ ".deck-title{color:#111}.deck-title.on-dark{color:#fff}"
701
+ "</style></head><body>"
702
+ "<section class='slide fullbleed' data-screen-label='01 测试页'>"
703
+ "<div class='edit-bg'><img class='bg-img' src='assets/ai_p1.png'></div>"
704
+ "<h2 class='deck-title on-dark'>标题标题标题</h2>"
705
+ "<img class='product-visual' src='assets/hero.png'>"
706
+ "</section></body></html>"
707
+ )
708
+ res = run_check(html, design())
709
+ self.assertNotIn("零效果", res.stdout)
710
+
711
+
712
+ if __name__ == "__main__":
713
+ unittest.main(verbosity=2)