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,397 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """检查 PPT design.json 是否真正进入产品详情页式构图。
4
+
5
+ 这个脚本只做确定性护栏:字段缺失、风格枚举非法、卡片/表格作为默认表达等会报 ERROR。
6
+ 它不评判审美好坏,但会阻止弱模型退回「archetype + 卡片墙」旧路径。
7
+
8
+ 视觉优先内容形状(2026-07-02,治「文本阅读型页面」)
9
+ ====================================================
10
+ 参考 visual-first 标准:一页一个想法、上屏文字 3–5 秒读完、短语不成段、
11
+ 论证细节进 speaker_notes(讲稿)或拆页。三条确定性检查:
12
+ ① max_visible_words 对账(ERROR)——页面自己声明了上屏字数预算却从无人核对,
13
+ takeaway+points 实际字数超声明×1.2 即违约(标题另由 design_budget_check 管长度);
14
+ ② 影像叙事页内容形状(ERROR)——hero-scene 等「图是主角」页,points 只许 ≤3 条短语(≤20 字);
15
+ ③ 文章化倾向(WARNING,保守防把对的改错)——≥4 条要点且平均 >18 字 = 文本阅读型排布。
16
+ `speaker_notes` 是可选讲稿字段(论证/机制/出处放这里,导出进 PPT 备注栏),
17
+ **不计入上屏字数、不参与卡片/表格倾向文本扫描**。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+
28
+
29
+ ALLOWED_STYLES = {
30
+ "product-hero",
31
+ "feature-theater",
32
+ "detail-closeup",
33
+ "scenario-fullbleed",
34
+ "data-visual",
35
+ "gallery-strip",
36
+ "chapter-cinematic",
37
+ "quote-focus",
38
+ }
39
+ ALLOWED_PAGE_GRAMMARS = {
40
+ "hero-scene",
41
+ "cinematic-feature",
42
+ "product-closeup",
43
+ "image-mosaic",
44
+ "spec-overlay",
45
+ "data-visual",
46
+ "before-after-scene",
47
+ }
48
+ TEXT_ZONES = {"left", "right", "top", "bottom", "center"}
49
+ TEXT_OVERLAY_MODES = {"scrim", "floating-text", "frosted-caption", "side-copy", "none"}
50
+ PRIMARY_VISUAL_TYPES = {
51
+ "ai-background",
52
+ "foreground-ai",
53
+ "source-enhanced-background",
54
+ "source-enhanced-hero",
55
+ "transparent-closeup",
56
+ "scene-photo",
57
+ "product-render",
58
+ "html-svg-chart",
59
+ "image-mosaic",
60
+ }
61
+ REQUIRED_FIELDS = {
62
+ "one_sentence_takeaway",
63
+ "composition_style",
64
+ "visual_focus",
65
+ "text_zone",
66
+ "visual_role",
67
+ "copy_budget",
68
+ "page_grammar",
69
+ "image_weight",
70
+ "text_overlay_mode",
71
+ "primary_visual_type",
72
+ "safe_text_zone",
73
+ "max_visible_words",
74
+ "main_visual_layer",
75
+ "supporting_visual_layer",
76
+ "text_overlay_zone",
77
+ "visual_story",
78
+ "semantic_image_prompt",
79
+ "forbidden_layout",
80
+ "visual_asset_contract",
81
+ }
82
+ TEXT_FOCUS_RE = re.compile(r"文字|文案|内容|要点|标题|段落|copy|text", re.I)
83
+ CARD_RE = re.compile(r"卡片墙|悬浮卡|网格|四卡|五卡|六卡|card|cards|panel", re.I)
84
+ TABLE_RE = re.compile(r"表格|矩阵|table|matrix", re.I)
85
+ NO_TEXT_RE = re.compile(r"no\s+(?:text|words|letters|logo|logos)|不要.*(?:文字|字母|logo|水印)", re.I)
86
+ HTML_SVG_RE = re.compile(r"^html-svg|HTML/SVG|svg|图表|柱状|折线|参数轴|坐标轴|流程|架构|关系|时间线|bar|line|axis|chart", re.I)
87
+ GENERIC_BG_RE = re.compile(r"^(?:vi)?(?:同色系)?(?:抽象)?(?:渐变|纹理|背景|氛围|色块|底图)$", re.I)
88
+ BACKGROUND_TEXT_MARKER_RE = re.compile(
89
+ r"(?:章节|序号|编号|号码|数字|标号|标签|题注|文字|文案|1\s*/\s*2\s*/\s*3\s*/\s*4|"
90
+ r"\b(?:number|numbers|numbered|chapter|chapters|label|labels|caption|captions|text|words|letters)\b)",
91
+ re.I,
92
+ )
93
+ IMAGE_LED_STYLES = {
94
+ "product-hero",
95
+ "feature-theater",
96
+ "detail-closeup",
97
+ "scenario-fullbleed",
98
+ "chapter-cinematic",
99
+ "quote-focus",
100
+ }
101
+ AI_VISUAL_TYPES = {
102
+ "ai-background",
103
+ "foreground-ai",
104
+ "source-enhanced-background",
105
+ "source-enhanced-hero",
106
+ "transparent-closeup",
107
+ "scene-photo",
108
+ "product-render",
109
+ "image-mosaic",
110
+ }
111
+ # 「图是主角」的影像叙事页语法:文字只许少量短语浮净区(视觉优先内容形状检查 ②)。
112
+ IMAGE_NARRATIVE_GRAMMARS = {"hero-scene", "cinematic-feature", "product-closeup", "before-after-scene"}
113
+ VISIBLE_WORDS_FACTOR = 1.2 # 对账余量:takeaway+points 超声明 max_visible_words×此系数才报
114
+ NARRATIVE_MAX_POINTS = 3 # 影像叙事页 points 条数硬顶
115
+ NARRATIVE_MAX_POINT_LEN = 20 # 影像叙事页单条短语字数硬顶
116
+ PROSE_MIN_POINTS = 4 # 文章化倾向:至少这么多条
117
+ PROSE_AVG_LEN = 18 # 且平均字长超此 → 文本阅读型排布 WARNING
118
+
119
+
120
+ def points_texts(page: dict) -> list[str]:
121
+ """归一化上屏要点文本:优先 points,缺则回退 blocks 的 content(与 design_budget_check 同口径)。"""
122
+ def _texts(v) -> list[str]:
123
+ out: list[str] = []
124
+ if not isinstance(v, list):
125
+ return out
126
+ for x in v:
127
+ if isinstance(x, str):
128
+ out.append(x)
129
+ elif isinstance(x, (int, float)):
130
+ out.append(str(x))
131
+ elif isinstance(x, dict):
132
+ c = x.get("content") or x.get("text") or ""
133
+ if isinstance(c, str):
134
+ out.append(c)
135
+ return out
136
+
137
+ pts = _texts(page.get("points"))
138
+ return pts if pts else _texts(page.get("blocks"))
139
+
140
+
141
+ def load_pages(path: Path):
142
+ with path.open(encoding="utf-8-sig") as f:
143
+ data = json.load(f)
144
+ pages = data.get("pages") if isinstance(data, dict) else None
145
+ if not isinstance(pages, list):
146
+ raise ValueError("design.json 必须是包含 pages 数组的对象")
147
+ return pages
148
+
149
+
150
+ def text_of(value) -> str:
151
+ if value is None:
152
+ return ""
153
+ if isinstance(value, str):
154
+ return value
155
+ if isinstance(value, (int, float, bool)):
156
+ return str(value)
157
+ if isinstance(value, list):
158
+ return " ".join(text_of(v) for v in value)
159
+ if isinstance(value, dict):
160
+ return " ".join(text_of(v) for v in value.values())
161
+ return str(value)
162
+
163
+
164
+ def check_page(page: dict, index: int):
165
+ label = page.get("page", index)
166
+ errors: list[str] = []
167
+ warnings: list[str] = []
168
+
169
+ missing = sorted(k for k in REQUIRED_FIELDS if k not in page)
170
+ if missing:
171
+ errors.append(f"缺少产品详情页式硬字段: {', '.join(missing)}")
172
+
173
+ style = page.get("composition_style")
174
+ if style is not None and style not in ALLOWED_STYLES:
175
+ errors.append(f"composition_style 非法: {style!r}")
176
+
177
+ grammar = page.get("page_grammar")
178
+ if grammar is not None and grammar not in ALLOWED_PAGE_GRAMMARS:
179
+ errors.append(f"page_grammar 非法: {grammar!r}")
180
+
181
+ zone = page.get("text_zone")
182
+ if zone is not None and zone not in TEXT_ZONES:
183
+ errors.append(f"text_zone 非法: {zone!r}")
184
+ safe_zone = page.get("safe_text_zone")
185
+ if safe_zone is not None and safe_zone not in TEXT_ZONES:
186
+ errors.append(f"safe_text_zone 非法: {safe_zone!r}")
187
+
188
+ overlay = page.get("text_overlay_mode")
189
+ if overlay is not None and overlay not in TEXT_OVERLAY_MODES:
190
+ errors.append(f"text_overlay_mode 非法: {overlay!r}")
191
+
192
+ visual_type = page.get("primary_visual_type")
193
+ if visual_type is not None and visual_type not in PRIMARY_VISUAL_TYPES:
194
+ errors.append(f"primary_visual_type 非法: {visual_type!r}")
195
+
196
+ contract = page.get("visual_asset_contract")
197
+ if "visual_asset_contract" in page:
198
+ if not isinstance(contract, dict):
199
+ errors.append("visual_asset_contract 必须是对象,声明背景/前景图片角色契约")
200
+ else:
201
+ for key in ("background_role", "foreground_role"):
202
+ if not str(contract.get(key) or "").strip():
203
+ errors.append(f"visual_asset_contract 缺少 {key}")
204
+ bg_asset = str(contract.get("background_asset") or "").strip()
205
+ fg_asset = str(contract.get("foreground_asset") or "").strip()
206
+ if bg_asset and fg_asset and bg_asset == fg_asset:
207
+ errors.append("visual_asset_contract 背景和前景不能使用同一张图片")
208
+ if fg_asset and re.fullmatch(r"ai_p\d+\.(?:png|jpg|jpeg|webp)", fg_asset, re.I):
209
+ errors.append("前景 AI 插图不能使用 ai_pN.png;必须独立规划 ai_pN_fg.png")
210
+
211
+ main_visual = str(page.get("main_visual_layer") or "").strip()
212
+ supporting_visual = str(page.get("supporting_visual_layer") or "").strip()
213
+ text_overlay_zone = page.get("text_overlay_zone")
214
+ semantic_prompt = str(page.get("semantic_image_prompt") or "").strip()
215
+ forbidden_layout = page.get("forbidden_layout")
216
+
217
+ if main_visual:
218
+ if TEXT_FOCUS_RE.search(main_visual):
219
+ errors.append(f"main_visual_layer 不能是文字/内容本身: {main_visual}")
220
+ if GENERIC_BG_RE.fullmatch(main_visual):
221
+ errors.append(f"main_visual_layer 不能只是抽象渐变/纹理: {main_visual}")
222
+
223
+ if text_overlay_zone is not None and not isinstance(text_overlay_zone, str):
224
+ errors.append("text_overlay_zone 必须是字符串,说明文字净区位置")
225
+ elif isinstance(text_overlay_zone, str) and not text_overlay_zone.strip():
226
+ errors.append("text_overlay_zone 不能为空")
227
+
228
+ if "semantic_image_prompt" in page and not semantic_prompt:
229
+ errors.append("semantic_image_prompt 不能为空")
230
+ if semantic_prompt:
231
+ is_html_svg_prompt = bool(HTML_SVG_RE.search(semantic_prompt))
232
+ if visual_type in AI_VISUAL_TYPES or (isinstance(page.get("visual"), dict) and str(page["visual"].get("route", "")).lower() in {"ai-a", "ai-b", "source-enhanced"}):
233
+ if is_html_svg_prompt:
234
+ errors.append("AI/图生图页面的 semantic_image_prompt 不能写成 html-svg,应是真实画面提示词")
235
+ if not NO_TEXT_RE.search(semantic_prompt):
236
+ errors.append("AI/图生图提示词必须明确 no text / no logo / no letters,避免图片里烧字")
237
+ bg_role = ""
238
+ if isinstance(contract, dict):
239
+ bg_role = str(contract.get("background_role") or "")
240
+ marker_source = " ".join(
241
+ str(page.get(k) or "")
242
+ for k in ("visual_focus", "visual_story", "main_visual_layer")
243
+ )
244
+ if ("background" in bg_role.lower() or visual_type == "ai-background") and BACKGROUND_TEXT_MARKER_RE.search(marker_source):
245
+ errors.append("AI 背景/场景图不得要求生成章节序号、编号、标签或文字;这些必须放到 HTML/SVG 可编辑前景层")
246
+ if visual_type == "html-svg-chart" or grammar == "data-visual":
247
+ if not is_html_svg_prompt:
248
+ errors.append("data-visual / html-svg-chart 页的 semantic_image_prompt 必须声明 html-svg 图表类型")
249
+
250
+ if "forbidden_layout" in page:
251
+ if not isinstance(forbidden_layout, list) or not forbidden_layout:
252
+ errors.append("forbidden_layout 必须是非空数组")
253
+ else:
254
+ joined = " ".join(str(x) for x in forbidden_layout)
255
+ if "card" not in joined.lower() and "卡" not in joined:
256
+ errors.append("forbidden_layout 必须显式禁止 card wall / 卡片墙")
257
+ if "table" not in joined.lower() and "表" not in joined:
258
+ errors.append("forbidden_layout 必须显式禁止 table wall / 表格墙")
259
+
260
+ image_weight = page.get("image_weight")
261
+ if "image_weight" in page:
262
+ if not isinstance(image_weight, (int, float)):
263
+ errors.append("image_weight 必须是 0.0-1.0 数字")
264
+ elif not 0 <= float(image_weight) <= 1:
265
+ errors.append(f"image_weight 越界: {image_weight!r}")
266
+
267
+ max_words = page.get("max_visible_words")
268
+ if "max_visible_words" in page:
269
+ if not isinstance(max_words, int):
270
+ errors.append("max_visible_words 必须是整数")
271
+ elif max_words > 80 and grammar not in {"data-visual", "image-mosaic"}:
272
+ errors.append("max_visible_words 过高,产品详情页式页面不应承载长文")
273
+
274
+ focus = str(page.get("visual_focus") or "").strip()
275
+ if focus and TEXT_FOCUS_RE.search(focus):
276
+ errors.append(f"visual_focus 不能是文字/内容本身: {focus}")
277
+
278
+ if style in IMAGE_LED_STYLES and semantic_prompt and HTML_SVG_RE.search(semantic_prompt):
279
+ errors.append(f"{style} 是影像叙事页,semantic_image_prompt 不应退成 HTML/SVG 图表")
280
+ if style == "quote-focus" and (not supporting_visual or supporting_visual.lower() in {"none", "无"}):
281
+ errors.append("quote-focus 也需要底部场景锚点、氛围背景或前景特写,不能只有文字")
282
+ if grammar == "before-after-scene" and zone == "bottom" and ("左" in focus and "右" in focus):
283
+ errors.append("before-after-scene 的左右对比画面不应把主文字净区放在 bottom,容易回到上下卡片化")
284
+
285
+ budget = page.get("copy_budget")
286
+ if "copy_budget" in page and not isinstance(budget, dict):
287
+ errors.append("copy_budget 必须是对象")
288
+ elif isinstance(budget, dict):
289
+ for key in ("title_max", "takeaway_max", "point_max", "note_max"):
290
+ if key not in budget:
291
+ errors.append(f"copy_budget 缺少 {key}")
292
+
293
+ archetype = str(page.get("archetype") or "")
294
+ scan_page = dict(page)
295
+ # forbidden_layout 里故意会写 no card wall / no table wall,不能反过来当作页面正在使用卡片/表格。
296
+ scan_page.pop("forbidden_layout", None)
297
+ # speaker_notes 是讲稿(论证/机制细节),提到「表格/矩阵/对比」是在讲内容、不是页面布局倾向。
298
+ scan_page.pop("speaker_notes", None)
299
+ all_text = text_of(scan_page)
300
+ if archetype == "cards" and style != "gallery-strip":
301
+ errors.append("cards archetype 只能用于 gallery-strip 或少量证据画廊,不得作为普通正文页默认")
302
+ if CARD_RE.search(all_text) and grammar != "image-mosaic" and style not in {"gallery-strip", "data-visual"}:
303
+ errors.append("页面仍含卡片墙/网格/悬浮卡倾向,但 composition_style 不是 gallery-strip/data-visual")
304
+ if TABLE_RE.search(all_text) and style != "data-visual" and grammar != "data-visual":
305
+ errors.append("页面仍含表格/矩阵倾向,但 composition_style 不是 data-visual")
306
+
307
+ if grammar == "image-mosaic":
308
+ if isinstance(max_words, int) and max_words > 55:
309
+ errors.append("image-mosaic 只能是影像拼贴 + 短标签,max_visible_words 不应超过 55")
310
+ if isinstance(image_weight, (int, float)) and float(image_weight) < 0.65:
311
+ errors.append("image-mosaic 的图片视觉重量必须至少 0.65")
312
+ elif grammar in {"hero-scene", "cinematic-feature", "product-closeup", "before-after-scene"}:
313
+ if isinstance(image_weight, (int, float)) and float(image_weight) < 0.60:
314
+ errors.append(f"{grammar} 的图片/场景视觉重量必须至少 0.60")
315
+ elif grammar == "spec-overlay":
316
+ if visual_type == "html-svg-chart":
317
+ warnings.append("spec-overlay 应以产品/场景图为主,参数只作叠层;若图表主导请改 data-visual")
318
+
319
+ points = page.get("points")
320
+ if isinstance(points, list) and len(points) > 5 and style not in {"data-visual", "gallery-strip"} and grammar not in {"data-visual", "image-mosaic"}:
321
+ warnings.append("points 超过 5 条,建议拆页或改成 data-visual")
322
+
323
+ # ── 视觉优先内容形状(治「文本阅读型页面」:屏上=短语/数字/图,论证进 speaker_notes/拆页)──
324
+ point_list = points_texts(page)
325
+ takeaway = str(page.get("one_sentence_takeaway") or "")
326
+ # ① max_visible_words 对账:页面自声明的上屏字数预算 vs takeaway+points 实际字数
327
+ # (标题长度另由 design_budget_check 管,不重复计入;1.2 余量防误伤贴线页)。
328
+ if isinstance(max_words, int) and max_words > 0:
329
+ visible = len(takeaway.strip()) + sum(len(str(p).strip()) for p in point_list)
330
+ visible_cap = int(max_words * VISIBLE_WORDS_FACTOR)
331
+ if visible > visible_cap:
332
+ errors.append(
333
+ f"上屏文案 {visible} 字超自声明 max_visible_words={max_words}(含余量上限 {visible_cap})"
334
+ ":points 短语化 / 论证移入 speaker_notes / 拆页——观众 3–5 秒要能读完一页"
335
+ )
336
+ # ② 影像叙事页内容形状:图是主角,文字只许 ≤3 条短语(≤20 字)浮净区
337
+ if grammar in IMAGE_NARRATIVE_GRAMMARS and point_list:
338
+ longest = max(len(str(p).strip()) for p in point_list)
339
+ if len(point_list) > NARRATIVE_MAX_POINTS or longest > NARRATIVE_MAX_POINT_LEN:
340
+ errors.append(
341
+ f"{grammar} 影像叙事页文字超载({len(point_list)} 条 / 最长 {longest} 字)"
342
+ f":points ≤{NARRATIVE_MAX_POINTS} 条且每条 ≤{NARRATIVE_MAX_POINT_LEN} 字短语;"
343
+ "完整论证进 speaker_notes 或拆页"
344
+ )
345
+ # ③ 文章化倾向(保守 WARNING):多条长句 = 文本阅读型排布
346
+ if len(point_list) >= PROSE_MIN_POINTS:
347
+ avg_len = sum(len(str(p).strip()) for p in point_list) / len(point_list)
348
+ if avg_len > PROSE_AVG_LEN:
349
+ warnings.append(
350
+ f"文本阅读型排布倾向({len(point_list)} 条要点平均 {avg_len:.0f} 字)"
351
+ ":改短语/数字标签、把论述交给图表或 speaker_notes"
352
+ )
353
+
354
+ title = str(page.get("title") or "")
355
+ if len(title) > 18 and style not in {"chapter-cinematic", "quote-focus"}:
356
+ warnings.append("标题偏长,可能破坏产品详情页式一眼重点")
357
+
358
+ return label, errors, warnings
359
+
360
+
361
+ def main(argv=None):
362
+ parser = argparse.ArgumentParser()
363
+ parser.add_argument("design", help="build/design.json")
364
+ parser.add_argument("-v", "--verbose", action="store_true")
365
+ args = parser.parse_args(argv)
366
+
367
+ try:
368
+ pages = load_pages(Path(args.design))
369
+ except Exception as exc: # noqa: BLE001
370
+ print(f"DESIGN_STYLE_FAILED: {exc}")
371
+ return 2
372
+
373
+ all_errors: list[str] = []
374
+ all_warnings: list[str] = []
375
+ for idx, page in enumerate(pages, 1):
376
+ if not isinstance(page, dict):
377
+ all_errors.append(f"第{idx}页: page 必须是对象")
378
+ continue
379
+ label, errors, warnings = check_page(page, idx)
380
+ all_errors.extend(f"第{label}页: {e}" for e in errors)
381
+ all_warnings.extend(f"第{label}页: {w}" for w in warnings)
382
+
383
+ for item in all_errors:
384
+ print(f"ERROR: {item}")
385
+ if args.verbose:
386
+ for item in all_warnings:
387
+ print(f"WARNING: {item}")
388
+
389
+ if all_errors:
390
+ print(f"DESIGN_STYLE_FAILED: {len(all_errors)} 个阻断问题")
391
+ return 1
392
+ print(f"DESIGN_STYLE_OK: {len(pages)} 页")
393
+ return 0
394
+
395
+
396
+ if __name__ == "__main__":
397
+ raise SystemExit(main())
@@ -0,0 +1,233 @@
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__)), "design_style_check.py")
13
+
14
+
15
+ def run_check(design):
16
+ with tempfile.NamedTemporaryFile("w", suffix=".json", encoding="utf-8", delete=False) as f:
17
+ json.dump(design, f, ensure_ascii=False)
18
+ path = f.name
19
+ try:
20
+ # 显式 UTF-8:中文 Windows 上 text=True 默认 locale=GBK,解码子进程 UTF-8 中文输出必崩
21
+ # (与 visual_quality_gate.py::_run 同一防御;bash.ts 的 PYTHONUTF8 只护工作流内运行)。
22
+ return subprocess.run(
23
+ [sys.executable, SCRIPT, path, "-v"],
24
+ text=True,
25
+ encoding="utf-8",
26
+ errors="replace",
27
+ stdout=subprocess.PIPE,
28
+ stderr=subprocess.PIPE,
29
+ check=False,
30
+ )
31
+ finally:
32
+ os.unlink(path)
33
+
34
+
35
+ def good_page(**extra):
36
+ page = {
37
+ "page": 1,
38
+ "archetype": "fullbleed",
39
+ "composition": "全幅浮层",
40
+ "composition_style": "scenario-fullbleed",
41
+ "page_grammar": "hero-scene",
42
+ "one_sentence_takeaway": "看见现场状态,才能更快闭环处置",
43
+ "visual_focus": "能源电网巡检场景背景",
44
+ "text_zone": "left",
45
+ "safe_text_zone": "left",
46
+ "visual_role": "AI 场景背景",
47
+ "primary_visual_type": "ai-background",
48
+ "image_weight": 0.78,
49
+ "text_overlay_mode": "scrim",
50
+ "max_visible_words": 42,
51
+ "copy_budget": {"title_max": 14, "takeaway_max": 24, "point_max": 3, "note_max": 24},
52
+ "main_visual_layer": "AI 能源电网巡检场景全幅底图",
53
+ "supporting_visual_layer": "底部设备剪影",
54
+ "text_overlay_zone": "left-center",
55
+ "visual_story": "用变电站巡检场景让观众先看到方案落地现场",
56
+ "semantic_image_prompt": "cinematic energy grid inspection scene, wide clean text-safe area on the left, premium product-detail-page lighting, no text, no words, no letters, no logos, no watermark",
57
+ "forbidden_layout": ["no card wall", "no table wall", "no white floating panels", "no image frame"],
58
+ "visual_asset_contract": {
59
+ "background_role": "ai_background",
60
+ "background_asset": "ai_p1.png",
61
+ "foreground_role": "none",
62
+ "foreground_asset": "",
63
+ },
64
+ "title": "现场状态可追溯",
65
+ "key_point": "用连续现场记录替代选择性拍照",
66
+ "points": ["覆盖更完整", "处置更直观"],
67
+ "visual": {"route": "ai-B", "side": "full"},
68
+ }
69
+ page.update(extra)
70
+ return page
71
+
72
+
73
+ class DesignStyleCheckTest(unittest.TestCase):
74
+ def test_good_design_passes(self):
75
+ res = run_check({"pages": [good_page()]})
76
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
77
+ self.assertIn("DESIGN_STYLE_OK", res.stdout)
78
+
79
+ def test_missing_hard_fields_fails(self):
80
+ page = good_page()
81
+ del page["composition_style"]
82
+ del page["visual_focus"]
83
+ res = run_check({"pages": [page]})
84
+ self.assertNotEqual(res.returncode, 0)
85
+ self.assertIn("缺少产品详情页式硬字段", res.stdout)
86
+
87
+ def test_cards_archetype_without_gallery_fails(self):
88
+ res = run_check({"pages": [good_page(archetype="cards", composition_style="feature-theater")]})
89
+ self.assertNotEqual(res.returncode, 0)
90
+ self.assertIn("cards archetype", res.stdout)
91
+
92
+ def test_table_needs_data_visual(self):
93
+ res = run_check({"pages": [good_page(blocks=[{"content": "对比矩阵表格", "render": "html+text"}])]})
94
+ self.assertNotEqual(res.returncode, 0)
95
+ self.assertIn("表格/矩阵", res.stdout)
96
+
97
+ def test_text_focus_fails(self):
98
+ res = run_check({"pages": [good_page(visual_focus="文字内容")]})
99
+ self.assertNotEqual(res.returncode, 0)
100
+ self.assertIn("visual_focus", res.stdout)
101
+
102
+ def test_main_visual_cannot_be_gradient(self):
103
+ res = run_check({"pages": [good_page(main_visual_layer="渐变")]})
104
+ self.assertNotEqual(res.returncode, 0)
105
+ self.assertIn("main_visual_layer", res.stdout)
106
+
107
+ def test_ai_prompt_must_forbid_text(self):
108
+ res = run_check({"pages": [good_page(semantic_image_prompt="cinematic energy grid inspection scene")]})
109
+ self.assertNotEqual(res.returncode, 0)
110
+ self.assertIn("no text", res.stdout)
111
+
112
+ def test_ai_background_cannot_request_chapter_numbers(self):
113
+ res = run_check({"pages": [good_page(
114
+ visual_focus="4个章节序号浮在浅蓝矢量准线上",
115
+ visual_story="背景用 1/2/3/4 编号给读者导览",
116
+ semantic_image_prompt="cinematic energy operations route background, no text, no words, no letters, no logos",
117
+ )]})
118
+ self.assertNotEqual(res.returncode, 0)
119
+ self.assertIn("章节序号", res.stdout)
120
+
121
+ def test_data_visual_requires_html_svg_prompt(self):
122
+ res = run_check({"pages": [good_page(
123
+ composition_style="data-visual",
124
+ page_grammar="data-visual",
125
+ primary_visual_type="html-svg-chart",
126
+ image_weight=0.55,
127
+ semantic_image_prompt="cinematic background, no text, no words, no letters",
128
+ )]})
129
+ self.assertNotEqual(res.returncode, 0)
130
+ self.assertIn("html-svg", res.stdout)
131
+
132
+ def test_forbidden_layout_must_ban_cards_and_tables(self):
133
+ res = run_check({"pages": [good_page(forbidden_layout=["no image frame"])]})
134
+ self.assertNotEqual(res.returncode, 0)
135
+ self.assertIn("forbidden_layout", res.stdout)
136
+
137
+ def test_image_mosaic_requires_image_weight_and_short_words(self):
138
+ res = run_check({"pages": [good_page(
139
+ composition_style="gallery-strip",
140
+ page_grammar="image-mosaic",
141
+ image_weight=0.5,
142
+ max_visible_words=90,
143
+ )]})
144
+ self.assertNotEqual(res.returncode, 0)
145
+ self.assertIn("image-mosaic", res.stdout)
146
+
147
+ def test_invalid_page_grammar_fails(self):
148
+ res = run_check({"pages": [good_page(page_grammar="text-cards")]})
149
+ self.assertNotEqual(res.returncode, 0)
150
+ self.assertIn("page_grammar", res.stdout)
151
+
152
+ def test_visual_asset_contract_required(self):
153
+ page = good_page()
154
+ del page["visual_asset_contract"]
155
+ res = run_check({"pages": [page]})
156
+ self.assertNotEqual(res.returncode, 0)
157
+ self.assertIn("visual_asset_contract", res.stdout)
158
+
159
+ def test_visual_asset_contract_rejects_repeated_asset(self):
160
+ res = run_check({"pages": [good_page(visual_asset_contract={
161
+ "background_role": "ai_background",
162
+ "background_asset": "ai_p7.png",
163
+ "foreground_role": "foreground_ai",
164
+ "foreground_asset": "ai_p7.png",
165
+ })]})
166
+ self.assertNotEqual(res.returncode, 0)
167
+ self.assertIn("背景和前景不能使用同一张图片", res.stdout)
168
+
169
+ def test_foreground_ai_must_use_fg_name(self):
170
+ res = run_check({"pages": [good_page(visual_asset_contract={
171
+ "background_role": "ai_background",
172
+ "background_asset": "ai_p7.png",
173
+ "foreground_role": "foreground_ai",
174
+ "foreground_asset": "ai_p7.png",
175
+ })]})
176
+ self.assertNotEqual(res.returncode, 0)
177
+ self.assertIn("ai_pN.png", res.stdout)
178
+
179
+ # ── 视觉优先内容形状(治「文本阅读型页面」)──
180
+
181
+ def test_visible_words_ledger_overflow_fails(self):
182
+ # 对账检查:takeaway+points 实际字数超页面自声明的 max_visible_words×1.2 → ERROR。
183
+ # 用非影像叙事语法(spec-overlay)隔离对账规则本身。
184
+ long_point = "这是一条超过二十个字的完整论述句用来测试上屏字数对账检查" # 28 字
185
+ res = run_check({"pages": [good_page(
186
+ page_grammar="spec-overlay",
187
+ points=[long_point, long_point, long_point, long_point],
188
+ )]})
189
+ self.assertNotEqual(res.returncode, 0)
190
+ self.assertIn("超自声明 max_visible_words", res.stdout)
191
+
192
+ def test_narrative_page_text_overload_fails(self):
193
+ # 影像叙事页(hero-scene 等「图是主角」):points >3 条 或 单条 >20 字 → ERROR。
194
+ res = run_check({"pages": [good_page(points=["短语一", "短语二", "短语三", "短语四"])]})
195
+ self.assertNotEqual(res.returncode, 0)
196
+ self.assertIn("影像叙事页文字超载", res.stdout)
197
+ res2 = run_check({"pages": [good_page(points=["这一条是超过二十个字的完整论述句子用于测试"])]})
198
+ self.assertNotEqual(res2.returncode, 0)
199
+ self.assertIn("影像叙事页文字超载", res2.stdout)
200
+
201
+ def test_narrative_short_phrases_pass(self):
202
+ # 3 条 ≤20 字短语 = 合规影像叙事页,不得误伤。
203
+ res = run_check({"pages": [good_page(points=["覆盖更完整", "处置更直观", "留痕更可信"])]})
204
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
205
+ self.assertIn("DESIGN_STYLE_OK", res.stdout)
206
+
207
+ def test_prose_tendency_warning_only(self):
208
+ # 文章化倾向(≥4 条且平均 >18 字)在预算内 → 只 WARNING 不阻断(保守防把对的改错)。
209
+ p = "全景相机行业落地效果对比论述句共十九字" # 19 字
210
+ res = run_check({"pages": [good_page(
211
+ composition_style="data-visual",
212
+ page_grammar="data-visual",
213
+ primary_visual_type="html-svg-chart",
214
+ semantic_image_prompt="html-svg:柱状图",
215
+ max_visible_words=80,
216
+ points=[p, p, p, p],
217
+ visual={"route": "html", "side": "none"},
218
+ )]})
219
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
220
+ self.assertIn("文本阅读型排布倾向", res.stdout)
221
+
222
+ def test_speaker_notes_excluded_from_layout_scan(self):
223
+ # speaker_notes 是讲稿:提到「对比矩阵/表格」是在讲内容,不得触发卡片/表格倾向 ERROR;
224
+ # 也不计入上屏字数对账。
225
+ res = run_check({"pages": [good_page(
226
+ speaker_notes="这一页的论证:与传统方案的对比矩阵和参数表格细节见附录,演讲时口述即可。",
227
+ )]})
228
+ self.assertEqual(res.returncode, 0, res.stdout + res.stderr)
229
+ self.assertIn("DESIGN_STYLE_OK", res.stdout)
230
+
231
+
232
+ if __name__ == "__main__":
233
+ unittest.main(verbosity=2)